/** * WavFormatDecoder - WAV/PCM implementation of IFormatDecoder. * * All WAV-specific stream logic lives here: header parsing, frame-aligned segment * sizing, segment wrapping with a synthesised 44-byte PCM header, and block-aligned * seek byte-offset calculation. StreamDecoder delegates to this via IFormatDecoder * and holds no WAV knowledge of its own. * * The low-level byte routines (parseHeader, createHeader, getSampleAlignedChunkSize) * are reused from WavUtils rather than duplicated — this is the same proven logic the * decoder used before the format-agnostic refactor, kept in one place. */ import { WavUtils } from '../wavutils.js'; import { FormatInfo, IFormatDecoder } from './IFormatDecoder.js'; export class WavFormatDecoder implements IFormatDecoder { tryParseHeader(chunks: Uint8Array[], totalSize: number): FormatInfo | null { const header = WavUtils.parseHeader(chunks, totalSize); if (!header) return null; return { sampleRate: header.sampleRate, channels: header.channels, bitsPerSample: header.bitsPerSample, byteRate: header.byteRate, blockAlign: header.blockAlign, // dataSize / byteRate is the exact PCM duration; guard against a zero // byteRate (malformed fmt chunk) rather than producing Infinity/NaN. totalDuration: header.byteRate > 0 ? header.dataSize / header.byteRate : null, audioDataOffset: header.headerSize, seekData: null }; } getAlignedSegmentSize( info: FormatInfo, availableBytes: number, requestedSize: number, streamComplete: boolean ): number { // blockAlign is the PCM frame size; reuse the original frame-alignment routine // verbatim so streaming/tail-drain behavior is unchanged. return WavUtils.getSampleAlignedChunkSize( { blockAlign: info.blockAlign }, requestedSize, availableBytes, streamComplete ); } wrapSegment(info: FormatInfo, rawBytes: Uint8Array): Uint8Array { const header = WavUtils.createHeader( { channels: info.channels, sampleRate: info.sampleRate, byteRate: info.byteRate, blockAlign: info.blockAlign, bitsPerSample: info.bitsPerSample }, rawBytes.length ); const wavFile = new Uint8Array(header.length + rawBytes.length); wavFile.set(header, 0); wavFile.set(rawBytes, header.length); return wavFile; } calculateByteOffset(info: FormatInfo, positionSeconds: number): number { // No header / malformed fmt chunk: cannot compute. Return the audio start so the // caller still has a valid (file-absolute) Range target rather than a negative. if (info.byteRate <= 0 || info.blockAlign <= 0) return info.audioDataOffset; const rawOffset = Math.floor(positionSeconds * info.byteRate); // Align to a PCM frame boundary for clean audio, then make it file-absolute by // adding the header size — the Range request is a byte position in the file on disk. const alignedAudioOffset = Math.floor(rawOffset / info.blockAlign) * info.blockAlign; return info.audioDataOffset + alignedAudioOffset; } }