Add midi tempo support

This commit is contained in:
Denis Redozubov 2023-06-09 18:57:36 +04:00
parent fd78edb4f6
commit 07fea4b393
3 changed files with 81 additions and 56 deletions

View file

@ -3,13 +3,12 @@ use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
use poly::dsl::dsl; use poly::dsl::dsl;
use poly::midi::core::{Part, create_smf}; use poly::midi::core::{create_smf, Part};
use poly::midi::time::TimeSignature; use poly::midi::time::TimeSignature;
use clap::*; use clap::*;
#[derive(Debug, Parser, Clone)]
#[derive(Debug, Parser)]
#[command(name = "poly")] #[command(name = "poly")]
#[command(author = "Denis Redozubov <denis.redozubov@gmail.com>")] #[command(author = "Denis Redozubov <denis.redozubov@gmail.com>")]
#[command(version = "0.1")] #[command(version = "0.1")]
@ -28,13 +27,13 @@ struct Cli {
crash: Option<String>, crash: Option<String>,
#[arg(short = 't', default_value = "120")] #[arg(short = 't', default_value = "120")]
tempo: String, tempo: u16,
#[arg(short = 's', default_value = "4/4")] #[arg(short = 's', default_value = "4/4")]
time_signature: String, time_signature: String,
#[arg(short = 'o', default_value = None)] #[arg(short = 'o', default_value = None)]
output: Option<String> output: Option<String>,
} }
fn part_to_string(part: Part) -> String { 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<String>, part: Part, patterns: &mut HashMap<Part, dsl::Groups>) -> () { fn validate_and_parse_part(
cli: Option<String>,
part: Part,
patterns: &mut HashMap<Part, dsl::Groups>,
) -> () {
match cli { match cli {
None => {}, None => {}
Some(pattern) => { Some(pattern) => match dsl::groups(pattern.as_str()) {
match dsl::groups(pattern.as_str()) { Ok((_, group)) => {
Ok((_, group)) => { patterns.insert(part, group); }, patterns.insert(part, group);
Err(_) => { }
panic!("{} pattern is malformed.", part_to_string(part)) Err(_) => {
} panic!("{} pattern is malformed.", part_to_string(part))
} }
} },
} }
} }
fn create_text_description(kick: &Option<String>, snare: &Option<String>, hihat: &Option<String>, crash: &Option<String>) -> 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() { fn main() {
let matches = Cli::parse(); let matches = Cli::parse();
match matches { 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 { if kick == None && snare == None && hihat == None && crash == None {
println!("No drum pattern was supplied, exiting..."); println!("No drum pattern was supplied, exiting...");
exit(1) exit(1)
} else { } 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(); let mut groups = HashMap::new();
validate_and_parse_part(kick, Part::KickDrum, &mut groups); validate_and_parse_part(kick, Part::KickDrum, &mut groups);
validate_and_parse_part(snare, Part::SnareDrum, &mut groups); validate_and_parse_part(snare, Part::SnareDrum, &mut groups);
validate_and_parse_part(hihat, Part::HiHat, &mut groups); validate_and_parse_part(hihat, Part::HiHat, &mut groups);
validate_and_parse_part(crash, Part::CrashCymbal, &mut groups); validate_and_parse_part(crash, Part::CrashCymbal, &mut groups);
let signature = match TimeSignature::from_str(&time_signature) { let output_file = output.clone();
Err(e) => panic!("Can't parse the time signature: {}", e),
Ok(x) => x
};
match output { match output_file {
None => { None => {
println!("No output file path was supplied, running a dry run..."); 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) => { Some(path) => {
match create_smf(groups, signature).save(path.clone()) { match create_smf(groups, signature, text_description.as_str(), tempo)
.save(path.clone())
{
Ok(_) => { Ok(_) => {
println!("{} was written successfully", path); println!("{} was written successfully", path);
exit(0) exit(0)
}, }
Err(e) => { Err(e) => {
println!("Failed to write {}: {}", path, e); println!("Failed to write {}: {}", path, e);
exit(1) exit(1)
}, }
}; };
} }
}; };

View file

@ -55,18 +55,6 @@ impl KnownLength for BasicLength {
} }
impl 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<Self, String> { pub fn from_num(n: u16) -> Result<Self, String> {
match n { match n {
64 => Ok(BasicLength::SixtyFourth), 64 => Ok(BasicLength::SixtyFourth),

View file

@ -423,7 +423,7 @@ impl Length {
} }
#[allow(dead_code)] #[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)] #[allow(dead_code)]
static MIDI_CLOCKS_PER_CLICK: u8 = 24; static MIDI_CLOCKS_PER_CLICK: u8 = 24;
@ -446,7 +446,7 @@ pub struct MidiTempo(u24);
impl MidiTempo { impl MidiTempo {
fn from_tempo(tempo: u16) -> Self { 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()) Self(mt.into())
} }
} }
@ -736,18 +736,11 @@ fn flatten_and_merge(
.unwrap_or(BAR_LIMIT.clone()); .unwrap_or(BAR_LIMIT.clone());
println!("Converges over {} bars", converges_over_bars); println!("Converges over {} bars", converges_over_bars);
let length_limit = converges_over_bars * time_signature.to_128th(); 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) { let (kick_grid, kick_repeats) = match groups.get(&KickDrum) {
Some(groups) => { Some(groups) => {
let length_128th = length_map.get(&KickDrum).unwrap(); let length_128th = length_map.get(&KickDrum).unwrap();
let number_of_groups = groups.0.len(); let number_of_groups = groups.0.len();
let times = length_limit / length_128th; 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), flatten_groups(KickDrum, groups),
number_of_groups * times as usize, 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. // 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<Part, Groups>, time_signature: TimeSignature) -> Smf<'a> { pub fn create_smf<'a>(groups: HashMap<Part, Groups>, time_signature: TimeSignature, text: &'a str, tempo: u16) -> Smf<'a> {
let tracks = create_tracks(groups, time_signature); // FIXME let tracks = create_tracks(groups, time_signature, text, MidiTempo::from_tempo(tempo)); // FIXME
// https://majicdesigns.github.io/MD_MIDIFile/page_timing.html // https://majicdesigns.github.io/MD_MIDIFile/page_timing.html
// says " If it is not specified the MIDI default is 48 ticks per quarter note." // 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. // As it's required in `Header`, let's use the same value.
@ -912,15 +905,25 @@ pub fn create_smf<'a>(groups: HashMap<Part, Groups>, time_signature: TimeSignatu
} }
/// Translates drum parts to a single MIDI track. /// 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>( fn create_tracks<'a>(
parts_and_groups: HashMap<Part, Groups>, parts_and_groups: HashMap<Part, Groups>,
time_signature: TimeSignature, time_signature: TimeSignature,
// tempo: u32 text_event: &'a str,
midi_tempo: MidiTempo
) -> Vec<Vec<midly::TrackEvent<'a>>> { ) -> Vec<Vec<midly::TrackEvent<'a>>> {
//FIXME: unhardcode time signature
let events_iter = flatten_and_merge(parts_and_groups, time_signature); let events_iter = flatten_and_merge(parts_and_groups, time_signature);
let events: Vec<Event<Tick>> = events_iter.collect(); let events: Vec<Event<Tick>> = events_iter.collect();
println!("events: {:?}", events);
// Notice this time can be incorrect, but it shouldn't matter. // Notice this time can be incorrect, but it shouldn't matter.
let time = match events.last() { let time = match events.last() {
Some(ev) => ev.tick, Some(ev) => ev.tick,
@ -960,12 +963,10 @@ fn create_tracks<'a>(
kind: TrackEventKind::Meta(MetaMessage::MidiPort(10.into())), 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.0)) });
// drums.push(TrackEvent { delta: 0.into(), kind: TrackEventKind::Meta(MetaMessage::Tempo(midi_tempo)) });
let (midi_time_signature_numerator, midi_time_signature_denominator) = let (midi_time_signature_numerator, midi_time_signature_denominator) =
time_signature.to_midi(); time_signature.to_midi();
println!("Midi time signature: {}, {}", midi_time_signature_numerator, midi_time_signature_denominator);
drums.push(TrackEvent { drums.push(TrackEvent {
delta: 0.into(), delta: 0.into(),
kind: TrackEventKind::Meta(MetaMessage::TimeSignature( 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 { for event in event_grid.events {
let midi_message = match event.event_type { let midi_message = match event.event_type {
NoteOn(part) => MidiMessage::NoteOn { NoteOn(part) => MidiMessage::NoteOn {