Skip to content

Parameter Control

EZ Web Audio provides a fluent API for controlling and automating audio parameters — gain, pan, frequency, detune — either immediately or scheduled relative to playback start.

Validation is unified across update() and the shorthand setters

update('gain').to(v).as('ratio') and sound.changeGainTo(v) enforce the identical rule (negative gain throws a ValidationError, values above 1 warn on the console) — they route through the same validation, so there's no case where one throws and the other silently accepts a bad value. Same for update('pan') / changePanTo() (out-of-range warns, doesn't throw — the Web Audio API clamps pan automatically).

Immediate Updates

Change a parameter right now using update().to().as():

typescript
// Set gain to 50%
sound.update('gain').to(0.5).as('ratio')

// Pan fully left
sound.update('pan').to(-1).as('ratio')

// Change oscillator frequency
oscillator.update('frequency').to(880).as('ratio')

The as() method specifies the unit type:

UnitRangeDescription
'ratio'0–1 (gain), -1–1 (pan)Direct value
'percent'0–100Percentage of full range — gain-like parameters only (see below)
'inverseRatio'0–1Inverted (1 - value)

'percent' is not supported for 'pan'

Pan's range is -1 (left) to 1 (right), but 'percent' (0–100) can only ever produce values 0 to 1 — there's no way to express "left" as a percentage. sound.update('pan').to(50).as('percent') throws a ValidationError rather than silently producing a pan that can never go left. Use 'ratio' for pan:

typescript
sound.update('pan').to(-1).as('ratio') // hard left
sound.update('pan').to(50).as('percent') // throws ValidationError

Scheduled Updates

Schedule parameter changes that apply automatically on the next play() call.

onPlaySet() — Envelope-Style Automation

Set a value and ramp it over time:

typescript
// Fade in: start at 0, ramp to full volume over 1 second
sound.onPlaySet('gain').to(0).endingAt(1, 'exponential')

// The parameter starts at 0 when play() is called,
// then ramps exponentially to its previous value by t=1s

onPlayRamp() — Linear Ramps

Ramp smoothly between two values:

typescript
// Ramp frequency from 200 to 800 over 0.5 seconds
oscillator.onPlayRamp('frequency').from(200).to(800).in(0.5)

// Ramp gain from 0 to 1 over 2 seconds
sound.onPlayRamp('gain').from(0).to(1).in(2)

Consume-Once Semantics

Scheduled values (onPlaySet/onPlayRamp) are cleared after each play() call. To repeat the automation on every play, re-schedule before each play():

typescript
function playWithFadeIn() {
  sound.onPlaySet('gain').to(0).endingAt(0.5, 'linear')
  sound.play()
}

Common Patterns

Fade In

typescript
// Exponential fade in over 0.5 seconds (sounds natural)
sound.onPlaySet('gain').to(0).endingAt(0.5, 'exponential')
sound.play()

Fade Out

Schedule a fade that ends at the sound's natural end:

typescript
const duration = sound.duration.raw

// Start fade at 80% through, reach silence at end
sound.onPlaySet('gain').to(1).endingAt(duration * 0.8, 'linear')
sound.onPlaySet('gain').to(0).endingAt(duration, 'linear')
sound.play()

Pitch Bend

typescript
// Bend up one octave over 1 second
osc.onPlayRamp('frequency').from(440).to(880).in(1)
osc.play()

Vibrato (manual LFO)

Consume-once semantics

onPlaySet and onPlayRamp schedules are consume-once — cleared after each play() call. Calling onPlayRamp multiple times for the same parameter replaces the previous schedule (last-write-wins). True vibrato requires a real-time loop that re-schedules ramps — it cannot be built with a pre-play schedule alone.

For a single pitch bend effect, scheduling works well:

typescript
// Bend from 440 Hz down to 220 Hz over 2 seconds on next play
osc.onPlayRamp('frequency').from(440).to(220).in(2)
osc.play()
// Schedule is now consumed — subsequent play() starts at the default frequency

For continuous vibrato, use the Web Audio API's OscillatorNode LFO pattern directly or re-schedule before each play.

Convenience Methods

For common fade operations, use the built-in convenience methods instead of manual scheduling:

Fade In

typescript
sound.fadeIn(0.5) // Play with gain ramping from 0 to current gain over 0.5s

Fade Out

typescript
await sound.fadeOut(1.0) // Ramp gain to 0 over 1s, then stop. Returns a Promise.

These methods handle the scheduling internally — no need to call onPlaySet() or manage timeouts yourself.

Narrowed Control Types

Sound.update(), Sound.onPlaySet(), and Sound.onPlayRamp() accept SoundControlType — only 'gain' | 'pan' | 'detune'. Oscillator overrides these methods to accept the full ControlType (which also includes 'frequency'). TypeScript catches mistakes at compile time:

typescript
import type { ControlType, OscillatorControlType, SoundControlType } from 'ez-web-audio'

sound.update('gain').to(0.5).as('ratio') // OK
sound.update('frequency').to(440).as('ratio') // TypeScript error!

osc.update('frequency').to(880).as('ratio') // OK — Oscillator accepts all

Controllers

The underlying SoundController and OscillatorController classes are exported for advanced consumers who need direct access to parameter scheduling logic. Most users will never need these — the fluent update()/onPlaySet()/onPlayRamp() API handles everything.

Extending ControlType

The parameter system can be extended for custom control types via TypeScript module augmentation:

typescript
// In your project's type declarations (e.g., global.d.ts)
declare module 'ez-web-audio' {
  interface ControlTypeMap {
    playbackRate: 'playbackRate'
  }
}

// Now 'playbackRate' is accepted by update(), onPlaySet(), etc. —
// on Oscillator (ControlType) AND on Sound/Track (SoundControlType), since
// SoundControlType is derived from ControlTypeMap too (Exclude<ControlType, 'frequency'>).
// Note: You must provide custom controller logic to handle the new type.

This is useful when wrapping custom AudioNodes that expose non-standard parameters. Augmenting ControlTypeMap extends every type derived from it — ControlType, OscillatorControlType, and SoundControlType all pick up the new member automatically, so you don't need to separately widen Sound/Track's narrower type.

API Quick Reference

MethodDescription
sound.update('gain').to(v).as('ratio')Immediate parameter update
sound.onPlaySet('gain').to(v).endingAt(t, curve)Schedule envelope-style ramp
sound.onPlayRamp('gain').from(a).to(b).in(t)Schedule linear ramp between values
sound.changeGainTo(v)Shorthand for gain update
sound.changePanTo(v)Shorthand for pan update
oscillator.update('frequency').to(v).as('ratio')Update oscillator frequency

Next Steps