import TimedEvent from './timedEvent'; /** * TimedTone generates a pure tone using the Web Audio API. It maps the * oscillator lifecycle to TimedEvent lifecycle methods. Note: OscillatorNodes * cannot always be restarted after they are stopped, so resume creates a * fresh oscillator when necessary. * * constructor(duration=0, frequency=440, waveform='sine', gain=1.0) * - duration: milliseconds (0 unknown, -1 loop) * - frequency: Hz of the oscillator * - waveform: oscillator type (sine, square, sawtooth, triangle, 'custom') * - gain: output gain (0.0 - 1.0) */ export default class TimedTone extends TimedEvent { frequency: number; waveform: OscillatorType; gain: number; oscillatorNode: OscillatorNode | null; gainNode: GainNode | null; constructor(duration = 0, frequency = 440, waveform: OscillatorType = 'sine', gain = 1.0) { // create audio nodes without using `this` (so we can call super first) const context = new AudioContext(); const osc = new OscillatorNode(context, { type: waveform, frequency }); const g = new GainNode(context, { gain }); osc.connect(g).connect(context.destination); super(duration); this.frequency = frequency; this.waveform = waveform; this.gain = gain; this.oscillatorNode = osc; this.gainNode = g; } start() { if (this.started) return; this.oscillatorNode?.start(); super.start(); } pause() { if (this.paused) return; this.oscillatorNode?.stop(); super.pause(); } resume() { if (!this.paused) return; // WebAudio OscillatorNodes cannot be restarted once stopped in many browsers. // To resume, create a new oscillator and reconnect. if (this.gainNode) { const ctx = (this.gainNode as GainNode).context; const newOsc = new OscillatorNode(ctx, { type: this.waveform, frequency: this.frequency }); newOsc.connect(this.gainNode); newOsc.start(); this.oscillatorNode = newOsc; } super.resume(); } cancel() { this.oscillatorNode?.stop(); super.cancel(); } setVolume(gain = 1.0) { this.gain = gain; if (this.gainNode) this.gainNode.gain.value = gain; } }