/**
* Чистые примитивы синтеза (перенос v1 tools/synth.mjs): детерминированный
* шум, полосовой фильтр, огибающие, нормализация, кроссфейд-луп. В v2 живёт
* в src движка (TS) — рантайм (SoundSpec/Music) и будущие тулзы делят одни
* формулы, поэтому спек-звук и WAV-звук звучат в одной палитре.
* Частота по умолчанию — RATE Гц (моно).
*/
/** Частота дискретизации процедурного звука. */
export const RATE = 22050;
/** Детерминированный ГПСЧ (mulberry32). */
export function makeRng(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (s + 0x6d2b79f5) >>> 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
/** Синтез: секунды → Float32[]. fn(t, i) → сэмпл. */
export function synth(seconds: number, fn: (t: number, i: number) => number, rate = RATE): Float32Array {
const n = Math.floor(seconds * rate);
const out = new Float32Array(n);
for (let i = 0; i < n; i++) out[i] = fn(i / rate, i);
return out;
}
/** Экспоненциальный спад: полный на старте, ~0 к концу. */
export function decay(t: number, dur: number, power = 4): number {
return Math.pow(Math.max(0, 1 - t / dur), power);
}
/** Колокольный партиал: негармоничный (частота × растяжка), быстрый спад. */
export function bellPartial(freq: number, stretch: number, dur: number, power: number): (t: number) => number {
return (t) => Math.sin(2 * Math.PI * freq * stretch * t) * decay(t, dur, power);
}
/** Полосовой шум через однополюсные фильтры (низ + верх). */
export function bandNoise(seed: number, lowHz: number, highHz: number, _dur = 0): () => number {
const r = makeRng(seed);
let l1 = 0;
let l2 = 0;
const aLow = 1 - Math.exp((-2 * Math.PI * lowHz) / RATE);
const aHigh = 1 - Math.exp((-2 * Math.PI * highHz) / RATE);
return () => {
const white = r() * 2 - 1;
l1 += aHigh * (white - l1); // фильтр верхов: выше highHz глушим
l2 += aLow * (l1 - l2); // фильтр низов: ниже lowHz глушим
return l2 - l1 < -1 ? -1 : Math.max(-1, Math.min(1, l1 - l2));
};
}
/** Нормализовать по пику. */
export function normalize(samples: Float32Array, peak = 0.9): Float32Array {
let max = 0;
for (const s of samples) max = Math.max(max, Math.abs(s));
if (max === 0) return samples;
const k = peak / max;
return samples.map((s) => s * k);
}
/** Сшить луп: кроссфейд первых fade-секунд с концом. */
export function loopify(samples: Float32Array, fadeSeconds = 0.5): Float32Array {
const n = Math.floor(fadeSeconds * RATE);
for (let i = 0; i < n; i++) {
const k = i / n;
samples[i] = samples[i]! * k + samples[samples.length - n + i]! * (1 - k);
}
return samples.slice(0, samples.length - n);
}