From c55f61c2896e88f4313e15918eccdf31048ebb8f Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Wed, 10 Jun 2026 16:30:50 -0400 Subject: [PATCH 01/12] Begin implementing heading seek --- src/lib/data/audio.ts | 83 +++++++++++++++++++++++++++++++++++- src/lib/data/stores/audio.ts | 1 + 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 5cf001923..68cd41fba 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -30,6 +30,8 @@ const timings = import.meta.glob('./*', { base: '/src/gen-assets/timings' }) as Record; +const AUDIO_SEEK_THRESHOLD = 0.1; + const cache = new MRUCache(10); let currentAudioPlayer: AudioPlayer | undefined = undefined; audioPlayerStore.subscribe(async (value: AudioPlayer) => { @@ -94,6 +96,7 @@ function createAudio(audioSource: string): HTMLAudioElement { } return audio; } + //gets the current audio async function getAudio() { if (!currentAudioPlayer || currentAudioPlayer.loaded) { @@ -160,10 +163,86 @@ export function playStop() { // changes chapter export async function skip(direction: number) { + console.log('skip'); + const wasPlaying = currentAudioPlayer?.playing; pause(); - await refs.skip(direction); - playMode.reset(); + + if (!currentAudioPlayer?.loaded || !currentAudioPlayer.timing) { + await refs.skip(direction); + playMode.reset(); + return; + } + + if (!currentAudioPlayer?.headingMarkers) { + currentAudioPlayer.headingMarkers = getHeadingMarkers(); + audioPlayerStore.set(currentAudioPlayer); + } + + if ((direction < 0 && currentAudioPlayer.progress < AUDIO_SEEK_THRESHOLD) || + (direction >= 0 && (currentAudioPlayer.timing?.at(-1)?.endtime! - + currentAudioPlayer.progress) < AUDIO_SEEK_THRESHOLD) + ) { + await refs.skip(direction); + playMode.reset(); + return; + } + + for (let i = 1; i < currentAudioPlayer.headingMarkers.length - 1; i++) { + const marker = currentAudioPlayer.headingMarkers[i]; + if (currentAudioPlayer.progress < (marker + AUDIO_SEEK_THRESHOLD)) { + if (direction < 0) { + seek(currentAudioPlayer.headingMarkers[i - 1]); + break; + } else if (currentAudioPlayer.progress > marker) { + seek(currentAudioPlayer.headingMarkers[i + 1]); + break; + } else { + seek(marker); + break; + } + } + } + + updateHighlights(); + if (wasPlaying) { play(); } } + +function getHeadingMarkers() { + let headingMarkers = [0.0]; + + const headings = document.querySelectorAll('div.s'); + headings.forEach((h) => { + console.log(`found heading ${h.getHTML()}`); + let next = nextElementDFS(h); + while (next && !next?.getAttribute('data-verse')) { + next = nextElementDFS(next); + } + + // If defined this is the first verse immediately after the heading + const verse = next?.getAttribute('data-verse'); + console.log(`candidate verse from ${next?.getHTML()}: (${verse})`); + if (verse === null || verse === undefined) { return; } + + + const marker = currentAudioPlayer?.timing?.find((v) => v.tag.includes(verse))?.starttime; + console.log(marker); + if (marker) { + headingMarkers.push(marker); + } + }); + + headingMarkers.push(currentAudioPlayer?.timing?.at(-1)?.endtime || + currentAudioPlayer?.duration!); + + console.log(`final heading markers: ${headingMarkers}`); + + return headingMarkers; +} + +function nextElementDFS(e: Element) { + return e.firstElementChild || e.nextElementSibling || e.parentElement?.nextElementSibling; +} + // formats timing information export function format(seconds: number) { if (isNaN(seconds)) { diff --git a/src/lib/data/stores/audio.ts b/src/lib/data/stores/audio.ts index bfe19eaca..8527392fd 100644 --- a/src/lib/data/stores/audio.ts +++ b/src/lib/data/stores/audio.ts @@ -37,6 +37,7 @@ export interface AudioPlayer { playing: boolean; timeIndex: number; timing: Timing[] | null; + headingMarkers?: number[]; collection?: string; book?: string; chapter?: string; From f5402970e1c964ee7bbf1f6a88cacf29aa3bcb6d Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Wed, 10 Jun 2026 17:17:01 -0400 Subject: [PATCH 02/12] Fix audio seek to heading for all known cases --- src/lib/data/audio.ts | 90 +++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 68cd41fba..c42450a3d 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -30,7 +30,7 @@ const timings = import.meta.glob('./*', { base: '/src/gen-assets/timings' }) as Record; -const AUDIO_SEEK_THRESHOLD = 0.1; +const AUDIO_SEEK_THRESHOLD = 0.25; const cache = new MRUCache(10); let currentAudioPlayer: AudioPlayer | undefined = undefined; @@ -168,46 +168,66 @@ export async function skip(direction: number) { pause(); if (!currentAudioPlayer?.loaded || !currentAudioPlayer.timing) { - await refs.skip(direction); - playMode.reset(); - return; - } + if ( + direction < 0 && + currentAudioPlayer?.progress && + currentAudioPlayer.progress >= AUDIO_SEEK_THRESHOLD + ) { + seek(0); + if (wasPlaying) { + play(); + } + } else { + await refs.skip(direction); + playMode.reset(); + } + } else { + const headingMarkers = getHeadingMarkers(); - if (!currentAudioPlayer?.headingMarkers) { - currentAudioPlayer.headingMarkers = getHeadingMarkers(); - audioPlayerStore.set(currentAudioPlayer); - } + if (!currentAudioPlayer?.headingMarkers) { + currentAudioPlayer.headingMarkers = headingMarkers; + audioPlayerStore.set(currentAudioPlayer); + } - if ((direction < 0 && currentAudioPlayer.progress < AUDIO_SEEK_THRESHOLD) || - (direction >= 0 && (currentAudioPlayer.timing?.at(-1)?.endtime! - - currentAudioPlayer.progress) < AUDIO_SEEK_THRESHOLD) - ) { - await refs.skip(direction); - playMode.reset(); - return; - } + if ( + (direction < 0 && currentAudioPlayer.progress < AUDIO_SEEK_THRESHOLD) || + (direction >= 0 && currentAudioPlayer.progress >= headingMarkers?.at(-2)!) + ) { + console.log( + `skipping at chapter bookend: ${currentAudioPlayer.progress} >= ${headingMarkers?.at(-2)!}` + ); + await refs.skip(direction); + playMode.reset(); + return; + } - for (let i = 1; i < currentAudioPlayer.headingMarkers.length - 1; i++) { - const marker = currentAudioPlayer.headingMarkers[i]; - if (currentAudioPlayer.progress < (marker + AUDIO_SEEK_THRESHOLD)) { - if (direction < 0) { - seek(currentAudioPlayer.headingMarkers[i - 1]); - break; - } else if (currentAudioPlayer.progress > marker) { - seek(currentAudioPlayer.headingMarkers[i + 1]); - break; - } else { - seek(marker); + for (let i = 1; i < currentAudioPlayer.headingMarkers.length; i++) { + const marker = currentAudioPlayer.headingMarkers[i]; + console.log(`checking marker ${marker}`); + if (currentAudioPlayer.progress < marker + AUDIO_SEEK_THRESHOLD) { + if (direction < 0) { + seek(currentAudioPlayer.headingMarkers[i - 1]); + } else if (currentAudioPlayer.progress > marker) { + seek(currentAudioPlayer.headingMarkers[i + 1]); + } else { + seek(marker); + } + if (wasPlaying) { + play(); + } break; } } } - updateHighlights(); - if (wasPlaying) { play(); } + updateTime(); } function getHeadingMarkers() { + if (currentAudioPlayer?.headingMarkers) { + return currentAudioPlayer.headingMarkers; + } + let headingMarkers = [0.0]; const headings = document.querySelectorAll('div.s'); @@ -221,8 +241,9 @@ function getHeadingMarkers() { // If defined this is the first verse immediately after the heading const verse = next?.getAttribute('data-verse'); console.log(`candidate verse from ${next?.getHTML()}: (${verse})`); - if (verse === null || verse === undefined) { return; } - + if (verse === null || verse === undefined) { + return; + } const marker = currentAudioPlayer?.timing?.find((v) => v.tag.includes(verse))?.starttime; console.log(marker); @@ -231,8 +252,9 @@ function getHeadingMarkers() { } }); - headingMarkers.push(currentAudioPlayer?.timing?.at(-1)?.endtime || - currentAudioPlayer?.duration!); + headingMarkers.push( + currentAudioPlayer?.timing?.at(-1)?.endtime || currentAudioPlayer?.duration! + ); console.log(`final heading markers: ${headingMarkers}`); From 1bc22147353dc008fb80933bfb8c6c5becd68f5e Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Wed, 10 Jun 2026 18:16:46 -0400 Subject: [PATCH 03/12] Fix lint errors and edge cases --- src/lib/data/audio.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index c42450a3d..81d31cd1e 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -189,9 +189,16 @@ export async function skip(direction: number) { audioPlayerStore.set(currentAudioPlayer); } + let finalIntermediateMarker; + if (headingMarkers.length > 1) { + finalIntermediateMarker = headingMarkers.at(-2); + } + if ( (direction < 0 && currentAudioPlayer.progress < AUDIO_SEEK_THRESHOLD) || - (direction >= 0 && currentAudioPlayer.progress >= headingMarkers?.at(-2)!) + (direction >= 0 && + finalIntermediateMarker && + currentAudioPlayer.progress >= finalIntermediateMarker) ) { console.log( `skipping at chapter bookend: ${currentAudioPlayer.progress} >= ${headingMarkers?.at(-2)!}` @@ -207,7 +214,10 @@ export async function skip(direction: number) { if (currentAudioPlayer.progress < marker + AUDIO_SEEK_THRESHOLD) { if (direction < 0) { seek(currentAudioPlayer.headingMarkers[i - 1]); - } else if (currentAudioPlayer.progress > marker) { + } else if ( + i < currentAudioPlayer.headingMarkers.length - 1 && + currentAudioPlayer.progress > marker + ) { seek(currentAudioPlayer.headingMarkers[i + 1]); } else { seek(marker); @@ -228,7 +238,7 @@ function getHeadingMarkers() { return currentAudioPlayer.headingMarkers; } - let headingMarkers = [0.0]; + const headingMarkers = [0.0]; const headings = document.querySelectorAll('div.s'); headings.forEach((h) => { @@ -252,9 +262,10 @@ function getHeadingMarkers() { } }); - headingMarkers.push( - currentAudioPlayer?.timing?.at(-1)?.endtime || currentAudioPlayer?.duration! - ); + const endMarker = currentAudioPlayer?.timing?.at(-1)?.endtime || currentAudioPlayer?.duration; + if (typeof endMarker === 'number') { + headingMarkers.push(endMarker); + } console.log(`final heading markers: ${headingMarkers}`); From 88b86118b2f6dd1d39fa9b90922e136bedda1572 Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Thu, 11 Jun 2026 10:22:26 -0400 Subject: [PATCH 04/12] Fix formatting, adjust seek threshold --- src/lib/data/audio.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 81d31cd1e..38daf54da 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -30,7 +30,7 @@ const timings = import.meta.glob('./*', { base: '/src/gen-assets/timings' }) as Record; -const AUDIO_SEEK_THRESHOLD = 0.25; +const AUDIO_SEEK_THRESHOLD = 2.0; const cache = new MRUCache(10); let currentAudioPlayer: AudioPlayer | undefined = undefined; @@ -96,7 +96,6 @@ function createAudio(audioSource: string): HTMLAudioElement { } return audio; } - //gets the current audio async function getAudio() { if (!currentAudioPlayer || currentAudioPlayer.loaded) { @@ -163,7 +162,6 @@ export function playStop() { // changes chapter export async function skip(direction: number) { - console.log('skip'); const wasPlaying = currentAudioPlayer?.playing; pause(); @@ -200,9 +198,6 @@ export async function skip(direction: number) { finalIntermediateMarker && currentAudioPlayer.progress >= finalIntermediateMarker) ) { - console.log( - `skipping at chapter bookend: ${currentAudioPlayer.progress} >= ${headingMarkers?.at(-2)!}` - ); await refs.skip(direction); playMode.reset(); return; @@ -210,7 +205,6 @@ export async function skip(direction: number) { for (let i = 1; i < currentAudioPlayer.headingMarkers.length; i++) { const marker = currentAudioPlayer.headingMarkers[i]; - console.log(`checking marker ${marker}`); if (currentAudioPlayer.progress < marker + AUDIO_SEEK_THRESHOLD) { if (direction < 0) { seek(currentAudioPlayer.headingMarkers[i - 1]); @@ -242,7 +236,6 @@ function getHeadingMarkers() { const headings = document.querySelectorAll('div.s'); headings.forEach((h) => { - console.log(`found heading ${h.getHTML()}`); let next = nextElementDFS(h); while (next && !next?.getAttribute('data-verse')) { next = nextElementDFS(next); @@ -250,13 +243,13 @@ function getHeadingMarkers() { // If defined this is the first verse immediately after the heading const verse = next?.getAttribute('data-verse'); - console.log(`candidate verse from ${next?.getHTML()}: (${verse})`); if (verse === null || verse === undefined) { return; } + // find() always returns the first element it matches, so + // this will locate the beginning of the first phrase of the verse const marker = currentAudioPlayer?.timing?.find((v) => v.tag.includes(verse))?.starttime; - console.log(marker); if (marker) { headingMarkers.push(marker); } @@ -267,8 +260,6 @@ function getHeadingMarkers() { headingMarkers.push(endMarker); } - console.log(`final heading markers: ${headingMarkers}`); - return headingMarkers; } From cafcc4a0ec9a9c98f6791765c770bc1985bf0173 Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Thu, 11 Jun 2026 11:23:28 -0400 Subject: [PATCH 05/12] Fix seek to zero not updating highlights --- src/lib/data/audio.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 38daf54da..61580932d 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -503,10 +503,10 @@ async function updateTime() { return; } currentAudioPlayer.progress = currentAudioPlayer.audio?.currentTime ?? 0; - if (!currentAudioPlayer.timing && currentAudioPlayer.progress) { + if (!currentAudioPlayer.timing) { audioPlayerStore.set(currentAudioPlayer); } - if (currentAudioPlayer.timing && currentAudioPlayer.progress) { + if (currentAudioPlayer.timing) { updateHighlights(); } await handlePlayMode(); From 765087f759c3326e5570ea06288e75247342f44b Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Thu, 11 Jun 2026 12:52:45 -0400 Subject: [PATCH 06/12] Fix audio heading time retrieval for deeply nested HTML --- src/lib/data/audio.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 61580932d..a36878c0f 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -238,8 +238,10 @@ function getHeadingMarkers() { headings.forEach((h) => { let next = nextElementDFS(h); while (next && !next?.getAttribute('data-verse')) { + console.log(`searching ${next.outerHTML}...`); next = nextElementDFS(next); } + console.log(`found ${next}`); // If defined this is the first verse immediately after the heading const verse = next?.getAttribute('data-verse'); @@ -260,11 +262,22 @@ function getHeadingMarkers() { headingMarkers.push(endMarker); } + console.log(`headingMarkers: ${headingMarkers}`); + return headingMarkers; } function nextElementDFS(e: Element) { - return e.firstElementChild || e.nextElementSibling || e.parentElement?.nextElementSibling; + let next = e.firstElementChild || e.nextElementSibling; + if ((next instanceof Element)) { + return next; + } + + let ancestor = e.parentElement; + while (ancestor instanceof Element && !ancestor.nextElementSibling) { + ancestor = ancestor.parentElement; + } + return ancestor?.nextElementSibling || null; } // formats timing information From 75d9e6db377ab024b4b2d9fe8cba5a64f15d9d53 Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Fri, 12 Jun 2026 11:49:55 -0400 Subject: [PATCH 07/12] Fix forward skip not advancing more than one heading while paused --- src/lib/data/audio.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index a36878c0f..495549f0b 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -203,17 +203,22 @@ export async function skip(direction: number) { return; } + + console.log(`(1) skip: currentAudioPlayer: progress is now ${currentAudioPlayer.progress}`); for (let i = 1; i < currentAudioPlayer.headingMarkers.length; i++) { const marker = currentAudioPlayer.headingMarkers[i]; if (currentAudioPlayer.progress < marker + AUDIO_SEEK_THRESHOLD) { if (direction < 0) { + console.log(`skip: seeking to previous heading`) seek(currentAudioPlayer.headingMarkers[i - 1]); } else if ( i < currentAudioPlayer.headingMarkers.length - 1 && - currentAudioPlayer.progress > marker + currentAudioPlayer.progress >= marker ) { + console.log(`skip: seeking to next heading`) seek(currentAudioPlayer.headingMarkers[i + 1]); } else { + console.log(`skip: seeking to current heading`) seek(marker); } if (wasPlaying) { @@ -238,10 +243,10 @@ function getHeadingMarkers() { headings.forEach((h) => { let next = nextElementDFS(h); while (next && !next?.getAttribute('data-verse')) { - console.log(`searching ${next.outerHTML}...`); + // console.log(`searching ${next.outerHTML}...`); next = nextElementDFS(next); } - console.log(`found ${next}`); + // console.log(`found ${next}`); // If defined this is the first verse immediately after the heading const verse = next?.getAttribute('data-verse'); From aeb82b8d71dfb52922f1d562283d66c2554dc4d5 Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Fri, 12 Jun 2026 11:55:32 -0400 Subject: [PATCH 08/12] Fix audio highlight showing in chapter with timing before audio has been played --- src/lib/data/audio.ts | 5 ++++- src/lib/data/stores/audio.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 495549f0b..ba2643973 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -524,7 +524,7 @@ async function updateTime() { if (!currentAudioPlayer.timing) { audioPlayerStore.set(currentAudioPlayer); } - if (currentAudioPlayer.timing) { + if (currentAudioPlayer.hasPlayed && currentAudioPlayer.timing) { updateHighlights(); } await handlePlayMode(); @@ -588,6 +588,9 @@ export function play() { } if (!currentAudioPlayer.playing) { + if (!currentAudioPlayer.hasPlayed) { + currentAudioPlayer.hasPlayed = true; + } currentAudioPlayer.audio?.play(); currentAudioPlayer.playStart = Date.now(); logAudioPlay(currentAudioPlayer); diff --git a/src/lib/data/stores/audio.ts b/src/lib/data/stores/audio.ts index 8527392fd..d68311274 100644 --- a/src/lib/data/stores/audio.ts +++ b/src/lib/data/stores/audio.ts @@ -35,6 +35,7 @@ export interface AudioPlayer { duration: number; progress: number; playing: boolean; + hasPlayed: boolean; timeIndex: number; timing: Timing[] | null; headingMarkers?: number[]; @@ -50,6 +51,7 @@ export const audioPlayerDefault = { duration: 0, progress: 0, playing: false, + hasPlayed: false, timeIndex: 0, timing: null, timer: null, From e818caef0cf2095bcc189ae5cd31043d3cdad878 Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Fri, 12 Jun 2026 13:27:41 -0400 Subject: [PATCH 09/12] Remove added debug logging --- src/lib/data/audio.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index ba2643973..117e11db0 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -204,21 +204,17 @@ export async function skip(direction: number) { } - console.log(`(1) skip: currentAudioPlayer: progress is now ${currentAudioPlayer.progress}`); for (let i = 1; i < currentAudioPlayer.headingMarkers.length; i++) { const marker = currentAudioPlayer.headingMarkers[i]; if (currentAudioPlayer.progress < marker + AUDIO_SEEK_THRESHOLD) { if (direction < 0) { - console.log(`skip: seeking to previous heading`) seek(currentAudioPlayer.headingMarkers[i - 1]); } else if ( i < currentAudioPlayer.headingMarkers.length - 1 && currentAudioPlayer.progress >= marker ) { - console.log(`skip: seeking to next heading`) seek(currentAudioPlayer.headingMarkers[i + 1]); } else { - console.log(`skip: seeking to current heading`) seek(marker); } if (wasPlaying) { @@ -243,10 +239,8 @@ function getHeadingMarkers() { headings.forEach((h) => { let next = nextElementDFS(h); while (next && !next?.getAttribute('data-verse')) { - // console.log(`searching ${next.outerHTML}...`); next = nextElementDFS(next); } - // console.log(`found ${next}`); // If defined this is the first verse immediately after the heading const verse = next?.getAttribute('data-verse'); @@ -267,8 +261,6 @@ function getHeadingMarkers() { headingMarkers.push(endMarker); } - console.log(`headingMarkers: ${headingMarkers}`); - return headingMarkers; } From 693fbf3e12055bb51f86e2fa30638db975f58ef4 Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Fri, 12 Jun 2026 13:29:40 -0400 Subject: [PATCH 10/12] Run formatter --- src/lib/data/audio.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index 117e11db0..b86b86967 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -203,7 +203,6 @@ export async function skip(direction: number) { return; } - for (let i = 1; i < currentAudioPlayer.headingMarkers.length; i++) { const marker = currentAudioPlayer.headingMarkers[i]; if (currentAudioPlayer.progress < marker + AUDIO_SEEK_THRESHOLD) { @@ -266,7 +265,7 @@ function getHeadingMarkers() { function nextElementDFS(e: Element) { let next = e.firstElementChild || e.nextElementSibling; - if ((next instanceof Element)) { + if (next instanceof Element) { return next; } From f69ee81f62e8f5acbb551b7a7b5dc723438cc33b Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Fri, 12 Jun 2026 13:59:12 -0400 Subject: [PATCH 11/12] Handle chapters without headings properly --- src/lib/data/audio.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index b86b86967..c9b8bab66 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -187,16 +187,17 @@ export async function skip(direction: number) { audioPlayerStore.set(currentAudioPlayer); } - let finalIntermediateMarker; + let finalIntermediateMarker: number | undefined; if (headingMarkers.length > 1) { - finalIntermediateMarker = headingMarkers.at(-2); + // Don't treat the initial marker at 0 as intermediate + finalIntermediateMarker = headingMarkers.at(-2) || undefined; } if ( (direction < 0 && currentAudioPlayer.progress < AUDIO_SEEK_THRESHOLD) || (direction >= 0 && - finalIntermediateMarker && - currentAudioPlayer.progress >= finalIntermediateMarker) + (!finalIntermediateMarker || + currentAudioPlayer.progress >= finalIntermediateMarker)) ) { await refs.skip(direction); playMode.reset(); @@ -264,7 +265,7 @@ function getHeadingMarkers() { } function nextElementDFS(e: Element) { - let next = e.firstElementChild || e.nextElementSibling; + const next = e.firstElementChild || e.nextElementSibling; if (next instanceof Element) { return next; } @@ -512,10 +513,10 @@ async function updateTime() { return; } currentAudioPlayer.progress = currentAudioPlayer.audio?.currentTime ?? 0; - if (!currentAudioPlayer.timing) { + if (!currentAudioPlayer.timing && currentAudioPlayer.hasPlayed) { audioPlayerStore.set(currentAudioPlayer); } - if (currentAudioPlayer.hasPlayed && currentAudioPlayer.timing) { + if (currentAudioPlayer.timing && currentAudioPlayer.hasPlayed) { updateHighlights(); } await handlePlayMode(); From ce7e2b467c3034d0ad4c1ddd0d78d62377906dec Mon Sep 17 00:00:00 2001 From: benpaj-cps Date: Fri, 12 Jun 2026 15:34:23 -0400 Subject: [PATCH 12/12] Add comments, error warn for seek to heading --- src/lib/data/audio.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/lib/data/audio.ts b/src/lib/data/audio.ts index c9b8bab66..f6d4170b1 100644 --- a/src/lib/data/audio.ts +++ b/src/lib/data/audio.ts @@ -160,7 +160,15 @@ export function playStop() { } } -// changes chapter +/** + * If there are headings and timing data in the current chapter, + * advances the audio to the nearest heading in the given direction. + * Otherwise, moves to the nearest chapter start time in the + * given direction (including the start of the current chapter) + * + * @param direction the direction in which to skip (backwards if negative, + * forwards otherwise) + */ export async function skip(direction: number) { const wasPlaying = currentAudioPlayer?.playing; pause(); @@ -171,6 +179,8 @@ export async function skip(direction: number) { currentAudioPlayer?.progress && currentAudioPlayer.progress >= AUDIO_SEEK_THRESHOLD ) { + // If there are no timings, and we are seeking backwards, and the + // audio has advanced beyond the threshold, then go to beginning seek(0); if (wasPlaying) { play(); @@ -187,6 +197,7 @@ export async function skip(direction: number) { audioPlayerStore.set(currentAudioPlayer); } + // Locate the last marker before the end of the track let finalIntermediateMarker: number | undefined; if (headingMarkers.length > 1) { // Don't treat the initial marker at 0 as intermediate @@ -228,6 +239,13 @@ export async function skip(direction: number) { updateTime(); } +/** + * Returns an array of numbers. The first number is the beginning of the current + * audio track (always at 0.0), the last number is the end of the + * last verse of the track, and any intermediate numbers are the start times + * of each verse immediately after a heading corresponding to a `\s` tag + * in the source UFSM. + */ function getHeadingMarkers() { if (currentAudioPlayer?.headingMarkers) { return currentAudioPlayer.headingMarkers; @@ -242,7 +260,7 @@ function getHeadingMarkers() { next = nextElementDFS(next); } - // If defined this is the first verse immediately after the heading + // If present this is the first verse immediately after the heading const verse = next?.getAttribute('data-verse'); if (verse === null || verse === undefined) { return; @@ -259,11 +277,17 @@ function getHeadingMarkers() { const endMarker = currentAudioPlayer?.timing?.at(-1)?.endtime || currentAudioPlayer?.duration; if (typeof endMarker === 'number') { headingMarkers.push(endMarker); + } else { + console.error('getHeadingMarkers: failed to locate end of current audio track'); } return headingMarkers; } +/** + * Returns the next Element after `e` in a pre-order DFS traversal of the DOM. + * @param e the element from which to perform the traversal step + */ function nextElementDFS(e: Element) { const next = e.firstElementChild || e.nextElementSibling; if (next instanceof Element) {