Skip to main content
cngx-src documentation

createToneGenerator

Variablecommon/audio

projects/common/audio/tone-generator/tone-generator.ts

Description#

Pure OscillatorNode synthesis — no audio assets. Each tone is an oscillator through a per-note gain envelope (5 ms attack, 30 ms release) into the engine's master-volume node. The envelope is not decoration: a bare start()/stop() on a full-amplitude oscillator produces an audible click at both ends; ramping in and out removes it.

create* pure factory (no inject()), so it composes inside the engine and runs against the shared createAudioContextMock in specs.

Type#

CngxAudioToneGeneratorFactory

Default value#

(deps) => {
  function schedule(
    freq: number,
    durationMs: number,
    opts: ToneOptions | undefined,
    whenSec: number,
  ): void {
    const ctx = deps.context();
    const dest = deps.destination();
    const start = ctx.currentTime + whenSec + (opts?.when ?? 0);
    const durSec = Math.max(durationMs / 1000, MIN_DURATION_SEC);
    const peak = opts?.gain ?? DEFAULT_TONE_GAIN;

    const osc = ctx.createOscillator();
    osc.type = opts?.type ?? 'sine';
    osc.frequency.setValueAtTime(freq, start);

    const env = ctx.createGain();
    const holdStart = start + ATTACK_SEC;
    const releaseStart = start + durSec - RELEASE_SEC;
    env.gain.setValueAtTime(0, start);
    env.gain.linearRampToValueAtTime(peak, holdStart);
    env.gain.setValueAtTime(peak, releaseStart);
    env.gain.linearRampToValueAtTime(0, start + durSec);

    osc.connect(env);
    env.connect(dest);
    osc.start(start);
    osc.stop(start + durSec);
    osc.onended = () => {
      osc.disconnect();
      env.disconnect();
    };
  }

  return {
    tone(freq, durationMs, opts) {
      schedule(freq, durationMs, opts, 0);
    },
    sequence(steps) {
      let offsetSec = 0;
      for (const step of steps) {
        offsetSec += (step.delay ?? 0) / 1000;
        schedule(step.freq, step.duration, { type: step.type, gain: step.gain }, offsetSec);
        offsetSec += Math.max(step.duration / 1000, MIN_DURATION_SEC);
      }
    },
  };
}