use super::*;
pub(crate) const CURRENT_RANGE_EPOCH: u16 = 2;
#[derive(Clone, Copy)]
enum RangeTarget {
Control(&'static str),
ModuleField {
family: Family,
field: ModuleSlotField,
},
}
struct RangeChange {
epoch: u16,
target: RangeTarget,
}
const RANGE_CHANGES: &[RangeChange] = &[
RangeChange {
epoch: 1,
target: RangeTarget::ModuleField {
family: Family::Filter,
field: ModuleSlotField::Time,
},
},
RangeChange {
epoch: 2,
target: RangeTarget::Control("pad.progression"),
},
];
impl RangeChange {
fn applies(&self, id: &str, controls: &FluidControls) -> bool {
match self.target {
RangeTarget::Control(target) => id == target,
RangeTarget::ModuleField { family, field } => module_slot_row(id, controls)
.is_some_and(|(slot, slot_field)| {
slot_field == field && slot.kind().is_some_and(|kind| kind.family == family)
}),
}
}
}
pub(crate) fn stale_modulation(song: &SongState, epoch: u16) -> Option<&'static str> {
let modulated = song
.automation
.routes()
.map(|(address, _)| address)
.chain(song.automation.envelopes().map(|(address, _)| address))
.chain(song.automation.captures.keys().copied());
for address in modulated {
if RANGE_CHANGES
.iter()
.any(|change| change.epoch > epoch && change.applies(address.id(), &song.controls))
{
return Some(address.id());
}
}
None
}
#[cfg(test)]
mod tests {
use super::super::song::{SongCodeError, encode_song_code_at_epoch};
use super::*;
const PRE_WIDENING_SONG_2: &str = "n1_Tk9PSQIAcwAAABgAAAAAj8IBAABUUwIAAEhhAwADAgQAAwIFAAMEBgADCAgAALieCQAAAAAKAACZmQ4AAwMQAAMFEwADAhgAAwMaAAMFHQADAx4AAwJ8AAB6lDMAAFwPNQAAcTk2AAIAAIhAlQABQwHFAAJKMPtEcQAARUUBOwAAAAMACgAAAMA_pDAAAABAP9XNKX4zAAAAeEEfBQAAAAAAmaB_xJUAAACAPy4FAAAAgD4AAAAAAAAAAAAA";
#[test]
fn an_old_code_sweeping_a_moved_dial_is_refused_by_name() {
let Err(error) = decode_song_code(PRE_WIDENING_SONG_2) else {
panic!("a stale code must not load");
};
assert_eq!(error, SongCodeError::StaleRange("perc.slot1.time"));
assert!(
error.to_string().contains("can no longer be loaded"),
"{error}"
);
}
#[test]
fn an_old_code_that_sweeps_no_moved_dial_loads_untouched() {
let song = decode_song_code("n1_Tk9PSQIADgAAAAIAlQAB5gPFAAJKMPtE")
.expect("nothing it modulates has moved");
let current = &decode_auto_states()[0];
for spec in all_specs().filter(|spec| spec.id != "clap.slot1.amount") {
assert_eq!(
spec.quantized_value(&song.controls),
spec.quantized_value(¤t.controls),
"{}",
spec.id
);
}
}
#[test]
fn an_old_code_sweeping_the_progression_dial_is_refused() {
let mut song = SongState::default();
song.automation
.open_or_create(ControlAddress::new("pad.progression"))
.depth_ratio = 0.5;
let stale = encode_song_code_at_epoch(&song, 1).unwrap();
assert_eq!(
decode_song_code(&stale).err(),
Some(SongCodeError::StaleRange("pad.progression"))
);
assert!(decode_song_code(&encode_song_code(&song).unwrap()).is_ok());
}
#[test]
fn no_built_in_song_modulates_the_progression() {
for song in decode_auto_states() {
assert_eq!(stale_modulation(&song, 1), None);
}
}
#[test]
fn current_range_epoch_is_the_newest_change() {
assert_eq!(
RANGE_CHANGES.last().map(|change| change.epoch),
Some(CURRENT_RANGE_EPOCH)
);
assert!(
RANGE_CHANGES
.windows(2)
.all(|pair| pair[1].epoch == pair[0].epoch + 1),
"epochs are consecutive and append-only"
);
}
}