Skip to main content

music21_rs/tuningsystem/
adaptive.rs

1use crate::{
2    FloatType, TuningSystem, UnsignedIntegerType,
3    tuningsystem::{get_frequency_at, get_ratio_at},
4};
5
6/// Adaptive tuning systems whose note frequencies depend on harmonic context.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum AdaptiveTuningSystem {
10    /// A recursive tuning system:
11    ///
12    /// ```text
13    /// frequency = base * root_tuning[context] * local_tuning[index]
14    /// ```
15    ///
16    /// `context` is the absolute root index, such as E above C.
17    /// `index` is the local interval above that root, such as a major third.
18    Recursive {
19        /// Tuning system used to place the chord root.
20        root_tuning_system: TuningSystem,
21
22        /// Tuning system used inside the chord root.
23        local_tuning_system: TuningSystem,
24    },
25}
26
27/// Recursive just intonation:
28///
29/// frequency = C_base * JustIntonation[root] * JustIntonation[local_degree]
30pub const RECURSIVE_JI: AdaptiveTuningSystem = AdaptiveTuningSystem::Recursive {
31    root_tuning_system: TuningSystem::JustIntonation,
32    local_tuning_system: TuningSystem::JustIntonation,
33};
34
35impl AdaptiveTuningSystem {
36    /// Returns the frequency in hertz for a local degree inside a harmonic context.
37    ///
38    /// For example, in recursive JI:
39    ///
40    /// ```text
41    /// context = 4  // E above C
42    /// index   = 4  // major third above E
43    ///
44    /// frequency = C * 5/4 * 5/4
45    ///           = C * 25/16
46    /// ```
47    pub fn frequency_at(
48        self,
49        context: FloatType,
50        index: FloatType,
51        size: Option<UnsignedIntegerType>,
52    ) -> FloatType {
53        match self {
54            Self::Recursive {
55                root_tuning_system,
56                local_tuning_system,
57            } => {
58                let root_ratio = get_ratio_at(root_tuning_system, context, size);
59                let local_frequency = get_frequency_at(local_tuning_system, index, size);
60
61                root_ratio * local_frequency
62            }
63        }
64    }
65
66    /// Returns cents offset against equal temperament for the resulting absolute pitch.
67    ///
68    /// This assumes `index` is a local interval above `context`, so the equal-tempered
69    /// comparison pitch is `context + index`.
70    pub fn cents_at(
71        self,
72        context: FloatType,
73        index: FloatType,
74        size: Option<UnsignedIntegerType>,
75    ) -> FloatType {
76        let octave_size = size.unwrap_or_else(|| match self {
77            Self::Recursive {
78                root_tuning_system, ..
79            } => root_tuning_system.octave_size(),
80        });
81
82        let reference_frequency = get_frequency_at(
83            TuningSystem::EqualTemperament { octave_size },
84            context + index,
85            Some(octave_size),
86        );
87
88        let comparison_frequency = self.frequency_at(context, index, size);
89
90        1200.0 * (comparison_frequency / reference_frequency).log2()
91    }
92
93    /// Returns cents offset against a fixed tuning table for the same absolute pitch.
94    ///
95    /// Useful for comparing recursive JI against fixed-C JI.
96    pub fn cents_vs_fixed_at(
97        self,
98        fixed_tuning_system: TuningSystem,
99        context: FloatType,
100        index: FloatType,
101        size: Option<UnsignedIntegerType>,
102    ) -> FloatType {
103        let fixed_frequency = get_frequency_at(fixed_tuning_system, context + index, size);
104        let comparison_frequency = self.frequency_at(context, index, size);
105
106        1200.0 * (comparison_frequency / fixed_frequency).log2()
107    }
108}