Media

Audio waveform

A WAV file's waveform for an audio player: the loudness of each slice from 0 to 1, or drawn as an SVG of bars.

Version 1.0.0 · 3 functions · 1 crate

#audio#players#svg

Functions

What use audio_wave.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use audio_wave_peaks, audio_wave_svg from audio_wave (Quillo 0.6).

audio_wave_peaks(path: str, buckets: int) → listor an error

The waveform as buckets numbers (1 to 10,000), each the loudest point of its slice from 0 (silence) to 1 (full scale): give them to a player that draws its own.

audio_wave_svg(path: str, options: any) → stror an error

The waveform drawn as SVG bars, mirrored about the middle: options (or null) width and height in pixels (600 × 80), bars (100), color ("#2a78d6"), and normalize true (the default) to scale the loudest bar to the full height.

audio_wave_info(path: str) → anyor an error

The file's length and loudest point: { duration, peak }, seconds and 0 to 1.

Get it

$ quillo install audio_wave               # audio_wave.qwer into this folder$ quillo install audio_wave --example     # and its example, audio_wave.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/audio_wave.qwer$ curl -O https://quillolang.org/plugins/files/audio_wave.qwe$ curl -O https://quillolang.org/plugins/files/audio_wave_sample.wav

Then, in your program, all of it or only what you call:

main.qwe
use audio_wave.qweruse audio_wave_peaks, audio_wave_svg from audio_wave      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
hound = "3.5"

Example

audio_wave.qwe
// A voice-note player: the waveform drawn under the audio, and the numbers for a JavaScript player.////   quillo run audio_wave.qwe      then open http://localhost:8080/use "audio_wave.qwer"let NOTE = "audio_wave_sample.wav"route GET /note.wav = file(NOTE)route GET /peaks = { info: audio_wave_info(NOTE)?, peaks: audio_wave_peaks(NOTE, 20)? }route GET /wave.svg = respond(200, audio_wave_svg(NOTE, { width: 400, height: 60, bars: 80 })?, { "content-type": "image/svg+xml" })route GET / = <html><body style="font-family: sans-serif">    <h1>Voice note</h1>    <div>{raw(audio_wave_svg(NOTE, { width: 400, height: 60, bars: 80, color: "#6250d6" })?)}</div>    <audio controls src="/note.wav"></audio></body></html>
$ quillo run audio_wave.qwe$ curl 'localhost:8080/peaks'$ curl 'localhost:8080/wave.svg'

Source

audio_wave.qwer
//! A WAV file's waveform for an audio player: the loudness of each slice as numbers from 0 to 1, or drawn as an SVG of bars, light enough to send with the page.//! version: 1.0.0//!//! [dependencies]//! hound = "3.5"use serde_json::{json, Value};const MAX_FRAMES: u64 = 3 * 3600 * 192_000;// The peak (largest sample, all channels together) of each of `buckets` equal slices, from 0 to 1.fn peaks(path: &str, buckets: i64) -> Result<(Vec<f64>, f64), String> {    let mut r = hound::WavReader::open(path).map_err(|e| format!("{}: {}", path, e))?;    let s = r.spec();    let frames = r.duration() as u64;    if frames > MAX_FRAMES {        return Err(format!("{}: longer than 3 hours", path));    }    let n = buckets.clamp(1, 10_000) as usize;    let ch = s.channels.max(1) as u64;    let full = if s.sample_format == hound::SampleFormat::Float { 1.0 } else { (1u64 << (s.bits_per_sample.clamp(1, 32) - 1)) as f64 };    let mut out = vec![0f64; n];    let per = (frames as f64 / n as f64).max(1.0);    let mut i = 0u64;    let mut put = |v: f64, i: u64| {        let b = (((i / ch) as f64 / per) as usize).min(n - 1);        let a = (v / full).abs().min(1.0);        if a > out[b] {            out[b] = a;        }    };    match s.sample_format {        hound::SampleFormat::Float => for x in r.samples::<f32>() { put(x.map_err(|e| e.to_string())? as f64, i); i += 1 },        hound::SampleFormat::Int => for x in r.samples::<i32>() { put(x.map_err(|e| e.to_string())? as f64, i); i += 1 },    }    Ok((out.into_iter().map(|p| (p * 1000.0).round() / 1000.0).collect(), frames as f64 / s.sample_rate as f64))}/// The waveform as `buckets` numbers (1 to 10,000), each the loudest point of its slice from 0 (silence) to 1 (full scale): give them to a player that draws its own.pub fn audio_wave_peaks(path: &str, buckets: i64) -> Result<Vec<f64>, String> {    Ok(peaks(path, buckets)?.0)}/// The waveform drawn as SVG bars, mirrored about the middle: `options` (or null) `width` and `height` in pixels (600 × 80), `bars` (100), `color` ("#2a78d6"),/// and `normalize` true (the default) to scale the loudest bar to the full height.pub fn audio_wave_svg(path: &str, options: serde_json::Value) -> Result<String, String> {    let num = |k: &str, d: f64, lo: f64, hi: f64| options.get(k).and_then(Value::as_f64).unwrap_or(d).clamp(lo, hi);    let (w, h) = (num("width", 600.0, 10.0, 10_000.0), num("height", 80.0, 4.0, 2000.0));    let n = num("bars", 100.0, 1.0, 2000.0) as i64;    let col = options.get("color").and_then(Value::as_str).filter(|c| c.starts_with('#') && c.len() <= 9 && c[1..].chars().all(|x| x.is_ascii_hexdigit())).unwrap_or("#2a78d6");    let (p, secs) = peaks(path, n)?;    let top = if options.get("normalize").and_then(Value::as_bool) != Some(false) { p.iter().cloned().fold(0.0, f64::max).max(1e-6) } else { 1.0 };    let slot = w / n as f64;    let bw = (slot * 0.7).max(1.0);    let mut out = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img"><title>Waveform, {:.1} seconds</title><path fill="{col}" d=""#, secs);    for (i, v) in p.iter().enumerate() {        let bh = ((v / top) * h).max(1.0);        out += &format!("M{:.1} {:.1}h{:.1}v{:.1}h-{:.1}z", i as f64 * slot + (slot - bw) / 2.0, (h - bh) / 2.0, bw, bh, bw);    }    out += "\"/></svg>";    Ok(out)}/// The file's length and loudest point: `{ duration, peak }`, seconds and 0 to 1.pub fn audio_wave_info(path: &str) -> Result<Value, String> {    let (p, secs) = peaks(path, 1)?;    Ok(json!({ "duration": (secs * 1000.0).round() / 1000.0, "peak": p[0] }))}
Audio info audio_info.qwer · 1.0.0An audio file's length, bitrate, sample rate, tags and cover art, for MP3, FLAC, M4A, Ogg, WAV and AIFF.audiofilesuploadspodcasts6 views · 8 downloadsAudio tags audio_tags.qwer · 1.0.0Audio tags written as well as read, for MP3, FLAC, Ogg, M4A, WAV and AIFF: title, artist, album, track, year, genre, composer, lyrics, BPM and more, cover art set, and every tag stripped; written to a copy.audiofilespodcastsprivacy8 views · 7 downloadsCUE sheets cue_sheet.qwer · 1.0.0CUE sheets read and written: REM comments, CATALOG, FILE lines, tracks with their titles, performers, ISRC, flags, PREGAP and POSTGAP, and indexes in mm:ss:ff at 75 frames a second, kept as exact frames. Each track's length given the audio's total, several files included, old Latin-1 and UTF-16 sheets read, and damage or oddities named by line.audiocdtrackschapters2 views · 7 downloadsGraphviz graphviz.qwer · 1.0.0Graphs in Graphviz's DOT language laid out and drawn as SVG, with no Graphviz installed: flowcharts, org charts and state machines.diagramssvggraphsdocs7 views · 8 downloadsInitials avatars avatar_initials.qwer · 1.0.0Round avatars with a person's initials as SVG, the colour from the name so each keeps theirs, the text readable on it.avatarsuserssvg6 views · 7 downloadsLoudness loudness.qwer · 1.0.0Loudness of WAV audio as broadcasters measure it (EBU R128): integrated LUFS, loudness range and true peak; silences found; leading and trailing silence trimmed; and files normalised to -16, -14 or -23 LUFS under a peak limit.audiopodcastssoundsfiles7 views · 9 downloads
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On Audio waveform: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.