This commit is contained in:
Andras Schmelczer 2026-05-13 21:07:10 +01:00
commit 39b0160064
136 changed files with 7129 additions and 1950 deletions

View file

@ -1,3 +1,4 @@
import { appConfig } from '../config';
import { clamp, clamp01 } from '../utils/clamp';
import { GardenAudioConfig } from './garden-audio-config';
import { GardenAudioGraph } from './garden-audio-graph';
@ -22,6 +23,9 @@ export class PianoSampler {
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 };
@ -56,12 +60,22 @@ export class PianoSampler {
return;
}
const scheduledStart = Math.max(context.currentTime + 0.002, startTime);
const scheduledStart = Math.max(
context.currentTime + appConfig.audioEngine.piano.scheduleAheadSeconds,
startTime
);
const noteVelocity = clamp01(velocity);
const noteGainValue = Math.max(0.0001, this.config.piano.gain * noteVelocity);
const noteGainValue = Math.max(
appConfig.audioEngine.piano.minGain,
this.config.piano.gain * noteVelocity
);
const sustainSeconds =
this.config.piano.sustainSeconds * (0.45 + noteVelocity * 0.55);
const sustainAt = scheduledStart + Math.max(0.08, durationSeconds);
this.config.piano.sustainSeconds *
(appConfig.audioEngine.piano.sustainBase +
noteVelocity * appConfig.audioEngine.piano.sustainVelocityRange);
const sustainAt =
scheduledStart +
Math.max(appConfig.audioEngine.piano.minDurationSeconds, durationSeconds);
const releaseAt = sustainAt + sustainSeconds;
const releaseSeconds = this.config.piano.releaseSeconds;
const stopAt = releaseAt + releaseSeconds;
@ -75,26 +89,55 @@ export class PianoSampler {
while (this.activeVoices.length >= this.config.piano.maxVoices) {
const oldest = this.activeVoices.shift();
oldest?.gain.gain.cancelScheduledValues(scheduledStart);
oldest?.gain.gain.setTargetAtTime(0.0001, scheduledStart, 0.025);
oldest?.source.stop(scheduledStart + 0.05);
oldest?.gain.gain.setTargetAtTime(
appConfig.audioEngine.piano.minGain,
scheduledStart,
appConfig.audioEngine.piano.voiceStealFadeSeconds
);
oldest?.source.stop(
scheduledStart + appConfig.audioEngine.piano.voiceStealStopSeconds
);
}
source.buffer = sample.buffer;
source.playbackRate.setValueAtTime(
Math.pow(2, (midi - sample.midi) / 12),
Math.pow(
2,
(midi - sample.midi) / appConfig.audioEngine.piano.pitchSemitonesPerOctave
),
scheduledStart
);
filter.type = 'lowpass';
filter.frequency.setValueAtTime(clamp(lowpassHz, 1400, 12000), scheduledStart);
filter.Q.value = 0.7;
gain.gain.setValueAtTime(0.0001, scheduledStart);
gain.gain.exponentialRampToValueAtTime(noteGainValue, scheduledStart + 0.006);
gain.gain.setTargetAtTime(
Math.max(0.0001, noteGainValue * this.config.piano.sustainLevel),
sustainAt,
Math.max(0.04, sustainSeconds * 0.45)
filter.frequency.setValueAtTime(
clamp(
lowpassHz,
appConfig.audioEngine.piano.lowpassMinHz,
appConfig.audioEngine.piano.lowpassMaxHz
),
scheduledStart
);
filter.Q.value = appConfig.audioEngine.piano.filterQ;
gain.gain.setValueAtTime(appConfig.audioEngine.piano.minGain, scheduledStart);
gain.gain.exponentialRampToValueAtTime(
noteGainValue,
scheduledStart + appConfig.audioEngine.piano.gainAttackSeconds
);
gain.gain.setTargetAtTime(
Math.max(
appConfig.audioEngine.piano.minGain,
noteGainValue * this.config.piano.sustainLevel
),
sustainAt,
Math.max(
appConfig.audioEngine.piano.minFadeSeconds,
sustainSeconds * appConfig.audioEngine.piano.sustainBase
)
);
gain.gain.setTargetAtTime(
appConfig.audioEngine.piano.minGain,
releaseAt,
releaseSeconds
);
gain.gain.setTargetAtTime(0.0001, releaseAt, releaseSeconds);
panner.pan.setValueAtTime(clamp(pan, -1, 1), scheduledStart);
source.connect(filter);
@ -102,7 +145,7 @@ export class PianoSampler {
gain.connect(panner);
panner.connect(eventBus);
if (delayInput && this.config.delay.enabled && delaySend > 0) {
if (delayInput && delaySend > 0) {
sendGain = context.createGain();
sendGain.gain.value = delaySend;
panner.connect(sendGain);
@ -110,8 +153,8 @@ export class PianoSampler {
}
source.start(scheduledStart);
source.stop(stopAt + 0.05);
this.activeVoices.push({ gain, source, stopAt });
source.stop(stopAt + appConfig.audioEngine.piano.tailStopExtraSeconds);
this.activeVoices.push({ gain, source, startAt: scheduledStart, stopAt });
source.addEventListener(
'ended',
@ -127,13 +170,35 @@ export class PianoSampler {
);
}
public fadeActive(now: number): void {
public fadeActive(
now: number,
fadeSeconds = appConfig.audioEngine.piano.defaultFadeSeconds
): void {
this.activeVoices.forEach((voice) => {
voice.gain.gain.cancelScheduledValues(now);
voice.gain.gain.setTargetAtTime(0.0001, now, 0.035);
voice.stopAt = Math.min(voice.stopAt, now + 0.28);
if (voice.startAt > now) {
voice.gain.gain.setValueAtTime(appConfig.audioEngine.piano.minGain, now);
voice.stopAt = now;
try {
voice.source.stop(now);
} catch {
// The source may already have a stop time scheduled.
}
return;
}
const stopAt = Math.min(voice.stopAt, now + fadeSeconds);
voice.gain.gain.setTargetAtTime(
appConfig.audioEngine.piano.minGain,
now,
Math.max(
appConfig.audioEngine.piano.minFadeSeconds,
fadeSeconds * appConfig.audioEngine.piano.fadeTimeConstantRatio
)
);
voice.stopAt = stopAt;
try {
voice.source.stop(now + 0.28);
voice.source.stop(stopAt);
} catch {
// The source may already have a stop time scheduled.
}