145 lines
4 KiB
TypeScript
145 lines
4 KiB
TypeScript
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]> = [
|
|
['A0v12.m4a', 21],
|
|
['C1v12.m4a', 24],
|
|
['Dsharp1v12.m4a', 27],
|
|
['Fsharp1v12.m4a', 30],
|
|
['A1v12.m4a', 33],
|
|
['C2v12.m4a', 36],
|
|
['Dsharp2v12.m4a', 39],
|
|
['Fsharp2v12.m4a', 42],
|
|
['A2v12.m4a', 45],
|
|
['C3v12.m4a', 48],
|
|
['Dsharp3v12.m4a', 51],
|
|
['Fsharp3v12.m4a', 54],
|
|
['A3v12.m4a', 57],
|
|
['C4v12.m4a', 60],
|
|
['Dsharp4v12.m4a', 63],
|
|
['Fsharp4v12.m4a', 66],
|
|
['A4v12.m4a', 69],
|
|
['C5v12.m4a', 72],
|
|
['Dsharp5v12.m4a', 75],
|
|
['Fsharp5v12.m4a', 78],
|
|
['A5v12.m4a', 81],
|
|
['C6v12.m4a', 84],
|
|
['Dsharp6v12.m4a', 87],
|
|
['Fsharp6v12.m4a', 90],
|
|
['A6v12.m4a', 93],
|
|
['C7v12.m4a', 96],
|
|
['Dsharp7v12.m4a', 99],
|
|
['Fsharp7v12.m4a', 102],
|
|
['A7v12.m4a', 105],
|
|
['C8v12.m4a', 108],
|
|
];
|
|
|
|
export const pianoSampleDefinitions: Array<PianoSampleDefinition> = sampleFiles
|
|
.map(([fileName, midi]) => ({
|
|
midi,
|
|
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);
|
|
});
|