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 */}
+
+
+
+
+ {/* 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;