use sim_kernel::{Error, Expr, Result, Symbol};
use sim_value::access;
use crate::{Channel, Pitch, Tick};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PerformanceInput {
pub input_time: Tick,
pub intent: PerformanceIntent,
}
impl PerformanceInput {
pub fn new(input_time: Tick, intent: PerformanceIntent) -> Self {
Self { input_time, intent }
}
pub fn note_on(input_time: Tick, channel: Channel, midi: u8, velocity: u8) -> Self {
Self::new(
input_time,
PerformanceIntent::NoteOn {
pitch: Pitch::from_midi(midi),
velocity,
channel,
},
)
}
pub fn note_off(input_time: Tick, channel: Channel, midi: u8, velocity: u8) -> Self {
Self::new(
input_time,
PerformanceIntent::NoteOff {
pitch: Pitch::from_midi(midi),
velocity,
channel,
},
)
}
pub fn sustain(input_time: Tick, channel: Channel, down: bool) -> Self {
Self::new(input_time, PerformanceIntent::Sustain { down, channel })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PerformanceIntent {
NoteOn {
pitch: Pitch,
velocity: u8,
channel: Channel,
},
NoteOff {
pitch: Pitch,
velocity: u8,
channel: Channel,
},
Aftertouch {
pitch: Pitch,
pressure: u8,
channel: Channel,
},
PitchBend {
value: u16,
channel: Channel,
},
Sustain {
down: bool,
channel: Channel,
},
Parameter {
target: Symbol,
value: i64,
},
Panic,
}
impl PerformanceIntent {
pub fn kind_symbol(&self) -> Symbol {
Symbol::qualified("music/performance-intent", self.kind_label())
}
pub fn kind_label(&self) -> &'static str {
match self {
Self::NoteOn { .. } => "note-on",
Self::NoteOff { .. } => "note-off",
Self::Aftertouch { .. } => "aftertouch",
Self::PitchBend { .. } => "pitch-bend",
Self::Sustain { .. } => "sustain",
Self::Parameter { .. } => "parameter",
Self::Panic => "panic",
}
}
pub fn to_expr(&self) -> Expr {
let mut entries = vec![(
Expr::Symbol(Symbol::new("kind")),
Expr::Symbol(self.kind_symbol()),
)];
match self {
Self::NoteOn {
pitch,
velocity,
channel,
}
| Self::NoteOff {
pitch,
velocity,
channel,
} => {
entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
entries.push((
Expr::Symbol(Symbol::new("velocity")),
Expr::String(velocity.to_string()),
));
entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
}
Self::Aftertouch {
pitch,
pressure,
channel,
} => {
entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
entries.push((
Expr::Symbol(Symbol::new("pressure")),
Expr::String(pressure.to_string()),
));
entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
}
Self::PitchBend { value, channel } => {
entries.push((
Expr::Symbol(Symbol::new("value")),
Expr::String(value.to_string()),
));
entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
}
Self::Sustain { down, channel } => {
entries.push((Expr::Symbol(Symbol::new("down")), Expr::Bool(*down)));
entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
}
Self::Parameter { target, value } => {
entries.push((
Expr::Symbol(Symbol::new("target")),
Expr::Symbol(target.clone()),
));
entries.push((
Expr::Symbol(Symbol::new("value")),
Expr::String(value.to_string()),
));
}
Self::Panic => {}
}
Expr::Map(entries)
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
let Expr::Map(entries) = expr else {
return Err(Error::Eval("performance intent must be a map".to_owned()));
};
match symbol_field(entries, "kind")?.as_qualified_str().as_str() {
"note-on" | "music/performance-intent/note-on" => Ok(Self::NoteOn {
pitch: pitch_field(entries, "pitch")?,
velocity: u8_field(entries, "velocity")?,
channel: channel_field(entries, "channel")?,
}),
"note-off" | "music/performance-intent/note-off" => Ok(Self::NoteOff {
pitch: pitch_field(entries, "pitch")?,
velocity: u8_field(entries, "velocity")?,
channel: channel_field(entries, "channel")?,
}),
"aftertouch" | "music/performance-intent/aftertouch" => Ok(Self::Aftertouch {
pitch: pitch_field(entries, "pitch")?,
pressure: u8_field(entries, "pressure")?,
channel: channel_field(entries, "channel")?,
}),
"pitch-bend" | "music/performance-intent/pitch-bend" => Ok(Self::PitchBend {
value: u16_field(entries, "value")?,
channel: channel_field(entries, "channel")?,
}),
"sustain" | "music/performance-intent/sustain" => Ok(Self::Sustain {
down: bool_field(entries, "down")?,
channel: channel_field(entries, "channel")?,
}),
"parameter" | "music/performance-intent/parameter" => Ok(Self::Parameter {
target: symbol_field(entries, "target")?.clone(),
value: i64_field(entries, "value")?,
}),
"panic" | "music/performance-intent/panic" => Ok(Self::Panic),
other => Err(Error::Eval(format!(
"unknown performance intent kind {other}"
))),
}
}
}
fn pitch_expr(pitch: Pitch) -> Expr {
Expr::String(pitch.semitone().to_string())
}
fn channel_expr(channel: Channel) -> Expr {
Expr::String(channel.0.to_string())
}
fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
access::entry_required_str(entries, name, "string field")
}
fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
access::entry_required_sym(entries, name, "symbol field")
}
fn bool_field(entries: &[(Expr, Expr)], name: &str) -> Result<bool> {
access::entry_required_bool(entries, name, "boolean field")
}
fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
string_field(entries, name)?
.parse::<i64>()
.map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
}
fn i32_field(entries: &[(Expr, Expr)], name: &str) -> Result<i32> {
string_field(entries, name)?
.parse::<i32>()
.map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
}
fn u8_field(entries: &[(Expr, Expr)], name: &str) -> Result<u8> {
string_field(entries, name)?
.parse::<u8>()
.map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
}
fn u16_field(entries: &[(Expr, Expr)], name: &str) -> Result<u16> {
string_field(entries, name)?
.parse::<u16>()
.map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
}
fn pitch_field(entries: &[(Expr, Expr)], name: &str) -> Result<Pitch> {
Ok(Pitch::from_semitone(i32_field(entries, name)?))
}
fn channel_field(entries: &[(Expr, Expr)], name: &str) -> Result<Channel> {
Channel::new(u8_field(entries, name)?).map_err(|err| Error::Eval(err.to_string()))
}