diff --git a/.github/workflows/main.yml b/.github/workflows/ci.yml
similarity index 65%
rename from .github/workflows/main.yml
rename to .github/workflows/ci.yml
index 2fc7e2e..f86b6b8 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/ci.yml
@@ -3,18 +3,14 @@ name: CI
on:
pull_request:
branches: [master]
- push:
- branches: [master]
workflow_dispatch:
- inputs:
- reason:
- description: 'Reason for manual trigger'
- required: false
- default: 'Manual release'
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
jobs:
validation:
- if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
@@ -39,7 +35,6 @@ jobs:
- run: pnpm build
- # Coverage summary chain (continue-on-error, always publish/upload/enforce)
- name: Generate coverage summary
id: coverage
run: pnpm coverage:summary
@@ -108,51 +103,3 @@ jobs:
- name: Enforce coverage summary status
if: steps.coverage.outcome == 'failure'
run: exit 1
-
- release:
- if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && !startsWith(github.event.head_commit.message, 'chore(release)'))
- runs-on: ubuntu-latest
-
- permissions:
- contents: write
- id-token: write
-
- steps:
- - uses: actions/checkout@v6
- with:
- filter: tree:0
- fetch-depth: 0
-
- - uses: pnpm/action-setup@v6
- name: Install pnpm
- with:
- version: 10.33.0
- run_install: false
-
- - name: Setup git creds
- env:
- config_email: ${{ secrets.GIT_CONFIG_EMAIL }}
- config_un: ${{ secrets.GIT_CONFIG_USERNAME }}
- run: |
- git config --local user.email ${config_email}
- git config --local user.name ${config_un}
-
- - run: pnpm install --frozen-lockfile
- env:
- HUSKY: 0
-
- - uses: nrwl/nx-set-shas@v5
-
- - run: pnpm build
-
- - name: Set up .npmrc for publishing
- run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
- env:
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
-
- - name: Publish to npm
- run: pnpm exec nx release --yes
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- NPM_CONFIG_PROVENANCE: true
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..d1a6faa
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,49 @@
+name: CodeQL
+
+on:
+ pull_request:
+ branches: [master]
+ types: [opened, reopened, synchronize, ready_for_review]
+ push:
+ branches: [master]
+ schedule:
+ - cron: '23 4 * * 1'
+
+permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+concurrency:
+ group: codeql-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ analyze:
+ name: codeql/analyze
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [javascript-typescript]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+
+ - uses: pnpm/action-setup@v6
+
+ - run: pnpm install --frozen-lockfile
+ env:
+ HUSKY: 0
+
+ - name: Build for analysis
+ run: pnpm build
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..23f132f
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,67 @@
+name: Release
+
+on:
+ push:
+ branches: [master]
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: Run nx release in dry-run mode
+ required: true
+ default: true
+ type: boolean
+
+concurrency:
+ group: release-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ filter: tree:0
+ fetch-depth: 0
+
+ - uses: pnpm/action-setup@v6
+ with:
+ version: 10.33.0
+ run_install: false
+
+ - name: Setup git creds
+ env:
+ config_email: ${{ secrets.GIT_CONFIG_EMAIL }}
+ config_un: ${{ secrets.GIT_CONFIG_USERNAME }}
+ run: |
+ git config --local user.email ${config_email}
+ git config --local user.name ${config_un}
+
+ - run: pnpm install --frozen-lockfile
+ env:
+ HUSKY: 0
+
+ - name: Set up .npmrc for publishing
+ run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Run release dry-run
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
+ run: pnpm exec nx release --dry-run
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Run release
+ if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.dry_run) }}
+ run: pnpm exec nx release --yes
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ NPM_CONFIG_PROVENANCE: true
diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml
index ecaf449..ec00463 100644
--- a/.github/workflows/storybook-pages.yml
+++ b/.github/workflows/storybook-pages.yml
@@ -1,24 +1,33 @@
name: Deploy Storybook to GitHub Pages
on:
- push:
- branches: [master]
+ workflow_run:
+ workflows: [Release]
+ types: [completed]
workflow_dispatch:
+ inputs:
+ deploy:
+ description: Deploy to GitHub Pages after build
+ required: true
+ default: false
+ type: boolean
permissions:
contents: read
- pages: write
- id-token: write
+
+concurrency:
+ group: docs-pages-${{ github.ref }}
+ cancel-in-progress: false
jobs:
build:
- if: github.event_name == 'workflow_dispatch' || startsWith(github.event.head_commit.message, 'chore(release)')
+ if: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
- fetch-depth: 0
+ ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }}
- uses: pnpm/action-setup@v6
@@ -30,16 +39,22 @@ jobs:
run: pnpm nx run @soundtouchjs/storybook:build-storybook
- name: Setup GitHub Pages
+ if: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || inputs.deploy }}
uses: actions/configure-pages@v6
- name: Upload Storybook artifact
+ if: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || inputs.deploy }}
uses: actions/upload-pages-artifact@v5
with:
path: ./static-storybook
deploy:
needs: build
+ if: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || inputs.deploy }}
runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
diff --git a/README.md b/README.md
index 0c689f6..b35436e 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A real-time audio processing library for pitch shifting and playback speed contr
## Monorepo
-This project is an [Nx](https://nx.dev) monorepo managed with [pnpm](https://pnpm.io/) workspaces. It publishes ten packages:
+This project is an [Nx](https://nx.dev) monorepo managed with [pnpm](https://pnpm.io/) workspaces. It publishes eleven packages:
| Package | npm | Description |
| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
@@ -15,6 +15,7 @@ This project is an [Nx](https://nx.dev) monorepo managed with [pnpm](https://pnp
| [`@soundtouchjs/stretch-phase-vocoder`](packages/stretch-phase-vocoder/README.md) | `npm install @soundtouchjs/stretch-phase-vocoder` | Phase vocoder time-stretch algorithm — implements `StretchPipe`, usable standalone or as a `SoundTouch` stretch stage |
| [`@soundtouchjs/phase-vocoder-worklet`](packages/phase-vocoder-worklet/README.md) | `npm install @soundtouchjs/phase-vocoder-worklet` | AudioWorklet implementation using the phase vocoder for smoother extreme-ratio time-stretching |
| [`@soundtouchjs/formant-correction-worklet`](packages/formant-correction-worklet/README.md) | `npm install @soundtouchjs/formant-correction-worklet` | AudioWorklet implementation with LPC-based formant preservation for natural-sounding vocal pitch shifts |
+| [`@soundtouchjs/worklet-base`](packages/worklet-base/README.md) | `npm install @soundtouchjs/worklet-base` | Abstract base class for building custom AudioWorklet processors on top of the SoundTouch engine |
| [`@soundtouchjs/interpolation-strategy-lanczos`](packages/interpolation-strategy-lanczos/README.md) | `npm install @soundtouchjs/interpolation-strategy-lanczos` | Lanczos interpolation strategy plugin (default strategy id: `lanczos`) |
| [`@soundtouchjs/interpolation-strategy-linear`](packages/interpolation-strategy-linear/README.md) | `npm install @soundtouchjs/interpolation-strategy-linear` | Linear interpolation strategy plugin (strategy id: `linear`) |
| [`@soundtouchjs/interpolation-strategy-hann`](packages/interpolation-strategy-hann/README.md) | `npm install @soundtouchjs/interpolation-strategy-hann` | Hann interpolation strategy plugin (strategy id: `hann`) |
@@ -37,6 +38,7 @@ If you are new to Web Audio, start with the demo guide: [apps/demo/README.md](ap
- Phase vocoder (time-stretch algorithm): [packages/stretch-phase-vocoder/README.md](packages/stretch-phase-vocoder/README.md)
- Phase vocoder AudioWorklet: [packages/phase-vocoder-worklet/README.md](packages/phase-vocoder-worklet/README.md)
- Formant correction AudioWorklet: [packages/formant-correction-worklet/README.md](packages/formant-correction-worklet/README.md)
+- Custom worklet processor base class: [packages/worklet-base/README.md](packages/worklet-base/README.md)
- Beginner Web Audio + demo architecture guide: [https://cutterscrossing.com/SoundTouchJS/?path=/docs/getting-started--docs](https://cutterscrossing.com/SoundTouchJS/?path=/docs/getting-started--docs)
## Quick start
diff --git a/packages/formant-correction-worklet/README.md b/packages/formant-correction-worklet/README.md
index c096e92..ab02b43 100644
--- a/packages/formant-correction-worklet/README.md
+++ b/packages/formant-correction-worklet/README.md
@@ -190,6 +190,10 @@ import {
| Best use case | Instruments, music | Vocals, speech |
| `formantStrength = 0` mode | — | Identical to `SoundTouchNode` |
+## Architecture
+
+`FormantCorrectionProcessor` extends `SoundTouchProcessorBase` from `@soundtouchjs/worklet-base`, sharing the DSP pipeline, runtime-update queue, and `STANDARD_PARAMETER_DESCRIPTORS` with the other SoundTouchJS worklet packages. The formant correction logic lives in `beforePipeProcess` (LPC analysis) and `extractSamples` (analysis/synthesis filter application + `formantStrength` blend) — both hooks defined by the base class contract.
+
## License
MPL-2.0 — see [LICENSE](../../LICENSE) for details.
diff --git a/packages/phase-vocoder-worklet/README.md b/packages/phase-vocoder-worklet/README.md
index a5f224b..3225328 100644
--- a/packages/phase-vocoder-worklet/README.md
+++ b/packages/phase-vocoder-worklet/README.md
@@ -171,6 +171,10 @@ node.addEventListener('metrics', (e) => {
| Startup latency | Lower | `fftSize` samples |
| Artifacts | Clicks / repeats | "Phasiness" / smearing |
+## Architecture
+
+`PhaseVocoderProcessor` extends `SoundTouchProcessorBase` from `@soundtouchjs/worklet-base`, sharing the DSP pipeline, runtime-update queue, and `STANDARD_PARAMETER_DESCRIPTORS` with the other SoundTouchJS worklet packages. Override `beforePipeProcess` and `extractSamples` from the base if you need to customise the processing hooks.
+
## License
MPL-2.0 — see [LICENSE](../../LICENSE) for details.
diff --git a/storybook/src/docs/audio-worklet/worklet-base.mdx b/storybook/src/docs/audio-worklet/worklet-base.mdx
new file mode 100644
index 0000000..9ad1eb9
--- /dev/null
+++ b/storybook/src/docs/audio-worklet/worklet-base.mdx
@@ -0,0 +1,148 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+
+
+
+# @soundtouchjs/worklet-base
+
+**What is this?**
+
+`@soundtouchjs/worklet-base` provides `SoundTouchProcessorBase` — the abstract base class shared by all SoundTouchJS AudioWorklet processor packages. It centralises the DSP pipeline, runtime-update queue, and sample-buffer management so that `SoundTouchProcessor`, `PhaseVocoderProcessor`, and `FormantCorrectionProcessor` all extend the same core without duplicating code.
+
+**When would I use it directly?**
+
+You do **not** need to install this package to use SoundTouchJS in a web app. Install one of the worklet packages instead:
+
+- [`@soundtouchjs/audio-worklet`](?path=/docs/audio-worklet-soundtouchnode--docs) — standard pitch/rate control
+- [`@soundtouchjs/phase-vocoder-worklet`](?path=/docs/audio-worklet-phase-vocoder-node--docs) — smoother extreme-ratio time-stretching
+- [`@soundtouchjs/formant-correction-worklet`](?path=/docs/audio-worklet-formant-correction-node--docs) — vocal pitch shifting with formant preservation
+
+Install `@soundtouchjs/worklet-base` only if you are building a **custom AudioWorklet processor** on top of the SoundTouch engine.
+
+## Installation
+
+```sh
+npm install @soundtouchjs/worklet-base
+```
+
+## Basic usage
+
+Extend `SoundTouchProcessorBase` inside your processor module and implement the required `onProcessComplete` hook:
+
+```ts
+import {
+ SoundTouchProcessorBase,
+ STANDARD_PARAMETER_DESCRIPTORS,
+} from '@soundtouchjs/worklet-base';
+import type { ProcessCoreResult } from '@soundtouchjs/worklet-base';
+
+class MyProcessor extends SoundTouchProcessorBase {
+ static get parameterDescriptors() {
+ return STANDARD_PARAMETER_DESCRIPTORS;
+ }
+
+ constructor() {
+ super('[MyProcessor]', {});
+ }
+
+ onProcessComplete(result: ProcessCoreResult): void {
+ this.port.postMessage({ type: 'metrics', ...result });
+ }
+}
+
+registerProcessor('my-processor', MyProcessor);
+```
+
+## Optional hooks
+
+Override these methods to customise the per-block DSP loop without reimplementing the pipeline:
+
+
+
+ | Method | When to override |
+
+
+
+ beforePipeProcess(left, right, frameCount, params) |
+ Pre-pipe analysis — e.g. LPC coefficient extraction for formant correction. Default is a no-op. |
+
+
+ extractSamples(leftOutput, rightOutput, frameCount) |
+ Full extraction/write-back override — e.g. apply analysis and synthesis filters after the pipe. Default writes both channels and returns RMS/peak metrics. |
+
+
+
+
+## Runtime messages
+
+Send messages to a running processor via `AudioWorkletNode.port.postMessage`. The base class handles these automatically:
+
+```ts
+// Change interpolation strategy
+node.port.postMessage({ type: 'setInterpolationStrategy', value: 'lanczos' });
+
+// Update strategy parameters
+node.port.postMessage({ type: 'setInterpolationStrategyParams', value: { ... } });
+
+// Update stretch parameters (tempo, pitch, rate)
+node.port.postMessage({ type: 'setStretchParameters', value: { ... } });
+```
+
+## API reference
+
+### `SoundTouchProcessorBase`
+
+Abstract class extending `AudioWorkletProcessor`.
+
+#### Constructor
+
+```ts
+new SoundTouchProcessorBase(processorLabel: string, pipeOptions: SoundTouchOptions)
+```
+
+
+
+ | Parameter | Description |
+
+
+ processorLabel | Label used in log/warning messages, e.g. '[MyProcessor]'. |
+ pipeOptions | Options forwarded to SoundTouch — interpolation strategy, stretch parameters, etc. |
+
+
+
+#### Abstract method
+
+```ts
+abstract onProcessComplete(result: ProcessCoreResult): void
+```
+
+Called at the end of every successfully processed block. `result` contains `{ outputRms, outputPeak }` unless `extractSamples` is overridden to return different values.
+
+#### Static method
+
+```ts
+static resolveStrategy(
+ id: RateTransposerInterpolationStrategy | undefined,
+ label: string,
+): RateTransposerInterpolationStrategy | undefined
+```
+
+Validates a strategy ID against the interpolation registry. Falls back to `'lanczos'` and logs a warning for unknown IDs.
+
+### `STANDARD_PARAMETER_DESCRIPTORS`
+
+Array of `AudioParamDescriptor` objects for the three standard k-rate parameters: `pitch`, `pitchSemitones`, and `playbackRate`. Spread or return this directly from your `parameterDescriptors` getter.
+
+### Message types
+
+
+
+ | Export | Shape |
+
+
+ SetInterpolationStrategyMessage | {'{ type: \'setInterpolationStrategy\', value: RateTransposerInterpolationStrategy }'} |
+ SetInterpolationStrategyParamsMessage | {'{ type: \'setInterpolationStrategyParams\', value: InterpolationStrategyParams }'} |
+ SetStretchParametersMessage | {'{ type: \'setStretchParameters\', value: StretchParameters }'} |
+ ProcessorMessage | Union of the three message types above. |
+ ProcessCoreResult | {'{ outputRms: number, outputPeak: number }'} |
+
+
diff --git a/storybook/src/docs/navigation.mdx b/storybook/src/docs/navigation.mdx
index 8ae5793..8eea165 100644
--- a/storybook/src/docs/navigation.mdx
+++ b/storybook/src/docs/navigation.mdx
@@ -40,6 +40,7 @@ A buffer is just a chunk of audio data—think of it as a container holding a pi
- **@soundtouchjs/stretch-phase-vocoder**: An alternative time-stretch algorithm (phase vocoder) that produces smoother results at extreme playback ratios. Can be used as a drop-in stretch stage inside `SoundTouch`, or on its own.
- **@soundtouchjs/phase-vocoder-worklet**: An AudioWorklet node backed by the phase vocoder. Use it instead of `SoundTouchNode` when you need very slow or very fast playback without artifacts.
- **@soundtouchjs/formant-correction-worklet**: An AudioWorklet node that pitch-shifts voices while keeping the speaker's natural timbre. Without this, shifting pitch up makes voices sound like a chipmunk; shifting down makes them sound like a giant.
+- **@soundtouchjs/worklet-base**: Abstract base class shared by all SoundTouchJS worklet processors. Use this only if you are building a custom AudioWorklet processor on top of the SoundTouch engine.
- **Interpolation plugins**: Five swappable modules (`lanczos`, `linear`, `hann`, `blackman`, `kaiser`) for tuning the quality and CPU cost of sample-rate conversion.
- **Demo app**: Try everything live in your browser ([apps/demo/](https://github.com/cutterbl/SoundTouchJS/tree/main/apps/demo)).
@@ -52,6 +53,7 @@ If you’re new to audio, start with the **Getting Started** guide. If you’re
- Use **@soundtouchjs/audio-worklet** for modern, real-time browser audio. This is the default for most apps.
- Use **@soundtouchjs/phase-vocoder-worklet** instead of audio-worklet when playback rate is extreme (< 0.5× or > 2×) and WSOLA artifacts are audible. If "phasiness" is acceptable, this gives smoother results.
- Use **@soundtouchjs/formant-correction-worklet** when pitch-shifting voices or speech. Without formant correction, large pitch shifts make voices sound like a chipmunk (up) or giant (down).
+- Use **@soundtouchjs/worklet-base** only if you are building a **custom AudioWorklet processor** that needs the SoundTouch DSP pipeline. All three worklet packages above extend it internally.
- Use **@soundtouchjs/core** for custom DSP, offline processing, or non-browser environments.
- Use interpolation plugins for quality/latency tradeoffs or to implement your own strategy.