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