0b0bcb3dee
StreamDecoder is now format-agnostic; WavFormatDecoder delegates to WavUtils; contentType flows C# to JS.
81 lines
3.3 KiB
TypeScript
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;
|
|
}
|
|
}
|