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 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)
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue