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.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user