Pure OscillatorNode synthesis — no audio assets
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#
CngxAudioToneGeneratorFactoryDefault 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);
}
},
};
}