feat: Add support for inbound "calls" from triggers (#2500)
This commit is contained in:
committed by
dermotduffy
parent
f4c686c298
commit
15e335a647
@@ -0,0 +1,115 @@
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { RingtoneFinishedHandler, Tone, ToneEnvelope } from './types';
|
||||
|
||||
// Shared scaffolding for tones generated via the Web Audio API: owns the
|
||||
// AudioContext lifecycle, the repeat timer, and the bell-shaped note envelope.
|
||||
// Subclasses implement `_play()` to define one iteration of their pattern, and
|
||||
// call `_scheduleNext()` to loop.
|
||||
//
|
||||
// `repeat` caps how many iterations are played per `start()`. `0` means loop
|
||||
// indefinitely; otherwise the tone schedules a final no-op timer to let the
|
||||
// last iteration's decay tail finish audibly, then fires `finishedHandler` and
|
||||
// self-stops.
|
||||
export abstract class GeneratedTone implements Tone {
|
||||
private _context: AudioContext | null = null;
|
||||
private _timer = new Timer();
|
||||
private _finishedHandler: RingtoneFinishedHandler | null = null;
|
||||
|
||||
private readonly _repeat: number;
|
||||
private _remaining = 0;
|
||||
|
||||
constructor(repeat: number) {
|
||||
this._repeat = repeat;
|
||||
}
|
||||
|
||||
public start(finishedHandler?: RingtoneFinishedHandler): void {
|
||||
if (this._context) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._context = new AudioContext();
|
||||
} catch {
|
||||
this._context = null;
|
||||
// Treat AudioContext construction failure as natural completion so the
|
||||
// caller can release any lock it holds on our behalf -- otherwise the
|
||||
// orchestrator can't tell silent failure from active playback.
|
||||
finishedHandler?.();
|
||||
return;
|
||||
}
|
||||
this._finishedHandler = finishedHandler ?? null;
|
||||
this._remaining = this._repeat;
|
||||
this._play();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this._timer.stop();
|
||||
this._context?.close().catch(() => {});
|
||||
this._context = null;
|
||||
|
||||
// Suppress any natural-completion callback -- the caller asked to stop.
|
||||
this._finishedHandler = null;
|
||||
}
|
||||
|
||||
// Current AudioContext time. Subclasses only read this from inside `_play()`
|
||||
// where the context is always set.
|
||||
protected get _currentTime(): number {
|
||||
/* istanbul ignore next: _currentTime is only read by subclasses from
|
||||
_play() during an active context -- @preserve */
|
||||
return this._context?.currentTime ?? 0;
|
||||
}
|
||||
|
||||
// Schedule the next iteration of `_play()`. Subclasses call this at the end
|
||||
// of their pattern to loop. No-ops if the context has already been closed so
|
||||
// a stopped tone can never re-arm its loop.
|
||||
protected _scheduleNext(intervalSeconds: number): void {
|
||||
/* istanbul ignore next: defensive guard against a subclass calling
|
||||
_scheduleNext after stop() — JS single-threading makes this unreachable
|
||||
from the existing subclasses -- @preserve */
|
||||
if (!this._context) {
|
||||
return;
|
||||
}
|
||||
if (this._repeat > 0 && --this._remaining <= 0) {
|
||||
// Schedule one last wait for the decay tail.
|
||||
this._timer.start(intervalSeconds, () => {
|
||||
const finishedHandler = this._finishedHandler;
|
||||
this.stop();
|
||||
finishedHandler?.();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._timer.start(intervalSeconds, () => {
|
||||
/* istanbul ignore next: Timer.stop() cancels pending callbacks, so this
|
||||
re-entry guard is unreachable in practice -- @preserve */
|
||||
if (!this._context) {
|
||||
return;
|
||||
}
|
||||
this._play();
|
||||
});
|
||||
}
|
||||
|
||||
// Plays one note: a smooth tone that rises to peak volume and then fades.
|
||||
protected _playNote(freq: number, when: number, envelope: ToneEnvelope): void {
|
||||
/* istanbul ignore next: _playNote is only called by subclasses from
|
||||
_play() during an active context -- @preserve */
|
||||
if (!this._context) {
|
||||
return;
|
||||
}
|
||||
const oscillator = this._context.createOscillator();
|
||||
const gain = this._context.createGain();
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.value = freq;
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this._context.destination);
|
||||
|
||||
gain.gain.setValueAtTime(0, when);
|
||||
gain.gain.linearRampToValueAtTime(envelope.peak, when + envelope.attack);
|
||||
gain.gain.setTargetAtTime(0, when + envelope.attack, envelope.decayTau);
|
||||
|
||||
oscillator.start(when);
|
||||
oscillator.stop(when + envelope.hold);
|
||||
}
|
||||
|
||||
// One iteration of the pattern. Implementations should call `_playNote(...)`
|
||||
// for each note and finish with `_scheduleNext(...)` to loop.
|
||||
protected abstract _play(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user