Waveform: decode peaks in-renderer via Web Audio (no external ffmpeg needed)
The packaged app doesn't bundle ffmpeg, so on Macs without a system ffmpeg the waveform stayed blank. Compute peaks by reading the file bytes (new file:read IPC) and decoding with Web Audio's decodeAudioData in a low-rate AudioContext; ffmpeg in main remains only as a fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
c98ebda03f
commit
0c2f2b8190
3 files changed
+47
-6
No files matched your search
@@ -134,6 +134,13 @@ function transcodeToMp4(input, output, opts) {
|
||||
});
|
||||
}
|
||||
|
||||
// Read raw file bytes for the renderer (used to decode waveforms via Web Audio,
|
||||
// which needs no external ffmpeg). Returns null if unreadable.
|
||||
ipcMain.handle('file:read', async (_e, filePath) => {
|
||||
try { return await fs.promises.readFile(filePath); }
|
||||
catch (e) { return null; }
|
||||
});
|
||||
|
||||
// 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();
|
||||
|
||||
+2
-1
@@ -35,5 +35,6 @@ contextBridge.exposeInMainWorld('djv', {
|
||||
savePlaylist: (data) => ipcRenderer.invoke('playlist:save', data),
|
||||
exportPlaylist: (data) => ipcRenderer.invoke('playlist:export', data),
|
||||
importPlaylist: () => ipcRenderer.invoke('playlist:import'),
|
||||
peaks: (path, buckets) => ipcRenderer.invoke('audio:peaks', path, buckets)
|
||||
peaks: (path, buckets) => ipcRenderer.invoke('audio:peaks', path, buckets),
|
||||
readFile: (path) => ipcRenderer.invoke('file:read', path)
|
||||
});
|
||||
+38
-5
@@ -206,14 +206,47 @@ const peaksCache = {}; // path -> Array(N) of 0..1 peak amplitudes (or 'lo
|
||||
const WAVE_BUCKETS = 400;
|
||||
let playCur = 0, playDur = 0; // live progress of the currently playing track
|
||||
|
||||
// Lazily fetch the waveform peaks for a file, then redraw its card.
|
||||
// A low-rate AudioContext used only to decode files into waveform peaks.
|
||||
let _peakCtx = null;
|
||||
function peakCtx() {
|
||||
if (!_peakCtx) {
|
||||
const AC = window.AudioContext || window.webkitAudioContext;
|
||||
try { _peakCtx = new AC({ sampleRate: 8000 }); } catch (e) { _peakCtx = new AC(); }
|
||||
}
|
||||
return _peakCtx;
|
||||
}
|
||||
function peaksFromAudioBuffer(buf, buckets) {
|
||||
const ch = buf.getChannelData(0);
|
||||
const n = ch.length, per = Math.max(1, Math.floor(n / buckets));
|
||||
const peaks = new Array(buckets).fill(0);
|
||||
let top = 0.0001;
|
||||
for (let b = 0; b < buckets; b++) {
|
||||
let m = 0; const s = b * per, e = Math.min(n, s + per);
|
||||
for (let i = s; i < e; i++) { const v = ch[i] < 0 ? -ch[i] : ch[i]; if (v > m) m = v; }
|
||||
peaks[b] = m; if (m > top) top = m;
|
||||
}
|
||||
for (let b = 0; b < buckets; b++) peaks[b] = Math.min(1, peaks[b] / top);
|
||||
return peaks;
|
||||
}
|
||||
// Decode a file's waveform in the renderer via Web Audio (no external ffmpeg).
|
||||
function decodePeaksWebAudio(path) {
|
||||
return djv.readFile(path).then((bytes) => {
|
||||
if (!bytes || !bytes.byteLength) throw new Error('no bytes');
|
||||
const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
return peakCtx().decodeAudioData(ab);
|
||||
}).then((audioBuf) => peaksFromAudioBuffer(audioBuf, WAVE_BUCKETS));
|
||||
}
|
||||
|
||||
// Lazily compute the waveform peaks for a file, then redraw its card. Tries
|
||||
// Web Audio first (works everywhere); falls back to ffmpeg in main if present.
|
||||
function ensurePeaks(path) {
|
||||
if (!path || peaksCache[path]) return;
|
||||
peaksCache[path] = 'loading';
|
||||
djv.peaks(path, WAVE_BUCKETS).then((p) => {
|
||||
peaksCache[path] = p && p.length ? p : null;
|
||||
renderPlaylist();
|
||||
}).catch(() => { peaksCache[path] = null; });
|
||||
decodePeaksWebAudio(path)
|
||||
.then((p) => { peaksCache[path] = p; renderPlaylist(); })
|
||||
.catch(() => djv.peaks(path, WAVE_BUCKETS)
|
||||
.then((p) => { peaksCache[path] = (p && p.length) ? p : null; renderPlaylist(); })
|
||||
.catch(() => { peaksCache[path] = null; }));
|
||||
}
|
||||
|
||||
// Draw a waveform on a canvas, shading the trimmed-out parts and the playhead.
|
||||
|
||||
Reference in new issue
Block a user