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);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue