use std::collections::BTreeSet;
use std::error::Error;
use std::fmt;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use super::range_epoch::{CURRENT_RANGE_EPOCH, stale_modulation};
use super::song_ids::{song_id_at, song_id_index};
use super::voice::{TONAL_MAX_LOOP_STEPS, TONAL_PHRASES, TonalSequenceState};
use super::{
AutomationState, ControlAddress, ControlKind, ControlSpec, DEFAULT_LFO_DEPTH_RATIO, EnvTrigger,
EnvelopeRoute, FluidControls, GESTURE_COUNT, GestureEnvelope, GestureKind, GestureState,
LfoRoute, LfoShape, MAX_AUTOMATION_LANES_PER_KIND, MAX_ENV_ATTACK_BEATS, MAX_ENV_DECAY_BEATS,
MAX_LFO_CYCLE_BEATS, MAX_LFO_OFFSET_BEATS, MAX_LFO_STEPS, MIN_LFO_CYCLE_BEATS, MUTE_BYTES,
ModKind, ModuleSlotField, MuteState, PAD_RHYTHM_ROWS, Step, TAB_COUNT, Tab, all_specs,
parse_module_slot_id, spec_by_id,
};
const MAGIC: &[u8; 4] = b"NOOI";
pub(crate) const CONTAINER_VERSION: u8 = 2;
pub(crate) const CODE_PREFIX: &str = "n1_";
pub(crate) const SNAPSHOT_RECORD: u8 = 0;
pub(crate) const AUTOMATION_RECORD: u8 = 1;
const TONAL_SEQUENCE_RECORD: u8 = 2;
const MUTE_RECORD: u8 = 3;
const GESTURE_RECORD: u8 = 4;
const RANGE_EPOCH_RECORD: u8 = 5;
const MIDI_ROWS_RECORD: u8 = 6;
const PAD_RHYTHM_ROWS_RECORD: u8 = 7;
const CAPTURE_RECORD: u8 = 8;
const LANE_BYPASS_RECORD: u8 = 9;
const CAPTURE_WIRE_VERSION: u8 = 3;
const GESTURE_HELD_FLAG: u8 = 1 << 0;
const LFO_SHAPE_TAGS: [(LfoShape, u8); 8] = [
(LfoShape::Sine, 0),
(LfoShape::Triangle, 1),
(LfoShape::RampUp, 2),
(LfoShape::RampDown, 3),
(LfoShape::Square, 4),
(LfoShape::RandomDrift, 5),
(LfoShape::SampleHold, 6),
(LfoShape::Steps, 7),
];
const ENV_TRIGGER_EVERY_BEATS: u8 = 0;
const ENV_TRIGGER_ON_KICK: u8 = 1;
const ENV_TRIGGER_ONCE: u8 = 2;
const DEFAULT_ENV_TRIGGER_BEATS: f32 = 4.0;
#[derive(Clone, Default)]
pub(crate) struct SongState {
pub(crate) controls: FluidControls,
pub(crate) automation: AutomationState,
pub(crate) tonal_sequence: Option<TonalSequenceState>,
pub(crate) muted: MuteState,
pub(crate) gestures: GestureState,
}
impl SongState {
pub(crate) fn from_controls(controls: FluidControls) -> Self {
Self {
controls,
automation: AutomationState::default(),
tonal_sequence: None,
muted: [false; TAB_COUNT],
gestures: GestureState::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SongCodeError {
MissingPrefix,
InvalidBase64,
InvalidMagic,
UnsupportedVersion(u8),
Truncated,
TooLarge,
InvalidValueTag(u8),
InvalidGestureTarget(u8),
InvalidGestureKind(u8),
InvalidGestureFlags(u8),
InvalidGestureCount(u8),
InvalidGestureAmount,
InvalidGestureTimeAnchor {
target: u8,
kind: u8,
},
DuplicateGesture {
target: u8,
kind: u8,
},
DuplicateGestureRecord,
InvalidMidiRows(u8),
DuplicateMidiRowsRecord,
InvalidPadRhythmRows(u8),
DuplicatePadRhythmRowsRecord,
InvalidCapture,
InvalidLaneBypass,
RetiredControl(&'static str),
NegativeLfoStep(&'static str),
StaleRange(&'static str),
UnknownValue {
id: &'static str,
value: i16,
},
NotATableValue(&'static str),
UnregisteredControl(&'static str),
}
impl fmt::Display for SongCodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCapture => write!(f, "song code has an invalid captured loop"),
Self::InvalidLaneBypass => write!(f, "song code has invalid lane bypass data"),
Self::MissingPrefix => write!(f, "song code must start with {CODE_PREFIX}"),
Self::InvalidBase64 => write!(f, "song code is not valid base64url"),
Self::InvalidMagic => write!(f, "song code is not a nooise snapshot"),
Self::UnsupportedVersion(version) => write!(
f,
"song code version {version} is from an older nooise and can no longer be read \
(this build writes and reads version {CONTAINER_VERSION})"
),
Self::Truncated => write!(f, "song code is truncated"),
Self::TooLarge => write!(f, "song code payload is too large"),
Self::InvalidValueTag(tag) => write!(f, "song code has unknown value tag {tag}"),
Self::InvalidGestureTarget(target) => {
write!(f, "song code has unknown gesture target {target}")
}
Self::InvalidGestureKind(kind) => {
write!(f, "song code has unknown gesture kind {kind}")
}
Self::InvalidGestureFlags(flags) => {
write!(f, "song code has unknown gesture flags {flags:#04x}")
}
Self::InvalidGestureCount(count) => {
write!(f, "song code has too many gesture entries ({count})")
}
Self::InvalidGestureAmount => {
write!(f, "song code has a gesture amount outside 0..=1")
}
Self::InvalidGestureTimeAnchor { target, kind } => write!(
f,
"gesture kind {kind} on target {target} was not rebased to song time zero"
),
Self::DuplicateGesture { target, kind } => write!(
f,
"song code repeats gesture kind {kind} on target {target}"
),
Self::DuplicateGestureRecord => write!(f, "song code repeats the gesture record"),
Self::InvalidMidiRows(rows) => {
write!(f, "song code has unknown MIDI row bits {rows:#04x}")
}
Self::DuplicateMidiRowsRecord => write!(f, "song code repeats the MIDI rows record"),
Self::InvalidPadRhythmRows(rows) => {
write!(f, "song code has unknown Pad rhythm row bits {rows:#04x}")
}
Self::DuplicatePadRhythmRowsRecord => {
write!(f, "song code repeats the Pad rhythm rows record")
}
Self::RetiredControl(id) => write!(
f,
"song code sets {id}, a control this build no longer has; the code predates the \
change that retired it and can no longer be loaded"
),
Self::NegativeLfoStep(id) => write!(
f,
"song code gives the LFO on {id} a negative step; steps now run 0 to 100% and \
the code can no longer be loaded"
),
Self::StaleRange(id) => write!(
f,
"song code modulates {id}, whose dial range changed after the code was saved; \
the code predates that change and can no longer be loaded"
),
Self::UnknownValue { id, value } => write!(
f,
"song code sets {id} to {value}, which this build does not have; the code is \
probably from a newer nooise"
),
Self::NotATableValue(id) => {
write!(f, "song code gives {id} a value that is not a whole number")
}
Self::UnregisteredControl(id) => {
write!(f, "control {id} is missing from the song id table")
}
}
}
}
impl Error for SongCodeError {}
pub(crate) fn encode_song_code(song: &SongState) -> Result<String, SongCodeError> {
encode_song_code_at_epoch(song, CURRENT_RANGE_EPOCH)
}
pub(crate) fn encode_song_code_at_epoch(
song: &SongState,
epoch: u16,
) -> Result<String, SongCodeError> {
let mut bytes = Vec::new();
bytes.extend_from_slice(MAGIC);
bytes.push(CONTAINER_VERSION);
write_record(RANGE_EPOCH_RECORD, &epoch.to_le_bytes(), &mut bytes)?;
let mut snapshot = Vec::new();
write_snapshot(&song.controls, &mut snapshot)?;
write_record(SNAPSHOT_RECORD, &snapshot, &mut bytes)?;
if automation_has_content(&song.automation) {
let mut automation = Vec::new();
write_automation(&song.automation, &mut automation)?;
write_record(AUTOMATION_RECORD, &automation, &mut bytes)?;
}
let mut bypass = Vec::new();
for (address, lfos, envelopes) in song.automation.bypass_masks() {
write_control_index(address.id(), &mut bypass)?;
bypass.extend_from_slice(&[lfos, envelopes]);
}
if !bypass.is_empty() {
write_record(LANE_BYPASS_RECORD, &bypass, &mut bytes)?;
}
if !song.automation.captures.is_empty() {
let mut captures = Vec::new();
if song.automation.captures.len() > super::MAX_CAPTURES {
return Err(SongCodeError::InvalidCapture);
}
captures.push(CAPTURE_WIRE_VERSION);
captures.push(song.automation.captures.len() as u8);
for (address, clip) in &song.automation.captures {
validate_capture(clip)?;
if !super::capture_eligible(&address.spec().contextual(&song.controls)) {
return Err(SongCodeError::InvalidCapture);
}
let id = song_id_index(address.id())
.ok_or(SongCodeError::UnregisteredControl(address.id()))?;
captures.extend_from_slice(&id.to_le_bytes());
captures.push(u8::from(clip.enabled));
captures.extend_from_slice(&clip.origin.to_le_bytes());
captures.extend_from_slice(&clip.launch.to_le_bytes());
captures.extend_from_slice(&clip.samples);
}
write_record(CAPTURE_RECORD, &captures, &mut bytes)?;
}
if let Some(sequence) = &song.tonal_sequence {
let mut tonal_sequence = Vec::new();
write_tonal_sequence(sequence, &mut tonal_sequence)?;
write_record(TONAL_SEQUENCE_RECORD, &tonal_sequence, &mut bytes)?;
}
if song.muted.iter().any(|muted| *muted) {
write_record(MUTE_RECORD, &mute_bytes(&song.muted), &mut bytes)?;
}
if song.controls.midi_rows != 0 {
write_record(MIDI_ROWS_RECORD, &[song.controls.midi_rows], &mut bytes)?;
}
if song.controls.hidden_pad_rhythm_rows != PAD_RHYTHM_ROWS {
write_record(
PAD_RHYTHM_ROWS_RECORD,
&[song.controls.hidden_pad_rhythm_rows],
&mut bytes,
)?;
}
let mut gestures = Vec::new();
if write_gestures(&song.gestures, &mut gestures)? {
write_record(GESTURE_RECORD, &gestures, &mut bytes)?;
}
Ok(format!("{CODE_PREFIX}{}", URL_SAFE_NO_PAD.encode(bytes)))
}
pub(crate) fn decode_song_code(code: &str) -> Result<SongState, SongCodeError> {
let (song, epoch) = decode_song_code_at_any_epoch(code)?;
match stale_modulation(&song, epoch) {
Some(id) => Err(SongCodeError::StaleRange(id)),
None => Ok(song),
}
}
fn decode_song_code_at_any_epoch(code: &str) -> Result<(SongState, u16), SongCodeError> {
let encoded = code
.strip_prefix(CODE_PREFIX)
.ok_or(SongCodeError::MissingPrefix)?;
let bytes = URL_SAFE_NO_PAD
.decode(encoded)
.map_err(|_| SongCodeError::InvalidBase64)?;
let mut reader = Reader::new(&bytes);
if reader.bytes(MAGIC.len())? != MAGIC {
return Err(SongCodeError::InvalidMagic);
}
match reader.u8()? {
CONTAINER_VERSION => decode_container(&mut reader),
version => Err(SongCodeError::UnsupportedVersion(version)),
}
}
fn decode_container(reader: &mut Reader) -> Result<(SongState, u16), SongCodeError> {
let mut song = SongState::default();
let mut gesture_record_seen = false;
let mut midi_rows_record_seen = false;
let mut pad_rhythm_rows_record_seen = false;
let mut capture_record_seen = false;
let mut lane_bypass = None;
let mut epoch = 0;
while !reader.is_empty() {
let record_type = reader.u8()?;
let len = reader.u32()? as usize;
let payload = reader.bytes(len)?;
match record_type {
LANE_BYPASS_RECORD => {
if lane_bypass.replace(payload).is_some() {
return Err(SongCodeError::InvalidLaneBypass);
}
}
CAPTURE_RECORD => {
if capture_record_seen {
return Err(SongCodeError::InvalidCapture);
}
capture_record_seen = true;
read_captures(payload, &mut song.automation)?;
}
SNAPSHOT_RECORD => read_snapshot(payload, &mut song.controls)?,
AUTOMATION_RECORD => read_automation(payload, &mut song.automation)?,
TONAL_SEQUENCE_RECORD => song.tonal_sequence = Some(read_tonal_sequence(payload)?),
MUTE_RECORD => read_mute(payload, &mut song.muted)?,
RANGE_EPOCH_RECORD => epoch = Reader::new(payload).u16()?,
MIDI_ROWS_RECORD => {
if midi_rows_record_seen {
return Err(SongCodeError::DuplicateMidiRowsRecord);
}
midi_rows_record_seen = true;
let mut rows = Reader::new(payload);
let bits = rows.u8()?;
if !rows.is_empty() || bits & !0b00_111111 != 0 {
return Err(SongCodeError::InvalidMidiRows(bits));
}
song.controls.midi_rows = bits;
}
PAD_RHYTHM_ROWS_RECORD => {
if pad_rhythm_rows_record_seen {
return Err(SongCodeError::DuplicatePadRhythmRowsRecord);
}
pad_rhythm_rows_record_seen = true;
let mut rows = Reader::new(payload);
let bits = rows.u8()?;
if !rows.is_empty() || bits & !PAD_RHYTHM_ROWS != 0 {
return Err(SongCodeError::InvalidPadRhythmRows(bits));
}
song.controls.hidden_pad_rhythm_rows = bits;
}
GESTURE_RECORD => {
if gesture_record_seen {
return Err(SongCodeError::DuplicateGestureRecord);
}
gesture_record_seen = true;
read_gestures(payload, &mut song.gestures)?;
}
_ => {}
}
}
if let Some(payload) = lane_bypass {
read_lane_bypass(payload, &mut song)?;
}
for address in song.automation.captures.keys() {
if !super::capture_eligible(&address.spec().contextual(&song.controls))
|| (parse_module_slot_id(address.id()).is_some()
&& super::module_slot_row(address.id(), &song.controls)
.is_none_or(|(slot, _)| slot.kind().is_none()))
{
return Err(SongCodeError::InvalidCapture);
}
}
Ok((song, epoch))
}
fn read_lane_bypass(payload: &[u8], song: &mut SongState) -> Result<(), SongCodeError> {
if payload.is_empty() || !payload.len().is_multiple_of(4) {
return Err(SongCodeError::InvalidLaneBypass);
}
let mut seen = BTreeSet::new();
for entry in payload.chunks_exact(4) {
let index = u16::from_le_bytes([entry[0], entry[1]]);
reject_retired_control(index)?;
let spec = control_at(index).ok_or(SongCodeError::InvalidLaneBypass)?;
let address = ControlAddress::new(spec.id);
let [lfos, envelopes] = [entry[2], entry[3]];
if !seen.insert(address)
|| lfos | envelopes == 0
|| (lfos | envelopes) & !0x0f != 0
|| (parse_module_slot_id(spec.id).is_some()
&& super::module_slot_row(spec.id, &song.controls)
.is_none_or(|(slot, _)| slot.kind().is_none()))
{
return Err(SongCodeError::InvalidLaneBypass);
}
for (kind, mask) in [(ModKind::Lfo, lfos), (ModKind::Envelope, envelopes)] {
for lane in 0..MAX_AUTOMATION_LANES_PER_KIND {
if mask & (1 << lane) != 0
&& !song.automation.set_lane_enabled(address, kind, lane, false)
{
return Err(SongCodeError::InvalidLaneBypass);
}
}
}
}
Ok(())
}
#[cfg(test)]
mod lane_bypass_codec_tests {
use super::*;
fn source() -> SongState {
let mut song = SongState::default();
let address = ControlAddress::new("master.level");
for index in 0..4 {
song.automation.add_route(
address,
LfoRoute {
enabled: index % 2 == 0,
depth_ratio: 0.25,
..LfoRoute::default()
},
);
song.automation.add_envelope(
address,
EnvelopeRoute {
enabled: index % 2 != 0,
amount: -0.5,
..EnvelopeRoute::default()
},
);
}
song
}
#[test]
fn bypass_masks_round_trip_and_can_precede_lane_records() {
let song = source();
let code = encode_song_code(&song).unwrap();
assert!(code.len() < 2000);
let loaded = decode_song_code(&code).unwrap();
assert_eq!(
song.automation.bypass_masks().collect::<Vec<_>>(),
loaded.automation.bypass_masks().collect::<Vec<_>>()
);
let id = song_id_index("master.level").unwrap().to_le_bytes();
let payload = [id[0], id[1], 0b1010, 0b0101];
let mut lanes = Vec::new();
write_automation(&song.automation, &mut lanes).unwrap();
let reversed = code_from_records(
CONTAINER_VERSION,
&[(LANE_BYPASS_RECORD, &payload), (AUTOMATION_RECORD, &lanes)],
);
assert_eq!(
decode_song_code(&reversed)
.unwrap()
.automation
.bypass_masks()
.collect::<Vec<_>>(),
loaded.automation.bypass_masks().collect::<Vec<_>>()
);
let old = code_from_records(CONTAINER_VERSION, &[(AUTOMATION_RECORD, &lanes)]);
let old = decode_song_code(&old).unwrap();
assert!(old.automation.routes().all(|(_, lane)| lane.enabled));
assert!(old.automation.envelopes().all(|(_, lane)| lane.enabled));
assert!(old.automation.bypass_masks().next().is_none());
}
#[test]
fn malformed_bypass_masks_are_refused_instead_of_ignored() {
let id = song_id_index("master.level").unwrap().to_le_bytes();
let valid = [id[0], id[1], 1, 0];
let mut lanes = Vec::new();
write_automation(&source().automation, &mut lanes).unwrap();
for invalid in [
vec![],
vec![id[0]],
vec![id[0], id[1], 1, 0, 0],
vec![id[0], id[1], 0, 0],
vec![id[0], id[1], 16, 0],
vec![id[0], id[1], 0, 16],
vec![255, 255, 1, 0],
valid.repeat(2),
] {
let code = code_from_records(
CONTAINER_VERSION,
&[(AUTOMATION_RECORD, &lanes), (LANE_BYPASS_RECORD, &invalid)],
);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::InvalidLaneBypass),
"{invalid:?}"
);
}
let duplicate = code_from_records(
CONTAINER_VERSION,
&[
(AUTOMATION_RECORD, &lanes),
(LANE_BYPASS_RECORD, &valid),
(LANE_BYPASS_RECORD, &valid),
],
);
assert_eq!(
decode_song_code(&duplicate).err(),
Some(SongCodeError::InvalidLaneBypass)
);
let missing = code_from_records(CONTAINER_VERSION, &[(LANE_BYPASS_RECORD, &valid)]);
assert_eq!(
decode_song_code(&missing).err(),
Some(SongCodeError::InvalidLaneBypass)
);
let retired = song_id_index("pad.reverb_mix").unwrap().to_le_bytes();
let mut one_lane = AutomationState::default();
one_lane.add_route(ControlAddress::new("master.level"), LfoRoute::default());
let mut one_lane_bytes = Vec::new();
write_automation(&one_lane, &mut one_lane_bytes).unwrap();
let sparse = code_from_records(
CONTAINER_VERSION,
&[
(AUTOMATION_RECORD, &one_lane_bytes),
(LANE_BYPASS_RECORD, &[id[0], id[1], 8, 0]),
],
);
assert_eq!(
decode_song_code(&sparse).err(),
Some(SongCodeError::InvalidLaneBypass)
);
let code = code_from_records(
CONTAINER_VERSION,
&[(LANE_BYPASS_RECORD, &[retired[0], retired[1], 1, 0])],
);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::RetiredControl("pad.reverb_mix"))
);
}
}
fn validate_capture(clip: &super::CaptureClip) -> Result<(), SongCodeError> {
if !clip.origin.is_finite()
|| !(-super::CAPTURE_BEATS..=0.0).contains(&clip.origin)
|| !clip.launch.is_finite()
|| !(0.0..=4.0).contains(&clip.launch)
{
return Err(SongCodeError::InvalidCapture);
}
Ok(())
}
#[cfg(test)]
mod capture_codec_tests {
use super::*;
fn payload(id: &str) -> Vec<u8> {
let mut payload = vec![CAPTURE_WIRE_VERSION, 1];
payload.extend_from_slice(&song_id_index(id).unwrap().to_le_bytes());
payload.push(1);
payload.extend_from_slice(&0.0f64.to_le_bytes());
payload.extend_from_slice(&0.0f64.to_le_bytes());
payload.extend_from_slice(&[128; super::super::CAPTURE_SAMPLES]);
payload
}
fn decode(payload: &[u8]) -> Result<SongState, SongCodeError> {
decode_song_code(&code_from_records(
CONTAINER_VERSION,
&[
(RANGE_EPOCH_RECORD, &CURRENT_RANGE_EPOCH.to_le_bytes()),
(CAPTURE_RECORD, payload),
],
))
}
#[test]
fn capture_record_refuses_invalid_anchors_flags_duplicates_and_targets() {
let original = payload("pad.level");
assert!(decode(&original).is_ok());
let mut invalid = original.clone();
invalid[4] = 2;
assert_eq!(decode(&invalid).err(), Some(SongCodeError::InvalidCapture));
invalid = original.clone();
invalid[5..13].copy_from_slice(&f64::NAN.to_le_bytes());
assert_eq!(decode(&invalid).err(), Some(SongCodeError::InvalidCapture));
invalid = original.clone();
invalid[13..21].copy_from_slice(&5.0f64.to_le_bytes());
assert_eq!(decode(&invalid).err(), Some(SongCodeError::InvalidCapture));
invalid = original.clone();
invalid[1] = 2;
invalid.extend_from_slice(&original[2..]);
assert_eq!(decode(&invalid).err(), Some(SongCodeError::InvalidCapture));
assert_eq!(
decode(&payload("pad.type")).err(),
Some(SongCodeError::InvalidCapture)
);
assert_eq!(
decode(&payload("kick.filter")).err(),
Some(SongCodeError::RetiredControl("kick.filter"))
);
assert_eq!(
decode(&original[..original.len() - 1]).err(),
Some(SongCodeError::Truncated)
);
}
#[test]
fn captured_filter_positions_obey_the_dial_range_epoch() {
let bytes = payload("bass.slot1.time");
let code = code_from_records(CONTAINER_VERSION, &[(CAPTURE_RECORD, &bytes)]);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::StaleRange("bass.slot1.time"))
);
}
#[test]
fn sixteen_bar_capture_records_refuse_instead_of_changing_duration() {
let mut old = payload("pad.level");
old[0] = 2;
assert_eq!(decode(&old).err(), Some(SongCodeError::InvalidCapture));
}
}
fn read_captures(payload: &[u8], automation: &mut AutomationState) -> Result<(), SongCodeError> {
let mut reader = Reader::new(payload);
if reader.u8()? != CAPTURE_WIRE_VERSION {
return Err(SongCodeError::InvalidCapture);
}
let count = reader.u8()? as usize;
if count > super::MAX_CAPTURES {
return Err(SongCodeError::InvalidCapture);
}
let mut seen = BTreeSet::new();
for _ in 0..count {
let index = reader.u16()?;
if !seen.insert(index) {
return Err(SongCodeError::InvalidCapture);
}
let enabled = match reader.u8()? {
0 => false,
1 => true,
_ => return Err(SongCodeError::InvalidCapture),
};
let mut anchor = [0; 8];
anchor.copy_from_slice(reader.bytes(8)?);
let origin = f64::from_le_bytes(anchor);
anchor.copy_from_slice(reader.bytes(8)?);
let launch = f64::from_le_bytes(anchor);
let mut samples = [0; super::CAPTURE_SAMPLES];
samples.copy_from_slice(reader.bytes(super::CAPTURE_SAMPLES)?);
let clip = super::CaptureClip {
samples,
origin,
launch,
enabled,
};
validate_capture(&clip)?;
if let Some(id) = song_id_at(index) {
if spec_by_id(id).is_none() {
return Err(SongCodeError::RetiredControl(id));
}
automation.captures.insert(ControlAddress::new(id), clip);
}
}
if !reader.is_empty() {
return Err(SongCodeError::InvalidCapture);
}
Ok(())
}
fn write_gestures(gestures: &GestureState, out: &mut Vec<u8>) -> Result<bool, SongCodeError> {
let target = u8::try_from(Tab::Master.mute_bit()).map_err(|_| SongCodeError::TooLarge)?;
let mut entries = Vec::new();
for kind in GestureKind::ALL {
let envelope = gestures.envelope(kind);
if !envelope.amount.is_finite() || !(0.0..=1.0).contains(&envelope.amount) {
return Err(SongCodeError::InvalidGestureAmount);
}
if !envelope.held && envelope.amount == 0.0 {
continue;
}
if envelope.at_seconds != 0.0 {
return Err(SongCodeError::InvalidGestureTimeAnchor {
target,
kind: kind.wire_tag(),
});
}
entries.push((
kind.wire_tag(),
u8::from(envelope.held) * GESTURE_HELD_FLAG,
unit_to_u16(envelope.amount),
));
}
if entries.is_empty() {
return Ok(false);
}
let count = u8::try_from(entries.len()).map_err(|_| SongCodeError::TooLarge)?;
out.push(count);
for (kind, flags, amount) in entries {
out.extend_from_slice(&[target, kind, flags]);
out.extend_from_slice(&amount.to_le_bytes());
}
Ok(true)
}
fn read_gestures(bytes: &[u8], gestures: &mut GestureState) -> Result<(), SongCodeError> {
let mut reader = Reader::new(bytes);
let count = reader.u8()?;
if count as usize > GESTURE_COUNT {
return Err(SongCodeError::InvalidGestureCount(count));
}
let expected_len = 1 + count as usize * 5;
if bytes.len() != expected_len {
return Err(SongCodeError::Truncated);
}
let mut seen: BTreeSet<u8> = GestureKind::ALL
.into_iter()
.filter(|kind| {
let envelope = gestures.envelope(*kind);
envelope.held || envelope.amount > 0.0
})
.map(GestureKind::wire_tag)
.collect();
for _ in 0..count {
let target = reader.u8()?;
match Tab::all()
.into_iter()
.find(|tab| tab.mute_bit() == target as usize)
{
Some(Tab::Master) => {}
Some(_) => return Err(SongCodeError::RetiredControl("per-layer gesture")),
None => return Err(SongCodeError::InvalidGestureTarget(target)),
}
let kind_tag = reader.u8()?;
if kind_tag == GestureKind::RETIRED_THIN_WIRE_TAG {
return Err(SongCodeError::RetiredControl("thin gesture"));
}
let kind = GestureKind::from_wire_tag(kind_tag)
.ok_or(SongCodeError::InvalidGestureKind(kind_tag))?;
let flags = reader.u8()?;
if flags & !GESTURE_HELD_FLAG != 0 {
return Err(SongCodeError::InvalidGestureFlags(flags));
}
if !seen.insert(kind_tag) {
return Err(SongCodeError::DuplicateGesture {
target,
kind: kind_tag,
});
}
let held = flags & GESTURE_HELD_FLAG != 0;
let amount = u16_to_unit(reader.u16()?);
gestures.lanes[kind as usize] = GestureEnvelope {
amount,
at_seconds: 0.0,
held,
restored: held,
};
}
if !reader.is_empty() {
return Err(SongCodeError::Truncated);
}
Ok(())
}
fn mute_bytes(muted: &MuteState) -> Vec<u8> {
let mut bytes = vec![0u8; MUTE_BYTES];
for tab in Tab::all() {
if muted[tab as usize] {
let bit = tab.mute_bit();
bytes[bit / 8] |= 1 << (bit % 8);
}
}
bytes
}
fn read_mute(bytes: &[u8], muted: &mut MuteState) -> Result<(), SongCodeError> {
if bytes.is_empty() || bytes.len() > MUTE_BYTES {
return Err(SongCodeError::Truncated);
}
for tab in Tab::all() {
let bit = tab.mute_bit();
muted[tab as usize] = bytes
.get(bit / 8)
.is_some_and(|byte| byte & (1 << (bit % 8)) != 0);
}
Ok(())
}
fn write_tonal_sequence(
sequence: &TonalSequenceState,
out: &mut Vec<u8>,
) -> Result<(), SongCodeError> {
let note_count = u8::try_from(sequence.notes.len()).map_err(|_| SongCodeError::TooLarge)?;
out.push(sequence.phrase as u8);
out.push(note_count);
for note in &sequence.notes {
out.extend_from_slice(¬e.to_le_bytes());
}
out.extend_from_slice(&sequence.evolution_seed.to_le_bytes());
out.extend_from_slice(&sequence.evolution_count.to_le_bytes());
Ok(())
}
fn read_tonal_sequence(bytes: &[u8]) -> Result<TonalSequenceState, SongCodeError> {
let mut reader = Reader::new(bytes);
let phrase = reader.u8()? as usize;
let note_count = reader.u8()? as usize;
if phrase >= TONAL_PHRASES.len() || note_count == 0 || note_count > TONAL_MAX_LOOP_STEPS {
return Err(SongCodeError::Truncated);
}
let mut notes = Vec::with_capacity(note_count);
for _ in 0..note_count {
notes.push(reader.i32()?);
}
let evolution_seed = reader.u64()?;
let evolution_count = reader.u64()?;
if !reader.is_empty() {
return Err(SongCodeError::Truncated);
}
Ok(TonalSequenceState {
phrase,
notes,
evolution_seed,
evolution_count,
})
}
pub(crate) const VALUE_TAG_POSITION: u8 = 0;
pub(crate) const VALUE_TAG_INT: u8 = 1;
pub(crate) const VALUE_TAG_FLOAT: u8 = 2;
pub(crate) const VALUE_TAG_SMALL_INT: u8 = 3;
const BIPOLAR_SPAN: f32 = 65_534.0;
#[derive(Clone, Copy, PartialEq)]
enum EncodedValue {
Position(u16),
SmallInt(i8),
Int(i16),
Float(f32),
}
impl EncodedValue {
fn encode(spec: &ControlSpec, value: f32, c: &FluidControls) -> Self {
if let Some(values) = spec.song_values {
return Self::SmallInt(values[value as usize]);
}
if !spec.exact_in_song
&& spec.kind != ControlKind::Discrete
&& matches!(spec.step, Step::Linear(_))
{
Self::Position(unit_to_u16(spec.ratio(value, c)))
} else {
Self::exact(value)
}
}
fn exact(value: f32) -> Self {
let rounded = value.round();
if value != rounded {
return Self::Float(value);
}
if (i8::MIN as f32..=i8::MAX as f32).contains(&rounded) {
Self::SmallInt(rounded as i8)
} else if (i16::MIN as f32..=i16::MAX as f32).contains(&rounded) {
Self::Int(rounded as i16)
} else {
Self::Float(value)
}
}
fn write(self, out: &mut Vec<u8>) {
match self {
Self::Position(position) => {
out.push(VALUE_TAG_POSITION);
out.extend_from_slice(&position.to_le_bytes());
}
Self::SmallInt(value) => {
out.push(VALUE_TAG_SMALL_INT);
out.extend_from_slice(&value.to_le_bytes());
}
Self::Int(value) => {
out.push(VALUE_TAG_INT);
out.extend_from_slice(&value.to_le_bytes());
}
Self::Float(value) => {
out.push(VALUE_TAG_FLOAT);
out.extend_from_slice(&value.to_le_bytes());
}
}
}
fn read(reader: &mut Reader) -> Result<Self, SongCodeError> {
match reader.u8()? {
VALUE_TAG_POSITION => Ok(Self::Position(reader.u16()?)),
VALUE_TAG_SMALL_INT => Ok(Self::SmallInt(reader.i8()?)),
VALUE_TAG_INT => Ok(Self::Int(reader.i16()?)),
VALUE_TAG_FLOAT => Ok(Self::Float(reader.f32()?)),
tag => Err(SongCodeError::InvalidValueTag(tag)),
}
}
fn resolve(self, spec: &ControlSpec) -> Result<f32, SongCodeError> {
if let Some(values) = spec.song_values {
let stored = match self {
Self::SmallInt(value) => value as i16,
Self::Int(value) => value,
Self::Float(_) | Self::Position(_) => {
return Err(SongCodeError::NotATableValue(spec.id));
}
};
return values
.iter()
.position(|&value| value as i16 == stored)
.map(|position| position as f32)
.ok_or(SongCodeError::UnknownValue {
id: spec.id,
value: stored,
});
}
let value = match self {
Self::Position(position) => {
spec.taper
.value_at(u16_to_unit(position), spec.min, spec.max)
}
Self::SmallInt(value) => value as f32,
Self::Int(value) => value as f32,
Self::Float(value) => value,
};
Ok(value.clamp(spec.min, spec.max))
}
}
fn unit_to_u16(value: f32) -> u16 {
(value.clamp(0.0, 1.0) * u16::MAX as f32).round() as u16
}
fn u16_to_unit(value: u16) -> f32 {
value as f32 / u16::MAX as f32
}
fn bipolar_to_u16(value: f32) -> u16 {
((value.clamp(-1.0, 1.0) + 1.0) * 0.5 * BIPOLAR_SPAN).round() as u16
}
fn u16_to_bipolar(value: u16) -> f32 {
(value as f32 / BIPOLAR_SPAN).min(1.0) * 2.0 - 1.0
}
fn write_control_index(id: &'static str, out: &mut Vec<u8>) -> Result<(), SongCodeError> {
let index = song_id_index(id).ok_or(SongCodeError::UnregisteredControl(id))?;
out.extend_from_slice(&index.to_le_bytes());
Ok(())
}
fn control_at(index: u16) -> Option<&'static ControlSpec> {
song_id_at(index).and_then(spec_by_id)
}
fn write_snapshot(controls: &FluidControls, out: &mut Vec<u8>) -> Result<(), SongCodeError> {
let defaults = FluidControls::default();
let mut entries = Vec::new();
let mut seen = BTreeSet::new();
for spec in all_specs() {
if !seen.insert(spec.id) {
continue;
}
let spec = spec.contextual(controls);
let value = EncodedValue::encode(&spec, spec.quantized_value(controls), controls);
let default = EncodedValue::encode(&spec, spec.quantized_value(&defaults), controls);
if value == default {
continue;
}
let index = song_id_index(spec.id).ok_or(SongCodeError::UnregisteredControl(spec.id))?;
entries.push((index, value));
}
write_u16(entries.len(), out)?;
for (index, value) in entries {
out.extend_from_slice(&index.to_le_bytes());
value.write(out);
}
Ok(())
}
fn read_snapshot(bytes: &[u8], controls: &mut FluidControls) -> Result<(), SongCodeError> {
let mut reader = Reader::new(bytes);
let count = reader.u16()?;
let mut entries = Vec::with_capacity(count as usize);
for _ in 0..count {
let index = reader.u16()?;
let value = EncodedValue::read(&mut reader)?;
entries.push((index, value));
}
for structural in [true, false] {
for &(index, value) in &entries {
let id = song_id_at(index).unwrap_or_default();
let is_structural = parse_module_slot_id(id).is_some_and(|(_, _, field)| {
matches!(
field,
ModuleSlotField::Kind | ModuleSlotField::Clock | ModuleSlotField::RightClock
)
});
if is_structural != structural {
continue;
}
if let Some(spec) = control_at(index) {
let spec = spec.contextual(controls);
spec.apply_quantized_value(value.resolve(&spec)?, controls);
} else if !structural {
reject_retired_control(index)?;
}
}
}
Ok(())
}
fn reject_retired_control(index: u16) -> Result<(), SongCodeError> {
match song_id_at(index) {
Some(id) if spec_by_id(id).is_none() => Err(SongCodeError::RetiredControl(id)),
_ => Ok(()),
}
}
fn write_automation(automation: &AutomationState, out: &mut Vec<u8>) -> Result<(), SongCodeError> {
write_u16(automation.routes().count(), out)?;
for (address, route) in automation.routes() {
write_control_index(address.id(), out)?;
out.extend_from_slice(&route.cycle_beats.to_le_bytes());
out.extend_from_slice(&unit_to_u16(route.depth_ratio).to_le_bytes());
out.push(shape_tag(route.shape));
out.extend_from_slice(&route.phase_offset_beats.to_le_bytes());
out.extend_from_slice(&route.seed.to_le_bytes());
if route.shape == LfoShape::Steps {
out.push(route.step_count);
out.extend_from_slice(&unit_to_u16(route.step_glide).to_le_bytes());
for value in &route.steps[..route.active_step_count()] {
out.extend_from_slice(&bipolar_to_u16(*value).to_le_bytes());
}
}
}
write_u16(0, out)?;
write_u16(automation.envelopes().count(), out)?;
for (address, route) in automation.envelopes() {
write_control_index(address.id(), out)?;
out.extend_from_slice(&bipolar_to_u16(route.amount).to_le_bytes());
out.extend_from_slice(&route.attack_beats.to_le_bytes());
out.extend_from_slice(&route.decay_beats.to_le_bytes());
let (tag, param) = env_trigger_tag(route.trigger);
out.push(tag);
out.extend_from_slice(¶m.to_le_bytes());
}
write_u16(0, out)?;
Ok(())
}
fn read_automation(bytes: &[u8], automation: &mut AutomationState) -> Result<(), SongCodeError> {
let mut reader = Reader::new(bytes);
let lfo_count = reader.u16()?;
for _ in 0..lfo_count {
let index = reader.u16()?;
let cycle_beats = reader.f32()?;
let depth_ratio = u16_to_unit(reader.u16()?);
let shape_byte = reader.u8()?;
let phase_offset_beats = reader.f32()?;
let seed = reader.u32()?;
let steps = if shape_from_tag(shape_byte) == Some(LfoShape::Steps) {
let step_count = reader.u8()?;
let step_glide = u16_to_unit(reader.u16()?);
let live = (step_count as usize).clamp(1, MAX_LFO_STEPS);
let mut values = [0.0f32; MAX_LFO_STEPS];
for value in values.iter_mut().take(live) {
*value = u16_to_bipolar(reader.u16()?);
}
Some((step_count, step_glide, values))
} else {
None
};
reject_retired_control(index)?;
let (Some(spec), Some(shape)) = (control_at(index), shape_from_tag(shape_byte)) else {
continue;
};
let mut route = build_lfo_route(cycle_beats, depth_ratio, shape, phase_offset_beats, seed);
if let Some((_, _, values)) = steps
&& values.iter().any(|value| *value < 0.0)
{
return Err(SongCodeError::NegativeLfoStep(spec.id));
}
if let Some((step_count, step_glide, values)) = steps {
route.step_count = step_count.clamp(1, MAX_LFO_STEPS as u8);
route.step_glide = step_glide;
route.steps = values;
}
automation.add_route(ControlAddress::new(spec.id), route);
}
let macro_count = reader.u16()?;
if macro_count > 0 {
return Err(SongCodeError::RetiredControl("macro route"));
}
let envelope_count = reader.u16()?;
for _ in 0..envelope_count {
let index = reader.u16()?;
let amount = u16_to_bipolar(reader.u16()?);
let attack_beats = reader.f32()?;
let decay_beats = reader.f32()?;
let trigger_tag = reader.u8()?;
let trigger_param = reader.f32()?;
reject_retired_control(index)?;
let (Some(spec), Some(trigger)) = (
control_at(index),
env_trigger_from_tag(
trigger_tag,
finite_or(trigger_param, DEFAULT_ENV_TRIGGER_BEATS),
),
) else {
continue;
};
automation.add_envelope(
ControlAddress::new(spec.id),
EnvelopeRoute {
enabled: true,
amount,
attack_beats: finite_or(attack_beats, 0.0).clamp(0.0, MAX_ENV_ATTACK_BEATS),
decay_beats: finite_or(decay_beats, 0.0).clamp(0.0, MAX_ENV_DECAY_BEATS),
trigger,
},
);
}
let field_macro_count = reader.u16()?;
if field_macro_count > 0 {
return Err(SongCodeError::RetiredControl("macro route"));
}
Ok(())
}
fn automation_has_content(automation: &AutomationState) -> bool {
automation.routes().next().is_some() || automation.envelopes().next().is_some()
}
fn build_lfo_route(
cycle_beats: f32,
depth_ratio: f32,
shape: LfoShape,
phase_offset_beats: f32,
seed: u32,
) -> LfoRoute {
LfoRoute {
cycle_beats: finite_or(cycle_beats, 2.0).clamp(MIN_LFO_CYCLE_BEATS, MAX_LFO_CYCLE_BEATS),
depth_ratio: finite_or(depth_ratio, DEFAULT_LFO_DEPTH_RATIO).clamp(0.0, 1.0),
shape,
phase_offset_beats: finite_or(phase_offset_beats, 0.0).clamp(0.0, MAX_LFO_OFFSET_BEATS),
seed,
..LfoRoute::default()
}
}
fn env_trigger_tag(trigger: EnvTrigger) -> (u8, f32) {
match trigger {
EnvTrigger::EveryBeats(beats) => (ENV_TRIGGER_EVERY_BEATS, beats),
EnvTrigger::OnKick => (ENV_TRIGGER_ON_KICK, 0.0),
EnvTrigger::Once => (ENV_TRIGGER_ONCE, 0.0),
}
}
fn env_trigger_from_tag(tag: u8, param: f32) -> Option<EnvTrigger> {
match tag {
ENV_TRIGGER_EVERY_BEATS => Some(EnvTrigger::EveryBeats(param)),
ENV_TRIGGER_ON_KICK => Some(EnvTrigger::OnKick),
ENV_TRIGGER_ONCE => Some(EnvTrigger::Once),
_ => None,
}
}
fn shape_tag(shape: LfoShape) -> u8 {
LFO_SHAPE_TAGS
.iter()
.find(|(candidate, _)| *candidate == shape)
.map(|(_, tag)| *tag)
.expect("LFO_SHAPE_TAGS has a row for every shape")
}
fn shape_from_tag(tag: u8) -> Option<LfoShape> {
LFO_SHAPE_TAGS
.iter()
.find(|(_, candidate)| *candidate == tag)
.map(|(shape, _)| *shape)
}
fn finite_or(value: f32, fallback: f32) -> f32 {
if value.is_finite() { value } else { fallback }
}
fn write_record(record_type: u8, payload: &[u8], out: &mut Vec<u8>) -> Result<(), SongCodeError> {
let len = u32::try_from(payload.len()).map_err(|_| SongCodeError::TooLarge)?;
out.push(record_type);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(payload);
Ok(())
}
#[cfg(test)]
pub(crate) fn code_from_records(version: u8, records: &[(u8, &[u8])]) -> String {
let mut bytes = Vec::new();
bytes.extend_from_slice(MAGIC);
bytes.push(version);
for (record_type, payload) in records {
write_record(*record_type, payload, &mut bytes).unwrap();
}
format!("{CODE_PREFIX}{}", URL_SAFE_NO_PAD.encode(bytes))
}
#[cfg(test)]
mod midi_rows_record_tests {
use super::*;
#[test]
fn rejects_unknown_row_bits() {
let code = code_from_records(CONTAINER_VERSION, &[(MIDI_ROWS_RECORD, &[0b0100_0000])]);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::InvalidMidiRows(0b0100_0000))
);
}
#[test]
fn rejects_duplicate_row_records() {
let code = code_from_records(
CONTAINER_VERSION,
&[(MIDI_ROWS_RECORD, &[1]), (MIDI_ROWS_RECORD, &[2])],
);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::DuplicateMidiRowsRecord)
);
}
}
#[cfg(test)]
mod pad_rhythm_rows_record_tests {
use super::*;
#[test]
fn rejects_unknown_hidden_row_bits() {
let code = code_from_records(
CONTAINER_VERSION,
&[(PAD_RHYTHM_ROWS_RECORD, &[0b0000_1000])],
);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::InvalidPadRhythmRows(0b0000_1000))
);
}
}
#[cfg(test)]
pub(crate) fn snapshot_payload(controls: &FluidControls) -> Vec<u8> {
let mut snapshot = Vec::new();
write_snapshot(controls, &mut snapshot).unwrap();
snapshot
}
fn write_u16(value: usize, out: &mut Vec<u8>) -> Result<(), SongCodeError> {
let value = u16::try_from(value).map_err(|_| SongCodeError::TooLarge)?;
out.extend_from_slice(&value.to_le_bytes());
Ok(())
}
struct Reader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn is_empty(&self) -> bool {
self.pos == self.bytes.len()
}
fn bytes(&mut self, len: usize) -> Result<&'a [u8], SongCodeError> {
let end = self.pos.checked_add(len).ok_or(SongCodeError::TooLarge)?;
let Some(bytes) = self.bytes.get(self.pos..end) else {
return Err(SongCodeError::Truncated);
};
self.pos = end;
Ok(bytes)
}
fn u8(&mut self) -> Result<u8, SongCodeError> {
Ok(self.bytes(1)?[0])
}
fn read_array<const N: usize>(&mut self) -> Result<[u8; N], SongCodeError> {
let mut bytes = [0u8; N];
bytes.copy_from_slice(self.bytes(N)?);
Ok(bytes)
}
fn u16(&mut self) -> Result<u16, SongCodeError> {
Ok(u16::from_le_bytes(self.read_array()?))
}
fn u32(&mut self) -> Result<u32, SongCodeError> {
Ok(u32::from_le_bytes(self.read_array()?))
}
fn u64(&mut self) -> Result<u64, SongCodeError> {
Ok(u64::from_le_bytes(self.read_array()?))
}
fn i8(&mut self) -> Result<i8, SongCodeError> {
Ok(self.u8()? as i8)
}
fn i16(&mut self) -> Result<i16, SongCodeError> {
Ok(i16::from_le_bytes(self.read_array()?))
}
fn i32(&mut self) -> Result<i32, SongCodeError> {
Ok(i32::from_le_bytes(self.read_array()?))
}
fn f32(&mut self) -> Result<f32, SongCodeError> {
Ok(f32::from_le_bytes(self.read_array()?))
}
}
#[cfg(test)]
mod gesture_record_tests {
use super::*;
const AMOUNT_STEP: f32 = 1.0 / u16::MAX as f32;
fn code_with_gesture_payload(payload: &[u8]) -> String {
code_from_records(CONTAINER_VERSION, &[(GESTURE_RECORD, payload)])
}
fn entry(target: u8, kind: u8, flags: u8, amount: u16) -> [u8; 5] {
let amount = amount.to_le_bytes();
[target, kind, flags, amount[0], amount[1]]
}
fn payload(entries: &[[u8; 5]]) -> Vec<u8> {
let mut payload = vec![entries.len() as u8];
payload.extend(entries.iter().flatten());
payload
}
#[test]
fn inactive_gestures_add_no_song_code_payload() {
let song = SongState::default();
let snapshot = snapshot_payload(&song.controls);
let epoch = CURRENT_RANGE_EPOCH.to_le_bytes();
let expected = code_from_records(
CONTAINER_VERSION,
&[(RANGE_EPOCH_RECORD, &epoch), (SNAPSHOT_RECORD, &snapshot)],
);
assert_eq!(encode_song_code(&song).unwrap(), expected);
}
#[test]
fn gesture_kind_wire_tags_are_stable() {
assert_eq!(GestureKind::ALL.map(GestureKind::wire_tag), [0, 1, 2, 4]);
}
#[test]
fn held_gesture_round_trip_resumes_rising_from_time_zero() {
let mut song = SongState::default();
song.gestures.lanes[GestureKind::Bloom as usize] = GestureEnvelope {
amount: 0.375,
at_seconds: 0.0,
held: true,
restored: false,
};
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
let envelope = decoded.gestures.envelope(GestureKind::Bloom);
assert!((envelope.amount - 0.375).abs() <= AMOUNT_STEP);
assert_eq!(envelope.at_seconds, 0.0);
assert!(envelope.held);
assert!(envelope.restored);
assert!(envelope.amount_at(GestureKind::Bloom, 0.1) > envelope.amount);
}
#[test]
fn zero_amount_held_gesture_is_not_omitted_as_inactive() {
let mut song = SongState::default();
song.gestures.lanes[GestureKind::Lift as usize].held = true;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
let envelope = decoded.gestures.envelope(GestureKind::Lift);
assert!(envelope.held);
assert!(envelope.restored);
}
#[test]
fn returning_gesture_round_trip_resumes_returning_from_time_zero() {
let mut song = SongState::default();
song.gestures.lanes[GestureKind::Submerge as usize] = GestureEnvelope {
amount: 0.625,
at_seconds: 0.0,
held: false,
restored: false,
};
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
let envelope = decoded.gestures.envelope(GestureKind::Submerge);
assert!((envelope.amount - 0.625).abs() <= AMOUNT_STEP);
assert_eq!(envelope.at_seconds, 0.0);
assert!(!envelope.held);
assert!(!envelope.restored);
assert!(envelope.amount_at(GestureKind::Submerge, 0.1) < envelope.amount);
}
#[test]
fn gesture_record_refuses_a_retired_per_layer_target() {
let payload = payload(&[entry(
Tab::Chords.mute_bit() as u8,
GestureKind::Echo.wire_tag(),
GESTURE_HELD_FLAG,
unit_to_u16(0.5),
)]);
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::RetiredControl("per-layer gesture"))
);
}
#[test]
fn gesture_target_wire_id_uses_the_stable_mute_bit() {
let payload = payload(&[entry(
Tab::Master.mute_bit() as u8,
GestureKind::Lift.wire_tag(),
0,
unit_to_u16(0.5),
)]);
let decoded = decode_song_code(&code_with_gesture_payload(&payload)).unwrap();
assert!(decoded.gestures.envelope(GestureKind::Lift).amount > 0.0);
}
#[test]
fn gesture_record_refuses_the_retired_thin_gesture() {
let payload = payload(&[entry(
Tab::Master.mute_bit() as u8,
GestureKind::RETIRED_THIN_WIRE_TAG,
0,
unit_to_u16(0.5),
)]);
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::RetiredControl("thin gesture"))
);
}
#[test]
fn gesture_record_rejects_unknown_target() {
let payload = payload(&[entry(255, GestureKind::Bloom.wire_tag(), 0, 1)]);
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::InvalidGestureTarget(255))
);
}
#[test]
fn gesture_record_rejects_unknown_kind() {
let payload = payload(&[entry(Tab::Master.mute_bit() as u8, 255, 0, 1)]);
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::InvalidGestureKind(255))
);
}
#[test]
fn gesture_record_rejects_unknown_flags() {
let payload = payload(&[entry(
Tab::Master.mute_bit() as u8,
GestureKind::Bloom.wire_tag(),
0b10,
1,
)]);
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::InvalidGestureFlags(0b10))
);
}
#[test]
fn gesture_record_rejects_more_entries_than_fixed_storage() {
let payload = [(GESTURE_COUNT + 1) as u8];
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::InvalidGestureCount(
(GESTURE_COUNT + 1) as u8
))
);
}
#[test]
fn gesture_record_rejects_duplicate_target_and_kind() {
let duplicate = entry(
Tab::Master.mute_bit() as u8,
GestureKind::Bloom.wire_tag(),
0,
1,
);
let payload = payload(&[duplicate, duplicate]);
assert_eq!(
decode_song_code(&code_with_gesture_payload(&payload)).err(),
Some(SongCodeError::DuplicateGesture {
target: Tab::Master.mute_bit() as u8,
kind: GestureKind::Bloom.wire_tag(),
})
);
}
#[test]
fn song_code_rejects_duplicate_gesture_records() {
let payload = payload(&[]);
let code = code_from_records(
CONTAINER_VERSION,
&[(GESTURE_RECORD, &payload), (GESTURE_RECORD, &payload)],
);
assert_eq!(
decode_song_code(&code).err(),
Some(SongCodeError::DuplicateGestureRecord)
);
}
#[test]
fn gesture_record_rejects_a_truncated_entry() {
let truncated = [
1,
Tab::Master.mute_bit() as u8,
GestureKind::Bloom.wire_tag(),
];
assert_eq!(
decode_song_code(&code_with_gesture_payload(&truncated)).err(),
Some(SongCodeError::Truncated)
);
}
#[test]
fn gesture_record_rejects_trailing_bytes() {
let trailing = [0, 0];
assert_eq!(
decode_song_code(&code_with_gesture_payload(&trailing)).err(),
Some(SongCodeError::Truncated)
);
}
#[test]
fn gesture_writer_rejects_non_finite_or_out_of_range_amounts() {
for amount in [f32::NAN, f32::INFINITY, -0.1, 1.1] {
let mut song = SongState::default();
song.gestures.lanes[GestureKind::Lift as usize].amount = amount;
assert_eq!(
encode_song_code(&song).err(),
Some(SongCodeError::InvalidGestureAmount)
);
}
}
#[test]
fn gesture_writer_rejects_an_active_envelope_not_rebased_to_time_zero() {
let mut song = SongState::default();
song.gestures.lanes[GestureKind::Echo as usize] = GestureEnvelope {
amount: 0.5,
at_seconds: 42.0,
held: false,
restored: false,
};
assert_eq!(
encode_song_code(&song).err(),
Some(SongCodeError::InvalidGestureTimeAnchor {
target: Tab::Master.mute_bit() as u8,
kind: GestureKind::Echo.wire_tag(),
})
);
}
#[test]
fn maximum_gesture_record_stays_below_120_characters() {
let mut song = SongState::default();
for kind in GestureKind::ALL {
song.gestures.lanes[kind as usize].amount = 1.0;
}
let code = encode_song_code(&song).unwrap();
assert!(code.len() < 120, "max gesture code is {} chars", code.len());
}
}
#[cfg(test)]
mod retired_control_tests {
use super::*;
fn snapshot_entry(index: u16) -> Vec<u8> {
let mut snapshot = Vec::new();
write_u16(1usize, &mut snapshot).unwrap();
snapshot.extend_from_slice(&index.to_le_bytes());
EncodedValue::Position(unit_to_u16(0.35)).write(&mut snapshot);
snapshot
}
fn code_setting(id: &str) -> String {
let index = song_id_index(id).expect("id is in the table");
code_from_records(
CONTAINER_VERSION,
&[(SNAPSHOT_RECORD, &snapshot_entry(index))],
)
}
#[test]
fn a_code_setting_a_retired_control_is_refused() {
assert_eq!(
decode_song_code(&code_setting("pad.reverb_mix")).err(),
Some(SongCodeError::RetiredControl("pad.reverb_mix"))
);
}
#[test]
fn a_code_setting_a_retired_filter_control_is_refused() {
assert_eq!(
decode_song_code(&code_setting("bass.cutoff")).err(),
Some(SongCodeError::RetiredControl("bass.cutoff"))
);
assert_eq!(
decode_song_code(&code_setting("kick.filter")).err(),
Some(SongCodeError::RetiredControl("kick.filter"))
);
assert_eq!(
decode_song_code(&code_setting("clap.filter")).err(),
Some(SongCodeError::RetiredControl("clap.filter"))
);
}
#[test]
fn a_code_setting_an_id_this_build_has_never_heard_of_still_loads() {
let code = code_from_records(
CONTAINER_VERSION,
&[(SNAPSHOT_RECORD, &snapshot_entry(u16::MAX))],
);
assert!(decode_song_code(&code).is_ok());
}
}
#[cfg(test)]
mod tag_tests {
use super::*;
#[test]
fn lfo_shape_tags_cover_every_shape() {
let mut tags = BTreeSet::new();
for shape in LfoShape::ALL {
let tag = shape_tag(shape);
assert_eq!(shape_from_tag(tag), Some(shape));
assert!(tags.insert(tag), "{shape:?}: tag {tag} reused");
}
assert_eq!(tags.len(), LFO_SHAPE_TAGS.len());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_one_byte_mute_payload_from_before_lead_still_names_master() {
let mut muted = [false; TAB_COUNT];
read_mute(&[1 << 7 | 1 << 4], &mut muted).unwrap();
assert!(muted[Tab::Master as usize]);
assert!(muted[Tab::Tonal as usize]);
assert!(!muted[Tab::Lead as usize]);
assert_eq!(read_mute(&[], &mut muted), Err(SongCodeError::Truncated));
assert_eq!(
read_mute(&[0, 0, 0], &mut muted),
Err(SongCodeError::Truncated)
);
}
}
#[cfg(test)]
mod song_value_tests {
use super::super::voice::{CUSTOM_PROGRESSION_INDEX, PROGRESSIONS, progression_index};
use super::*;
fn code_with_progression(value: EncodedValue) -> String {
let mut snapshot = Vec::new();
write_u16(1usize, &mut snapshot).unwrap();
let index = song_id_index("pad.progression").expect("id is in the table");
snapshot.extend_from_slice(&index.to_le_bytes());
value.write(&mut snapshot);
code_from_records(CONTAINER_VERSION, &[(SNAPSHOT_RECORD, &snapshot)])
}
fn decoded_progression(value: EncodedValue) -> Result<usize, SongCodeError> {
decode_song_code(&code_with_progression(value))
.map(|song| progression_index(song.controls.pad.progression))
}
#[test]
fn saved_progression_values_keep_their_meaning() {
assert_eq!(
decoded_progression(EncodedValue::SmallInt(8)),
Ok(CUSTOM_PROGRESSION_INDEX)
);
for (letter, value) in ('A'..='H').zip(0..8) {
let progression = decoded_progression(EncodedValue::SmallInt(value)).unwrap();
assert_eq!(
PROGRESSIONS[progression].song_value, value,
"progression {letter}"
);
}
}
#[test]
fn every_progression_round_trips_through_its_song_value() {
for position in 0..=CUSTOM_PROGRESSION_INDEX {
let mut song = SongState::default();
song.controls.pad.progression = position as f32;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(
progression_index(decoded.controls.pad.progression),
position
);
}
}
#[test]
fn an_unknown_progression_value_is_refused() {
assert_eq!(
decoded_progression(EncodedValue::SmallInt(99)),
Err(SongCodeError::UnknownValue {
id: "pad.progression",
value: 99
})
);
assert_eq!(
decoded_progression(EncodedValue::Float(2.5)),
Err(SongCodeError::NotATableValue("pad.progression"))
);
}
#[test]
fn chord_offset_round_trips() {
let mut song = SongState::default();
song.controls.pad.chord_offset = 4.0;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.controls.pad.chord_offset, 4.0);
}
#[test]
fn pad_stab_pattern_round_trips() {
let mut song = SongState::default();
song.controls.pad.trigger = 1.0;
song.controls.pad.swing = 0.5;
song.controls.pad.gate_beats = 1.0;
song.controls.pad.steps[1] = 1.0;
song.controls.pad.steps[4] = 0.0;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.controls.pad.trigger, 1.0);
assert!((decoded.controls.pad.swing - 0.5).abs() < 0.001);
assert_eq!(decoded.controls.pad.gate_beats, 1.0);
assert_eq!(decoded.controls.pad.steps, song.controls.pad.steps);
}
#[test]
fn arp_midi_mode_and_gate_round_trip() {
let mut song = SongState::default();
song.controls.arp.midi_out = 1.0;
song.controls.arp.midi_gate_beats = 1.0;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.controls.arp.midi_out, 1.0);
assert_eq!(decoded.controls.arp.midi_gate_beats, 1.0);
}
#[test]
fn pad_and_lead_midi_controls_round_trip() {
let mut song = SongState::default();
song.controls.pad.midi_out = 0.0;
song.controls.lead.midi_out = 1.0;
song.controls.lead.midi_gate_beats = 1.5;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.controls.pad.midi_out, 0.0);
assert_eq!(decoded.controls.lead.midi_out, 1.0);
assert_eq!(decoded.controls.lead.midi_gate_beats, 1.5);
}
#[test]
fn pad_chord_note_count_round_trips() {
let mut song = SongState::default();
song.controls.pad.chord_notes = 2.0;
let decoded = decode_song_code(&encode_song_code(&song).unwrap()).unwrap();
assert_eq!(decoded.controls.pad.chord_notes, 2.0);
}
}