Skip to main content

rataudio_meter/
meter.rs

1//! The [`Meter`] widget is used to display a horizontal audio meter.
2
3use crate::scaling::MeterScale;
4use ratatui::widgets::Block;
5
6/// Input type for the [`Meter`] widget
7pub enum MeterInput {
8    Mono(f32),
9    Stereo(f32, f32),
10}
11
12/// A widget to display an audio meter.
13///
14/// A `Meter` renders a bar filled according to the value given to [`Meter::db`], [`Meter::sample_amplitude`] or
15/// [`Meter::ratio`]. The bar width and height are defined by the [`Rect`] it is
16/// [rendered](Widget::render) in.
17///
18/// [`Meter`] is also a [`StatefulWidget`], which means you can use it with [`MeterState`] to allow
19/// the meter to hold its peak value for a certain amount of time.
20#[derive(Debug, Clone, PartialEq)]
21pub struct Meter<'a> {
22    pub(crate) block: Option<Block<'a>>,
23    pub(crate) ratio: [f32; 2],
24    pub(crate) channels: usize,
25    pub(crate) show_labels: bool,
26    pub(crate) show_scale: bool,
27}
28
29impl<'a> Meter<'a> {
30    /// Surrounds the `Meter` with a [`Block`].
31    ///
32    /// The meter is rendered in the inner portion of the block once space for borders and padding
33    /// is reserved. Styles set on the block do **not** affect the meter itself.
34    #[must_use = "method moves the value of self and returns the modified value"]
35    pub fn block(mut self, block: Block<'a>) -> Self {
36        self.block = Some(block);
37        self
38    }
39
40    /// Create a new mono [`Meter`] widget.
41    pub fn mono() -> Self {
42        Self {
43            block: None,
44            ratio: [0.0; 2],
45            channels: 1,
46            show_labels: true,
47            show_scale: true,
48        }
49    }
50
51    /// Create a new stereo [`Meter`] widget.
52    pub fn stereo() -> Self {
53        Self {
54            block: None,
55            ratio: [0.0; 2],
56            channels: 2,
57            show_labels: true,
58            show_scale: true,
59        }
60    }
61
62    /// Get the number of channels for this [`Meter`].
63    pub fn channels(&self) -> usize {
64        self.channels
65    }
66
67    /// Show or hide the decibel labels for the [`Meter`].
68    #[must_use = "method moves the value of self and returns the modified value"]
69    pub fn show_labels(mut self, show: bool) -> Self {
70        self.show_labels = show;
71        self
72    }
73
74    /// Show or hide the scale below the [`Meter`].
75    #[must_use = "method moves the value of self and returns the modified value"]
76    pub fn show_scale(mut self, show: bool) -> Self {
77        self.show_scale = show;
78        self
79    }
80
81    /// Set the value of the [`Meter`] widget in decibels relative to full scale.
82    /// This method will saturate values above 0.0dBFS to max.
83    #[must_use = "method moves the value of self and returns the modified value"]
84    pub fn db(mut self, input: MeterInput) -> Self {
85        match input {
86            MeterInput::Mono(dbfs) => {
87                self.ratio[0] = MeterScale::db_to_ratio(dbfs);
88                self.ratio[1] = 0.0;
89            }
90            MeterInput::Stereo(left_dbfs, right_dbfs) => {
91                self.ratio[0] = MeterScale::db_to_ratio(left_dbfs);
92                self.ratio[1] = MeterScale::db_to_ratio(right_dbfs);
93            }
94        }
95        self
96    }
97
98    /// Set the value of the [`Meter`] widget from a sample amplitude value between 0.0 and 1.0.
99    /// This method will panic if the value of `sample` is not between 0.0 and 1.0 inclusively.
100    #[must_use = "method moves the value of self and returns the modified value"]
101    pub fn sample_amplitude(mut self, input: MeterInput) -> Self {
102        match input {
103            MeterInput::Mono(ampl) => {
104                assert!(
105                    (0.0..=1.0).contains(&ampl),
106                    "Ratio should be between 0 and 1 inclusively."
107                );
108                self.ratio[0] = MeterScale::sample_to_ratio(ampl);
109                self.ratio[1] = 0.0;
110            }
111            MeterInput::Stereo(left_ampl, right_ampl) => {
112                assert!(
113                    (0.0..=1.0).contains(&left_ampl) && (0.0..=1.0).contains(&right_ampl),
114                    "Ratio should be between 0 and 1 inclusively."
115                );
116                self.ratio[0] = MeterScale::sample_to_ratio(left_ampl);
117                self.ratio[1] = MeterScale::sample_to_ratio(right_ampl);
118            }
119        }
120
121        self
122    }
123
124    /// Set the value of the [`Meter`] widget as a ratio.
125    ///
126    /// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
127    ///
128    /// # Panics
129    ///
130    /// This method will panic if the value of `ratio` is not between 0.0 and 1.0 inclusively.
131    #[must_use = "method moves the value of self and returns the modified value"]
132    pub fn ratio(mut self, input: MeterInput) -> Self {
133        match input {
134            MeterInput::Mono(ratio) => {
135                assert!(
136                    (0.0..=1.0).contains(&ratio),
137                    "Ratio should be between 0 and 1 inclusively."
138                );
139                self.ratio[0] = ratio;
140                self.ratio[1] = 0.0;
141            }
142            MeterInput::Stereo(left_ratio, right_ratio) => {
143                assert!(
144                    (0.0..=1.0).contains(&left_ratio) && (0.0..=1.0).contains(&right_ratio),
145                    "Ratio should be between 0 and 1 inclusively."
146                );
147                self.ratio[0] = left_ratio;
148                self.ratio[1] = right_ratio;
149            }
150        }
151        self
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn meter_db_zero() {
161        let meter = Meter::mono().db(MeterInput::Mono(0.0));
162        assert_eq!(meter.ratio[0], 1.0)
163    }
164
165    #[test]
166    fn meter_db_upper_bound() {
167        let meter = Meter::mono().db(MeterInput::Mono(0.1));
168        assert_eq!(meter.ratio[0], 1.0)
169    }
170
171    #[test]
172    fn meter_db_lower_bound() {
173        let meter = Meter::mono().db(MeterInput::Mono(-130.0));
174        assert_eq!(meter.ratio[0], 0.0);
175    }
176
177    #[test]
178    fn meter_stereo_db() {
179        let meter = Meter::stereo().db(MeterInput::Stereo(0.0, 0.0));
180        assert_eq!(meter.ratio[0], 1.0);
181        assert_eq!(meter.ratio[1], 1.0);
182    }
183
184    #[test]
185    fn meter_stereo_sample_amplitudes() {
186        let meter = Meter::stereo().sample_amplitude(MeterInput::Stereo(1.0, 0.0));
187        assert_eq!(meter.ratio[0], 1.0);
188        assert_eq!(meter.ratio[1], 0.0);
189    }
190
191    #[test]
192    #[should_panic = "Ratio should be between 0 and 1 inclusively"]
193    fn meter_invalid_ratio_upper_bound() {
194        let _ = Meter::mono().ratio(MeterInput::Mono(1.1));
195    }
196
197    #[test]
198    #[should_panic = "Ratio should be between 0 and 1 inclusively"]
199    fn meter_invalid_ratio_lower_bound() {
200        let _ = Meter::mono().ratio(MeterInput::Mono(-0.5));
201    }
202}