diff --git a/app/layout.tsx b/app/layout.tsx index df0051d..e86a252 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Lofi Generator", - description: "Web-based lofi hip hop beat generator - beats to relax/study to", + title: "Beat Generator", + description: "Create custom beats across Hip Hop, Classical, Trap, and Pop genres", }; export default function RootLayout({ diff --git a/bun.lock b/bun.lock index fb817bd..8a8e7f7 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "lofi-generator", "dependencies": { "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-toggle": "^1.1.10", @@ -96,6 +97,14 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -198,6 +207,8 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -206,26 +217,50 @@ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -342,6 +377,8 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], @@ -430,6 +467,8 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -528,6 +567,8 @@ "get-intrinsic": ["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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], @@ -766,6 +807,12 @@ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], @@ -884,6 +931,10 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -910,14 +961,30 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slider/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -954,10 +1021,22 @@ "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slider/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/components/lofi-generator/LayerBox.tsx b/components/lofi-generator/LayerBox.tsx new file mode 100644 index 0000000..69e636e --- /dev/null +++ b/components/lofi-generator/LayerBox.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { Toggle } from '@/components/ui/toggle'; +import { Slider } from '@/components/ui/slider'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Volume2, VolumeX } from 'lucide-react'; +import { ReactNode } from 'react'; + +interface InstrumentOption { + value: string; + label: string; +} + +interface LayerBoxProps { + title: string; + icon: ReactNode; + volume: number; + muted: boolean; + instrument: string; + instrumentOptions: InstrumentOption[]; + onVolumeChange: (volume: number) => void; + onToggleMute: () => void; + onInstrumentChange: (instrument: string) => void; + accentColor: 'orange' | 'pink' | 'purple' | 'blue' | 'green' | 'yellow'; +} + +const accentStyles = { + orange: { + border: 'border-lofi-orange/30 hover:border-lofi-orange/50', + bg: 'bg-lofi-orange/5', + icon: 'text-lofi-orange', + slider: '[&_[data-slot=slider-range]]:bg-lofi-orange [&_[data-slot=slider-thumb]]:border-lofi-orange', + }, + pink: { + border: 'border-lofi-pink/30 hover:border-lofi-pink/50', + bg: 'bg-lofi-pink/5', + icon: 'text-lofi-pink', + slider: '[&_[data-slot=slider-range]]:bg-lofi-pink [&_[data-slot=slider-thumb]]:border-lofi-pink', + }, + purple: { + border: 'border-lofi-purple/30 hover:border-lofi-purple/50', + bg: 'bg-lofi-purple/5', + icon: 'text-lofi-purple', + slider: '[&_[data-slot=slider-range]]:bg-lofi-purple [&_[data-slot=slider-thumb]]:border-lofi-purple', + }, + blue: { + border: 'border-blue-400/30 hover:border-blue-400/50', + bg: 'bg-blue-400/5', + icon: 'text-blue-400', + slider: '[&_[data-slot=slider-range]]:bg-blue-400 [&_[data-slot=slider-thumb]]:border-blue-400', + }, + green: { + border: 'border-emerald-400/30 hover:border-emerald-400/50', + bg: 'bg-emerald-400/5', + icon: 'text-emerald-400', + slider: '[&_[data-slot=slider-range]]:bg-emerald-400 [&_[data-slot=slider-thumb]]:border-emerald-400', + }, + yellow: { + border: 'border-yellow-400/30 hover:border-yellow-400/50', + bg: 'bg-yellow-400/5', + icon: 'text-yellow-400', + slider: '[&_[data-slot=slider-range]]:bg-yellow-400 [&_[data-slot=slider-thumb]]:border-yellow-400', + }, +}; + +export function LayerBox({ + title, + icon, + volume, + muted, + instrument, + instrumentOptions, + onVolumeChange, + onToggleMute, + onInstrumentChange, + accentColor, +}: LayerBoxProps) { + const styles = accentStyles[accentColor]; + + return ( +
+
+
+ {icon} + {title} +
+
+ + + {muted ? ( + + ) : ( + + )} + +
+
+ +
+ onVolumeChange(v)} + min={0} + max={1} + step={0.01} + className={`flex-1 ${styles.slider}`} + disabled={muted} + /> + + {Math.round(volume * 100)}% + +
+
+ ); +} diff --git a/components/lofi-generator/LayerMixer.tsx b/components/lofi-generator/LayerMixer.tsx deleted file mode 100644 index 3e9edbf..0000000 --- a/components/lofi-generator/LayerMixer.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; - -import { Toggle } from '@/components/ui/toggle'; -import { VolumeControl } from './VolumeControl'; -import { Volume2, VolumeX, Drum, Music, Cloud } from 'lucide-react'; -import { LayerName } from '@/types/audio'; - -interface LayerMixerProps { - volumes: { - drums: number; - chords: number; - ambient: number; - }; - muted: { - drums: boolean; - chords: boolean; - ambient: boolean; - }; - onVolumeChange: (layer: LayerName, volume: number) => void; - onToggleMute: (layer: LayerName) => void; -} - -const layers: { name: LayerName; label: string; icon: React.ReactNode }[] = [ - { name: 'drums', label: 'Drums', icon: }, - { name: 'chords', label: 'Chords', icon: }, - { name: 'ambient', label: 'Ambient', icon: }, -]; - -export function LayerMixer({ - volumes, - muted, - onVolumeChange, - onToggleMute, -}: LayerMixerProps) { - return ( -
-

- Layers -

-
- {layers.map(({ name, label, icon }) => ( -
- onToggleMute(name)} - size="sm" - className="shrink-0" - aria-label={`Toggle ${label}`} - > - {muted[name] ? ( - - ) : ( - - )} - -
- {icon} - {label} -
- onVolumeChange(name, v)} - className="flex-1" - /> -
- ))} -
-
- ); -} diff --git a/components/lofi-generator/LofiGenerator.tsx b/components/lofi-generator/LofiGenerator.tsx index cc71a92..1bd15b8 100644 --- a/components/lofi-generator/LofiGenerator.tsx +++ b/components/lofi-generator/LofiGenerator.tsx @@ -1,13 +1,31 @@ 'use client'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { TransportControls } from './TransportControls'; -import { VolumeControl } from './VolumeControl'; -import { LayerMixer } from './LayerMixer'; -import { Visualizer } from './Visualizer'; -import { useAudioEngine } from '@/hooks/useAudioEngine'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { Label } from '@/components/ui/label'; +import { LayerBox } from './LayerBox'; +import { Visualizer } from './Visualizer'; +import { useAudioEngine } from '@/hooks/useAudioEngine'; +import { + Play, + Pause, + Shuffle, + Drum, + Music, + Cloud, + Guitar, + Waves, + Piano, +} from 'lucide-react'; +import { Genre, GENRE_CONFIG, INSTRUMENT_OPTIONS } from '@/types/audio'; export function LofiGenerator() { const { @@ -18,50 +36,126 @@ export function LofiGenerator() { setMasterVolume, setLayerVolume, toggleMute, + setGenre, + setDuration, + setInstrument, setBpm, setSwing, } = useAudioEngine(); - return ( -
- - - - lofi generator - -

- beats to relax/study to -

-
+ const genres: { value: Genre; label: string }[] = [ + { value: 'hiphop', label: 'Hip Hop' }, + { value: 'classical', label: 'Classical' }, + { value: 'trap', label: 'Trap' }, + { value: 'pop', label: 'Pop' }, + ]; + + return ( +
+ {/* Modern Header */} +
+
+
+ +
+

+ Beat Generator +

+
+

+ Create custom beats across multiple genres +

+
+ + + + {/* Top Controls: Duration & Genre */} +
+
+ +
+ setDuration(v)} + min={1} + max={10} + step={1} + className="flex-1" + /> + + {state.duration} min + +
+
+
+ + +
+
+ + {/* Transport Controls */} +
+ + + +
- {/* Visualizer */} - {/* Transport Controls */} -
- -
- - {/* Master Volume */} -
- -
- - {/* BPM and Swing Controls */} -
+ {/* Master Controls */} +
- + + + {Math.round(state.volumes.master * 100)}% + +
+ setMasterVolume(v)} + min={0} + max={1} + step={0.01} + /> +
+
+
+ {state.bpm} @@ -70,13 +164,13 @@ export function LofiGenerator() { value={[state.bpm]} onValueChange={([v]) => setBpm(v)} min={60} - max={100} + max={180} step={1} />
- + {Math.round(state.swing * 100)}% @@ -91,17 +185,96 @@ export function LofiGenerator() {
- {/* Layer Mixer */} - + {/* Instrument Layers */} +
+

+ Instruments +

+ +
+ } + volume={state.volumes.drums} + muted={state.muted.drums} + instrument={state.instruments.drums} + instrumentOptions={INSTRUMENT_OPTIONS.drums} + onVolumeChange={(v) => setLayerVolume('drums', v)} + onToggleMute={() => toggleMute('drums')} + onInstrumentChange={(v) => setInstrument('drums', v)} + accentColor="orange" + /> + + } + volume={state.volumes.bass} + muted={state.muted.bass} + instrument={state.instruments.bass} + instrumentOptions={INSTRUMENT_OPTIONS.bass} + onVolumeChange={(v) => setLayerVolume('bass', v)} + onToggleMute={() => toggleMute('bass')} + onInstrumentChange={(v) => setInstrument('bass', v)} + accentColor="blue" + /> + + } + volume={state.volumes.piano} + muted={state.muted.piano} + instrument={state.instruments.piano} + instrumentOptions={INSTRUMENT_OPTIONS.piano} + onVolumeChange={(v) => setLayerVolume('piano', v)} + onToggleMute={() => toggleMute('piano')} + onInstrumentChange={(v) => setInstrument('piano', v)} + accentColor="green" + /> + + } + volume={state.volumes.brass} + muted={state.muted.brass} + instrument={state.instruments.brass} + instrumentOptions={INSTRUMENT_OPTIONS.brass} + onVolumeChange={(v) => setLayerVolume('brass', v)} + onToggleMute={() => toggleMute('brass')} + onInstrumentChange={(v) => setInstrument('brass', v)} + accentColor="yellow" + /> + + } + volume={state.volumes.chords} + muted={state.muted.chords} + instrument={state.instruments.chords} + instrumentOptions={INSTRUMENT_OPTIONS.chords} + onVolumeChange={(v) => setLayerVolume('chords', v)} + onToggleMute={() => toggleMute('chords')} + onInstrumentChange={(v) => setInstrument('chords', v)} + accentColor="pink" + /> + + } + volume={state.volumes.ambient} + muted={state.muted.ambient} + instrument={state.instruments.ambient} + instrumentOptions={INSTRUMENT_OPTIONS.ambient} + onVolumeChange={(v) => setLayerVolume('ambient', v)} + onToggleMute={() => toggleMute('ambient')} + onInstrumentChange={(v) => setInstrument('ambient', v)} + accentColor="purple" + /> +
+
{/* Footer */} -

- Click play to start the audio engine +

+ Click play to initialize audio engine • Genre: {GENRE_CONFIG[state.genre].name}

diff --git a/components/lofi-generator/TransportControls.tsx b/components/lofi-generator/TransportControls.tsx deleted file mode 100644 index 908e319..0000000 --- a/components/lofi-generator/TransportControls.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { Play, Pause, Shuffle } from 'lucide-react'; - -interface TransportControlsProps { - isPlaying: boolean; - isInitialized: boolean; - onTogglePlayback: () => void; - onGenerateNewBeat: () => void; -} - -export function TransportControls({ - isPlaying, - isInitialized, - onTogglePlayback, - onGenerateNewBeat, -}: TransportControlsProps) { - return ( -
- - - -
- ); -} diff --git a/components/lofi-generator/Visualizer.tsx b/components/lofi-generator/Visualizer.tsx index 877829d..4eb34e3 100644 --- a/components/lofi-generator/Visualizer.tsx +++ b/components/lofi-generator/Visualizer.tsx @@ -7,25 +7,31 @@ interface VisualizerProps { export function Visualizer({ currentStep, isPlaying }: VisualizerProps) { return ( -
+
{Array.from({ length: 16 }, (_, i) => { const isActive = isPlaying && currentStep === i; const isBeat = i % 4 === 0; + const isOffbeat = i % 2 === 1; + + // Vary heights for visual interest + const baseHeight = isBeat ? 'h-12' : isOffbeat ? 'h-6' : 'h-8'; + const activeHeight = 'h-14'; return (
); diff --git a/components/lofi-generator/VolumeControl.tsx b/components/lofi-generator/VolumeControl.tsx deleted file mode 100644 index 746b561..0000000 --- a/components/lofi-generator/VolumeControl.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; - -import { Slider } from '@/components/ui/slider'; -import { Label } from '@/components/ui/label'; - -interface VolumeControlProps { - label: string; - value: number; - onChange: (value: number) => void; - className?: string; -} - -export function VolumeControl({ - label, - value, - onChange, - className = '', -}: VolumeControlProps) { - return ( -
-
- - - {Math.round(value * 100)}% - -
- onChange(v)} - min={0} - max={1} - step={0.01} - className="w-full" - /> -
- ); -} diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..88302a8 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,190 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/hooks/useAudioEngine.ts b/hooks/useAudioEngine.ts index 557f210..3c34cb2 100644 --- a/hooks/useAudioEngine.ts +++ b/hooks/useAudioEngine.ts @@ -1,22 +1,38 @@ 'use client'; import { useState, useEffect, useCallback, useRef } from 'react'; -import { EngineState, LayerName } from '@/types/audio'; +import { EngineState, LayerName, Genre } from '@/types/audio'; const defaultState: EngineState = { isPlaying: false, isInitialized: false, - bpm: 78, - swing: 0.12, + bpm: 90, + swing: 0.15, currentStep: 0, + genre: 'hiphop', + duration: 3, + instruments: { + drums: 'acoustic', + bass: 'synth', + brass: 'trumpet', + piano: 'grand', + chords: 'pad', + ambient: 'rain', + }, volumes: { master: 0.8, drums: 0.8, + bass: 0.6, + brass: 0.4, + piano: 0.5, chords: 0.6, ambient: 0.4, }, muted: { drums: false, + bass: false, + brass: true, + piano: true, chords: false, ambient: false, }, @@ -28,7 +44,6 @@ export function useAudioEngine() { const engineRef = useRef(null); const isInitializingRef = useRef(false); - // Dynamically import the audio engine (client-side only) const getEngine = useCallback(async () => { if (typeof window === 'undefined') return null; @@ -39,7 +54,6 @@ export function useAudioEngine() { return engineRef.current; }, []); - // Initialize engine and set up callbacks const initialize = useCallback(async () => { if (isInitializingRef.current) return; isInitializingRef.current = true; @@ -64,7 +78,6 @@ export function useAudioEngine() { } }, [getEngine]); - // Play/pause toggle const togglePlayback = useCallback(async () => { const engine = await getEngine(); if (!engine) return; @@ -81,7 +94,6 @@ export function useAudioEngine() { } }, [getEngine, state.isInitialized, initialize]); - // Stop playback const stop = useCallback(async () => { const engine = await getEngine(); if (!engine) return; @@ -89,7 +101,6 @@ export function useAudioEngine() { setCurrentStep(0); }, [getEngine]); - // Generate new beat const generateNewBeat = useCallback(async () => { const engine = await getEngine(); if (!engine) return; @@ -101,46 +112,67 @@ export function useAudioEngine() { engine.generateNewBeat(); }, [getEngine, state.isInitialized, initialize]); - // Set BPM + const setGenre = useCallback(async (genre: Genre) => { + const engine = await getEngine(); + if (!engine) return; + + if (!state.isInitialized) { + await initialize(); + } + + engine.setGenre(genre); + }, [getEngine, state.isInitialized, initialize]); + + const setDuration = useCallback(async (minutes: number) => { + const engine = await getEngine(); + if (!engine) return; + engine.setDuration(minutes); + }, [getEngine]); + + const setInstrument = useCallback(async (layer: LayerName, instrument: string) => { + const engine = await getEngine(); + if (!engine) return; + + if (!state.isInitialized) { + await initialize(); + } + + engine.setInstrument(layer, instrument); + }, [getEngine, state.isInitialized, initialize]); + const setBpm = useCallback(async (bpm: number) => { const engine = await getEngine(); if (!engine) return; engine.setBpm(bpm); }, [getEngine]); - // Set swing const setSwing = useCallback(async (swing: number) => { const engine = await getEngine(); if (!engine) return; engine.setSwing(swing); }, [getEngine]); - // Set master volume const setMasterVolume = useCallback(async (volume: number) => { const engine = await getEngine(); if (!engine) return; engine.setMasterVolume(volume); }, [getEngine]); - // Set layer volume const setLayerVolume = useCallback(async (layer: LayerName, volume: number) => { const engine = await getEngine(); if (!engine) return; engine.setLayerVolume(layer, volume); }, [getEngine]); - // Toggle layer mute const toggleMute = useCallback(async (layer: LayerName) => { const engine = await getEngine(); if (!engine) return; engine.toggleMute(layer); }, [getEngine]); - // Cleanup on unmount useEffect(() => { return () => { - // Don't dispose on unmount to allow seamless navigation - // The engine is a singleton that persists + // Don't dispose on unmount }; }, []); @@ -151,6 +183,9 @@ export function useAudioEngine() { togglePlayback, stop, generateNewBeat, + setGenre, + setDuration, + setInstrument, setBpm, setSwing, setMasterVolume, diff --git a/lib/audio/ambientLayer.ts b/lib/audio/ambientLayer.ts index 38bd565..493691f 100644 --- a/lib/audio/ambientLayer.ts +++ b/lib/audio/ambientLayer.ts @@ -1,68 +1,119 @@ import * as Tone from 'tone'; +import { AmbientType } from '@/types/audio'; export class AmbientLayer { - private rainNoise: Tone.Noise; - private vinylNoise: Tone.Noise; - private rainFilter: Tone.Filter; - private vinylFilter: Tone.Filter; - private rainGain: Tone.Gain; - private vinylGain: Tone.Gain; + private noise1: Tone.Noise | null = null; + private noise2: Tone.Noise | null = null; + private filter1: Tone.Filter | null = null; + private filter2: Tone.Filter | null = null; + private gain1: Tone.Gain | null = null; + private gain2: Tone.Gain | null = null; private output: Tone.Gain; - private lfo: Tone.LFO; + private lfo: Tone.LFO | null = null; + private currentType: AmbientType = 'rain'; + private isPlaying = false; constructor(destination: Tone.InputNode) { this.output = new Tone.Gain(0.4); - - // Rain sound - filtered pink noise - this.rainNoise = new Tone.Noise('pink'); - this.rainFilter = new Tone.Filter({ - frequency: 3000, - type: 'lowpass', - rolloff: -24, - }); - this.rainGain = new Tone.Gain(0.15); - - // Vinyl crackle - filtered brown noise with modulation - this.vinylNoise = new Tone.Noise('brown'); - this.vinylFilter = new Tone.Filter({ - frequency: 1500, - type: 'bandpass', - Q: 2, - }); - this.vinylGain = new Tone.Gain(0.1); - - // LFO for subtle rain intensity variation - this.lfo = new Tone.LFO({ - frequency: 0.1, - min: 0.1, - max: 0.2, - }); - this.lfo.connect(this.rainGain.gain); - - // Chain rain: noise -> filter -> gain -> output - this.rainNoise.connect(this.rainFilter); - this.rainFilter.connect(this.rainGain); - this.rainGain.connect(this.output); - - // Chain vinyl: noise -> filter -> gain -> output - this.vinylNoise.connect(this.vinylFilter); - this.vinylFilter.connect(this.vinylGain); - this.vinylGain.connect(this.output); - - // Output to destination this.output.connect(destination); + this.createAmbient('rain'); + } + + private createAmbient(type: AmbientType): void { + // Dispose existing + this.noise1?.dispose(); + this.noise2?.dispose(); + this.filter1?.dispose(); + this.filter2?.dispose(); + this.gain1?.dispose(); + this.gain2?.dispose(); + this.lfo?.dispose(); + + const configs: Record = { + rain: { + noise1: { type: 'pink', filter: 3000, filterType: 'lowpass', gain: 0.15 }, + noise2: { type: 'brown', filter: 1500, filterType: 'bandpass', gain: 0.1 }, + lfoFreq: 0.1, + }, + vinyl: { + noise1: { type: 'brown', filter: 2000, filterType: 'bandpass', gain: 0.12 }, + noise2: { type: 'white', filter: 800, filterType: 'lowpass', gain: 0.05 }, + lfoFreq: 0.05, + }, + nature: { + noise1: { type: 'pink', filter: 4000, filterType: 'lowpass', gain: 0.1 }, + noise2: { type: 'brown', filter: 500, filterType: 'lowpass', gain: 0.08 }, + lfoFreq: 0.08, + }, + space: { + noise1: { type: 'pink', filter: 1000, filterType: 'lowpass', gain: 0.12 }, + noise2: { type: 'brown', filter: 300, filterType: 'lowpass', gain: 0.1 }, + lfoFreq: 0.02, + }, + }; + + const config = configs[type]; + + // Primary noise layer + this.noise1 = new Tone.Noise(config.noise1.type); + this.filter1 = new Tone.Filter({ + frequency: config.noise1.filter, + type: config.noise1.filterType, + }); + this.gain1 = new Tone.Gain(config.noise1.gain); + this.noise1.connect(this.filter1); + this.filter1.connect(this.gain1); + this.gain1.connect(this.output); + + // Secondary noise layer + this.noise2 = new Tone.Noise(config.noise2.type); + this.filter2 = new Tone.Filter({ + frequency: config.noise2.filter, + type: config.noise2.filterType, + }); + this.gain2 = new Tone.Gain(config.noise2.gain); + this.noise2.connect(this.filter2); + this.filter2.connect(this.gain2); + this.gain2.connect(this.output); + + // LFO for subtle variation + this.lfo = new Tone.LFO({ + frequency: config.lfoFreq, + min: config.noise1.gain * 0.7, + max: config.noise1.gain * 1.2, + }); + this.lfo.connect(this.gain1.gain); + + this.currentType = type; + + // Restart if was playing + if (this.isPlaying) { + this.noise1.start(); + this.noise2.start(); + this.lfo.start(); + } + } + + setInstrument(type: AmbientType): void { + this.createAmbient(type); } start(): void { - this.rainNoise.start(); - this.vinylNoise.start(); - this.lfo.start(); + this.noise1?.start(); + this.noise2?.start(); + this.lfo?.start(); + this.isPlaying = true; } stop(): void { - this.rainNoise.stop(); - this.vinylNoise.stop(); - this.lfo.stop(); + this.noise1?.stop(); + this.noise2?.stop(); + this.lfo?.stop(); + this.isPlaying = false; } setVolume(volume: number): void { @@ -73,25 +124,15 @@ export class AmbientLayer { this.output.gain.rampTo(muted ? 0 : 0.4, 0.1); } - setRainIntensity(intensity: number): void { - this.rainGain.gain.rampTo(intensity * 0.2, 0.5); - } - - setVinylIntensity(intensity: number): void { - this.vinylGain.gain.rampTo(intensity * 0.15, 0.5); - } - dispose(): void { - this.rainNoise.stop(); - this.vinylNoise.stop(); - this.lfo.stop(); - this.rainNoise.dispose(); - this.vinylNoise.dispose(); - this.rainFilter.dispose(); - this.vinylFilter.dispose(); - this.rainGain.dispose(); - this.vinylGain.dispose(); - this.lfo.dispose(); + this.stop(); + this.noise1?.dispose(); + this.noise2?.dispose(); + this.filter1?.dispose(); + this.filter2?.dispose(); + this.gain1?.dispose(); + this.gain2?.dispose(); + this.lfo?.dispose(); this.output.dispose(); } } diff --git a/lib/audio/audioEngine.ts b/lib/audio/audioEngine.ts index 06c5770..c94f2de 100644 --- a/lib/audio/audioEngine.ts +++ b/lib/audio/audioEngine.ts @@ -2,7 +2,22 @@ import * as Tone from 'tone'; import { DrumMachine } from './drumMachine'; import { ChordEngine } from './chordEngine'; import { AmbientLayer } from './ambientLayer'; -import { EngineState, AudioEngineCallbacks, LayerName } from '@/types/audio'; +import { BassEngine } from './bassEngine'; +import { BrassEngine } from './brassEngine'; +import { PianoEngine } from './pianoEngine'; +import { + EngineState, + AudioEngineCallbacks, + LayerName, + Genre, + GENRE_CONFIG, + DrumKit, + BassType, + BrassType, + PianoType, + ChordType, + AmbientType, +} from '@/types/audio'; class AudioEngine { private static instance: AudioEngine | null = null; @@ -10,6 +25,9 @@ class AudioEngine { private drumMachine: DrumMachine | null = null; private chordEngine: ChordEngine | null = null; private ambientLayer: AmbientLayer | null = null; + private bassEngine: BassEngine | null = null; + private brassEngine: BrassEngine | null = null; + private pianoEngine: PianoEngine | null = null; private masterGain: Tone.Gain | null = null; private masterCompressor: Tone.Compressor | null = null; @@ -21,17 +39,33 @@ class AudioEngine { private state: EngineState = { isPlaying: false, isInitialized: false, - bpm: 78, - swing: 0.12, + bpm: 90, + swing: 0.15, currentStep: 0, + genre: 'hiphop', + duration: 3, + instruments: { + drums: 'acoustic', + bass: 'synth', + brass: 'trumpet', + piano: 'grand', + chords: 'pad', + ambient: 'rain', + }, volumes: { master: 0.8, drums: 0.8, + bass: 0.6, + brass: 0.4, + piano: 0.5, chords: 0.6, ambient: 0.4, }, muted: { drums: false, + bass: false, + brass: true, + piano: true, chords: false, ambient: false, }, @@ -49,12 +83,11 @@ class AudioEngine { async initialize(): Promise { if (this.state.isInitialized) return; - // Start Tone.js audio context (requires user gesture) await Tone.start(); - // Set up transport - Tone.getTransport().bpm.value = this.state.bpm; - Tone.getTransport().swing = this.state.swing; + const genreConfig = GENRE_CONFIG[this.state.genre]; + Tone.getTransport().bpm.value = genreConfig.bpm; + Tone.getTransport().swing = genreConfig.swing; Tone.getTransport().swingSubdivision = '16n'; // Master chain @@ -71,16 +104,29 @@ class AudioEngine { wet: 0.15, }); - // Chain: gain -> reverb -> compressor -> limiter -> destination this.masterGain.connect(this.masterReverb); this.masterReverb.connect(this.masterCompressor); this.masterCompressor.connect(this.masterLimiter); this.masterLimiter.toDestination(); - // Initialize layers + // Initialize all layers this.drumMachine = new DrumMachine(this.masterGain); this.chordEngine = new ChordEngine(this.masterGain); this.ambientLayer = new AmbientLayer(this.masterGain); + this.bassEngine = new BassEngine(this.masterGain); + this.brassEngine = new BrassEngine(this.masterGain); + this.pianoEngine = new PianoEngine(this.masterGain); + + // Set initial genre for all engines + this.drumMachine.setGenre(this.state.genre); + this.chordEngine.setGenre(this.state.genre); + this.bassEngine.setGenre(this.state.genre); + this.brassEngine.setGenre(this.state.genre); + this.pianoEngine.setGenre(this.state.genre); + + // Apply initial mute states + this.brassEngine.mute(this.state.muted.brass); + this.pianoEngine.mute(this.state.muted.piano); // Create sequences this.drumMachine.createSequence((step) => { @@ -88,7 +134,12 @@ class AudioEngine { this.callbacks.onStepChange?.(step); }); this.chordEngine.createSequence(); + this.bassEngine.createSequence(); + this.brassEngine.createSequence(); + this.pianoEngine.createSequence(); + this.state.bpm = genreConfig.bpm; + this.state.swing = genreConfig.swing; this.state.isInitialized = true; this.notifyStateChange(); } @@ -130,11 +181,69 @@ class AudioEngine { generateNewBeat(): void { this.drumMachine?.randomize(); this.chordEngine?.randomize(); + this.bassEngine?.randomize(); + this.brassEngine?.randomize(); + this.pianoEngine?.randomize(); + this.notifyStateChange(); + } + + setGenre(genre: Genre): void { + this.state.genre = genre; + const genreConfig = GENRE_CONFIG[genre]; + + // Update BPM and swing for genre + this.state.bpm = genreConfig.bpm; + this.state.swing = genreConfig.swing; + Tone.getTransport().bpm.value = genreConfig.bpm; + Tone.getTransport().swing = genreConfig.swing; + + // Update all engines with new genre + this.drumMachine?.setGenre(genre); + this.chordEngine?.setGenre(genre); + this.bassEngine?.setGenre(genre); + this.brassEngine?.setGenre(genre); + this.pianoEngine?.setGenre(genre); + + this.notifyStateChange(); + } + + setDuration(minutes: number): void { + this.state.duration = Math.max(1, Math.min(10, minutes)); + this.notifyStateChange(); + } + + setInstrument(layer: LayerName, instrument: string): void { + switch (layer) { + case 'drums': + this.state.instruments.drums = instrument as DrumKit; + this.drumMachine?.setInstrument(instrument as DrumKit); + break; + case 'bass': + this.state.instruments.bass = instrument as BassType; + this.bassEngine?.setInstrument(instrument as BassType); + break; + case 'brass': + this.state.instruments.brass = instrument as BrassType; + this.brassEngine?.setInstrument(instrument as BrassType); + break; + case 'piano': + this.state.instruments.piano = instrument as PianoType; + this.pianoEngine?.setInstrument(instrument as PianoType); + break; + case 'chords': + this.state.instruments.chords = instrument as ChordType; + this.chordEngine?.setInstrument(instrument as ChordType); + break; + case 'ambient': + this.state.instruments.ambient = instrument as AmbientType; + this.ambientLayer?.setInstrument(instrument as AmbientType); + break; + } this.notifyStateChange(); } setBpm(bpm: number): void { - this.state.bpm = Math.max(60, Math.min(100, bpm)); + this.state.bpm = Math.max(60, Math.min(180, bpm)); Tone.getTransport().bpm.value = this.state.bpm; this.notifyStateChange(); } @@ -159,6 +268,15 @@ class AudioEngine { case 'drums': this.drumMachine?.setVolume(normalizedVolume); break; + case 'bass': + this.bassEngine?.setVolume(normalizedVolume); + break; + case 'brass': + this.brassEngine?.setVolume(normalizedVolume); + break; + case 'piano': + this.pianoEngine?.setVolume(normalizedVolume); + break; case 'chords': this.chordEngine?.setVolume(normalizedVolume); break; @@ -177,6 +295,15 @@ class AudioEngine { case 'drums': this.drumMachine?.mute(this.state.muted[layer]); break; + case 'bass': + this.bassEngine?.mute(this.state.muted[layer]); + break; + case 'brass': + this.brassEngine?.mute(this.state.muted[layer]); + break; + case 'piano': + this.pianoEngine?.mute(this.state.muted[layer]); + break; case 'chords': this.chordEngine?.mute(this.state.muted[layer]); break; @@ -195,6 +322,15 @@ class AudioEngine { case 'drums': this.drumMachine?.mute(muted); break; + case 'bass': + this.bassEngine?.mute(muted); + break; + case 'brass': + this.brassEngine?.mute(muted); + break; + case 'piano': + this.pianoEngine?.mute(muted); + break; case 'chords': this.chordEngine?.mute(muted); break; @@ -215,6 +351,9 @@ class AudioEngine { this.drumMachine?.dispose(); this.chordEngine?.dispose(); this.ambientLayer?.dispose(); + this.bassEngine?.dispose(); + this.brassEngine?.dispose(); + this.pianoEngine?.dispose(); this.masterGain?.dispose(); this.masterCompressor?.dispose(); this.masterLimiter?.dispose(); @@ -223,6 +362,9 @@ class AudioEngine { this.drumMachine = null; this.chordEngine = null; this.ambientLayer = null; + this.bassEngine = null; + this.brassEngine = null; + this.pianoEngine = null; this.masterGain = null; this.masterCompressor = null; this.masterLimiter = null; diff --git a/lib/audio/bassEngine.ts b/lib/audio/bassEngine.ts new file mode 100644 index 0000000..5223419 --- /dev/null +++ b/lib/audio/bassEngine.ts @@ -0,0 +1,117 @@ +import * as Tone from 'tone'; +import { BassType, Genre } from '@/types/audio'; +import { getRandomBassPattern } from './patterns'; + +export class BassEngine { + private synth: Tone.MonoSynth | null = null; + private sequence: Tone.Sequence | null = null; + private pattern: (string | null)[]; + private output: Tone.Gain; + private filter: Tone.Filter; + private currentType: BassType = 'synth'; + private genre: Genre = 'hiphop'; + + constructor(destination: Tone.InputNode) { + this.output = new Tone.Gain(0.6); + this.filter = new Tone.Filter({ + frequency: 800, + type: 'lowpass', + rolloff: -24, + }); + + this.filter.connect(this.output); + this.output.connect(destination); + + this.pattern = getRandomBassPattern(this.genre); + this.createSynth('synth'); + } + + private createSynth(type: BassType): void { + if (this.synth) { + this.synth.dispose(); + } + + const configs: Record> = { + synth: { + oscillator: { type: 'sawtooth' }, + envelope: { attack: 0.01, decay: 0.3, sustain: 0.4, release: 0.2 }, + filterEnvelope: { attack: 0.01, decay: 0.2, sustain: 0.5, release: 0.2, baseFrequency: 200, octaves: 2 }, + }, + sub: { + oscillator: { type: 'sine' }, + envelope: { attack: 0.02, decay: 0.5, sustain: 0.8, release: 0.4 }, + filterEnvelope: { attack: 0.01, decay: 0.1, sustain: 1, release: 0.1, baseFrequency: 100, octaves: 1 }, + }, + electric: { + oscillator: { type: 'triangle' }, + envelope: { attack: 0.005, decay: 0.2, sustain: 0.3, release: 0.15 }, + filterEnvelope: { attack: 0.01, decay: 0.15, sustain: 0.4, release: 0.2, baseFrequency: 300, octaves: 2.5 }, + }, + upright: { + oscillator: { type: 'triangle' }, + envelope: { attack: 0.02, decay: 0.4, sustain: 0.2, release: 0.3 }, + filterEnvelope: { attack: 0.02, decay: 0.3, sustain: 0.3, release: 0.3, baseFrequency: 250, octaves: 1.5 }, + }, + }; + + this.synth = new Tone.MonoSynth(configs[type]); + this.synth.volume.value = -6; + this.synth.connect(this.filter); + this.currentType = type; + } + + setInstrument(type: BassType): void { + this.createSynth(type); + } + + setGenre(genre: Genre): void { + this.genre = genre; + this.pattern = getRandomBassPattern(genre); + if (this.sequence) { + this.createSequence(); + } + } + + createSequence(): void { + if (this.sequence) { + this.sequence.dispose(); + } + + const steps = Array.from({ length: 16 }, (_, i) => i); + + this.sequence = new Tone.Sequence( + (time, step) => { + const note = this.pattern[step]; + if (note && this.synth) { + this.synth.triggerAttackRelease(note, '8n', time, 0.7); + } + }, + steps, + '16n' + ); + + this.sequence.start(0); + } + + randomize(): void { + this.pattern = getRandomBassPattern(this.genre); + if (this.sequence) { + this.createSequence(); + } + } + + setVolume(volume: number): void { + this.output.gain.rampTo(volume, 0.1); + } + + mute(muted: boolean): void { + this.output.gain.rampTo(muted ? 0 : 0.6, 0.1); + } + + dispose(): void { + this.sequence?.dispose(); + this.synth?.dispose(); + this.filter.dispose(); + this.output.dispose(); + } +} diff --git a/lib/audio/brassEngine.ts b/lib/audio/brassEngine.ts new file mode 100644 index 0000000..898542b --- /dev/null +++ b/lib/audio/brassEngine.ts @@ -0,0 +1,120 @@ +import * as Tone from 'tone'; +import { BrassType, Genre } from '@/types/audio'; +import { getRandomBrassPattern } from './patterns'; + +export class BrassEngine { + private synth: Tone.Synth | null = null; + private sequence: Tone.Sequence | null = null; + private pattern: (string | null)[]; + private output: Tone.Gain; + private filter: Tone.Filter; + private reverb: Tone.Reverb; + private currentType: BrassType = 'trumpet'; + private genre: Genre = 'hiphop'; + + constructor(destination: Tone.InputNode) { + this.output = new Tone.Gain(0.4); + this.filter = new Tone.Filter({ + frequency: 3000, + type: 'lowpass', + rolloff: -12, + }); + this.reverb = new Tone.Reverb({ + decay: 2, + wet: 0.3, + }); + + this.filter.connect(this.reverb); + this.reverb.connect(this.output); + this.output.connect(destination); + + this.pattern = getRandomBrassPattern(this.genre); + this.createSynth('trumpet'); + } + + private createSynth(type: BrassType): void { + if (this.synth) { + this.synth.dispose(); + } + + const configs: Record> = { + trumpet: { + oscillator: { type: 'sawtooth' }, + envelope: { attack: 0.05, decay: 0.2, sustain: 0.6, release: 0.3 }, + }, + horn: { + oscillator: { type: 'sine' }, + envelope: { attack: 0.1, decay: 0.3, sustain: 0.7, release: 0.5 }, + }, + 'synth-brass': { + oscillator: { type: 'fatsawtooth', spread: 20, count: 3 } as Tone.OmniOscillatorOptions, + envelope: { attack: 0.02, decay: 0.3, sustain: 0.5, release: 0.2 }, + }, + orchestra: { + oscillator: { type: 'sawtooth' }, + envelope: { attack: 0.15, decay: 0.4, sustain: 0.8, release: 0.6 }, + }, + }; + + this.synth = new Tone.Synth(configs[type]); + this.synth.volume.value = -8; + this.synth.connect(this.filter); + this.currentType = type; + } + + setInstrument(type: BrassType): void { + this.createSynth(type); + } + + setGenre(genre: Genre): void { + this.genre = genre; + this.pattern = getRandomBrassPattern(genre); + if (this.sequence) { + this.createSequence(); + } + } + + createSequence(): void { + if (this.sequence) { + this.sequence.dispose(); + } + + const steps = Array.from({ length: 16 }, (_, i) => i); + + this.sequence = new Tone.Sequence( + (time, step) => { + const note = this.pattern[step]; + if (note && this.synth) { + this.synth.triggerAttackRelease(note, '8n', time, 0.6); + } + }, + steps, + '16n' + ); + + this.sequence.start(0); + } + + randomize(): void { + this.pattern = getRandomBrassPattern(this.genre); + if (this.sequence) { + this.createSequence(); + } + } + + setVolume(volume: number): void { + this.output.gain.rampTo(volume, 0.1); + } + + mute(muted: boolean): void { + this.output.gain.rampTo(muted ? 0 : 0.4, 0.1); + } + + dispose(): void { + this.sequence?.dispose(); + this.synth?.dispose(); + this.filter.dispose(); + this.reverb.dispose(); + this.output.dispose(); + } +} diff --git a/lib/audio/chordEngine.ts b/lib/audio/chordEngine.ts index 45d7786..675a85b 100644 --- a/lib/audio/chordEngine.ts +++ b/lib/audio/chordEngine.ts @@ -1,33 +1,32 @@ import * as Tone from 'tone'; -import { ChordProgression } from '@/types/audio'; +import { ChordProgression, ChordType, Genre } from '@/types/audio'; import { getRandomProgression } from './patterns'; export class ChordEngine { - private synth: Tone.PolySynth; + private synth: Tone.PolySynth | null = null; private sequence: Tone.Sequence | null = null; private progression: ChordProgression; private output: Tone.Gain; private filter: Tone.Filter; private reverb: Tone.Reverb; private chorus: Tone.Chorus; + private currentType: ChordType = 'pad'; + private genre: Genre = 'hiphop'; constructor(destination: Tone.InputNode) { this.output = new Tone.Gain(0.6); - // Warm lofi filter this.filter = new Tone.Filter({ frequency: 2000, type: 'lowpass', rolloff: -24, }); - // Dreamy reverb this.reverb = new Tone.Reverb({ decay: 3, wet: 0.4, }); - // Subtle chorus for width this.chorus = new Tone.Chorus({ frequency: 0.5, delayTime: 3.5, @@ -35,42 +34,73 @@ export class ChordEngine { wet: 0.3, }).start(); - // FM Synth for warm, evolving pad sound - this.synth = new Tone.PolySynth(Tone.FMSynth, { - harmonicity: 2, - modulationIndex: 1.5, - oscillator: { - type: 'sine', - }, - envelope: { - attack: 0.3, - decay: 0.3, - sustain: 0.8, - release: 1.5, - }, - modulation: { - type: 'sine', - }, - modulationEnvelope: { - attack: 0.5, - decay: 0.2, - sustain: 0.5, - release: 0.5, - }, - }); - - // Lower the overall synth volume to prevent clipping - this.synth.volume.value = -12; - - // Chain: synth -> filter -> chorus -> reverb -> output -> destination - this.synth.connect(this.filter); this.filter.connect(this.chorus); this.chorus.connect(this.reverb); this.reverb.connect(this.output); this.output.connect(destination); - // Initialize with a random progression - this.progression = getRandomProgression(); + this.progression = getRandomProgression(this.genre); + this.createSynth('pad'); + } + + private createSynth(type: ChordType): void { + if (this.synth) { + this.synth.dispose(); + } + + const configs: Record = { + pad: { + synth: Tone.FMSynth, + options: { + harmonicity: 2, + modulationIndex: 1.5, + oscillator: { type: 'sine' }, + envelope: { attack: 0.3, decay: 0.3, sustain: 0.8, release: 1.5 }, + modulation: { type: 'sine' }, + modulationEnvelope: { attack: 0.5, decay: 0.2, sustain: 0.5, release: 0.5 }, + }, + }, + strings: { + synth: Tone.Synth, + options: { + oscillator: { type: 'sawtooth' }, + envelope: { attack: 0.4, decay: 0.3, sustain: 0.9, release: 2 }, + }, + }, + organ: { + synth: Tone.Synth, + options: { + oscillator: { type: 'sine' }, + envelope: { attack: 0.01, decay: 0.1, sustain: 0.9, release: 0.3 }, + }, + }, + synth: { + synth: Tone.Synth, + options: { + oscillator: { type: 'fatsawtooth', spread: 30, count: 3 }, + envelope: { attack: 0.1, decay: 0.2, sustain: 0.6, release: 0.8 }, + }, + }, + }; + + const config = configs[type]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.synth = new Tone.PolySynth(config.synth as any, config.options); + this.synth.volume.value = -12; + this.synth.connect(this.filter); + this.currentType = type; + } + + setInstrument(type: ChordType): void { + this.createSynth(type); + } + + setGenre(genre: Genre): void { + this.genre = genre; + this.progression = getRandomProgression(genre); + if (this.sequence) { + this.createSequence(); + } } createSequence(): void { @@ -85,9 +115,10 @@ export class ChordEngine { const chord = this.progression.chords[step]; const duration = this.progression.durations[step]; - // Release previous notes and play new chord - this.synth.releaseAll(time); - this.synth.triggerAttackRelease(chord, duration, time, 0.5); + if (this.synth) { + this.synth.releaseAll(time); + this.synth.triggerAttackRelease(chord, duration, time, 0.5); + } }, steps, '2n' @@ -98,14 +129,13 @@ export class ChordEngine { setProgression(progression: ChordProgression): void { this.progression = progression; - // Recreate sequence with new progression if (this.sequence) { this.createSequence(); } } randomize(): ChordProgression { - this.progression = getRandomProgression(); + this.progression = getRandomProgression(this.genre); if (this.sequence) { this.createSequence(); } @@ -130,7 +160,7 @@ export class ChordEngine { dispose(): void { this.sequence?.dispose(); - this.synth.dispose(); + this.synth?.dispose(); this.filter.dispose(); this.reverb.dispose(); this.chorus.dispose(); diff --git a/lib/audio/drumMachine.ts b/lib/audio/drumMachine.ts index 4292859..5379ff2 100644 --- a/lib/audio/drumMachine.ts +++ b/lib/audio/drumMachine.ts @@ -1,16 +1,21 @@ import * as Tone from 'tone'; -import { DrumPattern } from '@/types/audio'; -import { getRandomPattern, generateRandomPattern } from './patterns'; +import { DrumPattern, DrumKit, Genre } from '@/types/audio'; +import { getRandomPattern } from './patterns'; export class DrumMachine { - private kick: Tone.MembraneSynth; - private snare: Tone.NoiseSynth; - private hihat: Tone.NoiseSynth; - private openhat: Tone.NoiseSynth; + private kick: Tone.MembraneSynth | null = null; + private snare: Tone.NoiseSynth | null = null; + private hihat: Tone.NoiseSynth | null = null; + private openhat: Tone.NoiseSynth | null = null; + private snareFilter: Tone.Filter | null = null; + private hihatFilter: Tone.Filter | null = null; + private openhatFilter: Tone.Filter | null = null; private sequence: Tone.Sequence | null = null; private pattern: DrumPattern; private output: Tone.Gain; private lowpass: Tone.Filter; + private currentKit: DrumKit = 'acoustic'; + private genre: Genre = 'hiphop'; constructor(destination: Tone.InputNode) { this.output = new Tone.Gain(0.8); @@ -20,80 +25,116 @@ export class DrumMachine { rolloff: -12, }); - // Kick drum - deep and punchy - this.kick = new Tone.MembraneSynth({ - pitchDecay: 0.05, - octaves: 6, - oscillator: { type: 'sine' }, - envelope: { - attack: 0.001, - decay: 0.4, - sustain: 0.01, - release: 0.4, - }, - }); - - // Snare - filtered noise - this.snare = new Tone.NoiseSynth({ - noise: { type: 'white' }, - envelope: { - attack: 0.001, - decay: 0.2, - sustain: 0, - release: 0.1, - }, - }); - const snareFilter = new Tone.Filter({ - frequency: 5000, - type: 'bandpass', - Q: 1, - }); - this.snare.connect(snareFilter); - snareFilter.connect(this.lowpass); - - // Closed hi-hat - high filtered noise - this.hihat = new Tone.NoiseSynth({ - noise: { type: 'white' }, - envelope: { - attack: 0.001, - decay: 0.05, - sustain: 0, - release: 0.02, - }, - }); - const hihatFilter = new Tone.Filter({ - frequency: 10000, - type: 'highpass', - }); - this.hihat.connect(hihatFilter); - hihatFilter.connect(this.lowpass); - - // Open hi-hat - this.openhat = new Tone.NoiseSynth({ - noise: { type: 'white' }, - envelope: { - attack: 0.001, - decay: 0.3, - sustain: 0, - release: 0.15, - }, - }); - const openhatFilter = new Tone.Filter({ - frequency: 8000, - type: 'highpass', - }); - this.openhat.connect(openhatFilter); - openhatFilter.connect(this.lowpass); - - // Connect kick directly to lowpass - this.kick.connect(this.lowpass); - - // Chain: lowpass -> output -> destination this.lowpass.connect(this.output); this.output.connect(destination); - // Initialize with a random pattern - this.pattern = getRandomPattern(); + this.pattern = getRandomPattern(this.genre); + this.createKit('acoustic'); + } + + private createKit(kit: DrumKit): void { + // Dispose existing instruments + this.kick?.dispose(); + this.snare?.dispose(); + this.hihat?.dispose(); + this.openhat?.dispose(); + this.snareFilter?.dispose(); + this.hihatFilter?.dispose(); + this.openhatFilter?.dispose(); + + const kitConfigs: Record; + snareFilter: number; + hihatFilter: number; + snareDecay: number; + hihatDecay: number; + }> = { + acoustic: { + kick: { pitchDecay: 0.05, octaves: 6, envelope: { attack: 0.001, decay: 0.4, sustain: 0.01, release: 0.4 } }, + snareFilter: 5000, + hihatFilter: 10000, + snareDecay: 0.2, + hihatDecay: 0.05, + }, + electronic: { + kick: { pitchDecay: 0.08, octaves: 8, envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.3 } }, + snareFilter: 4000, + hihatFilter: 12000, + snareDecay: 0.15, + hihatDecay: 0.03, + }, + '808': { + kick: { pitchDecay: 0.15, octaves: 10, envelope: { attack: 0.001, decay: 0.8, sustain: 0.1, release: 0.6 } }, + snareFilter: 3000, + hihatFilter: 8000, + snareDecay: 0.25, + hihatDecay: 0.04, + }, + orchestral: { + kick: { pitchDecay: 0.02, octaves: 4, envelope: { attack: 0.01, decay: 0.5, sustain: 0.05, release: 0.5 } }, + snareFilter: 6000, + hihatFilter: 6000, + snareDecay: 0.3, + hihatDecay: 0.1, + }, + }; + + const config = kitConfigs[kit]; + + // Kick drum + this.kick = new Tone.MembraneSynth({ + ...config.kick, + oscillator: { type: 'sine' }, + }); + this.kick.connect(this.lowpass); + + // Snare + this.snareFilter = new Tone.Filter({ + frequency: config.snareFilter, + type: 'bandpass', + Q: 1, + }); + this.snare = new Tone.NoiseSynth({ + noise: { type: 'white' }, + envelope: { attack: 0.001, decay: config.snareDecay, sustain: 0, release: 0.1 }, + }); + this.snare.connect(this.snareFilter); + this.snareFilter.connect(this.lowpass); + + // Closed hi-hat + this.hihatFilter = new Tone.Filter({ + frequency: config.hihatFilter, + type: 'highpass', + }); + this.hihat = new Tone.NoiseSynth({ + noise: { type: 'white' }, + envelope: { attack: 0.001, decay: config.hihatDecay, sustain: 0, release: 0.02 }, + }); + this.hihat.connect(this.hihatFilter); + this.hihatFilter.connect(this.lowpass); + + // Open hi-hat + this.openhatFilter = new Tone.Filter({ + frequency: config.hihatFilter - 2000, + type: 'highpass', + }); + this.openhat = new Tone.NoiseSynth({ + noise: { type: 'white' }, + envelope: { attack: 0.001, decay: 0.3, sustain: 0, release: 0.15 }, + }); + this.openhat.connect(this.openhatFilter); + this.openhatFilter.connect(this.lowpass); + + this.currentKit = kit; + } + + setInstrument(kit: DrumKit): void { + this.createKit(kit); + } + + setGenre(genre: Genre): void { + this.genre = genre; + this.pattern = getRandomPattern(genre); } createSequence(onStep?: (step: number) => void): void { @@ -105,20 +146,19 @@ export class DrumMachine { this.sequence = new Tone.Sequence( (time, step) => { - if (this.pattern.kick[step]) { + if (this.pattern.kick[step] && this.kick) { this.kick.triggerAttackRelease('C1', '8n', time, 0.8); } - if (this.pattern.snare[step]) { + if (this.pattern.snare[step] && this.snare) { this.snare.triggerAttackRelease('8n', time, 0.5); } - if (this.pattern.hihat[step]) { + if (this.pattern.hihat[step] && this.hihat) { this.hihat.triggerAttackRelease('32n', time, 0.3); } - if (this.pattern.openhat[step]) { + if (this.pattern.openhat[step] && this.openhat) { this.openhat.triggerAttackRelease('16n', time, 0.25); } - // Call step callback on main thread if (onStep) { Tone.getDraw().schedule(() => { onStep(step); @@ -137,7 +177,7 @@ export class DrumMachine { } randomize(): DrumPattern { - this.pattern = Math.random() > 0.5 ? getRandomPattern() : generateRandomPattern(); + this.pattern = getRandomPattern(this.genre); return this.pattern; } @@ -155,10 +195,13 @@ export class DrumMachine { dispose(): void { this.sequence?.dispose(); - this.kick.dispose(); - this.snare.dispose(); - this.hihat.dispose(); - this.openhat.dispose(); + this.kick?.dispose(); + this.snare?.dispose(); + this.hihat?.dispose(); + this.openhat?.dispose(); + this.snareFilter?.dispose(); + this.hihatFilter?.dispose(); + this.openhatFilter?.dispose(); this.lowpass.dispose(); this.output.dispose(); } diff --git a/lib/audio/patterns.ts b/lib/audio/patterns.ts index 7e6a25e..1b5a548 100644 --- a/lib/audio/patterns.ts +++ b/lib/audio/patterns.ts @@ -1,145 +1,238 @@ -import { DrumPattern, ChordProgression } from '@/types/audio'; +import { DrumPattern, ChordProgression, Genre } from '@/types/audio'; -// Classic boom bap patterns -export const drumPatterns: DrumPattern[] = [ - { - // Classic boom bap - kick: [true, false, false, false, false, false, false, false, true, false, true, false, false, false, false, false], - snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], - hihat: [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false], - openhat: [false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true], - }, - { - // Laid back groove - kick: [true, false, false, false, false, false, true, false, true, false, false, false, false, false, false, false], - snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true], - hihat: [true, true, true, false, true, true, true, false, true, true, true, false, true, true, true, false], - openhat: [false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, false], - }, - { - // Minimal chill - kick: [true, false, false, false, false, false, false, false, true, false, false, false, false, false, true, false], - snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], - hihat: [true, false, true, true, true, false, true, true, true, false, true, true, true, false, true, true], - openhat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true], - }, - { - // Jazzy swing - kick: [true, false, false, true, false, false, true, false, false, false, true, false, false, false, false, false], - snare: [false, false, false, false, true, false, false, false, false, true, false, false, true, false, false, true], - hihat: [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false], - openhat: [false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false], - }, - { - // Deep pocket - kick: [true, false, false, false, false, false, false, true, false, false, true, false, false, false, false, false], - snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], - hihat: [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true], - openhat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true], - }, -]; +// Genre-specific drum patterns +export const drumPatterns: Record = { + hiphop: [ + { + kick: [true, false, false, false, false, false, false, false, true, false, true, false, false, false, false, false], + snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], + hihat: [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false], + openhat: [false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true], + }, + { + kick: [true, false, false, false, false, false, true, false, true, false, false, false, false, false, false, false], + snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true], + hihat: [true, true, true, false, true, true, true, false, true, true, true, false, true, true, true, false], + openhat: [false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, false], + }, + ], + classical: [ + { + kick: [true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false], + snare: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], + hihat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], + openhat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], + }, + { + kick: [true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false], + snare: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], + hihat: [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false], + openhat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], + }, + ], + trap: [ + { + kick: [true, false, false, false, false, false, true, false, false, false, true, false, false, false, false, false], + snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], + hihat: [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true], + openhat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, true, false], + }, + { + kick: [true, false, false, true, false, false, true, false, false, false, true, false, false, true, false, false], + snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, true], + hihat: [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true], + openhat: [false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true], + }, + ], + pop: [ + { + kick: [true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false], + snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], + hihat: [true, false, true, false, true, false, true, false, true, false, true, false, true, false, true, false], + openhat: [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, true], + }, + { + kick: [true, false, false, false, false, false, true, false, true, false, false, false, false, false, true, false], + snare: [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false], + hihat: [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true], + openhat: [false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, false], + }, + ], +}; -// Jazz chord progressions in lofi style -export const chordProgressions: ChordProgression[] = [ - { - name: 'Classic ii-V-I', - chords: [ - ['D3', 'F3', 'A3', 'C4'], // Dm7 - ['G2', 'B2', 'D3', 'F3'], // G7 - ['C3', 'E3', 'G3', 'B3'], // Cmaj7 - ['C3', 'E3', 'G3', 'B3'], // Cmaj7 - ], - durations: ['2n', '2n', '2n', '2n'], - }, - { - name: 'Minor Key Chill', - chords: [ - ['A2', 'C3', 'E3', 'G3'], // Am7 - ['D3', 'F3', 'A3', 'C4'], // Dm7 - ['E2', 'G#2', 'B2', 'D3'], // E7 - ['A2', 'C3', 'E3', 'G3'], // Am7 - ], - durations: ['2n', '2n', '2n', '2n'], - }, - { - name: 'Neo Soul', - chords: [ - ['F3', 'A3', 'C4', 'E4'], // Fmaj7 - ['E3', 'G3', 'B3', 'D4'], // Em7 - ['D3', 'F3', 'A3', 'C4'], // Dm7 - ['G2', 'B2', 'D3', 'F3'], // G7 - ], - durations: ['2n', '2n', '2n', '2n'], - }, - { - name: 'Dreamy', - chords: [ - ['C3', 'E3', 'G3', 'B3'], // Cmaj7 - ['A2', 'C3', 'E3', 'G3'], // Am7 - ['F3', 'A3', 'C4', 'E4'], // Fmaj7 - ['G2', 'B2', 'D3', 'F3'], // G7 - ], - durations: ['2n', '2n', '2n', '2n'], - }, - { - name: 'Melancholy', - chords: [ - ['D3', 'F3', 'A3', 'C4'], // Dm7 - ['G2', 'Bb2', 'D3', 'F3'], // Gm7 - ['C3', 'Eb3', 'G3', 'Bb3'],// Cm7 - ['F2', 'A2', 'C3', 'Eb3'], // F7 - ], - durations: ['2n', '2n', '2n', '2n'], - }, -]; +// Genre-specific chord progressions +export const chordProgressions: Record = { + hiphop: [ + { + name: 'Classic ii-V-I', + chords: [ + ['D3', 'F3', 'A3', 'C4'], + ['G2', 'B2', 'D3', 'F3'], + ['C3', 'E3', 'G3', 'B3'], + ['C3', 'E3', 'G3', 'B3'], + ], + durations: ['2n', '2n', '2n', '2n'], + }, + { + name: 'Minor Key Chill', + chords: [ + ['A2', 'C3', 'E3', 'G3'], + ['D3', 'F3', 'A3', 'C4'], + ['E2', 'G#2', 'B2', 'D3'], + ['A2', 'C3', 'E3', 'G3'], + ], + durations: ['2n', '2n', '2n', '2n'], + }, + ], + classical: [ + { + name: 'Romantic', + chords: [ + ['C3', 'E3', 'G3'], + ['F3', 'A3', 'C4'], + ['G3', 'B3', 'D4'], + ['C3', 'E3', 'G3'], + ], + durations: ['1n', '1n', '1n', '1n'], + }, + { + name: 'Baroque', + chords: [ + ['D3', 'F3', 'A3'], + ['G2', 'B2', 'D3'], + ['C3', 'E3', 'G3'], + ['A2', 'C3', 'E3'], + ], + durations: ['1n', '1n', '1n', '1n'], + }, + ], + trap: [ + { + name: 'Dark Minor', + chords: [ + ['A2', 'C3', 'E3'], + ['F2', 'A2', 'C3'], + ['G2', 'B2', 'D3'], + ['E2', 'G#2', 'B2'], + ], + durations: ['2n', '2n', '2n', '2n'], + }, + { + name: 'Eerie', + chords: [ + ['D3', 'F3', 'A3'], + ['Bb2', 'D3', 'F3'], + ['A2', 'C3', 'E3'], + ['G2', 'Bb2', 'D3'], + ], + durations: ['2n', '2n', '2n', '2n'], + }, + ], + pop: [ + { + name: 'Four Chord Pop', + chords: [ + ['C3', 'E3', 'G3'], + ['G2', 'B2', 'D3'], + ['A2', 'C3', 'E3'], + ['F2', 'A2', 'C3'], + ], + durations: ['1n', '1n', '1n', '1n'], + }, + { + name: 'Uplifting', + chords: [ + ['D3', 'F#3', 'A3'], + ['A2', 'C#3', 'E3'], + ['B2', 'D3', 'F#3'], + ['G2', 'B2', 'D3'], + ], + durations: ['1n', '1n', '1n', '1n'], + }, + ], +}; -export function getRandomPattern(): DrumPattern { - return drumPatterns[Math.floor(Math.random() * drumPatterns.length)]; +// Bass patterns by genre +export const bassPatterns: Record = { + hiphop: [ + ['C2', null, null, null, 'C2', null, 'G1', null, 'C2', null, null, null, 'E2', null, null, null], + ['A1', null, null, 'A1', null, null, 'G1', null, 'A1', null, null, null, 'E1', null, 'G1', null], + ], + classical: [ + ['C2', null, null, null, 'G2', null, null, null, 'C2', null, null, null, 'G2', null, null, null], + ['D2', null, null, null, 'A2', null, null, null, 'D2', null, null, null, 'A2', null, null, null], + ], + trap: [ + ['C1', null, null, 'C1', null, null, 'C1', null, null, null, 'G1', null, null, 'C1', null, null], + ['A0', null, null, 'A0', null, 'A0', null, null, 'A0', null, null, null, 'E1', null, 'A0', null], + ], + pop: [ + ['C2', null, 'C2', null, 'G2', null, 'G2', null, 'A2', null, 'A2', null, 'F2', null, 'F2', null], + ['D2', null, null, 'D2', 'A2', null, null, null, 'B2', null, null, 'B2', 'G2', null, null, null], + ], +}; + +// Brass melody patterns by genre +export const brassPatterns: Record = { + hiphop: [ + ['C4', null, null, null, 'E4', null, null, null, 'G4', null, null, null, 'E4', null, null, null], + [null, null, 'D4', null, null, null, 'F4', null, null, null, 'A4', null, null, null, 'G4', null], + ], + classical: [ + ['G4', null, null, null, null, null, null, null, 'E4', null, null, null, null, null, null, null], + ['C4', null, null, null, 'D4', null, null, null, 'E4', null, null, null, 'G4', null, null, null], + ], + trap: [ + ['A3', null, null, null, null, null, null, null, 'C4', null, null, null, null, null, null, null], + [null, null, null, null, 'E4', null, null, null, null, null, null, null, 'D4', null, null, null], + ], + pop: [ + ['E4', null, null, null, 'G4', null, null, null, 'A4', null, null, null, 'G4', null, null, null], + ['C4', null, 'D4', null, 'E4', null, 'G4', null, 'A4', null, 'G4', null, 'E4', null, 'D4', null], + ], +}; + +// Piano patterns by genre +export const pianoPatterns: Record = { + hiphop: [ + [['C4', 'E4', 'G4'], null, null, null, ['D4', 'F4', 'A4'], null, null, null, ['E4', 'G4', 'B4'], null, null, null, ['D4', 'F4', 'A4'], null, null, null], + [['A3', 'C4', 'E4'], null, ['A3', 'C4', 'E4'], null, null, null, ['G3', 'B3', 'D4'], null, ['G3', 'B3', 'D4'], null, null, null, ['F3', 'A3', 'C4'], null, null, null], + ], + classical: [ + [['C4', 'E4', 'G4'], ['E4'], ['G4'], ['C5'], ['G4'], ['E4'], ['C4', 'E4', 'G4'], null, ['D4', 'F4', 'A4'], ['F4'], ['A4'], ['D5'], ['A4'], ['F4'], ['D4', 'F4', 'A4'], null], + [['A3', 'C4', 'E4'], null, ['C4', 'E4'], null, ['A3', 'C4', 'E4'], null, ['C4', 'E4'], null, ['G3', 'B3', 'D4'], null, ['B3', 'D4'], null, ['G3', 'B3', 'D4'], null, null, null], + ], + trap: [ + [['A3', 'C4', 'E4'], null, null, null, null, null, null, null, ['G3', 'Bb3', 'D4'], null, null, null, null, null, null, null], + [['D4', 'F4', 'A4'], null, null, null, null, null, ['C4', 'E4', 'G4'], null, null, null, null, null, null, null, ['Bb3', 'D4', 'F4'], null], + ], + pop: [ + [['C4', 'E4', 'G4'], null, ['C4', 'E4', 'G4'], null, ['G3', 'B3', 'D4'], null, ['G3', 'B3', 'D4'], null, ['A3', 'C4', 'E4'], null, ['A3', 'C4', 'E4'], null, ['F3', 'A3', 'C4'], null, ['F3', 'A3', 'C4'], null], + [['D4', 'F#4', 'A4'], null, null, ['D4', 'F#4', 'A4'], ['A3', 'C#4', 'E4'], null, null, ['A3', 'C#4', 'E4'], ['B3', 'D4', 'F#4'], null, null, ['B3', 'D4', 'F#4'], ['G3', 'B3', 'D4'], null, null, null], + ], +}; + +export function getRandomPattern(genre: Genre): DrumPattern { + const patterns = drumPatterns[genre]; + return patterns[Math.floor(Math.random() * patterns.length)]; } -export function getRandomProgression(): ChordProgression { - return chordProgressions[Math.floor(Math.random() * chordProgressions.length)]; +export function getRandomProgression(genre: Genre): ChordProgression { + const progressions = chordProgressions[genre]; + return progressions[Math.floor(Math.random() * progressions.length)]; } -export function generateRandomPattern(): DrumPattern { - const pattern: DrumPattern = { - kick: new Array(16).fill(false), - snare: new Array(16).fill(false), - hihat: new Array(16).fill(false), - openhat: new Array(16).fill(false), - }; - - // Kick on 1 and somewhere in second half - pattern.kick[0] = true; - pattern.kick[8 + Math.floor(Math.random() * 4)] = true; - if (Math.random() > 0.5) { - pattern.kick[6 + Math.floor(Math.random() * 2)] = true; - } - - // Snare on 2 and 4 - pattern.snare[4] = true; - pattern.snare[12] = true; - // Ghost notes - if (Math.random() > 0.6) { - pattern.snare[Math.floor(Math.random() * 16)] = true; - } - - // Hi-hats with variation - for (let i = 0; i < 16; i++) { - if (i % 2 === 0) { - pattern.hihat[i] = Math.random() > 0.1; - } else { - pattern.hihat[i] = Math.random() > 0.5; - } - } - - // Open hi-hat occasionally - if (Math.random() > 0.3) { - pattern.openhat[7] = true; - } - if (Math.random() > 0.5) { - pattern.openhat[15] = true; - } - - return pattern; +export function getRandomBassPattern(genre: Genre): (string | null)[] { + const patterns = bassPatterns[genre]; + return patterns[Math.floor(Math.random() * patterns.length)]; +} + +export function getRandomBrassPattern(genre: Genre): (string | null)[] { + const patterns = brassPatterns[genre]; + return patterns[Math.floor(Math.random() * patterns.length)]; +} + +export function getRandomPianoPattern(genre: Genre): (string[] | null)[] { + const patterns = pianoPatterns[genre]; + return patterns[Math.floor(Math.random() * patterns.length)]; } diff --git a/lib/audio/pianoEngine.ts b/lib/audio/pianoEngine.ts new file mode 100644 index 0000000..8b6c56e --- /dev/null +++ b/lib/audio/pianoEngine.ts @@ -0,0 +1,138 @@ +import * as Tone from 'tone'; +import { PianoType, Genre } from '@/types/audio'; +import { getRandomPianoPattern } from './patterns'; + +export class PianoEngine { + private synth: Tone.PolySynth | null = null; + private sequence: Tone.Sequence | null = null; + private pattern: (string[] | null)[]; + private output: Tone.Gain; + private filter: Tone.Filter; + private reverb: Tone.Reverb; + private currentType: PianoType = 'grand'; + private genre: Genre = 'hiphop'; + + constructor(destination: Tone.InputNode) { + this.output = new Tone.Gain(0.5); + this.filter = new Tone.Filter({ + frequency: 5000, + type: 'lowpass', + rolloff: -12, + }); + this.reverb = new Tone.Reverb({ + decay: 2.5, + wet: 0.25, + }); + + this.filter.connect(this.reverb); + this.reverb.connect(this.output); + this.output.connect(destination); + + this.pattern = getRandomPianoPattern(this.genre); + this.createSynth('grand'); + } + + private createSynth(type: PianoType): void { + if (this.synth) { + this.synth.dispose(); + } + + const configs: Record = { + grand: { + synth: Tone.Synth, + options: { + oscillator: { type: 'triangle' }, + envelope: { attack: 0.005, decay: 0.8, sustain: 0.2, release: 1.2 }, + }, + }, + electric: { + synth: Tone.Synth, + options: { + oscillator: { type: 'sine' }, + envelope: { attack: 0.01, decay: 0.5, sustain: 0.3, release: 0.8 }, + }, + }, + rhodes: { + synth: Tone.FMSynth, + options: { + harmonicity: 3, + modulationIndex: 1, + oscillator: { type: 'sine' }, + envelope: { attack: 0.01, decay: 0.6, sustain: 0.3, release: 1 }, + modulation: { type: 'sine' }, + modulationEnvelope: { attack: 0.01, decay: 0.3, sustain: 0.5, release: 0.5 }, + }, + }, + synth: { + synth: Tone.Synth, + options: { + oscillator: { type: 'sawtooth' }, + envelope: { attack: 0.02, decay: 0.3, sustain: 0.4, release: 0.5 }, + }, + }, + }; + + const config = configs[type]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.synth = new Tone.PolySynth(config.synth as any, config.options); + this.synth.volume.value = -10; + this.synth.connect(this.filter); + this.currentType = type; + } + + setInstrument(type: PianoType): void { + this.createSynth(type); + } + + setGenre(genre: Genre): void { + this.genre = genre; + this.pattern = getRandomPianoPattern(genre); + if (this.sequence) { + this.createSequence(); + } + } + + createSequence(): void { + if (this.sequence) { + this.sequence.dispose(); + } + + const steps = Array.from({ length: 16 }, (_, i) => i); + + this.sequence = new Tone.Sequence( + (time, step) => { + const notes = this.pattern[step]; + if (notes && this.synth) { + this.synth.triggerAttackRelease(notes, '8n', time, 0.5); + } + }, + steps, + '16n' + ); + + this.sequence.start(0); + } + + randomize(): void { + this.pattern = getRandomPianoPattern(this.genre); + if (this.sequence) { + this.createSequence(); + } + } + + setVolume(volume: number): void { + this.output.gain.rampTo(volume, 0.1); + } + + mute(muted: boolean): void { + this.output.gain.rampTo(muted ? 0 : 0.5, 0.1); + } + + dispose(): void { + this.sequence?.dispose(); + this.synth?.dispose(); + this.filter.dispose(); + this.reverb.dispose(); + this.output.dispose(); + } +} diff --git a/package.json b/package.json index c4ee16a..40db6c1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-toggle": "^1.1.10", diff --git a/types/audio.ts b/types/audio.ts index 78c1af3..5e1ebf9 100644 --- a/types/audio.ts +++ b/types/audio.ts @@ -1,3 +1,12 @@ +export type Genre = 'hiphop' | 'classical' | 'trap' | 'pop'; + +export type DrumKit = 'acoustic' | 'electronic' | '808' | 'orchestral'; +export type BassType = 'synth' | 'sub' | 'electric' | 'upright'; +export type BrassType = 'trumpet' | 'horn' | 'synth-brass' | 'orchestra'; +export type PianoType = 'grand' | 'electric' | 'rhodes' | 'synth'; +export type ChordType = 'pad' | 'strings' | 'organ' | 'synth'; +export type AmbientType = 'rain' | 'vinyl' | 'nature' | 'space'; + export interface DrumPattern { kick: boolean[]; snare: boolean[]; @@ -11,28 +20,97 @@ export interface ChordProgression { durations: string[]; } +export interface BassPattern { + notes: (string | null)[]; + durations: string[]; +} + +export interface InstrumentConfig { + drums: DrumKit; + bass: BassType; + brass: BrassType; + piano: PianoType; + chords: ChordType; + ambient: AmbientType; +} + export interface EngineState { isPlaying: boolean; isInitialized: boolean; bpm: number; swing: number; currentStep: number; + genre: Genre; + duration: number; // in minutes + instruments: InstrumentConfig; volumes: { master: number; drums: number; + bass: number; + brass: number; + piano: number; chords: number; ambient: number; }; muted: { drums: boolean; + bass: boolean; + brass: boolean; + piano: boolean; chords: boolean; ambient: boolean; }; } -export type LayerName = 'drums' | 'chords' | 'ambient'; +export type LayerName = 'drums' | 'bass' | 'brass' | 'piano' | 'chords' | 'ambient'; export interface AudioEngineCallbacks { onStepChange?: (step: number) => void; onStateChange?: (state: EngineState) => void; } + +export const GENRE_CONFIG: Record = { + hiphop: { bpm: 90, swing: 0.15, name: 'Hip Hop' }, + classical: { bpm: 72, swing: 0, name: 'Classical' }, + trap: { bpm: 140, swing: 0.05, name: 'Trap' }, + pop: { bpm: 120, swing: 0.08, name: 'Pop' }, +}; + +export const INSTRUMENT_OPTIONS = { + drums: [ + { value: 'acoustic', label: 'Acoustic Kit' }, + { value: 'electronic', label: 'Electronic' }, + { value: '808', label: '808 Machine' }, + { value: 'orchestral', label: 'Orchestral' }, + ], + bass: [ + { value: 'synth', label: 'Synth Bass' }, + { value: 'sub', label: 'Sub Bass' }, + { value: 'electric', label: 'Electric Bass' }, + { value: 'upright', label: 'Upright Bass' }, + ], + brass: [ + { value: 'trumpet', label: 'Trumpet' }, + { value: 'horn', label: 'French Horn' }, + { value: 'synth-brass', label: 'Synth Brass' }, + { value: 'orchestra', label: 'Brass Section' }, + ], + piano: [ + { value: 'grand', label: 'Grand Piano' }, + { value: 'electric', label: 'Electric Piano' }, + { value: 'rhodes', label: 'Rhodes' }, + { value: 'synth', label: 'Synth Keys' }, + ], + chords: [ + { value: 'pad', label: 'Synth Pad' }, + { value: 'strings', label: 'Strings' }, + { value: 'organ', label: 'Organ' }, + { value: 'synth', label: 'Synth Lead' }, + ], + ambient: [ + { value: 'rain', label: 'Rain' }, + { value: 'vinyl', label: 'Vinyl Crackle' }, + { value: 'nature', label: 'Nature' }, + { value: 'space', label: 'Space Atmosphere' }, + ], +} as const;