mirror of
https://github.com/dredozubov/polyrhythmix.git
synced 2024-11-22 11:57:43 +00:00
Add midi tempo support
This commit is contained in:
parent
fd78edb4f6
commit
07fea4b393
3 changed files with 81 additions and 56 deletions
|
@ -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 <denis.redozubov@gmail.com>")]
|
||||
#[command(version = "0.1")]
|
||||
|
@ -28,13 +27,13 @@ struct Cli {
|
|||
crash: Option<String>,
|
||||
|
||||
#[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<String>
|
||||
output: Option<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 {
|
||||
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<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() {
|
||||
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)
|
||||
} 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)
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<Self, String> {
|
||||
match n {
|
||||
64 => Ok(BasicLength::SixtyFourth),
|
||||
|
|
|
@ -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<Part, Groups>, time_signature: TimeSignature) -> Smf<'a> {
|
||||
let tracks = create_tracks(groups, time_signature); // FIXME
|
||||
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, 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<Part, Groups>, 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<Part, Groups>,
|
||||
time_signature: TimeSignature,
|
||||
// tempo: u32
|
||||
text_event: &'a str,
|
||||
midi_tempo: MidiTempo
|
||||
) -> Vec<Vec<midly::TrackEvent<'a>>> {
|
||||
//FIXME: unhardcode time signature
|
||||
let events_iter = flatten_and_merge(parts_and_groups, time_signature);
|
||||
let events: Vec<Event<Tick>> = 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 {
|
||||
|
|
Loading…
Reference in a new issue