#![forbid(unsafe_code)]
use alloc::format;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use alloc::collections::BTreeMap;
use super::detect::{detect, DwLayout, DwVariant};
use super::header::{
DwArpeggio, DwPositionList, DwSample, DwSubSong, DwTrack, DwVolumeEnvelope, DW_NUM_CHANNELS,
};
use super::tables::PeriodTable;
use crate::core::fixed::units::{ChannelVolume, EnvValue, Finetune, Panning, Volume};
use crate::prelude::*;
use crate::tracker::import::bin_reader::{BinReader, ImportError};
type NoteRow = (usize, i16, usize, Option<u8>, Option<u16>, Option<u16>);
type SongTrace = (u16, u32, Vec<(u32, super::runtime::TickEvent)>);
#[derive(Debug, Clone)]
pub struct DwModule {
pub variant: DwVariant,
pub period_table: PeriodTable,
pub period_via_finetune: bool,
pub samples: Vec<DwSample>,
pub sub_song: Option<DwSubSong>,
pub sub_songs: Vec<DwSubSong>,
pub selected_sub_song: Option<usize>,
pub position_lists: [DwPositionList; DW_NUM_CHANNELS],
pub all_position_lists: Vec<[DwPositionList; DW_NUM_CHANNELS]>,
pub tracks: Vec<DwTrack>,
pub volume_envelopes: Vec<DwVolumeEnvelope>,
pub arpeggios: Vec<DwArpeggio>,
pub dispatcher: super::detect::DwDispatcher,
pub features: super::detect::DwFeatures,
pub volume_bracket_is_pitch: bool,
pub channel_volumes: [u16; DW_NUM_CHANNELS],
pub master_volume: Option<u16>,
pub use_pitch_arpeggio: bool,
pub use_arpeggio_pitch_lane: bool,
}
impl DwModule {
pub fn load(source: &[u8]) -> Result<Self, ImportError> {
let layout = detect(source).ok_or(ImportError::InvalidMagic("dw_detect"))?;
let samples = Self::parse_samples(source, &layout)?;
let sub_songs = Self::parse_sub_songs(source, &layout);
let selected = Self::pick_main_sub_song_index(
source,
&sub_songs,
layout.uses_32bit_pointers,
layout.start_offset,
layout.command_map.is_some(),
);
let mut all_position_lists: Vec<[DwPositionList; DW_NUM_CHANNELS]> = sub_songs
.iter()
.map(|s| position_lists_for(source, s, layout.uses_32bit_pointers, layout.start_offset))
.collect();
for lists in all_position_lists.iter_mut() {
follow_seq_ptr_loops(
source,
lists,
&layout.dispatcher,
&layout.features,
layout.command_map.as_ref(),
layout.uses_32bit_pointers,
layout.start_offset,
);
}
let position_lists = selected
.map(|i| all_position_lists[i].clone())
.unwrap_or_default();
let sub_song = selected.map(|i| sub_songs[i].clone());
let tracks = Self::parse_tracks(
source,
&all_position_lists,
&layout.dispatcher,
&layout.features,
layout.command_map.as_ref(),
);
let volume_envelopes = Self::parse_volume_envelopes(source, &layout);
let arpeggios = Self::parse_arpeggios(source, &layout);
let channel_volumes = Self::parse_channel_volumes(source, &layout);
let master_volume = layout.master_volume_offset.and_then(|off| {
source
.get(off..off + 2)
.map(|b| u16::from_be_bytes([b[0], b[1]]))
});
if samples.is_empty() && sub_songs.is_empty() && tracks.is_empty() {
return Err(ImportError::InvalidMagic("dw_no_song"));
}
Ok(Self {
variant: layout.variant,
period_table: layout.period_table,
period_via_finetune: layout.period_via_finetune,
samples,
sub_song,
sub_songs,
selected_sub_song: selected,
position_lists,
all_position_lists,
tracks,
volume_envelopes,
arpeggios,
dispatcher: layout.dispatcher.clone(),
features: layout.features,
volume_bracket_is_pitch: layout.volume_bracket_is_pitch,
channel_volumes,
master_volume,
use_pitch_arpeggio: layout.command_map.is_some(),
use_arpeggio_pitch_lane: !(layout.command_map.is_some()
&& matches!(layout.period_table, PeriodTable::P2)),
})
}
#[inline]
fn scale_master_volume(&self, v: u8) -> u8 {
match self.master_volume {
Some(master) if master != 64 => (((v as u32) * (master as u32)) >> 6).min(64) as u8,
_ => v,
}
}
fn parse_channel_volumes(
source: &[u8],
layout: &super::detect::DwLayout,
) -> [u16; DW_NUM_CHANNELS] {
let mut vols = [64u16; DW_NUM_CHANNELS];
if let Some(base) = layout.channel_volume_offset {
for (ch, slot) in vols.iter_mut().enumerate() {
let at = base + ch * 2;
if let Some(bytes) = source.get(at..at + 2) {
*slot = u16::from_be_bytes([bytes[0], bytes[1]]).min(64);
}
}
}
vols
}
fn pick_main_sub_song_index(
source: &[u8],
sub_songs: &[DwSubSong],
uses_32bit: bool,
start_offset: isize,
prefer_first: bool,
) -> Option<usize> {
if sub_songs.is_empty() {
return None;
}
#[cfg(feature = "std")]
if let Ok(v) = std::env::var("DW_SUBSONG") {
if let Ok(i) = v.trim().parse::<usize>() {
if i < sub_songs.len() {
return Some(i);
}
}
}
if prefer_first {
return Some(0);
}
let score = |s: &DwSubSong| -> usize {
position_lists_for(source, s, uses_32bit, start_offset)
.iter()
.map(|l| l.len())
.sum()
};
const DEGENERATE_MAX: usize = 8; if score(&sub_songs[0]) > DEGENERATE_MAX {
return Some(0);
}
let mut best = 0usize;
let mut best_score = 0usize;
for (i, s) in sub_songs.iter().enumerate() {
let sc = score(s);
if sc > best_score {
best_score = sc;
best = i;
}
}
Some(best)
}
pub fn to_module(&self) -> Module {
use crate::core::cell::{Cell, CellEvent};
use crate::core::daw::clip::Clip;
use crate::core::daw::sorted_clips::SortedClips;
use crate::core::daw::timeline::{TimelineEntry, TimelineMap};
use crate::core::daw::track::Track;
use crate::core::fixed::units::Volume;
use crate::core::pitch::Pitch;
use alloc::string::String;
use core::convert::TryFrom;
let mut module = Module::default();
module.name = String::from("David Whittaker (.dw)");
module.origin = Some(crate::tracker::format::ModuleFormat::Dw);
module.frequency_type = crate::tracker::period::FrequencyType::AmigaFrequencies;
module.quirks.pitch_slide_ticks_at_row_zero = true;
module.quirks.pitch_vibrato_ticks_continuously = true;
use crate::core::module::ChannelDefault;
module.channel_defaults = alloc::vec![
ChannelDefault {
panning: Some(Panning::LEFT),
..Default::default()
},
ChannelDefault {
panning: Some(Panning::RIGHT),
..Default::default()
},
ChannelDefault {
panning: Some(Panning::RIGHT),
..Default::default()
},
ChannelDefault {
panning: Some(Panning::LEFT),
..Default::default()
},
];
let helper = crate::tracker::period::PeriodHelper::new(module.frequency_type, false);
module.instrument.reserve(self.samples.len());
for s in &self.samples {
module.instrument.push(sample_to_instrument(s));
}
let bpm_for = |ss_idx: usize| -> usize {
let eff_delay = if self.features.enable_delay_counter {
let d = self.sub_songs[ss_idx].delay_speed as u32;
if self.features.enable_delay_multiply {
d.saturating_mul(16)
} else {
d
}
} else {
0
}
.min(224); ((125 * (256 - eff_delay)) / 256).max(32) as usize
};
let instr_count = module.instrument.len();
let clamp_sample = |s: u16| -> usize {
let s = s as usize;
if instr_count == 0 {
0
} else {
s.min(instr_count - 1)
}
};
let note_to_pitch = |note_byte: i16, sample_freq: u16, sample_transpose: i8| -> Pitch {
let period: u32 = if matches!(self.variant, DwVariant::Old) && !self.period_via_finetune
{
let idx = note_byte.clamp(0, 11) as usize;
super::tables::PERIODS1[idx] as u32
} else {
let max_idx = match self.period_table {
PeriodTable::P1 => 11,
PeriodTable::P2 => super::tables::PERIODS2.len() - 1,
PeriodTable::P3 => super::tables::PERIODS3.len() - 1,
};
let idx = (note_byte + sample_transpose as i16).clamp(0, max_idx as i16) as usize;
let base = self.period_table.period(idx).unwrap_or(0) as u32;
let finetune = 0x0036_9E99u32 / (sample_freq.max(1) as u32);
(base.saturating_mul(finetune)) >> 10
};
if period == 0 {
return Pitch::C5;
}
let p = crate::core::fixed::units::Period::from_raw(period.clamp(1, 0xFFFF) as u16);
let pitch_q8_8 = helper.period_to_pitch(p).as_q8_8_i32();
let semitone = ((pitch_q8_8 + 0x80) >> 8).clamp(0, 119) as u8;
Pitch::try_from(semitone).unwrap_or(Pitch::C5)
};
const MAX_SONGS: usize = 32;
let mut order: alloc::vec::Vec<usize> = alloc::vec::Vec::new();
if let Some(sel) = self.selected_sub_song {
order.push(sel);
}
for i in 0..self.all_position_lists.len() {
if order.len() >= MAX_SONGS {
break;
}
if Some(i) != self.selected_sub_song {
order.push(i);
}
}
module.default_tempo = order
.first()
.map(|&i| self.sub_songs[i].speed.max(1) as usize)
.unwrap_or(6);
module.default_bpm = order.first().map(|&i| bpm_for(i)).unwrap_or(125);
let mut clips_vec: Vec<Clip> = Vec::new();
let mut timeline_entries: Vec<TimelineEntry> = Vec::new();
let mut song_traces: Vec<SongTrace> = Vec::new();
let mut arp_instr_cache: BTreeMap<(usize, Option<u16>, Option<u16>), usize> =
BTreeMap::new();
for (song_u, &ss_idx) in order.iter().enumerate() {
let song = song_u as u16;
let lists = &self.all_position_lists[ss_idx];
let dw_speed = self.sub_songs[ss_idx].speed.max(1);
let speed: u8 = dw_speed;
let speed_u32 = dw_speed as u32;
let song_bpm = bpm_for(ss_idx) as u16;
let mut sim = super::runtime::Simulator::new_for_subsong(self, lists, dw_speed);
let cap = if song_u == 0 {
super::runtime::MAX_SIMULATION_FRAMES
} else {
6000
};
let (mut trace, period) = sim.run_with_loop(cap);
let drift_len: Option<u32> = if song_u == 0 && period == Some(cap) {
sim.first_wrap_frame
.iter()
.filter_map(|w| w.map(|f| f.saturating_sub(1)))
.max()
.filter(|&m| m > 0 && m < cap)
} else {
None
};
if let Some(dl) = drift_len {
let pass_end = |ch: usize| -> u32 {
sim.first_wrap_frame
.get(ch)
.and_then(|w| *w)
.map(|f| f.saturating_sub(1))
.unwrap_or(dl)
};
trace.retain(|(f, ev)| *f <= pass_end(ev.channel() as usize));
for (ch, w) in sim.first_wrap_frame.iter().enumerate() {
if let Some(end) = w.map(|f| f.saturating_sub(1)).filter(|&e| e > 0) {
let start = sim.loop_start_frame[ch]
.map(|f| f.saturating_sub(1))
.unwrap_or(0)
.min(end);
module
.channel_loops
.push(crate::core::daw::loop_region::ChannelLoop {
song,
channel: ch as u8,
start_tick: start,
end_tick: end,
});
}
}
}
let halted_via_stop = trace
.iter()
.any(|(_, ev)| matches!(ev, super::runtime::TickEvent::SongEnd { .. }));
if song_u == 0 && (period.is_some() || halted_via_stop) {
module.song_loop_to = Some(0);
}
if trace.is_empty() {
continue;
}
let last_frame = trace.iter().map(|(f, _)| *f).max().unwrap_or(0);
let length_frames = drift_len.or(period).unwrap_or(last_frame);
let total_rows = (length_frames.saturating_sub(1) / speed_u32 + 1) as usize;
let lane_rows: [usize; 4] = {
let mut lr = [total_rows; 4];
if drift_len.is_some() {
for (slot, &w) in lr.iter_mut().zip(sim.first_wrap_frame.iter()) {
if let Some(end) = w.map(|f| f.saturating_sub(1)).filter(|&e| e > 0) {
*slot = (end.saturating_sub(1) / speed_u32 + 1) as usize;
}
}
}
lr
};
let mut per_ch_notes: [Vec<NoteRow>; 4] = Default::default();
let mut arp_by_row: [BTreeMap<usize, u16>; 4] = Default::default();
for (frame, ev) in &trace {
if let super::runtime::TickEvent::NoteOn {
channel,
effective_note,
sample_index,
volume,
envelope_index,
arpeggio_index,
..
} = ev
{
let row = ((frame.saturating_sub(1)) / speed_u32) as usize;
if row >= total_rows {
continue;
}
let ch = *channel as usize;
if ch >= 4 {
continue;
}
let sample = sample_index.map(clamp_sample).unwrap_or(0);
let arp_for_seg = if self.use_pitch_arpeggio && !self.use_arpeggio_pitch_lane {
*arpeggio_index
} else {
None
};
per_ch_notes[ch].push((
row,
*effective_note,
sample,
*volume,
*envelope_index,
arp_for_seg,
));
if let Some(arp) = arpeggio_index {
arp_by_row[ch].insert(row, *arp);
}
}
}
for notes in per_ch_notes.iter_mut() {
let mut deduped: Vec<NoteRow> = Vec::with_capacity(notes.len());
for &tuple in notes.iter() {
if let Some(last) = deduped.last_mut() {
if last.0 == tuple.0 {
*last = tuple; continue;
}
}
deduped.push(tuple);
}
*notes = deduped;
}
for (ch, notes) in per_ch_notes.iter().enumerate() {
let segments = split_at_pattern_boundaries(build_segments(
notes,
lane_rows[ch],
self.use_pitch_arpeggio,
));
for (seg_idx, seg) in segments.iter().enumerate() {
let seg_len = seg.end_row + 1 - seg.start_row;
let mut rows = vec![Cell::default(); seg_len];
let (seg_sample_freq, seg_sample_transpose) = self
.samples
.get(seg.sample_index)
.map(|s| (s.frequency, s.transpose))
.unwrap_or((8372, 0));
for &(abs_row, note_byte, paula_vol, _env_idx) in &seg.notes {
if abs_row < seg.start_row || abs_row > seg.end_row {
continue;
}
let rel_row = abs_row - seg.start_row;
let pitch = note_to_pitch(note_byte, seg_sample_freq, seg_sample_transpose);
let effective_vol = paula_vol
.map(|v| v.min(64))
.or_else(|| {
if self.period_via_finetune {
Some(
self.samples
.get(seg.sample_index)
.map(|s| s.volume.min(64) as u8)
.unwrap_or(64),
)
} else if matches!(self.variant, DwVariant::Old) {
Some(self.channel_volumes[ch].min(64) as u8)
} else {
None
}
})
.map(|v| self.scale_master_volume(v));
let velocity = effective_vol
.map(Volume::from_byte_64)
.unwrap_or(Volume::FULL);
let mut effects: Vec<TrackEffect> = Vec::new();
if effective_vol.is_some() && seg.env_index.is_none() {
effects.push(TrackEffect::Volume {
value: velocity,
tick: 0,
});
}
if !self.use_pitch_arpeggio && !self.use_arpeggio_pitch_lane {
if let Some(&arp_idx) = arp_by_row[ch].get(&abs_row) {
if let Some(arp) = self.arpeggios.get(arp_idx as usize) {
let (half1, half2) = arp.classic_pair();
if half1 != 0 || half2 != 0 {
effects.push(TrackEffect::Arpeggio { half1, half2 });
}
}
}
}
rows[rel_row] = Cell {
event: CellEvent::NoteOn { pitch, velocity },
effects,
expression: crate::core::cell::NoteExpression::NEUTRAL,
};
}
if seg.env_index.is_none() {
attach_envelope_animation(
&mut rows,
&seg.notes,
seg.start_row,
speed_u32,
&self.volume_envelopes,
self.master_volume,
);
}
let instr_idx = resolve_synth_instrument(
&mut module.instrument,
&mut arp_instr_cache,
&self.samples,
&self.arpeggios,
&self.volume_envelopes,
self.master_volume,
seg.sample_index,
seg.arp_index,
seg.env_index,
);
let track_idx = module.tracks.len() as u32;
module.tracks.push(Track::Notes {
name: format!("ch{} seg{:02} smp{:02}", ch, seg_idx, seg.sample_index),
instrument: instr_idx,
rows,
muted: false,
});
let pat_rel_start = (seg.start_row % ROWS_PER_PATTERN as usize) as u32;
clips_vec.push(Clip {
track: track_idx,
song,
target_channel: ch as u8,
position_tick: (seg.start_row as u32) * speed_u32,
speed_at_start: speed,
track_row_offset: 0,
source_start_row: pat_rel_start,
end_tick: ((seg.end_row + 1) as u32) * speed_u32,
});
}
}
for r in 0..total_rows {
let r_u32 = r as u32;
let pattern = r_u32 / ROWS_PER_PATTERN;
timeline_entries.push(TimelineEntry {
song,
order_idx: pattern,
pattern_idx: pattern,
row_idx: r_u32 % ROWS_PER_PATTERN,
loop_iter: 0,
tick: r_u32 * speed_u32,
speed_at_row: speed,
bpm_at_row: song_bpm,
});
}
song_traces.push((song, speed_u32, trace));
}
module.clips = SortedClips::from_unsorted(clips_vec);
module.timeline_map = TimelineMap {
entries: timeline_entries,
};
crate::tracker::import::build::dedupe_tracks_by_content(&mut module);
for (song, song_speed, trace) in &song_traces {
attach_vibrato_lanes(&mut module, *song, trace, *song_speed);
attach_slide_lanes(&mut module, *song, trace, *song_speed);
if self.use_arpeggio_pitch_lane {
attach_arpeggio_pitch_lanes(&mut module, *song, trace);
}
}
collapse_dead_duplicate_modulation_lanes(&mut module);
module
}
#[doc(hidden)]
pub fn segment_count_for_diagnostics(&self) -> usize {
let speed = self.sub_song.as_ref().map(|s| s.speed.max(1)).unwrap_or(6);
let trace = self.simulate();
if trace.is_empty() {
return 0;
}
let last_frame = trace.iter().map(|(f, _)| *f).max().unwrap_or(0);
let total_rows = (last_frame.saturating_sub(1) / speed as u32 + 1) as usize;
let speed_u32 = speed as u32;
let instr_count = self.samples.len();
let clamp = |s: u16| -> usize {
let s = s as usize;
if instr_count == 0 {
0
} else {
s.min(instr_count - 1)
}
};
let mut per_ch_notes: [Vec<NoteRow>; 4] = Default::default();
for (frame, ev) in &trace {
if let super::runtime::TickEvent::NoteOn {
channel,
effective_note,
sample_index,
volume,
envelope_index,
..
} = ev
{
let row = ((frame.saturating_sub(1)) / speed_u32) as usize;
if row >= total_rows {
continue;
}
let ch = *channel as usize;
if ch >= 4 {
continue;
}
per_ch_notes[ch].push((
row,
*effective_note,
sample_index.map(clamp).unwrap_or(0),
*volume,
*envelope_index,
None,
));
}
}
let mut total = 0;
for notes in &per_ch_notes {
total += build_segments(notes, total_rows, false).len();
}
total
}
fn parse_samples(source: &[u8], layout: &DwLayout) -> Result<Vec<DwSample>, ImportError> {
let (Some(n_raw), Some(data_off)) = (layout.number_of_samples, layout.sample_data_offset)
else {
return Ok(Vec::new());
};
let n = n_raw as usize;
if n == 0 {
return Ok(Vec::new());
}
let info_off = layout.sample_info_offset;
let mut data_reader = open_reader(source, data_off)?;
let mut samples: Vec<DwSample> = Vec::with_capacity(n);
let mut pcm_pool: BTreeMap<u64, Vec<Arc<[i8]>>> = BTreeMap::new();
for idx in 0..n {
let Ok(length_bytes) = data_reader.read_u32_be() else {
break;
};
let Ok(frequency) = data_reader.read_u16_be() else {
break;
};
if length_bytes as usize > data_reader.remaining() {
break;
}
let Ok(pcm_bytes) = data_reader.read_slice(length_bytes as usize) else {
break;
};
let pcm_vec: Vec<i8> = pcm_bytes.iter().map(|&b| b as i8).collect();
let pcm: Arc<[i8]> = dedup_pcm(&mut pcm_pool, pcm_vec);
let info_stride = if layout.features.enable_sample_transpose {
16
} else {
12
};
let loop_start = if matches!(layout.variant, DwVariant::Old) {
None
} else {
info_off.and_then(|base| read_loop_start(source, base, idx, info_stride))
};
let transpose = if layout.features.enable_sample_transpose {
info_off
.and_then(|base| source.get(base + idx * 16 + 14).copied())
.map(|b| b as i8)
.unwrap_or(0)
} else {
0
};
let volume = if layout.features.enable_sample_transpose {
info_off
.and_then(|base| source.get(base + idx * 16 + 12..base + idx * 16 + 14))
.map(|b| u16::from_be_bytes([b[0], b[1]]).min(64))
.unwrap_or(64)
} else if layout.period_via_finetune {
layout
.instrument_volume_offset
.and_then(|base| source.get(base + idx * 0x10 + 0xE..base + idx * 0x10 + 0x10))
.map(|b| u16::from_be_bytes([b[0], b[1]]).min(64))
.unwrap_or(64)
} else {
64
};
samples.push(DwSample {
index: idx as u16,
loop_start: loop_start.filter(|&s| s < length_bytes),
length: length_bytes,
frequency,
volume,
transpose,
pcm,
});
}
Ok(samples)
}
}
impl DwModule {
fn parse_sub_songs(source: &[u8], layout: &DwLayout) -> Vec<DwSubSong> {
let row = match layout.sub_song_row_width {
8 | 10 | 18 => layout.sub_song_row_width,
_ => 10,
};
let header = layout.sub_song_header;
let ptr_width = if layout.uses_32bit_pointers { 4 } else { 2 };
let voices = row
.saturating_sub(header)
.checked_div(ptr_width)
.unwrap_or(DW_NUM_CHANNELS)
.clamp(1, DW_NUM_CHANNELS);
let mut out = Vec::new();
let Some(off) = layout.sub_song_list_offset else {
return out;
};
let mut min_list_off: usize = source.len();
let mut cursor = off;
for _ in 0..64 {
if cursor + row > source.len() {
break;
}
if cursor + row > min_list_off {
break;
}
let (speed, delay_speed) = if header == 0 {
(6u8, 0u8)
} else if layout.uses_32bit_pointers {
(source[cursor + 1], source[cursor])
} else if source[cursor] != 0 {
(source[cursor], source[cursor + 1])
} else {
(source[cursor + 1], 0u8)
};
let mut offs = [0u32; DW_NUM_CHANNELS];
let mut valid = true;
#[allow(clippy::needless_range_loop)]
for i in 0..voices {
let p = cursor + header + ptr_width * i;
let o = if ptr_width == 4 {
u32::from_be_bytes([source[p], source[p + 1], source[p + 2], source[p + 3]])
} else {
u16::from_be_bytes([source[p], source[p + 1]]) as u32
};
if o == 0 || (o as usize) >= source.len() {
valid = false;
break;
}
offs[i] = o;
}
if !valid || (!out.is_empty() && speed == 0) {
break;
}
let row_min = offs.iter().copied().min().unwrap_or(0) as usize;
min_list_off = min_list_off.min(row_min);
out.push(DwSubSong {
speed,
delay_speed,
channel_position_offsets: offs,
});
cursor += row;
}
out
}
}
pub fn position_lists_for(
source: &[u8],
sub_song: &DwSubSong,
uses_32bit_pointers: bool,
start_offset: isize,
) -> [DwPositionList; DW_NUM_CHANNELS] {
let mut lists: [DwPositionList; DW_NUM_CHANNELS] = Default::default();
for (ch, &off) in sub_song.channel_position_offsets.iter().enumerate() {
lists[ch] = read_position_list(source, off as usize, uses_32bit_pointers, start_offset);
}
lists
}
impl DwModule {
fn parse_volume_envelopes(
source: &[u8],
layout: &super::detect::DwLayout,
) -> Vec<DwVolumeEnvelope> {
const MAX_STEPS: usize = 128;
let (Some(table_off), Some(n)) = (
layout.volume_envelope_table_offset,
layout.volume_envelope_table_len,
) else {
return Vec::new();
};
let n = n as usize;
let mut out = Vec::with_capacity(n);
for idx in 0..n {
let entry_off = table_off + idx * 2;
if entry_off + 2 > source.len() {
break;
}
let raw = u16::from_be_bytes([source[entry_off], source[entry_off + 1]]) as usize;
let env_start = if raw == 0 {
0
} else {
match (raw as isize).checked_add(layout.start_offset) {
Some(f) if f >= 0 && (f as usize) < source.len() => f as usize,
_ => 0,
}
};
let step_interval = if env_start > 0 && env_start - 1 < source.len() {
source[env_start - 1]
} else {
0
};
let mut steps = Vec::new();
let mut cursor = env_start;
while cursor < source.len() && steps.len() < MAX_STEPS {
let b = source[cursor];
steps.push(b & 0x7F);
cursor += 1;
if b & 0x80 != 0 {
break;
}
}
out.push(DwVolumeEnvelope {
index: idx as u16,
step_interval,
steps,
});
}
out
}
fn parse_arpeggios(source: &[u8], layout: &DwLayout) -> Vec<DwArpeggio> {
const MAX_STEPS: usize = 64;
let (Some(table_off), Some(n)) = (layout.arpeggio_table_offset, layout.arpeggio_table_len)
else {
return Vec::new();
};
let n = n as usize;
let mut out = Vec::with_capacity(n);
let mut sane_entries = 0usize;
for idx in 0..n {
let entry_off = table_off + idx * 2;
if entry_off + 2 > source.len() {
break;
}
let raw = u16::from_be_bytes([source[entry_off], source[entry_off + 1]]) as usize;
let arp_start = if raw == 0 {
0
} else {
match (raw as isize).checked_add(layout.start_offset) {
Some(f) if f >= 0 && (f as usize) < source.len() => f as usize,
_ => 0,
}
};
let mut offsets = Vec::new();
let mut cursor = arp_start;
while cursor < source.len() && offsets.len() < MAX_STEPS {
let b = source[cursor];
let raw7 = (b & 0x7F) as i16;
let signed = if raw7 >= 0x40 {
(raw7 - 0x80) as i8
} else {
raw7 as i8
};
offsets.push(signed);
cursor += 1;
if b & 0x80 != 0 {
break;
}
}
if !offsets.is_empty() && offsets.iter().all(|&o| (-0x30..=0x30).contains(&o)) {
sane_entries += 1;
}
out.push(DwArpeggio {
index: idx as u16,
offsets,
});
}
if out.is_empty() || sane_entries * 2 < out.len() {
return Vec::new();
}
out
}
fn parse_tracks(
source: &[u8],
all_lists: &[[DwPositionList; DW_NUM_CHANNELS]],
dispatcher: &super::detect::DwDispatcher,
features: &super::detect::DwFeatures,
command_map: Option<&super::command_map::DwCommandMap>,
) -> Vec<DwTrack> {
let mut seen: BTreeMap<u32, DwTrack> = BTreeMap::new();
for lists in all_lists {
for list in lists {
for &offset in &list.entries {
seen.entry(offset).or_insert_with(|| {
let mut bytes = read_track_bytes(source, offset as usize);
let (events, used) = super::event::decode_track_counted(
&bytes,
dispatcher,
features,
command_map,
);
bytes.truncate(used);
DwTrack {
offset,
bytes,
events,
}
});
}
}
}
seen.into_values().collect()
}
}
fn attach_vibrato_lanes(
module: &mut Module,
song: u16,
trace: &[(u32, super::runtime::TickEvent)],
speed: u32,
) {
use crate::core::daw::automation::{AutomationLane, AutomationTarget, LaneKind, LfoEvent};
use crate::core::fixed::fixed::{Q15, Q8_8};
use crate::core::waveform::Waveform;
#[derive(Clone, Copy)]
struct VibParams {
speed: Q8_8,
depth: Q15,
}
let mut vib_by_ch: [alloc::vec::Vec<(u32, Option<VibParams>)>; 4] = Default::default();
for (frame, ev) in trace {
let tick = (frame.saturating_sub(1) / speed) * speed;
match ev {
super::runtime::TickEvent::VibratoStart {
channel,
speed: vspd,
depth,
} if (*channel as usize) < 4 => {
let denom = (*depth as u32).max(1);
let params = VibParams {
speed: Q8_8::from_raw(
((64 * *vspd as u32) / denom).clamp(1, i16::MAX as u32) as i16
),
depth: Q15::from_raw((*depth as i32).clamp(0, i16::MAX as i32) as i16),
};
vib_by_ch[*channel as usize].push((tick, Some(params)));
}
super::runtime::TickEvent::VibratoStop { channel } if (*channel as usize) < 4 => {
vib_by_ch[*channel as usize].push((tick, None));
}
_ => {}
}
}
let mut events_per_track: BTreeMap<u32, Vec<LfoEvent>> = BTreeMap::new();
for ch in 0..4u8 {
let evs = &mut vib_by_ch[ch as usize];
if evs.is_empty() {
continue;
}
evs.sort_by_key(|(t, _)| *t);
let clips = module.clips.lane(song, ch);
for (i, &(start_tick, params)) in evs.iter().enumerate() {
let Some(params) = params else { continue };
let span_end = evs.get(i + 1).map(|(t, _)| *t).unwrap_or(u32::MAX);
if span_end <= start_tick {
continue; }
for clip in clips {
let cs = clip.position_tick.max(start_tick);
let ce = clip.end_tick.min(span_end);
if cs >= ce {
continue;
}
let lane = events_per_track.entry(clip.track).or_default();
lane.push(LfoEvent::Set {
tick: cs,
speed: params.speed,
depth: params.depth,
waveform: Waveform::BipolarTriangle,
retrig: false,
});
lane.push(LfoEvent::Clear { tick: ce });
}
}
}
for (track_idx, mut events) in events_per_track {
if events.is_empty() {
continue;
}
events.sort_by_key(|e| {
let (tick, is_set) = match e {
LfoEvent::Set { tick, .. } => (*tick, 1u8),
LfoEvent::DepthOnly { tick, .. } => (*tick, 1),
LfoEvent::SpeedOnly { tick, .. } => (*tick, 1),
LfoEvent::WaveformOnly { tick, .. } => (*tick, 1),
LfoEvent::Clear { tick } => (*tick, 0),
};
(tick, is_set)
});
module.automation.push(
AutomationLane::new_with_kind(
AutomationTarget::TrackPitch(track_idx),
LaneKind::Lfo { events },
)
.with_song(song),
);
}
}
fn attach_slide_lanes(
module: &mut Module,
song: u16,
trace: &[(u32, super::runtime::TickEvent)],
speed: u32,
) {
use crate::core::daw::automation::{AutomationLane, AutomationTarget, LaneKind, SlideEvent};
use crate::core::fixed::fixed::Q15;
let mut events_per_track: BTreeMap<u32, Vec<SlideEvent>> = BTreeMap::new();
let mut open_slide_track: BTreeMap<u8, u32> = BTreeMap::new();
for (frame, ev) in trace {
let row = frame.saturating_sub(1) / speed;
let tick = row * speed;
let (ch, slide_ev) = match ev {
super::runtime::TickEvent::SlideStart {
channel,
speed: spd,
counter,
} => {
let rate_raw = -(*spd as i32);
let rate = Q15::from_raw(rate_raw.clamp(i16::MIN as i32, i16::MAX as i32) as i16);
(
*channel,
SlideEvent::Set {
tick: tick + *counter as u32,
rate,
fine: false,
},
)
}
super::runtime::TickEvent::SlideStop { channel } => {
(*channel, SlideEvent::Clear { tick })
}
_ => continue,
};
let target_track = match &slide_ev {
SlideEvent::Set { .. } => {
let t = module.clips.active_at(song, ch, tick).map(|(_, c)| c.track);
if let Some(t) = t {
open_slide_track.insert(ch, t);
}
t
}
SlideEvent::Clear { .. } => open_slide_track
.remove(&ch)
.or_else(|| module.clips.active_at(song, ch, tick).map(|(_, c)| c.track)),
};
if let Some(track) = target_track {
events_per_track.entry(track).or_default().push(slide_ev);
}
}
for (track_idx, mut events) in events_per_track {
if events.is_empty() {
continue;
}
events.sort_by_key(|e| match e {
SlideEvent::Set { tick, .. } => *tick,
SlideEvent::Clear { tick } => *tick,
});
module.automation.push(
AutomationLane::new_with_kind(
AutomationTarget::TrackPitch(track_idx),
LaneKind::Slide { events },
)
.with_song(song),
);
}
}
fn attach_arpeggio_pitch_lanes(
module: &mut Module,
song: u16,
trace: &[(u32, super::runtime::TickEvent)],
) {
use crate::core::daw::automation::{
AutomationLane, AutomationPoint, AutomationTarget, AutomationValue, LaneKind,
};
use crate::core::fixed::units::PitchDelta;
let mut by_track: BTreeMap<u32, BTreeMap<u32, i8>> = BTreeMap::new();
for (frame, ev) in trace {
let super::runtime::TickEvent::Arpeggio { channel, semitones } = ev else {
continue;
};
let tick = frame.saturating_sub(1);
let Some((_, clip)) = module.clips.active_at(song, *channel, tick) else {
continue;
};
by_track
.entry(clip.track)
.or_default()
.insert(tick, *semitones);
}
for (track_idx, offsets) in by_track {
if offsets.is_empty() {
continue;
}
let points: Vec<AutomationPoint> = offsets
.into_iter()
.map(|(tick, semis)| AutomationPoint {
tick,
value: AutomationValue::Pitch(PitchDelta::from_semitones(semis as i16)),
})
.collect();
module.automation.push(
AutomationLane::new_with_kind(
AutomationTarget::TrackPitch(track_idx),
LaneKind::Points(points),
)
.with_song(song),
);
}
}
fn read_track_bytes(source: &[u8], start: usize) -> Vec<u8> {
if start == 0 || start >= source.len() {
return Vec::new();
}
let end = (start + TRACK_BYTE_CAP).min(source.len());
source[start..end].to_vec()
}
const TRACK_BYTE_CAP: usize = 0x800;
fn read_position_list(
source: &[u8],
start: usize,
uses_32bit: bool,
start_offset: isize,
) -> DwPositionList {
const MAX_ENTRIES: usize = 1024;
let mut list = DwPositionList::default();
let width = if uses_32bit { 4 } else { 2 };
let rebase = |v: u32| -> Option<usize> {
let f = v as isize + start_offset;
(f >= 0 && (f as usize) < source.len()).then_some(f as usize)
};
let Some(mut cursor) = rebase(start as u32) else {
return list;
};
if start == 0 || cursor + width > source.len() {
return list;
}
let loop_mask: u32 = if uses_32bit { 0x8000_0000 } else { 0x0000_8000 };
let value_mask: u32 = !loop_mask;
for _ in 0..MAX_ENTRIES {
if cursor + width > source.len() {
break;
}
let raw: u32 = if uses_32bit {
u32::from_be_bytes([
source[cursor],
source[cursor + 1],
source[cursor + 2],
source[cursor + 3],
])
} else {
u16::from_be_bytes([source[cursor], source[cursor + 1]]) as u32
};
cursor += width;
if raw == 0 {
list.loop_to = Some(0);
break;
}
if raw & loop_mask != 0 {
let target_offset = rebase(raw & value_mask).unwrap_or(0) as u32;
let target_index = list
.entries
.iter()
.position(|&e| e == target_offset)
.unwrap_or(0) as u32;
list.loop_to = Some(target_index);
break;
}
let Some(file_off) = rebase(raw) else {
break;
};
list.entries.push(file_off as u32);
}
list
}
fn follow_seq_ptr_loops(
source: &[u8],
lists: &mut [DwPositionList; DW_NUM_CHANNELS],
dispatcher: &super::detect::DwDispatcher,
features: &super::detect::DwFeatures,
command_map: Option<&super::command_map::DwCommandMap>,
uses_32bit: bool,
start_offset: isize,
) {
#[allow(clippy::needless_range_loop)]
for ch in 0..DW_NUM_CHANNELS {
let Some(&intro_off) = lists[ch].entries.first() else {
continue;
};
let bytes = read_track_bytes(source, intro_off as usize);
let (events, _) =
super::event::decode_track_counted(&bytes, dispatcher, features, command_map);
let Some(x_raw) = events.iter().find_map(|e| match e {
super::event::DwTrackEvent::SeqPtr(x) => Some(*x),
_ => None,
}) else {
continue;
};
let sub = read_position_list(source, x_raw as usize, uses_32bit, start_offset);
if sub.entries.is_empty() {
continue;
}
let mut entries = alloc::vec![intro_off];
entries.extend_from_slice(&sub.entries);
lists[ch] = DwPositionList {
entries,
loop_to: Some(sub.loop_to.unwrap_or(0) + 1),
};
}
}
fn attach_envelope_animation(
rows: &mut [Cell],
notes: &[(usize, i16, Option<u8>, Option<u16>)],
seg_start: usize,
speed: u32,
envelopes: &[super::header::DwVolumeEnvelope],
master_volume: Option<u16>,
) {
let scale = |v: u8| -> u8 {
match master_volume {
Some(master) if master != 64 => (((v as u32) * (master as u32)) >> 6).min(64) as u8,
_ => v,
}
};
if notes.is_empty() || rows.is_empty() {
return;
}
let seg_end = seg_start + rows.len() - 1;
let speed = speed.max(1);
for (i, &(note_abs_row, _, _peak, env_idx)) in notes.iter().enumerate() {
let Some(env_idx) = env_idx else { continue };
let Some(env) = envelopes.get(env_idx as usize) else {
continue;
};
if env.steps.is_empty() {
continue;
}
let next_abs_row = notes
.get(i + 1)
.map(|&(r, _, _, _)| r)
.unwrap_or(seg_end + 1);
if note_abs_row > seg_end {
continue;
}
let lifetime_end = next_abs_row.min(seg_end + 1);
let step_for_row = |r: usize| -> u8 {
let frames = (r - note_abs_row) as u32 * speed;
let step_idx = (frames / (env.step_interval as u32 + 1)) as usize;
env.steps[step_idx.min(env.steps.len() - 1)]
};
let mut prev_paula: u8 = step_for_row(note_abs_row);
for r in (note_abs_row + 1)..lifetime_end {
let paula = step_for_row(r);
if paula == prev_paula {
continue;
}
prev_paula = paula;
let rel = r - seg_start;
if rel >= rows.len() {
break;
}
if !matches!(rows[rel].event, CellEvent::None) {
continue;
}
let velocity = Volume::from_byte_64(scale(paula.min(64)));
rows[rel] = Cell {
event: CellEvent::None,
effects: vec![TrackEffect::Volume {
value: velocity,
tick: 0,
}],
expression: crate::core::cell::NoteExpression::NEUTRAL,
};
}
}
}
#[derive(Debug)]
struct ChannelSegment {
start_row: usize,
end_row: usize,
sample_index: usize,
arp_index: Option<u16>,
env_index: Option<u16>,
notes: Vec<(usize, i16, Option<u8>, Option<u16>)>,
}
const ROWS_PER_PATTERN: u32 = 64;
fn split_at_pattern_boundaries(segs: Vec<ChannelSegment>) -> Vec<ChannelSegment> {
let rpp = ROWS_PER_PATTERN as usize;
let mut out: Vec<ChannelSegment> = Vec::with_capacity(segs.len());
for seg in segs {
let mut start = seg.start_row;
while start <= seg.end_row {
let pat = start / rpp;
let pat_last = ((pat + 1) * rpp).saturating_sub(1);
let chunk_end = pat_last.min(seg.end_row);
let notes_in_chunk: Vec<(usize, i16, Option<u8>, Option<u16>)> = seg
.notes
.iter()
.filter(|(r, _, _, _)| *r >= start && *r <= chunk_end)
.copied()
.collect();
out.push(ChannelSegment {
start_row: start,
end_row: chunk_end,
sample_index: seg.sample_index,
arp_index: seg.arp_index,
env_index: seg.env_index,
notes: notes_in_chunk,
});
start = chunk_end + 1;
}
}
out
}
fn build_segments(notes: &[NoteRow], total_rows: usize, split_by_env: bool) -> Vec<ChannelSegment> {
let mut out: Vec<ChannelSegment> = Vec::new();
if notes.is_empty() || total_rows == 0 {
return out;
}
let key_env = |e: Option<u16>| if split_by_env { e } else { None };
let mut seg_start = 0usize;
let mut seg_sample = notes[0].2;
let mut seg_arp = notes[0].5;
let mut seg_env = key_env(notes[0].4);
let mut buf: Vec<(usize, i16, Option<u8>, Option<u16>)> = Vec::new();
for (i, &(row, note_byte, sample, volume, env_idx, arp)) in notes.iter().enumerate() {
if i > 0 && (sample != seg_sample || arp != seg_arp || key_env(env_idx) != seg_env) {
let end_row = row.saturating_sub(1).max(seg_start);
out.push(ChannelSegment {
start_row: seg_start,
end_row,
sample_index: seg_sample,
arp_index: seg_arp,
env_index: seg_env,
notes: core::mem::take(&mut buf),
});
seg_start = row;
seg_sample = sample;
seg_arp = arp;
seg_env = key_env(env_idx);
}
buf.push((row, note_byte, volume, env_idx));
}
out.push(ChannelSegment {
start_row: seg_start,
end_row: total_rows.saturating_sub(1).max(seg_start),
sample_index: seg_sample,
arp_index: seg_arp,
env_index: seg_env,
notes: buf,
});
out
}
fn open_reader(source: &[u8], offset: usize) -> Result<BinReader<'_>, ImportError> {
if offset >= source.len() {
return Err(ImportError::OutOfRange("dw_table_offset"));
}
Ok(BinReader::new(&source[offset..]))
}
fn collapse_dead_duplicate_modulation_lanes(module: &mut Module) {
use crate::core::daw::automation::{AutomationTarget, LaneKind};
let mut seen: Vec<(AutomationTarget, u8)> = Vec::new();
module.automation.retain(|l| {
let disc: u8 = match &l.kind {
LaneKind::Points(_) => return true,
LaneKind::Lfo { .. } => 1,
LaneKind::Slide { .. } => 2,
LaneKind::Glide { .. } => 3,
};
let key = (l.target, disc);
if seen.contains(&key) {
false
} else {
seen.push(key);
true
}
});
}
fn dedup_pcm(pool: &mut BTreeMap<u64, Vec<Arc<[i8]>>>, pcm: Vec<i8>) -> Arc<[i8]> {
let mut h: u64 = 0xcbf29ce484222325;
for &b in &pcm {
h ^= b as u8 as u64;
h = h.wrapping_mul(0x100000001b3);
}
let bucket = pool.entry(h).or_default();
if let Some(existing) = bucket.iter().find(|a| a.as_ref() == pcm.as_slice()) {
return Arc::clone(existing);
}
let arc: Arc<[i8]> = Arc::from(pcm);
bucket.push(Arc::clone(&arc));
arc
}
fn read_loop_start(source: &[u8], base: usize, idx: usize, stride: usize) -> Option<u32> {
let row = base.checked_add(idx.checked_mul(stride)?)?;
let field = row.checked_add(4)?;
let bytes: [u8; 4] = source.get(field..field + 4)?.try_into().ok()?;
let v = i32::from_be_bytes(bytes);
if v >= 0 {
Some(v as u32)
} else {
None
}
}
fn sample_to_instrument(s: &DwSample) -> Instrument {
let loop_flag = if s.is_looping() {
LoopType::Forward
} else {
LoopType::No
};
let rp = 0;
let sample = Sample {
name: format!("dw#{:02}", s.index),
relative_pitch: rp,
finetune: Finetune::ZERO,
volume: ChannelVolume::from_byte_64(s.volume.min(64) as u8),
default_note_volume: Volume::FULL,
panning: Panning::CENTER,
loop_flag,
loop_start: s.loop_start.unwrap_or(0),
loop_length: s.loop_length().unwrap_or(0),
sustain_loop_flag: LoopType::No,
sustain_loop_start: 0,
sustain_loop_length: 0,
data: Some(SampleDataType::Mono8(s.pcm.clone())),
};
let mut instr = InstrDefault::default();
instr.sample = vec![Some(sample)];
instr.keyboard.map_all_to(0);
Instrument {
name: format!("Whittaker sample {} ({} Hz)", s.index, s.frequency),
instr_type: InstrumentType::Default(instr),
..Default::default()
}
}
fn arpeggio_to_pitch_envelope(arp: &DwArpeggio) -> Option<Envelope> {
if arp.offsets.is_empty() || arp.offsets.iter().all(|&o| o == 0) {
return None;
}
let enc = |o: i8| -> EnvValue {
EnvValue::from_signed_byte_64(((o as i16) * 2).clamp(-32, 32) as i8)
};
let mut point = Vec::with_capacity(arp.offsets.len() + 1);
point.push(EnvelopePoint {
frame: 0,
value: enc(0),
});
for (k, &o) in arp.offsets.iter().enumerate() {
point.push(EnvelopePoint {
frame: k + 1,
value: enc(o),
});
}
let last = point.len() - 1;
Some(Envelope {
enabled: true,
point,
sustain_enabled: true,
sustain_start_point: 1,
sustain_end_point: last,
loop_enabled: true,
loop_start_point: 1,
loop_end_point: last,
})
}
fn volume_to_volume_envelope(env: &DwVolumeEnvelope, master: Option<u16>) -> Option<Envelope> {
if env.steps.is_empty() {
return None;
}
let scale = |v: u8| -> u8 {
match master {
Some(m) if m != 64 => (((v as u32) * (m as u32)) >> 6).min(64) as u8,
_ => v.min(64),
}
};
let si = env.step_interval as usize + 1; let total = (env.steps.len() * si).clamp(1, 256);
let mut point = Vec::with_capacity(total);
for f in 0..total {
let k = (f / si).min(env.steps.len() - 1);
point.push(EnvelopePoint {
frame: f,
value: EnvValue::from_byte_64(scale(env.steps[k])),
});
}
let last = point.len() - 1;
Some(Envelope {
enabled: true,
point,
sustain_enabled: true,
sustain_start_point: last,
sustain_end_point: last,
loop_enabled: false,
loop_start_point: 0,
loop_end_point: 0,
})
}
#[allow(clippy::too_many_arguments)] fn resolve_synth_instrument(
instruments: &mut Vec<Instrument>,
cache: &mut BTreeMap<(usize, Option<u16>, Option<u16>), usize>,
samples: &[DwSample],
arpeggios: &[DwArpeggio],
volume_envelopes: &[DwVolumeEnvelope],
master_volume: Option<u16>,
sample_index: usize,
arp_index: Option<u16>,
env_index: Option<u16>,
) -> usize {
let base = sample_index.min(samples.len().saturating_sub(1));
let pitch_env = arp_index
.and_then(|a| arpeggios.get(a as usize))
.and_then(arpeggio_to_pitch_envelope);
let vol_env = env_index
.and_then(|e| volume_envelopes.get(e as usize))
.and_then(|e| volume_to_volume_envelope(e, master_volume));
if pitch_env.is_none() && vol_env.is_none() {
return base;
}
let key = (
base,
arp_index.filter(|_| pitch_env.is_some()),
env_index.filter(|_| vol_env.is_some()),
);
if let Some(&idx) = cache.get(&key) {
return idx;
}
let Some(s) = samples.get(base) else {
return base;
};
let mut instr = sample_to_instrument(s);
if let InstrumentType::Default(ref mut d) = instr.instr_type {
if let Some(pe) = pitch_env {
d.voice.pitch_envelope = pe;
d.voice.pitch_envelope_as_low_pass_filter = false;
}
if let Some(ve) = vol_env {
d.voice.volume_envelope = ve;
if let Some(Some(smp)) = d.sample.get_mut(0) {
smp.volume = ChannelVolume::from_byte_64(64);
}
}
}
let mut tag = alloc::string::String::new();
if let Some(a) = key.1 {
tag.push_str(&format!(" +arp{}", a));
}
if let Some(e) = key.2 {
tag.push_str(&format!(" +env{}", e));
}
instr.name = format!("{}{}", instr.name, tag);
let idx = instruments.len();
instruments.push(instr);
cache.insert(key, idx);
idx
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_garbage() {
let buf = [0xFFu8; 256];
assert!(matches!(
DwModule::load(&buf),
Err(ImportError::InvalidMagic("dw_detect"))
));
}
#[test]
fn rejects_truncated() {
assert!(DwModule::load(&[0u8; 16]).is_err());
}
}