1// Seeking, by section (SeekBar.jsx): jump the playing song to the start of 2// one of its sections. 3// 4// Back, or forward within what Jev has decided, is a jump: the scheduler 5// moves to the section's first cycle and the jev()s' playing time with it, 6// and the sections play again exactly as decided. 7// 8// Ahead of that there is nothing to jump to yet: Jev arranges the song as 9// it plays, each section told what came before. So the song buffers, like a 10// video seeking past what has loaded: the audio is held, Jev is asked about 11// each section up to the target in order, and then it jumps. If Jev ends 12// the song before the target, it lands on the section the song ends on. 13// 14// Sections are where it lands because that is where sung lines and held 15// notes begin: nothing is dropped into halfway through. 16// 17// Only the page's own song: one in the sandbox keeps its scheduler in the 18// frame, and a listening party runs on one clock for everyone. 19import { releaseSoundingVoices } from '@strudel/webaudio'; 20import { audioControls, pause, unpause } from './audioControls.mjs'; 21import { boothStore } from './boothStore.mjs'; 22import { decideThrough } from './jevCore.mjs'; 23import { lyricsSeeked } from './lyrics.mjs'; 24 25let state = { buffering: null }; // the segment being sought while Jev is asked 26const listeners = new Set(); 27function set(patch) { 28 state = { ...state, ...patch }; 29 for (const l of listeners) l(); 30} 31export const seekStore = { 32 subscribe(l) { 33 listeners.add(l); 34 return () => listeners.delete(l); 35 }, 36 get: () => state, 37}; 38 39// How many of the booth's sections are settled, counting from the first: 40// the song can jump anywhere before that without asking. 41export function decidedThrough(booth) { 42 let n = 0; 43 while (booth.decisions[n] && booth.decisions[n].status !== 'asking') n++; 44 return n - 1; 45} 46 47// The last section a seek may aim at: the song's last once known, else the 48// most the form allows. 49export const lastSection = (booth) => (booth.last !== null ? booth.last : booth.segments - 1); 50 51let seeks = 0; 52export async function seekToSection(editor, segment) { 53 const scheduler = editor?.repl?.scheduler; 54 const booth = boothStore.getSnapshot().booth; 55 if (!scheduler?.started || !booth?.jevs || !Number.isFinite(booth.segments)) return; 56 const mine = ++seeks; 57 let target = Math.max(0, Math.min(Math.floor(segment), lastSection(booth))); 58 if (target > decidedThrough(booth)) { 59 // buffer: hold the audio where it is while Jev decides up to there 60 const wasPaused = audioControls.get().paused; 61 set({ buffering: target }); 62 try { 63 await pause(); 64 target = await decideThrough(booth.jevs, target); 65 } finally { 66 if (mine === seeks) set({ buffering: null }); 67 } 68 // a newer seek, or a stop, took over while this one waited 69 if (mine !== seeks || !scheduler.started) return; 70 jump(scheduler, booth, target); 71 if (!wasPaused) await unpause(); 72 return; 73 } 74 jump(scheduler, booth, target); 75} 76 77function jump(scheduler, booth, segment) { 78 const cycle = segment * booth.every; 79 scheduler.seek(cycle); 80 for (const view of booth.jevs) view.seek?.(cycle); 81 // what was sounding belongs to where the song was 82 releaseSoundingVoices(); 83 lyricsSeeked(); 84}