use std::path::PathBuf;
use clap::{ArgAction, Parser, Subcommand};
use klib::core::{
base::{Parsable, Res, Void},
chord::{Chord, Chordable},
note::Note,
octave::Octave,
};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand, Debug)]
enum Command {
Describe {
symbol: String,
#[arg(short, long, default_value_t = 4i8)]
octave: i8,
},
Play {
symbol: String,
#[arg(short, long, default_value_t = 0.2f32)]
delay: f32,
#[arg(short, long, default_value_t = 3.0f32)]
length: f32,
#[arg(short, long, default_value_t = 0.1f32)]
fade_in: f32,
},
Loop {
chords: Vec<String>,
#[arg(short, long, default_value_t = 60f32)]
bpm: f32,
},
Guess {
notes: Vec<String>,
},
#[cfg(feature = "analyze_base")]
Analyze {
#[command(subcommand)]
analyze_command: Option<AnalyzeCommand>,
},
#[cfg(any(feature = "ml", feature = "ml_base", feature = "ml_train", feature = "ml_infer"))]
Ml {
#[command(subcommand)]
ml_command: Option<MlCommand>,
},
}
#[derive(Subcommand, Debug)]
enum AnalyzeCommand {
#[cfg(feature = "analyze_mic")]
Mic {
#[arg(short, long, default_value_t = 10)]
length: u8,
},
#[cfg(feature = "analyze_file")]
File {
#[arg(long = "no-preview", action=ArgAction::SetFalse, default_value_t = true)]
preview: bool,
#[arg(short, long)]
start_time: Option<String>,
#[arg(short, long)]
end_time: Option<String>,
source: PathBuf,
},
}
#[derive(Subcommand, Debug)]
enum MlCommand {
#[cfg(feature = "ml_train")]
Gather {
#[arg(short, long, default_value = ".hidden/samples")]
destination: String,
#[arg(short, long, default_value_t = 10)]
length: u8,
},
#[cfg(feature = "ml_train")]
Train {
#[arg(long, default_value = "samples")]
source: String,
#[arg(long, default_value = "model")]
destination: String,
#[arg(long, default_value = ".hidden/train_log")]
log: String,
#[arg(long, default_value = "gpu")]
device: String,
#[arg(long, default_value_t = 100)]
simulation_size: usize,
#[arg(long, default_value_t = 2.0)]
simulation_peak_radius: f32,
#[arg(long, default_value_t = 0.1)]
simulation_harmonic_decay: f32,
#[arg(long, default_value_t = 0.4)]
simulation_frequency_wobble: f32,
#[arg(long, default_value_t = 8)]
mha_heads: usize,
#[arg(long, default_value_t = 0.3)]
mha_dropout: f64,
#[arg(long, default_value_t = 64)]
model_epochs: usize,
#[arg(long, default_value_t = 100)]
model_batch_size: usize,
#[arg(long, default_value_t = 64)]
model_workers: usize,
#[arg(long, default_value_t = 76980)]
model_seed: u64,
#[arg(long, default_value_t = 1e-5)]
adam_learning_rate: f64,
#[arg(long, default_value_t = 5e-4)]
adam_weight_decay: f64,
#[arg(long, default_value_t = 0.9)]
adam_beta1: f32,
#[arg(long, default_value_t = 0.999)]
adam_beta2: f32,
#[arg(long, default_value_t = f32::EPSILON)]
adam_epsilon: f32,
#[arg(long, default_value_t = 1.0)]
sigmoid_strength: f32,
#[arg(long, action=ArgAction::SetTrue, default_value_t = false)]
no_plots: bool,
},
#[cfg(feature = "ml_infer")]
Infer {
#[command(subcommand)]
infer_command: Option<InferCommand>,
},
#[cfg(feature = "plot")]
Plot {
source: String,
#[arg(long, default_value_t = 0.0)]
x_min: f32,
#[arg(long, default_value_t = 8192.0)]
x_max: f32,
},
#[cfg(feature = "ml_train")]
Hpt {
#[arg(long, default_value = "samples")]
source: String,
#[arg(long, default_value = "model")]
destination: String,
#[arg(long, default_value = ".hidden/train_log")]
log: String,
#[arg(long, default_value = "gpu")]
device: String,
},
}
#[derive(Subcommand, Debug)]
enum InferCommand {
#[cfg(feature = "analyze_mic")]
Mic {
#[arg(short, long, default_value_t = 10)]
length: u8,
},
#[cfg(feature = "analyze_file")]
File {
#[arg(long = "no-preview", action=ArgAction::SetFalse, default_value_t = true)]
preview: bool,
#[arg(short, long)]
start_time: Option<String>,
#[arg(short, long)]
end_time: Option<String>,
source: PathBuf,
},
}
fn main() -> Void {
let args = Args::parse();
start(args)?;
Ok(())
}
fn start(args: Args) -> Void {
match args.command {
Some(Command::Describe { symbol, octave }) => {
let chord = Chord::parse(&symbol)?.with_octave(Octave::Zero + octave);
describe(&chord);
}
Some(Command::Play { symbol, delay, length, fade_in }) => {
let chord = Chord::parse(&symbol)?;
play(&chord, delay, length, fade_in)?;
}
Some(Command::Guess { notes }) => {
let notes = notes.into_iter().map(|n| Note::parse(&n)).collect::<Result<Vec<_>, _>>()?;
let candidates = Chord::try_from_notes(¬es)?;
for candidate in candidates {
describe(&candidate);
}
}
Some(Command::Loop { chords, bpm }) => {
let chord_pairs = chords
.into_iter()
.map(|c| {
let mut parts = c.split('|');
let chord = Chord::parse(parts.next().unwrap()).unwrap();
let length = parts.next().map_or(32, |l| l.parse::<u16>().unwrap());
(chord, length)
})
.collect::<Vec<_>>();
loop {
for (chord, length) in &chord_pairs {
let length = (*length as f32) * 60f32 / bpm / 8f32;
play(chord, 0.0, length, 0.1)?;
}
}
}
#[cfg(feature = "analyze_base")]
Some(Command::Analyze { analyze_command }) => match analyze_command {
#[cfg(feature = "analyze_mic")]
Some(AnalyzeCommand::Mic { length }) => {
let notes = futures::executor::block_on(Note::try_from_mic(length))?;
show_notes_and_chords(¬es)?;
}
#[cfg(feature = "analyze_file")]
Some(AnalyzeCommand::File { preview, start_time, end_time, source }) => {
use klib::analyze::file::{get_notes_from_audio_file, preview_audio_file_clip};
let start_time = if let Some(t) = start_time { Some(parse_duration0::parse(&t)?) } else { None };
let end_time = if let Some(t) = end_time { Some(parse_duration0::parse(&t)?) } else { None };
if preview {
preview_audio_file_clip(&source, start_time, end_time)?;
}
let notes = get_notes_from_audio_file(&source, start_time, end_time)?;
show_notes_and_chords(¬es)?;
}
None => {
return Err(anyhow::Error::msg("No subcommand given for `analyze`."));
}
},
#[cfg(feature = "ml_base")]
Some(Command::Ml { ml_command }) => match ml_command {
#[cfg(feature = "ml_train")]
Some(MlCommand::Gather { destination, length }) => {
klib::ml::base::gather::gather_sample(destination, length)?;
}
#[cfg(feature = "ml_train")]
Some(MlCommand::Train {
source,
destination,
log,
simulation_size,
device,
simulation_peak_radius,
simulation_harmonic_decay,
simulation_frequency_wobble,
mha_heads,
mha_dropout,
model_epochs,
model_batch_size,
model_workers,
model_seed,
adam_learning_rate,
adam_weight_decay,
adam_beta1,
adam_beta2,
adam_epsilon,
sigmoid_strength,
no_plots,
}) => {
use burn::backend::Autodiff;
use klib::ml::base::TrainConfig;
let config = TrainConfig {
source,
destination,
log,
simulation_size,
simulation_peak_radius,
simulation_harmonic_decay,
simulation_frequency_wobble,
mha_heads,
mha_dropout,
model_epochs,
model_batch_size,
model_workers,
model_seed,
adam_learning_rate,
adam_weight_decay,
adam_beta1,
adam_beta2,
adam_epsilon,
sigmoid_strength,
no_plots,
};
match device.as_str() {
#[cfg(feature = "ml_gpu")]
"gpu" => {
use burn_tch::{LibTorch, LibTorchDevice};
#[cfg(not(target_os = "macos"))]
let device = LibTorchDevice::Cuda(0);
#[cfg(target_os = "macos")]
let device = LibTorchDevice::Mps;
klib::ml::train::run_training::<Autodiff<LibTorch<f32>>>(device, &config, true, true)?;
}
#[cfg(feature = "ml_gpu")]
"wgpu" => {
use burn_wgpu::{AutoGraphicsApi, Wgpu, WgpuDevice};
let device = WgpuDevice::default();
klib::ml::train::run_training::<Autodiff<Wgpu<AutoGraphicsApi, f32, i32>>>(device, &config, true, true)?;
}
"cpu" => {
use burn_ndarray::{NdArray, NdArrayDevice};
let device = NdArrayDevice::Cpu;
klib::ml::train::run_training::<Autodiff<NdArray<f32>>>(device, &config, true, true)?;
}
_ => {
return Err(anyhow::Error::msg(
"Invalid device (must choose either `gpu` [requires `ml_gpu` feature], `wgpu` [requires `ml_gpu` feature] or `cpu`).",
));
}
}
}
#[cfg(feature = "ml_infer")]
Some(MlCommand::Infer { infer_command }) => match infer_command {
#[cfg(feature = "analyze_mic")]
Some(InferCommand::Mic { length }) => {
use klib::ml::infer::infer;
let audio_data = futures::executor::block_on(klib::analyze::mic::get_audio_data_from_microphone(length))?;
let notes = infer(&audio_data, length)?;
show_notes_and_chords(¬es)?;
}
#[cfg(feature = "analyze_file")]
Some(InferCommand::File { preview, start_time, end_time, source }) => {
use klib::{
analyze::file::{get_audio_data_from_file, preview_audio_file_clip},
ml::infer::infer,
};
let start_time = if let Some(t) = start_time { Some(parse_duration0::parse(&t)?) } else { None };
let end_time = if let Some(t) = end_time { Some(parse_duration0::parse(&t)?) } else { None };
if preview {
preview_audio_file_clip(&source, start_time, end_time)?;
}
let (audio_data, length) = get_audio_data_from_file(&source, start_time, end_time)?;
let notes = infer(&audio_data, length)?;
show_notes_and_chords(¬es)?;
}
_ => {
return Err(anyhow::Error::msg("Invalid inference command."));
}
},
#[cfg(feature = "plot")]
Some(MlCommand::Plot { source, x_min, x_max }) => {
use anyhow::Context;
use klib::{
analyze::base::{compute_cqt, translate_frequency_space_to_peak_space},
helpers::plot_frequency_space,
ml::base::{
helpers::{harmonic_convolution, load_kord_item, mel_filter_banks_from},
MEL_SPACE_SIZE,
},
};
let kord_item = load_kord_item(&source);
let path = std::path::Path::new(&source);
let name = path.file_name().context("Could not get file name.")?.to_str().context("Could not map file name to str.")?;
let frequency_file_name = format!("{}_frequency", name);
let frequency_space = kord_item.frequency_space.into_iter().enumerate().map(|(k, v)| (k as f32, v)).collect::<Vec<_>>();
plot_frequency_space(&frequency_space, "KordItem Frequency Space", &frequency_file_name, x_min, x_max);
let harmonic_file_name = format!("{}_harmonic", name);
let harmonic_space = harmonic_convolution(&kord_item.frequency_space).into_iter().enumerate().map(|(k, v)| (k as f32, v)).collect::<Vec<_>>();
plot_frequency_space(&harmonic_space, "KordItem Harmonic Space", &harmonic_file_name, x_min, x_max);
let cqt_file_name = format!("{}_cqt", name);
let cqt_space = compute_cqt(&kord_item.frequency_space).into_iter().enumerate().map(|(k, v)| (k as f32, v)).collect::<Vec<_>>();
plot_frequency_space(&cqt_space, "KordItem CQT Space", &cqt_file_name, 0.0, 256.0);
let mel_file_name = format!("{}_mel", name);
let mel_space = mel_filter_banks_from(&kord_item.frequency_space)
.into_iter()
.enumerate()
.map(|(k, v)| (k as f32, v))
.collect::<Vec<_>>();
plot_frequency_space(&mel_space, "KordItem Mel Space", &mel_file_name, 0.0, MEL_SPACE_SIZE as f32);
let peak_file_name = format!("{}_peak", name);
let mut peak_space = translate_frequency_space_to_peak_space(&frequency_space);
peak_space.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap());
peak_space.iter_mut().skip(12).for_each(|(_, v)| *v = 0.0);
peak_space.sort_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap());
plot_frequency_space(&peak_space, "KordItem Peak Space", &peak_file_name, x_min, x_max);
let mel_peak_file_name = format!("{}_mel_peak", name);
let peak_space = peak_space.into_iter().map(|(_, v)| v).collect::<Vec<_>>();
let mel_peak_space = mel_filter_banks_from(&peak_space).into_iter().enumerate().map(|(k, v)| (k as f32, v)).collect::<Vec<_>>();
plot_frequency_space(&mel_peak_space, "KordItem Mel Peak Space", &mel_peak_file_name, 0.0, MEL_SPACE_SIZE as f32);
let log_file_name = format!("{}_log", name);
let log_frequency_space = kord_item.frequency_space.into_iter().map(|v| v.log2()).collect::<Vec<_>>();
plot_frequency_space(
&log_frequency_space.iter().enumerate().map(|(k, v)| (k as f32, *v)).collect::<Vec<_>>(),
"KordItem Log Space",
&log_file_name,
x_min,
x_max,
);
let harmonic_file_name = format!("{}_time", name);
let time_space = klib::analyze::base::get_time_space(&peak_space);
plot_frequency_space(&time_space, "KordItem Time Space", &harmonic_file_name, x_min, x_max);
}
#[cfg(feature = "ml_train")]
Some(MlCommand::Hpt { source, destination, log, device }) => {
use klib::ml::train::execute::hyper_parameter_tuning;
hyper_parameter_tuning(source, destination, log, device)?;
}
None => {
return Err(anyhow::Error::msg("No subcommand given for `ml`."));
}
},
None => {
return Err(anyhow::Error::msg("No command given."));
}
}
Ok(())
}
fn describe(chord: &Chord) {
println!("{chord}");
}
fn play(chord: &Chord, delay: f32, length: f32, fade_in: f32) -> Void {
describe(chord);
#[cfg(feature = "audio")]
{
use klib::core::base::Playable;
use std::time::Duration;
let _playable = chord.play(Duration::from_secs_f32(delay), Duration::from_secs_f32(length), Duration::from_secs_f32(fade_in))?;
std::thread::sleep(Duration::from_secs_f32(length));
}
Ok(())
}
fn show_notes_and_chords(notes: &[Note]) -> Res<()> {
println!("Notes: {}", notes.iter().map(ToString::to_string).collect::<Vec<_>>().join(" "));
let candidates = Chord::try_from_notes(notes)?;
if candidates.is_empty() {
println!("No chord candidates found");
} else {
for candidate in candidates {
describe(&candidate);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe() {
start(Args {
command: Some(Command::Describe {
symbol: "Cmaj7b9@3^2!".to_string(),
octave: 4,
}),
})
.unwrap();
}
#[test]
fn test_guess() {
start(Args {
command: Some(Command::Guess {
notes: vec!["C".to_owned(), "E".to_owned(), "G".to_owned()],
}),
})
.unwrap();
}
}