Files
ArtPlayer/test/asr-encoding.test.js

184 lines
7.4 KiB
JavaScript

import assert from 'node:assert/strict'
import { Buffer } from 'node:buffer'
// eslint-disable-next-line test/no-import-node-test -- Pure ASR audio modules use the repository runner.
import test from 'node:test'
import { asrAudioEnvironment, runAsrWorklet } from './helpers/asr-audio.js'
import { asrHistorical } from './helpers/asr.js'
import { loadModules } from './helpers/load.js'
const root = 'packages/artplayer-plugin-asr/src'
const { encodeAudio, SampleQueue, recorderProcessorCode } = await loadModules({
encodeAudio: { file: `${root}/encoding`, name: 'encodeAudio' },
SampleQueue: { file: `${root}/sample-queue`, name: 'SampleQueue' },
recorderProcessorCode: { file: `${root}/worklet`, name: 'recorderProcessorCode' },
})
test('ASR encoder preserves signed PCM16 clamping, truncation, NaN and little endian bytes', () => {
const input = Float32Array.of(-Infinity, -2, -1, -0.5, -0, 0, 0.5, 1, 2, Infinity, Number.NaN)
const encoded = encodeAudio(input, 16000)
assert.deepEqual(Object.keys(encoded), ['pcm', 'wav'])
const expected = [0, 128, 0, 128, 0, 128, 0, 192, 0, 0, 0, 0, 255, 63, 255, 127, 255, 127, 255, 127, 0, 0]
assert.deepEqual([...new Uint8Array(encoded.pcm)], expected)
assert.equal(input[0], -Infinity)
assert.equal(Number.isNaN(input.at(-1)), true)
})
for (const sampleRate of [8000, 16000, 44100, 48000]) {
test(`ASR encoder writes exact mono WAV metadata at ${sampleRate} Hz`, () => {
const { pcm, wav } = encodeAudio(Float32Array.of(-1, 0, 1), sampleRate)
const data = Buffer.from(wav)
assert.equal(pcm.byteLength, 6)
assert.equal(data.length, 50)
assert.equal(data.toString('ascii', 0, 4), 'RIFF')
assert.equal(data.readUInt32LE(4), 42)
assert.equal(data.toString('ascii', 8, 16), 'WAVEfmt ')
assert.equal(data.readUInt32LE(16), 16)
assert.equal(data.readUInt16LE(20), 1)
assert.equal(data.readUInt16LE(22), 1)
assert.equal(data.readUInt32LE(24), sampleRate)
assert.equal(data.readUInt32LE(28), sampleRate * 2)
assert.equal(data.readUInt16LE(32), 2)
assert.equal(data.readUInt16LE(34), 16)
assert.equal(data.toString('ascii', 36, 40), 'data')
assert.equal(data.readUInt32LE(40), 6)
assert.deepEqual([...data.subarray(44)], [0, 128, 0, 0, 255, 127])
new Uint8Array(pcm).fill(0)
assert.deepEqual([...data.subarray(44)], [0, 128, 0, 0, 255, 127], 'The WAV payload owns its bytes')
})
}
test('ASR encoder empty and offset views retain exact payload boundaries', () => {
const empty = encodeAudio(new Float32Array(0), 16000)
assert.equal(empty.pcm.byteLength, 0)
assert.equal(empty.wav.byteLength, 44)
assert.equal(new DataView(empty.wav).getUint32(4, true), 36)
assert.equal(new DataView(empty.wav).getUint32(40, true), 0)
const backing = Float32Array.of(1, -1, 0.5, 1)
const { pcm } = encodeAudio(backing.subarray(1, 3), 16000)
assert.deepEqual([...new Uint8Array(pcm)], [0, 128, 255, 63])
})
for (const implementation of await asrHistorical()) {
test(`ASR encoder byte parity with ${implementation.name}`, async () => {
const chunks = []
const environment = asrAudioEnvironment(implementation, { sampleRate: 8000, interval: 1, onAudioChunk: chunk => chunks.push(chunk) })
await environment.emit('play')
const input = Float32Array.of(-Infinity, -1, -0.5, Number.NaN, 0, 0.5, 1, Infinity)
environment.send(input)
await environment.tick()
assert.equal(chunks.length, 1)
const candidate = encodeAudio(input, 8000)
assert.deepEqual(Buffer.from(candidate.pcm), Buffer.from(chunks[0].pcm))
assert.deepEqual(Buffer.from(candidate.wav), Buffer.from(chunks[0].wav))
await environment.plugin.stop()
})
}
test('ASR sample queue retains insufficient chunks and excess tails', () => {
const queue = new SampleQueue()
assert.equal(queue.take(8), null)
queue.push(Float32Array.of(1, 2, 3, 4))
assert.equal(queue.take(8), null)
assert.equal(queue.length, 4)
queue.push(Float32Array.of(5, 6, 7, 8, 9, 10))
assert.deepEqual([...queue.take(8)], [1, 2, 3, 4, 5, 6, 7, 8])
assert.equal(queue.length, 2)
assert.equal(queue.take(3), null)
queue.push(Float32Array.of(11))
assert.deepEqual([...queue.take(3)], [9, 10, 11])
assert.equal(queue.length, 0)
assert.equal(queue.take(1), null)
})
test('ASR sample queue copies input views and returns independently owned output', () => {
const queue = new SampleQueue()
const input = Float32Array.of(0, 1, 2, 3)
queue.push(input.subarray(1, 3))
input.fill(9)
const first = queue.take(1)
assert.deepEqual([...first], [1])
first[0] = 8
assert.deepEqual([...queue.take(1)], [2])
})
test('ASR sample queue clears partial blocks and can be reused after draining', () => {
const queue = new SampleQueue()
queue.push(Float32Array.of(1, 2, 3))
assert.deepEqual([...queue.take(1)], [1])
queue.clear()
queue.clear()
queue.push(new Float32Array(0))
assert.equal(queue.length, 0)
assert.equal(queue.take(1), null)
queue.push(Float32Array.of(4, 5))
assert.deepEqual([...queue.take(2)], [4, 5])
queue.push(Float32Array.of(6))
assert.deepEqual([...queue.take(1)], [6])
})
test('ASR sample queue rejects invalid count without losing buffered audio', () => {
const queue = new SampleQueue()
queue.push(Float32Array.of(1, 2))
for (const count of [0, -1, 0.5, Number.NaN, Infinity, -Infinity, Number.MAX_SAFE_INTEGER + 1]) {
assert.throws(() => queue.take(count), { name: 'RangeError', message: 'Sample count must be a positive safe integer' })
assert.equal(queue.length, 2)
}
assert.equal(queue.take(Number.MAX_SAFE_INTEGER), null)
assert.deepEqual([...queue.take(2)], [1, 2])
})
test('ASR sample queue handles many worklet blocks without dropping boundary samples', () => {
const queue = new SampleQueue()
for (let index = 0; index < 4096; index++)
queue.push(Float32Array.of(index, -index))
assert.equal(queue.length, 8192)
const first = queue.take(4097)
const second = queue.take(4095)
assert.equal(queue.length, 0)
const output = [...first, ...second]
for (let index = 0; index < 4096; index++) {
assert.equal(output[index * 2], index)
assert.equal(output[index * 2 + 1], -index)
}
})
test('ASR sample queue preserves FIFO through deterministic mixed push, take and clear operations', () => {
const queue = new SampleQueue()
let expected = []
let serial = 0
for (let index = 1; index <= 1000; index++) {
if (index % 113 === 0) {
queue.clear()
expected = []
}
else if (index % 3 !== 0) {
const chunk = Float32Array.from({ length: index % 17 }, () => ++serial)
queue.push(chunk)
expected.push(...chunk)
}
else {
const count = index % 23 + 1
const actual = queue.take(count)
if (expected.length < count)
assert.equal(actual, null)
else
assert.deepEqual([...actual], expected.splice(0, count))
}
assert.equal(queue.length, expected.length)
}
if (expected.length)
assert.deepEqual([...queue.take(expected.length)], expected)
})
test('ASR candidate Worklet keeps registration, first-channel identity and empty-input behavior', async () => {
const { processor, messages } = await runAsrWorklet(new Blob([recorderProcessorCode]))
const first = Float32Array.of(-0.5, 0.5)
const second = Float32Array.of(1, 1)
assert.equal(processor.process([]), true)
assert.equal(processor.process([[]]), true)
assert.equal(messages.length, 0)
assert.equal(processor.process([[first, second]]), true)
assert.equal(messages.length, 1)
assert.equal(messages[0], first)
})