Extend JamesDSP with convolution and spatial audio

This commit is contained in:
Jonathan Sykes
2026-10-10 19:25:06 +08:00
parent b3f0525a8e
commit ef12468a99
28 changed files with 540 additions and 143 deletions

View File

@@ -49,7 +49,7 @@ async function run(name){
await context.route('**/*',route=>route.request().url().startsWith(origin)?route.continue():route.abort());
const page=await context.newPage();page.on('pageerror',e=>result.errors.push(e.message));
const boot=async()=>{await page.goto(origin);await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#sidebarToggle'));await page.waitForTimeout(800);};
const settings=()=>page.evaluate(()=>JSON.parse(localStorage.getItem('_ytpdata')).settings);
const settings=()=>page.evaluate(()=>JSON.parse(JSON.stringify(data.settings)));
const openSettings=async()=>{await page.locator('[data-view="settings"]').first().evaluate(e=>e.click());await page.waitForSelector('#setSaveBeforePlay',{state:'attached'});};
const readUI=()=>page.evaluate(ids=>Object.fromEntries(ids.map(id=>{const e=document.getElementById(id);return[id,e.type==='checkbox'?e.checked:e.value];})),Object.values(ui).map(x=>x[0]));
await boot();await page.waitForFunction(()=>!!navigator.serviceWorker.controller);
@@ -67,13 +67,15 @@ async function run(name){
await page.waitForFunction(()=>!document.getElementById('modal').classList.contains('hidden')&&/Update available/.test(document.getElementById('modalTitle').textContent));
await page.locator('#modal').getByRole('button',{name:'Refresh UI',exact:true}).evaluate(e=>e.click());await page.waitForFunction(tag=>document.querySelector('meta[name="ytp-build"]')?.content===tag,result.newTag,{timeout:90000});await sleep(1000);await openSettings();
console.log(name,'real Refresh UI completed');result.upgraded=await settings();result.newRaw=await page.evaluate(()=>localStorage.getItem('_ytpdata'));result.newUI=await readUI();assert.deepEqual(result.newUI,result.oldUI,'checkbox/select values preserved');
const {migrate}=require(join(scratch,'new/frontend/eq-parameters.js'));const expected=structuredClone(result.old);if(expected.eq?.g?.length===5)expected.eq.g=migrate(expected.eq.g);
const {migrate}=require(join(scratch,'new/frontend/eq-parameters.js'));const expected=structuredClone(result.old);if(expected.eq?.g?.length===5)expected.eq.g=migrate(expected.eq.g);expected.jamesDsp={enabled:false,downloadForOffline:false,preset:'flat',params:{},irRef:null,...(expected.jamesDsp||{}),params:expected.jamesDsp?.params||{}};
for(const key of new Set([...Object.keys(result.old),...Object.keys(result.upgraded)]))result.diff.push({key,before:result.old[key],after:result.upgraded[key],equal:JSON.stringify(result.old[key])===JSON.stringify(result.upgraded[key]),expectedMigration:key==='eq'&&result.old.eq?.g?.length===5});
assert.deepEqual(result.upgraded,expected,'all persisted settings preserved except deliberate 5→10 EQ migration');await behaviour('upgraded');
await openSettings();await page.locator('[data-section="playback"]').evaluate(e=>e.click());await page.locator('#setDspPreset').evaluate(e=>{e.value='warm';e.dispatchEvent(new Event('change',{bubbles:true}));});
for(const [key,value] of [['eq.band0','3'],['tube.drive','1.5']])await page.locator(`[data-dsp-param="${key}"]`).evaluate((e,v)=>{e.value=v;e.dispatchEvent(new Event('input',{bubbles:true}));},value);
await sleep(600);await page.reload();await page.waitForFunction(()=>typeof data!=='undefined');await openSettings();await page.locator('[data-section="playback"]').evaluate(e=>e.click());
const james=await page.evaluate(()=>data.settings.jamesDsp);assert.deepEqual(james,{enabled:false,preset:'warm',params:{'eq.band0':3,'tube.drive':1.5}});assert.equal(await page.locator('#setSoundEngine').inputValue(),'standard');assert.equal(await page.locator('#setDspPreset').inputValue(),'warm');result.jamesDspSettingsPersisted=true;
const james=await page.evaluate(()=>data.settings.jamesDsp);assert.deepEqual(james,{enabled:false,downloadForOffline:false,preset:'warm',params:{'eq.band0':3,'tube.drive':1.5},irRef:null});assert.equal(await page.locator('#setSoundEngine').inputValue(),'standard');assert.equal(await page.locator('#setDspPreset').inputValue(),'warm');
await page.locator('#setDspOffline').check();const wasmReady=await poll(()=>page.evaluate(async()=>{const m=JSON.parse(document.getElementById('ytp-assets').textContent),c=await caches.open('ytplayer-assets');return !!await c.match('/vendor/jamesdsp/dsp/jamesdsp.wasm?v='+m.files['/vendor/jamesdsp/dsp/jamesdsp.wasm'].h);}));assert.equal(wasmReady,true,'offline opt-in eventually caches the optional WASM');
await page.reload();await page.waitForFunction(()=>typeof data!=='undefined');await openSettings();await page.locator('[data-section="playback"]').evaluate(e=>e.click());assert.equal(await page.locator('#setDspOffline').isChecked(),true);result.jamesDspSettingsPersisted=true;
await page.evaluate(()=>{data.settings.jamesDsp.enabled=false;persist();});
const local=await settings(),remote={...local,saveBeforePlay:false,autoPreload:false,layout:'classic',quality:'auto'};
async function profileCheck(updatedAt,expectedSave,stage){profileReply={ok:true,name:'probe',updatedAt,data:{settings:remote,playlists:[]}};await page.evaluate(s=>{data.settings=s;data.profile={name:'probe',syncedAt:200};localStorage.setItem('_ytpdata',JSON.stringify(data));},local);await boot();await page.waitForFunction(()=>_syncBusy===0);await openSettings();const s=await settings();assert.equal(s.saveBeforePlay,expectedSave);assert.equal(await page.locator('#setSaveBeforePlay').isChecked(),expectedSave);if(!expectedSave)assert.equal(s.layout,'classic');else assert.deepEqual(s,local);result.profileChecks.push({stage,serverUpdatedAt:updatedAt,localSyncedAt:200,saveBeforePlay:s.saveBeforePlay});}