Playlist: per-track start/end trim, total/elapsed/remaining times, waveform cards

- Per-track start/end points (mm:ss inputs, 'set to current' buttons, draggable
  handles on the waveform). Trim applied via audio.setTrim (seek to start, stop
  at end) and pushed live to the playing track.
- Wider cards showing elapsed / total / remaining for the trimmed range.
- Waveform rendered per track: peaks computed in main via ffmpeg (mono 8kHz PCM
  -> 400 normalized buckets, cached), drawn on a canvas with trim region +
  playhead. New audio:peaks IPC + djv.peaks preload binding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.8 committed 2026-06-22 16:36:54 +02:00
1 parent a8973619e6
commit 882dd4643a
6 files changed
+249 -18

No files matched your search

+27
View File
@@ -34,6 +34,9 @@ class AudioEngine {
this.stream = null; // MediaStream when using live input
this.mode = 'none';
this.onEnded = null; // callback fired when a (non-looping) track ends
this.trimStart = 0; // playback start point (s); seek here on load
this.trimEnd = 0; // playback end point (s); 0 = play to the natural end
this._trimFired = false;
this._videoSrc = null; // persistent source for the playlist video element
this._videoGain = null;
this._videoSrcEl = null;
@@ -68,6 +71,28 @@ class AudioEngine {
resume() { if (this.ctx.state === 'suspended') this.ctx.resume(); }
// Trim the current/next track to [start, end] seconds (end 0 = natural end).
setTrim(start, end) {
this.trimStart = Math.max(0, start || 0);
this.trimEnd = Math.max(0, end || 0);
this._trimFired = false;
}
// Seek a freshly-loaded media element to the trim start (waits for metadata).
seekToTrimStart(el) {
const go = () => { if (this.trimStart > 0) { try { el.currentTime = this.trimStart; } catch (e) {} } };
if (el.readyState >= 1) go(); else el.addEventListener('loadedmetadata', go, { once: true });
}
// Called every frame from the output loop: stop at the trim end point.
checkTrim() {
const el = this.mediaEl;
if (!el || el.paused || this._trimFired) return;
if (this.trimEnd > 0 && el.currentTime >= this.trimEnd) {
this._trimFired = true;
try { el.pause(); } catch (e) {}
if (this.onEnded) this.onEnded();
}
}
// Stop any current audio/video source but keep the context & analyser alive
// (meters read 0, recording keep-alive continues). Used for visual-only
// playlist interludes.
@@ -128,8 +153,10 @@ class AudioEngine {
this.sourceGain = g;
this.stream = null;
this.mode = 'file';
this._trimFired = false;
await el.play();
this.seekToTrimStart(el);
return el;
}