Newer
Older
rpg / v1 / packages / engine / tools / wav.mjs
/**
 * 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 };
}