Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3rdparty/libremidi
23 changes: 23 additions & 0 deletions src/ossia/audio/audio_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <ossia/audio/audio_engine.hpp>
#include <ossia/audio/dummy_protocol.hpp>
#include <ossia/audio/jack_protocol.hpp>
#include <ossia/audio/miniaudio_protocol.hpp>
#include <ossia/audio/pipewire_protocol.hpp>
#include <ossia/audio/portaudio_protocol.hpp>
#include <ossia/audio/pulseaudio_protocol.hpp>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<ossia::miniaudio_context>();
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)
Expand Down
10 changes: 10 additions & 0 deletions src/ossia/audio/miniaudio.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include <ossia/detail/config.hpp>

// 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(<miniaudio.h>)
#define MINIAUDIO_IMPLEMENTATION
#include <ossia/audio/miniaudio_protocol.hpp>
#endif
47 changes: 38 additions & 9 deletions src/ossia/audio/miniaudio_protocol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@

#if __has_include(<miniaudio.h>)
#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
#define MA_NO_RESOURCE_MANAGER 1
#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 <ossia/audio/audio_engine.hpp>
#include <ossia/detail/thread.hpp>

#include <kfr/base/conversion.hpp>

#include <miniaudio.h>

#define OSSIA_AUDIO_MINIAUDIO 1
Expand Down Expand Up @@ -144,18 +154,18 @@ 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 _
= [] {
ossia::set_thread_name("ossia audio 0");
ossia::set_thread_pinned(thread_type::Audio, 0);
return 0;
}();
#endif

auto& self = *static_cast<miniaudio_engine*>(pDevice->pUserData);
self.tick_start();
if(!self.m_start)
self.m_start = std::chrono::steady_clock::now();

if(self.stop_processing)
{
Expand All @@ -167,33 +177,52 @@ 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<const float*>(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();
std::memset(outs_data, 0, sizeof(float) * self.effective_outputs * nframes);
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<std::chrono::nanoseconds>(now - *self.m_start)
.count()
/ 1e9;
#endif

ossia::audio_tick_state ts{(float* const*)ins, outs, self.effective_inputs,
self.effective_outputs, nframes, nsecs};
self.audio_tick(ts);

self.tick_end();

kfr::interleave(
(float*)output, (const float**)outs, self.effective_outputs, nframes);
float* out_samples = static_cast<float*>(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<miniaudio_context> m_ctx;
ma_device m_stream;
#if defined(__EMSCRIPTEN__)
uint64_t m_frames_elapsed{};
#else
std::optional<std::chrono::steady_clock::time_point> m_start;
#endif

boost::container::vector<float> ins_data;
boost::container::vector<float*> ins;
Expand Down
25 changes: 25 additions & 0 deletions src/ossia_features.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,31 @@ if(OSSIA_DATAFLOW)
target_link_libraries(ossia PRIVATE $<BUILD_INTERFACE:ossia::sdl2>)
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 $<BUILD_INTERFACE:${OSSIA_3RDPARTY_FOLDER}/miniaudio>)
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 $<BUILD_INTERFACE:SampleRate::samplerate>)
endif()
Expand Down
Loading