use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use clap::Args;
use nord_format::formats::nsmp::{self, codec, encode};
use nord_format::formats::nsmpproj::{
self, NewZone, Project, Stroke, Zone, LOWEST_NOTE, PROJECT_RATE,
};
use nord_format::note;
use nord_format::Entity;
use nord_usb::ObjectClass;
use crate::edit::{print_byte_diff, write_edit, write_file};
use crate::editors;
use crate::slot::Target;
use crate::ui::Ui;
#[derive(Args)]
pub struct EditArgs {
#[arg(value_name = "FILE|BANK:SLOT")]
pub target: String,
#[command(flatten)]
pub common: crate::edit::SetArgs,
}
pub fn run(ui: &Ui, args: EditArgs) -> Result<(), String> {
let target = crate::slot::target(&args.target)?;
let original = match &target {
Target::File(path) => {
std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?
}
Target::Slot(at) => crate::device::fetch(*at, ObjectClass::Sample)?,
};
let mut entity = nord_format::from_stream(&mut std::io::Cursor::new(&original))
.map_err(|e| e.to_string())?;
if !matches!(entity, Entity::Sample(_)) {
return Err(crate::edit::mismatch(&mut entity, ObjectClass::Sample));
}
let staged = editors::stage(
ui,
args.common.fields,
&args.common.set,
crate::edit::editor_for(&mut entity)?.as_mut(),
)?;
let Some(changed) = staged else {
return Ok(());
};
if changed == 0 {
ui.note("no field changed; writing nothing");
return Ok(());
}
let edited = nord_format::to_bytes(&entity).map_err(|e| e.to_string())?;
print_byte_diff(ui, &original, &edited);
if args.common.dry_run {
ui.note("--dry-run: nothing written");
return Ok(());
}
match (target, args.common.out) {
(Target::File(path), out) => write_edit(ui, &path, out, args.common.yes, &edited),
(_, Some(out)) => write_file(ui, &out, &edited),
(Target::Slot(at), None) => crate::device::send(
ui,
&edited,
at,
ObjectClass::Sample,
args.common.yes,
"the edited sample",
None,
None,
),
}
}
#[derive(Args)]
pub struct DecodeArgs {
#[arg(required = true, value_name = "FILE|BANK:SLOT")]
pub targets: Vec<String>,
#[arg(short, long, value_name = "DIR")]
pub out: Option<PathBuf>,
}
#[derive(Args)]
pub struct EncodeArgs {
#[arg(value_name = "WAV")]
pub wav: PathBuf,
#[arg(short, long, value_name = "FILE")]
pub out: Option<PathBuf>,
#[arg(long)]
pub name: Option<String>,
#[arg(long, value_name = "NOTE", default_value = "C4")]
pub root_key: String,
#[arg(long, value_name = "NOTE")]
pub top_note: Option<String>,
#[arg(long = "loop", value_name = "START:END")]
pub loop_points: Option<String>,
#[arg(
long,
value_name = "FRAMES",
default_value_t = 0,
requires = "loop_points"
)]
pub loop_crossfade: usize,
#[arg(long)]
pub plain: bool,
#[arg(long, value_name = "N", default_value_t = 2, value_parser = clap::value_parser!(u8).range(2..=4))]
pub generation: u8,
#[arg(long, hide = true, value_name = "BITS", value_parser = clap::value_parser!(u8).range(0..=15))]
pub shift: Option<u8>,
#[arg(long)]
pub unverified: bool,
}
#[derive(Args)]
pub struct BuildArgs {
#[arg(value_name = "PROJECT")]
pub project: PathBuf,
#[arg(short, long, value_name = "FILE")]
pub out: Option<PathBuf>,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub plain: bool,
#[arg(long, value_name = "N", default_value_t = 2, value_parser = clap::value_parser!(u8).range(2..=4))]
pub generation: u8,
#[arg(long, hide = true, value_name = "BITS", value_parser = clap::value_parser!(u8).range(0..=15))]
pub shift: Option<u8>,
#[arg(long)]
pub unverified: bool,
}
#[derive(Args)]
pub struct VerifyArgs {
#[arg(required = true, value_name = "FILE|BANK:SLOT")]
pub targets: Vec<String>,
#[arg(long)]
pub deep: bool,
}
#[derive(Args)]
pub struct ProjectNewArgs {
#[arg(long = "zone", required = true, value_name = "WAV=NOTE")]
pub zones: Vec<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(short, long, value_name = "FILE")]
pub out: Option<PathBuf>,
}
#[derive(Default)]
struct Coverage {
files: usize,
zones: usize,
decoded: usize,
fields: usize,
differenced: usize,
reasons: BTreeMap<&'static str, usize>,
}
impl Coverage {
fn refuse(&mut self, reason: &'static str) {
*self.reasons.entry(reason).or_default() += 1;
}
fn line(&self) -> String {
let unsupported: usize = self.reasons.values().sum();
let mut line = format!(
"{} file(s), {} zone(s): {} decoded, {unsupported} unsupported",
self.files, self.zones, self.decoded
);
if self.fields > 0 {
line.push_str(&format!(
"; {:.1}% of decoded fields came through the predictor",
100.0 * self.differenced as f64 / self.fields as f64,
));
}
if !self.reasons.is_empty() {
let detail: Vec<String> = self
.reasons
.iter()
.map(|(reason, n)| format!("{reason} {n}"))
.collect();
line.push_str(&format!(" ({})", detail.join(", ")));
}
line
}
}
fn body(bytes: &[u8]) -> Result<nord_format::Sample, String> {
let entity =
nord_format::from_stream(&mut std::io::Cursor::new(bytes)).map_err(|e| e.to_string())?;
match entity {
Entity::Sample(sample) => Ok(sample),
other => Err(format!(
"a {} file, not a sample instrument",
crate::file::entity_tag(&other)
)),
}
}
fn read(origin: &Target) -> Result<Vec<u8>, String> {
match origin {
Target::File(path) => std::fs::read(path).map_err(|e| e.to_string()),
Target::Slot(at) => crate::device::fetch(*at, ObjectClass::Sample),
}
}
fn sanitized(name: &str) -> String {
let mut out = String::new();
for c in name.chars() {
if c.is_ascii_alphanumeric() || c == '_' {
out.push(c);
} else if !out.ends_with('-') {
out.push('-');
}
}
out.trim_matches('-').to_string()
}
fn stem(origin: &Target, body: &nord_format::Sample) -> String {
match origin {
Target::File(path) => path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "sample".into()),
Target::Slot(at) => match body {
nord_format::Sample::V2(s) => s.name().ok(),
nord_format::Sample::V3(s) => s.name().ok(),
}
.map(|name| sanitized(&name))
.filter(|name| !name.is_empty())
.unwrap_or_else(|| format!("{}-{}", at.user_bank(), at.user_slot())),
}
}
struct Wavs<'a> {
dir: &'a Path,
written: BTreeSet<PathBuf>,
}
impl Wavs<'_> {
fn claim(&mut self, name: &str) -> Result<PathBuf, String> {
let path = self.dir.join(name);
if !self.written.insert(path.clone()) {
return Err(format!(
"{}: an earlier target already wrote this file; decode targets that share \
a name into separate directories",
path.display()
));
}
Ok(path)
}
}
pub fn decode(ui: &Ui, args: DecodeArgs) -> Result<(), String> {
if let Some(dir) = &args.out {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
let mut wavs = args.out.as_deref().map(|dir| Wavs {
dir,
written: BTreeSet::new(),
});
let mut coverage = Coverage::default();
let mut failed = 0usize;
for spec in &args.targets {
ui.out(ui.bold(spec));
match decode_target(ui, spec, wavs.as_mut(), &mut coverage) {
Ok(()) => coverage.files += 1,
Err(e) => {
failed += 1;
ui.note(format!(" {} {e}", ui.danger("error")));
}
}
}
ui.note(coverage.line());
match failed {
0 => Ok(()),
n => Err(format!(
"{n} of {} target(s) did not decode",
args.targets.len()
)),
}
}
fn decode_target(
ui: &Ui,
spec: &str,
mut out: Option<&mut Wavs<'_>>,
coverage: &mut Coverage,
) -> Result<(), String> {
let origin = crate::slot::target(spec)?;
let bytes = read(&origin).map_err(|e| format!("{spec}: {e}"))?;
let body = body(&bytes).map_err(|e| format!("{spec}: {e}"))?;
let stem = stem(&origin, &body);
let layout = body.layout().map_err(|e| e.to_string())?;
for (index, zone) in body.zones().map_err(|e| e.to_string())?.iter().enumerate() {
coverage.zones += 1;
let n = index + 1;
let head = format!(
" zone{n:<2} root {:<4} top {:<4}",
note::name(zone.root_key),
note::name(zone.top_note),
);
match codec::decode(zone.stream, zone.at, layout) {
Ok(audio) => {
coverage.decoded += 1;
coverage.fields += audio.samples.len();
coverage.differenced += audio.differenced;
let mut notes = Vec::new();
if audio.differenced > 0 {
notes.push(format!(
"{}% predicted",
100 * audio.differenced / audio.samples.len().max(1)
));
}
if audio.clipped > 0 {
notes.push(format!("{} clipped", audio.clipped));
}
let mut row = format!(
"{head} {:>9} fields {:>7.3} s {}",
audio.samples.len(),
audio.seconds(),
ui.dim(notes.join(", ")),
);
if let Some(wavs) = out.as_mut() {
let file = wavs.claim(&format!("{stem}-zone{n}.wav"))?;
let wav =
nord_format::wav::pcm16(&audio.samples, codec::FIELD_RATE, audio.channels)
.map_err(|e| format!("{}: {e}", file.display()))?;
crate::edit::replace_file(&file, &wav)?;
row.push_str(&format!(" -> {}", file.display()));
}
ui.out(row);
}
Err(why) => {
coverage.refuse(why.reason());
ui.out(format!(
"{head} {} {}",
ui.danger("unsupported"),
ui.dim(why.to_string())
));
}
}
}
Ok(())
}
fn unverified_generation(generation: u8, acknowledged: bool) -> Result<(), String> {
if generation == 2 || acknowledged {
return Ok(());
}
Err(format!(
"no v{generation} encode has been played: no instrument that plays that \
generation has been available, so all that is known about the file is that it \
matches what Nord Sample Editor renders. Pass --unverified to write it anyway."
))
}
fn pcm_source(path: &Path) -> Result<nord_format::wav::Pcm16, String> {
let source = crate::wav::pcm16(path)?;
if source.rate != codec::SOURCE_RATE {
return Err(format!(
"{}: {} Hz — the field lattice is defined against {} Hz, and the instrument's \
own resampler is not decoded, so resample the WAV first",
path.display(),
source.rate,
codec::SOURCE_RATE,
));
}
Ok(source)
}
fn layout(generation: u8) -> Result<codec::Layout, String> {
match generation {
2 => Ok(codec::Layout::V2),
3 => Ok(codec::Layout::V3),
4 => Ok(codec::Layout::V4),
n => Err(format!("--generation {n}: the format has 2, 3 and 4")),
}
}
fn predictor(plain: bool) -> encode::Predictor {
if plain {
encode::Predictor::Plain
} else {
encode::Predictor::Minimising
}
}
fn stroke_line(stream: &[u8], at: usize, layout: codec::Layout) -> Result<String, String> {
let walk = codec::walk(stream, at, layout).map_err(|e| e.to_string())?;
let audio = codec::decode(stream, at, layout).map_err(|e| e.to_string())?;
let mut line = format!(
"{:>8} fields {:>7.3} s shift {}, peak {}, {} record(s), {}% predicted",
walk.fields,
audio.seconds(),
codec::shift(stream, layout).unwrap_or_default(),
codec::peak(stream, layout).unwrap_or_default(),
walk.records.len(),
100 * audio.differenced / audio.samples.len().max(1),
);
if let Some(record) = walk.records.iter().find(|r| r.mark) {
let fields = walk.fields - record.first_field;
line.push_str(&format!(
", loops the last {fields} field(s) ({:.3} s)",
fields as f64 / f64::from(codec::FIELD_RATE),
));
}
Ok(line)
}
fn loop_points(text: &str, crossfade: f64) -> Result<encode::Loop, String> {
let number = |part: &str, label: &str| {
part.trim()
.parse::<usize>()
.map_err(|_| format!("--loop wants START:END in frames; its {label} reads {part:?}"))
};
let (start, end) = text
.split_once(':')
.ok_or_else(|| format!("--loop wants START:END in frames, not {text:?}"))?;
Ok(encode::Loop::new(number(start, "start")?, number(end, "end")?).crossfade(crossfade))
}
pub fn encode(ui: &Ui, args: EncodeArgs) -> Result<(), String> {
unverified_generation(args.generation, args.unverified)?;
let layout = layout(args.generation)?;
let source = pcm_source(&args.wav)?;
let stem = args
.wav
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "sample".into());
let name = args.name.unwrap_or_else(|| stem.clone());
let mut options = encode::Options::new(&name)
.root_key(note::parse(&args.root_key)?)
.channels(source.channels)
.predictor(predictor(args.plain))
.layout(layout);
if let Some(top) = &args.top_note {
options = options.top_note(note::parse(top)?);
}
if let Some(points) = &args.loop_points {
options = options.loops(loop_points(points, args.loop_crossfade as f64)?);
}
if let Some(bits) = args.shift {
options = options.shift(bits);
}
let instrument = encode::instrument(&source.samples, &options).map_err(|e| e.to_string())?;
let out = instrument.to_bytes().map_err(|e| e.to_string())?;
let (at, stroke) = instrument.stroke_streams()[0];
ui.out(format!(
"{} frames -> {}",
source.frames(),
stroke_line(stroke, at, layout)?
));
let path = args.out.unwrap_or_else(|| {
args.wav
.with_file_name(format!("{stem}.{}", layout.extension()))
});
write_file(ui, &path, &out)
}
struct ProjectZone {
global_id: u32,
root_key: u8,
top_note: u8,
samples: Vec<i16>,
channels: u16,
start: usize,
source: PathBuf,
loops: Option<encode::Loop>,
secondary_start: f64,
repaired_secondary_start: Option<f64>,
gain: f64,
loop_decay: f32,
dropped: Vec<String>,
}
const WRAPPING_ZONE_GAIN: f64 = 16.0;
pub fn build(ui: &Ui, args: BuildArgs) -> Result<(), String> {
unverified_generation(args.generation, args.unverified)?;
let layout = layout(args.generation)?;
let project = match nord_format::from_path(&args.project)
.map_err(|e| format!("{}: {e}", args.project.display()))?
{
Entity::SampleProject(project) => project,
other => {
return Err(format!(
"{}: a {} file, not a Sample Editor project",
args.project.display(),
crate::file::entity_tag(&other)
))
}
};
let active_eq = project.active_eq().map_err(|e| e.to_string())?;
if !active_eq.is_empty() {
return Err(format!(
"the project enables {}, whose effect the editor bakes into the audio; \
disable it before building because this encoder cannot reproduce that processing",
active_eq.join(", ")
));
}
let preset = project_preset(&project, layout)?;
let dir = args.project.parent().unwrap_or_else(|| Path::new("."));
let resolved = project_zones(&project, dir, layout)?;
let name = match args.name {
Some(name) => name,
None => project.name().map_err(|e| e.to_string())?,
};
let zones: Vec<encode::NewZone> = resolved
.iter()
.map(|z| encode::NewZone {
source: &z.samples,
channels: z.channels,
root_key: z.root_key,
top_note: z.top_note,
global_id: z.global_id,
loops: z.loops,
secondary_start: z.secondary_start,
shift: args.shift,
gain: z.gain,
loop_decay: z.loop_decay,
})
.collect();
let map_gain = project.map_gain().map_err(|e| e.to_string())?;
let instrument = encode::multi_zone(
encode::Instrument {
name: &name,
map_gain,
predictor: predictor(args.plain),
layout,
preset,
},
&zones,
)
.map_err(|e| e.to_string())?;
let out = instrument.to_bytes().map_err(|e| e.to_string())?;
ui.out(format!("{} — {} zone(s)", ui.bold(&name), zones.len()));
let ceiling = 10f64.powf(encode::MAX_MAP_GAIN_DB / 20.0);
if !(0.0..=ceiling).contains(&map_gain) {
ui.note(ui.dim(format!(
"the map's own gain is {map_gain}, which the instrument clamps at \
+{:.3} dB as the editor does",
encode::MAX_MAP_GAIN_DB
)));
}
let placed = instrument.zones().map_err(|e| e.to_string())?;
for (index, zone) in resolved.iter().enumerate() {
let stream = placed
.get(index)
.ok_or_else(|| format!("zone{} did not reach the file", index + 1))?;
ui.out(format!(
" zone{:<2} root {:<4} top {:<4} {}",
index + 1,
note::name(zone.root_key),
note::name(zone.top_note),
stroke_line(stream.stream, stream.at, layout)?,
));
let gain = match zone.gain == 1.0 {
true => String::new(),
false => format!(" gain {:.3}", zone.gain),
};
ui.out(ui.dim(format!(
" stroke {}{gain} from {}",
zone.global_id,
zone.source.display()
)));
if zone.gain >= WRAPPING_ZONE_GAIN {
ui.warn(format!(
"zone{} sets gain {}, which overflows both of the instrument's gain \
fields; the file will state a far quieter level, as the editor's own \
render of this project does",
index + 1,
zone.gain
));
}
if !zone.dropped.is_empty() {
ui.warn(format!(
"zone{} sets {}, which the instrument has nowhere to hold",
index + 1,
zone.dropped.join(", ")
));
}
if let Some(stated) = zone.repaired_secondary_start {
ui.note(ui.dim(format!(
"zone{} states m_startSecondary = {stated}, which the editor repairs on \
load; encoded from frame {} as the editor would",
index + 1,
zone.secondary_start + zone.start as f64,
)));
}
}
let path = args
.out
.unwrap_or_else(|| args.project.with_extension(layout.extension()));
write_file(ui, &path, &out)
}
fn project_preset(project: &Project, layout: codec::Layout) -> Result<encode::Preset, String> {
let mut preset = encode::Preset {
dynamics_enabled: project.dynamics_enabled().map_err(|e| e.to_string())?,
..encode::Preset::default()
};
if layout == codec::Layout::V2 {
let defaults = project.velocity_defaults().map_err(|e| e.to_string())?;
preset.velocity_to_amplitude =
nsmp::velocity_level(defaults.amplitude).ok_or_else(|| {
format!(
"m_velAmpl = {} has no decoded v2 preset level",
defaults.amplitude
)
})?;
preset.velocity_to_timbre = nsmp::velocity_level(defaults.timbre).ok_or_else(|| {
format!(
"m_velTimbre = {} has no decoded v2 preset level",
defaults.timbre
)
})?;
}
Ok(preset)
}
fn project_zones(
project: &Project,
dir: &Path,
layout: codec::Layout,
) -> Result<Vec<ProjectZone>, String> {
let say = |e: nord_format::error::ParseError| e.to_string();
let files = project.audio_files().map_err(say)?;
let strokes = project.strokes().map_err(say)?;
let zones = project.zones().map_err(say)?;
let instrument_decay = project.loop_decay_enabled().map_err(say)?;
validate_key_ranges(&zones)?;
zones
.iter()
.enumerate()
.map(|(index, zone)| {
let at = format!("zone{}", index + 1);
if !zone.enabled {
return Err(format!(
"{at} is switched off in the project; turn it on or remove it — an \
instrument has no way to carry a zone that does not sound"
));
}
let [layer] = zone.strokes.as_slice() else {
return Err(format!(
"{at} plays {} strokes, which is a velocity split or a round robin; \
one stroke per zone is the layout this writer lays down",
zone.strokes.len()
));
};
if !layer.enabled {
return Err(format!("{at}'s only stroke is switched off"));
}
if layer.detune != 0 || layer.velocity != (0, 127) {
return Err(format!(
"{at} sets detune {} and velocity {}..={} on its stroke; where the \
instrument applies those is not decoded, so nothing here reproduces \
them",
layer.detune, layer.velocity.0, layer.velocity.1
));
}
let stroke = strokes
.iter()
.find(|s| s.global_id == layer.global_id)
.ok_or_else(|| {
format!(
"{at} names stroke {}, which the project does not hold",
layer.global_id
)
})?;
let file = files
.iter()
.find(|f| f.id == stroke.file_id)
.ok_or_else(|| {
format!(
"{at} plays audio file {}, which the project does not hold",
stroke.file_id
)
})?;
let path = dir.join(&file.path);
let source = pcm_source(&path)?;
let frames = source.frames();
let channels = usize::from(source.channels);
let start = frame(&at, "start", stroke.start, frames)?;
let stop = frame(&at, "stop", stroke.stop, frames)?;
if start >= stop {
return Err(format!(
"{at} plays frames {start}..{stop} of {}, which is nothing",
path.display()
));
}
let (loops, mut dropped) = zone_loop(&at, stroke, start, stop, layout)?;
if loops.is_some() && instrument_decay {
dropped.push("the instrument's own m_loopDecayEnabled".into());
}
let encoded_secondary = stroke.encoded_secondary_start();
Ok(ProjectZone {
global_id: layer.global_id,
root_key: zone.root_key,
top_note: zone.top_note,
channels: source.channels,
samples: source.samples[start * channels..stop * channels].to_vec(),
source: path,
loops,
start,
secondary_start: encoded_secondary - start as f64,
repaired_secondary_start: (encoded_secondary != stroke.start_secondary)
.then_some(stroke.start_secondary),
gain: layer.gain,
loop_decay: stroke.loop_decay as f32,
dropped,
})
})
.collect()
}
fn zone_loop(
at: &str,
stroke: &Stroke,
start: usize,
stop: usize,
layout: codec::Layout,
) -> Result<(Option<encode::Loop>, Vec<String>), String> {
if !stroke.loop_enabled {
return Ok((None, Vec::new()));
}
let short = stroke.short_loop_enabled;
let length = if short {
stroke.short_loop_length
} else {
stroke.loop_length
};
let named = if short {
"m_loopLengthShort"
} else {
"m_loopLengthLong"
};
let stated = stroke.encoded_loop_start();
let loop_start = frame(at, "loop start", stated, stop)?;
if !length.is_finite() || length <= 0.0 {
return Err(format!("{at}'s {named} is {length}, which is not a loop"));
}
let end = frame(at, "loop end", stated + length, stop)?;
if loop_start < start {
return Err(format!(
"{at} loops from frame {loop_start} but its audio is trimmed to start at \
{start}; the loop would begin before the sample does"
));
}
if !short && stroke.loop_crossfade_mode != 0 {
return Err(format!(
"{at} sets m_loopXFModeLong = {}; only the linear fade (mode 0) is decoded, \
and the fade is baked into the audio, so this one cannot be written",
stroke.loop_crossfade_mode
));
}
let crossfade = if short {
exact_frame(
at,
"short loop crossfade",
f64::from(stroke.short_loop_crossfade) / 100.0 * length,
stop,
)?
} else {
exact_frame(at, "loop crossfade", stroke.loop_crossfade, stop)?
};
let mut dropped = Vec::new();
if stroke.loop_detune != 0 {
dropped.push(format!("m_loopDetune = {}", stroke.loop_detune));
}
if stroke.loop_decay_enabled {
dropped.push(match layout {
codec::Layout::V2 => {
format!("m_loopDecayEnabled and m_loopDecay = {}", stroke.loop_decay)
}
codec::Layout::V3 | codec::Layout::V4 => {
"m_loopDecayEnabled — the amount is written, the switch is not".into()
}
});
}
if short && !stroke.short_loop_uses_pitch {
dropped.push("m_shortLoopUsesPitch = 0".into());
}
if short && stroke.loop_crossfade != 0.0 {
dropped.push(format!(
"m_loopXFadeLengthLong = {} — the short loop is the one encoded",
stroke.loop_crossfade
));
}
if short && stroke.loop_crossfade_mode != 0 {
dropped.push(format!("m_loopXFModeLong = {}", stroke.loop_crossfade_mode));
}
Ok((
Some(encode::Loop::new(loop_start - start, end - start).crossfade(crossfade)),
dropped,
))
}
fn validate_key_ranges(zones: &[Zone]) -> Result<(), String> {
for (index, zone) in zones.iter().enumerate() {
let at = format!("zone{}", index + 1);
if !(zone.bottom_note..=zone.top_note).contains(&zone.root_key) {
return Err(format!(
"{at}'s root note {} is outside its range {}..={}",
zone.root_key, zone.bottom_note, zone.top_note
));
}
let encoded_bottom = match zones.get(index + 1) {
Some(below) => {
let below_at = index + 2;
below.top_note.checked_add(1).ok_or_else(|| {
format!(
"zone{below_at} reaches note {}, leaving no range for {at}",
below.top_note
)
})?
}
None => LOWEST_NOTE,
};
if zone.bottom_note != encoded_bottom {
return Err(format!(
"{at} starts at note {}, but its encoded range would start at \
{encoded_bottom}; the encoded keyboard map tiles its zones, so that \
gap or overlap cannot be reproduced",
zone.bottom_note
));
}
}
Ok(())
}
fn frame(zone: &str, label: &str, value: f64, frames: usize) -> Result<usize, String> {
Ok(exact_frame(zone, label, value, frames)?.round() as usize)
}
fn exact_frame(zone: &str, label: &str, value: f64, frames: usize) -> Result<f64, String> {
if !value.is_finite() || !(0.0..=frames as f64).contains(&value) {
return Err(format!(
"{zone}'s {label} is at frame {value}, outside the {frames} frames its audio holds"
));
}
Ok(value)
}
pub fn verify(ui: &Ui, args: VerifyArgs) -> Result<(), String> {
crate::file::check_each(ui, &args.targets, "target(s) did not check out", |spec| {
verify_target(spec, args.deep)
})
}
fn verify_target(spec: &str, walk: bool) -> Result<String, String> {
let origin = crate::slot::target(spec).map_err(|e| format!("error {e}"))?;
let original = read(&origin).map_err(|e| format!("error {spec} ({e})"))?;
let round_trip = nord_format::from_stream(&mut std::io::Cursor::new(&original))
.and_then(|entity| nord_format::to_bytes(&entity))
.map_err(|e| format!("error {spec} ({e})"))?;
if round_trip != original {
return Err(format!(
"DIFFER {spec} (re-encode is not byte-identical; first difference at {})",
crate::file::first_difference(&round_trip, &original),
));
}
if !walk {
return Ok(format!("ok {spec} ({} bytes)", original.len()));
}
match deep(&original) {
Ok(note) => Ok(format!("ok {spec} ({note})")),
Err(e) => Err(format!("STREAM {spec} ({e})")),
}
}
fn deep(bytes: &[u8]) -> Result<String, String> {
let body = body(bytes)?;
deep_body(&body)
}
fn deep_body(body: &nord_format::Sample) -> Result<String, String> {
let layout = body.layout().map_err(|e| e.to_string())?;
let chain = body.chain().map_err(|e| e.to_string())?;
let streams = body.stroke_streams();
let mut records = 0usize;
let mut looped = 0usize;
for (index, (at, stroke)) in streams.iter().enumerate() {
let stream =
codec::walk(stroke, *at, layout).map_err(|e| format!("stroke {index}: {e}"))?;
records += stream.records.len();
let directory = codec::Directory::read(stroke)
.ok_or_else(|| format!("stroke {index} is too short for its word directory"))?;
let words = (stroke.len() - layout.header_len()) / layout.word();
let first = codec::Directory::resolve(directory.first_record, *at, layout);
let terminator = codec::Directory::resolve_end(directory.terminator, *at, layout, words);
if first != stream.first_record || terminator != stream.terminator {
return Err(format!(
"stroke {index}: directory says {first}..{terminator}, walk found {}..{}",
stream.first_record, stream.terminator
));
}
let names = |pointer: u16, word: usize| {
word % codec::WRAP == codec::Directory::resolve(pointer, *at, layout) % codec::WRAP
};
if !names(directory.resync, stream.terminator)
&& !stream.records.iter().any(|r| names(directory.resync, r.at))
{
return Err(format!("stroke {index}: resync does not name a record"));
}
let ends_at_mark = names(directory.mark, stream.terminator);
let named_by_mark: Vec<_> = stream
.records
.iter()
.filter(|r| names(directory.mark, r.at))
.collect();
let flagged: Vec<_> = stream.records.iter().filter(|r| r.mark).collect();
if flagged.len() > 1 {
return Err(format!(
"stroke {index}: {} records carry the mark bit",
flagged.len()
));
}
if let [record] = flagged.as_slice() {
if ends_at_mark || !names(directory.mark, record.at) {
return Err(format!(
"stroke {index}: the marked record is not the one the directory names"
));
}
}
if ends_at_mark {
continue;
}
let Some(mark) = named_by_mark.first() else {
return Err(format!(
"stroke {index}: the mark names neither a record nor the terminator"
));
};
if chain.flags_the_marked_record() && flagged.is_empty() {
return Err(format!(
"stroke {index}: the loop mark names a record that does not carry the mark bit"
));
}
looped += 1;
if layout == codec::Layout::V2 {
let packet = nsmp::stroke::packet_len(layout) / layout.word();
if !named_by_mark
.iter()
.any(|r| (terminator - r.at).is_multiple_of(packet))
{
return Err(format!(
"stroke {index}: the loop covers {} words, which is not whole packets",
terminator - mark.at
));
}
}
}
let mut note = format!(
"{}, {} stroke(s), {records} record(s), directory agrees",
body.generation(),
streams.len()
);
if looped > 0 {
note.push_str(&format!(", {looped} looped"));
}
Ok(note)
}
#[derive(Debug)]
struct ZoneSpec {
wav: PathBuf,
root_key: u8,
}
fn zone_spec(spec: &str) -> Result<ZoneSpec, String> {
let (wav, note) = spec
.rsplit_once('=')
.ok_or_else(|| format!("expected WAV=NOTE, got {spec:?}"))?;
if wav.is_empty() {
return Err(format!("{spec:?} names no WAV"));
}
Ok(ZoneSpec {
wav: PathBuf::from(wav),
root_key: note::parse(note)?,
})
}
fn project_frames(frames: usize, rate: u32) -> Result<u64, String> {
if rate == 0 {
return Err("the WAV declares 0 Hz".into());
}
u64::try_from(frames)
.ok()
.and_then(|frames| nsmpproj::project_frames(frames, rate))
.ok_or_else(|| format!("{frames} frames at {rate} Hz overflows a frame count"))
}
fn stored_path(wav: &Path, project: &Path) -> String {
let dir = project.parent().unwrap_or(Path::new(""));
if dir.as_os_str().is_empty() {
return wav.display().to_string();
}
wav.strip_prefix(dir).unwrap_or(wav).display().to_string()
}
fn zone(spec: &ZoneSpec, wav: &[u8], project: &Path) -> Result<NewZone, String> {
let named = |e: String| format!("{}: {e}", spec.wav.display());
let audio = nord_format::wav::read_pcm16(wav).map_err(|e| named(e.to_string()))?;
Ok(NewZone {
path: stored_path(&spec.wav, project),
sample_rate: audio.rate,
frames: project_frames(audio.frames(), audio.rate).map_err(named)?,
root_key: spec.root_key,
})
}
fn destination(name: Option<String>, out: Option<PathBuf>) -> Result<(String, PathBuf), String> {
match (name, out) {
(Some(name), Some(out)) => Ok((name, out)),
(Some(name), None) => {
let out = PathBuf::from(format!("{name}.{}", nsmpproj::FORMAT));
Ok((name, out))
}
(None, Some(out)) => {
let name = out
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.filter(|s| !s.is_empty())
.ok_or_else(|| {
format!(
"{}: no file stem to name the instrument after; pass --name",
out.display()
)
})?;
Ok((name, out))
}
(None, None) => Err(
"pass --name or -o: the name defaults to the output's stem, \
and the output to the name, so one of them has to be given"
.into(),
),
}
}
pub fn project_new(ui: &Ui, args: ProjectNewArgs) -> Result<(), String> {
let specs: Vec<ZoneSpec> = args
.zones
.iter()
.map(|spec| zone_spec(spec))
.collect::<Result<_, _>>()?;
let (name, out) = destination(args.name, args.out)?;
let mut zones = Vec::with_capacity(specs.len());
for spec in &specs {
let wav = std::fs::read(&spec.wav).map_err(|e| format!("{}: {e}", spec.wav.display()))?;
zones.push(zone(spec, &wav, &out)?);
}
let project =
Project::new(&name, &zones, crate::edit::unix_seconds_now()?).map_err(|e| e.to_string())?;
for z in &zones {
ui.out(format!(
" root {:<4} {:>6} Hz {:>10} frames {}",
note::name(z.root_key),
z.sample_rate,
z.frames,
z.path,
));
}
let rescaled = zones
.iter()
.filter(|z| u64::from(z.sample_rate) != PROJECT_RATE)
.count();
if rescaled > 0 {
ui.note(ui.dim(format!(
"{rescaled} zone(s) are not {PROJECT_RATE} Hz; a project counts every frame \
position at {PROJECT_RATE} Hz, so those counts are restated"
)));
}
write_file(ui, &out, project.render().as_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
fn wav(rate: u32, frames: usize) -> Vec<u8> {
nord_format::wav::mono_pcm16(&vec![0i16; frames], rate).unwrap()
}
fn encoded(name: &str) -> Vec<u8> {
let options = encode::Options::new(name).root_key(60);
encode::instrument(&vec![0i16; 4096], &options)
.unwrap()
.to_bytes()
.unwrap()
}
fn instrument(name: &str) -> nord_format::Sample {
let bytes = encoded(name);
match nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).unwrap() {
Entity::Sample(sample) => sample,
other => panic!("encoded a {}", other.identity().format),
}
}
fn scratch() -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("nord-sample-{}-{nanos}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn encode_args(wav: &Path, out: PathBuf, generation: u8, unverified: bool) -> EncodeArgs {
EncodeArgs {
wav: wav.to_path_buf(),
out: Some(out),
name: Some("Gate".into()),
root_key: "C4".into(),
top_note: None,
loop_points: None,
loop_crossfade: 0,
plain: false,
generation,
shift: None,
unverified,
}
}
#[test]
fn a_v2_encode_writes_unasked_and_the_unplayed_generations_do_not() {
let dir = scratch();
let source = dir.join("tone.wav");
std::fs::write(&source, wav(codec::SOURCE_RATE, 4096)).unwrap();
let ui = Ui::new(crate::ui::ColorChoice::Never);
let played = dir.join("gate.nsmp");
encode(&ui, encode_args(&source, played.clone(), 2, false))
.expect("v2 is hardware-verified and needs no acknowledgement");
assert!(played.is_file());
for generation in [3u8, 4] {
let out = dir.join(format!("gate.nsmp{generation}"));
let refused =
encode(&ui, encode_args(&source, out.clone(), generation, false)).unwrap_err();
assert!(refused.contains("--unverified"), "v{generation}: {refused}");
assert!(!out.exists(), "v{generation} was written anyway");
encode(&ui, encode_args(&source, out.clone(), generation, true)).expect("acknowledged");
assert!(out.is_file(), "v{generation}");
}
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
#[cfg(target_pointer_width = "64")]
fn a_frame_count_a_project_cannot_state_is_refused_by_name() {
assert_eq!(project_frames(2205, 22_050).unwrap(), 4410);
let rateless = project_frames(1, 0).unwrap_err();
assert!(rateless.contains("0 Hz"), "{rateless}");
let frames = (u64::MAX / PROJECT_RATE) as usize;
let over = project_frames(frames, u32::MAX).unwrap_err();
assert!(over.contains("overflows"), "{over}");
}
#[test]
fn a_wav_beside_the_project_is_stored_relative_to_it() {
let project = Path::new("kit/marimba.nsmpproj");
assert_eq!(stored_path(Path::new("kit/low.wav"), project), "low.wav");
assert_eq!(
stored_path(Path::new("/elsewhere/low.wav"), project),
"/elsewhere/low.wav",
);
assert_eq!(
stored_path(Path::new("low.wav"), Path::new("marimba.nsmpproj")),
"low.wav",
);
}
#[test]
fn a_built_project_reads_back_with_its_zones_roots_and_paths() {
let out = Path::new("kit/marimba.nsmpproj");
let specs = [
zone_spec("kit/low.wav=C3").unwrap(),
zone_spec("kit/high.wav=72").unwrap(),
];
let zones = [
zone(&specs[0], &wav(22_050, 2205), out).unwrap(),
zone(&specs[1], &wav(44_100, 4410), out).unwrap(),
];
assert_eq!((zones[0].path.as_str(), zones[0].frames), ("low.wav", 4410));
let bytes = Project::new("Marimba", &zones, 0).unwrap().render();
let read =
match nord_format::from_stream(&mut std::io::Cursor::new(bytes.as_bytes())).unwrap() {
Entity::SampleProject(project) => project,
other => panic!("wrote a {}", other.identity().format),
};
assert_eq!(read.name().unwrap(), "Marimba");
let roots: Vec<u8> = read.zones().unwrap().iter().map(|z| z.root_key).collect();
assert_eq!(roots, [72, 48], "zones are stored high to low");
let paths: Vec<String> = read
.audio_files()
.unwrap()
.into_iter()
.map(|f| f.path)
.collect();
assert_eq!(paths, ["low.wav", "high.wav"], "ids rise with the root key");
let rates: Vec<u32> = read
.audio_files()
.unwrap()
.iter()
.map(|f| f.sample_rate)
.collect();
assert_eq!(rates, [22_050, 44_100], "the file's own rate is kept");
}
#[test]
fn a_malformed_zone_says_what_it_expected() {
assert!(zone_spec("low.wav").unwrap_err().contains("WAV=NOTE"));
assert!(zone_spec("=C4").unwrap_err().contains("names no WAV"));
assert!(zone_spec("low.wav=H9").is_err());
assert!(zone_spec("low.wav=128").is_err());
assert_eq!(zone_spec("a=b.wav=C4").unwrap().wav, Path::new("a=b.wav"));
}
#[test]
fn a_project_needs_at_least_one_zone() {
let new = ["nord", "sample", "project", "new", "--name", "X"];
assert!(crate::Cli::try_parse_from(new).is_err());
assert!(crate::Cli::try_parse_from([&new[..], &["--zone", "a.wav=C4"]].concat()).is_ok());
}
#[test]
fn a_name_and_an_output_stand_in_for_each_other_but_not_for_nothing() {
assert_eq!(
destination(Some("Marimba".into()), None).unwrap(),
("Marimba".into(), PathBuf::from("Marimba.nsmpproj")),
);
assert_eq!(
destination(None, Some("kit/marimba.nsmpproj".into())).unwrap(),
("marimba".into(), PathBuf::from("kit/marimba.nsmpproj")),
);
assert!(destination(None, None).is_err());
}
#[test]
fn a_decoded_slot_names_its_wavs_after_the_instrument() {
let at = crate::slot::parse("2:7").unwrap();
assert_eq!(
stem(&Target::Slot(at), &instrument("Vibes 2/3")),
"Vibes-2-3"
);
assert_eq!(stem(&Target::Slot(at), &instrument("")), "2-7");
assert_eq!(
stem(&Target::File("kit/Bass.nsmp".into()), &instrument("Vibes")),
"Bass",
);
}
#[test]
fn an_output_that_is_the_input_takes_the_in_place_guard() {
let dir = scratch();
let path = dir.join("kit.nsmp");
let original = encoded("Kit");
std::fs::write(&path, &original).unwrap();
let args = EditArgs {
target: path.display().to_string(),
common: crate::edit::SetArgs {
set: vec!["name=Vibes".into()],
dry_run: false,
fields: false,
out: Some(dir.join(".").join("kit.nsmp")),
yes: false,
},
};
let err = run(&Ui::piped(), args).unwrap_err();
assert!(err.contains("--yes"), "{err}");
assert_eq!(std::fs::read(&path).unwrap(), original);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_project_under_sample_edit_is_steered_to_the_file_verb() {
let dir = scratch();
let path = dir.join("kit.nsmpproj");
let project = Project::new(
"Kit",
&[NewZone {
path: "kit.wav".into(),
sample_rate: 44_100,
frames: 44_100,
root_key: 60,
}],
0,
)
.unwrap();
std::fs::write(&path, project.render()).unwrap();
let args = EditArgs {
target: path.display().to_string(),
common: crate::edit::SetArgs {
set: vec!["name=Vibes".into()],
dry_run: false,
fields: false,
out: None,
yes: false,
},
};
let err = run(&Ui::piped(), args).unwrap_err();
assert!(err.contains(nsmpproj::FORMAT), "{err}");
assert!(err.contains("nord edit"), "{err}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_decode_that_loses_one_target_of_several_fails() {
let dir = scratch();
let kit = dir.join("kit.nsmp");
std::fs::write(&kit, encoded("Kit")).unwrap();
let ui = Ui::new(crate::ui::ColorChoice::Never);
let targets = |specs: &[&Path]| DecodeArgs {
targets: specs.iter().map(|p| p.display().to_string()).collect(),
out: None,
};
decode(&ui, targets(&[&kit])).expect("a target that decodes");
let err = decode(&ui, targets(&[&kit, &dir.join("absent.nsmp")])).unwrap_err();
assert!(err.contains("1 of 2"), "{err}");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn a_second_target_of_the_same_name_does_not_overwrite_the_first_targets_wavs() {
let dir = scratch();
let out = dir.join("wavs");
let mut kits = Vec::new();
for side in ["a", "b"] {
let held = dir.join(side);
std::fs::create_dir_all(&held).unwrap();
let kit = held.join("kit.nsmp");
std::fs::write(&kit, encoded("Kit")).unwrap();
kits.push(kit.display().to_string());
}
let err = decode(
&Ui::new(crate::ui::ColorChoice::Never),
DecodeArgs {
targets: kits,
out: Some(out.clone()),
},
)
.unwrap_err();
assert!(err.contains("1 of 2"), "{err}");
assert_eq!(std::fs::read_dir(&out).unwrap().count(), 1);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn verifying_a_target_that_is_neither_a_file_nor_a_slot_names_both_readings() {
let line = verify_target("no-such-instrument.nsmp", false).unwrap_err();
assert!(line.starts_with("error"), "{line}");
assert!(line.contains("no such file"), "{line}");
assert!(line.contains("not a slot"), "{line}");
}
fn map_zone(root_key: u8, bottom_note: u8, top_note: u8) -> Zone {
Zone {
zone_id: 0,
root_key,
enabled: true,
bottom_note,
top_note,
strokes: Vec::new(),
}
}
#[test]
fn a_project_key_map_must_be_representable_by_top_notes() {
let valid = [map_zone(72, 61, 84), map_zone(48, LOWEST_NOTE, 60)];
assert!(validate_key_ranges(&valid).is_ok());
let gap = [map_zone(72, 62, 84), map_zone(48, LOWEST_NOTE, 60)];
assert!(validate_key_ranges(&gap).is_err());
let misplaced_root = [map_zone(60, 61, 84), map_zone(48, LOWEST_NOTE, 60)];
assert!(validate_key_ranges(&misplaced_root).is_err());
let raised_floor = [map_zone(60, LOWEST_NOTE + 1, 84)];
assert!(validate_key_ranges(&raised_floor).is_err());
}
#[test]
fn loop_points_read_as_frames_around_a_colon() {
let points = loop_points("16384:32768", 1024.0).unwrap();
assert_eq!(points, encode::Loop::new(16_384, 32_768).crossfade(1_024.0));
assert_eq!(
loop_points(" 8 : 9 ", 0.0).unwrap(),
encode::Loop::new(8, 9)
);
for bad in ["16384", "16384:", "a:b", "16384:32768:1", "-1:5"] {
assert!(loop_points(bad, 0.0).is_err(), "{bad}");
}
}
#[test]
fn a_projects_loop_maps_onto_the_one_the_container_holds() {
let long = stroke_with(|s| s.loop_enabled = true);
let (points, dropped) =
zone_loop("zone1", &long, 1_000, 88_200, codec::Layout::V4).unwrap();
assert_eq!(points, Some(encode::Loop::new(15_384, 31_768)));
assert!(dropped.is_empty());
let short = stroke_with(|s| {
s.loop_enabled = true;
s.short_loop_enabled = true;
s.short_loop_length = 1_024.0;
s.short_loop_crossfade = 25;
s.loop_crossfade = 4_096.0;
s.loop_crossfade_mode = 1;
});
let (points, dropped) = zone_loop("zone1", &short, 0, 88_200, codec::Layout::V4).unwrap();
assert_eq!(
points,
Some(encode::Loop::new(16_384, 17_408).crossfade(256.0))
);
assert!(
dropped.iter().any(|d| d.contains("m_loopXFadeLengthLong")),
"{dropped:?}"
);
assert!(
dropped.iter().any(|d| d.contains("m_loopXFModeLong")),
"{dropped:?}"
);
let unfaded = stroke_with(|s| {
s.loop_enabled = true;
s.short_loop_enabled = true;
s.short_loop_length = 1_024.0;
s.short_loop_crossfade = 0;
});
let (points, _) = zone_loop("zone1", &unfaded, 0, 88_200, codec::Layout::V4).unwrap();
assert_eq!(points, Some(encode::Loop::new(16_384, 17_408)));
let off = stroke_with(|_| {});
assert_eq!(
zone_loop("zone1", &off, 0, 88_200, codec::Layout::V4)
.unwrap()
.0,
None
);
}
#[test]
fn loop_settings_with_nowhere_to_go_are_named() {
let refused = |edit: fn(&mut Stroke)| {
let mut s = stroke_with(|s| s.loop_enabled = true);
edit(&mut s);
zone_loop("zone1", &s, 0, 88_200, codec::Layout::V4).unwrap_err()
};
assert!(refused(|s| s.loop_crossfade_mode = 1).contains("m_loopXFModeLong"));
assert!(refused(|s| s.loop_length = 0.0).contains("m_loopLengthLong"));
assert!(refused(|s| s.loop_length = 90_000.0).contains("loop end"));
let trimmed = stroke_with(|s| s.loop_enabled = true);
assert!(zone_loop("zone1", &trimmed, 20_000, 88_200, codec::Layout::V4).is_err());
let mut noisy = stroke_with(|s| s.loop_enabled = true);
noisy.loop_detune = -50;
noisy.loop_decay_enabled = true;
let (points, dropped) = zone_loop("zone1", &noisy, 0, 88_200, codec::Layout::V4).unwrap();
assert!(points.is_some());
assert_eq!(dropped.len(), 2, "{dropped:?}");
assert!(dropped[0].contains("m_loopDetune"));
assert!(dropped[1].contains("m_loopDecay"));
let (_, narrow) = zone_loop("zone1", &noisy, 0, 88_200, codec::Layout::V2).unwrap();
assert!(narrow[1].contains("m_loopDecay = 20"), "{narrow:?}");
}
fn stroke_with(edit: impl FnOnce(&mut Stroke)) -> Stroke {
let mut stroke = Stroke {
zone_id: 129,
global_id: 1,
file_id: 1,
begin: 0.0,
end: 88_200.0,
start: 0.0,
start_secondary: 11_025.0,
stop: 88_200.0,
loop_enabled: false,
short_loop_enabled: false,
loop_start: 16_384.0,
loop_length: 16_384.0,
short_loop_length: 0.0,
loop_crossfade: 0.0,
loop_crossfade_mode: 0,
short_loop_crossfade: 10,
short_loop_uses_pitch: true,
loop_detune: 0,
loop_decay_enabled: false,
loop_decay: 20.0,
};
edit(&mut stroke);
stroke
}
#[test]
fn a_frame_position_is_checked_before_rounding() {
assert_eq!(frame("zone1", "start", 0.4, 10).unwrap(), 0);
for value in [-0.4, 10.4, f64::NAN, f64::INFINITY] {
assert!(frame("zone1", "start", value, 10).is_err(), "{value}");
}
}
#[test]
fn a_directory_cannot_claim_an_unmarked_record_as_a_loop() {
let mut sample =
encode::instrument(&[0; encode::MIN_FRAMES], &encode::Options::new("Unmarked"))
.unwrap();
let nord_format::Sample::V2(file) = &mut sample else {
panic!("the default options build the narrow chain");
};
let stroke = nord_format::formats::nsmp::section::find_mut(
&mut file.body.sections,
nord_format::formats::nsmp::section::STK,
)
.unwrap();
let first = stroke.payload[FIRST_RECORD..][..POINTER].to_vec();
stroke.payload[MARK..][..POINTER].copy_from_slice(&first);
let directory = codec::Directory::read(&stroke.payload).expect("a directory");
assert_eq!(directory.mark, directory.first_record);
assert!(deep_body(&sample)
.unwrap_err()
.contains("does not carry the mark bit"));
}
const POINTER: usize = 2;
const FIRST_RECORD: usize = 20;
const MARK: usize = FIRST_RECORD + 9 * 2;
}