1use std::convert::TryFrom;
2
3use crate::{Channel, ChannelMessage, MidiEvent, MidiPayload, TickTime, U7, synthetic_origin};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum NoteEchoScaleSnap {
8 Off,
10 PitchClasses(Vec<u8>),
13}
14
15impl NoteEchoScaleSnap {
16 pub fn major(tonic: u8) -> Self {
18 Self::PitchClasses(
19 [0, 2, 4, 5, 7, 9, 11]
20 .into_iter()
21 .map(|class| (class + tonic) % 12)
22 .collect(),
23 )
24 }
25
26 fn snap(&self, key: i16) -> Option<u8> {
27 if !(0..=127).contains(&key) {
28 return None;
29 }
30 match self {
31 Self::Off => Some(key as u8),
32 Self::PitchClasses(classes) => nearest_key_in_classes(key, classes),
33 }
34 }
35}
36
37#[derive(Copy, Clone, Debug, PartialEq, Eq)]
39pub enum NoteEchoChannelPolicy {
40 Preserve,
42 Fixed(Channel),
44 Offset(i8),
46}
47
48impl NoteEchoChannelPolicy {
49 fn apply(self, channel: Channel, repeat: u8) -> Channel {
50 match self {
51 Self::Preserve => channel,
52 Self::Fixed(channel) => channel,
53 Self::Offset(offset) => Channel(
54 (i16::from(channel.0) + i16::from(offset) * i16::from(repeat)).rem_euclid(16) as u8,
55 ),
56 }
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct NoteEchoConfig {
63 pub repeats: u8,
65 pub feedback_count: u8,
67 pub time_offset: TickTime,
69 pub fallback_gate: TickTime,
71 pub velocity_decay: u8,
73 pub pitch_offset: i8,
75 pub scale_snap: NoteEchoScaleSnap,
77 pub channel_policy: NoteEchoChannelPolicy,
79 pub include_source: bool,
81}
82
83impl NoteEchoConfig {
84 pub fn new(time_offset: TickTime) -> Self {
86 Self {
87 repeats: 1,
88 feedback_count: 1,
89 time_offset,
90 fallback_gate: time_offset,
91 velocity_decay: 0,
92 pitch_offset: 0,
93 scale_snap: NoteEchoScaleSnap::Off,
94 channel_policy: NoteEchoChannelPolicy::Preserve,
95 include_source: true,
96 }
97 }
98
99 pub fn repeat_count(&self) -> u8 {
102 if self.feedback_count == 0 {
103 self.repeats
104 } else {
105 self.repeats.min(self.feedback_count)
106 }
107 }
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct NoteEchoTrace {
113 pub source_index: usize,
115 pub repeat: u8,
117 pub time: TickTime,
119 pub key: U7,
121 pub velocity: U7,
123 pub channel: Channel,
125}
126
127#[derive(Clone, Debug, Default, PartialEq, Eq)]
130pub struct NoteEchoRender {
131 pub events: Vec<MidiEvent>,
133 pub traces: Vec<NoteEchoTrace>,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct NoteEchoPlayer {
141 pub config: NoteEchoConfig,
143}
144
145impl NoteEchoPlayer {
146 pub fn new(config: NoteEchoConfig) -> Self {
148 Self { config }
149 }
150
151 pub fn render(&self, input: &[MidiEvent]) -> NoteEchoRender {
153 let mut render = NoteEchoRender::default();
154 if self.config.include_source {
155 render.events.extend(input.iter().cloned());
156 }
157
158 for (source_index, event) in input.iter().enumerate() {
159 let Some((channel, key, velocity)) = note_on(event) else {
160 continue;
161 };
162 let duration =
163 note_duration(input, source_index, channel, key, self.config.fallback_gate);
164 for repeat in 1..=self.config.repeat_count() {
165 let Some((echo_key, echo_velocity, echo_channel)) =
166 self.echo_values(key, velocity, channel, repeat)
167 else {
168 continue;
169 };
170 let time = event.time + self.config.time_offset.mul_int(i64::from(repeat));
171 let off_time = time + duration;
172 render
173 .events
174 .push(note_on_event(time, echo_channel, echo_key, echo_velocity));
175 render
176 .events
177 .push(note_off_event(off_time, echo_channel, echo_key));
178 render.traces.push(NoteEchoTrace {
179 source_index,
180 repeat,
181 time,
182 key: echo_key,
183 velocity: echo_velocity,
184 channel: echo_channel,
185 });
186 }
187 }
188 stable_midi_event_order(&mut render.events);
189 render.traces.sort_by(|left, right| {
190 left.time
191 .cmp(&right.time)
192 .then_with(|| left.source_index.cmp(&right.source_index))
193 .then_with(|| left.repeat.cmp(&right.repeat))
194 });
195 render
196 }
197
198 pub fn freeze(&self, input: &[MidiEvent]) -> NoteEchoRender {
201 self.render(input)
202 }
203
204 fn echo_values(
205 &self,
206 key: U7,
207 velocity: U7,
208 channel: Channel,
209 repeat: u8,
210 ) -> Option<(U7, U7, Channel)> {
211 let drifted_key =
212 i16::from(key.0) + i16::from(self.config.pitch_offset) * i16::from(repeat);
213 let snapped_key = self.config.scale_snap.snap(drifted_key)?;
214 let decayed = velocity
215 .0
216 .saturating_sub(self.config.velocity_decay.saturating_mul(repeat))
217 .max(1);
218 Some((
219 U7::try_from(u16::from(snapped_key)).ok()?,
220 U7(decayed),
221 self.config.channel_policy.apply(channel, repeat),
222 ))
223 }
224}
225
226fn note_on(event: &MidiEvent) -> Option<(Channel, U7, U7)> {
227 match event.payload {
228 MidiPayload::Channel(ChannelMessage::NoteOn { ch, key, vel }) if vel.0 > 0 => {
229 Some((ch, key, vel))
230 }
231 _ => None,
232 }
233}
234
235fn is_note_off(event: &MidiEvent, channel: Channel, key: U7) -> bool {
236 matches!(
237 event.payload,
238 MidiPayload::Channel(ChannelMessage::NoteOff { ch, key: off_key, .. })
239 if ch == channel && off_key == key
240 ) || matches!(
241 event.payload,
242 MidiPayload::Channel(ChannelMessage::NoteOn { ch, key: off_key, vel })
243 if ch == channel && off_key == key && vel.0 == 0
244 )
245}
246
247fn note_duration(
248 input: &[MidiEvent],
249 source_index: usize,
250 channel: Channel,
251 key: U7,
252 fallback: TickTime,
253) -> TickTime {
254 let start = input[source_index].time;
255 input
256 .iter()
257 .skip(source_index + 1)
258 .find(|event| event.time > start && is_note_off(event, channel, key))
259 .map(|event| event.time - start)
260 .unwrap_or(fallback)
261}
262
263fn nearest_key_in_classes(key: i16, classes: &[u8]) -> Option<u8> {
264 if classes.is_empty() {
265 return Some(key as u8);
266 }
267 (-12..=12)
268 .filter_map(|delta| {
269 let candidate = key + delta;
270 if !(0..=127).contains(&candidate) {
271 return None;
272 }
273 let class = candidate.rem_euclid(12) as u8;
274 classes
275 .iter()
276 .any(|allowed| allowed % 12 == class)
277 .then_some((delta.abs(), candidate as u8))
278 })
279 .min_by_key(|(distance, candidate)| (*distance, *candidate))
280 .map(|(_, candidate)| candidate)
281}
282
283fn note_on_event(time: TickTime, ch: Channel, key: U7, vel: U7) -> MidiEvent {
284 MidiEvent {
285 time,
286 origin: synthetic_origin(),
287 payload: MidiPayload::Channel(ChannelMessage::NoteOn { ch, key, vel }),
288 }
289}
290
291fn note_off_event(time: TickTime, ch: Channel, key: U7) -> MidiEvent {
292 MidiEvent {
293 time,
294 origin: synthetic_origin(),
295 payload: MidiPayload::Channel(ChannelMessage::NoteOff {
296 ch,
297 key,
298 vel: U7(0),
299 }),
300 }
301}
302
303pub fn stable_midi_event_order(events: &mut [MidiEvent]) {
306 events.sort_by(|left, right| {
307 left.time
308 .cmp(&right.time)
309 .then_with(|| event_sort_key(left).cmp(&event_sort_key(right)))
310 });
311}
312
313fn event_sort_key(event: &MidiEvent) -> (u8, u8, u8, u8) {
314 match event.payload {
315 MidiPayload::Channel(ChannelMessage::NoteOff { ch, key, vel }) => (0, ch.0, key.0, vel.0),
316 MidiPayload::Channel(ChannelMessage::NoteOn { ch, key, vel }) => (1, ch.0, key.0, vel.0),
317 MidiPayload::Channel(_) => (2, 0, 0, 0),
318 MidiPayload::Meta(_) => (3, 0, 0, 0),
319 MidiPayload::SysEx(_) => (4, 0, 0, 0),
320 MidiPayload::Raw(_) => (5, 0, 0, 0),
321 }
322}