1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use crate::{
cell_note::CellNote,
effect::{GlobalEffect, TrackEffect},
fixed::units::Volume,
};
use core::fmt::*;
use serde::{Deserialize, Serialize};
use alloc::vec;
use alloc::vec::Vec;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TrackUnit {
pub note: CellNote,
/// Note-trigger velocity, Q1.15 in `[0, 1]`. Was `f32`.
/// MIDI-style: `Volume::FULL` for a normal trigger,
/// `Volume::SILENT` is a silent trigger.
pub velocity: Volume,
pub instrument: Option<usize>,
pub effects: Vec<TrackEffect>,
pub global_effects: Vec<GlobalEffect>,
}
impl Default for TrackUnit {
fn default() -> Self {
Self {
note: CellNote::default(),
velocity: Volume::FULL,
instrument: None,
effects: vec![],
global_effects: vec![],
}
}
}
impl TrackUnit {
pub fn has_arpeggio(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::Arpeggio { half1: _, half2: _ }))
}
pub fn has_delay(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteDelay(_)))
}
pub fn get_delay(&self) -> usize {
self.effects
.iter()
.find_map(|effect| {
if let TrackEffect::NoteDelay(delay) = effect {
Some(*delay)
} else {
None
}
})
.unwrap_or(0)
}
pub fn has_note_off(&self) -> bool {
let fx = self
.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteOff { tick: _, past: _ }));
fx || matches!(self.note, CellNote::KeyOff)
}
/// Returns true iff the slot carries a Kxx *effect* note-off.
///
/// Unlike [`Self::has_note_off`], this does NOT match a note-column
/// `===` (`CellNote::KeyOff`). It's intended for replayer paths
/// that need to apply FT2 Kxy-effect quirks without also misfiring
/// on note-column key-offs, which have different playback
/// semantics in FT2 (see `ft2_replayer.c:keyOff()` vs. the
/// `keyOffCmd` / K00 paths).
pub fn has_note_off_effect(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteOff { tick: _, past: _ }))
}
/// Returns true iff the slot carries a Kxx *effect* note-off whose
/// trigger tick is 0 — i.e. specifically K00, not K01..K1F.
///
/// Used by the FT2 "K00 eats note" quirk: in FT2, only K00 is handled
/// at tickZero via the special branch in `getNewNote`, and this is
/// what causes the note column to be skipped. Non-zero Kxy effects go
/// through the normal note trigger path and must not short-circuit it.
pub fn has_note_off_at_tick_zero(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::NoteOff { tick: 0, past: _ }))
}
pub fn has_tone_portamento(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::TonePortamento(_)))
}
pub fn has_vibrato(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::Vibrato { speed: _, depth: _ }))
}
/// Returns true iff the slot carries a standalone `VibratoDepth`
/// effect (as produced by XM vol-column Bx when no main-effect 4xx
/// is also on the row). Used by the FT2 vol-col-B quirk to keep the
/// vibrato LFO progressing across ticks even without an accompanying
/// main Vibrato effect.
pub fn has_vibrato_depth(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::VibratoDepth(_)))
}
pub fn has_volume_slide(&self) -> bool {
self.effects
.iter()
.any(|effect| matches!(effect, TrackEffect::VolumeSlide { speed: _, fine: _ }))
}
pub fn has_global_volume_slide(&self) -> bool {
self.global_effects
.iter()
.any(|effect| matches!(effect, GlobalEffect::VolumeSlide { speed: _, fine: _ }))
}
}