Skip to content

feat(audio): add jitter option for per-voice pitch/volume/rate variance#12

Merged
raphaelsalaja merged 1 commit intomainfrom
feat/play-options-jitter
Apr 26, 2026
Merged

feat(audio): add jitter option for per-voice pitch/volume/rate variance#12
raphaelsalaja merged 1 commit intomainfrom
feat/play-options-jitter

Conversation

@raphaelsalaja
Copy link
Copy Markdown
Owner

@raphaelsalaja raphaelsalaja commented Apr 26, 2026

Summary

Adds a per-call jitter option to PlayOptions so repeated triggers of the same sound no longer feel identical. Requested Twitter — buttons that fire the same click sound in quick succession now sound alive.

Users opt in to whichever parameters they want humanized:

click({ jitter: { detune: 60 } });              // ±60 cents of pitch
click({ jitter: { detune: 40, volume: 0.1 } }); // + ±10% gain
  • detune — random pitch in cents (±value)
  • volume — random gain offset (0.1 = ±10%)
  • playbackRate — random rate offset (0.05 = ±5%)

Implementation notes

  • Offsets are computed once per render() call, so layers inside a single multi-layer voice stay internally in tune while each trigger differs from the last.
  • Works across defineSound, useSound, patch.play, and defineSequence without hook or patch-layer changes, since they all forward PlayOptions into render.
  • Noise sources (no detuneParam) gracefully ignore detune jitter; sample sources pick up both detune and playbackRate jitter.
  • No changes to Layer, SoundDefinition, or the JSON patch schema — per-call only.

Files

  • packages/audio/src/types.tsjitter on PlayOptions
  • packages/audio/src/engine.ts — per-voice random offsets applied in render()
  • apps/web/content/docs/api/sounds/define-sound.mdx — docs table + example
  • .changeset/jitter-play-option.md — minor bump for @web-kits/audio

Test plan

  • turbo run typecheck and @web-kits/audio:build pass locally (ran in pre-commit hook)
  • Manual: press a button wired to click({ jitter: { detune: 60 } }) repeatedly and confirm pitch varies
  • Manual: noise-only sound with jitter: { detune: 100 } plays normally (no-op)
  • Manual: multi-layer sound retains internal tuning across layers while differing per trigger

Made with Cursor

Extends `PlayOptions` with an optional `jitter` bag so repeated triggers of
the same sound vary slightly instead of sounding identical. Users opt in to
any combination of `detune` (cents), `volume`, and `playbackRate`, and each
is applied as a symmetric random offset computed once per voice — so layers
within a single multi-layer voice stay internally in tune, but each press
differs from the last.

Made-with: Cursor
Copilot AI review requested due to automatic review settings April 26, 2026 18:54
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
audio Ready Ready Preview, Comment Apr 26, 2026 6:54pm

Request Review

@raphaelsalaja raphaelsalaja merged commit ec45185 into main Apr 26, 2026
5 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in jitter field to PlayOptions to introduce per-trigger random variation (detune/volume/playbackRate) when rendering sounds, and documents the new option for consumers of defineSound.

Changes:

  • Extend PlayOptions with a jitter object for per-voice detune/volume/playbackRate variance.
  • Apply jitter values once per render() call and fan them out to all layers of the rendered voice.
  • Update API docs and ship a changeset for a minor version bump of @web-kits/audio.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/audio/src/types.ts Adds PlayOptions.jitter type + inline docs/examples.
packages/audio/src/engine.ts Computes jitter once per voice and applies it to gain/detune/playbackRate during render.
apps/web/content/docs/api/sounds/define-sound.mdx Documents jitter under PlayOptions and adds a dedicated section + example.
.changeset/jitter-play-option.md Publishes the feature as a minor bump with a short usage snippet.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +578 to +587
const jitter = opts?.jitter;
const detuneJitter = jitter?.detune
? (Math.random() * 2 - 1) * jitter.detune
: 0;
const volumeJitter = jitter?.volume
? 1 + (Math.random() * 2 - 1) * jitter.volume
: 1;
const rateJitter = jitter?.playbackRate
? 1 + (Math.random() * 2 - 1) * jitter.playbackRate
: 1;
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says this works across defineSequence because it forwards PlayOptions into render(), but playSequence() currently passes { volume } when a step resolves a volume override, which drops any other options (including the new jitter). To match the stated behavior, preserve the rest of opts when overriding volume (e.g., merge with opts), or update the PR description/docs to call out this limitation.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants