From 6e28312947062cc823add72797ff1ab16c102ab1 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 4 Oct 2026 00:18:06 +0200 Subject: [PATCH] Ambient in der Player-Leiste; weniger Aussetzer bei den Klangwelten auf Telefonen; sw v80 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app.js: __dioraAmbientInfo (spielt, zuletzt gestartet, Klangwelt), meldet Moduswechsel an die Leiste; Radio/Folge übernimmt sie wieder - Drift (Telefone): Musik-Bett, Delay und Hall werden vom Ausgang getrennt, wenn eine Klangwelt ohne sie läuft (Braunes Rauschen …); Zugfahrt und Herzschlag planen 2,5 s voraus Co-Authored-By: Claude Opus 5.5 --- static/ambient/index.html | 30 ++++++++++++++++++--- static/js/app.js | 30 ++++++++++++++++++--- static/js/sw.js | 2 +- static/podcasts/native-podcast.js | 45 +++++++++++++++++++++++++++++-- 4 files changed, 98 insertions(+), 9 deletions(-) diff --git a/static/ambient/index.html b/static/ambient/index.html index a3757fd..32b33dc 100644 --- a/static/ambient/index.html +++ b/static/ambient/index.html @@ -296,7 +296,8 @@ function buildGraph() { nB.connect(sl).connect(sMod).connect(g.seaGain).connect(g.noiseBus); nA.start(); nB.start(0, 3.1); g.noiseBus.connect(g.mix); - const nSend = ctx.createGain(); nSend.gain.value = .3; g.noiseBus.connect(nSend).connect(g.conv); + const nSend = g.nSend = ctx.createGain(); nSend.gain.value = .3; g.noiseBus.connect(nSend).connect(g.conv); + S.musicLinked = true; S.revLinked = true; // siehe applyRev (Telefone) // Granular: Ringpuffer der Stimmen, daraus werden Körner geschnitten. Der ScriptProcessor läuft im // Hauptthread und hält bei Last den ganzen Graphen auf – auf Telefonen gibt es daher keine Körner. @@ -917,7 +918,11 @@ function buildWomb(K) { o.connect(og).connect(tl); o.start(t); o.stop(t + .3); }; const per = () => 60 / (55 + K.a * 60); - K.every(() => { const t = T() + .05; thump(t, 64, .9); thump(t + .27, 72, .6); }, per, () => per() * 1.01, .3); + // Schläge der nächsten Sekunden vorplanen: ein stockender Hauptthread lässt so keinen Schlag aus + K.nextBeat = T() + .3; + K.every(() => { + while (K.nextBeat < T() + 2.5) { const t = K.nextBeat; thump(t, 64, .9); thump(t + .27, 72, .6); K.nextBeat += per() * R(1, 1.01); } + }, .25, .25, .05); K.set = (a, b) => { sm(tl.frequency, 100 + 300 * b); sm(lp.frequency, 300 + 500 * b); }; } @@ -953,7 +958,7 @@ function buildTrain(K) { }; K.every(() => { const now = T(); - while (K.nextJoint < now + .5) { // alle Achsen eines Schienenstoßes vorplanen + while (K.nextJoint < now + 2.5) { // alle Achsen der Schienenstöße der nächsten Sekunden vorplanen (ein stockender Hauptthread reißt so keine Lücke) const v = K.v, t0 = K.nextJoint, amp = .8 * (.85 + Math.random() * .3); [0, 2.5 / v, 12.5 / v, 15 / v].forEach((o, i) => railClack(K, t0 + o, amp * (i % 2 ? .8 : 1))); K.nextJoint += 25 / v; @@ -1004,6 +1009,25 @@ function applyRev() { if (!S.g) return; const M = MODES[S.mode]; S.g.modeRev.gain.setTargetAtTime(M.music ? 0 : (M.rev || 0) * S.verb * 1.2, S.ctx.currentTime, .3); + if (!LITE) return; + // Telefone: Was gerade nicht klingt, vom Ausgang trennen – der Browser rechnet es dann gar nicht + // (Musik-Bett, Delay, Hall ohne Hall-Modus). Musik geht nach dem Ausblenden weg, damit nichts knackt. + const music = !!M.music; + if (music) linkMusic(true); + else setTimeout(() => { if (!MODES[S.mode].music) linkMusic(false); }, 1600); + const rev = music || !!M.rev; + if (S.revLinked !== rev) { + S.revLinked = rev; + try { rev ? S.g.conv.connect(S.g.mix) : S.g.conv.disconnect(S.g.mix); } catch (e) {} + } +} +function linkMusic(on) { + if (S.musicLinked === on) return; + S.musicLinked = on; + const G = S.g; + for (const [a, b] of [[G.dry, G.mix], [G.send, G.conv], [G.noiseBus, G.mix], [G.nSend, G.conv], [G.dlp, G.conv], [G.dlp, G.mix]]) { + try { on ? a.connect(b) : a.disconnect(b); } catch (e) {} + } } function startSoundscape() { const M = MODES[S.mode], K = newKit(M.gain || .7); diff --git a/static/js/app.js b/static/js/app.js index c942dc8..5e09935 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1246,6 +1246,7 @@ function showTab(name) { // {type: 'ambient', state: 'playing'|'stopped'} from it. // --------------------------------------------------------------------------- let _ambientPlaying = false; +let _ambientLast = false; // Ambient was started last: the player bar shows it (native-podcast.js) function _ambientFrame() { return $('ambient-frame'); @@ -1255,8 +1256,14 @@ function _ambientLoad() { const f = _ambientFrame(); if (!f) return; if (!f.getAttribute('src')) { - // Drift takes the page's volume before its first sound. - f.addEventListener('load', () => _ambientVolume()); + // Drift takes the page's volume before its first sound; the bar follows its sound world. + f.addEventListener('load', () => { + _ambientVolume(); + try { + f.contentDocument.getElementById('mode').addEventListener('change', () => + window.__dioraAmbientChanged && window.__dioraAmbientChanged()); + } catch (e) {} + }); f.src = f.dataset.src; } requestAnimationFrame(_ambientSize); @@ -1283,12 +1290,27 @@ window.__dioraAmbientCmd = function (cmd, value) { if (f && f.contentWindow && f.getAttribute('src')) f.contentWindow.postMessage({type: 'ambient', cmd, value}, '*'); }; -// The radio or a podcast starts: Ambient stops. +// The radio or a podcast starts: Ambient stops, and the player bar is theirs again. window.__dioraAmbientStop = function () { if (_ambientPlaying) window.__dioraAmbientCmd('stop'); + if (_ambientLast) { + _ambientLast = false; + if (window.__dioraAmbientChanged) window.__dioraAmbientChanged(); + } }; window.__dioraAmbientPlaying = () => _ambientPlaying; +// For the player bar: playing, started last, the sound world's name (the frame is ours). +window.__dioraAmbientInfo = function () { + const f = _ambientFrame(); + let mode = ''; + try { + const sel = f && f.contentDocument && f.contentDocument.getElementById('mode'); + if (sel && sel.selectedOptions[0]) mode = sel.selectedOptions[0].text; + } catch (e) {} + return {playing: _ambientPlaying, last: _ambientLast, mode}; +}; + // The page's volume (0–255, the bar's slider) is Drift's too; its own slider hides when // embedded. Not in the Android app: there the volume keys set it, as for everything. function _ambientVolume(val) { @@ -1305,6 +1327,8 @@ window.addEventListener('message', e => { const d = e.data; if (!f || e.source !== f.contentWindow || !d || d.type !== 'ambient' || !d.state) return; _ambientPlaying = d.state === 'playing'; + if (_ambientPlaying) _ambientLast = true; + if (window.__dioraAmbientChanged) window.__dioraAmbientChanged(); // The app keeps it going with the screen off (AmbientService); the browser's sleep timer counts it. if (window.DioraNative && DioraNative.ambientState) DioraNative.ambientState(_ambientPlaying); if (window.__dioraOnAmbient) window.__dioraOnAmbient(_ambientPlaying); diff --git a/static/js/sw.js b/static/js/sw.js index 9c4576b..907ce35 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v79'; +const CACHE = 'diora-v80'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index df2a096..4d63a72 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android 67a915e 2026-10-03: web-native/static/native-podcast.js +// Copied from diora-android 8ee6c2b 2026-10-04: web-native/static/native-podcast.js // by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android // and copy again. The browser host is static/js/podcasts-web.js. @@ -2798,7 +2798,8 @@ const text = el('button', 'nui-mini-text'); text.type = 'button'; text.append(el('span', 'nui-mini-station'), el('span', 'nui-mini-track')); - text.addEventListener('click', () => (miniKind === 'radio' ? openRadioPlayer() : openPlayer())); + text.addEventListener('click', () => (miniKind === 'ambient' ? showTab('ambient') + : miniKind === 'radio' ? openRadioPlayer() : openPlayer())); const devices = button('⇄', 'Wiedergabe auf …', deviceMenu, 'nui-mini-devices'); const here = button('⇩', 'Hier abspielen', takeOver, 'nui-mini-here'); devices.hidden = here.hidden = true; @@ -2808,6 +2809,7 @@ button('☆', 'Sender speichern', () => window.__dioraRadioSave && window.__dioraRadioSave(), 'nui-mini-save'), devices, here, button('▶', 'Abspielen', () => (miniKind === 'remote' ? remoteToggle() + : miniKind === 'ambient' ? ambientToggle() : miniKind === 'radio' ? window.__dioraRadioToggle && window.__dioraRadioToggle() : N.podcastToggle()), 'nui-mini-toggle')); dock.prepend(mini); // One player for podcasts and radio: the radio's own bar steps aside. @@ -2841,6 +2843,42 @@ b.classList.toggle('on', !!sleepLabel(true)); } + // Ambient (app.js) started last: the bar is its — the sound world's name, ▶/■, a tap opens + // its tab — until the radio or an episode starts. + function ambientInfo() { + return window.__dioraAmbientInfo ? window.__dioraAmbientInfo() : null; + } + + function ambientToggle() { + const a = ambientInfo(); + if (a && window.__dioraAmbientCmd) window.__dioraAmbientCmd(a.playing ? 'stop' : 'start'); + } + + window.__dioraAmbientChanged = () => renderMini(); + + function renderAmbientMini() { + const a = ambientInfo(); + if (!mini || !a || !a.last) return false; + miniKind = 'ambient'; + document.documentElement.classList.remove('nui-pod-playing'); + document.documentElement.classList.add('nui-pod-mini-open'); + mini.hidden = false; + mini.classList.add('nui-mini-radio'); + mini.classList.remove('nui-mini-remote'); + for (const c of ['.nui-pod-skip', '.nui-mini-save', '.nui-mini-devices', '.nui-mini-here']) mini.querySelector(c).hidden = true; + mini.querySelector('.nui-mini-station').textContent = 'Ambient'; + const z = sleepLabel(true); + mini.querySelector('.nui-mini-track').textContent = (a.mode || 'Drift') + (a.playing ? '' : ' · angehalten') + (z ? ' · ' + z : ''); + const t = mini.querySelector('.nui-mini-toggle'); + t.textContent = a.playing ? '■' : '▶'; + t.title = a.playing ? 'Ambient stoppen' : 'Ambient abspielen'; + const box = mini.querySelector('.nui-pod-art'); + setArt(box, ''); + if (!box.querySelector('.nui-amb-art')) { box.textContent = ''; box.append(el('span', 'nui-pod-initial nui-amb-art', '∿')); } + mini.querySelector('.nui-pod-mini-bar span').style.width = a.playing ? '100%' : '0'; + return true; + } + // The bar names a running sleep timer: "… · ☾ 12 min". function sleepSuffix() { const l = sleepLabel(); @@ -2871,10 +2909,13 @@ function renderMini() { renderAmbientSleep(); + if (renderAmbientMini()) return; const r = remote(); const isPod = player.kind === 'podcast'; document.documentElement.classList.toggle('nui-pod-playing', isPod || !!r); if (!mini) return; + const ambArt = mini.querySelector('.nui-amb-art'); + if (ambArt) ambArt.remove(); const radio = isPod || r ? null : radioNow(); miniKind = r ? 'remote' : isPod ? 'podcast' : radio ? 'radio' : ''; mini.hidden = !miniKind;