sw-synth
Version:
Lightweight sound synthesizer designed for real-time user interaction using the Web Audio API
123 lines • 4.21 kB
JavaScript
import { AperiodicVoice, OscillatorVoice, UnisonVoice, } from './voice/oscillator.js';
import { BufferVoice } from './voice/buffer.js';
export * from './voice/index.js';
// Tracking numbers for voice stealing
// Technically we could run out of note identifiers,
// but who is going to play 9007199254740991 notes in one session?
let NOTE_ID = 1;
/**
* Simple web audio synth of finite polyphony.
*/
export class Synth {
constructor(audioContext, destination, log) {
this.audioContext = audioContext;
this.destination = destination;
if (log === undefined) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.log = (msg) => { };
}
else {
this.log = log;
}
this.voices = [];
this.pitchBendNode = new ConstantSourceNode(this.audioContext, { offset: 0 });
this.pitchBend = this.pitchBendNode.offset;
this.pitchBendNode.start();
}
_newVoice() {
return new OscillatorVoice(this.audioContext, this.destination, this.log);
}
setPolyphony(maxPolyphony) {
if (!Number.isFinite(maxPolyphony) ||
maxPolyphony < 0 ||
!Number.isInteger(maxPolyphony)) {
throw new Error('Invalid max polyphony');
}
while (this.voices.length > maxPolyphony) {
const voice = this.voices.pop();
if (voice.pitchBend) {
this.pitchBendNode.disconnect(voice.pitchBend);
}
voice.dispose();
}
while (this.voices.length < maxPolyphony) {
const voice = this._newVoice();
if (voice.pitchBend) {
this.pitchBendNode.connect(voice.pitchBend);
}
this.voices.push(voice);
}
}
get maxPolyphony() {
return this.voices.length;
}
set maxPolyphony(value) {
this.setPolyphony(value);
}
_allocateVoice() {
// Allocate voices based on age.
// Boils down to:
// a) Pick the oldest released voice.
// b) If there are no released voices, replace the oldest currently playing voice.
let oldestVoice;
for (const voice of this.voices) {
voice.age++;
if (oldestVoice === undefined || voice.age > oldestVoice.age) {
oldestVoice = voice;
}
}
return oldestVoice;
}
/**
* Start playing a note at the specified frequency and velocity.
* @param frequency Frequency in Hertz.
* @param velocity Voice amplitude. Recommended range is 0 to 1.
* @param pitchBendRange Asymmetric pitch bend range. Determines the effect of {@link Synth.pitchBend} on this note.
* @returns A callback that stops playing the note.
*/
noteOn(frequency, velocity, pitchBendRange) {
const oldestVoice = this._allocateVoice();
if (oldestVoice === undefined) {
return () => { };
}
if (this.voiceParams === undefined) {
throw new Error('Synth.voiceParams must be set before calling Synth.noteOn');
}
return oldestVoice.noteOn(frequency, velocity, NOTE_ID++, this.voiceParams, pitchBendRange);
}
/**
* Trigger panic and release all notes.
*/
allNotesOff() {
for (const voice of this.voices) {
if (voice.lastNoteOff !== undefined) {
voice.lastNoteOff();
}
}
}
}
/**
* Web audio synth of finite polyphony where the voices are stacked in unison.
*/
export class UnisonSynth extends Synth {
_newVoice() {
return new UnisonVoice(this.audioContext, this.destination, this.log);
}
}
/**
* Web audio synth of finite polyphony that supports inharmonic timbres.
*/
export class AperiodicSynth extends Synth {
_newVoice() {
return new AperiodicVoice(this.audioContext, this.destination, this.log);
}
}
/**
* Web audio synth of finite polyphony with user-provided audio buffers.
*/
export class BufferSynth extends Synth {
_newVoice() {
return new BufferVoice(this.audioContext, this.destination, this.log);
}
}
//# sourceMappingURL=index.js.map