Ambient in der Player-Leiste; weniger Aussetzer bei den Klangwelten auf Telefonen; sw v80
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
2c64342c53
commit
6e28312947
4 changed files with 98 additions and 9 deletions
|
|
@ -296,7 +296,8 @@ function buildGraph() {
|
||||||
nB.connect(sl).connect(sMod).connect(g.seaGain).connect(g.noiseBus);
|
nB.connect(sl).connect(sMod).connect(g.seaGain).connect(g.noiseBus);
|
||||||
nA.start(); nB.start(0, 3.1);
|
nA.start(); nB.start(0, 3.1);
|
||||||
g.noiseBus.connect(g.mix);
|
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
|
// 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.
|
// 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);
|
o.connect(og).connect(tl); o.start(t); o.stop(t + .3);
|
||||||
};
|
};
|
||||||
const per = () => 60 / (55 + K.a * 60);
|
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); };
|
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(() => {
|
K.every(() => {
|
||||||
const now = T();
|
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);
|
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)));
|
[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;
|
K.nextJoint += 25 / v;
|
||||||
|
|
@ -1004,6 +1009,25 @@ function applyRev() {
|
||||||
if (!S.g) return;
|
if (!S.g) return;
|
||||||
const M = MODES[S.mode];
|
const M = MODES[S.mode];
|
||||||
S.g.modeRev.gain.setTargetAtTime(M.music ? 0 : (M.rev || 0) * S.verb * 1.2, S.ctx.currentTime, .3);
|
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() {
|
function startSoundscape() {
|
||||||
const M = MODES[S.mode], K = newKit(M.gain || .7);
|
const M = MODES[S.mode], K = newKit(M.gain || .7);
|
||||||
|
|
|
||||||
|
|
@ -1246,6 +1246,7 @@ function showTab(name) {
|
||||||
// {type: 'ambient', state: 'playing'|'stopped'} from it.
|
// {type: 'ambient', state: 'playing'|'stopped'} from it.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
let _ambientPlaying = false;
|
let _ambientPlaying = false;
|
||||||
|
let _ambientLast = false; // Ambient was started last: the player bar shows it (native-podcast.js)
|
||||||
|
|
||||||
function _ambientFrame() {
|
function _ambientFrame() {
|
||||||
return $('ambient-frame');
|
return $('ambient-frame');
|
||||||
|
|
@ -1255,8 +1256,14 @@ function _ambientLoad() {
|
||||||
const f = _ambientFrame();
|
const f = _ambientFrame();
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
if (!f.getAttribute('src')) {
|
if (!f.getAttribute('src')) {
|
||||||
// Drift takes the page's volume before its first sound.
|
// Drift takes the page's volume before its first sound; the bar follows its sound world.
|
||||||
f.addEventListener('load', () => _ambientVolume());
|
f.addEventListener('load', () => {
|
||||||
|
_ambientVolume();
|
||||||
|
try {
|
||||||
|
f.contentDocument.getElementById('mode').addEventListener('change', () =>
|
||||||
|
window.__dioraAmbientChanged && window.__dioraAmbientChanged());
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
f.src = f.dataset.src;
|
f.src = f.dataset.src;
|
||||||
}
|
}
|
||||||
requestAnimationFrame(_ambientSize);
|
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}, '*');
|
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 () {
|
window.__dioraAmbientStop = function () {
|
||||||
if (_ambientPlaying) window.__dioraAmbientCmd('stop');
|
if (_ambientPlaying) window.__dioraAmbientCmd('stop');
|
||||||
|
if (_ambientLast) {
|
||||||
|
_ambientLast = false;
|
||||||
|
if (window.__dioraAmbientChanged) window.__dioraAmbientChanged();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.__dioraAmbientPlaying = () => _ambientPlaying;
|
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
|
// 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.
|
// embedded. Not in the Android app: there the volume keys set it, as for everything.
|
||||||
function _ambientVolume(val) {
|
function _ambientVolume(val) {
|
||||||
|
|
@ -1305,6 +1327,8 @@ window.addEventListener('message', e => {
|
||||||
const d = e.data;
|
const d = e.data;
|
||||||
if (!f || e.source !== f.contentWindow || !d || d.type !== 'ambient' || !d.state) return;
|
if (!f || e.source !== f.contentWindow || !d || d.type !== 'ambient' || !d.state) return;
|
||||||
_ambientPlaying = d.state === 'playing';
|
_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.
|
// 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.DioraNative && DioraNative.ambientState) DioraNative.ambientState(_ambientPlaying);
|
||||||
if (window.__dioraOnAmbient) window.__dioraOnAmbient(_ambientPlaying);
|
if (window.__dioraOnAmbient) window.__dioraOnAmbient(_ambientPlaying);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* diora service worker — caches the app shell for offline use.
|
* diora service worker — caches the app shell for offline use.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE = 'diora-v79';
|
const CACHE = 'diora-v80';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
'/static/js/app.js',
|
'/static/js/app.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
|
// 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.
|
// and copy again. The browser host is static/js/podcasts-web.js.
|
||||||
|
|
||||||
|
|
@ -2798,7 +2798,8 @@
|
||||||
const text = el('button', 'nui-mini-text');
|
const text = el('button', 'nui-mini-text');
|
||||||
text.type = 'button';
|
text.type = 'button';
|
||||||
text.append(el('span', 'nui-mini-station'), el('span', 'nui-mini-track'));
|
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 devices = button('⇄', 'Wiedergabe auf …', deviceMenu, 'nui-mini-devices');
|
||||||
const here = button('⇩', 'Hier abspielen', takeOver, 'nui-mini-here');
|
const here = button('⇩', 'Hier abspielen', takeOver, 'nui-mini-here');
|
||||||
devices.hidden = here.hidden = true;
|
devices.hidden = here.hidden = true;
|
||||||
|
|
@ -2808,6 +2809,7 @@
|
||||||
button('☆', 'Sender speichern', () => window.__dioraRadioSave && window.__dioraRadioSave(), 'nui-mini-save'),
|
button('☆', 'Sender speichern', () => window.__dioraRadioSave && window.__dioraRadioSave(), 'nui-mini-save'),
|
||||||
devices, here,
|
devices, here,
|
||||||
button('▶', 'Abspielen', () => (miniKind === 'remote' ? remoteToggle()
|
button('▶', 'Abspielen', () => (miniKind === 'remote' ? remoteToggle()
|
||||||
|
: miniKind === 'ambient' ? ambientToggle()
|
||||||
: miniKind === 'radio' ? window.__dioraRadioToggle && window.__dioraRadioToggle() : N.podcastToggle()), 'nui-mini-toggle'));
|
: miniKind === 'radio' ? window.__dioraRadioToggle && window.__dioraRadioToggle() : N.podcastToggle()), 'nui-mini-toggle'));
|
||||||
dock.prepend(mini);
|
dock.prepend(mini);
|
||||||
// One player for podcasts and radio: the radio's own bar steps aside.
|
// One player for podcasts and radio: the radio's own bar steps aside.
|
||||||
|
|
@ -2841,6 +2843,42 @@
|
||||||
b.classList.toggle('on', !!sleepLabel(true));
|
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".
|
// The bar names a running sleep timer: "… · ☾ 12 min".
|
||||||
function sleepSuffix() {
|
function sleepSuffix() {
|
||||||
const l = sleepLabel();
|
const l = sleepLabel();
|
||||||
|
|
@ -2871,10 +2909,13 @@
|
||||||
|
|
||||||
function renderMini() {
|
function renderMini() {
|
||||||
renderAmbientSleep();
|
renderAmbientSleep();
|
||||||
|
if (renderAmbientMini()) return;
|
||||||
const r = remote();
|
const r = remote();
|
||||||
const isPod = player.kind === 'podcast';
|
const isPod = player.kind === 'podcast';
|
||||||
document.documentElement.classList.toggle('nui-pod-playing', isPod || !!r);
|
document.documentElement.classList.toggle('nui-pod-playing', isPod || !!r);
|
||||||
if (!mini) return;
|
if (!mini) return;
|
||||||
|
const ambArt = mini.querySelector('.nui-amb-art');
|
||||||
|
if (ambArt) ambArt.remove();
|
||||||
const radio = isPod || r ? null : radioNow();
|
const radio = isPod || r ? null : radioNow();
|
||||||
miniKind = r ? 'remote' : isPod ? 'podcast' : radio ? 'radio' : '';
|
miniKind = r ? 'remote' : isPod ? 'podcast' : radio ? 'radio' : '';
|
||||||
mini.hidden = !miniKind;
|
mini.hidden = !miniKind;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue