mod dto;
mod edo;
mod live;
mod midi;
mod mts;
use dto::{ScaleDto, ScaleItemDto, TuneDto};
use io::Read;
use live::LiveOptions;
use mts::MtsOptions;
use shared::SclCommand;
use std::fs::File;
use std::{
fmt::{self, Arguments, Debug},
io::{self, Write},
path::PathBuf,
};
use structopt::StructOpt;
use tune::key::PianoKey;
use tune::pitch::{Pitch, ReferencePitch};
use tune::ratio::Ratio;
use tune::scala::{Kbm, SclBuildError};
use tune::tuning::Tuning;
#[doc(hidden)]
pub mod shared;
#[derive(StructOpt)]
struct MainOptions {
#[structopt(long = "--of")]
output_file: Option<PathBuf>,
#[structopt(subcommand)]
command: MainCommand,
}
#[derive(StructOpt)]
enum MainCommand {
#[structopt(name = "scl")]
Scl(SclOptions),
#[structopt(name = "kbm")]
Kbm(KbmOptions),
#[structopt(name = "edo")]
Edo(EdoOptions),
#[structopt(name = "scale")]
Scale(ScaleOptions),
#[structopt(name = "dump")]
Dump(DumpOptions),
#[structopt(name = "diff")]
Diff(DiffOptions),
#[structopt(name = "mts")]
Mts(MtsOptions),
#[structopt(name = "live")]
Live(LiveOptions),
#[structopt(name = "devices")]
Devices,
}
#[derive(StructOpt)]
struct SclOptions {
#[structopt(long = "--name")]
name: Option<String>,
#[structopt(subcommand)]
command: SclCommand,
}
#[derive(StructOpt)]
struct EdoOptions {
num_steps_per_octave: u16,
}
#[derive(StructOpt)]
struct DumpOptions {
#[structopt(flatten)]
limit_params: LimitOptions,
}
#[derive(StructOpt)]
struct ScaleOptions {
#[structopt(flatten)]
kbm_params: KbmOptions,
#[structopt(subcommand)]
command: SclCommand,
}
#[derive(StructOpt)]
struct DiffOptions {
#[structopt(flatten)]
key_map_params: KbmOptions,
#[structopt(flatten)]
limit_params: LimitOptions,
#[structopt(subcommand)]
command: SclCommand,
}
#[derive(StructOpt)]
struct KbmOptions {
ref_pitch: ReferencePitch,
#[structopt(short = "r")]
root_note: Option<i16>,
}
#[derive(StructOpt)]
struct LimitOptions {
#[structopt(short = "l", default_value = "11")]
limit: u16,
}
pub fn run_in_shell_env(args: impl IntoIterator<Item = String>) -> CliResult<()> {
let options = MainOptions::from_iter_safe(args).map_err(|error| error.message)?;
let stdin = io::stdin();
let input = Box::new(stdin.lock());
let stdout = io::stdout();
let output: Box<dyn Write> = match options.output_file {
Some(output_file) => Box::new(File::create(output_file)?),
None => Box::new(stdout.lock()),
};
let stderr = io::stderr();
let error = Box::new(stderr.lock());
let mut app = App {
input,
output,
error,
};
app.run(options.command)
}
struct App<'a> {
input: Box<dyn 'a + Read>,
output: Box<dyn 'a + Write>,
error: Box<dyn 'a + Write>,
}
impl App<'_> {
fn run(&mut self, command: MainCommand) -> CliResult<()> {
match command {
MainCommand::Scl(SclOptions { name, command }) => {
self.execute_scl_command(name, command)?
}
MainCommand::Kbm(kbm) => self.execute_kbm_command(kbm)?,
MainCommand::Edo(EdoOptions {
num_steps_per_octave,
}) => edo::print_info(&mut self.output, num_steps_per_octave)?,
MainCommand::Scale(ScaleOptions {
kbm_params,
command,
}) => self.execute_scale_command(kbm_params, command)?,
MainCommand::Dump(DumpOptions { limit_params }) => {
self.dump_scale(limit_params.limit)?
}
MainCommand::Diff(DiffOptions {
limit_params,
key_map_params,
command,
}) => self.diff_scale(key_map_params, limit_params.limit, command)?,
MainCommand::Mts(options) => options.run(self)?,
MainCommand::Live(options) => options.run(self)?,
MainCommand::Devices => shared::print_midi_devices(&mut self.output, "tune-cli")?,
}
Ok(())
}
fn execute_scl_command(&mut self, name: Option<String>, command: SclCommand) -> CliResult<()> {
self.write(format_args!("{}", command.to_scl(name)?.export()))
.map_err(Into::into)
}
fn execute_kbm_command(&mut self, key_map_params: KbmOptions) -> io::Result<()> {
self.write(format_args!("{}", key_map_params.to_kbm().export()))
}
fn execute_scale_command(
&mut self,
key_map_params: KbmOptions,
command: SclCommand,
) -> CliResult<()> {
let key_map = key_map_params.to_kbm();
let tuning = (&command.to_scl(None)?, &key_map);
let items = scale_iter(tuning)
.map(|scale_item| ScaleItemDto {
key_midi_number: scale_item.piano_key.midi_number(),
pitch_in_hz: scale_item.pitch.as_hz(),
})
.collect();
let dump = ScaleDto {
root_key_midi_number: key_map.root_key.midi_number(),
root_pitch_in_hz: tuning.pitch_of(0).as_hz(),
items,
};
let dto = TuneDto::Scale(dump);
self.writeln(format_args!(
"{}",
serde_json::to_string_pretty(&dto).map_err(io::Error::from)?
))
.map_err(Into::into)
}
fn dump_scale(&mut self, limit: u16) -> io::Result<()> {
let in_scale = ScaleDto::read(&mut self.input)?;
let mut printer = ScaleTablePrinter {
app: self,
root_key: PianoKey::from_midi_number(in_scale.root_key_midi_number),
root_pitch: Pitch::from_hz(in_scale.root_pitch_in_hz),
limit,
};
printer.print_table_header()?;
for scale_item in in_scale.items {
let pitch = Pitch::from_hz(scale_item.pitch_in_hz);
let approximation = pitch.find_in(&());
let approx_value = approximation.approx_value;
let (letter, octave) = approx_value.letter_and_octave();
printer.print_table_row(
PianoKey::from_midi_number(scale_item.key_midi_number),
pitch,
approx_value.midi_number(),
format!("{:>6} {:>2}", letter, octave.octave_number()),
approximation.deviation,
)?;
}
Ok(())
}
fn diff_scale(
&mut self,
key_map_params: KbmOptions,
limit: u16,
command: SclCommand,
) -> CliResult<()> {
let in_scale = ScaleDto::read(&mut self.input)?;
let key_map = key_map_params.to_kbm();
let tuning = (command.to_scl(None)?, &key_map);
let mut printer = ScaleTablePrinter {
app: self,
root_pitch: Pitch::from_hz(in_scale.root_pitch_in_hz),
root_key: PianoKey::from_midi_number(in_scale.root_key_midi_number),
limit,
};
printer.print_table_header()?;
for item in in_scale.items {
let pitch = Pitch::from_hz(item.pitch_in_hz);
let approximation = tuning.find_by_pitch(pitch);
let index = key_map.root_key.num_keys_before(approximation.approx_value);
printer.print_table_row(
PianoKey::from_midi_number(item.key_midi_number),
pitch,
approximation.approx_value.midi_number(),
format!("IDX {:>5}", index),
approximation.deviation,
)?;
}
Ok(())
}
pub fn write(&mut self, args: Arguments) -> io::Result<()> {
self.output.write_fmt(args)
}
pub fn writeln(&mut self, args: Arguments) -> io::Result<()> {
writeln!(&mut self.output, "{}", args)
}
pub fn errln(&mut self, args: Arguments) -> io::Result<()> {
writeln!(&mut self.error, "{}", args)
}
pub fn read(&mut self) -> &mut dyn Read {
&mut self.input
}
}
impl KbmOptions {
pub fn to_kbm(&self) -> Kbm {
Kbm {
ref_pitch: self.ref_pitch,
root_key: self
.root_note
.map(i32::from)
.map(PianoKey::from_midi_number)
.unwrap_or_else(|| self.ref_pitch.key()),
}
}
}
fn scale_iter(tuning: impl Tuning<PianoKey>) -> impl Iterator<Item = ScaleItem> {
(1..128).map(move |midi_number| {
let piano_key = PianoKey::from_midi_number(midi_number);
ScaleItem {
piano_key,
pitch: tuning.pitch_of(piano_key),
}
})
}
struct ScaleItem {
piano_key: PianoKey,
pitch: Pitch,
}
struct ScaleTablePrinter<'a, 'b> {
app: &'a mut App<'b>,
root_key: PianoKey,
root_pitch: Pitch,
limit: u16,
}
impl ScaleTablePrinter<'_, '_> {
fn print_table_header(&mut self) -> io::Result<()> {
self.app.writeln(format_args!(
" {source:-^33} ‖ {pitch:-^14} ‖ {target:-^28}",
source = "Source Scale",
pitch = "Pitch",
target = "Target Scale"
))
}
fn print_table_row(
&mut self,
source_key: PianoKey,
pitch: Pitch,
target_midi: i32,
target_index: String,
deviation: Ratio,
) -> io::Result<()> {
let source_index = self.root_key.num_keys_before(source_key);
if source_index == 0 {
self.app.write(format_args!("> "))?;
} else {
self.app.write(format_args!(" "))?;
}
let nearest_fraction =
Ratio::between_pitches(self.root_pitch, pitch).nearest_fraction(self.limit);
self.app.writeln(format_args!(
"{source_midi:>3} | IDX {source_index:>4} | \
{numer:>2}/{denom:<2} {fract_deviation:>+4.0}¢ {fract_octaves:>+3}o ‖ \
{pitch:>11.3} Hz ‖ {target_midi:>4} | {target_index} | {deviation:>+8.3}¢",
source_midi = source_key.midi_number(),
source_index = source_index,
pitch = pitch.as_hz(),
numer = nearest_fraction.numer,
denom = nearest_fraction.denom,
fract_deviation = nearest_fraction.deviation.as_cents(),
fract_octaves = nearest_fraction.num_octaves,
target_midi = target_midi,
target_index = target_index,
deviation = deviation.as_cents(),
))
}
}
pub type CliResult<T> = Result<T, CliError>;
pub enum CliError {
IoError(io::Error),
CommandError(String),
}
impl Debug for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::IoError(err) => write!(f, "IO error / {}", err),
CliError::CommandError(err) => write!(f, "The command failed / {}", err),
}
}
}
impl From<String> for CliError {
fn from(v: String) -> Self {
CliError::CommandError(v)
}
}
impl From<SclBuildError> for CliError {
fn from(v: SclBuildError) -> Self {
CliError::CommandError(format!("Could not create scale ({:?})", v))
}
}
impl From<io::Error> for CliError {
fn from(v: io::Error) -> Self {
CliError::IoError(v)
}
}