Skip to main content

nord_format/formats/ne5/program/
effects.rs

1//! The effects panel — the four effect slots, the reverb, the rotary and the EQ.
2
3use crate::bits::Packed;
4use crate::components::sparse_enum;
5use crate::components::{EqBand, Frequency, Rate, RotorSpeed, Time};
6use crate::formats::ne5::Level;
7use crate::types::RangedU8;
8use nord_bits_derive::bitbody;
9
10use std::fmt::{self, Display, Formatter};
11
12// 0x93..=0xa4 — the effects panel.
13
14/// The effects panel: the four effect slots, the reverb, the rotary and the
15/// EQ.
16#[bitbody(18)]
17#[derive(Default)]
18pub struct EffectsPanel {
19    #[bits(0..=1)]
20    pub fx1: Routing,
21    #[bits(2..=5)]
22    pub fx1_type: Fx1Type,
23    /// The effect's rate, in hertz. ⚠️ Not a [`Level`]: the panel does not read this on
24    /// its 0..10 scale, which is why it is a [`Rate`].
25    #[bits(6..=12)]
26    pub fx1_rate: Rate,
27    #[bits(13..=14)]
28    pub fx2: Routing,
29    #[bits(15..=18)]
30    pub fx2_type: Fx2Type,
31    #[bits(19..=25)]
32    pub fx2_rate: Rate,
33    #[bits(26..=27)]
34    pub fx4: Routing,
35    #[bits(28..=29)]
36    pub fx4_feedback: RangedU8<3>,
37    /// Delay time. ⚠️ Runs **backwards**: the panel reads 750 ms at 0 and 20 ms at 127,
38    /// so this is not on the 0..10 scale and not monotonic with it either.
39    #[bits(30..=36)]
40    pub fx4_tempo: Time,
41    /// Delay wet/dry.
42    #[bits(37..=43)]
43    pub fx4_moisture: Level,
44    #[bits(44..=44)]
45    pub fx4_ping_pong: bool,
46    /// EQ engaged.
47    #[bits(45..=45)]
48    pub equalizer_on: bool,
49    /// Which part the equalizer applies to. Whether it is engaged at all is the separate
50    /// bit above.
51    #[bits(117..=118)]
52    pub equalizer_part: EqualizerPart,
53    /// The sweepable mid frequency, in hertz.
54    #[bits(47..=53)]
55    pub equalizer_freq: Frequency,
56    #[bits(54..=60)]
57    pub equalizer_treble: EqBand,
58    /// The mid band's boost/cut. ⚠️ Bipolar — its musical zero is the centre of the
59    /// slot, so the 0..10 reading a [`Level`] prints would show a cut as a small boost.
60    #[bits(61..=67)]
61    pub equalizer_freq_gain: EqBand,
62    #[bits(68..=74)]
63    pub equalizer_bass: EqBand,
64    #[bits(75..=76)]
65    pub fx3: Routing,
66    #[bits(77..=79)]
67    pub fx3_type: Fx3Type,
68    #[bits(80..=86)]
69    pub fx3_compression: Level,
70    #[bits(87..=87)]
71    pub fx5: bool,
72    #[bits(88..=90)]
73    pub fx5_type: Fx5Type,
74    #[bits(91..=97)]
75    pub fx5_moisture: Level,
76    #[bits(98..=98)]
77    pub rotary_stop: bool,
78    #[bits(99..=99)]
79    pub rotary_speed: RotorSpeed,
80    /// fx1 control pedal.
81    #[bits(115..=115)]
82    pub fx1_control: bool,
83    /// fx2 deep.
84    #[bits(116..=116)]
85    pub fx2_deep: bool,
86}
87
88/// Which part an effect is routed to.
89///
90/// The stored encoding is not the panel's numbering: off agrees at `0`, but the two
91/// engaged positions are `2` and `3`.
92///
93/// | stored | 0 | 1 | 2 | 3 |
94/// |---|---|---|---|---|
95/// | | off | [`Unknown`](Self::Unknown) | lower | upper |
96///
97/// Total over two bits, so decoding cannot fail.
98#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub enum Routing {
100    #[default]
101    Off = 0,
102    /// Off, as older firmware spelled it. Confirmed on hardware. It presents as off
103    /// (no light, no effect), and a front-panel store preserves it byte-for-byte
104    /// while the current panel's own off-writes are `0`. That it was the pre-2.04
105    /// encoding: Inferred from specimens; not confirmed on hardware. Every carrier
106    /// is a factory program or predates the instrument's 2.04 update.
107    Unknown = 1,
108    Lower = 2,
109    Upper = 3,
110}
111
112impl Routing {
113    /// Which part the effect actually reaches, or `None` when it is not engaged.
114    pub fn part(&self) -> Option<&'static str> {
115        match self {
116            Routing::Lower => Some("lower"),
117            Routing::Upper => Some("upper"),
118            Routing::Off | Routing::Unknown => None,
119        }
120    }
121
122    /// Whether this is the value with no known meaning. Unlike the sparse enumerations,
123    /// this one does occur in practice.
124    pub fn is_unknown(&self) -> bool {
125        matches!(self, Routing::Unknown)
126    }
127}
128
129impl Packed for Routing {
130    const MAX_BITS: u32 = 2;
131    const DECODE_BITS: u32 = 2;
132    const CONTROL: crate::fields::ControlKind = crate::fields::ControlKind::Selector;
133    type Error = std::convert::Infallible;
134
135    fn from_bits(bits: u64) -> Result<Self, Self::Error> {
136        Ok(match bits & 0b11 {
137            0 => Routing::Off,
138            1 => Routing::Unknown,
139            2 => Routing::Lower,
140            _ => Routing::Upper,
141        })
142    }
143
144    fn to_bits(&self) -> u64 {
145        *self as u64
146    }
147}
148
149impl Display for Routing {
150    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
151        match self {
152            Routing::Off => f.write_str("off"),
153            Routing::Unknown => f.write_str("unknown (1)"),
154            Routing::Lower => f.write_str("lower"),
155            Routing::Upper => f.write_str("upper"),
156        }
157    }
158}
159
160sparse_enum!(
161    /// Effect 1's modulation type.
162    ///
163    /// Values are the ones **as stored**, which is rotated relative to the panel's own
164    /// ordering — stored 0 is trem 1, not pan 1. Inferred from specimens; not confirmed
165    /// on hardware. Each specimen is named for the panel setting it was stored from.
166    Fx1Type, 4, {
167        0 => Trem1, "trem 1";
168        1 => Trem2, "trem 2";
169        2 => Trem1And2, "trem 1&2";
170        3 => Pan1, "pan 1";
171        4 => Pan2, "pan 2";
172        5 => Pan1And2, "pan 1&2";
173        6 => Wah, "wah";
174        7 => Rm, "rm";
175    }
176);
177
178sparse_enum!(
179    /// Effect 2's modulation type.
180    Fx2Type, 4, {
181        0 => Phaser1, "phaser 1";
182        1 => Phaser2, "phaser 2";
183        2 => Flanger, "flanger";
184        3 => Chorus1, "chorus 1";
185        4 => Chorus2, "chorus 2";
186        5 => Vibe, "vibe";
187    }
188);
189
190sparse_enum!(
191    /// The speaker / amp simulation.
192    Fx3Type, 3, {
193        0 => None_, "none";
194        1 => Small, "small";
195        2 => Jc, "jc";
196        3 => Twin, "twin";
197        4 => Rotary, "rotary";
198        5 => Comp, "comp";
199    }
200);
201
202sparse_enum!(
203    /// The reverb algorithm.
204    Fx5Type, 3, {
205        0 => Room, "room";
206        1 => StageSoft, "stage soft";
207        2 => Stage, "stage";
208        3 => HallSoft, "hall soft";
209        4 => Hall, "hall";
210    }
211);
212
213sparse_enum!(
214    /// Which part the equalizer applies to. Whether it is engaged is a separate bit, so
215    /// `Lower` means lower, not off.
216    EqualizerPart, 2, {
217        0 => Lower, "lower";
218        1 => Upper, "upper";
219        2 => Both, "lower+upper";
220    }
221);
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    /// A value with no known meaning reads, writes back the same bits, and says so.
228    #[test]
229    fn an_unrecognized_value_survives_and_announces_itself() {
230        let unknown = Fx1Type::from_bits(9).unwrap();
231        assert_eq!(unknown, Fx1Type::Unknown(9));
232        assert!(unknown.is_unknown());
233        assert_eq!(unknown.label(), None);
234        assert_eq!(unknown.to_string(), "unknown (9)");
235        assert_eq!(unknown.to_bits(), 9, "an unknown value must round-trip");
236    }
237
238    /// Every named value round-trips, and none of them is reported as unknown.
239    #[test]
240    fn recovered_values_round_trip() {
241        for bits in 0..8u64 {
242            let t = Fx1Type::from_bits(bits).unwrap();
243            assert!(!t.is_unknown(), "{bits} should be recovered");
244            assert_eq!(t.to_bits(), bits);
245        }
246        for bits in 0..3u64 {
247            assert_eq!(EqualizerPart::from_bits(bits).unwrap().to_bits(), bits);
248        }
249    }
250
251    #[test]
252    fn routing_matches_what_the_instrument_stores() {
253        assert_eq!(Routing::from_bits(0).unwrap(), Routing::Off);
254        assert_eq!(Routing::from_bits(1).unwrap(), Routing::Unknown);
255        assert_eq!(Routing::from_bits(2).unwrap(), Routing::Lower);
256        assert_eq!(Routing::from_bits(3).unwrap(), Routing::Upper);
257
258        for bits in 0..4u64 {
259            assert_eq!(Routing::from_bits(bits).unwrap().to_bits(), bits);
260        }
261
262        // The panel's three positions are 0, 1 and 2; the engaged two store 2 and 3.
263        assert_eq!(Routing::Lower.to_bits(), 2);
264        assert_eq!(Routing::Upper.to_bits(), 3);
265
266        // The unknown state must not render as `off`.
267        assert_eq!(Routing::Off.to_string(), "off");
268        assert_eq!(Routing::Unknown.to_string(), "unknown (1)");
269        assert!(Routing::Unknown.is_unknown());
270        assert!(!Routing::Off.is_unknown());
271        assert_eq!(Routing::Unknown.part(), None);
272    }
273}