use super::*;
pub(crate) type MuteState = [bool; TAB_COUNT];
#[derive(Clone)]
pub(crate) struct LiveSessionSnapshot {
pub(crate) generation: u64,
pub(crate) module_topology_revision: u64,
pub(crate) automation_revision: u64,
pub(crate) controls: FluidControls,
pub(crate) automation: AutomationState,
pub(crate) tonal_sequence: TonalSequenceState,
pub(crate) muted: MuteState,
pub(crate) lead_play: LeadPlayState,
pub(crate) gestures: GestureState,
pub(crate) transport: Transport,
pub(crate) transport_restart: u64,
}
impl LiveSessionSnapshot {
pub(crate) fn from_song(song: &SongState) -> Self {
Self {
generation: 0,
module_topology_revision: 0,
automation_revision: 0,
controls: song.controls.clone(),
automation: song.automation.clone(),
muted: song.muted,
lead_play: LeadPlayState::default(),
gestures: song.gestures.restored(),
transport: Transport::Playing,
transport_restart: 0,
tonal_sequence: song.tonal_sequence.clone().unwrap_or_else(|| {
TonalSequenceState::from_phrase(wrapped_index(
song.controls.tonal.phrase,
TONAL_PHRASES.len(),
))
}),
}
}
#[cfg(test)]
pub(crate) fn from_controls(controls: FluidControls) -> Self {
Self {
generation: 0,
module_topology_revision: 0,
automation_revision: 0,
tonal_sequence: TonalSequenceState::from_phrase(wrapped_index(
controls.tonal.phrase,
TONAL_PHRASES.len(),
)),
controls,
automation: AutomationState::default(),
muted: [false; TAB_COUNT],
lead_play: LeadPlayState::default(),
gestures: GestureState::default(),
transport: Transport::Playing,
transport_restart: 0,
}
}
}
#[derive(Clone)]
pub(crate) struct LiveSession {
published: Arc<ArcSwap<LiveSessionSnapshot>>,
audio_seconds_bits: Arc<AtomicU64>,
audio_beat_bits: Arc<AtomicU64>,
}
impl LiveSession {
pub(crate) fn new(snapshot: LiveSessionSnapshot) -> Self {
Self {
published: Arc::new(ArcSwap::from_pointee(snapshot)),
audio_seconds_bits: Arc::new(AtomicU64::new(0)),
audio_beat_bits: Arc::new(AtomicU64::new(0)),
}
}
pub(crate) fn load(&self) -> Arc<LiveSessionSnapshot> {
self.published.load_full()
}
pub(crate) fn audio_seconds(&self) -> f64 {
f64::from_bits(self.audio_seconds_bits.load(Ordering::Relaxed))
}
pub(crate) fn audio_beat(&self) -> f64 {
f64::from_bits(self.audio_beat_bits.load(Ordering::Relaxed))
}
pub(crate) fn publish_audio_beat(&self, beat: f64) {
self.audio_beat_bits
.store(beat.to_bits(), Ordering::Relaxed);
}
pub(crate) fn publish_audio_seconds(&self, seconds: f64) {
self.audio_seconds_bits
.store(seconds.to_bits(), Ordering::Relaxed);
}
pub(crate) fn transact<E>(
&self,
mut edit: impl FnMut(&mut LiveSessionSnapshot) -> Result<(), E>,
) -> Result<Arc<LiveSessionSnapshot>, E> {
loop {
let current = self.published.load_full();
let mut next = current.as_ref().clone();
edit(&mut next)?;
if Tab::all().iter().any(|&tab| {
current
.controls
.modules
.for_tab(tab)
.zip(next.controls.modules.for_tab(tab))
.is_some_and(|(before, after)| {
before
.iter()
.zip(after)
.any(|(before, after)| before.kind.to_bits() != after.kind.to_bits())
})
}) {
next.module_topology_revision = current.module_topology_revision.wrapping_add(1);
next.automation.captures.retain(|address, _| {
let Some((before, _)) = module_slot_row(address.id(), ¤t.controls) else {
return true;
};
module_slot_row(address.id(), &next.controls)
.is_some_and(|(after, _)| before.kind == after.kind)
});
}
if !current.automation.same_lanes(&next.automation) {
next.automation_revision = current.automation_revision.wrapping_add(1);
}
next.generation = current.generation.wrapping_add(1);
let next = Arc::new(next);
let previous = self.published.compare_and_swap(¤t, Arc::clone(&next));
if Arc::ptr_eq(&previous, ¤t) {
return Ok(next);
}
}
}
pub(crate) fn update(
&self,
mut edit: impl FnMut(&mut LiveSessionSnapshot),
) -> Arc<LiveSessionSnapshot> {
self.transact::<std::convert::Infallible>(|snapshot| {
edit(snapshot);
Ok(())
})
.expect("infallible live-session transaction")
}
}
#[cfg(test)]
mod tests {
use std::sync::Barrier;
use std::thread;
#[test]
fn module_topology_revision_tracks_replacement_but_not_parameter_edits() {
let session =
LiveSession::new(LiveSessionSnapshot::from_controls(FluidControls::default()));
session.update(|snapshot| snapshot.controls.modules.bass[0].time = 1234.0);
assert_eq!(session.load().module_topology_revision, 0);
session.update(|snapshot| snapshot.controls.modules.bass[0] = ModuleSlot::default());
session.update(|snapshot| snapshot.controls.modules.bass[0] = preset_slot("filter", 1.0));
assert_eq!(session.load().module_topology_revision, 2);
}
use super::*;
#[test]
fn transaction_publishes_the_complete_aggregate_once() {
let session =
LiveSession::new(LiveSessionSnapshot::from_controls(FluidControls::default()));
let before = session.load();
let published = session.update(|snapshot| {
snapshot.controls.master.bpm = 91.0;
snapshot
.automation
.open_or_create(ControlAddress::new("master.bpm"));
snapshot.tonal_sequence.evolution_count = 7;
snapshot.muted[Tab::Master as usize] = true;
});
assert_eq!(published.generation, before.generation + 1);
assert_eq!(published.controls.master.bpm, 91.0);
assert!(
published
.automation
.route(ControlAddress::new("master.bpm"))
.is_some()
);
assert_eq!(published.tonal_sequence.evolution_count, 7);
assert!(published.muted[Tab::Master as usize]);
assert!(Arc::ptr_eq(&published, &session.load()));
}
#[test]
fn failed_transaction_does_not_publish() {
let session =
LiveSession::new(LiveSessionSnapshot::from_controls(FluidControls::default()));
let before = session.load();
let result = session.transact(|snapshot| {
snapshot.controls.master.bpm = 200.0;
Err::<(), _>("rejected")
});
assert!(matches!(result, Err("rejected")));
assert!(Arc::ptr_eq(&before, &session.load()));
}
#[test]
fn concurrent_writers_do_not_lose_updates_or_mix_generations() {
const WRITES: usize = 100;
let session =
LiveSession::new(LiveSessionSnapshot::from_controls(FluidControls::default()));
let barrier = Arc::new(Barrier::new(3));
let mut writers = Vec::new();
for writer in 0..2 {
let session = session.clone();
let barrier = Arc::clone(&barrier);
writers.push(thread::spawn(move || {
barrier.wait();
for _ in 0..WRITES {
session.update(|snapshot| {
if writer == 0 {
snapshot.controls.master.bpm += 1.0;
} else {
snapshot.tonal_sequence.evolution_count += 1;
}
});
}
}));
}
barrier.wait();
for writer in writers {
writer.join().unwrap();
}
let snapshot = session.load();
assert_eq!(snapshot.generation, (WRITES * 2) as u64);
assert_eq!(
snapshot.controls.master.bpm,
FluidControls::default().master.bpm + WRITES as f32
);
assert_eq!(snapshot.tonal_sequence.evolution_count, WRITES as u64);
}
}