use clap::Parser;
use itertools::Itertools;
use lazy_static::lazy_static;
use ukebox::{
Chord, ChordChart, ChordSequence, ChordType, FretID, FretPattern, Semitones, Tuning, Voicing,
VoicingConfig, VoicingGraph,
};
const MAX_FRET_ID: FretID = 21;
const MAX_SPAN: Semitones = 5;
lazy_static! {
static ref DEFAULT_CONFIG: VoicingConfig = VoicingConfig::default();
static ref TUNING_STR: String = DEFAULT_CONFIG.tuning.to_string();
static ref MIN_FRET_STR: String = DEFAULT_CONFIG.min_fret.to_string();
static ref MAX_FRET_STR: String = DEFAULT_CONFIG.max_fret.to_string();
static ref MAX_SPAN_STR: String = DEFAULT_CONFIG.max_span.to_string();
}
#[derive(Parser)]
struct Ukebox {
#[arg(short, long, global = true, value_name = "TUNING", default_value = &**TUNING_STR, value_enum)]
tuning: Tuning,
#[command(subcommand)]
cmd: Subcommand,
}
#[derive(Parser)]
enum Subcommand {
Chords {},
#[command(verbatim_doc_comment)]
Chart {
#[arg(short, long)]
all: bool,
#[command(flatten)]
voicing_opts: VoicingOpts,
#[arg(value_name = "CHORD")]
chord: Chord,
},
Name {
#[arg(value_name = "FRET_PATTERN")]
fret_pattern: FretPattern,
},
VoiceLead {
#[command(flatten)]
voicing_opts: VoicingOpts,
#[arg(value_name = "CHORD_SEQUENCE")]
chord_seq: ChordSequence,
},
}
#[derive(Parser)]
pub struct VoicingOpts {
#[arg(long, value_name = "FRET_ID", default_value = &**MIN_FRET_STR, value_parser = clap::value_parser!(FretID).range(0..=MAX_FRET_ID as i64))]
min_fret: FretID,
#[arg(long, value_name = "FRET_ID", default_value = &**MAX_FRET_STR, value_parser = clap::value_parser!(FretID).range(0..=MAX_FRET_ID as i64))]
max_fret: FretID,
#[arg(long, value_name = "FRET_COUNT", default_value = &**MAX_SPAN_STR, value_parser = clap::value_parser!(Semitones).range(0..=MAX_SPAN as i64))]
max_span: Semitones,
#[arg(
long,
value_name = "SEMITONES",
allow_hyphen_values = true,
default_value = "0"
)]
transpose: i8,
}
fn main() {
let args = Ukebox::parse();
let tuning = args.tuning;
match args.cmd {
Subcommand::Chords {} => {
println!("Supported chord types and symbols\n");
println!("The root note C is used as an example.\n");
for chord_type in ChordType::values() {
let symbols = chord_type.symbols().map(|s| format!("C{s}")).join(", ");
println!("C {chord_type} - {symbols}");
}
}
Subcommand::Chart {
all,
voicing_opts,
chord,
} => {
let chord = chord.transpose(voicing_opts.transpose);
let config = VoicingConfig {
tuning,
min_fret: voicing_opts.min_fret,
max_fret: voicing_opts.max_fret,
max_span: voicing_opts.max_span,
};
let mut voicings = chord.voicings(config).peekable();
if voicings.peek().is_none() {
println!("No matching chord voicing was found");
} else {
println!("[{chord}]\n");
}
for voicing in voicings {
let chart = ChordChart::new(voicing, voicing_opts.max_span);
println!("{chart}");
if !all {
break;
}
}
}
Subcommand::Name { fret_pattern } => {
let voicing = Voicing::new(fret_pattern, tuning);
let chords = voicing.get_chords();
if chords.is_empty() {
println!("No matching chord was found");
}
for chord in chords {
println!("{chord}");
}
}
Subcommand::VoiceLead {
voicing_opts,
chord_seq,
} => {
let chord_seq = chord_seq.transpose(voicing_opts.transpose);
let config = VoicingConfig {
tuning,
min_fret: voicing_opts.min_fret,
max_fret: voicing_opts.max_fret,
max_span: voicing_opts.max_span,
};
let mut voicing_graph = VoicingGraph::new(config);
voicing_graph.add(&chord_seq);
let mut path_found = false;
for (path, _dist) in voicing_graph.paths(1) {
for (chord, voicing) in chord_seq.chords().zip(path.iter()) {
println!("[{chord}]\n");
let chart = ChordChart::new(*voicing, voicing_opts.max_span);
println!("{chart}");
}
path_found = true;
}
if !path_found {
println!("No matching chord voicing sequence was found");
}
}
}
}