Skip to content

Commit bf9ebb6

Browse files
committed
feat(workspace): Add Storybook docs and interpolation strategy packages
1 parent f888b71 commit bf9ebb6

128 files changed

Lines changed: 9331 additions & 869 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copilot Workspace Instructions
2+
3+
## Local Environment
4+
5+
- Operating system: macOS
6+
- Preferred shell: zsh
7+
- `rg` (ripgrep) is not available in this environment
8+
9+
## Terminal Command Guidance
10+
11+
- Use `zsh`-compatible commands and syntax.
12+
- Do not assume `rg` exists.
13+
- For file discovery, use alternatives such as:
14+
- `find . -type f`
15+
- `ls -R`
16+
- For text search, use alternatives such as:
17+
- `grep -R "pattern" .`
18+
- `grep -Rin "pattern" .`
19+
20+
## Behavior Expectation
21+
22+
- Always adapt command suggestions and scripts to this macOS + zsh setup.
23+
- If a command example would normally use `rg`, replace it with `find`/`grep` equivalents.
24+
- Keep documentation synchronized with code changes. When behavior, public APIs, defaults, or workflows change, update the relevant README/docs files in the same change.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
description: 'Use when modifying public behavior, APIs, package exports, configuration, or developer workflows. Keep docs synchronized with code changes.'
3+
applyTo: '**/*.{ts,tsx,js,jsx,mjs,cjs,json,yml,yaml}'
4+
---
5+
6+
# Documentation Maintenance
7+
8+
When code changes alter behavior, APIs, configuration, defaults, workflows, or package structure, update relevant documentation in the same change.
9+
10+
## Required actions
11+
12+
- Update package README files when user-facing usage, options, defaults, or examples change.
13+
- Update docs indexes and per-API docs under `packages/*/docs/` when public exports change.
14+
- Ensure examples compile conceptually with current constructor signatures and option names.
15+
- Add or adjust cross-links so new docs are discoverable from related READMEs.
16+
- If no documentation changes are needed, explicitly state why in the PR or summary.
17+
18+
## Minimum checklist for API-affecting changes
19+
20+
- `packages/core/README.md` reviewed
21+
- `packages/core/docs/README.md` reviewed
22+
- `packages/audio-worklet/README.md` reviewed (if worklet behavior is affected)
23+
- `packages/audio-worklet/docs/README.md` reviewed (if worklet exports are affected)
24+
- Relevant strategy package README reviewed when interpolation behavior or registration changes

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules
22
dist
3+
.dist
34
.nx
45
*.tsbuildinfo
56
.idea

AGENTS.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# SoundTouchJS Monorepo
22

3+
## Local Environment
4+
5+
- OS: macOS
6+
- Shell: zsh
7+
- `rg` (ripgrep) is not installed
8+
- Prefer `find` and `grep` when searching files/content
9+
310
## Architecture
411

512
Nx monorepo with pnpm workspaces and TypeScript project references.
@@ -32,14 +39,22 @@ pnpm dev # Start demo dev server (Vite on port 8080)
3239
pnpm prettier # Format all files
3340
```
3441

42+
Test execution rule:
43+
44+
- Always run Vitest with `--run` (never watch mode), e.g. `pnpm exec vitest --run`.
45+
3546
Individual project commands:
3647

3748
```sh
38-
pnpm nx build core # Build the library (TSC → packages/core/dist/)
49+
pnpm nx build core # Build the library (TSC → packages/core/.dist/)
3950
pnpm nx build demo # Build the demo app (Vite → apps/demo/dist/)
4051
pnpm nx dev demo # Dev server with HMR
4152
```
4253

54+
Package build output convention:
55+
56+
- Publishable packages build to `.dist/` (dot-prefixed) and publish ESM outputs from there.
57+
4358
## Conventions
4459

4560
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) with **sentence-case** subjects, enforced by commitlint + husky
@@ -48,11 +63,12 @@ pnpm nx dev demo # Dev server with HMR
4863
- Workspace dependency uses `"workspace:*"` protocol (pnpm)
4964
- CI runs on GitHub Actions (`.github/workflows/main.yml`)
5065
- Pre-commit hook runs typecheck (skipped in CI via `$CI` env var)
66+
- Keep docs up to date with code changes: update affected README/docs files whenever public behavior, APIs, defaults, or workflows are changed
5167

5268
## Do Not
5369

5470
- Do not add comments or docstrings to code unless the logic is non-obvious
5571
- Do not use CommonJS (`require`/`module.exports`) anywhere
5672
- Do not use `any` type — use `unknown` with narrowing or proper generics
5773
- Do not add dependencies to the core library — it has zero runtime dependencies
58-
- Do not commit `dist/`, `.nx/`, or `*.tsbuildinfo` files
74+
- Do not commit `.dist/`, `dist/`, `.nx/`, or `*.tsbuildinfo` files

README.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ A real-time audio processing library for pitch shifting, tempo adjustment, and r
44

55
## Monorepo
66

7-
This project is an [Nx](https://nx.dev) monorepo managed with [pnpm](https://pnpm.io/) workspaces. It publishes two packages:
7+
This project is an [Nx](https://nx.dev) monorepo managed with [pnpm](https://pnpm.io/) workspaces. It publishes four packages:
88

9-
| Package | npm | Description |
10-
| ----------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------ |
11-
| [`@soundtouchjs/core`](packages/core/README.md) | `npm install @soundtouchjs/core` | Core processing library — `SoundTouch`, `PitchShifter`, buffers, filters |
12-
| [`@soundtouchjs/audio-worklet`](packages/audio-worklet/README.md) | `npm install @soundtouchjs/audio-worklet` | AudioWorklet implementation with `AudioParam`-based controls |
9+
| Package | npm | Description |
10+
| -------------------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
11+
| [`@soundtouchjs/core`](packages/core/README.md) | `npm install @soundtouchjs/core` | Core processing library — `SoundTouch`, `PitchShifter`, buffers, filters |
12+
| [`@soundtouchjs/audio-worklet`](packages/audio-worklet/README.md) | `npm install @soundtouchjs/audio-worklet` | AudioWorklet implementation with `AudioParam`-based controls |
13+
| [`@cxing/interpolation-strategy-lanczos`](packages/interpolation-strategy-lanczos/README.md) | `npm install @cxing/interpolation-strategy-lanczos` | Lanczos interpolation strategy plugin (default strategy id: `lanczos8`) |
14+
| [`@cxing/interpolation-strategy-linear`](packages/interpolation-strategy-linear/README.md) | `npm install @cxing/interpolation-strategy-linear` | Linear interpolation strategy plugin (strategy id: `linear`) |
1315

1416
A development [demo app](apps/demo/) is included for testing both packages in a browser.
1517

@@ -18,7 +20,11 @@ If you are new to Web Audio, start with the demo guide: [apps/demo/README.md](ap
1820
## Documentation
1921

2022
- Core package API and concepts: [packages/core/README.md](packages/core/README.md)
23+
- Core API reference index: [packages/core/docs/README.md](packages/core/docs/README.md)
2124
- AudioWorklet package API and setup: [packages/audio-worklet/README.md](packages/audio-worklet/README.md)
25+
- AudioWorklet API reference index: [packages/audio-worklet/docs/README.md](packages/audio-worklet/docs/README.md)
26+
- SoundTouchNode reference: [packages/audio-worklet/docs/sound-touch-node.md](packages/audio-worklet/docs/sound-touch-node.md)
27+
- Interpolation strategy plugins: [packages/interpolation-strategy-lanczos/README.md](packages/interpolation-strategy-lanczos/README.md), [packages/interpolation-strategy-linear/README.md](packages/interpolation-strategy-linear/README.md)
2228
- Beginner Web Audio + demo architecture guide: [apps/demo/README.md](apps/demo/README.md)
2329

2430
## Quick start
@@ -33,7 +39,7 @@ import { SoundTouchNode } from '@soundtouchjs/audio-worklet';
3339
const audioCtx = new AudioContext();
3440
await SoundTouchNode.register(audioCtx, '/soundtouch-processor.js');
3541

36-
const stNode = new SoundTouchNode(audioCtx);
42+
const stNode = new SoundTouchNode({ context: audioCtx });
3743
stNode.connect(audioCtx.destination);
3844

3945
const source = audioCtx.createBufferSource();
@@ -47,6 +53,8 @@ source.start();
4753

4854
The audio-worklet package exposes wider real-time control ranges for `pitch`, `tempo`, `rate`, and `playbackRate` than earlier releases. Those controls are intentionally capped to balance flexibility with predictable real-time behavior: more extreme values can increase artifacts, reduce output quality, and make buffer behavior less stable on small AudioWorklet render blocks.
4955

56+
Interpolation defaults to `lanczos8` (Lanczos kernel plugin) in both core and audio-worklet flows. You can opt into `linear` for A/B testing and latency/quality comparisons.
57+
5058
### PitchShifter (ScriptProcessorNode)
5159

5260
The `@soundtouchjs/core` package provides a higher-level `PitchShifter` class with built-in playback tracking. This uses `ScriptProcessorNode`, which is deprecated but widely supported.
@@ -55,7 +63,11 @@ The `@soundtouchjs/core` package provides a higher-level `PitchShifter` class wi
5563
import { PitchShifter } from '@soundtouchjs/core';
5664

5765
const audioCtx = new AudioContext();
58-
const shifter = new PitchShifter(audioCtx, audioBuffer, 16384);
66+
const shifter = new PitchShifter({
67+
context: audioCtx,
68+
buffer: audioBuffer,
69+
bufferSize: 16384,
70+
});
5971
shifter.tempo = 1.2;
6072
shifter.pitch = 0.9;
6173

@@ -68,6 +80,18 @@ shifter.connect(audioCtx.destination);
6880

6981
See each package's README for full API documentation.
7082

83+
### Constructor API change
84+
85+
As of the latest release line, constructor calls use named options objects instead of positional arguments. Example:
86+
87+
```ts
88+
// before
89+
// new SoundTouchNode(audioCtx)
90+
91+
// now
92+
new SoundTouchNode({ context: audioCtx });
93+
```
94+
7195
## Development
7296

7397
### Prerequisites
@@ -95,6 +119,8 @@ Individual project commands via Nx:
95119
```sh
96120
pnpm nx build core # Build @soundtouchjs/core
97121
pnpm nx build audio-worklet # Build @soundtouchjs/audio-worklet
122+
pnpm nx build @cxing/interpolation-strategy-lanczos
123+
pnpm nx build @cxing/interpolation-strategy-linear
98124
pnpm nx test core # Run core tests
99125
pnpm nx test audio-worklet # Run audio-worklet tests
100126
pnpm nx dev demo # Dev server with HMR
@@ -120,6 +146,7 @@ The `v0.4` release is a ground-up modernization:
120146
- **TypeScript**: Full rewrite — strict mode, no `any`, complete type exports
121147
- **ESM only**: Pure ES modules targeting ES2024 (no CommonJS)
122148
- **AudioWorklet**: New `@soundtouchjs/audio-worklet` package replaces the [separate AudioWorklet repo](https://github.com/cutterbl/soundtouchjs-audio-worklet)
149+
- **Interpolation plugin model**: Strategy registry with plugin packages; `lanczos8` as default, `linear` as optional override
123150
- **ES2024 optimizations**: Resizable `ArrayBuffer` in `FifoSampleBuffer`, scratch buffer reuse, dirty-flag overlap buffers
124151
- **pnpm workspaces**: Workspace protocol (`workspace:*`) for inter-package dependencies
125152
- **Tooling**: Vite dev server, Vitest test runner, Prettier formatting, commitlint + husky, GitHub Actions CI, `nx release` for versioning and publishing
@@ -139,16 +166,10 @@ LGPL-3.0 — see [LICENSE](LICENSE) for details.
139166

140167
[I accept cash](https://paypal.me/cutterbl?locale.x=en_US) if you like what's been done.
141168

142-
## In Case You Are Interested
143-
144-
SoundTouchJS is based on the C++ implementation of [Soundtouch](https://www.surina.net/soundtouch/) by Olli Parviainen. The earliest implementation in JavaScript was written by [Ryan Berdeen](https://github.com/also/soundtouch-js) and later expanded by [Jakub Faila](https://github.com/jakubfiala/soundtouch-js). I have further expanded this library into a distributable package, refactored for es2015 development.
145-
146-
This package includes the `getWebAudioNode` utility written by [Adrian Holovaty](https://github.com/adrianholovaty), as well as the user-friendly `PitchShifter` wrapper from [Jakub Faila](https://github.com/jakubfiala/soundtouch-js).
147-
148169
## Contributors
149170

150171
- [Steve 'Cutter' Blades](https://cutterscrossing.com)
151172
- [Olli Parviainen](https://www.surina.net/soundtouch/)
152-
- [Ray Berdeen](http://ryanberdeen.com)
153-
- [Jakub Faila](http://fiala.space)
173+
- [Ryan Berdeen](http://ryanberdeen.com)
174+
- [Jakub Fiala](http://fiala.space)
154175
- [Adrian Holovaty](http://www.holovaty.com)

apps/demo/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,34 @@ Web Audio is a graph of connected nodes:
1212
In this demo, that graph is:
1313

1414
1. Source:
15+
1516
- `AudioBufferSourceNode` in Buffer mode
1617
- `HTMLAudioElement` (wrapped by `MediaElementAudioSourceNode`) in Element mode
1718

1819
2. Processor:
20+
1921
- `SoundTouchNode` from `@soundtouchjs/audio-worklet`
2022

2123
3. Output control:
24+
2225
- `GainNode` for volume
2326

2427
4. Speakers:
28+
2529
- `audioCtx.destination`
2630

2731
## Why there are two playback modes
2832

2933
The demo shows two common Web Audio input strategies:
3034

3135
1. Buffer mode:
36+
3237
- You fetch/decode audio into an `AudioBuffer`
3338
- You create an `AudioBufferSourceNode` to play it
3439
- Good for precise seeking and custom transport logic
3540

3641
2. Element mode:
42+
3743
- You use a regular `<audio>` element as source
3844
- Good when you want native media controls or browser buffering behavior
3945

@@ -42,21 +48,26 @@ Both modes route through `SoundTouchNode` so pitch and tempo controls are consis
4248
## Important Web Audio behavior (newcomer checklist)
4349

4450
1. `AudioContext` starts suspended in many browsers until user interaction.
51+
4552
- That is why the demo calls `audioCtx.resume()` in play paths.
4653

4754
2. `AudioBufferSourceNode` is one-shot.
55+
4856
- After `start()`, you cannot restart the same node.
4957
- Pause/resume works by creating a new source node and starting from an offset.
5058

5159
3. `createMediaElementSource()` should be done once per media element and context.
60+
5261
- The demo stores `elementSourceNode` and reuses it.
5362

5463
4. Time domains matter.
64+
5565
- `audioCtx.currentTime` is wall-clock time in the audio engine.
5666
- `pauseOffset` is source time (position in track).
5767
- With tempo changes, converting between these domains is required for accurate progress/seek.
5868

5969
5. Looping belongs to the source transport.
70+
6071
- Buffer mode: `sourceNode.loop`
6172
- Element mode: `audioEl.loop`
6273
- `SoundTouchNode` does processing, not transport lifecycle.
@@ -66,19 +77,23 @@ Both modes route through `SoundTouchNode` so pitch and tempo controls are consis
6677
The demo uses a recommended pairing:
6778

6879
1. Set source playback speed with transport playback rate:
80+
6981
- Buffer mode: `sourceNode.playbackRate.value = tempo`
7082
- Element mode: `audioEl.playbackRate = tempo`
7183

7284
2. Mirror that rate to SoundTouch:
85+
7386
- `stNode.playbackRate.value = tempo`
7487

7588
This keeps SoundTouch's pitch compensation aligned with source speed.
7689

7790
3. Apply pitch controls separately:
91+
7892
- `stNode.pitch.value` for continuous ratio
7993
- `stNode.pitchSemitones.value` for key changes in semitones
8094

8195
4. Set output volume after processing:
96+
8297
- `gainNode.gain.value`
8398

8499
## Why `preservesPitch = false` is set for HTML audio
@@ -93,12 +108,15 @@ Setting `audioEl.preservesPitch = false` ensures SoundTouch is the single pitch
93108
Buffer mode tracks three things:
94109

95110
1. `playStartTime`:
111+
96112
- `audioCtx.currentTime` at the moment playback started/resumed
97113

98114
2. `pauseOffset`:
115+
99116
- Source position in seconds where playback should start next
100117

101118
3. `currentTempo`:
119+
102120
- Needed to convert elapsed wall time into elapsed source time
103121

104122
When pausing or changing tempo during playback, the demo does:
@@ -119,18 +137,23 @@ Without wrapped progress, UI would pin at 100% while audio continues looping.
119137
## Common mistakes and symptoms
120138

121139
1. Forgot `await SoundTouchNode.register(...)`
140+
122141
- Symptom: node creation fails because processor is unknown.
123142

124143
2. Source playback rate and `stNode.playbackRate` are out of sync
144+
125145
- Symptom: pitch sounds wrong when changing tempo.
126146

127147
3. Reusing a started `AudioBufferSourceNode`
148+
128149
- Symptom: no sound after pause/resume attempt.
129150

130151
4. Not calling `audioCtx.resume()` from a user gesture path
152+
131153
- Symptom: graph appears connected but silent.
132154

133155
5. Leaving `audioEl.preservesPitch` enabled
156+
134157
- Symptom: double pitch handling artifacts.
135158

136159
## Fast map from concepts to code
@@ -147,3 +170,27 @@ Without wrapped progress, UI would pin at 100% while audio continues looping.
147170
2. Verify both modes after every change.
148171
3. Keep comments focused on cause/effect, not UI wording.
149172
4. If behavior differs between modes, check source-specific APIs first.
173+
174+
## API references
175+
176+
- SoundTouchNode (AudioWorklet main-thread API): [../../packages/audio-worklet/docs/sound-touch-node.md](../../packages/audio-worklet/docs/sound-touch-node.md)
177+
- AudioWorklet docs index: [../../packages/audio-worklet/docs/README.md](../../packages/audio-worklet/docs/README.md)
178+
- Core docs index: [../../packages/core/docs/README.md](../../packages/core/docs/README.md)
179+
180+
## Sample buffer A/B toggle
181+
182+
The demo uses circular sample buffers by default and can be switched to FIFO buffers:
183+
184+
1. URL: open the demo with `?sampleBufferType=fifo`
185+
2. UI: use the "Use FIFO sample buffers" checkbox (it reloads with the query flag)
186+
187+
Default (no query flag) keeps circular sample buffers enabled.
188+
189+
## Interpolation A/B toggle
190+
191+
The demo defaults to Lanczos interpolation (`lanczos8`) and supports a linear override.
192+
193+
1. URL: open the demo with `?interpolationStrategy=linear`
194+
2. UI: use the "Use linear interpolation" checkbox (it reloads with the query flag)
195+
196+
Unchecked uses default Lanczos behavior. Checked forces linear interpolation for side-by-side listening tests.

apps/demo/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ <h3>SoundTouchJS AudioWorklet Demo</h3>
7272
<button id="modeBuffer" class="active">AudioBuffer</button>
7373
<button id="modeElement">Audio Element</button>
7474
</div>
75+
<label style="display: flex; gap: 1rem">
76+
<input type="checkbox" id="circularAdapterToggle" />
77+
Use FIFO sample buffers (reloads demo)
78+
</label>
79+
<div id="adapterMode"></div>
80+
<label style="display: flex; gap: 1rem">
81+
<input type="checkbox" id="interpolationToggle" />
82+
Use linear interpolation (reloads demo)
83+
</label>
84+
<div id="interpolationMode"></div>
7585
<div id="bufferControls">
7686
<button id="play" disabled>Play</button>
7787
<button id="stop">Pause</button>

0 commit comments

Comments
 (0)