sw-synth
Version:
Lightweight sound synthesizer designed for real-time user interaction using the Web Audio API
84 lines • 3.6 kB
JavaScript
// Exponential approach conversion, smaller value results in more eager envelopes
const TIME_CONSTANT = 0.5;
// Large but finite number to signify voices that are off
const EXPIRED = 10000;
// Tracking numbers for logging purposes
let VOICE_ID = 1;
/**
* Oscillator with ADSR envelope.
* Represents a single "channel" of polyphony. Should be reused for multiple notes.
*/
export class VoiceBase {
constructor(context, destination, log) {
this.age = EXPIRED;
this.context = context;
this.envelope = new GainNode(context, { gain: 0 });
this.envelope.connect(destination);
this.log = log;
this.noteId = 0;
this.voiceId = VOICE_ID++;
// Piecewise linear transform:
// detune = bend < 0 ? bend * down : bend * up.
// bend is expected in [-1, 1], and down/up are cents.
this.pitchBend = new WaveShaperNode(this.context, {
curve: createAsymmetricCurve(0, 0),
oversample: 'none',
});
}
noteOn(frequency, velocity, noteId, params, pitchBendRange) {
this.log(`Voice ${this.voiceId}: Age = ${this.age}, note = ${noteId}, frequency = ${frequency}`);
this.age = 0;
this.noteId = noteId;
const now = this.context.currentTime + params.audioDelay;
this.log(`Voice ${this.voiceId}: On time = ${now}, sustain time = ${now + params.attackTime}`);
this.envelope.gain.setValueAtTime(0, now);
if (params.attackTime <= 0) {
this.envelope.gain.setValueAtTime(velocity, now);
}
else {
this.envelope.gain.linearRampToValueAtTime(velocity, now + params.attackTime);
}
if (params.decayTime <= 0) {
this.envelope.gain.setValueAtTime(velocity * params.sustainLevel, now + params.attackTime);
}
else {
this.envelope.gain.setTargetAtTime(velocity * params.sustainLevel, now + params.attackTime, params.decayTime * TIME_CONSTANT);
}
// Set pitchbend range
const up = pitchBendRange?.up ?? 0;
const down = pitchBendRange?.down ?? 0;
this.pitchBend.curve = createAsymmetricCurve(down, up);
// Construct a callback that turns this voice off.
const noteOff = () => {
// Do nothing if the voice has been stolen or already released.
if (this.noteId !== noteId) {
this.log(`Voice ${this.voiceId} had been stolen. Ignoring note off`);
return;
}
this.age = EXPIRED;
const then = this.context.currentTime + params.audioDelay;
this.log(`Voice ${this.voiceId}: Off time = ${then}`);
this.envelope.gain.cancelScheduledValues(then);
// NOTE: Canceling scheduled values doesn't hold intermediate values of linear ramps
if (then < now + params.attackTime) {
// Calculate correct linear ramp hold value
this.envelope.gain.setValueAtTime((velocity * (then - now)) / params.attackTime, then);
}
if (params.releaseTime <= 0) {
this.envelope.gain.setValueAtTime(0, then);
}
else {
this.envelope.gain.setTargetAtTime(0, then, params.releaseTime * TIME_CONSTANT);
}
// We're done here.
this.noteId = -1;
};
this.lastNoteOff = noteOff;
return noteOff;
}
dispose() { }
}
function createAsymmetricCurve(down, up) {
return new Float32Array([-down, 0, up]);
}
//# sourceMappingURL=base.js.map