harmos_signal/spectrum/
decibel.rs1#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct Decibel {
11 pub reference: f64,
13 pub factor: f64,
15}
16
17impl Decibel {
18 #[must_use]
20 pub fn amplitude(reference: f64) -> Self {
21 Self {
22 reference,
23 factor: 20.0,
24 }
25 }
26
27 #[must_use]
29 pub fn power(reference: f64) -> Self {
30 Self {
31 reference,
32 factor: 10.0,
33 }
34 }
35
36 #[must_use]
38 pub fn volt() -> Self {
39 Self::amplitude(1.0)
40 }
41
42 #[must_use]
44 pub fn millivolt() -> Self {
45 Self::amplitude(1e-3)
46 }
47
48 #[must_use]
51 pub fn microvolt() -> Self {
52 Self::amplitude(1e-6)
53 }
54
55 #[must_use]
57 pub fn ampere() -> Self {
58 Self::amplitude(1.0)
59 }
60
61 #[must_use]
63 pub fn milliampere() -> Self {
64 Self::amplitude(1e-3)
65 }
66
67 #[must_use]
69 pub fn microampere() -> Self {
70 Self::amplitude(1e-6)
71 }
72
73 #[must_use]
75 pub fn sound_pressure() -> Self {
76 Self::amplitude(20e-6)
77 }
78
79 #[must_use]
81 pub fn watt() -> Self {
82 Self::power(1.0)
83 }
84
85 #[must_use]
87 pub fn milliwatt() -> Self {
88 Self::power(1e-3)
89 }
90
91 #[must_use]
93 pub fn to_db(&self, linear: f64) -> f64 {
94 if linear > 0.0 {
95 self.factor * (linear / self.reference).log10()
96 } else {
97 f64::NEG_INFINITY
98 }
99 }
100
101 #[must_use]
103 pub fn to_linear(&self, db: f64) -> f64 {
104 self.reference * 10.0_f64.powf(db / self.factor)
105 }
106
107 #[must_use]
112 pub fn is_compatible(&self, other: &Self) -> bool {
113 (self.factor - other.factor).abs() < f64::EPSILON
114 }
115
116 #[must_use]
120 pub fn offset_from(&self, other: &Self) -> Option<f64> {
121 self.is_compatible(other)
122 .then(|| self.factor * (other.reference / self.reference).log10())
123 }
124}
125
126#[derive(Clone, Copy, Debug, Default, PartialEq)]
134pub enum Scale {
135 #[default]
137 Linear,
138 Decibel(Decibel),
140 Ratio {
142 factor: f64,
144 },
145}
146
147impl Scale {
148 #[must_use]
150 pub fn factor(&self) -> Option<f64> {
151 match self {
152 Self::Linear => None,
153 Self::Decibel(scale) => Some(scale.factor),
154 Self::Ratio { factor } => Some(*factor),
155 }
156 }
157
158 #[must_use]
160 pub fn reference(&self) -> Option<f64> {
161 match self {
162 Self::Decibel(scale) => Some(scale.reference),
163 Self::Linear | Self::Ratio { .. } => None,
164 }
165 }
166
167 #[must_use]
169 pub fn is_compatible(&self, other: &Self) -> bool {
170 match (self, other) {
171 (Self::Linear, Self::Linear) => true,
172 (Self::Decibel(left), Self::Decibel(right)) => left.is_compatible(right),
173 (Self::Decibel(scale), Self::Ratio { factor })
174 | (Self::Ratio { factor }, Self::Decibel(scale)) => {
175 (scale.factor - factor).abs() < f64::EPSILON
176 }
177 (Self::Ratio { factor: left }, Self::Ratio { factor: right }) => {
178 (left - right).abs() < f64::EPSILON
179 }
180 (Self::Linear, _) | (_, Self::Linear) => false,
181 }
182 }
183
184 #[must_use]
189 pub fn sum(&self, other: &Self) -> Option<Self> {
190 match (self, other) {
191 (Self::Linear, Self::Linear) => Some(Self::Linear),
192 (Self::Decibel(scale), Self::Ratio { .. })
193 | (Self::Ratio { .. }, Self::Decibel(scale))
194 if self.is_compatible(other) =>
195 {
196 Some(Self::Decibel(*scale))
197 }
198 (Self::Ratio { factor }, Self::Ratio { .. }) if self.is_compatible(other) => {
199 Some(Self::Ratio { factor: *factor })
200 }
201 _ => None,
202 }
203 }
204
205 #[must_use]
210 pub fn difference(&self, other: &Self) -> Option<Self> {
211 match (self, other) {
212 (Self::Linear, Self::Linear) => Some(Self::Linear),
213 (Self::Decibel(left), Self::Decibel(_)) if self.is_compatible(other) => {
214 Some(Self::Ratio {
215 factor: left.factor,
216 })
217 }
218 (Self::Decibel(scale), Self::Ratio { .. }) if self.is_compatible(other) => {
219 Some(Self::Decibel(*scale))
220 }
221 (Self::Ratio { factor }, Self::Ratio { .. }) if self.is_compatible(other) => {
222 Some(Self::Ratio { factor: *factor })
223 }
224 _ => None,
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn a_microvolt_scale_counts_twenty_db_per_decade_of_amplitude() {
235 let scale = Decibel::microvolt();
236
237 assert!(scale.to_db(1e-6).abs() < 1e-9);
238 assert!((scale.to_db(1e-3) - 60.0).abs() < 1e-9);
239 assert!((scale.to_db(1.0) - 120.0).abs() < 1e-9);
240 }
241
242 #[test]
243 fn a_conversion_and_its_inverse_return_the_value() {
244 let scale = Decibel::microvolt();
245
246 assert!((scale.to_linear(scale.to_db(0.005)) - 0.005).abs() < 1e-15);
247 }
248
249 #[test]
250 fn nothing_positive_is_negative_infinity_decibels() {
251 assert_eq!(Decibel::volt().to_db(0.0), f64::NEG_INFINITY);
252 assert_eq!(Decibel::volt().to_db(-1.0), f64::NEG_INFINITY);
253 }
254
255 #[test]
256 fn a_power_scale_counts_ten_db_per_decade() {
257 assert!((Decibel::milliwatt().to_db(1.0) - 30.0).abs() < 1e-9);
258 assert!((Decibel::watt().to_db(1e-3) + 30.0).abs() < 1e-9);
259 }
260
261 #[test]
262 fn amplitude_and_power_scales_do_not_mix() {
263 assert!(!Decibel::volt().is_compatible(&Decibel::milliwatt()));
264 assert!(Decibel::volt().is_compatible(&Decibel::microvolt()));
265 assert!(Decibel::milliwatt().offset_from(&Decibel::volt()).is_none());
266 }
267
268 #[test]
269 fn two_references_are_one_offset_apart() {
270 let offset = Decibel::microvolt().offset_from(&Decibel::millivolt());
271
272 assert!(offset.is_some_and(|offset| (offset - 60.0).abs() < 1e-9));
273 }
274
275 #[test]
276 fn a_scale_reports_the_factor_and_reference_it_carries() {
277 assert_eq!(Scale::Decibel(Decibel::microvolt()).factor(), Some(20.0));
278 assert_eq!(Scale::Decibel(Decibel::microvolt()).reference(), Some(1e-6));
279 assert_eq!(Scale::Ratio { factor: 20.0 }.factor(), Some(20.0));
280 assert_eq!(Scale::Ratio { factor: 20.0 }.reference(), None);
281 assert_eq!(Scale::default().factor(), None);
282 }
283
284 #[test]
285 fn linear_and_logarithmic_columns_are_never_compatible() {
286 assert!(!Scale::Linear.is_compatible(&Scale::Decibel(Decibel::microvolt())));
287 assert!(Scale::Linear.is_compatible(&Scale::Linear));
288 assert!(
289 Scale::Decibel(Decibel::microvolt())
290 .is_compatible(&Scale::Decibel(Decibel::millivolt()))
291 );
292 assert!(
293 !Scale::Decibel(Decibel::microvolt())
294 .is_compatible(&Scale::Decibel(Decibel::milliwatt()))
295 );
296 }
297
298 #[test]
299 fn two_levels_differ_by_a_ratio() {
300 let level = Scale::Decibel(Decibel::microvolt());
301 let limit = Scale::Decibel(Decibel::millivolt());
302
303 assert_eq!(
304 level.difference(&limit),
305 Some(Scale::Ratio { factor: 20.0 })
306 );
307 }
308
309 #[test]
310 fn a_margin_added_back_to_a_level_is_a_level() {
311 let level = Scale::Decibel(Decibel::microvolt());
312 let margin = Scale::Ratio { factor: 20.0 };
313
314 assert_eq!(level.sum(&margin), Some(level));
315 assert_eq!(margin.sum(&level), Some(level));
316 assert_eq!(level.difference(&margin), Some(level));
317 }
318
319 #[test]
320 fn a_ratio_never_becomes_a_measurement() {
321 let ratio = Scale::Ratio { factor: 20.0 };
322
323 assert_eq!(ratio.sum(&Scale::Linear), None);
324 assert_eq!(ratio.difference(&Scale::Linear), None);
325 assert_eq!(ratio.sum(&Scale::Ratio { factor: 10.0 }), None);
326 assert_eq!(ratio.difference(&ratio), Some(ratio));
327 }
328
329 #[test]
330 fn linear_columns_add_and_subtract_as_themselves() {
331 assert_eq!(Scale::Linear.sum(&Scale::Linear), Some(Scale::Linear));
332 assert_eq!(
333 Scale::Linear.difference(&Scale::Linear),
334 Some(Scale::Linear)
335 );
336 assert_eq!(Scale::Linear.sum(&Scale::Decibel(Decibel::volt())), None);
337 }
338
339 #[test]
340 fn two_levels_do_not_add() {
341 let level = Scale::Decibel(Decibel::microvolt());
342
343 assert_eq!(level.sum(&level), None);
344 }
345}