Compare commits
No commits in common. "feature/multi-genre-instruments" and "main" have entirely different histories.
feature/mu
...
main
@ -52,28 +52,28 @@
|
||||
/* Lofi dark theme - always dark mode */
|
||||
:root {
|
||||
--radius: 0.75rem;
|
||||
/* Light blue background */
|
||||
--background: oklch(0.92 0.04 220);
|
||||
--foreground: oklch(0.20 0.02 240);
|
||||
/* Black card background */
|
||||
--card: oklch(0.08 0.01 240);
|
||||
--card-foreground: oklch(0.95 0.01 220);
|
||||
--popover: oklch(0.08 0.01 240);
|
||||
--popover-foreground: oklch(0.95 0.01 220);
|
||||
/* Deep purple-tinted dark background */
|
||||
--background: oklch(0.15 0.02 280);
|
||||
--foreground: oklch(0.92 0.01 280);
|
||||
/* Card with subtle purple tint */
|
||||
--card: oklch(0.18 0.025 280);
|
||||
--card-foreground: oklch(0.92 0.01 280);
|
||||
--popover: oklch(0.18 0.025 280);
|
||||
--popover-foreground: oklch(0.92 0.01 280);
|
||||
/* Warm orange primary for lofi vibes */
|
||||
--primary: oklch(0.75 0.15 50);
|
||||
--primary-foreground: oklch(0.15 0.02 280);
|
||||
/* Dark secondary */
|
||||
--secondary: oklch(0.18 0.02 240);
|
||||
--secondary-foreground: oklch(0.92 0.01 220);
|
||||
--muted: oklch(0.15 0.02 240);
|
||||
--muted-foreground: oklch(0.70 0.02 220);
|
||||
/* Muted purple secondary */
|
||||
--secondary: oklch(0.25 0.04 280);
|
||||
--secondary-foreground: oklch(0.85 0.02 280);
|
||||
--muted: oklch(0.22 0.03 280);
|
||||
--muted-foreground: oklch(0.65 0.02 280);
|
||||
/* Accent with warm pink */
|
||||
--accent: oklch(0.65 0.12 350);
|
||||
--accent-foreground: oklch(0.95 0.01 280);
|
||||
--destructive: oklch(0.55 0.2 25);
|
||||
--border: oklch(0.25 0.02 240);
|
||||
--input: oklch(0.15 0.02 240);
|
||||
--border: oklch(0.28 0.03 280);
|
||||
--input: oklch(0.22 0.03 280);
|
||||
--ring: oklch(0.75 0.15 50);
|
||||
/* Lofi color palette */
|
||||
--lofi-purple: oklch(0.55 0.15 280);
|
||||
@ -112,7 +112,7 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: oklch(0.88 0.04 220);
|
||||
background: oklch(0.15 0.02 280);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
|
||||
@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Beat Generator",
|
||||
description: "Create custom beats across Hip Hop, Classical, Trap, and Pop genres",
|
||||
title: "Lofi Generator",
|
||||
description: "Web-based lofi hip hop beat generator - beats to relax/study to",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
79
bun.lock
79
bun.lock
@ -6,7 +6,6 @@
|
||||
"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",
|
||||
@ -97,14 +96,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -207,8 +198,6 @@
|
||||
|
||||
"@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=="],
|
||||
@ -217,50 +206,26 @@
|
||||
|
||||
"@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=="],
|
||||
@ -377,8 +342,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -467,8 +430,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -567,8 +528,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -807,12 +766,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -931,10 +884,6 @@
|
||||
|
||||
"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=="],
|
||||
@ -961,30 +910,14 @@
|
||||
|
||||
"@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=="],
|
||||
@ -1021,22 +954,10 @@
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,143 +0,0 @@
|
||||
'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 {
|
||||
readonly value: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
interface LayerBoxProps {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
instrument: string;
|
||||
instrumentOptions: readonly InstrumentOption[];
|
||||
onVolumeChange: (volume: number) => void;
|
||||
onToggleMute: () => void;
|
||||
onInstrumentChange: (instrument: string) => void;
|
||||
accentColor: 'grey' | 'red' | 'green' | 'babyblue' | 'pink' | 'purple';
|
||||
}
|
||||
|
||||
const accentStyles = {
|
||||
grey: {
|
||||
border: 'border-slate-400/30 hover:border-slate-400/50',
|
||||
bg: 'bg-slate-300/10',
|
||||
icon: 'text-slate-400',
|
||||
slider: '[&_[data-slot=slider-range]]:bg-slate-400 [&_[data-slot=slider-thumb]]:border-slate-400',
|
||||
},
|
||||
red: {
|
||||
border: 'border-rose-400/30 hover:border-rose-400/50',
|
||||
bg: 'bg-rose-400/8',
|
||||
icon: 'text-rose-400',
|
||||
slider: '[&_[data-slot=slider-range]]:bg-rose-400 [&_[data-slot=slider-thumb]]:border-rose-400',
|
||||
},
|
||||
green: {
|
||||
border: 'border-emerald-400/30 hover:border-emerald-400/50',
|
||||
bg: 'bg-gradient-to-r from-emerald-400/10 to-white/5',
|
||||
icon: 'text-emerald-400',
|
||||
slider: '[&_[data-slot=slider-range]]:bg-emerald-400 [&_[data-slot=slider-thumb]]:border-emerald-400',
|
||||
},
|
||||
babyblue: {
|
||||
border: 'border-sky-300/30 hover:border-sky-300/50',
|
||||
bg: 'bg-sky-300/8',
|
||||
icon: 'text-sky-300',
|
||||
slider: '[&_[data-slot=slider-range]]:bg-sky-300 [&_[data-slot=slider-thumb]]:border-sky-300',
|
||||
},
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
export function LayerBox({
|
||||
title,
|
||||
icon,
|
||||
volume,
|
||||
muted,
|
||||
instrument,
|
||||
instrumentOptions,
|
||||
onVolumeChange,
|
||||
onToggleMute,
|
||||
onInstrumentChange,
|
||||
accentColor,
|
||||
}: LayerBoxProps) {
|
||||
const styles = accentStyles[accentColor];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
rounded-xl border-2 p-4 transition-all duration-200
|
||||
${styles.border} ${styles.bg}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={styles.icon}>{icon}</span>
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={instrument} onValueChange={onInstrumentChange}>
|
||||
<SelectTrigger className="h-7 w-28 text-xs bg-secondary/50 border-border/50">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{instrumentOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Toggle
|
||||
pressed={!muted}
|
||||
onPressedChange={onToggleMute}
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
aria-label={`Toggle ${title}`}
|
||||
>
|
||||
{muted ? (
|
||||
<VolumeX className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Volume2 className={`h-3.5 w-3.5 ${styles.icon}`} />
|
||||
)}
|
||||
</Toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[volume]}
|
||||
onValueChange={([v]) => onVolumeChange(v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
className={`flex-1 ${styles.slider}`}
|
||||
disabled={muted}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground font-mono w-10 text-right">
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
components/lofi-generator/LayerMixer.tsx
Normal file
71
components/lofi-generator/LayerMixer.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
'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: <Drum className="h-4 w-4" /> },
|
||||
{ name: 'chords', label: 'Chords', icon: <Music className="h-4 w-4" /> },
|
||||
{ name: 'ambient', label: 'Ambient', icon: <Cloud className="h-4 w-4" /> },
|
||||
];
|
||||
|
||||
export function LayerMixer({
|
||||
volumes,
|
||||
muted,
|
||||
onVolumeChange,
|
||||
onToggleMute,
|
||||
}: LayerMixerProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Layers
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{layers.map(({ name, label, icon }) => (
|
||||
<div key={name} className="flex items-center gap-3">
|
||||
<Toggle
|
||||
pressed={!muted[name]}
|
||||
onPressedChange={() => onToggleMute(name)}
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
aria-label={`Toggle ${label}`}
|
||||
>
|
||||
{muted[name] ? (
|
||||
<VolumeX className="h-4 w-4" />
|
||||
) : (
|
||||
<Volume2 className="h-4 w-4" />
|
||||
)}
|
||||
</Toggle>
|
||||
<div className="flex items-center gap-2 shrink-0 w-20">
|
||||
{icon}
|
||||
<span className="text-sm">{label}</span>
|
||||
</div>
|
||||
<VolumeControl
|
||||
label=""
|
||||
value={volumes[name]}
|
||||
onChange={(v) => onVolumeChange(name, v)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,52 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
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 { 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 {
|
||||
Play,
|
||||
Pause,
|
||||
Shuffle,
|
||||
Drum,
|
||||
Cloud,
|
||||
Guitar,
|
||||
Waves,
|
||||
Piano,
|
||||
} from 'lucide-react';
|
||||
import { Genre, GENRE_CONFIG, INSTRUMENT_OPTIONS } from '@/types/audio';
|
||||
|
||||
// Custom Trumpet icon component
|
||||
function TrumpetIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 12h4l3-3v6l-3-3" />
|
||||
<path d="M10 12h8" />
|
||||
<circle cx="20" cy="12" r="2" />
|
||||
<path d="M7 9V6" />
|
||||
<path d="M7 15v3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
export function LofiGenerator() {
|
||||
const {
|
||||
@ -57,184 +18,50 @@ export function LofiGenerator() {
|
||||
setMasterVolume,
|
||||
setLayerVolume,
|
||||
toggleMute,
|
||||
setGenre,
|
||||
setDuration,
|
||||
setInstrument,
|
||||
setBpm,
|
||||
setSwing,
|
||||
} = useAudioEngine();
|
||||
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [generationProgress, setGenerationProgress] = useState(0);
|
||||
|
||||
const genres: { value: Genre; label: string }[] = [
|
||||
{ value: 'hiphop', label: 'Hip Hop' },
|
||||
{ value: 'classical', label: 'Classical' },
|
||||
{ value: 'trap', label: 'Trap' },
|
||||
{ value: 'pop', label: 'Pop' },
|
||||
];
|
||||
|
||||
const handleGenerateBeat = async () => {
|
||||
setIsGenerating(true);
|
||||
setGenerationProgress(0);
|
||||
|
||||
// Animate progress while generating
|
||||
let currentProgress = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
const jump = Math.random() > 0.5 ? Math.random() * 15 : Math.random() * 8;
|
||||
currentProgress = Math.min(currentProgress + jump, 90);
|
||||
setGenerationProgress(currentProgress);
|
||||
}, 50);
|
||||
|
||||
// Generate the beat
|
||||
await generateNewBeat();
|
||||
|
||||
clearInterval(progressInterval);
|
||||
setGenerationProgress(100);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsGenerating(false);
|
||||
setGenerationProgress(0);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center p-4 pt-8">
|
||||
{/* Modern Header */}
|
||||
<header className="w-full max-w-2xl mb-8">
|
||||
<div className="flex items-center justify-center gap-3 mb-2">
|
||||
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-lofi-orange via-lofi-pink to-lofi-purple flex items-center justify-center">
|
||||
<Waves className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-lofi-orange via-lofi-pink to-lofi-purple bg-clip-text text-transparent">
|
||||
Beat Generator
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Create custom beats across multiple genres
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Card className="w-full max-w-2xl bg-card/80 backdrop-blur-sm border-border/50">
|
||||
<CardContent className="p-6 space-y-6">
|
||||
{/* Top Controls: Duration & Genre */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||
Duration
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Slider
|
||||
value={[state.duration]}
|
||||
onValueChange={([v]) => setDuration(v)}
|
||||
min={1}
|
||||
max={3}
|
||||
step={1}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-mono w-12 text-right">
|
||||
{state.duration} min
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||
Genre
|
||||
</Label>
|
||||
<Select value={state.genre} onValueChange={(v) => setGenre(v as Genre)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{genres.map((g) => (
|
||||
<SelectItem key={g.value} value={g.value}>
|
||||
{g.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transport Controls */}
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={togglePlayback}
|
||||
disabled={isGenerating}
|
||||
className="h-14 w-14 rounded-full bg-gradient-to-br from-lofi-orange to-lofi-pink hover:opacity-90"
|
||||
>
|
||||
{state.isPlaying ? (
|
||||
<Pause className="h-6 w-6" />
|
||||
) : (
|
||||
<Play className="h-6 w-6 ml-1" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={handleGenerateBeat}
|
||||
disabled={isGenerating}
|
||||
className="gap-2"
|
||||
>
|
||||
<Shuffle className="h-4 w-4" />
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Generation Progress Bar */}
|
||||
{isGenerating && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Generating beat with song structure...</span>
|
||||
<span>{Math.round(generationProgress)}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-lofi-orange via-lofi-pink to-lofi-purple transition-all duration-100 ease-out"
|
||||
style={{ width: `${generationProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/60 text-center">
|
||||
Creating intro, verse, bridge, chorus, and outro...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Section Indicator */}
|
||||
{state.isPlaying && !isGenerating && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wider">Now Playing:</span>
|
||||
<span className="px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-lofi-orange/20 via-lofi-pink/20 to-lofi-purple/20 border border-lofi-pink/30 text-lofi-pink capitalize">
|
||||
{state.currentSection}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-h-screen flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md bg-card/80 backdrop-blur-sm border-border/50">
|
||||
<CardHeader className="text-center pb-2">
|
||||
<CardTitle className="text-2xl font-light tracking-wide">
|
||||
lofi generator
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
beats to relax/study to
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Visualizer */}
|
||||
<Visualizer currentStep={currentStep} isPlaying={state.isPlaying} />
|
||||
|
||||
{/* Master Controls */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* Transport Controls */}
|
||||
<div className="flex justify-center">
|
||||
<TransportControls
|
||||
isPlaying={state.isPlaying}
|
||||
isInitialized={state.isInitialized}
|
||||
onTogglePlayback={togglePlayback}
|
||||
onGenerateNewBeat={generateNewBeat}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Master Volume */}
|
||||
<div className="pt-2">
|
||||
<VolumeControl
|
||||
label="Master Volume"
|
||||
value={state.volumes.master}
|
||||
onChange={setMasterVolume}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* BPM and Swing Controls */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">Master</Label>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{Math.round(state.volumes.master * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[state.volumes.master]}
|
||||
onValueChange={([v]) => setMasterVolume(v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">BPM</Label>
|
||||
<Label className="text-sm text-muted-foreground">BPM</Label>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{state.bpm}
|
||||
</span>
|
||||
@ -243,13 +70,13 @@ export function LofiGenerator() {
|
||||
value={[state.bpm]}
|
||||
onValueChange={([v]) => setBpm(v)}
|
||||
min={60}
|
||||
max={180}
|
||||
max={100}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">Swing</Label>
|
||||
<Label className="text-sm text-muted-foreground">Swing</Label>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{Math.round(state.swing * 100)}%
|
||||
</span>
|
||||
@ -264,96 +91,17 @@ export function LofiGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instrument Layers */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Instruments
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<LayerBox
|
||||
title="Drums"
|
||||
icon={<Drum className="h-4 w-4" />}
|
||||
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="grey"
|
||||
/>
|
||||
|
||||
<LayerBox
|
||||
title="Bass"
|
||||
icon={<Guitar className="h-4 w-4" />}
|
||||
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="red"
|
||||
/>
|
||||
|
||||
<LayerBox
|
||||
title="Piano"
|
||||
icon={<Piano className="h-4 w-4" />}
|
||||
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"
|
||||
/>
|
||||
|
||||
<LayerBox
|
||||
title="Brass"
|
||||
icon={<TrumpetIcon className="h-4 w-4" />}
|
||||
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="babyblue"
|
||||
/>
|
||||
|
||||
<LayerBox
|
||||
title="Chords"
|
||||
icon={<Waves className="h-4 w-4" />}
|
||||
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"
|
||||
/>
|
||||
|
||||
<LayerBox
|
||||
title="Ambient"
|
||||
icon={<Cloud className="h-4 w-4" />}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Layer Mixer */}
|
||||
<LayerMixer
|
||||
volumes={state.volumes}
|
||||
muted={state.muted}
|
||||
onVolumeChange={setLayerVolume}
|
||||
onToggleMute={toggleMute}
|
||||
/>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-xs text-muted-foreground/60 pt-2">
|
||||
Click play to initialize audio engine • Genre: {GENRE_CONFIG[state.genre].name}
|
||||
<p className="text-center text-xs text-muted-foreground/60 pt-4">
|
||||
Click play to start the audio engine
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
45
components/lofi-generator/TransportControls.tsx
Normal file
45
components/lofi-generator/TransportControls.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
'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 (
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={onTogglePlayback}
|
||||
className="h-14 w-14 rounded-full"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause className="h-6 w-6" />
|
||||
) : (
|
||||
<Play className="h-6 w-6 ml-1" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
onClick={onGenerateNewBeat}
|
||||
disabled={!isInitialized && !isPlaying}
|
||||
className="gap-2"
|
||||
>
|
||||
<Shuffle className="h-4 w-4" />
|
||||
New Beat
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -7,31 +7,25 @@ interface VisualizerProps {
|
||||
|
||||
export function Visualizer({ currentStep, isPlaying }: VisualizerProps) {
|
||||
return (
|
||||
<div className="flex items-end justify-center gap-1 h-16 px-4 py-3 bg-secondary/50 rounded-lg border border-border/50">
|
||||
<div className="flex items-center justify-center gap-1.5 py-4">
|
||||
{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 (
|
||||
<div
|
||||
key={i}
|
||||
className={`
|
||||
transition-all duration-50 ease-out
|
||||
${isBeat ? 'w-4' : 'w-2.5'}
|
||||
${isActive ? activeHeight : baseHeight}
|
||||
transition-all duration-75
|
||||
${isBeat ? 'w-3 h-8' : 'w-2 h-6'}
|
||||
rounded-full
|
||||
${
|
||||
isActive
|
||||
? 'bg-gradient-to-t from-lofi-orange via-lofi-pink to-lofi-purple shadow-[0_0_12px_rgba(255,150,100,0.6)]'
|
||||
? 'bg-primary scale-110 shadow-lg shadow-primary/50'
|
||||
: isBeat
|
||||
? 'bg-gradient-to-t from-lofi-orange/60 to-lofi-orange/30'
|
||||
: 'bg-muted-foreground/30'
|
||||
? 'bg-muted-foreground/40'
|
||||
: 'bg-muted-foreground/20'
|
||||
}
|
||||
${isBeat ? 'rounded-sm' : 'rounded-[2px]'}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
37
components/lofi-generator/VolumeControl.tsx
Normal file
37
components/lofi-generator/VolumeControl.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
'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 (
|
||||
<div className={`space-y-2 ${className}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm text-muted-foreground">{label}</Label>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{Math.round(value * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[value]}
|
||||
onValueChange={([v]) => onChange(v)}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,190 +0,0 @@
|
||||
"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<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@ -1,39 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { EngineState, LayerName, Genre } from '@/types/audio';
|
||||
import { EngineState, LayerName } from '@/types/audio';
|
||||
|
||||
const defaultState: EngineState = {
|
||||
isPlaying: false,
|
||||
isInitialized: false,
|
||||
bpm: 90,
|
||||
swing: 0.15,
|
||||
bpm: 78,
|
||||
swing: 0.12,
|
||||
currentStep: 0,
|
||||
currentSection: 'intro',
|
||||
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,
|
||||
},
|
||||
@ -45,6 +28,7 @@ export function useAudioEngine() {
|
||||
const engineRef = useRef<typeof import('@/lib/audio/audioEngine').default | null>(null);
|
||||
const isInitializingRef = useRef(false);
|
||||
|
||||
// Dynamically import the audio engine (client-side only)
|
||||
const getEngine = useCallback(async () => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
@ -55,6 +39,7 @@ export function useAudioEngine() {
|
||||
return engineRef.current;
|
||||
}, []);
|
||||
|
||||
// Initialize engine and set up callbacks
|
||||
const initialize = useCallback(async () => {
|
||||
if (isInitializingRef.current) return;
|
||||
isInitializingRef.current = true;
|
||||
@ -79,6 +64,7 @@ export function useAudioEngine() {
|
||||
}
|
||||
}, [getEngine]);
|
||||
|
||||
// Play/pause toggle
|
||||
const togglePlayback = useCallback(async () => {
|
||||
const engine = await getEngine();
|
||||
if (!engine) return;
|
||||
@ -95,6 +81,7 @@ export function useAudioEngine() {
|
||||
}
|
||||
}, [getEngine, state.isInitialized, initialize]);
|
||||
|
||||
// Stop playback
|
||||
const stop = useCallback(async () => {
|
||||
const engine = await getEngine();
|
||||
if (!engine) return;
|
||||
@ -102,6 +89,7 @@ export function useAudioEngine() {
|
||||
setCurrentStep(0);
|
||||
}, [getEngine]);
|
||||
|
||||
// Generate new beat
|
||||
const generateNewBeat = useCallback(async () => {
|
||||
const engine = await getEngine();
|
||||
if (!engine) return;
|
||||
@ -113,67 +101,46 @@ export function useAudioEngine() {
|
||||
engine.generateNewBeat();
|
||||
}, [getEngine, state.isInitialized, initialize]);
|
||||
|
||||
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]);
|
||||
|
||||
// Set BPM
|
||||
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
|
||||
// Don't dispose on unmount to allow seamless navigation
|
||||
// The engine is a singleton that persists
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -184,9 +151,6 @@ export function useAudioEngine() {
|
||||
togglePlayback,
|
||||
stop,
|
||||
generateNewBeat,
|
||||
setGenre,
|
||||
setDuration,
|
||||
setInstrument,
|
||||
setBpm,
|
||||
setSwing,
|
||||
setMasterVolume,
|
||||
|
||||
@ -1,139 +1,97 @@
|
||||
import * as Tone from 'tone';
|
||||
import { AmbientType } from '@/types/audio';
|
||||
|
||||
export class AmbientLayer {
|
||||
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 rainNoise: Tone.Noise;
|
||||
private vinylNoise: Tone.Noise;
|
||||
private rainFilter: Tone.Filter;
|
||||
private vinylFilter: Tone.Filter;
|
||||
private rainGain: Tone.Gain;
|
||||
private vinylGain: Tone.Gain;
|
||||
private output: Tone.Gain;
|
||||
private lfo: Tone.LFO | null = null;
|
||||
private currentType: AmbientType = 'rain';
|
||||
private isPlaying = false;
|
||||
private currentVolume: number = 0.4;
|
||||
private isMuted: boolean = false;
|
||||
private lfo: Tone.LFO;
|
||||
|
||||
constructor(destination: Tone.InputNode) {
|
||||
this.output = new Tone.Gain(this.currentVolume);
|
||||
this.output.connect(destination);
|
||||
this.createAmbient('rain');
|
||||
}
|
||||
this.output = new Tone.Gain(0.4);
|
||||
|
||||
private createAmbient(type: AmbientType): void {
|
||||
this.noise1?.dispose();
|
||||
this.noise2?.dispose();
|
||||
this.filter1?.dispose();
|
||||
this.filter2?.dispose();
|
||||
this.gain1?.dispose();
|
||||
this.gain2?.dispose();
|
||||
this.lfo?.dispose();
|
||||
|
||||
const configs: Record<AmbientType, {
|
||||
noise1: { type: 'white' | 'pink' | 'brown'; filter: number; filterType: BiquadFilterType; gain: number };
|
||||
noise2: { type: 'white' | 'pink' | 'brown'; filter: number; filterType: BiquadFilterType; gain: number };
|
||||
lfoFreq: number;
|
||||
}> = {
|
||||
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];
|
||||
|
||||
this.noise1 = new Tone.Noise(config.noise1.type);
|
||||
this.filter1 = new Tone.Filter({
|
||||
frequency: config.noise1.filter,
|
||||
type: config.noise1.filterType,
|
||||
// Rain sound - filtered pink noise
|
||||
this.rainNoise = new Tone.Noise('pink');
|
||||
this.rainFilter = new Tone.Filter({
|
||||
frequency: 3000,
|
||||
type: 'lowpass',
|
||||
rolloff: -24,
|
||||
});
|
||||
this.gain1 = new Tone.Gain(config.noise1.gain);
|
||||
this.noise1.connect(this.filter1);
|
||||
this.filter1.connect(this.gain1);
|
||||
this.gain1.connect(this.output);
|
||||
this.rainGain = new Tone.Gain(0.15);
|
||||
|
||||
this.noise2 = new Tone.Noise(config.noise2.type);
|
||||
this.filter2 = new Tone.Filter({
|
||||
frequency: config.noise2.filter,
|
||||
type: config.noise2.filterType,
|
||||
// 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.gain2 = new Tone.Gain(config.noise2.gain);
|
||||
this.noise2.connect(this.filter2);
|
||||
this.filter2.connect(this.gain2);
|
||||
this.gain2.connect(this.output);
|
||||
this.vinylGain = new Tone.Gain(0.1);
|
||||
|
||||
// LFO for subtle rain intensity variation
|
||||
this.lfo = new Tone.LFO({
|
||||
frequency: config.lfoFreq,
|
||||
min: config.noise1.gain * 0.7,
|
||||
max: config.noise1.gain * 1.2,
|
||||
frequency: 0.1,
|
||||
min: 0.1,
|
||||
max: 0.2,
|
||||
});
|
||||
this.lfo.connect(this.gain1.gain);
|
||||
this.lfo.connect(this.rainGain.gain);
|
||||
|
||||
this.currentType = type;
|
||||
// Chain rain: noise -> filter -> gain -> output
|
||||
this.rainNoise.connect(this.rainFilter);
|
||||
this.rainFilter.connect(this.rainGain);
|
||||
this.rainGain.connect(this.output);
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.noise1.start();
|
||||
this.noise2.start();
|
||||
this.lfo.start();
|
||||
}
|
||||
}
|
||||
// Chain vinyl: noise -> filter -> gain -> output
|
||||
this.vinylNoise.connect(this.vinylFilter);
|
||||
this.vinylFilter.connect(this.vinylGain);
|
||||
this.vinylGain.connect(this.output);
|
||||
|
||||
setInstrument(type: AmbientType): void {
|
||||
this.createAmbient(type);
|
||||
// Output to destination
|
||||
this.output.connect(destination);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.noise1?.start();
|
||||
this.noise2?.start();
|
||||
this.lfo?.start();
|
||||
this.isPlaying = true;
|
||||
this.rainNoise.start();
|
||||
this.vinylNoise.start();
|
||||
this.lfo.start();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.noise1?.stop();
|
||||
this.noise2?.stop();
|
||||
this.lfo?.stop();
|
||||
this.isPlaying = false;
|
||||
this.rainNoise.stop();
|
||||
this.vinylNoise.stop();
|
||||
this.lfo.stop();
|
||||
}
|
||||
|
||||
setVolume(volume: number): void {
|
||||
this.currentVolume = volume;
|
||||
if (!this.isMuted) {
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
|
||||
mute(muted: boolean): void {
|
||||
this.isMuted = muted;
|
||||
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
|
||||
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.stop();
|
||||
this.noise1?.dispose();
|
||||
this.noise2?.dispose();
|
||||
this.filter1?.dispose();
|
||||
this.filter2?.dispose();
|
||||
this.gain1?.dispose();
|
||||
this.gain2?.dispose();
|
||||
this.lfo?.dispose();
|
||||
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.output.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,24 +2,7 @@ import * as Tone from 'tone';
|
||||
import { DrumMachine } from './drumMachine';
|
||||
import { ChordEngine } from './chordEngine';
|
||||
import { AmbientLayer } from './ambientLayer';
|
||||
import { BassEngine } from './bassEngine';
|
||||
import { BrassEngine } from './brassEngine';
|
||||
import { PianoEngine } from './pianoEngine';
|
||||
import { generateSongStructure, calculateSectionTimings, SongStructure } from './songStructure';
|
||||
import {
|
||||
EngineState,
|
||||
AudioEngineCallbacks,
|
||||
LayerName,
|
||||
Genre,
|
||||
GENRE_CONFIG,
|
||||
DrumKit,
|
||||
BassType,
|
||||
BrassType,
|
||||
PianoType,
|
||||
ChordType,
|
||||
AmbientType,
|
||||
SectionType,
|
||||
} from '@/types/audio';
|
||||
import { EngineState, AudioEngineCallbacks, LayerName } from '@/types/audio';
|
||||
|
||||
class AudioEngine {
|
||||
private static instance: AudioEngine | null = null;
|
||||
@ -27,52 +10,28 @@ 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;
|
||||
private masterLimiter: Tone.Limiter | null = null;
|
||||
private masterReverb: Tone.Reverb | null = null;
|
||||
|
||||
private songStructure: SongStructure | null = null;
|
||||
private scheduledEvents: number[] = [];
|
||||
private currentSection: SectionType = 'intro';
|
||||
|
||||
private callbacks: AudioEngineCallbacks = {};
|
||||
|
||||
private state: EngineState = {
|
||||
isPlaying: false,
|
||||
isInitialized: false,
|
||||
bpm: 90,
|
||||
swing: 0.15,
|
||||
bpm: 78,
|
||||
swing: 0.12,
|
||||
currentStep: 0,
|
||||
currentSection: 'intro',
|
||||
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,
|
||||
},
|
||||
@ -90,11 +49,12 @@ class AudioEngine {
|
||||
async initialize(): Promise<void> {
|
||||
if (this.state.isInitialized) return;
|
||||
|
||||
// Start Tone.js audio context (requires user gesture)
|
||||
await Tone.start();
|
||||
|
||||
const genreConfig = GENRE_CONFIG[this.state.genre];
|
||||
Tone.getTransport().bpm.value = genreConfig.bpm;
|
||||
Tone.getTransport().swing = genreConfig.swing;
|
||||
// Set up transport
|
||||
Tone.getTransport().bpm.value = this.state.bpm;
|
||||
Tone.getTransport().swing = this.state.swing;
|
||||
Tone.getTransport().swingSubdivision = '16n';
|
||||
|
||||
// Master chain
|
||||
@ -111,29 +71,16 @@ 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 all layers
|
||||
// Initialize 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) => {
|
||||
@ -141,12 +88,7 @@ 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();
|
||||
}
|
||||
@ -180,169 +122,19 @@ class AudioEngine {
|
||||
stop(): void {
|
||||
Tone.getTransport().stop();
|
||||
this.ambientLayer?.stop();
|
||||
this.clearScheduledEvents();
|
||||
this.state.isPlaying = false;
|
||||
this.state.currentStep = 0;
|
||||
this.state.currentSection = 'intro';
|
||||
this.notifyStateChange();
|
||||
}
|
||||
|
||||
generateNewBeat(): void {
|
||||
// Clear any previously scheduled events
|
||||
this.clearScheduledEvents();
|
||||
|
||||
// Randomize patterns for all instruments
|
||||
this.drumMachine?.randomize();
|
||||
this.chordEngine?.randomize();
|
||||
this.bassEngine?.randomize();
|
||||
this.brassEngine?.randomize();
|
||||
this.pianoEngine?.randomize();
|
||||
|
||||
// Generate song structure based on current settings
|
||||
this.songStructure = generateSongStructure(this.state.duration, this.state.bpm, this.state.genre);
|
||||
|
||||
// Calculate section timings and schedule changes
|
||||
const timings = calculateSectionTimings(this.songStructure, this.state.bpm);
|
||||
|
||||
timings.forEach(({ startTime, section }) => {
|
||||
const eventId = Tone.getTransport().schedule((time) => {
|
||||
Tone.getDraw().schedule(() => {
|
||||
this.applySection(section.type, section.instruments);
|
||||
}, time);
|
||||
}, startTime);
|
||||
this.scheduledEvents.push(eventId);
|
||||
});
|
||||
|
||||
// Set initial section
|
||||
if (timings.length > 0) {
|
||||
this.applySection(timings[0].section.type, timings[0].section.instruments);
|
||||
}
|
||||
|
||||
this.notifyStateChange();
|
||||
}
|
||||
|
||||
private clearScheduledEvents(): void {
|
||||
this.scheduledEvents.forEach(eventId => {
|
||||
Tone.getTransport().clear(eventId);
|
||||
});
|
||||
this.scheduledEvents = [];
|
||||
}
|
||||
|
||||
private applySection(sectionType: SectionType, instruments: {
|
||||
drums: { active: boolean; volume: number };
|
||||
bass: { active: boolean; volume: number };
|
||||
brass: { active: boolean; volume: number };
|
||||
piano: { active: boolean; volume: number };
|
||||
chords: { active: boolean; volume: number };
|
||||
ambient: { active: boolean; volume: number };
|
||||
}): void {
|
||||
this.currentSection = sectionType;
|
||||
this.state.currentSection = sectionType;
|
||||
|
||||
// Apply instrument volumes and mute states for this section
|
||||
// Use smooth transitions for a professional sound
|
||||
const layers: LayerName[] = ['drums', 'bass', 'brass', 'piano', 'chords', 'ambient'];
|
||||
|
||||
layers.forEach(layer => {
|
||||
const config = instruments[layer];
|
||||
|
||||
// Set mute state
|
||||
this.state.muted[layer] = !config.active;
|
||||
|
||||
// Set volume (scaled by user's master settings)
|
||||
const userMasterScale = this.state.volumes.master;
|
||||
const targetVolume = config.active ? config.volume * userMasterScale : 0;
|
||||
|
||||
switch (layer) {
|
||||
case 'drums':
|
||||
this.drumMachine?.mute(!config.active);
|
||||
if (config.active) this.drumMachine?.setVolume(targetVolume);
|
||||
break;
|
||||
case 'bass':
|
||||
this.bassEngine?.mute(!config.active);
|
||||
if (config.active) this.bassEngine?.setVolume(targetVolume);
|
||||
break;
|
||||
case 'brass':
|
||||
this.brassEngine?.mute(!config.active);
|
||||
if (config.active) this.brassEngine?.setVolume(targetVolume);
|
||||
break;
|
||||
case 'piano':
|
||||
this.pianoEngine?.mute(!config.active);
|
||||
if (config.active) this.pianoEngine?.setVolume(targetVolume);
|
||||
break;
|
||||
case 'chords':
|
||||
this.chordEngine?.mute(!config.active);
|
||||
if (config.active) this.chordEngine?.setVolume(targetVolume);
|
||||
break;
|
||||
case 'ambient':
|
||||
this.ambientLayer?.mute(!config.active);
|
||||
if (config.active) this.ambientLayer?.setVolume(targetVolume);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Notify callbacks
|
||||
this.callbacks.onSectionChange?.(sectionType);
|
||||
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(180, bpm));
|
||||
this.state.bpm = Math.max(60, Math.min(100, bpm));
|
||||
Tone.getTransport().bpm.value = this.state.bpm;
|
||||
this.notifyStateChange();
|
||||
}
|
||||
@ -367,15 +159,6 @@ 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;
|
||||
@ -394,15 +177,6 @@ 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;
|
||||
@ -421,15 +195,6 @@ 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;
|
||||
@ -447,13 +212,9 @@ class AudioEngine {
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this.clearScheduledEvents();
|
||||
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();
|
||||
@ -462,18 +223,13 @@ 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;
|
||||
this.masterReverb = null;
|
||||
this.songStructure = null;
|
||||
|
||||
this.state.isInitialized = false;
|
||||
this.state.isPlaying = false;
|
||||
this.state.currentSection = 'intro';
|
||||
AudioEngine.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
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';
|
||||
private currentVolume: number = 0.6;
|
||||
private isMuted: boolean = false;
|
||||
|
||||
constructor(destination: Tone.InputNode) {
|
||||
this.output = new Tone.Gain(this.currentVolume);
|
||||
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<BassType, { oscillator: { type: string }; envelope: object; filterEnvelope: object }> = {
|
||||
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 },
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.synth = new Tone.MonoSynth(configs[type] as any);
|
||||
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.currentVolume = volume;
|
||||
if (!this.isMuted) {
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
mute(muted: boolean): void {
|
||||
this.isMuted = muted;
|
||||
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.sequence?.dispose();
|
||||
this.synth?.dispose();
|
||||
this.filter.dispose();
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
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';
|
||||
private currentVolume: number = 0.4;
|
||||
private isMuted: boolean = false;
|
||||
|
||||
constructor(destination: Tone.InputNode) {
|
||||
this.output = new Tone.Gain(this.currentVolume);
|
||||
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<BrassType, { oscillator: object; envelope: object }> = {
|
||||
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 },
|
||||
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 },
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.synth = new Tone.Synth(configs[type] as any);
|
||||
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.currentVolume = volume;
|
||||
if (!this.isMuted) {
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
mute(muted: boolean): void {
|
||||
this.isMuted = muted;
|
||||
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.sequence?.dispose();
|
||||
this.synth?.dispose();
|
||||
this.filter.dispose();
|
||||
this.reverb.dispose();
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,34 +1,33 @@
|
||||
import * as Tone from 'tone';
|
||||
import { ChordProgression, ChordType, Genre } from '@/types/audio';
|
||||
import { ChordProgression } from '@/types/audio';
|
||||
import { getRandomProgression } from './patterns';
|
||||
|
||||
export class ChordEngine {
|
||||
private synth: Tone.PolySynth | null = null;
|
||||
private synth: Tone.PolySynth;
|
||||
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';
|
||||
private currentVolume: number = 0.6;
|
||||
private isMuted: boolean = false;
|
||||
|
||||
constructor(destination: Tone.InputNode) {
|
||||
this.output = new Tone.Gain(this.currentVolume);
|
||||
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,
|
||||
@ -36,73 +35,42 @@ 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);
|
||||
|
||||
this.progression = getRandomProgression(this.genre);
|
||||
this.createSynth('pad');
|
||||
}
|
||||
|
||||
private createSynth(type: ChordType): void {
|
||||
if (this.synth) {
|
||||
this.synth.dispose();
|
||||
}
|
||||
|
||||
const configs: Record<ChordType, { synth: typeof Tone.Synth | typeof Tone.FMSynth; options: object }> = {
|
||||
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();
|
||||
}
|
||||
// Initialize with a random progression
|
||||
this.progression = getRandomProgression();
|
||||
}
|
||||
|
||||
createSequence(): void {
|
||||
@ -117,10 +85,9 @@ export class ChordEngine {
|
||||
const chord = this.progression.chords[step];
|
||||
const duration = this.progression.durations[step];
|
||||
|
||||
if (this.synth) {
|
||||
this.synth.releaseAll(time);
|
||||
this.synth.triggerAttackRelease(chord, duration, time, 0.5);
|
||||
}
|
||||
// Release previous notes and play new chord
|
||||
this.synth.releaseAll(time);
|
||||
this.synth.triggerAttackRelease(chord, duration, time, 0.5);
|
||||
},
|
||||
steps,
|
||||
'2n'
|
||||
@ -131,13 +98,14 @@ 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.genre);
|
||||
this.progression = getRandomProgression();
|
||||
if (this.sequence) {
|
||||
this.createSequence();
|
||||
}
|
||||
@ -145,15 +113,11 @@ export class ChordEngine {
|
||||
}
|
||||
|
||||
setVolume(volume: number): void {
|
||||
this.currentVolume = volume;
|
||||
if (!this.isMuted) {
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
|
||||
mute(muted: boolean): void {
|
||||
this.isMuted = muted;
|
||||
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
|
||||
this.output.gain.rampTo(muted ? 0 : 0.6, 0.1);
|
||||
}
|
||||
|
||||
setFilterFrequency(freq: number): void {
|
||||
@ -166,7 +130,7 @@ export class ChordEngine {
|
||||
|
||||
dispose(): void {
|
||||
this.sequence?.dispose();
|
||||
this.synth?.dispose();
|
||||
this.synth.dispose();
|
||||
this.filter.dispose();
|
||||
this.reverb.dispose();
|
||||
this.chorus.dispose();
|
||||
|
||||
@ -1,138 +1,99 @@
|
||||
import * as Tone from 'tone';
|
||||
import { DrumPattern, DrumKit, Genre } from '@/types/audio';
|
||||
import { getRandomPattern } from './patterns';
|
||||
import { DrumPattern } from '@/types/audio';
|
||||
import { getRandomPattern, generateRandomPattern } from './patterns';
|
||||
|
||||
export class DrumMachine {
|
||||
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 kick: Tone.MembraneSynth;
|
||||
private snare: Tone.NoiseSynth;
|
||||
private hihat: Tone.NoiseSynth;
|
||||
private openhat: Tone.NoiseSynth;
|
||||
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';
|
||||
private currentVolume: number = 0.8;
|
||||
private isMuted: boolean = false;
|
||||
|
||||
constructor(destination: Tone.InputNode) {
|
||||
this.output = new Tone.Gain(this.currentVolume);
|
||||
this.output = new Tone.Gain(0.8);
|
||||
this.lowpass = new Tone.Filter({
|
||||
frequency: 8000,
|
||||
type: 'lowpass',
|
||||
rolloff: -12,
|
||||
});
|
||||
|
||||
this.lowpass.connect(this.output);
|
||||
this.output.connect(destination);
|
||||
|
||||
this.pattern = getRandomPattern(this.genre);
|
||||
this.createKit('acoustic');
|
||||
}
|
||||
|
||||
private createKit(kit: DrumKit): void {
|
||||
this.kick?.dispose();
|
||||
this.snare?.dispose();
|
||||
this.hihat?.dispose();
|
||||
this.openhat?.dispose();
|
||||
this.snareFilter?.dispose();
|
||||
this.hihatFilter?.dispose();
|
||||
this.openhatFilter?.dispose();
|
||||
|
||||
const kitConfigs: Record<DrumKit, {
|
||||
kick: object;
|
||||
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];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// Kick drum - deep and punchy
|
||||
this.kick = new Tone.MembraneSynth({
|
||||
...(config.kick as any),
|
||||
pitchDecay: 0.05,
|
||||
octaves: 6,
|
||||
oscillator: { type: 'sine' },
|
||||
envelope: {
|
||||
attack: 0.001,
|
||||
decay: 0.4,
|
||||
sustain: 0.01,
|
||||
release: 0.4,
|
||||
},
|
||||
});
|
||||
this.kick.connect(this.lowpass);
|
||||
|
||||
this.snareFilter = new Tone.Filter({
|
||||
frequency: config.snareFilter,
|
||||
// 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 = 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);
|
||||
this.snare.connect(snareFilter);
|
||||
snareFilter.connect(this.lowpass);
|
||||
|
||||
this.hihatFilter = new Tone.Filter({
|
||||
frequency: config.hihatFilter,
|
||||
type: 'highpass',
|
||||
});
|
||||
// Closed hi-hat - high filtered noise
|
||||
this.hihat = new Tone.NoiseSynth({
|
||||
noise: { type: 'white' },
|
||||
envelope: { attack: 0.001, decay: config.hihatDecay, sustain: 0, release: 0.02 },
|
||||
envelope: {
|
||||
attack: 0.001,
|
||||
decay: 0.05,
|
||||
sustain: 0,
|
||||
release: 0.02,
|
||||
},
|
||||
});
|
||||
this.hihat.connect(this.hihatFilter);
|
||||
this.hihatFilter.connect(this.lowpass);
|
||||
|
||||
this.openhatFilter = new Tone.Filter({
|
||||
frequency: config.hihatFilter - 2000,
|
||||
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 },
|
||||
envelope: {
|
||||
attack: 0.001,
|
||||
decay: 0.3,
|
||||
sustain: 0,
|
||||
release: 0.15,
|
||||
},
|
||||
});
|
||||
this.openhat.connect(this.openhatFilter);
|
||||
this.openhatFilter.connect(this.lowpass);
|
||||
const openhatFilter = new Tone.Filter({
|
||||
frequency: 8000,
|
||||
type: 'highpass',
|
||||
});
|
||||
this.openhat.connect(openhatFilter);
|
||||
openhatFilter.connect(this.lowpass);
|
||||
|
||||
this.currentKit = kit;
|
||||
}
|
||||
// Connect kick directly to lowpass
|
||||
this.kick.connect(this.lowpass);
|
||||
|
||||
setInstrument(kit: DrumKit): void {
|
||||
this.createKit(kit);
|
||||
}
|
||||
// Chain: lowpass -> output -> destination
|
||||
this.lowpass.connect(this.output);
|
||||
this.output.connect(destination);
|
||||
|
||||
setGenre(genre: Genre): void {
|
||||
this.genre = genre;
|
||||
this.pattern = getRandomPattern(genre);
|
||||
// Initialize with a random pattern
|
||||
this.pattern = getRandomPattern();
|
||||
}
|
||||
|
||||
createSequence(onStep?: (step: number) => void): void {
|
||||
@ -144,19 +105,20 @@ export class DrumMachine {
|
||||
|
||||
this.sequence = new Tone.Sequence(
|
||||
(time, step) => {
|
||||
if (this.pattern.kick[step] && this.kick) {
|
||||
if (this.pattern.kick[step]) {
|
||||
this.kick.triggerAttackRelease('C1', '8n', time, 0.8);
|
||||
}
|
||||
if (this.pattern.snare[step] && this.snare) {
|
||||
if (this.pattern.snare[step]) {
|
||||
this.snare.triggerAttackRelease('8n', time, 0.5);
|
||||
}
|
||||
if (this.pattern.hihat[step] && this.hihat) {
|
||||
if (this.pattern.hihat[step]) {
|
||||
this.hihat.triggerAttackRelease('32n', time, 0.3);
|
||||
}
|
||||
if (this.pattern.openhat[step] && this.openhat) {
|
||||
if (this.pattern.openhat[step]) {
|
||||
this.openhat.triggerAttackRelease('16n', time, 0.25);
|
||||
}
|
||||
|
||||
// Call step callback on main thread
|
||||
if (onStep) {
|
||||
Tone.getDraw().schedule(() => {
|
||||
onStep(step);
|
||||
@ -175,20 +137,16 @@ export class DrumMachine {
|
||||
}
|
||||
|
||||
randomize(): DrumPattern {
|
||||
this.pattern = getRandomPattern(this.genre);
|
||||
this.pattern = Math.random() > 0.5 ? getRandomPattern() : generateRandomPattern();
|
||||
return this.pattern;
|
||||
}
|
||||
|
||||
setVolume(volume: number): void {
|
||||
this.currentVolume = volume;
|
||||
if (!this.isMuted) {
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
|
||||
mute(muted: boolean): void {
|
||||
this.isMuted = muted;
|
||||
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
|
||||
this.output.gain.rampTo(muted ? 0 : 0.8, 0.1);
|
||||
}
|
||||
|
||||
getPattern(): DrumPattern {
|
||||
@ -197,13 +155,10 @@ export class DrumMachine {
|
||||
|
||||
dispose(): void {
|
||||
this.sequence?.dispose();
|
||||
this.kick?.dispose();
|
||||
this.snare?.dispose();
|
||||
this.hihat?.dispose();
|
||||
this.openhat?.dispose();
|
||||
this.snareFilter?.dispose();
|
||||
this.hihatFilter?.dispose();
|
||||
this.openhatFilter?.dispose();
|
||||
this.kick.dispose();
|
||||
this.snare.dispose();
|
||||
this.hihat.dispose();
|
||||
this.openhat.dispose();
|
||||
this.lowpass.dispose();
|
||||
this.output.dispose();
|
||||
}
|
||||
|
||||
@ -1,238 +1,145 @@
|
||||
import { DrumPattern, ChordProgression, Genre } from '@/types/audio';
|
||||
import { DrumPattern, ChordProgression } from '@/types/audio';
|
||||
|
||||
// Genre-specific drum patterns
|
||||
export const drumPatterns: Record<Genre, DrumPattern[]> = {
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
// 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 chord progressions
|
||||
export const chordProgressions: Record<Genre, ChordProgression[]> = {
|
||||
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'],
|
||||
},
|
||||
],
|
||||
};
|
||||
// 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'],
|
||||
},
|
||||
];
|
||||
|
||||
// Bass patterns by genre
|
||||
export const bassPatterns: Record<Genre, (string | null)[][]> = {
|
||||
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<Genre, (string | null)[][]> = {
|
||||
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<Genre, (string[] | null)[][]> = {
|
||||
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 getRandomPattern(): DrumPattern {
|
||||
return drumPatterns[Math.floor(Math.random() * drumPatterns.length)];
|
||||
}
|
||||
|
||||
export function getRandomProgression(genre: Genre): ChordProgression {
|
||||
const progressions = chordProgressions[genre];
|
||||
return progressions[Math.floor(Math.random() * progressions.length)];
|
||||
export function getRandomProgression(): ChordProgression {
|
||||
return chordProgressions[Math.floor(Math.random() * chordProgressions.length)];
|
||||
}
|
||||
|
||||
export function getRandomBassPattern(genre: Genre): (string | null)[] {
|
||||
const patterns = bassPatterns[genre];
|
||||
return patterns[Math.floor(Math.random() * patterns.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),
|
||||
};
|
||||
|
||||
export function getRandomBrassPattern(genre: Genre): (string | null)[] {
|
||||
const patterns = brassPatterns[genre];
|
||||
return patterns[Math.floor(Math.random() * patterns.length)];
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
export function getRandomPianoPattern(genre: Genre): (string[] | null)[] {
|
||||
const patterns = pianoPatterns[genre];
|
||||
return patterns[Math.floor(Math.random() * patterns.length)];
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -1,144 +0,0 @@
|
||||
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';
|
||||
private currentVolume: number = 0.5;
|
||||
private isMuted: boolean = false;
|
||||
|
||||
constructor(destination: Tone.InputNode) {
|
||||
this.output = new Tone.Gain(this.currentVolume);
|
||||
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<PianoType, { synth: typeof Tone.Synth | typeof Tone.FMSynth; options: object }> = {
|
||||
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 as any);
|
||||
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.currentVolume = volume;
|
||||
if (!this.isMuted) {
|
||||
this.output.gain.rampTo(volume, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
mute(muted: boolean): void {
|
||||
this.isMuted = muted;
|
||||
this.output.gain.rampTo(muted ? 0 : this.currentVolume, 0.1);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.sequence?.dispose();
|
||||
this.synth?.dispose();
|
||||
this.filter.dispose();
|
||||
this.reverb.dispose();
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
import { Genre, SectionType } from '@/types/audio';
|
||||
|
||||
export interface SongSection {
|
||||
type: SectionType;
|
||||
bars: number;
|
||||
instruments: {
|
||||
drums: { active: boolean; volume: number };
|
||||
bass: { active: boolean; volume: number };
|
||||
brass: { active: boolean; volume: number };
|
||||
piano: { active: boolean; volume: number };
|
||||
chords: { active: boolean; volume: number };
|
||||
ambient: { active: boolean; volume: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface SongStructure {
|
||||
sections: SongSection[];
|
||||
totalBars: number;
|
||||
}
|
||||
|
||||
// Genre-specific section configurations
|
||||
const sectionConfigs: Record<SectionType, {
|
||||
instruments: SongSection['instruments'];
|
||||
}> = {
|
||||
intro: {
|
||||
instruments: {
|
||||
drums: { active: false, volume: 0 },
|
||||
bass: { active: false, volume: 0 },
|
||||
brass: { active: false, volume: 0 },
|
||||
piano: { active: true, volume: 0.3 },
|
||||
chords: { active: true, volume: 0.4 },
|
||||
ambient: { active: true, volume: 0.6 },
|
||||
},
|
||||
},
|
||||
verse: {
|
||||
instruments: {
|
||||
drums: { active: true, volume: 0.6 },
|
||||
bass: { active: true, volume: 0.5 },
|
||||
brass: { active: false, volume: 0 },
|
||||
piano: { active: true, volume: 0.4 },
|
||||
chords: { active: true, volume: 0.5 },
|
||||
ambient: { active: true, volume: 0.4 },
|
||||
},
|
||||
},
|
||||
bridge: {
|
||||
instruments: {
|
||||
drums: { active: true, volume: 0.4 },
|
||||
bass: { active: true, volume: 0.4 },
|
||||
brass: { active: true, volume: 0.3 },
|
||||
piano: { active: false, volume: 0 },
|
||||
chords: { active: true, volume: 0.6 },
|
||||
ambient: { active: true, volume: 0.5 },
|
||||
},
|
||||
},
|
||||
chorus: {
|
||||
instruments: {
|
||||
drums: { active: true, volume: 0.8 },
|
||||
bass: { active: true, volume: 0.7 },
|
||||
brass: { active: true, volume: 0.5 },
|
||||
piano: { active: true, volume: 0.5 },
|
||||
chords: { active: true, volume: 0.7 },
|
||||
ambient: { active: true, volume: 0.3 },
|
||||
},
|
||||
},
|
||||
outro: {
|
||||
instruments: {
|
||||
drums: { active: true, volume: 0.3 },
|
||||
bass: { active: true, volume: 0.3 },
|
||||
brass: { active: false, volume: 0 },
|
||||
piano: { active: true, volume: 0.4 },
|
||||
chords: { active: true, volume: 0.5 },
|
||||
ambient: { active: true, volume: 0.7 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Generate song structure based on duration (in minutes)
|
||||
export function generateSongStructure(durationMinutes: number, bpm: number, genre: Genre): SongStructure {
|
||||
// Calculate total bars based on duration and BPM
|
||||
// 4 beats per bar, so bars = (minutes * bpm) / 4
|
||||
const totalBars = Math.floor((durationMinutes * bpm) / 4);
|
||||
|
||||
// Create a standard song structure that fits within the total bars
|
||||
const sections: SongSection[] = [];
|
||||
|
||||
if (durationMinutes === 1) {
|
||||
// Short format: Intro -> Verse -> Chorus -> Outro
|
||||
const introBars = Math.floor(totalBars * 0.15);
|
||||
const verseBars = Math.floor(totalBars * 0.35);
|
||||
const chorusBars = Math.floor(totalBars * 0.35);
|
||||
const outroBars = totalBars - introBars - verseBars - chorusBars;
|
||||
|
||||
sections.push({ type: 'intro', bars: introBars, instruments: { ...sectionConfigs.intro.instruments } });
|
||||
sections.push({ type: 'verse', bars: verseBars, instruments: { ...sectionConfigs.verse.instruments } });
|
||||
sections.push({ type: 'chorus', bars: chorusBars, instruments: { ...sectionConfigs.chorus.instruments } });
|
||||
sections.push({ type: 'outro', bars: outroBars, instruments: { ...sectionConfigs.outro.instruments } });
|
||||
} else if (durationMinutes === 2) {
|
||||
// Medium format: Intro -> Verse -> Bridge -> Chorus -> Outro
|
||||
const introBars = Math.floor(totalBars * 0.1);
|
||||
const verse1Bars = Math.floor(totalBars * 0.25);
|
||||
const bridgeBars = Math.floor(totalBars * 0.15);
|
||||
const chorusBars = Math.floor(totalBars * 0.35);
|
||||
const outroBars = totalBars - introBars - verse1Bars - bridgeBars - chorusBars;
|
||||
|
||||
sections.push({ type: 'intro', bars: introBars, instruments: { ...sectionConfigs.intro.instruments } });
|
||||
sections.push({ type: 'verse', bars: verse1Bars, instruments: { ...sectionConfigs.verse.instruments } });
|
||||
sections.push({ type: 'bridge', bars: bridgeBars, instruments: { ...sectionConfigs.bridge.instruments } });
|
||||
sections.push({ type: 'chorus', bars: chorusBars, instruments: { ...sectionConfigs.chorus.instruments } });
|
||||
sections.push({ type: 'outro', bars: outroBars, instruments: { ...sectionConfigs.outro.instruments } });
|
||||
} else {
|
||||
// Full format (3+ mins): Intro -> Verse -> Chorus -> Verse -> Bridge -> Chorus -> Outro
|
||||
const introBars = Math.floor(totalBars * 0.08);
|
||||
const verse1Bars = Math.floor(totalBars * 0.18);
|
||||
const chorus1Bars = Math.floor(totalBars * 0.18);
|
||||
const verse2Bars = Math.floor(totalBars * 0.15);
|
||||
const bridgeBars = Math.floor(totalBars * 0.12);
|
||||
const chorus2Bars = Math.floor(totalBars * 0.18);
|
||||
const outroBars = totalBars - introBars - verse1Bars - chorus1Bars - verse2Bars - bridgeBars - chorus2Bars;
|
||||
|
||||
sections.push({ type: 'intro', bars: introBars, instruments: { ...sectionConfigs.intro.instruments } });
|
||||
sections.push({ type: 'verse', bars: verse1Bars, instruments: { ...sectionConfigs.verse.instruments } });
|
||||
sections.push({ type: 'chorus', bars: chorus1Bars, instruments: { ...sectionConfigs.chorus.instruments } });
|
||||
sections.push({ type: 'verse', bars: verse2Bars, instruments: { ...sectionConfigs.verse.instruments } });
|
||||
sections.push({ type: 'bridge', bars: bridgeBars, instruments: { ...sectionConfigs.bridge.instruments } });
|
||||
sections.push({ type: 'chorus', bars: chorus2Bars, instruments: { ...sectionConfigs.chorus.instruments } });
|
||||
sections.push({ type: 'outro', bars: outroBars, instruments: { ...sectionConfigs.outro.instruments } });
|
||||
}
|
||||
|
||||
// Apply genre-specific modifications
|
||||
applyGenreModifications(sections, genre);
|
||||
|
||||
return { sections, totalBars };
|
||||
}
|
||||
|
||||
function applyGenreModifications(sections: SongSection[], genre: Genre): void {
|
||||
switch (genre) {
|
||||
case 'trap':
|
||||
// Trap: heavier bass, more brass in chorus
|
||||
sections.forEach(section => {
|
||||
if (section.instruments.bass.active) {
|
||||
section.instruments.bass.volume = Math.min(1, section.instruments.bass.volume * 1.3);
|
||||
}
|
||||
if (section.type === 'chorus') {
|
||||
section.instruments.brass.volume = Math.min(1, section.instruments.brass.volume * 1.2);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'classical':
|
||||
// Classical: more piano and chords, less drums
|
||||
sections.forEach(section => {
|
||||
section.instruments.piano.active = true;
|
||||
section.instruments.piano.volume = Math.min(1, section.instruments.piano.volume * 1.3);
|
||||
section.instruments.drums.volume *= 0.7;
|
||||
section.instruments.brass.active = true;
|
||||
section.instruments.brass.volume = Math.min(1, section.instruments.brass.volume + 0.2);
|
||||
});
|
||||
break;
|
||||
case 'pop':
|
||||
// Pop: balanced, catchy, more emphasis on chorus
|
||||
sections.forEach(section => {
|
||||
if (section.type === 'chorus') {
|
||||
Object.keys(section.instruments).forEach(key => {
|
||||
const inst = section.instruments[key as keyof typeof section.instruments];
|
||||
inst.volume = Math.min(1, inst.volume * 1.1);
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'hiphop':
|
||||
default:
|
||||
// Hip hop: heavy drums and bass, soulful samples
|
||||
sections.forEach(section => {
|
||||
if (section.type === 'verse' || section.type === 'chorus') {
|
||||
section.instruments.drums.volume = Math.min(1, section.instruments.drums.volume * 1.1);
|
||||
section.instruments.bass.volume = Math.min(1, section.instruments.bass.volume * 1.1);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate timing for each section in seconds
|
||||
export function calculateSectionTimings(structure: SongStructure, bpm: number): { startTime: number; endTime: number; section: SongSection }[] {
|
||||
const secondsPerBar = (60 / bpm) * 4; // 4 beats per bar
|
||||
let currentTime = 0;
|
||||
|
||||
return structure.sections.map(section => {
|
||||
const startTime = currentTime;
|
||||
const duration = section.bars * secondsPerBar;
|
||||
currentTime += duration;
|
||||
return {
|
||||
startTime,
|
||||
endTime: currentTime,
|
||||
section,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -10,7 +10,6 @@
|
||||
},
|
||||
"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",
|
||||
|
||||
@ -1,12 +1,3 @@
|
||||
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[];
|
||||
@ -20,101 +11,28 @@ 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 type SectionType = 'intro' | 'verse' | 'bridge' | 'chorus' | 'outro';
|
||||
|
||||
export interface EngineState {
|
||||
isPlaying: boolean;
|
||||
isInitialized: boolean;
|
||||
bpm: number;
|
||||
swing: number;
|
||||
currentStep: number;
|
||||
currentSection: SectionType;
|
||||
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' | 'bass' | 'brass' | 'piano' | 'chords' | 'ambient';
|
||||
export type LayerName = 'drums' | 'chords' | 'ambient';
|
||||
|
||||
export interface AudioEngineCallbacks {
|
||||
onStepChange?: (step: number) => void;
|
||||
onStateChange?: (state: EngineState) => void;
|
||||
onSectionChange?: (section: SectionType) => void;
|
||||
}
|
||||
|
||||
export const GENRE_CONFIG: Record<Genre, { bpm: number; swing: number; name: string }> = {
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user