1use dioxus::prelude::*;
4#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
5use std::cell::Cell;
6use std::fmt;
7#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
8use std::rc::{Rc, Weak};
9use std::sync::Arc;
10use std::time::Duration;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct AnalysisMetadata {
15 sample_rate: f32,
16 fft_size: u32,
17 min_decibels: f32,
18 max_decibels: f32,
19 smoothing: f64,
20}
21
22impl AnalysisMetadata {
23 pub fn new(
24 sample_rate: f32,
25 fft_size: u32,
26 min_decibels: f32,
27 max_decibels: f32,
28 smoothing: f64,
29 ) -> Self {
30 Self {
31 sample_rate,
32 fft_size,
33 min_decibels,
34 max_decibels,
35 smoothing,
36 }
37 }
38
39 pub fn sample_rate(self) -> f32 {
41 self.sample_rate
42 }
43
44 pub fn fft_size(self) -> u32 {
46 self.fft_size
47 }
48
49 pub fn frequency_bin_count(self) -> u32 {
51 self.fft_size / 2
52 }
53
54 pub fn frequency_bin_width(self) -> f32 {
56 if self.fft_size == 0 {
57 0.0
58 } else {
59 self.sample_rate / self.fft_size as f32
60 }
61 }
62
63 pub fn frequency_for_bin(self, bin: u32) -> Option<f32> {
65 (bin < self.frequency_bin_count()).then(|| bin as f32 * self.frequency_bin_width())
66 }
67
68 pub fn min_decibels(self) -> f32 {
69 self.min_decibels
70 }
71
72 pub fn max_decibels(self) -> f32 {
73 self.max_decibels
74 }
75
76 pub fn decibels_for_frequency_value(self, value: f32) -> f32 {
78 self.min_decibels + value.clamp(0.0, 1.0) * (self.max_decibels - self.min_decibels)
79 }
80
81 pub fn smoothing(self) -> f64 {
83 self.smoothing
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct LiveAnalysisOptions {
90 cadence: Duration,
91}
92
93impl LiveAnalysisOptions {
94 pub const MIN_CADENCE: Duration = Duration::from_millis(16);
95 pub const MAX_CADENCE: Duration = Duration::from_secs(1);
96
97 pub fn with_cadence(mut self, cadence: Duration) -> Self {
99 self.cadence = cadence.clamp(Self::MIN_CADENCE, Self::MAX_CADENCE);
100 self
101 }
102
103 pub fn cadence(self) -> Duration {
104 self.cadence
105 }
106}
107
108impl Default for LiveAnalysisOptions {
109 fn default() -> Self {
110 Self {
111 cadence: Duration::from_millis(50),
112 }
113 }
114}
115
116pub fn rms_level(samples: &[f32]) -> f32 {
118 if samples.is_empty() {
119 return 0.0;
120 }
121
122 let mean_square =
123 samples.iter().map(|sample| sample * sample).sum::<f32>() / samples.len() as f32;
124 mean_square.sqrt().clamp(0.0, 1.0)
125}
126
127#[derive(Clone, Debug, PartialEq)]
135pub struct LiveAnalysisSnapshot {
136 time_domain: Arc<[f32]>,
137 frequency_domain: Arc<[f32]>,
138 level: f32,
139 metadata: AnalysisMetadata,
140}
141
142impl LiveAnalysisSnapshot {
143 pub fn time_domain(&self) -> &[f32] {
144 &self.time_domain
145 }
146
147 pub fn frequency_domain(&self) -> &[f32] {
148 &self.frequency_domain
149 }
150
151 pub fn level(&self) -> f32 {
152 self.level
153 }
154
155 pub fn metadata(&self) -> AnalysisMetadata {
156 self.metadata
157 }
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum AnalysisDomain {
162 Waveform,
163 Spectrum,
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
168pub struct AnalysisUnavailable;
169
170impl fmt::Display for AnalysisUnavailable {
171 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172 formatter.write_str("Analysis is unavailable")
173 }
174}
175
176impl std::error::Error for AnalysisUnavailable {}
177
178#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
180#[derive(Clone)]
181pub struct AudioAnalyser {
182 inner: Weak<AudioAnalyserInner>,
183}
184
185#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
186struct AudioAnalyserInner {
187 node: web_sys::AnalyserNode,
188 sample_rate: f32,
189 available: Cell<bool>,
190}
191
192#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
193pub(crate) struct AudioAnalyserControl {
194 inner: Rc<AudioAnalyserInner>,
195}
196
197#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
198impl fmt::Debug for AudioAnalyser {
199 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200 formatter
201 .debug_struct("AudioAnalyser")
202 .field("available", &self.is_available())
203 .finish_non_exhaustive()
204 }
205}
206
207#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
208impl PartialEq for AudioAnalyser {
209 fn eq(&self, other: &Self) -> bool {
210 self.inner.ptr_eq(&other.inner)
211 }
212}
213
214#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
215impl AudioAnalyserControl {
216 pub(crate) fn new(node: web_sys::AnalyserNode, sample_rate: f32) -> (Self, AudioAnalyser) {
217 let inner = Rc::new(AudioAnalyserInner {
218 node,
219 sample_rate,
220 available: Cell::new(false),
221 });
222 let analyser = AudioAnalyser {
223 inner: Rc::downgrade(&inner),
224 };
225 (Self { inner }, analyser)
226 }
227
228 pub(crate) fn set_available(&self, available: bool) {
229 self.inner.available.set(available);
230 }
231
232 pub(crate) fn node(&self) -> &web_sys::AnalyserNode {
233 &self.inner.node
234 }
235}
236
237#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
238impl AudioAnalyser {
239 fn available_inner(&self) -> Result<Rc<AudioAnalyserInner>, AnalysisUnavailable> {
240 self.inner
241 .upgrade()
242 .filter(|inner| inner.available.get())
243 .ok_or(AnalysisUnavailable)
244 }
245
246 pub fn is_available(&self) -> bool {
247 self.available_inner().is_ok()
248 }
249
250 pub fn try_read(&self, domain: AnalysisDomain) -> Result<Vec<f32>, AnalysisUnavailable> {
252 let inner = self.available_inner()?;
253 Ok(read_node(&inner.node, domain))
254 }
255
256 pub fn read(&self, domain: AnalysisDomain) -> Vec<f32> {
261 self.try_read(domain).unwrap_or_default()
262 }
263
264 pub fn try_level(&self) -> Result<f32, AnalysisUnavailable> {
265 Ok(rms_level(&self.try_read(AnalysisDomain::Waveform)?))
266 }
267
268 pub fn level(&self) -> f32 {
269 self.try_level().unwrap_or(0.0)
270 }
271
272 fn snapshot(&self) -> Option<LiveAnalysisSnapshot> {
273 let inner = self.available_inner().ok()?;
274 let time_domain: Arc<[f32]> = read_node(&inner.node, AnalysisDomain::Waveform).into();
275 let frequency_domain = read_node(&inner.node, AnalysisDomain::Spectrum).into();
276 Some(LiveAnalysisSnapshot {
277 level: rms_level(&time_domain),
278 time_domain,
279 frequency_domain,
280 metadata: AnalysisMetadata::new(
281 inner.sample_rate,
282 inner.node.fft_size(),
283 inner.node.min_decibels() as f32,
284 inner.node.max_decibels() as f32,
285 inner.node.smoothing_time_constant(),
286 ),
287 })
288 }
289}
290
291#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
292fn read_node(node: &web_sys::AnalyserNode, domain: AnalysisDomain) -> Vec<f32> {
293 match domain {
294 AnalysisDomain::Waveform => {
295 let mut samples = vec![0_u8; node.fft_size() as usize];
296 node.get_byte_time_domain_data(&mut samples);
297 samples
298 .into_iter()
299 .map(|sample| ((sample as f32 - 128.0) / 128.0).clamp(-1.0, 1.0))
300 .collect()
301 }
302 AnalysisDomain::Spectrum => {
303 let samples = js_sys::Uint8Array::new_with_length(node.frequency_bin_count());
304 node.get_byte_frequency_data_with_u8_array(&samples);
305 let mut values = vec![0_u8; samples.length() as usize];
306 samples.copy_to(&mut values);
307 values
308 .into_iter()
309 .map(|sample| sample as f32 / 255.0)
310 .collect()
311 }
312 }
313}
314
315#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
316#[derive(Clone, Debug, PartialEq)]
317pub struct AudioAnalyser {
318 _private: (),
319}
320
321#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
322impl AudioAnalyser {
323 pub fn is_available(&self) -> bool {
324 false
325 }
326
327 pub fn try_read(&self, _domain: AnalysisDomain) -> Result<Vec<f32>, AnalysisUnavailable> {
328 Err(AnalysisUnavailable)
329 }
330
331 pub fn read(&self, _domain: AnalysisDomain) -> Vec<f32> {
332 Vec::new()
333 }
334
335 pub fn try_level(&self) -> Result<f32, AnalysisUnavailable> {
336 Err(AnalysisUnavailable)
337 }
338
339 pub fn level(&self) -> f32 {
340 0.0
341 }
342
343 fn snapshot(&self) -> Option<LiveAnalysisSnapshot> {
344 None
345 }
346}
347
348pub fn use_live_analysis(
355 analyser: ReadSignal<Option<AudioAnalyser>>,
356 options: LiveAnalysisOptions,
357) -> ReadSignal<Option<LiveAnalysisSnapshot>> {
358 use_scheduled_analysis(analyser, options, AudioAnalyser::snapshot)
359}
360
361pub(crate) fn use_live_analysis_domain(
362 analyser: ReadSignal<Option<AudioAnalyser>>,
363 domain: AnalysisDomain,
364) -> ReadSignal<Option<Arc<[f32]>>> {
365 match domain {
366 AnalysisDomain::Waveform => {
367 use_scheduled_analysis(analyser, LiveAnalysisOptions::default(), read_waveform)
368 }
369 AnalysisDomain::Spectrum => {
370 use_scheduled_analysis(analyser, LiveAnalysisOptions::default(), read_spectrum)
371 }
372 }
373}
374
375pub(crate) fn use_live_analysis_level(
376 analyser: ReadSignal<Option<AudioAnalyser>>,
377) -> ReadSignal<Option<f32>> {
378 use_scheduled_analysis(analyser, LiveAnalysisOptions::default(), |analyser| {
379 analyser.try_level().ok()
380 })
381}
382
383fn read_waveform(analyser: &AudioAnalyser) -> Option<Arc<[f32]>> {
384 Some(analyser.try_read(AnalysisDomain::Waveform).ok()?.into())
385}
386
387fn read_spectrum(analyser: &AudioAnalyser) -> Option<Arc<[f32]>> {
388 Some(analyser.try_read(AnalysisDomain::Spectrum).ok()?.into())
389}
390
391fn use_scheduled_analysis<T: 'static>(
392 analyser: ReadSignal<Option<AudioAnalyser>>,
393 options: LiveAnalysisOptions,
394 collect: fn(&AudioAnalyser) -> Option<T>,
395) -> ReadSignal<Option<T>> {
396 let parameters = use_memo(use_reactive!(|(analyser, options)| (analyser, options)));
397 #[allow(unused_mut)]
398 let mut value = use_signal(|| None::<T>);
399 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
400 let scheduler = use_live_analysis_scheduler();
401
402 use_effect(move || {
403 let (analyser, options) = parameters();
404 value.set(None);
405
406 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
407 {
408 let generation = scheduler.next_generation();
409 let Some(source_analyser) = analyser() else {
410 return;
411 };
412 let scheduler = scheduler.clone();
413 let scheduler_for_publish = scheduler.clone();
414 gloo_timers::callback::Timeout::new(0, move || {
415 wasm_bindgen_futures::spawn_local(run_live_analysis_schedule(
416 scheduler,
417 generation,
418 options.cadence(),
419 move || {
420 if analyser.peek().as_ref() != Some(&source_analyser) {
421 return false;
422 }
423 let next = collect(&source_analyser);
424 if !scheduler_for_publish.is_current(generation)
425 || analyser.peek().as_ref() != Some(&source_analyser)
426 {
427 return false;
428 }
429 if let Some(next) = next {
430 value.set(Some(next));
431 } else if value.peek().is_some() {
432 value.set(None);
433 }
434 true
435 },
436 ));
437 })
438 .forget();
439 }
440
441 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
442 {
443 let _ = (analyser, options, collect);
444 }
445 });
446
447 value.into()
448}
449
450#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
451pub(crate) struct LiveAnalysisScheduler {
452 generation: Cell<u64>,
453 mounted: Cell<bool>,
454}
455
456#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
457impl Default for LiveAnalysisScheduler {
458 fn default() -> Self {
459 Self {
460 generation: Cell::new(0),
461 mounted: Cell::new(true),
462 }
463 }
464}
465
466#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
467impl LiveAnalysisScheduler {
468 pub(crate) fn next_generation(&self) -> u64 {
469 let generation = self.generation.get().wrapping_add(1);
470 self.generation.set(generation);
471 generation
472 }
473
474 fn is_current(&self, generation: u64) -> bool {
475 self.mounted.get() && self.generation.get() == generation
476 }
477
478 fn unmount(&self) {
479 self.mounted.set(false);
480 self.next_generation();
481 }
482}
483
484#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
485struct AnalysisUnmountGuard(Weak<LiveAnalysisScheduler>);
486
487#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
488impl Drop for AnalysisUnmountGuard {
489 fn drop(&mut self) {
490 if let Some(scheduler) = self.0.upgrade() {
491 scheduler.unmount();
492 }
493 }
494}
495
496#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
497pub(crate) fn use_live_analysis_scheduler() -> Rc<LiveAnalysisScheduler> {
498 let scheduler = use_hook(|| Rc::new(LiveAnalysisScheduler::default()));
499 let scheduler_for_guard = Rc::downgrade(&scheduler);
500 use_hook(|| Rc::new(AnalysisUnmountGuard(scheduler_for_guard)));
501 scheduler
502}
503
504#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
505pub(crate) async fn run_live_analysis_schedule(
506 scheduler: Rc<LiveAnalysisScheduler>,
507 generation: u64,
508 cadence: Duration,
509 mut publish: impl FnMut() -> bool + 'static,
510) {
511 let cadence_millis = cadence.as_millis() as u32;
512 while scheduler.is_current(generation) {
513 if document_hidden() {
514 gloo_timers::future::TimeoutFuture::new(cadence_millis.max(250)).await;
515 continue;
516 }
517 if !publish() {
518 break;
519 }
520 gloo_timers::future::TimeoutFuture::new(cadence_millis).await;
521 }
522}
523
524#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
525fn document_hidden() -> bool {
526 web_sys::window()
527 .and_then(|window| window.document())
528 .is_some_and(|document| document.hidden())
529}
530
531pub fn downsample_peaks(peaks: &[u8], buckets: usize) -> Vec<u8> {
534 let bucket_count = peaks.len().min(buckets);
535 if bucket_count == 0 {
536 return Vec::new();
537 }
538
539 (0..bucket_count)
540 .map(|index| {
541 let start = index * peaks.len() / bucket_count;
542 let end = (index + 1) * peaks.len() / bucket_count;
543 peaks[start..end].iter().copied().max().unwrap_or(0)
544 })
545 .collect()
546}
547
548pub fn peak_amplitude(samples: &[u8]) -> u8 {
551 let distance = samples
552 .iter()
553 .map(|sample| (*sample as i16 - 128).unsigned_abs())
554 .max()
555 .unwrap_or(0);
556
557 ((u32::from(distance) * 255 + 64) / 128).min(255) as u8
558}
559
560#[derive(Clone, Copy, Debug, PartialEq)]
565pub struct WaveformSelection {
566 start: f64,
567 end: f64,
568}
569
570impl Eq for WaveformSelection {}
572
573impl WaveformSelection {
574 pub fn new(start: f64, end: f64) -> Self {
575 let start = finite_non_negative(start);
576 let end = finite_non_negative(end);
577
578 Self {
579 start: start.min(end),
580 end: start.max(end),
581 }
582 }
583
584 pub fn start(self) -> f64 {
585 self.start
586 }
587
588 pub fn end(self) -> f64 {
589 self.end
590 }
591
592 pub fn is_collapsed(self) -> bool {
593 self.start == self.end
594 }
595
596 pub fn with_start(self, start: f64) -> Self {
597 let start = if start.is_finite() {
598 start.clamp(0.0, self.end)
599 } else {
600 self.start
601 };
602 Self {
603 start,
604 end: self.end,
605 }
606 }
607
608 pub fn with_end(self, end: f64) -> Self {
609 let end = if end.is_finite() {
610 end.max(self.start)
611 } else {
612 self.end
613 };
614 Self {
615 start: self.start,
616 end,
617 }
618 }
619
620 pub fn clamped_to_duration(self, duration_secs: f64) -> Self {
622 let duration_secs = finite_non_negative(duration_secs);
623 Self {
624 start: self.start.min(duration_secs),
625 end: self.end.min(duration_secs),
626 }
627 }
628
629 pub fn is_playable_within(self, duration_secs: f64) -> bool {
631 duration_secs.is_finite()
632 && duration_secs > 0.0
633 && !self.is_collapsed()
634 && self.end <= duration_secs
635 }
636}
637
638fn finite_non_negative(value: f64) -> f64 {
639 if value.is_finite() {
640 value.max(0.0)
641 } else {
642 0.0
643 }
644}
645
646pub fn trim_interleaved_pcm<T: Clone>(
648 samples: &[T],
649 channels: usize,
650 duration_secs: f64,
651 selection: WaveformSelection,
652) -> Vec<T> {
653 if channels == 0 || !duration_secs.is_finite() || duration_secs <= 0.0 {
654 return Vec::new();
655 }
656
657 let selection = selection.clamped_to_duration(duration_secs);
658 if selection.is_collapsed() {
659 return Vec::new();
660 }
661
662 let frame_count = samples.len() / channels;
663 let first_frame = (selection.start() / duration_secs * frame_count as f64).floor() as usize;
664 let end_frame = (selection.end() / duration_secs * frame_count as f64).ceil() as usize;
665 samples[first_frame.min(frame_count) * channels..end_frame.min(frame_count) * channels].to_vec()
666}