pub struct DwModule {Show 19 fields
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; 4],
pub all_position_lists: Vec<[DwPositionList; 4]>,
pub tracks: Vec<DwTrack>,
pub volume_envelopes: Vec<DwVolumeEnvelope>,
pub arpeggios: Vec<DwArpeggio>,
pub dispatcher: DwDispatcher,
pub features: DwFeatures,
pub volume_bracket_is_pitch: bool,
pub channel_volumes: [u16; 4],
pub master_volume: Option<u16>,
pub use_pitch_arpeggio: bool,
pub use_arpeggio_pitch_lane: bool,
}Expand description
Parsed contents of a .dw file. Self-sufficient — once a
DwModule is built the original payload bytes can be dropped.
Fields§
§variant: DwVariantReplayer family the file targets.
period_table: PeriodTableWhich period table the replayer uses.
period_via_finetune: booltrue when AUDxPER is computed the new-player way — full-range
note index into Self::period_table scaled by the per-sample
fine-tune (table[note] × mult >> 10) — rather than the qball-era
one-octave composite PERIODS1[note % 12]. Always true on the
new player and on the old-stream players that carry the fine-tune
idiom (leviathan, empire); false on genuine qball composite
players. See super::detect::DwLayout::period_via_finetune.
samples: Vec<DwSample>The full sample bank. Indices match the on-disk SetSample
command argument minus the per-module newSampleCmd
threshold (which the runtime layer applies).
sub_song: Option<DwSubSong>Currently selected sub-song descriptor. None when no
usable sub-song could be located.
Multi-song .dw files are common: most game soundtracks
ship every track in a single binary, indexed by a small
integer the host passes via D0. The replayer multiplies
that index by the row width and uses the result as offset
into the sub-song table. The importer parses every entry
it can validate into Self::sub_songs, then picks the
one most likely to be the “main” song (heuristic — see
pick_main_sub_song_index). Callers that want a
different sub-song can read Self::sub_songs and
re-derive position lists via crate::tracker::import::dw::dw_module::position_lists_for.
sub_songs: Vec<DwSubSong>Every sub-song the importer could decode from the
sub_song_list_offset row table. Order matches the
on-disk index (sub-song 0 = first row); the chosen
“primary” sub-song is exposed separately as
Self::sub_song / Self::selected_sub_song. Empty
when no sub-song header was located.
selected_sub_song: Option<usize>Index into Self::sub_songs of the sub-song mirrored
into Self::sub_song / Self::position_lists.
None when no sub-song is available.
position_lists: [DwPositionList; 4]One position list per Paula channel for the selected sub-song, in channel order. Empty when the sub-song header could not be located or when the channel is intentionally silent for this song.
all_position_lists: Vec<[DwPositionList; 4]>Position lists for every sub-song, indexed to match
Self::sub_songs. Self::position_lists is a clone of
all_position_lists[selected_sub_song]. The DAW projection
walks this to render each sub-song as its own xmrs song.
tracks: Vec<DwTrack>Every distinct track byte stream referenced by at least one
position list, parsed up to its EndOfTrack terminator.
De-duplicated by file offset so a single track played by
many channels / many times contributes a single entry.
volume_envelopes: Vec<DwVolumeEnvelope>Per-channel volume envelopes — indexed by (track_byte - volume_envelope_threshold) at runtime. Empty when the module
either lacks the 0xA0.. dispatcher bracket or when the
table-base probe couldn’t locate the envelope-pointer
table. See DwVolumeEnvelope for the per-entry shape.
arpeggios: Vec<DwArpeggio>Per-channel pitch arpeggios — indexed by (track_byte - pitch_arpeggio_threshold) at runtime (the 0x90.. dispatcher
bracket). Empty when the module has no 0x90.. bracket or
the table-base probe failed. See DwArpeggio.
dispatcher: DwDispatcherPer-module command dispatcher thresholds — kept on the
DwModule so the simulator can resolve raw track bytes
(e.g. 0xA5) back to envelope-table indices without
re-reading the binary.
features: DwFeaturesDetect-time feature flags lifted directly from
super::detect::DwLayout::features. The simulator gates
module-dependent semantics (e.g. Effect8 → channel
transpose vs. global volume fade) on these.
volume_bracket_is_pitch: boolWhen true, the SetVolumeEnvelope dispatcher bracket arms a
pitch arpeggio (indexing Self::arpeggios) instead of
a volume envelope — see
super::detect::DwLayout::volume_bracket_is_pitch. tetris.dw
is the canonical case.
channel_volumes: [u16; 4]Old-player per-channel static volume (0..=64), indexed by
Paula channel. The qball-era replayer loads this into
AUDxVOL on every note trigger and never scales it by a
sample or envelope value — it is the channel’s loudness.
Read from super::detect::DwLayout::channel_volume_offset;
defaults to full scale on new-player modules (whose loudness
comes from the volume envelope) and when the table couldn’t be
located.
master_volume: Option<u16>Static global master volume (1..=64), or None when the
module has no master-volume scaling. The empire-era replayer
multiplies every Paula volume by this before a >> 6
(AUDxVOL = volByte × master >> 6), at both note-trigger and
per-tick envelope animation; the importer reproduces it by
scaling all emitted Paula volumes by master / 64. Read from
super::detect::DwLayout::master_volume_offset. None leaves
volumes unscaled (the common case — new players use the
identity master = 64, qball writes channel volumes raw).
use_pitch_arpeggio: boolWhen true, the volume envelope is baked per-tick on a
synthetic per-(sample, envelope) instrument (frame-resolution
loudness), as opposed to the row-resolution cell Volume
animation. Set for the jump-table (command_map) family —
bubble bobble & kin. (Historically this flag also drove the
pitch arpeggio via a looping pitch envelope; that role has
moved to the per-frame use_arpeggio_pitch_lane Points lane,
so this now governs only the volume-envelope split.)
use_arpeggio_pitch_lane: boolWhen true, per-frame pitch arpeggios are baked as a
TrackPitch LaneKind::Points curve of
crate::core::daw::automation::AutomationValue::Pitch points
— one per arpeggio-offset change, evaluated every player tick.
This is the faithful projection of the replayer’s per-channel,
per-frame arpeggio pointer (Ghidra play_tick held-note
branch: offset added to the period-table index = semitone
space, pointer continuous across notes), as opposed to the
classic three-step TrackEffect::Arpeggio (mod-3, trigger-row
only) or the old per-note pitch-envelope path. Set for every
DW module — the lane is the single arpeggio path for DW, so the
classic effect is suppressed and the arpeggio segment-split /
pitch-envelope are skipped. No-op for modules that never arm an
arpeggio (no points emitted). (use_pitch_arpeggio now governs
only the volume-envelope synthetic-instrument path, not the
arpeggio.)
Implementations§
Source§impl DwModule
impl DwModule
Sourcepub fn load(source: &[u8]) -> Result<Self, ImportError>
pub fn load(source: &[u8]) -> Result<Self, ImportError>
Parse a .dw payload. Returns InvalidMagic("dw_detect")
when the payload does not look like a David Whittaker
module.
Sourcepub fn to_module(&self) -> Module
pub fn to_module(&self) -> Module
Convert into the editor-friendly Module representation.
Populates the full DAW layer: the instrument bank (one
InstrDefault per parsed sample), one Track::Notes
per (Paula channel, current sample) run discovered by
the simulator, matching Clips placed on each channel
lane, and a TimelineMap covering the song length
yielded by Self::simulate. After segment creation
the standard xmrs crate::tracker::import::build::dedupe_tracks_by_content
pass fuses bit-identical segments so a single phrase
shared by several channels lives in one Track.
Quantisation: one row = one channel speed-tick (speed
frames at 50 Hz PAL). Notes whose Whittaker byte exceeds
the 120-position xmrs Pitch range are clamped to the
top — DW’s period tables cover at most 6 octaves while
xmrs has 10, so the clamp only fires on out-of-band
values.