Files
daniel-c-harvey 0b0bcb3dee refactor(audio): extract IFormatDecoder/WavFormatDecoder and wire Content-Type to JS format selection
StreamDecoder is now format-agnostic; WavFormatDecoder delegates to WavUtils; contentType flows C# to JS.
2026-06-11 06:08:09 -04:00

81 lines
3.3 KiB
TypeScript

/**
* 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;
}
}