/**
* Кэш декодированных аудио-буферов: реестр URL + decodeAudioData.
* Параллельные вызовы decode по одному ключу делят один in-flight промис —
* двойной fetch+decode при старте сцены исключён.
*/
export class BufferCache {
private urls = new Map<string, string>();
private buffers = new Map<string, AudioBuffer>();
private pending = new Map<string, Promise<AudioBuffer | null>>();
/** Зарегистрировать URL ключа (без загрузки — файл читается в decode). */
register(key: string, url: string): void {
this.urls.set(key, url);
}
/** Уже декодированный буфер (без загрузки); не декодирован — null. */
cached(key: string): AudioBuffer | null {
return this.buffers.get(key) ?? null;
}
/** Декодировать (fetch + decodeAudioData); сбой — null с warn в консоль. */
async decode(key: string, ctx: AudioContext): Promise<AudioBuffer | null> {
const cached = this.buffers.get(key);
if (cached) return cached;
let flight = this.pending.get(key);
if (!flight) {
const url = this.urls.get(key);
if (!url) return null;
flight = this.fetchDecode(key, url, ctx).finally(() => this.pending.delete(key));
this.pending.set(key, flight);
}
return flight;
}
private async fetchDecode(key: string, url: string, ctx: AudioContext): Promise<AudioBuffer | null> {
try {
const res = await fetch(url);
const buf = await ctx.decodeAudioData(await res.arrayBuffer());
this.buffers.set(key, buf);
return buf;
} catch (err) {
console.warn(`[audio] не удалось декодировать ${key}:`, err);
return null;
}
}
}