mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-26 11:36:51 -06:00
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:
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user