Skip to main content

music21_rs/
dynamics.rs

1//! Dynamic marks, ported from music21's `dynamics` module.
2//!
3//! A [`Dynamic`] is a mark such as `mf` and the loudness it stands for, a
4//! scalar between nought and one. Placed in a [`Stream`](crate::Stream) it is
5//! in force until the next one, which is what
6//! [`realize_volume`](crate::volume::realize_volume) reads.
7
8use std::fmt;
9
10use crate::{
11    defaults::FloatType,
12    display::Drawn,
13    error::{Error, Result},
14    notation::Placement,
15};
16
17/// The marks music21 lists, softest first: its `shortNames`.
18pub const SHORT_NAMES: [&str; 16] = [
19    "pppppp", "ppppp", "pppp", "ppp", "pp", "p", "mp", "mf", "f", "fp", "sf", "ff", "fff", "ffff",
20    "fffff", "ffffff",
21];
22
23/// The Italian word for a mark, where music21 gives one: its `longNames`.
24const LONG_NAMES: [(&str, &str); 10] = [
25    ("ppp", "pianississimo"),
26    ("pp", "pianissimo"),
27    ("p", "piano"),
28    ("mp", "mezzopiano"),
29    ("mf", "mezzoforte"),
30    ("f", "forte"),
31    ("fp", "fortepiano"),
32    ("sf", "sforzando"),
33    ("ff", "fortissimo"),
34    ("fff", "fortississimo"),
35];
36
37/// What a mark means in English, where music21 says: its `englishNames`.
38const ENGLISH_NAMES: [(&str, &str); 8] = [
39    ("ppp", "extremely soft"),
40    ("pp", "very soft"),
41    ("p", "soft"),
42    ("mp", "moderately soft"),
43    ("mf", "moderately loud"),
44    ("f", "loud"),
45    ("ff", "very loud"),
46    ("fff", "extremely loud"),
47];
48
49/// The loudness each mark stands for: music21's `dynamicStrToScalar`.
50const SCALARS: [(&str, FloatType); 13] = [
51    ("n", 0.0),
52    ("pppp", 0.1),
53    ("ppp", 0.15),
54    ("pp", 0.25),
55    ("p", 0.35),
56    ("mp", 0.45),
57    ("mf", 0.55),
58    ("f", 0.7),
59    ("fp", 0.75),
60    ("sf", 0.85),
61    ("ff", 0.85),
62    ("fff", 0.9),
63    ("ffff", 0.95),
64];
65
66/// The loudness of a mark music21 has no scalar for.
67const DEFAULT_SCALAR: FloatType = 0.5;
68
69fn looked_up<T: Copy>(table: &[(&str, T)], mark: &str) -> Option<T> {
70    table
71        .iter()
72        .find(|(name, _)| *name == mark)
73        .map(|(_, value)| *value)
74}
75
76/// The mark a loudness between nought and one falls under: music21's
77/// `dynamicStrFromDecimal`. Nought is `n`, niente, and `pppp` and `fff` are
78/// the ends of what it answers.
79pub fn dynamic_str_from_decimal(value: FloatType) -> &'static str {
80    match value {
81        value if value <= 0.0 => "n",
82        value if value < 0.11 => "pppp",
83        value if value < 0.16 => "ppp",
84        value if value < 0.26 => "pp",
85        value if value < 0.36 => "p",
86        value if value < 0.5 => "mp",
87        value if value < 0.65 => "mf",
88        value if value < 0.8 => "f",
89        value if value < 0.9 => "ff",
90        _ => "fff",
91    }
92}
93
94/// A dynamic mark and the loudness it stands for.
95///
96/// ```
97/// use music21_rs::Dynamic;
98///
99/// let forte = Dynamic::new("f");
100/// assert_eq!(forte.volume_scalar(), 0.7);
101/// assert_eq!(forte.long_name(), Some("forte"));
102///
103/// // A mark with no loudness of its own is read without its `s` and `z`.
104/// assert_eq!(Dynamic::new("sfz").volume_scalar(), 0.7);
105///
106/// // A loudness names the mark it falls under and keeps its own value.
107/// let soft = Dynamic::from_scalar(0.4)?;
108/// assert_eq!(soft.value(), "mp");
109/// assert_eq!(soft.volume_scalar(), 0.4);
110/// # Ok::<(), music21_rs::Error>(())
111/// ```
112#[derive(Clone, Debug, PartialEq)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114#[must_use]
115pub struct Dynamic {
116    value: String,
117    /// A loudness a caller gave, which wins over the mark's own.
118    #[cfg_attr(feature = "serde", serde(default))]
119    volume_scalar: Option<FloatType>,
120    #[cfg_attr(feature = "serde", serde(default))]
121    placement: Drawn<Option<Placement>>,
122}
123
124impl Dynamic {
125    /// A dynamic from its mark. Any text is a mark, as it is upstream; one
126    /// music21 has no loudness for sounds at the middle of the range.
127    pub fn new(value: impl Into<String>) -> Self {
128        Self {
129            value: value.into(),
130            volume_scalar: None,
131            placement: Drawn(None),
132        }
133    }
134
135    /// Which side of the staff it is written on, where the score says.
136    pub fn placement(&self) -> Option<Placement> {
137        self.placement.0
138    }
139
140    /// Says which side of the staff it is written on.
141    pub fn set_placement(&mut self, placement: Option<Placement>) {
142        self.placement.0 = placement;
143    }
144
145    /// A dynamic from a loudness between nought and one, named as the mark
146    /// that loudness falls under and keeping the loudness itself.
147    pub fn from_scalar(scalar: FloatType) -> Result<Self> {
148        let mut dynamic = Self::new(dynamic_str_from_decimal(scalar));
149        dynamic.set_volume_scalar(scalar)?;
150        Ok(dynamic)
151    }
152
153    /// The mark as written, `mf`.
154    pub fn value(&self) -> &str {
155        &self.value
156    }
157
158    /// Writes another mark. A loudness a caller gave is kept.
159    pub fn set_value(&mut self, value: impl Into<String>) {
160        self.value = value.into();
161    }
162
163    /// The Italian word for the mark, `mezzoforte`, where music21 has one.
164    pub fn long_name(&self) -> Option<&'static str> {
165        looked_up(&LONG_NAMES, &self.value)
166    }
167
168    /// What the mark means in English, where music21 says.
169    pub fn english_name(&self) -> Option<&'static str> {
170        looked_up(&ENGLISH_NAMES, &self.value)
171    }
172
173    /// The loudness the dynamic stands for, between nought and one:
174    /// music21's `volumeScalar`.
175    ///
176    /// The one a caller gave, else the mark's own. A mark with none of its
177    /// own is tried again without a leading letter where it has an `s` in
178    /// it and without a closing `z`, so `sfz` is as loud as `f`; failing
179    /// that it is the middle of the range.
180    pub fn volume_scalar(&self) -> FloatType {
181        if let Some(scalar) = self.volume_scalar {
182            return scalar;
183        }
184        if let Some(scalar) = looked_up(&SCALARS, &self.value) {
185            return scalar;
186        }
187        // music21 drops the first character wherever the mark has an `s`
188        // anywhere in it, which is what is reproduced.
189        let mut mark = self.value.as_str();
190        if mark.contains('s') {
191            let mut characters = mark.chars();
192            characters.next();
193            mark = characters.as_str();
194        }
195        let mark = mark.strip_suffix('z').unwrap_or(mark);
196        looked_up(&SCALARS, mark).unwrap_or(DEFAULT_SCALAR)
197    }
198
199    /// Gives the dynamic a loudness of its own, overriding its mark's.
200    pub fn set_volume_scalar(&mut self, scalar: FloatType) -> Result<()> {
201        if !(0.0..=1.0).contains(&scalar) {
202            return Err(Error::Volume(format!(
203                "cannot set as volume scalar to: {scalar}"
204            )));
205        }
206        self.volume_scalar = Some(scalar);
207        Ok(())
208    }
209}
210
211impl fmt::Display for Dynamic {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        write!(f, "{}", self.value)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    /// Read off music21; the tables themselves are held to it by
222    /// `table_parity`.
223    #[test]
224    fn a_mark_sounds_as_loud_as_music21_says() {
225        for (mark, scalar) in [
226            ("sf", 0.85),
227            ("sfz", 0.7),
228            ("fp", 0.75),
229            ("rfz", 0.5),
230            ("sffz", 0.85),
231            ("ppppp", 0.5),
232            ("n", 0.0),
233        ] {
234            assert_eq!(Dynamic::new(mark).volume_scalar(), scalar, "{mark}");
235        }
236        assert_eq!(Dynamic::new("sf").long_name(), Some("sforzando"));
237        assert_eq!(Dynamic::new("sf").english_name(), None);
238        assert_eq!(Dynamic::new("mf").english_name(), Some("moderately loud"));
239        assert!(Dynamic::from_scalar(1.5).is_err());
240        assert_eq!(dynamic_str_from_decimal(0.0), "n");
241        assert_eq!(dynamic_str_from_decimal(0.95), "fff");
242    }
243}