/**
* Музыкальные темы (перенос v1 data/music.ts): генератор партитур
* (детерминированный, сиды) + компиляция движковым секвенсором
* (renderMusicStereo → playLoopBuffer) прямо в рантайме — WAV-файлов нет.
* Тема лугов — пепельная ля-минорная пентатоника: пэд-дрон + редкая
* плак-мелодия восьмыми.
*/
import { Rng, type MusicSpec, type NoteEvent } from '@rpg/engine';
/** Ля-минорная пентатоника от A3 (A C D E G + октава). */
const PENTATONIC = [57, 60, 62, 64, 67, 69, 72] as const;
/** Дрон-пэд: тоника/субмедианта по половинам лупа. */
function drone(from: number, to: number, beats: number): NoteEvent[] {
return [
{ beat: 0, dur: beats / 2, midi: from, vel: 0.5 },
{ beat: beats / 2, dur: beats / 2, midi: to, vel: 0.5 },
];
}
/** Мелодия восьмыми с паузами (вероятность ноты 0.55) — сид фиксирует рисунок. */
function melody(seed: number, beats: number): NoteEvent[] {
const rng = new Rng(seed);
const notes: NoteEvent[] = [];
for (let eighth = 0; eighth < beats * 2; eighth++) {
if (!rng.chance(0.55)) continue;
notes.push({
beat: eighth * 0.5,
dur: 0.5,
midi: PENTATONIC[rng.int(PENTATONIC.length)]!,
vel: 0.35 + rng.next() * 0.25,
});
}
return notes;
}
/** Тема лугов: 16 долей на 70 bpm, дрон A2→G2 + плак по пентатонике (стерео). */
export const MEADOW_THEME: MusicSpec = {
bpm: 70,
beats: 16,
tracks: [
{ instrument: 'pad', notes: drone(45, 43, 16), gain: 0.9, pan: -0.3 },
{ instrument: 'pluck', notes: melody(20260907, 16), pan: 0.3 },
],
};
/** Ритмичный рифф боя: восьмые с пропусками, низкая пентатоника. */
function riff(seed: number, beats: number): NoteEvent[] {
const rng = new Rng(seed);
const low = [45, 48, 50, 52, 55] as const; // A2-минорная низь
const notes: NoteEvent[] = [];
for (let eighth = 0; eighth < beats * 2; eighth++) {
if (!rng.chance(0.7)) continue;
notes.push({
beat: eighth * 0.5,
dur: 0.5,
midi: low[rng.int(low.length)]!,
vel: 0.5 + rng.next() * 0.3,
});
}
return notes;
}
/** Тема боя: 112 bpm, глухой дрон A1 + настойчивый низкий рифф. */
export const COMBAT_THEME: MusicSpec = {
bpm: 112,
beats: 8,
tracks: [
{ instrument: 'pad', notes: drone(33, 33, 8), gain: 1, pan: 0 },
{ instrument: 'pluck', notes: riff(20260908, 8), pan: -0.2 },
],
};
/** Реестр тем (ключ → спек); область ссылается на тему полем `theme`. */
export const THEMES: Record<string, MusicSpec> = {
meadows: MEADOW_THEME,
combat: COMBAT_THEME,
};
/** Тема области по ключу (нет ключа или нет спека — тишина). */
export function themeOf(area: string): MusicSpec | null {
return THEMES[area] ?? null;
}