/**
* Аудио игры (трек 18): AudioManager движка + темы областей. Тема — процедурная
* партитура (data/music.ts): renderMusicStereo → AudioBuffer → луп на шине
* music с кроссфейдом при смене области (v1 LocationAudio.playTheme). Амбиент
* (климатические лупы v1) — не входит: на лугах тишина (по фидбеку v1),
* шаги и боевые звуки — треки 19+.
*/
import { AudioManager, renderMusicStereo } from '@rpg/engine';
import type { MusicHandle, MusicSpec } from '@rpg/engine';
import { themeOf } from './data/music';
/** Ширина кроссфейда темы (сек) — как fade v1 (2 с лупов амбиента, музыка резче). */
const THEME_FADE = 1.2;
export class GameAudio {
readonly manager = new AudioManager((key) => key);
/** Запущенные темы (для моста/гейта: факт запуска видно в шпионе). */
private current: string | null = null;
/** Хендл текущего лупа: startSlot не занимает слот музыки — стопим сами. */
private handle: MusicHandle | null = null;
/** Кольцевой буфер фактических запусков (мост/гейт — как в демке). */
readonly log: { key: string; volume: number }[] = [];
constructor() {
// контекст и шины синхронно: буферы компилируются до первого жеста
this.manager.prepare();
this.manager.unlock(); // в headless контекст разрешён без жеста
this.manager.onPlayed = (key, opts) => {
this.log.push({ key, volume: opts.volume ?? 1 });
if (this.log.length > 24) this.log.shift();
};
}
/** Тема области: кроссфейд на шине music; нет темы — тишина (v1: нет theme). */
async playTheme(areaId: string): Promise<void> {
const spec: MusicSpec | null = themeOf(areaId);
if (spec === null) {
if (this.current !== null) {
this.current = null;
this.handle?.stop(THEME_FADE);
this.handle = null;
}
return;
}
if (this.current === areaId) return; // та же область — не перезапускать
this.current = areaId;
const prev = this.handle;
this.handle = null;
const buf = this.manager.createBuffer(renderMusicStereo(spec), 22050, 2);
if (!buf) {
prev?.stop(THEME_FADE);
return;
}
const h = await this.manager.playLoopBuffer(`theme/${areaId}`, buf, {
bus: 'music', fade: THEME_FADE, volume: 0.55,
});
// за время await область могла смениться — неактуальный луп глушим сразу
if (this.current !== areaId) {
h?.stop(0);
prev?.stop(THEME_FADE);
return;
}
prev?.stop(THEME_FADE); // кроссфейд: прежняя тема затухает под новой
this.handle = h;
}
/** Что играет (мост/гейт). */
get theme(): string | null {
return this.current;
}
}