Skip to main content

music21_rs/
volume.rs

1//! How loud a note is: music21's `volume.Volume`.
2//!
3//! A volume is one number held two ways — a MIDI velocity from 0 to 127 and
4//! the same value as a scalar from 0 to 1 — plus the rule for reading it
5//! against the dynamic marks around it. music21 finds those marks by
6//! searching the stream the note sits in; there are no streams here, so
7//! [`Volume::realized_with`] takes the dynamic scalar and the articulation
8//! shift as arguments instead of going looking for them.
9
10use std::fmt;
11
12use crate::{
13    defaults::{FloatType, IntegerType},
14    error::{Error, Result},
15};
16
17/// The velocity music21 assumes when none was set, as a scalar. It is the
18/// `0.5` base level shifted by `0.20866`, which is what makes an unset note
19/// sound at a natural mezzo-forte rather than at half volume.
20const UNSET_VELOCITY_SHIFT: FloatType = 0.20866;
21
22/// The middle of the dynamic range that scalars move away from.
23const BASE_LEVEL: FloatType = 0.5;
24
25/// How loud a note is: music21's `volume.Volume`.
26#[derive(Clone, Debug, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28#[must_use]
29pub struct Volume {
30    velocity_scalar: Option<FloatType>,
31    velocity_is_relative: bool,
32}
33
34impl Default for Volume {
35    /// A volume with no velocity set, which reads as music21's default
36    /// loudness rather than as silence.
37    fn default() -> Self {
38        Self {
39            velocity_scalar: None,
40            velocity_is_relative: true,
41        }
42    }
43}
44
45impl Volume {
46    /// A volume with no velocity of its own.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Takes everything another volume says, the velocity and whether it is
52    /// relative: music21's `mergeAttributes`.
53    pub fn merge_attributes(&mut self, other: &Volume) {
54        self.velocity_scalar = other.velocity_scalar;
55        self.velocity_is_relative = other.velocity_is_relative;
56    }
57
58    /// A volume at a MIDI velocity, `0` to `127`, clamped to that range.
59    pub fn from_velocity(velocity: IntegerType) -> Self {
60        let mut volume = Self::new();
61        volume.set_velocity(Some(velocity));
62        volume
63    }
64
65    /// A volume at a scalar between `0` and `1`, clamped to that range.
66    pub fn from_velocity_scalar(scalar: FloatType) -> Result<Self> {
67        let mut volume = Self::new();
68        volume.set_velocity_scalar(Some(scalar))?;
69        Ok(volume)
70    }
71
72    /// The MIDI velocity, `0` to `127`, or `None` when none was set.
73    pub fn velocity(&self) -> Option<IntegerType> {
74        let scalar = self.velocity_scalar?;
75        let velocity = (scalar * 127.0).clamp(0.0, 127.0);
76        Some(velocity.round_ties_even() as IntegerType)
77    }
78
79    /// Sets the MIDI velocity, clamped to `0` through `127`, or clears it.
80    pub fn set_velocity(&mut self, velocity: Option<IntegerType>) {
81        self.velocity_scalar = velocity.map(|velocity| {
82            if velocity <= 0 {
83                0.0
84            } else if velocity >= 127 {
85                1.0
86            } else {
87                FloatType::from(velocity) / 127.0
88            }
89        });
90    }
91
92    /// The velocity as a scalar between `0` and `1`, or `None` when none was
93    /// set.
94    pub fn velocity_scalar(&self) -> Option<FloatType> {
95        self.velocity_scalar
96    }
97
98    /// Sets the velocity from a scalar, clamped to `0` through `1`, or
99    /// clears it. A value that is not a number is an error, as in music21.
100    pub fn set_velocity_scalar(&mut self, scalar: Option<FloatType>) -> Result<()> {
101        match scalar {
102            None => self.velocity_scalar = None,
103            Some(scalar) if scalar.is_nan() => {
104                return Err(Error::Volume(
105                    "value provided for velocityScalar must be a number, not NaN".to_string(),
106                ));
107            }
108            Some(scalar) => self.velocity_scalar = Some(scalar.clamp(0.0, 1.0)),
109        }
110        Ok(())
111    }
112
113    /// Whether the velocity shifts the dynamics around it (music21's
114    /// default) or fixes the loudness on its own, as a velocity read from a
115    /// MIDI file does.
116    pub fn velocity_is_relative(&self) -> bool {
117        self.velocity_is_relative
118    }
119
120    /// Sets whether the velocity is relative to its context.
121    pub fn set_velocity_is_relative(&mut self, relative: bool) {
122        self.velocity_is_relative = relative;
123    }
124
125    /// Whether a velocity was ever set: music21's `hasVolumeInformation` on
126    /// the note that owns it.
127    pub fn has_velocity_information(&self) -> bool {
128        self.velocity_scalar.is_some()
129    }
130
131    /// The loudness this volume comes to on its own, between `0` and `1`:
132    /// music21's `realized` with no dynamic or articulation context.
133    pub fn realized(&self) -> FloatType {
134        self.realized_with(None, 0.0, BASE_LEVEL, true)
135    }
136
137    /// The loudness this volume comes to against the dynamic in force and
138    /// whatever the articulations add: music21's `getRealized`, with the
139    /// context search it would do through a stream replaced by its answers.
140    ///
141    /// A relative velocity doubles the scalar range, so `0.5` leaves the base
142    /// level alone and `0.7` raises it; an absolute one decides the answer by
143    /// itself. The dynamic scales; an articulation *shifts*, which is why an
144    /// accent on a note nobody has marked lifts it rather than doubling it.
145    /// `clip` holds the result inside `0` to `1`.
146    pub fn realized_with(
147        &self,
148        dynamic_scalar: Option<FloatType>,
149        articulation_shift: FloatType,
150        base_level: FloatType,
151        clip: bool,
152    ) -> FloatType {
153        let mut value = base_level;
154        match self.velocity_scalar {
155            Some(scalar) if !self.velocity_is_relative => value = scalar,
156            Some(scalar) => value *= scalar * 2.0,
157            None => value += UNSET_VELOCITY_SHIFT,
158        }
159        if self.velocity_is_relative {
160            if let Some(dynamic_scalar) = dynamic_scalar {
161                value *= dynamic_scalar * 2.0;
162            }
163            value += articulation_shift;
164        }
165        if clip {
166            value = value.clamp(0.0, 1.0);
167        }
168        value
169    }
170
171    /// The realized loudness as one of music21's dynamic marks, `ppp`
172    /// through `fff`: `dynamics.dynamicStrFromDecimal` of the realized value.
173    pub fn realized_dynamic(&self) -> &'static str {
174        dynamic_name(self.realized())
175    }
176
177    /// The realized loudness written out to two places, which is what
178    /// music21's `getRealizedStr` and `cachedRealizedStr` answer.
179    pub fn realized_str(&self) -> String {
180        rounded_str(self.realized())
181    }
182}
183
184/// A loudness written out the way Python writes `str(round(value, 2))`: to
185/// two places, with the trailing zeros dropped but never the point.
186pub fn rounded_str(value: FloatType) -> String {
187    let rounded = (value * 100.0).round() / 100.0;
188    let written = format!("{rounded}");
189    if written.contains('.') {
190        written
191    } else {
192        format!("{written}.0")
193    }
194}
195
196/// The dynamic mark a realized loudness falls under, using music21's
197/// `dynamicStrFromDecimal` thresholds.
198fn dynamic_name(value: FloatType) -> &'static str {
199    match value {
200        value if value <= 0.0 => "n",
201        value if value < 0.11 => "pppp",
202        value if value < 0.16 => "ppp",
203        value if value < 0.26 => "pp",
204        value if value < 0.36 => "p",
205        value if value < 0.5 => "mp",
206        value if value < 0.65 => "mf",
207        value if value < 0.8 => "f",
208        value if value < 0.9 => "ff",
209        _ => "fff",
210    }
211}
212
213impl fmt::Display for Volume {
214    /// music21's `repr` body, `realized=0.71`.
215    ///
216    /// music21 writes `round(self.realized, 2)` into an f-string, so Python's
217    /// float formatting drops a trailing zero the way `{:.2}` does not: half
218    /// velocity reads `realized=0.5`, not `realized=0.50`.
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        let mut rounded = format!("{:.2}", self.realized());
221        while rounded.ends_with('0') && !rounded.ends_with(".0") {
222            rounded.pop();
223        }
224        write!(f, "realized={rounded}")
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn merging_takes_the_other_volumes_velocity() {
234        let mut loud = Volume::from_velocity(111);
235        loud.set_velocity_is_relative(false);
236        let mut volume = Volume::new();
237        volume.merge_attributes(&loud);
238        assert_eq!(volume.velocity(), Some(111));
239        assert!(!volume.velocity_is_relative());
240    }
241
242    #[test]
243    fn velocity_and_scalar_are_the_same_number() {
244        let mut volume = Volume::new();
245        assert_eq!(volume.velocity(), None);
246        assert_eq!(volume.velocity_scalar(), None);
247        assert!(!volume.has_velocity_information());
248
249        volume.set_velocity(Some(64));
250        assert_eq!(volume.velocity(), Some(64));
251        assert!(volume.has_velocity_information());
252        assert!((volume.velocity_scalar().unwrap() - 64.0 / 127.0).abs() < 1e-12);
253
254        volume.set_velocity(Some(200));
255        assert_eq!(volume.velocity(), Some(127));
256        volume.set_velocity(Some(-5));
257        assert_eq!(volume.velocity(), Some(0));
258        volume.set_velocity(None);
259        assert_eq!(volume.velocity(), None);
260    }
261
262    #[test]
263    fn scalars_clamp_and_round_trip() {
264        let volume = Volume::from_velocity_scalar(0.5).unwrap();
265        assert_eq!(volume.velocity(), Some(64));
266        assert_eq!(
267            Volume::from_velocity_scalar(2.0).unwrap().velocity_scalar(),
268            Some(1.0)
269        );
270        assert_eq!(
271            Volume::from_velocity_scalar(-1.0)
272                .unwrap()
273                .velocity_scalar(),
274            Some(0.0)
275        );
276        assert!(Volume::from_velocity_scalar(FloatType::NAN).is_err());
277        assert_eq!(Volume::from_velocity(127).velocity_scalar(), Some(1.0));
278    }
279
280    #[test]
281    fn realized_matches_music21() {
282        // An unset velocity realizes at the shifted base level, which is
283        // music21's default loudness for a note nobody has marked.
284        let unset = Volume::new();
285        assert!((unset.realized() - 0.70866).abs() < 1e-9);
286        assert_eq!(unset.realized_dynamic(), "f");
287        assert_eq!(unset.realized_str(), "0.71");
288
289        // A relative velocity doubles its scalar against the base level, so
290        // half velocity leaves the base level where it is.
291        let half = Volume::from_velocity_scalar(0.5).unwrap();
292        assert!((half.realized() - 0.5).abs() < 1e-9);
293        assert_eq!(half.realized_dynamic(), "mf");
294        assert_eq!(half.realized_str(), "0.5");
295
296        // An absolute velocity decides the answer by itself.
297        let mut absolute = Volume::from_velocity_scalar(0.5).unwrap();
298        absolute.set_velocity_is_relative(false);
299        assert!((absolute.realized() - 0.5).abs() < 1e-9);
300        let mut loud = Volume::from_velocity_scalar(1.0).unwrap();
301        loud.set_velocity_is_relative(false);
302        assert!((loud.realized() - 1.0).abs() < 1e-9);
303
304        // Relative velocities scale with the dynamic around them, and clip.
305        assert!((half.realized_with(Some(0.5), 0.0, 0.5, true) - 0.5).abs() < 1e-9);
306        assert!((half.realized_with(Some(1.0), 0.0, 0.5, true) - 1.0).abs() < 1e-9);
307        assert!((half.realized_with(Some(1.0), 0.0, 0.5, false) - 1.0).abs() < 1e-9);
308        assert!(loud.realized_with(Some(1.0), 0.0, 0.5, false) > 0.99);
309
310        // An articulation shifts rather than scales, so an accent on a note
311        // nobody has marked lifts it a little.
312        let unmarked = Volume::new();
313        assert!((unmarked.realized_with(None, 0.1, 0.5, true) - 0.80866).abs() < 1e-9);
314    }
315
316    #[test]
317    fn realized_names_cover_the_dynamic_range() {
318        assert_eq!(dynamic_name(0.0), "n");
319        assert_eq!(dynamic_name(0.05), "pppp");
320        assert_eq!(dynamic_name(0.12), "ppp");
321        assert_eq!(dynamic_name(0.2), "pp");
322        assert_eq!(dynamic_name(0.3), "p");
323        assert_eq!(dynamic_name(0.4), "mp");
324        assert_eq!(dynamic_name(0.6), "mf");
325        assert_eq!(dynamic_name(0.7), "f");
326        assert_eq!(dynamic_name(0.85), "ff");
327        assert_eq!(dynamic_name(1.0), "fff");
328        assert_eq!(Volume::from_velocity(64).to_string(), "realized=0.5");
329        assert_eq!(Volume::new().to_string(), "realized=0.71");
330        let mut silent = Volume::new();
331        silent.set_velocity(Some(0));
332        silent.set_velocity_is_relative(false);
333        assert_eq!(silent.to_string(), "realized=0.0");
334    }
335}