Front End Streaming Playback Improvements

This commit is contained in:
daniel-c-harvey
2025-09-13 15:22:26 -04:00
parent cdeb300d5e
commit 0fa8ac7379
8 changed files with 417 additions and 11 deletions
+118
View File
@@ -0,0 +1,118 @@
interface WavHeader {
sampleRate: number;
channels: number;
bitsPerSample: number;
byteRate: number;
blockAlign: number;
dataSize: number;
headerSize: number;
}
class WavUtils {
static parseHeader(chunks: Uint8Array[], totalSize: number): WavHeader | null {
if (totalSize < 44) return null;
const concatenated = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of chunks) {
concatenated.set(chunk, offset);
offset += chunk.length;
}
const view = new DataView(concatenated.buffer, 0, 44);
// Check RIFF header
const riff = new TextDecoder().decode(concatenated.slice(0, 4));
if (riff !== 'RIFF') return null;
const wave = new TextDecoder().decode(concatenated.slice(8, 12));
if (wave !== 'WAVE') return null;
// Find fmt chunk
let fmtOffset = 12;
while (fmtOffset < totalSize - 8) {
const chunkId = new TextDecoder().decode(concatenated.slice(fmtOffset, fmtOffset + 4));
const chunkSize = view.getUint32(fmtOffset + 4, true);
if (chunkId === 'fmt ') {
const channels = view.getUint16(fmtOffset + 10, true);
const sampleRate = view.getUint32(fmtOffset + 12, true);
const byteRate = view.getUint32(fmtOffset + 16, true);
const blockAlign = view.getUint16(fmtOffset + 20, true);
const bitsPerSample = view.getUint16(fmtOffset + 22, true);
return {
sampleRate,
channels,
bitsPerSample,
byteRate,
blockAlign,
dataSize: 0, // Will be updated when we find data chunk
headerSize: 44
};
}
fmtOffset += 8 + chunkSize;
}
return null;
}
static createHeader(wavHeader: WavHeader, dataSize: number): Uint8Array {
const header = new ArrayBuffer(44);
const view = new DataView(header);
// RIFF header
view.setUint8(0, 0x52); view.setUint8(1, 0x49); view.setUint8(2, 0x46); view.setUint8(3, 0x46); // "RIFF"
view.setUint32(4, 36 + dataSize, true); // File size
view.setUint8(8, 0x57); view.setUint8(9, 0x41); view.setUint8(10, 0x56); view.setUint8(11, 0x45); // "WAVE"
// fmt chunk
view.setUint8(12, 0x66); view.setUint8(13, 0x6d); view.setUint8(14, 0x74); view.setUint8(15, 0x20); // "fmt "
view.setUint32(16, 16, true); // fmt chunk size
view.setUint16(20, 1, true); // Audio format (PCM)
view.setUint16(22, wavHeader.channels, true);
view.setUint32(24, wavHeader.sampleRate, true);
view.setUint32(28, wavHeader.byteRate, true);
view.setUint16(32, wavHeader.blockAlign, true);
view.setUint16(34, wavHeader.bitsPerSample, true);
// data chunk header
view.setUint8(36, 0x64); view.setUint8(37, 0x61); view.setUint8(38, 0x74); view.setUint8(39, 0x61); // "data"
view.setUint32(40, dataSize, true);
return new Uint8Array(header);
}
static extractAudioData(chunks: Uint8Array[], totalSize: number, headerSize: number, chunkSize: number): Uint8Array {
const bufferData = new Uint8Array(chunkSize + headerSize);
let dataOffset = headerSize; // Skip header space initially
let remainingSize = chunkSize;
// Fill with audio data, skipping the header from the first chunk
let chunkIndex = 0;
let chunkOffset = headerSize; // Skip WAV header in first chunk
while (remainingSize > 0 && chunkIndex < chunks.length) {
const chunk = chunks[chunkIndex];
const availableInChunk = chunk.length - chunkOffset;
const toCopy = Math.min(availableInChunk, remainingSize);
if (toCopy > 0) {
bufferData.set(chunk.slice(chunkOffset, chunkOffset + toCopy), dataOffset);
dataOffset += toCopy;
remainingSize -= toCopy;
chunkOffset += toCopy;
}
if (chunkOffset >= chunk.length) {
chunkIndex++;
chunkOffset = 0; // No header to skip in subsequent chunks
}
}
return bufferData.slice(0, dataOffset);
}
}
export { WavHeader, WavUtils };