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: + + + + + + + + + + + + + + + +
MethodWhen 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) +``` + + + + + + + + + +
ParameterDescription
processorLabelLabel used in log/warning messages, e.g. '[MyProcessor]'.
pipeOptionsOptions 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 + + + + + + + + + + + + +
ExportShape
SetInterpolationStrategyMessage{'{ type: \'setInterpolationStrategy\', value: RateTransposerInterpolationStrategy }'}
SetInterpolationStrategyParamsMessage{'{ type: \'setInterpolationStrategyParams\', value: InterpolationStrategyParams }'}
SetStretchParametersMessage{'{ type: \'setStretchParameters\', value: StretchParameters }'}
ProcessorMessageUnion 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.