#![forbid(unsafe_code)]
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DwCmdKind {
Vibrato,
Slide,
SlideOn,
SlideOff,
ChannelTranspose,
GlobalTranspose,
NoteStop,
Retrigger,
ResetAll,
SeqAdvance,
SeqPtr,
GlobalParam,
Unknown,
}
impl DwCmdKind {
pub fn arg_bytes(self) -> u8 {
match self {
DwCmdKind::Vibrato => 2,
DwCmdKind::Slide => 2,
DwCmdKind::SlideOn => 2,
DwCmdKind::SlideOff => 0,
DwCmdKind::ChannelTranspose => 1,
DwCmdKind::GlobalTranspose => 1,
DwCmdKind::NoteStop => 0,
DwCmdKind::Retrigger => 0,
DwCmdKind::ResetAll => 0,
DwCmdKind::SeqAdvance => 0,
DwCmdKind::SeqPtr => 2,
DwCmdKind::GlobalParam => 1,
DwCmdKind::Unknown => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DwCmd {
pub kind: DwCmdKind,
pub args: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DwCommandMap {
pub entries: Vec<Option<DwCmd>>,
}
impl DwCommandMap {
pub fn get(&self, b: u8) -> Option<DwCmd> {
if b < 0x80 {
return None;
}
let idx = (b - 0x80) as usize;
self.entries.get(idx).copied().flatten()
}
}
mod sig {
pub const BSET1_A0: [u8; 6] = [0x08, 0xE8, 0x00, 0x01, 0x00, 0x00];
pub const CLRW_20_A0: [u8; 4] = [0x42, 0x68, 0x00, 0x20];
pub const ST_1_A0: [u8; 4] = [0x50, 0xE8, 0x00, 0x01];
pub const SF_1_A0: [u8; 4] = [0x51, 0xE8, 0x00, 0x01];
pub const MOVB_A1_2F: [u8; 4] = [0x11, 0x59, 0x00, 0x2F];
pub const MOVB_A1_03: [u8; 4] = [0x11, 0x59, 0x00, 0x03];
}
fn dmacon_kill_in(region: &[u8]) -> bool {
region.windows(8).any(|c| {
c[0] == 0x33
&& c[1] == 0xFC
&& c[2] == 0x00
&& (c[3] == 0x0F || c[3] == 0x07)
&& c[4] == 0x00
&& c[5] == 0xDF
&& c[6] == 0xF0
&& c[7] == 0x96
})
}
fn classify_handler(payload: &[u8], off: usize) -> DwCmd {
let branch_target: Option<usize> = match payload.get(off..off + 2) {
Some(&[0x60, 0x00]) | Some(&[0x61, 0x00]) | Some(&[0x4E, 0xBA]) => payload
.get(off + 2..off + 4)
.map(|d| off as isize + 2 + i16::from_be_bytes([d[0], d[1]]) as isize),
Some(&[0x61, b]) => Some(off as isize + 2 + (b as i8) as isize),
_ => None,
}
.filter(|&t| t >= 0)
.map(|t| t as usize);
if let Some(t) = branch_target {
let scan_end = (t + 0x60).min(payload.len());
const RETRIG_ENTRY: [u8; 4] = [0x21, 0x49, 0x00, 0x04];
if payload.get(t..t + 4) == Some(&RETRIG_ENTRY) {
return DwCmd {
kind: DwCmdKind::Retrigger,
args: DwCmdKind::Retrigger.arg_bytes(),
};
}
if payload.get(t..scan_end).map(dmacon_kill_in).unwrap_or(false) {
return DwCmd {
kind: DwCmdKind::ResetAll,
args: DwCmdKind::ResetAll.arg_bytes(),
};
}
}
let cap = (off + 0x40).min(payload.len());
let mut end = cap;
let mut j = off;
while j + 1 < cap {
if payload[j] == 0x60 && payload[j + 1] == 0x00 {
end = j;
break;
}
j += 2;
}
let w = match payload.get(off..end) {
Some(s) => s,
None => return DwCmd { kind: DwCmdKind::Unknown, args: 0 },
};
if dmacon_kill_in(w) {
return DwCmd {
kind: DwCmdKind::ResetAll,
args: DwCmdKind::ResetAll.arg_bytes(),
};
}
let has = |needle: &[u8]| -> bool { w.windows(needle.len()).any(|c| c == needle) };
let starts = |needle: &[u8]| -> bool { w.starts_with(needle) };
let kind = if starts(&sig::CLRW_20_A0) {
DwCmdKind::Slide
} else if starts(&sig::SF_1_A0) {
DwCmdKind::SlideOff
} else if starts(&sig::ST_1_A0) {
DwCmdKind::SlideOn
} else if has(&sig::BSET1_A0) {
DwCmdKind::Vibrato
} else if starts(&sig::MOVB_A1_2F) || starts(&sig::MOVB_A1_03) {
DwCmdKind::ChannelTranspose
} else if starts(&[0x11, 0x59, 0x00, 0x08]) {
DwCmdKind::SeqPtr
} else if starts(&[0x30, 0x28, 0x00, 0x0A]) {
DwCmdKind::SeqAdvance
} else if starts(&[0x21, 0x49, 0x00, 0x04]) {
DwCmdKind::Retrigger
} else if starts(&[0x17, 0x59]) {
DwCmdKind::GlobalTranspose
} else if has(&[0x2D, 0x7A]) {
DwCmdKind::NoteStop
} else if has(&[0x37, 0x40]) {
DwCmdKind::GlobalParam
} else {
DwCmdKind::Unknown
};
DwCmd { kind, args: kind.arg_bytes() }
}
fn find_dispatch_table(payload: &[u8]) -> Option<usize> {
let mut i = 0;
while i + 12 <= payload.len() {
if payload[i] == 0x45
&& payload[i + 1] == 0xFA
&& payload[i + 4] == 0x34
&& payload[i + 5] == 0x72
&& payload[i + 6] == 0x00
&& payload[i + 7] == 0x00
&& payload[i + 8] == 0x4E
&& payload[i + 9] == 0xF3
&& payload[i + 10] == 0xA0
&& payload[i + 11] == 0x00
{
let disp = i16::from_be_bytes([payload[i + 2], payload[i + 3]]) as isize;
let table = (i as isize) + 2 + disp;
if table >= 0 && (table as usize) < payload.len() {
return Some(table as usize);
}
}
i += 2;
}
None
}
pub fn detect_command_map(payload: &[u8], a3_base: isize) -> Option<DwCommandMap> {
let table = find_dispatch_table(payload)?;
let mut entries: Vec<Option<DwCmd>> = Vec::with_capacity(16);
let mut any = false;
for k in 0..16usize {
let eoff = table + k * 2;
let raw = match payload.get(eoff..eoff + 2) {
Some(b) => u16::from_be_bytes([b[0], b[1]]),
None => break,
};
if raw == 0 {
entries.push(None);
continue;
}
let handler = raw as isize + a3_base;
if handler < 0 || (handler as usize) >= payload.len() {
entries.push(None);
continue;
}
let cmd = classify_handler(payload, handler as usize);
any = true;
entries.push(Some(cmd));
}
if !any {
return None;
}
Some(DwCommandMap { entries })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_canonical_slide() {
let h = [
0x42, 0x68, 0x00, 0x20, 0x11, 0x59, 0x00, 0x14, 0x11, 0x59, 0x00, 0x15, 0x08, 0xE8,
0x00, 0x01, 0x00, 0x00, 0x60, 0x00, 0xFD, 0xB2,
];
let c = classify_handler(&h, 0);
assert_eq!(c.kind, DwCmdKind::Slide);
assert_eq!(c.args, 2);
}
#[test]
fn classifies_slide_on_off() {
let on = [
0x50, 0xE8, 0x00, 0x01, 0x11, 0x59, 0x00, 0x2C, 0x11, 0x59, 0x00, 0x2E, 0x42, 0x28,
0x00, 0x2D, 0x60, 0x00, 0xFD, 0x5A,
];
let c = classify_handler(&on, 0);
assert_eq!(c.kind, DwCmdKind::SlideOn);
assert_eq!(c.args, 2);
let off = [0x51, 0xE8, 0x00, 0x01, 0x60, 0x00, 0xFD, 0x52];
let c = classify_handler(&off, 0);
assert_eq!(c.kind, DwCmdKind::SlideOff);
assert_eq!(c.args, 0);
}
#[test]
fn classifies_transposes() {
let ch = [0x11, 0x59, 0x00, 0x2F, 0x60, 0x00, 0xFD, 0x6E];
let c = classify_handler(&ch, 0);
assert_eq!(c.kind, DwCmdKind::ChannelTranspose);
assert_eq!(c.args, 1);
let ch03 = [0x11, 0x59, 0x00, 0x03, 0x60, 0x00, 0xFD, 0x6E];
let c = classify_handler(&ch03, 0);
assert_eq!(c.kind, DwCmdKind::ChannelTranspose);
assert_eq!(c.args, 1);
let gl = [0x17, 0x59, 0x09, 0xA6, 0x60, 0x00, 0xFD, 0x76];
let c = classify_handler(&gl, 0);
assert_eq!(c.kind, DwCmdKind::GlobalTranspose);
assert_eq!(c.args, 1);
}
#[test]
fn classifies_note_stop() {
let h = [
0x31, 0x68, 0x00, 0x1C, 0x00, 0x1E, 0x21, 0x49, 0x00, 0x04, 0x29, 0x7A, 0x00, 0x92,
0x00, 0x00, 0x39, 0x7C, 0x00, 0x20, 0x00, 0x04, 0x4A, 0x2C, 0x00, 0x0B, 0x66, 0x0C,
0x2D, 0x7A, 0x00, 0x80, 0x00, 0xA0,
];
let c = classify_handler(&h, 0);
assert_eq!(c.kind, DwCmdKind::NoteStop);
assert_eq!(c.args, 0);
}
#[test]
fn arg_counts_match_handcheck() {
assert_eq!(DwCmdKind::Vibrato.arg_bytes(), 2); assert_eq!(DwCmdKind::ChannelTranspose.arg_bytes(), 1); assert_eq!(DwCmdKind::SlideOn.arg_bytes(), 2); assert_eq!(DwCmdKind::SlideOff.arg_bytes(), 0); }
#[test]
fn detects_bubble_bobble_map() {
let home = match std::env::var("HOME") {
Ok(h) => h,
Err(_) => return,
};
let path = std::path::Path::new(&home).join("Music/dw/bubble bobble.dw");
let payload = match std::fs::read(&path) {
Ok(p) => p,
Err(_) => return,
};
let layout = super::super::detect::detect(&payload).expect("bubble bobble must parse");
let io = layout.init_offset;
let disp = i16::from_be_bytes([payload[io + 2], payload[io + 3]]) as isize;
let a3_base = (io as isize + 2) + disp;
assert_eq!(a3_base, -0x442, "bubble bobble A3 base");
let map = detect_command_map(&payload, a3_base).expect("jump-table dispatcher");
assert_eq!(map.get(0x80).map(|c| c.kind), Some(DwCmdKind::SeqAdvance));
assert_eq!(map.get(0x81).map(|c| c.kind), Some(DwCmdKind::Slide));
assert_eq!(map.get(0x82).map(|c| c.kind), Some(DwCmdKind::NoteStop));
assert_eq!(map.get(0x85).map(|c| c.kind), Some(DwCmdKind::GlobalTranspose));
assert_eq!(map.get(0x86).map(|c| c.kind), Some(DwCmdKind::SlideOn));
assert_eq!(map.get(0x87).map(|c| c.kind), Some(DwCmdKind::SlideOff));
assert_eq!(map.get(0x88).map(|c| c.kind), Some(DwCmdKind::ChannelTranspose));
assert_eq!(map.get(0x89).map(|c| c.kind), Some(DwCmdKind::SeqPtr));
assert_eq!(map.get(0x8A).map(|c| c.kind), Some(DwCmdKind::GlobalParam));
assert_eq!(map.get(0x81).map(|c| c.args), Some(2)); assert_eq!(map.get(0x86).map(|c| c.args), Some(2)); assert_eq!(map.get(0x87).map(|c| c.args), Some(0)); assert_eq!(map.get(0x88).map(|c| c.args), Some(1)); }
}