Skip to main content

dioxus_audio/components/
visualizer.rs

1use dioxus::prelude::*;
2
3use crate::analysis::{
4    AnalysisDomain, AudioAnalyser, use_live_analysis_domain, use_live_analysis_level,
5};
6#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
7use crate::analysis::{
8    LiveAnalysisOptions, run_live_analysis_schedule, use_live_analysis_scheduler,
9};
10
11#[component]
12pub fn LiveWaveform(
13    analyser: ReadSignal<Option<AudioAnalyser>>,
14    #[props(default = false)] processing: bool,
15    #[props(default = 32)] bars: usize,
16    #[props(default)] label: Option<String>,
17) -> Element {
18    LiveVisualizer(
19        analyser,
20        AnalysisDomain::Waveform,
21        processing,
22        bars,
23        label.unwrap_or_else(|| "Live audio waveform".to_string()),
24    )
25}
26
27#[component]
28pub fn SpectrumVisualizer(
29    analyser: ReadSignal<Option<AudioAnalyser>>,
30    #[props(default = false)] processing: bool,
31    #[props(default = 32)] bars: usize,
32    #[props(default)] label: Option<String>,
33) -> Element {
34    LiveVisualizer(
35        analyser,
36        AnalysisDomain::Spectrum,
37        processing,
38        bars,
39        label.unwrap_or_else(|| "Live audio spectrum".to_string()),
40    )
41}
42
43#[allow(non_snake_case)]
44fn LiveVisualizer(
45    analyser: ReadSignal<Option<AudioAnalyser>>,
46    domain: AnalysisDomain,
47    processing: bool,
48    bars: usize,
49    label: String,
50) -> Element {
51    let bars = bars.clamp(1, 128);
52    let samples = use_live_analysis_domain(analyser, domain);
53    let has_samples = samples.read().is_some();
54    let processing_values = use_processing_values(processing && !has_samples, bars);
55    let values = samples()
56        .map(|samples| reduce_samples(&samples, bars, domain))
57        .unwrap_or_else(|| {
58            if processing {
59                processing_values()
60            } else {
61                vec![0.04; bars]
62            }
63        });
64
65    rsx! {
66        div {
67            class: "dioxus-audio dioxus-audio__visualizer",
68            role: "img",
69            aria_label: label,
70            "data-domain": match domain {
71                AnalysisDomain::Waveform => "waveform",
72                AnalysisDomain::Spectrum => "spectrum",
73            },
74            for (index, value) in values.iter().enumerate() {
75                {
76                    let bar_height = (value * 100.0).clamp(4.0, 100.0);
77                    rsx! {
78                        div {
79                            key: "{index}",
80                            class: "dioxus-audio__visualizer-bar",
81                            style: "height: {bar_height}%",
82                        }
83                    }
84                }
85            }
86        }
87    }
88}
89
90#[component]
91pub fn LevelMeter(
92    analyser: ReadSignal<Option<AudioAnalyser>>,
93    #[props(default)] label: Option<String>,
94) -> Element {
95    let level = use_live_analysis_level(analyser);
96    let percentage = (level().unwrap_or(0.0) * 100.0).clamp(0.0, 100.0);
97
98    rsx! {
99        div {
100            class: "dioxus-audio dioxus-audio__meter",
101            role: "meter",
102            aria_label: label.unwrap_or_else(|| "Microphone level".to_string()),
103            aria_valuemin: "0",
104            aria_valuemax: "100",
105            aria_valuenow: "{percentage:.0}",
106            div {
107                class: "dioxus-audio__meter-fill",
108                style: "width: {percentage}%",
109            }
110        }
111    }
112}
113
114fn use_processing_values(processing: bool, bars: usize) -> ReadSignal<Vec<f32>> {
115    let parameters = use_memo(use_reactive!(|(processing, bars)| (processing, bars)));
116    #[allow(unused_mut)]
117    let mut values = use_signal(|| vec![0.04; bars]);
118    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
119    let scheduler = use_live_analysis_scheduler();
120
121    use_effect(move || {
122        let (processing, bars) = parameters();
123        values.set(vec![0.04; bars]);
124        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
125        let current_generation = scheduler.next_generation();
126        if !processing {
127            return;
128        }
129        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
130        {
131            let scheduler = scheduler.clone();
132            let mut tick = 0.0_f32;
133            spawn(run_live_analysis_schedule(
134                scheduler,
135                current_generation,
136                LiveAnalysisOptions::default().cadence(),
137                move || {
138                    tick += 0.18;
139                    let next = (0..bars)
140                        .map(|index| {
141                            let phase = index as f32 / bars as f32 * std::f32::consts::TAU;
142                            (0.3 + (phase + tick).sin().abs() * 0.55).clamp(0.04, 1.0)
143                        })
144                        .collect();
145                    values.set(next);
146                    true
147                },
148            ));
149        }
150
151        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
152        {
153            let _ = (processing, bars);
154        }
155    });
156
157    values.into()
158}
159
160fn reduce_samples(samples: &[f32], bars: usize, domain: AnalysisDomain) -> Vec<f32> {
161    if samples.is_empty() {
162        return vec![0.04; bars];
163    }
164    let bucket_count = samples.len().min(bars);
165    let mut result: Vec<f32> = (0..bucket_count)
166        .map(|index| {
167            let start = index * samples.len() / bucket_count;
168            let end = (index + 1) * samples.len() / bucket_count;
169            samples[start..end]
170                .iter()
171                .map(|sample| match domain {
172                    AnalysisDomain::Waveform => sample.abs(),
173                    AnalysisDomain::Spectrum => *sample,
174                })
175                .fold(0.0_f32, f32::max)
176                .clamp(0.04, 1.0)
177        })
178        .collect();
179    result.resize(bars, 0.04);
180    result
181}