added generative audio, tailwind, improvements, adjustments
This commit is contained in:
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
nodejs 24.12.0
|
||||
440
package-lock.json
generated
440
package-lock.json
generated
@@ -8,6 +8,8 @@
|
||||
"name": "hear-on-out",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"p5": "^2.1.2",
|
||||
"p5-svelte": "^3.1.2",
|
||||
"tonal": "^6.4.2",
|
||||
"tone": "^15.1.22"
|
||||
},
|
||||
@@ -20,19 +22,21 @@
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitest/browser": "^3.2.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"openmeteo": "^1.2.0",
|
||||
"playwright": "^1.53.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-geolocation": "^1.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.2.6",
|
||||
@@ -89,6 +93,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@davepagurek/bezier-path": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@davepagurek/bezier-path/-/bezier-path-0.0.2.tgz",
|
||||
"integrity": "sha512-4L9ddgzZc9DRGyl1RrS3z5nwnVJoyjsAelVG4X1jh4tVxryEHr4H9QavhxW/my6Rn3669Qz6mhv8gd5O/WeFTA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
|
||||
@@ -778,6 +788,12 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@japont/unicode-range": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@japont/unicode-range/-/unicode-range-1.0.0.tgz",
|
||||
"integrity": "sha512-BckHvA2XdjRBVAWe2uceNuRf78lBeI28kyWEbfr/Q2pE17POkwuZ6WWY/UMv8FL9iBxhW4xfDoNLM9UVZaTeUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
@@ -1195,6 +1211,7 @@
|
||||
"integrity": "sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/cookie": "^0.6.0",
|
||||
@@ -1228,6 +1245,7 @@
|
||||
"integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
|
||||
"debug": "^4.4.1",
|
||||
@@ -1278,6 +1296,13 @@
|
||||
"tailwindcss": "4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
|
||||
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz",
|
||||
@@ -1539,12 +1564,20 @@
|
||||
"vite": "^5.2.0 || ^6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
|
||||
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -1907,6 +1940,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/p5": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.7.7.tgz",
|
||||
"integrity": "sha512-WFuP7jqc5CkkMtCK/NphgvMnJz1Qi9CMuK7t6xLu/tuXkRdGQA4q4AD0dUYcChC0Oibe8PE8gbKSFPNF0BqVNw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
||||
@@ -1953,6 +1993,7 @@
|
||||
"integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.34.1",
|
||||
"@typescript-eslint/types": "8.34.1",
|
||||
@@ -2183,6 +2224,7 @@
|
||||
"integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -2332,8 +2374,8 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2351,6 +2393,18 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -2434,6 +2488,43 @@
|
||||
"node": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.23",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"caniuse-lite": "^1.0.30001760",
|
||||
"fraction.js": "^5.3.4",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"autoprefixer": "bin/autoprefixer"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -2451,6 +2542,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -2475,6 +2576,41 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@@ -2495,6 +2631,27 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001761",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
|
||||
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
|
||||
@@ -2595,6 +2752,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorjs.io": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
|
||||
"integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2719,6 +2882,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
@@ -2781,6 +2951,16 @@
|
||||
"@esbuild/win32-x64": "0.25.5"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -2794,12 +2974,34 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/escodegen": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
|
||||
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esprima": "^4.0.1",
|
||||
"estraverse": "^5.2.0",
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"escodegen": "bin/escodegen.js",
|
||||
"esgenerate": "bin/esgenerate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
|
||||
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2960,6 +3162,19 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||
@@ -3000,7 +3215,6 @@
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
@@ -3020,7 +3234,6 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3125,6 +3338,12 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz",
|
||||
"integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -3183,6 +3402,20 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@@ -3198,6 +3431,12 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gifenc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz",
|
||||
"integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -3248,6 +3487,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "19.9.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz",
|
||||
"integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.3.1.tgz",
|
||||
"integrity": "sha512-KIToAzf8zwWvacgnRwJp63ase26o24AuNUlfNVJ5YZAFmdGhsJpmFClxXPuk9rv1FMI4lnc8zLSqgZPEZMrW4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -3427,6 +3684,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libtess": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/libtess/-/libtess-1.2.2.tgz",
|
||||
"integrity": "sha512-Nps8HPeVVcsmJxUvFLKVJcCgcz+1ajPTXDVAVPs6+giOQP4AHV31uZFFkh+CKow/bkB7GbZWKmwmit7myaqDSw==",
|
||||
"license": "SGI-B-2.0"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
@@ -3875,6 +4138,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/omggif": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
||||
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/openmeteo": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/openmeteo/-/openmeteo-1.2.0.tgz",
|
||||
@@ -3939,6 +4215,54 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p5": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/p5/-/p5-2.1.2.tgz",
|
||||
"integrity": "sha512-M96K3FSwd31Sawsl9TzJ8kBZFuUy06eqRHpWw0DIQAOmYOosVyPr7Eh5GR2LsScba0X2wSnfRBYMz64oVQVOdg==",
|
||||
"license": "LGPL-2.1",
|
||||
"dependencies": {
|
||||
"@davepagurek/bezier-path": "^0.0.2",
|
||||
"@japont/unicode-range": "^1.0.0",
|
||||
"acorn": "^8.12.1",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"colorjs.io": "^0.5.2",
|
||||
"escodegen": "^2.1.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"gifenc": "^1.0.3",
|
||||
"i18next": "^19.0.2",
|
||||
"i18next-browser-languagedetector": "^4.0.1",
|
||||
"libtess": "^1.2.2",
|
||||
"omggif": "^1.0.10",
|
||||
"pako": "^2.1.0",
|
||||
"pixelmatch": "^7.1.0",
|
||||
"zod": "^3.25.51"
|
||||
}
|
||||
},
|
||||
"node_modules/p5-svelte": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/p5-svelte/-/p5-svelte-3.1.2.tgz",
|
||||
"integrity": "sha512-lcfWh+cJ1/wRdIXHnjpYmDgj2h3TCy1QJVQnf/cBcFWS8CSkvyAN5F8u8H2U8qBUtZ4XaD3nd+1NoYUMHaMExQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p5": "^1.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/p5": "^1.4.2",
|
||||
"p5": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p5-svelte/node_modules/p5": {
|
||||
"version": "1.11.11",
|
||||
"resolved": "https://registry.npmjs.org/p5/-/p5-1.11.11.tgz",
|
||||
"integrity": "sha512-k58mfexvavFb+KNRpi70PbkKE2gCNiWQkzS4kVOyC2F9SKGgYy1jSO+JXZ24ikXV9OvZIAxGusiSVWEijYrmNg==",
|
||||
"license": "LGPL-2.1"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -4002,6 +4326,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4009,12 +4334,25 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
|
||||
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pngjs": "^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.53.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
|
||||
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.53.1"
|
||||
},
|
||||
@@ -4041,6 +4379,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -4061,6 +4408,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -4178,6 +4526,13 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -4194,6 +4549,7 @@
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -4210,6 +4566,7 @@
|
||||
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"prettier": "^3.0.0",
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
@@ -4537,6 +4894,16 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -4624,6 +4991,7 @@
|
||||
"integrity": "sha512-5PEg+QQKce4t1qiOtVUhUS3AQRTtxJyGBTpxLcNWnr0Ve8q4r06bMo0Gv8uhtCPWlztZHoi3Ye7elLhu+PCTMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -4715,9 +5083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz",
|
||||
"integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4912,6 +5280,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -4943,6 +5312,37 @@
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -4980,6 +5380,7 @@
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -5125,6 +5526,7 @@
|
||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/expect": "3.2.4",
|
||||
@@ -5285,21 +5687,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
@@ -5319,6 +5706,15 @@
|
||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,19 +24,21 @@
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitest/browser": "^3.2.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-svelte": "^3.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"openmeteo": "^1.2.0",
|
||||
"playwright": "^1.53.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-geolocation": "^1.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^6.2.6",
|
||||
@@ -45,6 +47,8 @@
|
||||
"vitest-browser-svelte": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"p5": "^2.1.2",
|
||||
"p5-svelte": "^3.1.2",
|
||||
"tonal": "^6.4.2",
|
||||
"tone": "^15.1.22"
|
||||
}
|
||||
|
||||
14
src/app.css
14
src/app.css
@@ -1,7 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
|
||||
.syne-mono-regular {
|
||||
font-family: "Syne Mono", monospace;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
@theme {
|
||||
--font-family-syne-mono: "Syne Mono", monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: var(--font-family-syne-mono);
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" class="bg-black text-white syne-mono-regular">
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
|
||||
35
src/lib/audio/audio-effects.ts
Normal file
35
src/lib/audio/audio-effects.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as Tone from 'tone';
|
||||
|
||||
export function createReverb(wetValue: number): Tone.Reverb {
|
||||
const reverb = new Tone.Reverb({
|
||||
decay: 16,
|
||||
preDelay: 0.5
|
||||
});
|
||||
reverb.wet.value = wetValue;
|
||||
return reverb;
|
||||
}
|
||||
|
||||
export function createDelay(delayTime: string, feedback: number): Tone.FeedbackDelay {
|
||||
const delay = new Tone.FeedbackDelay({
|
||||
delayTime: delayTime,
|
||||
feedback: feedback
|
||||
});
|
||||
delay.wet.value = 0.5;
|
||||
return delay;
|
||||
}
|
||||
|
||||
export function createFilter(frequency: number, resonance: number): Tone.Filter {
|
||||
return new Tone.Filter({
|
||||
type: 'lowpass',
|
||||
frequency: frequency,
|
||||
Q: resonance
|
||||
});
|
||||
}
|
||||
|
||||
export function createGain(volume: number): Tone.Gain {
|
||||
return new Tone.Gain(Tone.dbToGain(volume));
|
||||
}
|
||||
|
||||
export function createAnalyser(): Tone.Analyser {
|
||||
return new Tone.Analyser('fft', 512);
|
||||
}
|
||||
61
src/lib/audio/chord-progressions.ts
Normal file
61
src/lib/audio/chord-progressions.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export type ChordProgression = Array<{
|
||||
time: string;
|
||||
notes: string[];
|
||||
}>;
|
||||
|
||||
// Bright, uplifting - for pleasant daytime conditions
|
||||
export const brightProgression: ChordProgression = [
|
||||
{ time: '0:0:0', notes: ['C4', 'E4', 'G4', 'B4'] }, // Cmaj7
|
||||
{ time: '0:1:0', notes: ['F4', 'A4', 'C5', 'E5'] }, // Fmaj7
|
||||
{ time: '0:2:0', notes: ['G4', 'B4', 'D5', 'F5'] }, // G7
|
||||
{ time: '0:3:0', notes: ['A3', 'C4', 'E4', 'G4'] } // Am7
|
||||
];
|
||||
|
||||
// Dreamy, calm - for pleasant nighttime conditions
|
||||
export const dreamyProgression: ChordProgression = [
|
||||
{ time: '0:0:0', notes: ['D4', 'F#4', 'A4', 'C5'] }, // Dmaj7
|
||||
{ time: '0:1:0', notes: ['G3', 'B3', 'D4', 'F#4'] }, // Gmaj7
|
||||
{ time: '0:2:0', notes: ['E4', 'G4', 'B4', 'D5'] }, // Em7
|
||||
{ time: '0:3:0', notes: ['A3', 'C#4', 'E4', 'G4'] } // A7
|
||||
];
|
||||
|
||||
// Melancholic, introspective - for cold/rainy conditions
|
||||
export const melancholicProgression: ChordProgression = [
|
||||
{ time: '0:0:0', notes: ['A3', 'C4', 'E4', 'G4'] }, // Am7
|
||||
{ time: '0:1:0', notes: ['D3', 'F3', 'A3', 'C4'] }, // Dm7
|
||||
{ time: '0:2:0', notes: ['G3', 'Bb3', 'D4', 'F4'] }, // Gm7
|
||||
{ time: '0:3:0', notes: ['C3', 'E3', 'G3', 'Bb3'] } // C7
|
||||
];
|
||||
|
||||
// Tense, atmospheric - for stormy/extreme conditions
|
||||
export const tenseProgression: ChordProgression = [
|
||||
{ time: '0:0:0', notes: ['E3', 'G3', 'Bb3', 'D4'] }, // Em7b5
|
||||
{ time: '0:1:0', notes: ['F3', 'Ab3', 'C4', 'Eb4'] }, // Fm7
|
||||
{ time: '0:2:0', notes: ['Bb3', 'Db4', 'F4', 'Ab4'] }, // Bbm7
|
||||
{ time: '0:3:0', notes: ['Eb3', 'Gb3', 'Bb3', 'Db4'] } // Ebm7
|
||||
];
|
||||
|
||||
// Warm, intense - for hot conditions
|
||||
export const warmProgression: ChordProgression = [
|
||||
{ time: '0:0:0', notes: ['E4', 'G#4', 'B4', 'D5'] }, // E7
|
||||
{ time: '0:1:0', notes: ['A3', 'C#4', 'E4', 'G4'] }, // A7
|
||||
{ time: '0:2:0', notes: ['D4', 'F#4', 'A4', 'C5'] }, // D7
|
||||
{ time: '0:3:0', notes: ['G3', 'B3', 'D4', 'F4'] } // G7
|
||||
];
|
||||
|
||||
// Ethereal, floating - for foggy/misty conditions
|
||||
export const etherealProgression: ChordProgression = [
|
||||
{ time: '0:0:0', notes: ['F4', 'A4', 'C5', 'E5'] }, // Fmaj7
|
||||
{ time: '0:1:0', notes: ['C4', 'E4', 'G4', 'B4'] }, // Cmaj7
|
||||
{ time: '0:2:0', notes: ['G4', 'B4', 'D5', 'F#5'] }, // Gmaj7
|
||||
{ time: '0:3:0', notes: ['D4', 'F#4', 'A4', 'C#5'] } // Dmaj7
|
||||
];
|
||||
|
||||
export const allProgressions = [
|
||||
brightProgression,
|
||||
dreamyProgression,
|
||||
melancholicProgression,
|
||||
tenseProgression,
|
||||
warmProgression,
|
||||
etherealProgression
|
||||
];
|
||||
16
src/lib/audio/instruments/arpSynth.ts
Normal file
16
src/lib/audio/instruments/arpSynth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as Tone from 'tone';
|
||||
|
||||
export function createArpSynth(volume: number): Tone.Synth {
|
||||
return new Tone.Synth({
|
||||
oscillator: {
|
||||
type: 'triangle'
|
||||
},
|
||||
envelope: {
|
||||
attack: 0.005,
|
||||
decay: 0.2,
|
||||
sustain: 0,
|
||||
release: 0.3
|
||||
},
|
||||
volume: volume
|
||||
});
|
||||
}
|
||||
16
src/lib/audio/instruments/bassSynth.ts
Normal file
16
src/lib/audio/instruments/bassSynth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as Tone from 'tone';
|
||||
|
||||
export function createBassSynth(): Tone.Synth {
|
||||
return new Tone.Synth({
|
||||
oscillator: {
|
||||
type: 'sine'
|
||||
},
|
||||
envelope: {
|
||||
attack: 0.1,
|
||||
decay: 0.3,
|
||||
sustain: 0.8,
|
||||
release: 2.0
|
||||
},
|
||||
volume: -20
|
||||
});
|
||||
}
|
||||
16
src/lib/audio/instruments/noiseSynth.ts
Normal file
16
src/lib/audio/instruments/noiseSynth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as Tone from 'tone';
|
||||
|
||||
export function createNoiseSynth(volume: number): Tone.NoiseSynth {
|
||||
return new Tone.NoiseSynth({
|
||||
noise: {
|
||||
type: 'pink'
|
||||
},
|
||||
envelope: {
|
||||
attack: 0.005,
|
||||
decay: 0.1,
|
||||
sustain: 0,
|
||||
release: 0.1
|
||||
},
|
||||
volume: volume
|
||||
});
|
||||
}
|
||||
16
src/lib/audio/instruments/padSynth.ts
Normal file
16
src/lib/audio/instruments/padSynth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as Tone from 'tone';
|
||||
|
||||
export function createPadSynth(isDay: boolean): Tone.PolySynth {
|
||||
return new Tone.PolySynth(Tone.Synth, {
|
||||
oscillator: {
|
||||
type: isDay ? 'triangle' : 'sine'
|
||||
},
|
||||
envelope: {
|
||||
attack: 1.5,
|
||||
decay: 1,
|
||||
sustain: 0.7,
|
||||
release: 1.0
|
||||
},
|
||||
volume: -20
|
||||
});
|
||||
}
|
||||
21
src/lib/audio/instruments/pingSynth.ts
Normal file
21
src/lib/audio/instruments/pingSynth.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as Tone from 'tone';
|
||||
|
||||
/**
|
||||
* Creates a sharp, high-pitched ping synth for extreme weather indicators
|
||||
* @param volume - Volume in dB
|
||||
* @returns A configured Synth instance
|
||||
*/
|
||||
export function createPingSynth(volume: number): Tone.Synth {
|
||||
return new Tone.Synth({
|
||||
oscillator: {
|
||||
type: 'triangle'
|
||||
},
|
||||
envelope: {
|
||||
attack: 0.001, // Very sharp attack
|
||||
decay: 0.05,
|
||||
sustain: 0,
|
||||
release: 0.1
|
||||
},
|
||||
volume: volume
|
||||
});
|
||||
}
|
||||
97
src/lib/audio/weather-mood.ts
Normal file
97
src/lib/audio/weather-mood.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
brightProgression,
|
||||
dreamyProgression,
|
||||
melancholicProgression,
|
||||
tenseProgression,
|
||||
warmProgression,
|
||||
etherealProgression,
|
||||
type ChordProgression
|
||||
} from './chord-progressions';
|
||||
|
||||
interface WeatherConditions {
|
||||
temperature2m: number;
|
||||
relativeHumidity2m: number;
|
||||
cloudCover: number;
|
||||
windSpeed10m: number;
|
||||
precipitation: number;
|
||||
isDay: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a weather mood score based on various conditions
|
||||
* Returns values between 0 (harsh/extreme) and 1 (pleasant)
|
||||
*/
|
||||
export function calculateComfortScore(conditions: WeatherConditions): number {
|
||||
const { temperature2m, relativeHumidity2m, cloudCover, windSpeed10m, precipitation } =
|
||||
conditions;
|
||||
|
||||
// Temperature comfort: ideal 15-25°C, drops off outside this range
|
||||
let tempScore = 1.0;
|
||||
if (temperature2m < 15) {
|
||||
tempScore = Math.max(0, 1 - Math.abs(15 - temperature2m) / 30);
|
||||
} else if (temperature2m > 25) {
|
||||
tempScore = Math.max(0, 1 - Math.abs(temperature2m - 25) / 20);
|
||||
}
|
||||
|
||||
// Humidity comfort: ideal 40-60%, drops off outside
|
||||
let humidityScore = 1.0;
|
||||
if (relativeHumidity2m < 40) {
|
||||
humidityScore = Math.max(0, relativeHumidity2m / 40);
|
||||
} else if (relativeHumidity2m > 60) {
|
||||
humidityScore = Math.max(0, 1 - (relativeHumidity2m - 60) / 40);
|
||||
}
|
||||
|
||||
// Cloud cover: some clouds (30-70%) is pleasant, extremes less so
|
||||
const cloudScore = 1 - Math.abs(cloudCover - 50) / 50;
|
||||
|
||||
// Wind: light breeze (0-15 km/h) is nice, strong wind less so
|
||||
const windScore = Math.max(0, 1 - windSpeed10m / 40);
|
||||
|
||||
// Precipitation: any is somewhat unpleasant
|
||||
const precipScore = Math.max(0, 1 - precipitation / 10);
|
||||
|
||||
// Weighted average
|
||||
return (
|
||||
tempScore * 0.35 +
|
||||
humidityScore * 0.2 +
|
||||
cloudScore * 0.15 +
|
||||
windScore * 0.15 +
|
||||
precipScore * 0.15
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the appropriate chord progression based on weather conditions
|
||||
*/
|
||||
export function selectChordProgression(conditions: WeatherConditions): ChordProgression {
|
||||
const comfortScore = calculateComfortScore(conditions);
|
||||
const { temperature2m, precipitation, cloudCover, isDay } = conditions;
|
||||
|
||||
// Stormy/extreme conditions (heavy rain, very harsh)
|
||||
if (precipitation > 5 || comfortScore < 0.2) {
|
||||
return tenseProgression;
|
||||
}
|
||||
|
||||
// Very hot conditions
|
||||
if (temperature2m > 30) {
|
||||
return warmProgression;
|
||||
}
|
||||
|
||||
// Cold/rainy/gloomy conditions
|
||||
if (temperature2m < 5 || (precipitation > 1 && cloudCover > 70)) {
|
||||
return melancholicProgression;
|
||||
}
|
||||
|
||||
// Foggy/misty conditions (high humidity + clouds, low wind)
|
||||
if (conditions.relativeHumidity2m > 80 && cloudCover > 60 && temperature2m > 5) {
|
||||
return etherealProgression;
|
||||
}
|
||||
|
||||
// Pleasant conditions - choose based on day/night
|
||||
if (comfortScore > 0.6) {
|
||||
return isDay ? brightProgression : dreamyProgression;
|
||||
}
|
||||
|
||||
// Default: slightly unpleasant but not extreme
|
||||
return isDay ? dreamyProgression : melancholicProgression;
|
||||
}
|
||||
196
src/lib/components/AudioVisualization.svelte
Normal file
196
src/lib/components/AudioVisualization.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import P5 from 'p5-svelte';
|
||||
import type p5 from 'p5';
|
||||
import type * as Tone from 'tone';
|
||||
|
||||
let { isPlaying = false, width = 400, height = 400, analyser = null } = $props<{
|
||||
isPlaying: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
analyser: Tone.Analyser | null;
|
||||
}>();
|
||||
|
||||
let particles: Particle[] = [];
|
||||
const numParticles = 60;
|
||||
let audioData: Float32Array | null = null;
|
||||
|
||||
class Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
baseX: number;
|
||||
baseY: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
size: number;
|
||||
alpha: number;
|
||||
index: number;
|
||||
|
||||
constructor(p: p5, index: number) {
|
||||
this.index = index;
|
||||
this.x = p.random(p.width);
|
||||
this.y = p.random(p.height);
|
||||
this.baseX = this.x;
|
||||
this.baseY = this.y;
|
||||
this.vx = p.random(-0.5, 0.5);
|
||||
this.vy = p.random(-0.5, 0.5);
|
||||
this.size = p.random(2, 6);
|
||||
this.alpha = p.random(100, 255);
|
||||
}
|
||||
|
||||
update(p: p5, audioLevel: number, bass: number, mid: number) {
|
||||
// Base movement - gentle constant speed
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
|
||||
// Subtle audio reactive displacement
|
||||
const displacement = audioLevel * 20;
|
||||
const angle = p.noise(this.x * 0.01, this.y * 0.01, p.frameCount * 0.01) * p.TWO_PI;
|
||||
this.x += p.cos(angle) * displacement * 0.05;
|
||||
this.y += p.sin(angle) * displacement * 0.05;
|
||||
|
||||
// Subtle audio reactive size
|
||||
this.size = p.map(bass + mid, 0, 2, 3, 10);
|
||||
|
||||
// Wrap around edges
|
||||
if (this.x < -50) this.x = p.width + 50;
|
||||
if (this.x > p.width + 50) this.x = -50;
|
||||
if (this.y < -50) this.y = p.height + 50;
|
||||
if (this.y > p.height + 50) this.y = -50;
|
||||
}
|
||||
|
||||
display(p: p5, audioLevel: number) {
|
||||
p.noStroke();
|
||||
const dynamicAlpha = p.map(audioLevel, 0, 1, 120, 220);
|
||||
p.fill(255, dynamicAlpha);
|
||||
p.ellipse(this.x, this.y, this.size, this.size);
|
||||
}
|
||||
|
||||
connect(p: p5, other: Particle, maxDist: number) {
|
||||
const d = p.dist(this.x, this.y, other.x, other.y);
|
||||
if (d < maxDist) {
|
||||
const alpha = p.map(d, 0, maxDist, 100, 0);
|
||||
p.stroke(255, alpha);
|
||||
p.strokeWeight(1);
|
||||
p.line(this.x, this.y, other.x, other.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sketch = (p: p5) => {
|
||||
p.setup = () => {
|
||||
p.createCanvas(width, height);
|
||||
p.background(0);
|
||||
|
||||
// Initialize particles
|
||||
particles = [];
|
||||
for (let i = 0; i < numParticles; i++) {
|
||||
particles.push(new Particle(p, i));
|
||||
}
|
||||
};
|
||||
|
||||
p.draw = () => {
|
||||
p.background(0, 30); // Fade effect
|
||||
|
||||
if (isPlaying && analyser) {
|
||||
// Get FFT data (frequency analysis)
|
||||
audioData = analyser.getValue() as Float32Array;
|
||||
|
||||
// Calculate audio metrics from FFT data
|
||||
// FFT values are in decibels (negative values, typically -100 to 0)
|
||||
let sum = 0;
|
||||
let bass = 0;
|
||||
let mid = 0;
|
||||
let treble = 0;
|
||||
|
||||
const bassRange = Math.floor(audioData.length * 0.15); // Low frequencies
|
||||
const midRange = Math.floor(audioData.length * 0.4); // Mid frequencies
|
||||
|
||||
for (let i = 0; i < audioData.length; i++) {
|
||||
// Convert from decibels to linear scale (0-1)
|
||||
// FFT returns values from -100 to 0 dB
|
||||
const normalized = p.map(audioData[i], -100, -30, 0, 1, true);
|
||||
sum += normalized;
|
||||
|
||||
if (i < bassRange) {
|
||||
bass += normalized;
|
||||
} else if (i < midRange) {
|
||||
mid += normalized;
|
||||
} else {
|
||||
treble += normalized;
|
||||
}
|
||||
}
|
||||
|
||||
// Average and amplify moderately
|
||||
let audioLevel = (sum / audioData.length) * 2;
|
||||
bass = (bass / bassRange) * 2.5;
|
||||
mid = (mid / (midRange - bassRange)) * 2;
|
||||
treble = (treble / (audioData.length - midRange)) * 1.5;
|
||||
|
||||
// Clamp values
|
||||
audioLevel = p.constrain(audioLevel, 0, 1);
|
||||
bass = p.constrain(bass, 0, 1);
|
||||
mid = p.constrain(mid, 0, 1);
|
||||
treble = p.constrain(treble, 0, 1);
|
||||
|
||||
// Fixed connection distance for consistency
|
||||
const connectionDist = 100;
|
||||
|
||||
// Update and display particles
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
particles[i].update(p, audioLevel, bass, mid);
|
||||
particles[i].display(p, audioLevel);
|
||||
|
||||
// Connect nearby particles (non-reactive distance)
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
particles[i].connect(p, particles[j], connectionDist);
|
||||
}
|
||||
}
|
||||
} else if (!isPlaying) {
|
||||
// Static state when not playing
|
||||
p.fill(255, 50);
|
||||
p.noStroke();
|
||||
p.textAlign(p.CENTER, p.CENTER);
|
||||
p.textSize(16);
|
||||
p.text('Press Play', p.width / 2, p.height / 2);
|
||||
}
|
||||
};
|
||||
|
||||
p.windowResized = () => {
|
||||
p.resizeCanvas(width, height);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="visualization-container">
|
||||
{#if isPlaying}
|
||||
<P5 {sketch} />
|
||||
{:else}
|
||||
<div class="placeholder" style="width: {width}px; height: {height}px;">
|
||||
<p>Press Play</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.visualization-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.placeholder p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,238 @@
|
||||
<!-- TODO: ADD TONEJS GENERATOR -->
|
||||
<!-- https://www.npmjs.com/package/tonal -->
|
||||
<!-- https://www.npmjs.com/package/tone -->
|
||||
<script lang="ts">
|
||||
import * as Tone from 'tone';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import AudioVisualization from '$lib/components/AudioVisualization.svelte';
|
||||
import { createNoiseSynth } from '$lib/audio/instruments/noiseSynth';
|
||||
import { createReverb, createDelay, createGain, createAnalyser } from '$lib/audio/audio-effects';
|
||||
|
||||
// Component props - air quality values and shared weather params for effects
|
||||
let {
|
||||
dust = 0,
|
||||
pm10 = 0,
|
||||
pm25 = 0,
|
||||
relativeHumidity2m = 50,
|
||||
windSpeed10m = 0,
|
||||
volume = -15
|
||||
} = $props();
|
||||
|
||||
// Component state
|
||||
let isPlaying = $state(false);
|
||||
let isInitialized = $state(false);
|
||||
|
||||
// Audio components
|
||||
let noiseSynth: Tone.NoiseSynth | null = null;
|
||||
let reverb: Tone.Reverb | null = null;
|
||||
let delay: Tone.FeedbackDelay | null = null;
|
||||
let gain: Tone.Gain | null = null;
|
||||
let loop: Tone.Loop | null = null;
|
||||
let analyser: Tone.Analyser | null = null;
|
||||
|
||||
// Derive air quality index from pollution values
|
||||
// Higher values = dirtier air = more frequent bursts
|
||||
const airQualityIndex = $derived.by(() => {
|
||||
// Combine all pollution metrics (weighted average)
|
||||
// PM2.5 is most harmful, so weight it higher
|
||||
const pollutionScore = (pm25 * 2 + pm10 + dust) / 4;
|
||||
return Math.max(0, pollutionScore);
|
||||
});
|
||||
|
||||
// Burst interval: cleaner air = slower, dirtier air = faster (like geiger counter)
|
||||
const burstInterval = $derived.by(() => {
|
||||
// Map pollution: low pollution = 3-6s, high pollution = 0.3-1s
|
||||
// Less intense than before
|
||||
const minInterval = 0.3; // 300ms for very polluted
|
||||
const maxInterval = 6.0; // 6s for clean air
|
||||
const normalizedPollution = Math.min(airQualityIndex / 100, 1); // Normalize to 0-1
|
||||
return maxInterval - normalizedPollution * (maxInterval - minInterval);
|
||||
});
|
||||
|
||||
// Shared delay/reverb parameters (matching WeatherGen)
|
||||
const reverbWet = $derived.by(() => {
|
||||
const humidity = relativeHumidity2m ?? 50;
|
||||
return Math.max(0.3, Math.min(1, humidity / 100));
|
||||
});
|
||||
|
||||
const delayTime = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
return speed > 5 ? '4n' : '8n';
|
||||
});
|
||||
|
||||
const delayFeedback = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
return Math.max(0.2, Math.min(0.7, (speed / 20) * 0.5 + 0.2));
|
||||
});
|
||||
|
||||
// Initialize audio components
|
||||
const initializeAudio = async (): Promise<void> => {
|
||||
try {
|
||||
// Create instruments
|
||||
noiseSynth = createNoiseSynth(volume);
|
||||
|
||||
// Create effects with much more spacious reverb for air quality
|
||||
reverb = new Tone.Reverb({
|
||||
decay: 30, // Very long decay for spacious sound
|
||||
preDelay: 0.1
|
||||
});
|
||||
reverb.wet.value = 0.8; // Higher wet signal for more reverb
|
||||
|
||||
delay = createDelay(delayTime, delayFeedback);
|
||||
gain = createGain(volume);
|
||||
analyser = createAnalyser();
|
||||
|
||||
// Connect audio chain
|
||||
noiseSynth.chain(delay, reverb, gain, analyser, Tone.Destination);
|
||||
|
||||
// Generate reverb impulse
|
||||
await reverb.generate();
|
||||
|
||||
isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize air quality generator:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Start the geiger-counter-like loop
|
||||
const startLoop = async (): Promise<void> => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await initializeAudio();
|
||||
}
|
||||
|
||||
await Tone.start();
|
||||
|
||||
if (loop) {
|
||||
loop.dispose();
|
||||
loop = null;
|
||||
}
|
||||
|
||||
// Create a loop that triggers at random intervals
|
||||
loop = new Tone.Loop((time) => {
|
||||
if (noiseSynth) {
|
||||
// Trigger noise burst
|
||||
noiseSynth.triggerAttackRelease('16n', time);
|
||||
|
||||
// Schedule next burst with randomization
|
||||
const baseInterval = burstInterval;
|
||||
const randomFactor = 0.5 + Math.random(); // 0.5x to 1.5x variation
|
||||
const nextBurstTime = baseInterval * randomFactor;
|
||||
|
||||
if (loop) {
|
||||
loop.interval = nextBurstTime;
|
||||
}
|
||||
}
|
||||
}, burstInterval);
|
||||
|
||||
loop.start(0);
|
||||
|
||||
// Start transport if not already running
|
||||
if (Tone.getTransport().state !== 'started') {
|
||||
Tone.getTransport().start();
|
||||
}
|
||||
|
||||
isPlaying = true;
|
||||
} catch (error) {
|
||||
console.error('Error starting air quality loop:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Stop the loop
|
||||
const stopLoop = (): void => {
|
||||
if (loop) {
|
||||
loop.stop();
|
||||
loop.dispose();
|
||||
loop = null;
|
||||
}
|
||||
// Don't stop transport - let WeatherGen control it
|
||||
isPlaying = false;
|
||||
};
|
||||
|
||||
// Toggle playback
|
||||
const togglePlayback = async (): Promise<void> => {
|
||||
if (isPlaying) {
|
||||
stopLoop();
|
||||
} else {
|
||||
await startLoop();
|
||||
}
|
||||
};
|
||||
|
||||
// Reactive updates for environmental parameters
|
||||
// Note: Reverb wet is fixed at 0.8 for spacious sound, not reactive to humidity
|
||||
|
||||
$effect(() => {
|
||||
if (delay && isInitialized) {
|
||||
delay.delayTime.value = delayTime;
|
||||
delay.feedback.rampTo(delayFeedback, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (gain && isInitialized) {
|
||||
gain.gain.rampTo(Tone.dbToGain(volume), 0.1);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (loop && isPlaying) {
|
||||
// Update loop interval dynamically
|
||||
const baseInterval = burstInterval;
|
||||
loop.interval = baseInterval;
|
||||
}
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMount(() => {
|
||||
initializeAudio();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (loop) {
|
||||
loop.dispose();
|
||||
}
|
||||
if (noiseSynth) {
|
||||
noiseSynth.dispose();
|
||||
}
|
||||
if (reverb) {
|
||||
reverb.dispose();
|
||||
}
|
||||
if (delay) {
|
||||
delay.dispose();
|
||||
}
|
||||
if (gain) {
|
||||
gain.dispose();
|
||||
}
|
||||
if (analyser) {
|
||||
analyser.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="mx-auto mt-8 grid max-w-6xl grid-cols-1 gap-8 border-t border-white/20 p-4 pt-8 md:grid-cols-[300px_1fr]"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<button
|
||||
class="cursor-crosshair rounded-md border border-white/20 px-6 py-3 text-base transition-all duration-300 hover:border-white/40 disabled:cursor-not-allowed disabled:opacity-50 {isPlaying
|
||||
? 'bg-white text-black'
|
||||
: 'bg-transparent text-white'}"
|
||||
onclick={togglePlayback}
|
||||
disabled={!isInitialized}
|
||||
>
|
||||
{isPlaying ? 'Stop' : 'Start'} Air Quality Monitor
|
||||
</button>
|
||||
|
||||
{#if isPlaying}
|
||||
<div class="flex flex-col gap-2 text-sm opacity-80">
|
||||
<p class="m-0">PM2.5: {pm25.toFixed(1)} µg/m³</p>
|
||||
<p class="m-0">PM10: {pm10.toFixed(1)} µg/m³</p>
|
||||
<p class="m-0">Dust: {dust.toFixed(1)} µg/m³</p>
|
||||
<p class="my-1 opacity-40">---</p>
|
||||
<p class="m-0">Pollution Index: {airQualityIndex.toFixed(1)}</p>
|
||||
<p class="m-0">Burst Interval: {burstInterval.toFixed(2)}s</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<AudioVisualization {isPlaying} {analyser} width={400} height={400} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,238 +1,466 @@
|
||||
<script lang="ts">
|
||||
import * as Tone from 'tone';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import * as Tone from 'tone';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import AudioVisualization from '$lib/components/AudioVisualization.svelte';
|
||||
import { createPadSynth } from '$lib/audio/instruments/padSynth';
|
||||
import { createArpSynth } from '$lib/audio/instruments/arpSynth';
|
||||
import { createBassSynth } from '$lib/audio/instruments/bassSynth';
|
||||
import { createPingSynth } from '$lib/audio/instruments/pingSynth';
|
||||
import {
|
||||
createReverb,
|
||||
createDelay,
|
||||
createFilter,
|
||||
createGain,
|
||||
createAnalyser
|
||||
} from '$lib/audio/audio-effects';
|
||||
import { selectChordProgression, calculateComfortScore } from '$lib/audio/weather-mood';
|
||||
import type { ChordProgression } from '$lib/audio/chord-progressions';
|
||||
|
||||
// Component props with default values
|
||||
let {temperature2m = 20, relativeHumidity2m = 50, cloudCover = 30, volume = -10, windSpeed10m = 0, isDay} = $props();
|
||||
// Component props with default values
|
||||
let {
|
||||
temperature2m = 20,
|
||||
relativeHumidity2m = 50,
|
||||
cloudCover = 30,
|
||||
precipitation = 0,
|
||||
volume = -10,
|
||||
windSpeed10m = 0,
|
||||
isDay
|
||||
} = $props();
|
||||
|
||||
// Component state using runes
|
||||
let isPlaying = $state(false);
|
||||
let currentChordIndex = $state(0);
|
||||
let isInitialized = $state(false);
|
||||
// Component state using runes
|
||||
let isPlaying = $state(false);
|
||||
let isInitialized = $state(false);
|
||||
|
||||
// Audio components
|
||||
let synth: Tone.PolySynth | null = null;
|
||||
let reverb: Tone.Reverb | null = null;
|
||||
let delay: Tone.FeedbackDelay | null = null;
|
||||
let phaser: Tone.Phaser | null = null;
|
||||
let sequence: Tone.Sequence | null = null;
|
||||
let gain: Tone.Gain | null = null;
|
||||
// Audio components
|
||||
let synth: Tone.PolySynth | null = null;
|
||||
let arpSynth: Tone.Synth | null = null;
|
||||
let pingSynth: Tone.Synth | null = null;
|
||||
let bassSynth: Tone.Synth | null = null;
|
||||
let arpSequence: Tone.Sequence | null = null;
|
||||
let pingSequence: Tone.Sequence | null = null;
|
||||
let bassSequence: Tone.Sequence | null = null;
|
||||
let reverb: Tone.Reverb | null = null;
|
||||
let delay: Tone.FeedbackDelay | null = null;
|
||||
let filter: Tone.Filter | null = null;
|
||||
let phaser: Tone.Phaser | null = null;
|
||||
let sequence: Tone.Sequence | null = null;
|
||||
let gain: Tone.Gain | null = null;
|
||||
let analyser: Tone.Analyser | null = null;
|
||||
|
||||
// Select chord progression based on weather mood
|
||||
const currentProgression: ChordProgression = $derived.by(() => {
|
||||
return selectChordProgression({
|
||||
temperature2m,
|
||||
relativeHumidity2m,
|
||||
cloudCover,
|
||||
windSpeed10m,
|
||||
precipitation,
|
||||
isDay
|
||||
});
|
||||
});
|
||||
|
||||
//TODO - ADD DIFFERENT PROGRESSIONS
|
||||
const chordProgressions = [
|
||||
[
|
||||
{ time: "0:0:0", notes: ['C4', 'E4', 'G4', 'B4'] },
|
||||
{ time: "0:1:0", notes: ['A3', 'C4', 'E4', 'G4'] },
|
||||
{ time: "0:2:0", notes: ['F3', 'A3', 'C4', 'E4'] },
|
||||
{ time: "0:3:0", notes: ['G3', 'B3', 'D4', 'F4'] }
|
||||
],
|
||||
[
|
||||
{ time: "0:0:0", notes: ['D4', 'F4', 'A4', 'C5'] },
|
||||
{ time: "0:1:0", notes: ['G3', 'B3', 'D4', 'F4'] },
|
||||
{ time: "0:2:0", notes: ['C4', 'E4', 'G4', 'B4'] },
|
||||
{ time: "0:3:0", notes: ['A3', 'C4', 'E4', 'G4'] }
|
||||
],
|
||||
[
|
||||
{ time: "0:0:0", notes: ['E4', 'G4', 'B4', 'D5'] },
|
||||
{ time: "0:1:0", notes: ['A3', 'C4', 'E4', 'G4'] },
|
||||
{ time: "0:2:0", notes: ['D4', 'F4', 'A4', 'C5'] },
|
||||
{ time: "0:3:0", notes: ['G3', 'B3', 'D4', 'F4'] }
|
||||
],
|
||||
[
|
||||
{ time: "0:0:0", notes: ['F3', 'A3', 'C4', 'E4'] },
|
||||
{ time: "0:1:0", notes: ['E4', 'G4', 'B4', 'D5'] },
|
||||
{ time: "0:2:0", notes: ['D4', 'F4', 'A4', 'C5'] },
|
||||
{ time: "0:3:0", notes: ['C4', 'E4', 'G4', 'B4'] }
|
||||
],
|
||||
];
|
||||
// Derived reactive values using runes with safe fallbacks
|
||||
const bpm = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// BPM starts at 10 for 0°C and increases with temperature
|
||||
// Day: more energetic (2x scaling), Night: calmer (1x scaling)
|
||||
const tempAboveZero = Math.max(0, temp);
|
||||
const scaledBpm = isDay ? 10 + tempAboveZero * 2 : 10 + tempAboveZero;
|
||||
return Math.max(10, Math.min(200, scaledBpm));
|
||||
});
|
||||
|
||||
let currentProgression = $state(chordProgressions[0]);
|
||||
const reverbWet = $derived.by(() => {
|
||||
const humidity = relativeHumidity2m ?? 50;
|
||||
// Ensure minimum 0.3 wet signal so reverb is always audible
|
||||
return Math.max(0.3, Math.min(1, humidity / 100));
|
||||
});
|
||||
|
||||
// Derived reactive values using runes
|
||||
const bpm = $derived((isDay? temperature2m * 2 : temperature2m));
|
||||
const reverbWet = $derived(relativeHumidity2m/100);
|
||||
const delayWet = $derived(Math.round(windSpeed10m)/10);
|
||||
const phaserBase = $derived((1 / cloudCover) * 100);
|
||||
// Delay time: 8th note for calm, quarter note for windy
|
||||
const delayTime = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
return speed > 5 ? '4n' : '8n';
|
||||
});
|
||||
|
||||
// Initialize audio components
|
||||
const initializeAudio = async (): Promise<void> => {
|
||||
try {
|
||||
// Create dreamy synth
|
||||
synth = new Tone.PolySynth(Tone.Synth, {
|
||||
oscillator: {
|
||||
type: isDay? 'triangle' : 'sine',
|
||||
},
|
||||
envelope: {
|
||||
attack: 1.5,
|
||||
decay: 1,
|
||||
sustain: 0.7,
|
||||
release: 1.0,
|
||||
},
|
||||
volume: -20,
|
||||
});
|
||||
// Delay feedback: stronger with more wind
|
||||
const delayFeedback = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
// Map 0-20 m/s wind to 0.2-0.7 feedback range
|
||||
return Math.max(0.2, Math.min(0.7, (speed / 20) * 0.5 + 0.2));
|
||||
});
|
||||
|
||||
// Create reverb with long, dreamy tail
|
||||
reverb = new Tone.Reverb({
|
||||
decay: 16,
|
||||
wet: reverbWet,
|
||||
preDelay: 0.5,
|
||||
});
|
||||
// Filter cutoff: more clouds = darker/lower frequency
|
||||
const filterCutoff = $derived.by(() => {
|
||||
const cover = cloudCover ?? 30;
|
||||
// Map cloud cover: 0% clouds = 8000Hz (bright), 100% clouds = 400Hz (dark)
|
||||
return Math.max(400, Math.min(8000, 8000 - (cover / 100) * 7600));
|
||||
});
|
||||
|
||||
// Filter resonance: more wind = more resonant
|
||||
const filterResonance = $derived.by(() => {
|
||||
const speed = windSpeed10m ?? 0;
|
||||
// Map 0-30 m/s wind to 1-18 resonance (Q factor)
|
||||
return Math.max(1, Math.min(18, 1 + (speed / 30) * 17));
|
||||
});
|
||||
|
||||
delay = new Tone.FeedbackDelay({
|
||||
delayTime: '0.5',
|
||||
feedback: delayWet
|
||||
})
|
||||
// Arpeggio interval: slower in cold, faster in heat
|
||||
const arpInterval = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// Map temperature: <0°C = 1n (whole note), 30°C+ = 8n (eighth note)
|
||||
if (temp < 0) return '1n';
|
||||
if (temp < 10) return '2n';
|
||||
if (temp < 20) return '4n';
|
||||
return '8n';
|
||||
});
|
||||
|
||||
// Create a phaser
|
||||
phaser = new Tone.Phaser({
|
||||
frequency : phaserBase,
|
||||
octaves : 5,
|
||||
baseFrequency : 350
|
||||
})
|
||||
// Arpeggio volume: quieter in cold, louder in heat
|
||||
const arpVolume = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// Map temperature: <0°C = -22dB, 30°C+ = -10dB
|
||||
const normalizedTemp = Math.max(0, Math.min(30, temp));
|
||||
return -22 + (normalizedTemp / 30) * 12;
|
||||
});
|
||||
|
||||
// Create gain node
|
||||
gain = new Tone.Gain(Tone.dbToGain(volume));
|
||||
// Weather extremity: 0 = pleasant, 1 = extreme conditions
|
||||
const weatherExtremity = $derived.by(() => {
|
||||
const comfortScore = calculateComfortScore({
|
||||
temperature2m,
|
||||
relativeHumidity2m,
|
||||
cloudCover,
|
||||
windSpeed10m,
|
||||
precipitation,
|
||||
isDay
|
||||
});
|
||||
return 1 - comfortScore; // Invert: higher = more extreme
|
||||
});
|
||||
|
||||
// Connect audio chain
|
||||
synth.connect(phaser).connect(delay).connect(reverb).connect(gain).toDestination();
|
||||
// Ping volume: quieter in pleasant weather, louder in extreme weather
|
||||
const pingVolume = $derived.by(() => {
|
||||
// Map extremity: 0 (pleasant) = -22dB (present), 1 (extreme) = -8dB (prominent)
|
||||
return -22 + weatherExtremity * 14;
|
||||
});
|
||||
|
||||
// Generate reverb impulse
|
||||
await reverb.generate();
|
||||
// Ping interval: slower in cold, faster in heat
|
||||
const pingInterval = $derived.by(() => {
|
||||
const temp = temperature2m ?? 20;
|
||||
// Map temperature: 0°C = 1n (whole note, slow), 30°C = 16n (16th note, fast)
|
||||
if (temp <= 0) return '1n';
|
||||
if (temp <= 10) return '2n';
|
||||
if (temp <= 20) return '4n';
|
||||
if (temp <= 25) return '8n';
|
||||
return '16n';
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
console.log('Audio initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize audio:', error);
|
||||
}
|
||||
};
|
||||
// Initialize audio components
|
||||
const initializeAudio = async (): Promise<void> => {
|
||||
try {
|
||||
// Create instruments
|
||||
synth = createPadSynth(isDay);
|
||||
arpSynth = createArpSynth(arpVolume);
|
||||
pingSynth = createPingSynth(pingVolume);
|
||||
bassSynth = createBassSynth();
|
||||
|
||||
// Start the chord sequence
|
||||
const startSequence = async (): Promise<void> => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await initializeAudio();
|
||||
}
|
||||
// Create effects
|
||||
reverb = createReverb(reverbWet);
|
||||
delay = createDelay(delayTime, delayFeedback);
|
||||
filter = createFilter(filterCutoff, filterResonance);
|
||||
gain = createGain(volume);
|
||||
analyser = createAnalyser();
|
||||
|
||||
await Tone.start();
|
||||
// Connect audio chain using .chain() for clarity
|
||||
synth.chain(filter, delay, reverb, gain, analyser, Tone.Destination);
|
||||
arpSynth.chain(filter, delay, reverb, gain);
|
||||
pingSynth.chain(filter, delay, reverb, gain);
|
||||
bassSynth.chain(delay, reverb, gain);
|
||||
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
// Generate reverb impulse
|
||||
await reverb.generate();
|
||||
|
||||
// Set transport BPM
|
||||
Tone.getTransport().bpm.value = bpm;
|
||||
isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize audio:', error);
|
||||
}
|
||||
};
|
||||
|
||||
let progressionChangeCounter = 0;
|
||||
// Start the chord sequence
|
||||
const startSequence = async (): Promise<void> => {
|
||||
try {
|
||||
if (!isInitialized) {
|
||||
await initializeAudio();
|
||||
}
|
||||
|
||||
sequence = new Tone.Sequence((time: number, chord) => {
|
||||
if (synth && chord) {
|
||||
synth!.triggerAttackRelease(chord.notes, '4n', time);
|
||||
}
|
||||
await Tone.start();
|
||||
|
||||
progressionChangeCounter++;
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
|
||||
//Change progression every full cycle (4 chords) for variation
|
||||
if (progressionChangeCounter >= currentProgression.length) {
|
||||
currentChordIndex = (currentChordIndex + 1) % chordProgressions.length;
|
||||
currentProgression = chordProgressions[currentChordIndex];
|
||||
sequence!.events = currentProgression;
|
||||
progressionChangeCounter = 0;
|
||||
}
|
||||
}, currentProgression, "4n");
|
||||
// Set transport BPM
|
||||
Tone.getTransport().bpm.value = bpm;
|
||||
|
||||
sequence.start(0);
|
||||
Tone.getTransport().start();
|
||||
isPlaying = true;
|
||||
} catch (error) {
|
||||
console.error('Error starting sequence:', error);
|
||||
}
|
||||
};
|
||||
sequence = new Tone.Sequence(
|
||||
(time: number, chord) => {
|
||||
if (synth && chord) {
|
||||
synth!.triggerAttackRelease(chord.notes, '4n', time);
|
||||
}
|
||||
},
|
||||
currentProgression,
|
||||
'4n'
|
||||
);
|
||||
|
||||
// Stop the sequence
|
||||
const stopSequence = (): void => {
|
||||
if (sequence) {
|
||||
sequence.stop();
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
Tone.getTransport().stop();
|
||||
Tone.getTransport().cancel();
|
||||
isPlaying = false;
|
||||
};
|
||||
sequence.start(0);
|
||||
|
||||
// Toggle playback
|
||||
const togglePlayback = async (): Promise<void> => {
|
||||
if (isPlaying) {
|
||||
stopSequence();
|
||||
} else {
|
||||
await startSequence();
|
||||
}
|
||||
};
|
||||
// Create arpeggio sequence
|
||||
if (arpSequence) {
|
||||
arpSequence.dispose();
|
||||
arpSequence = null;
|
||||
}
|
||||
|
||||
// Reactive updates for environmental parameters using effects
|
||||
$effect(() => {
|
||||
if (reverb && isInitialized) {
|
||||
reverb.wet.rampTo(reverbWet, 0.5);
|
||||
}
|
||||
});
|
||||
arpSequence = new Tone.Sequence(
|
||||
(time: number, chord) => {
|
||||
if (arpSynth && chord && chord.notes) {
|
||||
// Play arpeggio pattern through the chord notes
|
||||
chord.notes.forEach((note: string, index: number) => {
|
||||
const noteTime = time + index * 0.15; // 150ms between notes
|
||||
arpSynth!.triggerAttackRelease(note, '16n', noteTime);
|
||||
});
|
||||
}
|
||||
},
|
||||
currentProgression,
|
||||
arpInterval
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (phaser && isInitialized) {
|
||||
phaser.frequency.rampTo(phaserBase, 0.5);
|
||||
}
|
||||
});
|
||||
arpSequence.start(0);
|
||||
|
||||
$effect(() => {
|
||||
if (gain && isInitialized) {
|
||||
gain.gain.rampTo(Tone.dbToGain(volume), 0.1);
|
||||
}
|
||||
});
|
||||
// Create ping sequence (reverse arpeggio - evenly spaced through chord duration)
|
||||
if (pingSequence) {
|
||||
pingSequence.dispose();
|
||||
pingSequence = null;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isPlaying && isInitialized) {
|
||||
Tone.getTransport().bpm.rampTo(bpm, 1.0);
|
||||
}
|
||||
});
|
||||
// Build a flat array of notes: 4 notes per chord, in reverse order, transposed up 2 octaves
|
||||
const pingNotes: string[] = [];
|
||||
currentProgression.forEach((chord) => {
|
||||
if (chord && chord.notes) {
|
||||
// Get 4 notes in reverse order (last to first, cycling if needed)
|
||||
const reversedNotes = [...chord.notes].reverse();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const note = reversedNotes[i % reversedNotes.length];
|
||||
const noteName = note.slice(0, -1);
|
||||
const octave = parseInt(note.slice(-1));
|
||||
const highNote = noteName + (octave + 2);
|
||||
pingNotes.push(highNote);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMount(() => {
|
||||
initializeAudio();
|
||||
});
|
||||
pingSequence = new Tone.Sequence(
|
||||
(time: number, note: string) => {
|
||||
if (pingSynth && note) {
|
||||
pingSynth.triggerAttackRelease(note, '32n', time);
|
||||
}
|
||||
},
|
||||
pingNotes,
|
||||
pingInterval // Temperature-reactive: 1n (cold) to 16n (hot)
|
||||
);
|
||||
|
||||
onDestroy(() => {
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
}
|
||||
if (synth) {
|
||||
synth.dispose();
|
||||
}
|
||||
if (reverb) {
|
||||
reverb.dispose();
|
||||
}
|
||||
if (phaser) {
|
||||
phaser.dispose();
|
||||
}
|
||||
if (gain) {
|
||||
gain.dispose();
|
||||
}
|
||||
});
|
||||
pingSequence.start(0);
|
||||
|
||||
// Create bass sequence
|
||||
if (bassSequence) {
|
||||
bassSequence.dispose();
|
||||
bassSequence = null;
|
||||
}
|
||||
|
||||
bassSequence = new Tone.Sequence(
|
||||
(time: number, chord) => {
|
||||
if (bassSynth && chord && chord.notes && chord.notes.length > 0) {
|
||||
// Get root note (first note of chord) and transpose down 2 octaves
|
||||
const rootNote = chord.notes[0];
|
||||
const noteName = rootNote.slice(0, -1); // e.g., 'C' from 'C4'
|
||||
const octave = parseInt(rootNote.slice(-1)); // e.g., 4 from 'C4'
|
||||
const bassNote = noteName + (octave - 2); // e.g., 'C2'
|
||||
|
||||
// Randomize release time: half to full chord duration
|
||||
// Quarter note = 1 beat, so random between 0.5 and 1.0 beats
|
||||
const randomRelease = 0.5 + Math.random() * 0.5;
|
||||
bassSynth.envelope.release = randomRelease * (60 / bpm);
|
||||
|
||||
// Trigger bass note
|
||||
bassSynth.triggerAttackRelease(bassNote, '4n', time);
|
||||
}
|
||||
},
|
||||
currentProgression,
|
||||
'4n'
|
||||
);
|
||||
|
||||
bassSequence.start(0);
|
||||
|
||||
Tone.getTransport().start();
|
||||
isPlaying = true;
|
||||
} catch (error) {
|
||||
console.error('Error starting sequence:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Stop the sequence
|
||||
const stopSequence = (): void => {
|
||||
if (sequence) {
|
||||
sequence.stop();
|
||||
sequence.dispose();
|
||||
sequence = null;
|
||||
}
|
||||
if (arpSequence) {
|
||||
arpSequence.stop();
|
||||
arpSequence.dispose();
|
||||
arpSequence = null;
|
||||
}
|
||||
if (pingSequence) {
|
||||
pingSequence.stop();
|
||||
pingSequence.dispose();
|
||||
pingSequence = null;
|
||||
}
|
||||
if (bassSequence) {
|
||||
bassSequence.stop();
|
||||
bassSequence.dispose();
|
||||
bassSequence = null;
|
||||
}
|
||||
Tone.getTransport().stop();
|
||||
Tone.getTransport().cancel();
|
||||
isPlaying = false;
|
||||
};
|
||||
|
||||
// Toggle playback
|
||||
const togglePlayback = async (): Promise<void> => {
|
||||
if (isPlaying) {
|
||||
stopSequence();
|
||||
} else {
|
||||
await startSequence();
|
||||
}
|
||||
};
|
||||
|
||||
// Reactive updates for environmental parameters using effects
|
||||
$effect(() => {
|
||||
if (reverb && isInitialized) {
|
||||
reverb.wet.rampTo(reverbWet, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (delay && isInitialized) {
|
||||
delay.delayTime.value = delayTime;
|
||||
delay.feedback.rampTo(delayFeedback, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (filter && isInitialized) {
|
||||
filter.frequency.rampTo(filterCutoff, 1.0);
|
||||
filter.Q.rampTo(filterResonance, 1.0);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (gain && isInitialized) {
|
||||
gain.gain.rampTo(Tone.dbToGain(volume), 0.1);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isPlaying && isInitialized) {
|
||||
Tone.getTransport().bpm.rampTo(bpm, 1.0);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (arpSynth && isInitialized) {
|
||||
arpSynth.volume.rampTo(arpVolume, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (pingSynth && isInitialized) {
|
||||
pingSynth.volume.rampTo(pingVolume, 0.5);
|
||||
}
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMount(() => {
|
||||
initializeAudio();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (sequence) {
|
||||
sequence.dispose();
|
||||
}
|
||||
if (arpSequence) {
|
||||
arpSequence.dispose();
|
||||
}
|
||||
if (pingSequence) {
|
||||
pingSequence.dispose();
|
||||
}
|
||||
if (bassSequence) {
|
||||
bassSequence.dispose();
|
||||
}
|
||||
if (synth) {
|
||||
synth.dispose();
|
||||
}
|
||||
if (arpSynth) {
|
||||
arpSynth.dispose();
|
||||
}
|
||||
if (pingSynth) {
|
||||
pingSynth.dispose();
|
||||
}
|
||||
if (bassSynth) {
|
||||
bassSynth.dispose();
|
||||
}
|
||||
if (reverb) {
|
||||
reverb.dispose();
|
||||
}
|
||||
if (delay) {
|
||||
delay.dispose();
|
||||
}
|
||||
if (filter) {
|
||||
filter.dispose();
|
||||
}
|
||||
if (gain) {
|
||||
gain.dispose();
|
||||
}
|
||||
if (analyser) {
|
||||
analyser.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 p-4 max-w-6xl mx-auto">
|
||||
<div class="flex flex-col gap-4">
|
||||
<button
|
||||
class="px-6 py-3 text-base cursor-crosshair transition-all duration-300 rounded-md border border-white/20 hover:border-white/40 disabled:opacity-50 disabled:cursor-not-allowed {isPlaying
|
||||
? 'bg-white text-black'
|
||||
: 'bg-transparent text-white'}"
|
||||
onclick={togglePlayback}
|
||||
disabled={!isInitialized}
|
||||
>
|
||||
{isPlaying ? 'Stop' : 'Play'} Weather Ambient
|
||||
</button>
|
||||
|
||||
<!-- TODO: ADD VIZUALZ https://www.npmjs.com/package/p5-svelte -->
|
||||
<!-- https://jsfiddle.net/aqilahmisuary/ztf5a72h/#base -->
|
||||
<div class="controls-container">
|
||||
<!-- Playback Control -->
|
||||
<div class="playback-section">
|
||||
<button
|
||||
class="play-button {isPlaying ? 'playing' : ''}"
|
||||
onclick = {togglePlayback}
|
||||
disabled={!isInitialized}
|
||||
>
|
||||
{isPlaying ? 'Stop' : 'Play'} ✨
|
||||
</button>
|
||||
</div>
|
||||
{#if isPlaying}
|
||||
<div class="flex flex-col gap-2 text-sm opacity-80">
|
||||
<p class="m-0">Temperature: {temperature2m.toFixed(1)}°C</p>
|
||||
<p class="m-0">Humidity: {relativeHumidity2m}%</p>
|
||||
<p class="m-0">Cloud Cover: {cloudCover}%</p>
|
||||
<p class="m-0">Wind Speed: {windSpeed10m.toFixed(1)} m/s</p>
|
||||
<p class="opacity-40 my-1">---</p>
|
||||
<p class="m-0">BPM: {bpm}</p>
|
||||
<p class="m-0">Weather Extremity: {(weatherExtremity * 100).toFixed(0)}%</p>
|
||||
<p class="m-0">Reverb: {reverbWet.toFixed(2)}</p>
|
||||
<p class="m-0">Delay: {delayTime} @ {delayFeedback.toFixed(2)} feedback</p>
|
||||
<p class="m-0">Filter: {Math.round(filterCutoff)}Hz Q:{filterResonance.toFixed(1)}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<AudioVisualization {isPlaying} {analyser} width={400} height={400} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
let error: GeolocationError | undefined = $state(undefined);
|
||||
|
||||
let options = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000, // milliseconds
|
||||
maximumAge: 60 * 60 * 1000, // milliseconds
|
||||
enableHighAccuracy: false, // Use WiFi/network location (faster than GPS)
|
||||
timeout: 10000, // milliseconds - increased for better reliability
|
||||
maximumAge: 0, // Don't use cached position
|
||||
};
|
||||
|
||||
$inspect(error);
|
||||
@@ -27,18 +27,18 @@
|
||||
|
||||
<div class="flex min-h-screen flex-col items-center justify-center">
|
||||
{#if !getPosition}
|
||||
<button class="animate-pulse cursor-crosshair text-xl" onclick={flipGetPosition}>
|
||||
<button class="animate-pulse cursor-crosshair text-xl hover:text-blue-400 transition-colors" onclick={flipGetPosition}>
|
||||
Let me show you...
|
||||
</button>
|
||||
{:else if loading}
|
||||
<p class="text-xl">Loading...</p>
|
||||
{:else if error}
|
||||
<p class="text-xl">We can't seem to find you.</p>
|
||||
<p class="text-xl text-red-400">We can't seem to find you.</p>
|
||||
{:else}
|
||||
<p class="mb-5 text-xl">
|
||||
<p class="mb-5 text-xl text-center px-4">
|
||||
Your Position is set as: {position?.coords?.latitude}, {position?.coords?.longitude}
|
||||
</p>
|
||||
<button class="animate-pulse cursor-crosshair text-lg" onclick={goToPlayer}
|
||||
<button class="animate-pulse cursor-crosshair text-lg hover:text-blue-400 transition-colors" onclick={goToPlayer}
|
||||
>Ok, on-out with it</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import WeatherGen from '$lib/generators/weather/WeatherGen.svelte';
|
||||
import AirQualityGen from '$lib/generators/air-quality/AirQualityGen.svelte';
|
||||
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
@@ -7,13 +8,17 @@
|
||||
const currentWeather = data.weatherData.current;
|
||||
const currentAirQuality = data.airQualityData.current;
|
||||
|
||||
|
||||
$inspect(currentWeather)
|
||||
$inspect(currentAirQuality)
|
||||
$inspect(currentWeather);
|
||||
$inspect(currentAirQuality);
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex min-h-screen flex-col items-center justify-center">
|
||||
<WeatherGen {...currentWeather}></WeatherGen>
|
||||
<div class="min-h-screen p-8">
|
||||
<div class="max-w-7xl mx-auto space-y-8">
|
||||
<WeatherGen {...currentWeather}></WeatherGen>
|
||||
<AirQualityGen
|
||||
{...currentAirQuality}
|
||||
relativeHumidity2m={currentWeather.relativeHumidity2m}
|
||||
windSpeed10m={currentWeather.windSpeed10m}
|
||||
></AirQualityGen>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const load: PageLoad = async ({ url }) => {
|
||||
};
|
||||
|
||||
|
||||
const getWeather = async (long: string, lat: string): Promise<WeatherData> => {
|
||||
const getWeather = async (lat: string, long: string): Promise<WeatherData> => {
|
||||
const apiParams = {
|
||||
"latitude": lat,
|
||||
"longitude": long,
|
||||
@@ -45,7 +45,7 @@ const getWeather = async (long: string, lat: string): Promise<WeatherData> => {
|
||||
return weatherData
|
||||
}
|
||||
|
||||
const getAirQuality = async (long: string, lat: string): Promise<AirQualityData> => {
|
||||
const getAirQuality = async (lat: string, long: string): Promise<AirQualityData> => {
|
||||
const params = {
|
||||
"latitude": lat,
|
||||
"longitude": long,
|
||||
|
||||
Reference in New Issue
Block a user