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 = sampleFiles .map(([fileName, midi]) => ({ midi, url: `${sampleBaseUrl}${fileName}`, })) .sort((a, b) => a.midi - b.midi); let loadedPianoSamples: Array | null = null; let pianoSampleLoadPromise: Promise> | null = null; export const preloadPianoSamples = ( onProgress?: (progress: PianoSampleLoadProgress) => void ): Promise> => { 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> => { 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 | null => loadedPianoSamples ? [...loadedPianoSamples] : null; export const resetPianoSampleCacheForTest = (): void => { loadedPianoSamples = null; pianoSampleLoadPromise = null; }; const loadPianoSample = async ( decodeContext: BaseAudioContext, sample: PianoSampleDefinition ): Promise => { 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 => new Promise((resolve, reject) => { const decodePromise = decodeContext.decodeAudioData(audioData, resolve, reject); decodePromise?.then(resolve, reject); });