Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 12 additions & 341 deletions static/app.js

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions static/js/count-in.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// See ./host.js: reading an unwired hook THROWS, and tests/js/host_contract.test.js
// fails CI if the hooks used here and the hooks app.js wires ever drift apart.
import { audio } from './audio-el.js';
import { host } from './host.js';
import { _audioSeek, _songEventPayload, jucePlayer, setPlayButtonState, togglePlay } from './transport.js';
import { loopA, loopB, setLoop } from './loops.js';
import { S } from './player-state.js';

Expand Down Expand Up @@ -182,7 +182,7 @@ export async function startCountIn(opts = {}) {
const gen = _countInGen;
const immediate = !!opts.immediate;
if (window._juceMode) {
await host.jucePlayer().pause().catch((err) => console.error('[app] host.jucePlayer().pause error in count-in:', err));
await jucePlayer.pause().catch((err) => console.error('[app] jucePlayer.pause error in count-in:', err));
} else {
audio.pause();
}
Expand Down Expand Up @@ -225,7 +225,7 @@ export async function startCountIn(opts = {}) {
// Rewind done — set final position and start count.
// Await the JUCE seek so the engine has repositioned before
// we start the click track (HTML5 path is synchronous).
host._audioSeek(loopA, 'loop-wrap').then((r) => {
_audioSeek(loopA, 'loop-wrap').then((r) => {
if (gen !== _countInGen) return; // teardown during seek
// Abort the loop restart in two cases:
// 1. Cancelled (player torn down): don't beginCount on a
Expand All @@ -247,10 +247,10 @@ export async function startCountIn(opts = {}) {
_countingIn = false;
if (S.isPlaying) {
S.isPlaying = false;
host.setPlayButtonState(false);
setPlayButtonState(false);
if (window.feedBack) {
window.feedBack.isPlaying = false;
window.feedBack.emit('song:pause', host._songEventPayload());
window.feedBack.emit('song:pause', _songEventPayload());
}
}
return;
Expand Down Expand Up @@ -282,21 +282,21 @@ export async function startCountIn(opts = {}) {
hideCountOverlay();
_countingIn = false;
if (window._juceMode) {
host.jucePlayer().play().then((started) => {
jucePlayer.play().then((started) => {
if (gen !== _countInGen) return; // teardown during play start
if (!started) return;
S.isPlaying = true;
host.setPlayButtonState(true);
setPlayButtonState(true);
window.feedBack.isPlaying = true;
const payload = host._songEventPayload();
const payload = _songEventPayload();
window.feedBack.emit('song:play', payload);
window.feedBack.emit('song:resume', payload);
}).catch((err) => console.error('[app] host.jucePlayer().play error:', err));
}).catch((err) => console.error('[app] jucePlayer.play error:', err));
} else {
audio.play().then(() => {
if (gen !== _countInGen) return;
S.isPlaying = true;
host.setPlayButtonState(true);
setPlayButtonState(true);
}).catch((err) => {
if (gen !== _countInGen) return;
// An engine reroute's deliberate pause aborts this play()
Expand All @@ -307,7 +307,7 @@ export async function startCountIn(opts = {}) {
// started if the Promise rejected.
console.error('[app] audio.play() rejected after count-in:', err);
S.isPlaying = false;
host.setPlayButtonState(false);
setPlayButtonState(false);
});
}
return;
Expand All @@ -333,7 +333,7 @@ export async function startSongCountIn() {
// bumps it and every delayed callback below bails.
const gen = _countInGen;
if (window._juceMode) {
await host.jucePlayer().pause().catch((err) => console.error('[app] host.jucePlayer().pause error in song count-in:', err));
await jucePlayer.pause().catch((err) => console.error('[app] jucePlayer.pause error in song count-in:', err));
} else {
audio.pause();
}
Expand All @@ -352,7 +352,7 @@ export async function startSongCountIn() {
_countingIn = false;
// Hand off to the normal play path — togglePlay() flips isPlaying,
// updates the button, and emits song:play/resume for plugins.
Promise.resolve(host.togglePlay()).catch((err) => console.warn('[app] play after count-in failed:', err));
Promise.resolve(togglePlay()).catch((err) => console.warn('[app] play after count-in failed:', err));
return;
}
showCountOverlay(count);
Expand Down
42 changes: 21 additions & 21 deletions static/js/juce-audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
// See ./host.js: reading an unwired hook THROWS, and tests/js/host_contract.test.js
// fails CI if the hooks used here and the hooks app.js wires ever drift apart.
import { audio } from './audio-el.js';
import { host } from './host.js';
import { _audioSeek, _songEventPayload, jucePlayer, setPlayButtonState } from './transport.js';
import { setSpeed } from './player-controls.js';
import { S } from './player-state.js';

Expand Down Expand Up @@ -199,23 +199,23 @@ import { S } from './player-state.js';
// transport off a stale `wasPlaying` snapshot would resume a song
// the user just paused. Only start it if playback is still wanted.
if (S.isPlaying) {
const started = await host.jucePlayer().play();
const started = await jucePlayer.play();
if (started === false) {
if (!_isStale(songAudio) && S.isPlaying) {
try { await audio.play(); } catch (_) { /* ignore */ }
}
throw new Error('host.jucePlayer().play() failed (transient transport start)');
throw new Error('jucePlayer.play() failed (transient transport start)');
}
}
if (_isStale(songAudio)) {
// Song changed while JUCE was spinning up — undo and bail.
await host.jucePlayer().pause().catch(() => {});
await jucePlayer.pause().catch(() => {});
return 'stale';
}
if (window.jucePlayer) {
host.jucePlayer()._dur = dur;
host.jucePlayer()._pos = pos;
host.jucePlayer()._pollAt = performance.now();
jucePlayer._dur = dur;
jucePlayer._pos = pos;
jucePlayer._pollAt = performance.now();
}
window._juceMode = true;
window._juceAudioUrl = url;
Expand Down Expand Up @@ -270,7 +270,7 @@ import { S } from './player-state.js';
async function _switchJuceToHtml5(songAudio) {
const url = songAudio.url;
const wasPlaying = S.isPlaying;
const pos = (window.jucePlayer ? host.jucePlayer().currentTime : 0) || 0;
const pos = (window.jucePlayer ? jucePlayer.currentTime : 0) || 0;
window.feedBack?.playback?.recordRouteChange?.({
routeKind: 'browser-media',
state: 'switching',
Expand All @@ -296,7 +296,7 @@ import { S } from './player-state.js';
};
let _resumeScheduled = false;
try {
await host.jucePlayer().pause().catch(() => {});
await jucePlayer.pause().catch(() => {});
if (_isStale(songAudio)) return; // song changed mid-pause
window._juceMode = false;
window._juceAudioUrl = null;
Expand Down Expand Up @@ -875,18 +875,18 @@ export let _resetJuceAudioShimChain = function () {};
const seekTime = batch.seekTime;
if (wantsPause && seekTime !== undefined) {
enqueue(async (gen) => {
const r = await host._audioSeek(seekTime, 'audio-element-shim');
const r = await _audioSeek(seekTime, 'audio-element-shim');
if (!r.completed) return; // seek cancelled by teardown
if (gen !== _juceShimGen) return;
if (!forUpcomingPlay) {
await host.jucePlayer().pause();
await jucePlayer.pause();
if (gen !== _juceShimGen) return;
S.isPlaying = false;
host.setPlayButtonState(false);
setPlayButtonState(false);
const sm = window.feedBack;
if (sm) {
sm.isPlaying = false;
sm.emit('song:pause', host._songEventPayload());
sm.emit('song:pause', _songEventPayload());
}
}
audio.dispatchEvent(new Event('seeked'));
Expand All @@ -895,21 +895,21 @@ export let _resetJuceAudioShimChain = function () {};
}
if (wantsPause) {
enqueue(async (gen) => {
await host.jucePlayer().pause();
await jucePlayer.pause();
if (gen !== _juceShimGen) return;
S.isPlaying = false;
host.setPlayButtonState(false);
setPlayButtonState(false);
const sm = window.feedBack;
if (sm) {
sm.isPlaying = false;
sm.emit('song:pause', host._songEventPayload());
sm.emit('song:pause', _songEventPayload());
}
});
return;
}
if (seekTime !== undefined) {
enqueue(async (gen) => {
const r = await host._audioSeek(seekTime, 'audio-element-shim');
const r = await _audioSeek(seekTime, 'audio-element-shim');
if (!r.completed) return; // seek cancelled by teardown
if (gen !== _juceShimGen) return;
audio.dispatchEvent(new Event('seeked'));
Expand Down Expand Up @@ -937,7 +937,7 @@ export let _resetJuceAudioShimChain = function () {};

Object.defineProperty(audio, 'currentTime', {
get() {
if (window._juceMode) return host.jucePlayer().currentTime;
if (window._juceMode) return jucePlayer.currentTime;
return ctDesc.get.call(this);
},
set(v) {
Expand Down Expand Up @@ -975,14 +975,14 @@ export let _resetJuceAudioShimChain = function () {};
if (window._juceMode) {
if (_juceShimBatch != null) flushJuceShimBatchNow({ forUpcomingPlay: true });
const p = enqueue(async (gen) => {
const started = await host.jucePlayer().play();
const started = await jucePlayer.play();
if (gen !== _juceShimGen || !started) return;
S.isPlaying = true;
host.setPlayButtonState(true);
setPlayButtonState(true);
const sm = window.feedBack;
if (sm) {
sm.isPlaying = true;
const payload = host._songEventPayload();
const payload = _songEventPayload();
sm.emit('song:play', payload);
sm.emit('song:resume', payload);
}
Expand Down
9 changes: 5 additions & 4 deletions static/js/loops.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// See ./host.js: reading an unwired hook THROWS, and tests/js/host_contract.test.js
// fails CI if the hooks used here and the hooks app.js wires ever drift apart.
import { esc, uiPrompt } from './dom.js';
import { _audioSeek, _audioTime } from './transport.js';
import { host } from './host.js';
import {
_setSectionPracticeMode,
Expand All @@ -39,14 +40,14 @@ export let loopB = null;
export let _loopMutationGen = 0;

export function setLoopStart() {
loopA = host._audioTime();
loopA = _audioTime();
document.getElementById('btn-loop-a').className = 'px-3 py-1.5 bg-green-900/50 rounded-lg text-xs text-green-300 transition';
updateLoopUI();
}

export function setLoopEnd() {
if (loopA === null) return;
loopB = host._audioTime();
loopB = _audioTime();
if (loopB <= loopA) { loopB = null; return; }
document.getElementById('btn-loop-b').className = 'px-3 py-1.5 bg-green-900/50 rounded-lg text-xs text-green-300 transition';
updateLoopUI();
Expand All @@ -72,7 +73,7 @@ export function clearLoop(options) {
document.getElementById('loop-label').textContent = '';
document.getElementById('saved-loops').value = '';
resetSelection();
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
if (hadLoop && emitTransportEvent && typeof window !== 'undefined') {
window.feedBack?.playback?.transportEvent?.('loop-cleared', {
requesterId: 'core.loop',
Expand Down Expand Up @@ -125,7 +126,7 @@ export async function setLoop(a, b, options) {
// Don't arm loopA/loopB before the seek lands — the 60Hz tick's wrap
// detector (`ct >= loopB`) would trigger startCountIn against
// half-applied state.
const r = await host._audioSeek(aNum, 'loop-set');
const r = await _audioSeek(aNum, 'loop-set');
if (!r.completed || Math.abs(r.to - aNum) > 0.05) return false;
// Caller-owned staleness gate, re-checked after the awaited seek and before
// we commit loopA/loopB. practiceSection() passes this so a superseded retry
Expand Down
23 changes: 12 additions & 11 deletions static/js/section-practice.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
// layer that catches it on the paths a smoke test never runs.
import { audio } from './audio-el.js';
import { esc } from './dom.js';
import { _audioDuration, _audioTime, audioSeekGen } from './transport.js';
import { host } from './host.js';

export function _sectionPracticeBarContains(el) {
Expand Down Expand Up @@ -85,7 +86,7 @@ export function _setSectionPracticeMode(on, opts = {}) {
if (opts.defaultWholeOn) {
_sectionPracticeWholeSection = true;
}
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
if (opts.defaultWholeOn) {
_syncSectionPracticePieceUi();
}
Expand All @@ -104,7 +105,7 @@ export function _setSectionPracticeMode(on, opts = {}) {
_sectionPracticeSelected = -1;
_sectionPracticeWholeSection = false;
_sectionPracticeSavedPartIndex = 0;
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
if (!opts.skipClearLoop && (host.loopA() !== null || host.loopB() !== null)) {
host.clearLoop();
}
Expand All @@ -129,7 +130,7 @@ function _sectionPracticeHighway() {
}

function _sectionPracticeDuration() {
const d = host._audioDuration();
const d = _audioDuration();
if (d && Number.isFinite(d) && d > 0) return d;
const cd = window.feedBack?.currentSong?.duration;
return (cd && Number.isFinite(cd) && cd > 0) ? cd : 0;
Expand Down Expand Up @@ -914,7 +915,7 @@ export function renderSectionPracticeBar() {
// matching one; run it before the piece UI so that reflects the result.
_syncSectionPracticeFromLoop();
_syncSectionPracticePieceUi();
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
}

export async function onSectionParentClick(parentIdx) {
Expand All @@ -927,7 +928,7 @@ export async function onSectionParentClick(parentIdx) {
_sectionPracticeSavedPartIndex = 0;
_sectionPracticeWholeSection = true;
_syncSectionPracticePieceUi();
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
if (_sectionPracticeActiveParentRange() || _sectionPracticeRanges.length) {
await practiceSection(0, { whole: true });
}
Expand Down Expand Up @@ -1019,7 +1020,7 @@ function _blurSectionPracticeFocusIfNeeded() {

export async function practiceSection(index, opts = {}) {
const requestGen = ++_sectionPracticeRequestGen;
const seekGen = host._audioSeekGen();
const seekGen = audioSeekGen();
const loopGen = host._loopMutationGen();
const whole = !!opts.whole;
const r = _sectionPracticeResolveLoopTarget(index, opts);
Expand All @@ -1045,7 +1046,7 @@ export async function practiceSection(index, opts = {}) {
let ok = false;
for (let attempt = 0; attempt < 5; attempt++) {
// A newer click or a song/arrangement change supersedes this retry.
if (requestGen !== _sectionPracticeRequestGen || seekGen !== host._audioSeekGen() || loopGen !== host._loopMutationGen()) return;
if (requestGen !== _sectionPracticeRequestGen || seekGen !== audioSeekGen() || loopGen !== host._loopMutationGen()) return;
try {
// skipSectionSync: this function owns the section-practice state and
// applies it below under the request-gen guard, so a stale retry
Expand All @@ -1055,7 +1056,7 @@ export async function practiceSection(index, opts = {}) {
// after its internal seek await, so a stale loop is never armed.
ok = await host.setLoop(start, end, {
skipSectionSync: true,
commitGuard: () => requestGen === _sectionPracticeRequestGen && seekGen === host._audioSeekGen() && loopGen === host._loopMutationGen(),
commitGuard: () => requestGen === _sectionPracticeRequestGen && seekGen === audioSeekGen() && loopGen === host._loopMutationGen(),
});
} catch (err) {
ok = false;
Expand All @@ -1064,7 +1065,7 @@ export async function practiceSection(index, opts = {}) {
await new Promise(res => setTimeout(res, 60 + attempt * 90));
}
// Re-check after the awaited retries before applying any loop/count-in state.
if (requestGen !== _sectionPracticeRequestGen || seekGen !== host._audioSeekGen() || loopGen !== host._loopMutationGen()) return;
if (requestGen !== _sectionPracticeRequestGen || seekGen !== audioSeekGen() || loopGen !== host._loopMutationGen()) return;

if (ok) {
_sectionPracticeWholeSection = whole;
Expand All @@ -1073,7 +1074,7 @@ export async function practiceSection(index, opts = {}) {
_sectionPracticeSavedPartIndex = index;
}
_blurSectionPracticeFocusIfNeeded();
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
host.startCountIn({ immediate: true });
} else {
_setSectionPracticeMode(false, { skipClearLoop: true });
Expand Down Expand Up @@ -1120,7 +1121,7 @@ export function _syncSectionPracticeFromLoop() {
} else if (_sectionPracticeMode) {
_setSectionPracticeMode(false, { skipClearLoop: true });
}
_updateSectionPracticeHighlight(host._audioTime());
_updateSectionPracticeHighlight(_audioTime());
}

function _sectionPracticeIndexAtTime(t) {
Expand Down
Loading
Loading