Skip to content

Commit

Permalink
Separate functionality into different files
Browse files Browse the repository at this point in the history
  • Loading branch information
maxwellmattryan committed Oct 31, 2024
1 parent 75080f4 commit 4365d21
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 127 deletions.
25 changes: 3 additions & 22 deletions src/commands/freq.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use async_trait::async_trait;
use regex::Regex;

use crate::{
cli::Command,
commands::midi::{midi_note_number_to_frequency, midi_note_number_to_music_note},
error::{FNoteError, FNoteResult},
convert::{frequency_to_midi_note, midi_note_number_to_frequency, midi_note_number_to_music_note},
error::FNoteResult,
parse::try_frequency_from_str,
};

/// Extracts the (nearest) MIDI note number and music note from a frequency.
Expand Down Expand Up @@ -37,22 +37,3 @@ impl Command for FreqCommand {
Ok(())
}
}

/// Converts a frequency to the nearest MIDI note number.
pub(crate) fn frequency_to_midi_note(frequency: f32) -> Option<u8> {
let midi_note = 69.0 + 12.0 * (frequency / 440.0).log2();
if (0..=127).contains(&(midi_note.round() as i32)) {
Some(midi_note.round() as u8)
} else {
None
}
}

fn try_frequency_from_str(arg: &str) -> FNoteResult<f32> {
let regex = Regex::new(r"^\d+(\.\d+)?$").unwrap();
if regex.is_match(arg) {
Ok(arg.parse::<f32>().unwrap())
} else {
Err(FNoteError::InvalidFrequency(arg.to_string()))
}
}
32 changes: 3 additions & 29 deletions src/commands/midi.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use async_trait::async_trait;
use regex::Regex;

use crate::{
cli::Command,
error::{FNoteError, FNoteResult},
convert::{midi_note_number_to_frequency, midi_note_number_to_music_note},
error::FNoteResult,
parse::try_midi_note_number_from_str,
};

/// Extracts the frequency and music note from a MIDI note number (0-127).
Expand All @@ -27,30 +28,3 @@ impl Command for MidiCommand {
Ok(())
}
}

/// Converts a MIDI note number to a music note.
pub(crate) fn midi_note_number_to_music_note(midi_note_number: u8) -> Option<String> {
let semitone_to_note = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];

let octave = (midi_note_number / 12) as i8 - 1;
let semitone_offset = (midi_note_number % 12) as usize;
let note = semitone_to_note.get(semitone_offset).unwrap();

Some(format!("{}{}", note, octave))
}

/// Converts a MIDI note number to a frequency (Hz).
pub(crate) fn midi_note_number_to_frequency(midi_note_number: u8) -> f32 {
let frequency = 440.0 * 2.0f32.powf((midi_note_number as f32 - 69.0) / 12.0);
(frequency * 100.0).round() / 100.0
}

/// Parses a MIDI note number from a string.
fn try_midi_note_number_from_str(arg: &str) -> FNoteResult<u8> {
let regex = Regex::new(r"^(?:\d|[1-9]\d|1[01]\d|12[0-7])$").unwrap();
if regex.is_match(arg) {
Ok(arg.parse::<u8>().unwrap())
} else {
Err(FNoteError::InvalidMidiNoteNumber(arg.to_string()))
}
}
79 changes: 3 additions & 76 deletions src/commands/note.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use async_trait::async_trait;
use regex::Regex;

use crate::{
cli::Command,
commands::midi::midi_note_number_to_frequency,
error::{FNoteError, FNoteResult},
convert::{midi_note_number_to_frequency, music_note_to_midi_note_number},
error::FNoteResult,
parse::try_music_note_from_str,
};

/// Extracts the frequency and MIDI note number from a music note (e.g. C5).
Expand All @@ -28,76 +28,3 @@ impl Command for NoteCommand {
Ok(())
}
}

/// Converts a music note to a MIDI note number.
fn music_note_to_midi_note_number(music_note: &str) -> FNoteResult<u8> {
let note_to_semitone = [
("Cb", -1), // -1 instead of 11 because Cb = B, meaning decrement octave by 1
("C", 0),
("C#", 1),
("Db", 1),
("D", 2),
("D#", 3),
("Eb", 3),
("E", 4),
("Fb", 4),
("F", 5),
("E#", 5),
("F#", 6),
("Gb", 6),
("G", 7),
("G#", 8),
("Ab", 8),
("A", 9),
("A#", 10),
("Bb", 10),
("B", 11),
("B#", 12), // 12 instead of 0 because B# = C, meaning increment octave by 1
]
.iter()
.cloned()
.collect::<std::collections::HashMap<_, _>>();

if music_note.len() < 2 || music_note.len() > 4 {
Err(FNoteError::InvalidMusicNote(music_note.to_string()))
} else {
let base_note = if music_note.chars().nth(1) == Some('#') || music_note.chars().nth(1) == Some('b') {
&music_note[..2]
} else {
&music_note[..1]
};

let octave_str = &music_note[base_note.len()..];
let octave: i8 = octave_str.parse().unwrap();
if !(-1..=9).contains(&octave) {
return Err(FNoteError::InvalidMusicNote(music_note.to_string()));
}

let semitone_offset = *note_to_semitone.get(base_note).unwrap();
let midi_number = ((octave + 1) * 12 + semitone_offset as i8) as i16;
if (0..=127).contains(&midi_number) {
Ok(midi_number as u8)
} else {
Err(FNoteError::InvalidMusicNote(music_note.to_string()))
}
}
}

/// Parses a music note from a string.
fn try_music_note_from_str(arg: &str) -> FNoteResult<String> {
let regex = Regex::new(r"^[A-Ga-g][#b]?(-1|[0-9])$").unwrap();
if regex.is_match(arg) {
let mut chars = arg.chars();
let mut result = String::new();

if let Some(note_char) = chars.next() {
result.push(note_char.to_ascii_uppercase());
}

result.push_str(chars.as_str());

Ok(result)
} else {
Err(FNoteError::InvalidMusicNote(arg.to_string()))
}
}
82 changes: 82 additions & 0 deletions src/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use crate::error::{FNoteError, FNoteResult};

/// Converts a frequency to the nearest MIDI note number.
pub(crate) fn frequency_to_midi_note(frequency: f32) -> Option<u8> {
let midi_note = 69.0 + 12.0 * (frequency / 440.0).log2();
if (0..=127).contains(&(midi_note.round() as i32)) {
Some(midi_note.round() as u8)
} else {
None
}
}

/// Converts a MIDI note number to a frequency (Hz).
pub(crate) fn midi_note_number_to_frequency(midi_note_number: u8) -> f32 {
let frequency = 440.0 * 2.0f32.powf((midi_note_number as f32 - 69.0) / 12.0);
(frequency * 100.0).round() / 100.0
}

/// Converts a MIDI note number to a music note.
pub(crate) fn midi_note_number_to_music_note(midi_note_number: u8) -> Option<String> {
let semitone_to_note = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];

let octave = (midi_note_number / 12) as i8 - 1;
let semitone_offset = (midi_note_number % 12) as usize;
let note = semitone_to_note.get(semitone_offset).unwrap();

Some(format!("{}{}", note, octave))
}

/// Converts a music note to a MIDI note number.
pub(crate) fn music_note_to_midi_note_number(music_note: &str) -> FNoteResult<u8> {
let note_to_semitone = [
("Cb", -1), // -1 instead of 11 because Cb = B, meaning decrement octave by 1
("C", 0),
("C#", 1),
("Db", 1),
("D", 2),
("D#", 3),
("Eb", 3),
("E", 4),
("Fb", 4),
("F", 5),
("E#", 5),
("F#", 6),
("Gb", 6),
("G", 7),
("G#", 8),
("Ab", 8),
("A", 9),
("A#", 10),
("Bb", 10),
("B", 11),
("B#", 12), // 12 instead of 0 because B# = C, meaning increment octave by 1
]
.iter()
.cloned()
.collect::<std::collections::HashMap<_, _>>();

if music_note.len() < 2 || music_note.len() > 4 {
Err(FNoteError::InvalidMusicNote(music_note.to_string()))
} else {
let base_note = if music_note.chars().nth(1) == Some('#') || music_note.chars().nth(1) == Some('b') {
&music_note[..2]
} else {
&music_note[..1]
};

let octave_str = &music_note[base_note.len()..];
let octave: i8 = octave_str.parse().unwrap();
if !(-1..=9).contains(&octave) {
return Err(FNoteError::InvalidMusicNote(music_note.to_string()));
}

let semitone_offset = *note_to_semitone.get(base_note).unwrap();
let midi_number = ((octave + 1) * 12 + semitone_offset as i8) as i16;
if (0..=127).contains(&midi_number) {
Ok(midi_number as u8)
} else {
Err(FNoteError::InvalidMusicNote(music_note.to_string()))
}
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ pub mod cli;
pub mod error;

pub(crate) mod commands;
pub(crate) mod convert;
pub(crate) mod parse;
42 changes: 42 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use regex::Regex;

use crate::error::{FNoteError, FNoteResult};

/// Parses a frequency from a string.
pub(crate) fn try_frequency_from_str(arg: &str) -> FNoteResult<f32> {
let regex = Regex::new(r"^\d+(\.\d+)?$").unwrap();
if regex.is_match(arg) {
Ok(arg.parse::<f32>().unwrap())
} else {
Err(FNoteError::InvalidFrequency(arg.to_string()))
}
}

/// Parses a MIDI note number from a string.
pub(crate) fn try_midi_note_number_from_str(arg: &str) -> FNoteResult<u8> {
let regex = Regex::new(r"^(?:\d|[1-9]\d|1[01]\d|12[0-7])$").unwrap();
if regex.is_match(arg) {
Ok(arg.parse::<u8>().unwrap())
} else {
Err(FNoteError::InvalidMidiNoteNumber(arg.to_string()))
}
}

/// Parses a music note from a string.
pub(crate) fn try_music_note_from_str(arg: &str) -> FNoteResult<String> {
let regex = Regex::new(r"^[A-Ga-g][#b]?(-1|[0-9])$").unwrap();
if regex.is_match(arg) {
let mut chars = arg.chars();
let mut result = String::new();

if let Some(note_char) = chars.next() {
result.push(note_char.to_ascii_uppercase());
}

result.push_str(chars.as_str());

Ok(result)
} else {
Err(FNoteError::InvalidMusicNote(arg.to_string()))
}
}

0 comments on commit 4365d21

Please sign in to comment.