This commit is contained in:
2025-10-10 23:13:57 +02:00
commit 58d1424adb
25 changed files with 1958 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# Svelte + TS + Vite
This template should help get you started developing with Svelte and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
## Need an official Svelte framework?
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
## Technical considerations
**Why use this over SvelteKit?**
- It brings its own routing solution which might not be preferable for some users.
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
**Why include `.vscode/extensions.json`?**
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
**Why enable `allowJs` in the TS template?**
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
**Why is HMR not preserving my local component state?**
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
```ts
// store.ts
// An extremely simple external store
import { writable } from 'svelte/store'
export default writable(0)
```

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vendingmachine</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "vendingmachine",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5",
"@types/node": "^24.6.0",
"svelte": "^5.39.6",
"svelte-check": "^4.3.2",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.1.14"
},
"overrides": {
"vite": "npm:rolldown-vite@7.1.14"
}
}

776
pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,776 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@sveltejs/vite-plugin-svelte':
specifier: ^6.2.1
version: 6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)
'@tsconfig/svelte':
specifier: ^5.0.5
version: 5.0.5
'@types/node':
specifier: ^24.6.0
version: 24.7.1
svelte:
specifier: ^5.39.6
version: 5.39.11
svelte-check:
specifier: ^4.3.2
version: 4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3)
typescript:
specifier: ~5.9.3
version: 5.9.3
vite:
specifier: npm:rolldown-vite@7.1.14
version: rolldown-vite@7.1.14(@types/node@24.7.1)
packages:
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
'@emnapi/runtime@1.5.0':
resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==}
'@emnapi/wasi-threads@1.1.0':
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/wasm-runtime@1.0.7':
resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==}
'@oxc-project/runtime@0.92.0':
resolution: {integrity: sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==}
engines: {node: ^20.19.0 || >=22.12.0}
'@oxc-project/types@0.93.0':
resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==}
'@rolldown/binding-android-arm64@1.0.0-beta.41':
resolution: {integrity: sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-beta.41':
resolution: {integrity: sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-beta.41':
resolution: {integrity: sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-beta.41':
resolution: {integrity: sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41':
resolution: {integrity: sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41':
resolution: {integrity: sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.41':
resolution: {integrity: sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.41':
resolution: {integrity: sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.41':
resolution: {integrity: sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.41':
resolution: {integrity: sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-beta.41':
resolution: {integrity: sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41':
resolution: {integrity: sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41':
resolution: {integrity: sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ia32]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.41':
resolution: {integrity: sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-beta.41':
resolution: {integrity: sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==}
'@sveltejs/acorn-typescript@1.0.6':
resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==}
peerDependencies:
acorn: ^8.9.0
'@sveltejs/vite-plugin-svelte-inspector@5.0.1':
resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==}
engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies:
'@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@sveltejs/vite-plugin-svelte@6.2.1':
resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==}
engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@tsconfig/svelte@5.0.5':
resolution: {integrity: sha512-48fAnUjKye38FvMiNOj0J9I/4XlQQiZlpe9xaNPfe8vy2Y1hFBt8g1yqf2EGjVvHavo4jf2lC+TQyENCr4BJBQ==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@24.7.1':
resolution: {integrity: sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
ansis@4.2.0:
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
engines: {node: '>=14'}
aria-query@5.3.2:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
esm-env@1.2.2:
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
esrap@2.1.0:
resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.30.2:
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.30.2:
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.30.2:
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.30.2:
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.30.2:
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.30.2:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'}
locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
rolldown-vite@7.1.14:
resolution: {integrity: sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
esbuild: ^0.25.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
rolldown@1.0.0-beta.41:
resolution: {integrity: sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
svelte-check@4.3.3:
resolution: {integrity: sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==}
engines: {node: '>= 18.0.0'}
hasBin: true
peerDependencies:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte@5.39.11:
resolution: {integrity: sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.14.0:
resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==}
vitefu@1.1.1:
resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
peerDependenciesMeta:
vite:
optional: true
zimmerframe@1.1.4:
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
snapshots:
'@emnapi/core@1.5.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.5.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.1.0':
dependencies:
tslib: 2.8.1
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/wasm-runtime@1.0.7':
dependencies:
'@emnapi/core': 1.5.0
'@emnapi/runtime': 1.5.0
'@tybys/wasm-util': 0.10.1
optional: true
'@oxc-project/runtime@0.92.0': {}
'@oxc-project/types@0.93.0': {}
'@rolldown/binding-android-arm64@1.0.0-beta.41':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.41':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.41':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.41':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.41':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.41':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.41':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.41':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.41':
dependencies:
'@napi-rs/wasm-runtime': 1.0.7
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41':
optional: true
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.41':
optional: true
'@rolldown/pluginutils@1.0.0-beta.41': {}
'@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)':
dependencies:
acorn: 8.15.0
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11))(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)
debug: 4.4.3
svelte: 5.39.11
vite: rolldown-vite@7.1.14(@types/node@24.7.1)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11))(rolldown-vite@7.1.14(@types/node@24.7.1))(svelte@5.39.11)
debug: 4.4.3
deepmerge: 4.3.1
magic-string: 0.30.19
svelte: 5.39.11
vite: rolldown-vite@7.1.14(@types/node@24.7.1)
vitefu: 1.1.1(rolldown-vite@7.1.14(@types/node@24.7.1))
transitivePeerDependencies:
- supports-color
'@tsconfig/svelte@5.0.5': {}
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
optional: true
'@types/estree@1.0.8': {}
'@types/node@24.7.1':
dependencies:
undici-types: 7.14.0
acorn@8.15.0: {}
ansis@4.2.0: {}
aria-query@5.3.2: {}
axobject-query@4.1.0: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
clsx@2.1.1: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
deepmerge@4.3.1: {}
detect-libc@2.1.2: {}
esm-env@1.2.2: {}
esrap@2.1.0:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fsevents@2.3.3:
optional: true
is-reference@3.0.3:
dependencies:
'@types/estree': 1.0.8
lightningcss-android-arm64@1.30.2:
optional: true
lightningcss-darwin-arm64@1.30.2:
optional: true
lightningcss-darwin-x64@1.30.2:
optional: true
lightningcss-freebsd-x64@1.30.2:
optional: true
lightningcss-linux-arm-gnueabihf@1.30.2:
optional: true
lightningcss-linux-arm64-gnu@1.30.2:
optional: true
lightningcss-linux-arm64-musl@1.30.2:
optional: true
lightningcss-linux-x64-gnu@1.30.2:
optional: true
lightningcss-linux-x64-musl@1.30.2:
optional: true
lightningcss-win32-arm64-msvc@1.30.2:
optional: true
lightningcss-win32-x64-msvc@1.30.2:
optional: true
lightningcss@1.30.2:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.30.2
lightningcss-darwin-arm64: 1.30.2
lightningcss-darwin-x64: 1.30.2
lightningcss-freebsd-x64: 1.30.2
lightningcss-linux-arm-gnueabihf: 1.30.2
lightningcss-linux-arm64-gnu: 1.30.2
lightningcss-linux-arm64-musl: 1.30.2
lightningcss-linux-x64-gnu: 1.30.2
lightningcss-linux-x64-musl: 1.30.2
lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2
locate-character@3.0.0: {}
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mri@1.2.0: {}
ms@2.1.3: {}
nanoid@3.3.11: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
readdirp@4.1.2: {}
rolldown-vite@7.1.14(@types/node@24.7.1):
dependencies:
'@oxc-project/runtime': 0.92.0
fdir: 6.5.0(picomatch@4.0.3)
lightningcss: 1.30.2
picomatch: 4.0.3
postcss: 8.5.6
rolldown: 1.0.0-beta.41
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.7.1
fsevents: 2.3.3
rolldown@1.0.0-beta.41:
dependencies:
'@oxc-project/types': 0.93.0
'@rolldown/pluginutils': 1.0.0-beta.41
ansis: 4.2.0
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-beta.41
'@rolldown/binding-darwin-arm64': 1.0.0-beta.41
'@rolldown/binding-darwin-x64': 1.0.0-beta.41
'@rolldown/binding-freebsd-x64': 1.0.0-beta.41
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.41
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.41
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.41
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.41
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.41
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.41
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.41
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.41
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.41
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.41
sade@1.8.1:
dependencies:
mri: 1.2.0
source-map-js@1.2.1: {}
svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.39.11)(typescript@5.9.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
chokidar: 4.0.3
fdir: 6.5.0(picomatch@4.0.3)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.39.11
typescript: 5.9.3
transitivePeerDependencies:
- picomatch
svelte@5.39.11:
dependencies:
'@jridgewell/remapping': 2.3.5
'@jridgewell/sourcemap-codec': 1.5.5
'@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0)
'@types/estree': 1.0.8
acorn: 8.15.0
aria-query: 5.3.2
axobject-query: 4.1.0
clsx: 2.1.1
esm-env: 1.2.2
esrap: 2.1.0
is-reference: 3.0.3
locate-character: 3.0.0
magic-string: 0.30.19
zimmerframe: 1.1.4
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tslib@2.8.1:
optional: true
typescript@5.9.3: {}
undici-types@7.14.0: {}
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)
zimmerframe@1.1.4: {}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

285
src/App.svelte Normal file
View File

@ -0,0 +1,285 @@
<script lang="ts">
import { onMount } from 'svelte';
import WaveformDisplay from './lib/components/WaveformDisplay.svelte';
import VUMeter from './lib/components/VUMeter.svelte';
import { TwoOpFM, type TwoOpFMParams } from './lib/audio/engines/TwoOpFM';
import { AudioService } from './lib/audio/services/AudioService';
import { downloadWAV } from './lib/audio/utils/WAVEncoder';
import { loadVolume, saveVolume, loadDuration, saveDuration } from './lib/utils/settings';
import { generateRandomColor } from './lib/utils/colors';
let currentMode = 'Mode 1';
const modes = ['Mode 1', 'Mode 2', 'Mode 3'];
const engine = new TwoOpFM();
const audioService = new AudioService();
let currentParams: TwoOpFMParams | null = null;
let currentBuffer: AudioBuffer | null = null;
let duration = loadDuration();
let volume = loadVolume();
let playbackPosition = 0;
let waveformColor = generateRandomColor();
onMount(() => {
audioService.setVolume(volume);
audioService.setPlaybackUpdateCallback((position) => {
playbackPosition = position;
});
generateRandom();
});
function generateRandom() {
currentParams = engine.randomParams();
waveformColor = generateRandomColor();
regenerateBuffer();
}
function mutate() {
if (!currentParams) {
generateRandom();
return;
}
currentParams = engine.mutateParams(currentParams);
waveformColor = generateRandomColor();
regenerateBuffer();
}
function regenerateBuffer() {
if (!currentParams) return;
const sampleRate = audioService.getSampleRate();
const data = engine.generate(currentParams, sampleRate, duration);
currentBuffer = audioService.createAudioBuffer(data);
audioService.play(currentBuffer);
}
function replaySound() {
if (currentBuffer) {
audioService.play(currentBuffer);
}
}
function download() {
if (!currentBuffer) return;
downloadWAV(currentBuffer, 'synth-sound.wav');
}
function handleVolumeChange(event: Event) {
const target = event.target as HTMLInputElement;
volume = parseFloat(target.value);
audioService.setVolume(volume);
saveVolume(volume);
}
function handleDurationChange(event: Event) {
const target = event.target as HTMLInputElement;
duration = parseFloat(target.value);
saveDuration(duration);
}
</script>
<div class="container">
<div class="top-bar">
<div class="mode-buttons">
{#each modes as mode}
<button
class:active={currentMode === mode}
onclick={() => currentMode = mode}
>
{mode}
</button>
{/each}
</div>
<div class="controls-group">
<div class="slider-control duration-slider">
<label for="duration">Duration: {duration.toFixed(2)}s</label>
<input
id="duration"
type="range"
min="0.05"
max="8"
step="0.01"
value={duration}
oninput={handleDurationChange}
/>
</div>
<div class="slider-control">
<label for="volume">Volume</label>
<input
id="volume"
type="range"
min="0"
max="1"
step="0.01"
value={volume}
oninput={handleVolumeChange}
/>
</div>
</div>
</div>
<div class="main-area">
<div class="waveform-container">
<WaveformDisplay
buffer={currentBuffer}
color={waveformColor}
playbackPosition={playbackPosition}
onclick={replaySound}
/>
<div class="bottom-controls">
<button onclick={generateRandom}>Random</button>
<button onclick={mutate}>Mutate</button>
<button onclick={download}>Download</button>
</div>
</div>
<div class="vu-meter-container">
<VUMeter
buffer={currentBuffer}
playbackPosition={playbackPosition}
/>
</div>
</div>
</div>
<style>
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.5rem;
background-color: #1a1a1a;
border-bottom: 1px solid #333;
}
.mode-buttons {
display: flex;
gap: 0.5rem;
}
.mode-buttons button {
opacity: 0.7;
}
.mode-buttons button.active {
opacity: 1;
border-color: #646cff;
}
.controls-group {
display: flex;
gap: 1rem;
align-items: center;
}
.slider-control {
display: flex;
align-items: center;
gap: 0.5rem;
}
.slider-control label {
font-size: 0.9rem;
white-space: nowrap;
}
.slider-control input[type="range"] {
width: 100px;
}
.duration-slider input[type="range"] {
width: 300px;
}
.main-area {
flex: 1;
display: flex;
background-color: #0a0a0a;
overflow: hidden;
}
.waveform-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.vu-meter-container {
width: 5%;
min-width: 40px;
max-width: 80px;
border-left: 1px solid #333;
}
.bottom-controls {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 1rem;
}
input[type="range"] {
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background: transparent;
height: 20px;
border-radius: 0;
}
input[type="range"]::-webkit-slider-track {
background: #333;
height: 4px;
border: 1px solid #444;
border-radius: 0;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
background: #fff;
border: 1px solid #000;
border-radius: 0;
cursor: pointer;
margin-top: -6px;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #ddd;
}
input[type="range"]::-moz-range-track {
background: #333;
height: 4px;
border: 1px solid #444;
border-radius: 0;
}
input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
background: #fff;
border: 1px solid #000;
border-radius: 0;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb:hover {
background: #ddd;
}
</style>

69
src/app.css Normal file
View File

@ -0,0 +1,69 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#app {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
button {
border: 1px solid transparent;
border-radius: 0;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

1
src/assets/svelte.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

10
src/lib/Counter.svelte Normal file
View File

@ -0,0 +1,10 @@
<script lang="ts">
let count: number = $state(0)
const increment = () => {
count += 1
}
</script>
<button onclick={increment}>
count is {count}
</button>

View File

@ -0,0 +1,10 @@
// Synthesis engines generate audio buffers with given parameters
// The duration parameter should be used to scale time-based parameters (envelopes, LFOs, etc.)
// Time-based parameters should be stored as ratios (0-1) and scaled by duration during generation
// Engines must generate stereo output: [leftChannel, rightChannel]
export interface SynthEngine<T = any> {
name: string;
generate(params: T, sampleRate: number, duration: number): [Float32Array, Float32Array];
randomParams(): T;
mutateParams(params: T, mutationAmount?: number): T;
}

View File

@ -0,0 +1,123 @@
import type { SynthEngine } from './SynthEngine';
export interface TwoOpFMParams {
carrierFreq: number;
modRatio: number;
modIndex: number;
attack: number; // 0-1, ratio of total duration
decay: number; // 0-1, ratio of total duration
sustain: number; // 0-1, amplitude level
release: number; // 0-1, ratio of total duration
vibratoRate: number; // Hz
vibratoDepth: number; // 0-1, pitch modulation depth
stereoWidth: number; // 0-1, amount of stereo separation
}
export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
name = '2-OP FM';
generate(params: TwoOpFMParams, sampleRate: number, duration: number): [Float32Array, Float32Array] {
const numSamples = Math.floor(sampleRate * duration);
const leftBuffer = new Float32Array(numSamples);
const rightBuffer = new Float32Array(numSamples);
const TAU = Math.PI * 2;
const detune = 1 + (params.stereoWidth * 0.002);
const leftFreq = params.carrierFreq / detune;
const rightFreq = params.carrierFreq * detune;
const modulatorFreq = params.carrierFreq * params.modRatio;
let carrierPhaseL = 0;
let carrierPhaseR = Math.PI * params.stereoWidth * 0.1;
let modulatorPhaseL = 0;
let modulatorPhaseR = 0;
let vibratoPhaseL = 0;
let vibratoPhaseR = Math.PI * params.stereoWidth * 0.3;
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const envelope = this.calculateEnvelope(t, duration, params);
const vibratoL = Math.sin(vibratoPhaseL) * params.vibratoDepth;
const vibratoR = Math.sin(vibratoPhaseR) * params.vibratoDepth;
const carrierFreqL = leftFreq * (1 + vibratoL);
const carrierFreqR = rightFreq * (1 + vibratoR);
const modulatorL = Math.sin(modulatorPhaseL);
const modulatorR = Math.sin(modulatorPhaseR);
const carrierL = Math.sin(carrierPhaseL + params.modIndex * modulatorL);
const carrierR = Math.sin(carrierPhaseR + params.modIndex * modulatorR);
leftBuffer[i] = carrierL * envelope;
rightBuffer[i] = carrierR * envelope;
carrierPhaseL += (TAU * carrierFreqL) / sampleRate;
carrierPhaseR += (TAU * carrierFreqR) / sampleRate;
modulatorPhaseL += (TAU * modulatorFreq) / sampleRate;
modulatorPhaseR += (TAU * modulatorFreq) / sampleRate;
vibratoPhaseL += (TAU * params.vibratoRate) / sampleRate;
vibratoPhaseR += (TAU * params.vibratoRate) / sampleRate;
}
return [leftBuffer, rightBuffer];
}
private calculateEnvelope(t: number, duration: number, params: TwoOpFMParams): number {
const attackTime = params.attack * duration;
const decayTime = params.decay * duration;
const releaseTime = params.release * duration;
const sustainStart = attackTime + decayTime;
const releaseStart = duration - releaseTime;
if (t < attackTime) {
return t / attackTime;
} else if (t < sustainStart) {
const decayProgress = (t - attackTime) / decayTime;
return 1 - decayProgress * (1 - params.sustain);
} else if (t < releaseStart) {
return params.sustain;
} else {
const releaseProgress = (t - releaseStart) / releaseTime;
return params.sustain * (1 - releaseProgress);
}
}
randomParams(): TwoOpFMParams {
return {
carrierFreq: this.randomRange(100, 800),
modRatio: this.randomRange(0.5, 8),
modIndex: this.randomRange(0, 10),
attack: this.randomRange(0.01, 0.15),
decay: this.randomRange(0.05, 0.2),
sustain: this.randomRange(0.3, 0.9),
release: this.randomRange(0.1, 0.4),
vibratoRate: this.randomRange(3, 8),
vibratoDepth: this.randomRange(0, 0.03),
stereoWidth: this.randomRange(0.3, 0.8),
};
}
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15): TwoOpFMParams {
return {
carrierFreq: this.mutateValue(params.carrierFreq, mutationAmount, 50, 1000),
modRatio: this.mutateValue(params.modRatio, mutationAmount, 0.25, 10),
modIndex: this.mutateValue(params.modIndex, mutationAmount, 0, 15),
attack: this.mutateValue(params.attack, mutationAmount, 0.001, 0.3),
decay: this.mutateValue(params.decay, mutationAmount, 0.01, 0.4),
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 1.0),
release: this.mutateValue(params.release, mutationAmount, 0.05, 0.6),
vibratoRate: this.mutateValue(params.vibratoRate, mutationAmount, 2, 12),
vibratoDepth: this.mutateValue(params.vibratoDepth, mutationAmount, 0, 0.05),
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0.0, 1.0),
};
}
private randomRange(min: number, max: number): number {
return min + Math.random() * (max - min);
}
private 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));
}
}

View File

@ -0,0 +1,97 @@
const DEFAULT_SAMPLE_RATE = 44100;
export class AudioService {
private context: AudioContext | null = null;
private currentSource: AudioBufferSourceNode | null = null;
private gainNode: GainNode | null = null;
private startTime = 0;
private isPlaying = false;
private onPlaybackUpdate: ((position: number) => void) | null = null;
private animationFrameId: number | null = null;
private getContext(): AudioContext {
if (!this.context) {
this.context = new AudioContext({ sampleRate: DEFAULT_SAMPLE_RATE });
this.gainNode = this.context.createGain();
this.gainNode.connect(this.context.destination);
}
return this.context;
}
getSampleRate(): number {
return DEFAULT_SAMPLE_RATE;
}
setVolume(volume: number): void {
if (this.gainNode) {
this.gainNode.gain.value = Math.max(0, Math.min(1, volume));
}
}
setPlaybackUpdateCallback(callback: ((position: number) => void) | null): void {
this.onPlaybackUpdate = callback;
}
createAudioBuffer(stereoData: [Float32Array, Float32Array]): AudioBuffer {
const ctx = this.getContext();
const [leftChannel, rightChannel] = stereoData;
const buffer = ctx.createBuffer(2, leftChannel.length, DEFAULT_SAMPLE_RATE);
buffer.copyToChannel(leftChannel, 0);
buffer.copyToChannel(rightChannel, 1);
return buffer;
}
play(buffer: AudioBuffer): void {
this.stop();
const ctx = this.getContext();
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(this.gainNode!);
this.startTime = ctx.currentTime;
this.isPlaying = true;
source.onended = () => {
this.isPlaying = false;
if (this.onPlaybackUpdate) {
this.onPlaybackUpdate(0);
}
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
};
source.start();
this.currentSource = source;
this.updatePlaybackPosition();
}
private updatePlaybackPosition(): void {
if (!this.isPlaying || !this.context || !this.onPlaybackUpdate) {
return;
}
const elapsed = this.context.currentTime - this.startTime;
this.onPlaybackUpdate(elapsed);
this.animationFrameId = requestAnimationFrame(() => this.updatePlaybackPosition());
}
stop(): void {
if (this.currentSource) {
try {
this.currentSource.stop();
} catch {
// Already stopped
}
this.currentSource = null;
}
this.isPlaying = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
}

View File

@ -0,0 +1,82 @@
const WAV_HEADER_SIZE = 44;
const IEEE_FLOAT_FORMAT = 3;
const BIT_DEPTH = 32;
export function encodeWAV(buffer: AudioBuffer): ArrayBuffer {
const numChannels = buffer.numberOfChannels;
const sampleRate = buffer.sampleRate;
const bytesPerSample = BIT_DEPTH / 8;
const blockAlign = numChannels * bytesPerSample;
const channelData: Float32Array[] = [];
for (let i = 0; i < numChannels; i++) {
const data = new Float32Array(buffer.length);
buffer.copyFromChannel(data, i);
channelData.push(data);
}
const dataLength = buffer.length * numChannels * bytesPerSample;
const bufferLength = WAV_HEADER_SIZE + dataLength;
const arrayBuffer = new ArrayBuffer(bufferLength);
const view = new DataView(arrayBuffer);
writeWAVHeader(view, numChannels, sampleRate, blockAlign, dataLength);
writePCMData(view, channelData, WAV_HEADER_SIZE);
return arrayBuffer;
}
function writeWAVHeader(
view: DataView,
numChannels: number,
sampleRate: number,
blockAlign: number,
dataLength: number
): void {
const writeString = (offset: number, string: string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(0, 'RIFF');
view.setUint32(4, 36 + dataLength, true);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, IEEE_FLOAT_FORMAT, true);
view.setUint16(22, numChannels, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * blockAlign, true);
view.setUint16(32, blockAlign, true);
view.setUint16(34, BIT_DEPTH, true);
writeString(36, 'data');
view.setUint32(40, dataLength, true);
}
function writePCMData(view: DataView, channelData: Float32Array[], startOffset: number): void {
const numChannels = channelData.length;
const numSamples = channelData[0].length;
let offset = startOffset;
for (let i = 0; i < numSamples; i++) {
for (let channel = 0; channel < numChannels; channel++) {
const sample = Math.max(-1, Math.min(1, channelData[channel][i]));
view.setFloat32(offset, sample, true);
offset += 4;
}
}
}
export function downloadWAV(buffer: AudioBuffer, filename: string = 'sound.wav'): void {
const wav = encodeWAV(buffer);
const blob = new Blob([wav], { type: 'audio/wav' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}

View File

@ -0,0 +1,158 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
buffer: AudioBuffer | null;
playbackPosition?: number;
}
let { buffer, playbackPosition = 0 }: Props = $props();
let canvas: HTMLCanvasElement;
onMount(() => {
const resizeObserver = new ResizeObserver(() => {
if (canvas) {
updateCanvasSize();
draw();
}
});
resizeObserver.observe(canvas.parentElement!);
return () => resizeObserver.disconnect();
});
$effect(() => {
buffer;
playbackPosition;
draw();
});
function updateCanvasSize() {
const parent = canvas.parentElement!;
canvas.width = parent.clientWidth;
canvas.height = parent.clientHeight;
}
function calculateLevels(): [number, number] {
if (!buffer) return [-Infinity, -Infinity];
const numChannels = buffer.numberOfChannels;
const duration = buffer.length / buffer.sampleRate;
if (playbackPosition <= 0 || playbackPosition >= duration) {
return [-Infinity, -Infinity];
}
const windowSize = Math.floor(buffer.sampleRate * 0.05);
const currentSample = Math.floor(playbackPosition * buffer.sampleRate);
const startSample = Math.max(0, currentSample - windowSize);
const endSample = Math.min(buffer.length, currentSample);
const leftData = new Float32Array(endSample - startSample);
buffer.copyFromChannel(leftData, 0, startSample);
let leftSum = 0;
for (let i = 0; i < leftData.length; i++) {
leftSum += leftData[i] * leftData[i];
}
const leftRMS = Math.sqrt(leftSum / leftData.length);
const leftDB = leftRMS > 0 ? 20 * Math.log10(leftRMS) : -Infinity;
let rightDB = leftDB;
if (numChannels > 1) {
const rightData = new Float32Array(endSample - startSample);
buffer.copyFromChannel(rightData, 1, startSample);
let rightSum = 0;
for (let i = 0; i < rightData.length; i++) {
rightSum += rightData[i] * rightData[i];
}
const rightRMS = Math.sqrt(rightSum / rightData.length);
rightDB = rightRMS > 0 ? 20 * Math.log10(rightRMS) : -Infinity;
}
return [leftDB, rightDB];
}
function draw() {
if (!canvas) return;
const ctx = canvas.getContext('2d')!;
const width = canvas.width;
const height = canvas.height;
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, width, height);
if (!buffer) return;
const [leftDB, rightDB] = calculateLevels();
const channelWidth = width / 2;
drawChannel(ctx, 0, leftDB, channelWidth, height);
drawChannel(ctx, channelWidth, rightDB, channelWidth, height);
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(channelWidth, 0);
ctx.lineTo(channelWidth, height);
ctx.stroke();
}
function dbToY(db: number, height: number): number {
const minDB = -60;
const maxDB = 0;
const clampedDB = Math.max(minDB, Math.min(maxDB, db));
const normalized = (clampedDB - minDB) / (maxDB - minDB);
return height - (normalized * height);
}
function drawChannel(ctx: CanvasRenderingContext2D, x: number, levelDB: number, width: number, height: number) {
const gridMarks = [0, -3, -6, -10, -20, -40, -60];
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
for (const db of gridMarks) {
const y = dbToY(db, height);
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.stroke();
}
if (levelDB === -Infinity) return;
const segments = [
{ startDB: -60, endDB: -18, color: '#00ff00' },
{ startDB: -18, endDB: -6, color: '#ffff00' },
{ startDB: -6, endDB: 0, color: '#ff0000' }
];
for (const segment of segments) {
if (levelDB >= segment.startDB) {
const startY = dbToY(segment.startDB, height);
const endY = dbToY(segment.endDB, height);
const clampedLevelDB = Math.min(levelDB, segment.endDB);
const levelY = dbToY(clampedLevelDB, height);
const segmentHeight = startY - levelY;
if (segmentHeight > 0) {
ctx.fillStyle = segment.color;
ctx.fillRect(x, levelY, width, segmentHeight);
}
}
}
}
</script>
<canvas bind:this={canvas}></canvas>
<style>
canvas {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,126 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
buffer: AudioBuffer | null;
color?: string;
playbackPosition?: number;
onclick?: () => void;
}
let { buffer, color = '#646cff', playbackPosition = 0, onclick }: Props = $props();
let canvas: HTMLCanvasElement;
onMount(() => {
const resizeObserver = new ResizeObserver(() => {
if (canvas) {
updateCanvasSize();
draw();
}
});
resizeObserver.observe(canvas.parentElement!);
return () => resizeObserver.disconnect();
});
$effect(() => {
buffer;
color;
playbackPosition;
draw();
});
function updateCanvasSize() {
const parent = canvas.parentElement!;
canvas.width = parent.clientWidth;
canvas.height = parent.clientHeight;
}
function handleClick() {
if (onclick) {
onclick();
}
}
function draw() {
if (!canvas) return;
const ctx = canvas.getContext('2d')!;
const width = canvas.width;
const height = canvas.height;
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, width, height);
if (!buffer) {
ctx.fillStyle = '#666';
ctx.font = '24px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('No waveform generated', width / 2, height / 2);
return;
}
const numChannels = buffer.numberOfChannels;
const channelHeight = height / numChannels;
const step = Math.ceil(buffer.length / width);
for (let channel = 0; channel < numChannels; channel++) {
const data = new Float32Array(buffer.length);
buffer.copyFromChannel(data, channel);
const channelTop = channel * channelHeight;
const channelCenter = channelTop + channelHeight / 2;
const amp = channelHeight / 2;
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < width; i++) {
const index = i * step;
const value = data[index] || 0;
const y = channelCenter - value * amp;
if (i === 0) {
ctx.moveTo(i, y);
} else {
ctx.lineTo(i, y);
}
}
ctx.stroke();
if (channel < numChannels - 1) {
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, channelTop + channelHeight);
ctx.lineTo(width, channelTop + channelHeight);
ctx.stroke();
}
}
if (playbackPosition > 0 && buffer) {
const duration = buffer.length / buffer.sampleRate;
const x = (playbackPosition / duration) * width;
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
}
</script>
<canvas bind:this={canvas} onclick={handleClick} style="cursor: pointer;"></canvas>
<style>
canvas {
width: 100%;
height: 100%;
}
</style>

6
src/lib/utils/colors.ts Normal file
View File

@ -0,0 +1,6 @@
export function generateRandomColor(): string {
const hue = Math.floor(Math.random() * 360);
const saturation = 60 + Math.floor(Math.random() * 30);
const lightness = 50 + Math.floor(Math.random() * 20);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}

25
src/lib/utils/settings.ts Normal file
View File

@ -0,0 +1,25 @@
const DEFAULT_VOLUME = 0.7;
const DEFAULT_DURATION = 1.0;
const STORAGE_KEYS = {
VOLUME: 'volume',
DURATION: 'duration',
} as const;
export function loadVolume(): number {
const stored = localStorage.getItem(STORAGE_KEYS.VOLUME);
return stored ? parseFloat(stored) : DEFAULT_VOLUME;
}
export function saveVolume(volume: number): void {
localStorage.setItem(STORAGE_KEYS.VOLUME, volume.toString());
}
export function loadDuration(): number {
const stored = localStorage.getItem(STORAGE_KEYS.DURATION);
return stored ? parseFloat(stored) : DEFAULT_DURATION;
}
export function saveDuration(duration: number): void {
localStorage.setItem(STORAGE_KEYS.DURATION, duration.toString());
}

9
src/main.ts Normal file
View File

@ -0,0 +1,9 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app')!,
})
export default app

8
svelte.config.js Normal file
View File

@ -0,0 +1,8 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

21
tsconfig.app.json Normal file
View File

@ -0,0 +1,21 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"types": ["svelte", "vite/client"],
"noEmit": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"moduleDetection": "force"
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

26
tsconfig.node.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
})