diff --git a/src/backend/internal/AudioMidiDriver.h b/src/backend/internal/AudioMidiDriver.h index 16e293f25f..6024e5ddd6 100644 --- a/src/backend/internal/AudioMidiDriver.h +++ b/src/backend/internal/AudioMidiDriver.h @@ -105,7 +105,7 @@ class AudioMidiDriver : public WithCommandQueue, bool get_active() const; uint32_t get_last_processed() const; - void wait_process(); + virtual void wait_process(); virtual std::vector find_external_ports( const char* maybe_name_regex, diff --git a/src/backend/internal/ObjectPool.h b/src/backend/internal/ObjectPool.h index b7c716f196..605190a8e6 100644 --- a/src/backend/internal/ObjectPool.h +++ b/src/backend/internal/ObjectPool.h @@ -7,6 +7,13 @@ #include "LoggingEnabled.h" +#ifdef _WIN32 + #include +#elif defined(__linux__) || defined(__APPLE__) + #include +#endif + + // A class which manages a queue of audio objects which can be // consumed lock-free. The queue is continuously replenished with newly allocated // objects asynchronously. @@ -40,6 +47,19 @@ class ObjectPool : public ModuleLoggingEnabled<"Backend.ObjectPool"> { // Start auto-replenishment m_replenish_thread = std::thread( [this]() { this->replenish_thread_fn(); }); + +#ifdef _WIN32 + HANDLE handle = m_replenish_thread.native_handle(); + int winPriority = THREAD_PRIORITY_ABOVE_NORMAL; + SetThreadPriority(handle, winPriority); +#elif defined(__linux__) || defined(__APPLE__) + pthread_t handle = m_replenish_thread.native_handle(); + struct sched_param sch; + int policy; + pthread_getschedparam(handle, &policy, &sch); + sch.sched_priority = 4; // "above normal" priority + pthread_setschedparam(handle, SCHED_FIFO, &sch); +#endif } ~ObjectPool() { diff --git a/src/backend/internal/jack/JackApi.h b/src/backend/internal/jack/JackApi.h index 1e29557ece..61dcdd0bb9 100644 --- a/src/backend/internal/jack/JackApi.h +++ b/src/backend/internal/jack/JackApi.h @@ -9,6 +9,8 @@ // TODO: this does not contain the complete API, only what we use class JackApi { public: + static constexpr bool supports_processing = true; + static auto get_ports(auto ...args) { return jack_get_ports(args...); } static auto port_by_name(auto ...args) { return jack_port_by_name(args...); } static auto port_flags(auto ...args) { return jack_port_flags(args...); } diff --git a/src/backend/internal/jack/JackAudioMidiDriver.cpp b/src/backend/internal/jack/JackAudioMidiDriver.cpp index 8ecec2c956..5969317767 100644 --- a/src/backend/internal/jack/JackAudioMidiDriver.cpp +++ b/src/backend/internal/jack/JackAudioMidiDriver.cpp @@ -3,10 +3,12 @@ #include "LoggingBackend.h" #include "MidiPort.h" #include "PortInterface.h" +#include #include #include #include #include +#include #include "jack/types.h" #include "run_in_thread_with_timeout.h" #include "JackAudioPort.h" @@ -242,5 +244,14 @@ std::vector GenericJackAudioMidiDriver::find_extern return rval; } +template +void GenericJackAudioMidiDriver::wait_process() { + if (API::supports_processing) { + AudioMidiDriver::wait_process(); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + template class GenericJackAudioMidiDriver; template class GenericJackAudioMidiDriver; \ No newline at end of file diff --git a/src/backend/internal/jack/JackAudioMidiDriver.h b/src/backend/internal/jack/JackAudioMidiDriver.h index 428091a796..d7aa47064f 100644 --- a/src/backend/internal/jack/JackAudioMidiDriver.h +++ b/src/backend/internal/jack/JackAudioMidiDriver.h @@ -46,6 +46,8 @@ class GenericJackAudioMidiDriver : void maybe_update_buffer_size() override; void maybe_update_dsp_load() override; + void wait_process() override; + public: GenericJackAudioMidiDriver(); ~GenericJackAudioMidiDriver() override; diff --git a/src/backend/internal/jack/JackTestApi.h b/src/backend/internal/jack/JackTestApi.h index 42625912ec..33d5a22c90 100644 --- a/src/backend/internal/jack/JackTestApi.h +++ b/src/backend/internal/jack/JackTestApi.h @@ -19,6 +19,8 @@ namespace jacktestapi_globals { // Note that only creation is supported. Closing clients and ports will not do anything. class JackTestApi { public: + static constexpr bool supports_processing = false; + enum class Type { Audio, Midi @@ -194,6 +196,9 @@ class JackTestApi { static const char** port_get_connections(const jack_port_t* port); static int set_port_registration_callback(jack_client_t* client, JackPortRegistrationCallback cb, void* arg); + // FIXME? + // static int set_process_callback(jack_client_t* client, JackProcessCallback cb, void* arg); + static void set_error_function(void (*fn)(const char*)); static void set_info_function(void (*fn)(const char*)); diff --git a/src/backend/internal/shoop_globals.h b/src/backend/internal/shoop_globals.h index 72e5c24b17..cd3931837c 100644 --- a/src/backend/internal/shoop_globals.h +++ b/src/backend/internal/shoop_globals.h @@ -36,7 +36,7 @@ constexpr uint32_t initial_max_loops = 512; constexpr uint32_t initial_max_ports = 1024; constexpr uint32_t initial_max_fx_chains = 128; constexpr uint32_t initial_max_decoupled_midi_ports = 512; -constexpr uint32_t n_buffers_in_pool = 512; +constexpr uint32_t n_buffers_in_pool = 2048; constexpr uint32_t audio_buffer_size = 4096; constexpr uint32_t command_queue_size = 2048; constexpr uint32_t audio_channel_initial_buffers = 128; diff --git a/src/backend/libshoopdaloop_backend.cpp b/src/backend/libshoopdaloop_backend.cpp index e11a7edfe8..5c3a1035c6 100644 --- a/src/backend/libshoopdaloop_backend.cpp +++ b/src/backend/libshoopdaloop_backend.cpp @@ -369,7 +369,9 @@ shoop_external_port_descriptors_t *find_external_ports( shoop_port_direction_t maybe_port_direction_filter, shoop_port_data_type_t maybe_data_type_filter) { - return api_impl("find_external_ports", [&]() -> shoop_external_port_descriptors_t* { + std::string name_regex = maybe_name_regex ? maybe_name_regex : ""; + std::string fn_name = "find_external_ports (regex \"" + name_regex + "\")"; + return api_impl(fn_name.c_str(), [&]() -> shoop_external_port_descriptors_t* { auto _driver = internal_audio_driver(driver); if (!_driver) { return nullptr; } auto ports = _driver->find_external_ports( diff --git a/src/python/shoopdaloop/lib/q_objects/Backend.py b/src/python/shoopdaloop/lib/q_objects/Backend.py index a0e2a9b682..656c73714c 100644 --- a/src/python/shoopdaloop/lib/q_objects/Backend.py +++ b/src/python/shoopdaloop/lib/q_objects/Backend.py @@ -22,6 +22,8 @@ all_active_backend_objs = set() +# FIXME delete this file + # Wraps the back-end session + driver in a single object. class Backend(ShoopQQuickItem): def __init__(self, parent=None): diff --git a/src/python/shoopdaloop/lib/q_objects/CompositeLoop.py b/src/python/shoopdaloop/lib/q_objects/CompositeLoop.py index 6d15518d52..a6b4aad6e8 100644 --- a/src/python/shoopdaloop/lib/q_objects/CompositeLoop.py +++ b/src/python/shoopdaloop/lib/q_objects/CompositeLoop.py @@ -671,7 +671,7 @@ def do_triggers(self, iteration, mode, trigger_callback = lambda self,loop,mode: self.do_triggers(0, self.mode, trigger_callback, True) def maybe_initialize(self): - if self._backend and self._backend.initialized and not self._initialized: + if self._backend and self._backend.property('ready') and not self._initialized: self.logger.debug(lambda: 'Found backend, initializing') self.initializedChanged.emit(True) diff --git a/src/python/shoopdaloop/lib/q_objects/FXChain.py b/src/python/shoopdaloop/lib/q_objects/FXChain.py index bca9ac109e..63643a37cb 100644 --- a/src/python/shoopdaloop/lib/q_objects/FXChain.py +++ b/src/python/shoopdaloop/lib/q_objects/FXChain.py @@ -180,7 +180,7 @@ def restore_state(self, state_str): self.logger.throw_error("Restoring internal state of uninitialized FX chain") def maybe_initialize(self): - if self._backend and self._backend.initialized and self._chain_type != None and not self._backend_object: + if self._backend and self._backend.property('ready') and self._chain_type != None and not self._backend_object: self._backend_object = self._backend.get_backend_session_obj().create_fx_chain(self._chain_type, self._title) if self._backend_object: self._initialized = True diff --git a/src/qml/CompositeLoop.qml b/src/qml/CompositeLoop.qml index 0c7114f63d..62125cd578 100644 --- a/src/qml/CompositeLoop.qml +++ b/src/qml/CompositeLoop.qml @@ -42,6 +42,7 @@ Item { property alias sync_position : py_loop.sync_position property alias length : py_loop.length property alias py_loop : py_loop + property alias backend : py_loop.backend PythonCompositeLoop { id: py_loop iteration: 0 diff --git a/src/qml/LoopWidget.qml b/src/qml/LoopWidget.qml index 34bad791f8..04d6312fef 100644 --- a/src/qml/LoopWidget.qml +++ b/src/qml/LoopWidget.qml @@ -497,38 +497,11 @@ Item { } } - // function create_backend_loop_impl() { - // if (!maybe_loop) { - // if (backend_loop_factory.status == Component.Error) { - // throw new Error("BackendLoopWithChannels: Failed to load factory: " + backend_loop_factory.errorString()) - // } else if (backend_loop_factory.status != Component.Ready) { - // throw new Error("BackendLoopWithChannels: Factory not ready: " + backend_loop_factory.status.toString()) - // } else { - // let gain = last_pushed_gain - // let balance = last_pushed_stereo_balance - // maybe_loop = backend_loop_factory.createObject(root, { - // 'initial_descriptor': root.initial_descriptor, - // 'sync_source': Qt.binding(() => (!is_sync && root.sync_loop && root.sync_loop.maybe_backend_loop) ? root.sync_loop.maybe_backend_loop : null), - // }) - // push_stereo_balance(balance) - // push_gain(gain) - // maybe_loop.onCycled.connect(root.cycled) - // } - // } - // } - - // ExecuteNextCycle { - // id: create_loop_next_cycle - // onExecute: root.create_backend_loop_impl() - // } - // function create_backend_loop() { - // create_loop_next_cycle.trigger() - // } - Component { id: composite_loop_factory CompositeLoop { loop_widget: root + backend: root.backend } } function create_composite_loop(composition={ diff --git a/src/qml/ProfilingWindow.qml b/src/qml/ProfilingWindow.qml index 89148fb2b4..999a30a549 100644 --- a/src/qml/ProfilingWindow.qml +++ b/src/qml/ProfilingWindow.qml @@ -25,7 +25,7 @@ ApplicationWindow { var _data = {} var bufsize = root.backend.get_buffer_size() - var samplerate = root.backend && root.backend.initialized ? root.backend.get_sample_rate() : 1 + var samplerate = root.backend && root.backend.ready ? root.backend.get_sample_rate() : 1 cycle_us = bufsize / samplerate * 1000000.0 Object.entries(profiling_data).forEach((p) => { @@ -58,7 +58,7 @@ ApplicationWindow { function update() { var data = root.backend.get_profiling_report() var bufsize = root.backend.get_buffer_size() - var samplerate = root.backend && root.backend.initialized ? root.backend.get_sample_rate() : 1 + var samplerate = root.backend && root.backend.ready ? root.backend.get_sample_rate() : 1 cycle_us = bufsize / samplerate * 1000000.0 root.profiling_data = data diff --git a/src/qml/test/ShoopSessionTestCase.qml b/src/qml/test/ShoopSessionTestCase.qml index 8b00fad0e8..a7869cccc7 100644 --- a/src/qml/test/ShoopSessionTestCase.qml +++ b/src/qml/test/ShoopSessionTestCase.qml @@ -19,7 +19,7 @@ ShoopTestCase { when: timer.done function check_backend() { - verify(backend && backend.initialized, "backend not initialized") + verify(backend && backend.ready, "backend not initialized") wait_updated(backend) } diff --git a/src/qml/test/tst_Backend.qml b/src/qml/test/tst_Backend.qml index 261d02e7bf..ca02a637ed 100644 --- a/src/qml/test/tst_Backend.qml +++ b/src/qml/test/tst_Backend.qml @@ -19,7 +19,7 @@ ShoopTestFile { test_fns: ({ 'test_backend': () => { - verify(backend.initialized) + verify(backend.ready) backend.close() } }) diff --git a/src/qml/test/tst_Backend_jack.qml b/src/qml/test/tst_Backend_jack.qml index c92115d508..6fe57428f9 100644 --- a/src/qml/test/tst_Backend_jack.qml +++ b/src/qml/test/tst_Backend_jack.qml @@ -17,7 +17,11 @@ ShoopTestFile { ShoopTestCase { name: 'JackBackend' filename : TestFilename.test_filename() - when: backend.initialized || backend.backend_type == null + when: { + let ready = backend.ready || backend.backend_type == null + console.log("Backend ready: " + ready) + return ready + } test_fns: ({ 'test_backend_jack': () => { @@ -27,7 +31,7 @@ ShoopTestFile { return } - verify(backend.initialized) + verify(backend.ready) wait(1000) verify_eq( backend.actual_backend_type, diff --git a/src/qml/test/tst_CompositeLoop_scheduling.qml b/src/qml/test/tst_CompositeLoop_scheduling.qml index dd21cefce1..36ee299339 100644 --- a/src/qml/test/tst_CompositeLoop_scheduling.qml +++ b/src/qml/test/tst_CompositeLoop_scheduling.qml @@ -15,6 +15,10 @@ ShoopTestFile { property int n_cycles: 1 property int position: 0 + signal positionChangedUnsafe(position : int) + signal lengthChangedUnsafe(length : int) + signal cycledUnsafe() + property var maybe_loop: this signal cycled(int cycle_nr) @@ -77,6 +81,9 @@ ShoopTestFile { CompositeLoop { id: sequential_sched_lut + backend: Item { + property bool ready : true + } ShoopTestCase { name: 'CompositeLoop_sequential_sched' diff --git a/src/qml/test/tst_Jack_ports.qml b/src/qml/test/tst_Jack_ports.qml index 1dac5da23f..95d0232bbc 100644 --- a/src/qml/test/tst_Jack_ports.qml +++ b/src/qml/test/tst_Jack_ports.qml @@ -30,6 +30,7 @@ ShoopTestFile { 'min_n_ringbuffer_samples': 0 }) + backend: backend is_internal: false id: audio_in } @@ -51,6 +52,7 @@ ShoopTestFile { is_internal: false id: audio_out + backend: backend } MidiPort { descriptor: ({ @@ -69,6 +71,7 @@ ShoopTestFile { is_internal: false id: midi_in + backend: backend } MidiPort { descriptor: ({ @@ -87,6 +90,7 @@ ShoopTestFile { is_internal: false id: midi_out + backend: backend } ShoopTestCase { @@ -102,7 +106,7 @@ ShoopTestFile { skip("Backend was built without Jack support") return } - verify(backend.initialized) + verify(backend.ready) wait(100) diff --git a/src/qml/test/tst_LuaScriptWithEngine.qml b/src/qml/test/tst_LuaScriptWithEngine.qml index f448bda2a0..f12978a133 100644 --- a/src/qml/test/tst_LuaScriptWithEngine.qml +++ b/src/qml/test/tst_LuaScriptWithEngine.qml @@ -48,7 +48,7 @@ ShoopTestFile { name: 'Lua_autoconnect' session: session filename : TestFilename.test_filename() - when: backend.initialized || backend.backend_type == null + when: backend.ready || backend.backend_type == null testcase_deinit_fn: () => { backend.close() } diff --git a/src/qml/test/tst_MidiControlPort.qml b/src/qml/test/tst_MidiControlPort.qml index 1e26ea57dc..ae29ebeaef 100644 --- a/src/qml/test/tst_MidiControlPort.qml +++ b/src/qml/test/tst_MidiControlPort.qml @@ -37,7 +37,7 @@ ShoopTestFile { ShoopTestCase { name: 'MidiControlPort' filename : TestFilename.test_filename() - when: backend.initialized || backend.backend_type == null + when: backend.ready || backend.backend_type == null testcase_deinit_fn: () => { backend.close() } diff --git a/src/qml/test/tst_Session_save_load.qml b/src/qml/test/tst_Session_save_load.qml index aa8ee1db11..ea1a2c44fe 100644 --- a/src/qml/test/tst_Session_save_load.qml +++ b/src/qml/test/tst_Session_save_load.qml @@ -265,7 +265,7 @@ ShoopTestFile { "test_save_load_session_audio_and_midi_resampled": () => { check_backend() - verify(other_session.backend && other_session.backend.initialized, "resampled backend not initialized") + verify(other_session.backend && other_session.backend.ready, "resampled backend not initialized") let midichan = [ { 'time': 120, 'data': [0x90, 70, 70] }, diff --git a/src/rust/backend_bindings/src/audio_driver.rs b/src/rust/backend_bindings/src/audio_driver.rs index 12d59ecc9e..9d3022057d 100644 --- a/src/rust/backend_bindings/src/audio_driver.rs +++ b/src/rust/backend_bindings/src/audio_driver.rs @@ -234,8 +234,22 @@ impl AudioDriver { pub fn find_external_ports(&self, maybe_name_regex: Option<&str>, port_direction: u32, data_type: u32) -> Vec { let obj = self.lock(); - let regex_ptr = maybe_name_regex.map_or(std::ptr::null(), |s| s.as_ptr() as *const i8); + let maybe_name_regex_updated = match maybe_name_regex { + Some(s) => match s { + "" => None, + _ => Some(std::ffi::CString::new(s).unwrap()), + } + None => None, + }; + let regex_ptr = maybe_name_regex_updated + .as_ref() + .map_or(std::ptr::null(), |s| { + s.as_ptr() as *const i8 + }); let result = unsafe { ffi::find_external_ports(*obj, regex_ptr, port_direction as ffi::shoop_port_direction_t, data_type as ffi::shoop_port_data_type_t) }; + if result.is_null() { + return Vec::new(); + } let ports = unsafe { std::slice::from_raw_parts((*result).ports, (*result).n_ports as usize) }; let mut port_descriptors = Vec::new(); unsafe { diff --git a/src/rust/frontend/src/cxx_qt_lib_shoop/include/cxx-qt-lib-shoop/qtimer.h b/src/rust/frontend/src/cxx_qt_lib_shoop/include/cxx-qt-lib-shoop/qtimer.h index 2138b34b61..771eb766de 100644 --- a/src/rust/frontend/src/cxx_qt_lib_shoop/include/cxx-qt-lib-shoop/qtimer.h +++ b/src/rust/frontend/src/cxx_qt_lib_shoop/include/cxx-qt-lib-shoop/qtimer.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "connect.h" inline void qtimerSetSingleShot(QTimer &timer, bool singleShot) { @@ -18,6 +19,15 @@ inline void qtimerStop(QTimer &timer) { timer.stop(); } +inline void qtimerStopQueued(QTimer &timer) { + QMetaMethod method = timer.metaObject()->method(timer.metaObject()->indexOfMethod("stop()")); + method.invoke(&timer, Qt::QueuedConnection); +} + +inline bool qtimerIsActive(QTimer const& timer) { + return timer.isActive(); +} + template inline void qtimerConnectTimeout(QTimer &timer, A *receiver, ::rust::String member) { connect(&timer, "timeout()", receiver, member); diff --git a/src/rust/frontend/src/cxx_qt_lib_shoop/rust/qtimer.rs b/src/rust/frontend/src/cxx_qt_lib_shoop/rust/qtimer.rs index 4d8717eee0..d69355c511 100644 --- a/src/rust/frontend/src/cxx_qt_lib_shoop/rust/qtimer.rs +++ b/src/rust/frontend/src/cxx_qt_lib_shoop/rust/qtimer.rs @@ -24,6 +24,12 @@ mod ffi { #[rust_name = "qtimer_stop"] fn qtimerStop(timer : Pin<&mut QTimer>); + #[rust_name = "qtimer_stop_queued"] + fn qtimerStopQueued(timer : Pin<&mut QTimer>); + + #[rust_name = "qtimer_is_active"] + fn qtimerIsActive(timer : &QTimer) -> bool; + #[rust_name = "qtimer_connect_timeout"] unsafe fn qtimerConnectTimeout(timer : Pin<&mut QTimer>, receiver : *mut QObject, slot : String) -> Result<()>; @@ -68,6 +74,14 @@ impl QTimer { ffi::qtimer_stop(self); } + pub fn stop_queued(self : Pin<&mut Self>) { + ffi::qtimer_stop_queued(self); + } + + pub fn is_active(self : &Self) -> bool { + ffi::qtimer_is_active(self) + } + pub fn connect_timeout(self : Pin<&mut Self>, receiver : *mut QObject, slot : String) -> Result<(), cxx::Exception> { unsafe { ffi::qtimer_connect_timeout(self, receiver, slot) } } diff --git a/src/rust/frontend/src/cxx_qt_shoop/rust/qobj_backend_wrapper.rs b/src/rust/frontend/src/cxx_qt_shoop/rust/qobj_backend_wrapper.rs index 6b5eaa3960..22b8e90f8d 100644 --- a/src/rust/frontend/src/cxx_qt_shoop/rust/qobj_backend_wrapper.rs +++ b/src/rust/frontend/src/cxx_qt_shoop/rust/qobj_backend_wrapper.rs @@ -8,6 +8,7 @@ use crate::cxx_qt_lib_shoop::qthread::QThread; use crate::cxx_qt_lib_shoop::qtimer::QTimer; use crate::cxx_qt_shoop::qobj_signature_backend_wrapper::constants; use std::slice; +use std::time::Duration; shoop_log_unit!("Frontend.BackendWrapper"); pub use crate::cxx_qt_shoop::qobj_backend_wrapper_bridge::*; @@ -141,6 +142,7 @@ impl BackendWrapper { } { + self.as_mut().set_actual_backend_type(driver_type as i32); self.as_mut().connect_updated_on_backend_thread( |this: Pin<&mut BackendWrapper>| { this.update_on_gui_thread(); @@ -195,8 +197,43 @@ impl BackendWrapper { } } - pub fn close(self: Pin<&mut BackendWrapper>) { - error!("close unimplemented") + pub fn close(mut self: Pin<&mut BackendWrapper>) { + let closed : bool; + { + let ref_self = self.as_ref(); + closed = ref_self.rust().closed; + } + + if closed { + trace!("Already closed"); + return; + } + + debug!("Closing"); + + unsafe { + let mut rust = self.as_mut().rust_mut(); + if !rust.update_timer.is_null() { + let timer_mut_ref = &mut *rust.update_timer; + let timer_slice = slice::from_raw_parts_mut(timer_mut_ref, 1); + let mut timer : Pin<&mut QTimer> = Pin::new_unchecked(&mut timer_slice[0]); + timer.as_mut().stop_queued(); + while timer.as_mut().is_active() { + std::thread::sleep(Duration::from_millis(1)); + } + } + if !rust.update_thread.is_null() { + let thread_mut_ref = &mut *rust.update_thread; + let thread_slice = slice::from_raw_parts_mut(thread_mut_ref, 1); + let thread : Pin<&mut QThread> = Pin::new_unchecked(&mut thread_slice[0]); + thread.exit(); + } + rust.closed = true; + } + + { + self.as_mut().set_ready(false); + } } pub fn update_on_gui_thread(mut self: Pin<&mut BackendWrapper>) { @@ -491,8 +528,9 @@ impl BackendWrapper { pub fn find_external_ports(self: Pin<&mut BackendWrapper>, maybe_name_regex: QString, port_direction: i32, data_type: i32) -> QList_QVariant { let rust = self.rust(); + let name_regex = maybe_name_regex.to_string(); let ports = rust.driver.as_ref().unwrap().find_external_ports( - Some(maybe_name_regex.to_string().as_str()), + Some(name_regex.as_str()), port_direction as u32, data_type as u32 );