diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6b1d7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +data/kv_data.json +data/kv.db +.springboard +index.html \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..83d0db3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=http://localhost:4873/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ef9ce34 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +# Springboard Development Guide + +This application is built with the **Springboard framework**. + +## Getting Started + +**Before writing any code, run:** + +```bash +npx sb docs context +``` + +This outputs comprehensive framework information including available documentation +sections, key concepts, and workflow guidance. + +## Recommended Workflow + +1. **Run `sb docs context`** at the start of your session +2. **Write code** using your knowledge + the context from step 1 +3. **Fetch specific docs** only when needed: `sb docs get
` +4. **View examples** for reference code: `sb docs examples show ` + +## Other Useful Commands + +- `sb docs --help` - See all available commands +- `sb docs types` - Get TypeScript type definitions +- `sb docs examples list` - See available example modules diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ef9ce34 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,27 @@ +# Springboard Development Guide + +This application is built with the **Springboard framework**. + +## Getting Started + +**Before writing any code, run:** + +```bash +npx sb docs context +``` + +This outputs comprehensive framework information including available documentation +sections, key concepts, and workflow guidance. + +## Recommended Workflow + +1. **Run `sb docs context`** at the start of your session +2. **Write code** using your knowledge + the context from step 1 +3. **Fetch specific docs** only when needed: `sb docs get
` +4. **View examples** for reference code: `sb docs examples show ` + +## Other Useful Commands + +- `sb docs --help` - See all available commands +- `sb docs types` - Get TypeScript type definitions +- `sb docs examples list` - See available example modules diff --git a/package.json b/package.json new file mode 100644 index 0000000..5b3d30e --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "jamapp", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "start": "node dist/node/node-entry.mjs", + "build": "npm run build:web && npm run build:node", + "build:web": "SPRINGBOARD_PLATFORM=web vite build", + "build:node": "SPRINGBOARD_PLATFORM=node vite build --outDir dist/node", + "check-types": "tsc --noEmit", + "postinstall": "npm rebuild better-sqlite3" + }, + "dependencies": { + "@hono/node-server": "^1.19.9", + "@jamtools/core": "0.0.1-dev-jamapp-10", + "better-sqlite3": "^12.6.2", + "crossws": "^0.4.4", + "hono": "^4.12.3", + "immer": "^11.1.4", + "kysely": "^0.28.11", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.13.1", + "rxjs": "^7.8.2", + "springboard": "0.0.1-dev-jamapp-10" + }, + "devDependencies": { + "@types/node": "^25.3.2", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "jsdom": "25.0.1", + "qrcode": "^1.5.4", + "react-guitar": "^1.1.3", + "tonal": "^6.4.3", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "wled-client": "^0.22.1" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..daadc16 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3024 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hono/node-server': + specifier: ^1.19.9 + version: 1.19.9(hono@4.12.3) + '@jamtools/core': + specifier: 0.0.1-dev-jamapp-10 + version: 0.0.1-dev-jamapp-10(@tonejs/midi@2.0.28)(springboard@0.0.1-dev-jamapp-10(crossws@0.4.4)(immer@11.1.4)(isomorphic-ws@4.0.1(ws@8.19.0))(kysely@0.28.11)(react-dom@19.2.4(react@19.2.4))(react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)(vite@7.3.1(@types/node@25.3.2))) + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 + crossws: + specifier: ^0.4.4 + version: 0.4.4 + hono: + specifier: ^4.12.3 + version: 4.12.3 + immer: + specifier: ^11.1.4 + version: 11.1.4 + kysely: + specifier: ^0.28.11 + version: 0.28.11 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-router: + specifier: ^7.13.1 + version: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + springboard: + specifier: 0.0.1-dev-jamapp-10 + version: 0.0.1-dev-jamapp-10(crossws@0.4.4)(immer@11.1.4)(isomorphic-ws@4.0.1(ws@8.19.0))(kysely@0.28.11)(react-dom@19.2.4(react@19.2.4))(react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)(vite@7.3.1(@types/node@25.3.2)) + devDependencies: + '@types/node': + specifier: ^25.3.2 + version: 25.3.2 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + jsdom: + specifier: 25.0.1 + version: 25.0.1 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + react-guitar: + specifier: ^1.1.3 + version: 1.1.3(@types/react@19.2.14)(react@19.2.4) + tonal: + specifier: ^6.4.3 + version: 6.4.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.3.2) + wled-client: + specifier: ^0.22.1 + version: 0.22.1 + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@hono/node-ws@1.3.0': + resolution: {integrity: sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q==} + engines: {node: '>=18.14.1'} + peerDependencies: + '@hono/node-server': ^1.19.2 + hono: ^4.6.0 + + '@jamtools/core@0.0.1-dev-jamapp-10': + resolution: {integrity: sha512-2VB5lKqFNKgzlf/fZSSLYHzP++MILB40sK/FjS5geVm/aI1JhFTcJC0bTRwqQJTVG0IlnGKqnhkqHU+J4qBHYA==} + peerDependencies: + '@tonejs/midi': ^2.0.0 + springboard: 0.0.1-dev-jamapp-10 + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@js-bits/fetch@1.0.0': + resolution: {integrity: sha512-zXacOWMe03O8RooMRuiplYcYLi3GZxwQHfXuiaI6j3tqbqFaB5m5VWuqCMWzzQ/Qn8jC8s+DDuKpvdsES3Jfgw==} + engines: {node: '>=12'} + + '@julusian/midi@3.6.1': + resolution: {integrity: sha512-sC6tTMAMZsHOQILAv/R0On5tKKhzBQUjdyYWzh9l0UQeNry12CFIyRWK1Mep5xCHWCTUB0w4gxngpciA5PgN/Q==} + engines: {node: '>=14.15'} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@tonaljs/abc-notation@4.9.1': + resolution: {integrity: sha512-2fDUdPsFDdgZgyIiZCTYGKy30QiwIQxSXCSN2thGrSMXbQKCp8iTC8haStYcrA+25MPWhWlmvC/pz3tGcTNAqQ==} + + '@tonaljs/array@4.8.4': + resolution: {integrity: sha512-97HVdpZy82PqNBDMM9PRSbO2DUrnxNg3++N4xqLpfby70fKHAHhTWrMXWZK+Dzs76HDPQLd+qhd4cq28eBZzjw==} + + '@tonaljs/chord-detect@4.9.1': + resolution: {integrity: sha512-rV/9+R7aZ9cQorQ3jdNMMMh63onosglYZM71Q0n7KKcWMAGrxF66MzxBG82xy+w1QDMJQslB3iHfDHUiS6wRjA==} + + '@tonaljs/chord-type@5.1.1': + resolution: {integrity: sha512-ti4WzRYvvjH7to0G3zlJFq7WsjHqmcqbk8Jv98aZSR5YumLdY/ua2yOPPyoPq82n6vfgjZsacnAZ3v8/SodOcw==} + + '@tonaljs/chord@6.1.2': + resolution: {integrity: sha512-39crsPVyBJxdGJ1DGl+4diM+6+mZ/ws6crkQMlqX4zTYHMUVfJOQrko4mPF1CD89AFhHJe1zCT9Js+nNp3Gffg==} + + '@tonaljs/collection@4.9.0': + resolution: {integrity: sha512-Mk0h7O54nT6PgNVcUYauzxa5KOB23+0AOKudWzRH7JhJIN9vhVIC7PtwZXE+/G051UTbHSFIcN/afkgF4nB/8A==} + + '@tonaljs/core@5.0.2': + resolution: {integrity: sha512-v/fIRsB+lz93/yJnDEWvjDX6nDJ/Pz3cb0znm/UD8qKpN9BhN+v93iIaZG5LX0J2JNS35TtgIhwKFO10j6zieQ==} + + '@tonaljs/duration-value@4.9.0': + resolution: {integrity: sha512-Muz54HyIe0nMYKWx6wyTa4y17ma29DtpJF4/oqJphy6A124rAVDe/SKit8JGOvDYAQj71FUXqs17sXBxO/ExVw==} + + '@tonaljs/interval@4.8.2': + resolution: {integrity: sha512-9SEuJuqFdmu9lnvMmJtxbReQcQvZ1YHXhCcxaF6Re23/w+BqiJvvkvNZhfWH+n1+g0uaSdTsuvMfamp7hAvPMw==} + + '@tonaljs/interval@5.1.0': + resolution: {integrity: sha512-GR9dUjn0j7yhjwjRh8HQxZYXOiVl05WfY3AFyMB9rfg807K4dSJmWfPTULPQXyHJ6NiZOPXcwRs8MxMLDxgdbg==} + + '@tonaljs/key@4.11.2': + resolution: {integrity: sha512-fc1y5+NwS4S3amirzYLLB324PxJDO00oIBbLJclAddc46UBeYnu4FNz+1nZboHCXRQ/06/ftuaWD/l7HrAoulw==} + + '@tonaljs/midi@4.10.2': + resolution: {integrity: sha512-MPamXhEwPL7L1udLfYMm3Ft8mLYtHr62Zi2w5zYHM2P7YwIvNoiX0+dvAN5is1Wvq4iVRa8AjFHerjOW/SZhGg==} + + '@tonaljs/mode@4.9.2': + resolution: {integrity: sha512-Il48fWX9SnGMzwNFVbizkWkr/SJ+aCIIHhjWfSf1agrQAoq81536qBXEyEyvia6aqEXC4OaXAQ5rSoBcmQRj1Q==} + + '@tonaljs/note@4.12.1': + resolution: {integrity: sha512-yh6cnhu21bUb0VuIQWxObSbWBVp2cHIkJj4zZ4qsNOW4jSqn74+wHpSbOTDF8q+2hl6upz7TtBRavtqwPtLUCQ==} + + '@tonaljs/pcset@4.10.1': + resolution: {integrity: sha512-CZG1rpKc38yMfpEJsbDTvsTmQqsek9xxcuMgK64Tr6sP1lEiDuZJbQeKVWWRnreBF2FQ1cEGLmfOpvSO+40csA==} + + '@tonaljs/pitch-distance@5.0.5': + resolution: {integrity: sha512-dTfjsU0zyrj5YmiFio5prPaD5w7sBmHp4nnmlEg70nHY+SerAH0KiO9HM9usttVgRFUaXl0Gc7OI8YMGfSFmug==} + + '@tonaljs/pitch-interval@5.0.2': + resolution: {integrity: sha512-bQmxeenRFNuuABs9mDc+bSUp3ySGw8I49pHMoGDdCcro9n3MDN8IsSwhvKoGOYErFMx76rNn1t+7WL8ukpvX5w==} + + '@tonaljs/pitch-interval@6.1.0': + resolution: {integrity: sha512-9ZMxA7V4UgySnOKPIG6HECzhDb8mbiTCIdNNIzCIfz5XpjUmohku2YZpVVWMacLHgyeQicJzNoRiPel5oSAn4A==} + + '@tonaljs/pitch-note@6.1.0': + resolution: {integrity: sha512-A4OSLo8DjM38u73862LnDmL4YInDDRBmg0fojXcvu4cyU3oOlqndyeHOra1OVoH/WW46uNIxNs1wJDZNPWL5KQ==} + + '@tonaljs/pitch@5.0.1': + resolution: {integrity: sha512-5HYkF4hGY0jS2y5V3Hf5gNFXX46kT4cAcI7JLEn+qQb9N1dU9Gz9koI9di0mD1Tbam+kviceiCsJl4WX/FqYjA==} + + '@tonaljs/pitch@5.0.2': + resolution: {integrity: sha512-mxaXJPPe+LIJdjzpZEl8I8Wx3dEvlzkBbsr2Ltwc2dTAdnErAZ5R0TxVq2egF27lMvQN2QPQPWI9iDPPdVUmrg==} + + '@tonaljs/progression@4.9.2': + resolution: {integrity: sha512-1Pmau5tKoWmY2H/fp9WVJ5Qm4c+xlu7IEZb3R3uLbc26kJq97DbuSnfArIAQoZTum2V6lOEN/Dt34E9OrGKnoA==} + + '@tonaljs/range@4.9.2': + resolution: {integrity: sha512-XhFbCJCrEEIVz3MNmdVp9QUyiJA6eqnNnQeNqto8AuT5BYuffHDoMkBmvvjPuV5Lh8zfF9D0aqglvqPbZ+neKQ==} + + '@tonaljs/rhythm-pattern@1.0.0': + resolution: {integrity: sha512-3hp4Yw49e6BTWApRNb7arJ6HiICsHRFZYijuPE+vR93lIo9+lxcOMh0OPc4DyupTN7cvFTSB70Z7B/IeEJxhCQ==} + + '@tonaljs/roman-numeral@4.9.1': + resolution: {integrity: sha512-dJGKBNHdPrNTE97ZDk4t6wpNmgYcpHyLkAvWkRP9I/HsDkeTFFQqNXosYIvspPWrFySlRpeqqFwcqV74MYoOag==} + + '@tonaljs/scale-type@4.9.2': + resolution: {integrity: sha512-XDRPySg0X6owJ6AVzVRpS4ZpgleAUQakuq5VtfQ0JitnJKGRYo5Y0w7Fl/wEz/X9aEMk2lxcdF8VGGMIsQFo0g==} + + '@tonaljs/scale@4.13.4': + resolution: {integrity: sha512-hyilnmsCmoGQiRhYWKM1QPrqaNH3KGEfrSfxeXcicPnEh12yvrVRm4djKmTVw7Rc/5P4GniPAA51+CS0C5dbLw==} + + '@tonaljs/time-signature@4.9.0': + resolution: {integrity: sha512-zRo8CBqg/2guzTlF2vxyVIuB5gmwWQaxlknJPUSDII8CTdQ/x3a1LlNoMKkvTUnqwCihe3WrNPZ5XUfOOTthYA==} + + '@tonaljs/voice-leading@5.1.2': + resolution: {integrity: sha512-lV5hsDZC4ekhh465d2aVFFqyG9AOZvwKdxU1+dNB/P6ldefU1yHC10mKdY7FrdhVgKeGZG1MbJh8rETaKVg7vA==} + + '@tonaljs/voicing-dictionary@5.1.3': + resolution: {integrity: sha512-kRM6IOHq+ULgZtJdbGNz5OmfUo+WyA3EeqKVrlA9Uj7QqrrfOkzeQdE3jXEBnjO1oGP5ql6NS5QUF3rST+SqUQ==} + + '@tonaljs/voicing@5.1.3': + resolution: {integrity: sha512-OLNcaIA086/4gwoXVa736yW0yP9TADi95/5LAS8KxMtzYBWPYzxyC1IxcE9bGqXsdBnI99au57J8+G6wroluyg==} + + '@tonejs/midi@2.0.28': + resolution: {integrity: sha512-RII6YpInPsOZ5t3Si/20QKpNqB1lZ2OCFJSOzJxz38YdY/3zqDr3uaml4JuCWkdixuPqP1/TBnXzhQ39csyoVg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.3.2': + resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + adsr@1.0.1: + resolution: {integrity: sha512-thr9LK4jxApOzBA33IWOA83bXJFbyfbeozpHXyrMQOIhUni198uRxXqDhobW0S/51iokqty2Yz2WbLZbE6tntQ==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + array-flatten@3.0.0: + resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + audio-loader@0.5.0: + resolution: {integrity: sha512-mEoYRjZhqkBSen/X9i2PNosqvafEsur8bI5MNoPr0wsJu9Nzlul3Yv1elYeMPsXxTxYhXLY8AZlScBvaK4mydg==} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + crossws@0.4.4: + resolution: {integrity: sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg==} + peerDependencies: + srvx: '>=0.7.1' + peerDependenciesMeta: + srvx: + optional: true + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dexie@4.3.0: + resolution: {integrity: sha512-5EeoQpJvMKHe6zWt/FSIIuRa3CWlZeIl6zKXt+Lz7BU6RoRRLgX9dZEynRfXrkLcldKYCBiz7xekTEylnie1Ug==} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + djipevents@2.0.7: + resolution: {integrity: sha512-KNFYaU85imxOCKOUsIR70Iz9E19r96/X7LSH+u0tSoZdpWcBdzoqtTsU+wuLhc6GMpSFob+KInkZAbfKi01Bjg==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + easymidi@3.1.0: + resolution: {integrity: sha512-bxEwfPysM1L+SO/qwHaYu9dvTxw2QHFjGV9EMzqGQJbhEP2MupKpg6eJMkj+uoXN0Ep1JhVPLbNLPmt3UkZRTw==} + engines: {node: '>=14.15'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + event-target-polyfill@0.0.4: + resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hono@4.12.3: + resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} + engines: {node: '>=16.9.0'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isomorphic-ws@4.0.1: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + + jazz-midi@1.7.9: + resolution: {integrity: sha512-c8c4BBgwxdsIr1iVm53nadCrtH7BUlnX3V95ciK/gbvXN/ndE5+POskBalXgqlc/r9p2XUbdLTrgrC6fou5p9w==} + engines: {node: '>=10.0.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-rpc-2.0@1.7.1: + resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} + + jzz@1.9.6: + resolution: {integrity: sha512-J7ENLhXwfm2BNDKRUrL8eKtPhUS/CtMBpiafxQHDBcOWSocLhearDKEdh+ylnZFcr5OXWTed0gj6l/txeQA9vg==} + + keyboard-key@1.1.0: + resolution: {integrity: sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==} + + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.range@3.2.0: + resolution: {integrity: sha512-Fgkb7SinmuzqgIhNhAElo0BL/R1rHCnhwSZf78omqSwvWqD0kD2ssOAutQonDKH/ldS8BxA72ORYI09qAY9CYg==} + + lodash.uniqueid@4.0.1: + resolution: {integrity: sha512-GQQWaIeGlL6DIIr06kj1j6sSmBxyNMwI8kaX9aKpHR/XsMTiaXDVPNPAkiboOTK9OJpTJF/dXT3xYoFQnj386Q==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + midi-file@1.2.4: + resolution: {integrity: sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==} + + midimessage@1.0.5: + resolution: {integrity: sha512-MPJ2tDupFOfZB5/PLp8fri1IS4fd9hPj0Bio//FBhWRQ+TsJA7/49CF1aJyraDxa0Jq8zMHAwrwXl2GINvLvgw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + note-parser@1.1.0: + resolution: {integrity: sha512-YTqWQBsRp40EFrEznnkGtmx68gcgOQ8CdoBspqGBA3G1/4mJwIYbDe/vuNpX3oGX2DhP7b1dBgTmj7p3Zr0P1Q==} + + note-parser@2.0.1: + resolution: {integrity: sha512-w9o6Fv46y3NsFxeezTZSmftBtUM/ypme6iZWVrTJvvsD5RN+w0XNDePWtfreNrZFL3jSjBFhadPoXb+pJO4UdA==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + partysocket@1.1.16: + resolution: {integrity: sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w==} + peerDependencies: + react: '>=17' + peerDependenciesMeta: + react: + optional: true + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-prebuilds@1.0.0: + resolution: {integrity: sha512-D9wlkXZCmjxj2kBHTw3fGSyjoahr33breGBoJcoezpi7ouYS59DJVOHMZ+dgqacSrZiJo4qtkXxLQTE+BqXJmQ==} + engines: {node: '>= 14.15.0'} + hasBin: true + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + + react-guitar@1.1.3: + resolution: {integrity: sha512-u6YG8ARZXg5iKa92lOr1u6cXj2E2qq/u5B71DL960x4hNJYeLO4u4aDdQmqZqASC1KfvK8pcseu7CxqDEDzzdQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-router@7.13.1: + resolution: {integrity: sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + reconnecting-websocket@4.4.0: + resolution: {integrity: sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sample-player@0.5.5: + resolution: {integrity: sha512-VQ9pXPJ1m/eTH8QK6OQ8Dn/HSVToNyY9w9vnv+y/yjkJeRm87tJ/gBEm66jItfSLhKe6VG1DfX8+oT+Mg7QUpg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + soundfont-player@0.12.0: + resolution: {integrity: sha512-8BJIsAt7h1PK3thSZDgF6zecgGhYkK74JnZO8WRZi3h34qG6H/DYlnv7cpRvL7Q9C8N6qld4Qwj7nJsX1gYjEA==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + springboard@0.0.1-dev-jamapp-10: + resolution: {integrity: sha512-RsB8S8hUNolAfV4ydVCZwOwLSNj/M+FV2Gwm8QNYg0+KOwOH8D59PPHArEfX55n/Z5IjPWRZ/NyX9/rveA+oIg==} + hasBin: true + peerDependencies: + '@tauri-apps/api': ^2.9.0 + '@tauri-apps/plugin-shell': ^2.3.3 + crossws: ^0.4.4 + immer: '>= 10' + isomorphic-ws: ^4.0.1 + kysely: '>= 0.24.0' + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-router: ^7.9.6 + rxjs: ^7.8.1 + vite: ^7.0.0 + peerDependenciesMeta: + '@tauri-apps/api': + optional: true + '@tauri-apps/plugin-shell': + optional: true + crossws: + optional: true + immer: + optional: true + isomorphic-ws: + optional: true + kysely: + optional: true + react: + optional: true + react-dom: + optional: true + react-router: + optional: true + rxjs: + optional: true + vite: + optional: true + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tonal@6.4.3: + resolution: {integrity: sha512-p+0kwBBma3Ko2xtHVq8oYa+IAGRozkAkBk03swRvcEfxYa8Ktr0yrBYW1fsZlywaerEu346oebghrS8jDQmBsA==} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webmidi@3.1.14: + resolution: {integrity: sha512-K9GzNm0J3R/61NJWAW7ipAJGWU5D/8bEjOir3PymFjLDpbQJ+ygjvm5jx/WQ8atQ1hu23St3lvnc5g1NKbOsrw==} + engines: {node: '>=8.5'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + wled-client@0.22.1: + resolution: {integrity: sha512-DIa8CfqOi8BKh4j3n6Ojs82/OL7OnSN2fbfX3Ur1Y2k9fU0c4BWZeitqZ3SPkIKqAPeDEUbEoldI9Uycj6W/kQ==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.28.6 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.4 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@hono/node-server@1.19.9(hono@4.12.3)': + dependencies: + hono: 4.12.3 + + '@hono/node-ws@1.3.0(@hono/node-server@1.19.9(hono@4.12.3))(hono@4.12.3)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.12.3) + hono: 4.12.3 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + optional: true + + '@jamtools/core@0.0.1-dev-jamapp-10(@tonejs/midi@2.0.28)(springboard@0.0.1-dev-jamapp-10(crossws@0.4.4)(immer@11.1.4)(isomorphic-ws@4.0.1(ws@8.19.0))(kysely@0.28.11)(react-dom@19.2.4(react@19.2.4))(react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)(vite@7.3.1(@types/node@25.3.2)))': + dependencies: + '@tonejs/midi': 2.0.28 + easymidi: 3.1.0 + immer: 10.1.1 + midi-file: 1.2.4 + soundfont-player: 0.12.0 + springboard: 0.0.1-dev-jamapp-10(crossws@0.4.4)(immer@11.1.4)(isomorphic-ws@4.0.1(ws@8.19.0))(kysely@0.28.11)(react-dom@19.2.4(react@19.2.4))(react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)(vite@7.3.1(@types/node@25.3.2)) + webmidi: 3.1.14 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-bits/fetch@1.0.0': + dependencies: + abort-controller: 3.0.0 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@julusian/midi@3.6.1': + dependencies: + node-addon-api: 6.1.0 + pkg-prebuilds: 1.0.0 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@tonaljs/abc-notation@4.9.1': + dependencies: + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/array@4.8.4': + dependencies: + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/chord-detect@4.9.1': + dependencies: + '@tonaljs/chord-type': 5.1.1 + '@tonaljs/pcset': 4.10.1 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/chord-type@5.1.1': + dependencies: + '@tonaljs/pcset': 4.10.1 + + '@tonaljs/chord@6.1.2': + dependencies: + '@tonaljs/chord-detect': 4.9.1 + '@tonaljs/chord-type': 5.1.1 + '@tonaljs/collection': 4.9.0 + '@tonaljs/interval': 5.1.0 + '@tonaljs/pcset': 4.10.1 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-note': 6.1.0 + '@tonaljs/scale-type': 4.9.2 + + '@tonaljs/collection@4.9.0': {} + + '@tonaljs/core@5.0.2': + dependencies: + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-interval': 6.1.0 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/duration-value@4.9.0': {} + + '@tonaljs/interval@4.8.2': + dependencies: + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-interval': 5.0.2 + + '@tonaljs/interval@5.1.0': + dependencies: + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-interval': 6.1.0 + + '@tonaljs/key@4.11.2': + dependencies: + '@tonaljs/note': 4.12.1 + '@tonaljs/pitch-note': 6.1.0 + '@tonaljs/roman-numeral': 4.9.1 + + '@tonaljs/midi@4.10.2': + dependencies: + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/mode@4.9.2': + dependencies: + '@tonaljs/collection': 4.9.0 + '@tonaljs/interval': 5.1.0 + '@tonaljs/pcset': 4.10.1 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-note': 6.1.0 + '@tonaljs/scale-type': 4.9.2 + + '@tonaljs/note@4.12.1': + dependencies: + '@tonaljs/midi': 4.10.2 + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-interval': 6.1.0 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/pcset@4.10.1': + dependencies: + '@tonaljs/collection': 4.9.0 + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-interval': 6.1.0 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/pitch-distance@5.0.5': + dependencies: + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-interval': 6.1.0 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/pitch-interval@5.0.2': + dependencies: + '@tonaljs/pitch': 5.0.1 + + '@tonaljs/pitch-interval@6.1.0': + dependencies: + '@tonaljs/pitch': 5.0.2 + + '@tonaljs/pitch-note@6.1.0': + dependencies: + '@tonaljs/pitch': 5.0.2 + + '@tonaljs/pitch@5.0.1': {} + + '@tonaljs/pitch@5.0.2': {} + + '@tonaljs/progression@4.9.2': + dependencies: + '@tonaljs/chord': 6.1.2 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-interval': 6.1.0 + '@tonaljs/pitch-note': 6.1.0 + '@tonaljs/roman-numeral': 4.9.1 + + '@tonaljs/range@4.9.2': + dependencies: + '@tonaljs/collection': 4.9.0 + '@tonaljs/midi': 4.10.2 + + '@tonaljs/rhythm-pattern@1.0.0': {} + + '@tonaljs/roman-numeral@4.9.1': + dependencies: + '@tonaljs/pitch': 5.0.2 + '@tonaljs/pitch-interval': 6.1.0 + '@tonaljs/pitch-note': 6.1.0 + + '@tonaljs/scale-type@4.9.2': + dependencies: + '@tonaljs/pcset': 4.10.1 + + '@tonaljs/scale@4.13.4': + dependencies: + '@tonaljs/chord-type': 5.1.1 + '@tonaljs/collection': 4.9.0 + '@tonaljs/note': 4.12.1 + '@tonaljs/pcset': 4.10.1 + '@tonaljs/pitch-distance': 5.0.5 + '@tonaljs/pitch-note': 6.1.0 + '@tonaljs/scale-type': 4.9.2 + + '@tonaljs/time-signature@4.9.0': {} + + '@tonaljs/voice-leading@5.1.2': + dependencies: + '@tonaljs/note': 4.12.1 + + '@tonaljs/voicing-dictionary@5.1.3': + dependencies: + '@tonaljs/chord': 6.1.2 + '@tonaljs/note': 4.12.1 + '@tonaljs/voice-leading': 5.1.2 + + '@tonaljs/voicing@5.1.3': + dependencies: + '@tonaljs/chord': 6.1.2 + '@tonaljs/interval': 5.1.0 + '@tonaljs/note': 4.12.1 + '@tonaljs/range': 4.9.2 + '@tonaljs/voice-leading': 5.1.2 + '@tonaljs/voicing-dictionary': 5.1.3 + + '@tonejs/midi@2.0.28': + dependencies: + array-flatten: 3.0.0 + midi-file: 1.2.4 + + '@types/estree@1.0.8': {} + + '@types/node@25.3.2': + dependencies: + undici-types: 7.18.2 + + '@types/parse-json@4.0.2': {} + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 25.3.2 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + adsr@1.0.1: {} + + agent-base@7.1.4: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + array-flatten@3.0.0: {} + + asynckit@0.4.0: {} + + audio-loader@0.5.0: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.28.6 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + + base64-js@1.5.1: {} + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + chownr@1.1.4: {} + + classnames@2.5.1: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@14.0.3: {} + + convert-source-map@1.9.0: {} + + cookie@1.1.1: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + crossws@0.4.4: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js@10.6.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dexie@4.3.0: {} + + dijkstrajs@1.0.3: {} + + djipevents@2.0.7: + dependencies: + '@babel/runtime': 7.28.6 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + easymidi@3.1.0: + dependencies: + '@julusian/midi': 3.6.1 + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + entities@6.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + event-target-polyfill@0.0.4: + optional: true + + event-target-shim@5.0.1: {} + + expand-template@2.0.3: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-uri-to-path@1.0.0: {} + + find-root@1.1.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + github-from-package@0.0.0: {} + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hono@4.12.3: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + immer@10.1.1: {} + + immer@11.1.4: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-fullwidth-code-point@3.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + isomorphic-ws@4.0.1(ws@8.19.0): + dependencies: + ws: 8.19.0 + + jazz-midi@1.7.9: + optional: true + + js-tokens@4.0.0: {} + + jsdom@25.0.1: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-rpc-2.0@1.7.1: {} + + jzz@1.9.6: + dependencies: + jazz-midi: 1.7.9 + optional: true + + keyboard-key@1.1.0: {} + + kysely@0.28.11: {} + + lines-and-columns@1.2.4: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.range@3.2.0: {} + + lodash.uniqueid@4.0.1: {} + + lru-cache@10.4.3: {} + + math-intrinsics@1.1.0: {} + + midi-file@1.2.4: {} + + midimessage@1.0.5: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-response@3.1.0: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-build-utils@2.0.0: {} + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + + node-addon-api@6.1.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + note-parser@1.1.0: {} + + note-parser@2.0.1: {} + + nwsapi@2.2.23: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + partysocket@1.1.16(react@19.2.4): + dependencies: + event-target-polyfill: 0.0.4 + optionalDependencies: + react: 19.2.4 + optional: true + + path-exists@4.0.0: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-prebuilds@1.0.0: + dependencies: + yargs: 17.7.2 + + pngjs@5.0.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + + react-guitar@1.1.3(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) + '@tonaljs/interval': 4.8.2 + '@tonaljs/note': 4.12.1 + classnames: 2.5.1 + color: 4.2.3 + keyboard-key: 1.1.0 + lodash.range: 3.2.0 + lodash.uniqueid: 4.0.1 + react: 19.2.4 + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-is@16.13.1: {} + + react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + cookie: 1.1.1 + react: 19.2.4 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.4(react@19.2.4) + + react@19.2.4: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + reconnecting-websocket@4.4.0: {} + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-from@4.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sample-player@0.5.5: + dependencies: + adsr: 1.0.1 + midimessage: 1.0.5 + note-parser: 1.1.0 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@7.7.4: {} + + set-blocking@2.0.0: {} + + set-cookie-parser@2.7.2: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + soundfont-player@0.12.0: + dependencies: + audio-loader: 0.5.0 + note-parser: 2.0.1 + sample-player: 0.5.5 + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + springboard@0.0.1-dev-jamapp-10(crossws@0.4.4)(immer@11.1.4)(isomorphic-ws@4.0.1(ws@8.19.0))(kysely@0.28.11)(react-dom@19.2.4(react@19.2.4))(react-router@7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)(vite@7.3.1(@types/node@25.3.2)): + dependencies: + commander: 14.0.3 + dexie: 4.3.0 + json-rpc-2.0: 1.7.1 + reconnecting-websocket: 4.4.0 + optionalDependencies: + '@hono/node-server': 1.19.9(hono@4.12.3) + '@hono/node-ws': 1.3.0(@hono/node-server@1.19.9(hono@4.12.3))(hono@4.12.3) + better-sqlite3: 12.6.2 + crossws: 0.4.4 + hono: 4.12.3 + immer: 11.1.4 + isomorphic-ws: 4.0.1(ws@8.19.0) + kysely: 0.28.11 + partysocket: 1.1.16(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-router: 7.13.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + rxjs: 7.8.2 + vite: 7.3.1(@types/node@25.3.2) + ws: 8.19.0 + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tonal@6.4.3: + dependencies: + '@tonaljs/abc-notation': 4.9.1 + '@tonaljs/array': 4.8.4 + '@tonaljs/chord': 6.1.2 + '@tonaljs/chord-type': 5.1.1 + '@tonaljs/collection': 4.9.0 + '@tonaljs/core': 5.0.2 + '@tonaljs/duration-value': 4.9.0 + '@tonaljs/interval': 5.1.0 + '@tonaljs/key': 4.11.2 + '@tonaljs/midi': 4.10.2 + '@tonaljs/mode': 4.9.2 + '@tonaljs/note': 4.12.1 + '@tonaljs/pcset': 4.10.1 + '@tonaljs/progression': 4.9.2 + '@tonaljs/range': 4.9.2 + '@tonaljs/rhythm-pattern': 1.0.0 + '@tonaljs/roman-numeral': 4.9.1 + '@tonaljs/scale': 4.13.4 + '@tonaljs/scale-type': 4.9.2 + '@tonaljs/time-signature': 4.9.0 + '@tonaljs/voice-leading': 5.1.2 + '@tonaljs/voicing': 5.1.3 + '@tonaljs/voicing-dictionary': 5.1.3 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@0.0.3: {} + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + util-deprecate@1.0.2: {} + + vite@7.3.1(@types/node@25.3.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.3.2 + fsevents: 2.3.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + webmidi@3.1.14: + dependencies: + djipevents: 2.0.7 + optionalDependencies: + jzz: 1.9.6 + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-module@2.0.1: {} + + wled-client@0.22.1: + dependencies: + '@js-bits/fetch': 1.0.0 + isomorphic-ws: 4.0.1(ws@8.19.0) + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yaml@1.10.2: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@21.1.1: {} + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + zod@3.25.76: + optional: true diff --git a/src/components/application_shell.tsx b/src/components/application_shell.tsx new file mode 100644 index 0000000..185d792 --- /dev/null +++ b/src/components/application_shell.tsx @@ -0,0 +1,190 @@ +import React, {useState, useEffect, createContext, useContext} from 'react'; +import {useLocation, useNavigate} from 'react-router'; +import type {Module} from 'springboard/module_registry/module_registry'; + +type Props = React.PropsWithChildren<{ + modules: Module[]; +}>; + +const ThemeContext = createContext({ isDark: false, toggleTheme: () => {} }); + +export const ApplicationShell = (props: Props) => { + const [isDark, setIsDark] = useState(() => { + // Check initial color scheme + const scheme = document.documentElement.style.colorScheme; + return scheme === 'dark'; + }); + + const toggleTheme = () => { + setIsDark(!isDark); + document.documentElement.style.colorScheme = isDark ? 'light' : 'dark'; + }; + + return ( + +
+
+
+ +
+ {props.children} +
+
+
+
+ ); +}; + +const Header = () => { + const { isDark, toggleTheme } = useContext(ThemeContext); + + return ( +
+

JamTools

+ +
+ ); +}; + +type SidebarProps = { + modules: Module[]; +}; + +const Sidebar = (props: SidebarProps) => { + const { isDark } = useContext(ThemeContext); + const location = useLocation(); + const navigate = useNavigate(); + const [expandedModule, setExpandedModule] = useState(null); + + // Parse current location to highlight active module/route + const parsed = location.pathname.split('/'); + let activeModuleId = ''; + let activeSubpath = ''; + + if (parsed.includes('modules')) { + const modIdx = parsed.indexOf('modules'); + activeModuleId = parsed[modIdx + 1] || ''; + activeSubpath = parsed.slice(modIdx + 2).join('/'); + } + + const modulesWithRoutes = props.modules.filter(m => m.routes); + + const navigateToRoute = (moduleId: string, route: string) => { + if (route.startsWith('/')) { + navigate(route); + return; + } + navigate(`/modules/${moduleId}/${route}`); + }; + + return ( + + ); +}; diff --git a/src/features/src/components/QRCode.tsx b/src/features/src/components/QRCode.tsx new file mode 100644 index 0000000..85a2713 --- /dev/null +++ b/src/features/src/components/QRCode.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import QRCodeLibrary from 'qrcode'; + +type Props = { + url: string; + style: React.CSSProperties; +} + +export const QRCode = (props: Props) => { + React.useEffect(() => { + QRCodeLibrary.toCanvas(document.getElementById('qr-canvas'), props.url, { + scale: 8 + }); + }, []); + + return ( + + ); +}; diff --git a/src/features/src/modules/dashboards/dashboards_module.tsx b/src/features/src/modules/dashboards/dashboards_module.tsx new file mode 100644 index 0000000..c226b3b --- /dev/null +++ b/src/features/src/modules/dashboards/dashboards_module.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import {Link} from 'react-router'; + +import springboard from 'springboard'; + +import {ModuleAPI} from 'springboard/engine/module_api'; + +import allDashboards from '.'; + +export type RegisteredDashboard = { + dashboard: (moduleAPI: ModuleAPI, dashboardId: string) => Promise; + id: string; + label: string; +} + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + Dashboards: DashboardsModuleReturnValue; + } +} + +type DashboardsModuleReturnValue = { + +}; + +springboard.registerModule('Dashboards', {}, async (moduleAPI): Promise => { + const promises = allDashboards.map(d => d.dashboard(moduleAPI, d.id)); + await Promise.all(promises); + + moduleAPI.registerRoute('', {}, () => { + return ( +
+

Dashboards:

+
    + {allDashboards.map(d => ( +
  • + + {d.label} + +
  • + ))} +
+
+ ); + }); + + return {}; +}); diff --git a/src/features/src/modules/dashboards/index.ts b/src/features/src/modules/dashboards/index.ts new file mode 100644 index 0000000..4ba8028 --- /dev/null +++ b/src/features/src/modules/dashboards/index.ts @@ -0,0 +1,11 @@ +import type {RegisteredDashboard} from './dashboards_module'; + +import keytarAndFootDashboard from './keytar_and_foot_dashboard/keytar_and_foot_dashboard'; + +export default [ + { + dashboard: keytarAndFootDashboard, + id: 'keytar_and_foot_dashboard', + label: 'Keytar and Foot dashboard', + }, +] as RegisteredDashboard[]; diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/chord_map.ts b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/chord_map.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/chord_player.ts b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/chord_player.ts new file mode 100644 index 0000000..be16cc7 --- /dev/null +++ b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/chord_player.ts @@ -0,0 +1,114 @@ +import {OutputMidiDevice} from '@jamtools/core/modules/macro_module/macro_handlers/outputs/musical_keyboard_output_macro_handler'; + +export type ChordNotes = number[]; + +export type ChordWithName = { + notes: ChordNotes; + name: string; +}; + +export const playChord = (scaleRoot: number, notePlayed: number, previousChord: ChordWithName | null, output: OutputMidiDevice): ChordWithName | null => { + const chord = getChord(scaleRoot, notePlayed); + if (!chord) { + return null; + } + + let notesToStop: ChordNotes = []; + let notesToPlay: ChordNotes = chord.notes; + + if (previousChord) { + notesToStop = previousChord.notes.filter(note => !chord.notes.includes(note)); + notesToPlay = chord.notes.filter(note => !previousChord.notes.includes(note)); + } + + notesToStop.forEach(note => { + output.send({type: 'noteoff', number: note + 12}); + }); + + notesToPlay.forEach(note => { + output.send({type: 'noteon', number: note + 12}); + }); + + return chord; +}; + +const getChord = (scaleRoot: number, notePlayed: number): ChordWithName | null => { + const diff = ((notePlayed - scaleRoot) + 12) % 12; + + const scaleType = scaleIntervals[diff]; + if (!scaleType) { + return null; + } + + const chord = chordMap[notePlayed % 12]![scaleType]; + + return { + notes: chord, + name: `${noteNames[notePlayed % 12]} ${scaleType}`, + }; +}; + +const scaleIntervals: Record = { + 0: 'major', + 2: 'minor', + 3: 'major', + 4: 'minor', + 5: 'major', + 7: 'major', + 8: 'major', + 9: 'minor', + 10: 'major', +}; + +export const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; + +const chordMap: Record> = { + 0: { // C + major: [36, 48, 52, 55, 60, 64], // Bass C added (MIDI 36) + minor: [36, 48, 51, 55, 60, 63], // Bass C added + }, + 1: { // C#/Db + major: [37, 49, 53, 56, 61, 65], // Bass C# (MIDI 37) + minor: [37, 49, 52, 56, 61, 64], // Bass C# added + }, + 2: { // D + major: [38, 50, 54, 57, 62, 66], // Bass D (MIDI 38) + minor: [38, 50, 53, 57, 62, 65], // Bass D added + }, + 3: { // D#/Eb + major: [39, 51, 55, 58, 63, 67], // Bass Eb (MIDI 39) + minor: [39, 51, 54, 58, 63, 66], // Bass Eb added + }, + 4: { // E + major: [40, 47, 52, 56, 59, 64], // Bass E (MIDI 40) + minor: [40, 47, 52, 55, 59, 64], // Bass E added + }, + 5: { // F + major: [41, 48, 53, 57, 60, 65], // Bass F (MIDI 41) + minor: [41, 48, 53, 56, 60, 65], // Bass F added + }, + 6: { // F#/Gb + major: [42, 49, 54, 58, 61, 66], // Bass F# (MIDI 42) + minor: [42, 49, 54, 57, 61, 66], // Bass F# added + }, + 7: { // G + major: [43, 47, 50, 55, 59, 67], // Bass G (MIDI 43) + minor: [43, 46, 50, 55, 58, 67], // Bass G added + }, + 8: { // G#/Ab + major: [44, 48, 51, 56, 60, 68], // Bass G# (MIDI 44) + minor: [44, 47, 51, 56, 59, 68], // Bass G# added + }, + 9: { // A + major: [33, 45, 49, 52, 57, 61, 64], // Bass A (MIDI 33) + minor: [33, 45, 48, 52, 57, 60, 64], // Bass A added + }, + 10: { // A#/Bb + major: [34, 46, 50, 53, 58, 62, 65], // Bass Bb (MIDI 34) + minor: [34, 46, 49, 53, 58, 61, 65], // Bass Bb added + }, + 11: { // B + major: [35, 47, 51, 54, 59, 63, 66], // Bass B (MIDI 35) + minor: [35, 47, 50, 54, 59, 62, 66], // Bass B added + } +}; diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx new file mode 100644 index 0000000..8010497 --- /dev/null +++ b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/keytar_and_foot_dashboard.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import {ModuleAPI} from 'springboard/engine/module_api'; + +import {MultiOctaveSupervisor} from './multi_octave_supervisor'; +import {SingleOctaveRootModeSupervisor} from './single_octave_root_mode_supervisor'; + +const KeytarAndFootDashboard = async (moduleAPI: ModuleAPI, dashboardName: string) => { + const multiOctaveSupervisor = new MultiOctaveSupervisor(moduleAPI, dashboardName + '|multi-octave'); + const singleOctaveSupervisor = new SingleOctaveRootModeSupervisor(moduleAPI, dashboardName + '|single-octave-root-mode'); + + const randomNoteMacro = await moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'randomNoteTrigger', 'midi_button_input', { + onTrigger: () => { + const randomNoteModule = moduleAPI.deps.module.moduleRegistry.getModule('RandomNote'); + randomNoteModule.togglePlaying(); + }, + }); + + await Promise.all([ + multiOctaveSupervisor.initialize(), + singleOctaveSupervisor.initialize(), + ]); + + moduleAPI.registerRoute('kiosk', {hideApplicationShell: true}, () => ( + + )); + + moduleAPI.registerRoute(dashboardName, {}, () => ( +
+

+ Keytar and Foot Dashboard +

+ +
+

Random note trigger

+ + +
+ +
+

Multi octave

+ +
+ +
+

Single octave

+ +
+
+ )); +}; + +export default KeytarAndFootDashboard; diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx new file mode 100644 index 0000000..42052f8 --- /dev/null +++ b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/module_or_snack_template.tsx @@ -0,0 +1,85 @@ +import React from 'react'; + +import springboard from 'springboard'; +import {ModuleAPI} from 'springboard/engine/module_api'; + +type AwaitedRecord>> = { + [Key in keyof Obj]: Awaited; +}; + +async function promiseAllObject>>( + obj: Obj +): Promise> { + const entries = Object.entries(obj); + const resolvedValues = await Promise.all(entries.map(([_, promise]) => promise)); + + return Object.fromEntries(entries.map(([key], index) => [key, resolvedValues[index]])) as AwaitedRecord; +} + +const createStates = async (moduleAPI: ModuleAPI) => { + return promiseAllObject({ + myState: moduleAPI.statesAPI.createSharedState('myState', 'initial state'), + }); +}; + +const createMacros = async (moduleAPI: ModuleAPI) => { + return promiseAllObject({ + myMacro: moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, '', 'midi_button_input', {}), + }); +}; + +type Actions = { + changeTheThing: (args: {newValue: string}) => void; +} + +springboard.registerModule('ModuleOrSnackTemplate', {}, async (moduleAPI): Promise => { + const states = await createStates(moduleAPI); + const macros = await createMacros(moduleAPI); + + const actions: Actions = { + changeTheThing: moduleAPI.createAction('changeTheThing', {}, async ({newValue}) => { + states.myState.setState(newValue); + }), + }; + + registerRoutes(moduleAPI, states, macros, actions); + + const sub = macros.myMacro.subject.subscribe(() => { + + }); + // moduleAPI.onDestroy(sub.unsubscribe); + + return {}; +}); + +type States = Awaited>; +type Macros = Awaited>; + +const registerRoutes = (moduleAPI: ModuleAPI, states: States, macros: Macros, actions: Actions) => { + moduleAPI.registerRoute('', {}, () => { + const myState = states.myState.useState(); + + return ( +
+ My state: {myState.toString()} + +
+ ); + }); +}; + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + ModuleOrSnackTemplate: ModuleOrSnackTemplateModuleReturnValue; + } +} + +type ModuleOrSnackTemplateModuleReturnValue = { + +}; diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/multi_octave_supervisor.tsx b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/multi_octave_supervisor.tsx new file mode 100644 index 0000000..42348d6 --- /dev/null +++ b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/multi_octave_supervisor.tsx @@ -0,0 +1,165 @@ +import React from 'react'; + +import {ModuleAPI} from 'springboard/engine/module_api'; +import {MidiEvent, MidiEventFull} from '@jamtools/core/modules/macro_module/macro_module_types'; + +type MultiOctaveSupervisorMidiState = { + currentlyHeldDownInputNotes: MidiEvent[]; +}; + +export class MultiOctaveSupervisor { + private macros!: Awaited>; + private states!: Awaited>; + private actions!: ReturnType; + + private midiState: MultiOctaveSupervisorMidiState = { + currentlyHeldDownInputNotes: [], + }; + + private handleKeyboardNote = async (fullEvent: MidiEventFull) => { + const event = fullEvent.event; + + this.macros.midiOutput.send({ + ...event, + velocity: 127, + }); + + if (event.type === 'noteon') { + if (!this.midiState.currentlyHeldDownInputNotes.find(e => e.number === event.number)) { + this.midiState = { + ...this.midiState, + currentlyHeldDownInputNotes: [ + ...this.midiState.currentlyHeldDownInputNotes, + event, + ], + }; + } + } + + if (event.type === 'noteoff') { + this.midiState = { + ...this.midiState, + currentlyHeldDownInputNotes: this.midiState.currentlyHeldDownInputNotes.filter(e => { + return e.number !== event.number; + }), + }; + } + + setTimeout(() => { + if (this.states.enableDebugging.getState()) { + this.states.debugSavedInputEvent.setState(fullEvent); + this.states.debugMidiState.setState(this.midiState); + } + }); + }; + + private createActions = () => ({ + toggleDebugging: this.moduleAPI.createAction(`${this.kvPrefix}|toggleDebugging`, {}, async () => { + console.log('toggling debug mode', !this.states.enableDebugging.getState()); + this.states.enableDebugging.setState(!this.states.enableDebugging.getState()); + }), + }); + + public render: React.ElementType = () => { + const enableDebugging = this.states.enableDebugging.useState(); + const debugSavedInputEvent = this.states.debugSavedInputEvent.useState(); + const debugMidiState = this.states.debugMidiState.useState(); + + return ( + <> + + + ); + }; + + private renderDebugData = ({debugSavedInputEvent, enableDebugging, debugMidiState}: {debugSavedInputEvent: MidiEventFull | null, enableDebugging: boolean, debugMidiState: MultiOctaveSupervisorMidiState}) => { + const [showDebugData, setShowDebugData] = React.useState(false); + + return ( + <> + + + + + {showDebugData && ( + <> +

Paged octave input:

+
+ +
+ +

Paged octave output:

+
+ +
+ + {debugSavedInputEvent && ( +
+                                {JSON.stringify(debugSavedInputEvent, null, 2)}
+                            
+ )} + + {debugMidiState && ( +
+                                {JSON.stringify(debugMidiState, null, 2)}
+                            
+ )} + + )} + + ); + }; + + constructor(private moduleAPI: ModuleAPI, private kvPrefix: string) { } + + public initialize = async () => { + this.macros = await this.createMacros(); + this.states = await this.createStates(); + this.actions = this.createActions(); + }; + + private createMacros = async () => { + const makeMacroName = (name: string) => `${this.kvPrefix}|${name}`; + + const pagedOctaveInput = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('pagedOctaveInput'), 'musical_keyboard_paged_octave_input', { + onTrigger: (event) => { + this.handleKeyboardNote(event); + }, + }); + + const midiOutput = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('midiOutput'), 'musical_keyboard_output', {}); + + return { + pagedOctaveInput, + midiOutput, + } as const; + }; + + private createStates = async () => { + const makeStateName = (name: string) => `${this.kvPrefix}|${name}`; + + const [ + enableDebugging, + debugSavedInputEvent, + debugMidiState, + ] = await Promise.all([ + this.moduleAPI.statesAPI.createPersistentState(makeStateName('enableDebugging'), true), + this.moduleAPI.statesAPI.createSharedState(makeStateName('debugSavedInputEvent'), null), + this.moduleAPI.statesAPI.createSharedState(makeStateName('debugMidiState'), this.midiState), + ]); + + return { + enableDebugging, + debugSavedInputEvent, + debugMidiState, + }; + }; +} diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/scale_supervisor.tsx b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/scale_supervisor.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/features/src/modules/dashboards/keytar_and_foot_dashboard/single_octave_root_mode_supervisor.tsx b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/single_octave_root_mode_supervisor.tsx new file mode 100644 index 0000000..3b5097c --- /dev/null +++ b/src/features/src/modules/dashboards/keytar_and_foot_dashboard/single_octave_root_mode_supervisor.tsx @@ -0,0 +1,385 @@ +import React from 'react'; + +import {ModuleAPI} from 'springboard/engine/module_api'; +import {MidiEvent, MidiEventFull} from '@jamtools/core/modules/macro_module/macro_module_types'; +import {playChord, ChordWithName, noteNames} from './chord_player'; +import {OutputMidiDevice} from '@jamtools/core/modules/macro_module/macro_handlers/outputs/musical_keyboard_output_macro_handler'; +import {QRCode} from '../../../components/QRCode'; + +type SingleOctaveRootModeSupervisorMidiState = { + currentlyHeldDownInputNotes: MidiEvent[]; + currentSustainingChord: ChordWithName | null; + currentChordRoot: number | null; + scaleRoot: number; + choosingScale: boolean; +}; + +const muteKeyboard = (midiOutput: OutputMidiDevice) => { + for (let i = 0; i < 84; i++) { + midiOutput.send({ + number: i, + type: 'noteoff', + }); + } +}; + +export class SingleOctaveRootModeSupervisor { + private macros!: Awaited>; + private states!: Awaited>; + private actions!: ReturnType; + + private midiState: SingleOctaveRootModeSupervisorMidiState = { + currentlyHeldDownInputNotes: [], + currentSustainingChord: null, + currentChordRoot: null, + scaleRoot: 0, + choosingScale: false, + }; + + private handleNoteOnForSustainedKeyboard = (event: MidiEvent) => { + // uncomment for regular non-chord hold mode + // this.macros.sustainedOutput.send(event); + // return + + if (this.midiState.choosingScale) { + this.midiState = { + ...this.midiState, + scaleRoot: event.number % 12, + choosingScale: false, + }; + return; + } + + const newChord = playChord(this.midiState.scaleRoot, event.number, this.midiState.currentSustainingChord, this.macros.sustainedOutput); + + + if (newChord) { + this.midiState = { + ...this.midiState, + currentSustainingChord: newChord, + }; + } + }; + + private handleNoteOffForSustainedKeyboard = (event: MidiEvent) => { + // no-op + }; + + private toggleChooseScale = () => { + this.midiState = { + ...this.midiState, + choosingScale: !this.midiState.choosingScale + }; + + setTimeout(() => { + if (this.states.enableDebugging.getState()) { + this.states.debugMidiState.setState(this.midiState); + } + }); + }; + + private handleNoteOnForStaccatoChord = (event: MidiEvent) => { + // TODO: play chord + // this.macros.stacattoOutput.send(event); + playChord(this.midiState.scaleRoot, event.number, null, this.macros.stacattoOutput); + }; + + private handleNoteOffForStaccatoChord = (event: MidiEvent) => { + muteKeyboard(this.macros.stacattoOutput); + }; + + private handleNoteOnForMonoBass = (event: MidiEvent) => { + // TODO: make sure this is a good octave. gotta allow octaves on outputs + // this.macros.monoBassOutput.send(event); + + if (event.number === this.midiState.currentChordRoot) { + return; + } + + if (this.midiState.currentChordRoot) { + this.macros.monoBassOutput.send({type: 'noteoff', number: this.midiState.currentChordRoot}); + } + + this.macros.monoBassOutput.send({type: 'noteon', number: event.number}); + + this.midiState = { + ...this.midiState, + currentChordRoot: event.number, + }; + + }; + + private handleNoteOffForMonoBass = (event: MidiEvent) => { + // this.macros.monoBassOutput.send(event); + }; + + private handleNoteOn = (event: MidiEvent) => { + this.handleNoteOnForSustainedKeyboard(event); + this.handleNoteOnForStaccatoChord(event); + this.handleNoteOnForMonoBass(event); + + if (!this.midiState.currentlyHeldDownInputNotes.find(e => e.number === event.number)) { + this.midiState = { + ...this.midiState, + currentlyHeldDownInputNotes: [ + ...this.midiState.currentlyHeldDownInputNotes, + event, + ], + }; + } + }; + + private handleNoteOff = (event: MidiEvent) => { + this.handleNoteOffForSustainedKeyboard(event); + this.handleNoteOffForStaccatoChord(event); + this.handleNoteOffForMonoBass(event); + + this.midiState = { + ...this.midiState, + currentlyHeldDownInputNotes: this.midiState.currentlyHeldDownInputNotes.filter(e => { + return e.number !== event.number; + }), + }; + }; + + private handleKeyboardNote = async (fullEvent: MidiEventFull) => { + const event = fullEvent.event; + + if (event.type === 'noteon') { + this.handleNoteOn(event); + } else if (event.type === 'noteoff') { + this.handleNoteOff(event); + } else { + return; + } + + setTimeout(() => { + if (this.states.enableDebugging.getState()) { + this.states.debugSavedInputEvent.setState(fullEvent); + this.states.debugMidiState.setState(this.midiState); + } + }); + }; + + private createActions = () => ({ + toggleDebugging: this.moduleAPI.createAction(`${this.kvPrefix}|toggleDebugging`, {}, async () => { + console.log('toggling debug mode', !this.states.enableDebugging.getState()); + this.states.enableDebugging.setState(!this.states.enableDebugging.getState()); + }), + }); + + public renderKiosk: React.ElementType = () => { + const debugMidiState = this.states.debugMidiState.useState(); + + return ( + <> + + + + ); + }; + + private renderScaleAndChords = ({debugMidiState}: {debugMidiState: SingleOctaveRootModeSupervisorMidiState}) => { + const smallFont = {fontSize: '48px', margin: '0px'}; + const bigFont = {fontSize: '96px', margin: '0px'}; + + const hrMargin = '40px'; + + return ( + <> +

+ Scale: +

+

+ {noteNames[debugMidiState.scaleRoot]} Major +

+
+

+ Chord: +

+

+ {debugMidiState.currentSustainingChord?.name || 'None'} +

+ + ); + }; + + private renderDiscordLink = () => { + const discordLinkStyle: React.CSSProperties = {position: 'absolute', right: '20px', fontSize: '40px'}; + + const url = 'jam.tools/discord'; + + return ( + <> + + {url} + + + + + ); + }; + + public render: React.ElementType = () => { + const enableDebugging = this.states.enableDebugging.useState(); + const debugSavedInputEvent = this.states.debugSavedInputEvent.useState(); + const debugMidiState = this.states.debugMidiState.useState(); + + return ( + <> + + + + ); + }; + + private renderDebugData = ({debugSavedInputEvent, enableDebugging, debugMidiState}: {debugSavedInputEvent: MidiEventFull | null, enableDebugging: boolean, debugMidiState: SingleOctaveRootModeSupervisorMidiState}) => { + const [showDebugData, setShowDebugData] = React.useState(false); + + return ( + <> + + + + + {showDebugData && ( + <> +

Single octave root mode input:

+
+ +
+ +

Sustained output:

+
+ +
+ +

Sustained output mute:

+
+ + +
+ +

Stacatto output:

+
+ +
+ +

Monobass output:

+
+ +
+ +

Scale chooser:

+
+ + +
+ + {debugSavedInputEvent && ( +
+                                {JSON.stringify(debugSavedInputEvent, null, 2)}
+                            
+ )} + + {debugMidiState && ( +
+                                {JSON.stringify(debugMidiState, null, 2)}
+                            
+ )} + + )} + + ); + }; + + constructor(private moduleAPI: ModuleAPI, private kvPrefix: string) { } + + public initialize = async () => { + this.macros = await this.createMacros(); + this.states = await this.createStates(); + this.actions = this.createActions(); + }; + + private createMacros = async () => { + const makeMacroName = (name: string) => `${this.kvPrefix}|${name}`; + + const singleOctaveInput = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('singleOctaveInput'), 'musical_keyboard_paged_octave_input', { + singleOctave: true, + enableQwerty: true, + onTrigger: (event) => { + this.handleKeyboardNote(event); + }, + }); + + const sustainedOutputMute = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('sustainedOutputMute'), 'midi_button_input', { + onTrigger: () => { + this.midiState = { + ...this.midiState, + currentSustainingChord: null, + currentChordRoot: null, + }; + + muteKeyboard(this.macros.sustainedOutput); + muteKeyboard(this.macros.monoBassOutput); + }, + }); + + const sustainedOutput = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('sustainedOutput'), 'musical_keyboard_output', {}); + + const stacattoOutput = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('stacattoOutput'), 'musical_keyboard_output', {}); + const monoBassOutput = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('monoBassOutput'), 'musical_keyboard_output', {}); + + const chooseScaleButton = await this.moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(this.moduleAPI, makeMacroName('chooseScaleButton'), 'midi_button_input', { + onTrigger: () => { + this.toggleChooseScale(); + }, + }); + + return { + singleOctaveInput, + sustainedOutputMute, + sustainedOutput, + stacattoOutput, + monoBassOutput, + chooseScaleButton, + } as const; + }; + + private createStates = async () => { + const makeStateName = (name: string) => `${this.kvPrefix}|${name}`; + + const [ + enableDebugging, + debugSavedInputEvent, + debugMidiState, + ] = await Promise.all([ + this.moduleAPI.statesAPI.createPersistentState(makeStateName('enableDebugging'), true), + this.moduleAPI.statesAPI.createSharedState(makeStateName('debugSavedInputEvent'), null), + this.moduleAPI.statesAPI.createSharedState(makeStateName('debugMidiState'), this.midiState), + ]); + + return { + enableDebugging, + debugSavedInputEvent, + debugMidiState, + }; + }; +} diff --git a/src/features/src/modules/daw_interaction_module.tsx b/src/features/src/modules/daw_interaction_module.tsx new file mode 100644 index 0000000..a2de578 --- /dev/null +++ b/src/features/src/modules/daw_interaction_module.tsx @@ -0,0 +1,88 @@ +import React from 'react'; + +import springboard from 'springboard'; + +// import {GuitarComponent} from './song_structures/components/guitar'; + +springboard.registerModule('daw_interaction', {}, async (moduleAPI) => { + const sliderPositionState1 = await moduleAPI.statesAPI.createSharedState('slider_position_1', 0); + const sliderPositionState2 = await moduleAPI.statesAPI.createSharedState('slider_position_2', 0); + + const ccOutput1 = await moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'cc_output_1', 'midi_control_change_output', {}); + const ccOutput2 = await moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'cc_output_2', 'midi_control_change_output', {}); + + moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'cc_input_1', 'midi_control_change_input', { + allowLocal: true, + onTrigger: (event => { + if (event.event.value) { + sliderPositionState1.setState(event.event.value); + ccOutput1.send(event.event.value); + } + }), + }); + + moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'cc_input_2', 'midi_control_change_input', { + allowLocal: true, + onTrigger: (event => { + if (event.event.value) { + sliderPositionState2.setState(event.event.value); + ccOutput2.send(event.event.value); + } + }), + }); + + const handleSliderDrag = moduleAPI.createAction('slider_drag', {}, async (args: {index: 0 | 1, value: number}) => { + const output = [ccOutput1, ccOutput2][args.index]!; + output.send(args.value); + + const state = [sliderPositionState1, sliderPositionState2][args.index]!; + state.setState(args.value); + }); + + moduleAPI.registerRoute('', {}, () => { + const sliderPosition1 = sliderPositionState1.useState(); + const sliderPosition2 = sliderPositionState2.useState(); + + return ( + handleSliderDrag({index, value})} + /> + ); + }); +}); + +type DawInteractionPageProps = { + sliderPosition1: number; + sliderPosition2: number; + handleSliderDrag: (index: 0 | 1, value: number) => void; +} + +const DawInteractionPage = ({sliderPosition1, sliderPosition2, handleSliderDrag}: DawInteractionPageProps) => { + const sliders = ([sliderPosition1, sliderPosition2] as const).map((position, i) => ( +
+ handleSliderDrag(i as 0 | 1, parseInt(e.target.value))} + style={{writingMode: 'vertical-rl', direction: 'rtl'}} + /> +
{position}
+
+ )); + + return ( +
+
+ {sliders} +
+
+ ); +}; diff --git a/src/features/src/modules/eventide/eventide_module.tsx b/src/features/src/modules/eventide/eventide_module.tsx new file mode 100644 index 0000000..51b4cd7 --- /dev/null +++ b/src/features/src/modules/eventide/eventide_module.tsx @@ -0,0 +1,186 @@ +import React, {useEffect, useMemo, useState} from 'react'; + +import springbord from 'springboard'; +import '@jamtools/core/modules/macro_module/macro_module'; + +import './index.css'; + +import {TIMEFACTOR_PRESETS} from './timefactor_preset_constants'; + +type EventidePresetState = { + name: string; + bankNumber: number; + subBankNumber: number; +} + +springbord.registerModule('Eventide', {}, async (moduleAPI) => { + const currentPresetState = await moduleAPI.statesAPI.createPersistentState('currentPresetState', null); + const favoritedPresetsState = await moduleAPI.statesAPI.createPersistentState('favoritedPresets', []); + + const macroModule = moduleAPI.deps.module.moduleRegistry.getModule('macro'); + const eventideMacro = await macroModule.createMacro(moduleAPI, 'eventide_pedal', 'musical_keyboard_output', {}); + + const changePreset = moduleAPI.createAction('changePreset', {}, async (args: EventidePresetState) => { + currentPresetState.setState(args); + const programNumber = ((args.bankNumber - 1) * 2) + (args.subBankNumber - 1); + + eventideMacro.send({ + type: 'program', + number: programNumber, + }); + }); + + const changePresetByName = moduleAPI.createAction('changePresetByName', {}, async (args: {presetName: string}) => { + const words = args.presetName.split(' '); + const bankParts = words[0]!.split(':'); + + changePreset({ + name: '', + bankNumber: parseInt(bankParts[0]!), + subBankNumber: parseInt(bankParts[1]!), + }); + }); + + const togglePresetFavorited = moduleAPI.createAction('togglePresetFavorited', {}, async (args: {presetName: string}) => { + favoritedPresetsState.setState(currentState => { + const index = currentState.indexOf(args.presetName); + if (index !== -1) { + return [ + ...currentState.slice(0, index), + ...currentState.slice(index + 1), + ]; + } + + return [...currentState, args.presetName]; + }); + }); + + const DisplayPreset = (props: { + presetName: string; + }) => { + return ( +
  • changePresetByName({presetName: props.presetName})} + style={{cursor: 'pointer'}} + > + {props.presetName} +
  • + ); + }; + + // hideNavbar should really be "hideApplicationShell", and also be a global option + moduleAPI.registerRoute('', {hideApplicationShell: false}, () => { + const currentPreset = currentPresetState.useState(); + const favoritedPresets = favoritedPresetsState.useState(); + + const [formState, setFormState] = useState(currentPreset || {name: '', bankNumber: 1, subBankNumber: 1}); + + useEffect(() => { + if (currentPreset) { + setFormState(currentPreset); + } + }, [currentPreset]); + + const presetIndex = useMemo(() => { + if (!currentPreset) { + return 0; + } + + return ((currentPreset.bankNumber - 1) * 2) + (currentPreset.subBankNumber - 1); + }, [currentPreset]); + + const selectedPresetName = TIMEFACTOR_PRESETS[presetIndex]; + + const isFavorited = useMemo(() => { + if (!selectedPresetName) { + return false; + } + + return favoritedPresets.includes(selectedPresetName); + }, [selectedPresetName, favoritedPresets]); + + return ( +
    + + {selectedPresetName && ( + <> +

    + {selectedPresetName} +

    + Favorite + { + togglePresetFavorited({presetName: selectedPresetName}); + }} + /> + + )} + +
    + + Favorite presets + +
      + {favoritedPresets.map(preset => ( + + ))} +
    +
    + +
    + + All presets + +
      + {TIMEFACTOR_PRESETS.map(preset => ( + + ))} +
    +
    + +
    + + Bank number form + +
    +
    + + setFormState(state => ({...state, bankNumber: parseInt(e.target.value)}))} + /> +
    +
    + + setFormState(state => ({...state, subBankNumber: parseInt(e.target.value)}))} + /> +
    + +
    +
    +
    + ); + }); +}); diff --git a/src/features/src/modules/eventide/index.css b/src/features/src/modules/eventide/index.css new file mode 100644 index 0000000..92156e7 --- /dev/null +++ b/src/features/src/modules/eventide/index.css @@ -0,0 +1,7 @@ +details { + margin: 30px; +} + +li { + margin-bottom: 15px; +} diff --git a/src/features/src/modules/eventide/timefactor_preset_constants.ts b/src/features/src/modules/eventide/timefactor_preset_constants.ts new file mode 100644 index 0000000..a9cfc30 --- /dev/null +++ b/src/features/src/modules/eventide/timefactor_preset_constants.ts @@ -0,0 +1,102 @@ +export const TIMEFACTOR_PRESETS = [ + '1:1 Pristine Digital Delay [DigitalDelay]', + '1:2 Airplane Hanger [Multitap]', + '2:1 Vintage Filtered Echoes [VintageDelay]', + '2:2 Bridge of Sighs [DigitalDelay]', + '3:1 Guitars in Space [BandDelay]', + '3:2 Backwards Riffs [Reverse]', + '4:1 Volume Swell Helper [DuckedDelay]', + '4:2 Classic Slap [TapeEcho]', + '5:1 TwinDelay [DigitalDelay]', + '5:2 Classic Chorus [VintageDelay]', + '6:1 Schizo Delay [FilterPong]', + '6:2 Ethereal Echoes [Reverse]', + '7:1 Copy Cat [DigitalDelay]', + '7:2 Every Lead You Fake [ModDelay]', + '8:1 Where the Avenues Have No Name [VintageDelay]', + '8:2 Over the Rainbow [BandDelay]', + '9:1 Lost Lunch [ModDelay]', + '9:2 Ghost Town [MultiTap]', + '10:1 Pedal Pitch [ModDelay]', + '10:2 Dring Modulator [Reverse]', + '11:1 Reggae Wahdelay Dark [BandDelay]', + '11:2 Reggae Wahdelay Bright [BandDelay]', + '12:1 500 Seater [MultiTap]', + '12:2 Big Swellpad1 [MultiTap]', + '13:1 MultiTap Hall [MultiTap]', + '13:2 Ambient MultiTap [MultiTap]', + '14:1 MultiTap WetLive [MultiTap]', + '14:2 What I’m Looking For [DigitalDelay]', + '15:1 Galloper MultiTap [DigitalDelay]', + '15:2 Tapper [MultiTap]', + '16:1 Springy Verb [FilterPong]', + '16:2 Strummer [MultiTap]', + '17:1 Strummer2 [MultiTap]', + '17:2 King\'s Missile [MultiTap]', + '18:1 Reverse Flutters [Reverse]', + '18:2 Time Slips1 [BandDelay]', + '19:1 Time Slips2 [BandDelay]', + '19:2 Time Slips3 [BandDelay]', + '20:1 1.. and 2 [DigitalDelay]', + '20:2 1.. and 2 Mod [ModDelay]', + '21:1 Lazyphasy [ModDelay]', + '21:2 Funk Echo [ModDelay]', + '22:1 PhaserEcho1 [ModDelay]', + '22:2 PhaserEcho2 [ModDelay]', + '23:1 Long Vintage [VintageDelay]', + '23:2 AutoMarch [MultiTap]', + '24:1 Reverse Flutters2 [Reverse]', + '24:2 Pong Echoes [FilterPong]', + '25:1 Nice Thick Thickener [FilterPong]', + '25:2 Warped Record [TapeEcho]', + '26:1 Record Head [TapeEcho]', + '26:2 Wah Echo1 [BandDelay]', + '27:1 Long Vintage2 [VintageDelay]', + '27:2 WubbaWubbaWub [FilterPong]', + '28:1 Science Museum [FilterPong]', + '28:2 Reverse Space [Reverse]', + '29:1 Backwards All [Reverse]', + '29:2 Reverse Space2 [Reverse]', + '30:1 Backwards All2 [Reverse]', + '30:2 Reverse Space3 [Reverse]', + '31:1 Reverse No Crystals [Reverse]', + '31:2 Inside Out [Reverse]', + '32:1 FlutterWow [TapeEcho]', + '32:2 Big Warm Digital [DigitalDelay]', + '33:1 Trem Delays [DigitalDelay]', + '33:2 Dual Big Warm Digital [DigitalDelay]', + '34:1 Big Nice Day [DigitalDelay]', + '34:2 Filtered Digital [DigitalDelay]', + '35:1 ChorusyTrem [DigitalDelay]', + '35:2 Flaser [DigitalDelay]', + '36:1 Storm Brewing1 [DigitalDelay]', + '36:2 Storm Brewing2 [DigitalDelay]', + '37:1 Haw Echoes1 [BandDelay]', + '37:2 Haw Echoes2 [BandDelay]', + '38:1 Phasery Band [BandDelay]', + '38:2 Phasery Band2 [BandDelay]', + '39:1 Country Compressor [DuckedDelay]', + '39:2 Two Vintage Echoes [VintageDelay]', + '40:1 SlapVintage [VintageDelay]', + '40:2 Nice Old Echoes [VintageDelay]', + '41:1 Ambient Vintage [VintageDelay]', + '41:2 Long Old Delays [VintageDelay]', + '42:1 Rotten Old Delays [VintageDelay]', + '42:2 The Lo-Rez trail [VintageDelay]', + '43:1 Be Wowed [TapeEcho]', + '43:2 Phasery Banddelay [BandDelay]', + '44:1 Ramp Up Reverse [Reverse]', + '44:2 Creepy [ModDelay]', + '45:1 GooeyFilteropong [FilterPong]', + '45:2 Chorusphase No Delay [ModDelay]', + '46:1 Unnatural Room [MultiTap]', + '46:2 Turnit Back [Reverse]', + '47:1 Slap Phase [BandDelay]', + '47:2 Deep Phase Echo [ModDelay]', + '48:1 And Wobblier [TapeEcho]', + '48:2 More Reverse Slaps [Reverse]', + '49:1 Trashcan Flange [DigitalDelay]', + '49:2 It\'s the 5-0 [VintageDelay]', + '50:1 The Closer [VintageDelay]', + '50:2 Filter Echo [BandDelay]', +]; diff --git a/src/features/src/modules/hand_raiser.css b/src/features/src/modules/hand_raiser.css new file mode 100644 index 0000000..e0244e5 --- /dev/null +++ b/src/features/src/modules/hand_raiser.css @@ -0,0 +1,60 @@ +.hand-raiser-main { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 20px; +} + +.hand-raiser-center { + display: flex; + gap: 10; + justify-content: center; + height: 100%; +} + + +.hand-slider-container { + width: 200px; + position: relative; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding: 0 20px; +} + +.hand-raiser-main .slider-container { + display: flex; + flex-direction: row; + gap: 10; + align-items: center; + z-index: 1; + margin-bottom: 20px; +} + + +.hand { + pointer-events: none; + left: 50%; + transform: translateX(-50%); +} + +.hand img { + width: 200px; +} + +.hand-raiser-main .slider { + display: inline-block; + width: 60px; +} + +.hand-raiser-main .slider input { + writing-mode: vertical-rl; + direction: rtl; +} + +.hand-raiser-main .slider pre { + display: inline; +} diff --git a/src/features/src/modules/hand_raiser_module.tsx b/src/features/src/modules/hand_raiser_module.tsx new file mode 100644 index 0000000..111895f --- /dev/null +++ b/src/features/src/modules/hand_raiser_module.tsx @@ -0,0 +1,177 @@ +import {MidiControlChangeInputResult} from '@jamtools/core/modules/macro_module/macro_handlers/inputs/midi_control_change_input_macro_handler'; +import React from 'react'; + +import springboard from 'springboard'; + +import './hand_raiser.css'; + +springboard.registerModule('HandRaiser', {}, async (m) => { + const macroModule = m.getModule('macro'); + macroModule.setLocalMode(true); + + const states = await m.createStates({ + handPositions: [0, 0], + }); + + const actions = m.createActions({ + changeHandPosition: async (args: {index: number, value: number}) => { + states.handPositions.setStateImmer((positions) => { + positions[args.index] = args.value; + }); + + return {success: true}; + }, + }); + + const macros = await macroModule.createMacros(m, { + slider0: { + type: 'midi_control_change_input', + config: { + onTrigger: (midiEvent => { + if (midiEvent.event.value) { + actions.changeHandPosition({index: 0, value: midiEvent.event.value}); + } + }), + } + }, + slider1: { + type: 'midi_control_change_input', + config: { + onTrigger: (midiEvent => { + if (midiEvent.event.value) { + actions.changeHandPosition({index: 1, value: midiEvent.event.value}); + } + }), + } + }, + }); + + m.registerRoute('/', {}, () => { + const positions = states.handPositions.useState(); + + return ( +
    +
    + {positions.map((position, index) => ( + { + await actions.changeHandPosition({index, value}); + }} + macro={index === 0 ? macros.slider0 : macros.slider1} + /> + ))} +
    +
    + ); + }); +}); + +type HandRaiserModuleProps = { + position: number; + handlePositionChange: (position: number) => void; + macro: MidiControlChangeInputResult; +}; + +const HandSliderContainer = (props: HandRaiserModuleProps) => { + return ( +
    + +
    + +
    + Configure MIDI + +
    +
    +
    + ); +}; + +type HandProps = { + position: number; +} + +const Hand = (props: HandProps) => { + const bottomSpace = (props.position / 127) * (100 - 40) + 20; + + return ( +
    + +
    + ); +}; + +type SliderProps = { + value: number; + onChange: (value: number) => void; +} + +const Slider = (props: SliderProps) => { + return ( +
    + props.onChange(parseInt(e.target.value))} + max={127} + /> +
    {props.value}
    +
    + ); +}; + +type DataSyncRootRouteProps = { + sliderPositions: number[]; + handleSliderDrag: (index: number, value: number) => void; +} + +type HandRaiserPageProps = { + sliderPositions: number[]; + handleSliderDrag: (index: number, value: number) => void; +} + +const HandRaiserPage = (props: HandRaiserPageProps) => { + const {sliderPositions, handleSliderDrag} = props; + + const sliders = sliderPositions.map((position, i) => ( + handleSliderDrag(i, newValue)} + /> + )); + + const sliderHands = sliderPositions.map((position, i) => ( + + )); + + return ( +
    +

    hey

    +
    + {sliders} +
    + +
    + {sliderHands} +
    +
    + ); +}; diff --git a/src/features/src/modules/index.ts b/src/features/src/modules/index.ts new file mode 100644 index 0000000..7a1efab --- /dev/null +++ b/src/features/src/modules/index.ts @@ -0,0 +1,12 @@ +import './lighting/wled/wled_module'; +import './hand_raiser_module'; + +import './phone_jam/phone_jam_module'; +import './daw_interaction_module'; +import './song_structures_dashboards/song_structures_dashboards_module'; +import './ultimate_guitar/ultimate_guitar_module'; +import './midi_playback/midi_playback_module'; + +import './dashboards/dashboards_module'; +import './eventide/eventide_module'; +import './song_structures/song_structures_module'; diff --git a/src/features/src/modules/lighting/wled/wled_module.tsx b/src/features/src/modules/lighting/wled/wled_module.tsx new file mode 100644 index 0000000..a50ad5e --- /dev/null +++ b/src/features/src/modules/lighting/wled/wled_module.tsx @@ -0,0 +1,124 @@ +import React, {createContext} from 'react'; + +import {Subject} from 'rxjs'; + +// @ts-ignore: This line suppresses an issue with missing types in wled-client +import {WLEDClient} from 'wled-client'; + +import {CoreDependencies, ModuleDependencies} from 'springboard/types/module_types'; +import {BaseModule, ModuleHookValue} from 'springboard/modules/base_module/base_module'; +import {Module} from 'springboard/module_registry/module_registry'; + +import springboard from 'springboard'; + +type WledClientStatus = { + url: string; + connectionState: string; +} + +type WledClientUrlPair = { + url: string; + client: WLEDClient; +} + +type WledState = { + controllers: WledClientStatus[]; +}; + +type WledHookValue = ModuleHookValue; + +const WledContext = createContext({} as WledHookValue); + +springboard.registerClassModule((coreDeps: CoreDependencies, modDependencies: ModuleDependencies) => { + return new WledModule(coreDeps, modDependencies); +}); + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + wled: WledModule; + } +} + +// TODO: proxy wled UI in jamtools UI +export class WledModule implements Module { + moduleId = 'wled'; + + enabled = false; + + cleanup: (() => void)[] = []; + + routes = { + '': { + component: () => { + const mod = WledModule.use(); + + return ( +
    +                        {JSON.stringify(mod.state)}
    +                    
    + ); + }, + }, + }; + + // wled controllers need to be stored as hostnames, + // so they are readable and stay consistent for that controller + state: WledState = { + controllers: [ + { + url: '192.168.0.123', + connectionState: 'connecting', + }, + ] + }; + + clients: WledClientUrlPair[] = []; + + initialize = async () => { + setTimeout(this.initializeClients); + + const ioModule = this.moduleDeps.moduleRegistry.getModule('io'); + + const sub = ioModule.qwertyInputSubject.subscribe(event => { + if (event.key === 'w') { + this.doSomething(); + } + }); + + this.cleanup.push(sub.unsubscribe); + }; + + private initializeClients = async () => { + for (const controller of this.state.controllers) { + try { + const client = new WLEDClient(controller.url); + await client.connect(); + controller.connectionState = 'connected'; + + this.clients.push({ + url: controller.url, + client, + }); + } catch (e) { + controller.connectionState = 'failed: ' + e; + } + } + + this.setState({...this.state}); + }; + + private currentPalette = 0; + + private doSomething = () => { + for (const clientPair of this.clients) { + this.currentPalette = (this.currentPalette + 1) % clientPair.client.palettes.length; + clientPair.client.setPalette(this.currentPalette); + } + }; + + Provider: React.ElementType = BaseModule.Provider(this, WledContext); + static use = BaseModule.useModule(WledContext); + private setState = BaseModule.setState(this); + + constructor(private coreDeps: CoreDependencies, private moduleDeps: ModuleDependencies) {} +} diff --git a/src/features/src/modules/midi_playback/midi_playback_module.tsx b/src/features/src/modules/midi_playback/midi_playback_module.tsx new file mode 100644 index 0000000..1f2f1ad --- /dev/null +++ b/src/features/src/modules/midi_playback/midi_playback_module.tsx @@ -0,0 +1,97 @@ +import React from 'react'; + +import springboard from 'springboard'; + +import {ParsedMidiFile} from '@jamtools/core/modules/midi_files/midi_file_parser/midi_file_parser'; +import '@jamtools/core/modules'; + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + MidiPlayback: MidiPlaybackModuleReturnValue; + } +} + +type MidiPlaybackModuleReturnValue = { + +}; + +springboard.registerModule('MidiPlayback', {}, async (moduleAPI): Promise => { + const midiFileModule = moduleAPI.deps.module.moduleRegistry.getModule('MidiFile'); + + const savedMidiFileData = await moduleAPI.statesAPI.createPersistentState('savedMidiFileData', null); + + const outputDevice = await moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'outputDevice', 'musical_keyboard_output', {}); + + let currentIndex = -1; + + const inputTrigger = await moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'inputTrigger', 'midi_button_input', {onTrigger: (event) => { + if (event.event.type !== 'noteon') { + return; + } + + const midiData = savedMidiFileData.getState(); + if (!midiData) { + throw new Error('no saved midi data'); + } + + currentIndex = (currentIndex + 1) % midiData.events.length; + + const clusterToSend = midiData.events[currentIndex]!; + + for (const note of clusterToSend.notes) { + outputDevice.send({ + type: 'noteon', + number: note.midiNumber, + velocity: 100, // TODO: record velocity of midi notes from midi file + }); + } + + setTimeout(() => { + for (const note of clusterToSend.notes) { + outputDevice.send({ + type: 'noteoff', + number: note.midiNumber, + velocity: 0, + }); + } + }, 100); + }}); + + const handleParsedMidiFile = moduleAPI.createAction('handleParsedMidiFile', {}, async (args: {data: ParsedMidiFile}) => { + savedMidiFileData.setState(args.data); + }); + + moduleAPI.registerRoute('', {hideApplicationShell: false}, () => { + const savedState = savedMidiFileData.useState(); + + return ( +
    + handleParsedMidiFile({data})} + /> + + Input trigger: + + + Output device: + + + {savedState?.events.map((event, clusterIndex) => ( +
      + {event.notes.map((note, noteIndex) => ( +
    • + {note.midiNumber} +
    • + ))} +
    + ))} + + {/*
    +                    {JSON.stringify(savedState?.events, null, 2)}
    +                
    */} +
    + ); + }); + + return {}; +}); diff --git a/src/features/src/modules/phone_jam/phone_jam_module.tsx b/src/features/src/modules/phone_jam/phone_jam_module.tsx new file mode 100644 index 0000000..c410fa8 --- /dev/null +++ b/src/features/src/modules/phone_jam/phone_jam_module.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import springboard from 'springboard'; + +springboard.registerModule('phone_jam', {}, async (moduleAPI) => { + const outputMacro = await moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(moduleAPI, 'local_output', 'musical_keyboard_output', {allowLocal: true}); + + const playSound = () => { + outputMacro.send({type: 'noteon', number: 36, velocity: 100}); + + setTimeout(() => { + outputMacro.send({type: 'noteoff', number: 36}); + }, 1000); + }; + + moduleAPI.registerRoute('', {}, () => { + return ( + + ); + }); +}); + +type PhoneJamViewProps = { + onClickPlaySound: () => void; +} + +const PhoneJamView = (props: PhoneJamViewProps) => { + return ( +
    +

    + Phone jam yay man +

    + +
    + +
    +
    + ); +}; diff --git a/src/features/src/modules/song_structures/components/chord_display.tsx b/src/features/src/modules/song_structures/components/chord_display.tsx new file mode 100644 index 0000000..b5a683f --- /dev/null +++ b/src/features/src/modules/song_structures/components/chord_display.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import {MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS} from '@jamtools/core/constants/midi_number_to_note_name_mappings'; + +export type ChordChoice = { + root: number; + quality: 'major' | 'minor'; +}; + +type Props = { + chord: ChordChoice; +} + +export const ChordDisplay = (props: Props) => { + const cycled = (props.chord.root % 12) as keyof typeof MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS; + const noteName = MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS[cycled]; + + return ( +
    + {noteName} {props.chord.quality} +
    + ); +}; diff --git a/src/features/src/modules/song_structures/components/guitar_tab_view.tsx b/src/features/src/modules/song_structures/components/guitar_tab_view.tsx new file mode 100644 index 0000000..875e724 --- /dev/null +++ b/src/features/src/modules/song_structures/components/guitar_tab_view.tsx @@ -0,0 +1,203 @@ +import React from 'react'; + +import GuitarImport from 'react-guitar'; +import {ChordChoice} from './chord_display'; +const Guitar = (GuitarImport as unknown as {default: typeof GuitarImport}).default; + +type Props = { + numberOfStrings: number; + chosenFrets: ChosenNote[]; +} + +type ChosenNote = { + fret: number; + string: number; +} + +type GuitarChordRootsDisplayProps = { + chords: ChordChoice[]; +} + +const convertRootToChosenFret = (chord: ChordChoice): ChosenNote => { + const root = chord.root; + const firstString = (root >= 4) && (root <= 9); + const stringNumber = firstString ? 3 : 2; + + const fretNumber = firstString ? root - 4 : (root + 3) % 12; + + return { + fret: fretNumber, + string: stringNumber, + }; +}; + +export const GuitarChordRootsDisplay = (props: GuitarChordRootsDisplayProps) => { + const chosenNotes: ChosenNote[] = props.chords.map(root => convertRootToChosenFret(root)); + + return ( + + ); +}; + +export const BasicGuitarTabView = (props: Props) => { + const result: string[] = []; + + const firstRow: string[] = ['']; + const secondRow: string[] = ['']; + const lastRow: string[] = ['']; + for (let i = 0; i < props.chosenFrets.length; i++) { + firstRow.push(' '); + // firstRow.push((i + 1).toString()); + secondRow.push('-'); + lastRow.push(' '); + } + + firstRow.push(''); + secondRow.push(''); + lastRow.push(''); + + // result.push(firstRow.join(' | ')); + // result.push(secondRow.join('-|-')); + + for (let i = 0; i < props.numberOfStrings; i++) { + const innerResult: string[] = []; + // const innerResult: string[] = ['']; + for (const chosenFret of props.chosenFrets) { + if (chosenFret.string === i) { + innerResult.push(chosenFret.fret.toString()); + } else { + innerResult.push('-'); + } + } + innerResult.push(''); + result.push(innerResult.join(' | ')); + // result.push(innerResult.join(' | ')); + } + + // result.push(lastRow.join('---')); + + const fullText = result.join('\n'); + console.log(result); + + // const result = props.chosenFrets.map(chosenFret => { + // const innerResult: string[] = []; + // for (let i = 0; i < props.numberOfStrings; i++) { + // if (chosenFret.string === i) { + // innerResult.push(chosenFret.fret.toString()); + // } else { + // innerResult.push('-'); + // } + // } + + // return innerResult; + // }); + + // let fullString = ''; + // for (let i = 0; i < props.numberOfStrings; i++) { + + // if (chosenFret.string === i) { + // innerResult.push(chosenFret.fret.toString()); + // } else { + // innerResult.push('-'); + // } + // } + + return ( +
    +
    +                {fullText}
    +            
    +
    + ); +}; + +export const GuitarTabView = (props: Props) => { + const chosen = props.chosenFrets; + + const str = Array.from('0'.repeat(props.numberOfStrings)); + + const frets = chosen.map(_value => str); + + const stringsNew = chosen.map(_value => 1); + + return ( +
    + + + {chosen.map((chosenRoot, i) => { + const strings: number[] = []; + for (let i = 0; i < props.numberOfStrings; i ++) { + if (chosenRoot.string === i) { + strings.push(1); + } else { + strings.push(-1); + } + } + return ( + { + return {chosenRoot.fret}; + // return ['A', 'B', 'C', 'D'][index]; + }} + frets={{from: 1, amount: 1}} + /> + ); + })} + +
    + + {/* {frets.map((fret, i) => { + const strings: number[] = []; + fret.forEach((f, j) => { + const key = `${j+1}-${i}`; + if (chosen.includes(key)) { + strings.push(i); + } else { + strings.push(-1); + } + }); + return ( + { + const key = `${v+1}-${i}`; + const index = chosen.findIndex(k => k === key); + return {index + 1}; + // return ['A', 'B', 'C', 'D'][index]; + }} + frets={{from: 1, amount: 0}} + /> + ); + })} */} +
    + ); +}; diff --git a/src/features/src/modules/song_structures/song_structures_module.tsx b/src/features/src/modules/song_structures/song_structures_module.tsx new file mode 100644 index 0000000..4d8ab05 --- /dev/null +++ b/src/features/src/modules/song_structures/song_structures_module.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import springboard from 'springboard'; + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + song_structures: SongStructuresModuleReturnValue; + } +} + +type SongStructuresModuleReturnValue = { + +}; + +springboard.registerModule('song_structures', {}, async (moduleAPI): Promise => { + return {}; +}); diff --git a/src/features/src/modules/song_structures_dashboards/song_structures_dashboards_module.tsx b/src/features/src/modules/song_structures_dashboards/song_structures_dashboards_module.tsx new file mode 100644 index 0000000..96c7018 --- /dev/null +++ b/src/features/src/modules/song_structures_dashboards/song_structures_dashboards_module.tsx @@ -0,0 +1,245 @@ +import React from 'react'; + +import {Link} from 'react-router'; + +import springboard from 'springboard'; +import {MidiEvent} from '@jamtools/core/modules/macro_module/macro_module_types'; + +import {GuitarChordRootsDisplay, GuitarTabView} from '../song_structures/components/guitar_tab_view'; +import {ChordChoice, ChordDisplay} from '../song_structures/components/chord_display'; + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + song_structures_dashboards: SongStructuresDashboardsModuleReturnValue; + } +} + +type SongStructuresDashboardsModuleReturnValue = { + +}; + +type GuitarDisplaySettings = { + showGuitar: boolean; + showLetters: boolean; +}; + +const initialGuitarDisplaySettings: GuitarDisplaySettings = { + showGuitar: false, + showLetters: false, +}; + +springboard.registerModule('song_structures_dashboards', {}, async (moduleAPI): Promise => { + const states = moduleAPI.statesAPI; + const macros = moduleAPI.deps.module.moduleRegistry.getModule('macro'); + + const state = await states.createUserAgentState('guitar_display_settings', initialGuitarDisplaySettings); + + const draftChordsState = await states.createSharedState('draft_chord_choices', null); + const confirmedChordsState = await states.createSharedState('confirmed_chord_choices', null); + + // const draftScaleChoice = moduleAPI.statesAPI.createSharedState('', true); + // const confirmedScaleChoise = moduleAPI.statesAPI.createSharedState('', true); + + const musicalKeyboardInputMacro = await macros.createMacro(moduleAPI, 'keyboard_in', 'musical_keyboard_input', {enableQwerty: false}); + const musicalKeyboardOutputMacro = await macros.createMacro(moduleAPI, 'keyboard_out', 'musical_keyboard_output', {}); + + const toggleChordChooseMode = await macros.createMacro(moduleAPI, 'toggle_chord_choose_mode', 'midi_button_input', {enableQwerty: false}); + // const toggleChordChooseMode = moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(); + // const toggleScaleChooseMode = moduleAPI.deps.module.moduleRegistry.getModule('macro').createMacro(); + + // const messageState = await states.createSharedState('message', ''); + + const currentlyHeldNotes = new Set(); + + musicalKeyboardInputMacro.subject.subscribe(midiEvent => { + midiEvent.event.number = midiEvent.event.number % 12; + if (midiEvent.event.type === 'noteon') { + currentlyHeldNotes.add(midiEvent.event.number); + } else { + currentlyHeldNotes.delete(midiEvent.event.number); + } + + const draftChords = draftChordsState.getState(); + // const draftScale = draftScaleChoice.getState(); + + // if (draftScale !== null) { + // handleScaleChoiceInput(midiEvent.event); + // return; + // } + + if (draftChords !== null) { + if (midiEvent.event.type === 'noteon') { + handleChordChoiceInput(midiEvent.event); + } + return; + } + + handleMusicalKeyboardInput(midiEvent.event); + }); + + function handleChordChoiceInput(midiEvent: MidiEvent) { + const heldDown = Array.from(currentlyHeldNotes); + if (!heldDown.length || heldDown.length > 2) { + return; + } + + if (heldDown.length == 1) { + // draftState.setState(state => [...state, midiEvent.event.number]); + return; + } + + let chord: ChordChoice; + + const diff = (12 + midiEvent.number - heldDown[0]) % 12; + if (diff === 3) { + chord = { + root: heldDown[0] % 12, + quality: 'minor', + }; + } else if (diff === 4) { + chord = { + root: heldDown[0] % 12, + quality: 'major', + }; + } else { + return; + } + + const current = draftChordsState.getState() || []; + draftChordsState.setState([...current, chord]); + } + + function handleMusicalKeyboardInput(midiEvent: MidiEvent) { + musicalKeyboardOutputMacro.send(midiEvent); + } + + toggleChordChooseMode.subject.subscribe(midiEvent => { + if (midiEvent.event.type === 'noteoff') { + return; + } + + const draftChords = draftChordsState.getState(); + // const draftScale = draftScaleChoice.getState(); + + // if (draftScale) { + // return; + // } + + if (draftChords) { + // confirm + confirmedChordsState.setState(draftChords); + draftChordsState.setState(null); + // alert('confirmed') + } else { + // start drafting + // alert('start drafting') + draftChordsState.setState([]); + } + }); + + moduleAPI.registerRoute('', {}, () => { + return ( +
    + + + +
    + ); + }); + + moduleAPI.registerRoute('bass_guitar', {}, () => { + const props: React.ComponentProps = { + numberOfStrings: 4, + chosenFrets: [ + { + fret: 2, + string: 2, + }, + { + fret: 2, + string: 1, + }, + { + fret: 5, + string: 2, + }, + { + fret: 2, + string: 0, + }, + ], + }; + + const displaySettings = state.useState(); + console.log(displaySettings); + + const draftChords = draftChordsState.useState(); + const confirmedChords = confirmedChordsState.useState(); + + return ( + <> +
    +
    + + +
    + {displaySettings.showLetters && ( +
    + {/* */} +
    + Confirmed: +
    + {confirmedChords?.map((c, i) => ( + + ))} +
    +
    +
    + Draft: +
    + {draftChords?.map((c, i) => ( + + ))} +
    +
    +
    + )} +
    + +
    + {/* {displaySettings.showGuitar && ( +
    + +
    + )} */} + {displaySettings.showGuitar && ( +
    + +
    + )} +
    + + + ); + }); + + return {}; +}); diff --git a/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_main_view.tsx b/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_main_view.tsx new file mode 100644 index 0000000..48c62f3 --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_main_view.tsx @@ -0,0 +1,60 @@ +import React, {useMemo, useState} from 'react'; + +import {UltimateGuitarSetlist, UltimateGuitarSetlistStatus, UltimateGuitarTab} from '../ultimate_guitar_types'; +import {getTabFromCurrentSetlistData, prepareLyricsWithChords} from '../ultimate_guitar_utils'; + +type UltimateGuitarMainViewProps = { + currentSetlistStatus: UltimateGuitarSetlistStatus | null; + savedSetlists: UltimateGuitarSetlist[]; + savedTabs: UltimateGuitarTab[]; +}; + +export const UltimateGuitarMainView = (props: UltimateGuitarMainViewProps) => { + const {song, tab} = getTabFromCurrentSetlistData(props.currentSetlistStatus, props.savedSetlists, props.savedTabs); + + const [showChords, setShowChords] = useState(true); + const [wrapText, setWrapText] = useState(true); + + const toggleChords = () => setShowChords(!showChords); + const toggleWrap = () => setWrapText(!wrapText); + + const displayContent = useMemo( + () => { + if (!song || !tab) { + return ''; + } + + return prepareLyricsWithChords(tab.tabLyrics, {showChords, transpose: song.transpose}); + }, + [showChords, song] + ); + + if (!song || !tab) { + return ( +
    + {'Waiting for band leader to choose the song'} +
    + ); + } + + const preTagStyle: React.CSSProperties = wrapText ? {whiteSpace: 'pre-wrap', wordWrap: 'break-word'} : {}; + + return ( +
    + + +
    +

    + {tab.title} +

    +
    +
    +                {displayContent}
    +            
    +
    + ); +}; diff --git a/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_manage_view.tsx b/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_manage_view.tsx new file mode 100644 index 0000000..a80f35a --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_manage_view.tsx @@ -0,0 +1,293 @@ +import React from 'react'; + +import {UltimateGuitarSetlist, UltimateGuitarSetlistSong, UltimateGuitarSetlistStatus, UltimateGuitarTab} from '../ultimate_guitar_types'; +import {getTabFromCurrentSetlistData} from '../ultimate_guitar_utils'; + +type UltimateGuitarManageViewProps = { + currentSetlistStatus: UltimateGuitarSetlistStatus | null; + savedSetlists: UltimateGuitarSetlist[]; + savedTabs: UltimateGuitarTab[]; + submitPlaylistUrl: (url: string) => void; + createNewSetlist: (name: string) => Promise; + addTabUrlToSetlist: (setlistId: string, url: string) => Promise; + startSetlist: (setlistId: string) => void; + reorderSongUrlsForSetlist: (setlistId: string, songs: UltimateGuitarSetlistSong[]) => void; + queueSongForNext: (setlistId: string, song: UltimateGuitarSetlistSong) => Promise; + gotoNextSong: () => void; + transposeSong: (setlistId: string, url: string, transpose: number) => Promise; + gotoSong: (setlistId: string, index: number) => Promise; +}; + +export const UltimateGuitarManageView = (props: UltimateGuitarManageViewProps) => { + const { + setlist, + tab: song, + } = getTabFromCurrentSetlistData(props.currentSetlistStatus, props.savedSetlists, props.savedTabs); + + const currentSetlistName = setlist ? setlist.name : 'none'; + const currentSongName = song ? (song.title || song.url) : 'none'; + + const currentSongIndex = props.currentSetlistStatus ? props.currentSetlistStatus.songIndex + 1 : ''; + + const statusHeader = ( +
    +

    + Current setlist: {currentSetlistName} +

    +

    + Current song: #{currentSongIndex} +

    +

    + {currentSongName} +

    + +
    + ); + + return ( +
    +

    + Ultimate Guitar +

    + {statusHeader} + +

    + Existing setlists: +

    + {props.savedSetlists.map(setlist => ( + props.gotoSong(setlist.id, index)} + /> + ))} +
    + ); +}; + +type CreateNewSetlistFormProps = { + createSetlist: (name: string) => Promise; +} + +const CreateNewSetlistForm = (props: CreateNewSetlistFormProps) => { + const [draftSetlistName, setDraftSetlistName] = React.useState(''); + + return ( +
    + Create new setlist +
    + + setDraftSetlistName(e.target.value)} + /> + +
    +
    + ); +}; + +type SetlistDetailsProps = { + setlist: UltimateGuitarSetlist; + savedTabs: UltimateGuitarTab[]; + addTabUrlToSetlist: (setlistId: string, url: string) => Promise; + currentSetlistStatus: UltimateGuitarSetlistStatus | null; + startSetlist: (setlistId: string) => void; + reorderSongUrlsForSetlist: (setlistId: string, songs: UltimateGuitarSetlistSong[]) => void; + queueSongForNext: (setlistId: string, song: UltimateGuitarSetlistSong) => Promise; + transposeSong: (setlistId: string, url: string, transpose: number) => Promise; + gotoSong: (index: number) => Promise; +} + +const SetlistDetails = (props: SetlistDetailsProps) => { + const [draftTabUrl, setDraftTabUrl] = React.useState(''); + const {setlist} = props; + + const [queuedSongs, setQueuedSongs] = React.useState([]); + + const currentSongIndex = props.currentSetlistStatus?.setlistId === props.setlist.id ? props.currentSetlistStatus?.songIndex : -1; + + const submitQueue = async () => { + if (!queuedSongs.length) { + return; + } + + for (const url of [...queuedSongs].reverse()) { + await props.queueSongForNext(props.setlist.id, url); + await new Promise(r => setTimeout(r, 10)); + } + + setQueuedSongs([]); + }; + + const queueSong = (song: UltimateGuitarSetlistSong) => { + if (queuedSongs.includes(song)) { + setQueuedSongs([ + ...queuedSongs.slice(0, queuedSongs.indexOf(song)), + ...queuedSongs.slice(queuedSongs.indexOf(song) + 1), + ]); + return; + } + + setQueuedSongs([...queuedSongs, song]); + }; + + return ( +
    + {setlist.name} +
    + + +
    +
    + Add new tab + setDraftTabUrl(e.target.value)} + /> + +
    +
      + {setlist.songs.map((song, i) => { + const url = song.url; + const savedTab = props.savedTabs.find(t => t.url === url); + + if (!savedTab) { + return ( +
    • Not found: {url}
    • + ); + } + + return ( + props.gotoSong(i)} + /> + ); + })} +
    +
    + ); +}; + +type SetlistSongProps = { + setlistId: string; + savedTab: UltimateGuitarTab; + isCurrentSong: boolean; + queueSong: (song: UltimateGuitarSetlistSong) => void; + queued: boolean; + song: UltimateGuitarSetlistSong; + transposeSong: (setlistId: string, url: string, transpose: number) => Promise; + gotoSong: () => Promise; +}; + +const SetlistSong = (props: SetlistSongProps) => { + const [editingTranspose, setEditingTranspose] = React.useState(null); + + const tabName = props.savedTab.title || props.savedTab.url; + + const transposeValue = props.song.transpose; + + const handleTransposeButtonClick = () => { + setEditingTranspose(props.song.transpose); + }; + + const handleTransposeCancel = () => { + setEditingTranspose(null); + }; + + const handleTransposeConfirm = async () => { + await props.transposeSong(props.setlistId, props.savedTab.url, editingTranspose!); + setEditingTranspose(null); + }; + + return ( +
  • + {tabName} + + + + {(editingTranspose !== null) && ( + <> + + {editingTranspose} + + + + + )} +
  • + ); +}; diff --git a/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_qr_code.tsx b/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_qr_code.tsx new file mode 100644 index 0000000..7070390 --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/components/ultimate_guitar_qr_code.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import QRCode from 'qrcode'; + +export const UltimateGuitarQRCode = () => { + React.useEffect(() => { + const url = 'https://sqrlfest.jam.tools/modules/Ultimate_Guitar'; + QRCode.toCanvas(document.getElementById('qr-canvas'), url, { + scale: 8 + }); + }, []); + + return ( + + ); +}; diff --git a/src/features/src/modules/ultimate_guitar/ultimate_guitar_module.tsx b/src/features/src/modules/ultimate_guitar/ultimate_guitar_module.tsx new file mode 100644 index 0000000..b477526 --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/ultimate_guitar_module.tsx @@ -0,0 +1,355 @@ +import React from 'react'; + +import springboard from 'springboard'; +import {isErrorResponse} from 'springboard/types/response_types'; + +import {parseUltimateGuitarHTMLContent} from './ultimate_guitar_utils'; +import {UltimateGuitarService} from './ultimate_guitar_service'; +import {UltimateGuitarMainView} from './components/ultimate_guitar_main_view'; +import {UltimateGuitarSetlist, UltimateGuitarSetlistSong, UltimateGuitarSetlistStatus, UltimateGuitarTab, parseUltimateGuitarTabUrl} from './ultimate_guitar_types'; +import {UltimateGuitarManageView} from './components/ultimate_guitar_manage_view'; +import {generateId} from 'springboard/utils/generate_id'; +import {ModuleAPI} from 'springboard/engine/module_api'; +import {StateSupervisor} from 'springboard/services/states/shared_state_service'; +import {UltimateGuitarQRCode} from './components/ultimate_guitar_qr_code'; + +type UltimateGuitarModuleDependencies = { + domParser(htmlData: string): Document; + ultimateGuitarService: UltimateGuitarService; +} + +type UltimateGuitarModuleReturnValue = { + // getSetlists(): SavedSetlist[]; + // getSongsForSetlistId(setlistId: string): SavedUltimateGuitarSong[]; + // getSong(setlistId: string, songId: string): SavedUltimateGuitarSong; +} + +// declare module 'springboard/module_registry/module_registry' { +// interface ExtraModuleDependencies { +// } +// } + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + Ultimate_Guitar: UltimateGuitarModuleReturnValue; + } +} + +springboard.registerModule('Ultimate_Guitar', {}, async (moduleAPI): Promise => { + const states = new States(moduleAPI); + await states.initialize(); + + const actions = new Actions(moduleAPI, states); + + moduleAPI.registerRoute('', {hideApplicationShell: true}, () => ( + + )); + + moduleAPI.registerRoute('manage', {}, () => ( + actions.submitPlaylistUrl({url})} + createNewSetlist={(name: string) => actions.createNewSetlist({name})} + addTabUrlToSetlist={(setlistId: string, url: string) => actions.addTabUrlToSetlist({setlistId, url})} + startSetlist={(setlistId: string) => actions.startSetlist({setlistId})} + reorderSongUrlsForSetlist={(setlistId: string, songs: UltimateGuitarSetlistSong[]) => actions.reorderSongUrlsForSetlist({setlistId, songs})} + // gotoSong={(setlistId: string, songIndex: number) => actions.gotoSong({setlistId, songIndex})} + gotoNextSong={() => actions.gotoNextSong({})} + queueSongForNext={(setlistId: string, song: UltimateGuitarSetlistSong) => actions.queueSongForNext({setlistId, song})} + transposeSong={(setlistId: string, url: string, transpose: number) => actions.transposeSong({setlistId, url, transpose})} + gotoSong={(setlistId: string, songIndex: number) => actions.gotoSong({setlistId, songIndex})} + /> + )); + + moduleAPI.registerRoute('qrcode', {}, () => ( + + )); + + return {}; +}); + +class States { + savedSetlists!: StateSupervisor; + savedTabs!: StateSupervisor; + currentSetlistStatus!: StateSupervisor; + + constructor(private moduleAPI: ModuleAPI) {} + + public initialize = async () => { + const [ + savedSetlists, + savedTabs, + currentSetlistStatus, + ] = await Promise.all([ + this.moduleAPI.statesAPI.createPersistentState('saved_setlists', []), + this.moduleAPI.statesAPI.createPersistentState('saved_tabs', []), + this.moduleAPI.statesAPI.createPersistentState('current_setlist_status', null), + ]); + + this.savedSetlists = savedSetlists; + this.savedTabs = savedTabs; + this.currentSetlistStatus = currentSetlistStatus; + }; +} + +class Actions { + constructor(private moduleAPI: ModuleAPI, private states: States) {} + + gotoSong = this.moduleAPI.createAction('gotoSong', {}, async (args: {setlistId: string, songIndex: number}) => { + this.states.currentSetlistStatus.setState({ + setlistId: args.setlistId, + songIndex: args.songIndex, + }); + }); + + gotoNextSong = this.moduleAPI.createAction('gotoNextSong', {}, async () => { + const {currentSetlistStatus, savedSetlists} = this.states; + + const status = currentSetlistStatus.getState(); + if (!status) { + const setlist = savedSetlists.getState()[0]; + if (!setlist) { + return; + } + + currentSetlistStatus.setState({setlistId: setlist.id, songIndex: 0}); + return; + } + + const currentSongIndex = status.songIndex; + const setlist = savedSetlists.getState().find(s => s.id === status.setlistId)!; + + const nextIndex = (currentSongIndex + 1) % setlist.songs.length; + currentSetlistStatus.setState({setlistId: setlist.id, songIndex: nextIndex}); + }); + + queueSongForNext = this.moduleAPI.createAction('queueSongForNext', {}, async (args: {setlistId: string, song: UltimateGuitarSetlistSong}) => { + const {savedSetlists, currentSetlistStatus} = this.states; + + const status = currentSetlistStatus.getState(); + if (!status) { + throw new Error('no setlist in progress'); + } + + if (status.setlistId !== args.setlistId) { + throw new Error('song is not part of the current setlist'); + } + + const setlists = savedSetlists.getState(); + const setlistStoredIndex = setlists.findIndex(s => s.id === args.setlistId); + const setlist = setlists[setlistStoredIndex]; + + const currentUrl = setlist.songs[status.songIndex]; + + const newSongsState = insertStringAtIndex(setlist.songs, args.song, (s => s.url), status.songIndex + 1); + + const newStatusIndex = newSongsState.indexOf(currentUrl); + currentSetlistStatus.setState({...status, songIndex: newStatusIndex}); + + savedSetlists.setState([ + ...setlists.slice(0, setlistStoredIndex), + { + ...setlist, + songs: newSongsState, + }, + ...setlists.slice(setlistStoredIndex + 1), + ]); + }); + + createNewSetlist = this.moduleAPI.createAction('createNewSetlist', {}, async (args: {name: string}) => { + const {savedSetlists} = this.states; + + const id = generateId(); + const setlist: UltimateGuitarSetlist = { + id, + name: args.name, + songs: [], + }; + + savedSetlists.setState([...savedSetlists.getState(), setlist]); + }); + + startSetlist = this.moduleAPI.createAction('startSetlist', {}, async (args: {setlistId: string}) => { + const {currentSetlistStatus} = this.states; + + currentSetlistStatus.setState({ + setlistId: args.setlistId, + songIndex: 0, + }); + }); + + addTabUrlToSetlist = this.moduleAPI.createAction('addTabUrlToSetlist', {}, async (args: {setlistId: string, url: string}) => { + const {savedTabs, savedSetlists} = this.states; + + const tabs = savedTabs.getState(); + const setlists = savedSetlists.getState(); + const setlistStoredIndex = setlists.findIndex(s => s.id === args.setlistId); + if (setlistStoredIndex === -1) { + throw new Error(`no setlist with id '${args.setlistId}'`); + } + + const setlist = setlists[setlistStoredIndex]!; + if (setlist.songs.find(s => s.url === args.url)) { + throw new Error('setlist already includes this url'); + } + + const foundTab = tabs.find(t => t.url === args.url); + if (!foundTab) { + const ugService = new UltimateGuitarService(); + const tab = await handleSubmitTabUrl(args.url, { + // TODO: this code is unimplemented now, to get rid of ExtraModuleDependencies + domParser: (htmlString: string) => document, + ultimateGuitarService: ugService, + }); + if (typeof tab === 'string') { + throw new Error(tab); + } + + savedTabs.setState([...savedTabs.getState(), tab]); + } + + const newSong: UltimateGuitarSetlistSong = { + url: args.url, + transpose: 0, + }; + + savedSetlists.setState([ + ...setlists.slice(0, setlistStoredIndex), + { + ...setlist, + songs: [ + ...setlist.songs, + newSong, + ], + }, + ...setlists.slice(setlistStoredIndex + 1), + ]); + }); + + transposeSong = this.moduleAPI.createAction('transposeSong', {}, async (args: {setlistId: string, url: string, transpose: number}) => { + const {savedTabs, savedSetlists} = this.states; + + const setlists = savedSetlists.getState(); + const setlistStoredIndex = setlists.findIndex(s => s.id === args.setlistId); + if (setlistStoredIndex === -1) { + throw new Error(`no setlist with id '${args.setlistId}'`); + } + + const setlist = setlists[setlistStoredIndex]!; + const songIndex = setlist.songs.findIndex(s => s.url === args.url); + if (songIndex === -1) { + throw new Error('setlist does not have this song'); + } + + const existingSong = setlist.songs[songIndex]; + const newSong: UltimateGuitarSetlistSong = {...existingSong, transpose: args.transpose}; + + const newSongs = [ + ...setlist.songs.slice(0, songIndex), + newSong, + ...setlist.songs.slice(songIndex + 1), + ]; + + const newSetlist = {...setlist, songs: newSongs}; + const newSetlists = [ + ...setlists.slice(0, setlistStoredIndex), + newSetlist, + ...setlists.slice(setlistStoredIndex + 1), + ]; + + savedSetlists.setState(newSetlists); + }); + + reorderSongUrlsForSetlist = this.moduleAPI.createAction('reorderSongUrlsForSetlist', {}, async (args: {setlistId: string, songs: UltimateGuitarSetlistSong[]}) => { + const {savedSetlists} = this.states; + + const setlists = savedSetlists.getState(); + const setlistStoredIndex = setlists.findIndex(s => s.id === args.setlistId); + if (setlistStoredIndex === -1) { + throw new Error(`no setlist with id '${args.setlistId}'`); + } + + const setlist = setlists[setlistStoredIndex]!; + + savedSetlists.setState([ + ...setlists.slice(0, setlistStoredIndex), + { + ...setlist, + songs: args.songs, + }, + ...setlists.slice(setlistStoredIndex + 1), + ]); + }); + + submitPlaylistUrl = this.moduleAPI.createAction('submitPlaylistUrl', {}, async (args: {url: string}) => { + }); +} + +const handleSubmitTabUrl = async (url: string, deps: UltimateGuitarModuleDependencies): Promise => { + const parsed = parseUltimateGuitarTabUrl(url); + if (typeof parsed === 'string') { + return parsed; + } + + if (parsed.type === 'chords') { + const response = await deps.ultimateGuitarService.getChordsTabForUrl(url); + if (isErrorResponse(response)) { + return response.error; + } + + const doc = deps.domParser(response.data); + const data = parseUltimateGuitarHTMLContent(doc); + if (!data) { + return 'failed to parse ultimate guitar document'; + } + + const tab: UltimateGuitarTab = { + ...parsed, + title: data.title, + tabLyrics: data.tabData, + }; + + return tab; + } + + if (parsed.type === 'official') { + const response = await deps.ultimateGuitarService.getOfficialTabForId(parsed.id); + if (isErrorResponse(response)) { + return response.error; + } + + const data = JSON.parse(response.data) as {lyrics: string}; + const tabLyrics = data.lyrics; + + const tab: UltimateGuitarTab = { + ...parsed, + title: '', + tabLyrics, + }; + + return tab; + } + + return `unexpected resource type '${parsed.type}'`; +}; + +const insertStringAtIndex = (arr: T[], objToInsert: T, getId: (t: T) => string, newIndex: number): T[] => { + const objId = getId(objToInsert); + + const existingIndex = arr.findIndex(obj => getId(obj) === objId); + if (existingIndex === -1) { + throw new Error('The object/string must be in the array.'); + } + + const newArr = [...arr]; + newArr.splice(existingIndex, 1); + newArr.splice(newIndex, 0, objToInsert); + + return newArr; +}; diff --git a/src/features/src/modules/ultimate_guitar/ultimate_guitar_service.ts b/src/features/src/modules/ultimate_guitar/ultimate_guitar_service.ts new file mode 100644 index 0000000..ec5f128 --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/ultimate_guitar_service.ts @@ -0,0 +1,36 @@ +import {ErrorResponse, isErrorResponse} from 'springboard/types/response_types'; + +type UltimateGuitarServiceResponse = { + data: string; +} | ErrorResponse; + +export class UltimateGuitarService { + constructor() {} + + public getChordsTabForUrl = async (url: string): Promise => { + return this.fetchUrl(url); + }; + + public getOfficialTabForId = async (id: string): Promise => { + const url = `https://api-web.ultimate-guitar.com/v1/tab/pro/meta?id=${id}`; + return this.fetchUrl(url); + }; + + private fetchUrl = async (url: string): Promise => { + try { + const response = await fetch(url); + if (!response.ok) { + return { + error: `Failed to fetch chords tab. Status: ${response.statusText}`, + }; + } + + const data = await response.text(); + return {data}; + } catch (e) { + return { + error: `Failed to fetch chords tab: '${(e as Error).message}'`, + }; + } + }; +} diff --git a/src/features/src/modules/ultimate_guitar/ultimate_guitar_types.ts b/src/features/src/modules/ultimate_guitar/ultimate_guitar_types.ts new file mode 100644 index 0000000..4deaa4f --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/ultimate_guitar_types.ts @@ -0,0 +1,63 @@ +export type UltimateGuitarTab = ParsedUltimateGuitarUrl & { + title: string; + tabLyrics: string; +} + +export type UltimateGuitarSetlistSong = { + url: string; + transpose: number; +} + +export type UltimateGuitarSetlist = { + id: string; + name: string; + songs: UltimateGuitarSetlistSong[]; +} + +export type UltimateGuitarSetlistStatus = { + setlistId: string; + songIndex: number; +}; + +type SavedUltimateGuitarSong = { + name: string; + url: string; + transpose: number; +} + +const UltimateGuitarResourceTypes = { + chords: 'chords', + official: 'official', +} as const; + +type ParsedUltimateGuitarUrl = { + url: string; + id: string; + type: keyof typeof UltimateGuitarResourceTypes; +} + +// Examples: +// https://tabs.ultimate-guitar.com/tab/some-band/some-song-official-2758167 +// https://tabs.ultimate-guitar.com/tab/some-band/some-song-chords-2758168 +export const parseUltimateGuitarTabUrl = (url: string): ParsedUltimateGuitarUrl | string => { + if (!url.startsWith('https://tabs.ultimate-guitar.com/tab/')) { + return 'Please provide a valid ultimate-guitar.com url. error: 1'; + } + + const dashParts = url.split('-'); + const id = dashParts.pop(); + const resourceType = dashParts.pop(); + if (!id || !resourceType) { + return 'Please provide a valid ultimate-guitar.com url. error: 2'; + } + + if (!(resourceType in UltimateGuitarResourceTypes)) { + return `Invalid ultimate-guitar resource type '${resourceType}'`; + } + + return { + url, + id, + type: resourceType as keyof typeof UltimateGuitarResourceTypes, + }; +}; diff --git a/src/features/src/modules/ultimate_guitar/ultimate_guitar_utils.ts b/src/features/src/modules/ultimate_guitar/ultimate_guitar_utils.ts new file mode 100644 index 0000000..94b80e5 --- /dev/null +++ b/src/features/src/modules/ultimate_guitar/ultimate_guitar_utils.ts @@ -0,0 +1,123 @@ +import {Note} from 'tonal'; + +import {UltimateGuitarSetlist, UltimateGuitarSetlistSong, UltimateGuitarSetlistStatus, UltimateGuitarTab} from './ultimate_guitar_types'; + +export type ParsedTabPageData = { + title: string; + tabData: string; +} + +export const parseUltimateGuitarHTMLContent = (doc: Document): ParsedTabPageData | null => { + let title = ''; + const titleContainer = doc.querySelector('meta[property="og:title"]'); + if (titleContainer) { + title = (titleContainer as HTMLMetaElement).content; + title = title.replace(' (Chords)', ''); + title = title.replace(' (Official)', ''); + title = title.replace(' (Bass)', ''); + } + + const el = doc.querySelector('.js-store'); + const content = el?.getAttribute('data-content'); + + // const newDom = new JSDOM(); + const houseForEscapedHTML = doc.createElement('div'); + + houseForEscapedHTML.innerHTML = content || ''; + const unescapedJSONData = houseForEscapedHTML.textContent || ''; + + let tabData: string | undefined; + try { + const jsonData = JSON.parse(unescapedJSONData); + tabData = jsonData?.store?.page.data.tab_view.wiki_tab.content; + } catch (e) { + console.error(e); + } + + if (!tabData) { + throw new Error('failed to parse chord sheet data'); + // return null; + } + + return { + title, + tabData, + }; +}; + +type GetTabFromCurrentSetlistDataReturnValue = { + setlist?: UltimateGuitarSetlist; + tab?: UltimateGuitarTab; + song?: UltimateGuitarSetlistSong; +} + +export const getTabFromCurrentSetlistData = (setlistStatus: UltimateGuitarSetlistStatus | null, savedSetlists: UltimateGuitarSetlist[], savedTabs: UltimateGuitarTab[]): GetTabFromCurrentSetlistDataReturnValue => { + if (!setlistStatus || setlistStatus.songIndex === -1) { + return { + setlist: undefined, + tab: undefined, + song: undefined, + }; + } + + const setlist = savedSetlists.find(s => s.id === setlistStatus.setlistId); + if (!setlist) { + return { + setlist: undefined, + tab: undefined, + song: undefined, + }; + } + + const currentSong = setlist.songs[setlistStatus.songIndex]!; + const tab = savedTabs.find(t => t.url === currentSong.url); + return { + setlist, + tab: tab, + song: currentSong, + }; +}; + +export const prepareLyricsWithChords = (tabLyrics: string, options: {showChords: boolean, transpose: number}): string => { + return tabLyrics + .replace(/\[ch.*?\](.*?)\[\/ch\]/g, (match, captureAny) => { + if (!options.showChords) { + return ''; + } + + if (!options.transpose || !transposeIntervals[options.transpose]) { + return captureAny; + } + + const capture = captureAny as string; + + let suffix = ''; + let mainPart = capture; + if (!isNaN(parseInt(capture[capture.length - 1]!))) { + suffix = capture[capture.length - 1]!; + mainPart = capture.substring(0, capture.length - 1); + } + + const interval = transposeIntervals[(options.transpose + 12) % 12]!; + const transposed = Note.transpose(mainPart, interval); + + return transposed + suffix; + }) + .replace(/\[\/?tab\]/g, '') + .replace(/\[\/?syllable.*?\]/g, '') + .replace(/\r\n/g, '\n') + .replace(/\n{2,}/g, '\n\n'); +}; + +const transposeIntervals: Record = { + 1: '2m', + 2: '2M', + 3: '3m', + 4: '3M', + 5: '4P', + 7: '5P', + 8: '6m', + 9: '6M', + 10: '7m', + 11: '7M', +}; diff --git a/src/features/src/snacks/index.ts b/src/features/src/snacks/index.ts new file mode 100644 index 0000000..baeb318 --- /dev/null +++ b/src/features/src/snacks/index.ts @@ -0,0 +1,4 @@ +import './midi_thru_snack'; +import './root_mode_snack/root_mode_snack'; +import './random_note_snack'; +import './midi_thru_cc_snack'; diff --git a/src/features/src/snacks/midi_thru_cc_snack.ts b/src/features/src/snacks/midi_thru_cc_snack.ts new file mode 100644 index 0000000..aeede53 --- /dev/null +++ b/src/features/src/snacks/midi_thru_cc_snack.ts @@ -0,0 +1,39 @@ +import springboard from 'springboard'; + +springboard.registerModule('midi_thru_cc', {}, async (moduleAPI) => { + const macroModule = moduleAPI.deps.module.moduleRegistry.getModule('macro'); + + const [input, output] = await Promise.all([ + macroModule.createMacro(moduleAPI, 'MIDI Input', 'midi_control_change_input', {}), + // macroModule.createMacro(moduleAPI, 'MIDI Input', 'musical_keyboard_input', {}), + macroModule.createMacro(moduleAPI, 'MIDI Output', 'musical_keyboard_output', {}), + ]); + + input.subject.subscribe(evt => { + if (evt.event.value && evt.event.value % 2 === 1) { + return; + } + + const noteNumber = (evt.event.value || 0) / 2; + + output.send({ + ...evt.event, + type: 'noteon', + number: noteNumber, + velocity: 100, + }); + + setTimeout(() => { + output.send({ + ...evt.event, + type: 'noteoff', + number: noteNumber, + velocity: 0, + }); + }, 50); + }); + + // input.onEventSubject.subscribe(evt => { + // output.send(evt.event); + // }); +}); diff --git a/src/features/src/snacks/midi_thru_snack.tsx b/src/features/src/snacks/midi_thru_snack.tsx new file mode 100644 index 0000000..88d7c3e --- /dev/null +++ b/src/features/src/snacks/midi_thru_snack.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import springboard from 'springboard'; + +import '@jamtools/core/modules/macro_module/macro_module'; + +springboard.registerModule('midi_thru', {}, async (moduleAPI) => { + const macroModule = moduleAPI.getModule('macro'); + + const {myInput, myOutput} = await macroModule.createMacros(moduleAPI, { + myInput: {type: 'musical_keyboard_input', config: {}}, + myOutput: {type: 'musical_keyboard_output', config: {}}, + }); + + myInput.subject.subscribe(evt => { + myOutput.send(evt.event); + }); + + moduleAPI.registerRoute('', {}, () => { + return ( +
    + + +
    + ); + }); +}); diff --git a/src/features/src/snacks/random_note_snack.ts b/src/features/src/snacks/random_note_snack.ts new file mode 100644 index 0000000..d5cf6b7 --- /dev/null +++ b/src/features/src/snacks/random_note_snack.ts @@ -0,0 +1,77 @@ +import springboard from 'springboard'; + +declare module 'springboard/module_registry/module_registry' { + interface AllModules { + RandomNote: RandomNoteModuleReturnValue; + } +} + +type RandomNoteModuleReturnValue = { + togglePlaying: () => void; +} + +springboard.registerModule('RandomNote', {}, async (moduleAPI): Promise => { + const macroModule = moduleAPI.deps.module.moduleRegistry.getModule('macro'); + + const inputTrigger = await macroModule.createMacro(moduleAPI, 'Input trigger', 'musical_keyboard_input', {enableQwerty: false}); + const output = await macroModule.createMacro(moduleAPI, 'Random output', 'musical_keyboard_output', {}); + + let playing = false; + let currentInterval: NodeJS.Timeout | undefined; + + const playRandomNote = () => { + const randomNumber = Math.random(); + const scaled = Math.round(randomNumber * 48); + const inOctave = scaled + 24; + + const randomVelocity = Math.floor(Math.random() * 128); + + output.send({ + number: inOctave, + type: 'noteon', + velocity: randomVelocity, + }); + + setTimeout(() => { + output.send({ + number: inOctave, + type: 'noteoff', + velocity: 0, + }); + }, 100); + }; + + const startPlaying = () => { + currentInterval = setInterval(() => { + // if (Math.random() < 0.7) { + playRandomNote(); + // } + }, 50); + }; + + const stopPlaying = () => { + clearInterval(currentInterval); + }; + + const togglePlaying = () => { + if (playing) { + stopPlaying(); + } else { + startPlaying(); + } + + playing = !playing; + }; + + inputTrigger.subject.subscribe((evt) => { + if (evt.event.type !== 'noteon') { + return; + } + + togglePlaying(); + }); + + return { + togglePlaying, + }; +}); diff --git a/src/features/src/snacks/root_mode_snack/root_mode_component.tsx b/src/features/src/snacks/root_mode_snack/root_mode_component.tsx new file mode 100644 index 0000000..568d4d1 --- /dev/null +++ b/src/features/src/snacks/root_mode_snack/root_mode_component.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import {MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS} from '@jamtools/core/constants/midi_number_to_note_name_mappings'; +import {ScaleDegreeInfo} from './root_mode_types'; + +type Props = { + chord: ScaleDegreeInfo | null; + scale: number; + onClick: () => void; +} + +export const RootModeComponent = (props: Props) => { + const scaleRootNoteName = MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS[props.scale as keyof typeof MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS]; + + return ( +
    +
    + Scale: {scaleRootNoteName} Major +
    + + + {props.chord && ( +
    + {props.chord.noteName} {props.chord.quality} +
    + )} +
    + ); +}; diff --git a/src/features/src/snacks/root_mode_snack/root_mode_snack.tsx b/src/features/src/snacks/root_mode_snack/root_mode_snack.tsx new file mode 100644 index 0000000..0508893 --- /dev/null +++ b/src/features/src/snacks/root_mode_snack/root_mode_snack.tsx @@ -0,0 +1,119 @@ +import React from 'react'; + +import {ScaleDegreeInfo, cycle, getScaleDegreeFromScaleAndNote} from './root_mode_types'; + +import {RootModeComponent} from './root_mode_component'; +import springboard from 'springboard'; + +import '@jamtools/core/modules/macro_module/macro_module'; + +type ChordState = { + chord: ScaleDegreeInfo | null; + note: number | null; + scale: number; +} + +springboard.registerModule('Main', {}, async (moduleAPI) => { + const states = await moduleAPI.createStates({ + chords: {chord: null, note: null, scale: 0} as ChordState, + }); + + const rootModeState = states.chords; + + const setScale = (newScale: number) => { + rootModeState.setState({ + chord: null, + note: null, + scale: newScale, + }); + }; + + moduleAPI.registerRoute('', {}, () => { + const state = rootModeState.useState(); + + const onClick = () => { + setScale(cycle(state.scale + 1)); + }; + + return ( + + ); + }); + + const macroModule = moduleAPI.getModule('macro'); + + const {input, output} = await macroModule.createMacros(moduleAPI, { + input: {type: 'musical_keyboard_input', config: {}}, + output: {type: 'musical_keyboard_output', config: {}}, + }); + + input.subject.subscribe(evt => { + const midiNumber = evt.event.number; + const scale = rootModeState.getState().scale; + + const scaleDegreeInfo = getScaleDegreeFromScaleAndNote(scale, midiNumber); + if (!scaleDegreeInfo) { + return; + } + + const chordNotes = getChordFromRootNote(scale, midiNumber); + if (!chordNotes.length) { + return; + } + + for (const noteNumber of chordNotes) { + const midiNumberToPlay = noteNumber; + output.send({...evt.event, number: midiNumberToPlay}); + } + + if (evt.event.type === 'noteon') { + rootModeState.setState({ + chord: scaleDegreeInfo, + note: midiNumber, + scale, + }); + } else if (evt.event.type === 'noteoff') { + if (rootModeState.getState().note !== midiNumber) { + return; + } + + rootModeState.setState({ + chord: null, + note: null, + scale, + }); + } + }); +}); + +const getChordFromRootNote = (scale: number, rootNote: number): number[] => { + const scaleDegreeInfo = getScaleDegreeFromScaleAndNote(scale, rootNote); + + if (!scaleDegreeInfo) { + return []; + } + + // This function could be made more interesting by performing inversions to keep notes in range + if (scaleDegreeInfo.quality === 'major') { + return [ + rootNote, + rootNote + 4, + rootNote + 7, + rootNote + 12, + ]; + } + + if (scaleDegreeInfo.quality === 'minor') { + return [ + rootNote, + rootNote + 3, + rootNote + 7, + rootNote + 12, + ]; + } + + return []; +}; diff --git a/src/features/src/snacks/root_mode_snack/root_mode_types.ts b/src/features/src/snacks/root_mode_snack/root_mode_types.ts new file mode 100644 index 0000000..fc290ae --- /dev/null +++ b/src/features/src/snacks/root_mode_snack/root_mode_types.ts @@ -0,0 +1,35 @@ +import {MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS} from '@jamtools/core/constants/midi_number_to_note_name_mappings'; + +export const cycle = (midiNumber: number) => midiNumber % 12; + +export const ionianScaleDegreeQualities = { + 0: 'major', + 2: 'minor', + 4: 'minor', + 5: 'major', + 7: 'major', + 9: 'minor', +} as const; + +export type ScaleDegreeInfo = { + noteName: string; + scaleDegree: number; // assumes Ionian mode and integer notation + quality: 'major' | 'minor'; +}; + +export const getScaleDegreeFromScaleAndNote = (scale: number, note: number): ScaleDegreeInfo | null => { + const scaleDegreeIndex = cycle(note - scale); + const scaleDegreeQuality = ionianScaleDegreeQualities[scaleDegreeIndex as keyof typeof ionianScaleDegreeQualities]; + + if (!scaleDegreeQuality) { + return null; + } + + const rootNote = cycle(note); + + return { + noteName: MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS[rootNote as keyof typeof MIDI_NUMBER_TO_NOTE_NAME_MAPPINGS], + scaleDegree: scaleDegreeIndex, + quality: scaleDegreeQuality, + }; +}; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..6076d7e --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import springboard from 'springboard'; + +import '@jamtools/core/modules'; +import './features/src/modules'; +import './modules/ui_main'; + +import '@jamtools/core/modules/macro_module/macro_module'; + +// Demo module to test the UI shell +springboard.registerModule('demo', {}, async (moduleAPI) => { + moduleAPI.registerRoute('', {}, () => { + return

    Demo Module Home

    Welcome to the demo module!

    ; + }); + + moduleAPI.registerRoute('settings', {}, () => { + return

    Settings

    Configure your settings here.

    ; + }); + + moduleAPI.registerRoute('about', {}, () => { + return

    About

    This is the about page.

    ; + }); + + return {}; +}); diff --git a/src/modules/ui_main.tsx b/src/modules/ui_main.tsx new file mode 100644 index 0000000..e9a49a8 --- /dev/null +++ b/src/modules/ui_main.tsx @@ -0,0 +1,8 @@ +import springboard from 'springboard'; +import {ApplicationShell} from '../components/application_shell'; + +springboard.registerModule('UIMain', {}, async (moduleAPI) => { + moduleAPI.registerApplicationShell(ApplicationShell); + + return {}; +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9627f1f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*", "vite.config.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..016563d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,42 @@ +import { defineConfig } from 'vite'; +import { springboard } from 'springboard/vite-plugin'; +import path from 'node:path'; + +const platformVariant = process.env.SPRINGBOARD_PLATFORM || ''; + +let platforms: ('browser' | 'node')[] = ['browser', 'node']; + +if (platformVariant === 'node') { + platforms = ['node']; +} else if (platformVariant === 'browser') { + platforms = ['browser']; +} + +export default defineConfig({ + plugins: [ + springboard({ + entry: './src/index.tsx', + platforms, + documentMeta: { + title: 'My App', + description: 'My really cool app', + }, + nodeServerPort: 3030, + }), + ], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + define: { + 'process.env.DEBUG_LOG_PERFORMANCE': '""', + }, + server: { + port: 3000, + host: true, + }, + ssr: { + noExternal: ['@jamtools/core'], + }, +});