Skip to main content

max / audiofiles

6.3 KB · 216 lines History Blame Raw
1 //! Instrument mode types: MIDI note mapping, ADSR envelope, and key zone configuration.
2
3 /// How the instrument maps MIDI notes to samples.
4 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5 pub enum InstrumentMode {
6 /// One sample pitch-shifted across the entire keyboard.
7 Chromatic,
8 /// Multiple samples assigned to specific key ranges.
9 MultiSample,
10 }
11
12 /// ADSR envelope parameters.
13 #[derive(Debug, Clone, Copy)]
14 pub struct AdsrEnvelope {
15 /// Attack time in seconds.
16 pub attack: f32,
17 /// Decay time in seconds.
18 pub decay: f32,
19 /// Sustain level (0.0 to 1.0).
20 pub sustain: f32,
21 /// Release time in seconds.
22 pub release: f32,
23 }
24
25 impl Default for AdsrEnvelope {
26 fn default() -> Self {
27 Self {
28 attack: 0.005,
29 decay: 0.1,
30 sustain: 1.0,
31 release: 0.05,
32 }
33 }
34 }
35
36 /// What velocity controls.
37 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38 pub enum VelocityTarget {
39 #[default]
40 Volume,
41 }
42
43 /// A key zone mapping a sample to a MIDI note range.
44 #[derive(Debug, Clone)]
45 pub struct KeyZone {
46 /// Content-addressed hash of the sample.
47 pub sample_hash: String,
48 /// Display name of the sample.
49 pub name: String,
50 /// MIDI note number that plays the sample at original pitch.
51 pub root_note: u8,
52 /// Lowest MIDI note in the zone (inclusive).
53 pub low_note: u8,
54 /// Highest MIDI note in the zone (inclusive).
55 pub high_note: u8,
56 /// Minimum velocity to trigger this zone (0.0 to 1.0).
57 pub vel_low: f32,
58 /// Maximum velocity to trigger this zone (0.0 to 1.0).
59 pub vel_high: f32,
60 }
61
62 /// Top-level instrument configuration.
63 #[derive(Debug, Clone)]
64 pub struct InstrumentConfig {
65 pub mode: InstrumentMode,
66 pub envelope: AdsrEnvelope,
67 pub velocity_target: VelocityTarget,
68 pub max_voices: usize,
69 }
70
71 impl Default for InstrumentConfig {
72 fn default() -> Self {
73 Self {
74 mode: InstrumentMode::Chromatic,
75 envelope: AdsrEnvelope::default(),
76 velocity_target: VelocityTarget::default(),
77 max_voices: 8,
78 }
79 }
80 }
81
82 /// Parse a musical key string like "A minor" or "F# major" into a MIDI root note (octave 3).
83 ///
84 /// Returns the note number for the pitch class at octave 3 (C3 = 48).
85 /// Supports sharp (#) and flat (b) accidentals. Case-insensitive on the pitch class.
86 pub fn key_to_root_note(key: &str) -> Option<u8> {
87 let key = key.trim();
88 if key.is_empty() {
89 return None;
90 }
91
92 let mut chars = key.chars();
93 let letter = chars.next()?.to_ascii_uppercase();
94
95 let base = match letter {
96 'C' => 0,
97 'D' => 2,
98 'E' => 4,
99 'F' => 5,
100 'G' => 7,
101 'A' => 9,
102 'B' => 11,
103 _ => return None,
104 };
105
106 // Check for accidental: next char after the letter
107 let rest = &key[letter.len_utf8()..];
108 let accidental = if rest.starts_with('#') {
109 1i8
110 } else if rest.starts_with('b') {
111 -1
112 } else {
113 0
114 };
115
116 // Octave 3: C3 = 48
117 let note = 48 + base as i8 + accidental;
118 if note < 0 { None } else { Some(note as u8) }
119 }
120
121 /// Convert a MIDI note number to a note name string like "C3" or "A#4".
122 pub fn note_name(note: u8) -> String {
123 const NAMES: [&str; 12] = [
124 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
125 ];
126 let octave = (note / 12) as i8 - 1; // MIDI 0 = C-1, 48 = C3, 60 = C4
127 let pitch = (note % 12) as usize;
128 format!("{}{}", NAMES[pitch], octave)
129 }
130
131 #[cfg(test)]
132 mod tests {
133 use super::*;
134
135 #[test]
136 fn default_envelope() {
137 let env = AdsrEnvelope::default();
138 assert!((env.attack - 0.005).abs() < f32::EPSILON);
139 assert!((env.decay - 0.1).abs() < f32::EPSILON);
140 assert!((env.sustain - 1.0).abs() < f32::EPSILON);
141 assert!((env.release - 0.05).abs() < f32::EPSILON);
142 }
143
144 #[test]
145 fn default_config() {
146 let cfg = InstrumentConfig::default();
147 assert_eq!(cfg.mode, InstrumentMode::Chromatic);
148 assert_eq!(cfg.velocity_target, VelocityTarget::Volume);
149 assert_eq!(cfg.max_voices, 8);
150 }
151
152 #[test]
153 fn key_to_root_note_natural_keys() {
154 assert_eq!(key_to_root_note("C major"), Some(48)); // C3
155 assert_eq!(key_to_root_note("D minor"), Some(50)); // D3
156 assert_eq!(key_to_root_note("E minor"), Some(52)); // E3
157 assert_eq!(key_to_root_note("F major"), Some(53)); // F3
158 assert_eq!(key_to_root_note("G major"), Some(55)); // G3
159 assert_eq!(key_to_root_note("A minor"), Some(57)); // A3
160 assert_eq!(key_to_root_note("B minor"), Some(59)); // B3
161 }
162
163 #[test]
164 fn key_to_root_note_sharps() {
165 assert_eq!(key_to_root_note("C# minor"), Some(49));
166 assert_eq!(key_to_root_note("D# minor"), Some(51));
167 assert_eq!(key_to_root_note("F# minor"), Some(54));
168 assert_eq!(key_to_root_note("G# minor"), Some(56));
169 assert_eq!(key_to_root_note("A# minor"), Some(58));
170 }
171
172 #[test]
173 fn key_to_root_note_flats() {
174 assert_eq!(key_to_root_note("Db major"), Some(49));
175 assert_eq!(key_to_root_note("Eb minor"), Some(51));
176 assert_eq!(key_to_root_note("Gb major"), Some(54));
177 assert_eq!(key_to_root_note("Ab minor"), Some(56));
178 assert_eq!(key_to_root_note("Bb major"), Some(58));
179 }
180
181 #[test]
182 fn key_to_root_note_invalid() {
183 assert_eq!(key_to_root_note(""), None);
184 assert_eq!(key_to_root_note("X major"), None);
185 assert_eq!(key_to_root_note(" "), None);
186 }
187
188 #[test]
189 fn note_name_standard_values() {
190 assert_eq!(note_name(60), "C4"); // Middle C
191 assert_eq!(note_name(69), "A4"); // A440
192 assert_eq!(note_name(48), "C3");
193 assert_eq!(note_name(0), "C-1");
194 assert_eq!(note_name(127), "G9");
195 }
196
197 #[test]
198 fn note_name_all_pitch_classes() {
199 let names: Vec<String> = (48..60).map(note_name).collect();
200 assert_eq!(
201 names,
202 vec!["C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3"]
203 );
204 }
205
206 #[test]
207 fn note_name_round_trip() {
208 // key_to_root_note("A minor") = 57 = A3
209 let note = key_to_root_note("A minor").unwrap();
210 assert_eq!(note_name(note), "A3");
211
212 let note = key_to_root_note("C major").unwrap();
213 assert_eq!(note_name(note), "C3");
214 }
215 }
216