diff --git a/package.json b/package.json index cbfe7d1..2b5eb9a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "vite": "npm:rolldown-vite@7.1.14" }, "dependencies": { + "@csound/browser": "7.0.0-beta11", "zzfx": "^1.3.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d2d462..9b65295 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@csound/browser': + specifier: 7.0.0-beta11 + version: 7.0.0-beta11 zzfx: specifier: ^1.3.2 version: 1.3.2 @@ -36,6 +39,13 @@ importers: packages: + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@csound/browser@7.0.0-beta11': + resolution: {integrity: sha512-BGFTMXUdOJA1Xz1ETzbE/y8B/X6dpnrKThiqxDqj45K+ctOWtMqefgH6MojzJjWFwRs8UqhrJmVUq78SbMwGlw==} + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -194,6 +204,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -202,18 +216,59 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + automation-events@7.1.13: + resolution: {integrity: sha512-1Hay5TQPzxsskSqPTH3YXyzE9Iirz82zZDse2vr3+kOR7Sc7om17qIEPsESchlNX0EgKxANwR40i2g/O3GM1Tw==} + engines: {node: '>=18.2.0'} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + clone-buffer@1.0.0: + resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==} + engines: {node: '>= 0.10'} + + clone-stats@1.0.0: + resolution: {integrity: sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + cloneable-readable@1.1.3: + resolution: {integrity: sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -237,6 +292,9 @@ packages: esrap@2.1.0: resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -246,14 +304,68 @@ packages: picomatch: optional: true + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + google-closure-compiler-java@20221102.0.1: + resolution: {integrity: sha512-rMKLEma3uSe/6MGHtivDezTv4u5iaDEyxoy9No+1WruPSZ5h1gBZLONcfCA8JaoGojFPdHZI1qbwT0EveEWnAg==} + + google-closure-compiler-linux@20221102.0.1: + resolution: {integrity: sha512-rj1E1whT4j/giidQ44v4RoO8GcvU81VU9YB5RlRM0hWDvCGWjQasDABGnF/YLWLl5PXAAfJpa/hy8ckv5/r97g==} + cpu: [x32, x64] + os: [linux] + + google-closure-compiler-osx@20221102.0.1: + resolution: {integrity: sha512-Cv993yr9a2DLFgYnsv4m6dNUk5jousd6W6la12x2fDbhxTLewYrw7CrCaVEVw1SU3XErVmdHOZQjFsVMhcZjCw==} + cpu: [x32, x64, arm64] + os: [darwin] + + google-closure-compiler-windows@20221102.0.1: + resolution: {integrity: sha512-jRwHGekG/oDihHdKAEiYN5z0cBF+brL0bYtuEOXx4fAmq5tHe4OxKtSEEprCnVZZL0aG/boGprACPvsDRsXT7Q==} + cpu: [x32, x64] + os: [win32] + + google-closure-compiler@20221102.0.1: + resolution: {integrity: sha512-edAlsyJEsy2I983xWBlBfdSme16uyY007HM2OwPOoWPEFgmR100ggUabJbIegXZgbSLH51kyeJMQKuWhiHgzcA==} + engines: {node: '>=10'} + hasBin: true + + google-closure-library@20221102.0.0: + resolution: {integrity: sha512-M5+LWPS99tMB9dOGpZjLT9CdIYpnwBZiwB+dCmZFOOvwJiOWytntzJ/a/hoNF6zxD15l3GWwRJiEkL636D6DRQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + jazz-midi@1.7.9: + resolution: {integrity: sha512-c8c4BBgwxdsIr1iVm53nadCrtH7BUlnX3V95ciK/gbvXN/ndE5+POskBalXgqlc/r9p2XUbdLTrgrC6fou5p9w==} + engines: {node: '>=10.0.0'} + + jzz@1.9.6: + resolution: {integrity: sha512-J7ENLhXwfm2BNDKRUrL8eKtPhUS/CtMBpiafxQHDBcOWSocLhearDKEdh+ylnZFcr5OXWTed0gj6l/txeQA9vg==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -330,6 +442,12 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -342,6 +460,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -353,10 +481,31 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + ramda@0.28.0: + resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + replace-ext@1.0.1: + resolution: {integrity: sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==} + engines: {node: '>= 0.10'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rolldown-vite@7.1.14: resolution: {integrity: sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -406,10 +555,27 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + standardized-audio-context@25.3.77: + resolution: {integrity: sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + svelte-check@4.3.3: resolution: {integrity: sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==} engines: {node: '>= 18.0.0'} @@ -422,6 +588,9 @@ packages: resolution: {integrity: sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==} engines: {node: '>=18'} + text-encoding-shim@1.0.5: + resolution: {integrity: sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -437,6 +606,19 @@ packages: undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + unmute-ios-audio@3.3.0: + resolution: {integrity: sha512-MmoCOrsS2gn3wLT2tT+hF56Q4V4kksIKn2LHrwAtX6umzQwQHDWSh1slMzH+0WuxTZ62s3w8/wsfIII1FQ7ACg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vinyl-sourcemaps-apply@0.2.1: + resolution: {integrity: sha512-+oDh3KYZBoZC8hfocrbrxbLUeaYtQK7J5WU5Br9VqWqmCll3tFJqKp97GC9GmMsVIL0qnx2DgEDVxdo5EZ5sSw==} + + vinyl@2.2.1: + resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==} + engines: {node: '>= 0.10'} + vitefu@1.1.1: resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: @@ -445,6 +627,12 @@ packages: vite: optional: true + web-midi-api@2.4.0: + resolution: {integrity: sha512-tTfLdxa5LpOP1NgWByV458iYKgSLhlsIwqCpfbcJuyjProNtuf5UnX97K4JNyuQCqkR+6thQAIsk2BOMSrKaCA==} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -453,6 +641,21 @@ packages: snapshots: + '@babel/runtime@7.28.4': {} + + '@csound/browser@7.0.0-beta11': + dependencies: + eventemitter3: 4.0.7 + google-closure-compiler: 20221102.0.1 + google-closure-library: 20221102.0.0 + pako: 2.1.0 + ramda: 0.28.0 + rimraf: 3.0.2 + standardized-audio-context: 25.3.77 + text-encoding-shim: 1.0.5 + unmute-ios-audio: 3.3.0 + web-midi-api: 2.4.0 + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -585,18 +788,61 @@ snapshots: acorn@8.15.0: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansis@4.2.0: {} aria-query@5.3.2: {} + automation-events@7.1.13: + dependencies: + '@babel/runtime': 7.28.4 + tslib: 2.8.1 + axobject-query@4.1.0: {} + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 + clone-buffer@1.0.0: {} + + clone-stats@1.0.0: {} + + clone@2.1.2: {} + + cloneable-readable@1.1.3: + dependencies: + inherits: 2.0.4 + process-nextick-args: 2.0.1 + readable-stream: 2.3.8 + clsx@2.1.1: {} + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + core-util-is@1.0.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -611,17 +857,72 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + eventemitter3@4.0.7: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + google-closure-compiler-java@20221102.0.1: {} + + google-closure-compiler-linux@20221102.0.1: + optional: true + + google-closure-compiler-osx@20221102.0.1: + optional: true + + google-closure-compiler-windows@20221102.0.1: + optional: true + + google-closure-compiler@20221102.0.1: + dependencies: + chalk: 4.1.2 + google-closure-compiler-java: 20221102.0.1 + minimist: 1.2.8 + vinyl: 2.2.1 + vinyl-sourcemaps-apply: 0.2.1 + optionalDependencies: + google-closure-compiler-linux: 20221102.0.1 + google-closure-compiler-osx: 20221102.0.1 + google-closure-compiler-windows: 20221102.0.1 + + google-closure-library@20221102.0.0: {} + + has-flag@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 + isarray@1.0.0: {} + + jazz-midi@1.7.9: {} + + jzz@1.9.6: + dependencies: + jazz-midi: 1.7.9 + lightningcss-android-arm64@1.30.2: optional: true @@ -677,12 +978,26 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + mri@1.2.0: {} ms@2.1.3: {} nanoid@3.3.11: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + pako@2.1.0: {} + + path-is-absolute@1.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -693,8 +1008,30 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + process-nextick-args@2.0.1: {} + + ramda@0.28.0: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@4.1.2: {} + remove-trailing-separator@1.1.0: {} + + replace-ext@1.0.1: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + rolldown-vite@7.1.14(@types/node@24.7.1): dependencies: '@oxc-project/runtime': 0.92.0 @@ -733,8 +1070,26 @@ snapshots: dependencies: mri: 1.2.0 + safe-buffer@5.1.2: {} + source-map-js@1.2.1: {} + source-map@0.5.7: {} + + standardized-audio-context@25.3.77: + dependencies: + '@babel/runtime': 7.28.4 + automation-events: 7.1.13 + tslib: 2.8.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -764,22 +1119,46 @@ snapshots: magic-string: 0.30.19 zimmerframe: 1.1.4 + text-encoding-shim@1.0.5: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} typescript@5.9.3: {} undici-types@7.14.0: {} + unmute-ios-audio@3.3.0: {} + + util-deprecate@1.0.2: {} + + vinyl-sourcemaps-apply@0.2.1: + dependencies: + source-map: 0.5.7 + + vinyl@2.2.1: + dependencies: + clone: 2.1.2 + clone-buffer: 1.0.0 + clone-stats: 1.0.0 + cloneable-readable: 1.1.3 + remove-trailing-separator: 1.1.0 + replace-ext: 1.0.1 + vitefu@1.1.1(rolldown-vite@7.1.14(@types/node@24.7.1)): optionalDependencies: vite: rolldown-vite@7.1.14(@types/node@24.7.1) + web-midi-api@2.4.0: + dependencies: + jzz: 1.9.6 + + wrappy@1.0.2: {} + zimmerframe@1.1.4: {} zzfx@1.3.2: {} diff --git a/src/App.svelte b/src/App.svelte index 1d28d37..9e65e29 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -185,11 +185,11 @@ pitchLockEnabled = !pitchLockEnabled; } - function regenerateBuffer() { + async function regenerateBuffer() { if (!currentParams) return; const sampleRate = audioService.getSampleRate(); - const data = engine.generate(currentParams, sampleRate, duration); + const data = await engine.generate(currentParams, sampleRate, duration, pitchLock); currentBuffer = audioService.createAudioBuffer(data); audioService.play(currentBuffer); } diff --git a/src/lib/audio/engines/CsoundEngine.ts b/src/lib/audio/engines/CsoundEngine.ts new file mode 100644 index 0000000..274d1a0 --- /dev/null +++ b/src/lib/audio/engines/CsoundEngine.ts @@ -0,0 +1,253 @@ +import { Csound } from '@csound/browser'; +import type { SynthEngine, PitchLock } from './SynthEngine'; + +export interface CsoundParameter { + channelName: string; + value: number; +} + +export abstract class CsoundEngine implements SynthEngine { + abstract getName(): string; + abstract getDescription(): string; + abstract getType(): 'generative' | 'sample' | 'input'; + + protected abstract getOrchestra(): string; + protected abstract getParametersForCsound(params: T): CsoundParameter[]; + + abstract randomParams(pitchLock?: PitchLock): T; + abstract mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T; + + async generate( + params: T, + sampleRate: number, + duration: number, + pitchLock?: PitchLock + ): Promise<[Float32Array, Float32Array]> { + const orchestra = this.getOrchestra(); + const csoundParams = this.getParametersForCsound(params); + const outputFile = '/output.wav'; + const csd = this.buildCSD(orchestra, duration, sampleRate, csoundParams, outputFile); + + try { + const csound = await Csound(); + + if (!csound) { + throw new Error('Failed to initialize Csound'); + } + + await csound.compileCSD(csd); + await csound.start(); + await csound.perform(); + await csound.cleanup(); + + const wavData = await csound.fs.readFile(outputFile); + const audioBuffer = await this.parseWavManually(wavData, sampleRate); + + await csound.terminateInstance(); + + const leftChannel = new Float32Array(audioBuffer.leftChannel); + const rightChannel = new Float32Array(audioBuffer.rightChannel); + + // Apply short fade-in to prevent click at start + this.applyFadeIn(leftChannel, rightChannel, sampleRate); + + const peak = this.findPeak(leftChannel, rightChannel); + if (peak > 0.001) { + const normalizeGain = 0.85 / peak; + this.applyGain(leftChannel, rightChannel, normalizeGain); + } + + return [leftChannel, rightChannel]; + } catch (error) { + console.error('Csound generation failed:', error); + const numSamples = Math.floor(sampleRate * duration); + return [new Float32Array(numSamples), new Float32Array(numSamples)]; + } + } + + private parseWavManually( + wavData: Uint8Array, + expectedSampleRate: number + ): { leftChannel: Float32Array; rightChannel: Float32Array } { + const view = new DataView(wavData.buffer); + + // Check RIFF header + const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); + if (riff !== 'RIFF') { + throw new Error('Invalid WAV file: no RIFF header'); + } + + // Check WAVE format + const wave = String.fromCharCode(view.getUint8(8), view.getUint8(9), view.getUint8(10), view.getUint8(11)); + if (wave !== 'WAVE') { + throw new Error('Invalid WAV file: no WAVE format'); + } + + // Find fmt chunk + let offset = 12; + while (offset < wavData.length) { + const chunkId = String.fromCharCode( + view.getUint8(offset), + view.getUint8(offset + 1), + view.getUint8(offset + 2), + view.getUint8(offset + 3) + ); + const chunkSize = view.getUint32(offset + 4, true); + + if (chunkId === 'fmt ') { + const audioFormat = view.getUint16(offset + 8, true); + const numChannels = view.getUint16(offset + 10, true); + const sampleRate = view.getUint32(offset + 12, true); + const bitsPerSample = view.getUint16(offset + 22, true); + + // Find data chunk + let dataOffset = offset + 8 + chunkSize; + while (dataOffset < wavData.length) { + const dataChunkId = String.fromCharCode( + view.getUint8(dataOffset), + view.getUint8(dataOffset + 1), + view.getUint8(dataOffset + 2), + view.getUint8(dataOffset + 3) + ); + const dataChunkSize = view.getUint32(dataOffset + 4, true); + + if (dataChunkId === 'data') { + const bytesPerSample = bitsPerSample / 8; + const numSamples = Math.floor(dataChunkSize / bytesPerSample / numChannels); + + const leftChannel = new Float32Array(numSamples); + const rightChannel = new Float32Array(numSamples); + + let audioDataOffset = dataOffset + 8; + + if (bitsPerSample === 16) { + // 16-bit PCM + for (let i = 0; i < numSamples; i++) { + const leftSample = view.getInt16(audioDataOffset, true); + leftChannel[i] = leftSample / 32768.0; + audioDataOffset += 2; + + if (numChannels > 1) { + const rightSample = view.getInt16(audioDataOffset, true); + rightChannel[i] = rightSample / 32768.0; + audioDataOffset += 2; + } else { + rightChannel[i] = leftChannel[i]; + } + } + } else if (bitsPerSample === 32 && audioFormat === 3) { + // 32-bit float + for (let i = 0; i < numSamples; i++) { + leftChannel[i] = view.getFloat32(audioDataOffset, true); + audioDataOffset += 4; + + if (numChannels > 1) { + rightChannel[i] = view.getFloat32(audioDataOffset, true); + audioDataOffset += 4; + } else { + rightChannel[i] = leftChannel[i]; + } + } + } else { + throw new Error(`Unsupported WAV format: ${bitsPerSample}-bit, format ${audioFormat}`); + } + + return { leftChannel, rightChannel }; + } + + dataOffset += 8 + dataChunkSize; + } + throw new Error('No data chunk found in WAV file'); + } + + offset += 8 + chunkSize; + } + + throw new Error('No fmt chunk found in WAV file'); + } + + private buildCSD( + orchestra: string, + duration: number, + sampleRate: number, + parameters: CsoundParameter[], + outputFile: string + ): string { + const paramInit = parameters + .map(p => `chnset ${p.value}, "${p.channelName}"`) + .join('\n'); + + return ` + +-W -d -m0 -o ${outputFile} + + +sr = ${sampleRate} +ksmps = 64 +nchnls = 2 +0dbfs = 1.0 + +${paramInit} + +${orchestra} + + + +i 1 0 ${duration} +e + +`; + } + + private findPeak(leftChannel: Float32Array, rightChannel: Float32Array): number { + let peak = 0; + for (let i = 0; i < leftChannel.length; i++) { + peak = Math.max(peak, Math.abs(leftChannel[i]), Math.abs(rightChannel[i])); + } + return peak; + } + + private applyGain( + leftChannel: Float32Array, + rightChannel: Float32Array, + gain: number + ): void { + for (let i = 0; i < leftChannel.length; i++) { + leftChannel[i] *= gain; + rightChannel[i] *= gain; + } + } + + private applyFadeIn( + leftChannel: Float32Array, + rightChannel: Float32Array, + sampleRate: number + ): void { + const fadeInMs = 5; // 5ms fade-in to prevent clicks + const fadeSamples = Math.floor((fadeInMs / 1000) * sampleRate); + const actualFadeSamples = Math.min(fadeSamples, leftChannel.length); + + for (let i = 0; i < actualFadeSamples; i++) { + const gain = i / actualFadeSamples; + leftChannel[i] *= gain; + rightChannel[i] *= gain; + } + } + + protected randomRange(min: number, max: number): number { + return min + Math.random() * (max - min); + } + + protected randomInt(min: number, max: number): number { + return Math.floor(this.randomRange(min, max + 1)); + } + + protected randomChoice(choices: readonly U[]): U { + return choices[Math.floor(Math.random() * choices.length)]; + } + + protected mutateValue(value: number, amount: number, min: number, max: number): number { + const variation = value * amount * (Math.random() * 2 - 1); + return Math.max(min, Math.min(max, value + variation)); + } +} diff --git a/src/lib/audio/engines/SubtractiveThreeOsc.ts b/src/lib/audio/engines/SubtractiveThreeOsc.ts new file mode 100644 index 0000000..2ae4ced --- /dev/null +++ b/src/lib/audio/engines/SubtractiveThreeOsc.ts @@ -0,0 +1,338 @@ +import { CsoundEngine, type CsoundParameter } from './CsoundEngine'; +import type { PitchLock } from './SynthEngine'; + +enum Waveform { + Sine = 0, + Saw = 1, + Square = 2, + Triangle = 3, +} + +interface OscillatorParams { + waveform: Waveform; + ratio: number; + level: number; + attack: number; + decay: number; + sustain: number; + release: number; +} + +interface FilterParams { + cutoff: number; + resonance: number; + envAmount: number; + attack: number; + decay: number; + sustain: number; + release: number; +} + +export interface SubtractiveThreeOscParams { + baseFreq: number; + osc1: OscillatorParams; + osc2: OscillatorParams; + osc3: OscillatorParams; + filter: FilterParams; + stereoWidth: number; +} + +export class SubtractiveThreeOsc extends CsoundEngine { + getName(): string { + return 'Subtractive 3-OSC'; + } + + getDescription(): string { + return 'Three-oscillator subtractive synthesis with resonant filter'; + } + + getType() { + return 'generative' as const; + } + + protected getOrchestra(): string { + return ` +instr 1 + ; Get base frequency + ibasefreq chnget "basefreq" + istereo chnget "stereowidth" + + ; Oscillator 1 parameters + iosc1wave chnget "osc1_wave" + iosc1ratio chnget "osc1_ratio" + iosc1level chnget "osc1_level" + iosc1attack chnget "osc1_attack" + iosc1decay chnget "osc1_decay" + iosc1sustain chnget "osc1_sustain" + iosc1release chnget "osc1_release" + + ; Oscillator 2 parameters + iosc2wave chnget "osc2_wave" + iosc2ratio chnget "osc2_ratio" + iosc2level chnget "osc2_level" + iosc2attack chnget "osc2_attack" + iosc2decay chnget "osc2_decay" + iosc2sustain chnget "osc2_sustain" + iosc2release chnget "osc2_release" + + ; Oscillator 3 parameters + iosc3wave chnget "osc3_wave" + iosc3ratio chnget "osc3_ratio" + iosc3level chnget "osc3_level" + iosc3attack chnget "osc3_attack" + iosc3decay chnget "osc3_decay" + iosc3sustain chnget "osc3_sustain" + iosc3release chnget "osc3_release" + + ; Filter parameters + ifiltcutoff chnget "filt_cutoff" + ifiltres chnget "filt_resonance" + ifiltenvamt chnget "filt_envamt" + ifiltattack chnget "filt_attack" + ifiltdecay chnget "filt_decay" + ifiltsustain chnget "filt_sustain" + ifiltrelease chnget "filt_release" + + idur = p3 + + ; Convert ratios to time values + iosc1att = iosc1attack * idur + iosc1dec = iosc1decay * idur + iosc1rel = iosc1release * idur + + iosc2att = iosc2attack * idur + iosc2dec = iosc2decay * idur + iosc2rel = iosc2release * idur + + iosc3att = iosc3attack * idur + iosc3dec = iosc3decay * idur + iosc3rel = iosc3release * idur + + ifiltatt = ifiltattack * idur + ifiltdec = ifiltdecay * idur + ifiltrel = ifiltrelease * idur + + ; Stereo detuning + idetune = 1 + (istereo * 0.001) + ifreqL = ibasefreq / idetune + ifreqR = ibasefreq * idetune + + ; Oscillator 1 envelopes + kenv1 madsr iosc1att, iosc1dec, iosc1sustain, iosc1rel + + ; Oscillator 1 - Left + if iosc1wave == 0 then + aosc1L oscili kenv1 * iosc1level, ifreqL * iosc1ratio + elseif iosc1wave == 1 then + aosc1L vco2 kenv1 * iosc1level, ifreqL * iosc1ratio, 0 + elseif iosc1wave == 2 then + aosc1L vco2 kenv1 * iosc1level, ifreqL * iosc1ratio, 10 + else + aosc1L vco2 kenv1 * iosc1level, ifreqL * iosc1ratio, 12 + endif + + ; Oscillator 1 - Right + if iosc1wave == 0 then + aosc1R oscili kenv1 * iosc1level, ifreqR * iosc1ratio + elseif iosc1wave == 1 then + aosc1R vco2 kenv1 * iosc1level, ifreqR * iosc1ratio, 0 + elseif iosc1wave == 2 then + aosc1R vco2 kenv1 * iosc1level, ifreqR * iosc1ratio, 10 + else + aosc1R vco2 kenv1 * iosc1level, ifreqR * iosc1ratio, 12 + endif + + ; Oscillator 2 envelopes + kenv2 madsr iosc2att, iosc2dec, iosc2sustain, iosc2rel + + ; Oscillator 2 - Left + if iosc2wave == 0 then + aosc2L oscili kenv2 * iosc2level, ifreqL * iosc2ratio + elseif iosc2wave == 1 then + aosc2L vco2 kenv2 * iosc2level, ifreqL * iosc2ratio, 0 + elseif iosc2wave == 2 then + aosc2L vco2 kenv2 * iosc2level, ifreqL * iosc2ratio, 10 + else + aosc2L vco2 kenv2 * iosc2level, ifreqL * iosc2ratio, 12 + endif + + ; Oscillator 2 - Right + if iosc2wave == 0 then + aosc2R oscili kenv2 * iosc2level, ifreqR * iosc2ratio + elseif iosc2wave == 1 then + aosc2R vco2 kenv2 * iosc2level, ifreqR * iosc2ratio, 0 + elseif iosc2wave == 2 then + aosc2R vco2 kenv2 * iosc2level, ifreqR * iosc2ratio, 10 + else + aosc2R vco2 kenv2 * iosc2level, ifreqR * iosc2ratio, 12 + endif + + ; Oscillator 3 envelopes + kenv3 madsr iosc3att, iosc3dec, iosc3sustain, iosc3rel + + ; Oscillator 3 - Left + if iosc3wave == 0 then + aosc3L oscili kenv3 * iosc3level, ifreqL * iosc3ratio + elseif iosc3wave == 1 then + aosc3L vco2 kenv3 * iosc3level, ifreqL * iosc3ratio, 0 + elseif iosc3wave == 2 then + aosc3L vco2 kenv3 * iosc3level, ifreqL * iosc3ratio, 10 + else + aosc3L vco2 kenv3 * iosc3level, ifreqL * iosc3ratio, 12 + endif + + ; Oscillator 3 - Right + if iosc3wave == 0 then + aosc3R oscili kenv3 * iosc3level, ifreqR * iosc3ratio + elseif iosc3wave == 1 then + aosc3R vco2 kenv3 * iosc3level, ifreqR * iosc3ratio, 0 + elseif iosc3wave == 2 then + aosc3R vco2 kenv3 * iosc3level, ifreqR * iosc3ratio, 10 + else + aosc3R vco2 kenv3 * iosc3level, ifreqR * iosc3ratio, 12 + endif + + ; Mix oscillators + amixL = aosc1L + aosc2L + aosc3L + amixR = aosc1R + aosc2R + aosc3R + + ; Filter envelope + kfiltenv madsr ifiltatt, ifiltdec, ifiltsustain, ifiltrel + kcutoff = ifiltcutoff + (kfiltenv * ifiltenvamt * 10000) + kcutoff = limit(kcutoff, 20, 20000) + + ; Apply moogladder filter + afiltL moogladder amixL, kcutoff, ifiltres + afiltR moogladder amixR, kcutoff, ifiltres + + outs afiltL, afiltR +endin +`; + } + + protected getParametersForCsound(params: SubtractiveThreeOscParams): CsoundParameter[] { + return [ + { channelName: 'basefreq', value: params.baseFreq }, + { channelName: 'stereowidth', value: params.stereoWidth }, + + { channelName: 'osc1_wave', value: params.osc1.waveform }, + { channelName: 'osc1_ratio', value: params.osc1.ratio }, + { channelName: 'osc1_level', value: params.osc1.level }, + { channelName: 'osc1_attack', value: params.osc1.attack }, + { channelName: 'osc1_decay', value: params.osc1.decay }, + { channelName: 'osc1_sustain', value: params.osc1.sustain }, + { channelName: 'osc1_release', value: params.osc1.release }, + + { channelName: 'osc2_wave', value: params.osc2.waveform }, + { channelName: 'osc2_ratio', value: params.osc2.ratio }, + { channelName: 'osc2_level', value: params.osc2.level }, + { channelName: 'osc2_attack', value: params.osc2.attack }, + { channelName: 'osc2_decay', value: params.osc2.decay }, + { channelName: 'osc2_sustain', value: params.osc2.sustain }, + { channelName: 'osc2_release', value: params.osc2.release }, + + { channelName: 'osc3_wave', value: params.osc3.waveform }, + { channelName: 'osc3_ratio', value: params.osc3.ratio }, + { channelName: 'osc3_level', value: params.osc3.level }, + { channelName: 'osc3_attack', value: params.osc3.attack }, + { channelName: 'osc3_decay', value: params.osc3.decay }, + { channelName: 'osc3_sustain', value: params.osc3.sustain }, + { channelName: 'osc3_release', value: params.osc3.release }, + + { channelName: 'filt_cutoff', value: params.filter.cutoff }, + { channelName: 'filt_resonance', value: params.filter.resonance }, + { channelName: 'filt_envamt', value: params.filter.envAmount }, + { channelName: 'filt_attack', value: params.filter.attack }, + { channelName: 'filt_decay', value: params.filter.decay }, + { channelName: 'filt_sustain', value: params.filter.sustain }, + { channelName: 'filt_release', value: params.filter.release }, + ]; + } + + randomParams(pitchLock?: PitchLock): SubtractiveThreeOscParams { + let baseFreq: number; + if (pitchLock?.enabled) { + baseFreq = pitchLock.frequency; + } else { + const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440]; + baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.98, 1.02); + } + + const harmonicRatios = [0.5, 1, 2, 3, 4]; + const detuneRatios = [0.99, 1.0, 1.01, 1.02, 0.98]; + + return { + baseFreq, + osc1: this.randomOscillator(harmonicRatios), + osc2: this.randomOscillator(detuneRatios), + osc3: this.randomOscillator(harmonicRatios), + filter: this.randomFilter(), + stereoWidth: this.randomRange(0.2, 0.8), + }; + } + + private randomOscillator(ratios: number[]): OscillatorParams { + return { + waveform: this.randomInt(0, 3) as Waveform, + ratio: this.randomChoice(ratios), + level: this.randomRange(0.2, 0.5), + attack: this.randomRange(0.001, 0.15), + decay: this.randomRange(0.02, 0.25), + sustain: this.randomRange(0.3, 0.8), + release: this.randomRange(0.05, 0.4), + }; + } + + private randomFilter(): FilterParams { + return { + cutoff: this.randomRange(200, 5000), + resonance: this.randomRange(0.1, 0.8), + envAmount: this.randomRange(0.2, 1.2), + attack: this.randomRange(0.001, 0.15), + decay: this.randomRange(0.05, 0.3), + sustain: this.randomRange(0.2, 0.7), + release: this.randomRange(0.05, 0.4), + }; + } + + mutateParams( + params: SubtractiveThreeOscParams, + mutationAmount: number = 0.15, + pitchLock?: PitchLock + ): SubtractiveThreeOscParams { + const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq; + + return { + baseFreq, + osc1: this.mutateOscillator(params.osc1, mutationAmount), + osc2: this.mutateOscillator(params.osc2, mutationAmount), + osc3: this.mutateOscillator(params.osc3, mutationAmount), + filter: this.mutateFilter(params.filter, mutationAmount), + stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1), + }; + } + + private mutateOscillator(osc: OscillatorParams, amount: number): OscillatorParams { + return { + waveform: Math.random() < 0.1 ? (this.randomInt(0, 3) as Waveform) : osc.waveform, + ratio: Math.random() < 0.1 ? this.randomChoice([0.5, 0.98, 0.99, 1, 1.01, 1.02, 2, 3, 4]) : osc.ratio, + level: this.mutateValue(osc.level, amount, 0.1, 0.7), + attack: this.mutateValue(osc.attack, amount, 0.001, 0.3), + decay: this.mutateValue(osc.decay, amount, 0.01, 0.4), + sustain: this.mutateValue(osc.sustain, amount, 0.1, 0.9), + release: this.mutateValue(osc.release, amount, 0.02, 0.6), + }; + } + + private mutateFilter(filter: FilterParams, amount: number): FilterParams { + return { + cutoff: this.mutateValue(filter.cutoff, amount, 100, 8000), + resonance: this.mutateValue(filter.resonance, amount, 0, 0.95), + envAmount: this.mutateValue(filter.envAmount, amount, 0, 1.5), + attack: this.mutateValue(filter.attack, amount, 0.001, 0.3), + decay: this.mutateValue(filter.decay, amount, 0.01, 0.4), + sustain: this.mutateValue(filter.sustain, amount, 0.1, 0.9), + release: this.mutateValue(filter.release, amount, 0.02, 0.6), + }; + } +} diff --git a/src/lib/audio/engines/SynthEngine.ts b/src/lib/audio/engines/SynthEngine.ts index 279c777..94c8e1b 100644 --- a/src/lib/audio/engines/SynthEngine.ts +++ b/src/lib/audio/engines/SynthEngine.ts @@ -15,7 +15,7 @@ export interface SynthEngine { getName(): string; getDescription(): string; getType(): EngineType; - generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array]; + generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array] | Promise<[Float32Array, Float32Array]>; randomParams(pitchLock?: PitchLock): T; mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T; } diff --git a/src/lib/audio/engines/registry.ts b/src/lib/audio/engines/registry.ts index 5507f74..d59a5de 100644 --- a/src/lib/audio/engines/registry.ts +++ b/src/lib/audio/engines/registry.ts @@ -16,6 +16,7 @@ import { BassDrum } from './BassDrum'; import { HiHat } from './HiHat'; import { ParticleNoise } from './ParticleNoise'; import { DustNoise } from './DustNoise'; +import { SubtractiveThreeOsc } from './SubtractiveThreeOsc'; export const engines: SynthEngine[] = [ new Sample(), @@ -35,4 +36,5 @@ export const engines: SynthEngine[] = [ new AdditiveEngine(), new ParticleNoise(), new DustNoise(), + new SubtractiveThreeOsc(), ];