#!/usr/bin/env node // End-to-end JamesDSP checks against the real local server, manifest cache and media fixture. import assert from 'node:assert/strict'; import { spawn, execFileSync } from 'node:child_process'; import { createRequire } from 'node:module'; import { createServer } from 'node:net'; import { mkdtempSync, mkdirSync, cpSync, writeFileSync, statSync, rmSync, existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { chromium, webkit } from 'playwright'; import { createThrottleProxy } from './proxy.mjs'; const repo = resolve(import.meta.dirname, '..'); const requested = process.argv.includes('--browser') ? process.argv[process.argv.indexOf('--browser') + 1] : 'all'; assert.ok(['all', 'chromium', 'webkit'].includes(requested)); const scratch = mkdtempSync(join(repo, 'perf/.tmp/dsp-')); const results = { results: [], failures: [] }; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); async function poll(fn, timeout = 90000) { const end = Date.now() + timeout; while (Date.now() < end) { if (await fn()) return true; await sleep(100); } return false; } async function run(engine) { const cwd = join(scratch, engine); mkdirSync(cwd); cpSync(join(repo, 'frontend'), join(cwd, 'public'), { recursive: true }); mkdirSync(join(cwd, 'media')); const portProbe = createServer(); await new Promise(resolve => portProbe.listen(0, '127.0.0.1', resolve)); const port = portProbe.address().port; await new Promise(resolve => portProbe.close(resolve)); let child, browser, proxy, context, log = ''; let origin = `http://127.0.0.1:${port}`; const result = { engine, errors: [], failures: [], rms: [], checks: [] }; results.results.push(result); async function stop() { if (!child) return; const proc = child; child = null; if (proc.exitCode === null) { proc.kill('SIGTERM'); await new Promise(resolve => proc.once('exit', resolve)); } } try { const clips = [ { id: 'dspfixture01', title: 'DSP test tone 110 Hz', channel: 'Local test', duration: 5, thumbnail: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' }, { id: 'dspfixture02', title: 'DSP test tone 660 Hz', channel: 'Local test', duration: 5, thumbnail: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' }, ]; child = spawn('bun', [join(repo, 'server/server.js')], { cwd, env: { ...process.env, PORT: String(port), DB_PATH: join(cwd, 'db.sqlite'), MEDIA_DIR: join(cwd, 'media'), UPLOAD_DIR: join(cwd, 'uploads'), DATA_DIR: join(cwd, 'data'), ASSET_HASHING: '1', ASSET_SYNC: '1', BUILD_TAG: '' }, stdio: ['ignore', 'pipe', 'pipe'] }); for (const stream of [child.stdout, child.stderr]) stream.on('data', chunk => { log = (log + chunk).slice(-8000); }); if (!await poll(async () => { if (child.exitCode !== null) throw Error(log); try { return (await fetch(origin + '/api/version')).ok; } catch { return false; } })) throw Error('Server did not start: ' + log); for (const [index, clip] of clips.entries()) { const mp4 = join(cwd, 'media', clip.id + '.1.mp4'); execFileSync('ffmpeg', ['-y', '-f', 'lavfi', '-i', 'color=c=blue:s=160x90:r=10:d=5', '-f', 'lavfi', '-i', `sine=frequency=${index ? 660 : 110}:duration=5`, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-movflags', '+faststart', '-shortest', mp4], { stdio: 'ignore' }); execFileSync('ffmpeg', ['-y', '-i', mp4, '-vn', '-c:a', 'copy', join(cwd, 'media', clip.id + '.1.m4a')], { stdio: 'ignore' }); assert.ok(existsSync(mp4), `generated local clip ${mp4}`); } const require = createRequire(import.meta.url); const { createClient } = require(join(repo, 'server/node_modules/@libsql/client')); const db = createClient({ url: 'file:' + join(cwd, 'db.sqlite') }); for (const clip of clips) { const file = join(cwd, 'media', clip.id + '.1.mp4'); await db.execute({ sql: 'INSERT OR REPLACE INTO media_cache (video_id,status,gen,size,height,vcodec,acodec,duration,meta,last_access) VALUES (?,?,?,?,?,?,?,?,?,?)', args: [clip.id, 'ready', 1, statSync(file).size, 90, 'h264', 'aac', 5, JSON.stringify(clip), Date.now()] }); } db.close(); proxy = createThrottleProxy({ targetPort: port, profile: 'unthrottled' }); origin = `http://127.0.0.1:${await proxy.listen()}`; const type = engine === 'chromium' ? chromium : webkit; browser = await type.launch({ headless: true, ...(engine === 'chromium' ? { args: ['--autoplay-policy=no-user-gesture-required'] } : {}) }); context = await browser.newContext({ viewport: { width: 414, height: 896 } }); await context.addInitScript(() => { window.__dspToasts = []; new MutationObserver(() => { const text = document.querySelector('#toastContainer')?.textContent?.trim(); if (text && window.__dspToasts.at(-1) !== text) window.__dspToasts.push(text); }).observe(document, { childList: true, subtree: true, characterData: true }); }); await context.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.fulfill({ status: 200, contentType: 'image/gif', body: Buffer.from('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', 'base64') })); const page = await context.newPage(); let offlinePhase = false, failedApiTransports = 0; page.on('pageerror', error => { if (offlinePhase && failedApiTransports && /TypeError: Load failed|\/api\//.test(error.message)) (result.expectedApiErrors ||= []).push(error.message); else result.errors.push(error.message); }); page.on('requestfailed', request => { const path = new URL(request.url()).pathname; if (path.startsWith('/api/')) failedApiTransports++; else if (!(offlinePhase && path === '/sw.js')) result.errors.push(`request failed ${request.url()}: ${request.failure()?.errorText}`); }); page.on('response', response => { const url = new URL(response.url()), path = url.pathname; // The local fixture IDs have no upstream YouTube artwork. These requests // are unrelated to the local media or DSP asset path under test. if (url.hostname === 'i.ytimg.com' && /\/vi\/dspfixture0[12]\//.test(path)) { (result.fixtureResponses ||= []).push({ url: response.url(), status: response.status() }); } else if (!path.startsWith('/api/') && response.status() >= 400) result.errors.push(`asset response ${response.status()} ${response.url()}`); }); page.on('response', response => { if (/jamesdsp\/(?:worklet\.mjs|dsp\/jamesdsp[^/?]*\.mjs)/.test(response.url())) result.requests ||= [], result.requests.push({ url: response.url(), status: response.status(), type: response.headers()['content-type'] }); }); page.on('requestfailed', request => { if (/jamesdsp\//.test(request.url())) result.requestsFailed ||= [], result.requestsFailed.push({ url: request.url(), error: request.failure()?.errorText }); }); const boot = async () => { await page.goto(origin); await page.waitForFunction(() => typeof data !== 'undefined' && document.querySelector('#sidebarToggle')); }; await boot(); const manifest = await (await fetch(origin + '/api/manifest')).json(); const checkComplete = () => page.evaluate(async ({ files, groups }) => { const cache = await caches.open('ytplayer-assets'), opted = !!(data.settings.jamesDsp?.enabled || data.settings.jamesDsp?.downloadForOffline); const optional = new Set(Object.values(groups).filter(group => group.offlineOptIn && !(opted && group.offlineOptIn === 'jamesDsp')).flatMap(group => group.files)); const missing = []; for (const [path, file] of Object.entries(files)) { if (optional.has(path)) continue; const r = await cache.match(path + '?v=' + file.h); if (!r || r.headers.get('X-Asset-Hash') !== file.h) missing.push(path); } return missing; }, manifest); assert.equal((await page.evaluate(async () => { const shell = await caches.open('ytplayer-shell'); return (await shell.keys()).some(r => r.url.includes('jamesdsp')); })), false, 'DSP is never in the shell cache'); assert.equal(await poll(async () => !(await checkComplete()).length, 120000), true, 'offline completion job fills manifest before offline test'); const optionalCount = Object.values(manifest.groups || {}).filter(group => group.offlineOptIn).reduce((n, group) => n + group.files.filter(path => manifest.files[path]).length, 0); result.checks.push({ name: 'required files cached; optional DSP deferred', files: Object.keys(manifest.files).length - optionalCount, optionalDeferred: optionalCount, manifestFiles: Object.keys(manifest.files).length }); await page.evaluate(async () => { await Lazy.load('feature:eq-core'); await Lazy.load('feature:vocal-reducer'); data.settings.jamesDsp = { enabled: false, downloadForOffline: false, preset: 'flat', params: {}, irRef: null }; EQ.setLevel(1); EQ.setLevel(0); EQ.apply(); }); await page.evaluate(clip => Player.loadVideo(clip), clips[0]); await page.locator('#playBtn').click({ force: true }).catch(() => {}); if (!await poll(() => page.evaluate(() => Player.master?.currentTime > 0.4), 12000)) { await page.evaluate(() => Player.master?.play().catch(() => {})); assert.equal(await poll(() => page.evaluate(() => Player.master?.currentTime > 0.4), 12000), true, 'local test clip starts'); } await page.evaluate(() => { if (Player.master) Player.master.loop = true; }); const rms = async () => page.evaluate(async () => { const analyser = EQ.analyser(); if (!analyser) throw new Error('EQ analyser unavailable'); const values = new Float32Array(analyser.fftSize); let sum = 0, count = 0, end = performance.now() + 600; while (performance.now() < end) { analyser.getFloatTimeDomainData(values); for (const v of values) { sum += v * v; count++; } await new Promise(resolve => setTimeout(resolve, 18)); } return Math.sqrt(sum / Math.max(1, count)); }); await page.evaluate(() => { EQ.apply(); }); await sleep(250); const standardRms = await rms(); assert.ok(standardRms > 0, `Standard output has nonzero RMS (${standardRms})`); await page.locator('#bottomNav [data-view="settings"]').click(); await page.evaluate(() => document.querySelector('[data-section="playback"]').click()); await page.selectOption('#setSoundEngine', 'jamesdsp'); const active = await poll(() => page.evaluate(() => !!EQ.state().jamesDsp?.active), 12000); if (!active) result.diagnostics = await page.evaluate(() => ({ enabled: data.settings.jamesDsp?.enabled, state: EQ.state(), lazyLoaded: Lazy.loaded('feature:jamesdsp'), toast: document.querySelector('#toastContainer')?.textContent || '', toastHistory: window.__dspToasts })); assert.equal(active, true, `JamesDSP worklet activates ${JSON.stringify(result.diagnostics || {})}`); assert.equal(await poll(async () => !(await checkComplete()).length, 120000), true, 'engine opt-in completes optional WASM and module cache'); await sleep(200); const flat = await rms(); result.rms.push({ effect: 'flat', rms: flat }); for (const preset of ['bass', 'vocal', 'loudness', 'headphone', 'bright', 'warm']) { await page.selectOption('#setDspPreset', preset); await sleep(900); const value = await rms(); assert.ok(Number.isFinite(value), `${preset} RMS is finite`); result.rms.push({ effect: preset, rms: value }); } const preConvolverRms = await rms(); result.irLoad = await page.evaluate(async () => { const reference = await EQ.loadDspBuiltinIR('Small room'); await new Promise(resolve => setTimeout(resolve, 300)); return { reference, settings: data.settings.jamesDsp, state: EQ.state().jamesDsp, toast: document.querySelector('#toastContainer')?.textContent || '' }; }); const convolverRms = await rms(); assert.equal(result.irLoad.state.irRef?.name, 'Small room', 'generated IR is loaded into the active worklet'); result.checks.push({ name: 'live generated IR loaded', beforeRms: preConvolverRms, afterRms: convolverRms, ir: result.irLoad.state.irRef }); await page.evaluate(clip => { Player.pause(); Player.loadVideo(clip); }, clips[1]); await sleep(500); await page.evaluate(() => Player.master?.play().catch(() => {})); const trackSwapActive = await poll(() => page.evaluate(() => Player.master?.currentSrc.includes('dspfixture02') && Player.master?.readyState >= 2 && EQ.state().jamesDsp?.active), 12000); if (!trackSwapActive) result.trackSwapDiagnostics = await page.evaluate(() => ({ time: Player.master?.currentTime, paused: Player.master?.paused, readyState: Player.master?.readyState, state: EQ.state().jamesDsp, graph: AudioGraph.snapshot(), source: Player.master?.currentSrc })); assert.equal(trackSwapActive, true, 'DSP remains active on the replacement media source'); result.checks.push({ name: 'track switch retains DSP route', graph: await page.evaluate(() => AudioGraph.snapshot()) }); await page.evaluate(() => { data.settings.jamesDsp.params['bass.enabled'] = 1; data.settings.jamesDsp.params['eq.enabled'] = 1; EQ.apply(); }); await sleep(800); const activeRms = await rms(); await page.evaluate(async () => { await EQ.toggleDspBypass(); }); await sleep(200); const bypassRms = await rms(); const bypassState = await page.evaluate(() => EQ.state().jamesDsp); assert.equal(bypassState.route, 'standard', 'A/B bypass routes directly to the unchanged Standard graph'); result.checks.push({ name: 'A/B exact Standard route', standardRms, activeRms, bypassRms, ...bypassState }); await page.evaluate(async () => { const state = EQ.state().jamesDsp, graph = AudioGraph.snapshot(); return { state, graph }; }).then(({ state, graph }) => { assert.equal(state.liveHeaps, 1); assert.equal(state.maxLiveHeaps, 1); assert.ok(graph.sourceCount <= 2, 'only existing player media sources'); result.checks.push({ name: 'one worklet heap / single graph', ...state, graph }); }); const dspRender = await page.evaluate(async () => { const { JamesDSPNode } = await import(Lazy.url('/vendor/jamesdsp/jamesdsp-node.mjs')); async function render(effect, rate = 32000) { const frames = 32768, context = new OfflineAudioContext(2, frames, rate); const engine = await JamesDSPNode.create(context, { wasmURL: Lazy.url('/vendor/jamesdsp/dsp/jamesdsp.wasm'), workletURL: Lazy.url('/vendor/jamesdsp/worklet.mjs') }); const buffer = context.createBuffer(2, 2048, rate); for (let i = 0; i < buffer.length; i++) { const sample = Math.sin(2 * Math.PI * 440 * i / rate) * 0.25; buffer.getChannelData(0)[i] = sample; buffer.getChannelData(1)[i] = sample; } if (effect === 'convolver') { const ir = context.createBuffer(4, 4096, rate); ir.getChannelData(0)[0] = 0.5; ir.getChannelData(1)[97] = 0.25; ir.getChannelData(2)[211] = -0.2; ir.getChannelData(3)[0] = 0.5; await engine.loadIR(ir); await engine.setParam('convolver.mix', 1); await engine.setParam('convolver.enabled', 1); } else if (effect === 'spatial') { await engine.setParam('spatial.enabled', 1); await engine.setParam('spatial.mode', 6); await engine.setParam('spatial.spread', 100); await engine.setParam('spatial.azimuth', 45); } await engine.setBypass(false); const source = context.createBufferSource(); source.buffer = buffer; source.connect(engine.node); engine.node.connect(context.destination); source.start(); const rendered = await context.startRendering(); const left = rendered.getChannelData(0), right = rendered.getChannelData(1); let l = 0, r = 0, diff = 0; for (let i = 0; i < frames; i++) { l += left[i] ** 2; r += right[i] ** 2; diff += (left[i] - right[i]) ** 2; } const metrics = { leftRms: Math.sqrt(l / frames), rightRms: Math.sqrt(r / frames), interauralRms: Math.sqrt(diff / frames), rate: rendered.sampleRate }; engine.destroy(); return metrics; } const dry = await render('dry'), convolver = await render('convolver'), spatial = await render('spatial'); return { dry, convolver, spatial }; }); assert.equal(dspRender.dry.rate, 32000, 'ASRC accepts an arbitrary AudioContext rate'); assert.ok(dspRender.convolver.interauralRms > dspRender.dry.interauralRms + 0.00001, `four-channel IR changes rendered stereo output (${JSON.stringify(dspRender)})`); assert.ok(dspRender.spatial.interauralRms > dspRender.dry.interauralRms + 0.00001, `3D stage changes interaural difference (${JSON.stringify(dspRender)})`); result.checks.push({ name: 'offline four-channel convolver, spatial difference and 32 kHz ASRC', ...dspRender }); await page.evaluate(() => persist()); await sleep(500); offlinePhase = true; if (engine === 'chromium') await context.setOffline(true); else await proxy.setOffline(true); await page.reload(); await page.waitForFunction(() => typeof data !== 'undefined' && document.querySelector('#sidebarToggle')); assert.equal(await poll(() => page.evaluate(() => !!EQ.state().jamesDsp?.active), 30000), true, 'offline reload restores enabled DSP from cached modules'); assert.equal(await page.evaluate(() => EQ.state().jamesDsp?.irRef?.name), 'Small room', 'stored generated IR is restored offline'); result.checks.push({ name: 'offline reload with engine and stored IR enabled', missing: await checkComplete(), ir: await page.evaluate(() => EQ.state().jamesDsp?.irRef) }); // A separate fresh profile corrupts the optional wasm response and verifies that Standard stays audible. offlinePhase = false; failedApiTransports = 0; if (engine === 'chromium') await context.setOffline(false); else await proxy.setOffline(false); const brokenContext = await browser.newContext({ viewport: { width: 414, height: 896 } }); await brokenContext.route('**/*', route => route.request().url().startsWith(origin) ? route.continue() : route.fulfill({ status: 200, contentType: 'image/gif', body: Buffer.from('R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', 'base64') })); await brokenContext.addInitScript(() => { const realFetch = window.fetch.bind(window); window.fetch = (input, init) => String(input).includes('/vendor/jamesdsp/dsp/jamesdsp.wasm') ? Promise.resolve(new Response('corrupt', { status: 503 })) : realFetch(input, init); }); const brokenPage = await brokenContext.newPage(); brokenPage.on('pageerror', e => result.errors.push('corrupt test: ' + e.message)); await brokenPage.goto(origin); await brokenPage.waitForFunction(() => typeof data !== 'undefined'); await brokenPage.locator('#bottomNav [data-view="settings"]').click(); await brokenPage.evaluate(() => document.querySelector('[data-section="playback"]').click()); await brokenPage.selectOption('#setSoundEngine', 'jamesdsp'); assert.equal(await poll(() => brokenPage.evaluate(() => data.settings.jamesDsp.enabled === false && EQ.state().wired), 30000), true, 'corrupt wasm falls back to Standard'); result.checks.push({ name: 'corrupt wasm returns to Standard', enabled: await brokenPage.evaluate(() => data.settings.jamesDsp.enabled), graph: await brokenPage.evaluate(() => AudioGraph.snapshot()) }); await brokenContext.close(); } catch (error) { result.failures.push(error.stack || error.message); } finally { await context?.close(); await browser?.close(); await proxy?.close(); await stop(); } } try { for (const engine of requested === 'all' ? ['chromium', 'webkit'] : [requested]) await run(engine); } finally { writeFileSync(join(repo, 'perf/.tmp/dsp-results.json'), JSON.stringify(results, null, 2) + '\n'); rmSync(scratch, { recursive: true, force: true }); } if (results.failures.length || results.results.some(result => result.failures.length || result.errors.length)) process.exitCode = 1;