Now that Phase 1 proved the split-cdylib pipeline (build #37 launched cleanly with 'wzp-native dlopen OK: version=42 msg=...' in logcat), this commit brings the real audio code into wzp-native without ever touching the Tauri crate: - cpp/oboe_bridge.{h,cpp}, oboe_stub.cpp, getauxval_fix.c copied verbatim from crates/wzp-android/cpp/ (same files that work in the legacy wzp-android .so on this phone) - build.rs near-identical to crates/wzp-android/build.rs: clones google/oboe@1.8.1 into OUT_DIR, compiles oboe_bridge.cpp + all oboe source files as a single static lib with c++_shared linkage, emits -llog + -lOpenSLES. On non-android hosts it compiles just oboe_stub.cpp so `cargo check` works locally without an NDK. - Cargo.toml gets cc = "1" in [build-dependencies]. This is SAFE because wzp-native is a single-cdylib crate — crate-type is only ["cdylib"], no staticlib, so rust-lang/rust#104707 does not apply. - src/lib.rs extends the FFI surface with the real audio API: wzp_native_audio_start() -> i32 wzp_native_audio_stop() wzp_native_audio_read_capture(*mut i16, usize) -> usize wzp_native_audio_write_playout(*const i16, usize) -> usize wzp_native_audio_capture_latency_ms() -> f32 wzp_native_audio_playout_latency_ms() -> f32 wzp_native_audio_is_running() -> i32 Plus a static AudioBackend singleton holding the two SPSC ring buffers (capture + playout) that are shared with the C++ Oboe callbacks via AtomicI32 cursors. The wzp_native_version() and wzp_native_hello() smoke tests from Phase 1 are preserved. Compiles cleanly on macOS host with the stub oboe .cpp. Next build will exercise the full cargo-ndk path inside docker to verify the whole Oboe compile still works standalone. Phase 3 (next commit): wzp-desktop engine.rs on Android calls wzp-native's audio FFI via the already-wired libloading handle, and the real CallEngine::start() is implemented for Android using the same codec/handshake/send/recv pipeline as desktop but with Oboe rings instead of CPAL rings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
279 lines
10 KiB
C++
279 lines
10 KiB
C++
// Full Oboe implementation for Android
|
|
// This file is compiled only when targeting Android
|
|
|
|
#include "oboe_bridge.h"
|
|
|
|
#ifdef __ANDROID__
|
|
#include <oboe/Oboe.h>
|
|
#include <android/log.h>
|
|
#include <cstring>
|
|
#include <atomic>
|
|
|
|
#define LOG_TAG "wzp-oboe"
|
|
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
|
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ring buffer helpers (SPSC, lock-free)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static inline int32_t ring_available_read(const wzp_atomic_int* write_idx,
|
|
const wzp_atomic_int* read_idx,
|
|
int32_t capacity) {
|
|
int32_t w = std::atomic_load_explicit(write_idx, std::memory_order_acquire);
|
|
int32_t r = std::atomic_load_explicit(read_idx, std::memory_order_relaxed);
|
|
int32_t avail = w - r;
|
|
if (avail < 0) avail += capacity;
|
|
return avail;
|
|
}
|
|
|
|
static inline int32_t ring_available_write(const wzp_atomic_int* write_idx,
|
|
const wzp_atomic_int* read_idx,
|
|
int32_t capacity) {
|
|
return capacity - 1 - ring_available_read(write_idx, read_idx, capacity);
|
|
}
|
|
|
|
static inline void ring_write(int16_t* buf, int32_t capacity,
|
|
wzp_atomic_int* write_idx, const wzp_atomic_int* read_idx,
|
|
const int16_t* src, int32_t count) {
|
|
int32_t w = std::atomic_load_explicit(write_idx, std::memory_order_relaxed);
|
|
for (int32_t i = 0; i < count; i++) {
|
|
buf[w] = src[i];
|
|
w++;
|
|
if (w >= capacity) w = 0;
|
|
}
|
|
std::atomic_store_explicit(write_idx, w, std::memory_order_release);
|
|
}
|
|
|
|
static inline void ring_read(int16_t* buf, int32_t capacity,
|
|
const wzp_atomic_int* write_idx, wzp_atomic_int* read_idx,
|
|
int16_t* dst, int32_t count) {
|
|
int32_t r = std::atomic_load_explicit(read_idx, std::memory_order_relaxed);
|
|
for (int32_t i = 0; i < count; i++) {
|
|
dst[i] = buf[r];
|
|
r++;
|
|
if (r >= capacity) r = 0;
|
|
}
|
|
std::atomic_store_explicit(read_idx, r, std::memory_order_release);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global state
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static std::shared_ptr<oboe::AudioStream> g_capture_stream;
|
|
static std::shared_ptr<oboe::AudioStream> g_playout_stream;
|
|
static const WzpOboeRings* g_rings = nullptr;
|
|
static std::atomic<bool> g_running{false};
|
|
static std::atomic<float> g_capture_latency_ms{0.0f};
|
|
static std::atomic<float> g_playout_latency_ms{0.0f};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Capture callback
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class CaptureCallback : public oboe::AudioStreamDataCallback {
|
|
public:
|
|
oboe::DataCallbackResult onAudioReady(
|
|
oboe::AudioStream* stream,
|
|
void* audioData,
|
|
int32_t numFrames) override {
|
|
if (!g_running.load(std::memory_order_relaxed) || !g_rings) {
|
|
return oboe::DataCallbackResult::Stop;
|
|
}
|
|
|
|
const int16_t* src = static_cast<const int16_t*>(audioData);
|
|
int32_t avail = ring_available_write(g_rings->capture_write_idx,
|
|
g_rings->capture_read_idx,
|
|
g_rings->capture_capacity);
|
|
int32_t to_write = (numFrames < avail) ? numFrames : avail;
|
|
if (to_write > 0) {
|
|
ring_write(g_rings->capture_buf, g_rings->capture_capacity,
|
|
g_rings->capture_write_idx, g_rings->capture_read_idx,
|
|
src, to_write);
|
|
}
|
|
|
|
// Update latency estimate
|
|
auto result = stream->calculateLatencyMillis();
|
|
if (result) {
|
|
g_capture_latency_ms.store(static_cast<float>(result.value()),
|
|
std::memory_order_relaxed);
|
|
}
|
|
|
|
return oboe::DataCallbackResult::Continue;
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Playout callback
|
|
// ---------------------------------------------------------------------------
|
|
|
|
class PlayoutCallback : public oboe::AudioStreamDataCallback {
|
|
public:
|
|
oboe::DataCallbackResult onAudioReady(
|
|
oboe::AudioStream* stream,
|
|
void* audioData,
|
|
int32_t numFrames) override {
|
|
if (!g_running.load(std::memory_order_relaxed) || !g_rings) {
|
|
memset(audioData, 0, numFrames * sizeof(int16_t));
|
|
return oboe::DataCallbackResult::Stop;
|
|
}
|
|
|
|
int16_t* dst = static_cast<int16_t*>(audioData);
|
|
int32_t avail = ring_available_read(g_rings->playout_write_idx,
|
|
g_rings->playout_read_idx,
|
|
g_rings->playout_capacity);
|
|
int32_t to_read = (numFrames < avail) ? numFrames : avail;
|
|
|
|
if (to_read > 0) {
|
|
ring_read(g_rings->playout_buf, g_rings->playout_capacity,
|
|
g_rings->playout_write_idx, g_rings->playout_read_idx,
|
|
dst, to_read);
|
|
}
|
|
// Fill remainder with silence on underrun
|
|
if (to_read < numFrames) {
|
|
memset(dst + to_read, 0, (numFrames - to_read) * sizeof(int16_t));
|
|
}
|
|
|
|
// Update latency estimate
|
|
auto result = stream->calculateLatencyMillis();
|
|
if (result) {
|
|
g_playout_latency_ms.store(static_cast<float>(result.value()),
|
|
std::memory_order_relaxed);
|
|
}
|
|
|
|
return oboe::DataCallbackResult::Continue;
|
|
}
|
|
};
|
|
|
|
static CaptureCallback g_capture_cb;
|
|
static PlayoutCallback g_playout_cb;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public C API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
|
|
if (g_running.load(std::memory_order_relaxed)) {
|
|
LOGW("wzp_oboe_start: already running");
|
|
return -1;
|
|
}
|
|
|
|
g_rings = rings;
|
|
|
|
// Build capture stream
|
|
oboe::AudioStreamBuilder captureBuilder;
|
|
captureBuilder.setDirection(oboe::Direction::Input)
|
|
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
|
->setSharingMode(oboe::SharingMode::Exclusive)
|
|
->setFormat(oboe::AudioFormat::I16)
|
|
->setChannelCount(config->channel_count)
|
|
->setSampleRate(config->sample_rate)
|
|
->setFramesPerDataCallback(config->frames_per_burst)
|
|
->setInputPreset(oboe::InputPreset::VoiceCommunication)
|
|
->setDataCallback(&g_capture_cb);
|
|
|
|
oboe::Result result = captureBuilder.openStream(g_capture_stream);
|
|
if (result != oboe::Result::OK) {
|
|
LOGE("Failed to open capture stream: %s", oboe::convertToText(result));
|
|
return -2;
|
|
}
|
|
|
|
// Build playout stream
|
|
oboe::AudioStreamBuilder playoutBuilder;
|
|
playoutBuilder.setDirection(oboe::Direction::Output)
|
|
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
|
->setSharingMode(oboe::SharingMode::Exclusive)
|
|
->setFormat(oboe::AudioFormat::I16)
|
|
->setChannelCount(config->channel_count)
|
|
->setSampleRate(config->sample_rate)
|
|
->setFramesPerDataCallback(config->frames_per_burst)
|
|
->setUsage(oboe::Usage::VoiceCommunication)
|
|
->setDataCallback(&g_playout_cb);
|
|
|
|
result = playoutBuilder.openStream(g_playout_stream);
|
|
if (result != oboe::Result::OK) {
|
|
LOGE("Failed to open playout stream: %s", oboe::convertToText(result));
|
|
g_capture_stream->close();
|
|
g_capture_stream.reset();
|
|
return -3;
|
|
}
|
|
|
|
g_running.store(true, std::memory_order_release);
|
|
|
|
// Start both streams
|
|
result = g_capture_stream->requestStart();
|
|
if (result != oboe::Result::OK) {
|
|
LOGE("Failed to start capture: %s", oboe::convertToText(result));
|
|
g_running.store(false, std::memory_order_release);
|
|
g_capture_stream->close();
|
|
g_playout_stream->close();
|
|
g_capture_stream.reset();
|
|
g_playout_stream.reset();
|
|
return -4;
|
|
}
|
|
|
|
result = g_playout_stream->requestStart();
|
|
if (result != oboe::Result::OK) {
|
|
LOGE("Failed to start playout: %s", oboe::convertToText(result));
|
|
g_running.store(false, std::memory_order_release);
|
|
g_capture_stream->requestStop();
|
|
g_capture_stream->close();
|
|
g_playout_stream->close();
|
|
g_capture_stream.reset();
|
|
g_playout_stream.reset();
|
|
return -5;
|
|
}
|
|
|
|
LOGI("Oboe started: sr=%d burst=%d ch=%d",
|
|
config->sample_rate, config->frames_per_burst, config->channel_count);
|
|
return 0;
|
|
}
|
|
|
|
void wzp_oboe_stop(void) {
|
|
g_running.store(false, std::memory_order_release);
|
|
|
|
if (g_capture_stream) {
|
|
g_capture_stream->requestStop();
|
|
g_capture_stream->close();
|
|
g_capture_stream.reset();
|
|
}
|
|
if (g_playout_stream) {
|
|
g_playout_stream->requestStop();
|
|
g_playout_stream->close();
|
|
g_playout_stream.reset();
|
|
}
|
|
|
|
g_rings = nullptr;
|
|
LOGI("Oboe stopped");
|
|
}
|
|
|
|
float wzp_oboe_capture_latency_ms(void) {
|
|
return g_capture_latency_ms.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
float wzp_oboe_playout_latency_ms(void) {
|
|
return g_playout_latency_ms.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
int wzp_oboe_is_running(void) {
|
|
return g_running.load(std::memory_order_relaxed) ? 1 : 0;
|
|
}
|
|
|
|
#else
|
|
// Non-Android fallback — should not be reached; oboe_stub.cpp is used instead.
|
|
// Provide empty implementations just in case.
|
|
|
|
int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
|
|
(void)config; (void)rings;
|
|
return -99;
|
|
}
|
|
|
|
void wzp_oboe_stop(void) {}
|
|
float wzp_oboe_capture_latency_ms(void) { return 0.0f; }
|
|
float wzp_oboe_playout_latency_ms(void) { return 0.0f; }
|
|
int wzp_oboe_is_running(void) { return 0; }
|
|
|
|
#endif // __ANDROID__
|