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

+31
View File
@@ -134,6 +134,37 @@ function transcodeToMp4(input, output, opts) {
});
}
// Decode a media file to mono 8 kHz PCM via ffmpeg and reduce it to `buckets`
// peak amplitudes (0..1) for drawing a waveform. Returns null on failure.
const peaksCache = new Map();
ipcMain.handle('audio:peaks', async (_e, filePath, buckets) => {
buckets = buckets || 400;
const key = filePath + '|' + buckets;
if (peaksCache.has(key)) return peaksCache.get(key);
const args = ['-v', 'quiet', '-i', filePath, '-ac', '1', '-ar', '8000', '-f', 's16le', '-'];
return new Promise((resolve) => {
execFile(ffmpegPath(), args, { encoding: 'buffer', maxBuffer: 1 << 28 }, (err, stdout) => {
if (err || !stdout || stdout.length < 4) { resolve(null); return; }
const u8 = Uint8Array.from(stdout);
const samples = new Int16Array(u8.buffer, 0, u8.length >> 1);
const n = samples.length;
const peaks = new Array(buckets).fill(0);
const per = Math.max(1, Math.floor(n / buckets));
for (let b = 0; b < buckets; b++) {
let max = 0;
const s = b * per, e = Math.min(n, s + per);
for (let i = s; i < e; i++) { const v = samples[i] < 0 ? -samples[i] : samples[i]; if (v > max) max = v; }
peaks[b] = max / 32768;
}
// Normalize so the loudest peak fills the height.
const top = Math.max(0.0001, ...peaks);
for (let b = 0; b < buckets; b++) peaks[b] = Math.min(1, peaks[b] / top);
peaksCache.set(key, peaks);
resolve(peaks);
});
});
});
function recordingsDir() {
const dir = path.join(app.getPath('videos'), 'DJ Visualizer');
fs.mkdirSync(dir, { recursive: true });