dioxus_audio/components/
visualizer.rs1use 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}