T1.5: Migrate emit/parse sites to v2 wire format

This commit is contained in:
Siavash Sameni
2026-05-11 12:36:45 +04:00
parent 9680b6ff34
commit c93d302656
120 changed files with 5953 additions and 2888 deletions

View File

@@ -11,8 +11,8 @@
#![cfg(target_os = "android")]
use jni::objects::{JObject, JString, JValue};
use jni::JavaVM;
use jni::objects::{JObject, JString, JValue};
/// Grab the JavaVM + current Activity from the ndk_context that Tauri's
/// mobile runtime sets up at process startup.
@@ -22,8 +22,7 @@ fn jvm_and_activity() -> Result<(JavaVM, JObject<'static>), String> {
if vm_ptr.is_null() {
return Err("ndk_context: JavaVM pointer is null".into());
}
let vm = unsafe { JavaVM::from_raw(vm_ptr) }
.map_err(|e| format!("JavaVM::from_raw: {e}"))?;
let vm = unsafe { JavaVM::from_raw(vm_ptr) }.map_err(|e| format!("JavaVM::from_raw: {e}"))?;
let activity_ptr = ctx.context() as jni::sys::jobject;
if activity_ptr.is_null() {
return Err("ndk_context: activity pointer is null".into());
@@ -140,13 +139,8 @@ pub fn start_bluetooth_sco() -> Result<(), String> {
let am = audio_manager(&mut env, &activity)?;
// Ensure speaker is off — mutually exclusive with BT.
env.call_method(
&am,
"setSpeakerphoneOn",
"(Z)V",
&[JValue::Bool(0)],
)
.map_err(|e| format!("setSpeakerphoneOn(false): {e}"))?;
env.call_method(&am, "setSpeakerphoneOn", "(Z)V", &[JValue::Bool(0)])
.map_err(|e| format!("setSpeakerphoneOn(false): {e}"))?;
// Try modern API first (API 31+): setCommunicationDevice(AudioDeviceInfo)
// Find a BT SCO or BLE device from getAvailableCommunicationDevices()
@@ -195,11 +189,7 @@ fn try_set_communication_device(
) -> Result<bool, String> {
// Check SDK_INT >= 31 (Android 12)
let sdk_int = env
.get_static_field(
"android/os/Build$VERSION",
"SDK_INT",
"I",
)
.get_static_field("android/os/Build$VERSION", "SDK_INT", "I")
.and_then(|v| v.i())
.unwrap_or(0);
@@ -261,11 +251,7 @@ fn try_set_communication_device(
.and_then(|v| v.z())
.unwrap_or(false);
tracing::info!(
device_type,
ok,
"setCommunicationDevice: set BT device"
);
tracing::info!(device_type, ok, "setCommunicationDevice: set BT device");
return Ok(ok);
}
}
@@ -293,7 +279,12 @@ pub fn is_bluetooth_sco_on() -> Result<bool, String> {
if sdk_int >= 31 {
// getCommunicationDevice() → AudioDeviceInfo (nullable)
let device = env
.call_method(am, "getCommunicationDevice", "()Landroid/media/AudioDeviceInfo;", &[])
.call_method(
am,
"getCommunicationDevice",
"()Landroid/media/AudioDeviceInfo;",
&[],
)
.and_then(|v| v.l())
.unwrap_or(JObject::null());
if device.is_null() {
@@ -351,7 +342,11 @@ pub fn is_bluetooth_available() -> Result<bool, String> {
.unwrap_or(0);
// TYPE_BLUETOOTH_SCO = 7, TYPE_BLUETOOTH_A2DP = 8
if device_type == 7 || device_type == 8 {
tracing::info!(device_type, idx = i, "is_bluetooth_available: found BT device");
tracing::info!(
device_type,
idx = i,
"is_bluetooth_available: found BT device"
);
return Ok(true);
}
}

View File

@@ -9,8 +9,8 @@
//! still fails cleanly but the rest of the engine code links in.
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
use std::time::Instant;
use tauri::Emitter;
use tokio::sync::Mutex;
@@ -120,7 +120,10 @@ fn codec_to_profile(codec: CodecId) -> QualityProfile {
frame_duration_ms: 20,
frames_per_block: 5,
},
other => QualityProfile { codec: other, ..QualityProfile::GOOD },
other => QualityProfile {
codec: other,
..QualityProfile::GOOD
},
}
}
@@ -289,8 +292,7 @@ impl DredRecvState {
// user can see "DRED is on the wire" in logcat. After
// that, sample every 100th parse to confirm the window
// is steady-state without drowning the log.
let should_log = self.parses_with_data == 1
|| self.parses_with_data % 100 == 0;
let should_log = self.parses_with_data == 1 || self.parses_with_data % 100 == 0;
if should_log && wzp_codec::dred_verbose_logs() {
info!(
seq,
@@ -467,8 +469,7 @@ impl CallEngine {
let relay_addr: SocketAddr = relay.parse()?;
info!(%relay_addr, "resolved relay addr");
let seed = crate::load_or_create_seed()
.map_err(|e| anyhow::anyhow!("identity: {e}"))?;
let seed = crate::load_or_create_seed().map_err(|e| anyhow::anyhow!("identity: {e}"))?;
let fp = seed.derive_identity().public_identity().fingerprint;
let fingerprint = fp.to_string();
info!(%fp, "identity loaded");
@@ -476,7 +477,10 @@ impl CallEngine {
// Transport source: either the pre-connected one from the
// dual-path race or build a fresh one here.
let transport = if let Some(t) = pre_connected_transport {
info!(t_ms = call_t0.elapsed().as_millis(), is_direct_p2p, "first-join diag: using pre-connected transport");
info!(
t_ms = call_t0.elapsed().as_millis(),
is_direct_p2p, "first-join diag: using pre-connected transport"
);
t
} else {
// QUIC transport + handshake (Phase 0 relay-only path).
@@ -492,8 +496,10 @@ impl CallEngine {
ep
} else {
let bind_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
let ep = wzp_transport::create_endpoint(bind_addr, None)
.map_err(|e| { error!("create_endpoint failed: {e}"); e })?;
let ep = wzp_transport::create_endpoint(bind_addr, None).map_err(|e| {
error!("create_endpoint failed: {e}");
e
})?;
info!(local_addr = ?ep.local_addr().ok(), "created new endpoint, dialing relay");
ep
};
@@ -501,18 +507,27 @@ impl CallEngine {
let conn = match tokio::time::timeout(
std::time::Duration::from_secs(CONNECT_TIMEOUT_SECS),
wzp_transport::connect(&endpoint, relay_addr, &room, client_config),
).await {
)
.await
{
Ok(Ok(c)) => c,
Ok(Err(e)) => {
error!("connect failed: {e}");
return Err(e.into());
}
Err(_) => {
error!("connect TIMED OUT after {CONNECT_TIMEOUT_SECS}s — QUIC handshake never completed. Relay may be unreachable from this endpoint.");
return Err(anyhow::anyhow!("QUIC connect timeout ({CONNECT_TIMEOUT_SECS}s)"));
error!(
"connect TIMED OUT after {CONNECT_TIMEOUT_SECS}s — QUIC handshake never completed. Relay may be unreachable from this endpoint."
);
return Err(anyhow::anyhow!(
"QUIC connect timeout ({CONNECT_TIMEOUT_SECS}s)"
));
}
};
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: QUIC connection established, performing handshake");
info!(
t_ms = call_t0.elapsed().as_millis(),
"first-join diag: QUIC connection established, performing handshake"
);
Arc::new(wzp_transport::QuinnTransport::new(conn))
};
@@ -526,16 +541,22 @@ impl CallEngine {
// through the signal channel (DirectCallOffer/Answer carry
// identity_pub + ephemeral_pub + signature).
if !is_direct_p2p {
let _session = wzp_client::handshake::perform_handshake(
&*transport,
&seed.0,
Some(&alias),
)
.await
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: connected to relay, handshake complete");
let _session =
wzp_client::handshake::perform_handshake(&*transport, &seed.0, Some(&alias))
.await
.map_err(|e| {
error!("perform_handshake failed: {e}");
e
})?;
info!(
t_ms = call_t0.elapsed().as_millis(),
"first-join diag: connected to relay, handshake complete"
);
} else {
info!(t_ms = call_t0.elapsed().as_millis(), "first-join diag: direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
info!(
t_ms = call_t0.elapsed().as_millis(),
"first-join diag: direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)"
);
}
event_cb("connected", &format!("joined room {room}"));
@@ -579,7 +600,9 @@ impl CallEngine {
let t_pre_audio = call_t0.elapsed().as_millis();
if let Err(code) = crate::wzp_native::audio_start() {
return Err(anyhow::anyhow!("wzp_native_audio_start failed: code {code}"));
return Err(anyhow::anyhow!(
"wzp_native_audio_start failed: code {code}"
));
}
// Fix C (task #36): prime the playout ring with 20ms of
@@ -688,15 +711,17 @@ impl CallEngine {
}
// RMS for UI meter
let sum_sq: f64 = buf[..frame_samples].iter().map(|&s| (s as f64) * (s as f64)).sum();
let sum_sq: f64 = buf[..frame_samples]
.iter()
.map(|&s| (s as f64) * (s as f64))
.sum();
let rms = (sum_sq / frame_samples as f64).sqrt() as u32;
send_level.store(rms, Ordering::Relaxed);
last_rms = rms;
if !first_nonzero_rms_logged && rms > 0 {
info!(
t_ms = send_t0.elapsed().as_millis(),
rms,
"first-join diag: send first non-zero capture RMS"
rms, "first-join diag: send first non-zero capture RMS"
);
first_nonzero_rms_logged = true;
}
@@ -763,11 +788,9 @@ impl CallEngine {
frames_since_dred_poll = 0;
let snap = send_t.quinn_path_stats();
let pq = send_t.path_quality();
if let Some(tuning) = dred_tuner.update(
snap.loss_pct,
snap.rtt_ms,
pq.jitter_ms,
) {
if let Some(tuning) =
dred_tuner.update(snap.loss_pct, snap.rtt_ms, pq.jitter_ms)
{
encoder.apply_dred_tuning(tuning);
if wzp_codec::dred_verbose_logs() {
info!(
@@ -874,9 +897,7 @@ impl CallEngine {
// independent of Oboe routing. Convert locally with e.g.
// ffmpeg -f s16le -ar 48000 -ac 1 -i decoded.pcm decoded.wav
use std::io::Write;
let recorder_path = crate::APP_DATA_DIR
.get()
.map(|p| p.join("decoded.pcm"));
let recorder_path = crate::APP_DATA_DIR.get().map(|p| p.join("decoded.pcm"));
let mut recorder = match recorder_path.as_ref() {
Some(p) => match std::fs::File::create(p) {
Ok(f) => {
@@ -954,7 +975,9 @@ impl CallEngine {
{
let mut rx = recv_rx_codec.lock().await;
let codec_name = format!("{:?}", pkt.header.codec_id);
if *rx != codec_name { *rx = codec_name; }
if *rx != codec_name {
*rx = codec_name;
}
}
if pkt.header.codec_id != current_codec {
let new_profile = codec_to_profile(pkt.header.codec_id);
@@ -980,9 +1003,8 @@ impl CallEngine {
// no-op.
if pkt.header.codec_id.is_opus() {
dred_recv.ingest_opus(pkt.header.seq, &pkt.payload);
let frame_samples_now = (48_000
* current_profile.frame_duration_ms as usize)
/ 1000;
let frame_samples_now =
(48_000 * current_profile.frame_duration_ms as usize) / 1000;
let spk_muted_flag = recv_spk.load(Ordering::Relaxed);
dred_recv.fill_gap_to(
&mut decoder,
@@ -1046,10 +1068,15 @@ impl CallEngine {
// Log sample range for the first few decoded frames and periodically
if decoded_frames <= 3 || decoded_frames % 100 == 0 {
let slice = &pcm[..n];
let (mut lo, mut hi, mut sumsq) = (i16::MAX, i16::MIN, 0i64);
let (mut lo, mut hi, mut sumsq) =
(i16::MAX, i16::MIN, 0i64);
for &s in slice.iter() {
if s < lo { lo = s; }
if s > hi { hi = s; }
if s < lo {
lo = s;
}
if s > hi {
hi = s;
}
sumsq += (s as i64) * (s as i64);
}
let rms = (sumsq as f64 / n as f64).sqrt() as i32;
@@ -1086,7 +1113,10 @@ impl CallEngine {
.saturating_add(byte_slice.len() as u64);
if recorder_bytes >= RECORDER_MAX_BYTES {
let _ = rec.flush();
info!(recorder_bytes, "decoded-pcm recorder: stopped after limit");
info!(
recorder_bytes,
"decoded-pcm recorder: stopped after limit"
);
}
}
}
@@ -1105,11 +1135,18 @@ impl CallEngine {
last_written = w;
written_samples = written_samples.saturating_add(w as u64);
if w < n && decoded_frames <= 10 {
tracing::warn!(n, w, "recv: partial playout write (ring nearly full)");
tracing::warn!(
n,
w,
"recv: partial playout write (ring nearly full)"
);
}
} else if decoded_frames <= 3 || decoded_frames % 100 == 0 {
// User clicked spk-mute — log it so we don't chase ghost bugs
tracing::info!(decoded_frames, "recv: spk_muted=true, skipping playout write");
tracing::info!(
decoded_frames,
"recv: spk_muted=true, skipping playout write"
);
}
}
Err(e) => {
@@ -1302,8 +1339,7 @@ impl CallEngine {
let relay_addr: SocketAddr = relay.parse()?;
let seed = crate::load_or_create_seed()
.map_err(|e| anyhow::anyhow!("identity: {e}"))?;
let seed = crate::load_or_create_seed().map_err(|e| anyhow::anyhow!("identity: {e}"))?;
let fp = seed.derive_identity().public_identity().fingerprint;
let fingerprint = fp.to_string();
info!(%fp, "identity loaded");
@@ -1325,15 +1361,20 @@ impl CallEngine {
ep
} else {
let bind_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
let ep = wzp_transport::create_endpoint(bind_addr, None)
.map_err(|e| { error!("create_endpoint failed: {e}"); e })?;
let ep = wzp_transport::create_endpoint(bind_addr, None).map_err(|e| {
error!("create_endpoint failed: {e}");
e
})?;
info!(local_addr = ?ep.local_addr().ok(), "created new endpoint, dialing relay");
ep
};
let client_config = wzp_transport::client_config();
let conn = wzp_transport::connect(&endpoint, relay_addr, &room, client_config)
.await
.map_err(|e| { error!("connect failed: {e}"); e })?;
.map_err(|e| {
error!("connect failed: {e}");
e
})?;
info!("QUIC connection established, performing handshake");
Arc::new(wzp_transport::QuinnTransport::new(conn))
};
@@ -1343,13 +1384,13 @@ impl CallEngine {
// accept_handshake handler. See the android branch's
// comment for the full rationale.
if !is_direct_p2p {
let _session = wzp_client::handshake::perform_handshake(
&*transport,
&seed.0,
Some(&alias),
)
.await
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
let _session =
wzp_client::handshake::perform_handshake(&*transport, &seed.0, Some(&alias))
.await
.map_err(|e| {
error!("perform_handshake failed: {e}");
e
})?;
} else {
info!("direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
}
@@ -1494,11 +1535,9 @@ impl CallEngine {
frames_since_dred_poll = 0;
let snap = send_t.quinn_path_stats();
let pq = send_t.path_quality();
if let Some(tuning) = dred_tuner.update(
snap.loss_pct,
snap.rtt_ms,
pq.jitter_ms,
) {
if let Some(tuning) =
dred_tuner.update(snap.loss_pct, snap.rtt_ms, pq.jitter_ms)
{
encoder.apply_dred_tuning(tuning);
}
}
@@ -1558,7 +1597,9 @@ impl CallEngine {
{
let mut rx = recv_rx_codec.lock().await;
let codec_name = format!("{:?}", pkt.header.codec_id);
if *rx != codec_name { *rx = codec_name; }
if *rx != codec_name {
*rx = codec_name;
}
}
// Auto-switch decoder if incoming codec differs
if pkt.header.codec_id != current_codec {
@@ -1575,9 +1616,8 @@ impl CallEngine {
// start() recv task for full commentary.
if pkt.header.codec_id.is_opus() {
dred_recv.ingest_opus(pkt.header.seq, &pkt.payload);
let frame_samples_now = (48_000
* current_profile.frame_duration_ms as usize)
/ 1000;
let frame_samples_now =
(48_000 * current_profile.frame_duration_ms as usize) / 1000;
let spk_muted_flag = recv_spk.load(Ordering::Relaxed);
dred_recv.fill_gap_to(
&mut decoder,

View File

@@ -74,7 +74,9 @@ fn save_to_disk(entries: &[CallHistoryEntry]) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let Ok(json) = serde_json::to_vec_pretty(entries) else { return };
let Ok(json) = serde_json::to_vec_pretty(entries) else {
return;
};
// Atomic write via temp file + rename so a crash mid-write doesn't
// leave us with a half-file on disk.
let tmp = path.with_extension("json.tmp");
@@ -94,12 +96,7 @@ fn now_unix() -> u64 {
/// Append a new entry to the store and persist to disk. Trims the store to
/// `MAX_ENTRIES` after insertion.
pub fn log(
call_id: String,
peer_fp: String,
peer_alias: Option<String>,
direction: CallDirection,
) {
pub fn log(call_id: String, peer_fp: String, peer_alias: Option<String>, direction: CallDirection) {
tracing::info!(
%call_id, %peer_fp, ?direction,
alias = ?peer_alias,

File diff suppressed because it is too large Load Diff

View File

@@ -29,8 +29,10 @@ static AUDIO_START: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
static AUDIO_START_BT: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
static AUDIO_STOP: OnceLock<unsafe extern "C" fn()> = OnceLock::new();
static AUDIO_CAPTURE_AVAILABLE: OnceLock<extern "C" fn() -> usize> = OnceLock::new();
static AUDIO_READ_CAPTURE: OnceLock<unsafe extern "C" fn(*mut i16, usize) -> usize> = OnceLock::new();
static AUDIO_WRITE_PLAYOUT: OnceLock<unsafe extern "C" fn(*const i16, usize) -> usize> = OnceLock::new();
static AUDIO_READ_CAPTURE: OnceLock<unsafe extern "C" fn(*mut i16, usize) -> usize> =
OnceLock::new();
static AUDIO_WRITE_PLAYOUT: OnceLock<unsafe extern "C" fn(*const i16, usize) -> usize> =
OnceLock::new();
static AUDIO_IS_RUNNING: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
static AUDIO_CAPTURE_LATENCY: OnceLock<unsafe extern "C" fn() -> f32> = OnceLock::new();
static AUDIO_PLAYOUT_LATENCY: OnceLock<unsafe extern "C" fn() -> f32> = OnceLock::new();
@@ -56,25 +58,68 @@ pub fn init() -> Result<(), String> {
unsafe {
macro_rules! resolve {
($cell:expr, $ty:ty, $name:expr) => {{
let sym: libloading::Symbol<$ty> = lib_ref.get($name)
.map_err(|e| format!("dlsym {}: {e}", core::str::from_utf8($name).unwrap_or("?")))?;
let sym: libloading::Symbol<$ty> = lib_ref.get($name).map_err(|e| {
format!("dlsym {}: {e}", core::str::from_utf8($name).unwrap_or("?"))
})?;
// Dereference the Symbol to extract the raw fn pointer;
// it stays valid because lib_ref is 'static.
$cell.set(*sym).map_err(|_| format!("{} already set", core::str::from_utf8($name).unwrap_or("?")))?;
$cell.set(*sym).map_err(|_| {
format!("{} already set", core::str::from_utf8($name).unwrap_or("?"))
})?;
}};
}
resolve!(VERSION, unsafe extern "C" fn() -> i32, b"wzp_native_version");
resolve!(HELLO, unsafe extern "C" fn(*mut u8, usize) -> usize, b"wzp_native_hello");
resolve!(AUDIO_START, unsafe extern "C" fn() -> i32, b"wzp_native_audio_start");
resolve!(AUDIO_START_BT, unsafe extern "C" fn() -> i32, b"wzp_native_audio_start_bt");
resolve!(
VERSION,
unsafe extern "C" fn() -> i32,
b"wzp_native_version"
);
resolve!(
HELLO,
unsafe extern "C" fn(*mut u8, usize) -> usize,
b"wzp_native_hello"
);
resolve!(
AUDIO_START,
unsafe extern "C" fn() -> i32,
b"wzp_native_audio_start"
);
resolve!(
AUDIO_START_BT,
unsafe extern "C" fn() -> i32,
b"wzp_native_audio_start_bt"
);
resolve!(AUDIO_STOP, unsafe extern "C" fn(), b"wzp_native_audio_stop");
resolve!(AUDIO_CAPTURE_AVAILABLE, extern "C" fn() -> usize, b"wzp_native_audio_capture_available");
resolve!(AUDIO_READ_CAPTURE, unsafe extern "C" fn(*mut i16, usize) -> usize, b"wzp_native_audio_read_capture");
resolve!(AUDIO_WRITE_PLAYOUT, unsafe extern "C" fn(*const i16, usize) -> usize, b"wzp_native_audio_write_playout");
resolve!(AUDIO_IS_RUNNING, unsafe extern "C" fn() -> i32, b"wzp_native_audio_is_running");
resolve!(AUDIO_CAPTURE_LATENCY, unsafe extern "C" fn() -> f32, b"wzp_native_audio_capture_latency_ms");
resolve!(AUDIO_PLAYOUT_LATENCY, unsafe extern "C" fn() -> f32, b"wzp_native_audio_playout_latency_ms");
resolve!(
AUDIO_CAPTURE_AVAILABLE,
extern "C" fn() -> usize,
b"wzp_native_audio_capture_available"
);
resolve!(
AUDIO_READ_CAPTURE,
unsafe extern "C" fn(*mut i16, usize) -> usize,
b"wzp_native_audio_read_capture"
);
resolve!(
AUDIO_WRITE_PLAYOUT,
unsafe extern "C" fn(*const i16, usize) -> usize,
b"wzp_native_audio_write_playout"
);
resolve!(
AUDIO_IS_RUNNING,
unsafe extern "C" fn() -> i32,
b"wzp_native_audio_is_running"
);
resolve!(
AUDIO_CAPTURE_LATENCY,
unsafe extern "C" fn() -> f32,
b"wzp_native_audio_capture_latency_ms"
);
resolve!(
AUDIO_PLAYOUT_LATENCY,
unsafe extern "C" fn() -> f32,
b"wzp_native_audio_playout_latency_ms"
);
}
Ok(())
@@ -92,7 +137,9 @@ pub fn version() -> i32 {
}
pub fn hello() -> String {
let Some(f) = HELLO.get() else { return String::new(); };
let Some(f) = HELLO.get() else {
return String::new();
};
let mut buf = [0u8; 64];
let n = unsafe { f(buf.as_mut_ptr(), buf.len()) };
String::from_utf8_lossy(&buf[..n]).into_owned()
@@ -125,32 +172,47 @@ pub fn audio_stop() {
/// Number of capture samples available to read without blocking.
pub fn audio_capture_available() -> usize {
let Some(f) = AUDIO_CAPTURE_AVAILABLE.get() else { return 0; };
let Some(f) = AUDIO_CAPTURE_AVAILABLE.get() else {
return 0;
};
f()
}
/// Read captured i16 PCM into `out`. Returns bytes actually copied.
pub fn audio_read_capture(out: &mut [i16]) -> usize {
let Some(f) = AUDIO_READ_CAPTURE.get() else { return 0; };
let Some(f) = AUDIO_READ_CAPTURE.get() else {
return 0;
};
unsafe { f(out.as_mut_ptr(), out.len()) }
}
/// Write i16 PCM into the playout ring. Returns samples enqueued.
pub fn audio_write_playout(input: &[i16]) -> usize {
let Some(f) = AUDIO_WRITE_PLAYOUT.get() else { return 0; };
let Some(f) = AUDIO_WRITE_PLAYOUT.get() else {
return 0;
};
unsafe { f(input.as_ptr(), input.len()) }
}
pub fn audio_is_running() -> bool {
AUDIO_IS_RUNNING.get().map(|f| unsafe { f() } != 0).unwrap_or(false)
AUDIO_IS_RUNNING
.get()
.map(|f| unsafe { f() } != 0)
.unwrap_or(false)
}
#[allow(dead_code)]
pub fn audio_capture_latency_ms() -> f32 {
AUDIO_CAPTURE_LATENCY.get().map(|f| unsafe { f() }).unwrap_or(0.0)
AUDIO_CAPTURE_LATENCY
.get()
.map(|f| unsafe { f() })
.unwrap_or(0.0)
}
#[allow(dead_code)]
pub fn audio_playout_latency_ms() -> f32 {
AUDIO_PLAYOUT_LATENCY.get().map(|f| unsafe { f() }).unwrap_or(0.0)
AUDIO_PLAYOUT_LATENCY
.get()
.map(|f| unsafe { f() })
.unwrap_or(0.0)
}