diff --git a/src/backend/internal/BasicLoop.cpp b/src/backend/internal/BasicLoop.cpp index b31e9d26ba..dd21cebc5d 100644 --- a/src/backend/internal/BasicLoop.cpp +++ b/src/backend/internal/BasicLoop.cpp @@ -17,7 +17,7 @@ BasicLoop::BasicLoop() : ma_triggering_now(false), ma_length(0), ma_position(0), - ma_maybe_next_planned_mode(LOOP_MODE_INVALID), + ma_maybe_next_planned_mode(LoopMode_Unknown), ma_maybe_next_planned_delay(-1), ma_already_triggered(false) { @@ -211,7 +211,7 @@ shoop_shared_ptr BasicLoop::get_sync_source(bool thread_safe) { void BasicLoop::PROC_update_planned_transition_cache() { ma_maybe_next_planned_mode = mp_planned_states.size() > 0 ? - (shoop_loop_mode_t) mp_planned_states.front() : LOOP_MODE_INVALID; + (shoop_loop_mode_t) mp_planned_states.front() : LoopMode_Unknown; ma_maybe_next_planned_delay = mp_planned_state_countdowns.size() > 0 ? mp_planned_state_countdowns.front() : -1; } @@ -261,7 +261,7 @@ void BasicLoop::PROC_handle_transition(shoop_loop_mode_t new_state) { set_length(0, false); } ma_mode = new_state; - if(ma_mode > LOOP_MODE_INVALID) { + if(ma_mode >= LOOP_MODE_INVALID) { throw_error("invalid mode"); } if (ma_mode == LoopMode_Stopped) { ma_position = 0; } @@ -415,11 +415,11 @@ shoop_loop_mode_t BasicLoop::get_mode() const { void BasicLoop::get_first_planned_transition(shoop_loop_mode_t &maybe_mode_out, uint32_t &delay_out) { shoop_loop_mode_t maybe_mode = ma_maybe_next_planned_mode; int maybe_delay = ma_maybe_next_planned_delay; - if (maybe_delay >= 0 && maybe_mode != LOOP_MODE_INVALID) { + if (maybe_delay >= 0 && maybe_mode != LoopMode_Unknown) { maybe_mode_out = maybe_mode; delay_out = maybe_delay; } else { - maybe_mode_out = LOOP_MODE_INVALID; + maybe_mode_out = LoopMode_Unknown; delay_out = 0; } } diff --git a/src/python/shoopdaloop/lib/backend_wrappers.py b/src/python/shoopdaloop/lib/backend_wrappers.py index 36bf59bbb9..bb72246bdd 100644 --- a/src/python/shoopdaloop/lib/backend_wrappers.py +++ b/src/python/shoopdaloop/lib/backend_wrappers.py @@ -159,27 +159,6 @@ def __init__(self, backend_state : 'bindings.loop_midi_channel_state_t' = None): self.n_preplay_samples = 0 @dataclass -class LoopState: - length: int - position: int - mode: Type[LoopMode] - maybe_next_mode: typing.Union[Type[LoopMode], None] - maybe_next_delay: typing.Union[int, None] - - def __init__(self, backend_loop_state : 'bindings.loop_state_t' = None): - if backend_loop_state: - self.length = to_int(backend_loop_state.length) - self.position = to_int(backend_loop_state.position) - self.mode = backend_loop_state.mode - self.maybe_next_mode = (None if backend_loop_state.maybe_next_mode == bindings.LOOP_MODE_INVALID else backend_loop_state.maybe_next_mode) - self.maybe_next_delay = (None if backend_loop_state.maybe_next_mode == bindings.LOOP_MODE_INVALID else backend_loop_state.maybe_next_mode_delay) - else: - self.length = 0 - self.position = 0 - self.mode = LoopMode.Unknown - self.maybe_next_mode = None - self.maybe_next_delay = None -@dataclass class AudioPortState: input_peak: float output_peak: float @@ -617,26 +596,16 @@ def transition_multiple(loops, to_state : Type['LoopMode'], maybe_to_sync_at_cycle : int): if len(loops) == 0: return - HandleType = POINTER(bindings.shoopdaloop_loop_t) - handles = (HandleType * len(loops))() - for idx,l in enumerate(loops): - handles[idx] = l.get_backend_obj() - bindings.loops_transition(len(loops), - handles, + loop_objs = [l._obj for l in loops] + shoop_py_backend.transition_multiple_loops( + loop_objs, to_state.value, maybe_cycles_delay, maybe_to_sync_at_cycle) - del handles + del loop_objs def get_state(self): - state = self._obj.get_state() - return LoopState( - length=state[1], - position=state[2], - mode=LoopMode(state[0]), - maybe_next_mode=LoopMode(state[3]) if state[3] is not None else None, - maybe_next_delay=state[4] - ) + return self._obj.get_state() def set_length(self, length): self._obj.set_length(length) diff --git a/src/python/shoopdaloop/lib/q_objects/Loop.py b/src/python/shoopdaloop/lib/q_objects/Loop.py index e7e0566d6d..e7a0864233 100644 --- a/src/python/shoopdaloop/lib/q_objects/Loop.py +++ b/src/python/shoopdaloop/lib/q_objects/Loop.py @@ -238,11 +238,11 @@ def updateOnOtherThread(self): prev_display_midi_events_triggered = self._display_midi_events_triggered state = self._backend_loop.get_state() - self._mode = state.mode + self._mode = int(state.mode) self._length = state.length self._position = state.position - self._next_mode = (state.maybe_next_mode if state.maybe_next_mode != None else state.mode) - self._next_transition_delay = (state.maybe_next_delay if state.maybe_next_delay != None else -1) + self._next_mode = (int(state.maybe_next_mode) if state.maybe_next_mode != None else int(state.mode)) + self._next_transition_delay = (state.maybe_next_mode_delay if state.maybe_next_mode_delay != None else -1) self._display_peaks = [c._output_peak for c in [a for a in audio_chans if a._mode in [ChannelMode.Direct.value, ChannelMode.Wet.value]]] self._display_midi_notes_active = (sum([c._n_notes_active for c in midi_chans]) if len(midi_chans) > 0 else 0) self._display_midi_events_triggered = (sum([c._n_events_triggered for c in midi_chans]) if len(midi_chans) > 0 else 0) diff --git a/src/rust/backend_bindings/src/shoop_loop.rs b/src/rust/backend_bindings/src/shoop_loop.rs index ea122d0814..64e1535a95 100644 --- a/src/rust/backend_bindings/src/shoop_loop.rs +++ b/src/rust/backend_bindings/src/shoop_loop.rs @@ -30,7 +30,7 @@ pub struct LoopState { impl LoopState { pub fn new(obj : &ffi::shoop_loop_state_info_t) -> Self { - let has_next_mode = obj.maybe_next_mode == ffi::shoop_loop_mode_t_LOOP_MODE_INVALID; + let has_next_mode = obj.maybe_next_mode != ffi::shoop_loop_mode_t_LoopMode_Unknown; return LoopState { mode : LoopMode::try_from(obj.mode).unwrap(), length : obj.length, @@ -101,32 +101,6 @@ impl Loop { Ok(()) } - pub fn transition_multiple( - loops: &[Loop], - to_state: LoopMode, - maybe_cycles_delay: i32, - maybe_to_sync_at_cycle: i32, - ) -> Result<(), anyhow::Error> { - if loops.is_empty() { - return Ok(()); - } - let handles: Vec<*mut ffi::shoopdaloop_loop_t> = loops - .iter() - .map(|l| unsafe { l.unsafe_backend_ptr() }) - .collect(); - let handles_ptr: *mut *mut ffi::shoopdaloop_loop_t = handles.as_ptr() as *mut *mut ffi::shoopdaloop_loop_t; - unsafe { - ffi::loops_transition( - handles.len() as u32, - handles_ptr, - to_state as u32, - maybe_cycles_delay, - maybe_to_sync_at_cycle, - ) - }; - Ok(()) - } - pub fn get_state(&self) -> Result { let guard = self.obj.lock().unwrap(); let obj = *guard; @@ -215,6 +189,32 @@ impl Loop { } } +pub fn transition_multiple_loops( + loops: &[&Loop], + to_state: LoopMode, + maybe_cycles_delay: i32, + maybe_to_sync_at_cycle: i32, +) -> Result<(), anyhow::Error> { + if loops.is_empty() { + return Ok(()); + } + let handles: Vec<*mut ffi::shoopdaloop_loop_t> = loops + .iter() + .map(|l| unsafe { l.unsafe_backend_ptr() }) + .collect(); + let handles_ptr: *mut *mut ffi::shoopdaloop_loop_t = handles.as_ptr() as *mut *mut ffi::shoopdaloop_loop_t; + unsafe { + ffi::loops_transition( + handles.len() as u32, + handles_ptr, + to_state as u32, + maybe_cycles_delay, + maybe_to_sync_at_cycle, + ) + }; + Ok(()) +} + impl Drop for Loop { fn drop(&mut self) { let guard = self.obj.lock().unwrap(); diff --git a/src/rust/shoopdaloop/src/shoop_py_backend/shoop_loop.rs b/src/rust/shoopdaloop/src/shoop_py_backend/shoop_loop.rs index dd406b1120..e5141486fc 100644 --- a/src/rust/shoopdaloop/src/shoop_py_backend/shoop_loop.rs +++ b/src/rust/shoopdaloop/src/shoop_py_backend/shoop_loop.rs @@ -8,6 +8,62 @@ use crate::shoop_py_backend::backend_session::BackendSession; use crate::shoop_py_backend::audio_channel::AudioChannel; use crate::shoop_py_backend::midi_channel::MidiChannel; +#[pyclass(eq, eq_int)] +#[derive(PartialEq, Clone)] +pub enum LoopMode { + Unknown = backend_bindings::LoopMode::Unknown as isize, + Stopped = backend_bindings::LoopMode::Stopped as isize, + Playing = backend_bindings::LoopMode::Playing as isize, + Recording = backend_bindings::LoopMode::Recording as isize, + Replacing = backend_bindings::LoopMode::Replacing as isize, + PlayingDryThroughWet = backend_bindings::LoopMode::PlayingDryThroughWet as isize, + RecordingDryIntoWet = backend_bindings::LoopMode::RecordingDryIntoWet as isize, +} + +impl TryFrom for LoopMode { + type Error = anyhow::Error; + fn try_from(value: backend_bindings::LoopMode) -> Result { + match value { + backend_bindings::LoopMode::Unknown => Ok(LoopMode::Unknown), + backend_bindings::LoopMode::Stopped => Ok(LoopMode::Stopped), + backend_bindings::LoopMode::Playing => Ok(LoopMode::Playing), + backend_bindings::LoopMode::Recording => Ok(LoopMode::Recording), + backend_bindings::LoopMode::Replacing => Ok(LoopMode::Replacing), + backend_bindings::LoopMode::PlayingDryThroughWet => Ok(LoopMode::PlayingDryThroughWet), + backend_bindings::LoopMode::RecordingDryIntoWet => Ok(LoopMode::RecordingDryIntoWet), + } + } +} + +#[pyclass] +pub struct LoopState { + #[pyo3(get)] + pub mode : LoopMode, + #[pyo3(get)] + pub length : u32, + #[pyo3(get)] + pub position : u32, + #[pyo3(get)] + pub maybe_next_mode : Option, + #[pyo3(get)] + pub maybe_next_mode_delay : Option, +} + +impl LoopState { + pub fn new(obj : backend_bindings::LoopState) -> Self { + return LoopState { + mode : LoopMode::try_from(obj.mode).unwrap(), + length : obj.length, + position : obj.position, + maybe_next_mode : match obj.maybe_next_mode { + Some(v) => Some (LoopMode::try_from(v).unwrap()), + None => None + }, + maybe_next_mode_delay : obj.maybe_next_mode_delay, + }; + } +} + #[pyclass] pub struct Loop { pub obj : backend_bindings::Loop, @@ -41,9 +97,63 @@ impl Loop { fn unsafe_backend_ptr (&self) -> usize { unsafe { self.obj.unsafe_backend_ptr() as usize } } + + #[pyo3(signature = (to_mode, maybe_cycles_delay=None, maybe_to_sync_at_cycle=None))] + fn transition(&self, to_mode: i32, maybe_cycles_delay: Option, maybe_to_sync_at_cycle: Option) -> PyResult<()> { + let to_mode = backend_bindings::LoopMode::try_from(to_mode) + .map_err(|_| PyErr::new::("Invalid loop mode"))?; + self.obj.transition(to_mode, maybe_cycles_delay.unwrap_or(-1), maybe_to_sync_at_cycle.unwrap_or(-1)) + .map_err(|e| PyErr::new::(format!("Transition failed: {:?}", e))) + } + + fn get_state(&self) -> PyResult { + let state = self.obj.get_state() + .map_err(|e| PyErr::new::(format!("Get state failed: {:?}", e)))?; + Ok(LoopState::new(state)) + } + + fn set_length(&self, length: u32) -> PyResult<()> { + self.obj.set_length(length) + .map_err(|e| PyErr::new::(format!("Set length failed: {:?}", e))) + } + + fn set_position(&self, position: u32) -> PyResult<()> { + self.obj.set_position(position) + .map_err(|e| PyErr::new::(format!("Set position failed: {:?}", e))) + } + + fn clear(&self, length: u32) -> PyResult<()> { + self.obj.clear(length) + .map_err(|e| PyErr::new::(format!("Clear failed: {:?}", e))) + } + + #[pyo3(signature = (loop_ref=None))] + fn set_sync_source(&self, loop_ref: Option<&Loop>) -> PyResult<()> { + self.obj.set_sync_source(loop_ref.map(|l| &l.obj)) + .map_err(|e| PyErr::new::(format!("Set sync source failed: {:?}", e))) + } + + #[pyo3(signature = (reverse_start_cycle=None, cycles_length=None, go_to_cycle=None, go_to_mode=0))] + fn adopt_ringbuffer_contents(&self, reverse_start_cycle: Option, cycles_length: Option, go_to_cycle: Option, go_to_mode: i32) -> PyResult<()> { + let go_to_mode = backend_bindings::LoopMode::try_from(go_to_mode) + .map_err(|_| PyErr::new::("Invalid loop mode"))?; + self.obj.adopt_ringbuffer_contents(reverse_start_cycle, cycles_length, go_to_cycle, go_to_mode) + .map_err(|e| PyErr::new::(format!("Adopt ringbuffer contents failed: {:?}", e))) + } } +#[pyfunction] +#[pyo3(signature = (loops, to_state=0, maybe_cycles_delay=None, maybe_to_sync_at_cycle=None))] +pub fn transition_multiple_loops(loops: Vec>, to_state: i32, maybe_cycles_delay: Option, maybe_to_sync_at_cycle: Option) -> PyResult<()> { + let to_state = backend_bindings::LoopMode::try_from(to_state) + .map_err(|_| PyErr::new::("Invalid loop mode"))?; + let rust_loops: Vec<_> = loops.iter().map(|l| &l.obj).collect(); + backend_bindings::transition_multiple_loops(&rust_loops, to_state, maybe_cycles_delay.unwrap_or(-1), maybe_to_sync_at_cycle.unwrap_or(-1)) + .map_err(|e| PyErr::new::(format!("Transition multiple failed: {:?}", e))) +} pub fn register_in_module<'py>(m: &Bound<'py, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(transition_multiple_loops, m)?)?; Ok(()) } \ No newline at end of file