Add the guarded JamesDSP audio controller
This commit is contained in:
@@ -663,6 +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: {} },
|
||||
parallelSaves: 4, // offline saves running at once (Settings → Offline cache)
|
||||
density: 'comfortable', // 'comfortable' | 'compact'
|
||||
perfMode: false, // disable heavy visual effects for speed
|
||||
@@ -672,7 +673,16 @@ const DEFAULT_SETTINGS = {
|
||||
autoBackupIntervalDays: 7,
|
||||
serviceVideo: false, // show the video panel inside service mode (lyric videos)
|
||||
};
|
||||
let data = { playlists: [], history: [], queue: [], customVideos: [], pinnedPlaylists: [], settings: { ...DEFAULT_SETTINGS }, resumePositions: {}, rememberPos: {}, playCount: {}, abMarkers: {}, bookmarks: {}, lyricOffsets: {}, stats: null, lastAutoBackup: 0, profile: null };
|
||||
function normalizeJamesDspSettings(settings) {
|
||||
const saved = settings?.jamesDsp && typeof settings.jamesDsp === 'object' ? settings.jamesDsp : {};
|
||||
settings.jamesDsp = {
|
||||
enabled: saved.enabled === true,
|
||||
preset: typeof saved.preset === 'string' ? saved.preset : 'flat',
|
||||
params: saved.params && typeof saved.params === 'object' && !Array.isArray(saved.params) ? { ...saved.params } : {},
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
let data = { playlists: [], history: [], queue: [], customVideos: [], pinnedPlaylists: [], settings: normalizeJamesDspSettings({ ...DEFAULT_SETTINGS }), resumePositions: {}, rememberPos: {}, playCount: {}, abMarkers: {}, bookmarks: {}, lyricOffsets: {}, stats: null, lastAutoBackup: 0, profile: null };
|
||||
let view = { type: 'search' }; // 'search'|'history'|'playlist'|'settings'|'queue'|'saved'|'downloads'|'channel'
|
||||
let searchResults = [];
|
||||
// Distinguishes "never searched" (hide the list pane, show the landing) from
|
||||
@@ -695,6 +705,8 @@ const IS_IOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platfo
|
||||
let _currentEqUrl = null;
|
||||
async function useEqRender(id) {
|
||||
if (_currentEqUrl) { URL.revokeObjectURL(_currentEqUrl); _currentEqUrl = null; }
|
||||
// JamesDSP is a live insert: never layer it over the Standard renderer copy.
|
||||
if (data.settings?.jamesDsp?.enabled) return null;
|
||||
if (typeof EqRender === 'undefined' || !WEB) return null;
|
||||
try { _currentEqUrl = await EqRender.urlFor(id); } catch { _currentEqUrl = null; }
|
||||
return _currentEqUrl;
|
||||
@@ -905,7 +917,7 @@ function applyProfileData(name, payload, updatedAt) {
|
||||
if (payload.bookmarks && typeof payload.bookmarks === 'object') data.bookmarks = payload.bookmarks;
|
||||
if (payload.lyricOffsets && typeof payload.lyricOffsets === 'object') data.lyricOffsets = payload.lyricOffsets;
|
||||
if (payload.stats && typeof payload.stats === 'object') data.stats = payload.stats;
|
||||
if (payload.settings && typeof payload.settings === 'object') data.settings = { ...DEFAULT_SETTINGS, ...payload.settings };
|
||||
if (payload.settings && typeof payload.settings === 'object') data.settings = normalizeJamesDspSettings({ ...DEFAULT_SETTINGS, ...payload.settings });
|
||||
data.profile = { name, syncedAt: updatedAt || 0, protected: !!payload.__protected, kind: payload.__kind || null };
|
||||
API.saveData(data).catch(() => {});
|
||||
}
|
||||
@@ -5334,7 +5346,7 @@ const Level = (() => {
|
||||
// so it keeps going when locked. One render at a time, in the background.
|
||||
const EqRender = (() => {
|
||||
const DIR = 'eq-renders', KEY = 'ytpEqRenders', RATE = 32000, MAX_SECONDS = 12 * 60;
|
||||
const active = () => EQ.isIOS() && WEB && !!window.OPFS && window.OPFS.isSupported() && typeof OfflineAudioContext !== 'undefined';
|
||||
const active = () => EQ.isIOS() && WEB && !data.settings?.jamesDsp?.enabled && !!window.OPFS && window.OPFS.isSupported() && typeof OfflineAudioContext !== 'undefined';
|
||||
const readMap = () => { try { return JSON.parse(localStorage.getItem(KEY) || '{}'); } catch { return {}; } };
|
||||
const writeMap = (m) => { try { localStorage.setItem(KEY, JSON.stringify(m)); } catch { /* blocked */ } };
|
||||
const queue = new Set();
|
||||
@@ -5504,10 +5516,12 @@ const PiP = (() => {
|
||||
const loadSoundFeatures=()=>Promise.all([Lazy.load('feature:eq-core'),Lazy.load('feature:vocal-reducer')]);
|
||||
const EQ_BANDS = EqParameters.bands;
|
||||
const EQ_PRESETS = EqParameters.presets;
|
||||
const JamesDSPFeature = Lazy.proxy('feature:jamesdsp', ['createController'], 'JamesDSPFeature');
|
||||
const EQ = (() => {
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
const AC = window.AudioContext || window.webkitAudioContext;
|
||||
const st = { ctx: null, filters: null };
|
||||
const st = { ctx: null, filters: null, levelDb: 0 };
|
||||
let dspController = null, dspControllerPromise = null;
|
||||
// Web Audio is off on iOS (it silences playback when the screen locks)
|
||||
// unless the user opted in through "Level volume" (Settings → Playback).
|
||||
// Live Web Audio is off on iOS (it silences playback when the screen locks)
|
||||
@@ -5522,6 +5536,43 @@ const EQ = (() => {
|
||||
};
|
||||
const isFlat = () => gains().every((v) => v === 0);
|
||||
|
||||
async function prepareDspGraph() {
|
||||
if (!available()) throw new Error(isIOS ? 'Turn on Live sound processing in Settings → Playback first.' : 'Web Audio is unavailable.');
|
||||
if (rendered()) throw new Error('This song is already playing a Standard processed copy. JamesDSP will start with the next original track.');
|
||||
if (!st.ctx && !AudioGraph.snapshot().contextCreated) {
|
||||
const context = AudioGraph.context({ sampleRate: 48000 });
|
||||
if (context.state === 'suspended') context.resume().catch(() => {});
|
||||
}
|
||||
await loadSoundFeatures();
|
||||
if (!st.ctx && !build()) throw new Error('The Standard fallback audio chain is not ready.');
|
||||
if (!st.ctx || !st.filters?.[0]) throw new Error('The Standard fallback audio chain could not be built.');
|
||||
attach(els.video);
|
||||
attach(els.audio);
|
||||
return { context: st.ctx, elements: [els.video, els.audio].filter(Boolean), standardInput: st.filters[0] };
|
||||
}
|
||||
|
||||
function dspFailed(error) {
|
||||
if (!data.settings.jamesDsp?.enabled) return;
|
||||
data.settings.jamesDsp.enabled = false;
|
||||
persist();
|
||||
toast(`JamesDSP unavailable; Standard sound is active. ${error?.message || ''}`.trim());
|
||||
apply();
|
||||
Level.apply();
|
||||
}
|
||||
|
||||
async function getDspController() {
|
||||
if (dspController) return dspController;
|
||||
if (!dspControllerPromise) dspControllerPromise = Lazy.load('feature:jamesdsp').then(() => {
|
||||
dspController = JamesDSPFeature.createController({ graph: AudioGraph, prepareGraph: prepareDspGraph, onError: dspFailed });
|
||||
return dspController;
|
||||
}).catch(error => { dspControllerPromise = null; throw error; });
|
||||
return dspControllerPromise;
|
||||
}
|
||||
|
||||
function startDsp() {
|
||||
getDspController().then(controller => controller.enable(data.settings.jamesDsp, st.levelDb)).catch(dspFailed);
|
||||
}
|
||||
|
||||
function build() {
|
||||
if (st.ctx) return true;
|
||||
if (!Lazy.loaded('feature:eq-core') || !Lazy.loaded('feature:vocal-reducer')) {
|
||||
@@ -5562,6 +5613,14 @@ const EQ = (() => {
|
||||
|
||||
// Apply the stored curve; wires the graph up the first time it's non-flat.
|
||||
function apply() {
|
||||
if (data.settings.jamesDsp?.enabled) {
|
||||
if (!available()) { dspFailed(new Error(isIOS ? 'Live sound processing is off on this iPhone.' : 'Web Audio is unavailable.')); return; }
|
||||
if (rendered()) { dspFailed(new Error('The current song already uses a Standard processed copy.')); return; }
|
||||
startDsp();
|
||||
renderButton();
|
||||
return;
|
||||
}
|
||||
if (dspController) dspController.disable().catch(() => {});
|
||||
if (!available()) return;
|
||||
if (!st.ctx && isFlat() && EqParameters.amount(data.settings.vocalLevel ?? 100) === 100 && EqParameters.width(data.settings.stereoWidth) === 0) return;
|
||||
if (!build()) return;
|
||||
@@ -5641,6 +5700,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 (!available() || !build()) return null;
|
||||
attach(els.video);
|
||||
attach(els.audio);
|
||||
@@ -5656,8 +5716,13 @@ const EQ = (() => {
|
||||
|
||||
// Set the levelling gain (dB). 0 dB never wires the graph on its own.
|
||||
function setLevel(db) {
|
||||
if (!available()) return false;
|
||||
if (rendered()) db = 0;
|
||||
st.levelDb = Number(db) || 0;
|
||||
if (data.settings.jamesDsp?.enabled) {
|
||||
if (dspController?.desired) dspController.setLevelDb(st.levelDb).catch(dspFailed);
|
||||
return true;
|
||||
}
|
||||
if (!available()) return false;
|
||||
if (!st.ctx && Math.abs(db) < 0.3) return true;
|
||||
if (!build()) return false;
|
||||
attach(els.video);
|
||||
@@ -8966,7 +9031,7 @@ async function boot() {
|
||||
lyricOffsets: loaded.lyricOffsets || {},
|
||||
stats: loaded.stats || null,
|
||||
lastAutoBackup: loaded.lastAutoBackup || 0,
|
||||
settings: { ...DEFAULT_SETTINGS, ...(loaded.settings || {}) },
|
||||
settings: normalizeJamesDspSettings({ ...DEFAULT_SETTINGS, ...(loaded.settings || {}) }),
|
||||
profile: loaded.profile || null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user