From 07fea4b3934d2a2db465bf0e66125eb26fcd155b Mon Sep 17 00:00:00 2001 From: Denis Redozubov Date: Fri, 9 Jun 2023 18:57:36 +0400 Subject: [PATCH] Add midi tempo support --- src/bin/main.rs | 87 +++++++++++++++++++++++++++++++++--------------- src/dsl/dsl.rs | 12 ------- src/midi/core.rs | 38 +++++++++++---------- 3 files changed, 81 insertions(+), 56 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index bf07859..67db71f 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -3,13 +3,12 @@ use std::process::exit; use std::str::FromStr; use poly::dsl::dsl; -use poly::midi::core::{Part, create_smf}; +use poly::midi::core::{create_smf, Part}; use poly::midi::time::TimeSignature; use clap::*; - -#[derive(Debug, Parser)] +#[derive(Debug, Parser, Clone)] #[command(name = "poly")] #[command(author = "Denis Redozubov ")] #[command(version = "0.1")] @@ -28,13 +27,13 @@ struct Cli { crash: Option, #[arg(short = 't', default_value = "120")] - tempo: String, + tempo: u16, #[arg(short = 's', default_value = "4/4")] time_signature: String, #[arg(short = 'o', default_value = None)] - output: Option + output: Option, } fn part_to_string(part: Part) -> String { @@ -46,54 +45,88 @@ fn part_to_string(part: Part) -> String { } } -fn validate_and_parse_part(cli: Option, part: Part, patterns: &mut HashMap) -> () { +fn validate_and_parse_part( + cli: Option, + part: Part, + patterns: &mut HashMap, +) -> () { match cli { - None => {}, - Some(pattern) => { - match dsl::groups(pattern.as_str()) { - Ok((_, group)) => { patterns.insert(part, group); }, - Err(_) => { - panic!("{} pattern is malformed.", part_to_string(part)) - } - } - } + None => {} + Some(pattern) => match dsl::groups(pattern.as_str()) { + Ok((_, group)) => { + patterns.insert(part, group); + } + Err(_) => { + panic!("{} pattern is malformed.", part_to_string(part)) + } + }, } } +fn create_text_description(kick: &Option, snare: &Option, hihat: &Option, crash: &Option) -> String { + let mut parts: String = "".to_string(); + if kick.is_some() { + parts.push_str(&format!("\nKick Drum - {}", kick.clone().unwrap())); + } + if snare.is_some() { + parts.push_str(&format!("\nSnare Drum - {}", snare.clone().unwrap())); + } + if hihat.is_some() { + parts.push_str(&format!("\nHi-Hat - {}", hihat.clone().unwrap())); + } + if crash.is_some() { + parts.push_str(&format!("\nCrash Cymbal - {}", crash.clone().unwrap())); + } + format!("{}{}", "Created using Poly. Part blueprints:", parts) +} + fn main() { let matches = Cli::parse(); match matches { - Cli { kick, snare, hihat, crash, tempo, time_signature, output} => { + Cli { + kick, + snare, + hihat, + crash, + tempo, + time_signature, + output, + } => { if kick == None && snare == None && hihat == None && crash == None { println!("No drum pattern was supplied, exiting..."); - exit(1) + exit(1) } else { + let signature = match TimeSignature::from_str(&time_signature) { + Err(e) => panic!("Can't parse the time signature: {}", e), + Ok(x) => x, + }; + let text_description = create_text_description(&kick, &snare, &hihat, &crash); + let mut groups = HashMap::new(); validate_and_parse_part(kick, Part::KickDrum, &mut groups); validate_and_parse_part(snare, Part::SnareDrum, &mut groups); validate_and_parse_part(hihat, Part::HiHat, &mut groups); validate_and_parse_part(crash, Part::CrashCymbal, &mut groups); - let signature = match TimeSignature::from_str(&time_signature) { - Err(e) => panic!("Can't parse the time signature: {}", e), - Ok(x) => x - }; + let output_file = output.clone(); - match output { + match output_file { None => { println!("No output file path was supplied, running a dry run..."); - create_smf(groups, signature) - }, + create_smf(groups, signature, text_description.as_str(), tempo) + } Some(path) => { - match create_smf(groups, signature).save(path.clone()) { + match create_smf(groups, signature, text_description.as_str(), tempo) + .save(path.clone()) + { Ok(_) => { println!("{} was written successfully", path); exit(0) - }, + } Err(e) => { println!("Failed to write {}: {}", path, e); exit(1) - }, + } }; } }; diff --git a/src/dsl/dsl.rs b/src/dsl/dsl.rs index 1024ff8..04e5bff 100644 --- a/src/dsl/dsl.rs +++ b/src/dsl/dsl.rs @@ -55,18 +55,6 @@ impl KnownLength for BasicLength { } impl BasicLength { - pub(crate) fn to_power_of_2(&self) -> u8 { - match self { - BasicLength::Whole => todo!(), - BasicLength::Half => todo!(), - BasicLength::Fourth => todo!(), - BasicLength::Eighth => todo!(), - BasicLength::Sixteenth => todo!(), - BasicLength::ThirtySecond => todo!(), - BasicLength::SixtyFourth => todo!(), - } - } - pub fn from_num(n: u16) -> Result { match n { 64 => Ok(BasicLength::SixtyFourth), diff --git a/src/midi/core.rs b/src/midi/core.rs index 44999dc..06ce6d1 100644 --- a/src/midi/core.rs +++ b/src/midi/core.rs @@ -423,7 +423,7 @@ impl Length { } #[allow(dead_code)] -static MICROSECONDS_PER_BPM: u128 = 50000 as u128 / TICKS_PER_QUARTER_NOTE as u128; +static MICROSECONDS_PER_MINUTE: u128 = 60000000 as u128; #[allow(dead_code)] static MIDI_CLOCKS_PER_CLICK: u8 = 24; @@ -446,7 +446,7 @@ pub struct MidiTempo(u24); impl MidiTempo { fn from_tempo(tempo: u16) -> Self { - let mt = tempo as u32 * MICROSECONDS_PER_BPM as u32; + let mt = MICROSECONDS_PER_MINUTE as u32 / tempo as u32; Self(mt.into()) } } @@ -736,18 +736,11 @@ fn flatten_and_merge( .unwrap_or(BAR_LIMIT.clone()); println!("Converges over {} bars", converges_over_bars); let length_limit = converges_over_bars * time_signature.to_128th(); - println!( - "TimeSignature {:?} in 128th: {}", - time_signature, - time_signature.to_128th() - ); - println!("length limit in 128th notes: {}", length_limit); let (kick_grid, kick_repeats) = match groups.get(&KickDrum) { Some(groups) => { let length_128th = length_map.get(&KickDrum).unwrap(); let number_of_groups = groups.0.len(); let times = length_limit / length_128th; - // let iterator = flatten_groups(KickDrum, groups).into_iter().cycle().take(number_of_groups * times as usize).peekable() ( flatten_groups(KickDrum, groups), number_of_groups * times as usize, @@ -896,8 +889,8 @@ fn test_flatten_and_merge() { } // The length of a beat is not standard, so in order to fully describe the length of a MIDI tick the MetaMessage::Tempo event should be present. -pub fn create_smf<'a>(groups: HashMap, time_signature: TimeSignature) -> Smf<'a> { - let tracks = create_tracks(groups, time_signature); // FIXME +pub fn create_smf<'a>(groups: HashMap, time_signature: TimeSignature, text: &'a str, tempo: u16) -> Smf<'a> { + let tracks = create_tracks(groups, time_signature, text, MidiTempo::from_tempo(tempo)); // FIXME // https://majicdesigns.github.io/MD_MIDIFile/page_timing.html // says " If it is not specified the MIDI default is 48 ticks per quarter note." // As it's required in `Header`, let's use the same value. @@ -912,15 +905,25 @@ pub fn create_smf<'a>(groups: HashMap, time_signature: TimeSignatu } /// Translates drum parts to a single MIDI track. +/// +/// /// # Arguments +/// +/// * `parts_and_groups` - Drum parts parsed from the command line. +/// * `time_signature` - Time signature parsed from the command line. +/// * `text_event` - Text message to be embedded into the MIDI file. +/// +/// # Returns +/// +/// Multi-track vectors of MIDI events in `midly` format. +/// fn create_tracks<'a>( parts_and_groups: HashMap, time_signature: TimeSignature, - // tempo: u32 + text_event: &'a str, + midi_tempo: MidiTempo ) -> Vec>> { - //FIXME: unhardcode time signature let events_iter = flatten_and_merge(parts_and_groups, time_signature); let events: Vec> = events_iter.collect(); - println!("events: {:?}", events); // Notice this time can be incorrect, but it shouldn't matter. let time = match events.last() { Some(ev) => ev.tick, @@ -960,12 +963,10 @@ fn create_tracks<'a>( kind: TrackEventKind::Meta(MetaMessage::MidiPort(10.into())), }); - let midi_tempo = MidiTempo::from_tempo(130).0; - // drums.push(TrackEvent { delta: 0.into(), kind: TrackEventKind::Meta(MetaMessage::Tempo(midi_tempo)) }); + drums.push(TrackEvent { delta: 0.into(), kind: TrackEventKind::Meta(MetaMessage::Tempo(midi_tempo.0)) }); let (midi_time_signature_numerator, midi_time_signature_denominator) = time_signature.to_midi(); - println!("Midi time signature: {}, {}", midi_time_signature_numerator, midi_time_signature_denominator); drums.push(TrackEvent { delta: 0.into(), kind: TrackEventKind::Meta(MetaMessage::TimeSignature( @@ -976,6 +977,9 @@ fn create_tracks<'a>( )), }); + // println!("{:?}", text_event.as_bytes()); + // drums.push(TrackEvent { delta: 0.into(), kind: TrackEventKind::Meta(MetaMessage::Text("!!!!!!!".as_bytes())) }); + for event in event_grid.events { let midi_message = match event.event_type { NoteOn(part) => MidiMessage::NoteOn {