diff --git a/web/src/audio.ts b/web/src/audio.ts index b0bad562..8fcf7775 100644 --- a/web/src/audio.ts +++ b/web/src/audio.ts @@ -18,6 +18,36 @@ import { showToast } from "./toast"; type AudioChapter = { index: number; title: string; start: number; end: number }; +// One playable file of an audiobook. Multi-file books (folder/zip +// layouts) carry several; single-file books carry exactly one spanning +// the whole file. start/end are GLOBAL seconds — the same coordinate +// system as chapters, bookmarks and audio_offset_seconds. +export type AudioTrack = { url: string; start: number; end: number; title?: string }; + +// trackIndexFor finds the track holding a global position: the last one +// whose start is at or before it (zero-duration/unknown-end tracks keep +// working because only start matters for membership). +export function trackIndexFor(tracks: AudioTrack[], globalSeconds: number): number { + for (let i = tracks.length - 1; i > 0; i -= 1) { + if (globalSeconds >= tracks[i].start) return i; + } + return 0; +} + +// localOffsetFor converts a global position to seconds within track i. +export function localOffsetFor(tracks: AudioTrack[], index: number, globalSeconds: number): number { + const track = tracks[index]; + if (!track) return Math.max(0, globalSeconds); + return Math.max(0, globalSeconds - track.start); +} + +// globalSecondsFor converts (track i, local seconds) back to the global +// position — the inverse of localOffsetFor. +export function globalSecondsFor(tracks: AudioTrack[], index: number, localSeconds: number): number { + const track = tracks[index]; + return (track ? track.start : 0) + Math.max(0, localSeconds); +} + const PROGRESS_INTERVAL_MS = 30_000; interface AudioConfig { @@ -57,6 +87,17 @@ Alpine.data("audioReader", () => ({ scrubbing: false, scrubValue: 0, + // Track manifest: one AudioTrack per playable file. Single-file books + // (m4b/mp3) carry exactly one track whose URL is the whole file, so + // every code path below can treat playback as (track, local seconds) + // and map through globalSecondsFor/localOffsetFor. + fileUrl: "", + tracks: [] as AudioTrack[], + activeTrack: 0, + // Local seconds to apply to the