/**
* WAV-тулкит без зависимостей: энкодер поверх чистых примитивов синтеза
* (сами примитивы — в synth.mjs, они же используются в рантайме браузера).
* Формат: моно, RATE Гц, 16-бит PCM.
*/
import {
RATE,
bandNoise,
bellPartial,
decay,
loopify,
makeRng,
normalize,
synth
} from './synth.mjs';
export { RATE, bandNoise, bellPartial, decay, loopify, makeRng, normalize, synth };
/** Собрать WAV-файл из Float32 сэмплов (-1..1). */
export function encodeWav(samples, rate = RATE) {
const n = samples.length;
const data = new ArrayBuffer(44 + n * 2);
const view = new DataView(data);
const str = (offset, s) => {
for (let i = 0; i < s.length; i++) view.setUint8(offset + i, s.charCodeAt(i));
};
str(0, 'RIFF');
view.setUint32(4, 36 + n * 2, true);
str(8, 'WAVE');
str(12, 'fmt ');
view.setUint32(16, 16, true); // размер fmt
view.setUint16(20, 1, true); // PCM
view.setUint16(22, 1, true); // моно
view.setUint32(24, rate, true);
view.setUint32(28, rate * 2, true); // байт/сек
view.setUint16(32, 2, true); // блок
view.setUint16(34, 16, true); // бит
str(36, 'data');
view.setUint32(40, n * 2, true);
for (let i = 0; i < n; i++) {
const s = Math.max(-1, Math.min(1, samples[i]));
view.setInt16(44 + i * 2, Math.round(s * 32767), true);
}
return Buffer.from(data);
}
/**
* Разобрать WAV в сэмплы: PCM 16-бит, моно или стерео (даунмикс в моно
* усреднением). Возвращает { samples: Float32Array (-1..1), rate }. Чанки
* (fmt/data) ищем по ID — файлы с LIST-примечаниями читаются корректно.
*/
export function decodeWav(buf) {
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
const tag = (offset, n) => String.fromCharCode(...Array.from({ length: n }, (_, i) => view.getUint8(offset + i)));
if (tag(0, 4) !== 'RIFF' || tag(8, 4) !== 'WAVE') throw new Error('не WAV-файл');
let rate = 0, bits = 0, channels = 0, data = null;
let off = 12;
while (off + 8 <= buf.length) {
const id = tag(off, 4);
const size = view.getUint32(off + 4, true);
if (id === 'fmt ') {
channels = view.getUint16(off + 10, true);
rate = view.getUint32(off + 12, true);
bits = view.getUint16(off + 22, true);
} else if (id === 'data') {
data = { off: off + 8, frames: Math.floor(size / 2 / channels) };
}
off += 8 + size + (size % 2); // чанк выравнивается по слову
}
if (!data || !rate) throw new Error('WAV без fmt/data или не PCM');
if (bits !== 16) throw new Error(`поддержан только PCM 16-бит (в файле ${bits})`);
const samples = new Float32Array(data.frames);
for (let i = 0; i < data.frames; i++) {
let sum = 0;
for (let c = 0; c < channels; c++) sum += view.getInt16(data.off + (i * channels + c) * 2, true) / 32768;
samples[i] = sum / channels;
}
return { samples, rate };
}