use crate::error::ParseError;
pub const COUNT_AT: usize = 785;
pub const RECORDS_AT: usize = COUNT_AT + 1;
pub const RECORD_LEN: usize = 15;
const STROKE_ID: usize = 2;
const TOP_NOTE: usize = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Zone {
pub top_note: u8,
pub stroke_id: u8,
}
pub fn count(map: &[u8]) -> Result<usize, ParseError> {
map.get(COUNT_AT).map(|n| *n as usize).ok_or_else(|| {
ParseError::AssertFail(format!(
"map section is {} bytes, too short for a zone table",
map.len()
))
})
}
pub fn read(map: &[u8]) -> Result<Vec<Zone>, ParseError> {
let n = count(map)?;
let need = RECORDS_AT + n * RECORD_LEN;
if map.len() < need {
return Err(ParseError::AssertFail(format!(
"map declares {n} zones, needing {need} bytes, but the section is {}",
map.len()
)));
}
Ok((0..n)
.map(|i| {
let r = &map[RECORDS_AT + i * RECORD_LEN..][..RECORD_LEN];
Zone {
top_note: r[TOP_NOTE],
stroke_id: r[STROKE_ID],
}
})
.collect())
}
pub fn set_top_note(map: &mut [u8], index: usize, note: u8) -> Result<(), ParseError> {
let n = count(map)?;
if index >= n {
return Err(ParseError::AssertFail(format!(
"zone {index} out of range, the instrument has {n}"
)));
}
map[RECORDS_AT + index * RECORD_LEN + TOP_NOTE] = note;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ZoneV3 {
pub stroke_gid: u32,
pub root_key: u8,
pub top_note: u8,
pub low_note: Option<u8>,
}
const WIDE_ROOT: usize = 0;
const WIDE_TOP: usize = 1;
const WIDE_LOW: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Field {
Root,
Top,
Low,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Wide {
V12,
V14,
V21,
}
impl Wide {
pub fn from_version(map_version: u32) -> Result<Wide, ParseError> {
match map_version {
12 => Ok(Wide::V12),
14 => Ok(Wide::V14),
21 => Ok(Wide::V21),
v => Err(ParseError::AssertFail(format!(
"map section version {v} has no zone layout derived from a specimen"
))),
}
}
pub const fn record_len(self) -> usize {
match self {
Wide::V12 => 11,
Wide::V14 | Wide::V21 => 16,
}
}
pub const fn gid_at(self) -> usize {
match self {
Wide::V12 => 5,
Wide::V14 | Wide::V21 => 8,
}
}
pub const fn stores_low(self) -> bool {
matches!(self, Wide::V14 | Wide::V21)
}
const fn field_at(self, field: Field) -> Option<usize> {
match field {
Field::Root => Some(WIDE_ROOT),
Field::Top => Some(WIDE_TOP),
Field::Low if self.stores_low() => Some(WIDE_LOW),
Field::Low => None,
}
}
pub const fn has_key_map(self) -> bool {
matches!(self, Wide::V21)
}
}
const KEY_TABLE_AT: usize = 6;
const KEY_STRIDE: usize = 10;
const KEY_QUAD_AT: usize = 6;
const KEYS: usize = 128;
const KEY_FLOOR: u8 = 17;
const PARTNER_UP: u8 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyMap {
Absent,
Neutral,
Populated,
}
fn ladder(zones: &[ZoneV3]) -> Result<Vec<(u8, u8, u8)>, ParseError> {
let mut out = zones
.iter()
.map(|z| {
let low = z.low_note.ok_or_else(|| {
ParseError::AssertFail(
"a per-key table needs each zone's low note, and this layout stores none"
.into(),
)
})?;
Ok((z.root_key, low, z.top_note))
})
.collect::<Result<Vec<_>, ParseError>>()?;
out.sort_by_key(|&(root, _, _)| root);
Ok(out)
}
fn span(ladder: &[(u8, u8, u8)]) -> Option<(u8, u8)> {
Some((ladder.first()?.1.max(KEY_FLOOR), ladder.last()?.2))
}
fn partners(ladder: &[(u8, u8, u8)], key: u8) -> (u8, u8) {
let identity = (key, key);
let Some((lo, hi)) = span(ladder) else {
return identity;
};
if !(lo..=hi).contains(&key) {
return identity;
}
let Some(claim) = ladder
.iter()
.enumerate()
.filter(|&(j, z)| if j == 0 { lo } else { z.1 } <= key)
.map(|(j, _)| j)
.max()
else {
return identity;
};
let root = ladder[claim].0;
let mut roots: Vec<u8> = ladder.iter().map(|&(r, _, _)| r).collect();
roots.dedup();
let below: Vec<u8> = roots
.iter()
.copied()
.filter(|&r| r < root && root - r <= PARTNER_UP)
.collect();
let above: Vec<u8> = roots.iter().copied().filter(|&r| r > root).collect();
let nearest = |set: &[u8]| set.iter().copied().min_by_key(|&r| (r.abs_diff(root), r));
let eligible: Vec<u8> = below.iter().chain(&above).copied().collect();
let Some(a) = nearest(&eligible) else {
return identity;
};
let b = if a < root {
let rest: Vec<u8> = eligible.iter().copied().filter(|&r| r != a).collect();
nearest(&rest).unwrap_or(a)
} else if let Some(&highest_below) = below.last() {
highest_below
} else {
above
.iter()
.copied()
.filter(|&r| r != a)
.min_by_key(|&r| r - root)
.unwrap_or(a)
};
(a, b)
}
const MAX_TAIL: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Table {
pub wide: Wide,
at: usize,
count: usize,
}
impl Table {
pub fn locate(
map_version: u32,
map: &[u8],
strokes: &[(u32, u8)],
) -> Result<Table, ParseError> {
let wide = Wide::from_version(map_version)?;
let count = strokes.len();
let mut first = None;
for tail in 0..=MAX_TAIL {
let table = count
.checked_mul(wide.record_len())
.and_then(|bytes| bytes.checked_add(tail))
.and_then(|len| map.len().checked_sub(len))
.filter(|&at| at >= 1)
.map(|at| Table { wide, at, count });
let attempt = match table {
Some(table) => table.read(map, strokes).map(|_| table),
None => Err(ParseError::AssertFail(format!(
"map section is {} bytes, too short for {count} zone records",
map.len()
))),
};
match attempt {
Ok(table) => return Ok(table),
Err(e) => first.get_or_insert(e),
};
}
Err(first.unwrap_or_else(|| {
ParseError::AssertFail(format!("map section is {} bytes", map.len()))
}))
}
pub fn count(&self) -> usize {
self.count
}
pub fn read(&self, map: &[u8], strokes: &[(u32, u8)]) -> Result<Vec<ZoneV3>, ParseError> {
self.fits(map)?;
if map[self.at - 1] as usize != self.count {
return Err(ParseError::AssertFail(format!(
"zone count {} does not match the {} strokes",
map[self.at - 1],
self.count
)));
}
(0..self.count)
.map(|i| {
let r = &map[self.at + i * self.wide.record_len()..][..self.wide.record_len()];
let gid_at = self.wide.gid_at();
let gid = u32::from_be_bytes(r[gid_at..gid_at + 4].try_into().unwrap());
let root = strokes.iter().find(|(g, _)| *g == gid).map(|(_, r)| *r);
match root {
Some(root) if root == r[WIDE_ROOT] => Ok(ZoneV3 {
stroke_gid: gid,
root_key: r[WIDE_ROOT],
top_note: r[WIDE_TOP],
low_note: self.wide.stores_low().then(|| r[WIDE_LOW]),
}),
Some(root) => Err(ParseError::AssertFail(format!(
"zone {i} carries root {} but its stroke {gid} holds {root}",
r[WIDE_ROOT]
))),
None => Err(ParseError::AssertFail(format!(
"zone {i} references stroke {gid}, which the body does not hold"
))),
}
})
.collect()
}
fn fits(&self, map: &[u8]) -> Result<(), ParseError> {
self.count
.checked_mul(self.wide.record_len())
.and_then(|bytes| bytes.checked_add(self.at))
.filter(|&end| self.at >= 1 && end <= map.len())
.map(|_| ())
.ok_or_else(|| {
ParseError::AssertFail(format!(
"map section is {} bytes, too short for {} zone records at {}",
map.len(),
self.count,
self.at
))
})
}
pub fn set(
&self,
map: &mut [u8],
index: usize,
field: Field,
note: u8,
) -> Result<(), ParseError> {
self.fits(map)?;
if index >= self.count {
return Err(ParseError::AssertFail(format!(
"zone {index} out of range, the instrument has {}",
self.count
)));
}
let at = self.wide.field_at(field).ok_or_else(|| {
ParseError::AssertFail(
"this map layout stores no low note: a zone reaches down to one above \
the next-lower zone's top"
.into(),
)
})?;
map[self.at + index * self.wide.record_len() + at] = note;
Ok(())
}
pub fn key_map(&self, map: &[u8]) -> Result<KeyMap, ParseError> {
if !self.wide.has_key_map() {
return Ok(KeyMap::Absent);
}
let mut neutral = true;
for key in 0..KEYS {
let at = KEY_TABLE_AT + key * KEY_STRIDE + KEY_QUAD_AT;
let quad = map.get(at..at + 4).ok_or_else(|| {
ParseError::AssertFail(format!(
"map section is {} bytes, too short for a per-key table",
map.len()
))
})?;
neutral &= quad == [key as u8; 4];
}
Ok(if neutral {
KeyMap::Neutral
} else {
KeyMap::Populated
})
}
pub fn plan_key_map(
&self,
map: &[u8],
zones: &[ZoneV3],
) -> Result<Vec<(usize, [u8; 4])>, ParseError> {
if self.key_map(map)? != KeyMap::Populated {
return Ok(Vec::new());
}
let ladder = ladder(zones)?;
let Some((lo, hi)) = span(&ladder) else {
return Ok(Vec::new());
};
let mut plan = Vec::new();
for key in 0..KEYS {
let k = key as u8;
let at = KEY_TABLE_AT + key * KEY_STRIDE + KEY_QUAD_AT;
let quad = map.get(at..at + 4).ok_or_else(|| {
ParseError::AssertFail(format!(
"map section is {} bytes, too short for a per-key table",
map.len()
))
})?;
if !(lo..=hi).contains(&k) && quad == [0, 0, 0, k] {
continue;
}
let (a, b) = partners(&ladder, k);
plan.push((at, [a, b, a, k]));
}
Ok(plan)
}
pub fn validate_key_map(&self, map: &[u8], zones: &[ZoneV3]) -> Result<(), ParseError> {
for (at, expected) in self.plan_key_map(map, zones)? {
let found = &map[at..at + expected.len()];
if found != expected {
return Err(ParseError::AssertFail(format!(
"per-key record at byte {at} does not match the zone layout"
)));
}
}
Ok(())
}
}
pub fn read_v3(
map_version: u32,
map: &[u8],
strokes: &[(u32, u8)],
) -> Result<Vec<ZoneV3>, ParseError> {
Table::locate(map_version, map, strokes)?.read(map, strokes)
}
pub fn derive_top_notes(roots_high_to_low: &[u8]) -> Vec<u8> {
roots_high_to_low
.iter()
.enumerate()
.map(|(i, &root)| {
if i == 0 {
root.saturating_add(24).min(127)
} else {
let above = roots_high_to_low[i - 1];
(u16::from(root) + u16::from(above))
.div_ceil(2)
.saturating_sub(1)
.min(127) as u8
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn table(tops: &[u8]) -> Vec<u8> {
table_with_ids(tops, &(1..=tops.len() as u8).rev().collect::<Vec<_>>())
}
fn table_with_ids(tops: &[u8], ids: &[u8]) -> Vec<u8> {
let mut m = vec![0u8; RECORDS_AT + tops.len() * RECORD_LEN];
m[COUNT_AT] = tops.len() as u8;
for (i, (&t, &id)) in tops.iter().zip(ids).enumerate() {
let r = RECORDS_AT + i * RECORD_LEN;
m[r + STROKE_ID] = id;
m[r + TOP_NOTE] = t;
}
m
}
#[test]
fn reads_the_table() {
let zones = read(&table(&[96, 65, 53])).unwrap();
assert_eq!(zones.len(), 3);
assert_eq!(zones[0].top_note, 96);
assert_eq!(zones[2].top_note, 53);
assert_eq!(zones[0].stroke_id, 3);
assert_eq!(zones[2].stroke_id, 1);
}
#[test]
fn stroke_ids_need_not_be_a_countdown() {
let zones = read(&table_with_ids(
&[108, 90, 77, 66, 60, 53],
&[13, 12, 6, 9, 5, 25],
))
.unwrap();
assert_eq!(
zones.iter().map(|z| z.stroke_id).collect::<Vec<_>>(),
[13, 12, 6, 9, 5, 25]
);
}
#[test]
fn set_top_note_moves_exactly_one_byte() {
let before = table(&[96, 65, 53]);
let mut after = before.clone();
set_top_note(&mut after, 1, 60).unwrap();
let differing: Vec<_> = (0..before.len())
.filter(|&i| before[i] != after[i])
.collect();
assert_eq!(differing, vec![RECORDS_AT + RECORD_LEN + TOP_NOTE]);
assert_eq!(read(&after).unwrap()[1].top_note, 60);
}
#[test]
fn out_of_range_zone_is_rejected() {
let mut m = table(&[96, 65]);
assert!(set_top_note(&mut m, 2, 60).is_err());
}
#[test]
fn short_map_is_rejected() {
assert!(read(&[0u8; 16]).is_err());
let mut m = table(&[96, 65]);
m[COUNT_AT] = 9; assert!(read(&m).is_err());
}
#[test]
fn derived_ranges_match_the_editor() {
assert_eq!(derive_top_notes(&[72, 60, 48]), vec![96, 65, 53]);
assert_eq!(derive_top_notes(&[60, 48]), vec![84, 53]);
assert_eq!(derive_top_notes(&[60]), vec![84]);
}
#[test]
fn derived_ranges_handle_an_odd_gap() {
assert_eq!(derive_top_notes(&[61, 60]), vec![85, 60]);
}
#[test]
fn derived_ranges_stay_in_the_midi_domain() {
assert_eq!(derive_top_notes(&[127]), vec![127]);
assert_eq!(derive_top_notes(&[0, 0]), vec![24, 0]);
assert_eq!(derive_top_notes(&[255, 255]), vec![127, 127]);
}
fn wide_map(version: u32, zones: &[(u32, u8, u8, u8)], tail: usize) -> Vec<u8> {
let wide = Wide::from_version(version).unwrap();
let preamble = match wide {
Wide::V21 => KEY_TABLE_AT + KEYS * KEY_STRIDE + 6 + 26,
Wide::V12 | Wide::V14 => 6 + 128 * 6,
};
let mut m = vec![0u8; preamble + 1 + zones.len() * wide.record_len() + tail];
if wide.has_key_map() {
for key in 0..KEYS {
let r = KEY_TABLE_AT + key * KEY_STRIDE;
m[r..r + 3].copy_from_slice(&[0x10, 0, 0]);
m[r + KEY_QUAD_AT..][..4].fill(key as u8);
}
}
m[preamble] = zones.len() as u8;
for (i, &(gid, root, top, low)) in zones.iter().enumerate() {
let r = preamble + 1 + i * wide.record_len();
m[r + WIDE_ROOT] = root;
m[r + WIDE_TOP] = top;
if wide.stores_low() {
m[r + WIDE_LOW] = low;
}
m[r + wide.gid_at()..][..4].copy_from_slice(&gid.to_be_bytes());
}
m
}
fn strokes(zones: &[(u32, u8, u8, u8)]) -> Vec<(u32, u8)> {
zones.iter().map(|&(gid, root, _, _)| (gid, root)).collect()
}
#[test]
fn a_wide_table_reads_behind_an_unmodelled_tail() {
for (version, tail) in [(12, 0), (14, 1), (21, 2), (21, 6)] {
let zones = [(9u32, 60u8, 84u8, 48u8), (22, 72, 108, 85)];
let map = wide_map(version, &zones, tail);
let read = read_v3(version, &map, &strokes(&zones)).unwrap_or_else(|e| {
panic!("map v{version} with a {tail}-byte tail: {e}");
});
assert_eq!(read.len(), 2);
assert_eq!(read[0].root_key, 60);
assert_eq!(read[0].top_note, 84);
assert_eq!(read[1].stroke_gid, 22);
assert_eq!(
read[0].low_note,
(version != 12).then_some(48),
"map v{version} low note"
);
}
}
#[test]
fn an_undescribed_map_version_is_refused() {
assert!(Wide::from_version(13).is_err());
assert!(Wide::from_version(0).is_err());
}
#[test]
fn wide_setters_move_exactly_one_byte() {
for version in [12, 14, 21] {
let zones = [(9u32, 60u8, 84u8, 48u8), (22, 72, 108, 85)];
let before = wide_map(version, &zones, 1);
let table = Table::locate(version, &before, &strokes(&zones)).unwrap();
for field in [Field::Top, Field::Root] {
let mut after = before.clone();
table.set(&mut after, 1, field, 55).unwrap();
let moved: Vec<_> = (0..before.len())
.filter(|&i| before[i] != after[i])
.collect();
assert_eq!(moved.len(), 1, "map v{version} {field:?}: {moved:?}");
}
}
}
#[test]
fn a_low_note_is_refused_where_zones_tile() {
let zones = [(9u32, 60u8, 84u8, 0u8)];
let mut map = wide_map(12, &zones, 0);
let table = Table::locate(12, &map, &strokes(&zones)).unwrap();
assert!(table.set(&mut map, 0, Field::Low, 48).is_err());
let zones = [(9u32, 60u8, 84u8, 48u8)];
let mut map = wide_map(14, &zones, 1);
let table = Table::locate(14, &map, &strokes(&zones)).unwrap();
table.set(&mut map, 0, Field::Low, 50).unwrap();
assert_eq!(
read_v3(14, &map, &strokes(&zones)).unwrap()[0].low_note,
Some(50)
);
}
#[test]
fn a_zone_past_the_table_is_refused() {
let zones = [(9u32, 60u8, 84u8, 48u8)];
let mut map = wide_map(21, &zones, 2);
let table = Table::locate(21, &map, &strokes(&zones)).unwrap();
assert!(table.set(&mut map, 1, Field::Top, 60).is_err());
}
#[test]
fn a_record_disagreeing_with_its_stroke_is_refused() {
let zones = [(9u32, 60u8, 84u8, 48u8)];
let mut map = wide_map(14, &zones, 1);
let table = Table::locate(14, &map, &strokes(&zones)).unwrap();
table.set(&mut map, 0, Field::Root, 48).unwrap();
assert!(read_v3(14, &map, &strokes(&zones)).is_err());
assert!(read_v3(14, &map, &[(9, 48)]).is_ok());
}
#[test]
fn a_neutral_key_map_is_told_from_a_populated_one() {
let zones = [(9u32, 60u8, 84u8, 48u8)];
let mut map = wide_map(21, &zones, 2);
let table = Table::locate(21, &map, &strokes(&zones)).unwrap();
assert_eq!(table.key_map(&map).unwrap(), KeyMap::Neutral);
map[KEY_TABLE_AT + 40 * KEY_STRIDE + KEY_QUAD_AT] = 55;
assert_eq!(table.key_map(&map).unwrap(), KeyMap::Populated);
for version in [12, 14] {
let map = wide_map(version, &zones, if version == 12 { 0 } else { 1 });
let table = Table::locate(version, &map, &strokes(&zones)).unwrap();
assert_eq!(table.key_map(&map).unwrap(), KeyMap::Absent);
}
}
#[test]
fn a_neutral_key_map_survives_an_edit() {
let zones = [(9u32, 60u8, 84u8, 48u8), (22, 72, 108, 85)];
let map = wide_map(21, &zones, 2);
let table = Table::locate(21, &map, &strokes(&zones)).unwrap();
let read = table.read(&map, &strokes(&zones)).unwrap();
assert!(table.plan_key_map(&map, &read).unwrap().is_empty());
}
#[test]
fn an_unknown_populated_key_map_is_refused() {
let zones = [(9u32, 60u8, 84u8, 48u8)];
let mut map = wide_map(21, &zones, 2);
let table = Table::locate(21, &map, &strokes(&zones)).unwrap();
let read = table.read(&map, &strokes(&zones)).unwrap();
map[KEY_TABLE_AT + 40 * KEY_STRIDE + KEY_QUAD_AT] = 55;
assert!(table.validate_key_map(&map, &read).is_err());
}
fn kalimba() -> Vec<(u8, u8, u8)> {
vec![
(47, 0, 49),
(51, 50, 53),
(55, 54, 57),
(59, 58, 60),
(62, 61, 64),
(66, 65, 68),
(71, 69, 73),
(75, 74, 77),
(80, 78, 82),
(84, 83, 86),
(88, 87, 90),
(92, 91, 94),
(96, 95, 97),
(99, 98, 100),
(102, 101, 103),
(105, 104, 108),
]
}
#[test]
fn the_partner_law_matches_a_populated_table() {
let zs = kalimba();
for (key, want) in [
(17, (51, 55)),
(49, (51, 55)),
(50, (55, 59)),
(58, (62, 66)),
(61, (59, 66)),
(64, (59, 66)),
(104, (102, 102)),
(16, (16, 16)),
(109, (109, 109)),
(0, (0, 0)),
(127, (127, 127)),
] {
assert_eq!(partners(&zs, key), want, "key {key}");
}
}
#[test]
fn the_second_partner_straddles_the_root() {
let zs = vec![(61, 17, 62), (64, 63, 65), (66, 66, 68), (71, 69, 73)];
assert_eq!(partners(&zs, 64), (66, 61));
}
#[test]
fn a_lone_zone_names_nobody() {
let zs = vec![(60, 17, 84)];
for key in [17, 60, 84] {
assert_eq!(partners(&zs, key), (key, key), "key {key}");
}
}
#[test]
fn the_span_starts_at_the_floor() {
let zs = vec![(47, 0, 49), (51, 50, 53)];
assert_eq!(span(&zs), Some((KEY_FLOOR, 53)));
assert_eq!(partners(&zs, 16), (16, 16));
assert_eq!(partners(&zs, 17), (51, 51));
assert_eq!(partners(&zs, 54), (54, 54));
}
#[test]
fn a_truncated_key_map_is_refused() {
let zones = [(9u32, 60u8, 84u8, 48u8)];
let map = wide_map(21, &zones, 2);
let table = Table::locate(21, &map, &strokes(&zones)).unwrap();
assert!(table.key_map(&[0u8; 32]).is_err());
}
}