use super::frequency::{hz_to_midi, midi_to_hz};
use super::notation::{acc_value, key_to_notes, pitch_to_semitone};
pub struct NoteToMidiParams {
pub round_midi: bool,
}
impl Default for NoteToMidiParams {
fn default() -> Self {
Self { round_midi: true }
}
}
pub struct MidiToNoteParams {
pub octave: bool,
pub cents: bool,
pub key: String,
pub unicode: bool,
}
impl Default for MidiToNoteParams {
fn default() -> Self {
Self {
octave: true,
cents: false,
key: "C:maj".to_string(),
unicode: true,
}
}
}
pub fn note_to_midi(note: &str, round_midi: bool) -> Result<f64, String> {
let chars: Vec<char> = note.chars().collect();
if chars.is_empty() {
return Err(format!("Improper note format: {}", note));
}
let pitch =
pitch_to_semitone(chars[0]).ok_or_else(|| format!("Improper note format: {}", note))?;
let mut pos = 1;
let mut acc_offset: i32 = 0;
while pos < chars.len() {
if let Some(v) = acc_value(chars[pos]) {
acc_offset += v;
pos += 1;
} else {
break;
}
}
let (octave, cents) = if pos < chars.len() {
let remaining: String = chars[pos..].iter().collect();
let (oct_str, rest) = split_first_number(&remaining);
if oct_str.is_empty() {
(0, 0)
} else {
let oct = oct_str
.parse::<i32>()
.map_err(|_| format!("Improper note format: {}", note))?;
let c = if !rest.is_empty() {
rest.parse::<i32>()
.map_err(|_| format!("Improper note format: {}", note))?
} else {
0
};
(oct, c)
}
} else {
(0, 0)
};
let note_value =
12.0 * (octave as f64 + 1.0) + pitch as f64 + acc_offset as f64 + cents as f64 * 1e-2;
if round_midi {
Ok(note_value.round())
} else {
Ok(note_value)
}
}
fn split_first_number(s: &str) -> (String, String) {
let chars: Vec<char> = s.chars().collect();
if chars.is_empty() {
return (String::new(), String::new());
}
let mut pos = 0;
if pos < chars.len() && (chars[pos] == '+' || chars[pos] == '-') {
pos += 1;
}
while pos < chars.len() && chars[pos].is_ascii_digit() {
pos += 1;
}
let first: String = chars[..pos].iter().collect();
let rest: String = chars[pos..].iter().collect();
(first, rest)
}
pub fn midi_to_note(midi: f64, params: &MidiToNoteParams) -> Result<String, String> {
if params.cents && !params.octave {
return Err("Cannot encode cents without octave information.".to_string());
}
let note_map = key_to_notes(¶ms.key, params.unicode)?;
let note_num = midi.round() as i32;
let note_cents = (100.0 * ((midi - note_num as f64) * 100.0).round() / 100.0) as i32;
let idx = ((note_num % 12) + 12) % 12;
let mut result = note_map[idx as usize].clone();
if params.octave {
let oct = if note_num >= 0 {
note_num / 12 - 1
} else {
(note_num as f64 / 12.0).floor() as i32 - 1
};
result = format!("{}{}", result, oct);
}
if params.cents {
result = format!("{}{:+}", result, note_cents);
}
Ok(result)
}
pub fn note_to_hz(note: &str, round_midi: bool) -> Result<f64, String> {
let midi = note_to_midi(note, round_midi)?;
Ok(midi_to_hz(midi))
}
pub fn hz_to_note(freq: f64, params: &MidiToNoteParams) -> Result<String, String> {
let midi = hz_to_midi(freq);
midi_to_note(midi, params)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_note_to_midi_basic() {
assert_eq!(note_to_midi("C", true).unwrap(), 12.0);
assert_eq!(note_to_midi("C#3", true).unwrap(), 49.0);
assert_eq!(note_to_midi("f4", true).unwrap(), 65.0);
assert_eq!(note_to_midi("Bb-1", true).unwrap(), 10.0);
assert_eq!(note_to_midi("A!8", true).unwrap(), 116.0);
}
#[test]
fn test_note_to_midi_unicode() {
assert_eq!(note_to_midi("C\u{266F}3", true).unwrap(), 49.0); assert_eq!(note_to_midi("C\u{266D}3", true).unwrap(), 47.0); }
#[test]
fn test_note_to_midi_double_accidentals() {
assert_eq!(note_to_midi("G\u{1D12A}6", true).unwrap(), 93.0); assert_eq!(note_to_midi("B\u{1D12B}6", true).unwrap(), 93.0); }
#[test]
fn test_note_to_midi_multiple_accidentals() {
assert_eq!(note_to_midi("C\u{266D}\u{1D12B}5", true).unwrap(), 69.0);
}
#[test]
fn test_note_to_midi_with_cents() {
let result = note_to_midi("C2-32", false).unwrap();
assert!((result - 35.68).abs() < 1e-10);
}
#[test]
fn test_note_to_midi_array() {
let notes = ["C", "E", "G"];
let midis: Vec<f64> = notes
.iter()
.map(|n| note_to_midi(n, true).unwrap())
.collect();
assert_eq!(midis, vec![12.0, 16.0, 19.0]);
}
#[test]
fn test_note_to_midi_invalid() {
assert!(note_to_midi("X", true).is_err());
assert!(note_to_midi("", true).is_err());
}
#[test]
fn test_midi_to_note_basic() {
let p = MidiToNoteParams::default();
assert_eq!(midi_to_note(0.0, &p).unwrap(), "C-1");
assert_eq!(midi_to_note(37.0, &p).unwrap(), "C\u{266F}2");
assert_eq!(midi_to_note(69.0, &p).unwrap(), "A4");
}
#[test]
fn test_midi_to_note_negative() {
let p = MidiToNoteParams::default();
assert_eq!(midi_to_note(-2.0, &p).unwrap(), "A\u{266F}-2");
}
#[test]
fn test_midi_to_note_ascii() {
let p = MidiToNoteParams {
unicode: false,
..Default::default()
};
assert_eq!(midi_to_note(37.0, &p).unwrap(), "C#2");
}
#[test]
fn test_midi_to_note_no_octave() {
let p = MidiToNoteParams {
octave: false,
..Default::default()
};
assert_eq!(midi_to_note(69.0, &p).unwrap(), "A");
}
#[test]
fn test_midi_to_note_with_cents() {
let p = MidiToNoteParams {
cents: true,
..Default::default()
};
assert_eq!(midi_to_note(104.7, &p).unwrap(), "A7-30");
}
#[test]
fn test_midi_to_note_cents_requires_octave() {
let p = MidiToNoteParams {
octave: false,
cents: true,
..Default::default()
};
assert!(midi_to_note(69.0, &p).is_err());
}
#[test]
fn test_midi_to_note_with_key() {
let p = MidiToNoteParams {
key: "F:min".to_string(),
..Default::default()
};
let notes: Vec<String> = (12..24)
.map(|m| midi_to_note(m as f64, &p).unwrap())
.collect();
assert_eq!(
notes,
vec![
"C0",
"D\u{266D}0",
"D0",
"E\u{266D}0",
"E0",
"F0",
"G\u{266D}0",
"G0",
"A\u{266D}0",
"A0",
"B\u{266D}0",
"B0"
]
);
}
#[test]
fn test_midi_to_note_chromatic() {
let p = MidiToNoteParams::default();
let notes: Vec<String> = (12..24)
.map(|m| midi_to_note(m as f64, &p).unwrap())
.collect();
assert_eq!(
notes,
vec![
"C0",
"C\u{266F}0",
"D0",
"D\u{266F}0",
"E0",
"F0",
"F\u{266F}0",
"G0",
"G\u{266F}0",
"A0",
"A\u{266F}0",
"B0"
]
);
}
#[test]
fn test_note_to_hz() {
let hz = note_to_hz("A4", true).unwrap();
assert!((hz - 440.0).abs() < 1e-10);
let hz = note_to_hz("A3", true).unwrap();
assert!((hz - 220.0).abs() < 1e-10);
}
#[test]
fn test_hz_to_note() {
let p = MidiToNoteParams::default();
assert_eq!(hz_to_note(440.0, &p).unwrap(), "A4");
}
#[test]
fn test_hz_to_note_with_cents() {
let p = MidiToNoteParams {
cents: true,
..Default::default()
};
let notes: Vec<String> = [32.0, 64.0]
.iter()
.map(|&f| hz_to_note(f, &p).unwrap())
.collect();
assert_eq!(notes, vec!["C1-38", "C2-38"]);
}
#[test]
fn test_note_midi_roundtrip() {
let p = MidiToNoteParams::default();
for note_str in &["C0", "C#3", "A4", "G9", "F-1"] {
let midi = note_to_midi(note_str, true).unwrap();
let back = midi_to_note(midi, &p).unwrap();
let midi2 = note_to_midi(&back, true).unwrap();
assert_eq!(midi, midi2, "Roundtrip failed for {}", note_str);
}
}
#[test]
fn test_note_hz_roundtrip() {
for note_str in &["A4", "C4", "G3"] {
let hz = note_to_hz(note_str, true).unwrap();
let p = MidiToNoteParams::default();
let back = hz_to_note(hz, &p).unwrap();
let hz2 = note_to_hz(&back, true).unwrap();
assert!(
(hz - hz2).abs() < 1e-10,
"Roundtrip failed for {}",
note_str
);
}
}
}