Repair a session's sample rate and tempo map when it loads

A session file with a sample rate of 0 or a tempo of 4e9 BPM opened
fine, but Strip Silence on the first and Extract Chords on the second
never finished (one also climbed past 1 GiB). Session::sanitize, which
repairs everything else read from a file, skipped both fields.

On load, a sample rate outside what sessions accept becomes 48 kHz, and
the tempo map keeps only the tempos and meters that set_tempo and
set_meter accept, in tick order, with one of each at tick 0.
This commit is contained in:
grovebit
2026-10-12 18:40:12 +01:00
parent 6cf896e76c
commit 1617733a98
2 changed files with 53 additions and 1 deletions
+13
View File
@@ -570,6 +570,10 @@ impl Session {
if !self.edit.zoom.samples_per_px.is_finite() || self.edit.zoom.samples_per_px <= 0.0 {
self.edit.zoom.samples_per_px = 1024.0;
}
if SampleRate::new(self.sample_rate.hz()).is_err() {
self.sample_rate = SampleRate::default();
}
self.tempo.sanitize();
}
}
@@ -641,6 +645,15 @@ mod tests {
assert_eq!(c.source_offset(), 580);
}
#[test]
fn a_bad_sample_rate_or_tempo_is_repaired_on_load() {
let mut v = serde_json::to_value(Session::default()).unwrap();
v["sample_rate"] = serde_json::json!(0);
v["tempo"]["tempos"] = serde_json::json!([{"tick": 0, "bpm": 4e9}]);
let s = Session::from_json(&v.to_string()).unwrap();
assert_eq!((s.sample_rate, s.tempo), (SampleRate::HZ_48000, TempoMap::default()));
}
#[test]
fn rejects_garbage_and_future_versions() {
assert!(Session::from_json("not json").is_err());
+40 -1
View File
@@ -55,6 +55,10 @@ fn valid_bpm(bpm: f64) -> Result<f64, TimeError> {
if bpm.is_finite() && (5.0..=1000.0).contains(&bpm) { Ok(bpm) } else { Err(TimeError::Tempo(bpm)) }
}
fn valid_meter(numerator: u32, denominator: u32) -> bool {
(1..=64).contains(&numerator) && [1, 2, 4, 8, 16, 32, 64].contains(&denominator)
}
impl TempoMap {
pub fn new(bpm: f64, numerator: u32, denominator: u32) -> Result<Self, TimeError> {
let mut m = TempoMap::default();
@@ -70,6 +74,25 @@ impl TempoMap {
&self.meters
}
/// Repair a map read from a file: only valid tempos and meters, in tick order, with one of each
/// at tick 0.
pub fn sanitize(&mut self) {
self.tempos.retain(|e| e.tick >= 0 && valid_bpm(e.bpm).is_ok());
self.tempos.sort_by_key(|e| e.tick);
self.tempos.dedup_by_key(|e| e.tick);
if self.tempos.first().is_none_or(|e| e.tick > 0) {
let bpm = self.tempos.first().map_or(120.0, |e| e.bpm);
self.tempos.insert(0, TempoEvent { tick: 0, bpm });
}
self.meters.retain(|m| m.tick >= 0 && valid_meter(m.numerator, m.denominator));
self.meters.sort_by_key(|m| m.tick);
self.meters.dedup_by_key(|m| m.tick);
if self.meters.first().is_none_or(|m| m.tick > 0) {
let (numerator, denominator) = self.meters.first().map_or((4, 4), |m| (m.numerator, m.denominator));
self.meters.insert(0, MeterEvent { tick: 0, numerator, denominator });
}
}
/// Insert or replace the tempo at `tick`.
pub fn set_tempo(&mut self, tick: i64, bpm: f64) -> Result<(), TimeError> {
let bpm = valid_bpm(bpm)?;
@@ -97,7 +120,7 @@ impl TempoMap {
/// Insert or replace the meter at `tick` (snapped to the bar line at or before it).
pub fn set_meter(&mut self, tick: i64, numerator: u32, denominator: u32) -> Result<(), TimeError> {
if !(1..=64).contains(&numerator) || ![1, 2, 4, 8, 16, 32, 64].contains(&denominator) {
if !valid_meter(numerator, denominator) {
return Err(TimeError::Meter(numerator, denominator));
}
let tick = if tick <= 0 { 0 } else { self.bar_start_tick(self.bar_beat_at_tick(tick).bar) };
@@ -317,6 +340,22 @@ mod tests {
assert_eq!(m.bar_beat_at_tick(TICKS_PER_QUARTER / 2), BarBeat { bar: 1, beat: 2, tick: 0 });
}
#[test]
fn sanitize_keeps_what_the_setters_accept() {
let tempo = |tick, bpm| TempoEvent { tick, bpm };
let meter = |tick, numerator, denominator| MeterEvent { tick, numerator, denominator };
let mut m = TempoMap {
tempos: vec![tempo(960, 4e9), tempo(3840, 90.0), tempo(-5, 100.0), tempo(1920, 60.0), tempo(1920, 70.0)],
meters: vec![meter(0, 0, 4), meter(3840, 3, 4)],
};
m.sanitize();
assert_eq!(m.tempos(), [tempo(0, 60.0), tempo(1920, 60.0), tempo(3840, 90.0)]);
assert_eq!(m.meters(), [meter(0, 3, 4), meter(3840, 3, 4)]);
let mut empty = TempoMap { tempos: Vec::new(), meters: Vec::new() };
empty.sanitize();
assert_eq!(empty, TempoMap::default());
}
#[test]
fn rejects_bad_values() {
let mut m = TempoMap::default();