From aed6eb0881b1fca2a1e04ad1c7102f13e706693a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Fri, 10 Jul 2026 15:18:24 +0200 Subject: [PATCH 1/6] 3rdparty: bump libremidi (wasm emscripten observer update() fix) Pulls the emscripten MIDI backend fix (observer update() notify arg) needed for the wasm build. See celtera/libremidi#228. --- 3rdparty/libremidi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/libremidi b/3rdparty/libremidi index cccc943045d..86d7e1d2fdd 160000 --- a/3rdparty/libremidi +++ b/3rdparty/libremidi @@ -1 +1 @@ -Subproject commit cccc943045dc4326395584cd4bc416a622de4b1a +Subproject commit 86d7e1d2fdd633388f4c8c9ad9fcdcc93876a67d From 96141ee31dfb734413c68d42213a6e9c790ba62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Tue, 21 Apr 2026 10:16:24 +0200 Subject: [PATCH 2/6] wip: wasm: use miniaudio instead of sdl in emscripten --- src/ossia/audio/audio_engine.cpp | 23 +++++++++++++++++++++ src/ossia/audio/miniaudio_protocol.hpp | 28 +++++++++++++++++++++++--- src/ossia_features.cmake | 7 +++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/ossia/audio/audio_engine.cpp b/src/ossia/audio/audio_engine.cpp index dbc2cf2a726..3fecbdef184 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_protocol.hpp b/src/ossia/audio/miniaudio_protocol.hpp index 83e492493ac..dcb19e12e83 100644 --- a/src/ossia/audio/miniaudio_protocol.hpp +++ b/src/ossia/audio/miniaudio_protocol.hpp @@ -8,8 +8,13 @@ #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 @@ -22,7 +27,7 @@ #include #include -#include +//#include #include @@ -144,6 +149,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 +157,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 +172,9 @@ 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; +#if !defined(__EMSCRIPTEN__) kfr::deinterleave(ins, (float*)input, self.effective_inputs, nframes); +#endif auto outs = self.outs.data(); auto outs_data = self.outs_data.data(); @@ -175,11 +182,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 +203,19 @@ class miniaudio_engine final : public audio_engine self.tick_end(); +#if !defined(__EMSCRIPTEN__) kfr::interleave( (float*)output, (const float**)outs, self.effective_outputs, nframes); +#endif } 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..5370a1ef7af 100644 --- a/src/ossia_features.cmake +++ b/src/ossia_features.cmake @@ -276,6 +276,13 @@ if(OSSIA_DATAFLOW) target_link_libraries(ossia PRIVATE $) endif() + # MiniAudio / Web Audio support + if(CMAKE_SYSTEM_NAME MATCHES Emscripten) + target_include_directories(ossia PRIVATE $) + target_compile_definitions(ossia PRIVATE MA_ENABLE_AUDIO_WORKLETS MA_ENABLE_WEBAUDIO) + target_link_options(ossia PUBLIC -sAUDIO_WORKLET=1 -sWASM_WORKERS=1) + endif() + if(OSSIA_ENABLE_LIBSAMPLERATE) target_link_libraries(ossia PRIVATE $) endif() From eaa8a986ea98a14d343c9813dfc5d84af4412ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sat, 11 Jul 2026 22:52:46 -0400 Subject: [PATCH 3/6] audio: drop kfr from miniaudio, use portable (de)interleave kfr::interleave/deinterleave for float are plain scalar loops (convert_sample is identity, no SIMD), so replace them with inline loops. This fixes two problems: - macOS/Linux: the kfr include had been commented out but kfr:: was still called in the non-emscripten path -> "use of undeclared identifier 'kfr'". The CoreAudio backend goes through this header. - WASM: the interleave call was #if'd out entirely, so the output buffer was never written -> silence. The portable loop runs on all platforms and fixes it. Also removes the last kfr dependency of the miniaudio backend. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019oPj1zRcxSQX7FNni7EHM6 --- src/ossia/audio/miniaudio_protocol.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ossia/audio/miniaudio_protocol.hpp b/src/ossia/audio/miniaudio_protocol.hpp index dcb19e12e83..b3e1f7e56ee 100644 --- a/src/ossia/audio/miniaudio_protocol.hpp +++ b/src/ossia/audio/miniaudio_protocol.hpp @@ -27,8 +27,6 @@ #include #include -//#include - #include #define OSSIA_AUDIO_MINIAUDIO 1 @@ -172,9 +170,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; -#if !defined(__EMSCRIPTEN__) - kfr::deinterleave(ins, (float*)input, self.effective_inputs, nframes); -#endif + + 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(); @@ -203,10 +203,10 @@ class miniaudio_engine final : public audio_engine self.tick_end(); -#if !defined(__EMSCRIPTEN__) - kfr::interleave( - (float*)output, (const float**)outs, self.effective_outputs, nframes); -#endif + 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; From 719044f3c7584e9d2833615f1f71200f699ce37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 12 Jul 2026 11:08:16 -0400 Subject: [PATCH 4/6] wasm: try to include miniaudio to make audio_engine use it by default --- src/ossia/audio/audio_engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ossia/audio/audio_engine.cpp b/src/ossia/audio/audio_engine.cpp index 3fecbdef184..4611e93dcce 100644 --- a/src/ossia/audio/audio_engine.cpp +++ b/src/ossia/audio/audio_engine.cpp @@ -3,7 +3,7 @@ #include #include #include -//#include +#include #include #include #include From b7437d1a09881417958dca4a05da4118df8563d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 12 Jul 2026 12:33:17 -0400 Subject: [PATCH 5/6] audio: compile miniaudio implementation once, in libossia miniaudio_protocol.hpp used to #define MINIAUDIO_IMPLEMENTATION itself, so every TU that included it compiled a full copy of the ma_* implementation. That was harmless only because the miniaudio include dir was private to score-plugin-audio. Enabling the miniaudio path in libossia's make_audio_engine gave a second copy and produced duplicate-symbol link errors once both objects end up in one binary (wasm, and any static build). Make libossia the single owner: - miniaudio_protocol.hpp is now declarations-only (no MINIAUDIO_IMPLEMENTATION) and marks the API with MA_API = OSSIA_EXPORT. - miniaudio.cpp is the one TU that defines MINIAUDIO_IMPLEMENTATION; kept out of unity builds so it can't be reordered past a declarations-only include. - ossia links it wherever miniaudio is used (macOS/Linux/BSD/wasm, not Windows) and pulls in the CoreAudio frameworks on Apple. OSSIA_EXPORT is empty in static builds (one definition, referenced across libraries), and export/import when ossia is a shared lib, so score-plugin-audio links this single copy in every configuration instead of recompiling it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019oPj1zRcxSQX7FNni7EHM6 --- src/ossia/audio/miniaudio.cpp | 10 ++++++++++ src/ossia/audio/miniaudio_protocol.hpp | 11 +++++++++-- src/ossia_features.cmake | 26 ++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 src/ossia/audio/miniaudio.cpp 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 b3e1f7e56ee..f5b9253d90f 100644 --- a/src/ossia/audio/miniaudio_protocol.hpp +++ b/src/ossia/audio/miniaudio_protocol.hpp @@ -3,7 +3,10 @@ #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 @@ -22,7 +25,11 @@ #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 diff --git a/src/ossia_features.cmake b/src/ossia_features.cmake index 5370a1ef7af..dd7ca90d6bb 100644 --- a/src/ossia_features.cmake +++ b/src/ossia_features.cmake @@ -276,11 +276,29 @@ if(OSSIA_DATAFLOW) target_link_libraries(ossia PRIVATE $) endif() - # MiniAudio / Web Audio support - if(CMAKE_SYSTEM_NAME MATCHES Emscripten) + # 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 $) - target_compile_definitions(ossia PRIVATE MA_ENABLE_AUDIO_WORKLETS MA_ENABLE_WEBAUDIO) - target_link_options(ossia PUBLIC -sAUDIO_WORKLET=1 -sWASM_WORKERS=1) + 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) From 74afb3fade009cb8a500d43cf948d8f5529aee6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 12 Jul 2026 14:32:08 -0400 Subject: [PATCH 6/6] wasm: more libremidi update --- 3rdparty/libremidi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/libremidi b/3rdparty/libremidi index 86d7e1d2fdd..70682d03676 160000 --- a/3rdparty/libremidi +++ b/3rdparty/libremidi @@ -1 +1 @@ -Subproject commit 86d7e1d2fdd633388f4c8c9ad9fcdcc93876a67d +Subproject commit 70682d03676ea92e1620c9814efd01a722c5b54e