From 9ea1eea3ea91f6fe194df616e2a79faf2707add5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= Date: Mon, 19 Sep 2022 22:27:20 +0200 Subject: [PATCH] feat: if `audiowaveform` is present, generate & cache peaks on backend requires https://github.com/bbc/audiowaveform/ to be installed and on $PATH --- src/previews/audio.rs | 91 ++++++++++++------- .../display/blobs/AudioViewer.svelte | 12 ++- 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/previews/audio.rs b/src/previews/audio.rs index 29b99f7..150f283 100644 --- a/src/previews/audio.rs +++ b/src/previews/audio.rs @@ -14,41 +14,68 @@ const COLOR: &str = "#dc322f"; // solarized red impl<'a> Previewable for AudioPath<'a> { fn get_thumbnail(&self, options: HashMap) -> Result>> { - let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?; + match options.get("type").map(|x| x.as_str()) { + Some("json") => { + let outfile = tempfile::Builder::new().suffix(".json").tempfile()?; - let color = options - .get("color") - .map(String::to_owned) - .unwrap_or_else(|| COLOR.into()); - let dimensions = options - .get("dimensions") - .map(String::to_owned) - .unwrap_or_else(|| "860x256".into()); + // -i long_clip.mp3 -o long_clip.json --pixels-per-second 20 --bits 8 + let audiowaveform_cmd = Command::new("audiowaveform") + .args(["-i", &self.0.to_string_lossy()]) + .args(["-o", &*outfile.path().to_string_lossy()]) + .args(["--pixels-per-second", "20"]) + .args(["--bits", "8"]) + .output()?; - let thumbnail_cmd = Command::new("ffmpeg") - .args(["-i", &self.0.to_string_lossy()]) - .args([ - "-filter_complex", - &format!( - "[0:a]aformat=channel_layouts=mono, compand=gain=-2, - showwavespic=s={dimensions}:colors={color}, - drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={color}" - ), - ]) - .args(["-vframes", "1"]) - .arg(&*outfile.path().to_string_lossy()) - .arg("-y") - .output()?; + if !audiowaveform_cmd.status.success() { + return Err(anyhow!( + "Failed to retrieve file duration: {:?}", + String::from_utf8_lossy(&audiowaveform_cmd.stderr) + )); + } - if !thumbnail_cmd.status.success() { - return Err(anyhow!( - "Failed to render thumbnail: {:?}", - String::from_utf8_lossy(&thumbnail_cmd.stderr) - )); + let mut buffer = Vec::new(); + outfile.as_file().read_to_end(&mut buffer)?; + Ok(Some(buffer)) + } + Some("image") | None => { + let outfile = tempfile::Builder::new().suffix(".webp").tempfile()?; + + let color = options + .get("color") + .map(String::to_owned) + .unwrap_or_else(|| COLOR.into()); + let dimensions = options + .get("dimensions") + .map(String::to_owned) + .unwrap_or_else(|| "860x256".into()); + + let thumbnail_cmd = Command::new("ffmpeg") + .args(["-i", &self.0.to_string_lossy()]) + .args([ + "-filter_complex", + &format!( + "[0:a]aformat=channel_layouts=mono, compand=gain=-2, + showwavespic=s={dimensions}:colors={color}, + drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=1:color={color}" + ), + ]) + .args(["-vframes", "1"]) + .arg(&*outfile.path().to_string_lossy()) + .arg("-y") + .output()?; + + if !thumbnail_cmd.status.success() { + return Err(anyhow!( + "Failed to render thumbnail: {:?}", + String::from_utf8_lossy(&thumbnail_cmd.stderr) + )); + } + + let mut buffer = Vec::new(); + outfile.as_file().read_to_end(&mut buffer)?; + Ok(Some(buffer)) + } + Some(_) => Err(anyhow!("type has to be one of: image, json")), } - - let mut buffer = Vec::new(); - outfile.as_file().read_to_end(&mut buffer)?; - Ok(Some(buffer)) } } diff --git a/webui/src/components/display/blobs/AudioViewer.svelte b/webui/src/components/display/blobs/AudioViewer.svelte index 3bf6720..ce38f48 100644 --- a/webui/src/components/display/blobs/AudioViewer.svelte +++ b/webui/src/components/display/blobs/AudioViewer.svelte @@ -147,6 +147,7 @@ responsive: true, backend: "MediaElement", mediaControls: true, + normalize: true, xhr: { cache: "force-cache" }, plugins: [ TimelinePlugin.default.create({ @@ -202,7 +203,16 @@ setTimeout(() => wavesurfer.setCurrentTime(region.start)); }); - wavesurfer.load(`${API_URL}/raw/${address}`); + try { + const peaksReq = await fetch(`${API_URL}/thumb/${address}?type=json`); + const peaks = await peaksReq.json(); + wavesurfer.load(`${API_URL}/raw/${address}`, peaks.data); + } catch (e) { + console.warn( + `Failed to load peaks from server (${e}), falling back to client-side render...` + ); + wavesurfer.load(`${API_URL}/raw/${address}`); + } });