Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 82 additions & 62 deletions src/engine/SynthEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
getLegacySynthPresetFromInstrument,
} from '../utils/trackInstrument';

type SynthSource = TrackInstrument | SynthPreset;
export type SynthSource = TrackInstrument | SynthPreset;
type RuntimeInstrument = SubtractiveTrackInstrument | FmTrackInstrument;
type SynthVoiceType = 'mono' | 'fm';
type SynthModulationEffectType = 'tremolo' | 'autoPanner' | 'autoFilter' | 'vibrato';
Expand All @@ -23,15 +23,20 @@ const MAX_FAT_SPREAD_CENTS = 120;
interface RuntimeModulationRack {
node: Tone.ToneAudioNode;
retriggerOnNote: boolean;
restart: () => void;
restart: (time?: number) => void;
dispose: () => void;
}

interface SynthInstance {
export interface SynthPlaybackChain {
synth: Tone.PolySynth;
signature: string;
gain: Tone.Gain;
modulation: RuntimeModulationRack | null;
restartModulation: (time?: number) => void;
dispose: () => void;
}

interface SynthInstance {
playback: SynthPlaybackChain;
signature: string;
}

export interface SynthRuntimeSpec {
Expand All @@ -50,6 +55,12 @@ export interface SynthModulationSpec {
options: Record<string, unknown>;
}

interface CreateSynthPlaybackChainOptions {
gainScale?: number;
connectTo?: Tone.InputNode;
routeToDestination?: boolean;
}

function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
Expand Down Expand Up @@ -318,11 +329,11 @@ function createRuntimeModulationRack(source: SynthSource): RuntimeModulationRack
return {
node,
retriggerOnNote: spec.retrigger,
restart: () => {
restart: (time) => {
if (!spec.retrigger) return;
const now = Tone.now();
node.stop(now);
node.start(now + 0.001);
const restartAt = time ?? Tone.now();
node.stop(restartAt);
node.start(restartAt + 0.001);
},
dispose: () => node.dispose(),
};
Expand All @@ -333,11 +344,11 @@ function createRuntimeModulationRack(source: SynthSource): RuntimeModulationRack
return {
node,
retriggerOnNote: spec.retrigger,
restart: () => {
restart: (time) => {
if (!spec.retrigger) return;
const now = Tone.now();
node.stop(now);
node.start(now + 0.001);
const restartAt = time ?? Tone.now();
node.stop(restartAt);
node.start(restartAt + 0.001);
},
dispose: () => node.dispose(),
};
Expand All @@ -348,11 +359,11 @@ function createRuntimeModulationRack(source: SynthSource): RuntimeModulationRack
return {
node,
retriggerOnNote: spec.retrigger,
restart: () => {
restart: (time) => {
if (!spec.retrigger) return;
const now = Tone.now();
node.stop(now);
node.start(now + 0.001);
const restartAt = time ?? Tone.now();
node.stop(restartAt);
node.start(restartAt + 0.001);
},
dispose: () => node.dispose(),
};
Expand All @@ -374,6 +385,7 @@ function connectSynthChain(
modulation: RuntimeModulationRack | null,
gain: Tone.Gain,
connectTo?: Tone.InputNode,
routeToDestination: boolean = true,
) {
if (modulation) {
synth.connect(modulation.node);
Expand All @@ -384,28 +396,49 @@ function connectSynthChain(

if (connectTo) {
gain.connect(connectTo);
} else {
} else if (routeToDestination) {
gain.toDestination();
}
}

function restartModulation(instance: SynthInstance | null | undefined) {
if (!instance?.modulation?.retriggerOnNote) return;
instance.modulation.restart();
export function createSynthPlaybackChain(
source: SynthSource,
options: CreateSynthPlaybackChainOptions = {},
): SynthPlaybackChain {
const { gainScale = 1, connectTo, routeToDestination = true } = options;
const spec = createSynthRuntimeSpec(source);
const synth = createSynthForSource(source);
const modulation = createRuntimeModulationRack(source);
const gain = new Tone.Gain(Math.max(0, Math.min(2, spec.gainLevel * gainScale)));
connectSynthChain(synth, modulation, gain, connectTo, routeToDestination);

return {
synth,
gain,
restartModulation: (time) => {
if (!modulation?.retriggerOnNote) return;
modulation.restart(time);
},
dispose: () => {
synth.releaseAll();
synth.dispose();
modulation?.dispose();
gain.dispose();
},
};
}

function restartPlaybackModulation(instance: SynthInstance | null | undefined, time?: number) {
instance?.playback.restartModulation(time);
}

function disposeSynthInstance(instance: SynthInstance) {
instance.synth.releaseAll();
instance.synth.dispose();
instance.modulation?.dispose();
instance.gain.dispose();
instance.playback.dispose();
}

class SynthEngine {
private synths = new Map<string, SynthInstance>();
private previewSynth: Tone.PolySynth | null = null;
private previewGain: Tone.Gain | null = null;
private previewModulation: RuntimeModulationRack | null = null;
private previewPlayback: SynthPlaybackChain | null = null;
private previewSignature: string | null = null;

async ensureStarted() {
Expand All @@ -417,51 +450,42 @@ class SynthEngine {
ensureTrackSynth(trackId: string, source: SynthSource, connectTo?: Tone.InputNode): Tone.PolySynth {
const signature = getSynthSignature(source);
const existing = this.synths.get(trackId);
if (existing && existing.signature === signature) return existing.synth;
if (existing && existing.signature === signature) return existing.playback.synth;

if (existing) {
disposeSynthInstance(existing);
}

const spec = createSynthRuntimeSpec(source);
const synth = createSynthForSource(source);
const modulation = createRuntimeModulationRack(source);
const gain = new Tone.Gain(spec.gainLevel);
connectSynthChain(synth, modulation, gain, connectTo);
this.synths.set(trackId, { synth, signature, gain, modulation });
return synth;
const playback = createSynthPlaybackChain(source, { connectTo, routeToDestination: true });
this.synths.set(trackId, { playback, signature });
return playback.synth;
}

getSynth(trackId: string): Tone.PolySynth | null {
return this.synths.get(trackId)?.synth ?? null;
return this.synths.get(trackId)?.playback.synth ?? null;
}

async previewNote(pitch: number, velocity = 100, duration = 0.3, source: SynthSource = 'piano') {
await this.ensureStarted();
const signature = getSynthSignature(source);

if (!this.previewSynth || !this.previewGain || this.previewSignature !== signature) {
this.previewSynth?.dispose();
this.previewModulation?.dispose();
this.previewGain?.dispose();
const spec = createSynthRuntimeSpec(source);
this.previewSynth = createSynthForSource(source);
this.previewModulation = createRuntimeModulationRack(source);
this.previewGain = new Tone.Gain(Math.max(0, Math.min(1, spec.gainLevel * 0.55)));
connectSynthChain(this.previewSynth, this.previewModulation, this.previewGain);
if (!this.previewPlayback || this.previewSignature !== signature) {
this.previewPlayback?.dispose();
this.previewPlayback = createSynthPlaybackChain(source, {
gainScale: 0.55,
routeToDestination: true,
});
this.previewSignature = signature;
}
const freq = Tone.Frequency(pitch, 'midi').toFrequency();
if (this.previewModulation?.retriggerOnNote) {
this.previewModulation.restart();
}
this.previewSynth.triggerAttackRelease(freq, duration, undefined, velocity / 127);
this.previewPlayback.restartModulation();
this.previewPlayback.synth.triggerAttackRelease(freq, duration, undefined, velocity / 127);
}

async playNote(trackId: string, pitch: number, velocity: number, duration: number, source: SynthSource) {
await this.ensureStarted();
const synth = this.ensureTrackSynth(trackId, source);
restartModulation(this.synths.get(trackId));
restartPlaybackModulation(this.synths.get(trackId));
const freq = Tone.Frequency(pitch, 'midi').toFrequency();
synth.triggerAttackRelease(freq, duration, undefined, velocity / 127);
}
Expand All @@ -481,7 +505,7 @@ class SynthEngine {
triggerRelease: (note: number, time?: string | number) => void;
triggerAttackRelease: (note: number, duration: number, time?: string | number, velocity?: number) => void;
};
restartModulation(this.synths.get(trackId));
restartPlaybackModulation(this.synths.get(trackId));
const glideTime = Math.max(0.03, Math.min(0.12, duration * 0.35));
const fromFreq = Tone.Frequency(fromPitch, 'midi').toFrequency();
const toFreq = Tone.Frequency(toPitch, 'midi').toFrequency();
Expand All @@ -495,23 +519,23 @@ class SynthEngine {
noteOn(trackId: string, pitch: number, velocity = 100) {
const instance = this.synths.get(trackId);
if (!instance) return;
restartModulation(instance);
restartPlaybackModulation(instance);
const freq = Tone.Frequency(pitch, 'midi').toFrequency();
instance.synth.triggerAttack(freq, undefined, velocity / 127);
instance.playback.synth.triggerAttack(freq, undefined, velocity / 127);
}

/** Trigger note off for a track synth. */
noteOff(trackId: string, pitch: number) {
const instance = this.synths.get(trackId);
if (!instance) return;
const freq = Tone.Frequency(pitch, 'midi').toFrequency();
instance.synth.triggerRelease(freq);
instance.playback.synth.triggerRelease(freq);
}

/** Release all currently sounding notes on all track synths. */
releaseAll() {
for (const instance of this.synths.values()) {
instance.synth.releaseAll();
instance.playback.synth.releaseAll();
}
}

Expand All @@ -526,12 +550,8 @@ class SynthEngine {
for (const trackId of this.synths.keys()) {
this.removeTrackSynth(trackId);
}
this.previewSynth?.dispose();
this.previewModulation?.dispose();
this.previewGain?.dispose();
this.previewSynth = null;
this.previewModulation = null;
this.previewGain = null;
this.previewPlayback?.dispose();
this.previewPlayback = null;
this.previewSignature = null;
}
}
Expand Down
Loading