nord_format/formats/ns3/program.rs
1//! The Stage 3 program body (`.ns3f`, `.ns3l`): 548 bytes, every documented
2//! parameter placed.
3//!
4//! The program-wide globals were decoded first and by hand; everything else — the
5//! organ's two presets and their drawbars, the piano, synth, extern and the whole
6//! effects chain — comes from the byte maps. Where the two disagree the hand
7//! decode wins: the map has one 22-bit `split` run where the companion doc, and
8//! this module, break it into ten fields.
9//!
10//! The body is 22 bytes of globals and then two [`Panel`]s — the program's two
11//! complete setups — so the panel is declared once and placed twice rather than
12//! spelled out either side. Registry paths follow: `panel_a.organ_type`.
13//!
14//! Values are raw except where the documentation enumerates them; see [the module
15//! docs](super) for what that ceiling is and why.
16
17use super::panel::Panel;
18use crate::cbin::{self, Cbin};
19use crate::components::{
20 sparse_enum, Level, MasterTempo, RotorSpeed, Selector, SplitNote, SplitWidth, StageTranspose,
21 SwitchMorph,
22};
23use crate::error::Error;
24use std::io::{Read, Seek};
25
26pub const FORMAT: &str = "ns3f";
27/// Schema versions this build's field offsets have been validated against:
28/// program v3.00 (OS v0.92) through v3.04 (OS v2.10 and later), stored ×100.
29pub const KNOWN_VERSIONS: &[u32] = &[300, 301, 302, 303, 304];
30pub const BODY_LEN: usize = 548;
31
32/// The Stage 3's octave shift: a nibble biased by 6.
33///
34/// **Corpus:** over the factory banks the slot holds 3..=9 with a decisive mode at 6,
35/// which is where an untransposed program has to sit. The bias differs per model — the
36/// Stage 2 centres on 7 and the Stage 4 stores two's complement — so each names its own.
37/// Inferred from specimens; not confirmed on hardware.
38///
39/// Total over the nibble: the widest encoding is `9 + 6 = 15`, so no stored pattern is
40/// refused.
41pub type OctaveShift = crate::components::OctaveShift<6, -6, 9>;
42
43sparse_enum!(
44 /// Which panels the program enables.
45 PanelEnable, 2, {
46 0 => AOnly, "A only";
47 1 => BOnly, "B only";
48 2 => Both, "A & B";
49 }
50);
51
52sparse_enum!(
53 /// What the second keyboard plays when Dual Keyboard is on.
54 DualKeyboardStyle, 2, {
55 0 => Panel, "Panel";
56 1 => Organ, "Organ";
57 2 => Piano, "Piano";
58 3 => Synth, "Synth";
59 }
60);
61
62/// The program-wide globals at the head of the body. Bits are MSB-first from body
63/// byte 0 (`0x2c` in a type-1 file), so byte 0x05 bit 7 is bit 40.
64///
65/// Reads and writes byte-exactly. A read verifies the container checksum, gates
66/// on [`KNOWN_VERSIONS`], and range-checks every field; unclaimed bits survive a
67/// re-encode verbatim. Placements from the community byte maps; values raw except
68/// where those maps enumerate them. Inferred from specimens; not confirmed on
69/// hardware.
70///
71/// ⚠️ The three split notes can be stored out of order — the panel reorders them on
72/// display (documented with specimens in the ns3-program-viewer sources). The
73/// decode reports what is stored.
74#[nord_bits_derive::bitbody(548)]
75pub struct Program {
76 #[bits(40..=40)]
77 pub panel_b_selected: bool,
78 #[bits(41..=42)]
79 pub panel_enable: PanelEnable,
80 #[bits(43..=43)]
81 pub split_enabled: bool,
82 #[bits(44..=44)]
83 pub split_low_enabled: bool,
84 #[bits(45..=45)]
85 pub split_mid_enabled: bool,
86 #[bits(46..=46)]
87 pub split_high_enabled: bool,
88 #[bits(47..=50)]
89 pub split_low_note: SplitNote,
90 #[bits(51..=54)]
91 pub split_mid_note: SplitNote,
92 #[bits(55..=58)]
93 pub split_high_note: SplitNote,
94 #[bits(59..=60)]
95 pub split_low_width: SplitWidth,
96 #[bits(61..=62)]
97 pub split_mid_width: SplitWidth,
98 #[bits(63..=64)]
99 pub split_high_width: SplitWidth,
100 #[bits(65..=66)]
101 pub piano_layer_detune: PianoLayerDetune,
102 #[bits(67..=67)]
103 pub organ_pitch_stick: bool,
104 #[bits(68..=70)]
105 pub organ_vibrato_mode: OrganVibratoMode,
106 #[bits(71..=71)]
107 pub rotary_speaker_speed: RotorSpeed,
108 #[bits(72..=72)]
109 pub rotary_speaker_stop_mode: bool,
110 #[bits(73..=75)]
111 pub rotary_speaker_speed_wheel: SwitchMorph,
112 #[bits(76..=78)]
113 pub rotary_speaker_speed_aftertouch: SwitchMorph,
114 #[bits(79..=81)]
115 pub rotary_speaker_speed_ctrl_pedal: SwitchMorph,
116 #[bits(96..=96)]
117 pub transpose_enabled: bool,
118 #[bits(97..=100)]
119 pub transpose: StageTranspose,
120 #[bits(101..=108)]
121 pub master_clock: MasterTempo,
122 #[bits(109..=115)]
123 pub rotary_speaker_drive: Level,
124 #[bits(116..=116)]
125 pub dual_keyboard: bool,
126 #[bits(118..=119)]
127 pub dual_keyboard_style: DualKeyboardStyle,
128 #[bits(120..=123)]
129 pub synth_pitch_stick_range: Selector<4>,
130
131 /// Panel A — the first of the program's two complete setups.
132 #[at(22..285)]
133 pub panel_a: Panel,
134
135 /// Panel B. Same type: the two are the same layout, and neither is
136 /// a copy of the other — `panel_enable` says which sound.
137 #[at(285..548)]
138 pub panel_b: Panel,
139}
140
141/// The `(bank, location)` pair from the header, uninterpreted.
142///
143/// Not validated: current exports hold bank 0..=15 and location 0..=24, but v3.00
144/// files in the wild hold out-of-range locations (norduserforum.com t=14414), so
145/// gating on them would refuse real files.
146pub fn location(file: &Cbin<Program>) -> (u16, u16) {
147 file.header.slot()
148}
149
150pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Program>, Error> {
151 let file: Cbin<Program> = cbin::read(reader, FORMAT)?;
152 crate::formats::known_version(FORMAT, file.header.version, KNOWN_VERSIONS)?;
153 Ok(file)
154}
155
156sparse_enum!(
157 /// From the `ns3-amp-sim-eq-amp-type` table in the Stage byte-map docs. Inferred from
158 /// specimens; not confirmed on hardware.
159 AmpSimEqAmpType, 3, {
160 0 => Clean, "Clean";
161 1 => Twin, "Twin";
162 2 => Jc, "JC";
163 3 => Small, "Small";
164 4 => Lp24, "LP24";
165 5 => Hp24, "HP24";
166 }
167);
168
169sparse_enum!(
170 /// From the `ns3-clavinet-model` table in the Stage byte-map docs. Inferred from
171 /// specimens; not confirmed on hardware.
172 ClavinetModel, 2, {
173 0 => Ca, "CA";
174 1 => Cb, "CB";
175 2 => Da, "DA";
176 3 => Db, "DB";
177 }
178);
179
180sparse_enum!(
181 /// From the `ns3-organ-type` table in the Stage byte-map docs. Inferred from
182 /// specimens; not confirmed on hardware.
183 OrganType, 3, {
184 0 => B3, "B3";
185 1 => Vox, "Vox";
186 2 => Farfisa, "Farfisa";
187 3 => Pipe1, "Pipe1";
188 4 => Pipe2, "Pipe2";
189 }
190);
191
192sparse_enum!(
193 /// The organ's vibrato/chorus selection — the six [`VibChorus`] modes, in the order
194 /// the `ns3-organ-vibrato-mode` table in the Stage byte-map docs stores them.
195 /// Inferred from specimens; not confirmed on hardware.
196 ///
197 /// [`VibChorus`]: crate::components::VibChorus
198 OrganVibratoMode, 3, {
199 0 => V1, "V1";
200 1 => C1, "C1";
201 2 => V2, "V2";
202 3 => C2, "C2";
203 4 => V3, "V3";
204 5 => C3, "C3";
205 }
206);
207
208sparse_enum!(
209 /// From the `ns3-piano-kb-touch` table in the Stage byte-map docs. Inferred from
210 /// specimens; not confirmed on hardware.
211 PianoKbTouch, 2, {
212 0 => Normal, "Normal";
213 1 => KbTouch1, "KB Touch 1";
214 2 => Touch2, "Touch 2";
215 3 => Touch3, "Touch 3";
216 }
217);
218
219sparse_enum!(
220 /// From the `ns3-piano-layer-detune` table in the Stage byte-map docs. Inferred from
221 /// specimens; not confirmed on hardware.
222 PianoLayerDetune, 2, {
223 0 => V0, "Off";
224 1 => V1, "1";
225 2 => V2, "2";
226 3 => V3, "3";
227 }
228);
229
230sparse_enum!(
231 /// From the `ns3-piano-timbre` table in the Stage byte-map docs. Inferred from
232 /// specimens; not confirmed on hardware.
233 PianoTimbre, 3, {
234 0 => None, "None";
235 1 => Soft, "Soft";
236 2 => Treble, "Treble";
237 3 => SoftTreble, "Soft+Treble";
238 4 => Brilliant, "Brilliant";
239 5 => SoftBrill, "Soft+Brill";
240 6 => TrebleBrill, "Treble+Brill";
241 7 => SoftTrbBrill, "Soft+Trb+Brill";
242 }
243);
244
245sparse_enum!(
246 /// From the `ns3-piano-type` table in the Stage byte-map docs. Inferred from
247 /// specimens; not confirmed on hardware.
248 PianoType, 3, {
249 0 => Grand, "Grand";
250 1 => Upright, "Upright";
251 2 => Electric, "Electric";
252 3 => Clav, "Clav";
253 4 => Digital, "Digital";
254 5 => Misc, "Misc";
255 }
256);
257
258sparse_enum!(
259 /// From the `ns3-synth-amp-env-velocity` table in the Stage byte-map docs. Inferred from
260 /// specimens; not confirmed on hardware.
261 SynthAmpEnvVelocity, 2, {
262 0 => V0, "Off";
263 1 => V1, "1";
264 2 => V2, "2";
265 3 => V3, "3";
266 }
267);
268
269sparse_enum!(
270 /// From the `ns3-synth-arp-pattern` table in the Stage byte-map docs. Inferred from
271 /// specimens; not confirmed on hardware.
272 SynthArpPattern, 2, {
273 0 => Up, "Up";
274 1 => Down, "Down";
275 2 => UpDown, "Up/Down";
276 3 => Random, "Random";
277 }
278);
279
280sparse_enum!(
281 /// From the `ns3-synth-arp-range` table in the Stage byte-map docs. Inferred from
282 /// specimens; not confirmed on hardware.
283 SynthArpRange, 2, {
284 0 => V1Octave, "1 Octave";
285 1 => V2Octaves, "2 Octaves";
286 2 => V3Octaves, "3 Octaves";
287 3 => V4Octaves, "4 Octaves";
288 }
289);
290
291sparse_enum!(
292 /// From the `ns3-synth-filter-drive` table in the Stage byte-map docs. Inferred from
293 /// specimens; not confirmed on hardware.
294 SynthFilterDrive, 2, {
295 0 => V0, "Off";
296 1 => V1, "1";
297 2 => V2, "2";
298 3 => V3, "3";
299 }
300);
301
302sparse_enum!(
303 /// From the `ns3-synth-filter-kb-track` table in the Stage byte-map docs. Inferred from
304 /// specimens; not confirmed on hardware.
305 SynthFilterKbTrack, 2, {
306 0 => V0, "Off";
307 1 => V1, "1/3";
308 2 => V2, "2/3";
309 3 => V3, "1";
310 }
311);
312
313sparse_enum!(
314 /// From the `ns3-synth-filter-type` table in the Stage byte-map docs. Inferred from
315 /// specimens; not confirmed on hardware.
316 SynthFilterType, 3, {
317 0 => Lp12, "LP12";
318 1 => Lp24, "LP24";
319 2 => MiniMoog, "Mini Moog";
320 3 => LpHp, "LP+HP";
321 4 => Bp24, "BP24";
322 5 => Hp24, "HP24";
323 }
324);
325
326sparse_enum!(
327 /// From the `ns3-synth-lfo-wave` table in the Stage byte-map docs. Inferred from
328 /// specimens; not confirmed on hardware.
329 SynthLfoWave, 3, {
330 0 => Triangle, "Triangle";
331 1 => Saw, "Saw";
332 2 => NegSaw, "Neg Saw";
333 3 => Square, "Square";
334 4 => SH, "S/H";
335 }
336);
337
338sparse_enum!(
339 /// From the `ns3-synth-oscillator-config` table in the Stage byte-map docs. Inferred from
340 /// specimens; not confirmed on hardware.
341 SynthOscillatorConfig, 4, {
342 0 => None, "None";
343 1 => Pitch, "Pitch";
344 2 => Shape, "Shape";
345 3 => Sync, "Sync";
346 4 => Detune, "Detune";
347 5 => Mixsin, "MixSin";
348 6 => Mixtri, "MixTri";
349 7 => Mixsaw, "MixSaw";
350 8 => Mixsqr, "MixSqr";
351 9 => Mixbell, "MixBell";
352 10 => Mixns1, "MixNs1";
353 11 => Mixns2, "MixNs2";
354 12 => Fm1, "FM1";
355 13 => Fm2, "FM2";
356 14 => Rm, "RM";
357 }
358);
359
360sparse_enum!(
361 /// From the `ns3-synth-oscillator-type` table in the Stage byte-map docs. Inferred from
362 /// specimens; not confirmed on hardware.
363 SynthOscillatorType, 3, {
364 0 => Classic, "Classic";
365 1 => Wave, "Wave";
366 2 => Formant, "Formant";
367 3 => Super, "Super";
368 4 => Sample, "Sample";
369 }
370);
371
372sparse_enum!(
373 /// From the `ns3-synth-unison` table in the Stage byte-map docs. Inferred from
374 /// specimens; not confirmed on hardware.
375 SynthUnison, 2, {
376 0 => V0, "Off";
377 1 => V1, "1";
378 2 => V2, "2";
379 3 => V3, "3";
380 }
381);
382
383sparse_enum!(
384 /// From the `ns3-synth-vibrato` table in the Stage byte-map docs. Inferred from
385 /// specimens; not confirmed on hardware.
386 SynthVibrato, 3, {
387 0 => Off, "Off";
388 1 => Delay1, "Delay 1";
389 2 => Delay2, "Delay 2";
390 3 => Delay3, "Delay 3";
391 4 => Wheel, "Wheel";
392 5 => AfterTouch, "After Touch";
393 }
394);
395
396sparse_enum!(
397 /// From the `ns3-synth-voice` table in the Stage byte-map docs. Inferred from
398 /// specimens; not confirmed on hardware.
399 SynthVoice, 2, {
400 0 => Poly, "Poly";
401 1 => Legato, "Legato";
402 2 => Mono, "Mono";
403 }
404);