midnote 0.11.1

A terminal application for viewing notes in a MIDI track, with audio.
Documentation
use std::{
	error::Error,
	fmt,
	io::{self, BufRead, Write},
};

use midir::{MidiOutput, MidiOutputConnection};
use midly::{MetaMessage, TrackEvent, TrackEventKind};
use nodi::Sheet;

pub fn list_devices() -> Result<(), Box<dyn Error>> {
	let midi_out = MidiOutput::new("nodi")?;

	let out_ports = midi_out.ports();

	if out_ports.is_empty() {
		println!("No active MIDI output device detected.");
	} else {
		for (i, p) in out_ports.iter().enumerate() {
			println!(
				"#{}: {}",
				i,
				midi_out
					.port_name(p)
					.as_deref()
					.unwrap_or("<no device name>")
			);
		}
	}

	Ok(())
}

pub fn get_midi(n: usize) -> Result<MidiOutputConnection, Box<dyn Error>> {
	let midi_out = MidiOutput::new("nodi")?;

	let out_ports = midi_out.ports();
	if out_ports.is_empty() {
		return Err("no MIDI output device detected".into());
	}
	if n >= out_ports.len() {
		return Err(format!(
			"only {} MIDI devices detected; run with --list  to see them",
			out_ports.len()
		)
		.into());
	}

	let out_port = &out_ports[n];
	let out = midi_out.connect(out_port, "cello-tabs")?;
	Ok(out)
}

struct Meta {
	name: Option<String>,
	instrument: Option<String>,
}

impl Meta {
	pub fn from_track(events: &[TrackEvent<'_>]) -> Self {
		let mut s = Self {
			name: None,
			instrument: None,
		};
		for e in events {
			match e.kind {
				TrackEventKind::Meta(MetaMessage::TrackName(name)) => {
					s.name = Some(String::from_utf8_lossy(name).to_string());
					if s.instrument.is_some() {
						break;
					}
				}
				TrackEventKind::Meta(MetaMessage::InstrumentName(inst)) => {
					s.instrument = Some(String::from_utf8_lossy(inst).to_string());
					if s.name.is_some() {
						break;
					}
				}
				_ => (),
			};
		}

		s
	}
}

impl fmt::Display for Meta {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		let name = self.name.as_deref().unwrap_or("Unnamed Track");
		if let Some(inst) = &self.instrument {
			write!(f, "{} ({})", name, &inst)
		} else {
			write!(f, "{}", name)
		}
	}
}

pub fn choose_track(tracks: &[Vec<TrackEvent<'_>>]) -> Sheet {
	let names = tracks.iter().map(|t| Meta::from_track(t));

	for (i, name) in names.enumerate() {
		println!("#{}: {}", i, &name);
	}

	loop {
		print!("choose a track (0-{}): ", tracks.len());
		io::stdout().flush().unwrap();
		let stdin = io::stdin();
		let n = stdin.lock().lines().next().unwrap().unwrap();

		let n = match n.parse::<usize>() {
			Err(_) => {
				println!("please enter a number between 0 and {}", tracks.len());
				continue;
			}
			Ok(n) if n >= tracks.len() => {
				println!("please enter a number between 0 and {}", tracks.len());
				continue;
			}
			Ok(n) => n,
		};

		return Sheet::single(&tracks[n]);
	}
}