This commit is contained in:
Andras Schmelczer 2026-05-16 15:05:35 +01:00
commit 1fe5015056
55 changed files with 2036 additions and 685 deletions

View file

@ -3,7 +3,7 @@ import { clamp, clamp01 } from '../utils/clamp';
import { GardenAudioConfig } from './garden-audio-config';
import { GardenAudioGraph } from './garden-audio-graph';
import { ActivePianoVoice, LoadedPianoSample, PianoNote } from './garden-audio-types';
import { pianoSampleDefinitions } from './piano-samples';
import { getLoadedPianoSamples, loadPianoSamples } from './piano-samples';
export class PianoSampler {
private sampleLoadPromise: Promise<void> | null = null;
@ -16,28 +16,20 @@ export class PianoSampler {
private readonly graph: GardenAudioGraph
) {}
public async load(context: AudioContext): Promise<void> {
public load(context: BaseAudioContext): Promise<void> {
const loadedSamples = getLoadedPianoSamples();
if (loadedSamples) {
this.setSamples(loadedSamples);
return Promise.resolve();
}
if (this.sampleLoadPromise) {
return this.sampleLoadPromise;
}
this.sampleLoadPromise = Promise.all(
pianoSampleDefinitions.map(async (sample) => {
const response = await fetch(sample.url);
if (!response.ok) {
throw new Error(`Unable to load piano sample ${sample.url}`);
}
const audioData = await response.arrayBuffer();
const buffer = await context.decodeAudioData(audioData);
return { midi: sample.midi, buffer };
})
)
.then((samples) => {
this.samples = samples.sort((a, b) => a.midi - b.midi);
})
.catch(() => {
this.samples = [];
});
this.sampleLoadPromise = loadPianoSamples(context).then((samples) => {
this.setSamples(samples);
});
return this.sampleLoadPromise;
}
@ -89,13 +81,9 @@ export class PianoSampler {
this.trimActiveVoices(scheduledStart);
while (this.activeVoices.length >= this.config.piano.maxVoices) {
const oldest = this.activeVoices.shift();
oldest?.gain.gain.cancelScheduledValues(scheduledStart);
oldest?.gain.gain.setTargetAtTime(
this.engineConfig.piano.minGain,
scheduledStart,
this.engineConfig.piano.voiceStealFadeSeconds
);
oldest?.source.stop(scheduledStart + this.engineConfig.piano.voiceStealStopSeconds);
if (oldest) {
this.stopVoice(oldest, scheduledStart);
}
}
source.buffer = sample.buffer;
@ -162,6 +150,21 @@ export class PianoSampler {
);
}
public stopAll(): void {
const context = this.graph.context;
if (!context) {
this.activeVoices = [];
return;
}
const now = context.currentTime;
this.activeVoices.forEach((voice) => {
this.stopVoice(voice, now);
});
this.activeVoices = [];
}
public reset(): void {
this.sampleLoadPromise = null;
this.samples = [];
@ -181,4 +184,25 @@ export class PianoSampler {
private trimActiveVoices(now: number): void {
this.activeVoices = this.activeVoices.filter((voice) => voice.stopAt > now);
}
private stopVoice(voice: ActivePianoVoice, now: number): void {
const stopAt = now + this.engineConfig.piano.voiceStealStopSeconds;
voice.gain.gain.cancelScheduledValues(now);
voice.gain.gain.setTargetAtTime(
this.engineConfig.piano.minGain,
now,
this.engineConfig.piano.voiceStealFadeSeconds
);
voice.stopAt = stopAt;
try {
voice.source.stop(stopAt);
} catch {
// The voice may already have ended; either way it is no longer part of the mix.
}
}
private setSamples(samples: Array<LoadedPianoSample>): void {
this.samples = samples.slice().sort((a, b) => a.midi - b.midi);
}
}