diff --git a/3rdparty/libremidi b/3rdparty/libremidi index cccc943045d..70682d03676 160000 --- a/3rdparty/libremidi +++ b/3rdparty/libremidi @@ -1 +1 @@ -Subproject commit cccc943045dc4326395584cd4bc416a622de4b1a +Subproject commit 70682d03676ea92e1620c9814efd01a722c5b54e diff --git a/src/ossia/audio/audio_engine.cpp b/src/ossia/audio/audio_engine.cpp index dbc2cf2a726..4611e93dcce 100644 --- a/src/ossia/audio/audio_engine.cpp +++ b/src/ossia/audio/audio_engine.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,13 @@ void audio_engine::sync() req++; request.store(req); +#if defined(__EMSCRIPTEN__) + // On WASM, the audio runs in a worklet thread. We cannot spin-wait + // on the main thread as it blocks the browser event loop entirely. + // The tick will pick up the new request on its next callback. + return; +#endif + // The engine has started running, we wait for a couple iterations // to leave some time for the ticks to be updated int k = 0; @@ -121,7 +129,22 @@ ossia::audio_engine* make_audio_engine( bs = 1024; inputs = 0; outputs = 2; +#if OSSIA_ENABLE_MINIAUDIO + { + static auto ctx = std::make_shared(); + static bool ctx_init = false; + if(!ctx_init) + { + auto cfg = ma_context_config_init(); + ma_context_init(nullptr, 0, &cfg, &ctx->context); + ctx_init = true; + } + ma_device_id in_id{}, out_id{}; + return new ossia::miniaudio_engine{ctx, name, in_id, out_id, inputs, outputs, rate, bs}; + } +#else return new ossia::sdl_protocol{rate, bs}; +#endif #endif if(0) diff --git a/src/ossia/audio/miniaudio.cpp b/src/ossia/audio/miniaudio.cpp new file mode 100644 index 00000000000..9cfe60acbf7 --- /dev/null +++ b/src/ossia/audio/miniaudio.cpp @@ -0,0 +1,10 @@ +#include + +// The one and only translation unit that compiles the miniaudio single-header +// implementation. Everywhere else miniaudio_protocol.hpp only brings in the +// declarations. Config macros (backends, MA_MAX_CHANNELS, MA_API) come from +// miniaudio_protocol.hpp so they stay identical between here and every user. +#if __has_include() +#define MINIAUDIO_IMPLEMENTATION +#include +#endif diff --git a/src/ossia/audio/miniaudio_protocol.hpp b/src/ossia/audio/miniaudio_protocol.hpp index 83e492493ac..f5b9253d90f 100644 --- a/src/ossia/audio/miniaudio_protocol.hpp +++ b/src/ossia/audio/miniaudio_protocol.hpp @@ -3,13 +3,21 @@ #if __has_include() #define OSSIA_ENABLE_MINIAUDIO 1 -#define MINIAUDIO_IMPLEMENTATION +// The single-header implementation is compiled exactly once, in miniaudio.cpp. +// Every other includer of this header only sees declarations, so there is no +// duplicate-symbol clash when libossia and score-plugin-audio are statically +// linked into the same binary (e.g. wasm). #if defined(__APPLE__) #define MA_NO_RUNTIME_LINKING 1 #endif #define MA_ENABLE_ONLY_SPECIFIC_BACKENDS 1 +#if defined(__EMSCRIPTEN__) +#define MA_ENABLE_WEBAUDIO 1 +#define MA_ENABLE_AUDIO_WORKLETS 1 +#else #define MA_ENABLE_COREAUDIO 1 #define MA_ENABLE_ALSA 1 +#endif #define MA_NO_WAV 1 #define MA_NO_FLAC 1 #define MA_NO_MP3 1 @@ -17,13 +25,15 @@ #define MA_NO_NODE_GRAPH 1 #define MA_NO_GENERATION 1 #define MA_MAX_CHANNELS 1024 -#define MA_API +// Export the miniaudio API from libossia so consumers (score-plugin-audio) +// link against this one copy instead of compiling their own. OSSIA_EXPORT is +// empty in static builds, dllexport/visibility when building the ossia shared +// lib, and dllimport when compiling against it. +#define MA_API OSSIA_EXPORT #include #include -#include - #include #define OSSIA_AUDIO_MINIAUDIO 1 @@ -144,6 +154,7 @@ class miniaudio_engine final : public audio_engine static void callback(ma_device* pDevice, void* output, const void* input, ma_uint32 nframes) { +#if !defined(__EMSCRIPTEN__) [[maybe_unused]] static const thread_local auto _ = [] { @@ -151,11 +162,10 @@ class miniaudio_engine final : public audio_engine ossia::set_thread_pinned(thread_type::Audio, 0); return 0; }(); +#endif auto& self = *static_cast(pDevice->pUserData); self.tick_start(); - if(!self.m_start) - self.m_start = std::chrono::steady_clock::now(); if(self.stop_processing) { @@ -167,7 +177,11 @@ class miniaudio_engine final : public audio_engine auto ins_data = self.ins_data.data(); for(int i = 0; i < self.effective_inputs; i++) ins[i] = ins_data + i * nframes; - kfr::deinterleave(ins, (float*)input, self.effective_inputs, nframes); + + const float* in_samples = static_cast(input); + for(int c = 0; c < self.effective_inputs; c++) + for(ma_uint32 f = 0; f < nframes; f++) + ins[c][f] = in_samples[f * self.effective_inputs + c]; auto outs = self.outs.data(); auto outs_data = self.outs_data.data(); @@ -175,11 +189,20 @@ class miniaudio_engine final : public audio_engine for(int i = 0; i < self.effective_outputs; i++) outs[i] = outs_data + i * nframes; +#if defined(__EMSCRIPTEN__) + // On WASM audio worklets, std::chrono::steady_clock is unavailable. + // Use the sample count to derive time instead. + self.m_frames_elapsed += nframes; + double nsecs = (double)self.m_frames_elapsed / self.effective_sample_rate; +#else + if(!self.m_start) + self.m_start = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now(); auto nsecs = std::chrono::duration_cast(now - *self.m_start) .count() / 1e9; +#endif ossia::audio_tick_state ts{(float* const*)ins, outs, self.effective_inputs, self.effective_outputs, nframes, nsecs}; @@ -187,13 +210,19 @@ class miniaudio_engine final : public audio_engine self.tick_end(); - kfr::interleave( - (float*)output, (const float**)outs, self.effective_outputs, nframes); + float* out_samples = static_cast(output); + for(int c = 0; c < self.effective_outputs; c++) + for(ma_uint32 f = 0; f < nframes; f++) + out_samples[f * self.effective_outputs + c] = outs[c][f]; } std::shared_ptr m_ctx; ma_device m_stream; +#if defined(__EMSCRIPTEN__) + uint64_t m_frames_elapsed{}; +#else std::optional m_start; +#endif boost::container::vector ins_data; boost::container::vector ins; diff --git a/src/ossia_features.cmake b/src/ossia_features.cmake index be16f23a5fd..dd7ca90d6bb 100644 --- a/src/ossia_features.cmake +++ b/src/ossia_features.cmake @@ -276,6 +276,31 @@ if(OSSIA_DATAFLOW) target_link_libraries(ossia PRIVATE $) endif() + # MiniAudio (CoreAudio on macOS, ALSA on Linux, Web Audio on wasm). + # The single-header implementation is compiled here, once, in miniaudio.cpp; + # consumers (score-plugin-audio) only see declarations and link the exported + # API, so there is no duplicate ma_* symbol when everything is statically + # linked into one binary. Not used on Windows (WASAPI/PortAudio there). + if(NOT WIN32) + target_sources(ossia PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/ossia/audio/miniaudio.cpp") + # Keep it its own TU: it is the only place MINIAUDIO_IMPLEMENTATION is + # defined, and unity-merging could reorder it past a declarations-only + # include and drop the implementation. + set_source_files_properties( + "${CMAKE_CURRENT_SOURCE_DIR}/ossia/audio/miniaudio.cpp" + PROPERTIES SKIP_UNITY_BUILD_INCLUSION ON) + target_include_directories(ossia PRIVATE $) + if(APPLE) + # miniaudio's CoreAudio backend, built with MA_NO_RUNTIME_LINKING. + target_link_libraries(ossia PRIVATE + "-framework CoreAudio" "-framework AudioToolbox" + "-framework AudioUnit" "-framework CoreFoundation") + elseif(CMAKE_SYSTEM_NAME MATCHES Emscripten) + target_link_options(ossia PUBLIC -sAUDIO_WORKLET=1 -sWASM_WORKERS=1) + endif() + endif() + if(OSSIA_ENABLE_LIBSAMPLERATE) target_link_libraries(ossia PRIVATE $) endif()