use eframe::egui;
use nord_format::fields::Field;
use nord_format::formats::nsmp::codec;
use nord_usb::{Location, ObjectClass};
use crate::device::Device;
use crate::fields;
use crate::log::Log;
use crate::queue::Queue;
use crate::strings;
use crate::tags::Tags;
use crate::workspace::{LocalEntity, Workspace};
mod advanced;
pub mod capability;
pub mod controls;
pub(crate) mod encode;
mod field;
mod header;
pub mod keys;
mod panel;
mod piano;
mod project;
pub(crate) mod sample;
mod setlist;
mod table;
mod verbatim;
use advanced::Advanced;
use controls::{Ctx, Sets};
pub use header::{Body, Cell, Extras, Face, Ink, Loud, SizeLine, Stage, StateLine, Tone};
pub use sample::note_picker;
pub const SCROLL: &str = "document_body";
pub const PAGE: &str = "document_page";
const BODY_MARGIN: f32 = 8.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Shape {
Fields,
SetList,
Sample,
Project,
Piano,
Verbatim,
Wav,
Undecoded,
}
#[derive(Clone, Copy)]
struct Asset<'a> {
entity: &'a LocalEntity,
shape: Shape,
}
impl<'a> Asset<'a> {
fn of(entity: &'a LocalEntity) -> Asset<'a> {
Asset {
entity,
shape: shape(entity),
}
}
fn decoded(&self) -> Option<&'a nord_format::Entity> {
self.entity.entity.as_ref()
}
}
fn shape(entity: &LocalEntity) -> Shape {
use nord_format::Entity as E;
let Some(decoded) = &entity.entity else {
return match encode::is_wav(&entity.bytes) {
true => Shape::Wav,
false => Shape::Undecoded,
};
};
match decoded {
E::Sample(_) => Shape::Sample,
E::SampleProject(_) => Shape::Project,
E::Piano(_) => Shape::Piano,
E::Song(_) => match fields::is_set_list(decoded) {
true => Shape::SetList,
false => Shape::Verbatim,
},
E::Bundle(_)
| E::Cne3(_)
| E::Live(_)
| E::Midi(_)
| E::OrganPreset(_)
| E::Performance(_)
| E::PianoLibrary(_)
| E::PianoPreset(_)
| E::PipeLibrary(_)
| E::Program(_)
| E::Settings(_)
| E::Synth(_)
| E::Sysex(_) => match fields::has_registry(decoded) {
true => Shape::Fields,
false => Shape::Verbatim,
},
}
}
pub struct SendBack {
pub id: u64,
pub class: ObjectClass,
pub at: Location,
}
pub struct Around<'a> {
pub queue: &'a Queue,
pub tags: &'a Tags,
}
#[derive(Default)]
pub struct Wants {
pub send: Option<SendBack>,
pub keep: bool,
pub open: Option<crate::browser::Item>,
}
enum Asked {
Zone(sample::Ask),
Root(piano::Ask),
Encode,
Export,
Open(crate::browser::Item),
Advanced,
}
struct Opened {
id: u64,
ctx: Ctx,
name: String,
variant: String,
paths: std::collections::HashMap<u32, String>,
error: Option<String>,
fetched_deps: bool,
wav: Option<(encode::Draft, encode::Source)>,
sample: sample::State,
fields: field::State,
list: setlist::State,
}
impl Opened {
fn new(asset: Asset<'_>, view: bool, renaming: (Option<String>, Option<String>)) -> Opened {
let entity = asset.entity;
let (name, variant) = header::boxes(entity, asset.shape, view, renaming);
Opened {
id: entity.id,
ctx: Ctx::default(),
name,
variant,
paths: std::collections::HashMap::new(),
error: None,
fetched_deps: false,
wav: match asset.shape {
Shape::Wav => Some((
encode::Draft::new(&entity.name),
encode::Source::read(&entity.bytes),
)),
Shape::Fields
| Shape::SetList
| Shape::Sample
| Shape::Project
| Shape::Piano
| Shape::Verbatim
| Shape::Undecoded => None,
},
sample: sample::State::default(),
fields: field::State::default(),
list: setlist::State::default(),
}
}
}
#[derive(Default)]
pub struct Document {
open: Option<Opened>,
views: std::collections::HashMap<u64, Face>,
advanced: Advanced,
audio: sample::Cache,
player: crate::audio::Player,
piano: piano::State,
}
impl Document {
pub fn ui(
&mut self,
ui: &mut egui::Ui,
id: u64,
workspace: &mut Workspace,
device: &mut Device,
log: &mut Log,
around: &Around<'_>,
) -> Wants {
let Some(entity) = workspace.get(id) else {
return Wants::default();
};
let stamp = entity.stamp;
let decoded = entity.entity.as_ref();
let registry = decoded.map(fields::fields_of).unwrap_or_default();
let viewing = workspace.is_view(id);
let asset = Asset::of(entity);
let shape = asset.shape;
if self.opened() != Some(id) {
self.open = Some(Opened::new(
asset,
viewing,
self.piano.renaming(asset.entity),
));
self.advanced.leave();
self.player.stop();
self.piano.leave();
}
self.audio.follow(id, entity.stamp);
if let Some(open) = &mut self.open {
sample::follow(&mut open.sample, id, &entity.saved);
}
self.player.settle();
if let Some(left) = self.piano.settle(ui.input(|input| input.time)) {
ui.ctx()
.request_repaint_after(std::time::Duration::from_secs_f64(left));
}
if self.player.playing().is_some() {
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(250));
}
if self.piano.applying() {
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(100));
}
let faces = faces(shape);
let face = showing(&faces, self.views.get(&id).copied().unwrap_or_default());
if let (Shape::Fields, Some(open)) = (shape, self.open.as_mut()) {
open.fields.follow(entity);
}
let doc = match (decoded, registry.as_deref()) {
(Some(decoded), Some(fields)) => Some(field::of(decoded, fields)),
_ => None,
};
let pending = match (doc.is_some(), self.open.as_ref()) {
(true, Some(open)) => open.fields.pending().len(),
_ => 0,
};
let extras = match shape {
Shape::Piano => self.piano.begin(id, entity, &device.state),
Shape::Fields
| Shape::SetList
| Shape::Sample
| Shape::Project
| Shape::Verbatim
| Shape::Wav
| Shape::Undecoded => extras(asset, device, workspace, pending),
};
let Some(open) = self.open.as_mut() else {
return Wants::default();
};
let mut sets: Sets = Vec::new();
let act = header::ui(
ui,
entity,
&header::Facts {
faces: &faces,
showing: face,
device: &device.state,
queue: around.queue,
tags: around.tags,
view: viewing,
renaming: self.piano.renaming(entity),
shape,
extras,
},
(&mut open.name, &mut open.variant),
&mut sets,
);
self.views.insert(id, act.face.unwrap_or(face));
let mut wants = Wants {
send: act.send,
..Wants::default()
};
let mut details = None;
let mut typed = false;
let mut asked = None;
let mut lookup = piano_lookup(entity, registry.as_deref(), device);
let mut page = ui.new_child(
egui::UiBuilder::new()
.id_salt(PAGE)
.max_rect(ui.available_rect_before_wrap().shrink(BODY_MARGIN)),
);
{
let ui = &mut page;
if viewing {
wants.keep = viewing_banner(ui, entity);
}
if let Some(why) = self.open.as_ref().and_then(|open| open.error.as_ref()) {
ui.label(egui::RichText::new(why).color(crate::app::bad(ui.visuals())));
}
if face == Face::Edit {
asked = self.pinned(ui, asset, doc.as_ref(), &mut sets);
}
egui::ScrollArea::vertical()
.id_salt(SCROLL)
.auto_shrink([false; 2])
.show(ui, |ui| {
ui.push_id(id, |ui| match face {
Face::Edit => {
if let Some(from_body) = self.body(
ui,
asset,
doc.as_ref(),
&mut lookup,
&setlist::Catalogue {
device: &device.state,
workspace,
},
&mut sets,
) {
asked = Some(from_body);
}
}
Face::Advanced => match shape {
Shape::Fields => {
if let (Some(doc), Some(open)) = (doc.as_ref(), self.open.as_ref())
{
Advanced::about(ui, &field::about(doc, entity));
let table = advanced::Table {
fields: registry.as_deref().unwrap_or_default(),
saved: open.fields.settled(),
changed: open.fields.pending(),
doc: Some(doc),
};
self.advanced.table(ui, &table, &mut sets);
typed = !sets.is_empty();
}
}
Shape::Piano => self.piano.advanced(ui),
Shape::SetList
| Shape::Sample
| Shape::Project
| Shape::Verbatim
| Shape::Wav
| Shape::Undecoded => capabilities(ui, asset),
},
Face::Metadata => {
match shape {
Shape::Piano => self.piano.meta(ui),
Shape::Fields
| Shape::SetList
| Shape::Sample
| Shape::Project
| Shape::Verbatim
| Shape::Wav
| Shape::Undecoded => record(ui, asset),
}
details = self.advanced.meta(ui, entity, device)
}
});
});
}
let drawn = page.min_rect();
ui.advance_cursor_after_rect(drawn.expand(BODY_MARGIN));
if let Some(details) = details {
for cmd in advanced::commands(details) {
device.send(cmd, log);
}
}
match asked {
Some(Asked::Open(item)) => wants.open = Some(item),
Some(Asked::Advanced) => {
self.views.insert(id, Face::Advanced);
}
Some(Asked::Export) => self.export(ui.ctx(), id, workspace),
Some(asked) => self.answer(id, asked, workspace, log),
None => {}
}
if let Some((class, at)) = workspace.get(id).and_then(|e| e.origin.slot()) {
if lookup.asked || self.owes_deps(&lookup, (class, at), device) {
if let Some(open) = &mut self.open {
open.fetched_deps = true;
}
device.send(crate::device::DeviceCmd::Deps { class, at }, log);
}
}
if let Some(name) = act.rename {
workspace.rename(id, name);
}
if act.export {
self.export(ui.ctx(), id, workspace);
}
if act.revert {
workspace.revert(id, log);
self.piano.forget(id);
self.open = None;
return wants;
}
if !sets.is_empty() {
let outcome = self.apply(id, sets, workspace, log);
if typed {
self.advanced.settled(outcome);
}
}
self.replan(id, workspace, log);
if workspace.get(id).is_some_and(|held| held.stamp != stamp) {
ui.ctx().request_repaint();
}
wants
}
fn replan(&mut self, id: u64, workspace: &Workspace, log: &mut Log) {
let Some(plan) = self.piano.drafted() else {
return;
};
let checked = match workspace.get(id) {
Some(entity) => piano::planned(&entity.saved.bytes, &plan).map(|_| ()),
None => return,
};
match checked {
Ok(()) => {
self.piano.commit(plan);
self.refused(None);
}
Err(why) => {
self.piano.discard();
log.error(why.clone());
self.refused(Some(why));
}
}
}
pub fn pends(&self, id: u64) -> bool {
self.piano.pending(id)
}
pub fn settle(
&mut self,
ctx: &egui::Context,
acts: Vec<crate::browser::Act>,
workspace: &mut Workspace,
log: &mut Log,
) -> Vec<crate::browser::Act> {
let mut out: Vec<crate::browser::Act> = acts
.into_iter()
.filter_map(|act| {
if let crate::browser::Act::Revert(id) | crate::browser::Act::Remove(id) = &act {
self.piano.forget(*id);
}
self.piano.hold(ctx, act, workspace)
})
.collect();
out.extend(self.released(ctx, workspace, log));
out
}
pub fn released(
&mut self,
ctx: &egui::Context,
workspace: &mut Workspace,
log: &mut Log,
) -> Vec<crate::browser::Act> {
match self.piano.answered(ctx, workspace) {
Some(applied) => self.put_back(applied, workspace, log),
None => Vec::new(),
}
}
fn put_back(
&mut self,
applied: piano::Applied,
workspace: &mut Workspace,
log: &mut Log,
) -> Vec<crate::browser::Act> {
match applied.made {
Some(Ok(bytes)) => {
self.refused(None);
workspace.replace_bytes(applied.id, bytes, log);
}
Some(Err(why)) => {
log.error(why.clone());
log.trouble("That library could not be laid out, so nothing was written.");
self.refused(Some(why));
}
None => {}
}
applied.acts
}
fn export(&mut self, ctx: &egui::Context, id: u64, workspace: &mut Workspace) {
if self
.piano
.hold(ctx, crate::browser::Act::Export(id), workspace)
.is_some()
{
workspace.export(id);
}
}
fn opened(&self) -> Option<u64> {
self.open.as_ref().map(|open| open.id)
}
#[cfg(test)]
fn refusal(&self) -> Option<&str> {
self.open.as_ref()?.error.as_deref()
}
fn refused(&mut self, why: Option<String>) {
if let Some(open) = &mut self.open {
open.error = why;
}
}
fn sounding_root(&self) -> Option<u8> {
let (id, root) = self.player.playing()?;
(Some(id) == self.opened()).then_some(())?;
u8::try_from(root).ok()
}
fn owes_deps(
&self,
lookup: &panel::PianoLookup,
slot: (ObjectClass, Location),
device: &Device,
) -> bool {
let fetched = self.open.as_ref().is_none_or(|open| open.fetched_deps);
if fetched || !lookup.can_ask || lookup.id.is_none() || lookup.name.is_some() {
return false;
}
let detail = &device.state.detail;
!(detail.at == Some(slot) && detail.deps.is_some())
}
pub fn leave(&mut self) {
self.open = None;
self.player.stop();
}
fn body(
&mut self,
ui: &mut egui::Ui,
asset: Asset<'_>,
doc: Option<&field::Doc<'_>>,
piano: &mut panel::PianoLookup,
seen: &setlist::Catalogue<'_>,
sets: &mut Sets,
) -> Option<Asked> {
match asset.shape {
Shape::Wav | Shape::Undecoded => self.wav_body(ui),
Shape::Sample => self
.sample_body(ui, asset.decoded()?, sets)
.map(Asked::Zone),
Shape::Project => {
self.project_body(ui, asset.decoded()?, sets);
None
}
Shape::SetList => {
let open = self.open.as_mut()?;
setlist::ui(ui, &mut open.list, asset.entity, seen, sets).map(Asked::Open)
}
Shape::Piano => {
let sounding = self.sounding_root();
self.piano.ui(ui, sounding).map(Asked::Root)
}
Shape::Fields => {
let open = self.open.as_mut()?;
field::body(ui, &open.ctx, &mut open.fields, doc?, piano, sets)
.then_some(Asked::Advanced)
}
Shape::Verbatim => verbatim::ui(ui, asset.entity).then_some(Asked::Export),
}
}
fn wav_body(&mut self, ui: &mut egui::Ui) -> Option<Asked> {
let Some((draft, source)) = self.open.as_mut().and_then(|open| open.wav.as_mut()) else {
ui.label(
egui::RichText::new(
"This file did not decode, so there is nothing to show but its bytes.",
)
.weak(),
);
return None;
};
encode::ui(ui, draft, source).then_some(Asked::Encode)
}
fn sample_body(
&mut self,
ui: &mut egui::Ui,
decoded: &nord_format::Entity,
sets: &mut Sets,
) -> Option<sample::Ask> {
let snapshot = match sample::snapshot(decoded)? {
Ok(snapshot) => snapshot,
Err(why) => {
ui.label(egui::RichText::new(why).color(crate::app::bad(ui.visuals())));
return None;
}
};
let sounding = self.player.playing();
let target = self.opened();
let sounds: Vec<sample::Sound> = (0..snapshot.zones.len())
.map(|index| sample::Sound {
decoded: self.audio.get(index),
playing: sounding == target.map(|id| (id, index)),
})
.collect();
let open = self.open.as_mut()?;
sample::ui(ui, &mut open.sample, &snapshot, &sounds, sets)
}
fn pinned(
&mut self,
ui: &mut egui::Ui,
asset: Asset<'_>,
doc: Option<&field::Doc<'_>>,
sets: &mut Sets,
) -> Option<Asked> {
match asset.shape {
Shape::Fields => {
field::nav(ui, &mut self.open.as_mut()?.fields, doc?);
None
}
Shape::Piano => self.piano.map(ui).map(Asked::Root),
Shape::Sample => match sample::snapshot(asset.decoded()?)? {
Ok(snapshot) => {
let open = self.open.as_mut()?;
sample::map(ui, &mut open.sample, &snapshot, sets).map(Asked::Zone)
}
Err(_) => None,
},
Shape::Project => {
if let Some(Ok(snapshot)) = project::snapshot(asset.decoded()?) {
project::map(ui, &mut self.open.as_mut()?.sample, &snapshot, sets);
}
None
}
Shape::SetList | Shape::Verbatim | Shape::Wav | Shape::Undecoded => None,
}
}
fn answer(&mut self, id: u64, asked: Asked, workspace: &mut Workspace, log: &mut Log) {
match asked {
Asked::Zone(sample::Ask::Decode(zone)) => {
if let Some(decoded) = workspace.get(id).and_then(|e| e.entity.as_ref()) {
self.audio.decode(decoded, zone);
}
}
Asked::Zone(sample::Ask::Strike { zone, semitones }) => {
if let Some(decoded) = workspace.get(id).and_then(|e| e.entity.as_ref()) {
self.audio.decode(decoded, zone);
}
let Some(Ok(decoded)) = self.audio.get(zone) else {
return;
};
let rate = crate::audio::rate(semitones);
if let Err(why) = self.player.strike(
(id, zone),
&decoded.audio.samples,
decoded.audio.channels,
rate,
) {
log.error(why);
log.trouble("This computer would not play that zone.");
}
}
Asked::Zone(sample::Ask::Play(zone)) => {
if self.player.playing() == Some((id, zone)) {
self.player.stop();
} else if let Some(Ok(decoded)) = self.audio.get(zone) {
if let Err(why) = self.player.toggle(
(id, zone),
&decoded.audio.samples,
decoded.audio.channels,
) {
log.error(why);
log.trouble("This computer would not play that zone.");
}
}
}
Asked::Zone(sample::Ask::Save(zone)) => {
let Some(Ok(decoded)) = self.audio.get(zone) else {
return;
};
let audio = &decoded.audio;
match nord_format::wav::pcm16(&audio.samples, codec::FIELD_RATE, audio.channels) {
Ok(bytes) => workspace.save_bytes(
crate::workspace::zone_wav_name(
&self.instrument_name(id, workspace),
zone + 1,
),
bytes,
),
Err(e) => {
log.error(e.to_string());
log.trouble("That zone could not be written as a WAV.");
}
}
}
Asked::Root(ask) => self.root_audio(id, ask, workspace, log),
Asked::Encode => self.encode(id, workspace, log),
Asked::Export | Asked::Open(_) | Asked::Advanced => {}
}
}
fn root_audio(&mut self, id: u64, ask: piano::Ask, workspace: &mut Workspace, log: &mut Log) {
let root = ask.root();
let Some(entity) = workspace.get(id) else {
return;
};
if let Err(why) = self.piano.decode(entity, root) {
log.error(why);
log.trouble("That root could not be decoded.");
return;
}
let Some(sound) = self.piano.sound(root) else {
return;
};
match ask {
piano::Ask::Play(_) => {
if let Err(why) =
self.player
.toggle((id, usize::from(root)), sound.samples, sound.channels)
{
log.error(why);
log.trouble("This computer would not play that root.");
}
}
piano::Ask::Strike { semitones, .. } => {
let rate = crate::audio::rate(semitones);
if let Err(why) =
self.player
.strike((id, usize::from(root)), sound.samples, sound.channels, rate)
{
log.error(why);
log.trouble("This computer would not play that root.");
}
}
piano::Ask::Save(_) => {
match nord_format::wav::pcm16(sound.samples, sound.rate, sound.channels) {
Ok(bytes) => workspace.save_bytes(sound.name, bytes),
Err(e) => {
log.error(e.to_string());
log.trouble("That root could not be written as a WAV.");
}
}
}
}
}
fn instrument_name(&self, id: u64, workspace: &Workspace) -> String {
let entity = workspace.get(id);
entity
.and_then(|e| e.entity.as_ref())
.and_then(sample::snapshot)
.and_then(Result::ok)
.map(|snapshot| snapshot.name)
.filter(|name| !name.trim().is_empty())
.or_else(|| entity.map(|e| e.name.clone()))
.unwrap_or_default()
}
fn encode(&mut self, id: u64, workspace: &mut Workspace, log: &mut Log) {
let Some((draft, source)) = self.open.as_ref().and_then(|open| open.wav.as_ref()) else {
return;
};
let made = encode::instrument(draft, source).map(|bytes| {
(
format!("{}.{}", draft.name, draft.layout.extension()),
bytes,
)
});
match made {
Ok((name, bytes)) => {
self.refused(None);
workspace.ingest(name, crate::workspace::Origin::Fresh, bytes, log);
}
Err(why) => {
log.error(format!(
"encode {}: {why}",
workspace.get(id).map_or("", |e| &e.name)
));
self.refused(Some(why));
}
}
}
fn project_body(&mut self, ui: &mut egui::Ui, decoded: &nord_format::Entity, sets: &mut Sets) {
let Some(open) = self.open.as_mut() else {
return;
};
match project::snapshot(decoded) {
Some(Ok(snapshot)) => {
project::ui(ui, &mut open.sample, &snapshot, &mut open.paths, sets)
}
Some(Err(why)) => {
ui.label(egui::RichText::new(why).color(crate::app::bad(ui.visuals())));
}
None => {}
}
}
fn apply(
&mut self,
id: u64,
sets: Sets,
workspace: &mut Workspace,
log: &mut Log,
) -> Result<(), String> {
let Some(entity) = workspace.get(id) else {
return Ok(());
};
let made = match shape(entity) {
Shape::Sample => sample::apply(&entity.bytes, &sets).map(Some),
Shape::Project => project::apply(&entity.bytes, &sets).map(Some),
Shape::Piano => self.piano.take(&sets).map(|()| None),
Shape::SetList => setlist::apply(&entity.bytes, &sets).map(Some),
Shape::Fields | Shape::Verbatim | Shape::Wav | Shape::Undecoded => {
fields::apply(&entity.bytes, &sets).map(|(_, out)| Some(out))
}
};
let made = match made {
Ok(made) => made,
Err(why) => {
log.error(why.clone());
self.refused(Some(why.clone()));
return Err(why);
}
};
self.refused(None);
if let Some(out) = made {
workspace.replace_bytes(id, out, log);
}
Ok(())
}
}
fn faces(shape: Shape) -> Vec<Face> {
let (panel, deep) = match shape {
Shape::Fields
| Shape::SetList
| Shape::Sample
| Shape::Project
| Shape::Piano
| Shape::Verbatim => (true, true),
Shape::Wav => (true, false),
Shape::Undecoded => (false, false),
};
let mut faces = Vec::new();
if panel {
faces.push(Face::Edit);
}
faces.push(Face::Metadata);
if deep {
faces.push(Face::Advanced);
}
faces
}
fn extras(
asset: Asset<'_>,
device: &Device,
workspace: &Workspace,
pending: usize,
) -> header::Extras {
let entity = asset.entity;
match asset.shape {
Shape::Fields => header::Extras {
edited: (pending > 0).then(|| StateLine {
words: format!("{pending} pending"),
ink: Ink::Warn,
hint: format!("raw ≠ bits on {pending} fields"),
}),
loud: queued(entity, device, pending),
..header::Extras::default()
},
Shape::SetList => header::Extras {
state: asset.decoded().and_then(|decoded| {
setlist::claim(
decoded,
&setlist::Catalogue {
device: &device.state,
workspace,
},
)
}),
..header::Extras::default()
},
Shape::Verbatim => {
let mut loud = header::action(entity, &device.state);
loud.label = "Send as-is".to_string();
loud.short = "Send".to_string();
header::Extras {
loud: Some(loud),
..header::Extras::default()
}
}
Shape::Sample | Shape::Project | Shape::Piano | Shape::Wav | Shape::Undecoded => {
header::Extras::default()
}
}
}
fn showing(faces: &[Face], remembered: Face) -> Face {
match faces.contains(&remembered) {
true => remembered,
false => faces.first().copied().unwrap_or(Face::Metadata),
}
}
fn record(ui: &mut egui::Ui, asset: Asset<'_>) {
let Some(decoded) = asset.decoded() else {
return;
};
match asset.shape {
Shape::Sample => {
if let Some(Ok(snapshot)) = sample::snapshot(decoded) {
sample::metadata(ui, &snapshot);
}
}
Shape::Project => {
if let Some(Ok(snapshot)) = project::snapshot(decoded) {
project::metadata(ui, &snapshot);
}
}
Shape::Fields
| Shape::SetList
| Shape::Piano
| Shape::Verbatim
| Shape::Wav
| Shape::Undecoded => {}
}
}
fn capabilities(ui: &mut egui::Ui, asset: Asset<'_>) {
let Some(decoded) = asset.decoded() else {
return;
};
match asset.shape {
Shape::Sample => {
if let Some(Ok(snapshot)) = sample::snapshot(decoded) {
capability::table(ui, &sample::capabilities(snapshot.generation));
capability::offsets(ui, &sample::offsets(&snapshot));
}
}
Shape::Project => {
if let Some(Ok(snapshot)) = project::snapshot(decoded) {
capability::table(ui, &project::capabilities());
capability::offsets(ui, &project::offsets(&snapshot));
}
}
Shape::SetList => setlist::stored(ui, decoded),
Shape::Verbatim => verbatim::bytes(ui, asset.entity),
Shape::Fields | Shape::Piano | Shape::Wav | Shape::Undecoded => {}
}
}
fn viewing_banner(ui: &mut egui::Ui, entity: &LocalEntity) -> bool {
let Some(where_) = entity
.origin
.slot()
.map(|(class, at)| strings::place(class, at))
else {
return false;
};
let mut keep = false;
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
ui.label(
egui::RichText::new(format!("Viewing {where_} on the instrument."))
.strong()
.small(),
);
ui.label(
egui::RichText::new(
"Edits and Send back work from here; it is not on this computer.",
)
.small()
.weak(),
);
keep = ui
.small_button("Keep on this computer")
.on_hover_text("put it in the list, where it stays after this tab closes")
.clicked();
});
});
keep
}
fn queued(entity: &LocalEntity, device: &Device, pending: usize) -> Option<Loud> {
if pending == 0 {
return None;
}
let loud = header::action(entity, &device.state);
if loud.tone != Tone::Ready {
return None;
}
Some(Loud {
label: format!("Queue send · {pending}"),
short: format!("Send {pending}"),
hint: format!("{pending} pending sets, applied as one batch — all or none"),
..loud
})
}
fn piano_lookup(
entity: &LocalEntity,
registry: Option<&[Field]>,
device: &Device,
) -> panel::PianoLookup {
let id = registry
.and_then(|fields| fields.iter().find(|field| field.path == "piano_panel.id"))
.and_then(|field| library_id(&field.value))
.filter(|id| *id != 0);
let slot = entity.origin.slot();
let name = id
.and_then(|id| device.state.dependency_name(slot, ObjectClass::Piano, id))
.map(str::to_string);
let models = registry
.map(|fields| piano_models(fields, device))
.unwrap_or_default();
let scan_disagrees = match (&name, registry) {
(Some(named), Some(fields)) => current_model(fields)
.and_then(|n| models.iter().find(|(i, _)| *i == n))
.map(|(_, scanned)| scanned)
.filter(|scanned| scanned.trim() != named.trim())
.cloned(),
_ => None,
};
panel::PianoLookup {
id,
name,
can_ask: slot.is_some() && device.state.connected(),
asked: false,
models,
scan_disagrees,
}
}
fn piano_models(fields: &[Field], device: &Device) -> Vec<(u32, String)> {
let Some(category) = fields
.iter()
.find(|field| field.path == "piano_panel.category")
else {
return Vec::new();
};
let Some(raw) = (category.spec.legal)()
.iter()
.position(|value| *value == category.value)
else {
return Vec::new();
};
let Some(slots) = device.state.bank(ObjectClass::Piano, raw as u32 + 1) else {
return Vec::new();
};
slots
.iter()
.enumerate()
.filter_map(|(position, info)| {
info.as_ref()
.map(|piano| (position as u32, piano.name.trim().to_string()))
})
.collect()
}
fn current_model(fields: &[Field]) -> Option<u32> {
fields
.iter()
.find(|field| field.path == "piano_panel.piano_model")
.and_then(|field| field.value.trim().parse().ok())
}
pub(crate) fn library_id(value: &str) -> Option<u32> {
let text = value.trim();
match text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
Some(hex) => u32::from_str_radix(hex, 16).ok(),
None => text.parse().ok(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workspace::{Fresh, Origin};
struct Open {
ctx: egui::Context,
workspace: Workspace,
device: Device,
log: Log,
queue: Queue,
tags: Tags,
document: Document,
id: u64,
width: f32,
}
const SCREEN: egui::Vec2 = egui::vec2(1280.0, 720.0);
impl Open {
fn fresh(kind: Fresh) -> Open {
let mut open = Open::empty();
open.id = open
.workspace
.create(kind, &mut open.log)
.expect("a fresh default");
open
}
fn file(name: &str, bytes: Vec<u8>) -> Open {
let mut open = Open::empty();
open.id =
open.workspace
.ingest(name.into(), Origin::File(name.into()), bytes, &mut open.log);
open
}
fn empty() -> Open {
let ctx = egui::Context::default();
ctx.all_styles_mut(crate::app::metrics);
ctx.set_fonts(crate::app::fonts());
Open {
workspace: Workspace::new(ctx.clone()),
device: Device::new(ctx.clone()),
ctx,
log: Log::default(),
queue: Queue::default(),
tags: Tags::default(),
document: Document::default(),
id: 0,
width: SCREEN.x,
}
}
fn entity(&self) -> &LocalEntity {
self.workspace.get(self.id).expect("it is still open")
}
fn state(&mut self) -> &mut Opened {
self.document.open.as_mut().expect("a document is open")
}
fn set(&mut self, sets: &[(&str, &str)]) {
let bytes = self.entity().bytes.clone();
let sets: Vec<(String, String)> = sets
.iter()
.map(|(path, value)| ((*path).to_string(), (*value).to_string()))
.collect();
let (_, edited) = fields::apply(&bytes, &sets).expect("the sets are legal");
self.workspace.replace_bytes(self.id, edited, &mut self.log);
}
fn frame(&mut self, events: Vec<egui::Event>) -> Vec<String> {
let input = egui::RawInput {
events,
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(self.width, SCREEN.y),
)),
..Default::default()
};
let output = self.ctx.run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
self.document.ui(
ui,
self.id,
&mut self.workspace,
&mut self.device,
&mut self.log,
&Around {
queue: &self.queue,
tags: &self.tags,
},
);
});
});
let mut said = Vec::new();
for clipped in &output.shapes {
words(&clipped.shape, &mut said);
}
said
}
fn output(&mut self, events: Vec<egui::Event>) -> egui::FullOutput {
let input = egui::RawInput {
events,
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(self.width, SCREEN.y),
)),
..Default::default()
};
self.ctx.clone().run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
self.document.ui(
ui,
self.id,
&mut self.workspace,
&mut self.device,
&mut self.log,
&Around {
queue: &self.queue,
tags: &self.tags,
},
);
});
})
}
fn twice(&mut self) -> Vec<String> {
self.frame(Vec::new());
self.frame(Vec::new())
}
}
fn words(shape: &egui::Shape, into: &mut Vec<String>) {
match shape {
egui::Shape::Text(text) => into.push(text.galley.text().to_string()),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|shape| words(shape, into)),
_ => {}
}
}
fn render(sets: &[(&str, &str)], kind: Fresh) {
render_view(sets, kind, Face::Edit);
}
#[test]
fn every_kind_wears_the_header_and_names_its_faces() {
let every = Fresh::FAMILIES.iter().flat_map(|family| family.kinds);
for kind in every.copied() {
for dark in [true, false] {
let mut open = Open::fresh(kind);
open.ctx.set_theme(match dark {
true => egui::ThemePreference::Dark,
false => egui::ThemePreference::Light,
});
let said = open.twice();
let has = |word: &str| said.iter().any(|held| held == word);
assert!(has("Edit"), "{kind:?} in {dark}: {said:?}");
assert!(has("Metadata"), "{kind:?} in {dark}: {said:?}");
assert!(has("Queue send"), "the loud action: {said:?}");
assert!(has("Revert") && has("Export…"), "the quiet ones: {said:?}");
}
}
}
#[test]
fn the_headers_words_never_run_under_its_controls() {
let mut open = Open::fresh(Fresh::Program);
let bytes = open.entity().bytes.clone();
open.id = open.workspace.ingest(
"Africa Split.ne5p".into(),
Origin::Device {
class: ObjectClass::Program,
at: Location { bank: 6, slot: 3 },
},
bytes,
&mut open.log,
);
open.set(&[("center_panel.organ_type", "Vox")]);
open.width = 720.0;
open.frame(Vec::new());
let output = open.output(Vec::new());
fn walk(shape: &egui::Shape, into: &mut Vec<(String, egui::Rect)>) {
match shape {
egui::Shape::Text(text) => into.push((
match text.galley.rows.len() {
1 => text.galley.text().to_string(),
rows => format!("{} (in {rows} rows)", text.galley.text()),
},
egui::Rect::from_min_size(text.pos, text.galley.size()),
)),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|shape| walk(shape, into)),
_ => {}
}
}
let mut words = Vec::new();
for clipped in &output.shapes {
walk(&clipped.shape, &mut words);
}
let header: Vec<&(String, egui::Rect)> =
words.iter().filter(|(_, rect)| rect.top() < 70.0).collect();
for (word, _) in &header {
assert!(
!word.ends_with(" rows)"),
"{word} was broken inside itself rather than moved whole"
);
}
const CONTROLS: [&str; 7] = [
"Send",
"Queue send",
"Edit",
"Metadata",
"Advanced",
"Revert",
"Export…",
];
type Placed<'a> = Vec<&'a (String, egui::Rect)>;
let (right, left): (Placed, Placed) = header.iter().partition(|(word, rect)| {
CONTROLS.contains(&word.as_str()) || (word == "⚠" && rect.left() > 300.0)
});
let edge = right
.iter()
.map(|(_, rect)| rect.left())
.fold(f32::MAX, f32::min);
let row_bottom = right
.iter()
.map(|(_, rect)| rect.bottom())
.fold(f32::MIN, f32::max);
assert!(
left.iter().any(|(word, _)| word == "1 pending"),
"the state phrase is what the left group runs out of room with: {header:?}"
);
for (word, rect) in &left {
if rect.right() > edge {
assert!(
rect.top() >= row_bottom - 1.0,
"{word:?} at {rect:?} runs under the controls, whose edge is {edge}"
);
}
}
assert!(
left.iter().any(|(_, rect)| rect.top() >= row_bottom - 1.0),
"at 720 px something has to wrap: {header:?}"
);
}
#[test]
fn a_narrowing_header_gives_up_its_words_in_one_order() {
let at = |width: f32| {
let mut open = Open::fresh(Fresh::Program);
open.width = width;
open.twice()
};
let full = at(1100.0);
assert!(full.contains(&"Export…".to_string()), "{full:?}");
assert!(full.contains(&"Edit".to_string()));
assert!(full.contains(&"Queue send".to_string()));
let quiet = at(900.0);
assert!(!quiet.contains(&"Export…".to_string()), "{quiet:?}");
assert!(quiet.contains(&"Edit".to_string()), "the faces keep theirs");
assert!(quiet.contains(&"Queue send".to_string()));
let faces = at(800.0);
assert!(!faces.contains(&"Edit".to_string()), "{faces:?}");
assert!(faces.contains(&"Queue send".to_string()));
let narrow = at(700.0);
assert!(!narrow.contains(&"Queue send".to_string()), "{narrow:?}");
assert!(
narrow.contains(&"Send".to_string()),
"the loud action is never a bare glyph: {narrow:?}"
);
}
#[test]
fn typing_in_the_name_box_renames_the_asset_and_keeps_its_tag() {
let mut open = Open::fresh(Fresh::Program);
assert_eq!(open.entity().name, "untitled.ne5p");
open.frame(Vec::new());
open.frame(vec![click(NAME_BOX)]);
open.frame(vec![egui::Event::Text("X".to_string())]);
open.frame(vec![enter()]);
let renamed = open.entity().name.clone();
assert_ne!(renamed, "untitled.ne5p", "the box was typed into");
assert!(renamed.ends_with(".ne5p"), "the tag survives: {renamed}");
assert!(renamed.contains('X'), "what was typed landed: {renamed}");
}
#[test]
fn renaming_a_wav_keeps_the_encode_draft_it_is_open_on() {
let mut open = Open::file("Marimba hit.wav", wav_bytes());
open.frame(Vec::new());
let (draft, _) = open.state().wav.as_mut().expect("a WAV opens the panel");
assert_ne!((draft.root_key, draft.top_note), (48, 60), "the defaults");
(draft.root_key, draft.top_note) = (48, 60);
open.frame(vec![click(NAME_BOX)]);
open.frame(vec![egui::Event::Text("X".to_string())]);
open.frame(vec![enter()]);
let renamed = open.entity().name.clone();
assert!(renamed.contains('X'), "the box was typed into: {renamed}");
assert!(renamed.ends_with(".wav"), "{renamed}");
open.frame(Vec::new());
let (draft, _) = open.state().wav.as_ref().expect("the same panel");
assert_eq!((draft.root_key, draft.top_note), (48, 60));
}
#[test]
fn typing_in_a_samples_name_box_writes_the_name_the_file_stores() {
let mut open = Open::file("whatever.nsmp", sample_bytes());
let stored = |open: &Open| {
sample::snapshot(open.entity().entity.as_ref().unwrap())
.unwrap()
.unwrap()
.name
};
assert_eq!(stored(&open), "Marimba");
open.frame(Vec::new());
open.frame(vec![click(NAME_BOX)]);
open.frame(vec![egui::Event::Text("X".to_string())]);
open.frame(vec![enter()]);
assert_ne!(stored(&open), "Marimba", "the instrument was renamed");
assert!(stored(&open).contains('X'), "{}", stored(&open));
assert_eq!(
open.entity().name,
"whatever.nsmp",
"the asset's own name is not what that box edits"
);
}
#[test]
fn a_stored_name_box_holds_no_more_bytes_than_the_field_does() {
let mut open = Open::file("whatever.nsmp", sample_bytes());
let held = |open: &Open| {
sample::snapshot(open.entity().entity.as_ref().expect("it decoded"))
.expect("an instrument")
.expect("it reads")
};
let limit = held(&open).max_name_len;
assert!(limit > 2, "there is room for an accented letter");
open.frame(Vec::new());
open.frame(vec![click(NAME_BOX)]);
open.frame(vec![egui::Event::Text("é".repeat(limit))]);
open.frame(vec![enter()]);
let stored = held(&open).name;
assert!(stored.contains('é'), "what was typed landed: {stored:?}");
assert!(stored.len() <= limit, "{} bytes: {stored:?}", stored.len());
assert_eq!(
open.document.refusal(),
None,
"the box never offers the field more than it holds"
);
}
#[test]
fn the_identity_row_carries_the_tags_the_asset_wears() {
let mut open = Open::fresh(Fresh::Program);
let said = open.twice();
assert!(
!said.iter().any(|word| word == "TAGS"),
"nothing is worn, so there is no cell: {said:?}"
);
let friday = open.tags.make("Friday — Blue Room").unwrap();
let organ = open.tags.make("Organ-heavy").unwrap();
for tag in [friday, organ] {
open.tags.set(open.id, tag, true);
}
let said = open.twice();
assert!(said.iter().any(|word| word == "TAGS"), "{said:?}");
for worn in ["Friday — Blue Room", "Organ-heavy"] {
assert!(said.iter().any(|word| word == worn), "{worn}: {said:?}");
}
}
const NAME_BOX: egui::Pos2 = egui::pos2(100.0, 19.0);
fn click(at: egui::Pos2) -> egui::Event {
egui::Event::PointerButton {
pos: at,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: egui::Modifiers::NONE,
}
}
fn enter() -> egui::Event {
egui::Event::Key {
key: egui::Key::Enter,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers::NONE,
}
}
fn sample_bytes() -> Vec<u8> {
let source = nord_format::wav::read_pcm16(&wav_bytes()).unwrap();
let options = nord_format::formats::nsmp::encode::Options::new("Marimba");
nord_format::formats::nsmp::encode::instrument(&source.samples, &options)
.unwrap()
.to_bytes()
.unwrap()
}
fn alone() -> (Queue, Tags) {
(Queue::default(), Tags::default())
}
fn render_view(sets: &[(&str, &str)], kind: Fresh, face: Face) {
let mut open = Open::fresh(kind);
open.document.views.insert(open.id, face);
if !sets.is_empty() {
open.set(sets);
}
open.twice();
}
#[test]
fn a_program_document_paints_for_every_organ_the_panel_offers() {
for organ in ["B3", "B3Bass", "Vox", "Farfisa", "Pipe", "unknown (6)"] {
render(&[("center_panel.organ_type", organ)], Fresh::Program);
}
}
#[test]
fn a_program_document_paints_with_each_part_in_use() {
for instrument in ["Organ", "Piano", "Sample"] {
render(&[("center_panel.upper_part", instrument)], Fresh::Program);
}
}
#[test]
fn the_header_counts_the_fields_that_differ_from_the_saved_bytes() {
let mut open = Open::fresh(Fresh::Program);
let said = open.twice();
assert!(
!said.iter().any(|word| word.ends_with("pending")),
"nothing has been touched: {said:?}"
);
open.set(&[("center_panel.gain", "96")]);
let said = open.twice();
assert!(said.iter().any(|word| word == "1 pending"), "{said:?}");
assert!(
!said.iter().any(|word| word == "edited"),
"a field document counts what moved: {said:?}"
);
open.set(&[("center_panel.split", "true")]);
let said = open.twice();
assert!(said.iter().any(|word| word == "2 pending"), "{said:?}");
}
#[test]
fn the_loud_action_carries_the_count_where_there_is_somewhere_to_send_it() {
let mut open = Open::fresh(Fresh::Program);
let bytes = open.entity().bytes.clone();
open.id = open.workspace.ingest(
"Africa Split.ne5p".into(),
Origin::Device {
class: ObjectClass::Program,
at: Location { bank: 6, slot: 3 },
},
bytes,
&mut open.log,
);
open.set(&[("center_panel.gain", "96")]);
let unattached = open.twice();
assert!(
unattached.iter().any(|word| word == "Queue send"),
"nothing is attached, so the count is not an offer: {unattached:?}"
);
open.device
.pretend_scanned(ObjectClass::Program, 7, &["", "", "", "Africa Split"]);
let attached = open.twice();
assert!(
attached.iter().any(|word| word == "Queue send · 1"),
"{attached:?}"
);
}
#[test]
fn a_group_the_instrument_is_not_using_is_named_rather_than_silently_absent() {
let mut open = Open::fresh(Fresh::Program);
let said = open.twice();
let idle: Vec<&String> = said
.iter()
.filter(|word| word.contains("stored but not in use"))
.collect();
assert!(!idle.is_empty(), "{said:?}");
assert!(
idle.iter().any(|line| line.contains("Vox")),
"a B3 program keeps the other models' registrations: {idle:?}"
);
assert!(
idle.iter().all(|line| line.contains("kept, not cleared")),
"{idle:?}"
);
}
#[test]
fn the_fields_the_edit_face_hides_are_rows_under_advanced() {
let mut open = Open::fresh(Fresh::Program);
open.document.views.insert(open.id, Face::Advanced);
let said = open.twice();
let reading = said
.iter()
.find(|word| word.contains("hidden from Edit"))
.unwrap_or_else(|| panic!("the table counts what Edit does not draw: {said:?}"));
assert!(!reading.contains("· 0 hidden"), "{reading}");
assert!(
said.iter().any(|word| word == "About this file"),
"{said:?}"
);
assert!(said.iter().any(|word| word == "Every field"), "{said:?}");
}
#[test]
fn both_stored_alternatives_paint_and_the_playing_one_says_so() {
let mut open = Open::fresh(Fresh::Program);
let said = open.twice();
for title in ["B3 · Preset 1", "B3 · Preset 2"] {
assert!(said.iter().any(|word| word == title), "{title}: {said:?}");
}
assert!(said.iter().any(|word| word == "playing"), "{said:?}");
assert!(said.iter().any(|word| word == "select"), "{said:?}");
}
#[test]
fn the_morph_lens_shows_what_a_control_becomes_under_the_wheel() {
let mut open = Open::file("blank.ns4y", Fresh::Stage4Synth.bytes().unwrap());
open.set(&[("synth_a_volume", "40"), ("synth_a_volume_wheel", "211")]);
let panel = open.twice();
assert!(
!panel.iter().any(|word| word == "211"),
"the panel shows the panel value: {panel:?}"
);
open.state().fields.pretend_lens(0);
let wheel = open.twice();
assert!(wheel.iter().any(|word| word == "211"), "{wheel:?}");
assert!(
wheel
.iter()
.any(|word| word.contains("writes the morph slot")),
"the banner says where an edit lands: {wheel:?}"
);
}
#[test]
fn a_position_the_library_could_not_name_is_shown_rather_than_hidden() {
let mut open = Open::fresh(Fresh::Program);
open.set(&[("center_panel.organ_type", "unknown (6)")]);
let said = open.twice();
assert!(
said.iter().any(|word| word == "unrecognized value (6)"),
"{said:?}"
);
}
#[test]
fn a_path_with_no_label_yet_reads_as_its_prettified_self() {
let said = Open::file("blank.ns4y", Fresh::Stage4Synth.bytes().unwrap()).twice();
assert!(!strings::known("synth_a_volume"));
assert!(
said.iter().any(|word| word == "Synth a volume"),
"{:?}",
&said[..said.len().min(40)]
);
}
#[test]
fn every_section_of_a_stage_program_is_open_and_named_in_the_nav() {
let bytes = Fresh::Stage4Program.bytes().unwrap();
let (registry, _) = fields::apply(&bytes, &[]).unwrap();
let resolved = nord_format::formats::ns4::program::PANEL.resolve(®istry);
let titles: Vec<&str> = resolved
.sections
.iter()
.filter(|section| section.relevant)
.map(|section| section.group.title)
.collect();
assert!(titles.len() > 1, "{titles:?}");
let mut open = Open::file("blank.ns4p", bytes);
let said = open.twice();
for title in titles {
let drawn = said.iter().filter(|word| *word == title).count();
assert!(
drawn >= 2,
"{title} was painted {drawn} times; the nav chip and the heading are two",
);
}
}
#[test]
fn the_meta_face_paints() {
render_view(
&[("center_panel.gain", "96")],
Fresh::Program,
Face::Metadata,
);
render_view(&[], Fresh::Settings, Face::Metadata);
}
#[test]
fn the_advanced_table_paints() {
render_view(&[], Fresh::Program, Face::Advanced);
render_view(&[], Fresh::Settings, Face::Advanced);
}
#[test]
fn the_faces_offered_are_the_ones_the_asset_has() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut log = Log::default();
let offered = |workspace: &Workspace, id: u64| -> Vec<&'static str> {
faces(shape(workspace.get(id).unwrap()))
.iter()
.map(|face| face.label())
.collect()
};
let program = workspace.create(Fresh::Program, &mut log).unwrap();
assert_eq!(
offered(&workspace, program),
["Edit", "Metadata", "Advanced"]
);
let song = workspace.ingest(
"blank.ne5t".into(),
Origin::File("blank.ne5t".into()),
Fresh::SetList.bytes().unwrap(),
&mut log,
);
assert_eq!(
offered(&workspace, song),
["Edit", "Metadata", "Advanced"],
"the four entries, the record, and the addresses as stored"
);
let stub = workspace.ingest(
"blank.ns3s".into(),
Origin::File("blank.ns3s".into()),
crate::fields::blank::stage3_song(),
&mut log,
);
assert_eq!(
offered(&workspace, stub),
["Edit", "Metadata", "Advanced"],
"a body no registry describes still says so, and shows its bytes"
);
let junk = workspace.ingest(
"junk.bin".into(),
Origin::File("junk.bin".into()),
b"not a nord file".to_vec(),
&mut log,
);
assert_eq!(offered(&workspace, junk), ["Metadata"]);
}
#[test]
fn a_document_falls_back_to_a_face_it_actually_has() {
let all = [Face::Edit, Face::Metadata, Face::Advanced];
assert_eq!(showing(&all, Face::Metadata), Face::Metadata);
assert_eq!(showing(&all[..2], Face::Advanced), Face::Edit);
let record_only = [Face::Metadata];
for left_on in [Face::Edit, Face::Advanced, Face::Metadata] {
assert_eq!(showing(&record_only, left_on), Face::Metadata);
}
let no_table = [Face::Edit, Face::Metadata];
assert_eq!(showing(&no_table, Face::Advanced), Face::Edit);
}
#[test]
fn a_refused_cell_keeps_its_error() {
let mut open = Open::fresh(Fresh::Program);
open.frame(Vec::new());
let (id, before) = (open.id, open.entity().bytes.clone());
let refused = open.document.apply(
id,
vec![("center_panel.gain".into(), "200".into())],
&mut open.workspace,
&mut open.log,
);
assert!(refused.is_err());
assert!(
refused.as_ref().unwrap_err().contains("0 .. 127"),
"the library's own words reach the operator: {refused:?}"
);
assert_eq!(
open.entity().bytes,
before,
"a refused value leaves the file untouched"
);
open.document.advanced.settled(refused);
assert!(open.document.refusal().is_some());
let taken = open.document.apply(
id,
vec![("center_panel.gain".into(), "96".into())],
&mut open.workspace,
&mut open.log,
);
assert!(taken.is_ok());
open.document.advanced.settled(taken);
assert!(open.document.refusal().is_none());
}
#[test]
fn a_set_that_leaves_the_bytes_alone_is_not_an_edit() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx);
let mut log = Log::default();
let mut document = Document::default();
let id = workspace.create(Fresh::Program, &mut log).expect("a fresh");
let entity = workspace.get(id).expect("it is open");
let (stamp, bytes) = (entity.stamp, entity.bytes.clone());
let held = fields::apply(&bytes, &[]).expect("it decodes").0;
let gain = held
.iter()
.find(|field| field.path == "center_panel.gain")
.expect("a gain field")
.value
.clone();
let outcome = document.apply(
id,
vec![("center_panel.gain".into(), gain)],
&mut workspace,
&mut log,
);
assert!(outcome.is_ok(), "{outcome:?}");
let entity = workspace.get(id).expect("it is open");
assert_eq!(entity.bytes, bytes);
assert_eq!(entity.stamp, stamp, "nothing new landed under this id");
assert!(!entity.is_unsaved());
}
#[test]
fn the_tab_strip_and_the_document_body_scroll_on_their_own() {
let ctx = egui::Context::default();
ctx.style_mut(crate::app::metrics);
ctx.set_fonts(crate::app::fonts());
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx.clone());
let mut log = Log::default();
let mut document = Document::default();
let mut tabs = crate::tabs::Tabs::default();
let (queue, tags) = alone();
let id = workspace.create(Fresh::Program, &mut log).unwrap();
tabs.open(id);
let mut ids = None;
for frame in 0..4 {
let input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(1280.0, 720.0),
)),
events: match frame {
0 => Vec::new(),
_ => vec![
egui::Event::PointerMoved(egui::pos2(900.0, 400.0)),
egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: egui::vec2(0.0, -200.0),
modifiers: egui::Modifiers::default(),
},
],
},
..Default::default()
};
let _ = ctx.run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
ids = Some((
ui.make_persistent_id(egui::Id::new(crate::tabs::SCROLL)),
ui.make_persistent_id(egui::Id::new(PAGE))
.with(egui::Id::new(SCROLL)),
));
tabs.ui(ui, &workspace, &mut Vec::new());
ui.separator();
document.ui(
ui,
id,
&mut workspace,
&mut device,
&mut log,
&Around {
queue: &queue,
tags: &tags,
},
);
});
});
}
let (strip, body) = ids.expect("the panel drew");
assert_ne!(strip, body, "one state each");
let offset = |id| egui::scroll_area::State::load(&ctx, id).map(|state| state.offset.y);
assert_eq!(offset(strip), Some(0.0), "the strip has nothing to scroll");
assert!(
offset(body).is_some_and(|y| y > 0.0),
"the body moved: {:?}",
offset(body)
);
}
#[test]
fn a_pianos_name_comes_off_the_instrument_or_not_at_all() {
use nord_usb::{Location, ObjectClass};
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let device = Device::new(ctx);
let mut log = Log::default();
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let bytes = workspace.get(id).unwrap().bytes.clone();
let fields = fields::apply(&bytes, &[]).unwrap().0;
let local = piano_lookup(workspace.get(id).unwrap(), Some(&fields), &device);
assert!(local.name.is_none(), "nothing has been asked");
assert!(!local.can_ask, "and there is nothing to ask");
let from_device = workspace.ingest(
"Africa-Split.ne5p".into(),
Origin::Device {
class: ObjectClass::Program,
at: Location { bank: 6, slot: 3 },
},
bytes,
&mut log,
);
let copied = piano_lookup(workspace.get(from_device).unwrap(), Some(&fields), &device);
assert!(copied.name.is_none(), "still nothing has been asked");
assert_eq!(copied.id, None);
}
#[test]
fn the_model_dial_lists_the_scanned_pianos_of_the_current_category() {
use nord_usb::ObjectClass;
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let bytes = workspace.get(id).unwrap().bytes.clone();
let fields = fields::apply(&bytes, &[]).unwrap().0;
let unscanned = piano_lookup(workspace.get(id).unwrap(), Some(&fields), &device);
assert!(unscanned.models.is_empty());
device.pretend_scanned(ObjectClass::Piano, 1, &["Royal Grand", "", "White Grand"]);
let scanned = piano_lookup(workspace.get(id).unwrap(), Some(&fields), &device);
assert_eq!(
scanned.models,
vec![
(0, "Royal Grand".to_string()),
(2, "White Grand".to_string())
],
"vacant slots are positions with no piano, not renumberings"
);
assert!(scanned.scan_disagrees.is_none());
let mut other = Device::new(egui::Context::default());
other.pretend_scanned(ObjectClass::Piano, 3, &["Clav D6"]);
let elsewhere = piano_lookup(workspace.get(id).unwrap(), Some(&fields), &other);
assert!(elsewhere.models.is_empty());
}
#[test]
fn a_half_typed_cell_does_not_follow_the_operator_into_the_next_document() {
let ctx = egui::Context::default();
ctx.all_styles_mut(crate::app::metrics);
ctx.set_fonts(crate::app::fonts());
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx.clone());
let mut log = Log::default();
let mut document = Document::default();
let (queue, tags) = alone();
let first = workspace.create(Fresh::Program, &mut log).unwrap();
let second = workspace.create(Fresh::Program, &mut log).unwrap();
let mut show = |document: &mut Document, id: u64| {
let _ = ctx.run(egui::RawInput::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
document.ui(
ui,
id,
&mut workspace,
&mut device,
&mut log,
&Around {
queue: &queue,
tags: &tags,
},
);
});
});
};
show(&mut document, first);
document.advanced.pretend_editing("center_panel.gain", "0");
assert_eq!(document.advanced.editing(), Some("center_panel.gain"));
show(&mut document, second);
assert_eq!(document.advanced.editing(), None, "left behind");
}
#[test]
fn an_enter_elsewhere_does_not_submit_a_refused_cell_again() {
let mut open = Open::fresh(Fresh::Program);
open.document.views.insert(open.id, Face::Advanced);
open.frame(Vec::new());
open.document
.advanced
.pretend_editing("center_panel.gain", "200");
open.frame(Vec::new());
open.frame(vec![enter()]);
assert!(
open.document.refusal().is_some(),
"the library turned 200 down"
);
assert_eq!(
open.document.advanced.editing(),
Some("center_panel.gain"),
"and the cell keeps what was typed"
);
open.frame(Vec::new());
open.frame(vec![click(NAME_BOX)]);
let said = open.log.len();
open.frame(vec![enter()]);
assert_eq!(open.log.len(), said, "nothing was submitted a second time");
}
#[test]
fn a_library_id_reads_in_either_spelling() {
assert_eq!(library_id("16909060"), Some(0x0102_0304));
assert_eq!(library_id("0x01020304"), Some(0x0102_0304));
assert_eq!(library_id(" 0 "), Some(0));
assert_eq!(library_id("nothing"), None);
}
#[test]
fn the_other_fresh_defaults_paint() {
render(&[], Fresh::Live);
render(&[], Fresh::Settings);
}
fn render_file(name: &str, bytes: Vec<u8>, face: Face) {
let mut open = Open::file(name, bytes);
open.document.views.insert(open.id, face);
open.twice();
}
#[test]
fn a_stage_document_paints_from_the_registry_alone() {
for (name, bytes) in [
("blank.ns2p", Fresh::Stage2Program.bytes().unwrap()),
("blank.ns3y", Fresh::Stage3Synth.bytes().unwrap()),
("blank.ns4p", Fresh::Stage4Program.bytes().unwrap()),
("blank.ns4o", Fresh::Stage4Organ.bytes().unwrap()),
("blank.ns4n", Fresh::Stage4Piano.bytes().unwrap()),
("blank.ns4y", Fresh::Stage4Synth.bytes().unwrap()),
] {
render_file(name, bytes.clone(), Face::Edit);
render_file(name, bytes, Face::Advanced);
}
}
#[test]
fn a_set_list_has_its_own_view() {
let bytes = Fresh::SetList.bytes().unwrap();
let song = nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).unwrap();
assert!(fields::is_set_list(&song));
assert!(!fields::has_registry(&song));
for face in [Face::Edit, Face::Metadata, Face::Advanced] {
render_file("blank.ne5t", bytes.clone(), face);
}
}
#[test]
fn a_body_with_no_registry_says_why_and_shows_its_bytes() {
let bytes = crate::fields::blank::stage3_song();
let song = nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).unwrap();
assert!(!fields::is_set_list(&song));
assert!(!fields::has_registry(&song));
let mut open = Open::file("blank.ns3s", bytes);
let said = open.twice();
let has = |word: &str| said.iter().any(|held| held == word);
assert!(has("Nothing to edit here yet"), "{said:?}");
assert!(
said.iter()
.any(|word| word.starts_with("No registry declares this model's fields yet")),
"the sentence: {said:?}"
);
for fact in ["Format", "Model", "Container", "Body", "Version", "Where"] {
assert!(has(fact), "{fact} is one of the facts: {said:?}");
}
assert!(has("Save a copy…"), "the one thing left to do: {said:?}");
assert!(
has("Send as-is"),
"the loud action is the same send in this body's words: {said:?}"
);
assert!(has("0000") && has("0020"), "the body as hex: {said:?}");
open.document.views.insert(open.id, Face::Advanced);
let said = open.twice();
assert!(
said.iter().any(|word| word == "Body bytes"),
"the whole body has a face of its own: {said:?}"
);
assert!(said.iter().any(|word| word == "3 rows"), "{said:?}");
}
fn piano_library_bytes() -> Vec<u8> {
use nord_format::cbin::{Cbin, Header, RawBody};
use nord_format::formats::nsclassic;
let file = Cbin {
header: Header::new(nsclassic::piano_library::FORMAT, (0, 0), 0),
body: RawBody(vec![0u8; 48]),
};
nord_format::to_bytes(&nord_format::Entity::PianoLibrary(file)).expect("a stub encodes")
}
#[test]
fn a_body_with_no_editor_of_its_own_is_verbatim_whatever_kind_it_is() {
let held = |bytes: Vec<u8>| shape(Open::file("held", bytes).entity());
assert_eq!(
held(crate::workspace::Fresh::Stage4Program.bytes().unwrap()),
Shape::Fields
);
assert_eq!(
held(crate::workspace::Fresh::SetList.bytes().unwrap()),
Shape::SetList
);
assert_eq!(held(sample_bytes()), Shape::Sample);
assert_eq!(held(project_bytes()), Shape::Project);
assert_eq!(held(piano_bytes()), Shape::Piano);
assert_eq!(held(fields::blank::stage3_song()), Shape::Verbatim);
assert_eq!(held(piano_library_bytes()), Shape::Verbatim);
assert_eq!(held(wav_bytes()), Shape::Wav);
assert_eq!(held(b"not a nord file".to_vec()), Shape::Undecoded);
}
#[test]
fn a_stage_classic_piano_library_wears_the_verbatim_faces() {
let mut open = Open::file("Grand.nsp", piano_library_bytes());
assert_eq!(
faces(shape(open.entity()))
.iter()
.map(|face| face.label())
.collect::<Vec<_>>(),
["Edit", "Metadata", "Advanced"],
);
let said = open.twice();
let has = |word: &str| said.iter().any(|held| held == word);
assert!(has("Nothing to edit here yet"), "{said:?}");
assert!(has("Send as-is"), "{said:?}");
}
#[test]
fn a_set_lists_header_claims_only_what_the_instrument_showed() {
let mut open = Open::file("Blue Room.ne5t", Fresh::SetList.bytes().unwrap());
let said = open.twice();
assert!(
!said.iter().any(|word| word.contains("needs attention")),
"nothing is attached, so nothing is claimed: {said:?}"
);
open.device.pretend_scanned(
ObjectClass::Program,
1,
&["Africa Split", "", "Gospel Perc"],
);
let said = open.twice();
assert!(
said.iter().any(|word| word == "1 entry needs attention"),
"{said:?}"
);
}
#[test]
fn a_file_that_did_not_decode_still_paints() {
let said = Open::file("junk.bin", b"not a nord file".to_vec()).twice();
assert!(
said.iter().any(|word| word.contains("did not decode")),
"the record is all these bytes have: {said:?}"
);
}
fn wav_bytes() -> Vec<u8> {
let samples: Vec<i16> = (0..codec::SOURCE_RATE as usize)
.map(|i| ((i as f64 / 40.0).sin() * 12_000.0) as i16)
.collect();
nord_format::wav::mono_pcm16(&samples, codec::SOURCE_RATE).unwrap()
}
#[test]
fn a_wav_offers_an_encode_and_leaves_itself_alone() {
let bytes = wav_bytes();
let mut open = Open::file("Marimba hit.wav", bytes.clone());
assert_eq!(
faces(shape(open.entity()))
.iter()
.map(|face| face.label())
.collect::<Vec<_>>(),
["Edit", "Metadata"],
);
open.frame(Vec::new());
let Open {
mut document,
mut workspace,
mut log,
id,
..
} = open;
document.answer(id, Asked::Encode, &mut workspace, &mut log);
assert!(document.refusal().is_none(), "{:?}", document.refusal());
assert_eq!(workspace.get(id).unwrap().bytes, bytes, "the WAV is intact");
let made = workspace
.entities()
.iter()
.find(|e| e.id != id)
.expect("an instrument was added");
assert_eq!(made.name, "Marimba hit.nsmp");
let snapshot = sample::snapshot(made.entity.as_ref().expect("it decoded"))
.unwrap()
.unwrap();
assert_eq!(snapshot.name, "Marimba hit");
assert_eq!(snapshot.zones.len(), 1);
}
#[test]
fn a_zone_decodes_on_request_and_exports_under_the_instruments_name() {
let source = nord_format::wav::read_pcm16(&wav_bytes()).unwrap();
let options = nord_format::formats::nsmp::encode::Options::new("Marimba").root_key(48);
let bytes = nord_format::formats::nsmp::encode::instrument(&source.samples, &options)
.unwrap()
.to_bytes()
.unwrap();
let mut open = Open::file("whatever-it-was-called.nsmp", bytes);
let id = open.id;
let stamp = open.entity().stamp;
open.document.audio.follow(id, stamp);
assert!(
open.document.audio.get(0).is_none(),
"nothing decodes unasked"
);
open.document.answer(
id,
Asked::Zone(sample::Ask::Decode(0)),
&mut open.workspace,
&mut open.log,
);
{
let decoded = open.document.audio.get(0).expect("asked for");
let decoded = decoded.as_ref().unwrap();
assert!(decoded.audio.seconds() > 0.5);
assert_eq!(decoded.audio.channels, 1);
assert!(!decoded.envelope.is_empty());
}
open.frame(Vec::new());
assert_eq!(
open.document.instrument_name(id, &open.workspace),
"Marimba"
);
assert_eq!(
crate::workspace::zone_wav_name(&open.document.instrument_name(id, &open.workspace), 1),
"Marimba-zone1.wav",
);
let edited =
sample::apply(&open.entity().bytes, &[("name".into(), "Vibes".into())]).unwrap();
open.workspace.replace_bytes(id, edited, &mut open.log);
let stamp = open.entity().stamp;
open.document.audio.follow(id, stamp);
assert!(
open.document.audio.get(0).is_none(),
"decoded from stale bytes"
);
assert_eq!(open.document.instrument_name(id, &open.workspace), "Vibes");
}
fn project_bytes() -> Vec<u8> {
include_bytes!("../../../nord-format/tests/fixtures/nsmpproj/three-zones.nsmpproj").to_vec()
}
#[test]
fn an_instrument_and_a_project_offer_all_three_faces() {
for (name, bytes) in [
("Marimba.nsmp", sample_bytes()),
("clarinet.nsmpproj", project_bytes()),
] {
let open = Open::file(name, bytes);
let entity = open.entity();
let registry = entity.entity.as_ref().and_then(fields::fields_of);
assert!(registry.is_none(), "{name} declares no field registry");
assert_eq!(
faces(shape(entity))
.iter()
.map(|face| face.label())
.collect::<Vec<_>>(),
["Edit", "Metadata", "Advanced"],
"{name}",
);
}
}
#[test]
fn a_project_document_paints_on_every_face() {
for face in [Face::Edit, Face::Metadata, Face::Advanced] {
render_file("clarinet.nsmpproj", project_bytes(), face);
render_file("Marimba.nsmp", sample_bytes(), face);
}
}
#[test]
fn the_key_map_is_painted_above_the_scrolling_rows() {
let mut open = Open::file("Marimba.nsmp", sample_bytes());
open.frame(Vec::new());
let output = open.output(Vec::new());
let placed = |word: &str| -> (egui::Rect, egui::Rect) {
fn walk(
shape: &egui::Shape,
clip: egui::Rect,
word: &str,
into: &mut Vec<(egui::Rect, egui::Rect)>,
) {
match shape {
egui::Shape::Text(text) if text.galley.text() == word => into.push((
egui::Rect::from_min_size(text.pos, text.galley.size()),
clip,
)),
egui::Shape::Vec(shapes) => shapes
.iter()
.for_each(|shape| walk(shape, clip, word, into)),
_ => {}
}
}
let mut found = Vec::new();
for clipped in &output.shapes {
walk(&clipped.shape, clipped.clip_rect, word, &mut found);
}
*found
.first()
.unwrap_or_else(|| panic!("{word} was never painted"))
};
let (map, pinned) = placed("Key map");
let (_, scrolling) = placed("Zones");
assert_ne!(pinned, scrolling, "two regions, not one");
assert!(
map.bottom() <= scrolling.top(),
"the map at {map:?} is inside the rows' own region {scrolling:?}",
);
}
#[test]
fn leaving_a_document_forgets_the_open_zone_and_keeps_the_edit() {
let mut open = Open::file("Marimba.nsmp", sample_bytes());
open.frame(Vec::new());
sample::pick_row(&mut open.state().sample, 0);
assert_eq!(sample::selected(&open.state().sample), Some(0));
let edited = sample::apply(
&open.entity().bytes,
&[("zone1.top_note".into(), "C6".into())],
)
.unwrap();
open.workspace.replace_bytes(open.id, edited, &mut open.log);
let elsewhere = open.workspace.ingest(
"other.nsmp".into(),
Origin::File("other.nsmp".into()),
sample_bytes(),
&mut open.log,
);
let id = open.id;
open.id = elsewhere;
open.frame(Vec::new());
open.id = id;
open.frame(Vec::new());
assert_eq!(
sample::selected(&open.state().sample),
None,
"the selection is the instrument's, not the editor's"
);
let snapshot = sample::snapshot(open.entity().entity.as_ref().unwrap())
.unwrap()
.unwrap();
assert_eq!(snapshot.zones[0].top_note, 84, "the edit stands");
}
fn piano_bytes() -> Vec<u8> {
nord_format::formats::npno::synthetic::Build::new()
.bytes()
.expect("the builder lays out a library")
}
#[test]
fn a_piano_document_offers_every_face_and_paints_on_each_of_them() {
let mut open = Open::file("Test Piano.npno", piano_bytes());
assert_eq!(
faces(shape(open.entity()))
.iter()
.map(|face| face.label())
.collect::<Vec<_>>(),
["Edit", "Metadata", "Advanced"],
);
for face in [Face::Edit, Face::Metadata, Face::Advanced] {
for dark in [true, false] {
let mut open = Open::file("Test Piano.npno", piano_bytes());
open.ctx.set_theme(match dark {
true => egui::ThemePreference::Dark,
false => egui::ThemePreference::Light,
});
open.document.views.insert(open.id, face);
let said = open.twice();
let has = |word: &str| said.iter().any(|held| held == word);
assert!(has("Test Piano"), "{face:?} in {dark}: {said:?}");
match face {
Face::Edit => assert!(has("Trim to fit"), "{said:?}"),
Face::Metadata => {
assert!(has("Metadata") && has("Container"), "{said:?}");
assert!(!has("Key map"), "the map is the Edit face's: {said:?}");
}
Face::Advanced => {
assert!(has("What this format holds") && has("Offsets"), "{said:?}")
}
}
}
}
open.document.views.insert(open.id, Face::Edit);
assert!(open.twice().iter().any(|word| word == "Key map"));
}
#[test]
fn a_piano_lays_its_plan_out_before_anything_carries_its_bytes() {
let mut open = Open::file("Test Piano.npno", piano_bytes());
let named = |open: &Open| {
piano::snapshot(open.entity().entity.as_ref().unwrap())
.unwrap()
.unwrap()
.name
};
open.frame(Vec::new());
open.frame(vec![click(NAME_BOX)]);
open.frame(vec![egui::Event::Text("X".to_string())]);
open.frame(vec![enter()]);
assert!(open.document.pends(open.id), "the rename is a plan");
assert_eq!(
open.entity().bytes,
open.entity().saved.bytes,
"and nothing has copied the body for it",
);
assert_eq!(named(&open), "Test Piano", "nor written it");
let ctx = open.ctx.clone();
let mut acts = open.document.settle(
&ctx,
vec![crate::browser::Act::SaveDoc(open.id)],
&mut open.workspace,
&mut open.log,
);
assert!(
acts.is_empty() || !open.document.pends(open.id),
"the save waits for the apply",
);
if acts.is_empty() {
let applied = open
.document
.piano
.awaited(&ctx, &open.workspace)
.expect("the apply the save waits on is in flight");
acts = open
.document
.put_back(applied, &mut open.workspace, &mut open.log);
}
assert!(
matches!(acts.as_slice(), [crate::browser::Act::SaveDoc(id)] if *id == open.id),
"the save is what comes back",
);
assert!(named(&open).contains('X'), "{}", named(&open));
assert!(open.entity().is_unsaved(), "and it is the save's to settle");
assert!(!open.document.pends(open.id));
}
#[test]
fn reverting_a_piano_from_the_menu_drops_the_plan_it_was_holding() {
let mut open = Open::file("Test Piano.npno", piano_bytes());
open.frame(Vec::new());
open.frame(vec![click(NAME_BOX)]);
open.frame(vec![egui::Event::Text("X".to_string())]);
open.frame(vec![enter()]);
assert!(open.document.pends(open.id));
let ctx = open.ctx.clone();
let acts = open.document.settle(
&ctx,
vec![crate::browser::Act::Revert(open.id)],
&mut open.workspace,
&mut open.log,
);
assert_eq!(acts.len(), 1, "the revert itself still runs");
assert!(!open.document.pends(open.id), "the plan is gone with it");
}
#[test]
fn a_sample_document_paints() {
let source = nord_format::wav::read_pcm16(&wav_bytes()).unwrap();
let options = nord_format::formats::nsmp::encode::Options::new("Marimba");
let bytes = nord_format::formats::nsmp::encode::instrument(&source.samples, &options)
.unwrap()
.to_bytes()
.unwrap();
render_file("Marimba.nsmp", bytes.clone(), Face::Edit);
render_file("Marimba.nsmp", bytes, Face::Metadata);
render_file("Marimba hit.wav", wav_bytes(), Face::Edit);
}
}