From 40d06638f0b72376500a484074bb05af43237757 Mon Sep 17 00:00:00 2001 From: mallae-rpi Date: Tue, 21 Apr 2026 20:00:38 -0400 Subject: [PATCH 1/4] Fix Repeat One bug --- src/components/NowPlaying.tsx | 9 +++++++-- src/components/SetList.tsx | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/NowPlaying.tsx b/src/components/NowPlaying.tsx index 85ca7ac..203ef94 100644 --- a/src/components/NowPlaying.tsx +++ b/src/components/NowPlaying.tsx @@ -116,10 +116,15 @@ export default function NowPlaying({ libraryItems }: NowPlayingProps) { }; const handlePlayPause = () => { - if (isPlaying) { + // Read state directly from the engine, not React state. + // play() is async (awaits ctx.resume), so transportState can lag or miss errors, + // leaving isPlaying stale and causing no-op play() calls. getState() stays accurate. + if (engine.transport.getState() === 'playing') { engine.transport.pause(); } else { - void engine.transport.play(); + engine.transport.play().catch((err: unknown) => { + console.error('Playback failed to start:', err); + }); } }; diff --git a/src/components/SetList.tsx b/src/components/SetList.tsx index 90481b2..54cf223 100644 --- a/src/components/SetList.tsx +++ b/src/components/SetList.tsx @@ -54,8 +54,14 @@ export default function SetList({ currentEntryIdRef.current = engine.getCurrentEntry()?.entryId ?? null; const unsubSong = engine.on('songChange', ({ entryId }) => { - if (repeatModeRef.current === 'one' && currentEntryIdRef.current !== null) { - // Song changed - seek back to the start of the previous song. + // Only handle real track changes. songChange also fires on seek within the same track, + // so without this guard, Repeat One would override seeks by jumping back to absoluteStart. + if ( + repeatModeRef.current === 'one' && + currentEntryIdRef.current !== null && + entryId !== currentEntryIdRef.current + ) { + // A genuine song transition occurred - seek back to the start of the previous song. const prev = engine.getTimeline().find((e) => e.entryId === currentEntryIdRef.current); if (prev) { engine.transport.seek(prev.absoluteStart); From 6c09fe3436a0d3d537d32586e1ea0bec0ff2541d Mon Sep 17 00:00:00 2001 From: mallae-rpi Date: Wed, 22 Apr 2026 23:07:14 -0400 Subject: [PATCH 2/4] Fix bug where Repeat One mode does not bring the last song in a setlist to the beginning of that song, but rather the beginning of the timeline --- src/components/SetList.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/SetList.tsx b/src/components/SetList.tsx index 54cf223..943015d 100644 --- a/src/components/SetList.tsx +++ b/src/components/SetList.tsx @@ -80,6 +80,17 @@ export default function SetList({ wasPlayingRef.current = false; engine.transport.seek(0); void engine.transport.play(); + } else if (repeatModeRef.current === 'one' && wasPlayingRef.current) { + // When the last song finishes, songChange doesn't fire, so Repeat One would silently fall through + // This seeks back to the start of the current song and replays it, similar to how songChange handles mid-playlist transitions + wasPlayingRef.current = false; + const entry = engine.getTimeline().find((e) => e.entryId === currentEntryIdRef.current); + if (entry) { + engine.transport.seek(entry.absoluteStart); + engine.transport.play().catch((err: unknown) => { + console.error('Repeat One: playback failed to restart:', err); + }); + } } else { wasPlayingRef.current = false; } From f6562fb064aab3789c821119b05ea096a9324508 Mon Sep 17 00:00:00 2001 From: mallae-rpi Date: Wed, 22 Apr 2026 23:22:13 -0400 Subject: [PATCH 3/4] Fixed behavior such that attempting to seek from Song A to Song B while in Repeat One mode will transfer to repeating to Song B, instead of forcing the user back to the beginning of Song A --- src/audio/AudioEngine.ts | 3 +++ src/audio/TransportController.ts | 6 +++--- src/audio/types.ts | 1 + src/components/SetList.tsx | 14 +++++++++++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/audio/AudioEngine.ts b/src/audio/AudioEngine.ts index baaf726..92676b2 100644 --- a/src/audio/AudioEngine.ts +++ b/src/audio/AudioEngine.ts @@ -65,6 +65,9 @@ export class AudioEngine extends EventEmitter { () => this.playlistManager.getTotalDuration(), (state) => this.emit('stateChange', { state }), (time) => { + // emit seeked first so listeners can update their tracking refs + // before the subsequent songChange fires + this.emit('seeked', { time }); const entry = this.playlist.getEntryAtTime(time); if (entry) this.emit('songChange', { diff --git a/src/audio/TransportController.ts b/src/audio/TransportController.ts index deed3e0..86a82c0 100644 --- a/src/audio/TransportController.ts +++ b/src/audio/TransportController.ts @@ -70,14 +70,14 @@ export class TransportController { const totalDuration = this.getDuration(); this.pausedAt = Math.max(0, Math.min(time, totalDuration)); + // Notify before restarting the scheduler to ensure seeked fires before songChange + this.onSeek(this.pausedAt); + if (this.state === 'playing') { this.scheduler.stopAll(); this.contextTimeAtPlay = this.ctx.currentTime; this.transportTimeAtPlay = this.pausedAt; this.scheduler.start(this.pausedAt, this.contextTimeAtPlay); - } else { - // paused/stopped: playhead moves but no audio, notify so song title updates - this.onSeek(this.pausedAt); } } diff --git a/src/audio/types.ts b/src/audio/types.ts index 29fc3d7..56591c7 100644 --- a/src/audio/types.ts +++ b/src/audio/types.ts @@ -66,6 +66,7 @@ export type AudioEngineEvents = { timelineChange: { entries: ScheduledEntry[] }; playlistChange: { entries: PlaylistEntry[] }; error: { message: string; error?: unknown }; + seeked: { time: Seconds }; }; export type { ID, Seconds, Gain, Fade, FadeType }; diff --git a/src/components/SetList.tsx b/src/components/SetList.tsx index 943015d..5cb44e4 100644 --- a/src/components/SetList.tsx +++ b/src/components/SetList.tsx @@ -61,7 +61,7 @@ export default function SetList({ currentEntryIdRef.current !== null && entryId !== currentEntryIdRef.current ) { - // A genuine song transition occurred - seek back to the start of the previous song. + // A genuine automatic song transition occurred - seek back to the start of the previous song. const prev = engine.getTimeline().find((e) => e.entryId === currentEntryIdRef.current); if (prev) { engine.transport.seek(prev.absoluteStart); @@ -71,6 +71,17 @@ export default function SetList({ currentEntryIdRef.current = entryId; }); + // When the user seeks on the timeline, update currentEntryIdRef to the song at the new position + // to prevent Repeat One from interfering with intentional navigation + const unsubSeeked = engine.on('seeked', ({ time }) => { + const entry = engine + .getTimeline() + .find((e) => time >= e.absoluteStart && time < e.absoluteEnd); + if (entry) { + currentEntryIdRef.current = entry.entryId; + } + }); + const unsubState = engine.on('stateChange', ({ state }) => { if (state === 'playing') { wasPlayingRef.current = true; @@ -99,6 +110,7 @@ export default function SetList({ return () => { unsubSong(); + unsubSeeked(); unsubState(); }; // engine is a stable singleton - this runs exactly once on mount. From c0a054d32f4d00dbbb5500cc566210194817410c Mon Sep 17 00:00:00 2001 From: mallae-rpi Date: Thu, 23 Apr 2026 15:49:18 -0400 Subject: [PATCH 4/4] Capitalize title --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index d86371d..b9d532a 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - dj-amber + DJ-AMBER