use std::path::Path;
use std::sync::Arc;
use oxideav_core::{Error, Result};
use super::articulation::Articulation;
use super::sample_voice::{SampleLoopMode, SamplePlayer, SamplePlayerConfig};
use super::wav_pcm::decode_pcm_bytes;
use super::{Instrument, Voice};
pub const RIFF_MAGIC: &[u8; 4] = b"RIFF";
pub const DLS_MAGIC: &[u8; 4] = b"DLS ";
pub const MAX_WAVE_BYTES: usize = 256 * 1024 * 1024;
pub const MAX_RECORDS: usize = 1 << 20;
#[derive(Clone, Debug, Default)]
pub struct DlsBank {
pub info: DlsInfo,
pub version: Option<(u16, u16, u16, u16)>,
pub declared_instrument_count: u32,
pub waves: Vec<DlsSample>,
pub wave_pool_offsets: Vec<u32>,
pub instruments: Vec<DlsInstrumentEntry>,
}
#[derive(Clone, Debug, Default)]
pub struct DlsInfo {
pub name: Option<String>,
pub copyright: Option<String>,
pub engineer: Option<String>,
pub comment: Option<String>,
pub software: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct DlsSample {
pub pool_offset: u32,
pub format_tag: u16,
pub channels: u16,
pub sample_rate: u32,
pub avg_bytes_per_sec: u32,
pub block_align: u16,
pub bits_per_sample: u16,
pub data: Vec<u8>,
pub wsmp: Option<DlsWaveSample>,
}
#[derive(Clone, Debug, Default)]
pub struct DlsWaveSample {
pub unity_note: u16,
pub fine_tune: i16,
pub gain: i32,
pub options: u32,
pub loops: Vec<DlsLoop>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct DlsLoop {
pub loop_type: u32,
pub start: u32,
pub length: u32,
}
#[derive(Clone, Debug, Default)]
pub struct DlsInstrumentEntry {
pub bank: u32,
pub program: u32,
pub name: Option<String>,
pub declared_region_count: u32,
pub articulation: Vec<DlsArticulationBlock>,
pub regions: Vec<DlsRegion>,
}
impl DlsInstrumentEntry {
pub fn is_drum(&self) -> bool {
(self.bank & 0x8000_0000) != 0
}
pub fn bank_msb(&self) -> u8 {
((self.bank >> 8) & 0x7F) as u8
}
pub fn bank_lsb(&self) -> u8 {
(self.bank & 0x7F) as u8
}
pub fn program_number(&self) -> u8 {
(self.program & 0x7F) as u8
}
}
#[derive(Clone, Debug, Default)]
pub struct DlsRegion {
pub key_lo: u16,
pub key_hi: u16,
pub vel_lo: u16,
pub vel_hi: u16,
pub options: u16,
pub key_group: u16,
pub layer: Option<u16>,
pub is_level2: bool,
pub wsmp: Option<DlsWaveSample>,
pub wlnk: Option<DlsWaveLink>,
pub articulation: Vec<DlsArticulationBlock>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct DlsWaveLink {
pub options: u16,
pub phase_group: u16,
pub channel: u32,
pub table_index: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DlsArtKind {
Art1,
Art2,
}
#[derive(Clone, Copy, Debug)]
pub struct DlsArticulationBlock {
pub kind: DlsArtKind,
pub source: u16,
pub control: u16,
pub destination: u16,
pub transform: u16,
pub scale: i32,
}
pub struct DlsInstrument {
name: String,
bank: DlsBank,
}
impl DlsInstrument {
pub fn open(path: &Path) -> Result<Self> {
let bytes = std::fs::read(path)?;
let bank = DlsBank::parse(&bytes)?;
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "dls".to_string());
Ok(Self { name, bank })
}
pub fn parse_bytes(name: impl Into<String>, bytes: &[u8]) -> Result<Self> {
let bank = DlsBank::parse(bytes)?;
Ok(Self {
name: name.into(),
bank,
})
}
pub fn bank(&self) -> &DlsBank {
&self.bank
}
pub fn probe(bytes: &[u8]) -> bool {
is_dls(bytes)
}
}
impl Instrument for DlsInstrument {
fn name(&self) -> &str {
&self.name
}
fn make_voice(
&self,
program: u8,
key: u8,
velocity: u8,
sample_rate: u32,
) -> Result<Box<dyn Voice>> {
let inst = self
.bank
.instruments
.iter()
.find(|i| i.program_number() == program)
.or_else(|| self.bank.instruments.first())
.ok_or_else(|| {
Error::unsupported(format!("DLS '{}': bank has no instruments", self.name,))
})?;
let region = inst
.regions
.iter()
.find(|r| {
u16::from(key) >= r.key_lo
&& u16::from(key) <= r.key_hi
&& u16::from(velocity) >= r.vel_lo
&& u16::from(velocity) <= r.vel_hi
})
.or_else(|| inst.regions.first())
.ok_or_else(|| {
Error::unsupported(format!("DLS '{}': instrument has no regions", self.name,))
})?;
let wlnk = region.wlnk.ok_or_else(|| {
Error::unsupported(format!(
"DLS '{}': region has no wlnk pointing at the wave pool",
self.name,
))
})?;
let pool_offset = self
.bank
.wave_pool_offsets
.get(wlnk.table_index as usize)
.copied()
.ok_or_else(|| {
Error::invalid(format!(
"DLS '{}': wlnk table_index {} out of range (ptbl has {} entries)",
self.name,
wlnk.table_index,
self.bank.wave_pool_offsets.len(),
))
})?;
let wave = self
.bank
.waves
.iter()
.find(|w| w.pool_offset == pool_offset)
.ok_or_else(|| {
Error::invalid(format!(
"DLS '{}': no wave-pool entry at pool_offset {pool_offset}",
self.name,
))
})?;
let pcm = decode_pcm_bytes(
&wave.data,
wave.sample_rate,
wave.channels,
wave.bits_per_sample,
wave.format_tag,
)
.map_err(|e| {
Error::invalid(format!(
"DLS '{}': failed to decode wave PCM: {e}",
self.name,
))
})?;
let wsmp = region.wsmp.as_ref().or(wave.wsmp.as_ref());
let art = Articulation::evaluate(®ion.articulation, &inst.articulation);
let cfg = build_dls_config(
wsmp,
&pcm.samples,
pcm.sample_rate,
key,
velocity,
region,
&art,
);
Ok(Box::new(SamplePlayer::new(cfg, sample_rate)))
}
}
fn build_dls_config(
wsmp: Option<&DlsWaveSample>,
samples: &[f32],
native_rate: u32,
key: u8,
velocity: u8,
region: &DlsRegion,
art: &Articulation,
) -> SamplePlayerConfig {
let unity_note = wsmp.map(|w| w.unity_note as u8).unwrap_or(60);
let fine_tune = wsmp.map(|w| w.fine_tune as i32).unwrap_or(0);
let semitones = key as i32 - unity_note as i32;
let tuning_cents = fine_tune as f64 + art.tuning_cents as f64;
let pitch_ratio = (2.0f64).powf(semitones as f64 / 12.0 + tuning_cents / 1200.0);
let v = velocity as f32 / 127.0;
let mut amplitude = v * v * 0.5 * art.gain_linear;
if let Some(w) = wsmp {
let cb = (w.gain as f32).clamp(-960.0, 480.0);
let db = cb / 10.0;
amplitude *= 10.0f32.powf(db / 20.0);
}
amplitude = amplitude.clamp(0.0, 1.0);
let total_frames = samples.len() as u32;
let (loop_start, loop_end, loop_mode) = match wsmp.and_then(|w| w.loops.first()) {
Some(l) => {
let start = l.start.min(total_frames);
let end = (l.start.saturating_add(l.length)).min(total_frames);
let mode = match l.loop_type {
0 => SampleLoopMode::LoopContinuous,
1 => SampleLoopMode::LoopSustain,
_ => SampleLoopMode::NoLoop,
};
(start, end, mode)
}
None => (0, total_frames, SampleLoopMode::NoLoop),
};
SamplePlayerConfig {
samples: Arc::from(samples.to_vec().into_boxed_slice()),
native_rate,
loop_start,
loop_end,
sample_end: total_frames,
loop_mode,
pitch_ratio,
amplitude,
envelope: art.envelope(),
vibrato: art.vibrato(),
mod_env: art.mod_env(),
filter: art.filter(),
exclusive_class: region.key_group,
}
}
pub fn is_dls(bytes: &[u8]) -> bool {
bytes.len() >= 12 && &bytes[0..4] == RIFF_MAGIC && &bytes[8..12] == DLS_MAGIC
}
struct Cursor<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.pos)
}
fn at_end(&self) -> bool {
self.pos >= self.bytes.len()
}
fn read_tag(&mut self) -> Result<[u8; 4]> {
if self.remaining() < 4 {
return Err(Error::invalid("DLS: truncated chunk tag (needed 4 bytes)"));
}
let mut tag = [0u8; 4];
tag.copy_from_slice(&self.bytes[self.pos..self.pos + 4]);
self.pos += 4;
Ok(tag)
}
fn read_u32_le(&mut self) -> Result<u32> {
if self.remaining() < 4 {
return Err(Error::invalid("DLS: truncated u32"));
}
let v = u32::from_le_bytes(self.bytes[self.pos..self.pos + 4].try_into().unwrap());
self.pos += 4;
Ok(v)
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
if self.remaining() < n {
return Err(Error::invalid(format!(
"DLS: truncated payload (needed {n} bytes, {} remain)",
self.remaining(),
)));
}
let out = &self.bytes[self.pos..self.pos + n];
self.pos += n;
Ok(out)
}
}
fn read_chunk<'a>(c: &mut Cursor<'a>) -> Result<([u8; 4], &'a [u8])> {
let tag = c.read_tag()?;
let size = c.read_u32_le()? as usize;
if size > c.remaining() {
return Err(Error::invalid(format!(
"DLS: chunk '{}' length {size} exceeds {} bytes remaining",
tag_str(&tag),
c.remaining(),
)));
}
let payload = c.take(size)?;
if size % 2 == 1 && c.remaining() >= 1 {
c.pos += 1;
}
Ok((tag, payload))
}
fn tag_str(tag: &[u8; 4]) -> String {
if tag.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
String::from_utf8_lossy(tag).into_owned()
} else {
format!("{:02X}{:02X}{:02X}{:02X}", tag[0], tag[1], tag[2], tag[3])
}
}
fn parse_zstr(payload: &[u8]) -> Option<String> {
let trimmed = payload.split(|b| *b == 0).next()?;
if trimmed.is_empty() {
return None;
}
Some(String::from_utf8_lossy(trimmed).into_owned())
}
impl DlsBank {
pub fn parse(bytes: &[u8]) -> Result<Self> {
if !is_dls(bytes) {
return Err(Error::invalid(
"DLS: file does not start with RIFF/DLS<space> magic",
));
}
let mut outer = Cursor::new(bytes);
let _ = outer.read_tag()?; let total = outer.read_u32_le()? as usize;
if total + 8 > bytes.len() {
return Err(Error::invalid(format!(
"DLS: outer RIFF size {total} exceeds file size {}",
bytes.len() - 8,
)));
}
let body = &bytes[8..8 + total.min(bytes.len() - 8)];
let mut body_cur = Cursor::new(body);
let form = body_cur.read_tag()?;
if &form != DLS_MAGIC {
return Err(Error::invalid(format!(
"DLS: outer form is '{}', expected 'DLS '",
tag_str(&form),
)));
}
let mut bank = DlsBank::default();
while !body_cur.at_end() {
let (tag, payload) = read_chunk(&mut body_cur)?;
match &tag {
b"colh" => bank.declared_instrument_count = parse_colh(payload)?,
b"vers" => bank.version = Some(parse_vers(payload)?),
b"ptbl" => bank.wave_pool_offsets = parse_ptbl(payload)?,
b"LIST" => {
if payload.len() < 4 {
return Err(Error::invalid("DLS: LIST payload < 4 bytes (no list type)"));
}
let mut list_type = [0u8; 4];
list_type.copy_from_slice(&payload[..4]);
let body = &payload[4..];
match &list_type {
b"INFO" => bank.info = parse_info_list(body)?,
b"lins" => bank.instruments = parse_lins_list(body)?,
b"wvpl" => bank.waves = parse_wvpl_list(body)?,
_ => {}
}
}
_ => {}
}
}
Ok(bank)
}
}
fn parse_colh(payload: &[u8]) -> Result<u32> {
if payload.len() < 4 {
return Err(Error::invalid(format!(
"DLS: colh payload {} < 4 bytes",
payload.len(),
)));
}
Ok(u32::from_le_bytes(payload[..4].try_into().unwrap()))
}
fn parse_vers(payload: &[u8]) -> Result<(u16, u16, u16, u16)> {
if payload.len() < 8 {
return Err(Error::invalid(format!(
"DLS: vers payload {} < 8 bytes",
payload.len(),
)));
}
let ms = u32::from_le_bytes(payload[0..4].try_into().unwrap());
let ls = u32::from_le_bytes(payload[4..8].try_into().unwrap());
Ok((
(ms >> 16) as u16, (ms & 0xFFFF) as u16, (ls >> 16) as u16, (ls & 0xFFFF) as u16, ))
}
fn parse_ptbl(payload: &[u8]) -> Result<Vec<u32>> {
if payload.len() < 8 {
return Err(Error::invalid(format!(
"DLS: ptbl payload {} < 8 bytes (cbSize + cCues)",
payload.len(),
)));
}
let cb_size = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
let cues = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
if cues > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: ptbl cues count {cues} exceeds cap {MAX_RECORDS}",
)));
}
let header_len = cb_size.max(8);
if header_len > payload.len() {
return Err(Error::invalid(format!(
"DLS: ptbl cbSize {cb_size} exceeds payload {}",
payload.len(),
)));
}
let cue_start = header_len;
let needed = cues
.checked_mul(4)
.ok_or_else(|| Error::invalid("DLS: ptbl cue count overflow"))?;
if cue_start + needed > payload.len() {
return Err(Error::invalid(format!(
"DLS: ptbl truncated — need {needed} bytes for {cues} cues at offset {cue_start}, \
payload is {}",
payload.len(),
)));
}
let mut out = Vec::with_capacity(cues);
for i in 0..cues {
let off = cue_start + i * 4;
out.push(u32::from_le_bytes(
payload[off..off + 4].try_into().unwrap(),
));
}
Ok(out)
}
fn parse_info_list(body: &[u8]) -> Result<DlsInfo> {
let mut info = DlsInfo::default();
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
let s = parse_zstr(payload);
match &tag {
b"INAM" => info.name = s,
b"ICOP" => info.copyright = s,
b"IENG" => info.engineer = s,
b"ICMT" => info.comment = s,
b"ISFT" => info.software = s,
_ => {}
}
}
Ok(info)
}
fn parse_info_inam(body: &[u8]) -> Result<Option<String>> {
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
if &tag == b"INAM" {
return Ok(parse_zstr(payload));
}
}
Ok(None)
}
fn parse_lins_list(body: &[u8]) -> Result<Vec<DlsInstrumentEntry>> {
let mut out = Vec::new();
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
if &tag != b"LIST" {
continue;
}
if payload.len() < 4 {
continue;
}
let mut sub_type = [0u8; 4];
sub_type.copy_from_slice(&payload[..4]);
if &sub_type != b"ins " {
continue;
}
out.push(parse_ins_list(&payload[4..])?);
if out.len() > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: lins instrument count exceeds cap {MAX_RECORDS}",
)));
}
}
Ok(out)
}
fn parse_ins_list(body: &[u8]) -> Result<DlsInstrumentEntry> {
let mut entry = DlsInstrumentEntry::default();
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
match &tag {
b"insh" => {
let (regions, bank, program) = parse_insh(payload)?;
entry.declared_region_count = regions;
entry.bank = bank;
entry.program = program;
}
b"LIST" => {
if payload.len() < 4 {
continue;
}
let mut sub_type = [0u8; 4];
sub_type.copy_from_slice(&payload[..4]);
let sub_body = &payload[4..];
match &sub_type {
b"INFO" => entry.name = parse_info_inam(sub_body)?,
b"lrgn" => entry.regions = parse_lrgn_list(sub_body)?,
b"lart" => entry
.articulation
.extend(parse_lart_or_lar2(sub_body, false)?),
b"lar2" => entry
.articulation
.extend(parse_lart_or_lar2(sub_body, true)?),
_ => {}
}
}
_ => {}
}
}
Ok(entry)
}
fn parse_insh(payload: &[u8]) -> Result<(u32, u32, u32)> {
if payload.len() < 12 {
return Err(Error::invalid(format!(
"DLS: insh payload {} < 12 bytes (cRegions + ulBank + ulInstrument)",
payload.len(),
)));
}
let regions = u32::from_le_bytes(payload[0..4].try_into().unwrap());
let bank = u32::from_le_bytes(payload[4..8].try_into().unwrap());
let program = u32::from_le_bytes(payload[8..12].try_into().unwrap());
if regions > MAX_RECORDS as u32 {
return Err(Error::invalid(format!(
"DLS: insh declared region count {regions} exceeds cap {MAX_RECORDS}",
)));
}
Ok((regions, bank, program))
}
fn parse_lrgn_list(body: &[u8]) -> Result<Vec<DlsRegion>> {
let mut out = Vec::new();
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
if &tag != b"LIST" {
continue;
}
if payload.len() < 4 {
continue;
}
let mut sub_type = [0u8; 4];
sub_type.copy_from_slice(&payload[..4]);
let is_level2 = match &sub_type {
b"rgn " => false,
b"rgn2" => true,
_ => continue,
};
out.push(parse_rgn_list(&payload[4..], is_level2)?);
if out.len() > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: lrgn region count exceeds cap {MAX_RECORDS}",
)));
}
}
Ok(out)
}
fn parse_rgn_list(body: &[u8], is_level2: bool) -> Result<DlsRegion> {
let mut region = DlsRegion {
is_level2,
..DlsRegion::default()
};
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
match &tag {
b"rgnh" => parse_rgnh(payload, &mut region)?,
b"wsmp" => region.wsmp = Some(parse_wsmp(payload)?),
b"wlnk" => region.wlnk = Some(parse_wlnk(payload)?),
b"LIST" => {
if payload.len() < 4 {
continue;
}
let mut sub_type = [0u8; 4];
sub_type.copy_from_slice(&payload[..4]);
let sub_body = &payload[4..];
match &sub_type {
b"lart" => region
.articulation
.extend(parse_lart_or_lar2(sub_body, false)?),
b"lar2" => region
.articulation
.extend(parse_lart_or_lar2(sub_body, true)?),
_ => {}
}
}
_ => {}
}
}
Ok(region)
}
fn parse_rgnh(payload: &[u8], out: &mut DlsRegion) -> Result<()> {
if payload.len() < 12 {
return Err(Error::invalid(format!(
"DLS: rgnh payload {} < 12 bytes (RangeKey+RangeVel+fusOptions+usKeyGroup)",
payload.len(),
)));
}
out.key_lo = u16::from_le_bytes(payload[0..2].try_into().unwrap());
out.key_hi = u16::from_le_bytes(payload[2..4].try_into().unwrap());
out.vel_lo = u16::from_le_bytes(payload[4..6].try_into().unwrap());
out.vel_hi = u16::from_le_bytes(payload[6..8].try_into().unwrap());
out.options = u16::from_le_bytes(payload[8..10].try_into().unwrap());
out.key_group = u16::from_le_bytes(payload[10..12].try_into().unwrap());
if payload.len() >= 14 {
out.layer = Some(u16::from_le_bytes(payload[12..14].try_into().unwrap()));
}
Ok(())
}
fn parse_wlnk(payload: &[u8]) -> Result<DlsWaveLink> {
if payload.len() < 12 {
return Err(Error::invalid(format!(
"DLS: wlnk payload {} < 12 bytes",
payload.len(),
)));
}
Ok(DlsWaveLink {
options: u16::from_le_bytes(payload[0..2].try_into().unwrap()),
phase_group: u16::from_le_bytes(payload[2..4].try_into().unwrap()),
channel: u32::from_le_bytes(payload[4..8].try_into().unwrap()),
table_index: u32::from_le_bytes(payload[8..12].try_into().unwrap()),
})
}
fn parse_wsmp(payload: &[u8]) -> Result<DlsWaveSample> {
if payload.len() < 20 {
return Err(Error::invalid(format!(
"DLS: wsmp payload {} < 20 bytes (header)",
payload.len(),
)));
}
let cb_size = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
let unity_note = u16::from_le_bytes(payload[4..6].try_into().unwrap());
let fine_tune = i16::from_le_bytes(payload[6..8].try_into().unwrap());
let gain = i32::from_le_bytes(payload[8..12].try_into().unwrap());
let options = u32::from_le_bytes(payload[12..16].try_into().unwrap());
let loops_count = u32::from_le_bytes(payload[16..20].try_into().unwrap()) as usize;
if loops_count > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: wsmp loops count {loops_count} exceeds cap {MAX_RECORDS}",
)));
}
let loop_start = cb_size.max(20);
if loop_start > payload.len() {
return Err(Error::invalid(format!(
"DLS: wsmp cbSize {cb_size} exceeds payload {}",
payload.len(),
)));
}
let mut loops = Vec::with_capacity(loops_count);
let mut off = loop_start;
for i in 0..loops_count {
if off + 16 > payload.len() {
return Err(Error::invalid(format!(
"DLS: wsmp truncated at loop {i} (offset {off}, payload {})",
payload.len(),
)));
}
let loop_cb = u32::from_le_bytes(payload[off..off + 4].try_into().unwrap()) as usize;
let loop_type = u32::from_le_bytes(payload[off + 4..off + 8].try_into().unwrap());
let loop_st = u32::from_le_bytes(payload[off + 8..off + 12].try_into().unwrap());
let loop_len = u32::from_le_bytes(payload[off + 12..off + 16].try_into().unwrap());
loops.push(DlsLoop {
loop_type,
start: loop_st,
length: loop_len,
});
let advance = loop_cb.max(16);
off = off
.checked_add(advance)
.ok_or_else(|| Error::invalid("DLS: wsmp loop offset overflow"))?;
}
Ok(DlsWaveSample {
unity_note,
fine_tune,
gain,
options,
loops,
})
}
fn parse_lart_or_lar2(body: &[u8], is_lar2: bool) -> Result<Vec<DlsArticulationBlock>> {
let mut out = Vec::new();
let mut c = Cursor::new(body);
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
let kind = match (&tag, is_lar2) {
(b"art1", _) => DlsArtKind::Art1,
(b"art2", _) => DlsArtKind::Art2,
(b"cdl ", _) => continue,
_ => continue,
};
let blocks = parse_art(payload, kind)?;
out.extend(blocks);
if out.len() > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: articulation block count exceeds cap {MAX_RECORDS}",
)));
}
}
Ok(out)
}
fn parse_art(payload: &[u8], kind: DlsArtKind) -> Result<Vec<DlsArticulationBlock>> {
if payload.len() < 8 {
return Err(Error::invalid(format!(
"DLS: art1/art2 payload {} < 8 bytes (cbSize + cConnectionBlocks)",
payload.len(),
)));
}
let cb_size = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
let count = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
if count > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: art connection-block count {count} exceeds cap {MAX_RECORDS}",
)));
}
let block_start = cb_size.max(8);
if block_start > payload.len() {
return Err(Error::invalid(format!(
"DLS: art cbSize {cb_size} exceeds payload {}",
payload.len(),
)));
}
let needed = count
.checked_mul(12)
.ok_or_else(|| Error::invalid("DLS: art block count overflow"))?;
if block_start + needed > payload.len() {
return Err(Error::invalid(format!(
"DLS: art truncated — need {needed} bytes for {count} blocks, payload {}",
payload.len(),
)));
}
let mut out = Vec::with_capacity(count);
for i in 0..count {
let off = block_start + i * 12;
out.push(DlsArticulationBlock {
kind,
source: u16::from_le_bytes(payload[off..off + 2].try_into().unwrap()),
control: u16::from_le_bytes(payload[off + 2..off + 4].try_into().unwrap()),
destination: u16::from_le_bytes(payload[off + 4..off + 6].try_into().unwrap()),
transform: u16::from_le_bytes(payload[off + 6..off + 8].try_into().unwrap()),
scale: i32::from_le_bytes(payload[off + 8..off + 12].try_into().unwrap()),
});
}
Ok(out)
}
fn parse_wvpl_list(body: &[u8]) -> Result<Vec<DlsSample>> {
let mut out = Vec::new();
let mut total_data_bytes: usize = 0;
let mut c = Cursor::new(body);
while !c.at_end() {
let pool_offset = c.pos as u32;
let (tag, payload) = read_chunk(&mut c)?;
if &tag != b"LIST" {
continue;
}
if payload.len() < 4 {
continue;
}
let mut sub_type = [0u8; 4];
sub_type.copy_from_slice(&payload[..4]);
if &sub_type != b"wave" {
continue;
}
let mut sample = parse_wave_list(&payload[4..])?;
sample.pool_offset = pool_offset;
total_data_bytes = total_data_bytes.saturating_add(sample.data.len());
if total_data_bytes > MAX_WAVE_BYTES {
return Err(Error::invalid(format!(
"DLS: cumulative wave data {total_data_bytes} exceeds cap {MAX_WAVE_BYTES}",
)));
}
out.push(sample);
if out.len() > MAX_RECORDS {
return Err(Error::invalid(format!(
"DLS: wave pool count exceeds cap {MAX_RECORDS}",
)));
}
}
Ok(out)
}
fn parse_wave_list(body: &[u8]) -> Result<DlsSample> {
let mut sample = DlsSample::default();
let mut c = Cursor::new(body);
let mut got_fmt = false;
let mut got_data = false;
while !c.at_end() {
let (tag, payload) = read_chunk(&mut c)?;
match &tag {
b"fmt " => {
parse_fmt(payload, &mut sample)?;
got_fmt = true;
}
b"data" => {
if payload.len() > MAX_WAVE_BYTES {
return Err(Error::invalid(format!(
"DLS: wave data {} exceeds cap {MAX_WAVE_BYTES}",
payload.len(),
)));
}
sample.data = payload.to_vec();
got_data = true;
}
b"wsmp" => sample.wsmp = Some(parse_wsmp(payload)?),
b"LIST" => {
}
_ => {}
}
}
if !got_fmt {
return Err(Error::invalid("DLS: wave-list missing 'fmt ' chunk"));
}
if !got_data {
return Err(Error::invalid("DLS: wave-list missing 'data' chunk"));
}
Ok(sample)
}
fn parse_fmt(payload: &[u8], out: &mut DlsSample) -> Result<()> {
if payload.len() < 16 {
return Err(Error::invalid(format!(
"DLS: fmt payload {} < 16 bytes",
payload.len(),
)));
}
out.format_tag = u16::from_le_bytes(payload[0..2].try_into().unwrap());
out.channels = u16::from_le_bytes(payload[2..4].try_into().unwrap());
out.sample_rate = u32::from_le_bytes(payload[4..8].try_into().unwrap());
out.avg_bytes_per_sec = u32::from_le_bytes(payload[8..12].try_into().unwrap());
out.block_align = u16::from_le_bytes(payload[12..14].try_into().unwrap());
out.bits_per_sample = u16::from_le_bytes(payload[14..16].try_into().unwrap());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_riff_dls_magic() {
let mut blob = vec![0u8; 32];
blob[0..4].copy_from_slice(b"RIFF");
blob[4..8].copy_from_slice(&24u32.to_le_bytes());
blob[8..12].copy_from_slice(b"DLS ");
assert!(is_dls(&blob));
assert!(DlsInstrument::probe(&blob));
}
#[test]
fn rejects_wrong_magic() {
assert!(!is_dls(b""));
assert!(!DlsInstrument::probe(b""));
let mut blob = vec![0u8; 16];
blob[0..4].copy_from_slice(b"RIFF");
blob[8..12].copy_from_slice(b"WAVE");
assert!(!is_dls(&blob));
}
pub(super) fn build_minimal_dls() -> Vec<u8> {
let pcm = vec![0x80u8, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0x80, 0x80];
let mut fmt = Vec::new();
fmt.extend_from_slice(&1u16.to_le_bytes()); fmt.extend_from_slice(&1u16.to_le_bytes()); fmt.extend_from_slice(&22_050u32.to_le_bytes()); fmt.extend_from_slice(&22_050u32.to_le_bytes()); fmt.extend_from_slice(&1u16.to_le_bytes()); fmt.extend_from_slice(&8u16.to_le_bytes());
let mut wave_wsmp = Vec::new();
wave_wsmp.extend_from_slice(&20u32.to_le_bytes()); wave_wsmp.extend_from_slice(&60u16.to_le_bytes()); wave_wsmp.extend_from_slice(&0i16.to_le_bytes()); wave_wsmp.extend_from_slice(&0i32.to_le_bytes()); wave_wsmp.extend_from_slice(&0u32.to_le_bytes()); wave_wsmp.extend_from_slice(&0u32.to_le_bytes());
let mut wave_body = Vec::from(b"wave" as &[u8]);
push_riff(&mut wave_body, b"fmt ", &fmt);
push_riff(&mut wave_body, b"data", &pcm);
push_riff(&mut wave_body, b"wsmp", &wave_wsmp);
let mut wvpl_body = Vec::from(b"wvpl" as &[u8]);
push_riff(&mut wvpl_body, b"LIST", &wave_body);
let mut ptbl = Vec::new();
ptbl.extend_from_slice(&8u32.to_le_bytes()); ptbl.extend_from_slice(&1u32.to_le_bytes()); ptbl.extend_from_slice(&0u32.to_le_bytes());
let mut colh = Vec::new();
colh.extend_from_slice(&1u32.to_le_bytes());
let mut vers = Vec::new();
vers.extend_from_slice(&((1u32 << 16) | 1u32).to_le_bytes()); vers.extend_from_slice(&0u32.to_le_bytes());
let mut info_body = Vec::from(b"INFO" as &[u8]);
push_riff(&mut info_body, b"INAM", b"TestBank\0");
push_riff(&mut info_body, b"ICOP", b"(c) Test\0");
let mut insh = Vec::new();
insh.extend_from_slice(&1u32.to_le_bytes()); insh.extend_from_slice(&0u32.to_le_bytes()); insh.extend_from_slice(&0u32.to_le_bytes());
let mut rgnh = Vec::new();
rgnh.extend_from_slice(&0u16.to_le_bytes()); rgnh.extend_from_slice(&127u16.to_le_bytes()); rgnh.extend_from_slice(&0u16.to_le_bytes()); rgnh.extend_from_slice(&127u16.to_le_bytes()); rgnh.extend_from_slice(&0u16.to_le_bytes()); rgnh.extend_from_slice(&0u16.to_le_bytes());
let mut rgn_wsmp = Vec::new();
rgn_wsmp.extend_from_slice(&20u32.to_le_bytes());
rgn_wsmp.extend_from_slice(&60u16.to_le_bytes());
rgn_wsmp.extend_from_slice(&0i16.to_le_bytes());
rgn_wsmp.extend_from_slice(&0i32.to_le_bytes());
rgn_wsmp.extend_from_slice(&0u32.to_le_bytes());
rgn_wsmp.extend_from_slice(&0u32.to_le_bytes());
let mut wlnk = Vec::new();
wlnk.extend_from_slice(&0u16.to_le_bytes());
wlnk.extend_from_slice(&0u16.to_le_bytes());
wlnk.extend_from_slice(&1u32.to_le_bytes());
wlnk.extend_from_slice(&0u32.to_le_bytes());
let mut art1 = Vec::new();
art1.extend_from_slice(&8u32.to_le_bytes()); art1.extend_from_slice(&1u32.to_le_bytes()); art1.extend_from_slice(&0x0001u16.to_le_bytes()); art1.extend_from_slice(&0x0000u16.to_le_bytes()); art1.extend_from_slice(&0x0003u16.to_le_bytes()); art1.extend_from_slice(&0x0000u16.to_le_bytes()); art1.extend_from_slice(&0i32.to_le_bytes());
let mut lart_body = Vec::from(b"lart" as &[u8]);
push_riff(&mut lart_body, b"art1", &art1);
let mut rgn_body = Vec::from(b"rgn " as &[u8]);
push_riff(&mut rgn_body, b"rgnh", &rgnh);
push_riff(&mut rgn_body, b"wsmp", &rgn_wsmp);
push_riff(&mut rgn_body, b"wlnk", &wlnk);
push_riff(&mut rgn_body, b"LIST", &lart_body);
let mut lrgn_body = Vec::from(b"lrgn" as &[u8]);
push_riff(&mut lrgn_body, b"LIST", &rgn_body);
let mut ins_info_body = Vec::from(b"INFO" as &[u8]);
push_riff(&mut ins_info_body, b"INAM", b"TestInstrument\0");
let mut ins_body = Vec::from(b"ins " as &[u8]);
push_riff(&mut ins_body, b"insh", &insh);
push_riff(&mut ins_body, b"LIST", &lrgn_body);
push_riff(&mut ins_body, b"LIST", &ins_info_body);
let mut lins_body = Vec::from(b"lins" as &[u8]);
push_riff(&mut lins_body, b"LIST", &ins_body);
let mut body = Vec::from(b"DLS " as &[u8]);
push_riff(&mut body, b"vers", &vers);
push_riff(&mut body, b"colh", &colh);
push_riff(&mut body, b"LIST", &lins_body);
push_riff(&mut body, b"ptbl", &ptbl);
push_riff(&mut body, b"LIST", &wvpl_body);
push_riff(&mut body, b"LIST", &info_body);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
pub(super) fn push_riff(out: &mut Vec<u8>, tag: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(tag);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if payload.len() % 2 == 1 {
out.push(0);
}
}
#[test]
fn parses_minimal_dls_top_level() {
let blob = build_minimal_dls();
let bank = DlsBank::parse(&blob).expect("parse minimal DLS");
assert_eq!(bank.declared_instrument_count, 1);
assert_eq!(bank.version, Some((1, 1, 0, 0)));
assert_eq!(bank.info.name.as_deref(), Some("TestBank"));
assert_eq!(bank.info.copyright.as_deref(), Some("(c) Test"));
assert_eq!(bank.wave_pool_offsets, vec![0]);
}
#[test]
fn parses_minimal_dls_wave_pool() {
let blob = build_minimal_dls();
let bank = DlsBank::parse(&blob).unwrap();
assert_eq!(bank.waves.len(), 1);
let w = &bank.waves[0];
assert_eq!(w.format_tag, 1);
assert_eq!(w.channels, 1);
assert_eq!(w.sample_rate, 22_050);
assert_eq!(w.bits_per_sample, 8);
assert_eq!(w.data.len(), 8);
assert_eq!(w.data[2], 0x90);
assert_eq!(w.pool_offset, 0); let wsmp = w.wsmp.as_ref().expect("wave-level wsmp");
assert_eq!(wsmp.unity_note, 60);
assert!(wsmp.loops.is_empty());
}
#[test]
fn parses_minimal_dls_instrument_table() {
let blob = build_minimal_dls();
let bank = DlsBank::parse(&blob).unwrap();
assert_eq!(bank.instruments.len(), 1);
let ins = &bank.instruments[0];
assert_eq!(ins.declared_region_count, 1);
assert_eq!(ins.bank, 0);
assert_eq!(ins.program, 0);
assert_eq!(ins.bank_msb(), 0);
assert_eq!(ins.bank_lsb(), 0);
assert_eq!(ins.program_number(), 0);
assert!(!ins.is_drum());
assert_eq!(ins.name.as_deref(), Some("TestInstrument"));
assert_eq!(ins.regions.len(), 1);
let r = &ins.regions[0];
assert_eq!(r.key_lo, 0);
assert_eq!(r.key_hi, 127);
assert_eq!(r.vel_lo, 0);
assert_eq!(r.vel_hi, 127);
assert_eq!(r.options, 0);
assert_eq!(r.key_group, 0);
assert!(r.layer.is_none());
assert!(!r.is_level2);
let wlnk = r.wlnk.as_ref().expect("region wlnk");
assert_eq!(wlnk.table_index, 0);
assert_eq!(wlnk.channel, 1);
let rsmp = r.wsmp.as_ref().expect("region wsmp");
assert_eq!(rsmp.unity_note, 60);
}
#[test]
fn parses_minimal_dls_articulation() {
let blob = build_minimal_dls();
let bank = DlsBank::parse(&blob).unwrap();
let ins = &bank.instruments[0];
let r = &ins.regions[0];
assert_eq!(r.articulation.len(), 1);
let a = &r.articulation[0];
assert_eq!(a.kind, DlsArtKind::Art1);
assert_eq!(a.source, 0x0001); assert_eq!(a.control, 0x0000); assert_eq!(a.destination, 0x0003); assert_eq!(a.transform, 0x0000); assert_eq!(a.scale, 0);
}
#[test]
fn parses_dls2_rgnh_with_uslayer() {
let mut payload = Vec::new();
payload.extend_from_slice(&36u16.to_le_bytes()); payload.extend_from_slice(&60u16.to_le_bytes()); payload.extend_from_slice(&0u16.to_le_bytes()); payload.extend_from_slice(&127u16.to_le_bytes()); payload.extend_from_slice(&1u16.to_le_bytes()); payload.extend_from_slice(&3u16.to_le_bytes()); payload.extend_from_slice(&7u16.to_le_bytes());
let mut region = DlsRegion::default();
parse_rgnh(&payload, &mut region).unwrap();
assert_eq!(region.key_lo, 36);
assert_eq!(region.key_hi, 60);
assert_eq!(region.vel_hi, 127);
assert_eq!(region.options, 1);
assert_eq!(region.key_group, 3);
assert_eq!(region.layer, Some(7));
}
#[test]
fn parses_art2_block() {
let mut payload = Vec::new();
payload.extend_from_slice(&8u32.to_le_bytes()); payload.extend_from_slice(&1u32.to_le_bytes()); payload.extend_from_slice(&0x0003u16.to_le_bytes()); payload.extend_from_slice(&0x0000u16.to_le_bytes()); payload.extend_from_slice(&0x0500u16.to_le_bytes()); payload.extend_from_slice(&0x0000u16.to_le_bytes()); payload.extend_from_slice(&50i32.to_le_bytes());
let blocks = parse_art(&payload, DlsArtKind::Art2).unwrap();
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].kind, DlsArtKind::Art2);
assert_eq!(blocks[0].source, 0x0003);
assert_eq!(blocks[0].destination, 0x0500);
assert_eq!(blocks[0].scale, 50);
}
#[test]
fn parses_wsmp_with_one_loop() {
let mut payload = Vec::new();
payload.extend_from_slice(&20u32.to_le_bytes()); payload.extend_from_slice(&60u16.to_le_bytes()); payload.extend_from_slice(&0i16.to_le_bytes()); payload.extend_from_slice(&(-1000i32).to_le_bytes()); payload.extend_from_slice(&0u32.to_le_bytes()); payload.extend_from_slice(&1u32.to_le_bytes()); payload.extend_from_slice(&16u32.to_le_bytes()); payload.extend_from_slice(&0u32.to_le_bytes()); payload.extend_from_slice(&128u32.to_le_bytes()); payload.extend_from_slice(&2048u32.to_le_bytes());
let wsmp = parse_wsmp(&payload).unwrap();
assert_eq!(wsmp.unity_note, 60);
assert_eq!(wsmp.gain, -1000);
assert_eq!(wsmp.loops.len(), 1);
assert_eq!(wsmp.loops[0].loop_type, 0);
assert_eq!(wsmp.loops[0].start, 128);
assert_eq!(wsmp.loops[0].length, 2048);
}
#[test]
fn parse_rejects_non_dls_bytes() {
let mut blob = Vec::from(b"RIFF" as &[u8]);
blob.extend_from_slice(&4u32.to_le_bytes());
blob.extend_from_slice(b"WAVE");
match DlsBank::parse(&blob) {
Err(Error::InvalidData(msg)) => {
assert!(msg.contains("DLS"), "got {msg}");
}
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn parse_rejects_truncated_outer_riff() {
let mut blob = Vec::from(b"RIFF" as &[u8]);
blob.extend_from_slice(&u32::MAX.to_le_bytes());
blob.extend_from_slice(b"DLS ");
match DlsBank::parse(&blob) {
Err(Error::InvalidData(_)) => {}
other => panic!("expected InvalidData, got {other:?}"),
}
}
#[test]
fn open_rejects_non_dls_path() {
let tmp = std::env::temp_dir().join("oxideav-midi-dls-test-not-dls");
std::fs::write(&tmp, b"not a dls file at all").unwrap();
match DlsInstrument::open(&tmp) {
Err(Error::InvalidData(msg)) => {
assert!(msg.contains("DLS"), "got {msg}");
}
Err(other) => panic!("expected InvalidData, got {other:?}"),
Ok(_) => panic!("expected error opening non-DLS file"),
}
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn make_voice_renders_pcm_for_minimal_dls() {
let blob = build_minimal_dls();
let inst = DlsInstrument::parse_bytes("min.dls", &blob).unwrap();
let mut voice = inst
.make_voice(0, 60, 100, 44_100)
.expect("voice generation");
let mut buf = vec![0.0f32; 256];
let _ = voice.render(&mut buf);
let nonzero = buf.iter().filter(|s| s.abs() > 0.001).count();
assert!(nonzero > 0, "expected non-silent output, got {nonzero}");
}
#[test]
fn make_voice_picks_region_by_key_and_velocity() {
let blob = build_two_region_dls_for_voice_test();
let inst = DlsInstrument::parse_bytes("two.dls", &blob).unwrap();
let _v_low = inst.make_voice(5, 30, 100, 44_100).expect("low key");
let _v_high = inst.make_voice(5, 100, 100, 44_100).expect("high key");
}
fn build_two_region_dls_for_voice_test() -> Vec<u8> {
let pcm = vec![0x80u8, 0x90, 0xA0, 0xB0, 0xC0, 0xB0, 0xA0, 0x80];
let mut fmt = Vec::new();
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&22_050u32.to_le_bytes());
fmt.extend_from_slice(&22_050u32.to_le_bytes());
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&8u16.to_le_bytes());
let mut wave_body = Vec::from(b"wave" as &[u8]);
push_riff(&mut wave_body, b"fmt ", &fmt);
push_riff(&mut wave_body, b"data", &pcm);
let mut wvpl_body = Vec::from(b"wvpl" as &[u8]);
push_riff(&mut wvpl_body, b"LIST", &wave_body);
let mut ptbl = Vec::new();
ptbl.extend_from_slice(&8u32.to_le_bytes());
ptbl.extend_from_slice(&1u32.to_le_bytes());
ptbl.extend_from_slice(&0u32.to_le_bytes());
let mut colh = Vec::new();
colh.extend_from_slice(&1u32.to_le_bytes());
let mut vers = Vec::new();
vers.extend_from_slice(&((1u32 << 16) | 1u32).to_le_bytes());
vers.extend_from_slice(&0u32.to_le_bytes());
let mut info_body = Vec::from(b"INFO" as &[u8]);
push_riff(&mut info_body, b"INAM", b"TwoRegion\0");
let mut insh = Vec::new();
insh.extend_from_slice(&2u32.to_le_bytes());
insh.extend_from_slice(&0u32.to_le_bytes());
insh.extend_from_slice(&5u32.to_le_bytes());
let mut rgnh0 = Vec::new();
rgnh0.extend_from_slice(&0u16.to_le_bytes());
rgnh0.extend_from_slice(&59u16.to_le_bytes());
rgnh0.extend_from_slice(&0u16.to_le_bytes());
rgnh0.extend_from_slice(&127u16.to_le_bytes());
rgnh0.extend_from_slice(&0u16.to_le_bytes());
rgnh0.extend_from_slice(&0u16.to_le_bytes());
let mut wlnk0 = Vec::new();
wlnk0.extend_from_slice(&0u16.to_le_bytes());
wlnk0.extend_from_slice(&0u16.to_le_bytes());
wlnk0.extend_from_slice(&1u32.to_le_bytes());
wlnk0.extend_from_slice(&0u32.to_le_bytes());
let mut rgn0_body = Vec::from(b"rgn " as &[u8]);
push_riff(&mut rgn0_body, b"rgnh", &rgnh0);
push_riff(&mut rgn0_body, b"wlnk", &wlnk0);
let mut rgnh1 = Vec::new();
rgnh1.extend_from_slice(&60u16.to_le_bytes());
rgnh1.extend_from_slice(&127u16.to_le_bytes());
rgnh1.extend_from_slice(&0u16.to_le_bytes());
rgnh1.extend_from_slice(&127u16.to_le_bytes());
rgnh1.extend_from_slice(&0u16.to_le_bytes());
rgnh1.extend_from_slice(&0u16.to_le_bytes());
let mut wlnk1 = Vec::new();
wlnk1.extend_from_slice(&0u16.to_le_bytes());
wlnk1.extend_from_slice(&0u16.to_le_bytes());
wlnk1.extend_from_slice(&1u32.to_le_bytes());
wlnk1.extend_from_slice(&0u32.to_le_bytes());
let mut rgn1_body = Vec::from(b"rgn " as &[u8]);
push_riff(&mut rgn1_body, b"rgnh", &rgnh1);
push_riff(&mut rgn1_body, b"wlnk", &wlnk1);
let mut lrgn_body = Vec::from(b"lrgn" as &[u8]);
push_riff(&mut lrgn_body, b"LIST", &rgn0_body);
push_riff(&mut lrgn_body, b"LIST", &rgn1_body);
let mut ins_body = Vec::from(b"ins " as &[u8]);
push_riff(&mut ins_body, b"insh", &insh);
push_riff(&mut ins_body, b"LIST", &lrgn_body);
let mut lins_body = Vec::from(b"lins" as &[u8]);
push_riff(&mut lins_body, b"LIST", &ins_body);
let mut body = Vec::from(b"DLS " as &[u8]);
push_riff(&mut body, b"vers", &vers);
push_riff(&mut body, b"colh", &colh);
push_riff(&mut body, b"LIST", &lins_body);
push_riff(&mut body, b"ptbl", &ptbl);
push_riff(&mut body, b"LIST", &wvpl_body);
push_riff(&mut body, b"LIST", &info_body);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
#[test]
fn drum_bit_decodes_correctly() {
let mut entry = DlsInstrumentEntry {
bank: 0x8000_0000, program: 0,
..DlsInstrumentEntry::default()
};
assert!(entry.is_drum());
entry.bank = 0;
assert!(!entry.is_drum());
entry.bank = 0x8000_0000 | (2 << 8) | 5;
assert_eq!(entry.bank_msb(), 2);
assert_eq!(entry.bank_lsb(), 5);
assert!(entry.is_drum());
}
#[test]
fn open_round_trip_through_disk() {
let tmp = std::env::temp_dir().join("oxideav-midi-dls-test-roundtrip.dls");
let _ = std::fs::remove_file(&tmp);
std::fs::write(&tmp, build_minimal_dls()).unwrap();
let inst = DlsInstrument::open(&tmp).unwrap();
assert_eq!(inst.bank().instruments.len(), 1);
assert_eq!(inst.bank().waves.len(), 1);
assert_eq!(inst.name(), "oxideav-midi-dls-test-roundtrip.dls");
let _ = std::fs::remove_file(&tmp);
}
}