Skip to main content

dioxus_audio/components/
visualizer.rs

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