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

@ -1,8 +1,16 @@
interface PianoSampleDefinition {
import type { LoadedPianoSample } from './garden-audio-types';
export interface PianoSampleDefinition {
midi: number;
url: string;
}
export interface PianoSampleLoadProgress {
loadedCount: number;
totalCount: number;
sample?: PianoSampleDefinition;
}
const sampleBaseUrl = `${import.meta.env.BASE_URL}audio/piano/`;
const sampleFiles: Array<[fileName: string, midi: number]> = [
@ -44,3 +52,94 @@ export const pianoSampleDefinitions: Array<PianoSampleDefinition> = sampleFiles
url: `${sampleBaseUrl}${fileName}`,
}))
.sort((a, b) => a.midi - b.midi);
let loadedPianoSamples: Array<LoadedPianoSample> | null = null;
let pianoSampleLoadPromise: Promise<Array<LoadedPianoSample>> | null = null;
export const preloadPianoSamples = (
onProgress?: (progress: PianoSampleLoadProgress) => void
): Promise<Array<LoadedPianoSample>> => {
const OfflineAudioContextConstructor =
globalThis.OfflineAudioContext ?? globalThis.webkitOfflineAudioContext;
if (!OfflineAudioContextConstructor) {
return Promise.reject(
new Error('OfflineAudioContext is required to preload piano samples.')
);
}
const decodeContext = new OfflineAudioContextConstructor(1, 1, 44_100);
return loadPianoSamples(decodeContext, onProgress);
};
export const loadPianoSamples = (
decodeContext: BaseAudioContext,
onProgress?: (progress: PianoSampleLoadProgress) => void
): Promise<Array<LoadedPianoSample>> => {
if (loadedPianoSamples) {
onProgress?.({
loadedCount: loadedPianoSamples.length,
totalCount: pianoSampleDefinitions.length,
});
return Promise.resolve([...loadedPianoSamples]);
}
if (pianoSampleLoadPromise) {
return pianoSampleLoadPromise;
}
let loadedCount = 0;
const totalCount = pianoSampleDefinitions.length;
onProgress?.({ loadedCount, totalCount });
pianoSampleLoadPromise = Promise.all(
pianoSampleDefinitions.map(async (sample) => {
const loadedSample = await loadPianoSample(decodeContext, sample);
loadedCount += 1;
onProgress?.({ loadedCount, totalCount, sample });
return loadedSample;
})
).then(
(samples) => {
loadedPianoSamples = samples.slice().sort((a, b) => a.midi - b.midi);
return [...loadedPianoSamples];
},
(error: unknown) => {
pianoSampleLoadPromise = null;
throw error;
}
);
return pianoSampleLoadPromise;
};
export const getLoadedPianoSamples = (): Array<LoadedPianoSample> | null =>
loadedPianoSamples ? [...loadedPianoSamples] : null;
export const resetPianoSampleCacheForTest = (): void => {
loadedPianoSamples = null;
pianoSampleLoadPromise = null;
};
const loadPianoSample = async (
decodeContext: BaseAudioContext,
sample: PianoSampleDefinition
): Promise<LoadedPianoSample> => {
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 decodeAudioData(decodeContext, audioData);
return { midi: sample.midi, buffer };
};
const decodeAudioData = (
decodeContext: BaseAudioContext,
audioData: ArrayBuffer
): Promise<AudioBuffer> =>
new Promise((resolve, reject) => {
const decodePromise = decodeContext.decodeAudioData(audioData, resolve, reject);
decodePromise?.then(resolve, reject);
});