refactor(live-atc): extract encodeWav to shared/utils, add tests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-10 09:10:29 +02:00
parent c8a07b980f
commit cf9c81c23f
3 changed files with 65 additions and 29 deletions

View File

@@ -1525,6 +1525,7 @@ import { createNoiseGenerators, getReadabilityProfile } from '../../shared/utils
import { createAtisAudioLoop, type AtisAudioLoop } from '../../shared/utils/atisAudioLoop'
import { normalizeManualFreq } from '../../shared/utils/frequency'
import { normalizeSimFreq, normalizeBridgeTelemetry, telemetrySignature } from '../../shared/utils/bridgeTelemetry'
import { encodeWav } from '../../shared/utils/wavEncoder'
import type {
CandidateTraceElimination,
CandidateTraceEntry,
@@ -4037,35 +4038,6 @@ const snapshotPrerecPCM = (): Float32Array => {
return out
}
const encodeWav = (pcm: Float32Array, sampleRate: number): Blob => {
const length = pcm.length
const buffer = new ArrayBuffer(44 + length * 2)
const view = new DataView(buffer)
const writeStr = (off: number, s: string) => {
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i))
}
writeStr(0, 'RIFF')
view.setUint32(4, 36 + length * 2, true)
writeStr(8, 'WAVE')
writeStr(12, 'fmt ')
view.setUint32(16, 16, true)
view.setUint16(20, 1, true)
view.setUint16(22, 1, true)
view.setUint32(24, sampleRate, true)
view.setUint32(28, sampleRate * 2, true)
view.setUint16(32, 2, true)
view.setUint16(34, 16, true)
writeStr(36, 'data')
view.setUint32(40, length * 2, true)
let off = 44
for (let i = 0; i < length; i++) {
const s = Math.max(-1, Math.min(1, pcm[i]))
view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
off += 2
}
return new Blob([buffer], { type: 'audio/wav' })
}
const stopPrerecCapture = () => {
prerecLiveCapture = false
prerecLiveChunks = []

View File

@@ -0,0 +1,28 @@
export const encodeWav = (pcm: Float32Array, sampleRate: number): Blob => {
const length = pcm.length
const buffer = new ArrayBuffer(44 + length * 2)
const view = new DataView(buffer)
const writeStr = (off: number, s: string) => {
for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i))
}
writeStr(0, 'RIFF')
view.setUint32(4, 36 + length * 2, true)
writeStr(8, 'WAVE')
writeStr(12, 'fmt ')
view.setUint32(16, 16, true)
view.setUint16(20, 1, true)
view.setUint16(22, 1, true)
view.setUint32(24, sampleRate, true)
view.setUint32(28, sampleRate * 2, true)
view.setUint16(32, 2, true)
view.setUint16(34, 16, true)
writeStr(36, 'data')
view.setUint32(40, length * 2, true)
let off = 44
for (let i = 0; i < length; i++) {
const s = Math.max(-1, Math.min(1, pcm[i]))
view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
off += 2
}
return new Blob([buffer], { type: 'audio/wav' })
}

View File

@@ -0,0 +1,36 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { encodeWav } from '../../shared/utils/wavEncoder.ts'
test('encodeWav produces a valid RIFF/WAVE header', async () => {
const pcm = new Float32Array([0, 0.5, -0.5, 1, -1])
const blob = encodeWav(pcm, 16000)
const buf = new Uint8Array(await blob.arrayBuffer())
const ascii = (start: number, len: number) =>
String.fromCharCode(...buf.slice(start, start + len))
assert.equal(ascii(0, 4), 'RIFF')
assert.equal(ascii(8, 4), 'WAVE')
assert.equal(ascii(12, 4), 'fmt ')
// sample rate is little-endian at byte offset 24
const view = new DataView(buf.buffer)
assert.equal(view.getUint32(24, true), 16000)
})
test('encodeWav clamps out-of-range samples instead of wrapping', async () => {
const pcm = new Float32Array([2, -2])
const blob = encodeWav(pcm, 16000)
const buf = new Uint8Array(await blob.arrayBuffer())
const view = new DataView(buf.buffer)
// 16-bit PCM data starts at byte 44 in a standard WAV header
assert.equal(view.getInt16(44, true), 32767)
assert.equal(view.getInt16(46, true), -32768)
})
test('encodeWav sets the correct data chunk size', async () => {
const pcm = new Float32Array(10)
const blob = encodeWav(pcm, 8000)
const buf = new Uint8Array(await blob.arrayBuffer())
assert.equal(buf.length, 44 + 20)
const view = new DataView(buf.buffer)
assert.equal(view.getUint32(40, true), 20)
})