use nord_format::cbin::Cbin;
use nord_format::formats::ne5;
use nord_format::formats::ne5::{Instrument, OrganModel};
use nord_format::formats::nsmp::zone::VelocityWindow;
use nord_format::formats::nsmp::{codec, stroke, Chain, Sample};
use nord_format::formats::nsmpproj;
use nord_format::note;
use nord_format::{Entity, Live, Program, Settings, Song};
use crate::slot::shown_at;
use crate::ui::Ui;
fn category(header: &nord_format::cbin::Header) -> String {
match nord_format::components::ProgramCategory::of(header) {
Some(category) => format!("{category:?}"),
None => format!("none ({:#010x})", header.aux),
}
}
fn yn(b: bool) -> &'static str {
if b {
"yes"
} else {
"no"
}
}
pub(crate) fn dep_id(id: u32) -> String {
match id {
0 => "none".to_string(),
id => format!("{id:#010x}"),
}
}
const LABEL_WIDTH: usize = 11;
const FX_WIDTH: usize = 7;
const SETTING_WIDTH: usize = 27;
pub(crate) fn version_label(v: u32) -> String {
format!("{}.{:02}", v / 100, v % 100)
}
fn field(ui: &Ui, indent: usize, label: &str, value: impl std::fmt::Display) -> String {
let label = format!("{label}:");
format!(
"{:indent$}{}{value}",
"",
ui.dim(format!("{label:<LABEL_WIDTH$}"))
)
}
fn section(ui: &Ui, name: &str) {
ui.out("");
ui.out(format!(" {}", ui.heading(name)));
}
fn level(position: u8) -> char {
const LEVELS: [char; 9] = ['·', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
LEVELS[(position as usize).min(8)]
}
fn digits(positions: &[u8]) -> String {
positions.iter().map(u8::to_string).collect()
}
fn drawbars(ui: &Ui, positions: &[u8]) -> String {
let digits = digits(positions);
if !ui.unicode() {
return digits;
}
let chart: String = positions.iter().map(|&p| level(p)).collect();
format!("{chart} {}", ui.dim(digits))
}
fn panels(ui: &Ui, kind: &str, at: (u16, u16), p: &ne5::Program) {
ui.out(field(ui, 2, "type", kind));
ui.out(field(ui, 2, "location", shown_at(at.0, at.1)));
keyboard(ui, p);
voices(ui, p);
effects(ui, p);
organ(ui, p);
}
fn keyboard(ui: &Ui, p: &ne5::Program) {
let split = if p.center_panel.split {
format!("yes @ {:?}", p.center_panel.split_point)
} else {
"no".to_string()
};
section(ui, "Keyboard");
for (name, part, octave, sustain, control) in [
(
"lower",
p.center_panel.lower_part,
p.center_panel.lower_octave_shift.inner(),
p.center_panel.lower_sustain,
p.center_panel.lower_control,
),
(
"upper",
p.center_panel.upper_part,
p.center_panel.upper_octave_shift.inner(),
p.center_panel.upper_sustain,
p.center_panel.upper_control,
),
] {
ui.out(field(
ui,
4,
name,
format!(
"{part:?} {} {octave:+} {} {} {} {}",
ui.dim("octave"),
ui.dim("sustain"),
yn(sustain),
ui.dim("control"),
yn(control),
),
));
}
ui.out(field(ui, 4, "split", split));
let transpose = format!("{:+}", p.center_panel.transpose.inner());
ui.out(field(
ui,
4,
"transpose",
if p.center_panel.transpose_enabled {
format!("{transpose} {}", ui.dim("(on)"))
} else {
ui.dim(format!("{transpose} (off)"))
},
));
ui.out(field(
ui,
4,
"part mix",
format!("{} {}", p.center_panel.part_mix, ui.dim("(lower/upper %)")),
));
ui.out(field(ui, 4, "gain", p.center_panel.gain));
}
fn voices(ui: &Ui, p: &ne5::Program) {
section(ui, "Voices");
let (piano, sample) = (&p.piano_panel, &p.sample_panel);
ui.out(field(
ui,
4,
"piano",
format!(
"{} {} {} {} {} {} {} {} {} {} {}",
piano.category,
ui.dim("model"),
piano.piano_model.as_u8(),
ui.dim("clav"),
piano.clav_model.as_u8(),
ui.dim("acoustics"),
piano.acoustics.as_u8(),
ui.dim("touch"),
piano.touch.as_u8(),
ui.dim("mono"),
yn(piano.mono),
),
));
ui.out(field(
ui,
4,
"sample",
format!(
"{} {} {} {} {} {} {} {} {} {}",
ui.dim("number"),
sample.number,
ui.dim("attack"),
sample.attack,
ui.dim("decay/rel"),
sample.decay_release,
ui.dim("dynamics"),
sample.dynamics.as_u8(),
ui.dim("filter"),
yn(sample.filter),
),
));
ui.out(field(
ui,
4,
"depends",
format!(
"{} {} {} {}",
ui.dim("piano"),
dep_id(piano.id.id()),
ui.dim("sample"),
dep_id(sample.id.id()),
),
));
}
fn effects(ui: &Ui, p: &ne5::Program) {
let fx = &p.effects_panel;
section(ui, "Effects");
ui.out(format!(
" {}",
ui.dim("stored value, with the panel's reading where the scale is known")
));
let off = |name: &str, value: &dyn std::fmt::Display| {
ui.out(ui.dim(format!(" {name:<FX_WIDTH$}{value}")))
};
match fx.fx1.part() {
Some(part) => ui.out(format!(
" {:<FX_WIDTH$}{part:<5} {:<9} {} {} {} {}",
"fx1",
fx.fx1_type,
ui.dim("rate"),
fx.fx1_rate,
ui.dim("control"),
yn(fx.fx1_control),
)),
None => off("fx1", &fx.fx1),
}
match fx.fx2.part() {
Some(part) => ui.out(format!(
" {:<FX_WIDTH$}{part:<5} {:<9} {} {} {} {}",
"fx2",
fx.fx2_type,
ui.dim("rate"),
fx.fx2_rate,
ui.dim("deep"),
yn(fx.fx2_deep),
)),
None => off("fx2", &fx.fx2),
}
match fx.fx3.part() {
Some(part) => ui.out(format!(
" {:<FX_WIDTH$}{part:<5} {:<9} {} {}",
"fx3",
fx.fx3_type,
ui.dim("compression"),
fx.fx3_compression,
)),
None => off("fx3", &fx.fx3),
}
match fx.fx4.part() {
Some(part) => ui.out(format!(
" {:<FX_WIDTH$}{part:<5} {} {} {} {} {} {} {} {}",
"delay",
ui.dim("feedback"),
fx.fx4_feedback.as_u8(),
ui.dim("tempo"),
fx.fx4_tempo,
ui.dim("wet"),
fx.fx4_moisture,
ui.dim("ping-pong"),
yn(fx.fx4_ping_pong),
)),
None => off("delay", &fx.fx4),
}
if fx.fx5 {
ui.out(format!(
" {:<FX_WIDTH$}{:<15} {} {}",
"reverb",
fx.fx5_type,
ui.dim("wet"),
fx.fx5_moisture,
));
} else {
off("reverb", &"off");
}
if fx.equalizer_on {
let part = fx.equalizer_part;
ui.out(format!(
" {:<FX_WIDTH$}{part:<11} {} {} {} {} {} {} {} {}",
"eq",
ui.dim("bass"),
fx.equalizer_bass,
ui.dim("freq"),
fx.equalizer_freq,
ui.dim("gain"),
fx.equalizer_freq_gain,
ui.dim("treble"),
fx.equalizer_treble,
));
} else {
off("eq", &"off");
}
ui.out(format!(
" {:<FX_WIDTH$}{} {} {} {}",
"rotary",
ui.dim("speed"),
fx.rotary_speed,
ui.dim("stop"),
if fx.rotary_stop { "on" } else { "off" },
));
}
fn organ(ui: &Ui, p: &ne5::Program) {
if p.center_panel.lower_part == Instrument::Organ
|| p.center_panel.upper_part == Instrument::Organ
{
let o = &p.organ_panel;
let selected = p.center_panel.organ_type;
let sel_model = selected.storage();
section(ui, "Organ");
ui.out(format!(
" {}",
ui.dim(format!(
"{selected} selected (*), active preset (<), drawbar positions 0-8"
))
));
for (model, label) in [
(OrganModel::B3, "b3"),
(OrganModel::Vox, "vox"),
(OrganModel::Farfisa, "farf"),
(OrganModel::Pipe, "pipe"),
] {
for preset in [ne5::Preset::One, ne5::Preset::Two] {
let mark = if Some(model) == sel_model { "*" } else { " " };
let live = if o.preset(model) == preset { "<" } else { " " };
let bars = if selected.is_b3_bass()
&& model == OrganModel::B3
&& preset == ne5::Preset::One
{
let b = o.b3_bass_drawbars();
let plain = format!("{}{}.......", b[0], b[1]);
if ui.unicode() {
format!("{}{}······· {}", level(b[0]), level(b[1]), ui.dim(plain))
} else {
plain
}
} else if model == OrganModel::Farfisa {
let (on, off) = if ui.unicode() {
('█', '·')
} else {
('|', '.')
};
let tabs: String = o
.farfisa_tabs(preset)
.iter()
.map(|t| if *t { on } else { off })
.collect();
let pos = digits(&o.drawbars(model, preset)[..]);
if ui.unicode() {
format!("{tabs} {}", ui.dim(format!("({pos})")))
} else {
format!("{tabs} ({pos})")
}
} else {
drawbars(ui, &o.drawbars(model, preset)[..])
};
let vib = match o.vib_type(model) {
Some(v) if o.vib_on(model, preset) => format!(" vib {v:?}"),
Some(_) => " vib off".to_string(),
None => String::new(),
};
let perc = if model == OrganModel::B3 {
if o.b3_perc_on(preset) {
let third = if o.b3_perc_third() { " +3rd" } else { "" };
format!(" perc {:?}{third}", o.b3_perc_speed())
} else {
" perc off".to_string()
}
} else {
String::new()
};
let name = format!("{label:<5}");
let name = if Some(model) == sel_model {
ui.bold(name)
} else {
name
};
ui.out(format!(" {mark}{name} p{preset}{live} {bars}{vib}{perc}"));
}
}
}
}
fn sample_v3(ui: &Ui, s: &Cbin<nord_format::formats::nsmp::SampleV3>) {
ui.out(field(ui, 2, "type", "sample instrument (nsmp3/nsmp4)"));
match (s.name(), s.sub_name()) {
(Ok(name), Ok(sub)) if !sub.is_empty() => {
ui.out(field(ui, 2, "name", format!("{name}_{sub}")))
}
(Ok(name), _) => ui.out(field(ui, 2, "name", name)),
(Err(e), _) => ui.warn(format!("name unreadable: {e}")),
}
let v = s.header.version;
ui.out(field(ui, 2, "version", version_label(v)));
ui.out(field(ui, 2, "strokes", s.stroke_count().to_string()));
section(ui, "Zones");
match s.zones() {
Err(e) => ui.warn(format!("zone table unreadable: {e}")),
Ok(zones) => {
for z in zones {
let range = match z.low_note {
Some(low) => format!("{}..={}", low, z.top_note),
None => format!("..={}", z.top_note),
};
let mut says = format!("root {} (stroke {})", z.root_key, z.stroke_gid);
if let Some(w) = z.velocity.filter(|w| *w != VelocityWindow::FULL) {
says.push_str(&format!(" velocity {}..={}", w.low, w.high));
}
ui.out(field(ui, 2, &range, says));
}
}
}
}
fn sample_project(ui: &Ui, p: &nsmpproj::Project) {
ui.out(field(ui, 2, "type", "Sample Editor project (nsmpproj)"));
match p.name() {
Ok(name) => ui.out(field(ui, 2, "name", name)),
Err(e) => ui.warn(format!("name unreadable: {e}")),
}
if let Ok((product, version)) = p.created_by() {
ui.out(field(ui, 2, "editor", format!("{product} {version}")));
}
if let Ok(v) = p.file_format_version() {
ui.out(field(ui, 2, "version", v));
}
section(ui, "Audio files");
match p.audio_files() {
Err(e) => ui.warn(format!("audio files unreadable: {e}")),
Ok(files) => {
for f in files {
ui.out(field(
ui,
4,
&format!("file {}", f.id),
format!("{} {} {}", f.path, ui.dim("Hz"), f.sample_rate),
));
}
}
}
section(ui, "Zones");
let (zones, strokes) = match (p.zones(), p.strokes()) {
(Ok(z), Ok(s)) => (z, s),
(Err(e), _) | (_, Err(e)) => {
ui.warn(format!("zone table unreadable: {e}"));
return;
}
};
ui.out(format!(" {}", ui.dim("high to low")));
for zone in &zones {
let range = format!(
"{}..{}",
note::name(zone.bottom_note),
note::name(zone.top_note)
);
let files: Vec<String> = zone
.strokes
.iter()
.map(|zs| {
strokes
.iter()
.find(|s| s.global_id == zs.global_id)
.map_or_else(
|| format!("stroke {}?", zs.global_id),
|s| format!("file {}", s.file_id),
)
})
.collect();
ui.out(field(
ui,
4,
&format!("zone {}", zone.zone_id),
format!(
"{range:<10} {} {} ({}) {}{}",
ui.dim("root"),
note::name(zone.root_key),
zone.root_key,
files.join(", "),
if zone.enabled { "" } else { " (disabled)" }
),
));
}
}
fn sample(ui: &Ui, s: &Cbin<Sample>) {
ui.out(field(ui, 2, "type", "sample instrument (nsmp)"));
let named = s.chain().is_ok_and(Chain::names_instrument);
match s.name() {
Ok(_) if !named => ui.out(field(ui, 2, "name", "(this library carries none)")),
Ok(name) => ui.out(field(ui, 2, "name", name)),
Err(e) => ui.warn(format!("name unreadable: {e}")),
}
let v = s.header.version;
ui.out(field(ui, 2, "version", version_label(v)));
let categories = s.categories();
if !categories.is_empty() {
ui.out(field(ui, 2, "category", categories.join(" / ")));
}
section(ui, "Zones");
let (zones, strokes) = match (s.zones(), s.strokes()) {
(Ok(z), Ok(st)) => (z, st),
(Err(e), _) | (_, Err(e)) => {
ui.warn(format!("zone table unreadable: {e}"));
return;
}
};
if zones.len() != strokes.len() {
ui.warn(format!(
"{} zones but {} strokes; showing what pairs up",
zones.len(),
strokes.len()
));
}
ui.out(format!(
" {}",
ui.dim("high to low; the last zone reaches the bottom of the keyboard")
));
for (i, (zone, stroke)) in zones.iter().zip(&strokes).enumerate() {
let range = match zones.get(i + 1) {
Some(below) => format!(
"{}..{}",
note::name(below.top_note.saturating_add(1)),
note::name(zone.top_note)
),
None => format!("..{}", note::name(zone.top_note)),
};
ui.out(field(
ui,
4,
&format!("zone {}", i + 1),
format!(
"{range:<10} {} {} ({}) {} {}",
ui.dim("root"),
note::name(stroke.root_key),
stroke.root_key,
ui.dim("packets"),
match stroke.packets {
Some(n) => n.to_string(),
None => "?".into(),
},
),
));
}
match s.key_table() {
Ok(table) => {
let level = table.instrument;
ui.out(field(
ui,
4,
"keyboard",
format!(
"{} {:.3} ({:+.1} dB) {} {:+.2} st {} {}",
ui.dim("gain"),
level.ratio(),
20.0 * level.ratio().log10(),
ui.dim("detune"),
level.semitones(),
ui.dim("keys adjusted"),
table.adjusted().count(),
),
));
}
Err(e) => ui.warn(format!("keyboard map unreadable: {e}")),
}
let counted: usize = strokes.iter().filter_map(|s| s.packets).sum();
let unknown = strokes.iter().filter(|s| s.packets.is_none()).count();
ui.out(field(
ui,
4,
"audio",
format!(
"{} {counted}{} {} {}",
ui.dim("packets"),
if unknown > 0 {
format!(" (+{unknown} stroke(s) whose header length is unknown)")
} else {
String::new()
},
ui.dim("encoded bytes"),
counted * stroke::packet_len(codec::Layout::V2),
),
));
}
pub fn print(ui: &Ui, entity: &Entity) {
match entity {
Entity::Program(Program::Electro5(p)) => {
panels(ui, "Electro 5 program (ne5p)", p.header.slot(), p)
}
Entity::Live(Live::Electro5(p)) => {
panels(ui, "Electro 5 live slot (ne5l)", p.header.slot(), p)
}
Entity::Song(Song::Electro5(s)) => {
let (bank, slot) = s.header.slot();
ui.out(field(ui, 2, "type", "Electro 5 song / set (ne5t)"));
ui.out(field(ui, 2, "location", shown_at(bank, slot)));
section(ui, "Programs");
for (n, slot) in ne5::song::Slot::ALL.into_iter().enumerate() {
let p = s.get(slot);
ui.out(field(
ui,
4,
&format!("slot {}", n + 1),
shown_at(p.x(), p.y()),
));
}
}
Entity::Settings(Settings::Electro5(s)) => {
ui.out(field(ui, 2, "type", "Electro 5 settings (ne5s)"));
let boot = &s.body;
section(ui, "Startup");
for (name, value) in [
(
"program",
shown_at(boot.startup_program.x(), boot.startup_program.y()),
),
("live mode", yn(boot.startup_live_mode).to_string()),
("live slot", boot.startup_live_slot.to_string()),
("set list mode", yn(boot.startup_set_list_mode).to_string()),
(
"set list song",
format!(
"list {} song {}",
boot.startup_song.x() + 1,
boot.startup_song.y() + 1
),
),
] {
ui.out(format!(
" {}{}",
ui.dim(format!("{name:<SETTING_WIDTH$}")),
value,
));
}
for (menu, fields) in s.by_menu() {
section(ui, menu.title());
for f in fields {
ui.out(format!(
" {}{}",
ui.dim(format!("{:<SETTING_WIDTH$}", f.name.replace('_', " "))),
f.value,
));
}
}
}
Entity::Piano(p) => {
ui.out(field(ui, 2, "type", "piano library (npno)"));
match p.name() {
Ok((name, variant)) if variant.is_empty() => ui.out(field(ui, 2, "name", name)),
Ok((name, variant)) => ui.out(field(ui, 2, "name", format!("{name} ({variant})"))),
Err(e) => ui.warn(format!("name unreadable: {e}")),
}
ui.out(field(
ui,
2,
"version",
version_label(p.file.header.version),
));
if let Ok(map) = p.key_map() {
let covered = map.iter().filter(|&&b| b != 0xFF).count();
ui.out(field(ui, 2, "notes", format!("{covered} covered")));
}
if let Ok(library) = p.library() {
ui.out(field(
ui,
2,
"strokes",
format!(
"{} over {} root(s), {} channel(s)",
library.strokes().len(),
library.roots().len(),
library.channels(),
),
));
}
}
Entity::Sample(nord_format::Sample::V2(s)) => sample(ui, s),
Entity::Sample(nord_format::Sample::V3(s)) => sample_v3(ui, s),
Entity::SampleProject(p) => sample_project(ui, p),
Entity::Bundle(nord_format::Bundle::Electro5(b)) => {
ui.out(field(ui, 2, "type", "backup bundle (zip)"));
ui.out(field(
ui,
2,
"note",
ui.dim("use --raw to list contained programs/songs"),
));
for (name, why) in b.skipped() {
ui.warn(format!("bundle entry skipped: {name}: {why}"));
}
}
Entity::Program(Program::Stage2(p)) => ns2_globals(ui, "Stage 2 program (ns2p)", p),
Entity::Live(Live::Stage2(p)) => ns2_globals(ui, "Stage 2 live slot (ns2l)", p),
Entity::Program(Program::Stage3(p)) => ns3_globals(ui, "Stage 3 program (ns3f)", p),
Entity::Live(Live::Stage3(p)) => ns3_globals(ui, "Stage 3 live slot (ns3l)", p),
Entity::Program(Program::Stage4(p)) => ns4_globals(ui, "Stage 4 program (ns4p)", p),
Entity::Live(Live::Stage4(p)) => ns4_globals(ui, "Stage 4 live slot (ns4l)", p),
Entity::Synth(nord_format::Synth::Stage4(y)) => {
ns4_head(ui, "Stage 4 synth preset (ns4y)", &y.header);
section(ui, "Layers");
ui.out(field(
ui,
4,
"on",
layers(&[
("A", y.synth_a_layer_enabled),
("B", y.synth_b_layer_enabled),
("C", y.synth_c_layer_enabled),
]),
));
ns4_note(ui);
}
Entity::PianoPreset(nord_format::PianoPreset::Stage4(n)) => {
ns4_head(ui, "Stage 4 piano preset (ns4n)", &n.header);
section(ui, "Layers");
ui.out(field(
ui,
4,
"on",
layers(&[
("A", n.piano_a_layer_enabled),
("B", n.piano_b_layer_enabled),
]),
));
ns4_note(ui);
}
Entity::OrganPreset(nord_format::OrganPreset::Stage4(o)) => {
ns4_head(ui, "Stage 4 organ preset (ns4o)", &o.header);
section(ui, "Layers");
ui.out(field(
ui,
4,
"on",
layers(&[
("A", o.organ_a_layer_enabled),
("B", o.organ_b_layer_enabled),
]),
));
ns4_note(ui);
}
Entity::Bundle(nord_format::Bundle::Drum2Bank(b)) => {
ui.out(field(ui, 2, "type", "Drum 2 bank (zip)"));
ui.out(field(ui, 2, "programs", b.programs.len().to_string()));
}
Entity::Bundle(nord_format::Bundle::Drum3KitBank(b)) => {
ui.out(field(ui, 2, "type", "Drum 3P kit bank (zip)"));
ui.out(field(ui, 2, "kits", b.kits.len().to_string()));
}
Entity::Bundle(nord_format::Bundle::Members(members)) => {
ui.out(field(ui, 2, "type", "bundle (zip)"));
let mut by_tag: std::collections::BTreeMap<String, usize> = Default::default();
for (_, m) in members {
*by_tag
.entry(String::from_utf8_lossy(&m.header.tag).replace('\0', ""))
.or_default() += 1;
}
let counts = by_tag
.iter()
.map(|(tag, n)| format!("{n} {tag}"))
.collect::<Vec<_>>()
.join(", ");
ui.out(field(ui, 2, "members", counts));
}
other => raw_summary(ui, other),
}
}
fn ns2_globals(ui: &Ui, kind: &str, p: &Cbin<nord_format::formats::ns2::Program>) {
ui.out(field(ui, 2, "type", kind));
let (bank, slot) = p.header.slot();
ui.out(field(ui, 2, "location", shown_at(bank, slot)));
ui.out(field(ui, 2, "category", category(&p.header)));
ui.out(field(ui, 2, "version", p.header.version.to_string()));
section(ui, "Globals");
ui.out(field(
ui,
4,
"transpose",
transpose(p.transpose_enabled, p.transpose),
));
let split = if p.split_three_zones {
format!("{:?} / {:?}", p.split_low_note, p.split_high_note)
} else if p.split_two_zones {
format!("{:?}", p.split_low_note)
} else {
"off".to_string()
};
ui.out(field(ui, 4, "split", split));
ui.out(field(
ui,
4,
"clock",
format!("{} bpm", p.master_clock.bpm()),
));
ui.out(field(ui, 4, "dual kb", yn(p.dual_keyboard)));
ui.out(field(ui, 4, "note", ui.dim("slots and effects unmapped")));
}
fn ns3_globals(ui: &Ui, kind: &str, p: &Cbin<nord_format::formats::ns3::Program>) {
ui.out(field(ui, 2, "type", kind));
let (bank, slot) = p.header.slot();
ui.out(field(ui, 2, "location", shown_at(bank, slot)));
ui.out(field(ui, 2, "category", category(&p.header)));
ui.out(field(ui, 2, "version", version_label(p.header.version)));
section(ui, "Globals");
ui.out(field(ui, 4, "panels", format!("{:?}", p.panel_enable)));
ui.out(field(
ui,
4,
"transpose",
transpose(p.transpose_enabled, p.transpose),
));
let split = if p.split_enabled {
let mut zones = Vec::new();
for (on, note, width) in [
(p.split_low_enabled, p.split_low_note, p.split_low_width),
(p.split_mid_enabled, p.split_mid_note, p.split_mid_width),
(p.split_high_enabled, p.split_high_note, p.split_high_width),
] {
if on {
zones.push(format!("{note:?} (width {width})"));
}
}
zones.join(" / ")
} else {
"off".to_string()
};
ui.out(field(ui, 4, "split", split));
ui.out(field(
ui,
4,
"clock",
format!("{} bpm", p.master_clock.bpm()),
));
let dual = if p.dual_keyboard {
format!("on ({:?})", p.dual_keyboard_style)
} else {
"off".to_string()
};
ui.out(field(ui, 4, "dual kb", dual));
ui.out(field(ui, 4, "note", ui.dim("panels and effects unmapped")));
}
fn layers(on: &[(&str, bool)]) -> String {
let live: Vec<&str> = on.iter().filter(|(_, on)| *on).map(|(n, _)| *n).collect();
if live.is_empty() {
"—".to_string()
} else {
live.join("+")
}
}
fn ns4_head(ui: &Ui, kind: &str, header: &nord_format::cbin::Header) {
ui.out(field(ui, 2, "type", kind));
let (bank, slot) = header.slot();
ui.out(field(ui, 2, "location", shown_at(bank, slot)));
if let Some(id) = header.category() {
ui.out(field(ui, 2, "category", format!("id {id}")));
}
ui.out(field(ui, 2, "version", version_label(header.version)));
}
fn ns4_note(ui: &Ui) {
ui.out(field(
ui,
4,
"note",
ui.dim("every parameter decodes; values are raw, use --raw to read them"),
));
}
fn ns4_globals(ui: &Ui, kind: &str, p: &Cbin<nord_format::formats::ns4::Program>) {
ns4_head(ui, kind, &p.header);
section(ui, "Globals");
ui.out(field(
ui,
4,
"sections",
format!(
"organ {}, piano {}, synth {}",
yn(p.organ_section_enabled),
yn(p.piano_section_enabled),
yn(p.synth_section_enabled),
),
));
ui.out(field(
ui,
4,
"layers",
format!(
"organ {}, piano {}, synth {}",
layers(&[
("A", p.organ_a_layer_enabled),
("B", p.organ_b_layer_enabled)
]),
layers(&[
("A", p.piano_a_layer_enabled),
("B", p.piano_b_layer_enabled)
]),
layers(&[
("A", p.synth_a_layer_enabled),
("B", p.synth_b_layer_enabled),
("C", p.synth_c_layer_enabled),
]),
),
));
let split = if p.split_enabled {
layers(&[
("1-2", p.kb_zones_1_2_split_point_enabled),
("2-3", p.kb_zones_2_3_split_point_enabled),
("3-4", p.kb_zones_3_4_split_point_enabled),
])
} else {
"off".to_string()
};
ui.out(field(ui, 4, "split", split));
ui.out(field(
ui,
4,
"transpose",
if p.program_transpose_enabled {
format!("on (stored {})", p.program_transpose_amount)
} else {
"off".to_string()
},
));
ui.out(field(
ui,
4,
"global fx",
format!(
"comp {}, delay {}, reverb {}",
yn(p.fx_comp_global_enabled),
yn(p.fx_delay_global_enabled),
yn(p.fx_reverb_global_enabled),
),
));
ns4_note(ui);
}
fn transpose(enabled: bool, t: nord_format::components::StageTranspose) -> String {
match (enabled, t.semitones()) {
(true, Some(s)) => format!("{s:+}"),
(true, None) => format!("unknown ({})", t.raw()),
(false, Some(0)) | (false, None) => "off".to_string(),
(false, Some(s)) => format!("off (stored {s:+})"),
}
}
fn raw_summary(ui: &Ui, entity: &Entity) {
let id = entity.identity();
let tag = id.format.trim_end_matches('\0');
ui.out(field(ui, 2, "type", format!("{} ({tag})", id.kind)));
if let Some(f) = entity.raw() {
let header = &f.header;
if header.location & 0xff00_ff00 == 0 {
let (bank, slot) = header.slot();
ui.out(field(ui, 2, "location", shown_at(bank, slot)));
} else {
ui.out(field(
ui,
2,
"location",
format!("{:#010x}", header.location),
));
}
ui.out(field(ui, 2, "version", header.version.to_string()));
ui.out(field(ui, 2, "body", format!("{} bytes", f.body.0.len())));
ui.out(field(
ui,
2,
"note",
ui.dim("body unmapped; container verified"),
));
return;
}
match entity {
Entity::Sysex(s) => {
ui.out(field(ui, 2, "family", format!("{:?}", s.family())));
ui.out(field(ui, 2, "messages", s.messages().count().to_string()));
ui.out(field(ui, 2, "bytes", s.data.len().to_string()));
}
Entity::Midi(m) => ui.out(field(ui, 2, "bytes", m.data.len().to_string())),
Entity::Cne3(c) => ui.out(field(ui, 2, "bytes", c.data.len().to_string())),
_ => {}
}
}