-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathtest.js
More file actions
424 lines (357 loc) · 13.8 KB
/
Copy pathtest.js
File metadata and controls
424 lines (357 loc) · 13.8 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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
import decode, { decoder } from './decode-webm.js'
import { execSync } from 'child_process'
import { existsSync, readFileSync, mkdirSync } from 'fs'
let pass = 0, fail = 0
function ok(cond, msg) {
if (cond) { pass++; console.log(' ok', msg) }
else { fail++; console.error(' FAIL', msg) }
}
const dur = r => r.channelData[0].length / r.sampleRate
const rms = f32 => { let s = 0; for (let i = 0; i < f32.length; i++) s += f32[i] * f32[i]; return Math.sqrt(s / f32.length) }
const near = (a, b, tol = 0.05) => Math.abs(a - b) < tol
// Generate test fixture with ffmpeg if available
let fixture = new URL('./fixtures/test.webm', import.meta.url)
let fixturePath = new URL('./fixtures/', import.meta.url)
let hasFixture = false
try {
mkdirSync(new URL(fixturePath), { recursive: true })
if (!existsSync(fixture)) {
execSync(`ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -c:a libopus -b:a 64k "${new URL(fixture).pathname}" -y 2>/dev/null`)
}
hasFixture = existsSync(fixture)
} catch { hasFixture = existsSync(fixture) }
// Also generate a stereo fixture
let stereoFixture = new URL('./fixtures/stereo.webm', import.meta.url)
let hasStereo = false
try {
if (!existsSync(stereoFixture)) {
execSync(`ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -f lavfi -i "sine=frequency=880:duration=1" -filter_complex "[0:a][1:a]amerge=inputs=2" -c:a libopus -b:a 64k "${new URL(stereoFixture).pathname}" -y 2>/dev/null`)
}
hasStereo = existsSync(stereoFixture)
} catch { hasStereo = existsSync(stereoFixture) }
// --- EBML parsing unit tests ---
console.log('EBML parsing')
// Test: valid WebM detection
ok(await (async () => {
try { await decode(new Uint8Array([0, 0, 0])); return false }
catch (e) { return e.message.includes('Not a WebM') }
})(), 'rejects too-small buffer')
ok(await (async () => {
try { await decode(new Uint8Array(100)); return false }
catch (e) { return e.message.includes('Not a WebM') }
})(), 'rejects non-WebM data')
ok(await (async () => {
try { await decode(null); return false }
catch { return true }
})(), 'rejects null')
ok(await (async () => {
try { await decode(new ArrayBuffer(0)); return false }
catch { return true }
})(), 'rejects empty ArrayBuffer')
ok(await (async () => {
let invalid = readFileSync(fixture)
let cluster = invalid.indexOf(Buffer.from([0x1f, 0x43, 0xb6, 0x75]))
if (cluster < 0) return false
invalid = invalid.subarray(0, cluster).slice()
let head = invalid.indexOf('OpusHead')
if (head < 0) return false
invalid[head + 9] = 0
try { await decode(invalid); return false }
catch (error) { return invalid.length < 8192 && error.message.includes('Invalid Opus CodecPrivate') }
})(), 'rejects a small zero-channel OpusHead')
// --- Decoder lifecycle ---
console.log('Decoder lifecycle')
ok(await (async () => {
let dec = await decoder()
dec.free()
try { dec.decode(new Uint8Array(10)); return false }
catch (e) { return e.message.includes('freed') }
})(), 'decode after free throws')
ok(await (async () => {
let dec = await decoder()
dec.free()
dec.free()
return true
})(), 'double free is safe')
ok(await (async () => {
let dec = await decoder()
let r = dec.flush()
return r.channelData.length === 0 && r.sampleRate === 0
})(), 'flush without data returns empty')
ok(await (async () => {
let dec = await decoder()
let r = dec.decode(null)
return r.channelData.length === 0 && r.sampleRate === 0
})(), 'decode null returns empty')
ok(await (async () => {
let dec = await decoder()
let r = dec.decode(new Uint8Array(0))
return r.channelData.length === 0 && r.sampleRate === 0
})(), 'decode empty returns empty')
// --- Synchronous decoder methods ---
console.log('Synchronous decoder methods')
for (let [name, file] of [
['opus', './fixtures/test.webm'],
['vorbis', './fixtures/vorbis-mono.webm']
]) {
let webm = readFileSync(new URL(file, import.meta.url))
let whole = await decode(webm)
let dec = await decoder(), results = [], sync = true
for (let offset = 0, size = 1; offset < webm.length; offset += size, size = Math.min(size * 3, 4093)) {
let chunk = webm.subarray(offset, offset + size)
let result = dec.decode(offset ? chunk : chunk.slice().buffer)
sync &&= !(result instanceof Promise)
results.push(result)
}
let tail = dec.flush()
sync &&= !(tail instanceof Promise)
results.push(tail)
let total = results.reduce((samples, result) => samples + (result.channelData[0]?.length || 0), 0)
ok(sync, name + ': decode and flush return values')
ok(total === whole.channelData[0].length, name + ': chunked length matches whole-file')
let threw = false
try { dec.decode(webm) } catch { threw = true }
ok(threw, name + ': decode after flush throws')
}
// --- Integration tests (require ffmpeg fixture) ---
if (hasFixture) {
console.log('WebM+Opus mono decode')
let webm = readFileSync(fixture)
ok(await (async () => {
let r = await decode(webm)
return r.channelData.length === 1 && r.sampleRate === 48000
})(), 'mono: 1 channel, 48kHz')
ok(await (async () => {
let r = await decode(webm)
return near(dur(r), 1.0, 0.1)
})(), 'mono: ~1s duration')
ok(await (async () => {
let r = await decode(webm)
return rms(r.channelData[0]) > 0.01
})(), 'mono: has audio content')
ok(await (async () => {
let r = await decode(new Uint8Array(webm))
return r.channelData.length >= 1
})(), 'accepts Uint8Array input')
ok(await (async () => {
let r = await decode(webm.buffer.slice(webm.byteOffset, webm.byteOffset + webm.byteLength))
return r.channelData.length >= 1
})(), 'accepts ArrayBuffer input')
// Streaming decoder test
ok(await (async () => {
let dec = await decoder()
let input = webm.buffer.slice(webm.byteOffset, webm.byteOffset + webm.byteLength)
let r = dec.decode(input)
let ok1 = !(r instanceof Promise) && r.channelData.length === 1 && r.sampleRate === 48000
let f = dec.flush()
return ok1 && !(f instanceof Promise) && f.sampleRate === 0
})(), 'streaming decoder works')
} else {
console.log('SKIP: WebM+Opus mono decode (no ffmpeg / fixture)')
}
if (hasStereo) {
console.log('WebM+Opus stereo decode')
let webm = readFileSync(stereoFixture)
ok(await (async () => {
let r = await decode(webm)
return r.channelData.length === 2 && r.sampleRate === 48000
})(), 'stereo: 2 channels, 48kHz')
ok(await (async () => {
let r = await decode(webm)
return near(dur(r), 1.0, 0.1)
})(), 'stereo: ~1s duration')
ok(await (async () => {
let r = await decode(webm)
let l = rms(r.channelData[0])
let rr = rms(r.channelData[1])
return l > 0.01 && rr > 0.01
})(), 'stereo: both channels have content')
} else {
console.log('SKIP: WebM+Opus stereo decode (no ffmpeg / fixture)')
}
// --- Video+Audio WebM (should extract audio only) ---
let videoAudioFixture = new URL('./fixtures/video-audio.webm', import.meta.url)
let hasVideoAudio = false
try {
if (!existsSync(videoAudioFixture)) {
execSync(`ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -f lavfi -i "color=c=black:s=2x2:d=1" -c:a libopus -b:a 64k -c:v libvpx -b:v 100k "${new URL(videoAudioFixture).pathname}" -y 2>/dev/null`)
}
hasVideoAudio = existsSync(videoAudioFixture)
} catch { hasVideoAudio = existsSync(videoAudioFixture) }
if (hasVideoAudio) {
console.log('WebM video+audio')
let webm = readFileSync(videoAudioFixture)
ok(await (async () => {
let r = await decode(webm)
return r.channelData.length >= 1 && r.sampleRate === 48000
})(), 'extracts audio from video+audio WebM')
ok(await (async () => {
let r = await decode(webm)
return near(dur(r), 1.0, 0.15)
})(), 'video+audio: ~1s duration')
ok(await (async () => {
let r = await decode(webm)
return rms(r.channelData[0]) > 0.01
})(), 'video+audio: has audio content')
} else {
console.log('SKIP: WebM video+audio (no ffmpeg / fixture)')
}
// --- Sequential determinism ---
if (hasFixture) {
console.log('Sequential determinism')
let webm = readFileSync(fixture)
ok(await (async () => {
let r1 = await decode(webm)
let r2 = await decode(webm)
if (r1.sampleRate !== r2.sampleRate) return false
if (r1.channelData.length !== r2.channelData.length) return false
for (let ch = 0; ch < r1.channelData.length; ch++) {
let a = r1.channelData[ch], b = r2.channelData[ch]
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
}
return true
})(), 'sequential decodes produce identical output')
} else {
console.log('SKIP: Sequential determinism (no fixture)')
}
// --- Concurrent decoders ---
if (hasFixture) {
console.log('Concurrent decoders')
let webm = readFileSync(fixture)
ok(await (async () => {
let results = await Promise.all([decode(webm), decode(webm), decode(webm)])
return results.every(r => r.channelData.length === 1 && r.sampleRate === 48000 && near(dur(r), 1.0, 0.1))
})(), 'Promise.all decode x3 succeeds')
} else {
console.log('SKIP: Concurrent decoders (no fixture)')
}
// --- NaN/Inf validation ---
if (hasFixture) {
console.log('NaN/Inf validation')
let webm = readFileSync(fixture)
ok(await (async () => {
let r = await decode(webm)
for (let ch of r.channelData)
for (let i = 0; i < ch.length; i++)
if (!isFinite(ch[i])) return false
return true
})(), 'no NaN or Infinity in output samples')
} else {
console.log('SKIP: NaN/Inf validation (no fixture)')
}
// --- Performance benchmark ---
if (hasFixture) {
console.log('Performance benchmark')
let webm = readFileSync(fixture)
let iters = 10
let t0 = performance.now()
for (let i = 0; i < iters; i++) await decode(webm)
let elapsed = performance.now() - t0
let avg = elapsed / iters
console.log(` ${iters} decodes in ${elapsed.toFixed(0)}ms (avg ${avg.toFixed(1)}ms)`)
ok(avg < 5000, `avg decode < 5s (got ${avg.toFixed(1)}ms)`)
} else {
console.log('SKIP: Performance benchmark (no fixture)')
}
// --- Real file: Opus mono (lena.webm) ---
console.log('real file: Opus mono')
{
let webm = readFileSync(new URL('lena.webm', import.meta.resolve('audio-lena')))
let r = await decode(webm)
ok(r.channelData.length === 1, 'mono')
ok(r.sampleRate === 48000, 'sampleRate 48000')
ok(near(r.channelData[0].length / r.sampleRate, 12.27, 0.3), 'duration ~12.27s')
ok(rms(r.channelData[0]) > 0.03, 'has audio content')
// no NaN/Inf
let bad = 0
for (let ch of r.channelData) for (let i = 0; i < ch.length; i++) if (!isFinite(ch[i])) bad++
ok(bad === 0, 'no NaN/Inf')
}
// --- Performance (real file) ---
console.log('performance (real file)')
{
let webm = readFileSync(new URL('lena.webm', import.meta.resolve('audio-lena')))
await decode(webm) // warmup
let t0 = performance.now(), N = 5
for (let i = 0; i < N; i++) await decode(webm)
let ms = (performance.now() - t0) / N
ok(ms < 2000, 'real file decode < 2s (' + ms.toFixed(0) + 'ms)')
console.log(' ' + ms.toFixed(0) + 'ms/decode (161KB, 12.3s audio)')
}
// --- Vorbis: mono ---
console.log('WebM+Vorbis mono decode')
{
let webm = readFileSync(new URL('./fixtures/vorbis-mono.webm', import.meta.url))
let r = await decode(webm)
ok(r.channelData.length === 1, 'vorbis mono: 1 channel')
ok(r.sampleRate === 44100, 'vorbis mono: sampleRate 44100')
ok(near(dur(r), 1.0, 0.1), 'vorbis mono: ~1s duration')
ok(rms(r.channelData[0]) > 0.1, 'vorbis mono: has audio content')
let bad = 0
for (let ch of r.channelData) for (let i = 0; i < ch.length; i++) if (!isFinite(ch[i])) bad++
ok(bad === 0, 'vorbis mono: no NaN/Inf')
}
// --- Vorbis: stereo ---
console.log('WebM+Vorbis stereo decode')
{
let webm = readFileSync(new URL('./fixtures/vorbis-stereo.webm', import.meta.url))
let r = await decode(webm)
ok(r.channelData.length === 2, 'vorbis stereo: 2 channels')
ok(r.sampleRate === 44100, 'vorbis stereo: sampleRate 44100')
ok(near(dur(r), 1.0, 0.1), 'vorbis stereo: ~1s duration')
ok(rms(r.channelData[0]) > 0.1 && rms(r.channelData[1]) > 0.1, 'vorbis stereo: both channels have content')
}
// --- Vorbis: real file (lena-vorbis.webm) ---
console.log('real file: Vorbis mono')
{
let webm = readFileSync(new URL('lena-vorbis.webm', import.meta.resolve('audio-lena')))
let r = await decode(webm)
ok(r.channelData.length === 1, 'vorbis lena: mono')
ok(r.sampleRate === 44100, 'vorbis lena: sampleRate 44100')
ok(near(r.channelData[0].length / r.sampleRate, 12.27, 0.3), 'vorbis lena: duration ~12.27s')
ok(rms(r.channelData[0]) > 0.03, 'vorbis lena: has audio content')
let bad = 0
for (let ch of r.channelData) for (let i = 0; i < ch.length; i++) if (!isFinite(ch[i])) bad++
ok(bad === 0, 'vorbis lena: no NaN/Inf')
}
// --- Vorbis: determinism ---
console.log('Vorbis determinism')
{
let webm = readFileSync(new URL('./fixtures/vorbis-mono.webm', import.meta.url))
ok(await (async () => {
let r1 = await decode(webm)
let r2 = await decode(webm)
if (r1.sampleRate !== r2.sampleRate) return false
if (r1.channelData.length !== r2.channelData.length) return false
for (let ch = 0; ch < r1.channelData.length; ch++) {
let a = r1.channelData[ch], b = r2.channelData[ch]
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false
}
return true
})(), 'vorbis: sequential decodes produce identical output')
}
// --- Vorbis: concurrent ---
console.log('Vorbis concurrent')
{
let webm = readFileSync(new URL('./fixtures/vorbis-mono.webm', import.meta.url))
ok(await (async () => {
let results = await Promise.all([decode(webm), decode(webm), decode(webm)])
return results.every(r => r.channelData.length === 1 && r.sampleRate === 44100 && near(dur(r), 1.0, 0.1))
})(), 'vorbis: Promise.all decode x3 succeeds')
}
// --- Vorbis: performance ---
console.log('Vorbis performance')
{
let webm = readFileSync(new URL('lena-vorbis.webm', import.meta.resolve('audio-lena')))
await decode(webm) // warmup
let t0 = performance.now(), N = 5
for (let i = 0; i < N; i++) await decode(webm)
let ms = (performance.now() - t0) / N
ok(ms < 2000, 'vorbis decode < 2s (' + ms.toFixed(0) + 'ms)')
console.log(' ' + ms.toFixed(0) + 'ms/decode (112KB, 12.3s audio)')
}
// --- Summary ---
console.log(`\n${pass} passed, ${fail} failed`)
if (fail) process.exit(1)