use eframe::egui;
use nord_format::formats::nsmp::codec::SOURCE_RATE;
use nord_format::formats::nsmp::{encode, MAX_NAME_LEN};
use nord_format::wav::Pcm16;
use super::sample::note_picker;
use crate::note;
pub fn is_wav(bytes: &[u8]) -> bool {
bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WAVE"
}
pub struct Draft {
pub name: String,
pub root_key: u8,
pub top_note: u8,
pub predict: bool,
}
impl Draft {
pub fn new(label: &str) -> Draft {
let stem = label.rsplit_once('.').map_or(label, |(stem, _)| stem);
Draft {
name: fits(stem),
root_key: 60,
top_note: 84,
predict: false,
}
}
}
fn fits(label: &str) -> String {
let mut out = String::new();
for c in label.chars() {
if out.len() + c.len_utf8() > MAX_NAME_LEN {
break;
}
out.push(c);
}
out
}
pub enum Source {
Read(Pcm16),
Unreadable(String),
}
impl Source {
pub fn read(bytes: &[u8]) -> Source {
match nord_format::wav::read_pcm16(bytes) {
Ok(pcm) => Source::Read(pcm),
Err(e) => Source::Unreadable(e.to_string()),
}
}
}
pub fn refusal(source: &Source) -> Option<String> {
let pcm = match source {
Source::Unreadable(why) => return Some(why.clone()),
Source::Read(pcm) => pcm,
};
if pcm.rate != SOURCE_RATE {
return Some(format!(
"{} Hz — the field lattice is defined against {SOURCE_RATE} Hz and the \
instrument's own resampler is not decoded, so resample the file first",
pcm.rate
));
}
if pcm.channels != 1 {
return Some(format!(
"{} channels — only mono is encoded; the format carries stereo as a stroke \
pair under one header, and that layout is not written yet",
pcm.channels
));
}
if pcm.frames() < encode::MIN_FRAMES {
return Some(format!(
"{} frames — shorter than {} means an unresolved opening the encoder does \
not model",
pcm.frames(),
encode::MIN_FRAMES
));
}
None
}
pub fn instrument(draft: &Draft, source: &Source) -> Result<Vec<u8>, String> {
if let Some(why) = refusal(source) {
return Err(why);
}
let Source::Read(pcm) = source else {
return Err("this file did not read as a WAV".into());
};
let options = encode::Options::new(&draft.name)
.root_key(draft.root_key)
.top_note(draft.top_note)
.predictor(match draft.predict {
true => encode::Predictor::Minimising,
false => encode::Predictor::Plain,
});
let instrument = encode::instrument(&pcm.samples, &options).map_err(|e| e.to_string())?;
instrument.to_bytes().map_err(|e| e.to_string())
}
pub fn ui(ui: &mut egui::Ui, draft: &mut Draft, source: &Source) -> bool {
ui.label(egui::RichText::new("This is a WAV, not a Nord file.").strong());
ui.label(
egui::RichText::new(
"It can be encoded into a one-zone sample instrument. Encoding is \
experimental: the file it writes is structurally sound and decodes back \
exactly, but it is not byte-identical to the editor's own output. \
Instruments encoded this way have been played on hardware.",
)
.small()
.weak(),
);
ui.add_space(6.0);
if let Source::Read(pcm) = source {
ui.label(
egui::RichText::new(format!(
"{} Hz, {}, {} frames ({:.3} s)",
pcm.rate,
match pcm.channels {
1 => "mono".to_string(),
n => format!("{n} channels"),
},
pcm.frames(),
pcm.frames() as f64 / f64::from(pcm.rate).max(1.0),
))
.small()
.weak(),
);
}
let refusal = refusal(source);
ui.add_enabled_ui(refusal.is_none(), |ui| {
ui.horizontal(|ui| {
ui.add_sized(
[120.0, ui.spacing().interact_size.y],
egui::Label::new("Name").halign(egui::Align::LEFT),
);
ui.add(
egui::TextEdit::singleline(&mut draft.name)
.desired_width(200.0)
.char_limit(MAX_NAME_LEN),
);
});
ui.horizontal(|ui| {
ui.add_sized(
[120.0, ui.spacing().interact_size.y],
egui::Label::new("Zone").halign(egui::Align::LEFT),
);
ui.label("root key");
if let Some(note) = note_picker(ui, ("encode_root", 0), draft.root_key) {
draft.root_key = note;
}
ui.label("top note");
if let Some(note) = note_picker(ui, ("encode_top", 0), draft.top_note) {
draft.top_note = note;
}
});
ui.horizontal(|ui| {
ui.add_space(120.0);
ui.checkbox(&mut draft.predict, "Predict").on_hover_text(
"the narrowest predictor order per cell: a smaller file, decoded \
back exactly either way",
);
});
});
if let Some(why) = &refusal {
ui.add_space(4.0);
ui.label(egui::RichText::new(why).color(crate::app::bad(ui.visuals())));
return false;
}
ui.add_space(8.0);
let mut asked = false;
ui.horizontal(|ui| {
asked = ui
.button("Encode")
.on_hover_text("adds a new sample instrument to this computer; the WAV stays as it is")
.clicked();
ui.label(
egui::RichText::new(format!(
"one zone, root {}, up to {}",
note::name(draft.root_key),
note::name(draft.top_note)
))
.small()
.weak(),
);
});
asked
}
#[cfg(test)]
mod tests {
use super::*;
fn wav(rate: u32, channels: u16, frames: usize) -> Vec<u8> {
let samples = vec![0i16; frames * usize::from(channels)];
nord_format::wav::pcm16(&samples, rate, channels).unwrap()
}
#[test]
fn a_riff_wave_container_is_what_offers_the_panel() {
assert!(is_wav(&wav(44_100, 1, 8)));
assert!(!is_wav(b"RIFF"));
assert!(!is_wav(b"not a wav at all"));
assert!(!is_wav(&[]));
}
#[test]
fn every_refusal_says_which_limit_it_hit() {
let refused = |bytes: Vec<u8>| refusal(&Source::read(&bytes));
let ok = wav(SOURCE_RATE, 1, encode::MIN_FRAMES);
assert_eq!(refused(ok), None);
let slow = refused(wav(22_050, 1, encode::MIN_FRAMES)).expect("the wrong rate");
assert!(slow.contains("22050 Hz"), "{slow}");
assert!(slow.contains(&SOURCE_RATE.to_string()), "{slow}");
let stereo = refused(wav(SOURCE_RATE, 2, encode::MIN_FRAMES)).expect("two channels");
assert!(stereo.contains("2 channels"), "{stereo}");
let short = refused(wav(SOURCE_RATE, 1, encode::MIN_FRAMES - 1)).expect("too short");
assert!(short.contains(&encode::MIN_FRAMES.to_string()), "{short}");
let unreadable = refused(b"RIFF\0\0\0\0WAVE".to_vec()).expect("not readable");
assert!(!unreadable.is_empty());
}
#[test]
fn an_encode_makes_the_instrument_the_panel_describes() {
let source = Source::read(&wav(SOURCE_RATE, 1, encode::MIN_FRAMES));
let mut draft = Draft::new("Marimba hit.wav");
assert_eq!(draft.name, "Marimba hit");
draft.root_key = 48;
draft.top_note = 60;
let bytes = instrument(&draft, &source).expect("it encodes");
let entity = nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).unwrap();
let snapshot = super::super::sample::snapshot(&entity)
.expect("a sample instrument")
.expect("it reads");
assert_eq!(snapshot.name, "Marimba hit");
assert_eq!(snapshot.generation, "v2");
assert_eq!(snapshot.zones.len(), 1);
assert_eq!(snapshot.zones[0].root_key, 48);
assert_eq!(snapshot.zones[0].top_note, 60);
assert_eq!(nord_format::to_bytes(&entity).unwrap(), bytes);
}
#[test]
fn a_refused_wav_is_not_encoded_anyway() {
let source = Source::read(&wav(48_000, 1, encode::MIN_FRAMES));
assert!(instrument(&Draft::new("x.wav"), &source).is_err());
}
#[test]
fn a_long_filename_opens_the_panel_on_a_name_that_fits() {
let draft = Draft::new("an extremely long marimba name.wav");
assert_eq!(draft.name.len(), MAX_NAME_LEN);
let source = Source::read(&wav(SOURCE_RATE, 1, encode::MIN_FRAMES));
assert!(instrument(&draft, &source).is_ok());
}
}