T1.5: Migrate emit/parse sites to v2 wire format
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user