mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-28 16:50:57 +02:00
127 lines
4.7 KiB
JavaScript
127 lines
4.7 KiB
JavaScript
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
import { basename, join, resolve } from 'node:path'
|
|
import { createRequire } from 'node:module'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const { lufsmeter } = require('../native/build/Release/visualizer_dsp.node')
|
|
|
|
const sourcePath = process.argv[2]
|
|
if (!sourcePath) {
|
|
console.error('Usage: npm run test:lufsmeter-ebu -- /path/to/extracted-ebu-loudness-test-set')
|
|
process.exit(2)
|
|
}
|
|
|
|
async function collectFiles(path) {
|
|
const details = await stat(path)
|
|
if (details.isFile()) return [path]
|
|
if (!details.isDirectory()) return []
|
|
|
|
const files = []
|
|
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
files.push(...await collectFiles(join(path, entry.name)))
|
|
}
|
|
return files
|
|
}
|
|
|
|
function decodeWave(buffer) {
|
|
if (buffer.length < 12 || buffer.toString('ascii', 0, 4) !== 'RIFF'
|
|
|| buffer.toString('ascii', 8, 12) !== 'WAVE') {
|
|
throw new Error('not a RIFF/WAVE file')
|
|
}
|
|
|
|
let format = null
|
|
let data = null
|
|
for (let offset = 12; offset + 8 <= buffer.length;) {
|
|
const chunkId = buffer.toString('ascii', offset, offset + 4)
|
|
const chunkSize = buffer.readUInt32LE(offset + 4)
|
|
const chunkOffset = offset + 8
|
|
if (chunkOffset + chunkSize > buffer.length) throw new Error(`truncated ${chunkId} chunk`)
|
|
|
|
if (chunkId === 'fmt ') {
|
|
if (chunkSize < 16) throw new Error('invalid fmt chunk')
|
|
let formatTag = buffer.readUInt16LE(chunkOffset)
|
|
if (formatTag === 0xfffe && chunkSize >= 40) {
|
|
formatTag = buffer.readUInt16LE(chunkOffset + 24)
|
|
}
|
|
format = {
|
|
formatTag,
|
|
channels: buffer.readUInt16LE(chunkOffset + 2),
|
|
sampleRate: buffer.readUInt32LE(chunkOffset + 4),
|
|
blockAlign: buffer.readUInt16LE(chunkOffset + 12),
|
|
bitsPerSample: buffer.readUInt16LE(chunkOffset + 14),
|
|
}
|
|
} else if (chunkId === 'data') {
|
|
data = buffer.subarray(chunkOffset, chunkOffset + chunkSize)
|
|
}
|
|
offset = chunkOffset + chunkSize + (chunkSize % 2)
|
|
}
|
|
|
|
if (!format || !data) throw new Error('missing fmt or data chunk')
|
|
if (format.channels < 1 || format.blockAlign < 1 || format.sampleRate < 1) {
|
|
throw new Error('invalid WAVE format')
|
|
}
|
|
|
|
const bytesPerSample = Math.ceil(format.bitsPerSample / 8)
|
|
const frameCount = Math.floor(data.length / format.blockAlign)
|
|
const left = new Float32Array(frameCount)
|
|
const right = new Float32Array(frameCount)
|
|
const readSample = (offset) => {
|
|
if (format.formatTag === 3 && format.bitsPerSample === 32) return data.readFloatLE(offset)
|
|
if (format.formatTag !== 1) throw new Error(`unsupported WAVE format tag ${format.formatTag}`)
|
|
if (format.bitsPerSample === 16) return data.readInt16LE(offset) / 0x8000
|
|
if (format.bitsPerSample === 24) return data.readIntLE(offset, 3) / 0x800000
|
|
if (format.bitsPerSample === 32) return data.readInt32LE(offset) / 0x80000000
|
|
throw new Error(`unsupported PCM depth ${format.bitsPerSample}`)
|
|
}
|
|
|
|
for (let frame = 0; frame < frameCount; frame += 1) {
|
|
const frameOffset = frame * format.blockAlign
|
|
left[frame] = readSample(frameOffset)
|
|
right[frame] = format.channels > 1 ? readSample(frameOffset + bytesPerSample) : left[frame]
|
|
}
|
|
return { sampleRate: format.sampleRate, left, right }
|
|
}
|
|
|
|
function expectedDbtp(caseNumber) {
|
|
if (caseNumber >= 15 && caseNumber <= 18) return -6
|
|
if (caseNumber === 19) return 3
|
|
return 0
|
|
}
|
|
|
|
const files = await collectFiles(resolve(sourcePath))
|
|
const cases = new Map()
|
|
for (const file of files) {
|
|
const match = basename(file).match(/3341[-_ ](1[5-9]|2[0-3])(?:\D|$)/i)
|
|
if (match && /\.wav(?:\.wav)?$/i.test(file)) cases.set(Number(match[1]), file)
|
|
}
|
|
|
|
const missing = []
|
|
for (let caseNumber = 15; caseNumber <= 23; caseNumber += 1) {
|
|
if (!cases.has(caseNumber)) missing.push(caseNumber)
|
|
}
|
|
if (missing.length > 0) {
|
|
console.error(`Could not find EBU Tech 3341 WAV cases: ${missing.join(', ')}`)
|
|
process.exit(2)
|
|
}
|
|
|
|
let failed = false
|
|
for (let caseNumber = 15; caseNumber <= 23; caseNumber += 1) {
|
|
const file = cases.get(caseNumber)
|
|
try {
|
|
const wave = decodeWave(await readFile(file))
|
|
lufsmeter.setSampleRate(wave.sampleRate)
|
|
lufsmeter.reset()
|
|
lufsmeter.pushSamples(wave.left, wave.right)
|
|
const actual = lufsmeter.getSnapshot().maxTruePeakDb
|
|
const expected = expectedDbtp(caseNumber)
|
|
const passed = actual >= expected - 0.4 && actual <= expected + 0.2
|
|
console.log(`${passed ? 'PASS' : 'FAIL'} EBU 3341-${caseNumber}: ${actual.toFixed(3)} dBTP (expected ${expected}, +0.2/-0.4)`)
|
|
failed ||= !passed
|
|
} catch (error) {
|
|
failed = true
|
|
console.error(`FAIL EBU 3341-${caseNumber}: ${error instanceof Error ? error.message : String(error)}`)
|
|
}
|
|
}
|
|
|
|
process.exit(failed ? 1 : 0)
|