nord_format/formats/nsmp/
keymap.rs1use super::zone;
14use crate::error::ParseError;
15
16pub(super) const VERSION: u8 = 10;
18
19pub(super) const VERSION_EARLY: u8 = 9;
23
24pub const GAIN_UNITY: u32 = zone::GAIN_UNITY;
27
28pub const GAIN_MAX: u32 = 0xFF_FFFF;
30
31pub const DETUNE_PER_SEMITONE: i32 = 256;
34
35const DETUNE_MIN: i32 = -(1 << 23);
36const DETUNE_MAX: i32 = (1 << 23) - 1;
37
38pub const KEYS: usize = 128;
40
41pub const RECORD_LEN: usize = 6;
43
44const LEVEL_AT: usize = 0;
46
47pub const KEY_TABLE_AT: usize = 15;
49
50const _: () = assert!(KEY_TABLE_AT + KEYS * RECORD_LEN + 2 == zone::COUNT_AT);
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Level {
55 gain: u32,
61 detune: i32,
66}
67
68impl Level {
69 pub const NEUTRAL: Level = Level {
71 gain: GAIN_UNITY,
72 detune: 0,
73 };
74
75 pub fn new(gain: u32, detune: i32) -> Result<Level, ParseError> {
77 if gain > GAIN_MAX {
78 return Err(ParseError::AssertFail(format!(
79 "gain {gain:#x} does not fit the 24-bit field (max {GAIN_MAX:#x})"
80 )));
81 }
82 if !(DETUNE_MIN..=DETUNE_MAX).contains(&detune) {
83 return Err(ParseError::AssertFail(format!(
84 "detune {detune} does not fit the 24-bit field ({DETUNE_MIN}..={DETUNE_MAX})"
85 )));
86 }
87 Ok(Level { gain, detune })
88 }
89
90 pub fn from_ratio(gain: f64, semitones: f64) -> Result<Level, ParseError> {
93 let units = gain * f64::from(GAIN_UNITY);
94 if !units.is_finite() || units < 0.0 || units > f64::from(GAIN_MAX) {
95 return Err(ParseError::AssertFail(format!(
96 "gain ratio {gain} is outside what the 24-bit field holds"
97 )));
98 }
99 let detune = semitones * f64::from(DETUNE_PER_SEMITONE);
100 if !detune.is_finite() || detune < f64::from(DETUNE_MIN) || detune > f64::from(DETUNE_MAX) {
101 return Err(ParseError::AssertFail(format!(
102 "detune of {semitones} semitones is outside what the 24-bit field holds"
103 )));
104 }
105 Level::new(units.round() as u32, detune.trunc() as i32)
106 }
107
108 pub const fn gain(self) -> u32 {
110 self.gain
111 }
112
113 pub const fn detune(self) -> i32 {
115 self.detune
116 }
117
118 pub fn ratio(&self) -> f64 {
120 f64::from(self.gain) / f64::from(GAIN_UNITY)
121 }
122
123 pub fn semitones(&self) -> f64 {
125 f64::from(self.detune) / f64::from(DETUNE_PER_SEMITONE)
126 }
127
128 fn read(record: &[u8]) -> Level {
129 let gain = u32::from_be_bytes([0, record[0], record[1], record[2]]);
130 let raw = u32::from_be_bytes([0, record[3], record[4], record[5]]);
131 let detune = ((raw << 8) as i32) >> 8;
133 Level { gain, detune }
134 }
135
136 fn write(&self, record: &mut [u8]) {
137 record[..3].copy_from_slice(&self.gain.to_be_bytes()[1..]);
138 record[3..RECORD_LEN].copy_from_slice(&(self.detune as u32).to_be_bytes()[1..]);
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct KeyTable {
145 pub instrument: Level,
147 keys: [Level; KEYS],
149}
150
151impl KeyTable {
152 pub const NEUTRAL: KeyTable = KeyTable {
154 instrument: Level::NEUTRAL,
155 keys: [Level::NEUTRAL; KEYS],
156 };
157
158 pub fn read(map: &[u8]) -> Result<KeyTable, ParseError> {
160 fits(map)?;
161 let mut keys = [Level::NEUTRAL; KEYS];
162 for (note, key) in keys.iter_mut().enumerate() {
163 *key = Level::read(&map[record_at(note)..][..RECORD_LEN]);
164 }
165 Ok(KeyTable {
166 instrument: Level::read(&map[LEVEL_AT..][..RECORD_LEN]),
167 keys,
168 })
169 }
170
171 pub fn write(&self, map: &mut [u8]) -> Result<(), ParseError> {
173 fits(map)?;
174 self.instrument.write(&mut map[LEVEL_AT..][..RECORD_LEN]);
175 for (note, key) in self.keys.iter().enumerate() {
176 key.write(&mut map[record_at(note)..][..RECORD_LEN]);
177 }
178 Ok(())
179 }
180
181 pub fn prefix(&self) -> [u8; zone::COUNT_AT] {
183 let mut out = [0u8; zone::COUNT_AT];
184 self.write(&mut out)
185 .expect("a buffer sized to the zone count holds the whole keyboard map");
186 out
187 }
188
189 pub fn key(&self, note: u8) -> Result<Level, ParseError> {
191 self.keys
192 .get(usize::from(note))
193 .copied()
194 .ok_or_else(|| ParseError::OutOfBounds {
195 value: format!("MIDI note {note}"),
196 bound: "a MIDI note from 0 through 127".into(),
197 })
198 }
199
200 pub fn set_key(&mut self, note: u8, level: Level) -> Result<(), ParseError> {
202 let key = self
203 .keys
204 .get_mut(usize::from(note))
205 .ok_or_else(|| ParseError::OutOfBounds {
206 value: format!("MIDI note {note}"),
207 bound: "a MIDI note from 0 through 127".into(),
208 })?;
209 *key = level;
210 Ok(())
211 }
212
213 pub fn adjusted(&self) -> impl Iterator<Item = u8> + '_ {
215 self.keys
216 .iter()
217 .enumerate()
218 .filter(|(_, level)| **level != Level::NEUTRAL)
219 .map(|(note, _)| note as u8)
220 }
221}
222
223const fn record_at(note: usize) -> usize {
224 KEY_TABLE_AT + RECORD_LEN * note
225}
226
227fn fits(map: &[u8]) -> Result<(), ParseError> {
228 if map.len() < zone::COUNT_AT {
229 return Err(ParseError::AssertFail(format!(
230 "map section is {} bytes, too short for a keyboard map",
231 map.len()
232 )));
233 }
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn neutral_prefix_is_unity_at_every_record_and_zero_between() {
243 let prefix = KeyTable::NEUTRAL.prefix();
244 assert_eq!(prefix.len(), zone::COUNT_AT);
245 for (i, b) in prefix.iter().enumerate() {
246 let expected = if i == LEVEL_AT
247 || (i >= KEY_TABLE_AT
248 && (i - KEY_TABLE_AT).is_multiple_of(RECORD_LEN)
249 && i < KEY_TABLE_AT + KEYS * RECORD_LEN)
250 {
251 0x10
252 } else {
253 0
254 };
255 assert_eq!(*b, expected, "byte {i}");
256 }
257 }
258
259 #[test]
260 fn records_round_trip_through_their_bytes() {
261 let mut table = KeyTable::NEUTRAL;
262 table.instrument = Level::new(0x2d_1819, -256).unwrap();
263 table
264 .set_key(60, Level::new(0x08_0000, 20).unwrap())
265 .unwrap();
266 table
267 .set_key(17, Level::new(0, -(1 << 23)).unwrap())
268 .unwrap();
269 table
270 .set_key(127, Level::new(GAIN_MAX, (1 << 23) - 1).unwrap())
271 .unwrap();
272 let mut map = vec![0xAA; zone::COUNT_AT + 1 + zone::RECORD_LEN];
273 table.write(&mut map).unwrap();
274 assert_eq!(KeyTable::read(&map).unwrap(), table);
275 assert!(map[LEVEL_AT + RECORD_LEN..KEY_TABLE_AT]
276 .iter()
277 .all(|&b| b == 0xAA));
278 assert!(map[KEY_TABLE_AT + KEYS * RECORD_LEN..zone::COUNT_AT]
279 .iter()
280 .all(|&b| b == 0xAA));
281 assert!(map[zone::COUNT_AT..].iter().all(|b| *b == 0xAA));
282 assert_eq!(table.adjusted().collect::<Vec<_>>(), [17, 60, 127]);
283 }
284
285 #[test]
286 fn detune_reads_signed() {
287 let mut map = vec![0u8; zone::COUNT_AT];
288 map[record_at(60) + 3..record_at(60) + 6].copy_from_slice(&[0xff, 0xff, 0xec]);
289 assert_eq!(KeyTable::read(&map).unwrap().key(60).unwrap().detune(), -20);
290 }
291
292 #[test]
293 fn fields_are_checked_against_their_width() {
294 assert!(Level::new(GAIN_MAX + 1, 0).is_err());
295 assert!(Level::new(0, 1 << 23).is_err());
296 assert!(Level::new(0, -(1 << 23) - 1).is_err());
297 assert_eq!(
298 Level::from_ratio(0.5, -1.0).unwrap(),
299 Level::new(0x08_0000, -256).unwrap()
300 );
301 assert_eq!(Level::from_ratio(1.0, 0.01).unwrap().detune(), 2);
302 assert_eq!(Level::from_ratio(1.0, -0.01).unwrap().detune(), -2);
303 assert!(Level::from_ratio(16.0, 0.0).is_err());
304 assert!(Level::from_ratio(-0.5, 0.0).is_err());
305 assert!(Level::from_ratio(1.0, f64::NAN).is_err());
306 }
307
308 #[test]
309 fn short_maps_and_notes_outside_midi_are_refused() {
310 let map = KeyTable::NEUTRAL.prefix().to_vec();
311 assert!(KeyTable::read(&map[..700]).is_err());
312 assert!(KeyTable::NEUTRAL.key(128).is_err());
313 let mut table = KeyTable::NEUTRAL;
314 assert!(table.set_key(128, Level::NEUTRAL).is_err());
315 }
316}