From ef12468a9947b24d0daddb2fcc8ca7a87bd02c8e Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Sat, 10 Oct 2026 19:25:06 +0800 Subject: [PATCH] Extend JamesDSP with convolution and spatial audio --- frontend/app.js | 65 ++++++++++++-- frontend/asset-sync-core.js | 37 +++++--- frontend/asset-sync-core.test.js | 14 +++ frontend/assets.json | 6 +- frontend/index.html | 2 +- frontend/jamesdsp-app.test.js | 9 +- frontend/jamesdsp-assets.test.js | 21 +++-- frontend/jamesdsp-controller.test.js | 15 ++-- frontend/jamesdsp-ir.test.js | 23 +++++ frontend/settings-persist.test.js | 8 +- frontend/styles.css | 3 + frontend/vendor/jamesdsp/README.md | 28 +++--- frontend/vendor/jamesdsp/VERSION.txt | 27 +++--- ...esdsp-77be0b2.mjs => jamesdsp-a982b53.mjs} | 2 +- frontend/vendor/jamesdsp/dsp/jamesdsp.wasm | Bin 1210362 -> 1216423 bytes frontend/vendor/jamesdsp/ir-builtins.mjs | 22 +++++ frontend/vendor/jamesdsp/ir-store.mjs | 24 ++++++ frontend/vendor/jamesdsp/ir-utils.mjs | 43 ++++++++++ frontend/vendor/jamesdsp/jamesdsp-feature.js | 70 ++++++++++++--- frontend/vendor/jamesdsp/jamesdsp-node.mjs | 70 ++++++++------- frontend/vendor/jamesdsp/params.mjs | 6 +- frontend/vendor/jamesdsp/worklet.mjs | 9 +- frontend/views-core.js | 80 ++++++++++++++++-- perf/README.md | 4 +- perf/dsp.mjs | 59 ++++++++++--- perf/lazy.mjs | 3 +- perf/offline-complete.mjs | 25 ++++-- perf/settings-persist.mjs | 8 +- 28 files changed, 540 insertions(+), 143 deletions(-) create mode 100644 frontend/jamesdsp-ir.test.js rename frontend/vendor/jamesdsp/dsp/{jamesdsp-77be0b2.mjs => jamesdsp-a982b53.mjs} (78%) create mode 100644 frontend/vendor/jamesdsp/ir-builtins.mjs create mode 100644 frontend/vendor/jamesdsp/ir-store.mjs create mode 100644 frontend/vendor/jamesdsp/ir-utils.mjs diff --git a/frontend/app.js b/frontend/app.js index 808bd92..b7a11f6 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -663,7 +663,7 @@ const DEFAULT_SETTINGS = { fontScale: 'normal', // 'small' | 'normal' | 'large' | 'xl' levelVolume: null, // loudness levelling; null = on (on iPhone: applied to saved songs' rendered copies) liveAudioIOS: false, // iPhone: live Web Audio EQ/levelling for streamed songs (stops when the screen locks) - jamesDsp: { enabled: false, preset: 'flat', params: {} }, + jamesDsp: { enabled: false, downloadForOffline: false, preset: 'flat', params: {}, irRef: null }, parallelSaves: 4, // offline saves running at once (Settings → Offline cache) density: 'comfortable', // 'comfortable' | 'compact' perfMode: false, // disable heavy visual effects for speed @@ -678,11 +678,15 @@ function normalizeJamesDspSettings(settings) { const params = saved.params && typeof saved.params === 'object' && !Array.isArray(saved.params) ? { ...saved.params } : {}; // Impulse-response PCM is intentionally session-only; do not re-enable the // convolver on a later boot without its uploaded audio buffer. - delete params['convolver.enabled']; + const irRef = saved.irRef && typeof saved.irRef.name === 'string' && typeof saved.irRef.hash === 'string' + ? { name: saved.irRef.name, hash: saved.irRef.hash } : null; + if (!irRef) delete params['convolver.enabled']; settings.jamesDsp = { enabled: saved.enabled === true, + downloadForOffline: saved.downloadForOffline === true, preset: typeof saved.preset === 'string' ? saved.preset : 'flat', params, + irRef, }; return settings; } @@ -5535,6 +5539,7 @@ const loadSoundFeatures=()=>Promise.all([Lazy.load('feature:eq-core'),Lazy.load( const EQ_BANDS = EqParameters.bands; const EQ_PRESETS = EqParameters.presets; const JamesDSPFeature = Lazy.proxy('feature:jamesdsp', ['createController'], 'JamesDSPFeature'); +const JamesDSPTools = Lazy.proxy('feature:jamesdsp', ['generateIR', 'decodeIR', 'prepareIR', 'saveIR', 'getIR', 'listIRs', 'deleteIR'], 'JamesDSPFeature'); const EQ = (() => { const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); const AC = window.AudioContext || window.webkitAudioContext; @@ -5726,7 +5731,7 @@ const EQ = (() => { // one-way step the EQ takes, so it is only offered where the EQ is (never // on iOS, where Web Audio would silence background playback). function analyser() { - if (dspController?.active) return dspController.analyser(); + if (dspController?.active && !dspController.state().bypassed) return dspController.analyser(); if (!available() || !build()) return null; attach(els.video); attach(els.audio); @@ -5765,16 +5770,50 @@ const EQ = (() => { catch (error) { dspFailed(error); return false; } } + function dspLowEndDevice() { + const cores=Number(navigator.hardwareConcurrency), memory=Number(navigator.deviceMemory); + return (cores>0&&cores<=4)||(memory>0&&memory<=4); + } + + async function loadDspIRBuffer(buffer, reference=null) { + if (!data.settings.jamesDsp?.enabled || !dspController?.active) throw new Error('Turn on JamesDSP before importing an impulse response.'); + await dspController.loadIR(buffer,reference); + data.settings.jamesDsp.irRef=reference; + data.settings.jamesDsp.params['convolver.enabled']=1; + persist(); + return reference; + } + async function loadDspIR(file) { if (!data.settings.jamesDsp?.enabled || !dspController?.active) throw new Error('Turn on JamesDSP before importing an impulse response.'); - const context = st.ctx; - if (!context?.decodeAudioData) throw new Error('Audio decoding is unavailable on this device.'); - const audioBuffer = await context.decodeAudioData(await file.arrayBuffer()); - return dspController.loadIR(audioBuffer); + const context=st.ctx;if(!context?.decodeAudioData)throw new Error('Audio decoding is unavailable on this device.'); + const buffer=await context.decodeAudioData(await file.arrayBuffer()); + const prepared=await JamesDSPTools.prepareIR(context,buffer,dspLowEndDevice()?24000:65536); + const name=String(file.name||'Uploaded IR').replace(/\.[^.]+$/,'').slice(0,100)||'Uploaded IR'; + let reference=null;try{reference=await JamesDSPTools.saveIR(name,prepared,{kind:'uploaded'});}catch(error){toast(`Impulse response stays on this device for this session: ${error.message}`);} + return loadDspIRBuffer(prepared,reference); + } + + async function loadDspBuiltinIR(name) { + if(!data.settings.jamesDsp?.enabled||!dspController?.active)throw new Error('Turn on JamesDSP before loading an impulse response.'); + const context=st.ctx;if(!context)throw new Error('Audio context is unavailable.'); + const lowEnd=dspLowEndDevice();if(lowEnd&&name!=='Small room')throw new Error('Long impulse responses are disabled on this lower-power device.'); + const buffer=await JamesDSPTools.generateIR(name,context.sampleRate,2,lowEnd?24000:65536); + let reference=null;try{reference=await JamesDSPTools.saveIR(name,buffer,{kind:'generated'});}catch(error){toast(`Generated response stays in this session: ${error.message}`);} + return loadDspIRBuffer(buffer,reference); + } + + async function loadDspSavedIR(name) { + const stored=await JamesDSPTools.getIR(name);if(!stored)throw new Error(`Stored impulse response not found: ${name}`); + return loadDspIRBuffer(stored.buffer,{name:stored.name,hash:stored.hash}); + } + + async function listDspIRs() { + return JamesDSPTools.listIRs(); } return { open, apply, renderButton, available, analyser, setLevel, gains, bands: EQ_BANDS, isIOS: () => isIOS, state: () => ({ wired: !!st.ctx, ctx: st.ctx && st.ctx.state, gains: st.filters ? st.filters.map((f) => f.gain.value) : null, - levelDb: st.level ? Math.round(200 * Math.log10(Math.max(1e-6, st.level.gain.value))) / 10 : null, jamesDsp: dspController?.state() || null }), toggleDspBypass, loadDspIR }; + levelDb: st.level ? Math.round(200 * Math.log10(Math.max(1e-6, st.level.gain.value))) / 10 : null, jamesDsp: dspController?.state() || null }), toggleDspBypass, loadDspIR, loadDspIRBuffer, loadDspBuiltinIR, loadDspSavedIR, listDspIRs }; })(); // ============================================================================ @@ -8857,6 +8896,8 @@ async function registerServiceWorker() { const reg = await navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }); _swReg = reg; Lazy.reportLayout(reg); + reportDspOfflineOptIn(reg); + navigator.serviceWorker.ready.then(readyReg => reportDspOfflineOptIn(readyReg)).catch(() => {}); // A waiting worker only means "update pending" when this page is already // controlled by a previous SW. On a FIRST install (fresh visit, or right @@ -8892,6 +8933,14 @@ async function registerServiceWorker() { } } +function reportDspOfflineOptIn(reg = _swReg) { + if (!WEB || !navigator.serviceWorker) return; + const dsp = data.settings?.jamesDsp; + const values = dsp?.enabled || dsp?.downloadForOffline ? ['jamesDsp'] : []; + const workers = new Set([navigator.serviceWorker.controller, reg?.installing, reg?.active, reg?.waiting]); + for (const worker of workers) worker?.postMessage({ type: 'OFFLINE_OPT_INS', values }); +} + // /?q= — a shareable search link (same family as Share's /?v=&t=). // Runs after wireUI() so the search form's submit handler exists; the param // is stripped immediately so a reload doesn't re-run the search. diff --git a/frontend/asset-sync-core.js b/frontend/asset-sync-core.js index 0c6ede2..366d7eb 100644 --- a/frontend/asset-sync-core.js +++ b/frontend/asset-sync-core.js @@ -3,12 +3,22 @@ 'use strict'; const CACHE = 'ytplayer-assets'; const STATE = '/__ytp_asset_state'; + const OPT_INS = '/__ytp_offline_opt_ins'; const url = (path, file) => path + '?v=' + file.h; function paths(manifest) { return Object.keys(manifest.files).sort(); } + function offlinePaths(manifest, optIns = []) { + const allowed = new Set(optIns || []), optional = new Map(); + for (const group of Object.values(manifest.groups || {})) { + if (!group.offlineOptIn || allowed.has(group.offlineOptIn)) continue; + for (const path of group.files || []) optional.set(path, true); + } + return paths(manifest).filter(path => !optional.has(path)); + } + function blocking(manifest, previous, activeLayout) { const names = new Set(Object.entries(manifest.groups) .filter(([name, group]) => name === 'core' || !group.background || @@ -61,13 +71,17 @@ return valid; } - async function status(manifest, cache, { previous, activeLayout, offline = true } = {}) { + async function readOptIns(cache) { + try { return (await (await cache.match(OPT_INS))?.json()) || []; } catch { return []; } + } + + async function status(manifest, cache, { previous, activeLayout, offline = true, optIns } = {}) { const found = await Promise.all(blocking(manifest, previous, activeLayout).map(async path => { const response = await cache.match(url(path, manifest.files[path])); return verifiedProbe(response, manifest.files[path].h); })); const missing = found.filter(value => !value).length; - return { ready: missing === 0, missing, version: manifest.buildTag, ...(offline ? await completeness(manifest, cache) : {}) }; + return { ready: missing === 0, missing, version: manifest.buildTag, ...(offline ? await completeness(manifest, cache, { optIns }) : {}) }; } // All concurrent messages/layout jobs in this worker share six download slots. @@ -89,10 +103,10 @@ finally { pendingDownloads.delete(key); } } - async function syncAssets(manifest, { cache, fetchFn, concurrency = 6, attempts = 3, activeLayout, groups, all = false }) { + async function syncAssets(manifest, { cache, fetchFn, concurrency = 6, attempts = 3, activeLayout, groups, all = false, optIns } = {}) { const previous = (await state(cache))?.current; const missing = []; - const selected = all ? paths(manifest) : groups ? [...new Set(groups.flatMap(name => manifest.groups[name]?.files || []))].sort() + const selected = all ? offlinePaths(manifest, optIns || await readOptIns(cache)) : groups ? [...new Set(groups.flatMap(name => manifest.groups[name]?.files || []))].sort() : blocking(manifest, previous, activeLayout); for (const path of selected) { const key = url(path, manifest.files[path]); @@ -135,8 +149,8 @@ // Verified cache entries are the durable progress journal. Counters are rebuilt // after worker termination or browser eviction; never trust a stored ready bit. - async function completeness(manifest, cache) { - const files = paths(manifest); + async function completeness(manifest, cache, { optIns } = {}) { + const files = offlinePaths(manifest, optIns || await readOptIns(cache)); const missingFiles = (await Promise.all(files.map(async path => { const response = await cache.match(url(path, manifest.files[path])); return verifiedProbe(response, manifest.files[path].h) ? null : path; @@ -146,12 +160,13 @@ } const completionKey = manifest => '/__ytp_completion/' + manifest.buildTag; - async function complete(manifest, { cache, fetchFn, cycles = 4, now = Date.now, + async function complete(manifest, { cache, fetchFn, cycles = 4, now = Date.now, optIns, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), baseDelay = 1000, maxDelay = 30000, notify = () => {}, force = false } = {}) { const key = completionKey(manifest), saved = await cache.match(key); let job = saved ? await saved.json() : { failures: 0, nextRetryAt: 0 }; - let snapshot = await completeness(manifest, cache); + const selectedOptIns = optIns || await readOptIns(cache); + let snapshot = await completeness(manifest, cache, { optIns: selectedOptIns }); const publish = async running => { job = { ...job, ...snapshot, running, version: manifest.buildTag }; await cache.put(key, Response.json(job)); @@ -162,9 +177,9 @@ for (let cycle = 0; cycle < cycles; cycle++) { await publish(true); let error; - try { await syncAssets(manifest, { cache, fetchFn, all: true }); } + try { await syncAssets(manifest, { cache, fetchFn, all: true, optIns: selectedOptIns }); } catch (failure) { error = failure; } - snapshot = await completeness(manifest, cache); + snapshot = await completeness(manifest, cache, { optIns: selectedOptIns }); if (snapshot.offlineReady) { job.failures = 0; job.nextRetryAt = 0; job.error = null; await publish(false); return job; } job.failures++; const delay = Math.min(maxDelay, baseDelay * 2 ** Math.min(job.failures - 1, 10)); @@ -198,7 +213,7 @@ return next; } - const api = { CACHE, STATE, url, paths, blocking, plan, retained, fallback, status, completeness, completionKey, complete, syncAssets, state, commit }; + const api = { CACHE, STATE, OPT_INS, url, paths, offlinePaths, blocking, plan, retained, fallback, status, completeness, completionKey, complete, syncAssets, readOptIns, state, commit }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.AssetSyncCore = api; })(typeof globalThis !== 'undefined' ? globalThis : this); diff --git a/frontend/asset-sync-core.test.js b/frontend/asset-sync-core.test.js index 60b366d..28f1da8 100644 --- a/frontend/asset-sync-core.test.js +++ b/frontend/asset-sync-core.test.js @@ -56,6 +56,20 @@ test('completion includes ungrouped runtime files and exposes verified missing p assert.equal((await core.status(m,c)).offlineReady,true); }); +test('offline completion skips opt-in DSP assets until the user enables offline audio effects', async () => { + const c=cache(),m=manifest();m.files['/vendor/jamesdsp/dsp/jamesdsp.wasm']={h:'wasm'}; + m.groups.jamesdsp={contract:1,eager:false,background:true,offlineOptIn:'jamesDsp',files:['/vendor/jamesdsp/dsp/jamesdsp.wasm']}; + const calls=[]; + const defaultJob=await core.complete(m,{cache:c,fetchFn:async u=>{calls.push(u);return response(u.split('=')[1])}}); + assert.deepEqual(calls.sort(),['/a.js?v=a','/index.html?v=b']); + assert.equal(defaultJob.total,2);assert.equal(defaultJob.offlineReady,true); + calls.length=0; + await c.put(core.OPT_INS,Response.json(['jamesDsp'])); + const optedJob=await core.complete(m,{cache:c,fetchFn:async u=>{calls.push(u);return response(u.split('=')[1])}}); + assert.deepEqual(calls,['/vendor/jamesdsp/dsp/jamesdsp.wasm?v=wasm']); + assert.equal(optedJob.total,3);assert.equal(optedJob.offlineReady,true); +}); + test('completion persists backoff and resumes only missing files after a worker restart',async()=>{ const c=cache(),m=manifest();let clock=1000;const calls=[]; await core.complete(m,{cache:c,cycles:1,now:()=>clock,baseDelay:10,fetchFn:async u=>{if(u.includes('index'))throw Error('offline');return response('a');}}); diff --git a/frontend/assets.json b/frontend/assets.json index 9a9f2de..ec2f500 100644 --- a/frontend/assets.json +++ b/frontend/assets.json @@ -365,12 +365,16 @@ "contract": 1, "eager": false, "background": true, + "offlineOptIn": "jamesDsp", "files": [ "/vendor/jamesdsp/jamesdsp-feature.js", "/vendor/jamesdsp/jamesdsp-node.mjs", "/vendor/jamesdsp/params.mjs", + "/vendor/jamesdsp/ir-utils.mjs", + "/vendor/jamesdsp/ir-builtins.mjs", + "/vendor/jamesdsp/ir-store.mjs", "/vendor/jamesdsp/worklet.mjs", - "/vendor/jamesdsp/dsp/jamesdsp-77be0b2.mjs", + "/vendor/jamesdsp/dsp/jamesdsp-a982b53.mjs", "/vendor/jamesdsp/dsp/jamesdsp.wasm" ], "dependencies": [ diff --git a/frontend/index.html b/frontend/index.html index c37b12b..f0f9bec 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -19,7 +19,7 @@ - + diff --git a/frontend/jamesdsp-app.test.js b/frontend/jamesdsp-app.test.js index ab42629..026b3ae 100644 --- a/frontend/jamesdsp-app.test.js +++ b/frontend/jamesdsp-app.test.js @@ -16,7 +16,8 @@ function parsePreset(text) { } test('the persisted engine is Standard by default and upgrades missing JamesDSP settings safely', () => { - assert.match(app, /jamesDsp: \{ enabled: false, preset: 'flat', params: \{\} \}/); + assert.match(app, /jamesDsp: \{ enabled: false, downloadForOffline: false, preset: 'flat', params: \{\}, irRef: null \}/); + assert.match(app, /downloadForOffline: saved\.downloadForOffline === true/); assert.match(app, /enabled: saved\.enabled === true/); assert.match(app, /preset: typeof saved\.preset === 'string' \? saved\.preset : 'flat'/); assert.match(app, /settings: normalizeJamesDspSettings\(\{ \.\.\.DEFAULT_SETTINGS, \.\.\.\(loaded\.settings \|\| \{\}\) \}\)/); @@ -47,6 +48,12 @@ test('Playback settings expose persisted presets, bands, safe import/export, IR, assert.match(views, /\[25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000\]/); assert.match(views, /imported\?\.texts/); assert.doesNotMatch(views, /liveprog|ddc|arbitrary/); + assert.match(views, /navigator\.hardwareConcurrency/); + assert.match(views, /Long impulse responses are disabled/); + assert.doesNotMatch(views, /44\.1 or 48 kHz/); + assert.match(views, /setDspSpatialMode/); + assert.match(views, /data-dsp-param="convolver\.predelayMs"/); + assert.match(views, /data-dsp-param="convolver\.mix"/); assert.match(app, /Bypass JamesDSP/); assert.match(app, /toggleDspBypass/); }); diff --git a/frontend/jamesdsp-assets.test.js b/frontend/jamesdsp-assets.test.js index edd3e49..0689349 100644 --- a/frontend/jamesdsp-assets.test.js +++ b/frontend/jamesdsp-assets.test.js @@ -12,13 +12,17 @@ test('JamesDSP runtime files live only in a lazy background group', () => { assert.equal(group.contract, 1); assert.equal(group.eager, false); assert.equal(group.background, true); + assert.equal(group.offlineOptIn, 'jamesDsp'); assert.deepEqual(group.dependencies, ['feature:eq-core', 'feature:vocal-reducer']); assert.deepEqual(group.files, [ '/vendor/jamesdsp/jamesdsp-feature.js', '/vendor/jamesdsp/jamesdsp-node.mjs', '/vendor/jamesdsp/params.mjs', + '/vendor/jamesdsp/ir-utils.mjs', + '/vendor/jamesdsp/ir-builtins.mjs', + '/vendor/jamesdsp/ir-store.mjs', '/vendor/jamesdsp/worklet.mjs', - '/vendor/jamesdsp/dsp/jamesdsp-77be0b2.mjs', + '/vendor/jamesdsp/dsp/jamesdsp-a982b53.mjs', '/vendor/jamesdsp/dsp/jamesdsp.wasm', ]); for (const path of group.files) assert.equal(assets.groups.core.files.includes(path), false, `${path} is not core`); @@ -33,24 +37,31 @@ test('JamesDSP resolves every module dependency through the page build manifest' assert.match(node, /assetURL\('\/vendor\/jamesdsp\/dsp\/jamesdsp\.wasm'/); assert.match(node, /assetURL\('\/vendor\/jamesdsp\/params\.mjs'/); assert.match(node, /processorOptions:\{wasmBytes\}/); - assert.match(worklet, /import createJamesDSP from '\.\/dsp\/jamesdsp-77be0b2\.mjs'/); + assert.match(worklet, /import createJamesDSP from '\.\/dsp\/jamesdsp-a982b53\.mjs'/); assert.doesNotMatch(worklet, /import\(/); + assert.match(node, /context\.sampleRate<8000\|\|context\.sampleRate>96000/); + assert.match(node, /!\[1,2,4\]\.includes\(buffer\.numberOfChannels\) \|\| buffer\.length>65536/); const feature = readFileSync(join(root, 'vendor/jamesdsp/jamesdsp-feature.js'), 'utf8'); assert.match(feature, /Lazy\.url\('\/vendor\/jamesdsp\/jamesdsp-node\.mjs'\)/); assert.match(feature, /Lazy\.url\('\/vendor\/jamesdsp\/dsp\/jamesdsp\.wasm'\)/); }); -test('v1 wrapper rejects text effects and omits them from portable presets', () => { +test('player wrapper rejects text effects while portable presets retain IR references', () => { const node = readFileSync(join(root, 'vendor/jamesdsp/jamesdsp-node.mjs'), 'utf8'); const worklet = readFileSync(join(root, 'vendor/jamesdsp/worklet.mjs'), 'utf8'); assert.match(node, /Text effects are disabled in this player version/); - assert.match(node, /toJSON\(\) \{return \{version:1,params:/); + assert.match(node, /toJSON\(\) \{return \{version:2,params:/); + assert.match(node, /irCapFrames:view\.getUint32\(32,true\)/); + assert.match(node, /getIR\(preset\.irRef\.name\)/); + const feature = readFileSync(join(root, 'vendor/jamesdsp/jamesdsp-feature.js'), 'utf8'); + assert.match(feature, /async function prepareIR\(context, buffer, maxFrames = 65536\)/); + assert.match(feature, /prepared\.length>Math\.min\(65536,maxFrames\)/); assert.doesNotMatch(worklet, /data\.type==='text'/); }); test('vendor provenance pins the source and retains both supplied license texts', () => { const version = readFileSync(join(root, 'vendor/jamesdsp/VERSION.txt'), 'utf8'); - assert.match(version, /77be0b2e825a20ba98feff33d16dd0a43232ef3b/); + assert.match(version, /a982b53c4ab347d1d9c50f4ce7ee3c2735eaa817/); assert.match(version, /GPL-2\.0 text/); assert.ok(readFileSync(join(root, 'vendor/jamesdsp/LICENSE.txt'), 'utf8').includes('Version 3')); assert.ok(readFileSync(join(root, 'vendor/jamesdsp/UPSTREAM-MAIN-LICENSE.txt'), 'utf8').includes('Version 2')); diff --git a/frontend/jamesdsp-controller.test.js b/frontend/jamesdsp-controller.test.js index e0782c0..f241f96 100644 --- a/frontend/jamesdsp-controller.test.js +++ b/frontend/jamesdsp-controller.test.js @@ -52,11 +52,11 @@ test('a rejected or unsupported engine leaves the Standard route active', async assert.equal(f.controller.state().liveHeaps, 0); assert.equal(f.errors.length, 1); - const wrongRate = fixture(); - wrongRate.context.sampleRate = 32000; - await assert.rejects(wrongRate.controller.enable({ preset: 'flat' }), /44\.1 or 48 kHz/); - assert.equal(wrongRate.created.length, 0); - assert.equal(wrongRate.controller.state().liveHeaps, 0); + const supportedRate = fixture(); + supportedRate.context.sampleRate = 32000; + await supportedRate.controller.enable({ preset: 'flat' }); + assert.equal(supportedRate.controller.state().active, true); + assert.equal(supportedRate.controller.state().sampleRate, 32000); }); test('a runtime worklet failure switches sources to Standard before destroying the heap', async () => { @@ -101,9 +101,14 @@ test('A/B bypass is retained when settings are reapplied', async () => { const f = fixture(); await f.controller.enable({ preset: 'warm' }, 0); assert.equal(await f.controller.toggleBypass(), true); + assert.equal(f.controller.state().route, 'standard'); + assert.deepEqual(f.routed.at(-1), [['video', 'audio'], f.standardInput], 'bypass follows the exact Standard route'); await f.controller.enable({ preset: 'bass' }, 0); assert.equal(f.controller.state().bypassed, true); + assert.equal(f.controller.state().route, 'standard'); + assert.deepEqual(f.routed.at(-1), [['video', 'audio'], f.standardInput], 'settings changes cannot replace the Standard route while bypassed'); assert.ok(f.bypass.at(-1)); assert.equal(await f.controller.toggleBypass(), false); + assert.equal(f.controller.state().route, 'jamesdsp'); assert.equal(f.bypass.at(-1), false); }); diff --git a/frontend/jamesdsp-ir.test.js b/frontend/jamesdsp-ir.test.js new file mode 100644 index 0000000..37fb5fc --- /dev/null +++ b/frontend/jamesdsp-ir.test.js @@ -0,0 +1,23 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); + +test('generated room responses support four true-stereo paths and respect the low-memory cap', async () => { + const { generateIR, BUILTIN_IRS } = await import('./vendor/jamesdsp/ir-builtins.mjs'); + const { resampleIR } = await import('./vendor/jamesdsp/ir-utils.mjs'); + const ir = generateIR('Hall', 48000, 4, { maxFrames: 24000 }); + assert.equal(ir.numberOfChannels, 4); + assert.equal(ir.length, 24000); + assert.equal(ir.truncated, true); + assert.ok(ir.getChannelData(0).some((sample, i) => sample !== ir.getChannelData(1)[i]), 'each matrix path has distinct taps'); + const converted = resampleIR(ir, 32000); + assert.equal(converted.sampleRate, 32000); + assert.equal(converted.numberOfChannels, 4); + assert.equal(converted.length, 16000); + assert.deepEqual(Object.keys(BUILTIN_IRS), ['Small room', 'Hall', 'Plate', 'Cathedral']); +}); + +test('the IndexedDB store rejects impulse responses beyond its measured internal-frame cap', async () => { + const { saveIR } = await import('./vendor/jamesdsp/ir-store.mjs'); + const oversized = { sampleRate: 48000, numberOfChannels: 4, length: 65537, getChannelData: () => new Float32Array(65537) }; + await assert.rejects(saveIR('oversized', oversized), /65,536-frame internal limit/); +}); diff --git a/frontend/settings-persist.test.js b/frontend/settings-persist.test.js index 9ed8f6f..62b9134 100644 --- a/frontend/settings-persist.test.js +++ b/frontend/settings-persist.test.js @@ -6,8 +6,8 @@ const source = fs.readFileSync(__dirname + '/app.js', 'utf8'); // Exercise the production persistence/profile functions without starting a DOM player. const declaration = name => source.match(new RegExp('(?:async )?function ' + name + '\\([^]*?\\n}'))[0]; function fixture(updatedAt) { - const settings = { saveBeforePlay: true, autoPreload: true, layout: 'f7-swipe', eq: { preset: 'custom', g: [1,2,3,4,5] }, jamesDsp: { enabled: true, preset: 'warm', params: { 'eq.band0': 3 } } }; - const defaults = { saveBeforePlay: false, autoPreload: true, layout: 'classic', jamesDsp: { enabled: false, preset: 'flat', params: {} } }; + const settings = { saveBeforePlay: true, autoPreload: true, layout: 'f7-swipe', eq: { preset: 'custom', g: [1,2,3,4,5] }, jamesDsp: { enabled: true, downloadForOffline: true, preset: 'warm', params: { 'eq.band0': 3 } } }; + const defaults = { saveBeforePlay: false, autoPreload: true, layout: 'classic', jamesDsp: { enabled: false, downloadForOffline: false, preset: 'flat', params: {} } }; const context = vm.createContext({ WEB: true, data: { settings, profile: { name: 'test', syncedAt: 200 } }, DEFAULT_SETTINGS: defaults, API: { saveData: async () => {} }, profileHeaders: () => ({}), scheduleProfilePush: () => context.pushes++, pushes: 0, fetch: async () => ({ status: 200, json: async () => ({ ok: true, name: 'test', updatedAt, data: { settings: { saveBeforePlay: false } } }) }), localStorage: { getItem: () => JSON.stringify({ settings }) } }); vm.runInContext([declaration('loadDataFromStorage'), declaration('normalizeJamesDspSettings'), declaration('applyProfileData'), declaration('pullProfileIfNewer')].join('\n'), context); return context; @@ -18,7 +18,7 @@ test('stored settings load verbatim instead of reverting to defaults', () => { assert.equal(loaded.settings.saveBeforePlay, true); assert.equal(loaded.settings.layout, 'f7-swipe'); assert.deepEqual(JSON.parse(JSON.stringify(loaded.settings.eq.g)), [1,2,3,4,5]); - assert.deepEqual(JSON.parse(JSON.stringify(loaded.settings.jamesDsp)), { enabled: true, preset: 'warm', params: { 'eq.band0': 3 } }); + assert.deepEqual(JSON.parse(JSON.stringify(loaded.settings.jamesDsp)), { enabled: true, downloadForOffline: true, preset: 'warm', params: { 'eq.band0': 3 } }); }); for (const updatedAt of [100,200]) test(`profile timestamp ${updatedAt} cannot overwrite local settings synced at 200`, async () => { const c = fixture(updatedAt), previous = c.data.settings; @@ -32,6 +32,6 @@ test('newer profile intentionally replaces settings and restores missing default assert.equal(c.data.settings.saveBeforePlay, false); assert.equal(c.data.settings.autoPreload, true); assert.equal(c.data.settings.layout, 'classic'); - assert.deepEqual(JSON.parse(JSON.stringify(c.data.settings.jamesDsp)), { enabled: false, preset: 'flat', params: {} }); + assert.deepEqual(JSON.parse(JSON.stringify(c.data.settings.jamesDsp)), { enabled: false, downloadForOffline: false, preset: 'flat', params: {}, irRef: null }); assert.equal(c.data.profile.syncedAt, 300); }); diff --git a/frontend/styles.css b/frontend/styles.css index fa7092b..dbe047a 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1165,6 +1165,9 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } .dsp-param-row input[type="range"] { width: min(46vw, 190px); } .dsp-actions { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 0; } .dsp-actions .btn { color: var(--text); } +.dsp-subsection { margin: 14px 0; padding: 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--bg-1); } +.dsp-subsection h4 { margin: 0 0 8px; color: var(--text); } +.dsp-risk { padding: 9px 11px; border-radius: 8px; color: var(--text); background: color-mix(in srgb, var(--accent) 13%, var(--bg-2)); font-size: .9em; } .set-row input[type="checkbox"] { width: 17px; height: 17px; accent-color: var(--accent); cursor: pointer; } .about-version { color: var(--text-dim); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; user-select: text; } .set-select { diff --git a/frontend/vendor/jamesdsp/README.md b/frontend/vendor/jamesdsp/README.md index 75962bf..0e84ecb 100644 --- a/frontend/vendor/jamesdsp/README.md +++ b/frontend/vendor/jamesdsp/README.md @@ -1,19 +1,17 @@ # JamesDSP vendor files -The WebAssembly engine, parameter table and browser adapter originate from -`jamesdsp-web` revision `77be0b2e825a20ba98feff33d16dd0a43232ef3b`. Source is -available in that repository and the underlying JamesDSPManager source is -referenced by its `upstream/` tree. The runtime files remain in the lazy -`feature:jamesdsp` asset group; license and provenance documents are not runtime -assets. +The browser port's worklet, 8–96 kHz streaming sample-rate adapter, matrix +convolver, HRTF/virtual-speaker spatial stage, generated IRs and IndexedDB IR +store are pinned to the source revision in VERSION.txt. Runtime files stay in +the lazy `feature:jamesdsp` group. A user must enable the engine or choose +“Download audio effects for offline” before the offline-completion job caches +them, including the WASM binary. -`jamesdsp-node.js` and `worklet.js` carry the integration changes described in -`VERSION.txt`. The generated C engine and `params.js` are unchanged from the -pinned revision. The Node wrapper intentionally rejects text-effect parameters -and does not serialize text, preventing Liveprog/DDC/arbitrary text payloads -from entering the v1 surface. +The adapted wrapper resolves dependencies through `Lazy.url()` and routes the +worklet through the existing AudioGraph. It rejects Liveprog, DDC and arbitrary +text effects. Impulse responses are limited to 65,536 internal frames; lower- +power devices use a 24,000-frame cap and warn before larger responses are used. -The upstream `upstream/Main/LICENSE` text says GPL-2.0 while the port repository -ships a GPL-3.0-or-later license. Both texts are retained as supplied. Confirm -the upstream provenance and redistribution position before a public release; -this note is not a legal conclusion. +The port is GPL-3.0-or-later (`LICENSE.txt`). The upstream license snapshot says +GPL-2.0; both supplied notices remain in this directory pending upstream +clarification. See VERSION.txt for the adaptation record. diff --git a/frontend/vendor/jamesdsp/VERSION.txt b/frontend/vendor/jamesdsp/VERSION.txt index ada2ffe..77a737b 100644 --- a/frontend/vendor/jamesdsp/VERSION.txt +++ b/frontend/vendor/jamesdsp/VERSION.txt @@ -1,15 +1,18 @@ -JamesDSP browser port source revision: 77be0b2e825a20ba98feff33d16dd0a43232ef3b +JamesDSP browser port source revision: a982b53c4ab347d1d9c50f4ce7ee3c2735eaa817 Repository: /home/josh/development/personal/jamesdsp-web (main) -Vendored from web/jamesdsp-node.js, web/params.js, web/worklet.js, -web/dsp/jamesdsp.js and web/dsp/jamesdsp.wasm. +Vendored source files: web/jamesdsp-node.js, params.js, ir-utils.js, +ir-builtins.js, ir-store.js, worklet.js, dsp/jamesdsp.js and dsp/jamesdsp.wasm. -Integration adaptations: manifest-hashed module URLs are selected through Lazy; -the worklet carries its engine URL in a fragment so its HTTP/CacheStorage key -remains the exact manifest URL; the wrapper and worklet do not expose text -effects (Liveprog, DDC, arbitrary EQ) in this v1 integration. Upstream source is -retained unchanged for the generated engine and parameter module. +Integration adaptations: wrapper dependencies and WASM URLs use the page's +manifest-hashed Lazy.url map; the worklet statically imports the pinned +jamesdsp-a982b53.mjs module because AudioWorkletGlobalScope rejects dynamic +import. The wrapper keeps the shared AudioGraph route and does not create a +MediaElementSource. Liveprog, DDC and arbitrary text effects are rejected and +not serialized in ytplayer. All runtime code remains in lazy feature:jamesdsp; +its offline completion membership requires the explicit jamesDsp opt-in. -License note: LICENSE.txt is the port's GPL-3.0-or-later license. The upstream -snapshot at upstream/Main/LICENSE contains GPL-2.0 text; the discrepancy is -preserved and recorded for release review. See README.md and JamesDSP's source -repository for corresponding source information. +The generated engine, parameter tables, IR utilities/store and built-in IR set +are copied from the pinned revision. The uploaded/generated IR cap is 65,536 +frames at the internal 44.1/48 kHz rate (low-power devices use 24,000 frames). +License provenance: LICENSE.txt is the port's GPL-3.0-or-later license. The +upstream snapshot at upstream/Main/LICENSE contains GPL-2.0 text; see README.md. diff --git a/frontend/vendor/jamesdsp/dsp/jamesdsp-77be0b2.mjs b/frontend/vendor/jamesdsp/dsp/jamesdsp-a982b53.mjs similarity index 78% rename from frontend/vendor/jamesdsp/dsp/jamesdsp-77be0b2.mjs rename to frontend/vendor/jamesdsp/dsp/jamesdsp-a982b53.mjs index 7de737c..468d173 100644 --- a/frontend/vendor/jamesdsp/dsp/jamesdsp-77be0b2.mjs +++ b/frontend/vendor/jamesdsp/dsp/jamesdsp-a982b53.mjs @@ -1,2 +1,2 @@ -async function createJamesDSP(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&process.versions?.node&&process.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var readyPromiseResolve,readyPromiseReject;var wasmMemory;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("jamesdsp.wasm")}return new URL("jamesdsp.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw'sync fetching of the wasm failed: you can preload it to Module["wasmBinary"] manually, or emcc.py will do that for you when generating HTML (but not JS)'}function instantiateSync(file,info){var module;var binary=getBinarySync(file);module=new WebAssembly.Module(binary);var instance=new WebAssembly.Instance(module,info);return[instance,module]}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();assignWasmExports(wasmExports);return wasmExports}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();var result=instantiateSync(wasmBinaryFile,info);return receiveInstance(result[0])}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);var ___syscall_chdir=path=>{};var SYSCALLS={varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;return 0}var ___syscall_getdents64=(fd,dirp,count)=>{};function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs}var _emscripten_get_now=()=>performance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;abortOnCannotGrowMemory(requestedSize)};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var _fd_close=fd=>52;var _fd_read=(fd,iov,iovcnt,pnum)=>52;function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var _jdsp_create,_jdsp_destroy,_free,_jdsp_process,_jdsp_faults,_jdsp_set_param,_jdsp_set_text,_jdsp_load_ir,_malloc,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current;function assignWasmExports(wasmExports){Module["_jdsp_create"]=_jdsp_create=wasmExports["jdsp_create"];Module["_jdsp_destroy"]=_jdsp_destroy=wasmExports["jdsp_destroy"];Module["_free"]=_free=wasmExports["free"];Module["_jdsp_process"]=_jdsp_process=wasmExports["jdsp_process"];Module["_jdsp_faults"]=_jdsp_faults=wasmExports["jdsp_faults"];Module["_jdsp_set_param"]=_jdsp_set_param=wasmExports["jdsp_set_param"];Module["_jdsp_set_text"]=_jdsp_set_text=wasmExports["jdsp_set_text"];Module["_jdsp_load_ir"]=_jdsp_load_ir=wasmExports["jdsp_load_ir"];Module["_malloc"]=_malloc=wasmExports["malloc"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"]}var wasmImports={__assert_fail:___assert_fail,__syscall_chdir:___syscall_chdir,__syscall_fcntl64:___syscall_fcntl64,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_openat:___syscall_openat,clock_time_get:_clock_time_get,emscripten_date_now:_emscripten_date_now,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,exit:_exit,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=createWasm();run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +async function createJamesDSP(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&process.versions?.node&&process.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var readyPromiseResolve,readyPromiseReject;var wasmMemory;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("jamesdsp.wasm")}return new URL("jamesdsp.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw'sync fetching of the wasm failed: you can preload it to Module["wasmBinary"] manually, or emcc.py will do that for you when generating HTML (but not JS)'}function instantiateSync(file,info){var module;var binary=getBinarySync(file);module=new WebAssembly.Module(binary);var instance=new WebAssembly.Instance(module,info);return[instance,module]}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];updateMemoryViews();assignWasmExports(wasmExports);return wasmExports}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();var result=instantiateSync(wasmBinaryFile,info);return receiveInstance(result[0])}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);var ___syscall_chdir=path=>{};var SYSCALLS={varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;return 0}var ___syscall_getdents64=(fd,dirp,count)=>{};function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;return 0}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs}var _emscripten_get_now=()=>performance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;abortOnCannotGrowMemory(requestedSize)};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var _fd_close=fd=>52;var _fd_read=(fd,iov,iovcnt,pnum)=>52;function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var _jdsp_create,_free,_jdsp_destroy,_jdsp_process,_jdsp_faults,_jdsp_get_info,_jdsp_set_param,_jdsp_set_text,_jdsp_load_ir_ex,_malloc,_jdsp_load_ir,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current;function assignWasmExports(wasmExports){Module["_jdsp_create"]=_jdsp_create=wasmExports["jdsp_create"];Module["_free"]=_free=wasmExports["free"];Module["_jdsp_destroy"]=_jdsp_destroy=wasmExports["jdsp_destroy"];Module["_jdsp_process"]=_jdsp_process=wasmExports["jdsp_process"];Module["_jdsp_faults"]=_jdsp_faults=wasmExports["jdsp_faults"];Module["_jdsp_get_info"]=_jdsp_get_info=wasmExports["jdsp_get_info"];Module["_jdsp_set_param"]=_jdsp_set_param=wasmExports["jdsp_set_param"];Module["_jdsp_set_text"]=_jdsp_set_text=wasmExports["jdsp_set_text"];Module["_jdsp_load_ir_ex"]=_jdsp_load_ir_ex=wasmExports["jdsp_load_ir_ex"];Module["_malloc"]=_malloc=wasmExports["malloc"];Module["_jdsp_load_ir"]=_jdsp_load_ir=wasmExports["jdsp_load_ir"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"]}var wasmImports={__assert_fail:___assert_fail,__syscall_chdir:___syscall_chdir,__syscall_fcntl64:___syscall_fcntl64,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_openat:___syscall_openat,clock_time_get:_clock_time_get,emscripten_date_now:_emscripten_date_now,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,exit:_exit,fd_close:_fd_close,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=createWasm();run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}export default createJamesDSP; diff --git a/frontend/vendor/jamesdsp/dsp/jamesdsp.wasm b/frontend/vendor/jamesdsp/dsp/jamesdsp.wasm index 884efd398ee189c49f6cda5a8b19a900a0571ca0..5999aaf1f6c27689118c8f7a5bfbbef2a5ba1946 100755 GIT binary patch delta 84084 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 z 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 M5IZ7NUDrMNKP$iV4*&oF delta 78364 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 m)R7E#^3{e6t{VDrQ6|qXH>amaf||UY&tr9))7[name,Object.freeze({...d,version:1})]))); +export function generateIR(name,sampleRate,channels=2,{maxFrames=65536}={}) { + const spec=BUILTIN_IRS[name];if(!spec||!Number.isInteger(sampleRate)||sampleRate<8000||sampleRate>96000||![1,2,4].includes(channels)) throw new RangeError('Unsupported generated IR'); + const frames=Math.max(1,Math.min(Math.round(spec.seconds*sampleRate),maxFrames));let seed=spec.seed>>>0; + const paths=Array.from({length:channels},()=>new Float32Array(frames)); + const next=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000*2-1;}; + for(let path=0;pathpaths[i]}; +} diff --git a/frontend/vendor/jamesdsp/ir-store.mjs b/frontend/vendor/jamesdsp/ir-store.mjs new file mode 100644 index 0000000..ab43958 --- /dev/null +++ b/frontend/vendor/jamesdsp/ir-store.mjs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +const DB='jamesdsp-ir', STORE='irs', VERSION=1; +function openDB() { + if(!globalThis.indexedDB) return Promise.reject(new Error('IndexedDB is unavailable')); + return new Promise((resolve,reject)=>{const request=indexedDB.open(DB,VERSION);request.onupgradeneeded=()=>request.result.createObjectStore(STORE,{keyPath:'name'});request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error);}); +} +async function transaction(mode,action) { + const db=await openDB();return new Promise((resolve,reject)=>{const tx=db.transaction(STORE,mode),store=tx.objectStore(STORE);let result;try {result=action(store);} catch(error){db.close();reject(error);return;}tx.oncomplete=()=>{db.close();resolve(result);};tx.onerror=()=>{db.close();reject(tx.error);};tx.onabort=()=>{db.close();reject(tx.error||new Error('IR storage transaction aborted'));};}); +} +async function digest(data) {const hash=await crypto.subtle.digest('SHA-256',data);return [...new Uint8Array(hash)].map(x=>x.toString(16).padStart(2,'0')).join('');} +export async function saveIR(name,buffer,metadata={}) { + if(typeof name!=='string'||!name.trim()||name.length>100||![1,2,4].includes(buffer?.numberOfChannels)||!Number.isFinite(buffer.sampleRate)||buffer.sampleRate<8000||buffer.sampleRate>96000||!buffer.length) throw new TypeError('Invalid named impulse response'); + const internalRate=buffer.sampleRate===44100?44100:48000; + if(Math.round(buffer.length*internalRate/buffer.sampleRate)>65536) throw new RangeError('Stored impulse response exceeds the 65,536-frame internal limit'); + const channels=Array.from({length:buffer.numberOfChannels},(_,i)=>new Float32Array(buffer.getChannelData(i))); + const pcm=new Float32Array(channels.length*buffer.length);for(let i=0;is.put(record));return {name:record.name,hash:record.hash}; +} +export async function getIR(name) { + const db=await openDB();return new Promise((resolve,reject)=>{const r=db.transaction(STORE,'readonly').objectStore(STORE).get(name);r.onsuccess=async()=>{try {const x=r.result;if(!x){resolve(null);return;}if(await digest(x.pcm.buffer)!==x.hash)throw new Error('Stored IR hash mismatch');const channels=Array.from({length:x.channels},()=>new Float32Array(x.frames));for(let i=0;ichannels[i]}});}catch(e){reject(e);}finally{db.close();}};r.onerror=()=>{db.close();reject(r.error);};}); +} +export async function listIRs() {const db=await openDB();return new Promise((resolve,reject)=>{const r=db.transaction(STORE,'readonly').objectStore(STORE).getAll();r.onsuccess=()=>{db.close();resolve(r.result.map(({name,hash,sampleRate,channels,frames,metadata})=>({name,hash,sampleRate,channels,frames,metadata})));};r.onerror=()=>{db.close();reject(r.error);};});} +export async function deleteIR(name) {await transaction('readwrite',s=>s.delete(name));} diff --git a/frontend/vendor/jamesdsp/ir-utils.mjs b/frontend/vendor/jamesdsp/ir-utils.mjs new file mode 100644 index 0000000..31bb834 --- /dev/null +++ b/frontend/vendor/jamesdsp/ir-utils.mjs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +const copyBuffer=(buffer,rate=buffer.sampleRate)=>{ + const channels=Array.from({length:buffer.numberOfChannels},(_,i)=>new Float32Array(buffer.getChannelData(i))); + return {sampleRate:rate,numberOfChannels:channels.length,length:channels[0]?.length||0,getChannelData:i=>channels[i]}; +}; +export async function decodeIR(context,bytes) { + const decoded=await context.decodeAudioData(bytes.slice(0)); + return resampleIR(decoded,context.sampleRate); +} +export function resampleIR(buffer,targetRate) { + if(!Number.isFinite(targetRate)||targetRate<8000||targetRate>96000||!buffer?.numberOfChannels||buffer.numberOfChannels>4||!buffer.length) throw new RangeError('Invalid IR or target sample rate'); + if(buffer.sampleRate===targetRate) return copyBuffer(buffer,targetRate); + const length=Math.max(1,Math.round(buffer.length*targetRate/buffer.sampleRate)); + const channels=[]; + for(let ch=0;chchannels[i]}; +} +export function trimIR(buffer,{thresholdDb=-80}={}) { + if(!Number.isFinite(thresholdDb)||thresholdDb>0||thresholdDb<-160) throw new RangeError('Invalid trim threshold'); + const channels=Array.from({length:buffer.numberOfChannels},(_,i)=>buffer.getChannelData(i)); + const threshold=10**(thresholdDb/20);let first=buffer.length,last=-1; + for(let i=0;i=threshold){first=Math.min(first,i);last=i;}} + if(last<0) first=last=0; + const result=channels.map(channel=>channel.slice(first,last+1)); + return {buffer:{sampleRate:buffer.sampleRate,numberOfChannels:result.length,length:result[0]?.length||0,getChannelData:i=>result[i]},startFrame:first,endFrame:last+1}; +} +export function normalizeIR(buffer,{peak=0.5}={}) { + if(!Number.isFinite(peak)||peak<=0||peak>1) throw new RangeError('Peak must be in (0, 1]'); + const channels=Array.from({length:buffer.numberOfChannels},(_,i)=>new Float32Array(buffer.getChannelData(i))); + let max=0;for(const channel of channels) for(const value of channel) max=Math.max(max,Math.abs(value)); + const gain=max===0?1:peak/max;for(const channel of channels) for(let i=0;ichannels[i]}; +} +export function prepareIR(context,buffer,{trim=false,normalize=true,peak=0.5,thresholdDb=-80}={}) { + let result=resampleIR(buffer,context.sampleRate); + if(trim) result=trimIR(result,{thresholdDb}).buffer; + if(normalize) result=normalizeIR(result,{peak}); + return result; +} diff --git a/frontend/vendor/jamesdsp/jamesdsp-feature.js b/frontend/vendor/jamesdsp/jamesdsp-feature.js index b9d0df7..15ff6e4 100644 --- a/frontend/vendor/jamesdsp/jamesdsp-feature.js +++ b/frontend/vendor/jamesdsp/jamesdsp-feature.js @@ -2,6 +2,13 @@ (function (root) { 'use strict'; + let irToolsPromise; + const irTools = () => irToolsPromise ||= Promise.all([ + import(root.Lazy.url('/vendor/jamesdsp/ir-utils.mjs')), + import(root.Lazy.url('/vendor/jamesdsp/ir-builtins.mjs')), + import(root.Lazy.url('/vendor/jamesdsp/ir-store.mjs')), + ]).then(([utils,builtins,store])=>({utils,builtins,store})); + const PRESETS = Object.freeze({ flat: { name: 'Flat', params: {} }, bass: { name: 'Bass', params: { 'eq.enabled': 1, 'eq.band0': 4, 'eq.band1': 3, 'bass.enabled': 1, 'bass.boost': 5, gain: -3 } }, @@ -18,7 +25,7 @@ const graph = options.graph; let desired = false, active = false, bypassed = false, nodeEntry = null, gainNode = null, context = null; let elements = [], standardInput = null, levelDb = 0, settings = null; - let tail = Promise.resolve(), maxLiveHeaps = 0, liveHeaps = 0, nodesCreated = 0, nodesDestroyed = 0; + let tail = Promise.resolve(), maxLiveHeaps = 0, liveHeaps = 0, nodesCreated = 0, nodesDestroyed = 0, loadedIRRef = null; let faultListener = null, notifying = false, dspAnalyser = null; const serial = fn => { @@ -27,7 +34,7 @@ return result; }; - function snapshot() { return { active, desired, bypassed, liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null }; } + function snapshot() { return { active, desired, bypassed, route: active && !bypassed ? 'jamesdsp' : 'standard', liveHeaps, maxLiveHeaps, nodesCreated, nodesDestroyed, sampleRate: context?.sampleRate || null, irRef: loadedIRRef }; } function routeStandard() { if (!elements.length) return; @@ -51,6 +58,7 @@ dspAnalyser = null; if (entry) { try { entry.node.destroy(); } catch { /* continue freeing references */ } + loadedIRRef = null; if (entry.live) { entry.live = false; liveHeaps = Math.max(0, liveHeaps - 1); nodesDestroyed++; } } } @@ -84,6 +92,7 @@ async function configure(instance, nextSettings, nextLevelDb) { const preset = PRESETS[nextSettings?.preset] || PRESETS.flat; const params = { ...preset.params, ...(nextSettings?.params || {}) }; + if (Number(params['spatial.enabled']) && Number(params['crossfeed.enabled'])) params['crossfeed.enabled'] = 0; params.gain = clampGain((Number(params.gain) || 0) + (Number(nextLevelDb) || 0)); params.bypass = 1; if (active && context?.state === 'running') { @@ -91,7 +100,10 @@ await new Promise(resolve => setTimeout(resolve, 20)); } await instance.setBypass(true); - await instance.applyPreset(params); + const ref = nextSettings?.irRef; + const needsIR = ref && (loadedIRRef?.name !== ref.name || loadedIRRef?.hash !== ref.hash); + await instance.applyPreset(needsIR ? { params, irRef: ref } : params); + if (needsIR) loadedIRRef = { name: ref.name, hash: ref.hash }; await instance.setParam('gain', params.gain); if (desired) await instance.setBypass(bypassed); if (desired) rampGain(1); @@ -117,7 +129,7 @@ context = prepared.context; elements = prepared.elements; standardInput = prepared.standardInput; - if (![44100, 48000].includes(context?.sampleRate)) throw new RangeError(`JamesDSP supports 44.1 or 48 kHz; this device uses ${context?.sampleRate || 'unknown'} Hz`); + if (!Number.isFinite(context?.sampleRate) || context.sampleRate < 8000 || context.sampleRate > 96000) throw new RangeError(`JamesDSP supports 8–96 kHz; this device uses ${context?.sampleRate || 'unknown'} Hz`); if (!desired) return false; let instance = nodeEntry?.node; if (!instance) { @@ -136,7 +148,7 @@ await configure(instance, settings, levelDb); if (!desired) { releaseNode(); return false; } if (context.state === 'suspended') await context.resume().catch(() => {}); - graph.routeAll(elements, instance.node); + graph.routeAll(elements, bypassed ? standardInput : instance.node); active = true; return true; }).catch(error => { fail(error); throw error; }); @@ -158,22 +170,35 @@ return enable(settings, levelDb); } - function setBypass(value) { + function changeBypass(value) { if (!nodeEntry) return Promise.resolve(false); - return serial(async () => { bypassed = Boolean(value); await nodeEntry.node.setBypass(bypassed); return bypassed; }).catch(error => { fail(error); throw error; }); + return serial(async () => { + const next = Boolean(value); + if (next === bypassed) return bypassed; + // A/B bypass is an exact route around the worklet. This preserves the + // Standard graph sample-for-sample instead of relying on DSP bypass math. + if (!next) await nodeEntry.node.setBypass(false); + if (active) { + rampGain(0); + await new Promise(resolve => setTimeout(resolve, 20)); + graph.routeAll(elements, next ? standardInput : nodeEntry.node); + rampGain(next ? 0 : 1); + } + if (next) await nodeEntry.node.setBypass(true); + bypassed = next; + return bypassed; + }).catch(error => { fail(error); throw error; }); } - function toggleBypass() { - if (!nodeEntry) return Promise.resolve(false); - return serial(async () => { bypassed = !bypassed; await nodeEntry.node.setBypass(bypassed); return bypassed; }).catch(error => { fail(error); throw error; }); - } + function setBypass(value) { return changeBypass(value); } + function toggleBypass() { return changeBypass(!bypassed); } - function loadIR(buffer) { + function loadIR(buffer, reference = null) { if (!active || !nodeEntry) return Promise.reject(new Error('Enable JamesDSP before loading an impulse response')); return serial(async () => { if (!active || !nodeEntry) throw new Error('JamesDSP is no longer active'); rampGain(0); - try { await nodeEntry.node.loadIR(buffer); await nodeEntry.node.setParam('convolver.enabled', 1); } + try { await nodeEntry.node.loadIR(buffer, reference); loadedIRRef = reference ? { name: reference.name, hash: reference.hash } : null; await nodeEntry.node.setParam('convolver.enabled', 1); } finally { if (active && desired) rampGain(1); } return true; }).catch(error => { fail(error); throw error; }); @@ -195,7 +220,24 @@ return { enable, disable, setLevelDb, setBypass, toggleBypass, loadIR, analyser, get active() { return active; }, get desired() { return desired; }, state: snapshot }; } - const api = { PRESETS, createController }; + async function generateIR(name, sampleRate, channels = 2, maxFrames = 65536) { + const {builtins}=await irTools(); + return builtins.generateIR(name,sampleRate,channels,{maxFrames}); + } + async function decodeIR(context, bytes) { const {utils}=await irTools();return utils.decodeIR(context,bytes); } + async function prepareIR(context, buffer, maxFrames = 65536) { + const {utils}=await irTools(); + const rate=context.sampleRate===44100?44100:48000; + const prepared=utils.resampleIR(utils.prepareIR(context,buffer,{normalize:false}),rate); + if (![1,2,4].includes(prepared.numberOfChannels) || prepared.length<1 || prepared.length>Math.min(65536,maxFrames)) throw new RangeError(`Impulse response exceeds the ${Math.min(65536,maxFrames)}-frame internal limit`); + return prepared; + } + async function saveIR(name, buffer, metadata) { const {store}=await irTools();return store.saveIR(name,buffer,metadata); } + async function getIR(name) { const {store}=await irTools();return store.getIR(name); } + async function listIRs() { const {store}=await irTools();return store.listIRs(); } + async function deleteIR(name) { const {store}=await irTools();return store.deleteIR(name); } + + const api = { PRESETS, createController, generateIR, decodeIR, prepareIR, saveIR, getIR, listIRs, deleteIR }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else { root.YT = root.YT || {}; diff --git a/frontend/vendor/jamesdsp/jamesdsp-node.mjs b/frontend/vendor/jamesdsp/jamesdsp-node.mjs index addbc3b..8371149 100644 --- a/frontend/vendor/jamesdsp/jamesdsp-node.mjs +++ b/frontend/vendor/jamesdsp/jamesdsp-node.mjs @@ -1,27 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-or-later +function assetURL(path, fallback) { return globalThis.Lazy?.url?.(path) || new URL(fallback, import.meta.url); } +const [parameterAPI, irUtils, irStore] = await Promise.all([ + import(assetURL('/vendor/jamesdsp/params.mjs', './params.mjs')), + import(assetURL('/vendor/jamesdsp/ir-utils.mjs', './ir-utils.mjs')), + import(assetURL('/vendor/jamesdsp/ir-store.mjs', './ir-store.mjs')), +]); +const {DEFAULT_PARAMS, validateParam} = parameterAPI; +const {prepareIR,resampleIR} = irUtils; +const {getIR} = irStore; const moduleLoads = new WeakMap(); -let paramsLoad; -function assetURL(path, relative) { return globalThis.Lazy?.url?.(path) || new URL(relative, import.meta.url); } -function loadParams(paramsURL) { if (!paramsLoad) paramsLoad = import(paramsURL); return paramsLoad; } export class JamesDSPNode extends EventTarget { - static async create(context, options={}) { - if (![44100,48000].includes(context.sampleRate)) throw new RangeError('Use a 44.1 or 48 kHz AudioContext'); - const wasmURL=options.wasmURL || assetURL('/vendor/jamesdsp/dsp/jamesdsp.wasm','./dsp/jamesdsp.wasm'); - const paramsURL=options.paramsURL || assetURL('/vendor/jamesdsp/params.mjs','./params.mjs'); - const baseWorkletURL=options.workletURL || assetURL('/vendor/jamesdsp/worklet.mjs','./worklet.mjs'); - const paramApi=await loadParams(paramsURL); + static async create(context, {wasmURL=assetURL('/vendor/jamesdsp/dsp/jamesdsp.wasm','./dsp/jamesdsp.wasm'), workletURL=assetURL('/vendor/jamesdsp/worklet.mjs','./worklet.mjs')}={}) { + if (!Number.isFinite(context.sampleRate)||context.sampleRate<8000||context.sampleRate>96000) throw new RangeError('AudioContext rate must be 8–96 kHz'); if (!moduleLoads.has(context)) moduleLoads.set(context,new Map()); - const cache=moduleLoads.get(context), key=String(baseWorkletURL); - if (!cache.has(key)) cache.set(key,context.audioWorklet.addModule(baseWorkletURL).catch(e=>{cache.delete(key);throw e;})); + const cache=moduleLoads.get(context), key=String(workletURL); + if (!cache.has(key)) cache.set(key,context.audioWorklet.addModule(workletURL).catch(e=>{cache.delete(key);throw e;})); const response=await fetch(wasmURL); if (!response.ok) throw new Error(`WASM fetch failed: ${response.status}`); const wasmBytes=await response.arrayBuffer(); await cache.get(key); - const wrapper=new JamesDSPNode(context,wasmBytes,paramApi); + const wrapper=new JamesDSPNode(context,wasmBytes); try { await wrapper.ready; return wrapper; } catch(e) { wrapper.destroy(); throw e; } } - constructor(context,wasmBytes,paramApi) { - super(); this.context=context; this.defaultParams=Object.fromEntries(Object.entries(paramApi.DEFAULT_PARAMS).filter(([name])=>!/^(?:liveprog|ddc|arbitrary)\./.test(name))); this.validateParam=paramApi.validateParam; this.params={...this.defaultParams}; this.pending=new Map(); this.serial=0; this.destroyed=false; + constructor(context,wasmBytes) { + super(); this.context=context; this.defaultParams=Object.fromEntries(Object.entries(DEFAULT_PARAMS).filter(([name])=>!/^(?:liveprog|ddc|arbitrary)\./.test(name))); this.params={...this.defaultParams}; this.pending=new Map(); this.serial=0; this.destroyed=false; this.source=null; this.irRef=null; this.node=new AudioWorkletNode(context,'jamesdsp',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2],channelCount:2,channelCountMode:'explicit',processorOptions:{wasmBytes}}); this.ready=new Promise((resolve,reject)=> { const timer=setTimeout(()=>reject(new Error('DSP initialization timed out')),15000); @@ -35,44 +37,48 @@ export class JamesDSPNode extends EventTarget { }); } fail(error) { for(const p of this.pending.values()) {clearTimeout(p.timer);p.reject(error);} this.pending.clear();this.dispatchEvent(new CustomEvent('error',{detail:error})); } - request(type,data={}) { + request(type,data={},transfer=[]) { if(this.destroyed) return Promise.reject(new Error('DSP has been destroyed')); const id=++this.serial; return new Promise((resolve,reject)=> { const timer=setTimeout(()=>{this.pending.delete(id);reject(new Error(`DSP ${type} timed out`));},15000); - this.pending.set(id,{resolve,reject,timer});this.node.port.postMessage({type,id,...data}); + this.pending.set(id,{resolve,reject,timer});this.node.port.postMessage({type,id,...data},transfer); }); } - // Routing is owned by AudioGraph. Expose the worklet node without creating a source. - async setParam(name,value) { - if (/^(?:liveprog|ddc|arbitrary)\./.test(name)) throw new Error('Text effects are disabled in this player version'); - this.validateParam(name,value);await this.request('param',{name,value});this.params[name]=value; + // One source → this DSP → destination. For arbitrary graphs use .node directly. + connect(source,destination=this.context.destination) { + if(this.destroyed) throw new Error('DSP has been destroyed'); + if(this.source) this.source.disconnect(this.node); + this.node.disconnect();this.source=source;source.connect(this.node);this.node.connect(destination);return destination; } + async setParam(name,value) {if(/^(?:liveprog|ddc|arbitrary)\./.test(name))throw new Error('Text effects are disabled in this player version');validateParam(name,value);await this.request('param',{name,value});this.params[name]=value;} setBypass(enabled) {return this.setParam('bypass',Number(Boolean(enabled)));} - async setText() { + async setText(effect,text) { throw new Error('Text effects are disabled in this player version'); } - async loadIR(buffer) { - if(buffer.sampleRate!==this.context.sampleRate || ![1,2].includes(buffer.numberOfChannels) || buffer.length>65536 || buffer.length<1) throw new RangeError('IR must be mono/stereo, at context rate, at most 65536 frames'); + async loadIR(buffer,reference=null) { + buffer=prepareIR(this.context,buffer,{normalize:false}); + const internalRate=this.context.sampleRate===44100?44100:48000; + buffer=resampleIR(buffer,internalRate); + if(![1,2,4].includes(buffer.numberOfChannels) || buffer.length>65536 || buffer.length<1) throw new RangeError('IR must have 1, 2, or 4 channels and at most 65536 frames'); const pcm=new Float32Array(buffer.length*buffer.numberOfChannels); for(let ch=0;chNumber(a.endsWith('.enabled'))-Number(b.endsWith('.enabled')))) await this.setParam(name,value); } - toJSON() {return {version:1,params:{...this.params}};} + toJSON() {return {version:2,params:{...this.params},...(this.irRef?{irRef:{...this.irRef}}:{})};} destroy() { if(this.destroyed) return; - this.node.disconnect(); + if(this.source) {try {this.source.disconnect(this.node);} catch {}} this.node.disconnect(); this.node.port.postMessage({type:'destroy'});this.destroyed=true;this.fail(new Error('DSP destroyed'));this.node.port.close(); } } diff --git a/frontend/vendor/jamesdsp/params.mjs b/frontend/vendor/jamesdsp/params.mjs index 664238e..e0e14c2 100644 --- a/frontend/vendor/jamesdsp/params.mjs +++ b/frontend/vendor/jamesdsp/params.mjs @@ -1,14 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later export const EQ_FREQUENCIES = [25,40,63,100,160,250,400,630,1000,1600,2500,4000,6300,10000,16000]; export const COMP_FREQUENCIES = [95,200,400,800,1600,3400,7500]; -export const EFFECTS = ['eq','compressor','bass','stereo','reverb','crossfeed','tube','convolver','liveprog','ddc','arbitrary']; +export const EFFECTS = ['eq','compressor','bass','stereo','reverb','crossfeed','spatial','tube','convolver','liveprog','ddc','arbitrary']; export const PARAMS = { bypass:[0,1,0], gain:[-24,12,0], 'limiter.threshold':[-24,0,-0.1], 'limiter.release':[10,1000,100], 'eq.mode':[0,5,1], 'eq.interpolation':[0,1,0], 'compressor.time':[0.01,2,0.1], 'compressor.mode':[0,3,3], 'compressor.granularity':[0,4,0], 'bass.boost':[0,12,6], 'stereo.mix':[0,1,0.5], 'reverb.preset':[0,18,0], + 'convolver.mix':[0,1,1], 'convolver.predelayMs':[0,250,0], 'crossfeed.mode':[0,5,0], 'tube.drive':[-12,12,0], + 'spatial.mode':[2,6,2], 'spatial.azimuth':[-90,90,0], 'spatial.spread':[0,120,60], + 'spatial.distance':[0.5,5,1], 'spatial.room':[0,1,0], 'spatial.centerDb':[0,6,0], 'spatial.mix':[0,1,1], }; for (const effect of EFFECTS) PARAMS[`${effect}.enabled`] = [0,1,0]; EQ_FREQUENCIES.forEach((_, i) => PARAMS[`eq.band${i}`] = [-12,12,0]); @@ -24,4 +27,5 @@ export const PRESETS = { 'Warm lows': {'eq.band0':4,'eq.band1':4,'eq.band2':3,'eq.band3':2,'eq.enabled':1,'gain':-3}, 'Headphones': {'crossfeed.enabled':1,'crossfeed.mode':1,'eq.band8':-2,'eq.enabled':1}, 'Small room': {'reverb.preset':7,'reverb.enabled':1,'gain':-3}, + 'Virtual speakers': {'spatial.enabled':1,'spatial.mode':6,'spatial.spread':90}, }; diff --git a/frontend/vendor/jamesdsp/worklet.mjs b/frontend/vendor/jamesdsp/worklet.mjs index 18511e3..506261b 100644 --- a/frontend/vendor/jamesdsp/worklet.mjs +++ b/frontend/vendor/jamesdsp/worklet.mjs @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// AudioWorkletGlobalScope does not support dynamic import; this immutable -// vendor path is versioned with the upstream pin and remapped by the SW cache. -import createJamesDSP from './dsp/jamesdsp-77be0b2.mjs'; +import createJamesDSP from './dsp/jamesdsp-a982b53.mjs'; class JamesDSPProcessor extends AudioWorkletProcessor { constructor(options) { super(); this.closed=false;this.failed=false; @@ -31,7 +29,10 @@ class JamesDSPProcessor extends AudioWorkletProcessor { if(data.type==='param') result=this.string(data.name,p=>this.wasm._jdsp_set_param(this.handle,p,data.value)); else if(data.type==='ir') { const p=this.wasm._malloc(data.pcm.byteLength);if(!p) throw new Error('IR allocation failed'); - try {this.wasm.HEAPF32.set(data.pcm,p/4);result=this.wasm._jdsp_load_ir(this.handle,p,data.frames,data.channels);} finally {this.wasm._free(p);} + try {this.wasm.HEAPF32.set(data.pcm,p/4);result=this.wasm._jdsp_load_ir_ex(this.handle,p,data.frames,data.channels,data.sourceRate);} finally {this.wasm._free(p);} + } else if(data.type==='info') { + const p=this.wasm._malloc(64);if(!p)throw new Error('DSP info allocation failed'); + try {result=this.wasm._jdsp_get_info(this.handle,p);if(result===0){const bytes=this.wasm.HEAPU8.slice(p,p+64);this.port.postMessage({type:'reply',id:data.id,result:{bytes:bytes.buffer,heapBytes:this.wasm.HEAPU8.buffer.byteLength}},[bytes.buffer]);return;}} finally {this.wasm._free(p);} } else throw new Error('Unknown DSP message'); if(result!==0) throw new Error(`DSP rejected ${data.name||data.effect||data.type}`); this.port.postMessage({type:'reply',id:data.id,result}); diff --git a/frontend/views-core.js b/frontend/views-core.js index 753a67f..7d0936b 100644 --- a/frontend/views-core.js +++ b/frontend/views-core.js @@ -732,9 +732,11 @@ async function renderSettings() { const qOptions = qualities .map((q) => ``) .join(''); - const jamesDsp = data.settings.jamesDsp || { enabled: false, preset: 'flat', params: {} }; + const jamesDsp = data.settings.jamesDsp || { enabled: false, downloadForOffline: false, preset: 'flat', params: {}, irRef: null }; + const dspCores=Number(navigator.hardwareConcurrency), dspMemory=Number(navigator.deviceMemory); + const dspLowEnd=(dspCores>0&&dspCores<=4)||(dspMemory>0&&dspMemory<=4); const dspBands = [25, 40, 63, 100, 160, 250, 400, 630, 1000, 1600, 2500, 4000, 6300, 10000, 16000]; - const dspParamDefaults = { 'bass.boost': 6, 'stereo.mix': 0.5, 'limiter.threshold': -0.1 }; + const dspParamDefaults = { 'bass.boost': 6, 'stereo.mix': 0.5, 'limiter.threshold': -0.1, 'convolver.mix': 1, 'convolver.predelayMs': 0, 'spatial.mode': 6, 'spatial.spread': 60, 'spatial.distance': 1, 'spatial.mix': 1 }; const dspParam = key => Number.isFinite(Number(jamesDsp.params?.[key])) ? Number(jamesDsp.params[key]) : (dspParamDefaults[key] ?? 0); const dspRange = (key, label, min, max, step = 1, unit = '') => ``; const dspBandRows = dspBands.map((hz, i) => dspRange(`eq.band${i}`, `${hz >= 1000 ? (hz / 1000) + ' kHz' : hz + ' Hz'}`, -12, 12, 0.5, ' dB')).join(''); @@ -742,6 +744,7 @@ async function renderSettings() { const sel = (id, val, opts) => ``; + const dspBuiltinIRSelect = ``; wrap.innerHTML = `
@@ -813,8 +816,10 @@ async function renderSettings() { +
-
Live processing${EQ.isIOS() ? 'On iPhone, enable Live sound processing above first. Audio may stop when the screen locks.' : 'Processing runs on this device. Large reverbs and convolution can use more CPU.'}
+
Live processing${EQ.isIOS() ? 'On iPhone, enable Live sound processing above first. Audio may stop when the screen locks.' : 'Processing runs on this device.'}
+ ${dspLowEnd ? '

This device may have limited audio headroom. Long impulse responses are disabled and the short room response is capped at 0.5 seconds.

' : ''}
${dspBandRows}
@@ -827,7 +832,27 @@ async function renderSettings() {
- +
+

Convolver

+ + + + + + + + +

${jamesDsp.irRef?.name ? `Loaded: ${jamesDsp.irRef.name}` : 'No impulse response loaded.'}

+
+
+

3D spatial stage

+ + + ${dspRange('spatial.azimuth','Azimuth',-90,90,1,'°')} + ${dspRange('spatial.spread','Speaker spread',0,120,1,'°')} + ${dspRange('spatial.distance','Distance',0.5,5,0.1,' m')} + ${dspRange('spatial.centerDb','Center boost',0,6,0.1,' dB')} +