1use sim_kernel::{Error, Expr, Result, Symbol};
2use sim_value::access;
3
4use crate::{Channel, Pitch, Tick};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct PerformanceInput {
12 pub input_time: Tick,
14 pub intent: PerformanceIntent,
16}
17
18impl PerformanceInput {
19 pub fn new(input_time: Tick, intent: PerformanceIntent) -> Self {
21 Self { input_time, intent }
22 }
23
24 pub fn note_on(input_time: Tick, channel: Channel, midi: u8, velocity: u8) -> Self {
26 Self::new(
27 input_time,
28 PerformanceIntent::NoteOn {
29 pitch: Pitch::from_midi(midi),
30 velocity,
31 channel,
32 },
33 )
34 }
35
36 pub fn note_off(input_time: Tick, channel: Channel, midi: u8, velocity: u8) -> Self {
38 Self::new(
39 input_time,
40 PerformanceIntent::NoteOff {
41 pitch: Pitch::from_midi(midi),
42 velocity,
43 channel,
44 },
45 )
46 }
47
48 pub fn sustain(input_time: Tick, channel: Channel, down: bool) -> Self {
50 Self::new(input_time, PerformanceIntent::Sustain { down, channel })
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum PerformanceIntent {
61 NoteOn {
63 pitch: Pitch,
65 velocity: u8,
67 channel: Channel,
69 },
70 NoteOff {
72 pitch: Pitch,
74 velocity: u8,
76 channel: Channel,
78 },
79 Aftertouch {
81 pitch: Pitch,
83 pressure: u8,
85 channel: Channel,
87 },
88 PitchBend {
90 value: u16,
92 channel: Channel,
94 },
95 Sustain {
97 down: bool,
99 channel: Channel,
101 },
102 Parameter {
104 target: Symbol,
106 value: i64,
108 },
109 Panic,
111}
112
113impl PerformanceIntent {
114 pub fn kind_symbol(&self) -> Symbol {
116 Symbol::qualified("music/performance-intent", self.kind_label())
117 }
118
119 pub fn kind_label(&self) -> &'static str {
121 match self {
122 Self::NoteOn { .. } => "note-on",
123 Self::NoteOff { .. } => "note-off",
124 Self::Aftertouch { .. } => "aftertouch",
125 Self::PitchBend { .. } => "pitch-bend",
126 Self::Sustain { .. } => "sustain",
127 Self::Parameter { .. } => "parameter",
128 Self::Panic => "panic",
129 }
130 }
131
132 pub fn to_expr(&self) -> Expr {
134 let mut entries = vec![(
135 Expr::Symbol(Symbol::new("kind")),
136 Expr::Symbol(self.kind_symbol()),
137 )];
138 match self {
139 Self::NoteOn {
140 pitch,
141 velocity,
142 channel,
143 }
144 | Self::NoteOff {
145 pitch,
146 velocity,
147 channel,
148 } => {
149 entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
150 entries.push((
151 Expr::Symbol(Symbol::new("velocity")),
152 Expr::String(velocity.to_string()),
153 ));
154 entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
155 }
156 Self::Aftertouch {
157 pitch,
158 pressure,
159 channel,
160 } => {
161 entries.push((Expr::Symbol(Symbol::new("pitch")), pitch_expr(*pitch)));
162 entries.push((
163 Expr::Symbol(Symbol::new("pressure")),
164 Expr::String(pressure.to_string()),
165 ));
166 entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
167 }
168 Self::PitchBend { value, channel } => {
169 entries.push((
170 Expr::Symbol(Symbol::new("value")),
171 Expr::String(value.to_string()),
172 ));
173 entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
174 }
175 Self::Sustain { down, channel } => {
176 entries.push((Expr::Symbol(Symbol::new("down")), Expr::Bool(*down)));
177 entries.push((Expr::Symbol(Symbol::new("channel")), channel_expr(*channel)));
178 }
179 Self::Parameter { target, value } => {
180 entries.push((
181 Expr::Symbol(Symbol::new("target")),
182 Expr::Symbol(target.clone()),
183 ));
184 entries.push((
185 Expr::Symbol(Symbol::new("value")),
186 Expr::String(value.to_string()),
187 ));
188 }
189 Self::Panic => {}
190 }
191 Expr::Map(entries)
192 }
193
194 pub fn from_expr(expr: &Expr) -> Result<Self> {
199 let Expr::Map(entries) = expr else {
200 return Err(Error::Eval("performance intent must be a map".to_owned()));
201 };
202 match symbol_field(entries, "kind")?.as_qualified_str().as_str() {
203 "note-on" | "music/performance-intent/note-on" => Ok(Self::NoteOn {
204 pitch: pitch_field(entries, "pitch")?,
205 velocity: u8_field(entries, "velocity")?,
206 channel: channel_field(entries, "channel")?,
207 }),
208 "note-off" | "music/performance-intent/note-off" => Ok(Self::NoteOff {
209 pitch: pitch_field(entries, "pitch")?,
210 velocity: u8_field(entries, "velocity")?,
211 channel: channel_field(entries, "channel")?,
212 }),
213 "aftertouch" | "music/performance-intent/aftertouch" => Ok(Self::Aftertouch {
214 pitch: pitch_field(entries, "pitch")?,
215 pressure: u8_field(entries, "pressure")?,
216 channel: channel_field(entries, "channel")?,
217 }),
218 "pitch-bend" | "music/performance-intent/pitch-bend" => Ok(Self::PitchBend {
219 value: u16_field(entries, "value")?,
220 channel: channel_field(entries, "channel")?,
221 }),
222 "sustain" | "music/performance-intent/sustain" => Ok(Self::Sustain {
223 down: bool_field(entries, "down")?,
224 channel: channel_field(entries, "channel")?,
225 }),
226 "parameter" | "music/performance-intent/parameter" => Ok(Self::Parameter {
227 target: symbol_field(entries, "target")?.clone(),
228 value: i64_field(entries, "value")?,
229 }),
230 "panic" | "music/performance-intent/panic" => Ok(Self::Panic),
231 other => Err(Error::Eval(format!(
232 "unknown performance intent kind {other}"
233 ))),
234 }
235 }
236}
237
238fn pitch_expr(pitch: Pitch) -> Expr {
239 Expr::String(pitch.semitone().to_string())
240}
241
242fn channel_expr(channel: Channel) -> Expr {
243 Expr::String(channel.0.to_string())
244}
245
246fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
247 access::entry_required_str(entries, name, "string field")
248}
249
250fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
251 access::entry_required_sym(entries, name, "symbol field")
252}
253
254fn bool_field(entries: &[(Expr, Expr)], name: &str) -> Result<bool> {
255 access::entry_required_bool(entries, name, "boolean field")
256}
257
258fn i64_field(entries: &[(Expr, Expr)], name: &str) -> Result<i64> {
259 string_field(entries, name)?
260 .parse::<i64>()
261 .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
262}
263
264fn i32_field(entries: &[(Expr, Expr)], name: &str) -> Result<i32> {
265 string_field(entries, name)?
266 .parse::<i32>()
267 .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
268}
269
270fn u8_field(entries: &[(Expr, Expr)], name: &str) -> Result<u8> {
271 string_field(entries, name)?
272 .parse::<u8>()
273 .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
274}
275
276fn u16_field(entries: &[(Expr, Expr)], name: &str) -> Result<u16> {
277 string_field(entries, name)?
278 .parse::<u16>()
279 .map_err(|err| Error::Eval(format!("invalid {name}: {err}")))
280}
281
282fn pitch_field(entries: &[(Expr, Expr)], name: &str) -> Result<Pitch> {
283 Ok(Pitch::from_semitone(i32_field(entries, name)?))
284}
285
286fn channel_field(entries: &[(Expr, Expr)], name: &str) -> Result<Channel> {
287 Channel::new(u8_field(entries, name)?).map_err(|err| Error::Eval(err.to_string()))
288}