Complete managed macOS platform bring-up
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s

This commit is contained in:
2026-09-18 18:51:53 +02:00
parent ab460ae12b
commit 310c0f09dd
21 changed files with 421 additions and 83 deletions
+5 -2
View File
@@ -453,7 +453,9 @@ VC_API vc_result vc_stream_feed_pcm(vc_client* c, uint32_t stream_id,
* channels : 1 or 2, matching the sender's stream configuration.
* sample_rate : always 48000 in the current implementation.
*
* Pass cb = NULL to disable (default: disabled; hardware playback only).
* Pass cb = NULL to disable (default: disabled; hardware playback only). Disabling is a
* quiescence barrier: when vc_set_pcm_sink(c, NULL, NULL) returns, no callback using the old
* user pointer is still running, so the caller may safely release that state.
* The callback MUST NOT block, lock, or allocate — copy what you need and return.
* PCM is still delivered to the hardware playback device regardless (dual output). */
typedef void (*vc_pcm_sink_cb)(void* user, uint32_t user_id, uint32_t stream_id,
@@ -483,7 +485,8 @@ VC_API vc_result vc_set_pcm_sink(vc_client* c, vc_pcm_sink_cb cb, void* user);
* steadily even with no remote streams (a silent block), so the renderer has a continuous
* clock. The callback MUST NOT block, lock, or allocate — copy into a lock-free ring and
* return. Independent of vc_set_pcm_sink (the per-stream tap), which still works. Pass cb=NULL
* to disable (default: disabled). */
* to disable (default: disabled). Disabling is a quiescence barrier with the same callback-state
* lifetime guarantee as vc_set_pcm_sink. */
typedef void (*vc_mixed_output_cb)(void* user, const int16_t* pcm,
size_t samples_per_channel, uint32_t channels,
uint32_t sample_rate);
+21 -11
View File
@@ -566,13 +566,19 @@ void AudioEngine::init_recv_stream(uint32_t ssrc, const codec::OpusParams& p,
}
void AudioEngine::set_pcm_sink(PcmSink cb, void* user) {
pcm_sink_user_.store(user, std::memory_order_relaxed);
pcm_sink_.store(cb, std::memory_order_release);
std::lock_guard setter_lk(pcm_sink_set_mu_);
pcm_sink_.store(nullptr);
while (pcm_sink_active_.load() != 0) std::this_thread::yield();
pcm_sink_user_.store(user);
pcm_sink_.store(cb);
}
void AudioEngine::set_mixed_output_sink(MixedSink cb, void* user) {
mixed_sink_user_.store(user, std::memory_order_relaxed);
mixed_sink_.store(cb, std::memory_order_release);
std::lock_guard setter_lk(mixed_sink_set_mu_);
mixed_sink_.store(nullptr);
while (mixed_sink_active_.load() != 0) std::this_thread::yield();
mixed_sink_user_.store(user);
mixed_sink_.store(cb);
}
void AudioEngine::capture_data_cb(ma_device* dev, void* /*out*/,
@@ -734,17 +740,19 @@ void AudioEngine::on_playback(int16_t* out, ma_uint32 frames) {
}
// PCM sink: deliver decoded per-stream audio to external consumer (bots,
// transcription, recording) before it enters the hardware mix. Atomic relaxed-
// load is safe on the RT thread — the fn-ptr and user-ptr are independent
// pointer-sized values written together by set_pcm_sink (release store).
if (auto sink = pcm_sink_.load(std::memory_order_relaxed)) {
sink(pcm_sink_user_.load(std::memory_order_relaxed),
// transcription, recording) before it enters the hardware mix. The reader count
// makes disabling the callback a quiescence barrier while keeping this RT path
// lock-free and non-blocking.
pcm_sink_active_.fetch_add(1);
if (auto sink = pcm_sink_.load()) {
sink(pcm_sink_user_.load(),
stream.user_id, stream.stream_id,
stream.decode_scratch.data(),
static_cast<size_t>(n),
static_cast<uint32_t>(dec_channels),
params_.sample_rate);
}
pcm_sink_active_.fetch_sub(1);
stream.push_ring(stream.decode_scratch.data(), static_cast<size_t>(n));
stream.playout_ts += static_cast<uint32_t>(n);
@@ -797,10 +805,12 @@ void AudioEngine::run_mixer_timer() {
auto next = clock::now();
while (!mixer_timer_stop_.load(std::memory_order_acquire)) {
on_playback(mixer_scratch_.data(), spc);
if (auto sink = mixed_sink_.load(std::memory_order_relaxed)) {
sink(mixed_sink_user_.load(std::memory_order_relaxed), mixer_scratch_.data(), spc, ch,
mixed_sink_active_.fetch_add(1);
if (auto sink = mixed_sink_.load()) {
sink(mixed_sink_user_.load(), mixer_scratch_.data(), spc, ch,
params_.sample_rate);
}
mixed_sink_active_.fetch_sub(1);
next += period;
// If we fell badly behind (e.g. the thread was descheduled), reset the deadline rather
// than spin to catch up — the VPIO renderer rides its own clock + jitter ring.
+8 -2
View File
@@ -514,16 +514,22 @@ class AudioEngine {
int frame_samples_ = 960; // 20 ms @48 kHz
// External PCM tap: atomic fn-ptr + user-ptr pair. Written by set_pcm_sink (any thread);
// read by on_playback (RT thread) via relaxed load — safe for pointer-sized atomics.
// External PCM tap. The in-flight counter lets set_pcm_sink(nullptr, nullptr) wait for an
// already-entered callback without making the RT thread lock or block. Sequentially
// consistent operations are intentional here: once the setter observes zero readers, a
// later reader must observe the disabled callback before it can dereference the old user.
std::atomic<PcmSink> pcm_sink_{nullptr};
std::atomic<void*> pcm_sink_user_{nullptr};
std::atomic<uint32_t> pcm_sink_active_{0};
std::mutex pcm_sink_set_mu_;
// External mixed-output sink + mode flag (iOS VPIO). mixed_sink_ is written by
// set_mixed_output_sink (any thread); read by run_mixer_timer via relaxed load.
// external_playback_ is read in start() to gate hardware-playback-device creation.
std::atomic<MixedSink> mixed_sink_{nullptr};
std::atomic<void*> mixed_sink_user_{nullptr};
std::atomic<uint32_t> mixed_sink_active_{0};
std::mutex mixed_sink_set_mu_;
bool external_playback_ = false;
::OpusDREDDecoder* dred_dec_ = nullptr; // shared DRED decoder; null if unsupported