tuning_library_rs/
lib.rs

1#![warn(missing_docs)]
2
3//! Micro-tuning format parsing and frequency finding library.
4//!
5//! This library provides parsing of SCL (scale) and KBM (keyboard mapping) files,
6//! constructing tunings from scales and keyboard mappings and finding frequencies of notes.
7//!
8//! This library is mostly a direct rewrite of Surge Synthesizer Teams's [`tuning-library`]. Even
9//! the documentation is taken almost word for word.
10//!
11//! [`tuning-library`]: https://github.com/surge-synthesizer/tuning-library
12
13use std::{error::Error, fmt::Display, fs};
14
15/// Frequency of a MIDI note 0. Equal to `440 * 2^(69/12)`.
16pub const MIDI_0_FREQ: f64 = 8.17579891564371;
17
18/// Errors
19#[derive(Debug)]
20pub enum TuningError {
21    /// Error parsing an SCL/KBM file/string.
22    ParseError(String),
23    /// Tone is a ratio with either denominator or numerator equal to 0.
24    InvalidTone,
25    /// Number of notes is less then 1 or not equal to listed notes.
26    InvalidNoteCount,
27    /// Scale contains no notes.
28    TooFewNotes,
29    /// Tuning attemped to tune unmapped key.
30    TuningUnmappedKey,
31    /// Mapping is longer than scale.
32    MappingLongerThanScale,
33    /// Error reading the file.
34    FileError,
35    /// Cannot divide zero span.
36    ZeroSpan,
37    /// Cannot divide non-positive cents amount.
38    NonPositiveCents,
39    /// Cannot divide into zero steps.
40    ZeroSteps,
41    /// Incomplete KBM file.
42    IncompleteKBM,
43    /// Incomplete SCL file.
44    IncompleteSCL,
45    /// Given amount of keys is not equal to number of listed keys.
46    InvalidKeyCount,
47}
48
49impl Display for TuningError {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            TuningError::ParseError(error) => write!(f, "Error parsing the file: {error}"),
53            TuningError::InvalidTone => write!(f, "Value is an invalid tone."),
54            TuningError::InvalidNoteCount => write!(f, "Invalid number of notes."),
55            TuningError::TooFewNotes => write!(f, "Too few notes."),
56            TuningError::TuningUnmappedKey => write!(f, "Attempted to tune unmapped key."),
57            TuningError::MappingLongerThanScale => {
58                write!(f, "Keyboard mapping is longer than the scale.")
59            }
60            TuningError::FileError => write!(f, "Error reading the file."),
61            TuningError::ZeroSpan => write!(f, "Cannot divide zero span."),
62            TuningError::NonPositiveCents => {
63                write!(f, "Cannot divide by non-positive cents amount.")
64            }
65            TuningError::ZeroSteps => write!(f, "Cannot divide by zero steps."),
66            TuningError::IncompleteKBM => write!(f, "KBM file is incomplete."),
67            TuningError::IncompleteSCL => write!(f, "SCL file is incomplete."),
68            TuningError::InvalidKeyCount => write!(f, "Invalid number of keys."),
69        }
70    }
71}
72
73impl Error for TuningError {}
74
75#[doc(hidden)]
76pub struct AllowTuningOnUnmapped(pub bool);
77
78/// Value of a tone.
79///
80/// The value of a tone is given either as a cents value or ratio.
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82#[derive(Copy, Clone, Debug, PartialEq)]
83pub enum ToneValue {
84    /// Value of a tone given as cents value.
85    /// ```
86    /// # use tuning_library_rs::*;
87    /// ToneValue::Cents(1200.0); // is an octave
88    /// ToneValue::Cents(700.0); // is an 12-EDO fifth
89    /// ```
90    Cents(f64),
91
92    /// Value of a tone given as a ratio.
93    /// ```
94    /// # use tuning_library_rs::*;
95    /// ToneValue::Ratio(2, 1); // is an octave as well
96    /// ToneValue::Ratio(3, 2); // is a JI fifth
97    /// ```
98    Ratio(i64, i64),
99}
100
101impl ToneValue {
102    fn cents(&self) -> f64 {
103        match self {
104            Self::Cents(value) => *value,
105            Self::Ratio(n, d) => 1200.0 * (*n as f64 / *d as f64).log2() / 2f64.log2(),
106        }
107    }
108
109    fn float_value(&self) -> f64 {
110        self.cents() / 1200.0 + 1.0
111    }
112}
113
114impl Default for ToneValue {
115    fn default() -> Self {
116        ToneValue::Ratio(1, 1)
117    }
118}
119
120impl Eq for ToneValue {}
121
122impl PartialOrd for ToneValue {
123    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
124        self.cents().partial_cmp(&other.cents())
125    }
126}
127
128impl Ord for ToneValue {
129    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
130        self.partial_cmp(other).unwrap()
131    }
132}
133
134impl Display for ToneValue {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        match self {
137            ToneValue::Cents(value) => write!(f, "{value}c"),
138            ToneValue::Ratio(n, d) => write!(f, "{n}/{d}"),
139        }
140    }
141}
142
143/// A Tone is a single entry in a SCL file. It is expressed either in cents or in a ratio, as
144/// described in the SCL documentation.
145///
146/// In most normal use, you will not use this struct, and it will be internal to a [`Scale`].
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
149pub struct Tone {
150    /// Value of the tone.
151    pub value: ToneValue,
152
153    /// String representation of the tone. Is set if parsed from string.
154    pub string_rep: String,
155
156    /// Number of line on which the tone was specified. Is set if parsed from file.
157    pub lineno: Option<usize>,
158}
159
160impl Tone {
161    /// Default constructor. Initializes tone with ratio `1/1` (or 0 cents).
162    pub fn new() -> Self {
163        Tone::default()
164    }
165
166    /// Returns cents value of a tone.
167    pub fn cents(&self) -> f64 {
168        self.value.cents()
169    }
170
171    /// Returns float value of a tone. Equal to `cents() / 1200 + 1`.
172    pub fn float_value(&self) -> f64 {
173        self.value.float_value()
174    }
175
176    /// Constructs a tone from a string.
177    ///
178    /// Returns a `TuningError::ParseError` if `line` is not a valid tone representation.
179    ///
180    /// Returns a `TuningError::InvalidTone` if `line` is a ratio with either denominator or
181    /// numerator equal to 0.
182    ///
183    /// Returns an `Ok` variant for valid tones.
184    pub fn from_string(line: &str, lineno: Option<usize>) -> Result<Self, TuningError> {
185        let mut t = Tone::new();
186        t.string_rep = line.to_string();
187        t.lineno = lineno;
188
189        if line.find('.').is_some() {
190            t.value = ToneValue::Cents(match line.parse() {
191                Ok(x) => x,
192                Err(_) => {
193                    return Err(TuningError::ParseError(format!(
194                        "Line {} contains . but is not numeric.",
195                        lineno.unwrap_or_default()
196                    )))
197                }
198            });
199
200            return Ok(t);
201        }
202
203        let parts: Vec<&str> = line.split('/').collect();
204        match parts[..] {
205            [one] => {
206                t.value = ToneValue::Ratio(
207                    match one.parse() {
208                        Ok(x) => x,
209                        Err(_) => {
210                            return Err(TuningError::ParseError(format!(
211                                "Numerator on line {} is not numeric.",
212                                lineno.unwrap_or_default()
213                            )))
214                        }
215                    },
216                    1,
217                )
218            }
219            [one, two] => {
220                t.value = ToneValue::Ratio(
221                    match one.parse() {
222                        Ok(x) => x,
223                        Err(_) => {
224                            return Err(TuningError::ParseError(format!(
225                                "Numerator on line {} is not numeric.",
226                                lineno.unwrap_or_default()
227                            )))
228                        }
229                    },
230                    match two.parse() {
231                        Ok(x) => x,
232                        Err(_) => {
233                            return Err(TuningError::ParseError(format!(
234                                "Denominator on line {} is not numeric.",
235                                lineno.unwrap_or_default()
236                            )))
237                        }
238                    },
239                )
240            }
241            _ => {
242                return Err(TuningError::ParseError(format!(
243                    "Value on line {} is not a valid fraction.",
244                    lineno.unwrap_or_default()
245                )))
246            }
247        }
248
249        if let ToneValue::Ratio(d, n) = t.value {
250            if d == 0 || n == 0 {
251                return Err(TuningError::InvalidTone);
252            }
253        }
254
255        Ok(t)
256    }
257}
258
259impl Display for Tone {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        write!(f, "{}", self.string_rep)
262    }
263}
264
265impl Default for Tone {
266    fn default() -> Self {
267        Tone {
268            value: ToneValue::default(),
269            string_rep: String::from("1/1"),
270            lineno: None,
271        }
272    }
273}
274
275/// The Scale is the representation of the SCL file.
276///
277/// It contain several key features. Most importantly it has a [`Scale::count()`] and a [vector of
278/// Tones][`Scale::tones`].
279///
280/// In most normal use, you will simply pass around instances of this struct to a [`Tuning`], but in
281/// some cases you may want to create or inspect this struct yourself. Especially if you are
282/// displaying this struct to your end users, you may want to use the [`Scale::raw_text`] or
283/// [`Scale::count()`] methods.
284#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285#[derive(Default, Clone, Debug)]
286pub struct Scale {
287    /// The name in the SCL file. Informational only.
288    pub name: String,
289
290    /// The description in the SCL file. Informational only.
291    pub description: String,
292
293    /// The raw text of the SCL file used to create this Scale.
294    pub raw_text: String,
295
296    /// The number of tones.
297    count: usize,
298
299    /// The tones.
300    pub tones: Vec<Tone>,
301}
302
303impl Scale {
304    /// Constructs an empty scale.
305    pub fn new() -> Self {
306        Scale {
307            name: String::from("empty scale"),
308            ..Default::default()
309        }
310    }
311
312    /// Number of tones in the scale.
313    pub fn count(&self) -> usize {
314        self.count
315    }
316
317    /// Returns a Scale or an error from the SCL file contents in `fname`.
318    pub fn read_scl_file<P>(fname: P) -> Result<Self, TuningError>
319    where
320        P: AsRef<std::path::Path>,
321    {
322        let content = match fs::read_to_string(fname) {
323            Ok(lines) => lines,
324            Err(_) => return Err(TuningError::FileError),
325        };
326
327        Scale::parse_scl_data(&content)
328    }
329
330    /// Returns a Scale or an error from the SCL file contents in memory.
331    pub fn parse_scl_data(scl_contents: &str) -> Result<Self, TuningError> {
332        enum State {
333            ReadHeader,
334            ReadCount,
335            ReadNote,
336            Trailing,
337        }
338        let mut state = State::ReadHeader;
339
340        let mut res = Scale::new();
341
342        for (lineno, line) in scl_contents.split('\n').map(|x| x.trim()).enumerate() {
343            if (matches!(state, State::ReadNote) && line.is_empty()) || line.starts_with('!') {
344                continue;
345            }
346
347            match state {
348                State::ReadHeader => {
349                    res.description = line.to_string();
350                    state = State::ReadCount
351                }
352                State::ReadCount => {
353                    res.count = match line.parse() {
354                        Ok(value) if value >= 1 => value,
355                        Ok(_) => return Err(TuningError::InvalidNoteCount),
356                        Err(_) => {
357                            return Err(TuningError::ParseError(format!(
358                                "Error parsing note count on line {lineno}."
359                            )))
360                        }
361                    };
362
363                    state = State::ReadNote;
364                }
365                State::ReadNote => {
366                    let t = Tone::from_string(line, Some(lineno))?;
367                    res.tones.push(t);
368
369                    if res.tones.len() == res.count {
370                        state = State::Trailing;
371                    }
372                }
373                State::Trailing => (),
374            }
375        }
376
377        if !matches!(state, State::ReadNote | State::Trailing) {
378            return Err(TuningError::IncompleteSCL);
379        }
380
381        if res.tones.len() != res.count {
382            return Err(TuningError::InvalidNoteCount);
383        }
384
385        res.raw_text = scl_contents.to_string();
386        Ok(res)
387    }
388
389    /// Provides a utility scale which is the "standard tuning" scale.
390    pub fn even_temperament_12_note_scale() -> Self {
391        let data = "! 12 Tone Equal Temperament.scl
392            !
393            12 Tone Equal Temperament | ED2-12 - Equal division of harmonic 2 into 12 parts
394             12
395            !
396             100.00000
397             200.00000
398             300.00000
399             400.00000
400             500.00000
401             600.00000
402             700.00000
403             800.00000
404             900.00000
405             1000.00000
406             1100.00000
407             2/1";
408
409        Scale::parse_scl_data(data).expect("This shouldn't fail")
410    }
411
412    /// Provides a scale refered to as "ED2-17" or "ED3-24" by dividing the `span` into `m` points.
413    /// `Scale::even_division_of_span_by_m(2, 12)` should be the
414    /// [`Scale::even_temperament_12_note_scale()`].
415    ///
416    /// ```
417    /// # use tuning_library_rs::Scale;
418    /// let s1 = Scale::even_division_of_span_by_m(2, 12).unwrap(); // equal to `even_temperament_12_note_scale()`
419    /// let s2 = Scale::even_division_of_span_by_m(2, 17).unwrap(); // ED2-17
420    /// let s3 = Scale::even_division_of_span_by_m(3, 24).unwrap(); // ED3-24
421    /// ```
422    pub fn even_division_of_span_by_m(span: u32, m: u32) -> Result<Self, TuningError> {
423        if span == 0 {
424            return Err(TuningError::ZeroSpan);
425        }
426
427        if m == 0 {
428            return Err(TuningError::ZeroSteps);
429        }
430
431        let mut data = String::new();
432        data += &format!("! Automatically generated ED{span}-{m} scale\n");
433        data += &format!("Automatically generated ED{span}-{m} scale\n");
434        data += &format!("{m}\n");
435        data += "!\n";
436
437        let top_cents = 1200.0 * (span as f64).log2() / 2f64.log2();
438        let d_cents = top_cents / m as f64;
439        data += &(1..m)
440            .map(|i| format!("{:.32}\n", d_cents * i as f64))
441            .collect::<String>();
442        data += &format!("{span}/1\n");
443
444        Scale::parse_scl_data(&data)
445    }
446
447    /// Provides a scale which divides `cents` into `m` steps. It is less frequently used than
448    /// [`Scale::even_division_of_span_by_m()`] for obvious reasons. If you want the last cents
449    /// label labeled differently than the cents argument, pass in the associated label.
450    pub fn even_division_of_cents_by_m(
451        cents: f64,
452        m: u32,
453        last_label: &str,
454    ) -> Result<Self, TuningError> {
455        if cents <= 0.0 {
456            return Err(TuningError::NonPositiveCents);
457        }
458
459        if m == 0 {
460            return Err(TuningError::ZeroSteps);
461        }
462
463        let mut data = String::new();
464        data += &format!("! Automatically generated Even Division of {cents} ct into {m} scale\n");
465        data += &format!("Automatically generated Even Division of {cents} ct into {m} scale\n");
466        data += &format!("{m}\n");
467        data += "!\n";
468
469        let top_cents = cents;
470        let d_cents = top_cents / m as f64;
471        data += &(1..m)
472            .map(|i| format!("{}\n", d_cents * i as f64))
473            .collect::<String>();
474
475        data += &match last_label {
476            "" => format!("{cents}\n"),
477            label => format!("{label}\n"),
478        };
479
480        Scale::parse_scl_data(&data)
481    }
482}
483
484/// The KeyboardMapping struct represents a KBM file.
485///
486/// In most cases, the salient features are the [`KeyboardMapping::tuning_constant_note`] and
487/// [`KeyboardMapping::tuning_frequency`], which allow you to pick a fixed note in the MIDI keyboard
488/// when retuning. The KBM file can also remap individual keys to individual points in a scale,
489/// which here is done with the [keys vector][`KeyboardMapping::keys`].
490///
491/// Just as with [`Scale`] the [`KeyboardMapping::raw_text`] member contains the text of the KBM
492/// file used.
493#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
494#[derive(Default, Clone, Debug)]
495pub struct KeyboardMapping {
496    /// Size of the mapping.
497    count: usize,
498
499    /// First MIDI note to be mapped.
500    pub first_midi: i32,
501
502    /// Last MIDI note to be mapped.
503    pub last_midi: i32,
504
505    /// Middle MIDI note.
506    pub middle_note: i32,
507
508    /// MIDI note to be tuned.
509    pub tuning_constant_note: i32,
510
511    /// Frequency of the tuned note.
512    pub tuning_frequency: f64,
513
514    /// Pitch of the tuned note. Equal to `tuning_frequency / MIDI_0_FREQ`.
515    pub tuning_pitch: f64,
516
517    /// Number of octave degrees.
518    pub octave_degrees: i32,
519
520    /// Mapped keys. Rather than an 'x', we use a '-1' for skipped notes (this should use Option or
521    /// similar).
522    pub keys: Vec<i32>, // TODO: use Option
523
524    /// Raw text of the KBM file.
525    pub raw_text: String,
526
527    /// Name of the mapping.
528    pub name: String,
529}
530
531impl KeyboardMapping {
532    /// Constructs a default `KeyboardMapping`.
533    pub fn new() -> Self {
534        let mut k = KeyboardMapping {
535            count: 0,
536            first_midi: 0,
537            last_midi: 127,
538            middle_note: 60,
539            tuning_constant_note: 60,
540            tuning_frequency: MIDI_0_FREQ * 32.0,
541            tuning_pitch: 32.0,
542            octave_degrees: 0,
543            raw_text: String::new(),
544            ..Default::default()
545        };
546
547        k.raw_text = format!(
548            "! Default KBM file\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
549            k.count,
550            k.first_midi,
551            k.last_midi,
552            k.middle_note,
553            k.tuning_constant_note,
554            k.tuning_frequency,
555            k.octave_degrees
556        );
557
558        k
559    }
560
561    /// Number of keys in the mapping.
562    pub fn count(&self) -> usize {
563        self.count
564    }
565
566    /// Returns a KeyboardMapping or an error from a KBM file in `fname`.
567    pub fn read_kbm_file<P>(fname: P) -> Result<Self, TuningError>
568    where
569        P: AsRef<std::path::Path>,
570    {
571        let content = match fs::read_to_string(&fname) {
572            Ok(lines) => lines,
573            Err(_) => return Err(TuningError::FileError),
574        };
575
576        let mut res = KeyboardMapping::parse_kbm_data(&content)?;
577        res.name = fname.as_ref().to_str().unwrap_or_default().to_string();
578        Ok(res)
579    }
580
581    /// Returns a KeyboardMapping or an error from a KBM data in memory.
582    pub fn parse_kbm_data(kbm_contents: &str) -> Result<Self, TuningError> {
583        enum ParsePosition {
584            MapSize,
585            FirstMidi,
586            LastMidi,
587            Middle,
588            Reference,
589            Freq,
590            Degree,
591            Keys,
592            Trailing,
593        }
594
595        impl ParsePosition {
596            fn next(&self) -> ParsePosition {
597                match self {
598                    ParsePosition::MapSize => Self::FirstMidi,
599                    ParsePosition::FirstMidi => Self::LastMidi,
600                    ParsePosition::LastMidi => Self::Middle,
601                    ParsePosition::Middle => Self::Reference,
602                    ParsePosition::Reference => Self::Freq,
603                    ParsePosition::Freq => Self::Degree,
604                    ParsePosition::Degree => Self::Keys,
605                    ParsePosition::Keys => Self::Trailing,
606                    ParsePosition::Trailing => panic!("this should not happen"),
607                }
608            }
609        }
610        let mut state = ParsePosition::MapSize;
611
612        let mut res = KeyboardMapping::new();
613
614        for (lineno, mut line) in kbm_contents.split('\n').map(|x| x.trim()).enumerate() {
615            if line.starts_with('!') {
616                continue;
617            }
618
619            if line == "x" {
620                line = "-1";
621            } else if !matches!(state, ParsePosition::Trailing) {
622                let lc = line.chars();
623                let mut valid_line = !line.is_empty();
624                let mut bad_char = '\0';
625
626                for val in lc {
627                    if !valid_line || val == '\0' {
628                        break;
629                    }
630
631                    if !(val == ' '
632                        || val.is_ascii_digit()
633                        || val == '.'
634                        || val == 13 as char
635                        || val == '\n')
636                    {
637                        valid_line = false;
638                        bad_char = val;
639                    }
640                }
641
642                if !valid_line {
643                    return Err(TuningError::ParseError(format!(
644                        "Bad character {bad_char} on line {lineno}"
645                    )));
646                }
647            }
648
649            let i = match line.parse::<i32>() {
650                Err(_) => Err(TuningError::ParseError(format!(
651                    "Value on line {lineno} is not integer value."
652                ))),
653                Ok(x) => Ok(x),
654            };
655            let v = match line.parse::<f64>() {
656                Err(_) => Err(TuningError::ParseError(format!(
657                    "Value on line {lineno} is not float value."
658                ))),
659                Ok(x) => Ok(x),
660            };
661
662            match state {
663                ParsePosition::MapSize => res.count = i? as usize,
664                ParsePosition::FirstMidi => res.first_midi = i?,
665                ParsePosition::LastMidi => res.last_midi = i?,
666                ParsePosition::Middle => res.middle_note = i?,
667                ParsePosition::Reference => res.tuning_constant_note = i?,
668                ParsePosition::Freq => {
669                    res.tuning_frequency = v?;
670                    res.tuning_pitch = res.tuning_frequency / MIDI_0_FREQ;
671                }
672                ParsePosition::Degree => res.octave_degrees = i?,
673                ParsePosition::Keys => {
674                    res.keys.push(i?);
675                    if res.keys.len() == res.count {
676                        state = ParsePosition::Trailing;
677                    }
678                }
679                ParsePosition::Trailing => {}
680            }
681
682            if !(matches!(state, ParsePosition::Keys | ParsePosition::Trailing)) {
683                state = state.next();
684            }
685            if matches!(state, ParsePosition::Keys) && res.count == 0 {
686                state = ParsePosition::Trailing;
687            }
688        }
689
690        if !matches!(state, ParsePosition::Keys | ParsePosition::Trailing) {
691            return Err(TuningError::IncompleteKBM);
692        }
693
694        if res.keys.len() != res.count {
695            return Err(TuningError::InvalidKeyCount);
696        }
697
698        res.raw_text = kbm_contents.to_string();
699        Ok(res)
700    }
701
702    /// Creates a KeyboardMapping which keeps the MIDI note 60 (A4) set to a constant given
703    /// frequency.
704    pub fn tune_a69_to(freq: f64) -> Self {
705        KeyboardMapping::tune_note_to(69, freq)
706    }
707
708    /// Creates a KeyboardMapping which keeps the MIDI note given set to a constant given
709    /// frequency.
710    pub fn tune_note_to(midi_note: i32, freq: f64) -> Self {
711        KeyboardMapping::start_scale_on_and_tune_note_to(60, midi_note, freq)
712    }
713
714    /// Generates a KBM where `scale_start` is the note 0 of the scale, where `midi_note` is the
715    /// tuned note, and where `freq` is the frequency.
716    pub fn start_scale_on_and_tune_note_to(scale_start: i32, midi_note: i32, freq: f64) -> Self {
717        let data = format!(
718            "! Automatically generated mapping, tuning note {midi_note} to {freq} Hz
719            !
720            ! Size of map
721            0
722            ! First and last MIDI notes to map - map the entire keyboard
723            0
724            127
725            ! Middle note where the first entry in the scale is mapped.
726            {scale_start}
727            ! Reference note where frequency is fixed
728            {midi_note}
729            ! Frequency for MIDI note {midi_note}
730            {freq}
731            ! Scale degree for formal octave. This is an empty mapping, so:
732            0
733            ! Mapping. This is an empty mapping so list no keys"
734        );
735
736        KeyboardMapping::parse_kbm_data(&data).expect("this should not fail")
737    }
738}
739
740const N: usize = 512;
741
742/// The Tuning struct is the primary place where you will interact with this library.
743///
744/// It is constrcted for a scale and mapping and then gives you the ability to determine frequencies
745/// across and beyond the MIDI keyboard. Since modulation can force key number well outside the [0,
746/// 127] range, we support a MIDI note range from -256 to +256 spanning more than the entire
747/// frequency space reasonable.
748///
749/// To use this struct, you construct a fresh instance every time you want to use a different
750/// [`Scale`] and [`KeyboardMapping`]. If you want to tune to a different scale or mapping, just
751/// construct a new instance.
752///
753/// ```
754/// # use tuning_library_rs::*;
755/// let s = Scale::even_temperament_12_note_scale(); // or any other function constructing a Scale
756/// let k = KeyboardMapping::tune_a69_to(432.0); // or any other function constructing a KeyboardMapping
757///
758/// let t1 = Tuning::from_scale(s.clone());
759/// let t2 = Tuning::from_keyboard_mapping(k.clone());
760/// let t3 = Tuning::from_scale_and_keyboard_mapping(s.clone(), k.clone(), AllowTuningOnUnmapped(false));
761/// ```
762#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
763#[derive(Clone, Debug)]
764pub struct Tuning {
765    /// Scale of the tuning.
766    pub scale: Scale,
767
768    /// Keyboard mapping of the tuning.
769    pub keyboard_mapping: KeyboardMapping,
770
771    ptable: Vec<f64>,
772    lptable: Vec<f64>,
773    scale_position_table: Vec<i32>,
774    allow_tuning_center_on_unmapped: bool,
775}
776
777impl Default for Tuning {
778    fn default() -> Self {
779        Self::new()
780    }
781}
782
783impl Tuning {
784    /// Constucts a `Tuning` with 12-EDO scale and standard mapping.
785    pub fn new() -> Self {
786        Tuning::from_scale_and_keyboard_mapping(
787            Scale::even_temperament_12_note_scale(),
788            KeyboardMapping::new(),
789            AllowTuningOnUnmapped(false),
790        )
791        .unwrap()
792    }
793
794    /// Constructs a `Tuning` with given `scale` and standard mapping.
795    pub fn from_scale(scale: Scale) -> Result<Self, TuningError> {
796        Tuning::from_scale_and_keyboard_mapping(
797            scale,
798            KeyboardMapping::new(),
799            AllowTuningOnUnmapped(false),
800        )
801    }
802
803    /// Constructs a `Tuning` with 12-EDO scale and given `keyboard_mapping`.
804    pub fn from_keyboard_mapping(keyboard_mapping: KeyboardMapping) -> Result<Self, TuningError> {
805        Tuning::from_scale_and_keyboard_mapping(
806            Scale::even_temperament_12_note_scale(),
807            keyboard_mapping,
808            AllowTuningOnUnmapped(false),
809        )
810    }
811
812    /// Constructs a `Tuning` with given `scale` and `keyboard_mapping`.
813    pub fn from_scale_and_keyboard_mapping(
814        scale: Scale,
815        keyboard_mapping: KeyboardMapping,
816        allow_tuning_center_on_unmapped: AllowTuningOnUnmapped,
817    ) -> Result<Self, TuningError> {
818        let mut tun = Tuning {
819            scale,
820            keyboard_mapping,
821            allow_tuning_center_on_unmapped: allow_tuning_center_on_unmapped.0,
822            ptable: vec![0f64; N],
823            lptable: vec![0f64; N],
824            scale_position_table: vec![0; N],
825        };
826
827        let mut o_sp = 0;
828        if tun.scale.count == 0 {
829            return Err(TuningError::TooFewNotes);
830        }
831
832        let kbm_rotations = tun
833            .keyboard_mapping
834            .keys
835            .iter()
836            .map(|x| (*x as f64 / tun.scale.count as f64).ceil() as i32)
837            .max()
838            .unwrap_or(1);
839
840        if kbm_rotations > 1 {
841            let mut new_s = tun.scale.clone();
842            new_s.count = tun.scale.count * kbm_rotations as usize;
843            let back_cents = tun.scale.tones.last().unwrap().value.cents();
844            let mut push_off = back_cents;
845
846            for _ in 1..kbm_rotations {
847                for t in &tun.scale.tones {
848                    let mut t_copy = t.clone();
849                    t_copy.value = ToneValue::Cents(t.value.cents() + push_off);
850                    new_s.tones.push(t_copy);
851                }
852                push_off += back_cents;
853            }
854
855            tun.scale = new_s;
856            tun.keyboard_mapping.octave_degrees *= kbm_rotations;
857            if tun.keyboard_mapping.octave_degrees == 0 {
858                tun.keyboard_mapping.octave_degrees = tun.scale.count as i32;
859            }
860        }
861
862        if tun.keyboard_mapping.octave_degrees > tun.scale.count as i32 {
863            return Err(TuningError::MappingLongerThanScale);
864        }
865
866        let mut pitches = [0.0; N];
867
868        let pos_pitch_0 = 256 + tun.keyboard_mapping.tuning_constant_note;
869        let pos_scale_0 = 256 + tun.keyboard_mapping.middle_note;
870
871        let pitch_mod = tun.keyboard_mapping.tuning_pitch.log2() / 2f64.log2() - 1.0;
872
873        let mut scale_position_of_tuning_note =
874            tun.keyboard_mapping.tuning_constant_note - tun.keyboard_mapping.middle_note;
875
876        if tun.keyboard_mapping.count > 0 {
877            while scale_position_of_tuning_note >= tun.keyboard_mapping.count as i32 {
878                scale_position_of_tuning_note -= tun.keyboard_mapping.count as i32;
879            }
880
881            while scale_position_of_tuning_note < 0 {
882                scale_position_of_tuning_note += tun.keyboard_mapping.count as i32;
883            }
884
885            o_sp = scale_position_of_tuning_note;
886            scale_position_of_tuning_note =
887                tun.keyboard_mapping.keys[scale_position_of_tuning_note as usize];
888
889            if scale_position_of_tuning_note == -1 && !tun.allow_tuning_center_on_unmapped {
890                return Err(TuningError::TuningUnmappedKey);
891            }
892        }
893
894        let tuning_center_pitch_offset;
895        if scale_position_of_tuning_note == 0 {
896            tuning_center_pitch_offset = 0.0;
897        } else if scale_position_of_tuning_note == -1 && tun.allow_tuning_center_on_unmapped {
898            let mut low = 0;
899            let mut high = 0;
900            let mut octave_up = false;
901            let mut octave_down = false;
902
903            let mut i = o_sp as usize - 1;
904            while i != o_sp as usize {
905                if tun.keyboard_mapping.keys[i] != -1 {
906                    low = tun.keyboard_mapping.keys[i];
907                    break;
908                }
909
910                if i > o_sp as usize {
911                    octave_down = true;
912                }
913
914                i = (i - 1) % tun.keyboard_mapping.count;
915            }
916
917            i = o_sp as usize + 1;
918            while i != o_sp as usize {
919                if tun.keyboard_mapping.keys[i] != -1 {
920                    high = tun.keyboard_mapping.keys[i];
921                    break;
922                }
923
924                if i < o_sp as usize {
925                    octave_up = true;
926                }
927
928                i = (i + 1) % tun.keyboard_mapping.count;
929            }
930
931            let dt = tun.scale.tones[tun.scale.count - 1].value.cents();
932            let pitch_low = if octave_down {
933                tun.scale.tones[low as usize - 1].value.cents() - dt
934            } else {
935                tun.scale.tones[low as usize - 1].value.float_value() - 1.0
936            };
937            let pitch_high = if octave_up {
938                tun.scale.tones[high as usize - 1].value.cents() + dt
939            } else {
940                tun.scale.tones[high as usize - 1].value.float_value() - 1.0
941            };
942            tuning_center_pitch_offset = (pitch_high + pitch_low) / 2.0;
943        } else {
944            let mut tshift = 0.0;
945            let dt = tun.scale.tones[tun.scale.count - 1].value.float_value() - 1.0;
946
947            while scale_position_of_tuning_note < 0 {
948                scale_position_of_tuning_note += tun.scale.count as i32;
949                tshift += dt;
950            }
951            while scale_position_of_tuning_note > tun.scale.count as i32 {
952                scale_position_of_tuning_note -= tun.scale.count as i32;
953                tshift -= dt;
954            }
955
956            if scale_position_of_tuning_note == 0 {
957                tuning_center_pitch_offset = -tshift;
958            } else {
959                tuning_center_pitch_offset = tun.scale.tones
960                    [scale_position_of_tuning_note as usize - 1]
961                    .value
962                    .float_value()
963                    - 1.0
964                    - tshift;
965            }
966        }
967
968        for (i, pitch) in pitches.iter_mut().enumerate().take(N) {
969            let distance_from_pitch_0 = i as i32 - pos_pitch_0;
970            let distance_from_scale_0 = i as i32 - pos_scale_0;
971
972            if distance_from_pitch_0 == 0 {
973                *pitch = 1.0;
974                tun.lptable[i] = *pitch + pitch_mod;
975                tun.ptable[i] = 2f64.powf(tun.lptable[i]);
976
977                if tun.keyboard_mapping.count > 0 {
978                    let mut mapping_key = distance_from_scale_0 % tun.keyboard_mapping.count as i32;
979                    if mapping_key < 0 {
980                        mapping_key += tun.keyboard_mapping.count as i32;
981                    }
982
983                    let cm = tun.keyboard_mapping.keys[mapping_key as usize];
984                    if !tun.allow_tuning_center_on_unmapped && cm < 0 {
985                        return Err(TuningError::TuningUnmappedKey);
986                    }
987                }
988
989                tun.scale_position_table[i] =
990                    scale_position_of_tuning_note % tun.scale.count as i32;
991            } else {
992                let mut rounds;
993                let mut this_round;
994                let mut disable = false;
995                if tun.keyboard_mapping.count == 0 {
996                    rounds = (distance_from_scale_0 - 1) / tun.scale.count as i32;
997                    this_round = (distance_from_scale_0 - 1) % tun.scale.count as i32;
998                } else {
999                    let mut mapping_key = distance_from_scale_0 % tun.keyboard_mapping.count as i32;
1000                    if mapping_key < 0 {
1001                        mapping_key += tun.keyboard_mapping.count as i32;
1002                    }
1003
1004                    let mut rotations = 0;
1005                    let mut dt = distance_from_scale_0;
1006                    if dt > 0 {
1007                        while dt >= tun.keyboard_mapping.count as i32 {
1008                            dt -= tun.keyboard_mapping.count as i32;
1009                            rotations += 1;
1010                        }
1011                    } else {
1012                        while dt < 0 {
1013                            dt += tun.keyboard_mapping.count as i32;
1014                            rotations -= 1;
1015                        }
1016                    }
1017
1018                    let cm = tun.keyboard_mapping.keys[mapping_key as usize];
1019
1020                    let mut push = 0;
1021                    if cm < 0 {
1022                        disable = true;
1023                    } else {
1024                        if cm > tun.scale.count as i32 {
1025                            return Err(TuningError::MappingLongerThanScale);
1026                        }
1027                        push = mapping_key - cm;
1028                    }
1029
1030                    if tun.keyboard_mapping.octave_degrees > 0
1031                        && tun.keyboard_mapping.octave_degrees != tun.keyboard_mapping.count as i32
1032                    {
1033                        rounds = rotations;
1034                        this_round = cm - 1;
1035                        if this_round < 0 {
1036                            this_round = tun.keyboard_mapping.octave_degrees - 1;
1037                            rounds -= 1;
1038                        }
1039                    } else {
1040                        rounds = (distance_from_scale_0 - push - 1) / tun.scale.count as i32;
1041                        this_round = (distance_from_scale_0 - push - 1) % tun.scale.count as i32;
1042                    }
1043                }
1044
1045                if this_round < 0 {
1046                    this_round += tun.scale.count as i32;
1047                    rounds -= 1;
1048                }
1049
1050                if disable {
1051                    *pitch = 0.0;
1052                    tun.scale_position_table[i] = -1;
1053                } else {
1054                    *pitch = tun.scale.tones[this_round as usize].value.float_value()
1055                        + rounds as f64
1056                            * (tun.scale.tones[tun.scale.count - 1].value.float_value() - 1.0)
1057                        - tuning_center_pitch_offset;
1058                    tun.scale_position_table[i] = (this_round + 1) % tun.scale.count as i32;
1059                }
1060
1061                tun.lptable[i] = *pitch + pitch_mod;
1062                tun.ptable[i] = 2f64.powf(*pitch + pitch_mod);
1063            }
1064        }
1065
1066        Ok(tun)
1067    }
1068
1069    /// Fills unmapped notes with interpolated vaules.
1070    pub fn with_skipped_notes_interpolated(&self) -> Self {
1071        let mut res = self.clone();
1072
1073        for i in 1..N - 1 {
1074            if self.scale_position_table[i] >= 0 {
1075                continue;
1076            }
1077
1078            let mut nxt = i + 1;
1079            let mut prv = i - 1;
1080            while self.scale_position_table[prv] < 0 {
1081                prv -= 1;
1082            }
1083            while nxt < N && self.scale_position_table[nxt] < 0 {
1084                nxt += 1;
1085            }
1086            let dist = (nxt - prv) as f64;
1087            let frac = (i - prv) as f64 / dist;
1088            res.lptable[i] = (1.0 - frac) * self.lptable[prv] + frac * self.lptable[nxt];
1089            res.ptable[i] = 2f64.powf(res.lptable[i]);
1090        }
1091
1092        res
1093    }
1094
1095    /// Retunrs the frequency in Hz for a given MIDI note.
1096    /// ```
1097    /// # use tuning_library_rs::*;
1098    /// let t = Tuning::new();
1099    /// assert!((t.frequency_for_midi_note(69) - 440.0).abs() < 1e-4); // A
1100    /// assert!((t.frequency_for_midi_note(60) - 261.6256).abs() < 1e-4); // middle C
1101    /// ```
1102    pub fn frequency_for_midi_note(&self, midi_note: i32) -> f64 {
1103        let mni = (N as i32 - 1).min(0.max(midi_note + 256));
1104        self.ptable[mni as usize] * MIDI_0_FREQ
1105    }
1106
1107    /// Returns the frequency but with the standard frequency of MIDI note 0 divided out.
1108    /// ```
1109    /// # use tuning_library_rs::*;
1110    /// let t = Tuning::new();
1111    /// assert_eq!(t.frequency_for_midi_note_scaled_by_midi0(0), 1.0);
1112    /// assert_eq!(t.frequency_for_midi_note_scaled_by_midi0(60), 32.0);
1113    /// ```
1114    pub fn frequency_for_midi_note_scaled_by_midi0(&self, midi_note: i32) -> f64 {
1115        let mni = (N as i32 - 1).min(0.max(midi_note + 256));
1116        self.ptable[mni as usize]
1117    }
1118
1119    /// Returns the log base 2 of the scaled frequency.
1120    /// ```
1121    /// # use tuning_library_rs::*;
1122    /// let t = Tuning::new();
1123    /// assert_eq!(t.log_scaled_frequency_for_midi_note(0), 0.0);
1124    /// assert_eq!(t.log_scaled_frequency_for_midi_note(60), 5.0);
1125    /// ```
1126    /// The value increases by one per frequency double.
1127    pub fn log_scaled_frequency_for_midi_note(&self, midi_note: i32) -> f64 {
1128        let mni = (N as i32 - 1).min(0.max(midi_note + 256));
1129        self.lptable[mni as usize]
1130    }
1131
1132    /// Returns the space in the logical scale. Note 0 is the root. It has a maximum value of
1133    /// `count-1`. Note that SCL files omit the root internally and so this logical scale position
1134    /// is off by 1 from the index in the tones vector of the Scale data.
1135    pub fn scale_position_for_midi_note(&self, midi_note: i32) -> i32 {
1136        let mni = (N as i32 - 1).min(0.max(midi_note + 256));
1137        self.scale_position_table[mni as usize]
1138    }
1139
1140    /// Returns whether a given `midi_note` is mapped in the `Tuning`'s `KeyboardMapping`.
1141    pub fn is_midi_note_mapped(&self, midi_note: i32) -> bool {
1142        let mni = (N as i32 - 1).min(0.max(midi_note + 256));
1143        self.scale_position_table[mni as usize] >= 0
1144    }
1145}