{"version":3,"file":"wav.cjs","names":[],"sources":["../../src/audio/wav.ts"],"sourcesContent":["/** Raw PCM ready to be wrapped in a RIFF container. */\nexport interface PcmAudio {\n    /** One `Float32Array` per channel, samples in −1…1. */\n    channels: readonly Float32Array[];\n    sampleRate: number;\n}\n\n/** Options for {@link blobToWav}. */\nexport interface WavOptions {\n    /**\n     * Mix down to one channel. Default `false`.\n     *\n     * Worth turning on for speech headed to a server: a microphone recorded in stereo\n     * carries two nearly identical channels and doubles the upload for nothing.\n     */\n    mono?: boolean;\n    /**\n     * Resample to this rate. Default: keep the source rate.\n     *\n     * 16000 is the rate most speech-to-text APIs want, and dropping 48 kHz to 16 kHz\n     * removes two thirds of the bytes with no audible loss on voice.\n     */\n    sampleRate?: number;\n}\n\n/** Bytes per sample in the output. 16-bit PCM is what every decoder accepts. */\nconst BYTES_PER_SAMPLE = 2;\n\n/**\n * Wrap PCM in a 16-bit RIFF/WAVE container.\n *\n * The header is the 44-byte canonical form: `RIFF` size, `WAVE`, a 16-byte `fmt `\n * chunk declaring format 1 (uncompressed PCM), then `data`. Samples are interleaved\n * and clamped before scaling, because a value even slightly past ±1 wraps around when\n * truncated to 16 bits and turns a loud passage into a burst of noise.\n *\n * Implemented here rather than pulled from a package: it is a fixed header and a\n * scaling loop, roughly forty lines, and a dependency for it would put its own\n * version bounds on every consumer of this SDK to save writing them.\n *\n * @param audio - Channels and sample rate.\n * @returns A `Blob` of type `audio/wav`.\n */\nexport function encodeWav({ channels, sampleRate }: PcmAudio): Blob {\n    const channelCount = Math.max(1, channels.length);\n    const frameCount = channels[0]?.length ?? 0;\n    const dataBytes = frameCount * channelCount * BYTES_PER_SAMPLE;\n    const buffer = new ArrayBuffer(44 + dataBytes);\n    const view = new DataView(buffer);\n\n    const ascii = (offset: number, text: string): void => {\n        for (let index = 0; index < text.length; index += 1) {\n            view.setUint8(offset + index, text.charCodeAt(index));\n        }\n    };\n\n    const byteRate = sampleRate * channelCount * BYTES_PER_SAMPLE;\n    ascii(0, \"RIFF\");\n    view.setUint32(4, 36 + dataBytes, true);\n    ascii(8, \"WAVE\");\n    ascii(12, \"fmt \");\n    view.setUint32(16, 16, true);\n    view.setUint16(20, 1, true);\n    view.setUint16(22, channelCount, true);\n    view.setUint32(24, sampleRate, true);\n    view.setUint32(28, byteRate, true);\n    view.setUint16(32, channelCount * BYTES_PER_SAMPLE, true);\n    view.setUint16(34, BYTES_PER_SAMPLE * 8, true);\n    ascii(36, \"data\");\n    view.setUint32(40, dataBytes, true);\n\n    let offset = 44;\n    for (let frame = 0; frame < frameCount; frame += 1) {\n        for (let channel = 0; channel < channelCount; channel += 1) {\n            const sample = channels[channel]?.[frame] ?? 0;\n            const clamped = sample < -1 ? -1 : sample > 1 ? 1 : sample;\n            // Asymmetric scaling: 16-bit PCM runs −32768…32767.\n            view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true);\n            offset += BYTES_PER_SAMPLE;\n        }\n    }\n\n    return new Blob([buffer], { type: \"audio/wav\" });\n}\n\n/** Average every channel of a decoded buffer into one. */\nfunction mixToMono(buffer: AudioBuffer): Float32Array {\n    const frames = buffer.length;\n    const out = new Float32Array(frames);\n    for (let channel = 0; channel < buffer.numberOfChannels; channel += 1) {\n        const data = buffer.getChannelData(channel);\n        for (let frame = 0; frame < frames; frame += 1) out[frame] += data[frame];\n    }\n    if (buffer.numberOfChannels > 1) {\n        for (let frame = 0; frame < frames; frame += 1) out[frame] /= buffer.numberOfChannels;\n    }\n    return out;\n}\n\n/**\n * Convert a recording to 16-bit WAV, with no dependency.\n *\n * `MediaRecorder` cannot produce WAV — it emits Opus on Chromium and Firefox and AAC\n * on Safari — so a backend that insists on WAV has to be served either by a\n * server-side transcode or by this. It decodes through `AudioContext.decodeAudioData`,\n * which is the browser's own decoder for whatever container the recorder chose, then\n * re-encodes the PCM.\n *\n * !!! The cost is real: WAV is uncompressed, so the same voice note that is 40 KB in\n * Opus is roughly 500 KB here at 48 kHz stereo. `{ mono: true, sampleRate: 16000 }`\n * takes that to about 80 KB and is what a speech-to-text endpoint wants anyway.\n *\n * Resampling uses `OfflineAudioContext`, i.e. the browser's own resampler, rather\n * than a hand-rolled one — this is exactly the \"we want the underlying call, not a\n * wrapper\" case.\n *\n * @param blob - A recording from {@link createAudioRecorder}, or any decodable audio.\n * @param options - See {@link WavOptions}.\n * @returns A `Blob` of type `audio/wav`.\n * @throws When the environment has no Web Audio, or the blob cannot be decoded.\n */\nexport async function blobToWav(blob: Blob, options: WavOptions = {}): Promise<Blob> {\n    const { mono = false, sampleRate: target } = options;\n\n    const Ctor: typeof AudioContext | undefined =\n        typeof AudioContext !== \"undefined\"\n            ? AudioContext\n            : (globalThis as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n    if (!Ctor) throw new Error(\"Web Audio is not available in this environment.\");\n\n    const context = new Ctor();\n    let decoded: AudioBuffer;\n    try {\n        decoded = await context.decodeAudioData(await blob.arrayBuffer());\n    } finally {\n        void context.close().catch(() => undefined);\n    }\n\n    const wantedChannels = mono ? 1 : decoded.numberOfChannels;\n    const wantedRate = target ?? decoded.sampleRate;\n\n    if (wantedRate === decoded.sampleRate) {\n        const channels = mono\n            ? [mixToMono(decoded)]\n            : Array.from({ length: decoded.numberOfChannels }, (_, index) =>\n                  decoded.getChannelData(index),\n              );\n        return encodeWav({ channels, sampleRate: decoded.sampleRate });\n    }\n\n    const Offline: typeof OfflineAudioContext | undefined =\n        typeof OfflineAudioContext !== \"undefined\" ? OfflineAudioContext : undefined;\n    if (!Offline) throw new Error(\"Resampling requires OfflineAudioContext.\");\n\n    const frames = Math.max(1, Math.round((decoded.duration * wantedRate) | 0) || 1);\n    const offline = new Offline(wantedChannels, frames, wantedRate);\n    const source = offline.createBufferSource();\n    source.buffer = decoded;\n    source.connect(offline.destination);\n    source.start();\n    const rendered = await offline.startRendering();\n\n    return encodeWav({\n        channels: Array.from({ length: rendered.numberOfChannels }, (_, index) =>\n            rendered.getChannelData(index),\n        ),\n        sampleRate: rendered.sampleRate,\n    });\n}\n"],"mappings":"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"}