Skip to main content

aether_midi/
mpe.rs

1//! MPE (MIDI Polyphonic Expression) support.
2//!
3//! MPE allows per-note control of pitch bend, pressure (aftertouch), and timbre (CC74).
4//! Each note is assigned to a separate MIDI channel (typically channels 2-15),
5//! with channel 1 reserved for global messages.
6//!
7//! # MPE Zones
8//!
9//! MPE defines two zones:
10//! - **Lower Zone:** Channels 1-8 (channel 1 = master, 2-8 = member channels)
11//! - **Upper Zone:** Channels 9-16 (channel 16 = master, 9-15 = member channels)
12//!
13//! # Per-Note Expression
14//!
15//! Each note can have independent:
16//! - **Pitch Bend:** ±48 semitones (configurable)
17//! - **Pressure:** Channel aftertouch (0-127)
18//! - **Timbre:** CC74 (0-127)
19
20use std::collections::HashMap;
21
22/// MPE configuration.
23#[derive(Debug, Clone, Copy)]
24pub struct MpeConfig {
25    /// Lower zone size (number of member channels, 0-15).
26    /// 0 = disabled, 15 = channels 2-16 are member channels.
27    pub lower_zone_size: u8,
28
29    /// Upper zone size (number of member channels, 0-15).
30    /// 0 = disabled, 15 = channels 1-15 are member channels.
31    pub upper_zone_size: u8,
32
33    /// Pitch bend range in semitones (typically 48).
34    pub pitch_bend_range: f32,
35}
36
37impl Default for MpeConfig {
38    fn default() -> Self {
39        Self {
40            lower_zone_size: 15, // Use all channels 2-16
41            upper_zone_size: 0,  // Upper zone disabled
42            pitch_bend_range: 48.0,
43        }
44    }
45}
46
47/// Per-note expression data.
48#[derive(Debug, Clone, Copy, Default)]
49pub struct NoteExpression {
50    /// MIDI note number (0-127).
51    pub note: u8,
52
53    /// MIDI channel (1-16).
54    pub channel: u8,
55
56    /// Velocity (0-127).
57    pub velocity: u8,
58
59    /// Pitch bend in semitones (±pitch_bend_range).
60    pub pitch_bend: f32,
61
62    /// Channel pressure/aftertouch (0.0-1.0).
63    pub pressure: f32,
64
65    /// Timbre/brightness CC74 (0.0-1.0).
66    pub timbre: f32,
67}
68
69/// MPE engine for managing per-note expression.
70pub struct MpeEngine {
71    config: MpeConfig,
72
73    /// Active notes: (channel, note) -> NoteExpression
74    active_notes: HashMap<(u8, u8), NoteExpression>,
75
76    /// Channel allocation: note -> channel
77    /// Used to assign notes to available channels in round-robin fashion.
78    note_to_channel: HashMap<u8, u8>,
79
80    /// Next channel to allocate (round-robin).
81    next_channel: u8,
82}
83
84impl MpeEngine {
85    /// Creates a new MPE engine with default configuration.
86    pub fn new() -> Self {
87        Self::with_config(MpeConfig::default())
88    }
89
90    /// Creates a new MPE engine with custom configuration.
91    pub fn with_config(config: MpeConfig) -> Self {
92        Self {
93            config,
94            active_notes: HashMap::new(),
95            note_to_channel: HashMap::new(),
96            next_channel: 2, // Start at channel 2 (first member channel)
97        }
98    }
99
100    /// Gets the current MPE configuration.
101    pub fn config(&self) -> &MpeConfig {
102        &self.config
103    }
104
105    /// Sets the MPE configuration.
106    pub fn set_config(&mut self, config: MpeConfig) {
107        self.config = config;
108    }
109
110    /// Handles a note-on event.
111    ///
112    /// Assigns the note to an available channel and creates a NoteExpression.
113    ///
114    /// # Arguments
115    ///
116    /// * `note` - MIDI note number (0-127)
117    /// * `velocity` - Note velocity (0-127)
118    ///
119    /// # Returns
120    ///
121    /// The NoteExpression for this note.
122    pub fn note_on(&mut self, note: u8, velocity: u8) -> NoteExpression {
123        // Allocate channel for this note
124        let channel = self.allocate_channel();
125        self.note_to_channel.insert(note, channel);
126
127        let expr = NoteExpression {
128            note,
129            channel,
130            velocity,
131            pitch_bend: 0.0,
132            pressure: 0.0,
133            timbre: 0.0,
134        };
135
136        self.active_notes.insert((channel, note), expr);
137        expr
138    }
139
140    /// Handles a note-off event.
141    ///
142    /// # Arguments
143    ///
144    /// * `note` - MIDI note number (0-127)
145    ///
146    /// # Returns
147    ///
148    /// The NoteExpression that was removed, or None if note wasn't active.
149    pub fn note_off(&mut self, note: u8) -> Option<NoteExpression> {
150        if let Some(&channel) = self.note_to_channel.get(&note) {
151            self.note_to_channel.remove(&note);
152            self.active_notes.remove(&(channel, note))
153        } else {
154            None
155        }
156    }
157
158    /// Handles a pitch bend event for a specific channel.
159    ///
160    /// # Arguments
161    ///
162    /// * `channel` - MIDI channel (1-16)
163    /// * `value` - Pitch bend value (0-16383, 8192 = center)
164    pub fn pitch_bend(&mut self, channel: u8, value: u16) {
165        // Convert 14-bit pitch bend to semitones
166        let normalized = (value as f32 - 8192.0) / 8192.0; // -1.0 to +1.0
167        let semitones = normalized * self.config.pitch_bend_range;
168
169        // Update all notes on this channel
170        for ((ch, _note), expr) in self.active_notes.iter_mut() {
171            if *ch == channel {
172                expr.pitch_bend = semitones;
173            }
174        }
175    }
176
177    /// Handles a channel pressure (aftertouch) event.
178    ///
179    /// # Arguments
180    ///
181    /// * `channel` - MIDI channel (1-16)
182    /// * `value` - Pressure value (0-127)
183    pub fn channel_pressure(&mut self, channel: u8, value: u8) {
184        let normalized = value as f32 / 127.0;
185
186        // Update all notes on this channel
187        for ((ch, _note), expr) in self.active_notes.iter_mut() {
188            if *ch == channel {
189                expr.pressure = normalized;
190            }
191        }
192    }
193
194    /// Handles a CC74 (timbre/brightness) event.
195    ///
196    /// # Arguments
197    ///
198    /// * `channel` - MIDI channel (1-16)
199    /// * `value` - CC74 value (0-127)
200    pub fn timbre(&mut self, channel: u8, value: u8) {
201        let normalized = value as f32 / 127.0;
202
203        // Update all notes on this channel
204        for ((ch, _note), expr) in self.active_notes.iter_mut() {
205            if *ch == channel {
206                expr.timbre = normalized;
207            }
208        }
209    }
210
211    /// Gets the expression data for a specific note.
212    ///
213    /// # Arguments
214    ///
215    /// * `note` - MIDI note number (0-127)
216    ///
217    /// # Returns
218    ///
219    /// The NoteExpression for this note, or None if not active.
220    pub fn get_note_expression(&self, note: u8) -> Option<&NoteExpression> {
221        if let Some(&channel) = self.note_to_channel.get(&note) {
222            self.active_notes.get(&(channel, note))
223        } else {
224            None
225        }
226    }
227
228    /// Gets all active note expressions.
229    pub fn active_notes(&self) -> impl Iterator<Item = &NoteExpression> {
230        self.active_notes.values()
231    }
232
233    /// Gets the number of active notes.
234    pub fn active_note_count(&self) -> usize {
235        self.active_notes.len()
236    }
237
238    /// Clears all active notes (panic button / all notes off).
239    pub fn clear_all_notes(&mut self) {
240        self.active_notes.clear();
241        self.note_to_channel.clear();
242        self.next_channel = 2;
243    }
244
245    /// Allocates a channel for a new note (round-robin).
246    fn allocate_channel(&mut self) -> u8 {
247        let max_channel = if self.config.lower_zone_size > 0 {
248            1 + self.config.lower_zone_size
249        } else {
250            16
251        };
252
253        let channel = self.next_channel;
254        self.next_channel += 1;
255        if self.next_channel > max_channel {
256            self.next_channel = 2; // Wrap around
257        }
258
259        channel
260    }
261}
262
263impl Default for MpeEngine {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_mpe_note_on_off() {
275        let mut mpe = MpeEngine::new();
276
277        // Note on
278        let expr = mpe.note_on(60, 100);
279        assert_eq!(expr.note, 60);
280        assert_eq!(expr.velocity, 100);
281        assert_eq!(expr.channel, 2); // First member channel
282        assert_eq!(mpe.active_note_count(), 1);
283
284        // Note off
285        let removed = mpe.note_off(60);
286        assert!(removed.is_some());
287        assert_eq!(mpe.active_note_count(), 0);
288    }
289
290    #[test]
291    fn test_mpe_pitch_bend() {
292        let mut mpe = MpeEngine::new();
293
294        let expr = mpe.note_on(60, 100);
295        let channel = expr.channel;
296
297        // Apply pitch bend (+1 semitone)
298        mpe.pitch_bend(channel, 8192 + 170); // ~+1 semitone
299
300        let updated = mpe.get_note_expression(60).unwrap();
301        assert!(updated.pitch_bend > 0.9 && updated.pitch_bend < 1.1);
302    }
303
304    #[test]
305    fn test_mpe_pressure() {
306        let mut mpe = MpeEngine::new();
307
308        let expr = mpe.note_on(60, 100);
309        let channel = expr.channel;
310
311        // Apply pressure
312        mpe.channel_pressure(channel, 64); // 50%
313
314        let updated = mpe.get_note_expression(60).unwrap();
315        assert!((updated.pressure - 0.5).abs() < 0.01);
316    }
317
318    #[test]
319    fn test_mpe_timbre() {
320        let mut mpe = MpeEngine::new();
321
322        let expr = mpe.note_on(60, 100);
323        let channel = expr.channel;
324
325        // Apply timbre
326        mpe.timbre(channel, 127); // 100%
327
328        let updated = mpe.get_note_expression(60).unwrap();
329        assert_eq!(updated.timbre, 1.0);
330    }
331
332    #[test]
333    fn test_mpe_channel_allocation() {
334        let mut mpe = MpeEngine::new();
335
336        // Allocate 3 notes
337        let expr1 = mpe.note_on(60, 100);
338        let expr2 = mpe.note_on(62, 100);
339        let expr3 = mpe.note_on(64, 100);
340
341        // Should get different channels
342        assert_eq!(expr1.channel, 2);
343        assert_eq!(expr2.channel, 3);
344        assert_eq!(expr3.channel, 4);
345    }
346
347    #[test]
348    fn test_mpe_clear_all() {
349        let mut mpe = MpeEngine::new();
350
351        mpe.note_on(60, 100);
352        mpe.note_on(62, 100);
353        mpe.note_on(64, 100);
354        assert_eq!(mpe.active_note_count(), 3);
355
356        mpe.clear_all_notes();
357        assert_eq!(mpe.active_note_count(), 0);
358    }
359}