Changeset e679bce in Klonkt for src/assets/js/audio-player.js


Ignore:
Timestamp:
08/22/2026 09:54:51 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
8bb3b09
Parents:
16fd4fe
Message:

Het bandje speelt als bandje: een cassette, een object, spoelen in seconden

Robins correctie (21-8): vooruit en achteruit deden hier next()/prev(), en dat
is een playlistgebaar. Dan is het een lijst met een cassetteplaatje erboven.
Een cassette kent geen "volgend nummer" -- je houdt de knop ingedrukt, de band
loopt door, en je laat los waar je bent.

Dat kan omdat audio-player.js een wachtrij al in EEN doorlopende
MediaSource-tijdlijn giet: "een trackwissel is een positie, geen omschakeling".
Een bandje IS die tijdlijn. Dus:

  • seekBy(seconden) op de speler, over de nummergrenzen heen. Op de blob-motor (iOS, geen MSE) bestaat die tijdlijn niet; daar loopt spoelen tot de rand van het nummer en stapt dan naar de buur. Grover, maar eerlijker dan doen alsof.
  • Bandmodus: de speler toont het BANDJE als titel met het lopende nummer eronder, en de teller loopt over de hele band in plaats van per nummer. Dat is wat "een object, geen losse tracks" betekent op het scherm.
  • Vasthouden versnelt (4x tot 16x), loslaten stopt -- ook als je buiten de knop loslaat. Een korte tik spoelt vijf seconden, want vasthouden is met een spatiebalk geen gebaar.

Een echte cassette, als SVG. Verhouding 100,4 x 63,8 mm, de maat van een
compact cassette, dus viewBox 314x200: behuizing, labelvlak met de titel erop,
venster, twee spoelen met tandjes die meedraaien, de bandpakketten, vier
schroefjes en de openingen voor de kop en de capstans. De vorige versie waren
twee CSS-schijfjes.

Twee dingen die alleen door het echt te draaien boven kwamen:

  • audio-player.js kiest zijn knoppen met een lijst KLASSENAMEN (PLAY_SELECTOR), niet met een regel over data-attributen. De cassetteknop droeg alle juiste data-pcms-* en deed niets. Er staat nu een test op.
  • De sessie sloeg displayTimes().cur op als trackpositie. In bandmodus is dat de bandteller, dus een hersteld bandje sprong naar een plek die in dat nummer niet bestaat. Daarvoor is trackTijd() afgesplitst.

MOD_V 53, style.css v107, audio-player v35.

Co-Authored-By: Claude Opus 5 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/assets/js/audio-player.js

    r16fd4fe re679bce  
    158158  let isPlaying = false;
    159159  let albumName = '';
     160  // BANDMODUS (Robins eis, 21-8): een mixtape is EEN object, geen wachtrij met
     161  // nummers. De MSE-keten hieronder maakt van een wachtrij toch al een
     162  // doorlopende tijdlijn -- "een trackwissel is een positie, geen omschakeling"
     163  // -- dus een bandje is precies die tijdlijn, alleen anders getoond en anders
     164  // bediend: je ziet de titel van het bandje, de teller loopt over het geheel,
     165  // en spoelen gaat in seconden in plaats van per nummer.
     166  let tapeMode = false;
    160167  // Playback pipeline. We fetch each track's bytes ourselves (X-Audio-Player
    161168  // gate; no plain media URL is ever exposed to the page) and feed them to the
     
    457464  // Current position/duration in TRACK coordinates (the MSE timeline is the
    458465  // whole chain; the UI always shows the single playing track).
    459   function displayTimes() {
     466  // De positie BINNEN het huidige nummer. Apart van displayTimes(), want die
     467  // geeft in bandmodus de teller over de hele band -- en het opslaan van de
     468  // sessie heeft juist de trackpositie nodig: bij het herstellen begint de
     469  // keten opnieuw en start dit nummer weer op nul.
     470  function trackTijd() {
    460471    if (useMse() && chain.length) {
    461472      const seg = currentSegment();
     
    463474    }
    464475    return { cur: audio.currentTime || 0, dur: audio.duration };
     476  }
     477
     478  function displayTimes() {
     479    // Een bandje heeft een teller, geen nummerpositie: hij telt door over de
     480    // kant heen. Dat is letterlijk de ketentijdlijn, dus hier juist NIET
     481    // terugrekenen naar trackcoordinaten.
     482    if (tapeMode && useMse() && chain.length) {
     483      return { cur: audio.currentTime || 0, dur: chain[chain.length - 1].end };
     484    }
     485    return trackTijd();
    465486  }
    466487
     
    505526  // session metadata. Called from loadTrack AND from the gapless boundary-cross.
    506527  function updateTrackChrome(t) {
    507     titleEl.textContent  = t.title  || 'Untitled';
    508     artistEl.textContent = t.artist || '';
    509     sheetTitle.textContent  = t.title  || 'Untitled';
    510     sheetArtist.textContent = t.artist || '';
     528    // In bandmodus staat het BANDJE op de speler. Wat er op dit moment klinkt
     529    // staat eronder, zoals een cassette een titel op het label heeft en de
     530    // nummers op het doosje.
     531    const hoofd = tapeMode ? (albumName || 'Mixtape') : (t.title || 'Untitled');
     532    const onder = tapeMode ? (t.title || '') : (t.artist || '');
     533    titleEl.textContent  = hoofd;
     534    artistEl.textContent = onder;
     535    sheetTitle.textContent  = hoofd;
     536    sheetArtist.textContent = onder;
    511537    sheetAlbum.textContent  = albumName || '';
    512538    setCoverImage(cover,      t.cover);
     
    579605    queue = Array.isArray(tracks) ? tracks.slice() : [];
    580606    albumName = (opts && opts.albumName) || '';
     607    tapeMode = !!(opts && opts.asTape);
    581608    if (!queue.length) return;
    582609    loadTrack(typeof startIdx === 'number' ? Math.max(0, Math.min(startIdx, queue.length - 1)) : 0, true);
     
    629656    queue = [];
    630657    albumName = '';
     658    tapeMode = false;
    631659    loadSeq++;  // cancel any in-flight load
    632660    dropPreload();
     
    10101038  // would leave dead buttons (flag baked into the snapshot, listener gone). One
    10111039  // delegated listener works regardless of how many times the DOM is (re)swapped.
     1040  // WELKE KNOPPEN DEZE SPELER BEDIENT. Let op: dit is een LIJST MET NAMEN, geen
     1041  // regel over data-attributen. Een nieuwe knop die keurig data-pcms-track-url
     1042  // en data-pcms-album-id draagt doet dus niets zolang hij hier niet bij staat.
     1043  // Precies daar liep de cassetteknop op vast (21-8): de opmaak klopte, de
     1044  // gegevens klopten, en er gebeurde niets.
    10121045  const PLAY_SELECTOR =
    1013     '.post-audio-track .pat-play, .post-album-tracks .pat-row, .post-album-cover-btn, .post-album-playall';
     1046    '.post-audio-track .pat-play, .post-album-tracks .pat-row, .post-album-cover-btn, .post-album-playall, .tape-btn--play';
    10141047  document.body.addEventListener('click', (e) => {
    10151048    const btn = e.target.closest(PLAY_SELECTOR);
     
    10381071        // (cover-btn and playall both want to start from the beginning).
    10391072        const startIdx = trackUrl ? tracks.findIndex(t => t.url === trackUrl) : 0;
    1040         setQueue(tracks, startIdx >= 0 ? startIdx : 0, { albumName: album.dataset.pcmsAlbumTitle || '' });
     1073        // Een mixtape gaat als EEN object de speler in: de titel van het bandje
     1074        // op de speler, de teller over de hele band, en spoelen in seconden.
     1075        // De soort staat al op het blok (data-pcms-album-kind), dus hier is het
     1076        // een doorgeefje en geen tweede plek die iets afleidt.
     1077        setQueue(tracks, startIdx >= 0 ? startIdx : 0, {
     1078          albumName: album.dataset.pcmsAlbumTitle || '',
     1079          asTape: album.dataset.pcmsAlbumKind === 'mixtape',
     1080        });
    10411081      } catch(err) { console.error('[pcms-audio] bad album JSON', err, album.dataset.pcmsAlbum); }
    10421082    } else if (trackData) {
     
    10921132  // 9. Public API
    10931133  // ============================================================
     1134  /**
     1135   * SPOELEN, in seconden over de hele band.
     1136   *
     1137   * Op de MSE-motor is dit precies wat een cassette doet: `audio.currentTime`
     1138   * IS de tijdlijn van de hele keten, dus over een nummergrens heen spoelen is
     1139   * gewoon doortellen. Geen trackwissel, geen nieuwe afspeelsessie.
     1140   *
     1141   * Op de blob-motor (iOS Safari, geen MSE) bestaat die doorlopende tijdlijn
     1142   * niet: daar is elk nummer een eigen bron. Spoelen loopt daar dus tot de rand
     1143   * van het huidige nummer en stapt dan naar de buur. Grover, maar het is
     1144   * eerlijker dan doen alsof de band doorloopt terwijl hij dat niet doet.
     1145   */
     1146  function seekBy(seconden) {
     1147    const d = Number(seconden) || 0;
     1148    if (!d || !audio) return;
     1149    if (useMse() && chain.length) {
     1150      const eind = chain[chain.length - 1].end;
     1151      const doel = Math.max(0, Math.min((audio.currentTime || 0) + d, Math.max(0, eind - 0.25)));
     1152      try { audio.currentTime = doel; } catch (e) { /* buffer nog niet zover */ }
     1153      updatePositionState();
     1154      return;
     1155    }
     1156    // Blob-motor: binnen het nummer blijven, en anders naar de buur.
     1157    const duur = audio.duration || 0;
     1158    const nu = audio.currentTime || 0;
     1159    if (duur && nu + d >= duur) { next(); return; }
     1160    if (nu + d < 0) { prev(); return; }
     1161    try { audio.currentTime = Math.max(0, nu + d); } catch (e) {}
     1162  }
     1163
     1164  /** Waar staat de teller, over het hele bandje. */
     1165  function tapeTijden() { return displayTimes(); }
     1166
    10941167  window.pcmsAudioPlayer = {
    1095     setQueue, play, pause, next, prev, close, openSheet, closeSheet,
     1168    setQueue, play, pause, next, prev, close, openSheet, closeSheet, seekBy, tapeTijden,
     1169    isTape: () => tapeMode,
    10961170    isPlaying: () => isPlaying,
    10971171    currentTrack: () => queue[currentIndex] || null,
     
    11131187      if (!queue.length) { sessionStorage.removeItem(PLAYER_STATE_KEY); return; }
    11141188      sessionStorage.setItem(PLAYER_STATE_KEY, JSON.stringify({
    1115         queue, currentIndex, albumName,
     1189        queue, currentIndex, albumName, tapeMode,
    11161190        // In-TRACK position (the MSE timeline spans the whole chain; a restore
    11171191        // starts a fresh chain where this track begins at 0).
    1118         time: displayTimes().cur || 0,
     1192        time: trackTijd().cur || 0,
    11191193        playing: !!audio.src && !audio.paused,
    11201194      }));
     
    11401214    queue = s.queue;
    11411215    albumName = s.albumName || '';
     1216    tapeMode = !!s.tapeMode;
    11421217    pendingSeek = s.time || 0;
    11431218    const idx = Math.max(0, Math.min(s.currentIndex || 0, queue.length - 1));
Note: See TracChangeset for help on using the changeset viewer.