Skip to main content

dioxus_audio/
components.rs

1//! Reusable visual audio components.
2
3use dioxus::prelude::*;
4
5mod devices;
6mod player;
7mod recorder;
8mod visualizer;
9mod waveform;
10
11pub use devices::{AudioInputSelector, MicrophoneStatusIndicator};
12pub use player::{
13    AudioPlayer, BoundedPlaybackAnnouncementLabels, PlaybackAnnouncementLabels,
14    PlaybackAudibilitySlider, PlaybackMuteButton, PlaybackPlayPauseButton, PlaybackRateButton,
15    PlaybackRepeatButton, PlaybackSeekSlider, PlaybackSkipButton, PlaybackStatusAnnouncer,
16    PlaybackStopButton,
17};
18pub use recorder::{
19    RecorderAnnouncementLabels, RecorderCancelButton, RecorderClearButton, RecorderControls,
20    RecorderPauseResumeButton, RecorderStartButton, RecorderStatusAnnouncer, RecorderStopButton,
21};
22pub use visualizer::{LevelMeter, LiveWaveform, SpectrumVisualizer};
23pub use waveform::{
24    InteractiveWaveform, NavigableWaveform, Waveform, WaveformPreview, WaveformRangeSelector,
25};
26
27/// The supported lower-level equivalent to rendering [`AudioStyles`].
28///
29/// Prefer [`AudioStyles`] unless the application needs to construct its own
30/// document stylesheet element.
31pub static STYLESHEET: Asset = asset!("/assets/dioxus-audio.css");
32
33/// The canonical component stylesheet loader.
34///
35/// Render this once near the application root so all audio components can use
36/// the packaged styles.
37#[component]
38pub fn AudioStyles() -> Element {
39    rsx! { document::Stylesheet { href: STYLESHEET } }
40}
41
42#[component]
43pub fn AudioScrubber(
44    position_secs: f64,
45    duration_secs: f64,
46    on_seek: EventHandler<f64>,
47    #[props(default = false)] disabled: bool,
48    #[props(default)] aria_label: Option<String>,
49    #[props(default)] value_text: Option<String>,
50) -> Element {
51    let duration_secs = finite_non_negative(duration_secs);
52    let position_secs = finite_non_negative(position_secs).min(duration_secs);
53    let progress = if duration_secs > 0.0 {
54        position_secs / duration_secs * 100.0
55    } else {
56        0.0
57    };
58    let aria_label = aria_label.unwrap_or_else(|| "Seek audio".to_string());
59    let value_text = value_text.unwrap_or_else(|| {
60        format!(
61            "{} of {}",
62            format_accessible_duration(position_secs),
63            format_accessible_duration(duration_secs)
64        )
65    });
66
67    rsx! {
68        div { class: "dioxus-audio dioxus-audio__scrubber",
69            div { class: "dioxus-audio__scrubber-track",
70                div {
71                    class: "dioxus-audio__scrubber-fill",
72                    style: "width: {progress}%",
73                }
74            }
75            input {
76                class: "dioxus-audio__scrubber-input",
77                r#type: "range",
78                min: "0",
79                max: "{duration_secs}",
80                step: "0.01",
81                value: "{position_secs}",
82                disabled,
83                aria_label,
84                aria_valuetext: value_text,
85                oninput: move |event| {
86                    if let Ok(value) = event.value().parse::<f64>() {
87                        on_seek.call(value);
88                    }
89                },
90            }
91        }
92    }
93}
94
95fn finite_non_negative(value: f64) -> f64 {
96    if value.is_finite() {
97        value.max(0.0)
98    } else {
99        0.0
100    }
101}
102
103fn format_accessible_duration(seconds: f64) -> String {
104    let minutes = (seconds / 60.0).floor() as u64;
105    let seconds = seconds - minutes as f64 * 60.0;
106    let seconds = format_number(seconds);
107
108    if minutes == 0 {
109        return format!(
110            "{seconds} {}",
111            if seconds == "1" { "second" } else { "seconds" }
112        );
113    }
114
115    format!(
116        "{minutes} {}, {seconds} {}",
117        if minutes == 1 { "minute" } else { "minutes" },
118        if seconds == "1" { "second" } else { "seconds" }
119    )
120}
121
122fn format_number(value: f64) -> String {
123    let rounded = (value * 100.0).round() / 100.0;
124    rounded.to_string()
125}