dioxus-audio 0.1.1

Audio recording, playback, analysis, and UI components for Dioxus
Documentation
use dioxus::prelude::*;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use std::cell::Cell;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use std::rc::Rc;

use crate::analysis::{AnalysisDomain, AudioAnalyser};

#[component]
pub fn LiveWaveform(
    analyser: ReadSignal<Option<AudioAnalyser>>,
    #[props(default = false)] processing: bool,
    #[props(default = 32)] bars: usize,
    #[props(default)] label: Option<String>,
) -> Element {
    LiveVisualizer(
        analyser,
        AnalysisDomain::Waveform,
        processing,
        bars,
        label.unwrap_or_else(|| "Live audio waveform".to_string()),
    )
}

#[component]
pub fn SpectrumVisualizer(
    analyser: ReadSignal<Option<AudioAnalyser>>,
    #[props(default = false)] processing: bool,
    #[props(default = 32)] bars: usize,
    #[props(default)] label: Option<String>,
) -> Element {
    LiveVisualizer(
        analyser,
        AnalysisDomain::Spectrum,
        processing,
        bars,
        label.unwrap_or_else(|| "Live audio spectrum".to_string()),
    )
}

#[allow(non_snake_case)]
fn LiveVisualizer(
    analyser: ReadSignal<Option<AudioAnalyser>>,
    domain: AnalysisDomain,
    processing: bool,
    bars: usize,
    label: String,
) -> Element {
    let bars = bars.clamp(1, 128);
    let values = use_live_values(analyser, domain, processing, bars);

    rsx! {
        div {
            class: "dioxus-audio dioxus-audio__visualizer",
            role: "img",
            aria_label: label,
            "data-domain": match domain {
                AnalysisDomain::Waveform => "waveform",
                AnalysisDomain::Spectrum => "spectrum",
            },
            for (index, value) in values().iter().enumerate() {
                {
                    let bar_height = (value * 100.0).clamp(4.0, 100.0);
                    rsx! {
                        div {
                            key: "{index}",
                            class: "dioxus-audio__visualizer-bar",
                            style: "height: {bar_height}%",
                        }
                    }
                }
            }
        }
    }
}

#[component]
pub fn LevelMeter(
    analyser: ReadSignal<Option<AudioAnalyser>>,
    #[props(default)] label: Option<String>,
) -> Element {
    let level = use_live_level(analyser);
    let percentage = (level() * 100.0).clamp(0.0, 100.0);

    rsx! {
        div {
            class: "dioxus-audio dioxus-audio__meter",
            role: "meter",
            aria_label: label.unwrap_or_else(|| "Microphone level".to_string()),
            aria_valuemin: "0",
            aria_valuemax: "100",
            aria_valuenow: "{percentage:.0}",
            div {
                class: "dioxus-audio__meter-fill",
                style: "width: {percentage}%",
            }
        }
    }
}

fn use_live_values(
    analyser: ReadSignal<Option<AudioAnalyser>>,
    domain: AnalysisDomain,
    processing: bool,
    bars: usize,
) -> ReadSignal<Vec<f32>> {
    let parameters = use_memo(use_reactive!(|(analyser, processing, bars)| (
        analyser, processing, bars
    )));
    #[allow(unused_mut)]
    let mut values = use_signal(|| vec![0.04; bars]);
    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
    let generation = use_hook(|| Rc::new(Cell::new(0_u64)));

    use_effect(move || {
        let (analyser, processing, bars) = parameters();
        values.set(vec![0.04; bars]);
        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
        let current_generation = {
            let current = generation.get().wrapping_add(1);
            generation.set(current);
            current
        };
        let has_analyser = analyser().is_some();
        if !has_analyser && !processing {
            return;
        }
        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
        {
            let generation = generation.clone();
            spawn(async move {
                let mut tick = 0.0_f32;
                loop {
                    if generation.get() != current_generation {
                        break;
                    }
                    let next = if let Some(analyser) = analyser() {
                        reduce_samples(&analyser.read(domain), bars, domain)
                    } else if processing {
                        tick += 0.18;
                        (0..bars)
                            .map(|index| {
                                let phase = index as f32 / bars as f32 * std::f32::consts::TAU;
                                (0.3 + (phase + tick).sin().abs() * 0.55).clamp(0.04, 1.0)
                            })
                            .collect()
                    } else {
                        vec![0.04; bars]
                    };
                    values.set(next);
                    gloo_timers::future::TimeoutFuture::new(50).await;
                }
            });
        }

        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
        {
            let _ = (analyser, domain, processing, bars);
        }
    });

    values.into()
}

fn use_live_level(analyser: ReadSignal<Option<AudioAnalyser>>) -> ReadSignal<f32> {
    let analyser_input = use_memo(use_reactive!(|(analyser,)| analyser));
    #[allow(unused_mut)]
    let mut level = use_signal(|| 0.0_f32);
    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
    let generation = use_hook(|| Rc::new(Cell::new(0_u64)));

    use_effect(move || {
        let analyser = analyser_input();
        level.set(0.0);
        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
        let current_generation = {
            let current = generation.get().wrapping_add(1);
            generation.set(current);
            current
        };
        if analyser().is_none() {
            return;
        }
        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
        {
            let generation = generation.clone();
            spawn(async move {
                loop {
                    if generation.get() != current_generation {
                        break;
                    }
                    level.set(analyser().map(|value| value.level()).unwrap_or(0.0));
                    gloo_timers::future::TimeoutFuture::new(50).await;
                }
            });
        }

        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
        {
            let _ = analyser;
        }
    });

    level.into()
}

#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
fn reduce_samples(samples: &[f32], bars: usize, domain: AnalysisDomain) -> Vec<f32> {
    if samples.is_empty() {
        return vec![0.04; bars];
    }
    let bucket_count = samples.len().min(bars);
    let mut result: Vec<f32> = (0..bucket_count)
        .map(|index| {
            let start = index * samples.len() / bucket_count;
            let end = (index + 1) * samples.len() / bucket_count;
            samples[start..end]
                .iter()
                .map(|sample| match domain {
                    AnalysisDomain::Waveform => sample.abs(),
                    AnalysisDomain::Spectrum => *sample,
                })
                .fold(0.0_f32, f32::max)
                .clamp(0.04, 1.0)
        })
        .collect();
    result.resize(bars, 0.04);
    result
}