-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathtest.js
More file actions
316 lines (275 loc) · 12.4 KB
/
Copy pathtest.js
File metadata and controls
316 lines (275 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import decode, { decoder } from './decode-wav.js'
import { readFileSync } from 'node:fs'
let pass = 0, fail = 0
function ok(cond, msg) {
if (cond) { pass++; console.log(' ok', msg) }
else { fail++; console.log(' FAIL', msg) }
}
function near(a, b, tol = 0.0001) { return Math.abs(a - b) < tol }
// ===== WAV fixture builder =====
function buildWav({ sr = 44100, ch = 1, bitDepth = 16, float = false, samples }) {
let formatId = float ? 3 : 1
let byteDepth = bitDepth / 8
let dataSize = samples.length * byteDepth
let buf = new ArrayBuffer(44 + dataSize)
let v = new DataView(buf), p = 0
let s = (str) => { for (let i = 0; i < str.length; i++) { v.setUint8(p++, str.charCodeAt(i)) } }
let u16 = (x) => { v.setUint16(p, x, true); p += 2 }
let u32 = (x) => { v.setUint32(p, x, true); p += 4 }
s('RIFF'); u32(36 + dataSize); s('WAVE')
s('fmt '); u32(16); u16(formatId); u16(ch); u32(sr)
u32(sr * ch * byteDepth); u16(ch * byteDepth); u16(bitDepth)
s('data'); u32(dataSize)
let dv = new DataView(buf, 44)
for (let i = 0; i < samples.length; i++) {
let s = samples[i]
if (bitDepth === 8) dv.setUint8(i, Math.round(s * 127 + 128))
else if (bitDepth === 16) dv.setInt16(i * 2, Math.round(s < 0 ? s * 32768 : s * 32767), true)
else if (bitDepth === 24) {
let v = Math.round(s < 0 ? s * 8388608 : s * 8388607)
if (v < 0) v += 0x1000000
dv.setUint8(i * 3, v & 0xFF)
dv.setUint8(i * 3 + 1, (v >> 8) & 0xFF)
dv.setUint8(i * 3 + 2, (v >> 16) & 0xFF)
} else if (bitDepth === 32 && !float) dv.setInt32(i * 4, Math.round(s < 0 ? s * 2147483648 : s * 2147483647), true)
else if (bitDepth === 32 && float) dv.setFloat32(i * 4, s, true)
else if (bitDepth === 64 && float) dv.setFloat64(i * 8, s, true)
}
return new Uint8Array(buf)
}
// sine wave samples
function sine(n, freq = 440, sr = 44100) {
return Array.from({ length: n }, (_, i) => Math.sin(2 * Math.PI * freq * i / sr))
}
// ===== bit depth round-trips =====
let signal = sine(1000)
{
let wav = buildWav({ bitDepth: 8, samples: signal })
let r = await decode(wav)
ok(r.channelData.length === 1, '8-bit: mono')
ok(r.sampleRate === 44100, '8-bit: sampleRate')
ok(r.channelData[0].length === 1000, '8-bit: frames')
ok(near(r.channelData[0][100], signal[100], 0.01), '8-bit: value (low precision expected)')
}
{
let wav = buildWav({ bitDepth: 16, samples: signal })
let r = await decode(wav)
ok(r.channelData.length === 1, '16-bit: mono')
ok(r.sampleRate === 44100, '16-bit: sampleRate')
ok(r.channelData[0].length === 1000, '16-bit: frames')
ok(near(r.channelData[0][100], signal[100], 0.00005), '16-bit: value')
}
{
let wav = buildWav({ bitDepth: 24, samples: signal })
let r = await decode(wav)
ok(r.channelData.length === 1, '24-bit: mono')
ok(r.channelData[0].length === 1000, '24-bit: frames')
ok(near(r.channelData[0][100], signal[100], 0.000001), '24-bit: value')
}
{
let wav = buildWav({ bitDepth: 32, samples: signal })
let r = await decode(wav)
ok(r.channelData.length === 1, '32-bit int: mono')
ok(r.channelData[0].length === 1000, '32-bit int: frames')
ok(near(r.channelData[0][100], signal[100], 0.000001), '32-bit int: value')
}
{
let wav = buildWav({ bitDepth: 32, float: true, samples: signal })
let r = await decode(wav)
ok(r.channelData.length === 1, '32-bit float: mono')
ok(r.channelData[0].length === 1000, '32-bit float: frames')
ok(near(r.channelData[0][100], signal[100], 0.000001), '32-bit float: value')
}
{
let wav = buildWav({ bitDepth: 64, float: true, samples: signal })
let r = await decode(wav)
ok(r.channelData.length === 1, '64-bit float: mono')
ok(r.channelData[0].length === 1000, '64-bit float: frames')
ok(near(r.channelData[0][100], signal[100], 0.000001), '64-bit float: value')
}
// ===== sync API =====
{
let wav = buildWav({ ch: 2, bitDepth: 16, samples: sine(500).flatMap((v, i) => [v, v]) })
let r = decode(wav)
ok(!(r instanceof Promise), 'decode returns value, not promise')
ok(r.channelData.length === 2, 'decode: stereo channels')
ok(r.channelData[0].length === 500, 'decode: frames')
let dec = decoder()
ok(!(dec instanceof Promise), 'decoder returns instance, not promise')
let bytes = new Uint8Array(wav)
let half = bytes.length >> 1
let a = dec.decode(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + half))
let b = dec.decode(bytes.subarray(half))
dec.free()
let total = (a.channelData[0]?.length || 0) + (b.channelData[0]?.length || 0)
ok(total === 500, 'decoder: chunked ArrayBuffer decode complete')
}
// ===== stereo =====
{
let l = sine(500, 440), r = sine(500, 880)
let interleaved = l.flatMap((v, i) => [v, r[i]])
let wav = buildWav({ ch: 2, bitDepth: 16, samples: interleaved })
let res = await decode(wav)
ok(res.channelData.length === 2, 'stereo: channels')
ok(res.channelData[0].length === 500, 'stereo: frames')
ok(near(res.channelData[0][100], l[100], 0.00005), 'stereo: left channel')
ok(near(res.channelData[1][100], r[100], 0.00005), 'stereo: right channel')
}
// ===== boundary values =====
{
// +1.0 and -1.0
let wav = buildWav({ bitDepth: 16, samples: [1, -1, 0] })
let r = await decode(wav)
ok(near(r.channelData[0][0], 1, 0.00005), 'boundary: +1.0 (16-bit)')
ok(near(r.channelData[0][1], -1, 0.00001), 'boundary: -1.0 (16-bit)')
ok(r.channelData[0][2] === 0, 'boundary: 0.0 (16-bit)')
}
{
// 24-bit min value: 0x800000 must decode to exactly -1.0
let wav = buildWav({ bitDepth: 24, samples: [-1, 1, 0] })
let r = await decode(wav)
ok(near(r.channelData[0][0], -1, 0.000001), 'boundary: -1.0 (24-bit)')
ok(near(r.channelData[0][1], 1, 0.000001), 'boundary: +1.0 (24-bit)')
}
// ===== extra chunks before data (JUNK chunk) =====
{
// JUNK chunk between fmt and data
let junkData = new Uint8Array(10)
let signal = sine(100)
let dataSize = signal.length * 2
let totalSize = 4 + (8 + 16) + (8 + 10) + (8 + dataSize)
let buf = new ArrayBuffer(8 + totalSize)
let v = new DataView(buf), p = 0
let s = (str) => { for (let i = 0; i < str.length; i++) v.setUint8(p++, str.charCodeAt(i)) }
let u16 = (x) => { v.setUint16(p, x, true); p += 2 }
let u32 = (x) => { v.setUint32(p, x, true); p += 4 }
s('RIFF'); u32(totalSize); s('WAVE')
s('fmt '); u32(16); u16(1); u16(1); u32(44100); u32(88200); u16(2); u16(16)
s('JUNK'); u32(10); p += 10
s('data'); u32(dataSize)
let dv = new DataView(buf, p)
for (let i = 0; i < signal.length; i++) dv.setInt16(i * 2, Math.round(signal[i] * 32767), true)
let r = await decode(new Uint8Array(buf))
ok(r.channelData[0].length === 100, 'extra chunk: frames correct')
ok(near(r.channelData[0][50], signal[50], 0.00005), 'extra chunk: values correct')
}
// ===== trailing chunks after data (#47) — must not be read as audio =====
{
// data chunk followed by a metadata chunk (e.g. LIST/cue/bext)
let signal = sine(100)
let dataSize = signal.length * 2
let junkSize = 40 // bytes of trailing chunk payload read as audio before the fix
let totalSize = 4 + (8 + 16) + (8 + dataSize) + (8 + junkSize)
let buf = new ArrayBuffer(8 + totalSize)
let v = new DataView(buf), p = 0
let s = (str) => { for (let i = 0; i < str.length; i++) v.setUint8(p++, str.charCodeAt(i)) }
let u16 = (x) => { v.setUint16(p, x, true); p += 2 }
let u32 = (x) => { v.setUint32(p, x, true); p += 4 }
s('RIFF'); u32(totalSize); s('WAVE')
s('fmt '); u32(16); u16(1); u16(1); u32(44100); u32(88200); u16(2); u16(16)
s('data'); u32(dataSize)
let dv = new DataView(buf, p)
for (let i = 0; i < signal.length; i++) dv.setInt16(i * 2, Math.round(signal[i] * 32767), true)
p += dataSize
s('LIST'); u32(junkSize); for (let i = 0; i < junkSize; i++) v.setUint8(p++, 0x55)
let r = await decode(new Uint8Array(buf))
ok(r.channelData[0].length === 100, 'trailing chunk: frames capped to data size')
// same, fed through the streaming decoder one byte-region at a time
let dec = await decoder()
let full = new Uint8Array(buf)
let mid = Math.floor(full.length / 2)
let n = dec.decode(full.subarray(0, mid)).channelData[0]?.length || 0
n += dec.decode(full.subarray(mid)).channelData[0]?.length || 0
dec.free()
ok(n === 100, 'trailing chunk: streaming also capped')
}
// ===== streaming decoder =====
{
let wav = buildWav({ bitDepth: 16, samples: sine(200) })
let dec = await decoder()
let r = dec.decode(wav)
ok(r.channelData[0].length === 200, 'stream: decode')
ok(dec.flush().sampleRate === 0, 'stream: flush returns EMPTY')
dec.free()
let threw = false
try { dec.decode(wav) } catch { threw = true }
ok(threw, 'stream: throws after free')
}
// ===== WAVE_FORMAT_EXTENSIBLE (0xFFFE) =====
function buildExtensibleWav({ sr = 44100, ch = 1, bitDepth = 16, subFormat = 1, samples }) {
let byteDepth = bitDepth / 8, dataSize = samples.length * byteDepth, fmtSize = 40
let buf = new ArrayBuffer(12 + 8 + fmtSize + 8 + dataSize)
let v = new DataView(buf), p = 0
let s = (str) => { for (let i = 0; i < str.length; i++) v.setUint8(p++, str.charCodeAt(i)) }
let u16 = (x) => { v.setUint16(p, x, true); p += 2 }
let u32 = (x) => { v.setUint32(p, x, true); p += 4 }
s('RIFF'); u32(4 + 8 + fmtSize + 8 + dataSize); s('WAVE')
s('fmt '); u32(fmtSize)
u16(0xFFFE); u16(ch); u32(sr); u32(sr * ch * byteDepth); u16(ch * byteDepth); u16(bitDepth)
u16(22); u16(bitDepth); u32(ch === 1 ? 0x4 : 0x3) // cbSize, validBits, channelMask
// SubFormat GUID: first 2 bytes = format tag, then standard KSDATAFORMAT suffix
for (let byte of [subFormat & 0xFF, subFormat >> 8, 0, 0, 0, 0, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71]) v.setUint8(p++, byte)
s('data'); u32(dataSize)
let dv = new DataView(buf, p)
for (let i = 0; i < samples.length; i++) dv.setInt16(i * 2, Math.round(samples[i] < 0 ? samples[i] * 32768 : samples[i] * 32767), true)
return new Uint8Array(buf)
}
{
let sig = sine(1000)
let ext = await decode(buildExtensibleWav({ bitDepth: 16, samples: sig }))
let plain = await decode(buildWav({ bitDepth: 16, samples: sig }))
ok(ext.sampleRate === 44100, 'extensible: sampleRate')
ok(ext.channelData[0].length === 1000, 'extensible: frames')
let same = ext.channelData[0].every((x, i) => x === plain.channelData[0][i])
ok(same, 'extensible PCM decodes identically to standard PCM')
// extensible IEEE float (subformat 0x0003)
let extF = await decode(buildExtensibleWav({ bitDepth: 16, subFormat: 3, samples: sig }))
ok(extF.channelData.length === 1, 'extensible float subformat parses')
}
// ===== G.711 A-law / µ-law (real ffmpeg fixtures) =====
function corr(a, b) {
let n = Math.min(a.length, b.length), sa = 0, sb = 0, sab = 0
for (let i = 0; i < n; i++) { sa += a[i] * a[i]; sb += b[i] * b[i]; sab += a[i] * b[i] }
return sab / Math.sqrt(sa * sb)
}
for (let name of ['alaw', 'mulaw']) {
let buf = new Uint8Array(readFileSync(new URL(`./fixtures/${name}.wav`, import.meta.url)))
let r = await decode(buf)
let ideal = sine(r.channelData[0].length, 440, 8000)
ok(r.sampleRate === 8000, `${name}: sampleRate 8000`)
ok(r.channelData.length === 1, `${name}: mono`)
ok(r.channelData[0].length === 2000, `${name}: 2000 frames`)
// 440Hz sine: correct decode correlates ~1; a sign flip would score ~-1
ok(corr(r.channelData[0], ideal) > 0.97, `${name}: matches 440Hz sine (sign + scale)`)
}
// ===== ADPCM (IMA / MS), real ffmpeg fixtures, bit-exact =====
for (let name of ['ima_mono', 'ms_mono', 'ima_stereo', 'ms_stereo']) {
let buf = new Uint8Array(readFileSync(new URL(`./fixtures/${name}.wav`, import.meta.url)))
let r = await decode(buf)
let nCh = name.includes('stereo') ? 2 : 1
ok(r.sampleRate === 22050, `${name}: sampleRate 22050`)
ok(r.channelData.length === nCh, `${name}: ${nCh}ch`)
ok(r.channelData[0].length > 10000, `${name}: decoded samples`)
// ch0 is a 440Hz sine; for stereo ch1 is 660Hz — validates per-channel nibble mapping.
// window inside the real audio (ADPCM pads the final block past the 11025-sample source)
let win = 11000
ok(corr(r.channelData[0].subarray(0, win), sine(win, 440, 22050)) > 0.99, `${name}: ch0 ≈ 440Hz`)
if (nCh === 2) ok(corr(r.channelData[1].subarray(0, win), sine(win, 660, 22050)) > 0.99, `${name}: ch1 ≈ 660Hz`)
}
// ===== error handling =====
{
let threw = false
try { await decode(new Uint8Array([0, 1, 2, 3])) } catch { threw = true }
ok(threw, 'error: rejects non-WAV')
}
{
let threw = false
// WAV with a genuinely unsupported format tag (0x0099)
let buf = buildWav({ bitDepth: 16, samples: [0] })
let v = new DataView(buf.buffer)
v.setUint16(20, 0x99, true)
try { await decode(buf) } catch { threw = true }
ok(threw, 'error: rejects unsupported format')
}
console.log(`\n${pass + fail} tests: ${pass} passed, ${fail} failed`)
if (fail) process.exit(1)