use std::collections::{HashMap, VecDeque};
use std::sync::mpsc::Receiver;
use eframe::egui;
use nord_format::accept::{Acceptance, Family};
use nord_usb::wire::{AllocationUnit, Bank, Dependency, ProgramInfo, Status};
use nord_usb::{Location, ObjectClass};
use crate::log::Log;
use crate::queue::Queue;
use crate::strings::{folder, place, shown};
use crate::tabs::Tabs;
use crate::workspace::{LocalEntity, Origin, Workspace};
mod scan;
mod worker;
#[cfg(not(target_arch = "wasm32"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
use native::Link;
#[cfg(target_arch = "wasm32")]
mod web;
#[cfg(target_arch = "wasm32")]
use web::Link;
pub use scan::{Progress, Scan};
pub use worker::{Emit, Flow};
pub(crate) fn user_bank(index: u32) -> Option<u32> {
index.checked_add(1)
}
pub struct Partition {
pub class: ObjectClass,
pub name: String,
pub native: bool,
pub unit: Option<AllocationUnit>,
}
#[derive(Clone)]
pub enum DeviceCmd {
ScanClass {
class: ObjectClass,
},
ScanBank {
class: ObjectClass,
bank: u32,
},
SlotInfo {
class: ObjectClass,
at: Location,
},
Deps {
class: ObjectClass,
at: Location,
},
Get {
class: ObjectClass,
at: Location,
why: Purpose,
},
Put {
id: u64,
class: ObjectClass,
at: Location,
name: String,
bytes: Vec<u8>,
},
SendAll {
class: ObjectClass,
items: Vec<Outgoing>,
},
Move {
class: ObjectClass,
from: Location,
to: Location,
},
Duplicate {
class: ObjectClass,
from: Location,
to: Location,
},
Delete {
class: ObjectClass,
at: Location,
},
Rename {
class: ObjectClass,
at: Location,
name: String,
},
Select {
class: ObjectClass,
at: Location,
},
Disconnect,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Purpose {
View,
Copy,
Compare,
}
#[derive(Clone)]
pub struct Outgoing {
pub id: u64,
pub at: Location,
pub name: String,
pub bytes: Vec<u8>,
}
#[derive(Clone)]
pub struct Words {
pub doing: String,
pub done: String,
pub failed: String,
}
fn words(verbs: (&str, &str, &str), what: String) -> Words {
Words {
doing: format!("{} {what}…", verbs.0),
done: format!("{} {what}", verbs.1),
failed: format!("Could not {} {what}", verbs.2),
}
}
const READING: (&str, &str, &str) = ("Reading", "Read", "read");
const COPYING: (&str, &str, &str) = ("Copying", "Copied", "copy");
impl DeviceCmd {
pub fn label(&self) -> String {
match self {
DeviceCmd::ScanClass { class } => format!("scan {}", class.label()),
DeviceCmd::ScanBank { bank, .. } => format!("scan bank {bank}"),
DeviceCmd::SlotInfo { at, .. } => format!("info {}", shown(*at)),
DeviceCmd::Deps { at, .. } => format!("deps {}", shown(*at)),
DeviceCmd::Get { at, why, .. } => match why {
Purpose::Compare => format!("get {} (to compare)", shown(*at)),
Purpose::Copy | Purpose::View => format!("get {}", shown(*at)),
},
DeviceCmd::Put { at, name, .. } => format!("put {name} -> {}", shown(*at)),
DeviceCmd::SendAll { class, items } => {
format!("put {} objects -> {}", items.len(), class.label())
}
DeviceCmd::Move { from, to, .. } => {
format!("move {} -> {}", shown(*from), shown(*to))
}
DeviceCmd::Duplicate { from, to, .. } => {
format!("duplicate {} -> {}", shown(*from), shown(*to))
}
DeviceCmd::Delete { at, .. } => format!("delete {}", shown(*at)),
DeviceCmd::Rename { at, name, .. } => format!("rename {} to {name:?}", shown(*at)),
DeviceCmd::Select { at, .. } => format!("select {}", shown(*at)),
DeviceCmd::Disconnect => "disconnect".into(),
}
}
pub fn words(&self) -> Words {
match self {
DeviceCmd::ScanClass { class } => words(READING, folder(*class).to_string()),
DeviceCmd::ScanBank { class, bank, .. } => {
words(READING, format!("{} — bank {bank}", folder(*class)))
}
DeviceCmd::SlotInfo { class, at } => words(READING, place(*class, *at)),
DeviceCmd::Deps { class, at } => {
words(READING, format!("what {} needs", place(*class, *at)))
}
DeviceCmd::Get {
class,
at,
why: Purpose::Compare,
..
} => words(READING, format!("what is in {}", place(*class, *at))),
DeviceCmd::Get { class, at, .. } => {
words(COPYING, format!("{} to this computer", place(*class, *at)))
}
DeviceCmd::Put {
class, at, name, ..
} => words(
("Sending", "Sent", "send"),
format!("“{name}” to {}", place(*class, *at)),
),
DeviceCmd::SendAll { class, items } => words(
("Sending", "Sent", "send"),
match items.len() {
1 => format!("1 sound to {}", folder(*class)),
n => format!("{n} sounds to {}", folder(*class)),
},
),
DeviceCmd::Move { class, from, to } => words(
("Moving", "Moved", "move"),
format!("{} to {}", place(*class, *from), place(*class, *to)),
),
DeviceCmd::Duplicate { class, from, to } => words(
COPYING,
format!("{} to {}", place(*class, *from), place(*class, *to)),
),
DeviceCmd::Delete { class, at } => {
words(("Deleting", "Deleted", "delete"), place(*class, *at))
}
DeviceCmd::Rename { class, at, name } => words(
("Renaming", "Renamed", "rename"),
format!("{} to “{name}”", place(*class, *at)),
),
DeviceCmd::Select { class, at } => words(
("Loading", "Loaded", "load"),
format!("{} on the instrument", place(*class, *at)),
),
DeviceCmd::Disconnect => words(
("Releasing", "Released", "release"),
"the instrument".into(),
),
}
}
}
pub enum DeviceEvent {
Connected(DeviceCard),
ConnectFailed(String),
Disconnected {
lost: bool,
},
Started(String),
Finished,
Partitions(Vec<Partition>),
ClassStatus {
class: ObjectClass,
status: Status,
banks: Option<u32>,
},
Geometry {
class: ObjectClass,
banks: Vec<Bank>,
},
Focus {
class: ObjectClass,
at: Option<Location>,
},
BankScanned {
class: ObjectClass,
bank: u32,
slots: Vec<Option<ProgramInfo>>,
},
SlotInfo {
class: ObjectClass,
at: Location,
info: Option<ProgramInfo>,
},
Deps {
class: ObjectClass,
at: Location,
deps: Vec<Dependency>,
},
Got {
name: String,
origin: Origin,
bytes: Vec<u8>,
why: Purpose,
},
Vacant {
class: ObjectClass,
at: Location,
why: Purpose,
},
Sent {
id: u64,
class: ObjectClass,
at: Location,
bytes: Vec<u8>,
},
Rescued {
at: Location,
name: String,
bytes: Vec<u8>,
},
Note(String),
OpOk(String),
OpFailed(String),
InstrumentChanged,
}
#[derive(Clone)]
pub struct DeviceCard {
pub product: String,
pub manufacturer: Option<String>,
pub vendor_id: u16,
pub product_id: u16,
pub serial: Option<String>,
pub interface: Option<u8>,
pub firmware: Option<u16>,
pub build: Option<u16>,
pub kind: Option<u16>,
pub max_transfer: Option<u32>,
}
#[derive(Default)]
pub enum Connection {
#[default]
Disconnected,
Connecting,
Connected(DeviceCard),
}
#[derive(Default)]
pub struct Detail {
pub at: Option<(ObjectClass, Location)>,
pub info: Option<Option<ProgramInfo>>,
pub deps: Option<Vec<Dependency>>,
}
#[derive(Default)]
pub struct DeviceState {
pub connection: Connection,
pub in_flight: Option<Words>,
pub inventory: Vec<Status>,
pub scan: Scan,
selected: HashMap<u32, Location>,
focus: HashMap<u32, Option<Location>>,
geometry: HashMap<u32, Vec<Bank>>,
partitions: Vec<Partition>,
banks: HashMap<(u32, u32), Vec<Option<ProgramInfo>>>,
pub detail: Detail,
}
impl DeviceState {
pub fn connected(&self) -> bool {
matches!(self.connection, Connection::Connected(_))
}
pub fn product(&self) -> Option<&str> {
self.card().map(|card| card.product.as_str())
}
pub fn card(&self) -> Option<&DeviceCard> {
match &self.connection {
Connection::Connected(card) => Some(card),
_ => None,
}
}
pub fn firmware(&self) -> Option<String> {
self.card()?
.firmware
.map(|held| format!("{}.{:02}", held / 100, held % 100))
}
pub fn bank_name(&self, class: ObjectClass, bank: u32) -> Option<&str> {
let name = self
.geometry
.get(&class.to_raw())?
.iter()
.find(|held| user_bank(held.index) == Some(bank))?
.name
.trim();
(!name.is_empty()).then_some(name)
}
pub fn focused(&self, class: ObjectClass) -> Option<Location> {
self.focus.get(&class.to_raw()).copied().flatten()
}
pub fn dependency_name(
&self,
slot: Option<(ObjectClass, Location)>,
class: ObjectClass,
id: u32,
) -> Option<&str> {
if self.detail.at != Some(slot?) {
return None;
}
self.detail
.deps
.as_ref()?
.iter()
.find(|dep| dep.class == class && dep.id == id)
.map(|dep| dep.name.trim())
}
pub fn bank(&self, class: ObjectClass, bank: u32) -> Option<&[Option<ProgramInfo>]> {
self.banks.get(&(class.to_raw(), bank)).map(Vec::as_slice)
}
pub fn classes(&self) -> Vec<ObjectClass> {
self.partitions
.iter()
.filter(|row| !row.native)
.map(|row| row.class)
.collect()
}
pub fn folder_name(&self, class: ObjectClass) -> &str {
let ObjectClass::Unknown(_) = class else {
return folder(class);
};
self.partition(class)
.map(|row| row.name.as_str())
.filter(|name| !name.is_empty())
.unwrap_or_else(|| folder(class))
}
pub fn allocation_unit(&self, class: ObjectClass) -> Option<AllocationUnit> {
self.partition(class)?.unit
}
fn partition(&self, class: ObjectClass) -> Option<&Partition> {
self.partitions
.iter()
.find(|row| !row.native && row.class == class)
}
pub fn banks(&self, class: ObjectClass) -> usize {
self.geometry.get(&class.to_raw()).map_or(0, Vec::len)
}
pub fn banks_of(&self, class: ObjectClass) -> Vec<u32> {
let mut banks: Vec<u32> = self
.banks
.keys()
.filter(|(raw, _)| *raw == class.to_raw())
.map(|(_, bank)| *bank)
.collect();
banks.sort_unstable();
banks
}
pub fn slot(&self, class: ObjectClass, at: Location) -> Option<Option<&ProgramInfo>> {
let bank = self.bank(class, user_bank(at.bank)?)?;
bank.get(at.slot as usize).map(Option::as_ref)
}
pub fn formats_in(&self, class: ObjectClass) -> Vec<String> {
let mut seen: Vec<String> = Vec::new();
for bank in self.banks_of(class) {
for info in self.bank(class, bank).into_iter().flatten().flatten() {
let format = info.format.trim();
if !format.is_empty() && !seen.iter().any(|held| held == format) {
seen.push(format.to_string());
}
}
}
seen
}
pub fn slots_of(
&self,
class: ObjectClass,
bank: u32,
) -> impl Iterator<Item = (Location, &Option<ProgramInfo>)> + '_ {
self.bank(class, bank)
.unwrap_or_default()
.iter()
.zip(1u32..)
.map(move |(held, slot)| (Location::from_user(bank, slot), held))
}
pub fn free_slots(&self, class: ObjectClass) -> impl Iterator<Item = Location> + '_ {
self.banks_of(class).into_iter().flat_map(move |bank| {
self.slots_of(class, bank)
.filter(|(_, held)| held.is_none())
.map(|(at, _)| at)
})
}
pub fn first_free(&self, class: ObjectClass, taken: &[Location]) -> Option<Location> {
self.free_slots(class).find(|at| !taken.contains(at))
}
fn forget_bank(&mut self, class: ObjectClass, bank: u32) {
self.banks.remove(&(class.to_raw(), bank));
}
fn forget_everything(&mut self) {
self.banks.clear();
self.focus.clear();
self.geometry.clear();
self.partitions.clear();
self.inventory.clear();
self.detail = Detail::default();
self.scan.clear();
self.selected.clear();
}
}
pub fn occupancy(
class: ObjectClass,
inventory: &[Status],
unit: Option<AllocationUnit>,
) -> Option<String> {
let status = inventory.iter().find(|status| status.class == class)?;
if let Some(slots) = status.slots() {
return Some(format!("{}/{slots}", status.count));
}
let Some(unit) = unit else {
return Some(format!("{} items", status.count));
};
let bytes = |units: u64| units.saturating_mul(u64::from(unit.get()));
Some(crate::room::measure_out_of(
bytes(u64::from(status.used)),
bytes(status.total()),
))
}
#[cfg(test)]
pub fn pretend_allocation_unit(class: ObjectClass, bytes: u32) -> AllocationUnit {
nord_usb::wire::Partition {
index: class.to_raw(),
name: String::new(),
native: false,
fields: bytes.to_be_bytes().to_vec(),
}
.allocation_unit()
.expect("a partition reporting a unit of at least one")
}
#[cfg(test)]
pub const ELECTRO5: [(ObjectClass, &str, u32); 6] = [
(ObjectClass::Piano, "Piano", 261_632),
(ObjectClass::Sample, "Samp Lib", 131_064),
(ObjectClass::Program, "Program", 1),
(ObjectClass::SetList, "Set List", 1),
(ObjectClass::Live, "Live", 1),
(ObjectClass::Settings, "Settings", 1),
];
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Fit {
Unattached,
Takes,
Warn(String),
Refuses(String),
}
impl Fit {
pub fn allowed(&self) -> bool {
!matches!(self, Fit::Refuses(_))
}
pub fn why(&self) -> Option<&str> {
match self {
Fit::Warn(why) | Fit::Refuses(why) => Some(why),
Fit::Unattached | Fit::Takes => None,
}
}
}
pub fn fit(state: &DeviceState, entity: &LocalEntity) -> Fit {
let Some(product) = state.product() else {
return Fit::Unattached;
};
let Some(class) = crate::browser::Kind::of(entity.entity.as_ref()).home() else {
return Fit::Takes;
};
let tag = entity.tag();
let resident = || crate::browser::foreign_format(&tag, &state.formats_in(class));
let unknown = || match resident() {
Some(why) => Fit::Warn(why),
None => Fit::Takes,
};
let (Some(slot), Some(family)) = (class.storage(), Family::from_product(product)) else {
return unknown();
};
match family.accepts(slot, &tag) {
Acceptance::Confirmed => Fit::Takes,
Acceptance::Inferred => Fit::Warn(format!(
"This is a {} file and the instrument is a {product}, but no file of this \
kind has ever been written to one. Sending it is untried.",
family.label()
)),
Acceptance::Refused => Fit::Refuses(match Family::of_tag(&tag) {
Some(owner) => format!(
"This is a {} file and the instrument is a {product}.",
owner.label()
),
None => format!("A {tag} file is not one a {product} takes."),
}),
Acceptance::Unknown => unknown(),
}
}
pub fn link(state: &DeviceState, entity: &LocalEntity) -> Option<(ObjectClass, Location)> {
if !fit(state, entity).allowed() {
return None;
}
let class = home(entity)?;
if let Some(origin) = entity
.origin
.slot()
.filter(|(class, at)| state.slot(*class, *at).flatten().is_some())
{
return Some(origin);
}
let by_body = matchable(entity).and_then(|(folder, crc)| among(state, folder, crc, entity));
match by_body {
Some(at) => Some((class, at)),
None => stands(state, entity).or_else(|| Some((class, named(state, class, entity)?))),
}
}
pub fn also_holding(state: &DeviceState, entity: &LocalEntity) -> usize {
let Some((class, here)) = matchable(entity) else {
return 0;
};
holding(state, class, here).count().saturating_sub(1)
}
fn home(entity: &LocalEntity) -> Option<ObjectClass> {
entity
.kept
.then(|| crate::browser::Kind::of(entity.entity.as_ref()).home())
.flatten()
}
fn matchable(entity: &LocalEntity) -> Option<(ObjectClass, u32)> {
Some((home(entity)?, entity.saved.crc32?))
}
fn named(state: &DeviceState, class: ObjectClass, entity: &LocalEntity) -> Option<Location> {
match class {
ObjectClass::Settings => {
let mut slots = occupied(state, class);
let (at, _) = slots.next()?;
slots.next().is_none().then_some(at)
}
ObjectClass::Sample | ObjectClass::Piano => {
let name = entity.name.trim();
occupied(state, class)
.find(|(_, info)| info.name.trim() == name)
.map(|(at, _)| at)
}
_ => None,
}
}
fn occupied(
state: &DeviceState,
class: ObjectClass,
) -> impl Iterator<Item = (Location, &ProgramInfo)> + '_ {
state.banks_of(class).into_iter().flat_map(move |bank| {
state
.bank(class, bank)
.unwrap_or_default()
.iter()
.enumerate()
.filter_map(move |(slot, held)| {
Some((Location::from_user(bank, slot as u32 + 1), held.as_ref()?))
})
})
}
fn holding(
state: &DeviceState,
class: ObjectClass,
crc: u32,
) -> impl Iterator<Item = Location> + '_ {
occupied(state, class)
.filter(move |(_, info)| info.crc32 == Some(crc))
.map(|(at, _)| at)
}
fn among(
state: &DeviceState,
class: ObjectClass,
crc: u32,
entity: &LocalEntity,
) -> Option<Location> {
let held = |known: Option<(ObjectClass, Location)>| {
known
.filter(|(held, at)| {
*held == class && holding(state, class, crc).any(|other| other == *at)
})
.map(|(_, at)| at)
};
held(entity.origin.slot())
.or_else(|| held(entity.link))
.or_else(|| holding(state, class, crc).next())
}
fn stands(state: &DeviceState, entity: &LocalEntity) -> Option<(ObjectClass, Location)> {
let (class, at) = entity.link?;
match state.slot(class, at) {
Some(None) => None,
Some(Some(_)) | None => Some((class, at)),
}
}
pub fn read_only(class: ObjectClass) -> bool {
matches!(class, ObjectClass::Unknown(_))
}
pub fn write_warning(class: ObjectClass) -> Option<&'static str> {
match class {
ObjectClass::Settings => Some(
"Writing settings reloads the selected program on the instrument. \
Panel changes that have not been stored will be lost.",
),
_ => None,
}
}
pub struct Device {
pub state: DeviceState,
events: Receiver<DeviceEvent>,
#[cfg(test)]
from_worker: std::sync::mpsc::Sender<DeviceEvent>,
link: Link,
pending: VecDeque<DeviceCmd>,
reading: Option<ObjectClass>,
rescan: Vec<(ObjectClass, u32)>,
reselect: Vec<(ObjectClass, Location)>,
writing: Option<ObjectClass>,
linked: u64,
}
impl Device {
pub fn new(ctx: egui::Context) -> Device {
let (sender, events) = std::sync::mpsc::channel();
Device {
state: DeviceState::default(),
events,
#[cfg(test)]
from_worker: sender.clone(),
link: Link::new(ctx, sender),
pending: VecDeque::new(),
reading: None,
rescan: Vec::new(),
reselect: Vec::new(),
writing: None,
linked: 0,
}
}
pub fn connect(&mut self, log: &mut Log) {
if !matches!(self.state.connection, Connection::Disconnected) {
return;
}
self.state.connection = Connection::Connecting;
log.say("Looking for an instrument…");
self.link.connect();
}
pub fn disconnect(&mut self, log: &mut Log) {
if !self.state.connected() {
return;
}
log.say("Releasing the instrument…");
self.pending.clear();
self.state.scan.clear();
self.link.disconnect();
}
#[cfg(not(target_arch = "wasm32"))]
pub fn release(&mut self) {
self.link.disconnect();
self.link.join(std::time::Duration::from_secs(2));
}
pub fn send(&mut self, cmd: DeviceCmd, log: &mut Log) {
if !self.state.connected() {
log.trouble("No instrument is attached.");
return;
}
self.pending.push_back(cmd);
}
pub fn read_class(&mut self, class: ObjectClass) {
self.state.scan.start(class);
}
pub fn resync(&mut self) {
for class in self.state.classes() {
self.read_class(class);
}
}
pub fn pump(&mut self) {
if !self.state.connected() || self.state.in_flight.is_some() {
return;
}
if let Some(cmd) = self.pending.pop_front() {
self.reading = None;
return self.dispatch(cmd);
}
let Some(class) = self.state.scan.take() else {
return;
};
self.reading = Some(class);
self.dispatch(DeviceCmd::ScanClass { class });
}
fn dispatch(&mut self, cmd: DeviceCmd) {
self.rescan = match &cmd {
DeviceCmd::Delete { class, at }
| DeviceCmd::Rename { class, at, .. }
| DeviceCmd::Put { class, at, .. } => user_bank(at.bank)
.map(|bank| (*class, bank))
.into_iter()
.collect(),
DeviceCmd::Move { class, from, to } | DeviceCmd::Duplicate { class, from, to } => {
[from, to]
.into_iter()
.filter_map(|at| Some((*class, user_bank(at.bank)?)))
.collect()
}
DeviceCmd::SendAll { class, items } => {
let mut banks: Vec<(ObjectClass, u32)> = items
.iter()
.filter_map(|item| Some((*class, user_bank(item.at.bank)?)))
.collect();
banks.sort_unstable_by_key(|(class, bank)| (class.to_raw(), *bank));
banks.dedup();
banks
}
_ => Vec::new(),
};
for (class, bank) in &self.rescan {
self.state.forget_bank(*class, *bank);
}
let loaded = |state: &DeviceState, class: &ObjectClass, at: &Location| {
state
.selected
.get(&class.to_raw())
.filter(|held| *held == at)
.map(|at| (*class, *at))
};
self.reselect = match &cmd {
DeviceCmd::Put { class, at, .. } | DeviceCmd::Rename { class, at, .. } => {
loaded(&self.state, class, at).into_iter().collect()
}
DeviceCmd::SendAll { class, items } => items
.iter()
.filter_map(|item| loaded(&self.state, class, &item.at))
.collect(),
_ => Vec::new(),
};
self.writing = match &cmd {
DeviceCmd::Put { class, .. } | DeviceCmd::SendAll { class, .. } => Some(*class),
_ => None,
};
if let DeviceCmd::Select { class, at } = &cmd {
self.state.selected.insert(class.to_raw(), *at);
}
self.state.in_flight = Some(cmd.words());
self.link.send(cmd);
}
#[cfg(test)]
pub fn pretend(&mut self, event: DeviceEvent) {
let _ = self.from_worker.send(event);
}
#[cfg(test)]
pub fn queued(&self) -> &VecDeque<DeviceCmd> {
&self.pending
}
#[cfg(test)]
pub fn pretend_attached(&mut self) {
self.pretend_attached_as("Nord Electro 5");
}
#[cfg(test)]
pub fn pretend_attached_as(&mut self, product: &str) {
self.state.connection = Connection::Connected(DeviceCard {
build: Some(7),
firmware: Some(204),
interface: Some(3),
kind: Some(1),
manufacturer: Some("Clavia DMI AB".into()),
max_transfer: Some(4096),
product: product.to_string(),
product_id: 0,
serial: None,
vendor_id: 0x0ffc,
});
}
#[cfg(test)]
pub fn pretend_scanned(&mut self, class: ObjectClass, bank: u32, names: &[&str]) {
use nord_usb::wire::ProgramInfo;
self.pretend_attached();
let slots = names
.iter()
.enumerate()
.map(|(slot, name)| {
(!name.is_empty()).then(|| ProgramInfo {
location: Location::from_user(bank, slot as u32 + 1),
body_len: 121,
format: "ne5p".into(),
version: 4,
crc32: None,
name: (*name).to_string(),
})
})
.collect();
self.state.banks.insert((class.to_raw(), bank), slots);
}
#[cfg(test)]
pub fn pretend_partitions(&mut self, table: &[(ObjectClass, &str, u32)]) {
self.state.partitions = table
.iter()
.map(|(class, name, bytes)| Partition {
class: *class,
name: (*name).to_string(),
native: false,
unit: Some(pretend_allocation_unit(*class, *bytes)),
})
.collect();
}
#[cfg(test)]
pub fn pretend_bodies(&mut self, class: ObjectClass, bank: u32, slots: &[Option<(&str, u32)>]) {
self.pretend_attached();
let slots = slots
.iter()
.enumerate()
.map(|(slot, held)| {
held.map(|(name, crc)| ProgramInfo {
location: Location::from_user(bank, slot as u32 + 1),
body_len: 121,
format: "ne5p".into(),
version: 4,
crc32: Some(crc),
name: name.to_string(),
})
})
.collect();
self.state.banks.insert((class.to_raw(), bank), slots);
}
#[cfg(test)]
pub fn pretend_geometry(&mut self, class: ObjectClass, banks: &[(&str, u32)]) {
let banks = banks
.iter()
.enumerate()
.map(|(index, (name, slots))| Bank {
index: index as u32,
name: (*name).to_string(),
slots: *slots,
})
.collect();
self.state.geometry.insert(class.to_raw(), banks);
}
#[cfg(test)]
pub fn pretend_focused(&mut self, class: ObjectClass, at: Location) {
self.state.focus.insert(class.to_raw(), Some(at));
}
pub fn relink(&self, workspace: &mut Workspace) {
let state = &self.state;
workspace.relink(|entity| link(state, entity));
}
fn forget(&mut self, workspace: &mut Workspace) {
self.state.forget_everything();
workspace.relink(|_| None);
workspace.forget_writes();
}
fn disagreements(&self, class: ObjectClass, bank: u32, workspace: &Workspace, log: &mut Log) {
for entity in workspace.listed() {
let Some(at) = entity
.link
.filter(|(held, at)| *held == class && user_bank(at.bank) == Some(bank))
.map(|(_, at)| at)
else {
continue;
};
let (Some(here), Some(there)) = (
entity.saved.crc32,
self.state
.slot(class, at)
.flatten()
.and_then(|info| info.crc32),
) else {
continue;
};
if here != there {
log.info(format!(
"{} reports crc32 {there:#010x}; “{}” is saved as {here:#010x}",
place(class, at),
entity.name
));
}
}
}
pub fn poll(
&mut self,
log: &mut Log,
workspace: &mut Workspace,
tabs: &mut Tabs,
queue: &mut Queue,
) {
let now = workspace.ctx().input(|input| input.time);
let mut heard = false;
while let Ok(event) = self.events.try_recv() {
heard = true;
match event {
DeviceEvent::Connected(card) => {
log.info(format!(
"connected: {} ({:04x}:{:04x})",
card.product, card.vendor_id, card.product_id
));
log.say(format!("{} is attached.", card.product));
self.state.connection = Connection::Connected(card);
self.forget(workspace);
self.pending.clear();
}
DeviceEvent::ConnectFailed(why) => {
log.error(why);
log.trouble("No instrument could be opened.");
self.state.connection = Connection::Disconnected;
}
DeviceEvent::Disconnected { lost } => {
match lost {
true => log.trouble("The instrument went away — reconnect when it's back."),
false => log.say("The instrument was released."),
}
self.state.connection = Connection::Disconnected;
self.state.in_flight = None;
self.forget(workspace);
self.pending.clear();
self.reading = None;
self.rescan.clear();
self.reselect.clear();
self.writing = None;
}
DeviceEvent::Started(what) => log.info(what),
DeviceEvent::Finished => {
if let Some(class) = self.reading.take() {
self.state.scan.finished(class);
self.state.scan.heard(class, now);
}
for (class, at) in std::mem::take(&mut self.reselect) {
self.pending.push_back(DeviceCmd::Select { class, at });
}
for (class, bank) in std::mem::take(&mut self.rescan) {
self.pending.push_back(DeviceCmd::ScanBank { class, bank });
}
self.writing = None;
self.state.in_flight = None;
}
DeviceEvent::ClassStatus {
class,
status,
banks,
} => {
self.state.inventory.retain(|held| held.class != class);
self.state.inventory.push(status);
self.state.scan.expect(class, banks);
}
DeviceEvent::Partitions(partitions) => {
self.state.partitions = partitions;
crate::queue::refit(workspace, &self.state, queue, log);
crate::queue::reattach(workspace, self, queue, log);
self.resync();
}
DeviceEvent::Geometry { class, banks } => {
self.state.geometry.insert(class.to_raw(), banks);
}
DeviceEvent::Focus { class, at } => {
self.state.focus.insert(class.to_raw(), at);
}
DeviceEvent::BankScanned { class, bank, slots } => {
self.state.banks.insert((class.to_raw(), bank), slots);
self.state.scan.bank(class, bank);
self.state.scan.heard(class, now);
self.disagreements(class, bank, workspace, log);
}
DeviceEvent::SlotInfo { class, at, info } => {
self.state.detail = Detail {
at: Some((class, at)),
info: Some(info),
deps: None,
};
}
DeviceEvent::Deps { class, at, deps } => {
if self.state.detail.at != Some((class, at)) {
self.state.detail = Detail {
at: Some((class, at)),
..Detail::default()
};
}
self.state.detail.deps = Some(deps);
}
DeviceEvent::Got {
name,
origin,
bytes,
why,
} => match why {
Purpose::View => {
let id = workspace.view(name, origin, bytes, log);
tabs.open(id);
}
Purpose::Copy => {
workspace.ingest(name, origin, bytes, log);
}
Purpose::Compare => {
if let Some((class, at)) = origin.slot() {
queue.arrived(class, at, &name, &bytes, workspace);
}
}
},
DeviceEvent::Vacant { class, at, why } => match why {
Purpose::Compare => queue.vacant(class, at),
Purpose::Copy | Purpose::View => {
log.error(format!("{} holds nothing to read", shown(at)));
log.trouble(format!("{} is empty.", place(class, at)));
}
},
DeviceEvent::Rescued { at, name, bytes } => {
log.error(format!(
"{} could not be restored; its bytes are in the local list as {name}",
shown(at)
));
log.trouble(format!(
"{} is empty — what was in it is on this computer as “{name}”.",
shown(at)
));
workspace.ingest(name, Origin::Rescued { at }, bytes, log);
}
DeviceEvent::Sent {
id,
class,
at,
bytes,
} => {
queue.forget(id);
workspace.landed(id, class, at, bytes);
}
DeviceEvent::Note(text) => log.info(text),
DeviceEvent::OpOk(text) => {
log.info(text);
if let Some(words) = &self.state.in_flight {
log.say(format!("{}.", words.done));
}
}
DeviceEvent::OpFailed(text) => {
if let Some(class) = self.writing {
queue.stumbled(class, &text);
}
log.error(text);
match &self.state.in_flight {
Some(words) => {
let failed = words.failed.clone();
log.trouble(format!("{failed}. The details are below."));
}
None => log.trouble("Something went wrong. The details are below."),
}
}
DeviceEvent::InstrumentChanged => {
log.warn("the instrument changed under us — every cached name is dropped");
log.say("Something changed on the instrument. Reading it again…");
self.resync();
}
}
}
if heard || self.linked != workspace.revision() {
self.linked = workspace.revision();
self.relink(workspace);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn named() -> impl Iterator<Item = ObjectClass> {
crate::browser::Kind::ALL
.into_iter()
.filter_map(|kind| kind.home())
}
fn table() -> Vec<Partition> {
vec![
Partition {
class: ObjectClass::Unknown(0),
name: "Piano (Native)".into(),
native: true,
unit: None,
},
Partition {
class: ObjectClass::Piano,
name: "Piano".into(),
native: false,
unit: Some(pretend_allocation_unit(ObjectClass::Piano, 261_632)),
},
Partition {
class: ObjectClass::Sample,
name: "Samp Lib".into(),
native: false,
unit: Some(pretend_allocation_unit(ObjectClass::Sample, 131_064)),
},
Partition {
class: ObjectClass::Unknown(9),
name: "Rhythms".into(),
native: false,
unit: None,
},
]
}
#[test]
fn the_instrument_says_which_classes_it_has() {
let ctx = egui::Context::default();
let mut device = Device::new(ctx.clone());
let mut workspace = Workspace::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
assert!(device.state.classes().is_empty(), "nothing read yet");
device.pretend(DeviceEvent::Partitions(table()));
device.poll(&mut log, &mut workspace, &mut tabs, &mut Queue::default());
assert_eq!(
device.state.classes(),
vec![
ObjectClass::Piano,
ObjectClass::Sample,
ObjectClass::Unknown(9)
]
);
assert_eq!(device.state.folder_name(ObjectClass::Piano), "Pianos");
assert_eq!(device.state.folder_name(ObjectClass::Unknown(9)), "Rhythms");
assert!(read_only(ObjectClass::Unknown(9)));
for class in device.state.classes() {
assert!(device.state.scan.progress(class).is_some(), "{class:?}");
}
assert!(device.state.scan.progress(ObjectClass::Program).is_none());
device.pretend(DeviceEvent::Disconnected { lost: false });
device.poll(&mut log, &mut workspace, &mut tabs, &mut Queue::default());
assert!(device.state.classes().is_empty());
}
#[test]
fn a_count_is_measured_in_the_unit_its_partition_reports() {
let ctx = egui::Context::default();
let mut device = Device::new(ctx.clone());
let mut workspace = Workspace::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let class = ObjectClass::Sample;
assert_eq!(
device.state.allocation_unit(class),
None,
"nothing read yet"
);
device.pretend(DeviceEvent::Partitions(table()));
device.poll(&mut log, &mut workspace, &mut tabs, &mut Queue::default());
assert_eq!(
device.state.allocation_unit(class).map(|unit| unit.get()),
Some(131_064)
);
assert_eq!(
device.state.allocation_unit(ObjectClass::Unknown(9)),
None,
"a partition that reported none"
);
device.pretend(DeviceEvent::Disconnected { lost: false });
device.poll(&mut log, &mut workspace, &mut tabs, &mut Queue::default());
assert_eq!(device.state.allocation_unit(class), None);
}
#[test]
fn a_library_reads_in_bytes_only_once_its_allocation_unit_has_arrived() {
let class = ObjectClass::Sample;
let inventory = [Status {
class,
count: 84,
free: 60,
used: 1472,
dirty: 0,
spare: 4,
}];
assert_eq!(
occupancy(class, &inventory, None).as_deref(),
Some("84 items"),
"the count alone until the unit lands"
);
assert_eq!(
occupancy(
class,
&inventory,
Some(pretend_allocation_unit(class, 131_064))
)
.as_deref(),
Some("184.0/192.0 MB")
);
let programs = [Status {
class: ObjectClass::Program,
count: 128,
free: 272 * 121,
used: 128 * 121,
dirty: 0,
spare: 0,
}];
assert_eq!(
occupancy(ObjectClass::Program, &programs, None).as_deref(),
Some("128/400")
);
}
#[test]
fn a_partition_reads_in_the_unit_its_total_deserves() {
let partition = |class, used: u32, free: u32| {
[Status {
class,
count: 0,
free,
used,
dirty: 0,
spare: 0,
}]
};
let byte = |class| Some(pretend_allocation_unit(class, 1));
let live = ObjectClass::Live;
assert_eq!(
occupancy(live, &partition(live, 121, 379), byte(live)).as_deref(),
Some("121/500 B")
);
let settings = ObjectClass::Settings;
assert_eq!(
occupancy(settings, &partition(settings, 500, 24_076), byte(settings)).as_deref(),
Some("0.5/24.0 kB")
);
let samples = ObjectClass::Sample;
assert_eq!(
occupancy(
samples,
&partition(samples, 128, 128),
Some(pretend_allocation_unit(samples, 1024 * 1024))
)
.as_deref(),
Some("128.0/256.0 MB")
);
}
#[test]
fn only_a_class_with_no_name_is_read_only() {
for class in named() {
assert!(!read_only(class), "{}", folder(class));
}
assert!(read_only(ObjectClass::Unknown(9)));
}
#[test]
fn a_bank_that_scans_as_empty_replaces_what_it_held() {
let ctx = egui::Context::default();
let mut device = Device::new(ctx.clone());
let mut workspace = Workspace::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let class = ObjectClass::Sample;
device.pretend_scanned(class, 1, &["Marimba"]);
assert_eq!(device.state.bank(class, 1).map(<[_]>::len), Some(1));
device.pretend(DeviceEvent::BankScanned {
class,
bank: 1,
slots: Vec::new(),
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut Queue::default());
let slots = device.state.bank(class, 1).expect("the bank was read");
assert!(slots.is_empty(), "read and empty, not never read");
}
#[test]
fn a_bank_index_with_no_panel_number_is_refused() {
let ctx = egui::Context::default();
let mut device = Device::new(ctx);
let class = ObjectClass::Program;
device.pretend_scanned(class, 1, &["Africa Split"]);
device.state.geometry.insert(
class.to_raw(),
vec![Bank {
index: u32::MAX,
name: "Nowhere".into(),
slots: 1,
}],
);
assert!(device
.state
.slot(class, Location { bank: 0, slot: 0 })
.is_some());
assert!(device
.state
.slot(
class,
Location {
bank: u32::MAX,
slot: 0
}
)
.is_none());
assert_eq!(device.state.bank_name(class, 0), None);
}
#[test]
fn a_settings_write_warns_that_the_panel_reloads() {
let why = write_warning(ObjectClass::Settings).expect("must warn");
assert!(why.contains("reloads the selected program"), "{why}");
for class in named().filter(|class| *class != ObjectClass::Settings) {
assert!(write_warning(class).is_none(), "{}", folder(class));
}
}
#[test]
fn a_sent_event_clears_the_object_it_names_and_no_other() {
use crate::workspace::{Fresh, Origin};
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let bytes = {
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let bytes = workspace.get(id).unwrap().bytes.clone();
workspace.remove(id, &mut log);
bytes
};
let at = |slot| Location { bank: 6, slot };
let landed = workspace.ingest(
"Africa-Split.ne5p".into(),
Origin::Device {
class: ObjectClass::Program,
at: at(3),
},
bytes.clone(),
&mut log,
);
let still_owed = workspace.ingest(
"Squabble-B.ne5p".into(),
Origin::Device {
class: ObjectClass::Program,
at: at(4),
},
bytes,
&mut log,
);
let mut queue = Queue::default();
for (id, slot) in [(landed, 3), (still_owed, 4)] {
crate::queue::enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
ObjectClass::Program,
at(slot),
);
}
device.pretend(DeviceEvent::Sent {
id: landed,
class: ObjectClass::Program,
at: at(3),
bytes: workspace.get(landed).unwrap().bytes.clone(),
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
assert!(!queue.holds(landed), "it was written");
assert!(queue.holds(still_owed), "still waiting");
assert_eq!(queue.ids(), vec![still_owed]);
}
fn program(workspace: &mut Workspace, log: &mut Log, origin: Origin) -> (u64, u32) {
let made = workspace
.create(crate::workspace::Fresh::Program, log)
.unwrap();
let bytes = workspace.get(made).unwrap().bytes.clone();
workspace.remove(made, log);
let id = workspace.ingest("Africa-Split.ne5p".into(), origin, bytes, log);
let crc = workspace
.get(id)
.and_then(|entity| entity.saved.crc32)
.expect("every CBIN container has one");
(id, crc)
}
fn stage(workspace: &mut Workspace, log: &mut Log, origin: Origin) -> (u64, u32) {
let made = workspace
.create(crate::workspace::Fresh::Stage4Program, log)
.unwrap();
let bytes = workspace.get(made).unwrap().bytes.clone();
workspace.remove(made, log);
let id = workspace.ingest("Africa-Split.ns4p".into(), origin, bytes, log);
let crc = workspace
.get(id)
.and_then(|entity| entity.saved.crc32)
.expect("every CBIN container has one");
(id, crc)
}
#[test]
fn what_the_instrument_takes_is_decided_by_the_table_then_by_the_folder() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let (own, _) = program(&mut workspace, &mut log, Origin::Fresh);
let (other, _) = stage(&mut workspace, &mut log, Origin::Fresh);
fn held(device: &Device, workspace: &Workspace, id: u64) -> Fit {
fit(&device.state, workspace.get(id).expect("it is on the list"))
}
let held = |device: &Device, id| held(device, &workspace, id);
assert_eq!(held(&device, own), Fit::Unattached);
device.pretend_attached();
assert_eq!(held(&device, own), Fit::Takes, "confirmed on hardware");
match held(&device, other) {
Fit::Refuses(why) => {
assert!(why.contains("Stage 4"), "{why}");
assert!(why.contains("Nord Electro 5"), "{why}");
}
answer => panic!("a Stage 4 program on an Electro 5: {answer:?}"),
}
device.pretend_attached_as("Nord Stage 4 88");
match held(&device, other) {
Fit::Warn(why) => assert!(why.contains("untried"), "{why}"),
answer => panic!("a Stage 4 program on a Stage 4: {answer:?}"),
}
device.pretend_attached_as("unnamed device");
assert_eq!(held(&device, own), Fit::Takes);
device.pretend_scanned(ObjectClass::Program, 7, &["Africa Split"]);
device.pretend_attached_as("unnamed device");
match held(&device, other) {
Fit::Warn(why) => assert!(why.contains("ns4p"), "{why}"),
answer => panic!("an unnameable instrument holding ne5p: {answer:?}"),
}
}
#[test]
fn an_asset_the_instrument_refuses_is_linked_to_nothing() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let (own, crc) = program(&mut workspace, &mut log, Origin::Fresh);
let (other, _) = stage(&mut workspace, &mut log, Origin::Fresh);
device.pretend_bodies(
ObjectClass::Program,
7,
&[Some(("Africa Split", crc)), Some(("Squabble B", crc))],
);
device.relink(&mut workspace);
assert!(workspace.get(own).unwrap().link.is_some());
assert_eq!(workspace.get(other).unwrap().link, None);
}
#[test]
fn a_link_is_a_slot_of_its_own_folder_reporting_its_own_body() {
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, crc) = program(&mut workspace, &mut log, Origin::Fresh);
device.pretend_bodies(ObjectClass::SetList, 1, &[Some(("Sunday", crc))]);
device.pretend_bodies(
ObjectClass::Program,
7,
&[None, Some(("Africa Split", crc))],
);
device.relink(&mut workspace);
assert_eq!(
workspace.get(id).unwrap().link,
Some((ObjectClass::Program, Location { bank: 6, slot: 1 }))
);
device.pretend_scanned(ObjectClass::Program, 7, &["", "Africa Split"]);
let (fresh, _) = program(&mut workspace, &mut log, Origin::Fresh);
device.relink(&mut workspace);
assert_eq!(
workspace.get(fresh).unwrap().link,
None,
"a folder reporting no checksum links nothing"
);
}
#[test]
fn a_link_keeps_the_slot_it_has_when_several_hold_the_bytes() {
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, crc) = program(&mut workspace, &mut log, Origin::File("Bells.ne5p".into()));
let class = ObjectClass::Program;
let (low, high) = (Location { bank: 6, slot: 0 }, Location { bank: 6, slot: 2 });
device.pretend_bodies(
class,
7,
&[
Some(("Circling Bells", crc)),
None,
Some(("Circling Bells", crc)),
],
);
device.relink(&mut workspace);
assert_eq!(
workspace.get(id).unwrap().link,
Some((class, low)),
"standing nowhere, it takes the lowest address holding it"
);
let sent = workspace.get(id).unwrap().bytes.clone();
workspace.landed(id, class, high, sent);
device.relink(&mut workspace);
assert_eq!(
workspace.get(id).unwrap().link,
Some((class, high)),
"it stands where it was written, not where else the bytes are"
);
}
#[test]
fn an_asset_links_as_soon_as_it_arrives() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let mut queue = Queue::default();
let class = ObjectClass::Program;
let (first, crc) = program(&mut workspace, &mut log, Origin::Fresh);
device.pretend_bodies(class, 7, &[None, Some(("Circling Bells", crc))]);
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let at = workspace.get(first).unwrap().link;
assert_eq!(at, Some((class, Location { bank: 6, slot: 1 })));
let (second, _) = program(&mut workspace, &mut log, Origin::Fresh);
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
assert_eq!(
workspace.get(second).unwrap().link,
at,
"it links off the cache rather than off the next walk"
);
}
#[test]
fn an_asset_with_no_checksum_of_its_own_links_to_nothing() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let (_, crc) = program(&mut workspace, &mut log, Origin::Fresh);
let loose = workspace.ingest(
"notes.txt".into(),
Origin::Device {
class: ObjectClass::Program,
at: Location { bank: 6, slot: 0 },
},
b"not a Nord file at all".to_vec(),
&mut log,
);
assert!(workspace.get(loose).unwrap().container.is_none());
device.pretend_bodies(ObjectClass::Program, 7, &[Some(("Africa Split", crc))]);
device.relink(&mut workspace);
assert_eq!(workspace.get(loose).unwrap().link, None);
}
#[test]
fn a_link_prefers_the_slot_it_came_off_and_otherwise_the_lowest_address() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let at = |slot| Location { bank: 6, slot };
let (fresh, crc) = program(&mut workspace, &mut log, Origin::Fresh);
let (copied, _) = program(
&mut workspace,
&mut log,
Origin::Device {
class: ObjectClass::Program,
at: at(2),
},
);
device.pretend_bodies(
ObjectClass::Program,
7,
&[
Some(("Africa Split", crc)),
Some(("Africa 2", crc)),
Some(("Africa 3", crc)),
],
);
device.relink(&mut workspace);
assert_eq!(
workspace.get(fresh).unwrap().link,
Some((ObjectClass::Program, at(0)))
);
assert_eq!(
workspace.get(copied).unwrap().link,
Some((ObjectClass::Program, at(2)))
);
assert_eq!(
also_holding(&device.state, workspace.get(fresh).unwrap()),
2,
"the hover has the rest to count"
);
}
#[test]
fn the_slot_an_asset_came_off_is_its_link_while_the_instrument_holds_it() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let at = Location { bank: 0, slot: 0 };
let class = ObjectClass::Program;
let (id, crc) = program(&mut workspace, &mut log, Origin::Device { class, at });
let bytes = workspace.get(id).unwrap().bytes.clone();
let (_, edited) =
crate::fields::apply(&bytes, &[("center_panel.gain".into(), "96".into())]).unwrap();
workspace.replace_bytes(id, edited, &mut log);
workspace.mark_saved(id);
let now = workspace.get(id).unwrap().saved.crc32.unwrap();
assert_ne!(now, crc, "the edit moved the body");
device.relink(&mut workspace);
assert_eq!(workspace.get(id).unwrap().link, None, "nothing is read yet");
device.pretend_bodies(
class,
1,
&[Some(("Africa Split", crc)), Some(("Squabble B", now))],
);
device.relink(&mut workspace);
assert_eq!(workspace.get(id).unwrap().link, Some((class, at)));
device.pretend_bodies(class, 1, &[None, Some(("Squabble B", now))]);
device.relink(&mut workspace);
assert_eq!(
workspace.get(id).unwrap().link,
Some((class, Location { bank: 0, slot: 1 })),
);
}
#[test]
fn a_class_reporting_no_checksum_links_by_its_singleton_or_by_name() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let settings = workspace
.create(crate::workspace::Fresh::Settings, &mut log)
.unwrap();
device.pretend_scanned(ObjectClass::Settings, 1, &["Settings"]);
device.pretend_scanned(ObjectClass::Sample, 1, &["Bass Clarinet", "Rhodes"]);
device.relink(&mut workspace);
assert_eq!(
workspace.get(settings).unwrap().link,
Some((ObjectClass::Settings, Location { bank: 0, slot: 0 })),
);
let bytes = workspace.get(settings).unwrap().bytes.clone();
let by_name = |workspace: &mut Workspace, name: &str, log: &mut Log| {
let id = workspace.ingest(name.into(), Origin::Fresh, bytes.clone(), log);
let at = super::named(
&device.state,
ObjectClass::Sample,
workspace.get(id).unwrap(),
);
workspace.remove(id, log);
at
};
let slot = |slot| Some(Location { bank: 0, slot });
assert_eq!(by_name(&mut workspace, "Rhodes", &mut log), slot(1));
assert_eq!(
by_name(&mut workspace, " Bass Clarinet ", &mut log),
slot(0)
);
assert_eq!(
by_name(&mut workspace, "rhodes", &mut log),
None,
"case is part of a name"
);
assert_eq!(by_name(&mut workspace, "Wurlitzer", &mut log), None);
device.pretend_scanned(ObjectClass::Settings, 1, &["Settings", "Settings 2"]);
let second = workspace.ingest("Settings".into(), Origin::Fresh, bytes, &mut log);
device.relink(&mut workspace);
assert_eq!(workspace.get(second).unwrap().link, None);
}
#[test]
fn a_link_arrives_with_a_walk_and_goes_when_the_instrument_does() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let mut queue = Queue::default();
let (id, crc) = program(&mut workspace, &mut log, Origin::Fresh);
let class = ObjectClass::Program;
device.pretend(DeviceEvent::BankScanned {
class,
bank: 7,
slots: vec![Some(ProgramInfo {
location: Location { bank: 6, slot: 0 },
body_len: 121,
format: "ne5p".into(),
version: 4,
crc32: Some(crc),
name: "Africa Split".into(),
})],
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
assert_eq!(
workspace.get(id).unwrap().link,
Some((class, Location { bank: 6, slot: 0 }))
);
device.pretend(DeviceEvent::Disconnected { lost: true });
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
assert_eq!(workspace.get(id).unwrap().link, None);
assert!(workspace.get(id).is_some(), "the asset itself stays");
}
#[test]
fn a_send_lands_its_asset_on_the_slot_it_wrote() {
enum Rescan {
Skipped,
Same,
Other,
Silent,
}
let class = ObjectClass::Program;
let at = Location { bank: 4, slot: 2 };
let sent = |rescan: Rescan| {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let mut queue = Queue::default();
let origin = Origin::File("Africa-Split.ne5p".into());
let (id, crc) = program(&mut workspace, &mut log, origin);
device.pretend_attached();
let sent = workspace.get(id).expect("it is on the list").bytes.clone();
device.pretend(DeviceEvent::Sent {
id,
class,
at,
bytes: sent,
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let reported = match rescan {
Rescan::Skipped => None,
Rescan::Same => Some(Some(crc)),
Rescan::Other => Some(Some(crc ^ 1)),
Rescan::Silent => Some(None),
};
if let Some(crc32) = reported {
device.pretend(DeviceEvent::BankScanned {
class,
bank: at.bank + 1,
slots: vec![
None,
None,
Some(ProgramInfo {
location: at,
body_len: 121,
format: "ne5p".into(),
version: 4,
crc32,
name: "Africa Split".into(),
}),
],
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
}
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let entity = workspace.get(id).expect("it is on the list");
(
entity.link,
crate::library::keyboard_mark(entity, &device.state, &queue),
log.transcript(),
crc,
)
};
let (good, warn) = (crate::library::Mark::Agrees, crate::library::Mark::Differs);
let there = Some((class, at));
let (link, mark, said, _) = sent(Rescan::Same);
assert_eq!(link, there);
assert_eq!(mark, Some(good));
assert!(!said.contains("crc32"), "the two agree: {said}");
let (link, mark, said, crc) = sent(Rescan::Other);
assert_eq!(
link, there,
"it is where it was written, holding what it holds"
);
assert_eq!(mark, Some(warn));
assert!(
said.contains(&format!(
"Programs 5:3 reports crc32 {:#010x}; “Africa-Split.ne5p” is saved as \
{crc:#010x}",
crc ^ 1
)),
"{said}"
);
assert_eq!(
said.matches("reports crc32").count(),
1,
"once, on the read"
);
let (link, mark, _, _) = sent(Rescan::Silent);
assert_eq!(link, there);
assert_eq!(
mark,
Some(good),
"this app wrote those bytes and the slot reports as many"
);
let (link, _, _, _) = sent(Rescan::Skipped);
assert_eq!(link, there, "a bank nothing has read says neither way");
}
#[test]
fn a_send_settles_the_baseline_on_the_bytes_it_carried_and_not_on_a_later_edit() {
let class = ObjectClass::Program;
let at = Location { bank: 4, slot: 2 };
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let mut tabs = Tabs::default();
let mut queue = Queue::default();
let (id, _) = program(&mut workspace, &mut log, Origin::Fresh);
device.pretend_attached();
crate::queue::enqueue(&workspace, &mut device, &mut queue, &mut log, id, class, at);
let sent = workspace.get(id).expect("it is on the list").bytes.clone();
let (_, edited) = crate::fields::apply(&sent, &[("center_panel.gain".into(), "96".into())])
.expect("the registry takes the set");
workspace.replace_bytes(id, edited.clone(), &mut log);
device.pretend(DeviceEvent::Sent {
id,
class,
at,
bytes: sent.clone(),
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let entity = workspace.get(id).expect("it is on the list");
assert_eq!(entity.saved.bytes, sent, "the instrument holds these");
assert_eq!(entity.bytes, edited);
assert!(entity.is_unsaved(), "the edit never went anywhere");
assert_eq!(entity.link, Some((class, at)));
assert!(!queue.holds(id), "it landed");
}
#[test]
fn a_dependency_name_answers_only_for_what_was_asked() {
let at = Location { bank: 6, slot: 3 };
let elsewhere = Location { bank: 0, slot: 0 };
let detail = Detail {
at: Some((ObjectClass::Program, at)),
info: Some(None),
deps: Some(vec![Dependency {
flag: 0,
class: ObjectClass::Piano,
id: 0x0102_0304,
name: "Royal Grand 3D ".into(),
location: None,
}]),
};
let state = DeviceState {
detail,
..DeviceState::default()
};
let piano = |slot, id| {
state
.dependency_name(Some((ObjectClass::Program, slot)), ObjectClass::Piano, id)
.map(str::to_string)
};
assert_eq!(piano(at, 0x0102_0304).as_deref(), Some("Royal Grand 3D"));
assert_eq!(piano(elsewhere, 0x0102_0304), None, "another slot's list");
assert_eq!(piano(at, 0x0999_0999), None, "an id it did not report");
assert_eq!(
state.dependency_name(
Some((ObjectClass::Sample, at)),
ObjectClass::Piano,
0x0102_0304
),
None,
"another class at the same address",
);
assert_eq!(
state.dependency_name(
Some((ObjectClass::Program, at)),
ObjectClass::Sample,
0x0102_0304
),
None,
"a piano is not a sample"
);
assert_eq!(state.dependency_name(None, ObjectClass::Piano, 1), None);
}
#[test]
fn the_status_sentences_never_quote_the_protocol() {
let cmd = DeviceCmd::Put {
id: 1,
class: ObjectClass::Program,
at: Location { bank: 6, slot: 3 },
name: "Africa Split".into(),
bytes: Vec::new(),
};
let words = cmd.words();
assert_eq!(words.doing, "Sending “Africa Split” to Programs 7:4…");
assert_eq!(words.done, "Sent “Africa Split” to Programs 7:4");
assert_eq!(
words.failed,
"Could not send “Africa Split” to Programs 7:4"
);
}
#[test]
fn a_place_reads_as_a_folder_and_a_slot() {
let at = Location { bank: 0, slot: 0 };
assert_eq!(place(ObjectClass::SetList, at), "Set lists 1:1");
assert_eq!(folder(ObjectClass::Unknown(9)), "Other");
}
}