OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
miniaudio_protocol.hpp
1#pragma once
2#include <ossia/detail/config.hpp>
3
4#if __has_include(<miniaudio.h>)
5#define OSSIA_ENABLE_MINIAUDIO 1
6// The single-header implementation is compiled exactly once, in miniaudio.cpp.
7// Every other includer of this header only sees declarations, so there is no
8// duplicate-symbol clash when libossia and score-plugin-audio are statically
9// linked into the same binary (e.g. wasm).
10#if defined(__APPLE__)
11#define MA_NO_RUNTIME_LINKING 1
12#endif
13#define MA_ENABLE_ONLY_SPECIFIC_BACKENDS 1
14#if defined(__EMSCRIPTEN__)
15#define MA_ENABLE_WEBAUDIO 1
16#define MA_ENABLE_AUDIO_WORKLETS 1
17// The AudioWorklet thread runs the whole ossia execution tick (every node,
18// including deep gfx/ISF exec-node call chains). miniaudio allocates a fixed
19// heap buffer for that thread's stack; its 128KB default is far too small and
20// a deep tick silently overruns it, corrupting adjacent heap allocations
21// (manifesting much later as OOB / null-function crashes on the main thread).
22// Give it a desktop-sized stack instead.
23#define MA_AUDIO_WORKLETS_THREAD_STACK_SIZE 4194304
24#elif defined(_WIN32)
25#define MA_ENABLE_WASAPI 1
26#else
27#define MA_ENABLE_COREAUDIO 1
28#define MA_ENABLE_ALSA 1
29#endif
30#define MA_NO_WAV 1
31#define MA_NO_FLAC 1
32#define MA_NO_MP3 1
33#define MA_NO_RESOURCE_MANAGER 1
34#define MA_NO_NODE_GRAPH 1
35#define MA_NO_GENERATION 1
36#define MA_MAX_CHANNELS 1024
37// The audio thread is created with this priority when the rtprio limit
38// allows it; the callback raises it otherwise (it asks for the maximum by
39// default, which the usual limit of 95 or 98 refuses).
40#define MA_PTHREAD_REALTIME_THREAD_PRIORITY 95
41// Export the miniaudio API from libossia so consumers (score-plugin-audio)
42// link against this one copy instead of compiling their own. OSSIA_EXPORT is
43// empty in static builds, dllexport/visibility when building the ossia shared
44// lib, and dllimport when compiling against it.
45#define MA_API OSSIA_EXPORT
46
47#include <ossia/audio/audio_engine.hpp>
49#include <ossia/detail/pod_vector.hpp>
50#include <ossia/detail/thread.hpp>
51
52#include <miniaudio.h>
53
54#if defined(__EMSCRIPTEN__)
55#include <emscripten/em_asm.h>
56#endif
57
58#define OSSIA_AUDIO_MINIAUDIO 1
59
60namespace ossia
61{
62struct miniaudio_context
63{
64 ma_context context;
65};
66
67class miniaudio_engine final : public audio_engine
68{
69
70public:
71 std::vector<const ma_device_info*> devices;
72 bool is_duplex(const ma_device_id& card_in, const ma_device_id& card_out)
73 {
74#if defined(__APPLE__)
75 return true;
76 std::string_view i = card_in.coreaudio;
77 std::string_view o = card_out.coreaudio;
78 if(i.length() != o.length() || i.empty() || o.empty())
79 return false;
80 if(i.substr(0, i.size() - 1) == o.substr(0, o.size() - 1))
81 return true;
82
83#endif
84 return memcmp(&card_in, &card_out, sizeof(ma_device_id)) == 0;
85 }
86 miniaudio_engine(
87 std::shared_ptr<miniaudio_context> ctx, std::string name,
88 const ma_device_id& card_in, const ma_device_id& card_out, int inputs, int outputs,
89 int rate, int bs, ma_share_mode share = ma_share_mode_shared)
90 : m_ctx{std::move(ctx)}
91 {
92 ma_device_type dtype = ma_device_type_duplex;
93 if(inputs == 0)
94 dtype = ma_device_type_playback;
95 else if(outputs == 0)
96 dtype = ma_device_type_capture;
97
98 ma_device_config config = ma_device_config_init(dtype);
99
100 config.sampleRate = rate;
101 config.periodSizeInFrames = bs;
102 // As for PortAudio: periods shorter than 2 ms get one more.
103 config.periods = int64_t(bs) * 1000 < 2 * int64_t(rate) ? 4 : 3;
104 // A zeroed device id means "system default device". Passing NULL as the
105 // pDeviceID makes miniaudio open the OS default device and, on backends
106 // that support it (WASAPI in particular), automatically follow the OS
107 // default output/input when the user changes it while the stream runs.
108 auto is_default_id = [](const ma_device_id& id) {
109 ma_device_id zero{};
110 return memcmp(&id, &zero, sizeof(ma_device_id)) == 0;
111 };
112
113 if(outputs > 0)
114 {
115 config.playback.pDeviceID = is_default_id(card_out) ? nullptr : &card_out;
116 config.playback.channels = outputs;
117 config.playback.format = ma_format_f32;
118 }
119
120 if(inputs > 0)
121 {
122 config.capture.pDeviceID = is_default_id(card_in) ? nullptr : &card_in;
123 config.capture.channels = inputs;
124 config.capture.format = ma_format_f32;
125 }
126
127 config.dataCallback = callback;
128
129 config.performanceProfile = ma_performance_profile_low_latency;
130 config.noFixedSizedCallback = false;
131 config.noClip = false;
132 config.noDisableDenormals = false;
133 config.noPreSilencedOutputBuffer = false;
134
135 config.pUserData = this;
136
137 this->effective_buffer_size = bs;
138 this->effective_sample_rate = rate;
139 this->effective_inputs = inputs;
140 this->effective_outputs = outputs;
141
142 ins_data.resize(effective_inputs * bs + 16);
143 outs_data.resize(effective_outputs * bs + 16);
144 ins.resize(effective_inputs + 2);
145 outs.resize(effective_outputs + 2);
146
147 // Exclusive access (ALSA "hw:") gives a period of bs frames; shared access
148 // goes through the system mixer (ALSA dmix), whose own period is used
149 // whatever bs is. A device another program holds can only be shared.
150 ma_result res = MA_ERROR;
151 for(ma_share_mode mode : {share, ma_share_mode_shared})
152 {
153 config.playback.shareMode = mode;
154 config.capture.shareMode = mode;
155 // The performance profile only matters without a period size: no
156 // retry with another one.
157 res = ma_device_init(&m_ctx->context, &config, &m_stream);
158 if(res == MA_SUCCESS)
159 {
160 m_exclusive = mode == ma_share_mode_exclusive;
161 if(mode != share)
162 ossia::logger().warn(
163 "miniaudio: exclusive access refused, using the device in shared mode");
164 break;
165 }
166 if(mode == ma_share_mode_shared)
167 break;
168 }
169 if(res != MA_SUCCESS)
170 throw std::runtime_error(
171 std::string("miniaudio: cannot open the device: ") + ma_result_description(res));
172
173 if(res = ma_device_start(&m_stream); res != MA_SUCCESS)
174 {
175 ma_device_uninit(&m_stream);
176 throw std::runtime_error(
177 std::string("miniaudio: cannot start the device: ")
178 + ma_result_description(res));
179 }
180
181 auto to_client_frames = [rate](ma_uint32 frames, ma_uint32 device_rate) {
182 return device_rate > 0 ? int(int64_t(frames) * rate / device_rate) : int(frames);
183 };
184 if(outputs > 0)
185 effective_output_latency = to_client_frames(
186 m_stream.playback.internalPeriodSizeInFrames * m_stream.playback.internalPeriods,
187 m_stream.playback.internalSampleRate);
188 if(inputs > 0)
189 effective_input_latency = to_client_frames(
190 m_stream.capture.internalPeriodSizeInFrames,
191 m_stream.capture.internalSampleRate);
192
193#if defined(__EMSCRIPTEN__)
194 // A duplex device only reaches the output from inside getUserMedia()'s
195 // then(): a page with no microphone, or one whose capture fails for any
196 // other reason, plays nothing at all. Connecting an already connected node
197 // is a no-op, so doing it here costs nothing and makes playback independent
198 // of capture.
199 EM_ASM(
200 {
201 const node = emscriptenGetAudioObject($0);
202 const ctx = emscriptenGetAudioObject($1);
203 if(node && ctx)
204 node.connect(ctx.destination);
205 },
206 m_stream.webaudio.audioWorklet, m_stream.webaudio.audioContext);
207#endif
208
209 m_active = true;
210 }
211
213 bool exclusive() const noexcept { return m_exclusive; }
214
215 bool running() const override
216 {
217 return m_active && ma_device_get_state(&m_stream) == ma_device_state_started;
218 }
219
220 bool failed() const noexcept override
221 {
222 return m_active && !stop_processing
223 && ma_device_get_state(&m_stream) == ma_device_state_stopped;
224 }
225
226 void stop() override
227 {
228 audio_engine::stop();
229
230 if(m_active)
231 {
232 ma_device_stop(&m_stream);
233 ma_device_uninit(&m_stream);
234 }
235 m_active = false;
236 }
237
238 ~miniaudio_engine() override { stop(); }
239
240private:
241 static void
242 callback(ma_device* pDevice, void* output, const void* input, ma_uint32 nframes)
243 {
244#if !defined(__EMSCRIPTEN__)
245 [[maybe_unused]]
246 static const thread_local auto _
247 = [] {
248 ossia::set_thread_name("ossia audio 0");
249 ossia::set_thread_pinned(thread_type::Audio, 0);
250#if defined(__linux__)
251 ossia::set_current_thread_realtime(95, true, false);
252#endif
253 return 0;
254 }();
255#endif
256
257 auto& self = *static_cast<miniaudio_engine*>(pDevice->pUserData);
258 self.tick_start();
259
260 if(self.stop_processing)
261 {
262 self.tick_clear();
263 return;
264 }
265
266 auto ins = self.ins.data();
267 auto ins_data = self.ins_data.data();
268 for(int i = 0; i < self.effective_inputs; i++)
269 ins[i] = ins_data + i * nframes;
270
271 const float* in_samples = static_cast<const float*>(input);
272 for(int c = 0; c < self.effective_inputs; c++)
273 for(ma_uint32 f = 0; f < nframes; f++)
274 ins[c][f] = in_samples[f * self.effective_inputs + c];
275
276 auto outs = self.outs.data();
277 auto outs_data = self.outs_data.data();
278 std::memset(outs_data, 0, sizeof(float) * self.effective_outputs * nframes);
279 for(int i = 0; i < self.effective_outputs; i++)
280 outs[i] = outs_data + i * nframes;
281
282#if defined(__EMSCRIPTEN__)
283 // On WASM audio worklets, std::chrono::steady_clock is unavailable.
284 // Use the sample count to derive time instead.
285 self.m_frames_elapsed += nframes;
286 double nsecs = (double)self.m_frames_elapsed / self.effective_sample_rate;
287#else
288 if(!self.m_start)
289 self.m_start = std::chrono::steady_clock::now();
290 auto now = std::chrono::steady_clock::now();
291 auto nsecs
292 = std::chrono::duration_cast<std::chrono::nanoseconds>(now - *self.m_start)
293 .count()
294 / 1e9;
295#endif
296
297 ossia::audio_tick_state ts{(float* const*)ins, outs, self.effective_inputs,
298 self.effective_outputs, nframes, nsecs};
299 if(self.audio_tick)
300 self.audio_tick(ts);
301
302 self.tick_end();
303
304 float* out_samples = static_cast<float*>(output);
305 for(int c = 0; c < self.effective_outputs; c++)
306 for(ma_uint32 f = 0; f < nframes; f++)
307 out_samples[f * self.effective_outputs + c] = outs[c][f];
308 }
309
310 std::shared_ptr<miniaudio_context> m_ctx;
311 ma_device m_stream;
312#if defined(__EMSCRIPTEN__)
313 uint64_t m_frames_elapsed{};
314#else
315 std::optional<std::chrono::steady_clock::time_point> m_start;
316#endif
317
318 ossia::float_vector ins_data;
319 ossia::pod_vector<float*> ins;
320 ossia::float_vector outs_data;
321 ossia::pod_vector<float*> outs;
322
323 bool m_active{};
324 bool m_exclusive{};
325};
326}
327
328#endif
329// #endif
Definition git_info.h:7
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:120
bool set_current_thread_realtime(int prio, bool algo_fifo, bool log)
For threads created by a library, e.g.
Definition thread.cpp:197