mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 10:36:46 -06:00
Merge pull request #653 from marcoarnulfo/fix/plugin-edits-dirty
Plugin-only editor changes mark the session modified
This commit is contained in:
@@ -340,7 +340,6 @@ impl PluginEditor for ClapEditor {
|
||||
|
||||
fn idle(&mut self) -> Vec<(String, f32)> {
|
||||
let raw = self.link.idle();
|
||||
let _ = self.link.host().dirty.swap(false, Ordering::Relaxed);
|
||||
let mut out: Vec<(String, f32)> = Vec::new();
|
||||
for (cid, v) in raw {
|
||||
let Some(i) = self.ids.iter().position(|&x| x == cid) else { continue };
|
||||
@@ -354,4 +353,8 @@ impl PluginEditor for ClapEditor {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn state_changed(&mut self) -> bool {
|
||||
self.link.host().dirty.swap(false, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,11 @@ pub trait PluginEditor: Send {
|
||||
/// user closed, and returns parameter edits made in the editor as `(param id, value)` in
|
||||
/// SoundCraft units (ids and ranges of [`PluginInfo::params`]).
|
||||
fn idle(&mut self) -> Vec<(String, f32)>;
|
||||
/// True once after the plugin said its state changed in a way its parameters don't show
|
||||
/// (CLAP `state.mark_dirty`), so the session stores the new state.
|
||||
fn state_changed(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Tells the editor that a parameter changed elsewhere (generic editor, automation), so the
|
||||
/// GUI shows it. Values are in SoundCraft units.
|
||||
fn set_param(&mut self, _id: &str, _value: f32) {}
|
||||
|
||||
@@ -48,6 +48,15 @@ fn insert_state(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
Some(Value::String(s)) => Some(b64::decode(s).ok_or_else(|| bad(CMD, "`state` is not valid base64 (or is too large)"))?),
|
||||
Some(_) => return Err(bad(CMD, "`state` must be a base64 string or null")),
|
||||
};
|
||||
let n = state.as_ref().map_or(0, Vec::len);
|
||||
// Plugins report changes that leave their state as it was: don't modify the session for those.
|
||||
let stored = e.session().track(t).and_then(|tr| match slot {
|
||||
Some(i) => tr.mixer.inserts.get(i).and_then(Option::as_ref),
|
||||
None => tr.instrument.as_ref(),
|
||||
});
|
||||
if stored.is_some_and(|ins| ins.state_bytes().as_deref() == state.as_deref().filter(|b| !b.is_empty())) {
|
||||
return Ok(json!({"bytes": n}));
|
||||
}
|
||||
let s = e.session_mut();
|
||||
let tr = s.track_mut(t).ok_or_else(|| bad(CMD, "no track"))?;
|
||||
let ins = match slot {
|
||||
@@ -55,7 +64,6 @@ fn insert_state(e: &mut Engine, p: &Value) -> Result<Value> {
|
||||
None => tr.instrument.as_mut(),
|
||||
}
|
||||
.ok_or_else(|| bad(CMD, "empty slot"))?;
|
||||
let n = state.as_ref().map_or(0, Vec::len);
|
||||
match state {
|
||||
Some(bytes) => ins.set_state_bytes(&bytes),
|
||||
None => ins.state = None,
|
||||
@@ -97,6 +105,19 @@ mod tests {
|
||||
assert_eq!(e.session().tracks[0].mixer.inserts[2].as_ref().unwrap().state, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storing_the_same_state_again_changes_nothing() {
|
||||
let mut e = Engine::default();
|
||||
e.execute("track.new", &json!({})).unwrap();
|
||||
let t = e.session().tracks[0].id.0;
|
||||
e.execute("mix.insert", &json!({"track": t, "plugin": "gain", "slot": 0})).unwrap();
|
||||
let blob = b64::encode(b"same");
|
||||
e.execute("mix.insert_state", &json!({"track": t, "slot": 0, "state": blob})).unwrap();
|
||||
let rev = e.revision;
|
||||
e.execute("mix.insert_state", &json!({"track": t, "slot": 0, "state": blob})).unwrap();
|
||||
assert_eq!(e.revision, rev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_params_are_errors() {
|
||||
let mut e = Engine::default();
|
||||
|
||||
@@ -394,6 +394,11 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// The gesture `execute_merged` is continuing, if any.
|
||||
pub fn merge_key(&self) -> Option<&str> {
|
||||
self.merge_key.as_deref()
|
||||
}
|
||||
|
||||
/// End a gesture started with `execute_merged`.
|
||||
pub fn end_merge(&mut self) {
|
||||
self.merge_key = None;
|
||||
|
||||
@@ -685,6 +685,17 @@ impl Player {
|
||||
self.side().capture_states()
|
||||
}
|
||||
|
||||
/// The plugins that said their state changed since the last call, so the session can keep
|
||||
/// changes made only in plugin editors.
|
||||
pub fn changed_states(&self) -> Vec<(TrackId, usize)> {
|
||||
self.side().changed_states()
|
||||
}
|
||||
|
||||
/// Reads the state of the plugins in `slots`.
|
||||
pub fn capture_states_of(&self, slots: &[(TrackId, usize)]) -> PluginStates {
|
||||
self.side().capture_states_of(slots)
|
||||
}
|
||||
|
||||
/// Per-UI-frame housekeeping: drops retired instances and services plugin editors. Returns
|
||||
/// the parameter edits made in open editors since the last call.
|
||||
pub fn idle(&self) -> Vec<EditorEdit> {
|
||||
@@ -813,6 +824,24 @@ impl Side {
|
||||
self.states.iter_mut().filter(|(k, _)| !self.refused.contains(*k)).filter_map(|(&(t, slot), r)| Some((t, slot, r.save_state()?))).collect()
|
||||
}
|
||||
|
||||
fn changed_states(&mut self) -> Vec<(TrackId, usize)> {
|
||||
self.editors.iter_mut().filter_map(|(k, e)| e.state_changed().then_some(*k)).collect()
|
||||
}
|
||||
|
||||
fn capture_states_of(&mut self, slots: &[(TrackId, usize)]) -> PluginStates {
|
||||
self.states
|
||||
.iter_mut()
|
||||
.filter(|(k, _)| !self.refused.contains(*k) && slots.contains(k))
|
||||
.filter_map(|(&(t, slot), r)| Some((t, slot, r.save_state()?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A plugin that marks its state changed while it loads or restores it hasn't changed.
|
||||
fn add_editor(&mut self, key: (TrackId, usize), mut e: Box<dyn PluginEditor>) {
|
||||
let _ = e.state_changed();
|
||||
self.editors.insert(key, e);
|
||||
}
|
||||
|
||||
fn open_editor(&mut self, key: (TrackId, usize)) -> Result<(), String> {
|
||||
let e = self.editors.get_mut(&key).ok_or_else(|| "this plugin has no editor (or it is not loaded)".to_string())?;
|
||||
e.open()?;
|
||||
@@ -849,7 +878,7 @@ impl Side {
|
||||
self.shadow.insert(key, (spec.id.clone(), spec.ch, made.is_some()));
|
||||
if let Some(soundcraft_mix::NewInstance { mut plugin, refused_state }) = made {
|
||||
if let Some(e) = plugin.editor() {
|
||||
self.editors.insert(key, e);
|
||||
self.add_editor(key, e);
|
||||
}
|
||||
if let Some(r) = plugin.state_reader() {
|
||||
self.states.insert(key, r);
|
||||
@@ -1391,6 +1420,55 @@ mod tests {
|
||||
assert_eq!(side.capture_states(), vec![(TrackId(1), 0, b"defaults".to_vec())]);
|
||||
}
|
||||
|
||||
/// An editor whose plugin said its state changed (CLAP `state.mark_dirty`).
|
||||
struct Dirty(bool);
|
||||
|
||||
impl PluginEditor for Dirty {
|
||||
fn open(&mut self) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
fn close(&mut self) {}
|
||||
fn is_open(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn idle(&mut self) -> Vec<(String, f32)> {
|
||||
Vec::new()
|
||||
}
|
||||
fn state_changed(&mut self) -> bool {
|
||||
std::mem::take(&mut self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_states_are_reported_once_and_read_for_the_session() {
|
||||
let mut side = Side::new(48_000.0, channel().1);
|
||||
let (a, b) = (TrackId(1), TrackId(2));
|
||||
let synth = (a, soundcraft_mix::INSTRUMENT_SLOT);
|
||||
side.states.insert((a, 0), Box::new(|| Some(b"a0".to_vec())));
|
||||
side.states.insert((a, 1), Box::new(|| Some(b"a1".to_vec())));
|
||||
side.states.insert(synth, Box::new(|| Some(b"synth".to_vec())));
|
||||
side.states.insert((b, 0), Box::new(|| Some(b"b0".to_vec())));
|
||||
side.editors.insert((a, 0), Box::new(Dirty(true)));
|
||||
side.editors.insert((a, 1), Box::new(Dirty(false)));
|
||||
side.editors.insert((b, 0), Box::new(Dirty(true)));
|
||||
side.refused.insert((b, 0));
|
||||
let mut changed = side.changed_states();
|
||||
changed.sort();
|
||||
assert_eq!(changed, vec![(a, 0), (b, 0)]);
|
||||
assert!(side.changed_states().is_empty(), "each change is reported once");
|
||||
let want = vec![(a, 0, b"a0".to_vec()), (a, soundcraft_mix::INSTRUMENT_SLOT, b"synth".to_vec())];
|
||||
assert_eq!(side.capture_states_of(&[(a, 0), synth, (b, 0)]), want);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_state_marked_changed_while_loading_is_not_a_change() {
|
||||
let mut side = Side::new(48_000.0, channel().1);
|
||||
let k = (TrackId(1), 0);
|
||||
side.states.insert(k, Box::new(|| Some(b"loaded".to_vec())));
|
||||
side.add_editor(k, Box::new(Dirty(true)));
|
||||
assert!(side.changed_states().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn side_creates_each_missing_instance_once() {
|
||||
use soundcraft_model::{ChannelFormat, Insert, TrackKind};
|
||||
|
||||
+221
-7
@@ -323,6 +323,8 @@ pub struct SoundApp {
|
||||
touched: std::collections::HashSet<TrackId>,
|
||||
/// The pass's last point; `i64::MIN` after a stop or a jump, so the next point erases nothing it skipped.
|
||||
last_write_at: Samples,
|
||||
/// Plugins changed in their editors whose state the session hasn't taken yet (see `editor_states_due`).
|
||||
editor_states: Vec<(TrackId, usize)>,
|
||||
pub frame_ms: f32,
|
||||
last_frame: Option<f64>,
|
||||
last_meter_poll: f64,
|
||||
@@ -336,6 +338,29 @@ pub struct SoundApp {
|
||||
_palette_lock: theme::TestPaletteGuard,
|
||||
}
|
||||
|
||||
/// The plugins whose state the session takes this frame, out of those changed in their editors
|
||||
/// (`pending`, kept across frames). A plugin editor doesn't say when a gesture ends: a plugin's
|
||||
/// gesture lasts while its editor reports a change every frame, and its state is read on the first
|
||||
/// frame its editor reports none, or as soon as the editor is closed. A drag reads and stores the
|
||||
/// state once, not every frame, and a plugin that keeps changing doesn't hold back the others.
|
||||
fn editor_states_due(
|
||||
pending: &mut Vec<(TrackId, usize)>,
|
||||
edits: &[soundcraft_playback::EditorEdit],
|
||||
changed: Vec<(TrackId, usize)>,
|
||||
closed: impl Fn((TrackId, usize)) -> bool,
|
||||
) -> Vec<(TrackId, usize)> {
|
||||
let instruments = edits.iter().filter(|e| e.1 == soundcraft_mix::INSTRUMENT_SLOT).map(|e| (e.0, e.1));
|
||||
let reported: Vec<(TrackId, usize)> = instruments.chain(changed).collect();
|
||||
for k in &reported {
|
||||
if !pending.contains(k) {
|
||||
pending.push(*k);
|
||||
}
|
||||
}
|
||||
let (due, rest) = std::mem::take(pending).into_iter().partition(|&k| !reported.contains(&k) || closed(k));
|
||||
*pending = rest;
|
||||
due
|
||||
}
|
||||
|
||||
/// Whether another automation-write pass is due: the transport moved at least
|
||||
/// `step` since `last` in either direction. `last == i64::MIN` marks "never
|
||||
/// written", so the first pass after (re)start is always due. Saturating
|
||||
@@ -400,6 +425,7 @@ impl SoundApp {
|
||||
arrange_request: false,
|
||||
touched: std::collections::HashSet::new(),
|
||||
last_write_at: i64::MIN,
|
||||
editor_states: Vec::new(),
|
||||
frame_ms: 0.0,
|
||||
last_frame: None,
|
||||
last_meter_poll: f64::NEG_INFINITY,
|
||||
@@ -613,10 +639,21 @@ impl SoundApp {
|
||||
/// (`mix.insert_state`), so saves and bounces include it.
|
||||
pub fn capture_plugin_states(&mut self) {
|
||||
let Some(p) = &self.player else { return };
|
||||
for (t, slot, data) in p.capture_states() {
|
||||
let states = p.capture_states();
|
||||
self.store_plugin_states(states);
|
||||
}
|
||||
|
||||
fn store_plugin_states(&mut self, states: soundcraft_playback::PluginStates) {
|
||||
for (t, slot, data) in states {
|
||||
let slot = if slot == soundcraft_mix::INSTRUMENT_SLOT { json!("instrument") } else { json!(slot) };
|
||||
let state = soundcraft_model::b64::encode(&data);
|
||||
if let Err(e) = self.engine.execute("mix.insert_state", &json!({"track": t.0, "slot": slot, "state": state})) {
|
||||
let params = json!({"track": t.0, "slot": slot, "state": soundcraft_model::b64::encode(&data)});
|
||||
// A state read while a knob is dragged (or a pass recorded) joins that gesture, so it
|
||||
// stays one undo step.
|
||||
let stored = match self.engine.merge_key().map(str::to_string) {
|
||||
Some(key) => self.engine.execute_merged("mix.insert_state", ¶ms, &key),
|
||||
None => self.engine.execute("mix.insert_state", ¶ms),
|
||||
};
|
||||
if let Err(e) = stored {
|
||||
log::warn!("storing plugin state: {e}");
|
||||
}
|
||||
}
|
||||
@@ -644,15 +681,36 @@ impl SoundApp {
|
||||
}
|
||||
|
||||
/// Parameter edits made in plugin editors go into the session (one undo step per gesture).
|
||||
/// Hosted instruments keep their parameters in their own state, which the session takes
|
||||
/// instead, as it does for plugins that said their state changed: either way the session is
|
||||
/// modified, so it is autosaved and asks to be saved. The state is read once per gesture (see
|
||||
/// `editor_states_due`).
|
||||
fn apply_editor_edits(&mut self) {
|
||||
let Some(p) = &self.player else { return };
|
||||
for (t, slot, param, value) in p.idle() {
|
||||
let Some(p) = &self.player else {
|
||||
self.editor_states.clear();
|
||||
return;
|
||||
};
|
||||
let edits = p.idle();
|
||||
let due = editor_states_due(&mut self.editor_states, &edits, p.changed_states(), |(t, slot)| !p.editor_open(t, slot));
|
||||
let states = if due.is_empty() { Vec::new() } else { p.capture_states_of(&due) };
|
||||
self.store_editor_changes(edits, states);
|
||||
}
|
||||
|
||||
fn store_editor_changes(&mut self, edits: Vec<soundcraft_playback::EditorEdit>, states: soundcraft_playback::PluginStates) {
|
||||
for (t, slot, param, value) in edits {
|
||||
if slot == soundcraft_mix::INSTRUMENT_SLOT {
|
||||
continue;
|
||||
}
|
||||
let key = format!("param:{}:{slot}:{param}", t.0);
|
||||
let _ = self.engine.execute_merged("mix.insert_param", &json!({"track": t.0, "slot": slot, "param": param, "value": value}), &key);
|
||||
}
|
||||
self.store_plugin_states(states);
|
||||
}
|
||||
|
||||
/// The session is modified, or a plugin editor gesture still holds a state the session hasn't
|
||||
/// taken (see `editor_states_due`): both read the plugins' states before autosaving.
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.engine.is_dirty() || !self.editor_states.is_empty()
|
||||
}
|
||||
|
||||
/// Every two minutes, copy a modified session to the autosave folder (not while recording).
|
||||
@@ -660,7 +718,7 @@ impl SoundApp {
|
||||
fn autosave(&mut self) {
|
||||
let Some(dir) = self.autosave_dir.clone() else { return };
|
||||
let last = *self.last_autosave.get_or_insert_with(std::time::Instant::now);
|
||||
if last.elapsed().as_secs() < 120 || !self.engine.is_dirty() || self.engine.transport.recording {
|
||||
if last.elapsed().as_secs() < 120 || !self.has_unsaved_changes() || self.engine.transport.recording {
|
||||
return;
|
||||
}
|
||||
self.last_autosave = Some(std::time::Instant::now());
|
||||
@@ -723,7 +781,7 @@ impl SoundApp {
|
||||
if recording {
|
||||
self.stop_play();
|
||||
}
|
||||
let keep = self.engine.is_dirty() && (recording || !self.quit_requested);
|
||||
let keep = self.has_unsaved_changes() && (recording || !self.quit_requested);
|
||||
if keep {
|
||||
self.capture_plugin_states();
|
||||
self.write_autosave();
|
||||
@@ -1331,6 +1389,7 @@ fn failed_plugin_status(s: &soundcraft_model::Session, track: TrackId, slot: usi
|
||||
mod tests {
|
||||
use super::{Services, SoundApp, automation_step_due};
|
||||
use serde_json::json;
|
||||
use soundcraft_model::TrackId;
|
||||
|
||||
#[test]
|
||||
fn new_session_stops_playback_and_the_old_clock() {
|
||||
@@ -1418,6 +1477,41 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A clean demo session with its autosave folder, and an editor gesture still open on Keys
|
||||
/// when `gesture` is set (its state hasn't reached the session yet).
|
||||
fn app_with_editor_gesture(name: &str, gesture: bool) -> (SoundApp, std::path::PathBuf) {
|
||||
let dir = std::env::temp_dir().join(format!("soundcraft-{name}-{}-{gesture}", std::process::id()));
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
app.autosave_dir = Some(dir.clone());
|
||||
if gesture {
|
||||
let keys = app.engine.session().track_by_name("Keys").unwrap().id;
|
||||
app.editor_states.push((keys, soundcraft_mix::INSTRUMENT_SLOT));
|
||||
}
|
||||
assert!(!app.engine.is_dirty());
|
||||
(app, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_open_plugin_editor_gesture_is_autosaved() {
|
||||
for gesture in [false, true] {
|
||||
let (mut app, dir) = app_with_editor_gesture("gesture-autosave", gesture);
|
||||
app.last_autosave = std::time::Instant::now().checked_sub(std::time::Duration::from_secs(121));
|
||||
app.autosave();
|
||||
assert_eq!(app.latest_autosave().is_some(), gesture);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_during_a_plugin_editor_gesture_keeps_the_autosave() {
|
||||
for gesture in [false, true] {
|
||||
let (mut app, dir) = app_with_editor_gesture("gesture-close", gesture);
|
||||
app.close_autosave();
|
||||
assert_eq!(app.latest_autosave().is_some(), gesture);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_reset_does_not_apply_old_stop_edits_to_the_new_session() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
@@ -1527,6 +1621,126 @@ mod tests {
|
||||
assert_eq!(app.engine.undo_history().len(), steps + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_states_read_during_a_drag_keep_it_one_undo_step() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let kick = app.engine.session().track_by_name("Kick").unwrap().id;
|
||||
app.run("mix.insert", json!({"track": kick.0, "plugin": "gain", "slot": 0})).unwrap();
|
||||
let steps = app.engine.undo_history().len();
|
||||
for frame in 0..60u8 {
|
||||
// What a plugin editor reports each frame of a drag, and the state the plugin marked dirty.
|
||||
let gain = -10.0 + f32::from(frame) / 10.0;
|
||||
app.engine
|
||||
.execute_merged("mix.insert_param", &json!({"track": kick.0, "slot": 0, "param": "gain", "value": gain}), "param:drag")
|
||||
.unwrap();
|
||||
app.store_plugin_states(vec![(kick, 0, vec![frame, 1])]);
|
||||
}
|
||||
assert_eq!(app.engine.undo_history().len(), steps + 1);
|
||||
assert_eq!(app.engine.session().track(kick).unwrap().mixer.inserts[0].as_ref().unwrap().state_bytes(), Some(vec![59, 1]));
|
||||
}
|
||||
|
||||
/// One UI frame of plugin editors: the instruments edited in theirs, the plugins that said
|
||||
/// their state changed, the editors closed, and the state every plugin has now. Returns the
|
||||
/// plugins whose state was read.
|
||||
fn editor_frame(app: &mut SoundApp, edited: &[TrackId], changed: &[(TrackId, usize)], closed: &[TrackId], now: u8) -> Vec<(TrackId, usize)> {
|
||||
let slot = soundcraft_mix::INSTRUMENT_SLOT;
|
||||
let edits: Vec<_> = edited.iter().map(|&t| (t, slot, "cutoff".to_string(), f32::from(now))).collect();
|
||||
let due = super::editor_states_due(&mut app.editor_states, &edits, changed.to_vec(), |(t, _)| closed.contains(&t));
|
||||
let states = due.iter().map(|&(t, slot)| (t, slot, vec![now, 1])).collect();
|
||||
app.store_editor_changes(edits, states);
|
||||
due
|
||||
}
|
||||
|
||||
fn instrument_state(app: &SoundApp, t: TrackId) -> Option<Vec<u8>> {
|
||||
app.engine.session().track(t).unwrap().instrument.as_ref().unwrap().state_bytes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instrument_editor_drag_reads_and_stores_its_state_once() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let keys = app.engine.session().track_by_name("Keys").unwrap().id;
|
||||
let mut reads = Vec::new();
|
||||
let mut revisions = Vec::new();
|
||||
for now in 0..60 {
|
||||
reads.extend(editor_frame(&mut app, &[keys], &[], &[], now));
|
||||
revisions.push(app.engine.revision);
|
||||
}
|
||||
reads.extend(editor_frame(&mut app, &[], &[], &[], 59));
|
||||
revisions.push(app.engine.revision);
|
||||
assert_eq!(reads, [(keys, soundcraft_mix::INSTRUMENT_SLOT)]);
|
||||
// The session goes to the audio thread on each new revision: once, not every frame.
|
||||
revisions.dedup();
|
||||
assert_eq!(revisions.len(), 2, "{revisions:?}");
|
||||
assert_eq!(instrument_state(&app, keys), Some(vec![59, 1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_instrument_editor_drags_store_its_state_twice() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let keys = app.engine.session().track_by_name("Keys").unwrap().id;
|
||||
let mut reads = Vec::new();
|
||||
for now in 0..10 {
|
||||
reads.extend(editor_frame(&mut app, &[keys], &[], &[], now));
|
||||
}
|
||||
reads.extend(editor_frame(&mut app, &[], &[], &[], 9));
|
||||
assert_eq!(instrument_state(&app, keys), Some(vec![9, 1]));
|
||||
for now in 20..30 {
|
||||
reads.extend(editor_frame(&mut app, &[keys], &[], &[], now));
|
||||
}
|
||||
reads.extend(editor_frame(&mut app, &[], &[], &[], 29));
|
||||
assert_eq!(reads.len(), 2);
|
||||
assert_eq!(instrument_state(&app, keys), Some(vec![29, 1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_an_instrument_editor_mid_gesture_still_stores_its_state() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let keys = app.engine.session().track_by_name("Keys").unwrap().id;
|
||||
let lead = app.engine.session().track_by_name("Lead").unwrap().id;
|
||||
let lead_before = instrument_state(&app, lead);
|
||||
for now in 0..10 {
|
||||
editor_frame(&mut app, &[keys, lead], &[], &[], now);
|
||||
}
|
||||
// Keys' editor closes on a frame it still reports a change in.
|
||||
let reads = editor_frame(&mut app, &[keys, lead], &[], &[keys], 10);
|
||||
assert_eq!(reads, [(keys, soundcraft_mix::INSTRUMENT_SLOT)]);
|
||||
assert_eq!(instrument_state(&app, keys), Some(vec![10, 1]));
|
||||
assert_eq!(instrument_state(&app, lead), lead_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plugin_that_keeps_changing_does_not_hold_back_another_ones_state() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let keys = app.engine.session().track_by_name("Keys").unwrap().id;
|
||||
let kick = app.engine.session().track_by_name("Kick").unwrap().id;
|
||||
app.run("mix.insert", json!({"track": kick.0, "plugin": "gain", "slot": 0})).unwrap();
|
||||
// A plugin on Kick marks its state changed on every frame while Keys is dragged, then
|
||||
// keeps doing it after the drag.
|
||||
for now in 0..10 {
|
||||
editor_frame(&mut app, &[keys], &[(kick, 0)], &[], now);
|
||||
}
|
||||
let reads = editor_frame(&mut app, &[], &[(kick, 0)], &[], 10);
|
||||
assert_eq!(reads, [(keys, soundcraft_mix::INSTRUMENT_SLOT)]);
|
||||
assert_eq!(instrument_state(&app, keys), Some(vec![10, 1]));
|
||||
assert_eq!(app.editor_states, [(kick, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_preset_changed_in_an_effect_editor_is_stored_once() {
|
||||
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
|
||||
let kick = app.engine.session().track_by_name("Kick").unwrap().id;
|
||||
app.run("mix.insert", json!({"track": kick.0, "plugin": "gain", "slot": 0})).unwrap();
|
||||
// No parameter edits: the plugin only says its state changed, frame after frame.
|
||||
let mut reads = Vec::new();
|
||||
for now in 0..5 {
|
||||
reads.extend(editor_frame(&mut app, &[], &[(kick, 0)], &[], now));
|
||||
}
|
||||
reads.extend(editor_frame(&mut app, &[], &[], &[], 4));
|
||||
assert_eq!(reads, [(kick, 0)]);
|
||||
let state = app.engine.session().track(kick).unwrap().mixer.inserts[0].as_ref().unwrap().state_bytes();
|
||||
assert_eq!(state, Some(vec![4, 1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferences_saved_before_the_window_was_remembered_keep_full_scale() {
|
||||
let ui: super::UiState = serde_json::from_value(json!({"show_transport": true})).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user