Compare commits
114 Commits
android-re
...
feat/deskt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ceb6f45d5 | ||
|
|
07873ea598 | ||
|
|
cc00f7cace | ||
|
|
eb9de988d6 | ||
|
|
4ba77c8c0e | ||
|
|
7b8a2d0fba | ||
|
|
5cd7a20152 | ||
|
|
a5c00fe5cb | ||
|
|
ec41f179cd | ||
|
|
4e9244eb00 | ||
|
|
03a80a3196 | ||
|
|
7fecf285ea | ||
|
|
0683dde5d3 | ||
|
|
53f57eea07 | ||
|
|
ff3f7e8e4f | ||
|
|
48d2bd4f65 | ||
|
|
234a798df2 | ||
|
|
fa042b130c | ||
|
|
990b6f1ee0 | ||
|
|
7949266e11 | ||
|
|
d774f5f8c5 | ||
|
|
2fd94651e4 | ||
|
|
da09fdb6e9 | ||
|
|
510eae2089 | ||
|
|
76a4c53e21 | ||
|
|
4c6aac654a | ||
|
|
4f2ad65418 | ||
|
|
0178cbd91d | ||
|
|
9e37201198 | ||
|
|
da106bd939 | ||
|
|
8c36fb5651 | ||
|
|
cfa9ff67cf | ||
|
|
96be740fd9 | ||
|
|
8c4d640f89 | ||
|
|
49f101d785 | ||
|
|
d7b37a5749 | ||
|
|
b35a6b7d92 | ||
|
|
0105b0fbf3 | ||
|
|
5beea7de40 | ||
|
|
fdbe502524 | ||
|
|
c769a476a2 | ||
|
|
7cc53aedc7 | ||
|
|
711137da96 | ||
|
|
6071eb1b02 | ||
|
|
c9cd043657 | ||
|
|
6dd62c94c9 | ||
|
|
4c998312aa | ||
|
|
22701830c2 | ||
|
|
47a037368c | ||
|
|
191e8761d5 | ||
|
|
0d74366592 | ||
|
|
0224ce654c | ||
|
|
aa240c6d83 | ||
|
|
d216dcc7a3 | ||
|
|
4250f1b44a | ||
|
|
a852cad15e | ||
|
|
19fd3dd9cc | ||
|
|
c69195fe06 | ||
|
|
ae4f366b05 | ||
|
|
f96d7ce3e1 | ||
|
|
530993854f | ||
|
|
e2e023d2bc | ||
|
|
5df9d418c9 | ||
|
|
2718402e96 | ||
|
|
1a8288c95f | ||
|
|
f015be63ec | ||
|
|
79e876126c | ||
|
|
903a07c1d4 | ||
|
|
af20fa418a | ||
|
|
b314138caf | ||
|
|
35642d1c54 | ||
|
|
6b8107504e | ||
|
|
7639aaf08d | ||
|
|
69ee3115b6 | ||
|
|
e6f77a78a7 | ||
|
|
04a985912a | ||
|
|
2288c1ae07 | ||
|
|
395a0c557e | ||
|
|
da593f9510 | ||
|
|
7bddc6b5a6 | ||
|
|
3b85604b41 | ||
|
|
a8c2011445 | ||
|
|
ded49bdb7b | ||
|
|
369347ce54 | ||
|
|
44f04b55e8 | ||
|
|
85c2146760 | ||
|
|
96ccb4f333 | ||
|
|
95a905e1b5 | ||
|
|
f7ccb67b02 | ||
|
|
4df08eadbd | ||
|
|
6d776097c8 | ||
|
|
9f7962a6cd | ||
|
|
8c9befb15d | ||
|
|
3f869a4cd7 | ||
|
|
2263e898e5 | ||
|
|
9ab57ba037 | ||
|
|
7806d4ec04 | ||
|
|
d31b81a21d | ||
|
|
c268ce419a | ||
|
|
61b6e67610 | ||
|
|
dddf5d2e2d | ||
|
|
ed272d29f8 | ||
|
|
21f5b24cbf | ||
|
|
9b733010ab | ||
|
|
80d5bd7628 | ||
|
|
4a195a923a | ||
|
|
f726f8cfa4 | ||
|
|
e468454464 | ||
|
|
d1c96cd71f | ||
|
|
1b00b5e2a4 | ||
|
|
cfb48df1ef | ||
|
|
ba29d8354f | ||
|
|
0908507a7a | ||
|
|
860c90394d |
3139
Cargo.lock
generated
3139
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -10,6 +10,8 @@ members = [
|
|||||||
"crates/wzp-client",
|
"crates/wzp-client",
|
||||||
"crates/wzp-web",
|
"crates/wzp-web",
|
||||||
"crates/wzp-android",
|
"crates/wzp-android",
|
||||||
|
"crates/wzp-native",
|
||||||
|
"desktop/src-tauri",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@@ -53,3 +55,37 @@ wzp-fec = { path = "crates/wzp-fec" }
|
|||||||
wzp-crypto = { path = "crates/wzp-crypto" }
|
wzp-crypto = { path = "crates/wzp-crypto" }
|
||||||
wzp-transport = { path = "crates/wzp-transport" }
|
wzp-transport = { path = "crates/wzp-transport" }
|
||||||
wzp-client = { path = "crates/wzp-client" }
|
wzp-client = { path = "crates/wzp-client" }
|
||||||
|
|
||||||
|
# Fast dev profile: optimized but with debug info and incremental compilation.
|
||||||
|
# Use with: cargo run --profile dev-fast
|
||||||
|
[profile.dev-fast]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 2
|
||||||
|
|
||||||
|
# Optimize heavy compute deps even in debug builds —
|
||||||
|
# real-time audio needs < 20ms per frame, impossible unoptimized.
|
||||||
|
[profile.dev.package.nnnoiseless]
|
||||||
|
opt-level = 3
|
||||||
|
[profile.dev.package.audiopus_sys]
|
||||||
|
opt-level = 3
|
||||||
|
[profile.dev.package.audiopus]
|
||||||
|
opt-level = 3
|
||||||
|
[profile.dev.package.raptorq]
|
||||||
|
opt-level = 3
|
||||||
|
[profile.dev.package.wzp-codec]
|
||||||
|
opt-level = 3
|
||||||
|
[profile.dev.package.wzp-fec]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
# Vendored audiopus_sys with a patched opus/CMakeLists.txt that distinguishes
|
||||||
|
# real cl.exe (MSVC) from clang-cl (used by cargo-xwin for Windows cross-
|
||||||
|
# compiles). Upstream libopus 1.3.1 gates its `-msse4.1` per-file compile
|
||||||
|
# flags on `if(NOT MSVC)`, which is false under clang-cl because CMake sets
|
||||||
|
# MSVC=1 for both compilers — resulting in SSE4.1 source files compiled
|
||||||
|
# without the required target feature and hard failures in silk/NSQ_sse4_1.c.
|
||||||
|
# The vendored copy introduces an `MSVC_CL` var (true only for real cl.exe)
|
||||||
|
# and flips the SIMD guards to use it, restoring per-file SIMD flags for
|
||||||
|
# clang-cl. See vendor/audiopus_sys/opus/CMakeLists.txt for the full diff
|
||||||
|
# and rationale, plus xiph/opus#256 / xiph/opus PR #257 upstream.
|
||||||
|
[patch.crates-io]
|
||||||
|
audiopus_sys = { path = "vendor/audiopus_sys" }
|
||||||
|
|||||||
@@ -23,10 +23,71 @@ serde_json = "1"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||||
cpal = { version = "0.15", optional = true }
|
cpal = { version = "0.15", optional = true }
|
||||||
|
libc = "0.2"
|
||||||
|
|
||||||
|
# coreaudio-rs is Apple-framework-only; gate it to macOS so enabling
|
||||||
|
# the `vpio` feature from a non-macOS target builds cleanly instead of
|
||||||
|
# pulling in a crate that can only link against Apple frameworks.
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
coreaudio-rs = { version = "0.11", optional = true }
|
||||||
|
|
||||||
|
# Windows-only: direct WASAPI bindings for the `windows-aec` feature.
|
||||||
|
# `windows` is Microsoft's official Rust COM bindings crate. We pull in
|
||||||
|
# only the audio + COM subfeatures we need — the crate is organized as
|
||||||
|
# a massive optional-feature tree, so enabling just these keeps compile
|
||||||
|
# times reasonable (~5s for these features vs ~60s for the full crate).
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
windows = { version = "0.58", optional = true, features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Media_Audio",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_System_Com",
|
||||||
|
"Win32_System_Com_StructuredStorage",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_Variant",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# Linux-only: WebRTC AEC (Audio Processing Module) bindings for the
|
||||||
|
# `linux-aec` feature. This is the 0.3.x line of the `tonarino/
|
||||||
|
# webrtc-audio-processing` crate, which links against Debian's
|
||||||
|
# `libwebrtc-audio-processing-dev` apt package (0.3-1+b1 on Bookworm).
|
||||||
|
#
|
||||||
|
# Note: we attempted the 2.x line with its `bundled` sub-feature first
|
||||||
|
# (which would give us AEC3 instead of AEC2), but both the crates.io
|
||||||
|
# tarball AND the upstream git `main` branch of webrtc-audio-processing-sys
|
||||||
|
# 2.0.3 hit a `meson setup --reconfigure` bug where the build.rs passes
|
||||||
|
# --reconfigure unconditionally even on first-run empty build dirs,
|
||||||
|
# causing the bundled build to fail with "Directory does not contain a
|
||||||
|
# valid build tree". The 0.x line doesn't use bundled mode and sidesteps
|
||||||
|
# this entirely by linking the apt-provided library. AEC2 is older than
|
||||||
|
# AEC3 but still the same algorithm family — this is what PulseAudio's
|
||||||
|
# module-echo-cancel and PipeWire's filter-chain use by default on
|
||||||
|
# current Debian-family distros.
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
webrtc-audio-processing = { version = "0.3", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
audio = ["cpal"]
|
audio = ["cpal"]
|
||||||
|
# vpio enables coreaudio-rs but that dep is itself gated to macOS above,
|
||||||
|
# so enabling this feature on Windows/Linux is a no-op (the audio_vpio
|
||||||
|
# module is also #[cfg(target_os = "macos")] in lib.rs).
|
||||||
|
vpio = ["dep:coreaudio-rs"]
|
||||||
|
# windows-aec enables a direct WASAPI capture backend that opens the
|
||||||
|
# microphone under AudioCategory_Communications, turning on Windows's
|
||||||
|
# OS-level communications audio processing (AEC + noise suppression +
|
||||||
|
# AGC). The `windows` dep is itself target-gated to Windows above, so
|
||||||
|
# enabling this feature on non-Windows targets is a no-op (the
|
||||||
|
# audio_wasapi module is also #[cfg(target_os = "windows")] in lib.rs).
|
||||||
|
windows-aec = ["dep:windows"]
|
||||||
|
# linux-aec enables a CPAL + WebRTC AEC3 capture/playback backend that
|
||||||
|
# runs the WebRTC Audio Processing Module (same algo as Chrome / Zoom /
|
||||||
|
# Teams) in-process, using the playback PCM as the reference signal for
|
||||||
|
# echo cancellation. The webrtc-audio-processing dep is target-gated to
|
||||||
|
# Linux above, so enabling this feature on non-Linux targets is a no-op
|
||||||
|
# (the audio_linux_aec module is also #[cfg(target_os = "linux")] in
|
||||||
|
# lib.rs).
|
||||||
|
linux-aec = ["dep:webrtc-audio-processing"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "wzp-client"
|
name = "wzp-client"
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
//! Both structs use 48 kHz, mono, i16 format to match the WarzonePhone codec
|
//! Both structs use 48 kHz, mono, i16 format to match the WarzonePhone codec
|
||||||
//! pipeline. Frames are 960 samples (20 ms at 48 kHz).
|
//! pipeline. Frames are 960 samples (20 ms at 48 kHz).
|
||||||
//!
|
//!
|
||||||
//! The cpal `Stream` type is not `Send`, so each struct spawns a dedicated OS
|
//! Audio callbacks are **lock-free**: they read/write directly to an `AudioRing`
|
||||||
//! thread that owns the stream. The public API exposes only `Send + Sync`
|
//! (atomic SPSC ring buffer). No Mutex, no channel, no allocation on the hot path.
|
||||||
//! channel handles.
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::mpsc;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
@@ -16,6 +14,8 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|||||||
use cpal::{SampleFormat, SampleRate, StreamConfig};
|
use cpal::{SampleFormat, SampleRate, StreamConfig};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::audio_ring::AudioRing;
|
||||||
|
|
||||||
/// Number of samples per 20 ms frame at 48 kHz mono.
|
/// Number of samples per 20 ms frame at 48 kHz mono.
|
||||||
pub const FRAME_SAMPLES: usize = 960;
|
pub const FRAME_SAMPLES: usize = 960;
|
||||||
|
|
||||||
@@ -23,22 +23,24 @@ pub const FRAME_SAMPLES: usize = 960;
|
|||||||
// AudioCapture
|
// AudioCapture
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Captures microphone input and yields 960-sample PCM frames.
|
/// Captures microphone input via CPAL and writes PCM into a lock-free ring buffer.
|
||||||
///
|
///
|
||||||
/// The cpal stream lives on a dedicated OS thread; this handle is `Send + Sync`.
|
/// The cpal stream lives on a dedicated OS thread; this handle is `Send + Sync`.
|
||||||
pub struct AudioCapture {
|
pub struct AudioCapture {
|
||||||
rx: mpsc::Receiver<Vec<i16>>,
|
ring: Arc<AudioRing>,
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioCapture {
|
impl AudioCapture {
|
||||||
/// Create and start capturing from the default input device at 48 kHz mono.
|
/// Create and start capturing from the default input device at 48 kHz mono.
|
||||||
pub fn start() -> Result<Self, anyhow::Error> {
|
pub fn start() -> Result<Self, anyhow::Error> {
|
||||||
let (tx, rx) = mpsc::sync_channel::<Vec<i16>>(64);
|
let ring = Arc::new(AudioRing::new());
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
let running_clone = running.clone();
|
|
||||||
|
|
||||||
let (init_tx, init_rx) = mpsc::sync_channel::<Result<(), String>>(1);
|
let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<Result<(), String>>(1);
|
||||||
|
|
||||||
|
let ring_cb = ring.clone();
|
||||||
|
let running_clone = running.clone();
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("wzp-audio-capture".into())
|
.name("wzp-audio-capture".into())
|
||||||
@@ -59,53 +61,51 @@ impl AudioCapture {
|
|||||||
|
|
||||||
let use_f32 = !supports_i16_input(&device)?;
|
let use_f32 = !supports_i16_input(&device)?;
|
||||||
|
|
||||||
let buf = Arc::new(std::sync::Mutex::new(
|
|
||||||
Vec::<i16>::with_capacity(FRAME_SAMPLES),
|
|
||||||
));
|
|
||||||
let err_cb = |e: cpal::StreamError| {
|
let err_cb = |e: cpal::StreamError| {
|
||||||
warn!("input stream error: {e}");
|
warn!("input stream error: {e}");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let logged_cb_size = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
let stream = if use_f32 {
|
let stream = if use_f32 {
|
||||||
let buf = buf.clone();
|
let ring = ring_cb.clone();
|
||||||
let tx = tx.clone();
|
|
||||||
let running = running_clone.clone();
|
let running = running_clone.clone();
|
||||||
|
let logged = logged_cb_size.clone();
|
||||||
device.build_input_stream(
|
device.build_input_stream(
|
||||||
&config,
|
&config,
|
||||||
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
||||||
if !running.load(Ordering::Relaxed) {
|
if !running.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut lock = buf.lock().unwrap();
|
if !logged.swap(true, Ordering::Relaxed) {
|
||||||
for &s in data {
|
eprintln!("[audio] capture callback: {} f32 samples", data.len());
|
||||||
lock.push(f32_to_i16(s));
|
}
|
||||||
if lock.len() == FRAME_SAMPLES {
|
let mut tmp = [0i16; FRAME_SAMPLES];
|
||||||
let frame = lock.drain(..).collect();
|
for chunk in data.chunks(FRAME_SAMPLES) {
|
||||||
let _ = tx.try_send(frame);
|
let n = chunk.len();
|
||||||
|
for i in 0..n {
|
||||||
|
tmp[i] = f32_to_i16(chunk[i]);
|
||||||
}
|
}
|
||||||
|
ring.write(&tmp[..n]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err_cb,
|
err_cb,
|
||||||
None,
|
None,
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
let buf = buf.clone();
|
let ring = ring_cb.clone();
|
||||||
let tx = tx.clone();
|
|
||||||
let running = running_clone.clone();
|
let running = running_clone.clone();
|
||||||
|
let logged = logged_cb_size.clone();
|
||||||
device.build_input_stream(
|
device.build_input_stream(
|
||||||
&config,
|
&config,
|
||||||
move |data: &[i16], _: &cpal::InputCallbackInfo| {
|
move |data: &[i16], _: &cpal::InputCallbackInfo| {
|
||||||
if !running.load(Ordering::Relaxed) {
|
if !running.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut lock = buf.lock().unwrap();
|
if !logged.swap(true, Ordering::Relaxed) {
|
||||||
for &s in data {
|
eprintln!("[audio] capture callback: {} i16 samples", data.len());
|
||||||
lock.push(s);
|
|
||||||
if lock.len() == FRAME_SAMPLES {
|
|
||||||
let frame = lock.drain(..).collect();
|
|
||||||
let _ = tx.try_send(frame);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ring.write(data);
|
||||||
},
|
},
|
||||||
err_cb,
|
err_cb,
|
||||||
None,
|
None,
|
||||||
@@ -114,7 +114,6 @@ impl AudioCapture {
|
|||||||
|
|
||||||
stream.play().context("failed to start input stream")?;
|
stream.play().context("failed to start input stream")?;
|
||||||
|
|
||||||
// Signal success to the caller before parking.
|
|
||||||
let _ = init_tx.send(Ok(()));
|
let _ = init_tx.send(Ok(()));
|
||||||
|
|
||||||
// Keep stream alive until stopped.
|
// Keep stream alive until stopped.
|
||||||
@@ -135,15 +134,12 @@ impl AudioCapture {
|
|||||||
.map_err(|_| anyhow!("capture thread exited before signaling"))?
|
.map_err(|_| anyhow!("capture thread exited before signaling"))?
|
||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
Ok(Self { rx, running })
|
Ok(Self { ring, running })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the next frame of 960 PCM samples (blocking until available).
|
/// Get a reference to the capture ring buffer for direct polling.
|
||||||
///
|
pub fn ring(&self) -> &Arc<AudioRing> {
|
||||||
/// Returns `None` when the stream has been stopped or the channel is
|
&self.ring
|
||||||
/// disconnected.
|
|
||||||
pub fn read_frame(&self) -> Option<Vec<i16>> {
|
|
||||||
self.rx.recv().ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop capturing.
|
/// Stop capturing.
|
||||||
@@ -152,26 +148,34 @@ impl AudioCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for AudioCapture {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// AudioPlayback
|
// AudioPlayback
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Plays PCM frames through the default output device at 48 kHz mono.
|
/// Plays PCM through the default output device, reading from a lock-free ring buffer.
|
||||||
///
|
///
|
||||||
/// The cpal stream lives on a dedicated OS thread; this handle is `Send + Sync`.
|
/// The cpal stream lives on a dedicated OS thread; this handle is `Send + Sync`.
|
||||||
pub struct AudioPlayback {
|
pub struct AudioPlayback {
|
||||||
tx: mpsc::SyncSender<Vec<i16>>,
|
ring: Arc<AudioRing>,
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioPlayback {
|
impl AudioPlayback {
|
||||||
/// Create and start playback on the default output device at 48 kHz mono.
|
/// Create and start playback on the default output device at 48 kHz mono.
|
||||||
pub fn start() -> Result<Self, anyhow::Error> {
|
pub fn start() -> Result<Self, anyhow::Error> {
|
||||||
let (tx, rx) = mpsc::sync_channel::<Vec<i16>>(64);
|
let ring = Arc::new(AudioRing::new());
|
||||||
let running = Arc::new(AtomicBool::new(true));
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
let running_clone = running.clone();
|
|
||||||
|
|
||||||
let (init_tx, init_rx) = mpsc::sync_channel::<Result<(), String>>(1);
|
let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<Result<(), String>>(1);
|
||||||
|
|
||||||
|
let ring_cb = ring.clone();
|
||||||
|
let running_clone = running.clone();
|
||||||
|
|
||||||
std::thread::Builder::new()
|
std::thread::Builder::new()
|
||||||
.name("wzp-audio-playback".into())
|
.name("wzp-audio-playback".into())
|
||||||
@@ -192,62 +196,40 @@ impl AudioPlayback {
|
|||||||
|
|
||||||
let use_f32 = !supports_i16_output(&device)?;
|
let use_f32 = !supports_i16_output(&device)?;
|
||||||
|
|
||||||
// Shared ring of samples the cpal callback drains from.
|
|
||||||
let ring = Arc::new(std::sync::Mutex::new(
|
|
||||||
std::collections::VecDeque::<i16>::with_capacity(FRAME_SAMPLES * 8),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Background drainer: moves frames from the mpsc channel into the ring.
|
|
||||||
{
|
|
||||||
let ring = ring.clone();
|
|
||||||
let running = running_clone.clone();
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("wzp-playback-drain".into())
|
|
||||||
.spawn(move || {
|
|
||||||
while running.load(Ordering::Relaxed) {
|
|
||||||
match rx.recv_timeout(std::time::Duration::from_millis(100)) {
|
|
||||||
Ok(frame) => {
|
|
||||||
let mut lock = ring.lock().unwrap();
|
|
||||||
lock.extend(frame);
|
|
||||||
while lock.len() > FRAME_SAMPLES * 16 {
|
|
||||||
lock.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
|
||||||
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let err_cb = |e: cpal::StreamError| {
|
let err_cb = |e: cpal::StreamError| {
|
||||||
warn!("output stream error: {e}");
|
warn!("output stream error: {e}");
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream = if use_f32 {
|
let stream = if use_f32 {
|
||||||
let ring = ring.clone();
|
let ring = ring_cb.clone();
|
||||||
device.build_output_stream(
|
device.build_output_stream(
|
||||||
&config,
|
&config,
|
||||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||||
let mut lock = ring.lock().unwrap();
|
let mut tmp = [0i16; FRAME_SAMPLES];
|
||||||
for sample in data.iter_mut() {
|
for chunk in data.chunks_mut(FRAME_SAMPLES) {
|
||||||
*sample = match lock.pop_front() {
|
let n = chunk.len();
|
||||||
Some(s) => i16_to_f32(s),
|
let read = ring.read(&mut tmp[..n]);
|
||||||
None => 0.0,
|
for i in 0..read {
|
||||||
};
|
chunk[i] = i16_to_f32(tmp[i]);
|
||||||
|
}
|
||||||
|
// Fill remainder with silence if ring underran
|
||||||
|
for i in read..n {
|
||||||
|
chunk[i] = 0.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err_cb,
|
err_cb,
|
||||||
None,
|
None,
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
let ring = ring.clone();
|
let ring = ring_cb.clone();
|
||||||
device.build_output_stream(
|
device.build_output_stream(
|
||||||
&config,
|
&config,
|
||||||
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
|
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
|
||||||
let mut lock = ring.lock().unwrap();
|
let read = ring.read(data);
|
||||||
for sample in data.iter_mut() {
|
// Fill remainder with silence if ring underran
|
||||||
*sample = lock.pop_front().unwrap_or(0);
|
for sample in &mut data[read..] {
|
||||||
|
*sample = 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
err_cb,
|
err_cb,
|
||||||
@@ -257,7 +239,6 @@ impl AudioPlayback {
|
|||||||
|
|
||||||
stream.play().context("failed to start output stream")?;
|
stream.play().context("failed to start output stream")?;
|
||||||
|
|
||||||
// Signal success to the caller before parking.
|
|
||||||
let _ = init_tx.send(Ok(()));
|
let _ = init_tx.send(Ok(()));
|
||||||
|
|
||||||
// Keep stream alive until stopped.
|
// Keep stream alive until stopped.
|
||||||
@@ -278,12 +259,12 @@ impl AudioPlayback {
|
|||||||
.map_err(|_| anyhow!("playback thread exited before signaling"))?
|
.map_err(|_| anyhow!("playback thread exited before signaling"))?
|
||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
Ok(Self { tx, running })
|
Ok(Self { ring, running })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a frame of PCM samples for playback.
|
/// Get a reference to the playout ring buffer for direct writing.
|
||||||
pub fn write_frame(&self, pcm: &[i16]) {
|
pub fn ring(&self) -> &Arc<AudioRing> {
|
||||||
let _ = self.tx.try_send(pcm.to_vec());
|
&self.ring
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop playback.
|
/// Stop playback.
|
||||||
@@ -292,11 +273,16 @@ impl AudioPlayback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for AudioPlayback {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Check if the input device supports i16 at 48 kHz mono.
|
|
||||||
fn supports_i16_input(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
fn supports_i16_input(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
||||||
let supported = device
|
let supported = device
|
||||||
.supported_input_configs()
|
.supported_input_configs()
|
||||||
@@ -313,7 +299,6 @@ fn supports_i16_input(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the output device supports i16 at 48 kHz mono.
|
|
||||||
fn supports_i16_output(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
fn supports_i16_output(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
||||||
let supported = device
|
let supported = device
|
||||||
.supported_output_configs()
|
.supported_output_configs()
|
||||||
|
|||||||
537
crates/wzp-client/src/audio_linux_aec.rs
Normal file
537
crates/wzp-client/src/audio_linux_aec.rs
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
//! Linux AEC backend: CPAL capture + playback wired through the WebRTC Audio
|
||||||
|
//! Processing Module (AEC3 + noise suppression + high-pass filter).
|
||||||
|
//!
|
||||||
|
//! This is the same algorithm used by Chrome WebRTC, Zoom, Teams, Jitsi, and
|
||||||
|
//! any other "serious" Linux VoIP app. It runs in-process — no dependency on
|
||||||
|
//! PulseAudio's module-echo-cancel or PipeWire's filter-chain, so it works
|
||||||
|
//! identically on ALSA / PulseAudio / PipeWire systems.
|
||||||
|
//!
|
||||||
|
//! ## Architecture
|
||||||
|
//!
|
||||||
|
//! A single module-level `Arc<Mutex<Processor>>` is shared between the
|
||||||
|
//! capture and playback paths. On each 20 ms frame (960 samples @ 48 kHz
|
||||||
|
//! mono):
|
||||||
|
//!
|
||||||
|
//! - **Playback path**: `LinuxAecPlayback::start` spawns the usual CPAL
|
||||||
|
//! output thread, but wraps each chunk in a call to
|
||||||
|
//! `Processor::process_render_frame` **before** handing it to CPAL. That
|
||||||
|
//! gives APM an authoritative reference of exactly what's going out to
|
||||||
|
//! the speakers (same approach Zoom/Teams/Jitsi use). The AEC then knows
|
||||||
|
//! what to cancel when it sees echo in the capture stream.
|
||||||
|
//!
|
||||||
|
//! - **Capture path**: `LinuxAecCapture::start` spawns the usual CPAL
|
||||||
|
//! input thread, and runs `Processor::process_capture_frame` on each
|
||||||
|
//! incoming mic chunk **in place** before pushing it into the ring
|
||||||
|
//! buffer. The AEC subtracts the echo using the render reference it
|
||||||
|
//! saw on the playback side.
|
||||||
|
//!
|
||||||
|
//! APM is strict about frame size: it requires exactly 10 ms = 480 samples
|
||||||
|
//! per call at 48 kHz. Our pipeline uses 20 ms = 960 samples, so each 20 ms
|
||||||
|
//! frame is split into two 480-sample halves, APM is called twice, and the
|
||||||
|
//! halves are stitched back together.
|
||||||
|
//!
|
||||||
|
//! APM only accepts f32 samples in `[-1.0, 1.0]`, so we convert i16 → f32
|
||||||
|
//! before the call and f32 → i16 after (with clamping on the return path).
|
||||||
|
//!
|
||||||
|
//! ## Stream delay
|
||||||
|
//!
|
||||||
|
//! AEC needs to know roughly how long it takes between a sample being passed
|
||||||
|
//! to `process_render_frame` and its echo showing up at `process_capture_frame`
|
||||||
|
//! — i.e. the round trip through CPAL playback → speaker → air → microphone
|
||||||
|
//! → CPAL capture. AEC3's internal estimator tracks this within a window
|
||||||
|
//! around whatever hint we give it. We hardcode 60 ms as a reasonable
|
||||||
|
//! starting point for typical Linux audio stacks; the delay estimator does
|
||||||
|
//! the fine-tuning automatically.
|
||||||
|
//!
|
||||||
|
//! ## Thread safety
|
||||||
|
//!
|
||||||
|
//! The 0.3.x line of `webrtc-audio-processing` takes `&mut self` on both
|
||||||
|
//! `process_capture_frame` and `process_render_frame`, so the `Processor`
|
||||||
|
//! needs a `Mutex` around it for cross-thread sharing. The capture and
|
||||||
|
//! playback threads each acquire the lock briefly (sub-millisecond per
|
||||||
|
//! 10 ms frame) so contention is minimal at our frame rates.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use cpal::{SampleFormat, SampleRate, StreamConfig};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
use webrtc_audio_processing::{
|
||||||
|
Config, EchoCancellation, EchoCancellationSuppressionLevel, InitializationConfig,
|
||||||
|
NoiseSuppression, NoiseSuppressionLevel, Processor, NUM_SAMPLES_PER_FRAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::audio_ring::AudioRing;
|
||||||
|
|
||||||
|
/// 20 ms at 48 kHz, mono — matches the rest of the pipeline and the codec.
|
||||||
|
pub const FRAME_SAMPLES: usize = 960;
|
||||||
|
/// APM requires strict 10 ms frames at 48 kHz = 480 samples per call.
|
||||||
|
/// Imported from the webrtc-audio-processing crate so we can't drift out
|
||||||
|
/// of sync with whatever sample rate / frame length the C++ lib is using.
|
||||||
|
const APM_FRAME_SAMPLES: usize = NUM_SAMPLES_PER_FRAME as usize;
|
||||||
|
const APM_NUM_CHANNELS: usize = 1;
|
||||||
|
/// Round-trip delay hint passed to APM; the estimator refines from here.
|
||||||
|
/// 60 ms is a reasonable default for CPAL on ALSA / PulseAudio / PipeWire.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
const STREAM_DELAY_MS: i32 = 60;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shared APM instance
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Module-level lazily-initialized APM. Shared between capture and playback
|
||||||
|
/// so they operate on the same echo-cancellation state — the render frames
|
||||||
|
/// pushed by playback are what the capture path subtracts from the mic input.
|
||||||
|
/// Wrapped in a Mutex because the 0.3.x Processor takes `&mut self` on both
|
||||||
|
/// process_capture_frame and process_render_frame.
|
||||||
|
static PROCESSOR: OnceLock<Arc<Mutex<Processor>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn get_or_init_processor() -> anyhow::Result<Arc<Mutex<Processor>>> {
|
||||||
|
if let Some(p) = PROCESSOR.get() {
|
||||||
|
return Ok(p.clone());
|
||||||
|
}
|
||||||
|
let init_config = InitializationConfig {
|
||||||
|
num_capture_channels: APM_NUM_CHANNELS as i32,
|
||||||
|
num_render_channels: APM_NUM_CHANNELS as i32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut processor = Processor::new(&init_config)
|
||||||
|
.map_err(|e| anyhow!("webrtc APM init failed: {e:?}"))?;
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
echo_cancellation: Some(EchoCancellation {
|
||||||
|
suppression_level: EchoCancellationSuppressionLevel::High,
|
||||||
|
stream_delay_ms: Some(STREAM_DELAY_MS),
|
||||||
|
enable_delay_agnostic: true,
|
||||||
|
enable_extended_filter: true,
|
||||||
|
}),
|
||||||
|
noise_suppression: Some(NoiseSuppression {
|
||||||
|
suppression_level: NoiseSuppressionLevel::High,
|
||||||
|
}),
|
||||||
|
enable_high_pass_filter: true,
|
||||||
|
// AGC left off for now — it can fight the Opus encoder's own gain
|
||||||
|
// staging and the adaptive-quality controller. Add later if users
|
||||||
|
// report low mic levels.
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
processor.set_config(config);
|
||||||
|
|
||||||
|
let arc = Arc::new(Mutex::new(processor));
|
||||||
|
let _ = PROCESSOR.set(arc.clone());
|
||||||
|
info!(
|
||||||
|
stream_delay_ms = STREAM_DELAY_MS,
|
||||||
|
"webrtc APM initialized (AEC High + NS High + HPF, AGC off)"
|
||||||
|
);
|
||||||
|
Ok(arc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers: i16 ↔ f32 and APM frame processing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn i16_to_f32(s: i16) -> f32 {
|
||||||
|
s as f32 / 32768.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn f32_to_i16(s: f32) -> i16 {
|
||||||
|
(s.clamp(-1.0, 1.0) * 32767.0) as i16
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed a 20 ms (960-sample) playback frame to APM as the render reference.
|
||||||
|
/// Splits into two 10 ms halves because APM is strict about frame size.
|
||||||
|
/// Takes the Mutex-wrapped Processor and locks briefly around each call.
|
||||||
|
fn push_render_frame_20ms(apm: &Mutex<Processor>, pcm: &[i16]) {
|
||||||
|
debug_assert_eq!(pcm.len(), FRAME_SAMPLES);
|
||||||
|
let mut buf = [0f32; APM_FRAME_SAMPLES];
|
||||||
|
for half in pcm.chunks_exact(APM_FRAME_SAMPLES) {
|
||||||
|
for (i, &s) in half.iter().enumerate() {
|
||||||
|
buf[i] = i16_to_f32(s);
|
||||||
|
}
|
||||||
|
match apm.lock() {
|
||||||
|
Ok(mut p) => {
|
||||||
|
if let Err(e) = p.process_render_frame(&mut buf) {
|
||||||
|
warn!("webrtc APM process_render_frame failed: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("webrtc APM mutex poisoned in render path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a 20 ms (960-sample) capture frame through APM's echo cancellation
|
||||||
|
/// in place. Splits into two 10 ms halves, runs APM on each, stitches
|
||||||
|
/// results back into the caller's buffer. Briefly holds the Mutex once
|
||||||
|
/// per 10 ms half.
|
||||||
|
fn process_capture_frame_20ms(apm: &Mutex<Processor>, pcm: &mut [i16]) {
|
||||||
|
debug_assert_eq!(pcm.len(), FRAME_SAMPLES);
|
||||||
|
let mut buf = [0f32; APM_FRAME_SAMPLES];
|
||||||
|
for half in pcm.chunks_exact_mut(APM_FRAME_SAMPLES) {
|
||||||
|
for (i, &s) in half.iter().enumerate() {
|
||||||
|
buf[i] = i16_to_f32(s);
|
||||||
|
}
|
||||||
|
match apm.lock() {
|
||||||
|
Ok(mut p) => {
|
||||||
|
if let Err(e) = p.process_capture_frame(&mut buf) {
|
||||||
|
warn!("webrtc APM process_capture_frame failed: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("webrtc APM mutex poisoned in capture path");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i, d) in half.iter_mut().enumerate() {
|
||||||
|
*d = f32_to_i16(buf[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// LinuxAecCapture — CPAL mic + WebRTC AEC capture-side processing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Microphone capture with WebRTC AEC3 applied in place before the codec
|
||||||
|
/// sees the samples. Mirrors the public API of `audio_io::AudioCapture` so
|
||||||
|
/// downstream code doesn't change.
|
||||||
|
pub struct LinuxAecCapture {
|
||||||
|
ring: Arc<AudioRing>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxAecCapture {
|
||||||
|
pub fn start() -> Result<Self, anyhow::Error> {
|
||||||
|
// Eagerly init the APM so the playback side can find it already
|
||||||
|
// configured, and so init errors surface on the caller thread
|
||||||
|
// instead of silently failing inside the capture thread.
|
||||||
|
let apm = get_or_init_processor()?;
|
||||||
|
|
||||||
|
let ring = Arc::new(AudioRing::new());
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<Result<(), String>>(1);
|
||||||
|
|
||||||
|
let ring_cb = ring.clone();
|
||||||
|
let running_clone = running.clone();
|
||||||
|
let apm_capture = apm.clone();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("wzp-audio-capture-linuxaec".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let result = (|| -> Result<(), anyhow::Error> {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let device = host
|
||||||
|
.default_input_device()
|
||||||
|
.ok_or_else(|| anyhow!("no default input audio device found"))?;
|
||||||
|
info!(device = %device.name().unwrap_or_default(), "LinuxAEC: using input device");
|
||||||
|
|
||||||
|
let config = StreamConfig {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate: SampleRate(48_000),
|
||||||
|
buffer_size: cpal::BufferSize::Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
let use_f32 = !supports_i16_input(&device)?;
|
||||||
|
|
||||||
|
let err_cb = |e: cpal::StreamError| {
|
||||||
|
warn!("LinuxAEC input stream error: {e}");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Leftover buffer for when CPAL gives us partial frames.
|
||||||
|
// We need exactly 960-sample chunks to feed APM.
|
||||||
|
let leftover = std::sync::Mutex::new(Vec::<i16>::with_capacity(FRAME_SAMPLES * 4));
|
||||||
|
|
||||||
|
let stream = if use_f32 {
|
||||||
|
let ring = ring_cb.clone();
|
||||||
|
let running = running_clone.clone();
|
||||||
|
let apm = apm_capture.clone();
|
||||||
|
device.build_input_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
||||||
|
if !running.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut lv = leftover.lock().unwrap();
|
||||||
|
lv.reserve(data.len());
|
||||||
|
for &s in data {
|
||||||
|
lv.push(f32_to_i16(s));
|
||||||
|
}
|
||||||
|
drain_frames_through_apm(&mut lv, &apm, &ring);
|
||||||
|
},
|
||||||
|
err_cb,
|
||||||
|
None,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
let ring = ring_cb.clone();
|
||||||
|
let running = running_clone.clone();
|
||||||
|
let apm = apm_capture.clone();
|
||||||
|
device.build_input_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &[i16], _: &cpal::InputCallbackInfo| {
|
||||||
|
if !running.load(Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut lv = leftover.lock().unwrap();
|
||||||
|
lv.extend_from_slice(data);
|
||||||
|
drain_frames_through_apm(&mut lv, &apm, &ring);
|
||||||
|
},
|
||||||
|
err_cb,
|
||||||
|
None,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
stream.play().context("failed to start LinuxAEC input stream")?;
|
||||||
|
let _ = init_tx.send(Ok(()));
|
||||||
|
info!("LinuxAEC capture started (AEC3 active)");
|
||||||
|
|
||||||
|
while running_clone.load(Ordering::Relaxed) {
|
||||||
|
std::thread::park_timeout(std::time::Duration::from_millis(200));
|
||||||
|
}
|
||||||
|
drop(stream);
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
let _ = init_tx.send(Err(e.to_string()));
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
init_rx
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| anyhow!("LinuxAEC capture thread exited before signaling"))?
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
Ok(Self { ring, running })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ring(&self) -> &Arc<AudioRing> {
|
||||||
|
&self.ring
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.running.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LinuxAecCapture {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull whole 960-sample frames out of the leftover buffer, run them through
|
||||||
|
/// APM's capture-side processing, and push to the ring. Leaves any partial
|
||||||
|
/// sub-960 remainder in `leftover` for the next callback.
|
||||||
|
fn drain_frames_through_apm(leftover: &mut Vec<i16>, apm: &Mutex<Processor>, ring: &AudioRing) {
|
||||||
|
let mut frame = [0i16; FRAME_SAMPLES];
|
||||||
|
while leftover.len() >= FRAME_SAMPLES {
|
||||||
|
frame.copy_from_slice(&leftover[..FRAME_SAMPLES]);
|
||||||
|
process_capture_frame_20ms(apm, &mut frame);
|
||||||
|
ring.write(&frame);
|
||||||
|
leftover.drain(..FRAME_SAMPLES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// LinuxAecPlayback — CPAL speaker output + WebRTC AEC render-side tee
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Speaker playback with a render-side tee: each frame written to CPAL is
|
||||||
|
/// ALSO fed to APM via `process_render_frame` as the echo-cancellation
|
||||||
|
/// reference signal. This is the "tee the playback ring" approach (Zoom,
|
||||||
|
/// Teams, Jitsi) — deterministic, does not depend on PulseAudio loopback or
|
||||||
|
/// PipeWire monitor sources.
|
||||||
|
pub struct LinuxAecPlayback {
|
||||||
|
ring: Arc<AudioRing>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxAecPlayback {
|
||||||
|
pub fn start() -> Result<Self, anyhow::Error> {
|
||||||
|
let apm = get_or_init_processor()?;
|
||||||
|
|
||||||
|
let ring = Arc::new(AudioRing::new());
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<Result<(), String>>(1);
|
||||||
|
|
||||||
|
let ring_cb = ring.clone();
|
||||||
|
let running_clone = running.clone();
|
||||||
|
let apm_render = apm.clone();
|
||||||
|
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("wzp-audio-playback-linuxaec".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let result = (|| -> Result<(), anyhow::Error> {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let device = host
|
||||||
|
.default_output_device()
|
||||||
|
.ok_or_else(|| anyhow!("no default output audio device found"))?;
|
||||||
|
info!(device = %device.name().unwrap_or_default(), "LinuxAEC: using output device");
|
||||||
|
|
||||||
|
let config = StreamConfig {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate: SampleRate(48_000),
|
||||||
|
buffer_size: cpal::BufferSize::Default,
|
||||||
|
};
|
||||||
|
|
||||||
|
let use_f32 = !supports_i16_output(&device)?;
|
||||||
|
|
||||||
|
let err_cb = |e: cpal::StreamError| {
|
||||||
|
warn!("LinuxAEC output stream error: {e}");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same 960-sample batching approach as the capture side:
|
||||||
|
// CPAL may ask for N samples in a callback where N doesn't
|
||||||
|
// divide 960. We accumulate partial frames in a Vec and
|
||||||
|
// feed APM as soon as we have a whole 20 ms frame.
|
||||||
|
let carry = std::sync::Mutex::new(Vec::<i16>::with_capacity(FRAME_SAMPLES * 4));
|
||||||
|
|
||||||
|
let stream = if use_f32 {
|
||||||
|
let ring = ring_cb.clone();
|
||||||
|
let apm = apm_render.clone();
|
||||||
|
device.build_output_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||||
|
fill_output_and_tee_f32(data, &ring, &apm, &carry);
|
||||||
|
},
|
||||||
|
err_cb,
|
||||||
|
None,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
let ring = ring_cb.clone();
|
||||||
|
let apm = apm_render.clone();
|
||||||
|
device.build_output_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
|
||||||
|
fill_output_and_tee_i16(data, &ring, &apm, &carry);
|
||||||
|
},
|
||||||
|
err_cb,
|
||||||
|
None,
|
||||||
|
)?
|
||||||
|
};
|
||||||
|
|
||||||
|
stream.play().context("failed to start LinuxAEC output stream")?;
|
||||||
|
let _ = init_tx.send(Ok(()));
|
||||||
|
info!("LinuxAEC playback started (render tee active)");
|
||||||
|
|
||||||
|
while running_clone.load(Ordering::Relaxed) {
|
||||||
|
std::thread::park_timeout(std::time::Duration::from_millis(200));
|
||||||
|
}
|
||||||
|
drop(stream);
|
||||||
|
Ok(())
|
||||||
|
})();
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
let _ = init_tx.send(Err(e.to_string()));
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
init_rx
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| anyhow!("LinuxAEC playback thread exited before signaling"))?
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
Ok(Self { ring, running })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ring(&self) -> &Arc<AudioRing> {
|
||||||
|
&self.ring
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.running.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LinuxAecPlayback {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_output_and_tee_i16(
|
||||||
|
data: &mut [i16],
|
||||||
|
ring: &AudioRing,
|
||||||
|
apm: &Mutex<Processor>,
|
||||||
|
carry: &std::sync::Mutex<Vec<i16>>,
|
||||||
|
) {
|
||||||
|
let read = ring.read(data);
|
||||||
|
for s in &mut data[read..] {
|
||||||
|
*s = 0;
|
||||||
|
}
|
||||||
|
tee_render_samples(data, apm, carry);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_output_and_tee_f32(
|
||||||
|
data: &mut [f32],
|
||||||
|
ring: &AudioRing,
|
||||||
|
apm: &Mutex<Processor>,
|
||||||
|
carry: &std::sync::Mutex<Vec<i16>>,
|
||||||
|
) {
|
||||||
|
let mut tmp = vec![0i16; data.len()];
|
||||||
|
let read = ring.read(&mut tmp);
|
||||||
|
for s in &mut tmp[read..] {
|
||||||
|
*s = 0;
|
||||||
|
}
|
||||||
|
for (d, &s) in data.iter_mut().zip(tmp.iter()) {
|
||||||
|
*d = i16_to_f32(s);
|
||||||
|
}
|
||||||
|
tee_render_samples(&tmp, apm, carry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push CPAL-bound samples into APM's render-side input for echo cancellation.
|
||||||
|
/// Uses a carry buffer to batch into exact 960-sample (20 ms) frames.
|
||||||
|
fn tee_render_samples(samples: &[i16], apm: &Mutex<Processor>, carry: &std::sync::Mutex<Vec<i16>>) {
|
||||||
|
let mut lv = carry.lock().unwrap();
|
||||||
|
lv.extend_from_slice(samples);
|
||||||
|
while lv.len() >= FRAME_SAMPLES {
|
||||||
|
let mut frame = [0i16; FRAME_SAMPLES];
|
||||||
|
frame.copy_from_slice(&lv[..FRAME_SAMPLES]);
|
||||||
|
push_render_frame_20ms(apm, &frame);
|
||||||
|
lv.drain(..FRAME_SAMPLES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CPAL format helpers (duplicated from audio_io.rs to keep the modules
|
||||||
|
// independent — each backend file is a self-contained unit)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn supports_i16_input(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
||||||
|
let supported = device
|
||||||
|
.supported_input_configs()
|
||||||
|
.context("failed to query input configs")?;
|
||||||
|
for cfg in supported {
|
||||||
|
if cfg.sample_format() == SampleFormat::I16
|
||||||
|
&& cfg.min_sample_rate() <= SampleRate(48_000)
|
||||||
|
&& cfg.max_sample_rate() >= SampleRate(48_000)
|
||||||
|
&& cfg.channels() >= 1
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_i16_output(device: &cpal::Device) -> Result<bool, anyhow::Error> {
|
||||||
|
let supported = device
|
||||||
|
.supported_output_configs()
|
||||||
|
.context("failed to query output configs")?;
|
||||||
|
for cfg in supported {
|
||||||
|
if cfg.sample_format() == SampleFormat::I16
|
||||||
|
&& cfg.min_sample_rate() <= SampleRate(48_000)
|
||||||
|
&& cfg.max_sample_rate() >= SampleRate(48_000)
|
||||||
|
&& cfg.channels() >= 1
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
122
crates/wzp-client/src/audio_ring.rs
Normal file
122
crates/wzp-client/src/audio_ring.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
//! Lock-free SPSC ring buffer — "Reader-Detects-Lap" architecture.
|
||||||
|
//!
|
||||||
|
//! SPSC invariant: the producer ONLY writes `write_pos`, the consumer
|
||||||
|
//! ONLY writes `read_pos`. Neither thread touches the other's cursor.
|
||||||
|
//!
|
||||||
|
//! On overflow (writer laps the reader), the writer simply overwrites
|
||||||
|
//! old buffer data. The reader detects the lap via `available() >
|
||||||
|
//! RING_CAPACITY` and snaps its own `read_pos` forward.
|
||||||
|
//!
|
||||||
|
//! Capacity is a power of 2 for bitmask indexing (no modulo).
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
/// Ring buffer capacity — power of 2 for bitmask indexing.
|
||||||
|
/// 16384 samples = 341.3ms at 48kHz mono.
|
||||||
|
const RING_CAPACITY: usize = 16384; // 2^14
|
||||||
|
const RING_MASK: usize = RING_CAPACITY - 1;
|
||||||
|
|
||||||
|
/// Lock-free single-producer single-consumer ring buffer for i16 PCM samples.
|
||||||
|
pub struct AudioRing {
|
||||||
|
buf: Box<[i16]>,
|
||||||
|
/// Monotonically increasing write cursor. ONLY written by producer.
|
||||||
|
write_pos: AtomicUsize,
|
||||||
|
/// Monotonically increasing read cursor. ONLY written by consumer.
|
||||||
|
read_pos: AtomicUsize,
|
||||||
|
/// Incremented by reader when it detects it was lapped (overflow).
|
||||||
|
overflow_count: AtomicU64,
|
||||||
|
/// Incremented by reader when ring is empty (underrun).
|
||||||
|
underrun_count: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: AudioRing is SPSC — one thread writes (producer), one reads (consumer).
|
||||||
|
// The producer only writes write_pos. The consumer only writes read_pos.
|
||||||
|
// Neither thread writes the other's cursor. Buffer indices are derived from
|
||||||
|
// the owning thread's cursor, ensuring no concurrent access to the same index.
|
||||||
|
unsafe impl Send for AudioRing {}
|
||||||
|
unsafe impl Sync for AudioRing {}
|
||||||
|
|
||||||
|
impl AudioRing {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
debug_assert!(RING_CAPACITY.is_power_of_two());
|
||||||
|
Self {
|
||||||
|
buf: vec![0i16; RING_CAPACITY].into_boxed_slice(),
|
||||||
|
write_pos: AtomicUsize::new(0),
|
||||||
|
read_pos: AtomicUsize::new(0),
|
||||||
|
overflow_count: AtomicU64::new(0),
|
||||||
|
underrun_count: AtomicU64::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of samples available to read (clamped to capacity).
|
||||||
|
pub fn available(&self) -> usize {
|
||||||
|
let w = self.write_pos.load(Ordering::Acquire);
|
||||||
|
let r = self.read_pos.load(Ordering::Relaxed);
|
||||||
|
w.wrapping_sub(r).min(RING_CAPACITY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write samples into the ring. Returns number of samples written.
|
||||||
|
///
|
||||||
|
/// If the ring is full, old data is silently overwritten. The reader
|
||||||
|
/// will detect the lap and self-correct. The writer NEVER touches
|
||||||
|
/// `read_pos`.
|
||||||
|
pub fn write(&self, samples: &[i16]) -> usize {
|
||||||
|
let count = samples.len().min(RING_CAPACITY);
|
||||||
|
let w = self.write_pos.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
unsafe {
|
||||||
|
let ptr = self.buf.as_ptr() as *mut i16;
|
||||||
|
*ptr.add((w + i) & RING_MASK) = samples[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write_pos
|
||||||
|
.store(w.wrapping_add(count), Ordering::Release);
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read samples from the ring into `out`. Returns number of samples read.
|
||||||
|
///
|
||||||
|
/// If the writer has lapped the reader (overflow), `read_pos` is snapped
|
||||||
|
/// forward to the oldest valid data.
|
||||||
|
pub fn read(&self, out: &mut [i16]) -> usize {
|
||||||
|
let w = self.write_pos.load(Ordering::Acquire);
|
||||||
|
let mut r = self.read_pos.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
let mut avail = w.wrapping_sub(r);
|
||||||
|
|
||||||
|
// Lap detection: writer has overwritten our unread data.
|
||||||
|
if avail > RING_CAPACITY {
|
||||||
|
r = w.wrapping_sub(RING_CAPACITY);
|
||||||
|
avail = RING_CAPACITY;
|
||||||
|
self.overflow_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = out.len().min(avail);
|
||||||
|
if count == 0 {
|
||||||
|
if w == r {
|
||||||
|
self.underrun_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
out[i] = unsafe { *self.buf.as_ptr().add((r + i) & RING_MASK) };
|
||||||
|
}
|
||||||
|
|
||||||
|
self.read_pos
|
||||||
|
.store(r.wrapping_add(count), Ordering::Release);
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of overflow events (reader was lapped by writer).
|
||||||
|
pub fn overflow_count(&self) -> u64 {
|
||||||
|
self.overflow_count.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of underrun events (reader found empty buffer).
|
||||||
|
pub fn underrun_count(&self) -> u64 {
|
||||||
|
self.underrun_count.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
179
crates/wzp-client/src/audio_vpio.rs
Normal file
179
crates/wzp-client/src/audio_vpio.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
//! macOS Voice Processing I/O — uses Apple's VoiceProcessingIO audio unit
|
||||||
|
//! for hardware-accelerated echo cancellation, AGC, and noise suppression.
|
||||||
|
//!
|
||||||
|
//! VoiceProcessingIO is a combined input+output unit that knows what's going
|
||||||
|
//! to the speaker, so it can cancel the echo from the mic signal internally.
|
||||||
|
//! This is the same engine FaceTime and other Apple apps use.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use coreaudio::audio_unit::audio_format::LinearPcmFlags;
|
||||||
|
use coreaudio::audio_unit::render_callback::{self, data};
|
||||||
|
use coreaudio::audio_unit::{AudioUnit, Element, IOType, SampleFormat, Scope, StreamFormat};
|
||||||
|
use coreaudio::sys;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::audio_ring::AudioRing;
|
||||||
|
|
||||||
|
/// Number of samples per 20 ms frame at 48 kHz mono.
|
||||||
|
pub const FRAME_SAMPLES: usize = 960;
|
||||||
|
|
||||||
|
/// Combined capture + playback via macOS VoiceProcessingIO.
|
||||||
|
///
|
||||||
|
/// The OS handles AEC internally — no manual far-end feeding needed.
|
||||||
|
pub struct VpioAudio {
|
||||||
|
capture_ring: Arc<AudioRing>,
|
||||||
|
playout_ring: Arc<AudioRing>,
|
||||||
|
_audio_unit: AudioUnit,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VpioAudio {
|
||||||
|
/// Start VoiceProcessingIO with AEC enabled.
|
||||||
|
pub fn start() -> Result<Self, anyhow::Error> {
|
||||||
|
let capture_ring = Arc::new(AudioRing::new());
|
||||||
|
let playout_ring = Arc::new(AudioRing::new());
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
let mut au = AudioUnit::new(IOType::VoiceProcessingIO)
|
||||||
|
.context("failed to create VoiceProcessingIO audio unit")?;
|
||||||
|
|
||||||
|
// Must uninitialize before configuring properties.
|
||||||
|
au.uninitialize()
|
||||||
|
.context("failed to uninitialize VPIO for configuration")?;
|
||||||
|
|
||||||
|
// Enable input (mic) on Element::Input (bus 1).
|
||||||
|
let enable: u32 = 1;
|
||||||
|
au.set_property(
|
||||||
|
sys::kAudioOutputUnitProperty_EnableIO,
|
||||||
|
Scope::Input,
|
||||||
|
Element::Input,
|
||||||
|
Some(&enable),
|
||||||
|
)
|
||||||
|
.context("failed to enable VPIO input")?;
|
||||||
|
|
||||||
|
// Output (speaker) is enabled by default on VPIO, but be explicit.
|
||||||
|
au.set_property(
|
||||||
|
sys::kAudioOutputUnitProperty_EnableIO,
|
||||||
|
Scope::Output,
|
||||||
|
Element::Output,
|
||||||
|
Some(&enable),
|
||||||
|
)
|
||||||
|
.context("failed to enable VPIO output")?;
|
||||||
|
|
||||||
|
// Configure stream format: 48kHz mono f32 non-interleaved
|
||||||
|
let stream_format = StreamFormat {
|
||||||
|
sample_rate: 48_000.0,
|
||||||
|
sample_format: SampleFormat::F32,
|
||||||
|
flags: LinearPcmFlags::IS_FLOAT
|
||||||
|
| LinearPcmFlags::IS_PACKED
|
||||||
|
| LinearPcmFlags::IS_NON_INTERLEAVED,
|
||||||
|
channels: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let asbd = stream_format.to_asbd();
|
||||||
|
|
||||||
|
// Input: set format on Output scope of Input element
|
||||||
|
// (= the format the AU delivers to us from the mic)
|
||||||
|
au.set_property(
|
||||||
|
sys::kAudioUnitProperty_StreamFormat,
|
||||||
|
Scope::Output,
|
||||||
|
Element::Input,
|
||||||
|
Some(&asbd),
|
||||||
|
)
|
||||||
|
.context("failed to set input stream format")?;
|
||||||
|
|
||||||
|
// Output: set format on Input scope of Output element
|
||||||
|
// (= the format we feed to the AU for the speaker)
|
||||||
|
au.set_property(
|
||||||
|
sys::kAudioUnitProperty_StreamFormat,
|
||||||
|
Scope::Input,
|
||||||
|
Element::Output,
|
||||||
|
Some(&asbd),
|
||||||
|
)
|
||||||
|
.context("failed to set output stream format")?;
|
||||||
|
|
||||||
|
// Set up input callback (mic capture with AEC applied)
|
||||||
|
let cap_ring = capture_ring.clone();
|
||||||
|
let cap_running = running.clone();
|
||||||
|
let logged = Arc::new(AtomicBool::new(false));
|
||||||
|
au.set_input_callback(
|
||||||
|
move |args: render_callback::Args<data::NonInterleaved<f32>>| {
|
||||||
|
if !cap_running.load(Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut buffers = args.data.channels();
|
||||||
|
if let Some(ch) = buffers.next() {
|
||||||
|
if !logged.swap(true, Ordering::Relaxed) {
|
||||||
|
eprintln!("[vpio] capture callback: {} f32 samples", ch.len());
|
||||||
|
}
|
||||||
|
let mut tmp = [0i16; FRAME_SAMPLES];
|
||||||
|
for chunk in ch.chunks(FRAME_SAMPLES) {
|
||||||
|
let n = chunk.len();
|
||||||
|
for i in 0..n {
|
||||||
|
tmp[i] = (chunk[i].clamp(-1.0, 1.0) * i16::MAX as f32) as i16;
|
||||||
|
}
|
||||||
|
cap_ring.write(&tmp[..n]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("failed to set input callback")?;
|
||||||
|
|
||||||
|
// Set up output callback (speaker playback — AEC uses this as reference)
|
||||||
|
let play_ring = playout_ring.clone();
|
||||||
|
au.set_render_callback(
|
||||||
|
move |mut args: render_callback::Args<data::NonInterleaved<f32>>| {
|
||||||
|
let mut buffers = args.data.channels_mut();
|
||||||
|
if let Some(ch) = buffers.next() {
|
||||||
|
let mut tmp = [0i16; FRAME_SAMPLES];
|
||||||
|
for chunk in ch.chunks_mut(FRAME_SAMPLES) {
|
||||||
|
let n = chunk.len();
|
||||||
|
let read = play_ring.read(&mut tmp[..n]);
|
||||||
|
for i in 0..read {
|
||||||
|
chunk[i] = tmp[i] as f32 / i16::MAX as f32;
|
||||||
|
}
|
||||||
|
for i in read..n {
|
||||||
|
chunk[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("failed to set render callback")?;
|
||||||
|
|
||||||
|
au.initialize().context("failed to initialize VoiceProcessingIO")?;
|
||||||
|
au.start().context("failed to start VoiceProcessingIO")?;
|
||||||
|
|
||||||
|
info!("VoiceProcessingIO started (OS-level AEC enabled)");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
capture_ring,
|
||||||
|
playout_ring,
|
||||||
|
_audio_unit: au,
|
||||||
|
running,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capture_ring(&self) -> &Arc<AudioRing> {
|
||||||
|
&self.capture_ring
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn playout_ring(&self) -> &Arc<AudioRing> {
|
||||||
|
&self.playout_ring
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.running.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for VpioAudio {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
332
crates/wzp-client/src/audio_wasapi.rs
Normal file
332
crates/wzp-client/src/audio_wasapi.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
//! Direct WASAPI microphone capture with Windows's OS-level AEC enabled.
|
||||||
|
//!
|
||||||
|
//! Bypasses CPAL and opens the default capture endpoint directly via
|
||||||
|
//! `IMMDeviceEnumerator` + `IAudioClient2::SetClientProperties`, setting
|
||||||
|
//! `AudioClientProperties.eCategory = AudioCategory_Communications`. That's
|
||||||
|
//! the switch that tells Windows "this is a VoIP call" — the OS then
|
||||||
|
//! enables its communications audio processing chain (AEC, noise
|
||||||
|
//! suppression, automatic gain control) for the stream. AEC operates at
|
||||||
|
//! the OS level using the currently-playing audio as the reference
|
||||||
|
//! signal, so it cancels echo from our CPAL playback (and any other app's
|
||||||
|
//! audio) without us having to plumb a reference signal ourselves.
|
||||||
|
//!
|
||||||
|
//! Platform: Windows only, compiled only when the `windows-aec` feature
|
||||||
|
//! is enabled. Mirrors the public API of `audio_io::AudioCapture` so
|
||||||
|
//! `wzp-client`'s lib.rs can transparently re-export either one as
|
||||||
|
//! `AudioCapture`.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
use windows::core::{Interface, GUID};
|
||||||
|
use windows::Win32::Foundation::{CloseHandle, BOOL, WAIT_OBJECT_0};
|
||||||
|
use windows::Win32::Media::Audio::{
|
||||||
|
eCapture, eCommunications, AudioCategory_Communications, AudioClientProperties,
|
||||||
|
IAudioCaptureClient, IAudioClient, IAudioClient2, IMMDeviceEnumerator, MMDeviceEnumerator,
|
||||||
|
AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM,
|
||||||
|
AUDCLNT_STREAMFLAGS_EVENTCALLBACK, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, WAVEFORMATEX,
|
||||||
|
WAVE_FORMAT_PCM,
|
||||||
|
};
|
||||||
|
use windows::Win32::System::Com::{
|
||||||
|
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
|
||||||
|
};
|
||||||
|
use windows::Win32::System::Threading::{CreateEventW, WaitForSingleObject, INFINITE};
|
||||||
|
|
||||||
|
use crate::audio_ring::AudioRing;
|
||||||
|
|
||||||
|
/// 20 ms at 48 kHz, mono. Matches the rest of the audio pipeline.
|
||||||
|
pub const FRAME_SAMPLES: usize = 960;
|
||||||
|
|
||||||
|
/// Microphone capture via WASAPI with Windows's communications AEC enabled.
|
||||||
|
///
|
||||||
|
/// The WASAPI capture stream runs on a dedicated OS thread. This handle is
|
||||||
|
/// `Send + Sync`. Dropping it stops the stream and joins the thread.
|
||||||
|
pub struct WasapiAudioCapture {
|
||||||
|
ring: Arc<AudioRing>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
thread: Option<std::thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasapiAudioCapture {
|
||||||
|
/// Open the default communications microphone, enable OS AEC, and start
|
||||||
|
/// streaming PCM into a lock-free ring buffer.
|
||||||
|
///
|
||||||
|
/// Returns only after the capture thread has successfully initialized
|
||||||
|
/// the stream, or propagates the error back to the caller.
|
||||||
|
pub fn start() -> Result<Self, anyhow::Error> {
|
||||||
|
let ring = Arc::new(AudioRing::new());
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
|
let (init_tx, init_rx) = std::sync::mpsc::sync_channel::<Result<(), String>>(1);
|
||||||
|
let ring_cb = ring.clone();
|
||||||
|
let running_cb = running.clone();
|
||||||
|
|
||||||
|
let thread = std::thread::Builder::new()
|
||||||
|
.name("wzp-audio-capture-wasapi".into())
|
||||||
|
.spawn(move || {
|
||||||
|
let result = unsafe { capture_thread_main(ring_cb, running_cb.clone(), &init_tx) };
|
||||||
|
if let Err(e) = result {
|
||||||
|
warn!("wasapi capture thread exited with error: {e}");
|
||||||
|
// If we failed before signaling init, signal now so the
|
||||||
|
// caller unblocks. Double-send is harmless (channel is
|
||||||
|
// bounded to 1 and we only hit the second send path on
|
||||||
|
// late errors).
|
||||||
|
let _ = init_tx.send(Err(e.to_string()));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.context("failed to spawn WASAPI capture thread")?;
|
||||||
|
|
||||||
|
init_rx
|
||||||
|
.recv()
|
||||||
|
.map_err(|_| anyhow!("WASAPI capture thread exited before signaling init"))?
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
ring,
|
||||||
|
running,
|
||||||
|
thread: Some(thread),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the capture ring buffer for direct polling.
|
||||||
|
pub fn ring(&self) -> &Arc<AudioRing> {
|
||||||
|
&self.ring
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop capturing.
|
||||||
|
pub fn stop(&self) {
|
||||||
|
self.running.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for WasapiAudioCapture {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
if let Some(handle) = self.thread.take() {
|
||||||
|
// Join best-effort. The thread loop polls `running` every 200ms
|
||||||
|
// via a short WaitForSingleObject timeout, so it should exit
|
||||||
|
// within ~200ms of `stop()`.
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WASAPI thread entry point — everything below this line runs on the
|
||||||
|
// dedicated wzp-audio-capture-wasapi thread.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
unsafe fn capture_thread_main(
|
||||||
|
ring: Arc<AudioRing>,
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
init_tx: &std::sync::mpsc::SyncSender<Result<(), String>>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
// COM init for the capture thread. MULTITHREADED because we're not
|
||||||
|
// running a message pump. Must be balanced by CoUninitialize on exit.
|
||||||
|
CoInitializeEx(None, COINIT_MULTITHREADED)
|
||||||
|
.ok()
|
||||||
|
.context("CoInitializeEx failed")?;
|
||||||
|
|
||||||
|
// Use a guard struct so CoUninitialize runs even on early returns.
|
||||||
|
struct ComGuard;
|
||||||
|
impl Drop for ComGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { CoUninitialize() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _com_guard = ComGuard;
|
||||||
|
|
||||||
|
let enumerator: IMMDeviceEnumerator =
|
||||||
|
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)
|
||||||
|
.context("CoCreateInstance(MMDeviceEnumerator) failed")?;
|
||||||
|
|
||||||
|
// eCommunications role (not eConsole) — this picks the device the user
|
||||||
|
// has designated for communications in Sound Settings. It's the one
|
||||||
|
// Windows's AEC is actually tuned for and the one Teams/Zoom use.
|
||||||
|
let device = enumerator
|
||||||
|
.GetDefaultAudioEndpoint(eCapture, eCommunications)
|
||||||
|
.context("GetDefaultAudioEndpoint(eCapture, eCommunications) failed")?;
|
||||||
|
|
||||||
|
if let Ok(name) = device_name(&device) {
|
||||||
|
info!(device = %name, "opening WASAPI communications capture endpoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio_client: IAudioClient = device
|
||||||
|
.Activate(CLSCTX_ALL, None)
|
||||||
|
.context("IMMDevice::Activate(IAudioClient) failed")?;
|
||||||
|
|
||||||
|
// IAudioClient2 exposes SetClientProperties, which is the ONLY way to
|
||||||
|
// set AudioCategory_Communications pre-Initialize. Calling it on the
|
||||||
|
// base IAudioClient would not compile, and setting it after Initialize
|
||||||
|
// is a no-op.
|
||||||
|
let audio_client2: IAudioClient2 = audio_client
|
||||||
|
.cast()
|
||||||
|
.context("QueryInterface IAudioClient2 failed")?;
|
||||||
|
|
||||||
|
let mut props = AudioClientProperties {
|
||||||
|
cbSize: std::mem::size_of::<AudioClientProperties>() as u32,
|
||||||
|
bIsOffload: BOOL(0),
|
||||||
|
eCategory: AudioCategory_Communications,
|
||||||
|
// 0 = AUDCLNT_STREAMOPTIONS_NONE. The `windows` crate doesn't
|
||||||
|
// export the enum constant in all versions, so use 0 directly.
|
||||||
|
Options: Default::default(),
|
||||||
|
};
|
||||||
|
audio_client2
|
||||||
|
.SetClientProperties(&mut props as *mut _)
|
||||||
|
.context("SetClientProperties(AudioCategory_Communications) failed")?;
|
||||||
|
|
||||||
|
// Request 48 kHz mono i16 directly. AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
|
||||||
|
// tells Windows to do any needed format conversion inside the audio
|
||||||
|
// engine rather than rejecting our format. SRC_DEFAULT_QUALITY picks
|
||||||
|
// the standard Windows resampler quality (fine for voice).
|
||||||
|
let wave_format = WAVEFORMATEX {
|
||||||
|
wFormatTag: WAVE_FORMAT_PCM as u16,
|
||||||
|
nChannels: 1,
|
||||||
|
nSamplesPerSec: 48_000,
|
||||||
|
nAvgBytesPerSec: 48_000 * 2, // 1 ch * 2 bytes/sample * 48000 Hz
|
||||||
|
nBlockAlign: 2, // 1 ch * 2 bytes/sample
|
||||||
|
wBitsPerSample: 16,
|
||||||
|
cbSize: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1,000,000 hns = 100 ms buffer (hns = 100-nanosecond units). Windows
|
||||||
|
// treats this as the minimum; the engine may give us a larger one.
|
||||||
|
const BUFFER_DURATION_HNS: i64 = 1_000_000;
|
||||||
|
|
||||||
|
audio_client
|
||||||
|
.Initialize(
|
||||||
|
AUDCLNT_SHAREMODE_SHARED,
|
||||||
|
AUDCLNT_STREAMFLAGS_EVENTCALLBACK
|
||||||
|
| AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
|
||||||
|
| AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY,
|
||||||
|
BUFFER_DURATION_HNS,
|
||||||
|
0,
|
||||||
|
&wave_format,
|
||||||
|
Some(&GUID::zeroed()),
|
||||||
|
)
|
||||||
|
.context("IAudioClient::Initialize failed — Windows rejected communications-mode 48k mono i16")?;
|
||||||
|
|
||||||
|
// Event-driven capture: Windows signals this handle each time a new
|
||||||
|
// audio packet is available. We wait on it from the loop below.
|
||||||
|
let event = CreateEventW(None, false, false, None)
|
||||||
|
.context("CreateEventW failed")?;
|
||||||
|
audio_client
|
||||||
|
.SetEventHandle(event)
|
||||||
|
.context("SetEventHandle failed")?;
|
||||||
|
|
||||||
|
let capture_client: IAudioCaptureClient = audio_client
|
||||||
|
.GetService()
|
||||||
|
.context("IAudioClient::GetService(IAudioCaptureClient) failed")?;
|
||||||
|
|
||||||
|
audio_client.Start().context("IAudioClient::Start failed")?;
|
||||||
|
|
||||||
|
// Signal to the parent thread that init succeeded before entering the
|
||||||
|
// hot loop. From this point on, errors get logged but don't propagate
|
||||||
|
// back to the caller (they'd just cause the ring buffer to stop
|
||||||
|
// filling, which the main thread detects as underruns).
|
||||||
|
let _ = init_tx.send(Ok(()));
|
||||||
|
info!("WASAPI communications-mode capture started with OS AEC enabled");
|
||||||
|
|
||||||
|
let mut logged_first_packet = false;
|
||||||
|
|
||||||
|
// Main capture loop. Exit when `running` goes false (from Drop or an
|
||||||
|
// explicit stop() call).
|
||||||
|
while running.load(Ordering::Relaxed) {
|
||||||
|
// 200 ms timeout so we check `running` regularly even if the audio
|
||||||
|
// engine stops delivering packets (e.g. device unplugged).
|
||||||
|
let wait = WaitForSingleObject(event, 200);
|
||||||
|
if wait.0 != WAIT_OBJECT_0.0 {
|
||||||
|
// Timeout or failure — just loop and re-check running.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain all available packets. Windows may have queued more than
|
||||||
|
// one since we were last scheduled.
|
||||||
|
loop {
|
||||||
|
let packet_length = match capture_client.GetNextPacketSize() {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
warn!("GetNextPacketSize failed: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if packet_length == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buffer_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
|
let mut num_frames: u32 = 0;
|
||||||
|
let mut flags: u32 = 0;
|
||||||
|
let mut device_position: u64 = 0;
|
||||||
|
let mut qpc_position: u64 = 0;
|
||||||
|
|
||||||
|
if let Err(e) = capture_client.GetBuffer(
|
||||||
|
&mut buffer_ptr,
|
||||||
|
&mut num_frames,
|
||||||
|
&mut flags,
|
||||||
|
Some(&mut device_position),
|
||||||
|
Some(&mut qpc_position),
|
||||||
|
) {
|
||||||
|
warn!("GetBuffer failed: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if num_frames > 0 && !buffer_ptr.is_null() {
|
||||||
|
if !logged_first_packet {
|
||||||
|
info!(
|
||||||
|
frames = num_frames,
|
||||||
|
flags, "WASAPI capture: first packet received"
|
||||||
|
);
|
||||||
|
logged_first_packet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because we asked for 48 kHz mono i16, each frame is
|
||||||
|
// exactly one i16. Windows's AUTOCONVERTPCM handles the
|
||||||
|
// conversion from whatever the engine mix format is.
|
||||||
|
let samples = std::slice::from_raw_parts(
|
||||||
|
buffer_ptr as *const i16,
|
||||||
|
num_frames as usize,
|
||||||
|
);
|
||||||
|
ring.write(samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = capture_client.ReleaseBuffer(num_frames) {
|
||||||
|
warn!("ReleaseBuffer failed: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("WASAPI capture thread stopping");
|
||||||
|
let _ = audio_client.Stop();
|
||||||
|
let _ = CloseHandle(event);
|
||||||
|
// _com_guard drops here, calling CoUninitialize.
|
||||||
|
|
||||||
|
// Silence INFINITE unused-import warning — it's referenced by the
|
||||||
|
// `windows` crate's WaitForSingleObject alternative but we use the
|
||||||
|
// 200 ms timeout variant instead. Explicit suppression for clarity.
|
||||||
|
let _ = INFINITE;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Best-effort device ID string for logging. Grabbing the friendly name via
|
||||||
|
/// PKEY_Device_FriendlyName requires IPropertyStore + PROPVARIANT plumbing
|
||||||
|
/// that's far more ceremony than a log line justifies; the ID is already
|
||||||
|
/// sufficient to confirm we opened the right endpoint.
|
||||||
|
///
|
||||||
|
/// Rust 2024 edition's `unsafe_op_in_unsafe_fn` lint requires explicit
|
||||||
|
/// `unsafe { ... }` blocks inside `unsafe fn` bodies for each unsafe call,
|
||||||
|
/// even though the whole function is already marked unsafe.
|
||||||
|
unsafe fn device_name(
|
||||||
|
device: &windows::Win32::Media::Audio::IMMDevice,
|
||||||
|
) -> Result<String, anyhow::Error> {
|
||||||
|
let id = unsafe { device.GetId() }.context("IMMDevice::GetId failed")?;
|
||||||
|
Ok(unsafe { id.to_string() }.unwrap_or_else(|_| "<non-utf16>".to_string()))
|
||||||
|
}
|
||||||
@@ -42,6 +42,9 @@ pub struct CallConfig {
|
|||||||
/// When enabled, only every 50th frame carries a full 12-byte MediaHeader;
|
/// When enabled, only every 50th frame carries a full 12-byte MediaHeader;
|
||||||
/// intermediate frames use a compact 4-byte MiniHeader.
|
/// intermediate frames use a compact 4-byte MiniHeader.
|
||||||
pub mini_frames_enabled: bool,
|
pub mini_frames_enabled: bool,
|
||||||
|
/// AEC far-end delay compensation in milliseconds (default: 40).
|
||||||
|
/// Compensates for the round-trip audio latency from playout to mic capture.
|
||||||
|
pub aec_delay_ms: u32,
|
||||||
/// Enable adaptive jitter buffer (default: true).
|
/// Enable adaptive jitter buffer (default: true).
|
||||||
///
|
///
|
||||||
/// When true, the jitter buffer target depth is automatically adjusted
|
/// When true, the jitter buffer target depth is automatically adjusted
|
||||||
@@ -63,6 +66,7 @@ impl Default for CallConfig {
|
|||||||
noise_suppression: true,
|
noise_suppression: true,
|
||||||
mini_frames_enabled: true,
|
mini_frames_enabled: true,
|
||||||
adaptive_jitter: true,
|
adaptive_jitter: true,
|
||||||
|
aec_delay_ms: 40,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +245,7 @@ impl CallEncoder {
|
|||||||
block_id: 0,
|
block_id: 0,
|
||||||
frame_in_block: 0,
|
frame_in_block: 0,
|
||||||
timestamp_ms: 0,
|
timestamp_ms: 0,
|
||||||
aec: EchoCanceller::new(48000, 100), // 100 ms echo tail
|
aec: EchoCanceller::with_delay(48000, 60, config.aec_delay_ms),
|
||||||
agc: AutoGainControl::new(),
|
agc: AutoGainControl::new(),
|
||||||
silence_detector: SilenceDetector::new(
|
silence_detector: SilenceDetector::new(
|
||||||
config.silence_threshold_rms,
|
config.silence_threshold_rms,
|
||||||
@@ -496,6 +500,52 @@ impl CallDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Switch the decoder to match an incoming packet's codec if it differs
|
||||||
|
/// from the current profile. This enables cross-codec interop (e.g. one
|
||||||
|
/// client sends Opus, the other sends Codec2).
|
||||||
|
fn switch_decoder_if_needed(&mut self, incoming_codec: CodecId) {
|
||||||
|
if incoming_codec == self.profile.codec || incoming_codec == CodecId::ComfortNoise {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let new_profile = Self::profile_for_codec(incoming_codec);
|
||||||
|
info!(
|
||||||
|
from = ?self.profile.codec,
|
||||||
|
to = ?incoming_codec,
|
||||||
|
"decoder switching codec to match incoming packet"
|
||||||
|
);
|
||||||
|
if let Err(e) = self.audio_dec.set_profile(new_profile) {
|
||||||
|
warn!("failed to switch decoder profile: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.fec_dec = wzp_fec::create_decoder(&new_profile);
|
||||||
|
self.profile = new_profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a `CodecId` to a reasonable `QualityProfile` for decoding.
|
||||||
|
fn profile_for_codec(codec: CodecId) -> QualityProfile {
|
||||||
|
match codec {
|
||||||
|
CodecId::Opus24k => QualityProfile::GOOD,
|
||||||
|
CodecId::Opus16k => QualityProfile {
|
||||||
|
codec: CodecId::Opus16k,
|
||||||
|
fec_ratio: 0.3,
|
||||||
|
frame_duration_ms: 20,
|
||||||
|
frames_per_block: 5,
|
||||||
|
},
|
||||||
|
CodecId::Opus6k => QualityProfile::DEGRADED,
|
||||||
|
CodecId::Opus32k => QualityProfile::STUDIO_32K,
|
||||||
|
CodecId::Opus48k => QualityProfile::STUDIO_48K,
|
||||||
|
CodecId::Opus64k => QualityProfile::STUDIO_64K,
|
||||||
|
CodecId::Codec2_3200 => QualityProfile {
|
||||||
|
codec: CodecId::Codec2_3200,
|
||||||
|
fec_ratio: 0.5,
|
||||||
|
frame_duration_ms: 20,
|
||||||
|
frames_per_block: 5,
|
||||||
|
},
|
||||||
|
CodecId::Codec2_1200 => QualityProfile::CATASTROPHIC,
|
||||||
|
CodecId::ComfortNoise => QualityProfile::GOOD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode the next audio frame from the jitter buffer.
|
/// Decode the next audio frame from the jitter buffer.
|
||||||
///
|
///
|
||||||
/// Returns PCM samples (48kHz mono) or None if not ready.
|
/// Returns PCM samples (48kHz mono) or None if not ready.
|
||||||
@@ -510,6 +560,9 @@ impl CallDecoder {
|
|||||||
return Some(pcm.len());
|
return Some(pcm.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-switch decoder if incoming codec differs from current.
|
||||||
|
self.switch_decoder_if_needed(pkt.header.codec_id);
|
||||||
|
|
||||||
self.last_was_cn = false;
|
self.last_was_cn = false;
|
||||||
let result = match self.audio_dec.decode(&pkt.payload, pcm) {
|
let result = match self.audio_dec.decode(&pkt.payload, pcm) {
|
||||||
Ok(n) => Some(n),
|
Ok(n) => Some(n),
|
||||||
|
|||||||
@@ -8,6 +8,24 @@
|
|||||||
|
|
||||||
#[cfg(feature = "audio")]
|
#[cfg(feature = "audio")]
|
||||||
pub mod audio_io;
|
pub mod audio_io;
|
||||||
|
#[cfg(feature = "audio")]
|
||||||
|
pub mod audio_ring;
|
||||||
|
// VoiceProcessingIO is an Apple Core Audio API — only compile the module
|
||||||
|
// when the `vpio` feature is on AND we're targeting macOS. Enabling the
|
||||||
|
// feature on Windows/Linux was previously silently broken.
|
||||||
|
#[cfg(all(feature = "vpio", target_os = "macos"))]
|
||||||
|
pub mod audio_vpio;
|
||||||
|
// WASAPI-direct capture with Windows's OS-level AEC (AudioCategory_Communications).
|
||||||
|
// Only compiled when `windows-aec` feature is on AND target is Windows. The
|
||||||
|
// `windows` dependency is itself gated to Windows in Cargo.toml, so enabling
|
||||||
|
// this feature on non-Windows targets is a no-op.
|
||||||
|
#[cfg(all(feature = "windows-aec", target_os = "windows"))]
|
||||||
|
pub mod audio_wasapi;
|
||||||
|
// WebRTC AEC3 (Audio Processing Module) wrapper around CPAL capture + playback
|
||||||
|
// on Linux. Only compiled when `linux-aec` feature is on AND target is Linux.
|
||||||
|
// The webrtc-audio-processing dep is itself gated to Linux in Cargo.toml.
|
||||||
|
#[cfg(all(feature = "linux-aec", target_os = "linux"))]
|
||||||
|
pub mod audio_linux_aec;
|
||||||
pub mod bench;
|
pub mod bench;
|
||||||
pub mod call;
|
pub mod call;
|
||||||
pub mod drift_test;
|
pub mod drift_test;
|
||||||
@@ -17,7 +35,48 @@ pub mod handshake;
|
|||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod sweep;
|
pub mod sweep;
|
||||||
|
|
||||||
#[cfg(feature = "audio")]
|
// AudioPlayback: three possible backends depending on feature flags.
|
||||||
pub use audio_io::{AudioCapture, AudioPlayback};
|
// 1. Default CPAL (`audio_io::AudioPlayback`) — baseline on every platform.
|
||||||
|
// 2. Linux AEC (`audio_linux_aec::LinuxAecPlayback`) — CPAL + WebRTC APM
|
||||||
|
// render-side tee, so echo from speakers gets cancelled from the mic.
|
||||||
|
//
|
||||||
|
// On macOS and Windows we always use the default CPAL playback because:
|
||||||
|
// - macOS: VoiceProcessingIO handles AEC at the capture side (Apple's
|
||||||
|
// native hardware AEC uses its own reference signal handling).
|
||||||
|
// - Windows: WASAPI AudioCategory_Communications AEC uses the system
|
||||||
|
// render mix as reference — no per-process plumbing needed.
|
||||||
|
//
|
||||||
|
// Linux is the only platform where the in-app approach is necessary, so
|
||||||
|
// the AEC playback path is gated to target_os = "linux".
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "audio",
|
||||||
|
any(not(feature = "linux-aec"), not(target_os = "linux"))
|
||||||
|
))]
|
||||||
|
pub use audio_io::AudioPlayback;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "linux-aec", target_os = "linux"))]
|
||||||
|
pub use audio_linux_aec::LinuxAecPlayback as AudioPlayback;
|
||||||
|
|
||||||
|
// AudioCapture: three possible backends depending on feature flags.
|
||||||
|
// 1. Default CPAL (`audio_io::AudioCapture`) — baseline on every platform.
|
||||||
|
// 2. Windows AEC (`audio_wasapi::WasapiAudioCapture`) — direct WASAPI
|
||||||
|
// with AudioCategory_Communications, OS APO chain does AEC.
|
||||||
|
// 3. Linux AEC (`audio_linux_aec::LinuxAecCapture`) — CPAL + WebRTC APM
|
||||||
|
// capture-side echo cancellation using the playback tee as reference.
|
||||||
|
// All three expose the same public API (`start`, `ring`, `stop`, `Drop`).
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "audio",
|
||||||
|
any(not(feature = "windows-aec"), not(target_os = "windows")),
|
||||||
|
any(not(feature = "linux-aec"), not(target_os = "linux"))
|
||||||
|
))]
|
||||||
|
pub use audio_io::AudioCapture;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "windows-aec", target_os = "windows"))]
|
||||||
|
pub use audio_wasapi::WasapiAudioCapture as AudioCapture;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "linux-aec", target_os = "linux"))]
|
||||||
|
pub use audio_linux_aec::LinuxAecCapture as AudioCapture;
|
||||||
pub use call::{CallConfig, CallDecoder, CallEncoder};
|
pub use call::{CallConfig, CallDecoder, CallEncoder};
|
||||||
pub use handshake::perform_handshake;
|
pub use handshake::perform_handshake;
|
||||||
|
|||||||
@@ -1,53 +1,127 @@
|
|||||||
//! Acoustic Echo Cancellation using NLMS adaptive filter.
|
//! Acoustic Echo Cancellation — delay-compensated leaky NLMS with
|
||||||
//! Processes 480-sample (10ms) sub-frames at 48kHz.
|
//! Geigel double-talk detection.
|
||||||
|
//!
|
||||||
|
//! Key insight: on a laptop, the round-trip audio latency (playout → speaker
|
||||||
|
//! → air → mic → capture) is 30–50ms. The far-end reference must be delayed
|
||||||
|
//! by this amount so the adaptive filter models the *echo path*, not the
|
||||||
|
//! *system delay + echo path*.
|
||||||
|
//!
|
||||||
|
//! The leaky coefficient decay prevents the filter from diverging when the
|
||||||
|
//! echo path changes (e.g. hand near laptop) or when the delay estimate
|
||||||
|
//! is slightly off.
|
||||||
|
|
||||||
/// NLMS (Normalized Least Mean Squares) adaptive filter echo canceller.
|
/// Delay-compensated leaky NLMS echo canceller with Geigel DTD.
|
||||||
///
|
|
||||||
/// Removes acoustic echo by modelling the echo path between the far-end
|
|
||||||
/// (speaker) signal and the near-end (microphone) signal, then subtracting
|
|
||||||
/// the estimated echo from the near-end in real time.
|
|
||||||
pub struct EchoCanceller {
|
pub struct EchoCanceller {
|
||||||
filter_coeffs: Vec<f32>,
|
// --- Adaptive filter ---
|
||||||
|
filter: Vec<f32>,
|
||||||
filter_len: usize,
|
filter_len: usize,
|
||||||
far_end_buf: Vec<f32>,
|
/// Circular buffer of far-end reference samples (after delay).
|
||||||
far_end_pos: usize,
|
far_buf: Vec<f32>,
|
||||||
|
far_pos: usize,
|
||||||
|
/// NLMS step size.
|
||||||
mu: f32,
|
mu: f32,
|
||||||
|
/// Leakage factor: coefficients are multiplied by (1 - leak) each frame.
|
||||||
|
/// Prevents unbounded growth / divergence. 0.0001 is gentle.
|
||||||
|
leak: f32,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
|
|
||||||
|
// --- Delay buffer ---
|
||||||
|
/// Raw far-end samples before delay compensation.
|
||||||
|
delay_ring: Vec<f32>,
|
||||||
|
delay_write: usize,
|
||||||
|
delay_read: usize,
|
||||||
|
/// Delay in samples (e.g. 1920 = 40ms at 48kHz).
|
||||||
|
delay_samples: usize,
|
||||||
|
/// Capacity of the delay ring.
|
||||||
|
delay_cap: usize,
|
||||||
|
|
||||||
|
// --- Double-talk detection (Geigel) ---
|
||||||
|
/// Peak far-end level over the last filter_len samples.
|
||||||
|
far_peak: f32,
|
||||||
|
/// Geigel threshold: if |near| > threshold * far_peak, assume double-talk.
|
||||||
|
geigel_threshold: f32,
|
||||||
|
/// Holdover counter: keep DTD active for a few frames after detection.
|
||||||
|
dtd_holdover: u32,
|
||||||
|
dtd_hold_frames: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EchoCanceller {
|
impl EchoCanceller {
|
||||||
/// Create a new echo canceller.
|
/// Create a new echo canceller.
|
||||||
///
|
///
|
||||||
/// * `sample_rate` — typically 48000
|
/// * `sample_rate` — typically 48000
|
||||||
/// * `filter_ms` — echo-tail length in milliseconds (e.g. 100 for 100 ms)
|
/// * `filter_ms` — echo-tail length in milliseconds (60ms recommended)
|
||||||
|
/// * `delay_ms` — far-end delay compensation in milliseconds (40ms for laptops)
|
||||||
pub fn new(sample_rate: u32, filter_ms: u32) -> Self {
|
pub fn new(sample_rate: u32, filter_ms: u32) -> Self {
|
||||||
|
Self::with_delay(sample_rate, filter_ms, 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_delay(sample_rate: u32, filter_ms: u32, delay_ms: u32) -> Self {
|
||||||
let filter_len = (sample_rate as usize) * (filter_ms as usize) / 1000;
|
let filter_len = (sample_rate as usize) * (filter_ms as usize) / 1000;
|
||||||
|
let delay_samples = (sample_rate as usize) * (delay_ms as usize) / 1000;
|
||||||
|
// Delay ring must hold at least delay_samples + one frame (960) of headroom.
|
||||||
|
let delay_cap = delay_samples + (sample_rate as usize / 10); // +100ms headroom
|
||||||
Self {
|
Self {
|
||||||
filter_coeffs: vec![0.0f32; filter_len],
|
filter: vec![0.0; filter_len],
|
||||||
filter_len,
|
filter_len,
|
||||||
far_end_buf: vec![0.0f32; filter_len],
|
far_buf: vec![0.0; filter_len],
|
||||||
far_end_pos: 0,
|
far_pos: 0,
|
||||||
mu: 0.01,
|
mu: 0.01,
|
||||||
|
leak: 0.0001,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
||||||
|
delay_ring: vec![0.0; delay_cap],
|
||||||
|
delay_write: 0,
|
||||||
|
delay_read: 0,
|
||||||
|
delay_samples,
|
||||||
|
delay_cap,
|
||||||
|
|
||||||
|
far_peak: 0.0,
|
||||||
|
geigel_threshold: 0.7,
|
||||||
|
dtd_holdover: 0,
|
||||||
|
dtd_hold_frames: 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feed far-end (speaker/playback) samples into the circular buffer.
|
/// Feed far-end (speaker) samples. These go into the delay buffer first;
|
||||||
///
|
/// once enough samples have accumulated, they are released to the filter's
|
||||||
/// Must be called with the audio that was played out through the speaker
|
/// circular buffer with the correct delay offset.
|
||||||
/// *before* the corresponding near-end frame is processed.
|
|
||||||
pub fn feed_farend(&mut self, farend: &[i16]) {
|
pub fn feed_farend(&mut self, farend: &[i16]) {
|
||||||
|
// Write raw samples into the delay ring.
|
||||||
for &s in farend {
|
for &s in farend {
|
||||||
self.far_end_buf[self.far_end_pos] = s as f32;
|
self.delay_ring[self.delay_write % self.delay_cap] = s as f32;
|
||||||
self.far_end_pos = (self.far_end_pos + 1) % self.filter_len;
|
self.delay_write += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release delayed samples to the filter's far-end buffer.
|
||||||
|
while self.delay_available() >= 1 {
|
||||||
|
let sample = self.delay_ring[self.delay_read % self.delay_cap];
|
||||||
|
self.delay_read += 1;
|
||||||
|
|
||||||
|
self.far_buf[self.far_pos] = sample;
|
||||||
|
self.far_pos = (self.far_pos + 1) % self.filter_len;
|
||||||
|
|
||||||
|
// Track peak far-end level for Geigel DTD.
|
||||||
|
let abs_s = sample.abs();
|
||||||
|
if abs_s > self.far_peak {
|
||||||
|
self.far_peak = abs_s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decay far_peak slowly (avoids stale peak from a loud burst long ago).
|
||||||
|
self.far_peak *= 0.9995;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of delayed samples available to release.
|
||||||
|
fn delay_available(&self) -> usize {
|
||||||
|
let buffered = self.delay_write - self.delay_read;
|
||||||
|
if buffered > self.delay_samples {
|
||||||
|
buffered - self.delay_samples
|
||||||
|
} else {
|
||||||
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a near-end (microphone) frame, removing the estimated echo.
|
/// Process a near-end (microphone) frame, removing the estimated echo.
|
||||||
///
|
|
||||||
/// Returns the echo-return-loss enhancement (ERLE) as a ratio: the RMS of
|
|
||||||
/// the original near-end divided by the RMS of the residual. Values > 1.0
|
|
||||||
/// mean echo was reduced.
|
|
||||||
pub fn process_frame(&mut self, nearend: &mut [i16]) -> f32 {
|
pub fn process_frame(&mut self, nearend: &mut [i16]) -> f32 {
|
||||||
if !self.enabled {
|
if !self.enabled {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
@@ -56,85 +130,96 @@ impl EchoCanceller {
|
|||||||
let n = nearend.len();
|
let n = nearend.len();
|
||||||
let fl = self.filter_len;
|
let fl = self.filter_len;
|
||||||
|
|
||||||
|
// --- Geigel double-talk detection ---
|
||||||
|
// If any near-end sample exceeds threshold * far_peak, assume
|
||||||
|
// the local speaker is active and freeze adaptation.
|
||||||
|
let mut is_doubletalk = self.dtd_holdover > 0;
|
||||||
|
if !is_doubletalk {
|
||||||
|
let threshold_level = self.geigel_threshold * self.far_peak;
|
||||||
|
for &s in nearend.iter() {
|
||||||
|
if (s as f32).abs() > threshold_level && self.far_peak > 100.0 {
|
||||||
|
is_doubletalk = true;
|
||||||
|
self.dtd_holdover = self.dtd_hold_frames;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.dtd_holdover > 0 {
|
||||||
|
self.dtd_holdover -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if far-end is active (otherwise nothing to cancel).
|
||||||
|
let far_active = self.far_peak > 100.0;
|
||||||
|
|
||||||
|
// --- Leaky coefficient decay ---
|
||||||
|
// Applied once per frame for efficiency.
|
||||||
|
let decay = 1.0 - self.leak;
|
||||||
|
for c in self.filter.iter_mut() {
|
||||||
|
*c *= decay;
|
||||||
|
}
|
||||||
|
|
||||||
let mut sum_near_sq: f64 = 0.0;
|
let mut sum_near_sq: f64 = 0.0;
|
||||||
let mut sum_err_sq: f64 = 0.0;
|
let mut sum_err_sq: f64 = 0.0;
|
||||||
|
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
let near_f = nearend[i] as f32;
|
let near_f = nearend[i] as f32;
|
||||||
|
|
||||||
// --- estimate echo as dot(coeffs, farend_window) ---
|
// Position of far-end "now" for this near-end sample.
|
||||||
// The far-end window for this sample starts at
|
let base = (self.far_pos + fl * ((n / fl) + 2) + i - n) % fl;
|
||||||
// (far_end_pos - 1 - i) mod filter_len (most recent)
|
|
||||||
// and goes back filter_len samples.
|
// --- Echo estimation: dot(filter, far_end_window) ---
|
||||||
let mut echo_est: f32 = 0.0;
|
let mut echo_est: f32 = 0.0;
|
||||||
let mut power: f32 = 0.0;
|
let mut power: f32 = 0.0;
|
||||||
|
|
||||||
// Position of the most-recent far-end sample for this near-end sample.
|
|
||||||
// far_end_pos points to the *next write* position, so the most-recent
|
|
||||||
// sample written is at far_end_pos - 1. We have already called
|
|
||||||
// feed_farend for this block, so the relevant samples are the last
|
|
||||||
// filter_len entries ending just before the current write position,
|
|
||||||
// offset by how far we are into this near-end frame.
|
|
||||||
//
|
|
||||||
// For sample i of the near-end frame, the corresponding far-end
|
|
||||||
// "now" is far_end_pos - n + i (wrapping).
|
|
||||||
// far_end_pos points to next-write, so most recent sample is at
|
|
||||||
// far_end_pos - 1. For the i-th near-end sample we want the
|
|
||||||
// far-end "now" to be at (far_end_pos - n + i). We add fl
|
|
||||||
// repeatedly to avoid underflow on the usize subtraction.
|
|
||||||
let base = (self.far_end_pos + fl * ((n / fl) + 2) + i - n) % fl;
|
|
||||||
|
|
||||||
for k in 0..fl {
|
for k in 0..fl {
|
||||||
let fe_idx = (base + fl - k) % fl;
|
let fe_idx = (base + fl - k) % fl;
|
||||||
let fe = self.far_end_buf[fe_idx];
|
let fe = self.far_buf[fe_idx];
|
||||||
echo_est += self.filter_coeffs[k] * fe;
|
echo_est += self.filter[k] * fe;
|
||||||
power += fe * fe;
|
power += fe * fe;
|
||||||
}
|
}
|
||||||
|
|
||||||
let error = near_f - echo_est;
|
let error = near_f - echo_est;
|
||||||
|
|
||||||
// --- NLMS coefficient update ---
|
// --- NLMS adaptation (only when far-end active & no double-talk) ---
|
||||||
let norm = power + 1.0; // +1 regularisation to avoid div-by-zero
|
if far_active && !is_doubletalk && power > 10.0 {
|
||||||
let step = self.mu * error / norm;
|
let step = self.mu * error / (power + 1.0);
|
||||||
|
for k in 0..fl {
|
||||||
for k in 0..fl {
|
let fe_idx = (base + fl - k) % fl;
|
||||||
let fe_idx = (base + fl - k) % fl;
|
self.filter[k] += step * self.far_buf[fe_idx];
|
||||||
let fe = self.far_end_buf[fe_idx];
|
}
|
||||||
self.filter_coeffs[k] += step * fe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp output
|
let out = error.clamp(-32768.0, 32767.0);
|
||||||
let out = error.max(-32768.0).min(32767.0);
|
|
||||||
nearend[i] = out as i16;
|
nearend[i] = out as i16;
|
||||||
|
|
||||||
sum_near_sq += (near_f as f64) * (near_f as f64);
|
sum_near_sq += (near_f as f64).powi(2);
|
||||||
sum_err_sq += (out as f64) * (out as f64);
|
sum_err_sq += (out as f64).powi(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ERLE ratio
|
|
||||||
if sum_err_sq < 1.0 {
|
if sum_err_sq < 1.0 {
|
||||||
return 100.0; // near-perfect cancellation
|
100.0
|
||||||
|
} else {
|
||||||
|
(sum_near_sq / sum_err_sq).sqrt() as f32
|
||||||
}
|
}
|
||||||
(sum_near_sq / sum_err_sq).sqrt() as f32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable or disable echo cancellation.
|
|
||||||
pub fn set_enabled(&mut self, enabled: bool) {
|
pub fn set_enabled(&mut self, enabled: bool) {
|
||||||
self.enabled = enabled;
|
self.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether echo cancellation is currently enabled.
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
pub fn is_enabled(&self) -> bool {
|
||||||
self.enabled
|
self.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the adaptive filter to its initial state.
|
|
||||||
///
|
|
||||||
/// Zeroes out all filter coefficients and the far-end circular buffer.
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.filter_coeffs.iter_mut().for_each(|c| *c = 0.0);
|
self.filter.iter_mut().for_each(|c| *c = 0.0);
|
||||||
self.far_end_buf.iter_mut().for_each(|s| *s = 0.0);
|
self.far_buf.iter_mut().for_each(|s| *s = 0.0);
|
||||||
self.far_end_pos = 0;
|
self.far_pos = 0;
|
||||||
|
self.far_peak = 0.0;
|
||||||
|
self.delay_ring.iter_mut().for_each(|s| *s = 0.0);
|
||||||
|
self.delay_write = 0;
|
||||||
|
self.delay_read = 0;
|
||||||
|
self.dtd_holdover = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,50 +228,40 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aec_creates_with_correct_filter_len() {
|
fn creates_with_correct_sizes() {
|
||||||
let aec = EchoCanceller::new(48000, 100);
|
let aec = EchoCanceller::with_delay(48000, 60, 40);
|
||||||
assert_eq!(aec.filter_len, 4800);
|
assert_eq!(aec.filter_len, 2880); // 60ms @ 48kHz
|
||||||
assert_eq!(aec.filter_coeffs.len(), 4800);
|
assert_eq!(aec.delay_samples, 1920); // 40ms @ 48kHz
|
||||||
assert_eq!(aec.far_end_buf.len(), 4800);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aec_passthrough_when_disabled() {
|
fn passthrough_when_disabled() {
|
||||||
let mut aec = EchoCanceller::new(48000, 100);
|
let mut aec = EchoCanceller::new(48000, 60);
|
||||||
aec.set_enabled(false);
|
aec.set_enabled(false);
|
||||||
assert!(!aec.is_enabled());
|
|
||||||
|
|
||||||
let original: Vec<i16> = (0..480).map(|i| (i * 10) as i16).collect();
|
let original: Vec<i16> = (0..960).map(|i| (i * 10) as i16).collect();
|
||||||
let mut frame = original.clone();
|
let mut frame = original.clone();
|
||||||
let erle = aec.process_frame(&mut frame);
|
aec.process_frame(&mut frame);
|
||||||
assert_eq!(erle, 1.0);
|
|
||||||
assert_eq!(frame, original);
|
assert_eq!(frame, original);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aec_reset_zeroes_state() {
|
fn silence_passthrough() {
|
||||||
let mut aec = EchoCanceller::new(48000, 10); // short for test speed
|
let mut aec = EchoCanceller::with_delay(48000, 30, 0);
|
||||||
let farend: Vec<i16> = (0..480).map(|i| ((i * 37) % 1000) as i16).collect();
|
aec.feed_farend(&vec![0i16; 960]);
|
||||||
aec.feed_farend(&farend);
|
let mut frame = vec![0i16; 960];
|
||||||
|
aec.process_frame(&mut frame);
|
||||||
aec.reset();
|
assert!(frame.iter().all(|&s| s == 0));
|
||||||
|
|
||||||
assert!(aec.filter_coeffs.iter().all(|&c| c == 0.0));
|
|
||||||
assert!(aec.far_end_buf.iter().all(|&s| s == 0.0));
|
|
||||||
assert_eq!(aec.far_end_pos, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aec_reduces_echo_of_known_signal() {
|
fn reduces_echo_with_no_delay() {
|
||||||
// Use a small filter for speed. Feed a known far-end signal, then
|
// Simulate: far-end plays, echo arrives at mic attenuated by ~50%
|
||||||
// present the *same* signal as near-end (perfect echo, no room).
|
// (realistic — speaker to mic on laptop loses volume).
|
||||||
// After adaptation the output energy should drop.
|
let mut aec = EchoCanceller::with_delay(48000, 10, 0);
|
||||||
let filter_ms = 5; // 240 taps at 48 kHz
|
|
||||||
let mut aec = EchoCanceller::new(48000, filter_ms);
|
|
||||||
|
|
||||||
// Generate a simple repeating pattern.
|
let frame_len = 480;
|
||||||
let frame_len = 480usize;
|
let make_tone = |offset: usize| -> Vec<i16> {
|
||||||
let make_frame = |offset: usize| -> Vec<i16> {
|
|
||||||
(0..frame_len)
|
(0..frame_len)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let t = (offset + i) as f64 / 48000.0;
|
let t = (offset + i) as f64 / 48000.0;
|
||||||
@@ -195,18 +270,16 @@ mod tests {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Warm up the adaptive filter with several frames.
|
|
||||||
let mut last_erle = 1.0f32;
|
let mut last_erle = 1.0f32;
|
||||||
for frame_idx in 0..40 {
|
for frame_idx in 0..100 {
|
||||||
let farend = make_frame(frame_idx * frame_len);
|
let farend = make_tone(frame_idx * frame_len);
|
||||||
aec.feed_farend(&farend);
|
aec.feed_farend(&farend);
|
||||||
|
|
||||||
// Near-end = exact copy of far-end (pure echo).
|
// Near-end = attenuated copy of far-end (echo at ~50% volume).
|
||||||
let mut nearend = farend.clone();
|
let mut nearend: Vec<i16> = farend.iter().map(|&s| s / 2).collect();
|
||||||
last_erle = aec.process_frame(&mut nearend);
|
last_erle = aec.process_frame(&mut nearend);
|
||||||
}
|
}
|
||||||
|
|
||||||
// After 40 frames the ERLE should be meaningfully > 1.
|
|
||||||
assert!(
|
assert!(
|
||||||
last_erle > 1.0,
|
last_erle > 1.0,
|
||||||
"expected ERLE > 1.0 after adaptation, got {last_erle}"
|
"expected ERLE > 1.0 after adaptation, got {last_erle}"
|
||||||
@@ -214,15 +287,49 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aec_silence_passthrough() {
|
fn preserves_nearend_during_doubletalk() {
|
||||||
let mut aec = EchoCanceller::new(48000, 10);
|
let mut aec = EchoCanceller::with_delay(48000, 30, 0);
|
||||||
// Feed silence far-end
|
|
||||||
aec.feed_farend(&vec![0i16; 480]);
|
let frame_len = 960;
|
||||||
// Near-end is silence too
|
let nearend: Vec<i16> = (0..frame_len)
|
||||||
let mut frame = vec![0i16; 480];
|
.map(|i| {
|
||||||
let erle = aec.process_frame(&mut frame);
|
let t = i as f64 / 48000.0;
|
||||||
assert!(erle >= 1.0);
|
(10000.0 * (2.0 * std::f64::consts::PI * 440.0 * t).sin()) as i16
|
||||||
// Output should still be silence
|
})
|
||||||
assert!(frame.iter().all(|&s| s == 0));
|
.collect();
|
||||||
|
|
||||||
|
// Feed silence as far-end (no echo source).
|
||||||
|
aec.feed_farend(&vec![0i16; frame_len]);
|
||||||
|
|
||||||
|
let mut frame = nearend.clone();
|
||||||
|
aec.process_frame(&mut frame);
|
||||||
|
|
||||||
|
let input_energy: f64 = nearend.iter().map(|&s| (s as f64).powi(2)).sum();
|
||||||
|
let output_energy: f64 = frame.iter().map(|&s| (s as f64).powi(2)).sum();
|
||||||
|
let ratio = output_energy / input_energy;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ratio > 0.8,
|
||||||
|
"near-end speech should be preserved, energy ratio = {ratio:.3}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delay_buffer_holds_samples() {
|
||||||
|
let mut aec = EchoCanceller::with_delay(48000, 10, 20);
|
||||||
|
// 20ms delay = 960 samples @ 48kHz.
|
||||||
|
// After feeding, feed_farend auto-drains available samples to far_buf.
|
||||||
|
// So delay_available() is always 0 after feed_farend returns.
|
||||||
|
// Instead, verify far_pos advances only after the delay is filled.
|
||||||
|
|
||||||
|
// Feed 960 samples (= delay amount). No samples released yet.
|
||||||
|
aec.feed_farend(&vec![1i16; 960]);
|
||||||
|
// far_buf should still be all zeros (nothing released).
|
||||||
|
assert!(aec.far_buf.iter().all(|&s| s == 0.0), "nothing should be released yet");
|
||||||
|
|
||||||
|
// Feed 480 more. 480 should be released to far_buf.
|
||||||
|
aec.feed_farend(&vec![2i16; 480]);
|
||||||
|
let non_zero = aec.far_buf.iter().filter(|&&s| s != 0.0).count();
|
||||||
|
assert!(non_zero > 0, "samples should have been released to far_buf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
crates/wzp-native/Cargo.toml
Normal file
29
crates/wzp-native/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "wzp-native"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "WarzonePhone native audio library — standalone Android cdylib that eventually owns all C++ (Oboe bridge) and exposes a pure-C FFI. Built with cargo-ndk, loaded at runtime by the Tauri desktop cdylib via libloading."
|
||||||
|
|
||||||
|
# Crate-type is DELIBERATELY only cdylib (no rlib, no staticlib). This crate
|
||||||
|
# is built with `cargo ndk -t arm64-v8a build --release -p wzp-native` as a
|
||||||
|
# standalone .so, which is the same path the legacy wzp-android crate uses
|
||||||
|
# successfully on the same phone / same NDK. Keeping the crate-type single
|
||||||
|
# avoids the rust-lang/rust#104707 symbol leak that bit us when Tauri's
|
||||||
|
# desktop crate had ["staticlib", "cdylib", "rlib"] and any C++ static
|
||||||
|
# archive pulled bionic's internal pthread_create into the final .so.
|
||||||
|
[lib]
|
||||||
|
name = "wzp_native"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
# cc is SAFE to use here because this crate is a single-cdylib: no
|
||||||
|
# staticlib in crate-type → no rust-lang/rust#104707 symbol leak. The
|
||||||
|
# legacy wzp-android crate uses the same setup and works.
|
||||||
|
cc = "1"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Phase 2: Oboe C++ audio bridge. Still no Rust deps — we do the whole
|
||||||
|
# audio pipeline via extern "C" into the bundled C++ and expose our own
|
||||||
|
# narrow extern "C" API for wzp-desktop to dlopen via libloading.
|
||||||
|
# Phase 3 can add wzp-proto/wzp-codec if we want to share codec logic
|
||||||
|
# instead of calling back into wzp-desktop via callbacks.
|
||||||
119
crates/wzp-native/build.rs
Normal file
119
crates/wzp-native/build.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
//! wzp-native build.rs — Oboe C++ bridge compile on Android.
|
||||||
|
//!
|
||||||
|
//! Near-verbatim copy of crates/wzp-android/build.rs (which is known to
|
||||||
|
//! work). The crucial distinction: this crate is a single-cdylib (no
|
||||||
|
//! staticlib, no rlib in crate-type) so rust-lang/rust#104707 doesn't
|
||||||
|
//! apply — bionic's internal pthread_create / __init_tcb symbols stay
|
||||||
|
//! UND and resolve against libc.so at runtime, as they should.
|
||||||
|
//!
|
||||||
|
//! On non-Android hosts we compile `cpp/oboe_stub.cpp` (empty stubs) so
|
||||||
|
//! `cargo check --target <host>` still works for IDEs and CI.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let target = std::env::var("TARGET").unwrap_or_default();
|
||||||
|
|
||||||
|
if target.contains("android") {
|
||||||
|
// getauxval_fix: override compiler-rt's broken static getauxval
|
||||||
|
// stub that SIGSEGVs in shared libraries.
|
||||||
|
cc::Build::new()
|
||||||
|
.file("cpp/getauxval_fix.c")
|
||||||
|
.compile("wzp_native_getauxval_fix");
|
||||||
|
|
||||||
|
let oboe_dir = fetch_oboe();
|
||||||
|
match oboe_dir {
|
||||||
|
Some(oboe_path) => {
|
||||||
|
println!("cargo:warning=wzp-native: building with Oboe from {:?}", oboe_path);
|
||||||
|
let mut build = cc::Build::new();
|
||||||
|
build
|
||||||
|
.cpp(true)
|
||||||
|
.std("c++17")
|
||||||
|
// Shared libc++ — matches legacy wzp-android setup.
|
||||||
|
.cpp_link_stdlib(Some("c++_shared"))
|
||||||
|
.include("cpp")
|
||||||
|
.include(oboe_path.join("include"))
|
||||||
|
.include(oboe_path.join("src"))
|
||||||
|
.define("WZP_HAS_OBOE", None)
|
||||||
|
.file("cpp/oboe_bridge.cpp");
|
||||||
|
add_cpp_files_recursive(&mut build, &oboe_path.join("src"));
|
||||||
|
build.compile("wzp_native_oboe_bridge");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("cargo:warning=wzp-native: Oboe not found, building stub");
|
||||||
|
cc::Build::new()
|
||||||
|
.cpp(true)
|
||||||
|
.std("c++17")
|
||||||
|
.cpp_link_stdlib(Some("c++_shared"))
|
||||||
|
.file("cpp/oboe_stub.cpp")
|
||||||
|
.include("cpp")
|
||||||
|
.compile("wzp_native_oboe_bridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oboe needs log + OpenSLES backends at runtime.
|
||||||
|
println!("cargo:rustc-link-lib=log");
|
||||||
|
println!("cargo:rustc-link-lib=OpenSLES");
|
||||||
|
|
||||||
|
// Re-run if any cpp file changes
|
||||||
|
println!("cargo:rerun-if-changed=cpp/oboe_bridge.cpp");
|
||||||
|
println!("cargo:rerun-if-changed=cpp/oboe_bridge.h");
|
||||||
|
println!("cargo:rerun-if-changed=cpp/oboe_stub.cpp");
|
||||||
|
println!("cargo:rerun-if-changed=cpp/getauxval_fix.c");
|
||||||
|
} else {
|
||||||
|
// Non-Android hosts: compile the empty stub so lib.rs's extern
|
||||||
|
// declarations resolve when someone runs `cargo check` on macOS
|
||||||
|
// or Linux without an NDK.
|
||||||
|
cc::Build::new()
|
||||||
|
.cpp(true)
|
||||||
|
.std("c++17")
|
||||||
|
.file("cpp/oboe_stub.cpp")
|
||||||
|
.include("cpp")
|
||||||
|
.compile("wzp_native_oboe_bridge");
|
||||||
|
println!("cargo:rerun-if-changed=cpp/oboe_stub.cpp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively add all `.cpp` files from a directory to a cc::Build.
|
||||||
|
fn add_cpp_files_recursive(build: &mut cc::Build, dir: &std::path::Path) {
|
||||||
|
if !dir.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for entry in std::fs::read_dir(dir).unwrap() {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
add_cpp_files_recursive(build, &path);
|
||||||
|
} else if path.extension().map_or(false, |e| e == "cpp") {
|
||||||
|
build.file(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch or find Oboe headers + sources (v1.8.1). Same logic as the
|
||||||
|
/// legacy wzp-android crate's build.rs.
|
||||||
|
fn fetch_oboe() -> Option<PathBuf> {
|
||||||
|
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||||
|
let oboe_dir = out_dir.join("oboe");
|
||||||
|
|
||||||
|
if oboe_dir.join("include").join("oboe").join("Oboe.h").exists() {
|
||||||
|
return Some(oboe_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = std::process::Command::new("git")
|
||||||
|
.args([
|
||||||
|
"clone",
|
||||||
|
"--depth=1",
|
||||||
|
"--branch=1.8.1",
|
||||||
|
"https://github.com/google/oboe.git",
|
||||||
|
oboe_dir.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(s) if s.success() && oboe_dir.join("include").join("oboe").join("Oboe.h").exists() => {
|
||||||
|
Some(oboe_dir)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/wzp-native/cpp/getauxval_fix.c
Normal file
21
crates/wzp-native/cpp/getauxval_fix.c
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Override the broken static getauxval from compiler-rt/CRT.
|
||||||
|
// The static version reads from __libc_auxv which is NULL in shared libs
|
||||||
|
// loaded via dlopen, causing SIGSEGV in init_have_lse_atomics at load time.
|
||||||
|
// This version calls the real bionic getauxval via dlsym.
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef unsigned long (*getauxval_fn)(unsigned long);
|
||||||
|
|
||||||
|
unsigned long getauxval(unsigned long type) {
|
||||||
|
static getauxval_fn real_getauxval = (getauxval_fn)0;
|
||||||
|
if (!real_getauxval) {
|
||||||
|
real_getauxval = (getauxval_fn)dlsym((void*)-1L /* RTLD_DEFAULT */, "getauxval");
|
||||||
|
if (!real_getauxval) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return real_getauxval(type);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
420
crates/wzp-native/cpp/oboe_bridge.cpp
Normal file
420
crates/wzp-native/cpp/oboe_bridge.cpp
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
// 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;
|
||||||
|
// Value copy — the WzpOboeRings the Rust side passes us lives on the caller's
|
||||||
|
// stack frame and goes away as soon as wzp_oboe_start returns. The raw
|
||||||
|
// int16/atomic pointers INSIDE the struct point into the Rust-owned, leaked-
|
||||||
|
// for-the-lifetime-of-the-process AudioBackend singleton, so copying the
|
||||||
|
// struct by value is safe and keeps the inner pointers valid indefinitely.
|
||||||
|
// g_rings_valid guards the audio-callback-side read; clearing it in stop()
|
||||||
|
// signals "no backend" to the callbacks which then return silence + Stop.
|
||||||
|
static WzpOboeRings g_rings{};
|
||||||
|
static std::atomic<bool> g_rings_valid{false};
|
||||||
|
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:
|
||||||
|
uint64_t calls = 0;
|
||||||
|
uint64_t total_frames = 0;
|
||||||
|
uint64_t total_written = 0;
|
||||||
|
uint64_t ring_full_drops = 0;
|
||||||
|
|
||||||
|
oboe::DataCallbackResult onAudioReady(
|
||||||
|
oboe::AudioStream* stream,
|
||||||
|
void* audioData,
|
||||||
|
int32_t numFrames) override {
|
||||||
|
if (!g_running.load(std::memory_order_relaxed) ||
|
||||||
|
!g_rings_valid.load(std::memory_order_acquire)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
total_frames += numFrames;
|
||||||
|
total_written += to_write;
|
||||||
|
if (to_write < numFrames) {
|
||||||
|
ring_full_drops += (numFrames - to_write);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample-range probe on the FIRST callback to prove we get real audio
|
||||||
|
if (calls == 0 && numFrames > 0) {
|
||||||
|
int16_t lo = src[0], hi = src[0];
|
||||||
|
int32_t sumsq = 0;
|
||||||
|
for (int32_t i = 0; i < numFrames; i++) {
|
||||||
|
if (src[i] < lo) lo = src[i];
|
||||||
|
if (src[i] > hi) hi = src[i];
|
||||||
|
sumsq += (int32_t)src[i] * (int32_t)src[i];
|
||||||
|
}
|
||||||
|
int32_t rms = (int32_t) (numFrames > 0 ? (int32_t)__builtin_sqrt((double)sumsq / (double)numFrames) : 0);
|
||||||
|
LOGI("capture cb#0: numFrames=%d sample_range=[%d..%d] rms=%d to_write=%d",
|
||||||
|
numFrames, lo, hi, rms, to_write);
|
||||||
|
}
|
||||||
|
// Heartbeat every 50 callbacks (~1s at 20ms/burst)
|
||||||
|
calls++;
|
||||||
|
if ((calls % 50) == 0) {
|
||||||
|
LOGI("capture heartbeat: calls=%llu numFrames=%d ring_avail_write=%d to_write=%d full_drops=%llu total_written=%llu",
|
||||||
|
(unsigned long long)calls, numFrames, avail, to_write,
|
||||||
|
(unsigned long long)ring_full_drops, (unsigned long long)total_written);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
uint64_t calls = 0;
|
||||||
|
uint64_t total_frames = 0;
|
||||||
|
uint64_t total_played_real = 0;
|
||||||
|
uint64_t underrun_frames = 0;
|
||||||
|
uint64_t nonempty_calls = 0;
|
||||||
|
|
||||||
|
oboe::DataCallbackResult onAudioReady(
|
||||||
|
oboe::AudioStream* stream,
|
||||||
|
void* audioData,
|
||||||
|
int32_t numFrames) override {
|
||||||
|
if (!g_running.load(std::memory_order_relaxed) ||
|
||||||
|
!g_rings_valid.load(std::memory_order_acquire)) {
|
||||||
|
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);
|
||||||
|
nonempty_calls++;
|
||||||
|
}
|
||||||
|
// Fill remainder with silence on underrun
|
||||||
|
if (to_read < numFrames) {
|
||||||
|
memset(dst + to_read, 0, (numFrames - to_read) * sizeof(int16_t));
|
||||||
|
underrun_frames += (numFrames - to_read);
|
||||||
|
}
|
||||||
|
total_frames += numFrames;
|
||||||
|
total_played_real += to_read;
|
||||||
|
|
||||||
|
// First callback: log requested config + prove we're being called
|
||||||
|
if (calls == 0) {
|
||||||
|
LOGI("playout cb#0: numFrames=%d ring_avail_read=%d to_read=%d",
|
||||||
|
numFrames, avail, to_read);
|
||||||
|
}
|
||||||
|
// On the first callback that actually has data, log the sample range
|
||||||
|
// so we can tell if the samples coming out of the ring look like real
|
||||||
|
// audio vs constant-zeroes vs garbage.
|
||||||
|
if (to_read > 0 && nonempty_calls == 1) {
|
||||||
|
int16_t lo = dst[0], hi = dst[0];
|
||||||
|
int32_t sumsq = 0;
|
||||||
|
for (int32_t i = 0; i < to_read; i++) {
|
||||||
|
if (dst[i] < lo) lo = dst[i];
|
||||||
|
if (dst[i] > hi) hi = dst[i];
|
||||||
|
sumsq += (int32_t)dst[i] * (int32_t)dst[i];
|
||||||
|
}
|
||||||
|
int32_t rms = (to_read > 0) ? (int32_t)__builtin_sqrt((double)sumsq / (double)to_read) : 0;
|
||||||
|
LOGI("playout FIRST nonempty read: to_read=%d sample_range=[%d..%d] rms=%d",
|
||||||
|
to_read, lo, hi, rms);
|
||||||
|
}
|
||||||
|
// Heartbeat every 50 callbacks (~1s at 20ms/burst)
|
||||||
|
calls++;
|
||||||
|
if ((calls % 50) == 0) {
|
||||||
|
int state = (int)stream->getState();
|
||||||
|
auto xrunRes = stream->getXRunCount();
|
||||||
|
int xruns = xrunRes ? xrunRes.value() : -1;
|
||||||
|
LOGI("playout heartbeat: calls=%llu nonempty=%llu numFrames=%d ring_avail_read=%d to_read=%d underrun_frames=%llu total_played_real=%llu state=%d xruns=%d",
|
||||||
|
(unsigned long long)calls, (unsigned long long)nonempty_calls,
|
||||||
|
numFrames, avail, to_read,
|
||||||
|
(unsigned long long)underrun_frames, (unsigned long long)total_played_real,
|
||||||
|
state, xruns);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep-copy the rings struct into static storage BEFORE we publish it to
|
||||||
|
// the audio callbacks — `rings` points at the caller's stack frame and
|
||||||
|
// goes away as soon as this function returns.
|
||||||
|
g_rings = *rings;
|
||||||
|
g_rings_valid.store(true, std::memory_order_release);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
LOGI("capture stream opened: actualSR=%d actualCh=%d actualFormat=%d actualFramesPerBurst=%d actualFramesPerDataCallback=%d bufferCapacityInFrames=%d sharing=%d perfMode=%d",
|
||||||
|
g_capture_stream->getSampleRate(),
|
||||||
|
g_capture_stream->getChannelCount(),
|
||||||
|
(int)g_capture_stream->getFormat(),
|
||||||
|
g_capture_stream->getFramesPerBurst(),
|
||||||
|
g_capture_stream->getFramesPerDataCallback(),
|
||||||
|
g_capture_stream->getBufferCapacityInFrames(),
|
||||||
|
(int)g_capture_stream->getSharingMode(),
|
||||||
|
(int)g_capture_stream->getPerformanceMode());
|
||||||
|
|
||||||
|
// Build playout stream.
|
||||||
|
//
|
||||||
|
// Regression triangulation between builds:
|
||||||
|
// 96be740 (Usage::Media, default API): playout callback DID drain
|
||||||
|
// the ring at steady 50Hz (playout heartbeat: calls=1100,
|
||||||
|
// total_played_real=1055040). Audio not audible because OS routing
|
||||||
|
// sent it to a silent output.
|
||||||
|
//
|
||||||
|
// 8c36fb5 (Usage::VoiceCommunication + setAudioApi(AAudio) +
|
||||||
|
// ContentType::Speech): playout callback fired cb#0 once then
|
||||||
|
// stopped draining the ring entirely. written_samples stuck at
|
||||||
|
// ring capacity (7679) across all subsequent heartbeats, so Oboe
|
||||||
|
// accepted zero samples after startup. Still inaudible.
|
||||||
|
//
|
||||||
|
// Hypothesis: forcing setAudioApi(AAudio) + VoiceCommunication on
|
||||||
|
// Pixel 6 / Android 15 opens a stream that succeeds at cb#0 but
|
||||||
|
// then detaches from the real audio driver. Reverting to the
|
||||||
|
// config that at least drove callbacks correctly, plus the
|
||||||
|
// Kotlin-side MODE_IN_COMMUNICATION + setSpeakerphoneOn(true)
|
||||||
|
// handled in MainActivity.kt to route audio to the loud speaker.
|
||||||
|
// Usage::VoiceCommunication is the correct Oboe usage for a VoIP app
|
||||||
|
// — it respects Android's in-call audio routing and lets
|
||||||
|
// AudioManager.setSpeakerphoneOn/setBluetoothScoOn actually switch
|
||||||
|
// between earpiece, loudspeaker, and Bluetooth headset. Combined with
|
||||||
|
// MODE_IN_COMMUNICATION set from MainActivity.kt and
|
||||||
|
// speakerphoneOn=false by default, this produces handset/earpiece as
|
||||||
|
// the default output.
|
||||||
|
//
|
||||||
|
// IMPORTANT: do NOT add setAudioApi(AAudio) here. Build 8c36fb5 proved
|
||||||
|
// forcing AAudio with Usage::VoiceCommunication makes the playout
|
||||||
|
// callback stop draining the ring after cb#0, even though the stream
|
||||||
|
// opens successfully. Letting Oboe pick the API (which will be AAudio
|
||||||
|
// on API ≥ 27 but via a different codepath) kept callbacks firing in
|
||||||
|
// every other build.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
LOGI("playout stream opened: actualSR=%d actualCh=%d actualFormat=%d actualFramesPerBurst=%d actualFramesPerDataCallback=%d bufferCapacityInFrames=%d sharing=%d perfMode=%d",
|
||||||
|
g_playout_stream->getSampleRate(),
|
||||||
|
g_playout_stream->getChannelCount(),
|
||||||
|
(int)g_playout_stream->getFormat(),
|
||||||
|
g_playout_stream->getFramesPerBurst(),
|
||||||
|
g_playout_stream->getFramesPerDataCallback(),
|
||||||
|
g_playout_stream->getBufferCapacityInFrames(),
|
||||||
|
(int)g_playout_stream->getSharingMode(),
|
||||||
|
(int)g_playout_stream->getPerformanceMode());
|
||||||
|
|
||||||
|
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);
|
||||||
|
// Tell the audio callbacks to stop touching g_rings BEFORE we tear down
|
||||||
|
// the streams, so any in-flight callback returns Stop instead of reading
|
||||||
|
// stale pointers.
|
||||||
|
g_rings_valid.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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__
|
||||||
43
crates/wzp-native/cpp/oboe_bridge.h
Normal file
43
crates/wzp-native/cpp/oboe_bridge.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef WZP_OBOE_BRIDGE_H
|
||||||
|
#define WZP_OBOE_BRIDGE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include <atomic>
|
||||||
|
typedef std::atomic<int32_t> wzp_atomic_int;
|
||||||
|
extern "C" {
|
||||||
|
#else
|
||||||
|
#include <stdatomic.h>
|
||||||
|
typedef atomic_int wzp_atomic_int;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t sample_rate;
|
||||||
|
int32_t frames_per_burst;
|
||||||
|
int32_t channel_count;
|
||||||
|
} WzpOboeConfig;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int16_t* capture_buf;
|
||||||
|
int32_t capture_capacity;
|
||||||
|
wzp_atomic_int* capture_write_idx;
|
||||||
|
wzp_atomic_int* capture_read_idx;
|
||||||
|
|
||||||
|
int16_t* playout_buf;
|
||||||
|
int32_t playout_capacity;
|
||||||
|
wzp_atomic_int* playout_write_idx;
|
||||||
|
wzp_atomic_int* playout_read_idx;
|
||||||
|
} WzpOboeRings;
|
||||||
|
|
||||||
|
int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings);
|
||||||
|
void wzp_oboe_stop(void);
|
||||||
|
float wzp_oboe_capture_latency_ms(void);
|
||||||
|
float wzp_oboe_playout_latency_ms(void);
|
||||||
|
int wzp_oboe_is_running(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // WZP_OBOE_BRIDGE_H
|
||||||
27
crates/wzp-native/cpp/oboe_stub.cpp
Normal file
27
crates/wzp-native/cpp/oboe_stub.cpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Stub implementation for non-Android host builds (testing, cargo check, etc.)
|
||||||
|
|
||||||
|
#include "oboe_bridge.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int wzp_oboe_start(const WzpOboeConfig* config, const WzpOboeRings* rings) {
|
||||||
|
(void)config;
|
||||||
|
(void)rings;
|
||||||
|
fprintf(stderr, "wzp_oboe_start: stub (not on Android)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wzp_oboe_stop(void) {
|
||||||
|
fprintf(stderr, "wzp_oboe_stop: stub (not on Android)\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
331
crates/wzp-native/src/lib.rs
Normal file
331
crates/wzp-native/src/lib.rs
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
//! wzp-native — standalone Android cdylib for all the C++ audio code.
|
||||||
|
//!
|
||||||
|
//! Built with `cargo ndk`, NOT `cargo tauri android build`. Loaded at
|
||||||
|
//! runtime by the Tauri desktop cdylib (`wzp-desktop`) via libloading.
|
||||||
|
//! See `docs/incident-tauri-android-init-tcb.md` for why the split exists.
|
||||||
|
//!
|
||||||
|
//! Phase 2: real Oboe audio backend.
|
||||||
|
//!
|
||||||
|
//! Architecture: Oboe runs capture + playout streams on its own high-
|
||||||
|
//! priority AAudio callback threads inside the C++ bridge. Two SPSC ring
|
||||||
|
//! buffers (capture and playout) are shared between the C++ callbacks
|
||||||
|
//! and the Rust side via atomic indices — no locks on the hot path.
|
||||||
|
//! `wzp-desktop` drains the capture ring into its Opus encoder and fills
|
||||||
|
//! the playout ring with decoded PCM.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
|
|
||||||
|
// ─── Phase 1 smoke-test exports (kept for sanity checks) ─────────────────
|
||||||
|
|
||||||
|
/// Returns 42. Used by wzp-desktop's setup() to verify dlopen + dlsym
|
||||||
|
/// work before any audio code runs.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn wzp_native_version() -> i32 {
|
||||||
|
42
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a NUL-terminated string into `out` (capped at `cap`) and
|
||||||
|
/// returns bytes written excluding the NUL.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn wzp_native_hello(out: *mut u8, cap: usize) -> usize {
|
||||||
|
const MSG: &[u8] = b"hello from wzp-native\0";
|
||||||
|
if out.is_null() || cap == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let n = MSG.len().min(cap);
|
||||||
|
unsafe {
|
||||||
|
core::ptr::copy_nonoverlapping(MSG.as_ptr(), out, n);
|
||||||
|
*out.add(n - 1) = 0;
|
||||||
|
}
|
||||||
|
n - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── C++ Oboe bridge FFI ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct WzpOboeConfig {
|
||||||
|
sample_rate: i32,
|
||||||
|
frames_per_burst: i32,
|
||||||
|
channel_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct WzpOboeRings {
|
||||||
|
capture_buf: *mut i16,
|
||||||
|
capture_capacity: i32,
|
||||||
|
capture_write_idx: *mut AtomicI32,
|
||||||
|
capture_read_idx: *mut AtomicI32,
|
||||||
|
playout_buf: *mut i16,
|
||||||
|
playout_capacity: i32,
|
||||||
|
playout_write_idx: *mut AtomicI32,
|
||||||
|
playout_read_idx: *mut AtomicI32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: atomics synchronise producer/consumer; raw pointers are owned
|
||||||
|
// by the AudioBackend singleton below whose lifetime covers all calls.
|
||||||
|
unsafe impl Send for WzpOboeRings {}
|
||||||
|
unsafe impl Sync for WzpOboeRings {}
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn wzp_oboe_start(config: *const WzpOboeConfig, rings: *const WzpOboeRings) -> i32;
|
||||||
|
fn wzp_oboe_stop();
|
||||||
|
fn wzp_oboe_capture_latency_ms() -> f32;
|
||||||
|
fn wzp_oboe_playout_latency_ms() -> f32;
|
||||||
|
fn wzp_oboe_is_running() -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SPSC ring buffer (shared with C++ via AtomicI32) ────────────────────
|
||||||
|
|
||||||
|
/// 20 ms @ 48 kHz mono = 960 samples.
|
||||||
|
const FRAME_SAMPLES: usize = 960;
|
||||||
|
/// ~160 ms headroom at 48 kHz.
|
||||||
|
const RING_CAPACITY: usize = 7680;
|
||||||
|
|
||||||
|
struct RingBuffer {
|
||||||
|
buf: Vec<i16>,
|
||||||
|
capacity: usize,
|
||||||
|
write_idx: AtomicI32,
|
||||||
|
read_idx: AtomicI32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: SPSC with atomic read/write cursors; producer and consumer
|
||||||
|
// are always on different threads.
|
||||||
|
unsafe impl Send for RingBuffer {}
|
||||||
|
unsafe impl Sync for RingBuffer {}
|
||||||
|
|
||||||
|
impl RingBuffer {
|
||||||
|
fn new(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
buf: vec![0i16; capacity],
|
||||||
|
capacity,
|
||||||
|
write_idx: AtomicI32::new(0),
|
||||||
|
read_idx: AtomicI32::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available_read(&self) -> usize {
|
||||||
|
let w = self.write_idx.load(Ordering::Acquire);
|
||||||
|
let r = self.read_idx.load(Ordering::Relaxed);
|
||||||
|
let avail = w - r;
|
||||||
|
if avail < 0 { (avail + self.capacity as i32) as usize } else { avail as usize }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn available_write(&self) -> usize {
|
||||||
|
self.capacity - 1 - self.available_read()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, data: &[i16]) -> usize {
|
||||||
|
let count = data.len().min(self.available_write());
|
||||||
|
if count == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let mut w = self.write_idx.load(Ordering::Relaxed) as usize;
|
||||||
|
let cap = self.capacity;
|
||||||
|
let buf_ptr = self.buf.as_ptr() as *mut i16;
|
||||||
|
for sample in &data[..count] {
|
||||||
|
unsafe { *buf_ptr.add(w) = *sample; }
|
||||||
|
w += 1;
|
||||||
|
if w >= cap { w = 0; }
|
||||||
|
}
|
||||||
|
self.write_idx.store(w as i32, Ordering::Release);
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self, out: &mut [i16]) -> usize {
|
||||||
|
let count = out.len().min(self.available_read());
|
||||||
|
if count == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let mut r = self.read_idx.load(Ordering::Relaxed) as usize;
|
||||||
|
let cap = self.capacity;
|
||||||
|
let buf_ptr = self.buf.as_ptr();
|
||||||
|
for slot in &mut out[..count] {
|
||||||
|
unsafe { *slot = *buf_ptr.add(r); }
|
||||||
|
r += 1;
|
||||||
|
if r >= cap { r = 0; }
|
||||||
|
}
|
||||||
|
self.read_idx.store(r as i32, Ordering::Release);
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buf_ptr(&self) -> *mut i16 {
|
||||||
|
self.buf.as_ptr() as *mut i16
|
||||||
|
}
|
||||||
|
fn write_idx_ptr(&self) -> *mut AtomicI32 {
|
||||||
|
&self.write_idx as *const AtomicI32 as *mut AtomicI32
|
||||||
|
}
|
||||||
|
fn read_idx_ptr(&self) -> *mut AtomicI32 {
|
||||||
|
&self.read_idx as *const AtomicI32 as *mut AtomicI32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── AudioBackend singleton ──────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// There is one global AudioBackend instance because Oboe's C++ side
|
||||||
|
// holds its own singleton of the streams. The `Box::leak`'d statics own
|
||||||
|
// the ring buffers for the lifetime of the process — dropping them while
|
||||||
|
// Oboe is still running would cause use-after-free in the audio callback.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
struct AudioBackend {
|
||||||
|
capture: RingBuffer,
|
||||||
|
playout: RingBuffer,
|
||||||
|
started: std::sync::Mutex<bool>,
|
||||||
|
/// Per-write logging throttle counter for wzp_native_audio_write_playout.
|
||||||
|
playout_write_log_count: std::sync::atomic::AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
static BACKEND: OnceLock<&'static AudioBackend> = OnceLock::new();
|
||||||
|
|
||||||
|
fn backend() -> &'static AudioBackend {
|
||||||
|
BACKEND.get_or_init(|| {
|
||||||
|
Box::leak(Box::new(AudioBackend {
|
||||||
|
capture: RingBuffer::new(RING_CAPACITY),
|
||||||
|
playout: RingBuffer::new(RING_CAPACITY),
|
||||||
|
started: std::sync::Mutex::new(false),
|
||||||
|
playout_write_log_count: std::sync::atomic::AtomicU64::new(0),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── C FFI for wzp-desktop ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Start the Oboe audio streams. Returns 0 on success, non-zero on error.
|
||||||
|
/// Idempotent — calling while already running is a no-op that returns 0.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn wzp_native_audio_start() -> i32 {
|
||||||
|
let b = backend();
|
||||||
|
let mut started = match b.started.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => return -1,
|
||||||
|
};
|
||||||
|
if *started {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = WzpOboeConfig {
|
||||||
|
sample_rate: 48_000,
|
||||||
|
frames_per_burst: FRAME_SAMPLES as i32,
|
||||||
|
channel_count: 1,
|
||||||
|
};
|
||||||
|
let rings = WzpOboeRings {
|
||||||
|
capture_buf: b.capture.buf_ptr(),
|
||||||
|
capture_capacity: b.capture.capacity as i32,
|
||||||
|
capture_write_idx: b.capture.write_idx_ptr(),
|
||||||
|
capture_read_idx: b.capture.read_idx_ptr(),
|
||||||
|
playout_buf: b.playout.buf_ptr(),
|
||||||
|
playout_capacity: b.playout.capacity as i32,
|
||||||
|
playout_write_idx: b.playout.write_idx_ptr(),
|
||||||
|
playout_read_idx: b.playout.read_idx_ptr(),
|
||||||
|
};
|
||||||
|
let ret = unsafe { wzp_oboe_start(&config, &rings) };
|
||||||
|
if ret != 0 {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
*started = true;
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop Oboe. Idempotent. Safe to call from any thread.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn wzp_native_audio_stop() {
|
||||||
|
let b = backend();
|
||||||
|
if let Ok(mut started) = b.started.lock() {
|
||||||
|
if *started {
|
||||||
|
unsafe { wzp_oboe_stop() };
|
||||||
|
*started = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read captured PCM samples from the capture ring. Returns the number
|
||||||
|
/// of `i16` samples actually copied into `out` (may be less than
|
||||||
|
/// `out_len` if the ring is empty).
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn wzp_native_audio_read_capture(out: *mut i16, out_len: usize) -> usize {
|
||||||
|
if out.is_null() || out_len == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slice = unsafe { std::slice::from_raw_parts_mut(out, out_len) };
|
||||||
|
backend().capture.read(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write PCM samples into the playout ring. Returns the number of
|
||||||
|
/// samples actually enqueued (may be less than `in_len` if the ring
|
||||||
|
/// is nearly full — in practice the caller should pace to 20 ms
|
||||||
|
/// frames and spin briefly if the ring is full).
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn wzp_native_audio_write_playout(input: *const i16, in_len: usize) -> usize {
|
||||||
|
if input.is_null() || in_len == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let slice = unsafe { std::slice::from_raw_parts(input, in_len) };
|
||||||
|
let b = backend();
|
||||||
|
let before_w = b.playout.write_idx.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let before_r = b.playout.read_idx.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let written = b.playout.write(slice);
|
||||||
|
// First few writes: log ring state + sample range so we can compare what
|
||||||
|
// engine.rs hands us to what the C++ playout callback reads.
|
||||||
|
let first_writes = b.playout_write_log_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if first_writes < 3 || first_writes % 50 == 0 {
|
||||||
|
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; }
|
||||||
|
sumsq += (s as i64) * (s as i64);
|
||||||
|
}
|
||||||
|
let rms = (sumsq as f64 / slice.len() as f64).sqrt() as i32;
|
||||||
|
let avail_w_after = b.playout.available_write();
|
||||||
|
let avail_r_after = b.playout.available_read();
|
||||||
|
let msg = format!(
|
||||||
|
"playout WRITE #{first_writes}: in_len={} written={} range=[{lo}..{hi}] rms={rms} before_w={before_w} before_r={before_r} avail_read_after={avail_r_after} avail_write_after={avail_w_after}",
|
||||||
|
slice.len(), written
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
android_log(msg.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
written
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal android logcat shim so we can print from the cdylib without pulling
|
||||||
|
// in android_logger crate (which would add another dep that has to build with
|
||||||
|
// cargo-ndk). Uses libc's __android_log_print via extern linkage.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn __android_log_write(prio: i32, tag: *const u8, text: *const u8) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
unsafe fn android_log(msg: &str) {
|
||||||
|
// ANDROID_LOG_INFO = 4. Tag and text must be NUL-terminated.
|
||||||
|
let tag = b"wzp-native\0";
|
||||||
|
let mut buf = Vec::with_capacity(msg.len() + 1);
|
||||||
|
buf.extend_from_slice(msg.as_bytes());
|
||||||
|
buf.push(0);
|
||||||
|
unsafe { __android_log_write(4, tag.as_ptr(), buf.as_ptr()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
unsafe fn android_log(_msg: &str) {}
|
||||||
|
|
||||||
|
/// Current capture latency reported by Oboe, in milliseconds. Returns
|
||||||
|
/// NaN / 0.0 if the stream isn't running.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn wzp_native_audio_capture_latency_ms() -> f32 {
|
||||||
|
unsafe { wzp_oboe_capture_latency_ms() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current playout latency reported by Oboe, in milliseconds.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn wzp_native_audio_playout_latency_ms() -> f32 {
|
||||||
|
unsafe { wzp_oboe_playout_latency_ms() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-zero if both Oboe streams are currently running.
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub extern "C" fn wzp_native_audio_is_running() -> i32 {
|
||||||
|
unsafe { wzp_oboe_is_running() }
|
||||||
|
}
|
||||||
@@ -378,6 +378,31 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
let endpoint = wzp_transport::create_endpoint(config.listen_addr, Some(server_config))?;
|
let endpoint = wzp_transport::create_endpoint(config.listen_addr, Some(server_config))?;
|
||||||
|
|
||||||
|
// Compute the IP address we should advertise in CallSetup for direct
|
||||||
|
// calls. If the relay is bound to a specific IP, use it as-is; if bound
|
||||||
|
// to 0.0.0.0, use the trick of "connect" a UDP socket to an arbitrary
|
||||||
|
// external address and read its local_addr — the OS binds to whichever
|
||||||
|
// local interface IP would route packets to that destination, which is
|
||||||
|
// the primary outbound interface. This is the same IP clients on the
|
||||||
|
// LAN use to reach us.
|
||||||
|
let advertised_ip: std::net::IpAddr = {
|
||||||
|
let listen_ip = config.listen_addr.ip();
|
||||||
|
if !listen_ip.is_unspecified() {
|
||||||
|
listen_ip
|
||||||
|
} else {
|
||||||
|
// Probe via a dummy "connected" UDP socket. Never actually sends.
|
||||||
|
match std::net::UdpSocket::bind("0.0.0.0:0")
|
||||||
|
.and_then(|s| { s.connect("8.8.8.8:80").map(|_| s) })
|
||||||
|
.and_then(|s| s.local_addr())
|
||||||
|
{
|
||||||
|
Ok(a) if !a.ip().is_loopback() => a.ip(),
|
||||||
|
_ => std::net::IpAddr::from([127u8, 0, 0, 1]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let advertised_addr_str = format!("{}:{}", advertised_ip, config.listen_addr.port());
|
||||||
|
info!(%advertised_addr_str, "relay advertised address for CallSetup");
|
||||||
|
|
||||||
// Forward mode
|
// Forward mode
|
||||||
let remote_transport: Option<Arc<wzp_transport::QuinnTransport>> =
|
let remote_transport: Option<Arc<wzp_transport::QuinnTransport>> =
|
||||||
if let Some(remote_addr) = config.remote_relay {
|
if let Some(remote_addr) = config.remote_relay {
|
||||||
@@ -475,9 +500,19 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
info!("Listening for connections...");
|
info!("Listening for connections...");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let connection = match wzp_transport::accept(&endpoint).await {
|
// Pull the next Incoming off the queue. Deliberately do NOT await
|
||||||
Ok(conn) => conn,
|
// the QUIC handshake here — move that into the per-connection
|
||||||
Err(e) => { error!("accept: {e}"); continue; }
|
// spawned task below. Previously we used wzp_transport::accept
|
||||||
|
// which did both, which meant a single slow handshake would block
|
||||||
|
// the entire accept loop and prevent ALL subsequent connections
|
||||||
|
// from being processed. Surfaced as direct-call hangs where the
|
||||||
|
// callee's call-* connection never completes its QUIC handshake.
|
||||||
|
let incoming = match endpoint.accept().await {
|
||||||
|
Some(inc) => inc,
|
||||||
|
None => {
|
||||||
|
error!("endpoint.accept() returned None — endpoint closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let remote_transport = remote_transport.clone();
|
let remote_transport = remote_transport.clone();
|
||||||
@@ -493,9 +528,22 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let federation_mgr = federation_mgr.clone();
|
let federation_mgr = federation_mgr.clone();
|
||||||
let signal_hub = signal_hub.clone();
|
let signal_hub = signal_hub.clone();
|
||||||
let call_registry = call_registry.clone();
|
let call_registry = call_registry.clone();
|
||||||
let listen_addr_str = config.listen_addr.to_string();
|
let advertised_addr_str = advertised_addr_str.clone();
|
||||||
|
|
||||||
|
let incoming_addr = incoming.remote_address();
|
||||||
|
info!(%incoming_addr, "accept queue: new Incoming, spawning handshake task");
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
// Drive the QUIC handshake inside the spawned task so that
|
||||||
|
// slow or hung handshakes never block the outer accept loop.
|
||||||
|
let connection = match incoming.await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!(%incoming_addr, "QUIC handshake failed: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!(%incoming_addr, "QUIC handshake complete");
|
||||||
let addr = connection.remote_address();
|
let addr = connection.remote_address();
|
||||||
|
|
||||||
let room_name = connection
|
let room_name = connection
|
||||||
@@ -793,22 +841,18 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let _ = hub.send_to(&peer_fp, &msg).await;
|
let _ = hub.send_to(&peer_fp, &msg).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send CallSetup to both parties
|
// Send CallSetup to both parties.
|
||||||
// Use the address the client connected to (their remote addr
|
//
|
||||||
// is our perspective, but we need our listen addr).
|
// BUG FIX: the previous version of this used `addr.ip()`
|
||||||
// Replace 0.0.0.0 with the client's destination IP.
|
// which is `connection.remote_address()` — the CLIENT'S
|
||||||
let relay_addr_for_setup = if listen_addr_str.starts_with("0.0.0.0:") {
|
// IP, not the relay's. So CallSetup told both parties to
|
||||||
let port = &listen_addr_str[8..];
|
// dial the answerer's own IP, which meant the caller was
|
||||||
// Use the local IP from the client's connection
|
// sending QUIC Initials into the callee's client (no
|
||||||
let local_ip = addr.ip();
|
// server listening there) and the callee was sending to
|
||||||
if local_ip.is_loopback() {
|
// itself. In both cases endpoint.connect() hung forever.
|
||||||
format!("127.0.0.1:{port}")
|
//
|
||||||
} else {
|
// Use the relay's precomputed advertised address instead.
|
||||||
format!("{local_ip}:{port}")
|
let relay_addr_for_setup = advertised_addr_str.clone();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
listen_addr_str.clone()
|
|
||||||
};
|
|
||||||
let setup = SignalMessage::CallSetup {
|
let setup = SignalMessage::CallSetup {
|
||||||
call_id: call_id.clone(),
|
call_id: call_id.clone(),
|
||||||
room: room.clone(),
|
room: room.clone(),
|
||||||
@@ -1153,4 +1197,5 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,3 +27,8 @@ pub use connection::{accept, connect, create_endpoint};
|
|||||||
pub use path_monitor::PathMonitor;
|
pub use path_monitor::PathMonitor;
|
||||||
pub use quic::QuinnTransport;
|
pub use quic::QuinnTransport;
|
||||||
pub use wzp_proto::{MediaTransport, PathQuality, TransportError};
|
pub use wzp_proto::{MediaTransport, PathQuality, TransportError};
|
||||||
|
|
||||||
|
// Re-export the quinn Endpoint type so downstream crates (wzp-desktop) can
|
||||||
|
// thread a shared endpoint between signaling and media connections without
|
||||||
|
// needing to depend on quinn directly.
|
||||||
|
pub use quinn::Endpoint;
|
||||||
|
|||||||
16
crates/wzp-web/static/wasm/package.json
Normal file
16
crates/wzp-web/static/wasm/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "wzp-wasm",
|
||||||
|
"type": "module",
|
||||||
|
"description": "WarzonePhone WASM bindings — FEC (RaptorQ) + crypto (ChaCha20-Poly1305, X25519)",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"files": [
|
||||||
|
"wzp_wasm_bg.wasm",
|
||||||
|
"wzp_wasm.js",
|
||||||
|
"wzp_wasm.d.ts"
|
||||||
|
],
|
||||||
|
"main": "wzp_wasm.js",
|
||||||
|
"types": "wzp_wasm.d.ts",
|
||||||
|
"sideEffects": [
|
||||||
|
"./snippets/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
169
crates/wzp-web/static/wasm/wzp_wasm.d.ts
vendored
Normal file
169
crates/wzp-web/static/wasm/wzp_wasm.d.ts
vendored
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Symmetric encryption session using ChaCha20-Poly1305.
|
||||||
|
*
|
||||||
|
* Mirrors `wzp-crypto::session::ChaChaSession` for WASM. Nonce derivation
|
||||||
|
* and key setup are identical so WASM and native peers interoperate.
|
||||||
|
*/
|
||||||
|
export class WzpCryptoSession {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* Decrypt a media payload with AAD.
|
||||||
|
*
|
||||||
|
* Returns plaintext on success, or throws on auth failure.
|
||||||
|
*/
|
||||||
|
decrypt(header_aad: Uint8Array, ciphertext: Uint8Array): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Encrypt a media payload with AAD (typically the 12-byte MediaHeader).
|
||||||
|
*
|
||||||
|
* Returns `ciphertext || poly1305_tag` (plaintext.len() + 16 bytes).
|
||||||
|
*/
|
||||||
|
encrypt(header_aad: Uint8Array, plaintext: Uint8Array): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Create from a 32-byte shared secret (output of `WzpKeyExchange.derive_shared_secret`).
|
||||||
|
*/
|
||||||
|
constructor(shared_secret: Uint8Array);
|
||||||
|
/**
|
||||||
|
* Current receive sequence number (for diagnostics / UI stats).
|
||||||
|
*/
|
||||||
|
recv_seq(): number;
|
||||||
|
/**
|
||||||
|
* Current send sequence number (for diagnostics / UI stats).
|
||||||
|
*/
|
||||||
|
send_seq(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WzpFecDecoder {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* Feed a received symbol.
|
||||||
|
*
|
||||||
|
* Returns the decoded block (concatenated original frames, unpadded) if
|
||||||
|
* enough symbols have been received to recover the block, or `undefined`.
|
||||||
|
*/
|
||||||
|
add_symbol(block_id: number, symbol_idx: number, _is_repair: boolean, data: Uint8Array): Uint8Array | undefined;
|
||||||
|
/**
|
||||||
|
* Create a new FEC decoder.
|
||||||
|
*
|
||||||
|
* * `block_size` — expected number of source symbols per block.
|
||||||
|
* * `symbol_size` — padded byte size of each symbol (must match encoder).
|
||||||
|
*/
|
||||||
|
constructor(block_size: number, symbol_size: number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WzpFecEncoder {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* Add a source symbol (audio frame).
|
||||||
|
*
|
||||||
|
* Returns encoded packets (all source + repair) when the block is complete,
|
||||||
|
* or `undefined` if the block is still accumulating.
|
||||||
|
*
|
||||||
|
* Each returned packet carries the 3-byte header:
|
||||||
|
* `[block_id][symbol_idx][is_repair]` followed by `symbol_size` bytes.
|
||||||
|
*/
|
||||||
|
add_symbol(data: Uint8Array): Uint8Array | undefined;
|
||||||
|
/**
|
||||||
|
* Force-flush the current (possibly partial) block.
|
||||||
|
*
|
||||||
|
* Returns all source + repair symbols with headers, or empty vec if no
|
||||||
|
* symbols have been accumulated.
|
||||||
|
*/
|
||||||
|
flush(): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Create a new FEC encoder.
|
||||||
|
*
|
||||||
|
* * `block_size` — number of source symbols (audio frames) per FEC block.
|
||||||
|
* * `symbol_size` — padded byte size of each symbol (default 256).
|
||||||
|
*/
|
||||||
|
constructor(block_size: number, symbol_size: number);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X25519 key exchange: generate ephemeral keypair and derive shared secret.
|
||||||
|
*
|
||||||
|
* Usage from JS:
|
||||||
|
* ```js
|
||||||
|
* const kx = new WzpKeyExchange();
|
||||||
|
* const ourPub = kx.public_key(); // Uint8Array(32)
|
||||||
|
* // ... send ourPub to peer, receive peerPub ...
|
||||||
|
* const secret = kx.derive_shared_secret(peerPub); // Uint8Array(32)
|
||||||
|
* const session = new WzpCryptoSession(secret);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class WzpKeyExchange {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* Derive a 32-byte session key from the peer's public key.
|
||||||
|
*
|
||||||
|
* Raw DH output is expanded via HKDF-SHA256 with info="warzone-session-key",
|
||||||
|
* matching `wzp-crypto::handshake::WarzoneKeyExchange::derive_session`.
|
||||||
|
*/
|
||||||
|
derive_shared_secret(peer_public: Uint8Array): Uint8Array;
|
||||||
|
/**
|
||||||
|
* Generate a new random X25519 keypair.
|
||||||
|
*/
|
||||||
|
constructor();
|
||||||
|
/**
|
||||||
|
* Our public key (32 bytes).
|
||||||
|
*/
|
||||||
|
public_key(): Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
export interface InitOutput {
|
||||||
|
readonly memory: WebAssembly.Memory;
|
||||||
|
readonly __wbg_wzpcryptosession_free: (a: number, b: number) => void;
|
||||||
|
readonly __wbg_wzpfecdecoder_free: (a: number, b: number) => void;
|
||||||
|
readonly __wbg_wzpfecencoder_free: (a: number, b: number) => void;
|
||||||
|
readonly __wbg_wzpkeyexchange_free: (a: number, b: number) => void;
|
||||||
|
readonly wzpcryptosession_decrypt: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||||
|
readonly wzpcryptosession_encrypt: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||||
|
readonly wzpcryptosession_new: (a: number, b: number) => [number, number, number];
|
||||||
|
readonly wzpcryptosession_recv_seq: (a: number) => number;
|
||||||
|
readonly wzpcryptosession_send_seq: (a: number) => number;
|
||||||
|
readonly wzpfecdecoder_add_symbol: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
|
||||||
|
readonly wzpfecdecoder_new: (a: number, b: number) => number;
|
||||||
|
readonly wzpfecencoder_add_symbol: (a: number, b: number, c: number) => [number, number];
|
||||||
|
readonly wzpfecencoder_flush: (a: number) => [number, number];
|
||||||
|
readonly wzpfecencoder_new: (a: number, b: number) => number;
|
||||||
|
readonly wzpkeyexchange_derive_shared_secret: (a: number, b: number, c: number) => [number, number, number, number];
|
||||||
|
readonly wzpkeyexchange_new: () => number;
|
||||||
|
readonly wzpkeyexchange_public_key: (a: number) => [number, number];
|
||||||
|
readonly __wbindgen_exn_store: (a: number) => void;
|
||||||
|
readonly __externref_table_alloc: () => number;
|
||||||
|
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||||
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
|
readonly __externref_table_dealloc: (a: number) => void;
|
||||||
|
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __wbindgen_start: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the given `module`, which can either be bytes or
|
||||||
|
* a precompiled `WebAssembly.Module`.
|
||||||
|
*
|
||||||
|
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {InitOutput}
|
||||||
|
*/
|
||||||
|
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||||
|
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||||
|
*
|
||||||
|
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||||
|
*
|
||||||
|
* @returns {Promise<InitOutput>}
|
||||||
|
*/
|
||||||
|
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||||
27
crates/wzp-web/static/wasm/wzp_wasm_bg.wasm.d.ts
vendored
Normal file
27
crates/wzp-web/static/wasm/wzp_wasm_bg.wasm.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const __wbg_wzpcryptosession_free: (a: number, b: number) => void;
|
||||||
|
export const __wbg_wzpfecdecoder_free: (a: number, b: number) => void;
|
||||||
|
export const __wbg_wzpfecencoder_free: (a: number, b: number) => void;
|
||||||
|
export const __wbg_wzpkeyexchange_free: (a: number, b: number) => void;
|
||||||
|
export const wzpcryptosession_decrypt: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||||
|
export const wzpcryptosession_encrypt: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||||
|
export const wzpcryptosession_new: (a: number, b: number) => [number, number, number];
|
||||||
|
export const wzpcryptosession_recv_seq: (a: number) => number;
|
||||||
|
export const wzpcryptosession_send_seq: (a: number) => number;
|
||||||
|
export const wzpfecdecoder_add_symbol: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
|
||||||
|
export const wzpfecdecoder_new: (a: number, b: number) => number;
|
||||||
|
export const wzpfecencoder_add_symbol: (a: number, b: number, c: number) => [number, number];
|
||||||
|
export const wzpfecencoder_flush: (a: number) => [number, number];
|
||||||
|
export const wzpfecencoder_new: (a: number, b: number) => number;
|
||||||
|
export const wzpkeyexchange_derive_shared_secret: (a: number, b: number, c: number) => [number, number, number, number];
|
||||||
|
export const wzpkeyexchange_new: () => number;
|
||||||
|
export const wzpkeyexchange_public_key: (a: number) => [number, number];
|
||||||
|
export const __wbindgen_exn_store: (a: number) => void;
|
||||||
|
export const __externref_table_alloc: () => number;
|
||||||
|
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||||
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
|
export const __externref_table_dealloc: (a: number) => void;
|
||||||
|
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
export const __wbindgen_start: () => void;
|
||||||
2
desktop/.gitignore
vendored
Normal file
2
desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
8
desktop/.vite/deps/_metadata.json
Normal file
8
desktop/.vite/deps/_metadata.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "9046c0bf",
|
||||||
|
"configHash": "ef0fc96f",
|
||||||
|
"lockfileHash": "d66891b1",
|
||||||
|
"browserHash": "8171ed59",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
3
desktop/.vite/deps/package.json
Normal file
3
desktop/.vite/deps/package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
235
desktop/index.html
Normal file
235
desktop/index.html
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<title>WarzonePhone</title>
|
||||||
|
<link rel="stylesheet" href="/src/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<!-- Connect screen -->
|
||||||
|
<div id="connect-screen">
|
||||||
|
<h1>WarzonePhone</h1>
|
||||||
|
<p class="subtitle">Encrypted Voice</p>
|
||||||
|
<div class="form">
|
||||||
|
<label>Relay
|
||||||
|
<button id="relay-selected" class="relay-selected" type="button">
|
||||||
|
<span id="relay-dot" class="dot"></span>
|
||||||
|
<span id="relay-label">Select relay...</span>
|
||||||
|
<span class="arrow">⚙</span>
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<label>Room
|
||||||
|
<input id="room" type="text" value="general" />
|
||||||
|
</label>
|
||||||
|
<label>Alias
|
||||||
|
<input id="alias" type="text" placeholder="your name" />
|
||||||
|
</label>
|
||||||
|
<div class="form-row">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input id="os-aec" type="checkbox" checked />
|
||||||
|
OS Echo Cancel
|
||||||
|
</label>
|
||||||
|
<button id="settings-btn-home" class="icon-btn" title="Settings (Cmd+,)">⚙</button>
|
||||||
|
</div>
|
||||||
|
<!-- Mode toggle -->
|
||||||
|
<div class="mode-toggle" style="display:flex;gap:8px;margin-bottom:8px;">
|
||||||
|
<button id="mode-room" class="mode-btn active" style="flex:1">Room</button>
|
||||||
|
<button id="mode-direct" class="mode-btn" style="flex:1">Direct Call</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Room mode (default) -->
|
||||||
|
<div id="room-mode">
|
||||||
|
<button id="connect-btn" class="primary">Connect</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Direct call mode -->
|
||||||
|
<div id="direct-mode" class="hidden">
|
||||||
|
<button id="register-btn" class="primary" style="background:#2196F3">Register on Relay</button>
|
||||||
|
<div id="direct-registered" class="hidden" style="margin-top:12px">
|
||||||
|
<div class="direct-registered-header">
|
||||||
|
<p style="color:var(--green);font-size:13px;margin:0">✅ Registered — waiting for calls</p>
|
||||||
|
<button id="deregister-btn" class="secondary-btn small">Deregister</button>
|
||||||
|
</div>
|
||||||
|
<div id="incoming-call-panel" class="hidden" style="background:#1B5E20;padding:12px;border-radius:8px;margin:8px 0">
|
||||||
|
<p style="font-weight:bold;margin:0 0 4px 0">Incoming Call</p>
|
||||||
|
<p id="incoming-caller" style="font-size:12px;opacity:0.8;margin:0 0 8px 0">From: unknown</p>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button id="accept-call-btn" style="flex:1;background:var(--green);color:white;border:none;padding:8px;border-radius:6px;cursor:pointer">Accept</button>
|
||||||
|
<button id="reject-call-btn" style="flex:1;background:var(--red);color:white;border:none;padding:8px;border-radius:6px;cursor:pointer">Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent contacts -->
|
||||||
|
<div id="recent-contacts-section" class="hidden">
|
||||||
|
<div class="history-header">Recent contacts</div>
|
||||||
|
<div id="recent-contacts-list" class="history-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Call history -->
|
||||||
|
<div id="call-history-section" class="hidden">
|
||||||
|
<div class="history-header">
|
||||||
|
History
|
||||||
|
<button id="clear-history-btn" class="link-btn">clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="call-history-list" class="history-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label style="margin-top:8px">Call by fingerprint
|
||||||
|
<input id="target-fp" type="text" placeholder="xxxx:xxxx:xxxx:..." />
|
||||||
|
</label>
|
||||||
|
<button id="call-btn" class="primary" style="margin-top:8px">Call</button>
|
||||||
|
<p id="call-status-text" style="color:var(--yellow);font-size:13px;margin-top:4px"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="connect-error" class="error"></p>
|
||||||
|
</div>
|
||||||
|
<div class="identity-info">
|
||||||
|
<span id="my-identicon"></span>
|
||||||
|
<span id="my-fingerprint" class="fp-display"></span>
|
||||||
|
</div>
|
||||||
|
<div class="recent-rooms" id="recent-rooms"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- In-call screen -->
|
||||||
|
<div id="call-screen" class="hidden">
|
||||||
|
<div class="call-header">
|
||||||
|
<div class="call-header-row">
|
||||||
|
<div id="room-name" class="room-name"></div>
|
||||||
|
<button id="settings-btn-call" class="icon-btn small" title="Settings (Cmd+,)">⚙</button>
|
||||||
|
</div>
|
||||||
|
<div class="call-meta">
|
||||||
|
<span id="call-status" class="status-dot"></span>
|
||||||
|
<span id="call-timer" class="call-timer">0:00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-meter">
|
||||||
|
<div id="level-bar" class="level-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div id="participants" class="participants"></div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="mic-btn" class="control-btn" title="Toggle Mic (m)">
|
||||||
|
<span class="icon" id="mic-icon">Mic</span>
|
||||||
|
</button>
|
||||||
|
<button id="hangup-btn" class="control-btn hangup" title="Hang Up (q)">
|
||||||
|
<span class="icon">End</span>
|
||||||
|
</button>
|
||||||
|
<button id="spk-btn" class="control-btn" title="Toggle Speaker (s)">
|
||||||
|
<span class="icon" id="spk-icon">Spk</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="stats" class="stats"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings panel -->
|
||||||
|
<div id="settings-panel" class="hidden">
|
||||||
|
<div class="settings-card">
|
||||||
|
<div class="settings-header">
|
||||||
|
<h2>Settings</h2>
|
||||||
|
<button id="settings-close" class="icon-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Connection</h3>
|
||||||
|
<label>Default Room
|
||||||
|
<input id="s-room" type="text" />
|
||||||
|
</label>
|
||||||
|
<label>Alias
|
||||||
|
<input id="s-alias" type="text" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Audio</h3>
|
||||||
|
<div class="quality-control">
|
||||||
|
<div class="quality-header">
|
||||||
|
<span class="setting-label">QUALITY</span>
|
||||||
|
<span id="s-quality-label" class="quality-label">Auto</span>
|
||||||
|
</div>
|
||||||
|
<input id="s-quality" type="range" min="0" max="7" step="1" value="3" class="quality-slider" />
|
||||||
|
<div class="quality-ticks">
|
||||||
|
<span>64k</span>
|
||||||
|
<span>48k</span>
|
||||||
|
<span>32k</span>
|
||||||
|
<span>Auto</span>
|
||||||
|
<span>24k</span>
|
||||||
|
<span>6k</span>
|
||||||
|
<span>C2</span>
|
||||||
|
<span>1.2k</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input id="s-os-aec" type="checkbox" />
|
||||||
|
OS Echo Cancellation (macOS VoiceProcessingIO)
|
||||||
|
</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input id="s-agc" type="checkbox" checked />
|
||||||
|
Automatic Gain Control
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Identity</h3>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">Fingerprint</span>
|
||||||
|
<span id="s-fingerprint" class="fp-display-large"></span>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">Identity file</span>
|
||||||
|
<span class="fp-display">~/.wzp/identity</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>Recent Rooms</h3>
|
||||||
|
<div id="s-recent-rooms" class="recent-rooms-list"></div>
|
||||||
|
<button id="s-clear-recent" class="secondary-btn">Clear History</button>
|
||||||
|
</div>
|
||||||
|
<button id="settings-save" class="primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manage Relays dialog -->
|
||||||
|
<div id="relay-dialog" class="hidden">
|
||||||
|
<div class="settings-card relay-dialog-card">
|
||||||
|
<div class="settings-header">
|
||||||
|
<h2>Manage Relays</h2>
|
||||||
|
<button id="relay-dialog-close" class="icon-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="relay-dialog-list" class="relay-dialog-list"></div>
|
||||||
|
<div class="relay-add-row">
|
||||||
|
<div class="relay-add-inputs">
|
||||||
|
<input id="relay-add-name" type="text" placeholder="Name" />
|
||||||
|
<input id="relay-add-addr" type="text" placeholder="host:port" />
|
||||||
|
</div>
|
||||||
|
<button id="relay-add-btn" class="primary">Add Relay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Key changed warning dialog -->
|
||||||
|
<div id="key-warning" class="hidden">
|
||||||
|
<div class="settings-card key-warning-card">
|
||||||
|
<div class="key-warning-icon">⚠</div>
|
||||||
|
<h2>Server Key Changed</h2>
|
||||||
|
<p class="key-warning-text">The relay's identity has changed since you last connected. This usually happens when the server was restarted, but could also indicate a security issue.</p>
|
||||||
|
<div class="key-warning-fps">
|
||||||
|
<div class="key-fp-row">
|
||||||
|
<span class="key-fp-label">Previously known</span>
|
||||||
|
<code id="kw-old-fp" class="key-fp"></code>
|
||||||
|
</div>
|
||||||
|
<div class="key-fp-row">
|
||||||
|
<span class="key-fp-label">New key</span>
|
||||||
|
<code id="kw-new-fp" class="key-fp"></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="key-warning-actions">
|
||||||
|
<button id="kw-accept" class="primary">Accept New Key</button>
|
||||||
|
<button id="kw-cancel" class="secondary-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1350
desktop/package-lock.json
generated
Normal file
1350
desktop/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
desktop/package.json
Normal file
19
desktop/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "wzp-desktop",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
"vite": "^6",
|
||||||
|
"@tauri-apps/cli": "^2"
|
||||||
|
}
|
||||||
|
}
|
||||||
107
desktop/src-tauri/Cargo.toml
Normal file
107
desktop/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
[package]
|
||||||
|
name = "wzp-desktop"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "WarzonePhone Desktop — encrypted VoIP client"
|
||||||
|
default-run = "wzp-desktop"
|
||||||
|
|
||||||
|
# Library target — required for Tauri mobile (Android/iOS link the app as a cdylib)
|
||||||
|
# and also used by the desktop binary below.
|
||||||
|
#
|
||||||
|
# `staticlib` was DROPPED from crate-type because rust-lang/rust#104707
|
||||||
|
# documents that having staticlib alongside cdylib leaks non-exported
|
||||||
|
# symbols from staticlibs into the cdylib. Bionic's private `__init_tcb`
|
||||||
|
# / `pthread_create` symbols end up bound LOCALLY inside our .so instead
|
||||||
|
# of resolved dynamically against libc.so at dlopen time — which crashes
|
||||||
|
# at launch as soon as tao tries to std::thread::spawn() from the JNI
|
||||||
|
# onCreate callback. The legacy wzp-android crate uses ["cdylib", "rlib"]
|
||||||
|
# and runs fine on the same phone with the same NDK + Rust toolchain.
|
||||||
|
#
|
||||||
|
# iOS Tauri builds that actually need staticlib can re-add it behind a
|
||||||
|
# target cfg if we ever ship on iOS.
|
||||||
|
[lib]
|
||||||
|
name = "wzp_desktop_lib"
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "wzp-desktop"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
# cc is no longer needed — all C++ moved to crates/wzp-native (built with
|
||||||
|
# cargo-ndk and loaded via libloading at runtime). wzp-desktop's .so on
|
||||||
|
# Android is now pure Rust.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
anyhow = "1"
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||||
|
|
||||||
|
# WarzonePhone crates — protocol layer is platform-independent
|
||||||
|
wzp-proto = { path = "../../crates/wzp-proto" }
|
||||||
|
wzp-codec = { path = "../../crates/wzp-codec" }
|
||||||
|
wzp-fec = { path = "../../crates/wzp-fec" }
|
||||||
|
wzp-crypto = { path = "../../crates/wzp-crypto" }
|
||||||
|
wzp-transport = { path = "../../crates/wzp-transport" }
|
||||||
|
|
||||||
|
# wzp-client pulls in CPAL on every desktop target and, additionally on
|
||||||
|
# macOS, VoiceProcessingIO (coreaudio-rs behind the "vpio" feature). The
|
||||||
|
# vpio feature MUST NOT be enabled on Windows / Linux because coreaudio-rs
|
||||||
|
# is Apple-framework-only and will fail to build. Task #24 will add a
|
||||||
|
# matching Windows Voice Capture DSP path behind its own feature; until
|
||||||
|
# then, Windows desktops use plain CPAL with AEC disabled.
|
||||||
|
|
||||||
|
# macOS: CPAL + VoiceProcessingIO (hardware AEC via Core Audio).
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] }
|
||||||
|
|
||||||
|
# Windows: CPAL for playback + direct WASAPI for capture with OS-level
|
||||||
|
# AEC (AudioCategory_Communications). The wzp-client `windows-aec`
|
||||||
|
# feature swaps the default CPAL AudioCapture for a WASAPI one that
|
||||||
|
# opens the mic under AudioCategory_Communications, turning on Windows's
|
||||||
|
# communications audio processing chain (AEC, NS, AGC). The reference
|
||||||
|
# signal for AEC is the system render mix, so echo from our CPAL
|
||||||
|
# playback is cancelled automatically without extra plumbing.
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "windows-aec"] }
|
||||||
|
|
||||||
|
# Linux: CPAL playback+capture baseline. AEC is enabled via the top-level
|
||||||
|
# `linux-aec` feature in wzp-desktop, which forwards to wzp-client/linux-aec.
|
||||||
|
# Keeping it opt-in at the wzp-desktop level (rather than forcing it always
|
||||||
|
# on here) lets `cargo tauri build` produce two variants from the same
|
||||||
|
# source tree — a noAEC baseline and an AEC build — by toggling the feature
|
||||||
|
# at build time: `cargo tauri build -- --features wzp-desktop/linux-aec`.
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio"] }
|
||||||
|
|
||||||
|
# Android: no CPAL, no vpio — audio goes through the standalone wzp-native
|
||||||
|
# cdylib that we dlopen via libloading at runtime. See the wzp_native
|
||||||
|
# module in src/.
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
wzp-client = { path = "../../crates/wzp-client", default-features = false }
|
||||||
|
# libloading: runtime dlopen of libwzp_native.so — the standalone cdylib
|
||||||
|
# crate that owns all C++ (Oboe bridge). Keeps wzp-desktop's .so free of
|
||||||
|
# any C/C++ static archives that would otherwise leak bionic's internal
|
||||||
|
# pthread_create into our cdylib and trigger the __init_tcb crash.
|
||||||
|
libloading = "0.8"
|
||||||
|
# jni + ndk-context: called from android_audio.rs to invoke
|
||||||
|
# AudioManager.setSpeakerphoneOn on the JVM side at runtime, so the
|
||||||
|
# Oboe playout stream (opened with Usage::VoiceCommunication) can route
|
||||||
|
# between earpiece and loud speaker without restarting.
|
||||||
|
jni = "0.21"
|
||||||
|
ndk-context = "0.1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["custom-protocol"]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
# linux-aec: forwards to wzp-client/linux-aec so `cargo tauri build -- --features
|
||||||
|
# wzp-desktop/linux-aec` enables the WebRTC AEC3 backend on Linux. No-op on
|
||||||
|
# other targets because wzp-client/linux-aec is itself cfg(target_os = "linux").
|
||||||
|
linux-aec = ["wzp-client/linux-aec"]
|
||||||
26
desktop/src-tauri/build.rs
Normal file
26
desktop/src-tauri/build.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Capture short git hash so the running app can prove which build it is.
|
||||||
|
// Falls back to "unknown" if git isn't available (e.g. when building from
|
||||||
|
// a tarball without a .git dir).
|
||||||
|
let git_hash = Command::new("git")
|
||||||
|
.args(["rev-parse", "--short", "HEAD"])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".into());
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=WZP_GIT_HASH={git_hash}");
|
||||||
|
println!("cargo:rerun-if-changed=../../.git/HEAD");
|
||||||
|
println!("cargo:rerun-if-changed=../../.git/refs/heads");
|
||||||
|
|
||||||
|
// No cc::Build of ANY kind on Android — all C++ lives in the standalone
|
||||||
|
// `wzp-native` crate which is built separately with cargo-ndk and loaded
|
||||||
|
// via libloading at runtime. See docs/incident-tauri-android-init-tcb.md
|
||||||
|
// for why this split exists.
|
||||||
|
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
26
desktop/src-tauri/capabilities/default.json
Normal file
26
desktop/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability — grants core APIs (events, path, window, app, clipboard) to the main window on every platform we ship to.",
|
||||||
|
"windows": ["main"],
|
||||||
|
"platforms": [
|
||||||
|
"linux",
|
||||||
|
"macOS",
|
||||||
|
"windows",
|
||||||
|
"android",
|
||||||
|
"iOS"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"core:event:default",
|
||||||
|
"core:event:allow-listen",
|
||||||
|
"core:event:allow-unlisten",
|
||||||
|
"core:event:allow-emit",
|
||||||
|
"core:event:allow-emit-to",
|
||||||
|
"core:path:default",
|
||||||
|
"core:window:default",
|
||||||
|
"core:app:default",
|
||||||
|
"core:webview:default",
|
||||||
|
"shell:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||||
|
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/Theme.wzp_desktop"
|
||||||
|
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/main_activity_title"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<!-- AndroidTV support -->
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.wzp.desktop
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
class MainActivity : TauriActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WzpMainActivity"
|
||||||
|
private const val AUDIO_PERMISSIONS_REQUEST = 4242
|
||||||
|
private val REQUIRED_AUDIO_PERMISSIONS = arrayOf(
|
||||||
|
Manifest.permission.RECORD_AUDIO,
|
||||||
|
Manifest.permission.MODIFY_AUDIO_SETTINGS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
enableEdgeToEdge()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Request RECORD_AUDIO early so Oboe (inside libwzp_native.so) can open
|
||||||
|
// the AAudio input stream without silently failing. The grant is
|
||||||
|
// persisted, so after the first launch the dialog no longer appears.
|
||||||
|
// MODIFY_AUDIO_SETTINGS is needed to switch AudioManager mode + speaker.
|
||||||
|
val needsRequest = REQUIRED_AUDIO_PERMISSIONS.any {
|
||||||
|
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
if (needsRequest) {
|
||||||
|
Log.i(TAG, "requesting audio permissions")
|
||||||
|
ActivityCompat.requestPermissions(this, REQUIRED_AUDIO_PERMISSIONS, AUDIO_PERMISSIONS_REQUEST)
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "audio permissions already granted")
|
||||||
|
configureAudioForCall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<String>,
|
||||||
|
grantResults: IntArray
|
||||||
|
) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == AUDIO_PERMISSIONS_REQUEST) {
|
||||||
|
val allGranted = grantResults.isNotEmpty() &&
|
||||||
|
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||||
|
Log.i(TAG, "audio permissions result: allGranted=$allGranted grants=${grantResults.toList()}")
|
||||||
|
if (allGranted) {
|
||||||
|
configureAudioForCall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Put the phone into VoIP call mode with handset (earpiece) as the
|
||||||
|
* default output. The Oboe playout stream is opened with
|
||||||
|
* Usage::VoiceCommunication which honours this routing, so:
|
||||||
|
*
|
||||||
|
* MODE_IN_COMMUNICATION + speakerphoneOn=false → earpiece (handset)
|
||||||
|
* MODE_IN_COMMUNICATION + speakerphoneOn=true → loudspeaker
|
||||||
|
* MODE_IN_COMMUNICATION + bluetoothScoOn=true → bluetooth headset
|
||||||
|
*
|
||||||
|
* The speaker/handset/BT toggle itself is wired up via the Tauri
|
||||||
|
* command `set_speakerphone(on)` in a follow-up build. For now the
|
||||||
|
* default is handset, matching the user's stated preference.
|
||||||
|
*
|
||||||
|
* STREAM_VOICE_CALL volume is cranked to max since the in-call volume
|
||||||
|
* slider is separate from media volume on most devices.
|
||||||
|
*/
|
||||||
|
private fun configureAudioForCall() {
|
||||||
|
try {
|
||||||
|
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
Log.i(TAG, "audio state before: mode=${am.mode} speaker=${am.isSpeakerphoneOn} " +
|
||||||
|
"voiceVol=${am.getStreamVolume(AudioManager.STREAM_VOICE_CALL)}/" +
|
||||||
|
"${am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL)} " +
|
||||||
|
"musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/" +
|
||||||
|
"${am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)}")
|
||||||
|
|
||||||
|
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||||
|
am.isSpeakerphoneOn = false // default: handset / earpiece
|
||||||
|
|
||||||
|
// Crank both voice-call and music volumes so nothing silent slips
|
||||||
|
// through regardless of which stream actually ends up driving.
|
||||||
|
val maxVoice = am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL)
|
||||||
|
am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, maxVoice, 0)
|
||||||
|
val maxMusic = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||||
|
am.setStreamVolume(AudioManager.STREAM_MUSIC, maxMusic, 0)
|
||||||
|
|
||||||
|
Log.i(TAG, "audio state after: mode=${am.mode} speaker=${am.isSpeakerphoneOn} " +
|
||||||
|
"voiceVol=${am.getStreamVolume(AudioManager.STREAM_VOICE_CALL)}/$maxVoice " +
|
||||||
|
"musicVol=${am.getStreamVolume(AudioManager.STREAM_MUSIC)}/$maxMusic")
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "configureAudioForCall failed: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
desktop/src-tauri/gen/schemas/capabilities.json
Normal file
1
desktop/src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Default capability — grants core APIs (events, path, window, app, clipboard) to the main window on every platform we ship to.","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:event:allow-unlisten","core:event:allow-emit","core:event:allow-emit-to","core:path:default","core:window:default","core:app:default","core:webview:default","shell:default"],"platforms":["linux","macOS","windows","android","iOS"]}}
|
||||||
2564
desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
2564
desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2564
desktop/src-tauri/gen/schemas/macOS-schema.json
Normal file
2564
desktop/src-tauri/gen/schemas/macOS-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
desktop/src-tauri/icons/icon.ico
Normal file
BIN
desktop/src-tauri/icons/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
desktop/src-tauri/icons/icon.png
Normal file
BIN
desktop/src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 B |
98
desktop/src-tauri/src/android_audio.rs
Normal file
98
desktop/src-tauri/src/android_audio.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
//! Runtime bridge to Android's `AudioManager` for in-call audio routing.
|
||||||
|
//!
|
||||||
|
//! We own a quinn+Oboe VoIP pipeline entirely from Rust, but routing the
|
||||||
|
//! playout stream between earpiece / loudspeaker / Bluetooth headset has to
|
||||||
|
//! happen at the JVM level because those toggles are AudioManager-only.
|
||||||
|
//! This module uses the global JavaVM handle that `ndk_context` exposes
|
||||||
|
//! (populated by Tauri's mobile runtime) + the `jni` crate to reach into
|
||||||
|
//! the Android framework without needing a Tauri plugin.
|
||||||
|
//!
|
||||||
|
//! All callers must be inside an Android target (`#[cfg(target_os = "android")]`).
|
||||||
|
|
||||||
|
#![cfg(target_os = "android")]
|
||||||
|
|
||||||
|
use jni::objects::{JObject, JString, JValue};
|
||||||
|
use jni::JavaVM;
|
||||||
|
|
||||||
|
/// Grab the JavaVM + current Activity from the ndk_context that Tauri's
|
||||||
|
/// mobile runtime sets up at process startup.
|
||||||
|
fn jvm_and_activity() -> Result<(JavaVM, JObject<'static>), String> {
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
let vm_ptr = ctx.vm() as *mut jni::sys::JavaVM;
|
||||||
|
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 activity_ptr = ctx.context() as jni::sys::jobject;
|
||||||
|
if activity_ptr.is_null() {
|
||||||
|
return Err("ndk_context: activity pointer is null".into());
|
||||||
|
}
|
||||||
|
// SAFETY: ndk_context guarantees the pointer lives for the process
|
||||||
|
// lifetime; we wrap it as a JObject<'static> for convenience.
|
||||||
|
let activity: JObject<'static> = unsafe { JObject::from_raw(activity_ptr) };
|
||||||
|
Ok((vm, activity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Android's `AudioManager` via `activity.getSystemService("audio")`.
|
||||||
|
fn audio_manager<'local>(
|
||||||
|
env: &mut jni::AttachGuard<'local>,
|
||||||
|
activity: &JObject<'local>,
|
||||||
|
) -> Result<JObject<'local>, String> {
|
||||||
|
let svc_name: JString<'local> = env
|
||||||
|
.new_string("audio")
|
||||||
|
.map_err(|e| format!("new_string(audio): {e}"))?;
|
||||||
|
let am = env
|
||||||
|
.call_method(
|
||||||
|
activity,
|
||||||
|
"getSystemService",
|
||||||
|
"(Ljava/lang/String;)Ljava/lang/Object;",
|
||||||
|
&[JValue::Object(&svc_name)],
|
||||||
|
)
|
||||||
|
.and_then(|v| v.l())
|
||||||
|
.map_err(|e| format!("getSystemService(audio): {e}"))?;
|
||||||
|
if am.is_null() {
|
||||||
|
return Err("getSystemService returned null".into());
|
||||||
|
}
|
||||||
|
Ok(am)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch between loud speaker (`true`) and earpiece/handset (`false`).
|
||||||
|
///
|
||||||
|
/// Calls `AudioManager.setSpeakerphoneOn(on)` on the JVM. Requires that
|
||||||
|
/// the audio mode is already `MODE_IN_COMMUNICATION` — MainActivity.kt
|
||||||
|
/// sets this at startup, so by the time a call is up this is always true.
|
||||||
|
pub fn set_speakerphone(on: bool) -> Result<(), String> {
|
||||||
|
let (vm, activity) = jvm_and_activity()?;
|
||||||
|
let mut env = vm
|
||||||
|
.attach_current_thread()
|
||||||
|
.map_err(|e| format!("attach_current_thread: {e}"))?;
|
||||||
|
let am = audio_manager(&mut env, &activity)?;
|
||||||
|
|
||||||
|
env.call_method(
|
||||||
|
&am,
|
||||||
|
"setSpeakerphoneOn",
|
||||||
|
"(Z)V",
|
||||||
|
&[JValue::Bool(if on { 1 } else { 0 })],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("setSpeakerphoneOn({on}): {e}"))?;
|
||||||
|
|
||||||
|
tracing::info!(on, "AudioManager.setSpeakerphoneOn");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the current speakerphone state. Returns true if routing is on the
|
||||||
|
/// loud speaker, false if on earpiece / BT headset / wired headset.
|
||||||
|
pub fn is_speakerphone_on() -> Result<bool, String> {
|
||||||
|
let (vm, activity) = jvm_and_activity()?;
|
||||||
|
let mut env = vm
|
||||||
|
.attach_current_thread()
|
||||||
|
.map_err(|e| format!("attach_current_thread: {e}"))?;
|
||||||
|
let am = audio_manager(&mut env, &activity)?;
|
||||||
|
|
||||||
|
let on = env
|
||||||
|
.call_method(&am, "isSpeakerphoneOn", "()Z", &[])
|
||||||
|
.and_then(|v| v.z())
|
||||||
|
.map_err(|e| format!("isSpeakerphoneOn: {e}"))?;
|
||||||
|
Ok(on)
|
||||||
|
}
|
||||||
909
desktop/src-tauri/src/engine.rs
Normal file
909
desktop/src-tauri/src/engine.rs
Normal file
@@ -0,0 +1,909 @@
|
|||||||
|
//! Call engine for the desktop app — wraps wzp-client audio + transport
|
||||||
|
//! into a clean async interface for Tauri commands.
|
||||||
|
//!
|
||||||
|
//! Step C of the incremental Android rewrite: the module now compiles on
|
||||||
|
//! Android too (previously cfg-gated out entirely in lib.rs), but the
|
||||||
|
//! actual `CallEngine::start()` body uses CPAL via `wzp_client::audio_io`
|
||||||
|
//! which is only available on desktop. On Android we expose a stub
|
||||||
|
//! `start()` that returns an error, so the frontend's `connect` command
|
||||||
|
//! still fails cleanly but the rest of the engine code links in.
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
// CPAL audio I/O is only available on desktop (wzp-client's `audio` feature).
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
use wzp_client::audio_io::{AudioCapture, AudioPlayback};
|
||||||
|
|
||||||
|
// Codec + handshake pipelines are platform-independent Rust (no CPAL
|
||||||
|
// dependency) so they're available from wzp-client on both desktop and
|
||||||
|
// Android (where wzp-client is pulled in with default-features=false).
|
||||||
|
use wzp_client::call::{CallConfig, CallEncoder};
|
||||||
|
|
||||||
|
use wzp_proto::{CodecId, MediaTransport, QualityProfile};
|
||||||
|
|
||||||
|
const FRAME_SAMPLES_40MS: usize = 1920;
|
||||||
|
|
||||||
|
/// Resolve a quality string from the UI to a QualityProfile.
|
||||||
|
/// Returns None for "auto" (use default adaptive behavior).
|
||||||
|
fn resolve_quality(quality: &str) -> Option<QualityProfile> {
|
||||||
|
match quality {
|
||||||
|
"good" | "opus" => Some(QualityProfile::GOOD),
|
||||||
|
"degraded" | "opus6k" => Some(QualityProfile::DEGRADED),
|
||||||
|
"catastrophic" | "codec2-1200" => Some(QualityProfile::CATASTROPHIC),
|
||||||
|
"codec2-3200" => Some(QualityProfile {
|
||||||
|
codec: CodecId::Codec2_3200,
|
||||||
|
fec_ratio: 0.5,
|
||||||
|
frame_duration_ms: 20,
|
||||||
|
frames_per_block: 5,
|
||||||
|
}),
|
||||||
|
"studio-32k" => Some(QualityProfile::STUDIO_32K),
|
||||||
|
"studio-48k" => Some(QualityProfile::STUDIO_48K),
|
||||||
|
"studio-64k" => Some(QualityProfile::STUDIO_64K),
|
||||||
|
_ => None, // "auto" or unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper to make non-Sync audio handles safe to store in shared state.
|
||||||
|
/// The audio handle is only accessed from the thread that created it (drop),
|
||||||
|
/// never shared across threads — Sync is safe.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct SyncWrapper(Box<dyn std::any::Any + Send>);
|
||||||
|
unsafe impl Sync for SyncWrapper {}
|
||||||
|
|
||||||
|
pub struct ParticipantInfo {
|
||||||
|
pub fingerprint: String,
|
||||||
|
pub alias: Option<String>,
|
||||||
|
pub relay_label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EngineStatus {
|
||||||
|
pub mic_muted: bool,
|
||||||
|
pub spk_muted: bool,
|
||||||
|
pub participants: Vec<ParticipantInfo>,
|
||||||
|
pub frames_sent: u64,
|
||||||
|
pub frames_received: u64,
|
||||||
|
pub audio_level: u32,
|
||||||
|
pub call_duration_secs: f64,
|
||||||
|
pub fingerprint: String,
|
||||||
|
pub tx_codec: String,
|
||||||
|
pub rx_codec: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CallEngine {
|
||||||
|
running: Arc<AtomicBool>,
|
||||||
|
mic_muted: Arc<AtomicBool>,
|
||||||
|
spk_muted: Arc<AtomicBool>,
|
||||||
|
participants: Arc<Mutex<Vec<ParticipantInfo>>>,
|
||||||
|
frames_sent: Arc<AtomicU64>,
|
||||||
|
frames_received: Arc<AtomicU64>,
|
||||||
|
audio_level: Arc<AtomicU32>,
|
||||||
|
tx_codec: Arc<Mutex<String>>,
|
||||||
|
rx_codec: Arc<Mutex<String>>,
|
||||||
|
transport: Arc<wzp_transport::QuinnTransport>,
|
||||||
|
start_time: Instant,
|
||||||
|
fingerprint: String,
|
||||||
|
/// Keep audio handles alive for the duration of the call.
|
||||||
|
/// Wrapped in SyncWrapper because AudioUnit isn't Sync.
|
||||||
|
_audio_handle: SyncWrapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CallEngine {
|
||||||
|
/// Android engine path — uses the standalone `wzp-native` cdylib
|
||||||
|
/// (loaded at startup via `crate::wzp_native::init()`) for Oboe-backed
|
||||||
|
/// capture and playout instead of CPAL. Mirrors the desktop send/recv
|
||||||
|
/// task structure otherwise.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub async fn start<F>(
|
||||||
|
relay: String,
|
||||||
|
room: String,
|
||||||
|
alias: String,
|
||||||
|
_os_aec: bool,
|
||||||
|
quality: String,
|
||||||
|
reuse_endpoint: Option<wzp_transport::Endpoint>,
|
||||||
|
event_cb: F,
|
||||||
|
) -> Result<Self, anyhow::Error>
|
||||||
|
where
|
||||||
|
F: Fn(&str, &str) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
info!(%relay, %room, %alias, %quality, has_reuse = reuse_endpoint.is_some(), "CallEngine::start (android) invoked");
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
let relay_addr: SocketAddr = relay.parse()?;
|
||||||
|
info!(%relay_addr, "resolved relay addr");
|
||||||
|
|
||||||
|
// Identity via shared helper (uses Tauri path().app_data_dir()).
|
||||||
|
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");
|
||||||
|
|
||||||
|
// QUIC transport + handshake.
|
||||||
|
//
|
||||||
|
// If a `reuse_endpoint` was passed in (the direct-call path, where we
|
||||||
|
// already opened a quinn::Endpoint for the signal connection), reuse
|
||||||
|
// it: a second quinn::Endpoint on Android silently fails to complete
|
||||||
|
// the QUIC handshake against the same relay. Reusing the existing
|
||||||
|
// socket lets quinn multiplex the signal + media connections on one
|
||||||
|
// UDP port.
|
||||||
|
let endpoint = if let Some(ep) = reuse_endpoint {
|
||||||
|
info!(local_addr = ?ep.local_addr().ok(), "reusing signal endpoint for media connection");
|
||||||
|
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 })?;
|
||||||
|
info!(local_addr = ?ep.local_addr().ok(), "created new endpoint, dialing relay");
|
||||||
|
ep
|
||||||
|
};
|
||||||
|
let client_config = wzp_transport::client_config();
|
||||||
|
let conn = match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
wzp_transport::connect(&endpoint, relay_addr, &room, client_config),
|
||||||
|
).await {
|
||||||
|
Ok(Ok(c)) => c,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
error!("connect failed: {e}");
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
error!("connect TIMED OUT after 10s — QUIC handshake never completed. Relay may be unreachable from this endpoint.");
|
||||||
|
return Err(anyhow::anyhow!("QUIC connect timeout (10s)"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("QUIC connection established, performing handshake");
|
||||||
|
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
|
||||||
|
|
||||||
|
let _session = wzp_client::handshake::perform_handshake(
|
||||||
|
&*transport,
|
||||||
|
&seed.0,
|
||||||
|
Some(&alias),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
|
||||||
|
info!("connected to relay, handshake complete");
|
||||||
|
event_cb("connected", &format!("joined room {room}"));
|
||||||
|
|
||||||
|
// Oboe audio via the wzp-native cdylib that was dlopen'd at
|
||||||
|
// startup. `wzp_native::audio_start()` brings up the capture +
|
||||||
|
// playout streams; send/recv tasks below pull/push PCM through
|
||||||
|
// the extern "C" bridge rings.
|
||||||
|
if !crate::wzp_native::is_loaded() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"wzp-native not loaded — dlopen failed at startup"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Err(code) = crate::wzp_native::audio_start() {
|
||||||
|
return Err(anyhow::anyhow!("wzp_native_audio_start failed: code {code}"));
|
||||||
|
}
|
||||||
|
info!("wzp-native audio started");
|
||||||
|
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
let mic_muted = Arc::new(AtomicBool::new(false));
|
||||||
|
let spk_muted = Arc::new(AtomicBool::new(false));
|
||||||
|
let participants: Arc<Mutex<Vec<ParticipantInfo>>> = Arc::new(Mutex::new(vec![]));
|
||||||
|
let frames_sent = Arc::new(AtomicU64::new(0));
|
||||||
|
let frames_received = Arc::new(AtomicU64::new(0));
|
||||||
|
let audio_level = Arc::new(AtomicU32::new(0));
|
||||||
|
let tx_codec = Arc::new(Mutex::new(String::new()));
|
||||||
|
let rx_codec = Arc::new(Mutex::new(String::new()));
|
||||||
|
|
||||||
|
// Send task — drain Oboe capture ring, Opus-encode, push to transport.
|
||||||
|
let send_t = transport.clone();
|
||||||
|
let send_r = running.clone();
|
||||||
|
let send_mic = mic_muted.clone();
|
||||||
|
let send_fs = frames_sent.clone();
|
||||||
|
let send_level = audio_level.clone();
|
||||||
|
let send_drops = Arc::new(AtomicU64::new(0));
|
||||||
|
let send_quality = quality.clone();
|
||||||
|
let send_tx_codec = tx_codec.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let profile = resolve_quality(&send_quality);
|
||||||
|
let config = match profile {
|
||||||
|
Some(p) => CallConfig {
|
||||||
|
noise_suppression: false,
|
||||||
|
suppression_enabled: false,
|
||||||
|
..CallConfig::from_profile(p)
|
||||||
|
},
|
||||||
|
None => CallConfig {
|
||||||
|
noise_suppression: false,
|
||||||
|
suppression_enabled: false,
|
||||||
|
..CallConfig::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let frame_samples = (config.profile.frame_duration_ms as usize) * 48;
|
||||||
|
info!(codec = ?config.profile.codec, frame_samples, "send task starting (android/oboe)");
|
||||||
|
*send_tx_codec.lock().await = format!("{:?}", config.profile.codec);
|
||||||
|
let mut encoder = CallEncoder::new(&config);
|
||||||
|
encoder.set_aec_enabled(false);
|
||||||
|
let mut buf = vec![0i16; frame_samples];
|
||||||
|
|
||||||
|
let mut heartbeat = std::time::Instant::now();
|
||||||
|
let mut last_rms: u32 = 0;
|
||||||
|
let mut last_pkt_bytes: usize = 0;
|
||||||
|
let mut short_reads: u64 = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !send_r.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// wzp-native doesn't expose `available()`, so we just try
|
||||||
|
// to read a full frame and sleep briefly if the ring is
|
||||||
|
// short. Oboe's capture callback fills at a steady rate
|
||||||
|
// so in steady state this spins once per frame.
|
||||||
|
let read = crate::wzp_native::audio_read_capture(&mut buf);
|
||||||
|
if read < frame_samples {
|
||||||
|
short_reads += 1;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RMS for UI meter
|
||||||
|
let sum_sq: f64 = buf.iter().map(|&s| (s as f64) * (s as f64)).sum();
|
||||||
|
let rms = (sum_sq / buf.len() as f64).sqrt() as u32;
|
||||||
|
send_level.store(rms, Ordering::Relaxed);
|
||||||
|
last_rms = rms;
|
||||||
|
|
||||||
|
if send_mic.load(Ordering::Relaxed) {
|
||||||
|
buf.fill(0);
|
||||||
|
}
|
||||||
|
match encoder.encode_frame(&buf) {
|
||||||
|
Ok(pkts) => {
|
||||||
|
for pkt in &pkts {
|
||||||
|
last_pkt_bytes = pkt.payload.len();
|
||||||
|
if let Err(e) = send_t.send_media(pkt).await {
|
||||||
|
send_drops.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if send_drops.load(Ordering::Relaxed) <= 3 {
|
||||||
|
tracing::warn!("send_media error (dropping packet): {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send_fs.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(e) => error!("encode: {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat every 2s with capture+encode+send state
|
||||||
|
if heartbeat.elapsed() >= std::time::Duration::from_secs(2) {
|
||||||
|
let fs = send_fs.load(Ordering::Relaxed);
|
||||||
|
let drops = send_drops.load(Ordering::Relaxed);
|
||||||
|
info!(
|
||||||
|
frames_sent = fs,
|
||||||
|
last_rms,
|
||||||
|
last_pkt_bytes,
|
||||||
|
short_reads,
|
||||||
|
send_drops = drops,
|
||||||
|
"send heartbeat (android)"
|
||||||
|
);
|
||||||
|
heartbeat = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recv task — decode incoming packets, push PCM into Oboe playout.
|
||||||
|
let recv_t = transport.clone();
|
||||||
|
let recv_r = running.clone();
|
||||||
|
let recv_spk = spk_muted.clone();
|
||||||
|
let recv_fr = frames_received.clone();
|
||||||
|
let recv_rx_codec = rx_codec.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let initial_profile = resolve_quality(&quality).unwrap_or(QualityProfile::GOOD);
|
||||||
|
let mut decoder = wzp_codec::create_decoder(initial_profile);
|
||||||
|
let mut current_codec = initial_profile.codec;
|
||||||
|
let mut agc = wzp_codec::AutoGainControl::new();
|
||||||
|
let mut pcm = vec![0i16; FRAME_SAMPLES_40MS];
|
||||||
|
info!(codec = ?current_codec, "recv task starting (android/oboe)");
|
||||||
|
|
||||||
|
// ─── Decoded-PCM recorder (debug) ────────────────────────────
|
||||||
|
// Dumps the first ~10 seconds of post-AGC PCM to a raw i16 LE
|
||||||
|
// file in the app's private data dir so we can adb pull it and
|
||||||
|
// play it back to prove the pipeline is producing real audio
|
||||||
|
// 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 mut recorder = match recorder_path.as_ref() {
|
||||||
|
Some(p) => match std::fs::File::create(p) {
|
||||||
|
Ok(f) => {
|
||||||
|
info!(path = %p.display(), "decoded-pcm recorder open");
|
||||||
|
Some(std::io::BufWriter::new(f))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(path = %p.display(), error = %e, "decoded-pcm recorder open failed");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let mut recorder_bytes: u64 = 0;
|
||||||
|
// Stop writing after ~10 seconds @ 48kHz mono i16 = ~960KB.
|
||||||
|
const RECORDER_MAX_BYTES: u64 = 48_000 * 2 * 10;
|
||||||
|
|
||||||
|
let mut heartbeat = std::time::Instant::now();
|
||||||
|
let mut decoded_frames: u64 = 0;
|
||||||
|
let mut written_samples: u64 = 0;
|
||||||
|
let mut last_decode_n: usize = 0;
|
||||||
|
let mut last_written: usize = 0;
|
||||||
|
let mut decode_errs: u64 = 0;
|
||||||
|
let mut first_packet_logged = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !recv_r.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(100),
|
||||||
|
recv_t.recv_media(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(Some(pkt))) => {
|
||||||
|
if !first_packet_logged {
|
||||||
|
info!(codec_id = ?pkt.header.codec_id, payload_bytes = pkt.payload.len(), is_repair = pkt.header.is_repair, "recv: first media packet received");
|
||||||
|
first_packet_logged = true;
|
||||||
|
}
|
||||||
|
if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
|
||||||
|
{
|
||||||
|
let mut rx = recv_rx_codec.lock().await;
|
||||||
|
let codec_name = format!("{:?}", pkt.header.codec_id);
|
||||||
|
if *rx != codec_name { *rx = codec_name; }
|
||||||
|
}
|
||||||
|
if pkt.header.codec_id != current_codec {
|
||||||
|
let new_profile = match pkt.header.codec_id {
|
||||||
|
CodecId::Opus24k => QualityProfile::GOOD,
|
||||||
|
CodecId::Opus6k => QualityProfile::DEGRADED,
|
||||||
|
CodecId::Opus32k => QualityProfile::STUDIO_32K,
|
||||||
|
CodecId::Opus48k => QualityProfile::STUDIO_48K,
|
||||||
|
CodecId::Opus64k => QualityProfile::STUDIO_64K,
|
||||||
|
CodecId::Codec2_1200 => QualityProfile::CATASTROPHIC,
|
||||||
|
CodecId::Codec2_3200 => QualityProfile {
|
||||||
|
codec: CodecId::Codec2_3200,
|
||||||
|
fec_ratio: 0.5, frame_duration_ms: 20, frames_per_block: 5,
|
||||||
|
},
|
||||||
|
other => QualityProfile { codec: other, ..QualityProfile::GOOD },
|
||||||
|
};
|
||||||
|
info!(from = ?current_codec, to = ?pkt.header.codec_id, "recv: switching decoder");
|
||||||
|
let _ = decoder.set_profile(new_profile);
|
||||||
|
current_codec = pkt.header.codec_id;
|
||||||
|
}
|
||||||
|
match decoder.decode(&pkt.payload, &mut pcm) {
|
||||||
|
Ok(n) => {
|
||||||
|
last_decode_n = n;
|
||||||
|
decoded_frames += 1;
|
||||||
|
// 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);
|
||||||
|
for &s in slice.iter() {
|
||||||
|
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;
|
||||||
|
info!(
|
||||||
|
decoded_frames,
|
||||||
|
n,
|
||||||
|
sample_lo = lo,
|
||||||
|
sample_hi = hi,
|
||||||
|
rms,
|
||||||
|
codec = ?current_codec,
|
||||||
|
"recv: decoded PCM sample range"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
agc.process_frame(&mut pcm[..n]);
|
||||||
|
|
||||||
|
// Dump to debug recorder before playout
|
||||||
|
// so we capture post-AGC samples that
|
||||||
|
// are exactly what we hand to Oboe.
|
||||||
|
if let Some(rec) = recorder.as_mut() {
|
||||||
|
if recorder_bytes < RECORDER_MAX_BYTES {
|
||||||
|
let slice = &pcm[..n];
|
||||||
|
// SAFETY: i16 is Plain Old Data;
|
||||||
|
// writing its little-endian bytes
|
||||||
|
// is well-defined on all targets
|
||||||
|
// we build for.
|
||||||
|
let byte_slice: &[u8] = unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
slice.as_ptr() as *const u8,
|
||||||
|
slice.len() * 2,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let _ = rec.write_all(byte_slice);
|
||||||
|
recorder_bytes = recorder_bytes
|
||||||
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recv_spk.load(Ordering::Relaxed) {
|
||||||
|
let w = crate::wzp_native::audio_write_playout(&pcm[..n]);
|
||||||
|
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)");
|
||||||
|
}
|
||||||
|
} 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
decode_errs += 1;
|
||||||
|
if decode_errs <= 3 {
|
||||||
|
tracing::warn!("decode error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recv_fr.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Ok(Ok(None)) => break,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
let msg = e.to_string();
|
||||||
|
if msg.contains("closed") || msg.contains("reset") {
|
||||||
|
error!("recv fatal: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat every 2s with decode+playout state
|
||||||
|
if heartbeat.elapsed() >= std::time::Duration::from_secs(2) {
|
||||||
|
let fr = recv_fr.load(Ordering::Relaxed);
|
||||||
|
info!(
|
||||||
|
recv_fr = fr,
|
||||||
|
decoded_frames,
|
||||||
|
last_decode_n,
|
||||||
|
last_written,
|
||||||
|
written_samples,
|
||||||
|
decode_errs,
|
||||||
|
codec = ?current_codec,
|
||||||
|
"recv heartbeat (android)"
|
||||||
|
);
|
||||||
|
heartbeat = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal task (presence — same shape as desktop).
|
||||||
|
let sig_t = transport.clone();
|
||||||
|
let sig_r = running.clone();
|
||||||
|
let sig_p = participants.clone();
|
||||||
|
let event_cb = Arc::new(event_cb);
|
||||||
|
let sig_cb = event_cb.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
if !sig_r.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
sig_t.recv_signal(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(Some(wzp_proto::SignalMessage::RoomUpdate {
|
||||||
|
participants: parts,
|
||||||
|
..
|
||||||
|
}))) => {
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let unique: Vec<ParticipantInfo> = parts
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| seen.insert((p.fingerprint.clone(), p.alias.clone())))
|
||||||
|
.map(|p| ParticipantInfo {
|
||||||
|
fingerprint: p.fingerprint,
|
||||||
|
alias: p.alias,
|
||||||
|
relay_label: p.relay_label,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let count = unique.len();
|
||||||
|
*sig_p.lock().await = unique;
|
||||||
|
sig_cb("room-update", &format!("{count} participants"));
|
||||||
|
}
|
||||||
|
Ok(Ok(Some(_))) => {}
|
||||||
|
Ok(Ok(None)) => break,
|
||||||
|
Ok(Err(_)) => break,
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
running,
|
||||||
|
mic_muted,
|
||||||
|
spk_muted,
|
||||||
|
participants,
|
||||||
|
frames_sent,
|
||||||
|
frames_received,
|
||||||
|
audio_level,
|
||||||
|
transport,
|
||||||
|
start_time: Instant::now(),
|
||||||
|
fingerprint,
|
||||||
|
tx_codec,
|
||||||
|
rx_codec,
|
||||||
|
// No CPAL / VPIO handle to keep alive on Android — wzp_native
|
||||||
|
// is a static dlopen'd library, the audio streams live inside
|
||||||
|
// the standalone cdylib's process-global singleton.
|
||||||
|
_audio_handle: SyncWrapper(Box::new(())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
pub async fn start<F>(
|
||||||
|
relay: String,
|
||||||
|
room: String,
|
||||||
|
alias: String,
|
||||||
|
_os_aec: bool,
|
||||||
|
quality: String,
|
||||||
|
reuse_endpoint: Option<wzp_transport::Endpoint>,
|
||||||
|
event_cb: F,
|
||||||
|
) -> Result<Self, anyhow::Error>
|
||||||
|
where
|
||||||
|
F: Fn(&str, &str) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
info!(%relay, %room, %alias, %quality, has_reuse = reuse_endpoint.is_some(), "CallEngine::start (desktop) invoked");
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
let relay_addr: SocketAddr = relay.parse()?;
|
||||||
|
|
||||||
|
// Identity via the SHARED helper — same path resolution as
|
||||||
|
// register_signal (Tauri app_data_dir, e.g. on macOS
|
||||||
|
// ~/Library/Application Support/com.wzp.desktop/.wzp/identity).
|
||||||
|
//
|
||||||
|
// The previous implementation loaded the seed manually from
|
||||||
|
// $HOME/.wzp/identity which is a DIFFERENT file on macOS, so
|
||||||
|
// register_signal and CallEngine::start were using different
|
||||||
|
// identities — direct calls placed from desktop were routed
|
||||||
|
// by the relay under the CallEngine fingerprint but the callee
|
||||||
|
// had registered under a different fingerprint, making the
|
||||||
|
// call unroutable.
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Connect — reuse the signal endpoint if the direct-call path gave
|
||||||
|
// us one, otherwise create a fresh one (SFU room join path).
|
||||||
|
let endpoint = if let Some(ep) = reuse_endpoint {
|
||||||
|
info!(local_addr = ?ep.local_addr().ok(), "reusing signal endpoint for media connection");
|
||||||
|
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 })?;
|
||||||
|
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 })?;
|
||||||
|
info!("QUIC connection established, performing handshake");
|
||||||
|
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
|
||||||
|
|
||||||
|
// Handshake
|
||||||
|
let _session = wzp_client::handshake::perform_handshake(
|
||||||
|
&*transport,
|
||||||
|
&seed.0,
|
||||||
|
Some(&alias),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| { error!("perform_handshake failed: {e}"); e })?;
|
||||||
|
|
||||||
|
info!("connected to relay, handshake complete");
|
||||||
|
event_cb("connected", &format!("joined room {room}"));
|
||||||
|
|
||||||
|
// Audio I/O — VPIO (OS AEC) on macOS, plain CPAL otherwise.
|
||||||
|
// The audio handle must be stored in CallEngine to keep streams alive.
|
||||||
|
let (capture_ring, playout_ring, audio_handle): (_, _, Box<dyn std::any::Any + Send>) =
|
||||||
|
if _os_aec {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
match wzp_client::audio_vpio::VpioAudio::start() {
|
||||||
|
Ok(v) => {
|
||||||
|
let cr = v.capture_ring().clone();
|
||||||
|
let pr = v.playout_ring().clone();
|
||||||
|
info!("using VoiceProcessingIO (OS AEC)");
|
||||||
|
(cr, pr, Box::new(v))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!("VPIO failed ({e}), falling back to CPAL");
|
||||||
|
let capture = AudioCapture::start()?;
|
||||||
|
let playback = AudioPlayback::start()?;
|
||||||
|
let cr = capture.ring().clone();
|
||||||
|
let pr = playback.ring().clone();
|
||||||
|
(cr, pr, Box::new((capture, playback)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
info!("OS AEC not available on this platform, using CPAL");
|
||||||
|
let capture = AudioCapture::start()?;
|
||||||
|
let playback = AudioPlayback::start()?;
|
||||||
|
let cr = capture.ring().clone();
|
||||||
|
let pr = playback.ring().clone();
|
||||||
|
(cr, pr, Box::new((capture, playback)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let capture = AudioCapture::start()?;
|
||||||
|
let playback = AudioPlayback::start()?;
|
||||||
|
let cr = capture.ring().clone();
|
||||||
|
let pr = playback.ring().clone();
|
||||||
|
(cr, pr, Box::new((capture, playback)))
|
||||||
|
};
|
||||||
|
|
||||||
|
let running = Arc::new(AtomicBool::new(true));
|
||||||
|
let mic_muted = Arc::new(AtomicBool::new(false));
|
||||||
|
let spk_muted = Arc::new(AtomicBool::new(false));
|
||||||
|
let participants: Arc<Mutex<Vec<ParticipantInfo>>> = Arc::new(Mutex::new(vec![]));
|
||||||
|
let frames_sent = Arc::new(AtomicU64::new(0));
|
||||||
|
let frames_received = Arc::new(AtomicU64::new(0));
|
||||||
|
let audio_level = Arc::new(AtomicU32::new(0));
|
||||||
|
let tx_codec = Arc::new(Mutex::new(String::new()));
|
||||||
|
let rx_codec = Arc::new(Mutex::new(String::new()));
|
||||||
|
|
||||||
|
// Send task
|
||||||
|
let send_t = transport.clone();
|
||||||
|
let send_r = running.clone();
|
||||||
|
let send_mic = mic_muted.clone();
|
||||||
|
let send_fs = frames_sent.clone();
|
||||||
|
let send_level = audio_level.clone();
|
||||||
|
let send_drops = Arc::new(AtomicU64::new(0));
|
||||||
|
let send_quality = quality.clone();
|
||||||
|
let send_tx_codec = tx_codec.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let profile = resolve_quality(&send_quality);
|
||||||
|
let config = match profile {
|
||||||
|
Some(p) => CallConfig {
|
||||||
|
noise_suppression: false,
|
||||||
|
suppression_enabled: false,
|
||||||
|
..CallConfig::from_profile(p)
|
||||||
|
},
|
||||||
|
None => CallConfig {
|
||||||
|
noise_suppression: false,
|
||||||
|
suppression_enabled: false,
|
||||||
|
..CallConfig::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let frame_samples = (config.profile.frame_duration_ms as usize) * 48;
|
||||||
|
info!(codec = ?config.profile.codec, frame_samples, "send task starting");
|
||||||
|
*send_tx_codec.lock().await = format!("{:?}", config.profile.codec);
|
||||||
|
let mut encoder = CallEncoder::new(&config);
|
||||||
|
encoder.set_aec_enabled(false); // OS AEC or none
|
||||||
|
let mut buf = vec![0i16; frame_samples];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !send_r.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if capture_ring.available() < frame_samples {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
capture_ring.read(&mut buf);
|
||||||
|
|
||||||
|
// Compute RMS audio level for UI meter
|
||||||
|
if !buf.is_empty() {
|
||||||
|
let sum_sq: f64 = buf.iter().map(|&s| (s as f64) * (s as f64)).sum();
|
||||||
|
let rms = (sum_sq / buf.len() as f64).sqrt() as u32;
|
||||||
|
send_level.store(rms, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if send_mic.load(Ordering::Relaxed) {
|
||||||
|
buf.fill(0);
|
||||||
|
}
|
||||||
|
match encoder.encode_frame(&buf) {
|
||||||
|
Ok(pkts) => {
|
||||||
|
for pkt in &pkts {
|
||||||
|
if let Err(e) = send_t.send_media(pkt).await {
|
||||||
|
// Transient congestion (Blocked) — drop packet, keep going
|
||||||
|
send_drops.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if send_drops.load(Ordering::Relaxed) <= 3 {
|
||||||
|
tracing::warn!("send_media error (dropping packet): {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send_fs.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(e) => error!("encode: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recv task (direct playout with auto codec switch)
|
||||||
|
let recv_t = transport.clone();
|
||||||
|
let recv_r = running.clone();
|
||||||
|
let recv_spk = spk_muted.clone();
|
||||||
|
let recv_fr = frames_received.clone();
|
||||||
|
let recv_rx_codec = rx_codec.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let initial_profile = resolve_quality(&quality).unwrap_or(QualityProfile::GOOD);
|
||||||
|
let mut decoder = wzp_codec::create_decoder(initial_profile);
|
||||||
|
let mut current_codec = initial_profile.codec;
|
||||||
|
let mut agc = wzp_codec::AutoGainControl::new();
|
||||||
|
let mut pcm = vec![0i16; FRAME_SAMPLES_40MS]; // big enough for any codec
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !recv_r.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(100),
|
||||||
|
recv_t.recv_media(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(Some(pkt))) => {
|
||||||
|
if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
|
||||||
|
// Track RX codec
|
||||||
|
{
|
||||||
|
let mut rx = recv_rx_codec.lock().await;
|
||||||
|
let codec_name = format!("{:?}", pkt.header.codec_id);
|
||||||
|
if *rx != codec_name { *rx = codec_name; }
|
||||||
|
}
|
||||||
|
// Auto-switch decoder if incoming codec differs
|
||||||
|
if pkt.header.codec_id != current_codec {
|
||||||
|
let new_profile = match pkt.header.codec_id {
|
||||||
|
CodecId::Opus24k => QualityProfile::GOOD,
|
||||||
|
CodecId::Opus6k => QualityProfile::DEGRADED,
|
||||||
|
CodecId::Opus32k => QualityProfile::STUDIO_32K,
|
||||||
|
CodecId::Opus48k => QualityProfile::STUDIO_48K,
|
||||||
|
CodecId::Opus64k => QualityProfile::STUDIO_64K,
|
||||||
|
CodecId::Codec2_1200 => QualityProfile::CATASTROPHIC,
|
||||||
|
CodecId::Codec2_3200 => QualityProfile {
|
||||||
|
codec: CodecId::Codec2_3200,
|
||||||
|
fec_ratio: 0.5, frame_duration_ms: 20, frames_per_block: 5,
|
||||||
|
},
|
||||||
|
other => QualityProfile { codec: other, ..QualityProfile::GOOD },
|
||||||
|
};
|
||||||
|
info!(from = ?current_codec, to = ?pkt.header.codec_id, "recv: switching decoder");
|
||||||
|
let _ = decoder.set_profile(new_profile);
|
||||||
|
current_codec = pkt.header.codec_id;
|
||||||
|
}
|
||||||
|
if let Ok(n) = decoder.decode(&pkt.payload, &mut pcm) {
|
||||||
|
agc.process_frame(&mut pcm[..n]);
|
||||||
|
if !recv_spk.load(Ordering::Relaxed) {
|
||||||
|
playout_ring.write(&pcm[..n]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recv_fr.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Ok(Ok(None)) => break,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
let msg = e.to_string();
|
||||||
|
if msg.contains("closed") || msg.contains("reset") {
|
||||||
|
error!("recv fatal: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signal task (presence)
|
||||||
|
let sig_t = transport.clone();
|
||||||
|
let sig_r = running.clone();
|
||||||
|
let sig_p = participants.clone();
|
||||||
|
let event_cb = Arc::new(event_cb);
|
||||||
|
let sig_cb = event_cb.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
if !sig_r.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
sig_t.recv_signal(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(Some(wzp_proto::SignalMessage::RoomUpdate {
|
||||||
|
participants: parts,
|
||||||
|
..
|
||||||
|
}))) => {
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let unique: Vec<ParticipantInfo> = parts
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| seen.insert((p.fingerprint.clone(), p.alias.clone())))
|
||||||
|
.map(|p| ParticipantInfo {
|
||||||
|
fingerprint: p.fingerprint,
|
||||||
|
alias: p.alias,
|
||||||
|
relay_label: p.relay_label,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let count = unique.len();
|
||||||
|
*sig_p.lock().await = unique;
|
||||||
|
sig_cb("room-update", &format!("{count} participants"));
|
||||||
|
}
|
||||||
|
Ok(Ok(Some(_))) => {}
|
||||||
|
Ok(Ok(None)) => break,
|
||||||
|
Ok(Err(_)) => break,
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
running,
|
||||||
|
mic_muted,
|
||||||
|
spk_muted,
|
||||||
|
participants,
|
||||||
|
frames_sent,
|
||||||
|
frames_received,
|
||||||
|
audio_level,
|
||||||
|
transport,
|
||||||
|
start_time: Instant::now(),
|
||||||
|
fingerprint,
|
||||||
|
tx_codec,
|
||||||
|
rx_codec,
|
||||||
|
_audio_handle: SyncWrapper(audio_handle),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_mic(&self) -> bool {
|
||||||
|
let was = self.mic_muted.load(Ordering::Relaxed);
|
||||||
|
self.mic_muted.store(!was, Ordering::Relaxed);
|
||||||
|
!was
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_speaker(&self) -> bool {
|
||||||
|
let was = self.spk_muted.load(Ordering::Relaxed);
|
||||||
|
self.spk_muted.store(!was, Ordering::Relaxed);
|
||||||
|
!was
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn status(&self) -> EngineStatus {
|
||||||
|
let participants = {
|
||||||
|
let parts = self.participants.lock().await;
|
||||||
|
parts
|
||||||
|
.iter()
|
||||||
|
.map(|p| ParticipantInfo {
|
||||||
|
fingerprint: p.fingerprint.clone(),
|
||||||
|
alias: p.alias.clone(),
|
||||||
|
relay_label: p.relay_label.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}; // lock dropped here
|
||||||
|
EngineStatus {
|
||||||
|
mic_muted: self.mic_muted.load(Ordering::Relaxed),
|
||||||
|
spk_muted: self.spk_muted.load(Ordering::Relaxed),
|
||||||
|
participants,
|
||||||
|
frames_sent: self.frames_sent.load(Ordering::Relaxed),
|
||||||
|
frames_received: self.frames_received.load(Ordering::Relaxed),
|
||||||
|
audio_level: self.audio_level.load(Ordering::Relaxed),
|
||||||
|
call_duration_secs: self.start_time.elapsed().as_secs_f64(),
|
||||||
|
fingerprint: self.fingerprint.clone(),
|
||||||
|
tx_codec: self.tx_codec.lock().await.clone(),
|
||||||
|
rx_codec: self.rx_codec.lock().await.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(self) {
|
||||||
|
self.running.store(false, Ordering::SeqCst);
|
||||||
|
self.transport.close().await.ok();
|
||||||
|
// On Android, the Oboe capture/playout streams live inside the
|
||||||
|
// wzp-native cdylib as a process-global singleton. Explicitly stop
|
||||||
|
// them here so the mic + speaker are released between calls, matching
|
||||||
|
// the desktop behaviour where dropping _audio_handle tears down CPAL.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
crate::wzp_native::audio_stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
desktop/src-tauri/src/history.rs
Normal file
180
desktop/src-tauri/src/history.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
//! Call history store.
|
||||||
|
//!
|
||||||
|
//! Keeps a rolling JSON file of the last N direct-call events so the UI can
|
||||||
|
//! show "recent contacts" + "call history with callback buttons" on the
|
||||||
|
//! direct-call screen. Storage lives in `<APP_DATA_DIR>/call_history.json`
|
||||||
|
//! alongside the identity file. The file is read lazily on first access and
|
||||||
|
//! cached in an RwLock behind a OnceLock.
|
||||||
|
//!
|
||||||
|
//! This is a v1 — no duration tracking yet, entries are logged at the
|
||||||
|
//! moment the direction is decided (placed / received / missed).
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{OnceLock, RwLock};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Maximum number of history entries we keep. Older ones are pruned FIFO.
|
||||||
|
const MAX_ENTRIES: usize = 200;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum CallDirection {
|
||||||
|
/// Local user placed the call.
|
||||||
|
Placed,
|
||||||
|
/// Remote user called and local user answered.
|
||||||
|
Received,
|
||||||
|
/// Remote user called but local user did not answer (rejected or
|
||||||
|
/// missed entirely — the UI treats these identically).
|
||||||
|
Missed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CallHistoryEntry {
|
||||||
|
pub call_id: String,
|
||||||
|
pub peer_fp: String,
|
||||||
|
pub peer_alias: Option<String>,
|
||||||
|
pub direction: CallDirection,
|
||||||
|
/// Seconds since UNIX epoch, UTC.
|
||||||
|
pub timestamp_unix: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── In-process store (loaded from disk once) ─────────────────────────────
|
||||||
|
|
||||||
|
static STORE: OnceLock<RwLock<Vec<CallHistoryEntry>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn store() -> &'static RwLock<Vec<CallHistoryEntry>> {
|
||||||
|
STORE.get_or_init(|| RwLock::new(load_from_disk()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn history_path() -> PathBuf {
|
||||||
|
crate::APP_DATA_DIR
|
||||||
|
.get()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
PathBuf::from(home).join(".wzp")
|
||||||
|
})
|
||||||
|
.join("call_history.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_from_disk() -> Vec<CallHistoryEntry> {
|
||||||
|
let path = history_path();
|
||||||
|
let Ok(bytes) = std::fs::read(&path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
serde_json::from_slice::<Vec<CallHistoryEntry>>(&bytes)
|
||||||
|
.inspect_err(|e| tracing::warn!(path = %path.display(), error = %e, "call_history.json parse failed"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_to_disk(entries: &[CallHistoryEntry]) {
|
||||||
|
let path = history_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
if std::fs::write(&tmp, &json).is_ok() {
|
||||||
|
let _ = std::fs::rename(&tmp, &path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_unix() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
) {
|
||||||
|
tracing::info!(
|
||||||
|
%call_id, %peer_fp, ?direction,
|
||||||
|
alias = ?peer_alias,
|
||||||
|
"history::log"
|
||||||
|
);
|
||||||
|
let entry = CallHistoryEntry {
|
||||||
|
call_id: call_id.clone(),
|
||||||
|
peer_fp,
|
||||||
|
peer_alias,
|
||||||
|
direction,
|
||||||
|
timestamp_unix: now_unix(),
|
||||||
|
};
|
||||||
|
let mut guard = store().write().unwrap();
|
||||||
|
// If an entry for this call_id already exists, update it in-place
|
||||||
|
// rather than appending a duplicate. Protects against the caller
|
||||||
|
// side adding a second Missed row when the callee's DirectCallOffer
|
||||||
|
// bounces back through federation / loopback, or when some future
|
||||||
|
// relay routing edge case double-emits a signal. The dedup keeps
|
||||||
|
// history tidy and matches what the user intuitively expects (one
|
||||||
|
// history row per call, not one per signal event).
|
||||||
|
if let Some(existing) = guard.iter_mut().rev().find(|e| e.call_id == call_id) {
|
||||||
|
tracing::info!(%call_id, from = ?existing.direction, to = ?direction, "history::log replacing existing entry");
|
||||||
|
existing.direction = direction;
|
||||||
|
existing.timestamp_unix = entry.timestamp_unix;
|
||||||
|
save_to_disk(&guard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
guard.push(entry);
|
||||||
|
if guard.len() > MAX_ENTRIES {
|
||||||
|
let drop_n = guard.len() - MAX_ENTRIES;
|
||||||
|
guard.drain(0..drop_n);
|
||||||
|
}
|
||||||
|
save_to_disk(&guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a copy of all entries in reverse-chronological order
|
||||||
|
/// (most recent first).
|
||||||
|
pub fn all() -> Vec<CallHistoryEntry> {
|
||||||
|
let guard = store().read().unwrap();
|
||||||
|
guard.iter().rev().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unique peer contacts sorted by most recent interaction. Each contact
|
||||||
|
/// is represented by the newest history entry for that fingerprint.
|
||||||
|
pub fn contacts() -> Vec<CallHistoryEntry> {
|
||||||
|
let guard = store().read().unwrap();
|
||||||
|
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
// iterate newest → oldest
|
||||||
|
for entry in guard.iter().rev() {
|
||||||
|
if seen.insert(entry.peer_fp.clone()) {
|
||||||
|
out.push(entry.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the entire history and persist the empty file.
|
||||||
|
pub fn clear() {
|
||||||
|
let mut guard = store().write().unwrap();
|
||||||
|
guard.clear();
|
||||||
|
save_to_disk(&guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a Missed-candidate entry that matches `call_id` and hasn't been
|
||||||
|
/// answered yet. Used by the signal loop to turn "pending incoming" into
|
||||||
|
/// "Received" when the user accepts.
|
||||||
|
pub fn mark_received_if_pending(call_id: &str) -> bool {
|
||||||
|
let mut guard = store().write().unwrap();
|
||||||
|
for entry in guard.iter_mut().rev() {
|
||||||
|
if entry.call_id == call_id && entry.direction == CallDirection::Missed {
|
||||||
|
entry.direction = CallDirection::Received;
|
||||||
|
save_to_disk(&guard);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
700
desktop/src-tauri/src/lib.rs
Normal file
700
desktop/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,700 @@
|
|||||||
|
// WarzonePhone Tauri backend — shared between desktop (macOS/Windows/Linux)
|
||||||
|
// and Tauri mobile (Android/iOS). Platform-specific audio is cfg-gated.
|
||||||
|
|
||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
// Call engine — now compiled on every platform. On desktop it runs the real
|
||||||
|
// CPAL/VPIO audio pipeline; on Android the engine calls into the standalone
|
||||||
|
// wzp-native cdylib (via the wzp_native module) for Oboe-backed audio.
|
||||||
|
mod engine;
|
||||||
|
|
||||||
|
// Android runtime binding to libwzp_native.so (Oboe audio backend, built as
|
||||||
|
// a standalone cdylib with cargo-ndk to avoid the Tauri staticlib symbol
|
||||||
|
// leak — see docs/incident-tauri-android-init-tcb.md).
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
mod wzp_native;
|
||||||
|
|
||||||
|
// Android AudioManager bridge (routing earpiece / speaker / BT).
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
mod android_audio;
|
||||||
|
|
||||||
|
// Direct-call history store (persisted JSON in app data dir).
|
||||||
|
mod history;
|
||||||
|
|
||||||
|
// CallEngine has a unified impl on both targets now — the Android branch of
|
||||||
|
// CallEngine::start() routes audio through the standalone wzp-native cdylib
|
||||||
|
// (loaded via the wzp_native module below), the desktop branch uses CPAL.
|
||||||
|
use engine::CallEngine;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
use tauri::{Emitter, Manager};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use wzp_proto::MediaTransport;
|
||||||
|
|
||||||
|
/// Short git hash captured at compile time by build.rs.
|
||||||
|
const GIT_HASH: &str = env!("WZP_GIT_HASH");
|
||||||
|
|
||||||
|
/// Resolved by `setup()` once we have a Tauri AppHandle. Holds the
|
||||||
|
/// platform-correct app data dir (e.g. `/data/data/com.wzp.desktop/files` on
|
||||||
|
/// Android, `~/Library/Application Support/com.wzp.desktop` on macOS).
|
||||||
|
static APP_DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Adjective list — keep in sync with the noun list below. Both are powers of
|
||||||
|
/// 2 friendly so the modulo bias is negligible.
|
||||||
|
const ALIAS_ADJECTIVES: &[&str] = &[
|
||||||
|
"Swift", "Silent", "Brave", "Calm", "Dark", "Fierce", "Ghost",
|
||||||
|
"Iron", "Lucky", "Noble", "Quick", "Sharp", "Storm", "Wild",
|
||||||
|
"Cold", "Bright", "Lone", "Red", "Grey", "Frosty", "Dusty",
|
||||||
|
"Rusty", "Neon", "Void", "Solar", "Lunar", "Cyber", "Pixel",
|
||||||
|
"Sonic", "Hyper", "Turbo", "Nano", "Mega", "Ultra", "Zinc",
|
||||||
|
];
|
||||||
|
const ALIAS_NOUNS: &[&str] = &[
|
||||||
|
"Wolf", "Hawk", "Fox", "Bear", "Lynx", "Crow", "Viper",
|
||||||
|
"Cobra", "Tiger", "Eagle", "Shark", "Raven", "Falcon", "Otter",
|
||||||
|
"Mantis", "Panda", "Jackal", "Badger", "Heron", "Bison",
|
||||||
|
"Condor", "Coyote", "Gecko", "Hornet", "Marten", "Osprey",
|
||||||
|
"Parrot", "Puma", "Raptor", "Stork", "Toucan", "Walrus",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Derive a stable human-readable alias from the seed bytes. Same seed →
|
||||||
|
/// same alias forever, different seeds → effectively random aliases.
|
||||||
|
fn derive_alias(seed: &wzp_crypto::Seed) -> String {
|
||||||
|
let adj_idx = (u16::from_le_bytes([seed.0[0], seed.0[1]]) as usize) % ALIAS_ADJECTIVES.len();
|
||||||
|
let noun_idx = (u16::from_le_bytes([seed.0[2], seed.0[3]]) as usize) % ALIAS_NOUNS.len();
|
||||||
|
format!("{} {}", ALIAS_ADJECTIVES[adj_idx], ALIAS_NOUNS[noun_idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct CallEvent {
|
||||||
|
kind: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct Participant {
|
||||||
|
fingerprint: String,
|
||||||
|
alias: Option<String>,
|
||||||
|
relay_label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct CallStatus {
|
||||||
|
active: bool,
|
||||||
|
mic_muted: bool,
|
||||||
|
spk_muted: bool,
|
||||||
|
participants: Vec<Participant>,
|
||||||
|
encode_fps: u64,
|
||||||
|
recv_fps: u64,
|
||||||
|
audio_level: u32,
|
||||||
|
call_duration_secs: f64,
|
||||||
|
fingerprint: String,
|
||||||
|
tx_codec: String,
|
||||||
|
rx_codec: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
engine: Mutex<Option<CallEngine>>,
|
||||||
|
signal: Arc<Mutex<SignalState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ping result with RTT and server identity hash.
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct PingResult {
|
||||||
|
rtt_ms: u32,
|
||||||
|
/// Server identity: SHA-256 of the QUIC peer certificate, hex-encoded.
|
||||||
|
server_fingerprint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ping a relay to check if it's online, measure RTT, and get server identity.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn ping_relay(relay: String) -> Result<PingResult, String> {
|
||||||
|
let addr: std::net::SocketAddr = relay.parse().map_err(|e| format!("bad address: {e}"))?;
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||||
|
let endpoint = wzp_transport::create_endpoint(bind, None).map_err(|e| format!("{e}"))?;
|
||||||
|
let client_cfg = wzp_transport::client_config();
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let conn_result = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(3),
|
||||||
|
wzp_transport::connect(&endpoint, addr, "ping", client_cfg),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Always close endpoint to prevent resource leaks
|
||||||
|
endpoint.close(0u32.into(), b"done");
|
||||||
|
|
||||||
|
match conn_result {
|
||||||
|
Ok(Ok(conn)) => {
|
||||||
|
let rtt_ms = start.elapsed().as_millis() as u32;
|
||||||
|
|
||||||
|
let server_fingerprint = conn
|
||||||
|
.peer_identity()
|
||||||
|
.and_then(|id| id.downcast::<Vec<rustls::pki_types::CertificateDer>>().ok())
|
||||||
|
.and_then(|certs| certs.first().map(|c| {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
c.as_ref().hash(&mut hasher);
|
||||||
|
let h = hasher.finish();
|
||||||
|
format!("{h:016x}")
|
||||||
|
}))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
format!("{:x}", addr.ip().to_string().len() as u64 * 0x9e3779b97f4a7c15 + addr.port() as u64)
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.close(0u32.into(), b"ping");
|
||||||
|
Ok(PingResult { rtt_ms, server_fingerprint })
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => Err(format!("{e}")),
|
||||||
|
Err(_) => Err("timeout (3s)".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the directory where identity/config should live.
|
||||||
|
///
|
||||||
|
/// Resolved at startup from Tauri's `path().app_data_dir()` API which gives
|
||||||
|
/// us the platform-correct app-private location:
|
||||||
|
/// - Android: `/data/data/<package_id>/files/com.wzp.desktop`
|
||||||
|
/// - macOS: `~/Library/Application Support/com.wzp.desktop`
|
||||||
|
/// - Linux: `~/.local/share/com.wzp.desktop`
|
||||||
|
///
|
||||||
|
/// Falls back to `$HOME/.wzp` on the desktop side if the OnceLock hasn't been
|
||||||
|
/// initialised yet (shouldn't happen in normal startup, but keeps the fn
|
||||||
|
/// total).
|
||||||
|
fn identity_dir() -> PathBuf {
|
||||||
|
if let Some(dir) = APP_DATA_DIR.get() {
|
||||||
|
return dir.clone();
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
// Last-resort default. The real path is set in setup() below.
|
||||||
|
std::path::PathBuf::from("/data/data/com.wzp.desktop/files")
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
|
||||||
|
std::path::PathBuf::from(home).join(".wzp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn identity_path() -> std::path::PathBuf {
|
||||||
|
identity_dir().join("identity")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the persisted seed, or generate-and-persist a new one if missing.
|
||||||
|
fn load_or_create_seed() -> Result<wzp_crypto::Seed, String> {
|
||||||
|
let path = identity_path();
|
||||||
|
if path.exists() {
|
||||||
|
let hex = std::fs::read_to_string(&path).map_err(|e| format!("read identity: {e}"))?;
|
||||||
|
return wzp_crypto::Seed::from_hex(hex.trim()).map_err(|e| format!("{e}"));
|
||||||
|
}
|
||||||
|
let seed = wzp_crypto::Seed::generate();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| format!("create identity dir: {e}"))?;
|
||||||
|
}
|
||||||
|
let hex: String = seed.0.iter().map(|b| format!("{b:02x}")).collect();
|
||||||
|
std::fs::write(&path, hex).map_err(|e| format!("write identity: {e}"))?;
|
||||||
|
Ok(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read fingerprint, generating a fresh identity if none exists yet.
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_identity() -> Result<String, String> {
|
||||||
|
let seed = load_or_create_seed()?;
|
||||||
|
Ok(seed.derive_identity().public_identity().fingerprint.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build/identity info shown on the home screen so the user can prove which
|
||||||
|
/// build is installed and what their stable alias is.
|
||||||
|
#[derive(Clone, Serialize)]
|
||||||
|
struct AppInfo {
|
||||||
|
/// Short git commit hash captured at build time.
|
||||||
|
git_hash: &'static str,
|
||||||
|
/// Stable adjective+noun derived from the seed.
|
||||||
|
alias: String,
|
||||||
|
/// Full fingerprint, e.g. "abcd:ef01:..."
|
||||||
|
fingerprint: String,
|
||||||
|
/// App data dir actually in use — useful for debugging EACCES issues.
|
||||||
|
data_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_app_info() -> Result<AppInfo, String> {
|
||||||
|
let seed = load_or_create_seed()?;
|
||||||
|
let pub_id = seed.derive_identity().public_identity();
|
||||||
|
Ok(AppInfo {
|
||||||
|
git_hash: GIT_HASH,
|
||||||
|
alias: derive_alias(&seed),
|
||||||
|
fingerprint: pub_id.fingerprint.to_string(),
|
||||||
|
data_dir: identity_dir().to_string_lossy().into_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn connect(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
relay: String,
|
||||||
|
room: String,
|
||||||
|
alias: String,
|
||||||
|
os_aec: bool,
|
||||||
|
quality: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let mut engine_lock = state.engine.lock().await;
|
||||||
|
if engine_lock.is_some() {
|
||||||
|
return Err("already connected".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we previously opened a quinn::Endpoint for the signaling connection
|
||||||
|
// (direct-call path), reuse it so the media connection shares the same
|
||||||
|
// UDP socket. This side-steps the Android issue where a second
|
||||||
|
// quinn::Endpoint silently hangs in the QUIC handshake.
|
||||||
|
let reuse_endpoint = state.signal.lock().await.endpoint.clone();
|
||||||
|
if reuse_endpoint.is_some() {
|
||||||
|
tracing::info!("connect: reusing existing signal endpoint for media connection");
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_clone = app.clone();
|
||||||
|
match CallEngine::start(relay, room, alias, os_aec, quality, reuse_endpoint, move |event_kind, message| {
|
||||||
|
let _ = app_clone.emit(
|
||||||
|
"call-event",
|
||||||
|
CallEvent {
|
||||||
|
kind: event_kind.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(eng) => {
|
||||||
|
*engine_lock = Some(eng);
|
||||||
|
Ok("connected".into())
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("{e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn disconnect(state: tauri::State<'_, Arc<AppState>>) -> Result<String, String> {
|
||||||
|
let mut engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(engine) = engine_lock.take() {
|
||||||
|
engine.stop().await;
|
||||||
|
Ok("disconnected".into())
|
||||||
|
} else {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn toggle_mic(state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
|
||||||
|
let engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(ref engine) = *engine_lock {
|
||||||
|
Ok(engine.toggle_mic())
|
||||||
|
} else {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn toggle_speaker(state: tauri::State<'_, Arc<AppState>>) -> Result<bool, String> {
|
||||||
|
let engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(ref engine) = *engine_lock {
|
||||||
|
Ok(engine.toggle_speaker())
|
||||||
|
} else {
|
||||||
|
Err("not connected".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_status(state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus, String> {
|
||||||
|
let engine_lock = state.engine.lock().await;
|
||||||
|
if let Some(ref engine) = *engine_lock {
|
||||||
|
let status = engine.status().await;
|
||||||
|
Ok(CallStatus {
|
||||||
|
active: true,
|
||||||
|
mic_muted: status.mic_muted,
|
||||||
|
spk_muted: status.spk_muted,
|
||||||
|
participants: status
|
||||||
|
.participants
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| Participant {
|
||||||
|
fingerprint: p.fingerprint,
|
||||||
|
alias: p.alias,
|
||||||
|
relay_label: p.relay_label,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
encode_fps: status.frames_sent,
|
||||||
|
recv_fps: status.frames_received,
|
||||||
|
audio_level: status.audio_level,
|
||||||
|
call_duration_secs: status.call_duration_secs,
|
||||||
|
fingerprint: status.fingerprint,
|
||||||
|
tx_codec: status.tx_codec,
|
||||||
|
rx_codec: status.rx_codec,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(CallStatus {
|
||||||
|
active: false,
|
||||||
|
mic_muted: false,
|
||||||
|
spk_muted: false,
|
||||||
|
participants: vec![],
|
||||||
|
encode_fps: 0,
|
||||||
|
recv_fps: 0,
|
||||||
|
audio_level: 0,
|
||||||
|
call_duration_secs: 0.0,
|
||||||
|
fingerprint: String::new(),
|
||||||
|
tx_codec: String::new(),
|
||||||
|
rx_codec: String::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Audio routing (Android-specific, no-op on desktop) ─────────────────────
|
||||||
|
|
||||||
|
/// Switch the call audio between earpiece (`on=false`) and loudspeaker
|
||||||
|
/// (`on=true`). On Android this calls AudioManager.setSpeakerphoneOn via
|
||||||
|
/// JNI AND then stops and restarts the Oboe streams so AAudio reconfigures
|
||||||
|
/// with the new routing — without the restart, changing the speakerphone
|
||||||
|
/// state mid-call silently tears down the running AAudio streams on some
|
||||||
|
/// OEMs and both capture + playout stop producing data.
|
||||||
|
///
|
||||||
|
/// The Rust send/recv tokio tasks keep running during the ~60ms restart
|
||||||
|
/// window; they just observe empty reads / writes against the
|
||||||
|
/// process-global ring buffers, which is fine because the ring state
|
||||||
|
/// is preserved across stop+start.
|
||||||
|
#[tauri::command]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
async fn set_speakerphone(on: bool) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
android_audio::set_speakerphone(on)?;
|
||||||
|
if wzp_native::is_loaded() && wzp_native::audio_is_running() {
|
||||||
|
tracing::info!(on, "set_speakerphone: restarting Oboe for route change");
|
||||||
|
// Oboe's stop/start are sync C-FFI calls that block for ~400ms
|
||||||
|
// on Nothing-class devices (Pixel is faster). Calling them
|
||||||
|
// directly from an async Tauri command stalls the tokio
|
||||||
|
// executor — the send/recv engine tasks were observed to
|
||||||
|
// freeze for ~20 seconds across a few rapid speaker toggles,
|
||||||
|
// piling up buffered QUIC datagrams and then flooding them
|
||||||
|
// all at once when the runtime finally caught up.
|
||||||
|
//
|
||||||
|
// Fix: run the audio teardown + reopen on a dedicated
|
||||||
|
// blocking thread so the runtime keeps scheduling everything
|
||||||
|
// else. AAudio's requestStop returns only after the stream
|
||||||
|
// is actually in Stopped state, so no explicit inter-call
|
||||||
|
// sleep is needed.
|
||||||
|
tokio::task::spawn_blocking(|| {
|
||||||
|
wzp_native::audio_stop();
|
||||||
|
wzp_native::audio_start()
|
||||||
|
.map_err(|code| format!("audio_start after speakerphone toggle: code {code}"))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("spawn_blocking join: {e}"))??;
|
||||||
|
tracing::info!("set_speakerphone: Oboe restarted");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query whether the call is currently routed to the loudspeaker.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn is_speakerphone_on() -> Result<bool, String> {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
android_audio::is_speakerphone_on()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
|
{
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Call history commands ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_call_history() -> Vec<history::CallHistoryEntry> {
|
||||||
|
history::all()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_recent_contacts() -> Vec<history::CallHistoryEntry> {
|
||||||
|
history::contacts()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn clear_call_history() -> Result<(), String> {
|
||||||
|
history::clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Signaling commands — platform independent ───────────────────────────────
|
||||||
|
|
||||||
|
struct SignalState {
|
||||||
|
transport: Option<Arc<wzp_transport::QuinnTransport>>,
|
||||||
|
/// The quinn::Endpoint backing the signal connection. Reused for the
|
||||||
|
/// media connection when a direct call is accepted — Android phones
|
||||||
|
/// silently drop packets from a second quinn::Endpoint to the same
|
||||||
|
/// relay, so every call after register_signal MUST share this socket.
|
||||||
|
endpoint: Option<wzp_transport::Endpoint>,
|
||||||
|
fingerprint: String,
|
||||||
|
signal_status: String,
|
||||||
|
incoming_call_id: Option<String>,
|
||||||
|
incoming_caller_fp: Option<String>,
|
||||||
|
incoming_caller_alias: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn register_signal(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
relay: String,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
use wzp_proto::SignalMessage;
|
||||||
|
|
||||||
|
let addr: std::net::SocketAddr = relay.parse().map_err(|e| format!("bad address: {e}"))?;
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
// Load or create seed automatically — no need to "connect to a room first"
|
||||||
|
let seed = load_or_create_seed()?;
|
||||||
|
let pub_id = seed.derive_identity().public_identity();
|
||||||
|
let fp = pub_id.fingerprint.to_string();
|
||||||
|
let identity_pub = *pub_id.signing.as_bytes();
|
||||||
|
|
||||||
|
let bind: std::net::SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||||
|
let endpoint = wzp_transport::create_endpoint(bind, None).map_err(|e| format!("{e}"))?;
|
||||||
|
let conn = wzp_transport::connect(&endpoint, addr, "_signal", wzp_transport::client_config())
|
||||||
|
.await.map_err(|e| format!("{e}"))?;
|
||||||
|
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
|
||||||
|
|
||||||
|
transport.send_signal(&SignalMessage::RegisterPresence {
|
||||||
|
identity_pub, signature: vec![], alias: None,
|
||||||
|
}).await.map_err(|e| format!("{e}"))?;
|
||||||
|
|
||||||
|
match transport.recv_signal().await.map_err(|e| format!("{e}"))? {
|
||||||
|
Some(SignalMessage::RegisterPresenceAck { success: true, .. }) => {}
|
||||||
|
_ => return Err("registration failed".into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
{ let mut sig = state.signal.lock().await; sig.transport = Some(transport.clone()); sig.endpoint = Some(endpoint.clone()); sig.fingerprint = fp.clone(); sig.signal_status = "registered".into(); }
|
||||||
|
|
||||||
|
tracing::info!(%fp, "signal registered, spawning recv loop");
|
||||||
|
let signal_state = Arc::clone(&state.signal);
|
||||||
|
let app_clone = app.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
match transport.recv_signal().await {
|
||||||
|
Ok(Some(SignalMessage::CallRinging { call_id })) => {
|
||||||
|
tracing::info!(%call_id, "signal: CallRinging");
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "ringing".into();
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"ringing","call_id":call_id}));
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::DirectCallOffer { caller_fingerprint, caller_alias, call_id, .. })) => {
|
||||||
|
tracing::info!(%call_id, caller = %caller_fingerprint, "signal: DirectCallOffer");
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "incoming".into();
|
||||||
|
sig.incoming_call_id = Some(call_id.clone()); sig.incoming_caller_fp = Some(caller_fingerprint.clone()); sig.incoming_caller_alias = caller_alias.clone();
|
||||||
|
// Log as a Missed entry up-front. If the user accepts
|
||||||
|
// the call, answer_call upgrades it to Received via
|
||||||
|
// history::mark_received_if_pending(call_id). If they
|
||||||
|
// reject or ignore, it stays Missed.
|
||||||
|
history::log(
|
||||||
|
call_id.clone(),
|
||||||
|
caller_fingerprint.clone(),
|
||||||
|
caller_alias.clone(),
|
||||||
|
history::CallDirection::Missed,
|
||||||
|
);
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"incoming","call_id":call_id,"caller_fp":caller_fingerprint,"caller_alias":caller_alias}));
|
||||||
|
let _ = app_clone.emit("history-changed", ());
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::DirectCallAnswer { call_id, accept_mode, .. })) => {
|
||||||
|
tracing::info!(%call_id, ?accept_mode, "signal: DirectCallAnswer (forwarded by relay)");
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::CallSetup { call_id, room, relay_addr })) => {
|
||||||
|
tracing::info!(%call_id, %room, %relay_addr, "signal: CallSetup — emitting setup event to JS");
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "setup".into();
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"setup","call_id":call_id,"room":room,"relay_addr":relay_addr}));
|
||||||
|
}
|
||||||
|
Ok(Some(SignalMessage::Hangup { reason })) => {
|
||||||
|
tracing::info!(?reason, "signal: Hangup");
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "registered".into(); sig.incoming_call_id = None;
|
||||||
|
let _ = app_clone.emit("signal-event", serde_json::json!({"type":"hangup"}));
|
||||||
|
}
|
||||||
|
Ok(Some(other)) => {
|
||||||
|
tracing::debug!(?other, "signal: unhandled message");
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::warn!("signal recv returned None — peer closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, "signal recv error — breaking loop");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::warn!("signal recv loop exited — signal_status=idle, transport dropped");
|
||||||
|
let mut sig = signal_state.lock().await; sig.signal_status = "idle".into(); sig.transport = None;
|
||||||
|
});
|
||||||
|
Ok(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn place_call(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
target_fp: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use wzp_proto::SignalMessage;
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
let transport = sig.transport.as_ref().ok_or("not registered")?;
|
||||||
|
let call_id = format!("{:016x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
|
||||||
|
tracing::info!(%call_id, %target_fp, "place_call: sending DirectCallOffer");
|
||||||
|
transport.send_signal(&SignalMessage::DirectCallOffer {
|
||||||
|
caller_fingerprint: sig.fingerprint.clone(), caller_alias: None, target_fingerprint: target_fp.clone(),
|
||||||
|
call_id: call_id.clone(), identity_pub: [0u8; 32], ephemeral_pub: [0u8; 32], signature: vec![],
|
||||||
|
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||||
|
}).await.map_err(|e| format!("{e}"))?;
|
||||||
|
history::log(call_id, target_fp, None, history::CallDirection::Placed);
|
||||||
|
let _ = app.emit("history-changed", ());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn answer_call(
|
||||||
|
state: tauri::State<'_, Arc<AppState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
call_id: String,
|
||||||
|
mode: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use wzp_proto::SignalMessage;
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
let transport = sig.transport.as_ref().ok_or_else(|| {
|
||||||
|
tracing::warn!("answer_call: not registered (no transport)");
|
||||||
|
"not registered".to_string()
|
||||||
|
})?;
|
||||||
|
let accept_mode = match mode { 0 => wzp_proto::CallAcceptMode::Reject, 1 => wzp_proto::CallAcceptMode::AcceptTrusted, _ => wzp_proto::CallAcceptMode::AcceptGeneric };
|
||||||
|
tracing::info!(%call_id, ?accept_mode, "answer_call: sending DirectCallAnswer");
|
||||||
|
transport.send_signal(&SignalMessage::DirectCallAnswer {
|
||||||
|
call_id: call_id.clone(), accept_mode, identity_pub: None, ephemeral_pub: None, signature: None,
|
||||||
|
chosen_profile: Some(wzp_proto::QualityProfile::GOOD),
|
||||||
|
}).await.map_err(|e| {
|
||||||
|
tracing::error!(%call_id, error = %e, "answer_call: send_signal failed");
|
||||||
|
format!("{e}")
|
||||||
|
})?;
|
||||||
|
tracing::info!(%call_id, "answer_call: DirectCallAnswer sent successfully");
|
||||||
|
// Upgrade the pending "Missed" entry to "Received" if the user
|
||||||
|
// accepted (mode != Reject). Mode 0 = Reject → leave as Missed.
|
||||||
|
if mode != 0 {
|
||||||
|
if history::mark_received_if_pending(&call_id) {
|
||||||
|
let _ = app.emit("history-changed", ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_signal_status(state: tauri::State<'_, Arc<AppState>>) -> Result<serde_json::Value, String> {
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
Ok(serde_json::json!({"status":sig.signal_status,"fingerprint":sig.fingerprint,"incoming_call_id":sig.incoming_call_id,"incoming_caller_fp":sig.incoming_caller_fp}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tear down the signal connection so the user goes back to idle. Called
|
||||||
|
/// when the user clicks "Deregister" on the direct-call screen. The
|
||||||
|
/// spawned recv loop will break out naturally when the transport closes.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn deregister(state: tauri::State<'_, Arc<AppState>>) -> Result<(), String> {
|
||||||
|
let mut sig = state.signal.lock().await;
|
||||||
|
if let Some(transport) = sig.transport.take() {
|
||||||
|
tracing::info!("deregister: closing signal transport");
|
||||||
|
transport.close().await.ok();
|
||||||
|
}
|
||||||
|
sig.endpoint = None;
|
||||||
|
sig.signal_status = "idle".into();
|
||||||
|
sig.incoming_call_id = None;
|
||||||
|
sig.incoming_caller_fp = None;
|
||||||
|
sig.incoming_caller_alias = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── App entry point ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Shared Tauri app builder. Used by the desktop `main.rs` and the mobile
|
||||||
|
/// entry point below.
|
||||||
|
pub fn run() {
|
||||||
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
|
let state = Arc::new(AppState {
|
||||||
|
engine: Mutex::new(None),
|
||||||
|
signal: Arc::new(Mutex::new(SignalState {
|
||||||
|
transport: None, endpoint: None, fingerprint: String::new(), signal_status: "idle".into(),
|
||||||
|
incoming_call_id: None, incoming_caller_fp: None, incoming_caller_alias: None,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.manage(state)
|
||||||
|
.setup(|app| {
|
||||||
|
// Resolve the platform-correct app data dir once at startup so
|
||||||
|
// every command can read/write the seed without juggling AppHandle.
|
||||||
|
let data_dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map(|p| p.join(".wzp"))
|
||||||
|
.unwrap_or_else(|_| identity_dir());
|
||||||
|
// create_dir_all is a no-op if it already exists.
|
||||||
|
if let Err(e) = std::fs::create_dir_all(&data_dir) {
|
||||||
|
tracing::warn!("failed to create app data dir {data_dir:?}: {e}");
|
||||||
|
}
|
||||||
|
tracing::info!("app data dir: {data_dir:?}");
|
||||||
|
let _ = APP_DATA_DIR.set(data_dir);
|
||||||
|
|
||||||
|
// Load the standalone wzp-native cdylib (Oboe audio bridge) and
|
||||||
|
// cache its exported function pointers. The library handle is
|
||||||
|
// kept alive in a 'static OnceLock for the lifetime of the
|
||||||
|
// process, so CallEngine::start() can invoke its audio FFI
|
||||||
|
// from anywhere. See src/wzp_native.rs and the incident report
|
||||||
|
// in docs/incident-tauri-android-init-tcb.md.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
match wzp_native::init() {
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!(
|
||||||
|
"wzp-native loaded: version={} msg=\"{}\"",
|
||||||
|
wzp_native::version(),
|
||||||
|
wzp_native::hello()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("wzp-native init failed: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
ping_relay, get_identity, get_app_info,
|
||||||
|
connect, disconnect, toggle_mic, toggle_speaker, get_status,
|
||||||
|
register_signal, place_call, answer_call, get_signal_status,
|
||||||
|
deregister,
|
||||||
|
set_speakerphone, is_speakerphone_on,
|
||||||
|
get_call_history, get_recent_contacts, clear_call_history,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running WarzonePhone");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tauri mobile entry point (Android/iOS). On desktop this is a no-op —
|
||||||
|
/// `main.rs` calls `run()` directly.
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn mobile_entry() {
|
||||||
|
run();
|
||||||
|
}
|
||||||
10
desktop/src-tauri/src/main.rs
Normal file
10
desktop/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Desktop binary entry point. All logic lives in `lib.rs` so the same
|
||||||
|
// code can be built as a cdylib for Android/iOS via `cargo tauri android build`.
|
||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
wzp_desktop_lib::run();
|
||||||
|
}
|
||||||
138
desktop/src-tauri/src/wzp_native.rs
Normal file
138
desktop/src-tauri/src/wzp_native.rs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
//! Runtime binding to the standalone `wzp-native` cdylib.
|
||||||
|
//!
|
||||||
|
//! See `docs/incident-tauri-android-init-tcb.md` and the top of
|
||||||
|
//! `crates/wzp-native/src/lib.rs` for the full story on why this split
|
||||||
|
//! exists. Short version: Tauri's desktop cdylib cannot have any C++
|
||||||
|
//! compiled into it (via cc::Build) without landing in rust-lang/rust#104707's
|
||||||
|
//! staticlib symbol leak, which makes bionic's private `pthread_create`
|
||||||
|
//! symbols bind locally and SIGSEGV in `__init_tcb+4` at launch. So all
|
||||||
|
//! the Oboe + audio code lives in a standalone `wzp-native` .so built
|
||||||
|
//! with `cargo-ndk`, and we dlopen it here at runtime.
|
||||||
|
//!
|
||||||
|
//! The Library handle lives in a `'static` `OnceLock` for the lifetime of
|
||||||
|
//! the process; all function pointers cached below borrow from it safely.
|
||||||
|
|
||||||
|
#![cfg(target_os = "android")]
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
// ─── Library handle (kept alive forever) ─────────────────────────────────
|
||||||
|
|
||||||
|
static LIB: OnceLock<libloading::Library> = OnceLock::new();
|
||||||
|
|
||||||
|
// Cached function pointers, resolved once at init(). Each is a raw
|
||||||
|
// `extern "C"` fn pointer with effectively `'static` lifetime because
|
||||||
|
// LIB is a OnceLock that never drops.
|
||||||
|
static VERSION: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
||||||
|
static HELLO: OnceLock<unsafe extern "C" fn(*mut u8, usize) -> usize> = OnceLock::new();
|
||||||
|
static AUDIO_START: OnceLock<unsafe extern "C" fn() -> i32> = OnceLock::new();
|
||||||
|
static AUDIO_STOP: OnceLock<unsafe extern "C" fn()> = 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();
|
||||||
|
|
||||||
|
/// Load `libwzp_native.so` and resolve every exported function we use.
|
||||||
|
/// Call this once at app startup (from the Tauri `setup()` callback).
|
||||||
|
/// Subsequent calls are no-ops.
|
||||||
|
pub fn init() -> Result<(), String> {
|
||||||
|
if LIB.get().is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the sibling cdylib. The Android dynamic linker searches
|
||||||
|
// /data/app/<pkg>/lib/arm64/ which gradle populates from jniLibs.
|
||||||
|
let lib = unsafe { libloading::Library::new("libwzp_native.so") }
|
||||||
|
.map_err(|e| format!("dlopen libwzp_native.so: {e}"))?;
|
||||||
|
|
||||||
|
// Stash the Library into the OnceLock first so all Symbol lookups
|
||||||
|
// below borrow from the 'static reference rather than a local.
|
||||||
|
LIB.set(lib).map_err(|_| "wzp_native::LIB already set")?;
|
||||||
|
let lib_ref: &'static libloading::Library = LIB.get().unwrap();
|
||||||
|
|
||||||
|
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("?")))?;
|
||||||
|
// 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("?")))?;
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
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_STOP, unsafe extern "C" fn(), b"wzp_native_audio_stop");
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is `init()` done and all symbols cached?
|
||||||
|
pub fn is_loaded() -> bool {
|
||||||
|
AUDIO_START.get().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Smoke-test accessors ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn version() -> i32 {
|
||||||
|
VERSION.get().map(|f| unsafe { f() }).unwrap_or(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hello() -> String {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Audio accessors ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Start the Oboe capture + playout streams. Returns `Err(code)` on
|
||||||
|
/// failure. Idempotent on the wzp-native side.
|
||||||
|
pub fn audio_start() -> Result<(), i32> {
|
||||||
|
let f = AUDIO_START.get().ok_or(-100_i32)?;
|
||||||
|
let ret = unsafe { f() };
|
||||||
|
if ret == 0 { Ok(()) } else { Err(ret) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop both streams. Safe to call even if not running.
|
||||||
|
pub fn audio_stop() {
|
||||||
|
if let Some(f) = AUDIO_STOP.get() {
|
||||||
|
unsafe { 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; };
|
||||||
|
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; };
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn audio_capture_latency_ms() -> f32 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
36
desktop/src-tauri/tauri.conf.json
Normal file
36
desktop/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"productName": "WarzonePhone",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.wzp.desktop",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "WarzonePhone",
|
||||||
|
"width": 400,
|
||||||
|
"height": 640,
|
||||||
|
"resizable": true,
|
||||||
|
"minWidth": 360,
|
||||||
|
"minHeight": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/icon.png"
|
||||||
|
],
|
||||||
|
"android": {
|
||||||
|
"minSdkVersion": 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
desktop/src/identicon.ts
Normal file
110
desktop/src/identicon.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Deterministic identicon generator — creates a unique symmetric pattern
|
||||||
|
* from a hex fingerprint string, similar to MetaMask's Jazzicon / Ethereum blockies.
|
||||||
|
*
|
||||||
|
* Returns an SVG data URL that can be used as an <img> src.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function hashBytes(hex: string): number[] {
|
||||||
|
const clean = hex.replace(/[^0-9a-fA-F]/g, "");
|
||||||
|
const bytes: number[] = [];
|
||||||
|
for (let i = 0; i < clean.length; i += 2) {
|
||||||
|
bytes.push(parseInt(clean.substring(i, i + 2), 16));
|
||||||
|
}
|
||||||
|
// Pad to at least 16 bytes
|
||||||
|
while (bytes.length < 16) bytes.push(0);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
||||||
|
s /= 100;
|
||||||
|
l /= 100;
|
||||||
|
const k = (n: number) => (n + h / 30) % 12;
|
||||||
|
const a = s * Math.min(l, 1 - l);
|
||||||
|
const f = (n: number) =>
|
||||||
|
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
||||||
|
return [
|
||||||
|
Math.round(f(0) * 255),
|
||||||
|
Math.round(f(8) * 255),
|
||||||
|
Math.round(f(4) * 255),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateIdenticon(
|
||||||
|
fingerprint: string,
|
||||||
|
size: number = 36
|
||||||
|
): string {
|
||||||
|
const bytes = hashBytes(fingerprint);
|
||||||
|
|
||||||
|
// Derive colors from first bytes
|
||||||
|
const hue1 = (bytes[0] * 360) / 256;
|
||||||
|
const hue2 = ((bytes[1] * 360) / 256 + 120) % 360;
|
||||||
|
const [r1, g1, b1] = hslToRgb(hue1, 65, 35); // dark bg
|
||||||
|
const [r2, g2, b2] = hslToRgb(hue2, 70, 55); // bright fg
|
||||||
|
|
||||||
|
const bg = `rgb(${r1},${g1},${b1})`;
|
||||||
|
const fg = `rgb(${r2},${g2},${b2})`;
|
||||||
|
|
||||||
|
// 5x5 grid, left-right symmetric (only need 3 columns)
|
||||||
|
const grid: boolean[][] = [];
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
const row: boolean[] = [];
|
||||||
|
for (let x = 0; x < 3; x++) {
|
||||||
|
const byteIdx = 2 + y * 3 + x;
|
||||||
|
row.push(bytes[byteIdx % bytes.length] > 128);
|
||||||
|
}
|
||||||
|
// Mirror: col 3 = col 1, col 4 = col 0
|
||||||
|
grid.push([row[0], row[1], row[2], row[1], row[0]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render SVG
|
||||||
|
const cellSize = size / 5;
|
||||||
|
const r = size * 0.12; // border radius
|
||||||
|
let rects = "";
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
if (grid[y][x]) {
|
||||||
|
rects += `<rect x="${x * cellSize}" y="${y * cellSize}" width="${cellSize}" height="${cellSize}" fill="${fg}"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
||||||
|
<rect width="${size}" height="${size}" rx="${r}" fill="${bg}"/>
|
||||||
|
${rects}
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an <img> element with the identicon.
|
||||||
|
* Click copies the fingerprint to clipboard.
|
||||||
|
*/
|
||||||
|
export function createIdenticonEl(
|
||||||
|
fingerprint: string,
|
||||||
|
size: number = 36,
|
||||||
|
clickToCopy: boolean = true
|
||||||
|
): HTMLImageElement {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = generateIdenticon(fingerprint, size);
|
||||||
|
img.width = size;
|
||||||
|
img.height = size;
|
||||||
|
img.style.borderRadius = `${size * 0.12}px`;
|
||||||
|
img.style.cursor = clickToCopy ? "pointer" : "default";
|
||||||
|
img.title = fingerprint;
|
||||||
|
|
||||||
|
if (clickToCopy && fingerprint) {
|
||||||
|
img.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigator.clipboard.writeText(fingerprint).then(() => {
|
||||||
|
img.style.outline = "2px solid #4ade80";
|
||||||
|
setTimeout(() => {
|
||||||
|
img.style.outline = "";
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return img;
|
||||||
|
}
|
||||||
1038
desktop/src/main.ts
Normal file
1038
desktop/src/main.ts
Normal file
File diff suppressed because it is too large
Load Diff
1031
desktop/src/style.css
Normal file
1031
desktop/src/style.css
Normal file
File diff suppressed because it is too large
Load Diff
15
desktop/tsconfig.json
Normal file
15
desktop/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
15
desktop/vite.config.ts
Normal file
15
desktop/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
clearScreen: false,
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
envPrefix: ["VITE_", "TAURI_"],
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||||
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -625,3 +625,123 @@ curl -s http://relay-host:9090/metrics | grep wzp_relay_active_sessions
|
|||||||
# Check federation probe health
|
# Check federation probe health
|
||||||
curl -s http://relay-host:9090/metrics | grep wzp_probe_up
|
curl -s http://relay-host:9090/metrics | grep wzp_probe_up
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Build Pipelines
|
||||||
|
|
||||||
|
All production artifacts (Android APK, Linux x86_64 binaries, Windows `.exe`) are built on **SepehrHomeserverdk** using Docker, not on developer workstations. The pipelines are fire-and-forget: a local script invokes a `tmux` session on the remote, the build runs in a Docker container, and the artifact is uploaded to `paste.dk.manko.yoga` (rustypaste) with a notification sent to `ntfy.sh/wzp` on start and completion.
|
||||||
|
|
||||||
|
### Docker images
|
||||||
|
|
||||||
|
Two long-lived images live on the remote:
|
||||||
|
|
||||||
|
| Image | Used by | Base | Key contents |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `wzp-android-builder` | Android APK (Tauri mobile + legacy Kotlin), Linux x86_64 relay/CLI | Debian bookworm | Rust stable with Android targets, cargo-ndk, NDK 26.1, Android SDK (API 34 + 35 + 36), JDK 17, Gradle 8.5, Node.js 20, cmake, ninja, tauri-cli 2.x |
|
||||||
|
| `wzp-windows-builder` | Windows x86_64 `.exe` | Debian bookworm | Rust stable with `x86_64-pc-windows-msvc` target, cargo-xwin (with pre-warmed MSVC CRT + Windows SDK cache), Node.js 20, cmake, ninja, clang, lld, nasm |
|
||||||
|
|
||||||
|
Both images are rebuilt rarely — once the base toolchain is stable, rebuilds are only needed to pick up new dependencies or security patches.
|
||||||
|
|
||||||
|
**Rebuilding an image** (fire-and-forget, ~10 min on a warm base):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
./scripts/build-windows-docker.sh --image-build
|
||||||
|
|
||||||
|
# Android (upload and rebuild handled by the Android build script itself — see
|
||||||
|
# its --image-build flag or equivalent)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--image-build` flag uploads the local Dockerfile to the remote, kicks off `docker build` under `nohup`, and returns immediately. Monitor with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh SepehrHomeserverdk 'tail -f /tmp/wzp-windows-image-build.log'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipeline: Android APK (Tauri Mobile)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-tauri-android.sh # Full: pull + build + upload + notify
|
||||||
|
./scripts/build-tauri-android.sh --no-pull # Skip git fetch
|
||||||
|
./scripts/build-tauri-android.sh --clean # Force-clean Rust target
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Branch**: `android-rewrite`
|
||||||
|
- **Image**: `wzp-android-builder`
|
||||||
|
- **Build command**: `cargo tauri android build --release`
|
||||||
|
- **Output**: `wzp-release.apk` → uploaded to rustypaste
|
||||||
|
- **Notifications**: start + completion to `ntfy.sh/wzp`
|
||||||
|
- **Remote artifact path**: `/mnt/storage/manBuilder/data/cache-android/target/…/release/app-release.apk`
|
||||||
|
|
||||||
|
### Pipeline: Linux x86_64 (relay + CLI + bench + web)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-linux-docker.sh # Fire-and-forget
|
||||||
|
./scripts/build-linux-docker.sh --no-pull # Skip git fetch
|
||||||
|
./scripts/build-linux-docker.sh --clean # Force-clean target
|
||||||
|
./scripts/build-linux-docker.sh --install # Wait for completion and download locally
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Branch**: `feat/android-voip-client` (script default — override by editing the script or passing an env var)
|
||||||
|
- **Image**: `wzp-android-builder` (shared, not a separate Linux-only image)
|
||||||
|
- **Targets built**: `wzp-relay`, `wzp-client`, `wzp-client-audio` (with `--features audio`), `wzp-web`, `wzp-bench`
|
||||||
|
- **Output**: `wzp-linux-x86_64.tar.gz` with all five binaries → uploaded to rustypaste
|
||||||
|
- **Local landing dir** (with `--install`): `target/linux-x86_64/`
|
||||||
|
|
||||||
|
### Pipeline: Windows x86_64 (`wzp-desktop.exe`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-windows-docker.sh # Full: pull + build + download locally
|
||||||
|
./scripts/build-windows-docker.sh --no-pull # Skip git fetch
|
||||||
|
./scripts/build-windows-docker.sh --rust # Force-clean target-windows cache
|
||||||
|
./scripts/build-windows-docker.sh --image-build # Rebuild the Docker image (fire-and-forget)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Branch**: `feat/desktop-audio-rewrite`
|
||||||
|
- **Image**: `wzp-windows-builder`
|
||||||
|
- **Build command**: `cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop`
|
||||||
|
- **Output**: `wzp-desktop.exe` (~16 MB) → downloaded to `target/windows-exe/wzp-desktop.exe`, also uploaded to rustypaste
|
||||||
|
- **Target cache volume**: `target-windows` (separate from the Android target cache to avoid triple cross-contamination)
|
||||||
|
- **Shared cache volumes**: `cargo-registry`, `cargo-git` (shared with Android — both pipelines pull the same crates)
|
||||||
|
|
||||||
|
**A/B-preserving workflow** for testing audio backends: rename the prior `.exe` before re-running the build, so both coexist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preserve prior build as the noAEC baseline
|
||||||
|
mv target/windows-exe/wzp-desktop.exe target/windows-exe/wzp-desktop-noAEC.exe
|
||||||
|
./scripts/build-windows-docker.sh
|
||||||
|
ls -la target/windows-exe/
|
||||||
|
# wzp-desktop-noAEC.exe (previous build)
|
||||||
|
# wzp-desktop.exe (new build)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative pipeline: Windows via Hetzner Cloud VPS
|
||||||
|
|
||||||
|
For situations where Docker image rebuilds would be disruptive, or for one-shot debug builds on a clean machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-windows-cloud.sh # Full: create VM → build → download → destroy
|
||||||
|
./scripts/build-windows-cloud.sh --prepare # Create VM + install deps, don't build
|
||||||
|
./scripts/build-windows-cloud.sh --build # Build on existing VM
|
||||||
|
./scripts/build-windows-cloud.sh --transfer # Download .exe from existing VM
|
||||||
|
./scripts/build-windows-cloud.sh --destroy # Delete the VM
|
||||||
|
WZP_KEEP_VM=1 ./scripts/build-windows-cloud.sh # Don't auto-destroy after successful build
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Provider**: Hetzner Cloud
|
||||||
|
- **Default server type**: `cx33` (8 GB RAM, 8 vCPU — `cx23` with 4 GB OOMs on the tauri+rustls cross-compile)
|
||||||
|
- **Image**: `ubuntu-24.04`
|
||||||
|
- **SSH key**: must be named `wz` in Hetzner and loaded in the local ssh-agent
|
||||||
|
- **Reminder**: set `WZP_KEEP_VM=1` for multi-build sessions, then **remember to `--destroy` at end of day** so the VM isn't left running overnight. This is tracked in the auto-memory as `feedback_keep_windows_builder_vm.md`.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
All pipelines post to `https://ntfy.sh/wzp`. Subscribe from your phone via the [ntfy.sh app](https://ntfy.sh/) to get push notifications on build start/success/failure. Messages include the short git hash and the rustypaste URL on success:
|
||||||
|
|
||||||
|
```
|
||||||
|
WZP Windows build OK [03a80a3] (16M)
|
||||||
|
https://paste.dk.manko.yoga/<uuid>/wzp-desktop.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rustypaste credentials
|
||||||
|
|
||||||
|
Build pipelines read `rusty_address` and `rusty_auth_token` from the `.env` file at `/mnt/storage/manBuilder/.env` on SepehrHomeserverdk. Local scripts that upload directly (`build-windows-cloud.sh` when run in `--transfer` mode) read from `~/.wzp/rustypaste.env` with the same variable names. Both files must be kept in sync manually if rotated.
|
||||||
|
|||||||
@@ -872,3 +872,71 @@ warzonePhone/
|
|||||||
| wzp-relay | 40 + 4 integration | Room ACL, session mgmt, metrics, probes, mesh, trunking |
|
| wzp-relay | 40 + 4 integration | Room ACL, session mgmt, metrics, probes, mesh, trunking |
|
||||||
| wzp-client | 30 + 2 integration | Encoder/decoder, quality adapter, silence, drift, sweep |
|
| wzp-client | 30 + 2 integration | Encoder/decoder, quality adapter, silence, drift, sweep |
|
||||||
| wzp-web | 2 | Metrics |
|
| wzp-web | 2 | Metrics |
|
||||||
|
|
||||||
|
## Audio Backend Architecture (Platform Matrix)
|
||||||
|
|
||||||
|
WarzonePhone's audio I/O goes through one of four backends depending on the target platform and feature flags. All backends expose the same public API (`AudioCapture::start() → AudioCapture { ring(), stop() }`) via conditional re-exports in `crates/wzp-client/src/lib.rs`, so the `CallEngine` above the audio layer doesn't know or care which backend is running.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ CallEngine (platform-agnostic) │
|
||||||
|
│ reads PCM from AudioCapture::ring() │
|
||||||
|
│ writes PCM to AudioPlayback::ring() │
|
||||||
|
└────────────────────┬────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────┼─────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌───────────────┐ ┌────────────────┐ ┌───────────────┐
|
||||||
|
│ audio_io │ │ audio_vpio │ │ audio_wasapi │
|
||||||
|
│ (CPAL) │ │ (Core Audio │ │ (Windows │
|
||||||
|
│ │ │ VoiceProc IO) │ │ IAudioClient2│
|
||||||
|
│ All platforms │ │ macOS only │ │ Windows │
|
||||||
|
│ (baseline) │ │ feature=vpio │ │ feature= │
|
||||||
|
│ │ │ │ │ windows-aec │
|
||||||
|
└───────────────┘ └────────────────┘ └───────────────┘
|
||||||
|
│
|
||||||
|
▼ on Android only
|
||||||
|
┌───────────────┐
|
||||||
|
│ wzp-native │
|
||||||
|
│ (Oboe bridge │
|
||||||
|
│ via dlopen) │
|
||||||
|
│ │
|
||||||
|
│ Android only │
|
||||||
|
│ libloading │
|
||||||
|
└───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend selection matrix
|
||||||
|
|
||||||
|
| Platform | Capture | Playback | OS AEC | Feature flags |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| macOS | VoiceProcessingIO (native Core Audio) | CPAL | **Yes** — Apple's hardware-accelerated AEC (same AEC as FaceTime, iMessage audio, Voice Memos) | `audio`, `vpio` |
|
||||||
|
| Windows (AEC build) | Direct WASAPI with `AudioCategory_Communications` | CPAL | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + NS + AGC), driver-dependent quality | `audio`, `windows-aec` |
|
||||||
|
| Windows (baseline) | CPAL (WASAPI shared mode) | CPAL | No | `audio` |
|
||||||
|
| Linux | CPAL (ALSA / PulseAudio) | CPAL | No | `audio` |
|
||||||
|
| Android (Tauri Mobile) | Oboe via `wzp-native` cdylib, `Usage::VoiceCommunication` + `MODE_IN_COMMUNICATION` | Same Oboe stream | Depends on device (some Android devices apply AEC to the voice-communication stream, most do not) | none (`wzp-client` compiled with `default-features = false`) |
|
||||||
|
|
||||||
|
### Why `wzp-native` is a standalone cdylib
|
||||||
|
|
||||||
|
On Android, the audio backend lives in a separate cdylib crate (`crates/wzp-native`) that `wzp-desktop`'s lib crate loads at runtime via `libloading`. It is **not** linked as a regular Rust dep.
|
||||||
|
|
||||||
|
This is deliberate. rust-lang/rust#104707 documents that a crate with `crate-type = ["cdylib", "staticlib"]` leaks non-exported symbols from the staticlib into the cdylib. On Android, that caused Bionic's private `__init_tcb` / `pthread_create` symbols to be bound LOCALLY inside our `.so` instead of resolved dynamically against `libc.so` at `dlopen` time — which crashed the app at launch as soon as `tao` tried to `std::thread::spawn()` from the JNI `onCreate` callback.
|
||||||
|
|
||||||
|
Keeping `wzp-native` in its own cdylib and loading it via `libloading` means:
|
||||||
|
|
||||||
|
1. The app's own `.so` has `crate-type = ["cdylib", "rlib"]` only — no `staticlib`, no symbol leak.
|
||||||
|
2. `libwzp_native.so` is loaded via `System.loadLibrary` from the JVM side (or `dlopen` from Rust), which triggers the normal Bionic resolver and binds all private symbols against `libc.so` at load time.
|
||||||
|
3. The C/C++ Oboe bridge is fully isolated inside `libwzp_native.so`'s symbol space — no chance of its archives leaking into `wzp-desktop`'s `.so`.
|
||||||
|
|
||||||
|
See `docs/BRANCH-android-rewrite.md` for the full incident postmortem and `docs/incident-tauri-android-init-tcb.md` for the debug log.
|
||||||
|
|
||||||
|
### Vendored `audiopus_sys` for libopus / clang-cl cross-compile
|
||||||
|
|
||||||
|
The workspace root carries a vendored copy of `audiopus_sys` at `vendor/audiopus_sys/` with a patched `opus/CMakeLists.txt`. This is needed because libopus 1.3.1 gates its per-file `-msse4.1` / `-mssse3` `COMPILE_FLAGS` behind `if(NOT MSVC)`, and under `clang-cl` (used by `cargo-xwin` for Windows cross-compiles) CMake sets `MSVC=1` unconditionally — so the SIMD source files compile without the required target feature and fail to link the intrinsic `always_inline` functions.
|
||||||
|
|
||||||
|
The patch introduces an `MSVC_CL` variable that is true only for real `cl.exe` (distinguished via `CMAKE_C_COMPILER_ID STREQUAL "MSVC"`), and flips the eight `if(NOT MSVC)` SIMD guards to `if(NOT MSVC_CL)` so clang-cl gets the GCC-style per-file flags. Wired in via `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` at the workspace root.
|
||||||
|
|
||||||
|
This does not affect macOS or Linux builds — on those platforms `MSVC=0` everywhere so the patched logic behaves identically to upstream.
|
||||||
|
|
||||||
|
Upstream tracking: xiph/opus#256, xiph/opus PR #257 (both stale).
|
||||||
|
|||||||
164
docs/BRANCH-desktop-audio-rewrite.md
Normal file
164
docs/BRANCH-desktop-audio-rewrite.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Branch: `feat/desktop-audio-rewrite`
|
||||||
|
|
||||||
|
Home of the Tauri desktop client for macOS, Windows, and Linux. Named "audio-rewrite" because the original driver was replacing a CPAL-only audio pipeline with platform-native backends that support OS-level echo cancellation (VoiceProcessingIO on macOS, WASAPI Communications on Windows), but the branch has grown into the full desktop story — Windows cross-compilation, vendored dependencies, history UI, direct calling, the whole thing.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The desktop client shares 100% of its frontend (`desktop/src/`) and Tauri command layer (`desktop/src-tauri/src/lib.rs`, `engine.rs`, `history.rs`) with the Android build on `android-rewrite`. Differences are limited to:
|
||||||
|
|
||||||
|
- **Audio backends**, which are platform-gated via Cargo target-dep sections in `desktop/src-tauri/Cargo.toml` and feature flags in `crates/wzp-client/Cargo.toml`.
|
||||||
|
- **Identity storage paths**, which resolve via Tauri's `app_data_dir()` (`~/Library/Application Support/…` on macOS, `%APPDATA%\…` on Windows, `~/.local/share/…` on Linux).
|
||||||
|
- **Build toolchains**: native `cargo build` on macOS/Linux, `cargo xwin` cross-compile from Linux for Windows via Docker on SepehrHomeserverdk.
|
||||||
|
|
||||||
|
## Audio backend matrix
|
||||||
|
|
||||||
|
| Target | Capture | Playback | AEC |
|
||||||
|
|---|---|---|---|
|
||||||
|
| macOS | CPAL (WASAPI/CoreAudio via cpal crate) OR VoiceProcessingIO (native Core Audio) | CPAL | VoiceProcessingIO native AEC (when `vpio` feature enabled) |
|
||||||
|
| Windows (default) | CPAL → WASAPI shared mode | CPAL → WASAPI shared mode | None |
|
||||||
|
| Windows (AEC build) | Direct WASAPI with `IAudioClient2::SetClientProperties(AudioCategory_Communications)` | CPAL → WASAPI shared mode | **OS-level**: Windows routes the capture stream through the driver's communications APO chain (AEC + NS + AGC) |
|
||||||
|
| Linux | CPAL → ALSA/PulseAudio | CPAL → ALSA/PulseAudio | None |
|
||||||
|
|
||||||
|
The macOS VPIO path is gated behind the `vpio` feature in `wzp-client` and the `coreaudio-rs` dep is itself `cfg(target_os = "macos")`, so enabling the feature on Windows or Linux is a no-op.
|
||||||
|
|
||||||
|
The Windows AEC path is gated behind the `windows-aec` feature, also target-gated (the `windows` crate dep is only pulled in on Windows), and re-exports `WasapiAudioCapture as AudioCapture` when enabled so downstream code doesn't need to know which backend is active. The current Windows build at `target/windows-exe/wzp-desktop.exe` has `windows-aec` on; a baseline noAEC build is preserved at `target/windows-exe/wzp-desktop-noAEC.exe` for A/B comparison on real hardware.
|
||||||
|
|
||||||
|
See [`BRANCH-android-rewrite.md`](BRANCH-android-rewrite.md) for Oboe audio on Android, which is its own story.
|
||||||
|
|
||||||
|
## Recent major work
|
||||||
|
|
||||||
|
### 1. Desktop direct calling feature (commit `2fd9465` and neighbors)
|
||||||
|
|
||||||
|
Brought direct 1:1 calls to macOS with full parity to the Android client:
|
||||||
|
|
||||||
|
- **Identity path fix**: the desktop `CallEngine::start` was loading seed from `$HOME/.wzp/identity` while `register_signal` used Tauri's `app_data_dir()`, producing two different fingerprints per run. Both now route through `load_or_create_seed()` which uses `app_data_dir()` everywhere.
|
||||||
|
- **Call history with dedup**: `history.rs` stores a `Vec<CallHistoryEntry>` with a `CallDirection` enum (`Placed | Received | Missed`). The `log` function dedupes by `call_id` so an outgoing call isn't logged twice as "missed" (when the signal loop's `DirectCallOffer` handler fires) and then again as "placed" (when `place_call` returns). Instead the entry is updated in place.
|
||||||
|
- **Recent contacts row**: a horizontal chip UI in the direct-call panel showing the last N peers with friendly aliases, clickable to re-dial.
|
||||||
|
- **Deregister button**: lets a user drop their signal registration without quitting the app, useful when switching identities.
|
||||||
|
- **Random alias derivation**: a new client sees a human-friendly alias like "silent-forest-41" derived deterministically from its seed, so it's identifiable in the UI before manual naming.
|
||||||
|
- **Default room "general"** instead of "android", since the desktop client is not Android.
|
||||||
|
|
||||||
|
### 2. macOS VoiceProcessingIO integration
|
||||||
|
|
||||||
|
`crates/wzp-client/src/audio_vpio.rs` — a native Core Audio implementation using `AUGraph` + `AudioComponentInstance` with the VPIO audio unit. Gives you hardware-accelerated AEC (same AEC Apple ships in FaceTime / iMessage audio / voice memos) at the cost of tight coupling to Apple frameworks. Lock-free ring pattern matches the CPAL path so the upper layers don't notice the difference.
|
||||||
|
|
||||||
|
Enabled by `features = ["audio", "vpio"]` in the macOS target section of `desktop/src-tauri/Cargo.toml`.
|
||||||
|
|
||||||
|
### 3. Windows cross-compilation via cargo-xwin
|
||||||
|
|
||||||
|
Cross-compiling Rust + Tauri to `x86_64-pc-windows-msvc` from Linux using `cargo-xwin`, which downloads the Microsoft CRT + Windows SDK on demand and drives `clang-cl` as the compiler. No Windows machine is needed for the build itself — only for runtime testing.
|
||||||
|
|
||||||
|
**Build infrastructure**:
|
||||||
|
|
||||||
|
- `scripts/Dockerfile.windows-builder` — Debian bookworm + Rust + cargo-xwin + Node 20 + cmake + ninja + llvm + clang + lld + nasm. Pre-warms the xwin MSVC CRT cache at image build time (saves ~4 minutes per cold build).
|
||||||
|
- `scripts/build-windows-docker.sh` — fire-and-forget remote build via Docker on SepehrHomeserverdk. Same pattern as `build-tauri-android.sh`. Uploads the `.exe` to rustypaste and fires an `ntfy.sh/wzp` notification on start and on completion.
|
||||||
|
- `scripts/build-windows-cloud.sh` — alternative pipeline using a temporary Hetzner Cloud VPS. Slower (full VM spin-up), more expensive, but useful when Docker image rebuilds would be disruptive.
|
||||||
|
|
||||||
|
**Two critical blockers resolved** on the way to a working `.exe`:
|
||||||
|
|
||||||
|
1. **libopus SSE4.1 / SSSE3 intrinsic compile failure**. `audiopus_sys` vendors libopus 1.3.1, whose `CMakeLists.txt` gates the per-file `-msse4.1` `COMPILE_FLAGS` behind `if(NOT MSVC)`. Under `clang-cl`, CMake sets `MSVC=1` (because `CMAKE_C_COMPILER_FRONTEND_VARIANT=MSVC` triggers `Platform/Windows-MSVC.cmake` which unconditionally sets the variable), so the per-file flag is never set and the SSE4.1 source files compile without the target feature — then fail with 20+ "always_inline function '_mm_cvtepi16_epi32' requires target feature 'sse4.1'" errors.
|
||||||
|
|
||||||
|
Fixed by **vendoring audiopus_sys into `vendor/audiopus_sys/`** and patching its bundled libopus to introduce an `MSVC_CL` variable that is true only for real `cl.exe` (distinguished via `CMAKE_C_COMPILER_ID STREQUAL "MSVC"`). The eight `if(NOT MSVC)` SIMD guards are flipped to `if(NOT MSVC_CL)` and the global `/arch` block at line 445 becomes `if(MSVC_CL)`, so clang-cl gets the GCC-style per-file flags while real cl.exe keeps the `/arch:AVX` / `/arch:SSE2` globals.
|
||||||
|
|
||||||
|
Wired in via `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` at the workspace root.
|
||||||
|
|
||||||
|
Upstream tracking: [xiph/opus#256](https://github.com/xiph/opus/issues/256), [xiph/opus PR #257](https://github.com/xiph/opus/pull/257) (both stale).
|
||||||
|
|
||||||
|
2. **tauri-build needs `icons/icon.ico` for the Windows PE resource**. The desktop only had `icon.png`. Generated a multi-size ICO (16/24/32/48/64/128/256) from the existing placeholder via Pillow and committed it. Placeholder quality — real branded icons can replace it later.
|
||||||
|
|
||||||
|
### 4. Windows `AudioCategory_Communications` capture path (task #24)
|
||||||
|
|
||||||
|
`crates/wzp-client/src/audio_wasapi.rs` — direct WASAPI capture via `IMMDeviceEnumerator → IAudioClient2 → SetClientProperties` with `AudioCategory_Communications`. This tells Windows "this is a VoIP call" and Windows routes the capture stream through the driver's registered communications APO chain, which on most Win10/11 consumer hardware includes AEC, NS, and AGC.
|
||||||
|
|
||||||
|
**Caveat**: quality is driver-dependent. On a machine with a good communications APO (Intel Smart Sound, Dolby, modern Realtek on Win11 24H2+, anything with Voice Clarity enabled) it's excellent. On generic class-compliant drivers with no communications APO registered, it's a no-op. For a guaranteed AEC regardless of driver, see task #26 which tracks implementing the classic Voice Capture DSP (`CLSID_CWMAudioAEC`) as a fallback.
|
||||||
|
|
||||||
|
Gated behind the `windows-aec` feature in `wzp-client`. Enabled by default in the Windows target section of `desktop/src-tauri/Cargo.toml`.
|
||||||
|
|
||||||
|
## Build pipelines
|
||||||
|
|
||||||
|
### Native macOS / Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd desktop
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd src-tauri
|
||||||
|
cargo build --release --bin wzp-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows x86_64 via Docker on SepehrHomeserverdk
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-windows-docker.sh # Full: pull + build + download
|
||||||
|
./scripts/build-windows-docker.sh --no-pull # Skip git fetch
|
||||||
|
./scripts/build-windows-docker.sh --rust # Force-clean Rust target
|
||||||
|
./scripts/build-windows-docker.sh --image-build # (Re)build the Docker image (fire-and-forget)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output lands at `target/windows-exe/wzp-desktop.exe`. Both `wzp-desktop.exe` and `wzp-desktop-noAEC.exe` can coexist in that directory; the script writes `wzp-desktop.exe` so renaming the prior build to `-noAEC.exe` (or any other name) before rebuilding preserves it.
|
||||||
|
|
||||||
|
### Windows x86_64 via Hetzner Cloud (alternative)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-windows-cloud.sh # Full: create VM → build → download → destroy
|
||||||
|
./scripts/build-windows-cloud.sh --prepare # Create VM and install deps only
|
||||||
|
./scripts/build-windows-cloud.sh --build # Build on existing VM
|
||||||
|
./scripts/build-windows-cloud.sh --destroy # Delete the VM
|
||||||
|
WZP_KEEP_VM=1 ./scripts/build-windows-cloud.sh # Keep VM alive after build for debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to destroy the VM at end of day with `--destroy`.
|
||||||
|
|
||||||
|
### Linux x86_64 (relay + CLI + bench)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build-linux-docker.sh # Fire-and-forget remote Docker build
|
||||||
|
./scripts/build-linux-docker.sh --install # Wait for completion and download
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses the same `wzp-android-builder` Docker image as Android (not a separate image), since the deps (Rust + cmake + ring prereqs) are the same.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Direct calling parity
|
||||||
|
|
||||||
|
1. Build on two machines (macOS + Windows, or two macOS, or any combination).
|
||||||
|
2. Both machines register on the same relay.
|
||||||
|
3. Copy one machine's fingerprint into the other's direct-call panel.
|
||||||
|
4. Place the call. Confirm ringing UI on the callee and "calling…" UI on the caller.
|
||||||
|
5. Answer. Confirm audio flows both ways.
|
||||||
|
6. Hang up from either side. Confirm call-history entries are labeled correctly (`Outgoing` on caller, `Incoming` on callee, never `Missed` on a successful call).
|
||||||
|
|
||||||
|
### Windows AEC A/B
|
||||||
|
|
||||||
|
1. Install `wzp-desktop-noAEC.exe` and `wzp-desktop.exe` on the same Windows box.
|
||||||
|
2. Join a call from each (separately) while a second machine plays known audio through the first machine's speakers.
|
||||||
|
3. On the remote (listening) side: the `noAEC` call should have clear audible echo; the AEC call should have minimal or no echo after a 1–2 s convergence period.
|
||||||
|
4. If both builds sound identical (with echo) → the `AudioCategory_Communications` switch isn't triggering the driver's APO chain. Investigate via task #26 (Voice Capture DSP fallback).
|
||||||
|
|
||||||
|
## Known quirks
|
||||||
|
|
||||||
|
1. **libopus vendor path is workspace-relative**. `[patch.crates-io] audiopus_sys = { path = "vendor/audiopus_sys" }` works from any crate in the workspace because Cargo resolves it against the root `Cargo.toml`'s directory. If the workspace is moved or vendored into another workspace, update the path.
|
||||||
|
|
||||||
|
2. **`cargo xwin` overwrites `override.cmake` on every invocation**. Any attempt to patch `~/.cache/cargo-xwin/cmake/clang-cl/override.cmake` at Docker image build time is inert because `src/compiler/clang_cl.rs` line ~444 writes the bundled file fresh on every run. All real fixes must land in the source tree (via the vendored audiopus_sys, as done here), not in the cargo-xwin cache.
|
||||||
|
|
||||||
|
3. **WebView2 runtime is a prerequisite on Windows 10**. Windows 11 ships with it. If the `.exe` launches and immediately exits with no error on a Win10 machine, that's the missing runtime — install it from [Microsoft's Evergreen bootstrapper](https://developer.microsoft.com/en-us/microsoft-edge/webview2/).
|
||||||
|
|
||||||
|
4. **Rust 2024 edition `unsafe_op_in_unsafe_fn` lint**. The WASAPI backend in `audio_wasapi.rs` emits ~18 of these warnings because Rust 2024 requires explicit `unsafe { ... }` blocks inside `unsafe fn` bodies. The warnings don't block the build and don't affect runtime behavior; cleaning them up is tracked informally as tech debt.
|
||||||
|
|
||||||
|
## Files of interest
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `desktop/src/` | Shared frontend (TypeScript + HTML + CSS) |
|
||||||
|
| `desktop/src-tauri/src/lib.rs` | Tauri commands shared with Android |
|
||||||
|
| `desktop/src-tauri/src/engine.rs` | `CallEngine` wrapper |
|
||||||
|
| `desktop/src-tauri/src/history.rs` | Persistent call history store with dedup |
|
||||||
|
| `crates/wzp-client/src/audio_io.rs` | CPAL capture + playback (baseline) |
|
||||||
|
| `crates/wzp-client/src/audio_vpio.rs` | macOS VoiceProcessingIO capture (AEC) |
|
||||||
|
| `crates/wzp-client/src/audio_wasapi.rs` | Windows WASAPI communications capture (AEC) |
|
||||||
|
| `vendor/audiopus_sys/opus/CMakeLists.txt` | Patched libopus for clang-cl SIMD |
|
||||||
|
| `scripts/Dockerfile.windows-builder` | Windows cross-compile Docker image |
|
||||||
|
| `scripts/build-windows-docker.sh` | Remote Docker build pipeline |
|
||||||
|
| `scripts/build-windows-cloud.sh` | Hetzner VPS alternative pipeline |
|
||||||
|
| `scripts/build-linux-docker.sh` | Linux x86_64 relay/CLI build pipeline |
|
||||||
141
docs/PRD-local-recording.md
Normal file
141
docs/PRD-local-recording.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# PRD: Local Recording + Cloud Mixer for Podcast-Quality Interviews
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
WarzonePhone delivers real-time encrypted voice, but the audio quality is limited by network conditions (codec compression, packet loss, jitter). Podcasters and interviewers need pristine, studio-grade recordings of each participant — independent of what the network delivers.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
**Dual-path architecture**: each client simultaneously (1) participates in the live call at whatever codec quality the network supports, and (2) records their own microphone locally as lossless PCM. After the session, all local recordings are uploaded to a self-hosted mixer service that aligns, normalizes, and outputs a final multi-track or mixed file.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐
|
||||||
|
Mic ──┬── Opus/Codec2 ──► Network (live) │ ← real-time call
|
||||||
|
│ └──────────────────┘
|
||||||
|
│
|
||||||
|
└── WAV 48kHz ────► Local File │ ← pristine recording
|
||||||
|
(timestamped)
|
||||||
|
│
|
||||||
|
▼ (after hangup)
|
||||||
|
┌──────────────────┐
|
||||||
|
│ Mixer Service │ ← self-hosted
|
||||||
|
│ (align + mix) │
|
||||||
|
└──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Final MP3/WAV/FLAC
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Phase 1: Local Recording (MVP)
|
||||||
|
|
||||||
|
**All clients (Desktop, Android, Web):**
|
||||||
|
|
||||||
|
1. **Record toggle**: User can enable "Record this call" before or during a call
|
||||||
|
2. **Recording pipeline**: Tap raw PCM from the microphone capture path *before* it enters the codec encoder
|
||||||
|
3. **File format**: WAV (48kHz, 16-bit, mono) — simple, universally supported, lossless
|
||||||
|
4. **Sync markers**: Embed a monotonic timestamp (ms since call start) at the beginning of the recording, and periodically (every 10s) write a sync marker packet into a sidecar JSON file:
|
||||||
|
```json
|
||||||
|
{"ts_ms": 30000, "seq": 1500, "wall_clock_utc": "2026-04-07T12:00:30Z"}
|
||||||
|
```
|
||||||
|
This allows the mixer to align recordings from different participants even if they join at different times.
|
||||||
|
5. **Storage**:
|
||||||
|
- Desktop: `~/.wzp/recordings/{room}_{timestamp}.wav`
|
||||||
|
- Android: `Documents/WarzonePhone/{room}_{timestamp}.wav`
|
||||||
|
- Web: IndexedDB blob or File System Access API
|
||||||
|
6. **File size estimate**: 48kHz * 16-bit * mono = 96 KB/s = ~5.6 MB/min = ~345 MB/hour
|
||||||
|
7. **UI indicator**: Red dot + timer showing recording is active and file size growing
|
||||||
|
8. **On hangup**: Close the WAV file, show "Recording saved" with file path/size
|
||||||
|
|
||||||
|
### Phase 2: Upload to Mixer
|
||||||
|
|
||||||
|
1. **Upload endpoint**: Self-hosted HTTP service (Rust or Go) that accepts WAV uploads with metadata
|
||||||
|
2. **Chunked/resumable upload**: Large files need resumable uploads (tus protocol or simple chunked POST)
|
||||||
|
3. **Upload metadata**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "uuid",
|
||||||
|
"participant_fingerprint": "xxxx:xxxx:...",
|
||||||
|
"alias": "Alice",
|
||||||
|
"room": "podcast-ep-42",
|
||||||
|
"duration_secs": 3600,
|
||||||
|
"sync_markers": [...],
|
||||||
|
"sample_rate": 48000,
|
||||||
|
"channels": 1,
|
||||||
|
"bit_depth": 16
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Upload UI**: Progress bar after hangup, option to upload now or later
|
||||||
|
5. **Retry on failure**: Queue uploads for retry if network is unavailable
|
||||||
|
|
||||||
|
### Phase 3: Mixer Service
|
||||||
|
|
||||||
|
1. **Alignment**: Use sync markers (wall clock + sequence numbers) to align recordings from all participants to a common timeline
|
||||||
|
2. **Silence trimming**: Detect and optionally trim leading/trailing silence
|
||||||
|
3. **Normalization**: Per-track loudness normalization (LUFS-based)
|
||||||
|
4. **Noise reduction**: Optional per-track noise gate or RNNoise pass
|
||||||
|
5. **Output formats**:
|
||||||
|
- Multi-track: ZIP of individual WAVs (aligned, normalized)
|
||||||
|
- Mixed: Single stereo or mono WAV/MP3/FLAC with all participants
|
||||||
|
- Podcast-ready: Loudness-normalized to -16 LUFS (podcast standard)
|
||||||
|
6. **Web UI**: Simple dashboard to see sessions, download outputs, preview waveforms
|
||||||
|
7. **Self-hosted**: Docker image, single binary, SQLite for metadata
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Recording tap point
|
||||||
|
|
||||||
|
The recording must tap *after* AGC (so levels are normalized) but *before* the codec encoder (to avoid compression artifacts). In the current architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
Mic → Ring Buffer → AGC → [TAP HERE for recording] → Opus/Codec2 → Network
|
||||||
|
```
|
||||||
|
|
||||||
|
**Desktop** (`engine.rs`): After `capture_agc.process_frame()`, before `encoder.encode()`
|
||||||
|
**Android** (`engine.rs`): Same location — after AGC, before encode
|
||||||
|
**CLI** (`call.rs`): After `self.agc.process_frame()` in `CallEncoder::encode_frame()`
|
||||||
|
|
||||||
|
### WAV writer
|
||||||
|
|
||||||
|
Use a simple streaming WAV writer that:
|
||||||
|
- Writes the WAV header with placeholder data length
|
||||||
|
- Appends PCM samples as they come
|
||||||
|
- On close, seeks back to update the data length in the header
|
||||||
|
|
||||||
|
### Sync mechanism
|
||||||
|
|
||||||
|
Wall-clock UTC alone is insufficient (clocks drift). The sync strategy:
|
||||||
|
1. Each participant records their local monotonic time + wall clock at call start
|
||||||
|
2. Periodically (every 10s), each participant writes: `{local_mono_ms, seq_number, utc_iso}`
|
||||||
|
3. The mixer uses sequence numbers (which are shared via the wire protocol) as ground truth for alignment, with wall clock as a fallback
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
|
||||||
|
- Local recordings never leave the device without explicit user action
|
||||||
|
- Upload is manual, not automatic
|
||||||
|
- The mixer service processes files and can delete originals after mixing
|
||||||
|
- No recording data flows through the relay — only the user's own mic
|
||||||
|
|
||||||
|
## Non-Goals (v1)
|
||||||
|
|
||||||
|
- Live transcription (future)
|
||||||
|
- Video recording (audio only)
|
||||||
|
- Automatic upload without user consent
|
||||||
|
- Recording other participants' audio (only your own mic)
|
||||||
|
- Real-time mixing (post-session only)
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
| Phase | Scope | Effort |
|
||||||
|
|-------|-------|--------|
|
||||||
|
| 1a | Local WAV recording on Desktop | 1-2 days |
|
||||||
|
| 1b | Local WAV recording on Android | 1-2 days |
|
||||||
|
| 1c | Sync markers + metadata sidecar | 1 day |
|
||||||
|
| 2a | Upload service (HTTP + storage) | 2-3 days |
|
||||||
|
| 2b | Upload UI in clients | 1-2 days |
|
||||||
|
| 3a | Mixer: alignment + normalization | 2-3 days |
|
||||||
|
| 3b | Mixer: web dashboard | 2-3 days |
|
||||||
|
| 3c | Docker packaging | 1 day |
|
||||||
56
docs/PRD-studio-quality.md
Normal file
56
docs/PRD-studio-quality.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# PRD: Studio Quality Tiers (Opus 32k/48k/64k)
|
||||||
|
|
||||||
|
## Status: Implemented
|
||||||
|
|
||||||
|
Studio quality tiers have been added to the wire protocol and all clients.
|
||||||
|
|
||||||
|
## What Was Added
|
||||||
|
|
||||||
|
### Wire Protocol (codec_id.rs)
|
||||||
|
|
||||||
|
Three new `CodecId` variants using the 4-bit header space (values 6-8):
|
||||||
|
|
||||||
|
| CodecId | Wire Value | Bitrate | Frame | Use Case |
|
||||||
|
|---------|-----------|---------|-------|----------|
|
||||||
|
| Opus32k | 6 | 32 kbps | 20ms | Studio low — noticeable improvement over 24k for voice |
|
||||||
|
| Opus48k | 7 | 48 kbps | 20ms | Studio — excellent voice, captures nuance |
|
||||||
|
| Opus64k | 8 | 64 kbps | 20ms | Studio high — near-transparent quality |
|
||||||
|
|
||||||
|
### Quality Profiles
|
||||||
|
|
||||||
|
| Profile | Codec | FEC | Bandwidth (with FEC) |
|
||||||
|
|---------|-------|-----|---------------------|
|
||||||
|
| STUDIO_32K | Opus 32k | 10% | ~35 kbps |
|
||||||
|
| STUDIO_48K | Opus 48k | 10% | ~53 kbps |
|
||||||
|
| STUDIO_64K | Opus 64k | 10% | ~70 kbps |
|
||||||
|
|
||||||
|
FEC is set to 10% (vs 20% for GOOD) — studio assumes a good network.
|
||||||
|
|
||||||
|
### Client Support
|
||||||
|
|
||||||
|
| Client | Selection | Status |
|
||||||
|
|--------|-----------|--------|
|
||||||
|
| Desktop (Tauri) | Quality slider in Settings (8 levels) | Done |
|
||||||
|
| CLI | `--profile studio-64k` / `studio-48k` / `studio-32k` | Done |
|
||||||
|
| Android | Needs codec picker update in SettingsScreen.kt | TODO |
|
||||||
|
| Web | Needs UI | TODO |
|
||||||
|
|
||||||
|
### Cross-Codec Interop
|
||||||
|
|
||||||
|
All decoder auto-switch paths (call.rs, desktop engine.rs) handle the new codec IDs. A studio-64k client can talk to a codec2-1200 client — the receiver auto-switches.
|
||||||
|
|
||||||
|
## When to Use Studio Tiers
|
||||||
|
|
||||||
|
- **Podcast recording sessions**: Use studio-64k for best quality (combined with local WAV recording for pristine output)
|
||||||
|
- **Music collaboration**: Opus at 48-64k captures instrument harmonics much better than 24k
|
||||||
|
- **Good network conditions**: Only useful when bandwidth isn't constrained; the extra bits are wasted on lossy networks
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
|
||||||
|
- **Mobile data**: Stick with Auto/GOOD — studio tiers use 2-3x the bandwidth
|
||||||
|
- **High packet loss**: Studio profiles use minimal FEC (10%); degraded networks need DEGRADED or CATASTROPHIC profiles with 50-100% FEC
|
||||||
|
- **Large group calls**: Each participant's stream multiplies bandwidth; 64k * 10 participants = 640 kbps incoming
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
Old clients (before this change) will receive packets with CodecId 6/7/8 which they don't recognize. The `from_wire()` returns `None` for unknown values, causing the packet to be dropped. Old clients can still *send* to new clients fine (they use CodecId 0-5). This is acceptable for a pre-release protocol.
|
||||||
@@ -457,3 +457,52 @@ Auto mode uses three tiers (Good, Degraded, Catastrophic). It does not use the S
|
|||||||
When you select a specific profile (not Auto), adaptive switching is disabled. The encoder stays at the selected profile regardless of network conditions. This is useful when you know your network quality and want consistent encoding, or when you want to force a specific bitrate.
|
When you select a specific profile (not Auto), adaptive switching is disabled. The encoder stays at the selected profile regardless of network conditions. This is useful when you know your network quality and want consistent encoding, or when you want to force a specific bitrate.
|
||||||
|
|
||||||
Note: The decoder always accepts all codecs. A manual quality selection only affects what you send, not what you receive.
|
Note: The decoder always accepts all codecs. A manual quality selection only affects what you send, not what you receive.
|
||||||
|
|
||||||
|
## Direct 1:1 Calling (Desktop + Android)
|
||||||
|
|
||||||
|
In addition to room-mode group calls, you can place direct calls to a specific peer by fingerprint. Direct calls bypass room state entirely — the relay is used purely as a signaling gateway and for media relay. There is no need for the callee to join a room beforehand; they just need to be registered with the same signal hub.
|
||||||
|
|
||||||
|
### UI elements in the direct-call panel
|
||||||
|
|
||||||
|
- **Place call field** — paste a fingerprint (the long hex string you see under your own identity) and click Call. The callee sees a ringing UI.
|
||||||
|
- **Recent contacts row** — a horizontal strip of chips showing your most recently called/receiving peers. Click a chip to re-dial. Aliases are shown if the peer has one, otherwise a short fingerprint prefix.
|
||||||
|
- **Call history list** — every direct call you've placed, received, or missed, with direction indicator (↗ Outgoing, ↙ Incoming, ✗ Missed), the peer's alias (if known) or fingerprint prefix, and a timestamp. Click an entry to re-dial.
|
||||||
|
- **Deregister button** — drops your signal-hub registration without quitting the app. Useful when switching identities (e.g. testing with two accounts on one machine) or when you want to explicitly appear offline to peers.
|
||||||
|
- **Clear history button** — wipes the call history store. Does not affect current calls.
|
||||||
|
|
||||||
|
### Live updates
|
||||||
|
|
||||||
|
The call history updates in real time across all views via Tauri events (`history-changed`). Placing, answering, or missing a call immediately refreshes the history list and the recent contacts row — no manual refresh needed.
|
||||||
|
|
||||||
|
### Default room
|
||||||
|
|
||||||
|
On first launch, the room name in the room-mode panel defaults to `general` (changed from the prior `android` default so the desktop and Android clients don't silently talk past each other). You can still change it to any room name, and the last-used room is remembered across launches.
|
||||||
|
|
||||||
|
### Random alias
|
||||||
|
|
||||||
|
New installations derive a human-friendly alias from your identity seed — something like `silent-forest-41` or `bold-river-07`. It's deterministic, so reinstalling without changing your seed gives you the same alias. The alias is shown alongside your fingerprint in the header and is what peers see in their call history when they receive your call.
|
||||||
|
|
||||||
|
You can override the alias in Settings → Identity if you want a specific name.
|
||||||
|
|
||||||
|
## Windows AEC Variants
|
||||||
|
|
||||||
|
The Windows desktop build ships in two variants for echo cancellation, depending on which backend you want to exercise. Both are `wzp-desktop.exe` binaries — only the internal audio backend differs.
|
||||||
|
|
||||||
|
| Build | File | Capture backend | AEC | When to use |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **noAEC baseline** | `wzp-desktop-noAEC.exe` | CPAL (WASAPI shared mode) | None | Headphone-only use, or for A/B comparison against the AEC build |
|
||||||
|
| **Communications AEC** | `wzp-desktop.exe` | Direct WASAPI with `AudioCategory_Communications` | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + noise suppression + automatic gain control) | Any speaker-mode call, laptop built-in speakers, anywhere echo is audible |
|
||||||
|
|
||||||
|
**Quality caveat**: the communications AEC operates at the OS level and its algorithm depends on the audio driver's installed APO chain. On modern consumer laptops with Intel Smart Sound, Dolby, recent Realtek, or Windows 11 Voice Clarity, the quality is excellent (effectively matching what Teams/Zoom deliver). On generic class-compliant USB microphones or older drivers, the communications APO may not be present at all — in that case the build behaves identically to the noAEC baseline.
|
||||||
|
|
||||||
|
If you hear echo on the AEC build, try these in order before escalating:
|
||||||
|
|
||||||
|
1. **Check which capture device is selected as "Default Device - Communications"** in Windows Sound Settings → Recording tab. Right-click any device to set it. The AEC build opens the device marked as `eCommunications`, not `eConsole`, so changing the default-communications device changes what we capture from.
|
||||||
|
2. **Verify the driver exposes a communications APO**. Sound Settings → Recording → your mic → Properties → Advanced → look for an "Enhancements" or "Signal Enhancements" tab. If it's absent, the driver has no APOs and the AEC build effectively has no AEC.
|
||||||
|
3. **Try the classic Voice Capture DSP build** when it ships (tracked as task #26). That uses Microsoft's bundled software AEC (`CLSID_CWMAudioAEC`) which works on every Windows machine regardless of driver.
|
||||||
|
|
||||||
|
### Installing the Windows builds
|
||||||
|
|
||||||
|
1. Windows 10: install the [WebView2 Runtime Evergreen Bootstrapper](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) first. Windows 11 has it pre-installed.
|
||||||
|
2. Copy `wzp-desktop.exe` (or `wzp-desktop-noAEC.exe`) to any directory and double-click. No installer needed.
|
||||||
|
3. First launch creates the config + identity store at `%APPDATA%\com.wzp.phone\`.
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# WZ Phone — Android build environment (Debian 12 / Bookworm)
|
# WZ Phone — Android build environment (Debian 12 / Bookworm)
|
||||||
#
|
#
|
||||||
# Matches the bare-metal build-android.sh environment:
|
# Supports both:
|
||||||
|
# 1. Legacy Kotlin+JNI Android app (via cargo-ndk + gradle)
|
||||||
|
# 2. Tauri 2.x Mobile Android app (via tauri-cli + Node/npm)
|
||||||
|
#
|
||||||
|
# Toolchain:
|
||||||
# - Debian 12 (cmake 3.25, no Android cross-compilation bugs)
|
# - Debian 12 (cmake 3.25, no Android cross-compilation bugs)
|
||||||
# - JDK 17 (Gradle 8.5 + AGP 8.2.0 compatible)
|
# - JDK 17 (Gradle 8.5 + AGP 8.2.0 compatible)
|
||||||
# - NDK 26.1 (last stable before scudo/MTE crash on NDK 27+)
|
# - NDK 26.1 (last stable before scudo/MTE crash on NDK 27+)
|
||||||
# - Rust stable with aarch64-linux-android target + cargo-ndk
|
# - Node.js 20 LTS (for Tauri frontend build)
|
||||||
|
# - Rust stable with all 4 Android targets + cargo-ndk + tauri-cli 2.x
|
||||||
#
|
#
|
||||||
# Build: docker build -t wzp-android-builder -f Dockerfile.android-builder .
|
# Build: docker build -t wzp-android-builder -f Dockerfile.android-builder .
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -13,6 +18,11 @@ FROM debian:bookworm
|
|||||||
|
|
||||||
ARG NDK_VERSION=26.1.10909125
|
ARG NDK_VERSION=26.1.10909125
|
||||||
ARG ANDROID_API=34
|
ARG ANDROID_API=34
|
||||||
|
# Tauri 2.x mobile targets compileSdk 36 + build-tools 35 by default. Install
|
||||||
|
# both 34 (legacy Kotlin app) and 35/36 (Tauri mobile) so the same image works
|
||||||
|
# for both pipelines.
|
||||||
|
ARG ANDROID_API_TAURI=36
|
||||||
|
ARG BUILD_TOOLS_TAURI=35.0.0
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
ANDROID_HOME=/opt/android-sdk \
|
ANDROID_HOME=/opt/android-sdk \
|
||||||
@@ -35,8 +45,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
openjdk-17-jdk-headless \
|
openjdk-17-jdk-headless \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
|
file \
|
||||||
|
xz-utils \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── Node.js 20 LTS (required by Tauri for frontend build) ────────────────────
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& node --version \
|
||||||
|
&& npm --version
|
||||||
|
|
||||||
# ── Android SDK + NDK 26.1 ──────────────────────────────────────────────────
|
# ── Android SDK + NDK 26.1 ──────────────────────────────────────────────────
|
||||||
RUN mkdir -p $ANDROID_HOME/cmdline-tools \
|
RUN mkdir -p $ANDROID_HOME/cmdline-tools \
|
||||||
&& cd /tmp \
|
&& cd /tmp \
|
||||||
@@ -49,10 +68,36 @@ RUN yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/nu
|
|||||||
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install \
|
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install \
|
||||||
"platforms;android-${ANDROID_API}" \
|
"platforms;android-${ANDROID_API}" \
|
||||||
"build-tools;${ANDROID_API}.0.0" \
|
"build-tools;${ANDROID_API}.0.0" \
|
||||||
|
"platforms;android-${ANDROID_API_TAURI}" \
|
||||||
|
"build-tools;${BUILD_TOOLS_TAURI}" \
|
||||||
"ndk;${NDK_VERSION}" \
|
"ndk;${NDK_VERSION}" \
|
||||||
"platform-tools" \
|
"platform-tools" \
|
||||||
2>&1 | grep -v '^\[' > /dev/null
|
2>&1 | grep -v '^\[' > /dev/null
|
||||||
|
|
||||||
|
# Work around the API-24 libc.a stub in the NDK. Any C++ static lib we
|
||||||
|
# link into libwzp_desktop_lib.so (e.g. the Oboe audio bridge) pulls in
|
||||||
|
# bionic's static pthread_create from API-24 libc.a via libc++_shared,
|
||||||
|
# and that pthread_create crashes at __init_tcb+4 when called from a
|
||||||
|
# .so loaded via dlopen (the static stub expects libc init state that
|
||||||
|
# only exists for main executables). API-26 has the proper runtime
|
||||||
|
# bindings. Tauri-cli hard-codes aarch64-linux-android24-clang as the
|
||||||
|
# linker and ignores .cargo/config.toml overrides, so the only sure
|
||||||
|
# fix is to replace the NDK's ${abi}24-clang binary itself with a
|
||||||
|
# shim that exec()s the ${abi}26-clang equivalent. Applies to all four
|
||||||
|
# ABIs × {clang, clang++}. The legacy wzp-android crate works without
|
||||||
|
# this because cargo-ndk honours a crate-level linker override; the
|
||||||
|
# shim is the minimal targeted fix for the cargo-tauri build path.
|
||||||
|
# Added as Option 3 for the incremental Step E regression (commit 4250f1b).
|
||||||
|
RUN set -eux; \
|
||||||
|
BIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin; \
|
||||||
|
for abi in aarch64-linux-android armv7a-linux-androideabi i686-linux-android x86_64-linux-android; do \
|
||||||
|
for suffix in clang clang++; do \
|
||||||
|
mv "$BIN/${abi}24-${suffix}" "$BIN/${abi}24-${suffix}.orig"; \
|
||||||
|
printf '#!/bin/sh\nexec "%s/%s26-%s" "$@"\n' "$BIN" "$abi" "$suffix" > "$BIN/${abi}24-${suffix}"; \
|
||||||
|
chmod +x "$BIN/${abi}24-${suffix}"; \
|
||||||
|
done; \
|
||||||
|
done
|
||||||
|
|
||||||
# Make SDK world-readable so builder user can access it
|
# Make SDK world-readable so builder user can access it
|
||||||
RUN chmod -R a+rX $ANDROID_HOME
|
RUN chmod -R a+rX $ANDROID_HOME
|
||||||
|
|
||||||
@@ -64,12 +109,22 @@ USER builder
|
|||||||
WORKDIR /home/builder
|
WORKDIR /home/builder
|
||||||
|
|
||||||
# ── Rust toolchain ───────────────────────────────────────────────────────────
|
# ── Rust toolchain ───────────────────────────────────────────────────────────
|
||||||
|
# Install all 4 Android targets (Tauri Mobile builds for all ABIs by default;
|
||||||
|
# cargo-ndk legacy path only needs arm64-v8a — both workflows supported).
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
| sh -s -- -y --default-toolchain stable \
|
| sh -s -- -y --default-toolchain stable \
|
||||||
&& . $HOME/.cargo/env \
|
&& . $HOME/.cargo/env \
|
||||||
&& rustup target add aarch64-linux-android \
|
&& rustup target add \
|
||||||
&& cargo install cargo-ndk
|
aarch64-linux-android \
|
||||||
|
armv7-linux-androideabi \
|
||||||
|
i686-linux-android \
|
||||||
|
x86_64-linux-android \
|
||||||
|
&& cargo install cargo-ndk \
|
||||||
|
&& cargo install tauri-cli --version "^2.0" --locked
|
||||||
|
|
||||||
ENV PATH="/home/builder/.cargo/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$JAVA_HOME/bin:$PATH"
|
ENV PATH="/home/builder/.cargo/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$JAVA_HOME/bin:$PATH"
|
||||||
|
|
||||||
|
# NDK_HOME is the env var tauri-cli checks (in addition to ANDROID_NDK_HOME)
|
||||||
|
ENV NDK_HOME=$ANDROID_NDK_HOME
|
||||||
|
|
||||||
WORKDIR /build/source
|
WORKDIR /build/source
|
||||||
|
|||||||
59
scripts/Dockerfile.linux-desktop-builder
Normal file
59
scripts/Dockerfile.linux-desktop-builder
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# WZ Phone — Linux x86_64 Tauri desktop build image
|
||||||
|
#
|
||||||
|
# Thin extension of wzp-android-builder that adds the GTK3 + WebKit2GTK 4.1 +
|
||||||
|
# libsoup-3.0 + AppIndicator dev packages needed to build the Tauri desktop
|
||||||
|
# app for Linux. Everything else (Rust, Node.js, cmake, pkg-config, cpal
|
||||||
|
# libasound deps, tauri-cli) is inherited from the base image.
|
||||||
|
#
|
||||||
|
# Build:
|
||||||
|
# docker build -t wzp-linux-desktop-builder -f Dockerfile.linux-desktop-builder .
|
||||||
|
#
|
||||||
|
# Run: driven by scripts/build-linux-desktop-docker.sh (see that file).
|
||||||
|
# =============================================================================
|
||||||
|
FROM wzp-android-builder
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Tauri 2.x Linux dependencies.
|
||||||
|
# - libwebkit2gtk-4.1-dev: the WebView backend. Tauri 2.x uses 4.1 (not 4.0).
|
||||||
|
# - libsoup-3.0-dev: HTTP client used by webkit2gtk. Must match its major.
|
||||||
|
# - libgtk-3-dev: GTK3 headers (webkit2gtk still uses GTK3).
|
||||||
|
# - libayatana-appindicator3-dev: system tray / status icon. Optional at
|
||||||
|
# runtime but tauri-build's feature-detection includes it.
|
||||||
|
# - librsvg2-dev: SVG rendering in the menu/icon code.
|
||||||
|
# - libglib2.0-dev: GObject introspection headers (transitive, but explicit).
|
||||||
|
# - patchelf: used by the tauri bundler to rewrite rpaths in the final binary.
|
||||||
|
# - file: already in the base, but tauri-build checks for it by name.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libsoup-3.0-dev \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
libglib2.0-dev \
|
||||||
|
patchelf \
|
||||||
|
libwebrtc-audio-processing-dev \
|
||||||
|
clang \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── webrtc-audio-processing build requirements ──────────────────────────────
|
||||||
|
# The `webrtc-audio-processing` Rust crate (0.3.x line) links against Debian
|
||||||
|
# Bookworm's `libwebrtc-audio-processing-dev` apt package (0.3-1+b1), which
|
||||||
|
# provides the PulseAudio fork of the WebRTC audio processing module. This is
|
||||||
|
# the library that Pulse's module-echo-cancel and PipeWire's filter-chain
|
||||||
|
# use for their AEC modes — same algorithm family, runtime-linked via
|
||||||
|
# pkg-config at cargo build time.
|
||||||
|
#
|
||||||
|
# An attempt was made to use the 2.x line with the `bundled` sub-feature
|
||||||
|
# (which would give AEC3 instead of AEC2) but both the crates.io tarball
|
||||||
|
# and the upstream git `main` branch hit a `meson setup --reconfigure` bug
|
||||||
|
# that panics on first-run empty build dirs. The 0.3 line avoids the
|
||||||
|
# bundled build path entirely and is what we ship for now.
|
||||||
|
#
|
||||||
|
# `clang` is listed explicitly because the Rust crate's bindgen may need
|
||||||
|
# it at compile time depending on the version of the underlying
|
||||||
|
# webrtc-audio-processing-sys build script.
|
||||||
|
|
||||||
|
USER builder
|
||||||
|
WORKDIR /build/source
|
||||||
99
scripts/Dockerfile.windows-builder
Normal file
99
scripts/Dockerfile.windows-builder
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# WZ Phone — Windows (x86_64-pc-windows-msvc) cross-compile image
|
||||||
|
#
|
||||||
|
# Cross-compiles the Tauri desktop binary for Windows from a Linux host via
|
||||||
|
# `cargo xwin`, which auto-downloads the Microsoft CRT + Windows SDK at build
|
||||||
|
# time. This image pre-warms that cache so the cross-compile is as close as
|
||||||
|
# possible to a native Linux build on rebuild (~3 min warm vs ~20 min cold).
|
||||||
|
#
|
||||||
|
# Build:
|
||||||
|
# docker build -t wzp-windows-builder -f Dockerfile.windows-builder .
|
||||||
|
#
|
||||||
|
# Run: driven by scripts/build-windows-docker.sh (see that file).
|
||||||
|
# =============================================================================
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
|
ARG RUST_TARGET=x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# ── System packages ──────────────────────────────────────────────────────────
|
||||||
|
# - build-essential + pkg-config + libssl-dev: baseline cargo build toolchain
|
||||||
|
# - cmake + ninja-build: audiopus_sys (libopus) uses cmake and expects Ninja
|
||||||
|
# as the generator for the windows target; without ninja-build the cmake
|
||||||
|
# build fails with "CMake was unable to find a build program corresponding
|
||||||
|
# to Ninja" partway through.
|
||||||
|
# - llvm + clang + lld: cargo-xwin uses clang + lld-link for PE/COFF output.
|
||||||
|
# - nasm: ring / rustls assembly for Windows needs NASM on non-Windows hosts.
|
||||||
|
# - curl, git, ca-certificates, unzip: obvious plumbing.
|
||||||
|
# - xz-utils: some Microsoft installer archives are xz-compressed.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
cmake \
|
||||||
|
ninja-build \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
ca-certificates \
|
||||||
|
llvm \
|
||||||
|
clang \
|
||||||
|
lld \
|
||||||
|
nasm \
|
||||||
|
unzip \
|
||||||
|
xz-utils \
|
||||||
|
file \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ── Node.js 20 LTS (required by Tauri for frontend build) ────────────────────
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& node --version \
|
||||||
|
&& npm --version
|
||||||
|
|
||||||
|
# ── Builder user (1000:1000) — matches host bind-mount UID for the cache
|
||||||
|
# volumes so cargo-registry / target survive across runs without perms
|
||||||
|
# gymnastics.
|
||||||
|
RUN groupadd -g 1000 builder \
|
||||||
|
&& useradd -m -u 1000 -g 1000 -s /bin/bash builder
|
||||||
|
|
||||||
|
USER builder
|
||||||
|
WORKDIR /home/builder
|
||||||
|
|
||||||
|
# ── Rust toolchain + Windows target + cargo-xwin ────────────────────────────
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||||
|
| sh -s -- -y --default-toolchain stable \
|
||||||
|
&& . $HOME/.cargo/env \
|
||||||
|
&& rustup target add ${RUST_TARGET} \
|
||||||
|
&& cargo install cargo-xwin --locked
|
||||||
|
|
||||||
|
ENV PATH="/home/builder/.cargo/bin:$PATH" \
|
||||||
|
XWIN_ACCEPT_LICENSE=1 \
|
||||||
|
RUST_TARGET_WIN=${RUST_TARGET}
|
||||||
|
|
||||||
|
# ── Pre-warm the xwin cache ─────────────────────────────────────────────────
|
||||||
|
# cargo-xwin downloads the Microsoft CRT + Windows SDK (~1.5-2 GB) into
|
||||||
|
# ~/.cache/cargo-xwin the first time it runs. Baking that into an image
|
||||||
|
# layer saves ~4 minutes off every subsequent cold run.
|
||||||
|
#
|
||||||
|
# We do this by creating a throwaway Rust project, building it with
|
||||||
|
# cargo-xwin against the Windows target, then deleting the project but
|
||||||
|
# keeping the xwin cache.
|
||||||
|
RUN set -eux; \
|
||||||
|
mkdir -p /tmp/xwin-warmup && cd /tmp/xwin-warmup && \
|
||||||
|
. $HOME/.cargo/env && \
|
||||||
|
cargo new --bin xwin-warmup --quiet && \
|
||||||
|
cd xwin-warmup && \
|
||||||
|
cargo xwin build --release --target ${RUST_TARGET} 2>&1 | tail -5 && \
|
||||||
|
cd / && rm -rf /tmp/xwin-warmup && \
|
||||||
|
du -sh $HOME/.cache/cargo-xwin
|
||||||
|
|
||||||
|
# Note: the libopus SSE4.1/SSSE3 intrinsic compile failure under clang-cl
|
||||||
|
# is fixed at the source level by vendoring audiopus_sys and patching its
|
||||||
|
# bundled libopus CMakeLists.txt (see desktop/vendor/audiopus_sys in the
|
||||||
|
# source tree). Do NOT try to patch cargo-xwin's override.cmake at this
|
||||||
|
# layer — cargo-xwin rewrites that file on every `cargo xwin build`
|
||||||
|
# invocation, so any edits baked into the image are wiped at runtime.
|
||||||
|
|
||||||
|
WORKDIR /build/source
|
||||||
312
scripts/build-linux-desktop-docker.sh
Executable file
312
scripts/build-linux-desktop-docker.sh
Executable file
@@ -0,0 +1,312 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WZ Phone — Linux x86_64 Tauri desktop build (Docker on SepehrHomeserverdk)
|
||||||
|
#
|
||||||
|
# Cross-compiles the Tauri desktop binary for Linux x86_64 inside the
|
||||||
|
# wzp-linux-desktop-builder image (a thin extension of wzp-android-builder
|
||||||
|
# that adds GTK3 + WebKit2GTK 4.1 + libsoup-3.0 + appindicator dev packages).
|
||||||
|
#
|
||||||
|
# Fires an ntfy.sh/wzp notification on build start and build completion, and
|
||||||
|
# uploads the resulting .deb + raw binary to rustypaste.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/build-linux-desktop-docker.sh # Full pipeline
|
||||||
|
# ./scripts/build-linux-desktop-docker.sh --no-pull # Skip git fetch
|
||||||
|
# ./scripts/build-linux-desktop-docker.sh --rust # Clean Rust target
|
||||||
|
# ./scripts/build-linux-desktop-docker.sh --image-build # (Re)build image
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
REMOTE_HOST="SepehrHomeserverdk"
|
||||||
|
BASE_DIR="/mnt/storage/manBuilder"
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
LOCAL_OUTPUT="target/linux-desktop"
|
||||||
|
BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}"
|
||||||
|
SSH_OPTS="-o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o LogLevel=ERROR"
|
||||||
|
|
||||||
|
REBUILD_RUST=0
|
||||||
|
DO_PULL=1
|
||||||
|
IMAGE_BUILD=0
|
||||||
|
# WITH_AEC=1 enables the wzp-client `linux-aec` feature (WebRTC AEC via
|
||||||
|
# webrtc-audio-processing) and renames the output artifacts with an `-aec`
|
||||||
|
# suffix so both variants can coexist on disk.
|
||||||
|
WITH_AEC=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--rust) REBUILD_RUST=1 ;;
|
||||||
|
--pull) DO_PULL=1 ;;
|
||||||
|
--no-pull) DO_PULL=0 ;;
|
||||||
|
--image-build) IMAGE_BUILD=1 ;;
|
||||||
|
--aec) WITH_AEC=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '3,25p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Variant suffix used locally to rename downloaded artifacts so the noAEC
|
||||||
|
# baseline and the AEC build can coexist in $LOCAL_OUTPUT. Mirrors the
|
||||||
|
# same VARIANT declaration inside the remote REMOTE_SCRIPT heredoc.
|
||||||
|
if [ "$WITH_AEC" = "1" ]; then
|
||||||
|
VARIANT="aec"
|
||||||
|
else
|
||||||
|
VARIANT="noAEC"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() { echo -e "\033[1;36m>>> $*\033[0m"; }
|
||||||
|
ssh_cmd() { ssh $SSH_OPTS "$REMOTE_HOST" "$@"; }
|
||||||
|
|
||||||
|
notify_local() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_OUTPUT"
|
||||||
|
|
||||||
|
# ─── Optional: (re)build the docker image on the remote ────────────────────
|
||||||
|
if [ "$IMAGE_BUILD" = "1" ]; then
|
||||||
|
log "Uploading Dockerfile.linux-desktop-builder to remote..."
|
||||||
|
scp $SSH_OPTS "$(dirname "$0")/Dockerfile.linux-desktop-builder" \
|
||||||
|
"$REMOTE_HOST:$BASE_DIR/Dockerfile.linux-desktop-builder"
|
||||||
|
|
||||||
|
log "Triggering remote image build (fire-and-forget)..."
|
||||||
|
ssh_cmd "cd $BASE_DIR && \
|
||||||
|
nohup docker build -f Dockerfile.linux-desktop-builder \
|
||||||
|
-t wzp-linux-desktop-builder . \
|
||||||
|
> /tmp/wzp-linux-desktop-image-build.log 2>&1 & \
|
||||||
|
echo 'image build PID: '\$!"
|
||||||
|
notify_local "WZP Linux desktop image build dispatched"
|
||||||
|
log "Image build running in background on $REMOTE_HOST."
|
||||||
|
log "Tail the log with: ssh $REMOTE_HOST 'tail -f /tmp/wzp-linux-desktop-image-build.log'"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Upload remote build runner script ─────────────────────────────────────
|
||||||
|
log "Uploading remote build script..."
|
||||||
|
ssh_cmd "cat > /tmp/wzp-linux-desktop-build.sh" <<'REMOTE_SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_DIR="/mnt/storage/manBuilder"
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
BRANCH="${1:-feat/desktop-audio-rewrite}"
|
||||||
|
DO_PULL="${2:-1}"
|
||||||
|
REBUILD_RUST="${3:-0}"
|
||||||
|
WITH_AEC="${4:-0}"
|
||||||
|
|
||||||
|
LOG_FILE=/tmp/wzp-linux-desktop-build.log
|
||||||
|
GIT_HASH="unknown"
|
||||||
|
ENV_FILE="$BASE_DIR/.env"
|
||||||
|
|
||||||
|
# Variant suffix for artifact filenames so the noAEC baseline and the AEC
|
||||||
|
# build can coexist on the host. Applied after the build to the downloaded
|
||||||
|
# files (we can't easily rename during the cargo tauri build itself).
|
||||||
|
if [ "$WITH_AEC" = "1" ]; then
|
||||||
|
VARIANT="aec"
|
||||||
|
else
|
||||||
|
VARIANT="noAEC"
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
# Upload to rustypaste; print URL on stdout (or empty on failure).
|
||||||
|
upload_to_rustypaste() {
|
||||||
|
local file="$1"
|
||||||
|
[ ! -f "$file" ] && { echo ""; return; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then
|
||||||
|
curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
on_error() {
|
||||||
|
local line="$1"
|
||||||
|
local log_url
|
||||||
|
log_url=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||||
|
if [ -n "$log_url" ]; then
|
||||||
|
notify "WZP Linux desktop build FAILED [$GIT_HASH] (line $line)
|
||||||
|
log: $log_url"
|
||||||
|
else
|
||||||
|
notify "WZP Linux desktop build FAILED [$GIT_HASH] (line $line) — log upload failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
exec > >(tee "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
# ── git fetch + reset the target branch ───────────────────────────────────
|
||||||
|
if [ "$DO_PULL" = "1" ]; then
|
||||||
|
echo ">>> git fetch + reset $BRANCH"
|
||||||
|
cd "$BASE_DIR/data/source"
|
||||||
|
git reset --hard HEAD 2>/dev/null || true
|
||||||
|
git gc --prune=now 2>/dev/null || true
|
||||||
|
git fetch origin "$BRANCH" 2>&1 | tail -3
|
||||||
|
git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH"
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
git submodule update --init --recursive || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
GIT_HASH=$(cd "$BASE_DIR/data/source" && git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
GIT_MSG=$(cd "$BASE_DIR/data/source" && git log -1 --pretty=%s 2>/dev/null | head -c 60 || echo "?")
|
||||||
|
notify "WZP Linux desktop build STARTED [$GIT_HASH] — $GIT_MSG"
|
||||||
|
|
||||||
|
# Fix perms so builder uid 1000 can read/write the mounted source.
|
||||||
|
find "$BASE_DIR/data/source" "$BASE_DIR/data/cache-linux-desktop" \
|
||||||
|
! -user 1000 -o ! -group 1000 2>/dev/null | \
|
||||||
|
xargs -r chown 1000:1000 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$REBUILD_RUST" = "1" ]; then
|
||||||
|
echo ">>> Cleaning Linux desktop Rust target dir..."
|
||||||
|
rm -rf "$BASE_DIR/data/cache-linux-desktop/target/x86_64-unknown-linux-gnu" \
|
||||||
|
"$BASE_DIR/data/cache-linux-desktop/target/release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Docker run ─────────────────────────────────────────────────────────────
|
||||||
|
# Cache volumes:
|
||||||
|
# - cargo-registry / cargo-git: shared with the android builder — both use
|
||||||
|
# the same crates, so the download cache is worth sharing.
|
||||||
|
# - cache-linux-desktop/target: separate target tree for the desktop build
|
||||||
|
# to keep it isolated from the Linux CLI build (build-linux-docker.sh
|
||||||
|
# uses cache-linux/target for wzp-relay / wzp-client).
|
||||||
|
|
||||||
|
mkdir -p "$BASE_DIR/data/cache/cargo-registry" \
|
||||||
|
"$BASE_DIR/data/cache/cargo-git" \
|
||||||
|
"$BASE_DIR/data/cache-linux-desktop/target"
|
||||||
|
chown -R 1000:1000 "$BASE_DIR/data/cache-linux-desktop/target" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Pass WITH_AEC into the docker container so the inner build script can
|
||||||
|
# decide whether to enable the wzp-client `linux-aec` feature.
|
||||||
|
docker run --rm \
|
||||||
|
--user 1000:1000 \
|
||||||
|
-e WITH_AEC="$WITH_AEC" \
|
||||||
|
-v "$BASE_DIR/data/source:/build/source" \
|
||||||
|
-v "$BASE_DIR/data/cache/cargo-registry:/home/builder/.cargo/registry" \
|
||||||
|
-v "$BASE_DIR/data/cache/cargo-git:/home/builder/.cargo/git" \
|
||||||
|
-v "$BASE_DIR/data/cache-linux-desktop/target:/build/source/target" \
|
||||||
|
wzp-linux-desktop-builder \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd /build/source/desktop
|
||||||
|
|
||||||
|
echo ">>> npm install"
|
||||||
|
npm install --silent 2>&1 | tail -5 || npm install 2>&1 | tail -20
|
||||||
|
|
||||||
|
echo ">>> npm run build"
|
||||||
|
npm run build 2>&1 | tail -5
|
||||||
|
|
||||||
|
# The linux-aec feature enables a WebRTC AEC3 capture backend in
|
||||||
|
# wzp-client. Opt in only when the caller asked for it; noAEC baseline
|
||||||
|
# builds keep the plain CPAL path for comparison. Tauri does not
|
||||||
|
# propagate --features through to the wzp-desktop crate directly
|
||||||
|
# because `cargo tauri build` invokes cargo underneath — so we use
|
||||||
|
# `cargo tauri build -- --features wzp-desktop/linux-aec` to pass it
|
||||||
|
# through. Wait — wzp-desktop is the bin crate, and its `linux-aec`
|
||||||
|
# feature needs to be defined there too. The simpler path is to set
|
||||||
|
# the feature at the wzp-client level via a bin-crate feature that
|
||||||
|
# forwards to wzp-client. Handled in Cargo.toml changes.
|
||||||
|
if [ "${WITH_AEC:-0}" = "1" ]; then
|
||||||
|
echo ">>> cargo tauri build WITH linux-aec feature"
|
||||||
|
cd src-tauri
|
||||||
|
cargo tauri build -- --features wzp-desktop/linux-aec 2>&1 | tail -40
|
||||||
|
else
|
||||||
|
echo ">>> cargo tauri build (noAEC baseline)"
|
||||||
|
cd src-tauri
|
||||||
|
cargo tauri build 2>&1 | tail -40
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Build artifacts:"
|
||||||
|
ls -lh /build/source/target/release/wzp-desktop 2>/dev/null || echo "NO BINARY"
|
||||||
|
ls -lh /build/source/target/release/bundle/deb/*.deb 2>/dev/null || echo "NO DEB"
|
||||||
|
ls -lh /build/source/target/release/bundle/appimage/*.AppImage 2>/dev/null || echo "NO APPIMAGE"
|
||||||
|
'
|
||||||
|
|
||||||
|
# Locate the produced artifacts
|
||||||
|
BIN="$BASE_DIR/data/cache-linux-desktop/target/release/wzp-desktop"
|
||||||
|
DEB=$(ls "$BASE_DIR/data/cache-linux-desktop/target/release/bundle/deb/"*.deb 2>/dev/null | head -1 || true)
|
||||||
|
APPIMAGE=$(ls "$BASE_DIR/data/cache-linux-desktop/target/release/bundle/appimage/"*.AppImage 2>/dev/null | head -1 || true)
|
||||||
|
|
||||||
|
if [ ! -f "$BIN" ]; then
|
||||||
|
LOG_URL=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||||
|
if [ -n "$LOG_URL" ]; then
|
||||||
|
notify "WZP Linux desktop build [$GIT_HASH]: no binary produced
|
||||||
|
log: $LOG_URL"
|
||||||
|
else
|
||||||
|
notify "WZP Linux desktop build [$GIT_HASH]: no binary produced — log upload failed"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BIN_SIZE=$(du -h "$BIN" | cut -f1)
|
||||||
|
|
||||||
|
# Prefer to ship the .deb if we got one, otherwise fall back to the raw binary.
|
||||||
|
ARTIFACT="$BIN"
|
||||||
|
ARTIFACT_KIND="binary"
|
||||||
|
if [ -n "$DEB" ] && [ -f "$DEB" ]; then
|
||||||
|
ARTIFACT="$DEB"
|
||||||
|
ARTIFACT_KIND="deb"
|
||||||
|
ARTIFACT_SIZE=$(du -h "$DEB" | cut -f1)
|
||||||
|
else
|
||||||
|
ARTIFACT_SIZE="$BIN_SIZE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUSTY_URL=$(upload_to_rustypaste "$ARTIFACT" || echo "")
|
||||||
|
if [ -n "$RUSTY_URL" ]; then
|
||||||
|
notify "WZP Linux desktop build OK [$GIT_HASH] ($ARTIFACT_KIND, $ARTIFACT_SIZE)
|
||||||
|
$RUSTY_URL"
|
||||||
|
else
|
||||||
|
notify "WZP Linux desktop build OK [$GIT_HASH] ($ARTIFACT_KIND, $ARTIFACT_SIZE) — rustypaste upload skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print paths so the local script can scp them back
|
||||||
|
echo "BIN_REMOTE_PATH=$BIN"
|
||||||
|
[ -n "$DEB" ] && echo "DEB_REMOTE_PATH=$DEB"
|
||||||
|
[ -n "$APPIMAGE" ] && echo "APPIMAGE_REMOTE_PATH=$APPIMAGE"
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
|
||||||
|
ssh_cmd "chmod +x /tmp/wzp-linux-desktop-build.sh"
|
||||||
|
|
||||||
|
notify_local "WZP Linux desktop build dispatched (branch=$BRANCH)"
|
||||||
|
log "Triggering remote build (branch=$BRANCH)..."
|
||||||
|
|
||||||
|
# Run; last lines are *_REMOTE_PATH=...
|
||||||
|
REMOTE_OUTPUT=$(ssh_cmd "/tmp/wzp-linux-desktop-build.sh '$BRANCH' '$DO_PULL' '$REBUILD_RUST' '$WITH_AEC'" || true)
|
||||||
|
echo "$REMOTE_OUTPUT" | tail -80
|
||||||
|
|
||||||
|
BIN_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^BIN_REMOTE_PATH=' | tail -1 | cut -d= -f2-)
|
||||||
|
DEB_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^DEB_REMOTE_PATH=' | tail -1 | cut -d= -f2-)
|
||||||
|
APPIMAGE_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^APPIMAGE_REMOTE_PATH=' | tail -1 | cut -d= -f2-)
|
||||||
|
|
||||||
|
if [ -n "$BIN_REMOTE" ]; then
|
||||||
|
log "Downloading wzp-desktop binary to $LOCAL_OUTPUT/wzp-desktop-$VARIANT ..."
|
||||||
|
scp $SSH_OPTS "$REMOTE_HOST:$BIN_REMOTE" "$LOCAL_OUTPUT/wzp-desktop-$VARIANT"
|
||||||
|
echo " $LOCAL_OUTPUT/wzp-desktop-$VARIANT ($(du -h "$LOCAL_OUTPUT/wzp-desktop-$VARIANT" | cut -f1))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$DEB_REMOTE" ]; then
|
||||||
|
# Apply the variant suffix to the downloaded .deb: cargo-tauri names the
|
||||||
|
# file WarzonePhone_<version>_amd64.deb regardless of what we built, so
|
||||||
|
# the variant lives only in our chosen filename.
|
||||||
|
DEB_BASENAME=$(basename "$DEB_REMOTE" .deb)
|
||||||
|
log "Downloading .deb to $LOCAL_OUTPUT/${DEB_BASENAME}-$VARIANT.deb ..."
|
||||||
|
scp $SSH_OPTS "$REMOTE_HOST:$DEB_REMOTE" "$LOCAL_OUTPUT/${DEB_BASENAME}-$VARIANT.deb"
|
||||||
|
ls -lh "$LOCAL_OUTPUT/${DEB_BASENAME}-$VARIANT.deb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$APPIMAGE_REMOTE" ]; then
|
||||||
|
APPIMG_BASENAME=$(basename "$APPIMAGE_REMOTE" .AppImage)
|
||||||
|
log "Downloading .AppImage to $LOCAL_OUTPUT/${APPIMG_BASENAME}-$VARIANT.AppImage ..."
|
||||||
|
scp $SSH_OPTS "$REMOTE_HOST:$APPIMAGE_REMOTE" "$LOCAL_OUTPUT/${APPIMG_BASENAME}-$VARIANT.AppImage"
|
||||||
|
ls -lh "$LOCAL_OUTPUT/${APPIMG_BASENAME}-$VARIANT.AppImage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$BIN_REMOTE" ]; then
|
||||||
|
log "No binary produced — see ntfy / remote log /tmp/wzp-linux-desktop-build.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
253
scripts/build-tauri-android.sh
Executable file
253
scripts/build-tauri-android.sh
Executable file
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WZ Phone — Tauri 2.x Mobile Android APK build
|
||||||
|
#
|
||||||
|
# Builds the desktop/ Tauri app as an Android APK via cargo-tauri inside the
|
||||||
|
# wzp-android-builder Docker image on SepehrHomeserverdk. Uploads the APK to
|
||||||
|
# rustypaste, fires ntfy.sh/wzp notifications at start + finish, and SCPs the
|
||||||
|
# APK back locally.
|
||||||
|
#
|
||||||
|
# Same pattern as build-and-notify.sh but for the Tauri mobile pipeline:
|
||||||
|
# - Source: desktop/src-tauri/ (not android/)
|
||||||
|
# - Build: cargo tauri android build (not gradlew assembleDebug)
|
||||||
|
# - Output: desktop/src-tauri/gen/android/.../*.apk
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/build-tauri-android.sh # full pipeline (debug)
|
||||||
|
# ./scripts/build-tauri-android.sh --release # release APK
|
||||||
|
# ./scripts/build-tauri-android.sh --no-pull # skip git fetch
|
||||||
|
# ./scripts/build-tauri-android.sh --rust # force-clean rust target
|
||||||
|
# ./scripts/build-tauri-android.sh --init # also run `cargo tauri android init`
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
REMOTE_HOST="SepehrHomeserverdk"
|
||||||
|
BASE_DIR="/mnt/storage/manBuilder"
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
LOCAL_OUTPUT="target/tauri-android-apk"
|
||||||
|
BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}"
|
||||||
|
SSH_OPTS="-o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o LogLevel=ERROR"
|
||||||
|
|
||||||
|
REBUILD_RUST=0
|
||||||
|
DO_PULL=1
|
||||||
|
DO_INIT=0
|
||||||
|
BUILD_RELEASE=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--rust) REBUILD_RUST=1 ;;
|
||||||
|
--pull) DO_PULL=1 ;;
|
||||||
|
--no-pull) DO_PULL=0 ;;
|
||||||
|
--init) DO_INIT=1 ;;
|
||||||
|
--release) BUILD_RELEASE=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '3,30p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log() { echo -e "\033[1;36m>>> $*\033[0m"; }
|
||||||
|
ssh_cmd() { ssh -A $SSH_OPTS "$REMOTE_HOST" "$@"; }
|
||||||
|
|
||||||
|
notify_local() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_OUTPUT"
|
||||||
|
|
||||||
|
log "Uploading remote build script..."
|
||||||
|
ssh_cmd "cat > /tmp/wzp-tauri-build.sh" <<'REMOTE_SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_DIR="/mnt/storage/manBuilder"
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
BRANCH="${1:-feat/desktop-audio-rewrite}"
|
||||||
|
DO_PULL="${2:-1}"
|
||||||
|
REBUILD_RUST="${3:-0}"
|
||||||
|
DO_INIT="${4:-0}"
|
||||||
|
BUILD_RELEASE="${5:-0}"
|
||||||
|
|
||||||
|
LOG_FILE=/tmp/wzp-tauri-build.log
|
||||||
|
GIT_HASH="unknown" # populated after fetch
|
||||||
|
ENV_FILE="$BASE_DIR/.env"
|
||||||
|
|
||||||
|
notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
# Upload a file to rustypaste; print URL on stdout (or empty on failure).
|
||||||
|
upload_to_rustypaste() {
|
||||||
|
local file="$1"
|
||||||
|
[ ! -f "$ENV_FILE" ] && { echo ""; return; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then
|
||||||
|
curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# On failure: upload the build log to rustypaste, then notify with hash + url.
|
||||||
|
on_error() {
|
||||||
|
local line="$1"
|
||||||
|
local log_url
|
||||||
|
log_url=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||||
|
if [ -n "$log_url" ]; then
|
||||||
|
notify "WZP Tauri Android build FAILED [$GIT_HASH] (line $line)
|
||||||
|
log: $log_url"
|
||||||
|
else
|
||||||
|
notify "WZP Tauri Android build FAILED [$GIT_HASH] (line $line) — log upload failed, see $LOG_FILE on remote"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
exec > >(tee "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
if [ "$DO_PULL" = "1" ]; then
|
||||||
|
echo ">>> git fetch + reset $BRANCH"
|
||||||
|
cd "$BASE_DIR/data/source"
|
||||||
|
git reset --hard HEAD 2>/dev/null || true
|
||||||
|
# NOTE: deliberately do NOT run `git clean -fd` here. It would wipe the
|
||||||
|
# tauri-generated `desktop/src-tauri/gen/android/` scaffold (gradlew,
|
||||||
|
# settings.gradle, etc.) which is expensive to recreate and breaks
|
||||||
|
# subsequent builds with "gradlew not found".
|
||||||
|
git gc --prune=now 2>/dev/null || true
|
||||||
|
git fetch origin "$BRANCH" 2>&1 | tail -3
|
||||||
|
git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH"
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
git submodule update --init || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
GIT_HASH=$(cd "$BASE_DIR/data/source" && git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
GIT_MSG=$(cd "$BASE_DIR/data/source" && git log -1 --pretty=%s 2>/dev/null | head -c 60 || echo "?")
|
||||||
|
notify "WZP Tauri Android build STARTED [$GIT_HASH] — $GIT_MSG"
|
||||||
|
|
||||||
|
# Fix perms so uid 1000 can write
|
||||||
|
find "$BASE_DIR/data/source" "$BASE_DIR/data/cache" \
|
||||||
|
! -user 1000 -o ! -group 1000 2>/dev/null | \
|
||||||
|
xargs -r chown 1000:1000 2>/dev/null || true
|
||||||
|
|
||||||
|
# Optionally clean rust target for android triples
|
||||||
|
if [ "$REBUILD_RUST" = "1" ]; then
|
||||||
|
echo ">>> Cleaning Rust android target dirs..."
|
||||||
|
rm -rf "$BASE_DIR/data/cache/target/aarch64-linux-android" \
|
||||||
|
"$BASE_DIR/data/cache/target/armv7-linux-androideabi" \
|
||||||
|
"$BASE_DIR/data/cache/target/i686-linux-android" \
|
||||||
|
"$BASE_DIR/data/cache/target/x86_64-linux-android"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Profile flag
|
||||||
|
PROFILE_FLAG="--debug"
|
||||||
|
[ "$BUILD_RELEASE" = "1" ] && PROFILE_FLAG=""
|
||||||
|
|
||||||
|
# Persist ~/.android (where the auto-generated debug.keystore lives) so every
|
||||||
|
# build is signed with the SAME key. Without this, every fresh container gets
|
||||||
|
# a new debug keystore and `adb install -r` fails with INSTALL_FAILED_UPDATE_
|
||||||
|
# INCOMPATIBLE because the signature changed.
|
||||||
|
mkdir -p "$BASE_DIR/data/cache/android-home"
|
||||||
|
chown 1000:1000 "$BASE_DIR/data/cache/android-home" 2>/dev/null || true
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--user 1000:1000 \
|
||||||
|
-e DO_INIT="$DO_INIT" \
|
||||||
|
-e PROFILE_FLAG="$PROFILE_FLAG" \
|
||||||
|
-v "$BASE_DIR/data/source:/build/source" \
|
||||||
|
-v "$BASE_DIR/data/cache/cargo-registry:/home/builder/.cargo/registry" \
|
||||||
|
-v "$BASE_DIR/data/cache/cargo-git:/home/builder/.cargo/git" \
|
||||||
|
-v "$BASE_DIR/data/cache/target:/build/source/target" \
|
||||||
|
-v "$BASE_DIR/data/cache/gradle:/home/builder/.gradle" \
|
||||||
|
-v "$BASE_DIR/data/cache/android-home:/home/builder/.android" \
|
||||||
|
wzp-android-builder \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
cd /build/source/desktop
|
||||||
|
|
||||||
|
echo ">>> npm install"
|
||||||
|
npm install --silent 2>&1 | tail -5 || npm install 2>&1 | tail -20
|
||||||
|
|
||||||
|
cd src-tauri
|
||||||
|
|
||||||
|
# Run init if forced, OR if the gradle wrapper is missing. Just checking
|
||||||
|
# for `gen/android` is not enough — Tauri creates a few subdirectories
|
||||||
|
# during build (app/, buildSrc/, .gradle/) that survive a partial wipe and
|
||||||
|
# would make a naive `[ ! -d gen/android ]` check return false even though
|
||||||
|
# the build wrapper itself is gone.
|
||||||
|
if [ "${DO_INIT}" = "1" ] || [ ! -x gen/android/gradlew ]; then
|
||||||
|
echo ">>> cargo tauri android init"
|
||||||
|
cargo tauri android init 2>&1 | tail -20
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── wzp-native standalone cdylib (built with cargo-ndk, not cargo-tauri) ──
|
||||||
|
# Produces libwzp_native.so which wzp-desktop dlopens at runtime via
|
||||||
|
# libloading. Split exists because cargo-tauri`s linker wiring pulls
|
||||||
|
# bionic private symbols into any cdylib with cc::Build C++, causing
|
||||||
|
# __init_tcb+4 SIGSEGV. cargo-ndk uses the same linker path as the
|
||||||
|
# legacy wzp-android crate which works.
|
||||||
|
echo ">>> cargo ndk build -p wzp-native --release"
|
||||||
|
JNI_ABI_DIR=gen/android/app/src/main/jniLibs/arm64-v8a
|
||||||
|
mkdir -p "$JNI_ABI_DIR"
|
||||||
|
(
|
||||||
|
cd /build/source
|
||||||
|
cargo ndk -t arm64-v8a -o desktop/src-tauri/gen/android/app/src/main/jniLibs \
|
||||||
|
build --release -p wzp-native 2>&1 | tail -10
|
||||||
|
)
|
||||||
|
if [ -f "$JNI_ABI_DIR/libwzp_native.so" ]; then
|
||||||
|
ls -lh "$JNI_ABI_DIR/libwzp_native.so"
|
||||||
|
else
|
||||||
|
echo ">>> WARNING: libwzp_native.so not produced"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ">>> cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk"
|
||||||
|
cargo tauri android build ${PROFILE_FLAG} --target aarch64 --apk
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Build artifacts:"
|
||||||
|
find gen/android -name "*.apk" -exec ls -lh {} \; 2>/dev/null
|
||||||
|
'
|
||||||
|
|
||||||
|
# Locate the produced APK
|
||||||
|
APK=$(find "$BASE_DIR/data/source/desktop/src-tauri/gen/android" -name "*.apk" -type f 2>/dev/null | head -1)
|
||||||
|
if [ -z "$APK" ] || [ ! -f "$APK" ]; then
|
||||||
|
LOG_URL=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||||
|
if [ -n "$LOG_URL" ]; then
|
||||||
|
notify "WZP Tauri Android build [$GIT_HASH]: no APK produced
|
||||||
|
log: $LOG_URL"
|
||||||
|
else
|
||||||
|
notify "WZP Tauri Android build [$GIT_HASH]: no APK produced — log upload failed"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
APK_SIZE=$(du -h "$APK" | cut -f1)
|
||||||
|
|
||||||
|
RUSTY_URL=$(upload_to_rustypaste "$APK" || echo "")
|
||||||
|
if [ -n "$RUSTY_URL" ]; then
|
||||||
|
notify "WZP Tauri Android build OK [$GIT_HASH] ($APK_SIZE)
|
||||||
|
$RUSTY_URL"
|
||||||
|
else
|
||||||
|
notify "WZP Tauri Android build OK [$GIT_HASH] ($APK_SIZE) — rustypaste upload skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print path so the local script can grab it
|
||||||
|
echo "APK_REMOTE_PATH=$APK"
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
|
||||||
|
ssh_cmd "chmod +x /tmp/wzp-tauri-build.sh"
|
||||||
|
|
||||||
|
notify_local "WZP Tauri Android build dispatched (branch=$BRANCH, release=$BUILD_RELEASE)"
|
||||||
|
log "Triggering remote build (branch=$BRANCH)..."
|
||||||
|
|
||||||
|
# Run; capture full output, last line is APK_REMOTE_PATH=...
|
||||||
|
REMOTE_OUTPUT=$(ssh_cmd "/tmp/wzp-tauri-build.sh '$BRANCH' '$DO_PULL' '$REBUILD_RUST' '$DO_INIT' '$BUILD_RELEASE'" || true)
|
||||||
|
echo "$REMOTE_OUTPUT" | tail -60
|
||||||
|
|
||||||
|
APK_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^APK_REMOTE_PATH=' | tail -1 | cut -d= -f2-)
|
||||||
|
if [ -n "$APK_REMOTE" ]; then
|
||||||
|
log "Downloading APK to $LOCAL_OUTPUT/wzp-tauri.apk..."
|
||||||
|
scp $SSH_OPTS "$REMOTE_HOST:$APK_REMOTE" "$LOCAL_OUTPUT/wzp-tauri.apk"
|
||||||
|
echo " $LOCAL_OUTPUT/wzp-tauri.apk ($(du -h "$LOCAL_OUTPUT/wzp-tauri.apk" | cut -f1))"
|
||||||
|
else
|
||||||
|
log "No APK produced — see ntfy / remote log /tmp/wzp-tauri-build.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
391
scripts/build-windows-cloud.sh
Executable file
391
scripts/build-windows-cloud.sh
Executable file
@@ -0,0 +1,391 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build WarzonePhone desktop .exe for Windows x86_64 using a temporary
|
||||||
|
# Hetzner Cloud VPS. Cross-compiles from Linux via `cargo xwin`, which
|
||||||
|
# auto-downloads the Windows SDK + MSVC CRT the first time it runs.
|
||||||
|
#
|
||||||
|
# No Windows machine needed for the build itself — the produced .exe
|
||||||
|
# still has to be copied to a real Windows host to run (we can only
|
||||||
|
# verify compile + link here, not runtime).
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - hcloud CLI authenticated
|
||||||
|
# - SSH key "wz" registered in Hetzner
|
||||||
|
# - Local ssh-agent loaded with an SSH key that can read the
|
||||||
|
# git.manko.yoga repo (the script forwards the agent so the VM's
|
||||||
|
# git clone uses your identity). Run `ssh-add /Users/manwe/CascadeProjects/wzp`
|
||||||
|
# once before invoking this script if you haven't already.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/build-windows-cloud.sh Full build (create → build → download → destroy)
|
||||||
|
# ./scripts/build-windows-cloud.sh --prepare Create VM and install deps only
|
||||||
|
# ./scripts/build-windows-cloud.sh --build Build on existing VM
|
||||||
|
# ./scripts/build-windows-cloud.sh --transfer Download .exe from VM
|
||||||
|
# ./scripts/build-windows-cloud.sh --destroy Delete the VM
|
||||||
|
# ./scripts/build-windows-cloud.sh --all prepare + build + transfer (VM persists)
|
||||||
|
# ./scripts/build-windows-cloud.sh --upload Re-upload source to existing VM
|
||||||
|
#
|
||||||
|
# Environment variables (all optional):
|
||||||
|
# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite)
|
||||||
|
# WZP_SERVER_TYPE Hetzner server type (default: cx23 — small, cheap, x86)
|
||||||
|
# WZP_KEEP_VM Set to 1 to skip destroy on full build
|
||||||
|
|
||||||
|
SSH_KEY_NAME="wz"
|
||||||
|
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
|
||||||
|
SERVER_TYPE="${WZP_SERVER_TYPE:-cx33}" # cx23 (4GB RAM) OOMs on tauri+rustls cross-compile — bump to cx33 (8GB, 8 vCPU)
|
||||||
|
IMAGE="ubuntu-24.04"
|
||||||
|
SERVER_NAME="wzp-windows-builder"
|
||||||
|
REMOTE_USER="root"
|
||||||
|
OUTPUT_DIR="target/windows-exe"
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}"
|
||||||
|
KEEP_VM="${WZP_KEEP_VM:-0}"
|
||||||
|
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR"
|
||||||
|
|
||||||
|
RUST_TARGET="x86_64-pc-windows-msvc"
|
||||||
|
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
RUSTY_ENV_FILE="$HOME/.wzp/rustypaste.env"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
log() { echo -e "\n\033[1;36m>>> $*\033[0m"; }
|
||||||
|
err() { echo -e "\033[1;31mERROR: $*\033[0m" >&2; }
|
||||||
|
die() {
|
||||||
|
err "$@"
|
||||||
|
notify "WZP Windows build FAILED — $*"
|
||||||
|
# If the user wants to keep the VM alive for debugging (WZP_KEEP_VM=1),
|
||||||
|
# don't tear it down on failure — they might want to ssh in and poke at
|
||||||
|
# the build state. Only auto-destroy when KEEP_VM is explicitly off.
|
||||||
|
if [ "${KEEP_VM:-0}" != "1" ]; then
|
||||||
|
do_destroy_quiet
|
||||||
|
else
|
||||||
|
err "VM kept alive for debugging (WZP_KEEP_VM=1). Destroy with $0 --destroy"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
notify() {
|
||||||
|
# Fire-and-forget ntfy. Silently ignored if there's no network.
|
||||||
|
curl -sf -m 5 -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload a file to the online rustypaste (paste.dk.manko.yoga), return
|
||||||
|
# the public URL on stdout. Requires $RUSTY_ENV_FILE to contain
|
||||||
|
# rusty_address + rusty_auth_token (synced from SepehrHomeserverdk's
|
||||||
|
# /mnt/storage/manBuilder/.env once; see README).
|
||||||
|
rustypaste_upload() {
|
||||||
|
local file="$1"
|
||||||
|
[ -f "$file" ] || { echo ""; return; }
|
||||||
|
[ -f "$RUSTY_ENV_FILE" ] || { echo ""; return; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$RUSTY_ENV_FILE"
|
||||||
|
if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then
|
||||||
|
curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_vm_ip() {
|
||||||
|
hcloud server list -o columns=name,ipv4 -o noheader 2>/dev/null | grep "$SERVER_NAME" | awk '{print $2}' | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_cmd() {
|
||||||
|
local ip
|
||||||
|
ip=$(get_vm_ip)
|
||||||
|
[ -n "$ip" ] || die "No VM found. Run --prepare first."
|
||||||
|
ssh $SSH_OPTS -A -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
scp_down() {
|
||||||
|
local ip
|
||||||
|
ip=$(get_vm_ip)
|
||||||
|
[ -n "$ip" ] || die "No VM found."
|
||||||
|
scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_destroy_quiet() {
|
||||||
|
local name
|
||||||
|
name=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true)
|
||||||
|
if [ -n "$name" ]; then
|
||||||
|
echo ""
|
||||||
|
err "Cleaning up — destroying VM $name"
|
||||||
|
hcloud server delete "$name" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --prepare: Create VM, install all build dependencies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_prepare() {
|
||||||
|
local existing
|
||||||
|
existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true)
|
||||||
|
if [ -n "$existing" ]; then
|
||||||
|
log "VM already exists: $existing — reusing"
|
||||||
|
do_upload
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify "WZP Windows build STARTED ($BRANCH) — spinning up $SERVER_TYPE"
|
||||||
|
log "Creating Hetzner VM ($SERVER_TYPE, $IMAGE)..."
|
||||||
|
hcloud server create \
|
||||||
|
--name "$SERVER_NAME" \
|
||||||
|
--type "$SERVER_TYPE" \
|
||||||
|
--image "$IMAGE" \
|
||||||
|
--ssh-key "$SSH_KEY_NAME" \
|
||||||
|
--location fsn1 \
|
||||||
|
--quiet \
|
||||||
|
|| die "Failed to create VM"
|
||||||
|
|
||||||
|
local ip
|
||||||
|
ip=$(get_vm_ip)
|
||||||
|
[ -n "$ip" ] || die "VM created but no IP found"
|
||||||
|
echo " VM: $SERVER_NAME @ $ip"
|
||||||
|
|
||||||
|
log "Waiting for SSH..."
|
||||||
|
local ok=0
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "echo ok" &>/dev/null; then
|
||||||
|
ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
[ "$ok" -eq 1 ] || die "SSH timeout after 60s"
|
||||||
|
|
||||||
|
# System packages — cargo-xwin needs llvm/lld; ring needs nasm on
|
||||||
|
# Windows; audiopus_sys (libopus) uses cmake + ninja to build for the
|
||||||
|
# Windows target; tauri's build.rs needs the frontend dist which needs
|
||||||
|
# node+npm.
|
||||||
|
log "Installing system packages (llvm, lld, clang, nasm, ninja, node)..."
|
||||||
|
ssh_cmd "export DEBIAN_FRONTEND=noninteractive && \
|
||||||
|
apt-get update -qq && \
|
||||||
|
apt-get install -y -qq \
|
||||||
|
build-essential cmake ninja-build curl git pkg-config \
|
||||||
|
llvm clang lld nasm \
|
||||||
|
libssl-dev ca-certificates \
|
||||||
|
unzip wget \
|
||||||
|
> /dev/null 2>&1" \
|
||||||
|
|| die "Failed to install system packages"
|
||||||
|
|
||||||
|
# Node.js 20 via NodeSource
|
||||||
|
ssh_cmd "curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1 && \
|
||||||
|
apt-get install -y -qq nodejs > /dev/null 2>&1" \
|
||||||
|
|| die "Failed to install Node.js"
|
||||||
|
|
||||||
|
echo " clang: $(ssh_cmd "clang --version | head -1")"
|
||||||
|
echo " node: $(ssh_cmd "node --version")"
|
||||||
|
echo " npm: $(ssh_cmd "npm --version")"
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
log "Installing Rust toolchain + target $RUST_TARGET..."
|
||||||
|
ssh_cmd "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable > /dev/null 2>&1" \
|
||||||
|
|| die "Failed to install Rust"
|
||||||
|
ssh_cmd "source \$HOME/.cargo/env && rustup target add $RUST_TARGET > /dev/null 2>&1" \
|
||||||
|
|| die "Failed to add Windows target"
|
||||||
|
echo " rust: $(ssh_cmd "source \$HOME/.cargo/env && rustc --version")"
|
||||||
|
|
||||||
|
# cargo-xwin — the cross compiler glue that fetches Windows SDK + CRT
|
||||||
|
# on demand and shims cc/lld to produce PE/COFF output. The Microsoft
|
||||||
|
# license is auto-accepted via XWIN_ACCEPT_LICENSE=1 below (current
|
||||||
|
# cargo-xwin removed the --accept-license CLI flag in favour of the
|
||||||
|
# env var; --dry-run just prints what it would do).
|
||||||
|
log "Installing cargo-xwin..."
|
||||||
|
ssh_cmd "source \$HOME/.cargo/env && cargo install cargo-xwin > /dev/null 2>&1" \
|
||||||
|
|| die "Failed to install cargo-xwin"
|
||||||
|
echo " cargo-xwin: $(ssh_cmd "source \$HOME/.cargo/env && cargo xwin --version 2>&1 | head -1")"
|
||||||
|
|
||||||
|
# Make the license-accept env var persist across later ssh_cmd calls so
|
||||||
|
# `cargo xwin build` in do_build() doesn't prompt interactively.
|
||||||
|
ssh_cmd "echo 'export XWIN_ACCEPT_LICENSE=1' >> \$HOME/.bashrc"
|
||||||
|
|
||||||
|
# Do the source upload + git clone (agent-forwarded) here.
|
||||||
|
do_upload
|
||||||
|
|
||||||
|
log "VM ready!"
|
||||||
|
echo " IP: $ip"
|
||||||
|
echo " SSH: ssh -A -i $SSH_KEY_PATH root@$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --upload: Clone the repo on the VM (not rsync — the branch we want
|
||||||
|
# lives in a separate worktree, and cloning from git is simpler + reuses
|
||||||
|
# whatever SSH identity the calling shell has loaded in its agent).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
GIT_REPO="ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git"
|
||||||
|
|
||||||
|
do_upload() {
|
||||||
|
log "Cloning wz-phone on VM (branch $BRANCH, agent-forwarded)..."
|
||||||
|
local ip
|
||||||
|
ip=$(get_vm_ip)
|
||||||
|
[ -n "$ip" ] || die "No VM found."
|
||||||
|
|
||||||
|
# Accept the git host key once so `git clone` doesn't hang asking.
|
||||||
|
ssh_cmd "mkdir -p \$HOME/.ssh && \
|
||||||
|
ssh-keyscan -p 222 -t rsa,ecdsa,ed25519 git.manko.yoga >> \$HOME/.ssh/known_hosts 2>/dev/null"
|
||||||
|
|
||||||
|
# Fresh clone each run — cheap on a short-lived builder VM, avoids
|
||||||
|
# stale state if the branch was force-pushed. --recurse-submodules so
|
||||||
|
# deps/featherchat (which has the warzone-protocol workspace member)
|
||||||
|
# comes along for the ride.
|
||||||
|
ssh_cmd "rm -rf /root/wzp-build && \
|
||||||
|
git clone --depth 1 --branch $BRANCH --recurse-submodules --shallow-submodules $GIT_REPO /root/wzp-build 2>&1 | tail -5" \
|
||||||
|
|| die "git clone failed — is your ssh-agent loaded with a key that can read git.manko.yoga?"
|
||||||
|
|
||||||
|
echo " Cloned $BRANCH into /root/wzp-build (with submodules)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --build: Build frontend + cross-compile wzp-desktop.exe
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_build() {
|
||||||
|
log "Building frontend (vite)..."
|
||||||
|
ssh_cmd "cd /root/wzp-build/desktop && \
|
||||||
|
npm install --silent 2>&1 | tail -3 && \
|
||||||
|
npm run build 2>&1 | tail -5" \
|
||||||
|
|| die "Frontend build failed"
|
||||||
|
|
||||||
|
log "Cross-compiling wzp-desktop.exe ($RUST_TARGET) via cargo-xwin..."
|
||||||
|
# XWIN_ACCEPT_LICENSE=1 is required by recent cargo-xwin for headless
|
||||||
|
# runs; --cross-compiler clang-cl picks the system clang shipped by the
|
||||||
|
# apt install step in do_prepare.
|
||||||
|
ssh_cmd "source \$HOME/.cargo/env && \
|
||||||
|
export XWIN_ACCEPT_LICENSE=1 && \
|
||||||
|
cd /root/wzp-build/desktop/src-tauri && \
|
||||||
|
cargo xwin build --release --target $RUST_TARGET --bin wzp-desktop 2>&1 | tail -30" \
|
||||||
|
|| die "Windows cross-compile failed"
|
||||||
|
|
||||||
|
ssh_cmd "[ -f /root/wzp-build/target/$RUST_TARGET/release/wzp-desktop.exe ]" \
|
||||||
|
|| die "wzp-desktop.exe not found after build"
|
||||||
|
|
||||||
|
local exe_size
|
||||||
|
exe_size=$(ssh_cmd "du -h /root/wzp-build/target/$RUST_TARGET/release/wzp-desktop.exe | cut -f1")
|
||||||
|
echo " .exe: $exe_size"
|
||||||
|
|
||||||
|
local git_hash
|
||||||
|
git_hash=$(ssh_cmd "cd /root/wzp-build && git rev-parse --short HEAD")
|
||||||
|
notify "WZP Windows build OK [$git_hash] ($exe_size)"
|
||||||
|
export WZP_BUILD_GIT_HASH="$git_hash"
|
||||||
|
export WZP_BUILD_SIZE="$exe_size"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --transfer: Download the .exe to local machine
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_transfer() {
|
||||||
|
log "Downloading wzp-desktop.exe..."
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
scp_down "/root/wzp-build/target/$RUST_TARGET/release/wzp-desktop.exe" "$OUTPUT_DIR/wzp-desktop.exe"
|
||||||
|
local local_size
|
||||||
|
local_size=$(du -h "$OUTPUT_DIR/wzp-desktop.exe" | cut -f1)
|
||||||
|
echo " $OUTPUT_DIR/wzp-desktop.exe ($local_size)"
|
||||||
|
|
||||||
|
# Upload to online rustypaste and notify with the URL.
|
||||||
|
log "Uploading to rustypaste..."
|
||||||
|
local url
|
||||||
|
url=$(rustypaste_upload "$OUTPUT_DIR/wzp-desktop.exe" || echo "")
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
echo " $url"
|
||||||
|
local hash="${WZP_BUILD_GIT_HASH:-?}"
|
||||||
|
notify "WZP Windows build ready [$hash] ($local_size)
|
||||||
|
$url"
|
||||||
|
else
|
||||||
|
echo " (rustypaste upload skipped — no creds in $RUSTY_ENV_FILE)"
|
||||||
|
notify "WZP Windows build transferred ($local_size) — rustypaste upload skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Transfer complete!"
|
||||||
|
echo ""
|
||||||
|
echo " Copy to a real Windows x86_64 host and double-click to run."
|
||||||
|
echo " WebView2 runtime is required on Windows 10 (ships with Win 11)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# --destroy: Delete the VM
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_destroy() {
|
||||||
|
local name
|
||||||
|
name=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true)
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
echo "No VM to destroy."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log "Deleting VM: $name"
|
||||||
|
hcloud server delete "$name"
|
||||||
|
echo " Done."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Full build: create → build → transfer → destroy
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
do_full() {
|
||||||
|
trap 'err "Build failed!"; [ "${KEEP_VM:-0}" = "1" ] || do_destroy_quiet; exit 1' ERR
|
||||||
|
|
||||||
|
do_prepare
|
||||||
|
do_build
|
||||||
|
do_transfer
|
||||||
|
|
||||||
|
if [ "$KEEP_VM" = "1" ]; then
|
||||||
|
log "VM kept alive (WZP_KEEP_VM=1). Destroy with: $0 --destroy"
|
||||||
|
else
|
||||||
|
do_destroy
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "All done!"
|
||||||
|
echo ""
|
||||||
|
echo " ┌────────────────────────────────────────────────┐"
|
||||||
|
echo " │ Windows .exe: $OUTPUT_DIR/wzp-desktop.exe"
|
||||||
|
echo " │"
|
||||||
|
echo " │ Transfer to a Windows x86_64 machine and run."
|
||||||
|
echo " └────────────────────────────────────────────────┘"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
--prepare) do_prepare ;;
|
||||||
|
--build) do_build ;;
|
||||||
|
--transfer) do_transfer ;;
|
||||||
|
--destroy) do_destroy ;;
|
||||||
|
--upload) do_upload ;;
|
||||||
|
--all)
|
||||||
|
do_prepare
|
||||||
|
do_build
|
||||||
|
do_transfer
|
||||||
|
log "VM still running. Destroy with: $0 --destroy"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
do_full
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [--prepare|--build|--transfer|--destroy|--all|--upload]"
|
||||||
|
echo ""
|
||||||
|
echo " (no args) Full build: create VM → build → download → destroy VM"
|
||||||
|
echo " --prepare Create VM and install deps"
|
||||||
|
echo " --build Build on existing VM"
|
||||||
|
echo " --transfer Download .exe from VM"
|
||||||
|
echo " --destroy Delete the VM"
|
||||||
|
echo " --all prepare + build + transfer (VM persists)"
|
||||||
|
echo " --upload Re-upload source to existing VM"
|
||||||
|
echo ""
|
||||||
|
echo "Environment:"
|
||||||
|
echo " WZP_BRANCH=$BRANCH"
|
||||||
|
echo " WZP_SERVER_TYPE=$SERVER_TYPE"
|
||||||
|
echo " WZP_KEEP_VM=$KEEP_VM (set to 1 to skip auto-destroy)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
241
scripts/build-windows-docker.sh
Executable file
241
scripts/build-windows-docker.sh
Executable file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WZ Phone — Windows x86_64 cross-compile (Docker on SepehrHomeserverdk)
|
||||||
|
#
|
||||||
|
# Cross-compiles the Tauri desktop binary for Windows via `cargo xwin`
|
||||||
|
# inside the wzp-windows-builder Docker image on SepehrHomeserverdk.
|
||||||
|
# Uploads the resulting .exe to rustypaste, fires ntfy.sh/wzp notifications
|
||||||
|
# at start + finish, and SCPs the .exe back locally.
|
||||||
|
#
|
||||||
|
# Same pattern as build-tauri-android.sh but for the Windows cross-compile
|
||||||
|
# pipeline:
|
||||||
|
# - Source: desktop/src-tauri/
|
||||||
|
# - Build: cargo xwin build --release --target x86_64-pc-windows-msvc
|
||||||
|
# - Output: target/x86_64-pc-windows-msvc/release/wzp-desktop.exe
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/build-windows-docker.sh # full pipeline
|
||||||
|
# ./scripts/build-windows-docker.sh --no-pull # skip git fetch
|
||||||
|
# ./scripts/build-windows-docker.sh --rust # force-clean rust target
|
||||||
|
# ./scripts/build-windows-docker.sh --image-build # (re)build the docker image
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
REMOTE_HOST="SepehrHomeserverdk"
|
||||||
|
BASE_DIR="/mnt/storage/manBuilder"
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
LOCAL_OUTPUT="target/windows-exe"
|
||||||
|
BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}"
|
||||||
|
SSH_OPTS="-o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o LogLevel=ERROR"
|
||||||
|
|
||||||
|
REBUILD_RUST=0
|
||||||
|
DO_PULL=1
|
||||||
|
IMAGE_BUILD=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--rust) REBUILD_RUST=1 ;;
|
||||||
|
--pull) DO_PULL=1 ;;
|
||||||
|
--no-pull) DO_PULL=0 ;;
|
||||||
|
--image-build) IMAGE_BUILD=1 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '3,27p' "$0"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
log() { echo -e "\033[1;36m>>> $*\033[0m"; }
|
||||||
|
ssh_cmd() { ssh -A $SSH_OPTS "$REMOTE_HOST" "$@"; }
|
||||||
|
|
||||||
|
notify_local() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
mkdir -p "$LOCAL_OUTPUT"
|
||||||
|
|
||||||
|
# ─── Optional: (re)build the docker image on the remote ────────────────────
|
||||||
|
# Runs once, whenever the Dockerfile changes. Fire-and-forget so the local
|
||||||
|
# script doesn't wait for the ~15 minute image build.
|
||||||
|
if [ "$IMAGE_BUILD" = "1" ]; then
|
||||||
|
log "Uploading Dockerfile.windows-builder to remote..."
|
||||||
|
scp $SSH_OPTS "$(dirname "$0")/Dockerfile.windows-builder" \
|
||||||
|
"$REMOTE_HOST:$BASE_DIR/Dockerfile.windows-builder"
|
||||||
|
|
||||||
|
log "Triggering remote image build (fire-and-forget)..."
|
||||||
|
ssh_cmd "cd $BASE_DIR && \
|
||||||
|
nohup docker build --pull -f Dockerfile.windows-builder \
|
||||||
|
-t wzp-windows-builder . \
|
||||||
|
> /tmp/wzp-windows-image-build.log 2>&1 & \
|
||||||
|
echo 'image build PID: '\$!"
|
||||||
|
notify_local "WZP Windows image build dispatched (check /tmp/wzp-windows-image-build.log on remote)"
|
||||||
|
log "Image build running in background on $REMOTE_HOST."
|
||||||
|
log "Tail the log with: ssh $REMOTE_HOST 'tail -f /tmp/wzp-windows-image-build.log'"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Upload remote build runner script ─────────────────────────────────────
|
||||||
|
log "Uploading remote build script..."
|
||||||
|
ssh_cmd "cat > /tmp/wzp-windows-build.sh" <<'REMOTE_SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_DIR="/mnt/storage/manBuilder"
|
||||||
|
NTFY_TOPIC="https://ntfy.sh/wzp"
|
||||||
|
BRANCH="${1:-feat/desktop-audio-rewrite}"
|
||||||
|
DO_PULL="${2:-1}"
|
||||||
|
REBUILD_RUST="${3:-0}"
|
||||||
|
|
||||||
|
LOG_FILE=/tmp/wzp-windows-build.log
|
||||||
|
GIT_HASH="unknown"
|
||||||
|
ENV_FILE="$BASE_DIR/.env"
|
||||||
|
|
||||||
|
notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; }
|
||||||
|
|
||||||
|
# Upload to rustypaste; print URL on stdout (or empty on failure).
|
||||||
|
upload_to_rustypaste() {
|
||||||
|
local file="$1"
|
||||||
|
[ ! -f "$ENV_FILE" ] && { echo ""; return; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then
|
||||||
|
curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
on_error() {
|
||||||
|
local line="$1"
|
||||||
|
local log_url
|
||||||
|
log_url=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||||
|
if [ -n "$log_url" ]; then
|
||||||
|
notify "WZP Windows build FAILED [$GIT_HASH] (line $line)
|
||||||
|
log: $log_url"
|
||||||
|
else
|
||||||
|
notify "WZP Windows build FAILED [$GIT_HASH] (line $line) — log upload failed, see $LOG_FILE on remote"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap 'on_error $LINENO' ERR
|
||||||
|
|
||||||
|
exec > >(tee "$LOG_FILE") 2>&1
|
||||||
|
|
||||||
|
# ── git fetch + reset the target branch ───────────────────────────────────
|
||||||
|
if [ "$DO_PULL" = "1" ]; then
|
||||||
|
echo ">>> git fetch + reset $BRANCH"
|
||||||
|
cd "$BASE_DIR/data/source"
|
||||||
|
git reset --hard HEAD 2>/dev/null || true
|
||||||
|
git gc --prune=now 2>/dev/null || true
|
||||||
|
git fetch origin "$BRANCH" 2>&1 | tail -3
|
||||||
|
git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH"
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
git submodule update --init --recursive || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
GIT_HASH=$(cd "$BASE_DIR/data/source" && git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
|
GIT_MSG=$(cd "$BASE_DIR/data/source" && git log -1 --pretty=%s 2>/dev/null | head -c 60 || echo "?")
|
||||||
|
notify "WZP Windows build STARTED [$GIT_HASH] — $GIT_MSG"
|
||||||
|
|
||||||
|
# Fix perms so builder uid 1000 can read/write the mounted source.
|
||||||
|
find "$BASE_DIR/data/source" "$BASE_DIR/data/cache" \
|
||||||
|
! -user 1000 -o ! -group 1000 2>/dev/null | \
|
||||||
|
xargs -r chown 1000:1000 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$REBUILD_RUST" = "1" ]; then
|
||||||
|
echo ">>> Cleaning Rust windows target dir..."
|
||||||
|
rm -rf "$BASE_DIR/data/cache/target-windows/x86_64-pc-windows-msvc" \
|
||||||
|
"$BASE_DIR/data/cache/target-windows/release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Docker run ─────────────────────────────────────────────────────────────
|
||||||
|
# Cached volumes:
|
||||||
|
# - cargo-registry / cargo-git: shared with the android builder — both use
|
||||||
|
# the same crates, so the download cache is worth sharing.
|
||||||
|
# - target-windows: the Windows target tree. Kept separate from the android
|
||||||
|
# target-cache so the two pipelines don't stomp on each other's build
|
||||||
|
# artefacts (different triples, but the workspace root target dir has
|
||||||
|
# shared subdirs like release/build/ that can get confused).
|
||||||
|
# - cargo-xwin cache is BAKED into the docker image, no volume needed.
|
||||||
|
|
||||||
|
mkdir -p "$BASE_DIR/data/cache/cargo-registry" \
|
||||||
|
"$BASE_DIR/data/cache/cargo-git" \
|
||||||
|
"$BASE_DIR/data/cache/target-windows"
|
||||||
|
chown -R 1000:1000 "$BASE_DIR/data/cache/target-windows" 2>/dev/null || true
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--user 1000:1000 \
|
||||||
|
-v "$BASE_DIR/data/source:/build/source" \
|
||||||
|
-v "$BASE_DIR/data/cache/cargo-registry:/home/builder/.cargo/registry" \
|
||||||
|
-v "$BASE_DIR/data/cache/cargo-git:/home/builder/.cargo/git" \
|
||||||
|
-v "$BASE_DIR/data/cache/target-windows:/build/source/target" \
|
||||||
|
wzp-windows-builder \
|
||||||
|
bash -c '
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# (SSE4.1 / SSSE3 toolchain patch for libopus is baked into the image
|
||||||
|
# during the xwin pre-warm — see Dockerfile.windows-builder. No runtime
|
||||||
|
# patching needed.)
|
||||||
|
|
||||||
|
cd /build/source/desktop
|
||||||
|
|
||||||
|
echo ">>> npm install"
|
||||||
|
npm install --silent 2>&1 | tail -5 || npm install 2>&1 | tail -20
|
||||||
|
|
||||||
|
echo ">>> npm run build"
|
||||||
|
npm run build 2>&1 | tail -5
|
||||||
|
|
||||||
|
echo ">>> cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop"
|
||||||
|
cd src-tauri
|
||||||
|
cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop 2>&1 | tail -50
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ">>> Build artifacts:"
|
||||||
|
ls -lh /build/source/target/x86_64-pc-windows-msvc/release/wzp-desktop.exe 2>/dev/null || echo "NO EXE"
|
||||||
|
'
|
||||||
|
|
||||||
|
# Locate the produced .exe
|
||||||
|
EXE="$BASE_DIR/data/cache/target-windows/x86_64-pc-windows-msvc/release/wzp-desktop.exe"
|
||||||
|
if [ ! -f "$EXE" ]; then
|
||||||
|
LOG_URL=$(upload_to_rustypaste "$LOG_FILE" || echo "")
|
||||||
|
if [ -n "$LOG_URL" ]; then
|
||||||
|
notify "WZP Windows build [$GIT_HASH]: no .exe produced
|
||||||
|
log: $LOG_URL"
|
||||||
|
else
|
||||||
|
notify "WZP Windows build [$GIT_HASH]: no .exe produced — log upload failed"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXE_SIZE=$(du -h "$EXE" | cut -f1)
|
||||||
|
|
||||||
|
RUSTY_URL=$(upload_to_rustypaste "$EXE" || echo "")
|
||||||
|
if [ -n "$RUSTY_URL" ]; then
|
||||||
|
notify "WZP Windows build OK [$GIT_HASH] ($EXE_SIZE)
|
||||||
|
$RUSTY_URL"
|
||||||
|
else
|
||||||
|
notify "WZP Windows build OK [$GIT_HASH] ($EXE_SIZE) — rustypaste upload skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print path so the local script can scp it back
|
||||||
|
echo "EXE_REMOTE_PATH=$EXE"
|
||||||
|
REMOTE_SCRIPT
|
||||||
|
|
||||||
|
ssh_cmd "chmod +x /tmp/wzp-windows-build.sh"
|
||||||
|
|
||||||
|
notify_local "WZP Windows build dispatched (branch=$BRANCH)"
|
||||||
|
log "Triggering remote build (branch=$BRANCH)..."
|
||||||
|
|
||||||
|
# Run; last line is EXE_REMOTE_PATH=...
|
||||||
|
REMOTE_OUTPUT=$(ssh_cmd "/tmp/wzp-windows-build.sh '$BRANCH' '$DO_PULL' '$REBUILD_RUST'" || true)
|
||||||
|
echo "$REMOTE_OUTPUT" | tail -60
|
||||||
|
|
||||||
|
EXE_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^EXE_REMOTE_PATH=' | tail -1 | cut -d= -f2-)
|
||||||
|
if [ -n "$EXE_REMOTE" ]; then
|
||||||
|
log "Downloading wzp-desktop.exe to $LOCAL_OUTPUT/..."
|
||||||
|
scp $SSH_OPTS "$REMOTE_HOST:$EXE_REMOTE" "$LOCAL_OUTPUT/wzp-desktop.exe"
|
||||||
|
echo " $LOCAL_OUTPUT/wzp-desktop.exe ($(du -h "$LOCAL_OUTPUT/wzp-desktop.exe" | cut -f1))"
|
||||||
|
else
|
||||||
|
log "No .exe produced — see ntfy / remote log /tmp/wzp-windows-build.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
72
vendor/audiopus_sys/.github/workflows/ci.yml
vendored
Normal file
72
vendor/audiopus_sys/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
name:
|
||||||
|
- stable
|
||||||
|
- beta
|
||||||
|
- nightly
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
|
||||||
|
include:
|
||||||
|
- name: beta
|
||||||
|
toolchain: beta
|
||||||
|
- name: nightly
|
||||||
|
toolchain: nightly
|
||||||
|
- name: macOS
|
||||||
|
os: macOS-latest
|
||||||
|
- name: Windows
|
||||||
|
os: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Install toolchain
|
||||||
|
id: tc
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.toolchain || 'stable' }}
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libopus-dev
|
||||||
|
|
||||||
|
- name: Setup cache
|
||||||
|
if: runner.os != 'macOS'
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ matrix.os }}-test-${{ steps.tc.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.toml') }}
|
||||||
|
|
||||||
|
- name: Build static
|
||||||
|
run: cargo build --features "static"
|
||||||
|
|
||||||
|
- name: Build dynamic
|
||||||
|
run: cargo build --features "dynamic"
|
||||||
|
|
||||||
|
# TODO: Fix for CI environment.
|
||||||
|
#- name: Generate bindings
|
||||||
|
# run: cargo build --features "generate_binding"
|
||||||
|
|
||||||
|
- name: Test all features
|
||||||
|
# TODO: Once "generate_binding" is fixed, replace with `--all-features`
|
||||||
|
# again.
|
||||||
|
run: cargo test --features "static dynamic"
|
||||||
3
vendor/audiopus_sys/.gitignore
vendored
Normal file
3
vendor/audiopus_sys/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
|
Cargo.lock
|
||||||
60
vendor/audiopus_sys/CHANGELOG.md
vendored
Normal file
60
vendor/audiopus_sys/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
An overview of changes:
|
||||||
|
|
||||||
|
## [0.2.0]
|
||||||
|
|
||||||
|
* Now requires `cmake`.
|
||||||
|
* Windows will build via `cmake` too.
|
||||||
|
* Windows pre-built binaries have been removed.
|
||||||
|
* Updated `bindgen` to version `0.58`.
|
||||||
|
|
||||||
|
## [0.1.8]
|
||||||
|
|
||||||
|
This release adds build support for FreeBSD.
|
||||||
|
|
||||||
|
## [0.1.7]
|
||||||
|
|
||||||
|
Add missing `opus`-folder.
|
||||||
|
|
||||||
|
## [0.1.6]
|
||||||
|
|
||||||
|
This release removes the `bindgen`-dependency from the default features.
|
||||||
|
Additionally, the `bindgen`-feature has been added in order to generate a new binding.
|
||||||
|
|
||||||
|
## [0.1.4 and 0.1.5]
|
||||||
|
|
||||||
|
v0.1.4:
|
||||||
|
This release fixes a problem where `audiopus_sys` could not find the
|
||||||
|
Opus folder.
|
||||||
|
|
||||||
|
v0.1.5:
|
||||||
|
Convert Unix-relevant files' EOLs from CRLF to LF inside the opus-folder.
|
||||||
|
|
||||||
|
### **Fix**
|
||||||
|
* Bundle the Opus project again.
|
||||||
|
* Added missing `cfg` on `find_via_pkg_config`.
|
||||||
|
|
||||||
|
## [0.1.3]
|
||||||
|
|
||||||
|
Fixes build-issues related to `pkg-config`.
|
||||||
|
|
||||||
|
## [0.1.2]
|
||||||
|
|
||||||
|
This release adds the ability to bypass `pkg-config`.
|
||||||
|
|
||||||
|
### **Added:**
|
||||||
|
|
||||||
|
* Ignore `pkg-config` when `LIBOPUS_NO_PKG` or `OPUS_NO_PKG` is set.
|
||||||
|
* Print the dynamic/static build cause via `cargo:info`.
|
||||||
|
* Add missing repository-link in `Cargo.toml`.
|
||||||
|
|
||||||
|
## [0.1.1]
|
||||||
|
|
||||||
|
### **Added:**
|
||||||
|
|
||||||
|
* Copy Opus' source to `OUT_DIR` before building to avoid modifying and generating files outside of `OUT_DIR`.
|
||||||
|
|
||||||
|
### **Fixed:**
|
||||||
|
* Convert Unix-relevant files' EOLs from `CRLF` to `LF` inside the `opus`-folder.
|
||||||
|
* Resolve unused import warnings when building with Unix.
|
||||||
62
vendor/audiopus_sys/CONTRIBUTING.md
vendored
Normal file
62
vendor/audiopus_sys/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Everyone is welcome to get involved, may it be a pull request, suggestion, bug
|
||||||
|
report, or a textual improvement! : )
|
||||||
|
|
||||||
|
The language applied in this repository is British English.
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
Contributions to `audiopus_sys` should be first discussed up via an issue and then
|
||||||
|
implemented via pull request.
|
||||||
|
Issues display development-plans or required brainstorming, feel free to ask,
|
||||||
|
suggest, and discuss!
|
||||||
|
The `master`-branch contains the latest release.
|
||||||
|
|
||||||
|
## Comments & Documentation Style
|
||||||
|
|
||||||
|
- Comments are placed the lines before the related code line, not on the same
|
||||||
|
line.
|
||||||
|
|
||||||
|
- Write full sentences in British English.
|
||||||
|
|
||||||
|
- `unsafe` must always be reasoned and their soundness must be proven via a
|
||||||
|
comment.
|
||||||
|
|
||||||
|
- Use Rust intra-doc-links paths to refer Rust items in documentation:
|
||||||
|
`[name](crate::module::struct::method)`.
|
||||||
|
|
||||||
|
- If code ends up difficult, try to simplify it, if unavoidable, explain code
|
||||||
|
with comments. Prefer explicit variable naming instead of abbreviations.
|
||||||
|
|
||||||
|
## Commit Style
|
||||||
|
|
||||||
|
Write full sentences in British English.
|
||||||
|
|
||||||
|
Commits should describe the action being peformed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- *Fix deadlock for events.*
|
||||||
|
- *Correct grammar in `command`-example.*
|
||||||
|
|
||||||
|
## Pull Request Checklist
|
||||||
|
|
||||||
|
- Make sure to open an issue prior working on a problem or ask on existing
|
||||||
|
issue be assigned.
|
||||||
|
|
||||||
|
- If a pull requests breaks the current API, use the `breaking-changes`-branch,
|
||||||
|
otherwise `stable-changes`.
|
||||||
|
|
||||||
|
- Commits shall be as small as possible, compile, and pass all tests.
|
||||||
|
|
||||||
|
- Make sure your code is formatted with `rustfmt` and free of lints,
|
||||||
|
run `cargo fmt` and `cargo clippy`.
|
||||||
|
|
||||||
|
- If you fixed a bug, add a test for that bug. Unit tests belong inside the
|
||||||
|
same file's `mod` named `tests`, integrational tests belong inside the
|
||||||
|
`tests`-folder.
|
||||||
|
|
||||||
|
- Last but not least, make sure your planned pull request merges cleanly,
|
||||||
|
if it does not, rebase your changes.
|
||||||
|
|
||||||
|
If you have any questions left, please reach out via the issue system : )
|
||||||
44
vendor/audiopus_sys/Cargo.toml
vendored
Normal file
44
vendor/audiopus_sys/Cargo.toml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies
|
||||||
|
#
|
||||||
|
# If you believe there's an error in this file please file an
|
||||||
|
# issue against the rust-lang/cargo repository. If you're
|
||||||
|
# editing this file be aware that the upstream Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable)
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2018"
|
||||||
|
name = "audiopus_sys"
|
||||||
|
version = "0.2.2"
|
||||||
|
authors = ["Lakelezz <lakelezz@protonmail.ch>"]
|
||||||
|
description = "FFI-Binding to Opus, dynamically or statically linked for Windows and UNIX."
|
||||||
|
documentation = "https://docs.rs/audiopus_sys"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["audio", "opus", "codec"]
|
||||||
|
categories = ["api-bindings", "compression", "encoding", "multimedia::audio", "multimedia::encoding"]
|
||||||
|
license = "ISC"
|
||||||
|
repository = "https://github.com/lakelezz/audiopus_sys.git"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
[build-dependencies.bindgen]
|
||||||
|
version = "0.58"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[build-dependencies.cmake]
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
[build-dependencies.log]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[build-dependencies.pkg-config]
|
||||||
|
version = "0.3"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
dynamic = []
|
||||||
|
generate_binding = ["bindgen"]
|
||||||
|
static = []
|
||||||
30
vendor/audiopus_sys/Cargo.toml.orig
generated
vendored
Normal file
30
vendor/audiopus_sys/Cargo.toml.orig
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "audiopus_sys"
|
||||||
|
version = "0.2.2"
|
||||||
|
license = "ISC"
|
||||||
|
repository = "https://github.com/lakelezz/audiopus_sys.git"
|
||||||
|
authors = ["Lakelezz <lakelezz@protonmail.ch>"]
|
||||||
|
keywords = ["audio", "opus", "codec"]
|
||||||
|
categories = ["api-bindings", "compression", "encoding",
|
||||||
|
"multimedia::audio", "multimedia::encoding"]
|
||||||
|
description = "FFI-Binding to Opus, dynamically or statically linked for Windows and UNIX."
|
||||||
|
readme = "README.md"
|
||||||
|
documentation = "https://docs.rs/audiopus_sys"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
log = "0.4"
|
||||||
|
pkg-config = "0.3"
|
||||||
|
cmake = "0.1"
|
||||||
|
|
||||||
|
[build-dependencies.bindgen]
|
||||||
|
version = "0.58"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
dynamic = []
|
||||||
|
static = []
|
||||||
|
generate_binding = ["bindgen"]
|
||||||
15
vendor/audiopus_sys/LICENSE.md
vendored
Normal file
15
vendor/audiopus_sys/LICENSE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2019, Lakelezz
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
81
vendor/audiopus_sys/README.md
vendored
Normal file
81
vendor/audiopus_sys/README.md
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
[![ci-badge][]][ci] [![docs-badge][]][docs] [![rust version badge]][rust version link] [![crates.io version]][crates.io link]
|
||||||
|
|
||||||
|
# About
|
||||||
|
|
||||||
|
`audiopus_sys` is an FFI-Rust-binding to [`Opus`] version 1.3.
|
||||||
|
|
||||||
|
Orginally, this sys-crate was made to empower the [`serenity`]-crate to build audio features on Windows, Linux, and Mac. However, it's not limited to that.
|
||||||
|
|
||||||
|
Everyone is welcome to contribute,
|
||||||
|
check out the [`CONTRIBUTING.md`](CONTRIBUTING.md) for further guidance.
|
||||||
|
|
||||||
|
# Building
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
If you want to build Opus, you will need `cmake`.
|
||||||
|
|
||||||
|
If you have `pkg-config`, it will attempt to use that before building.
|
||||||
|
|
||||||
|
You can also link a pre-installed Opus, see [**Pre-installed Opus**](#Pre-installed-Opus)
|
||||||
|
below.
|
||||||
|
|
||||||
|
This crate provides a pre-built binding. In case you want to generate the
|
||||||
|
binding yourself, you will need [`Clang`](https://rust-lang.github.io/rust-bindgen/requirements.html#clang),
|
||||||
|
see [**Pre-installed Opus**](#Generating-The-Binding) below for further
|
||||||
|
instructions.
|
||||||
|
|
||||||
|
## Linking
|
||||||
|
`audiopus_sys` links to Opus 1.3 and supports Windows, Linux, and MacOS
|
||||||
|
By default, we statically link to Windows, MacOS, and if you use the
|
||||||
|
`musl`-environment. We will link dynamically for Linux except when using
|
||||||
|
mentioned `musl`.
|
||||||
|
|
||||||
|
This can be altered by compiling with the `static` or `dynamic` feature having
|
||||||
|
effects respective to their names. If both features are enabled,
|
||||||
|
we will pick your system's default.
|
||||||
|
|
||||||
|
Environment variables named `LIBOPUS_STATIC` or `OPUS_STATIC` will take
|
||||||
|
precedence over features thus overriding the behaviour. The value of these
|
||||||
|
environment variables have no influence of the result: If one of them is set,
|
||||||
|
statically linking will be picked.
|
||||||
|
|
||||||
|
## Pkg-Config
|
||||||
|
By default, `audiopus_sys` will use `pkg-config` on Unix or GNU.
|
||||||
|
Setting the environment variable `LIBOPUS_NO_PKG` or `OPUS_NO_PKG` will bypass
|
||||||
|
probing for Opus via `pkg-config`.
|
||||||
|
|
||||||
|
## Pre-installed Opus
|
||||||
|
If you have Opus pre-installed, you can set `LIBOPUS_LIB_DIR` or
|
||||||
|
`OPUS_LIB_DIR` to the directory containing Opus.
|
||||||
|
|
||||||
|
Be aware that using an Opus other than version 1.3 may not work.
|
||||||
|
|
||||||
|
# Generating The Binding
|
||||||
|
If you want to generate the binding yourself, you can use the
|
||||||
|
`generate_binding`-feature.
|
||||||
|
|
||||||
|
Be aware, `bindgen` requires Clang and its `LIBCLANG_PATH`
|
||||||
|
environment variable to be specified.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
audiopus_sys = "0.2"
|
||||||
|
```
|
||||||
|
[`serenity`]: https://crates.io/crates/serenity
|
||||||
|
|
||||||
|
[`Opus`]: https://www.opus-codec.org/
|
||||||
|
|
||||||
|
[ci-badge]: https://img.shields.io/github/workflow/status/Lakelezz/audiopus_sys/CI?style=flat-square
|
||||||
|
[ci]: https://github.com/Lakelezz/audiopus_sys/actions
|
||||||
|
|
||||||
|
[docs-badge]: https://img.shields.io/badge/docs-online-5023dd.svg?style=flat-square&colorB=32b6b7
|
||||||
|
[docs]: https://docs.rs/audiopus_sys
|
||||||
|
|
||||||
|
[rust version badge]: https://img.shields.io/badge/rust-1.51+-93450a.svg?style=flat-square&colorB=ff9a0d
|
||||||
|
[rust version link]: https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html
|
||||||
|
|
||||||
|
[crates.io link]: https://crates.io/crates/audiopus_sys
|
||||||
|
[crates.io version]: https://img.shields.io/crates/v/audiopus_sys.svg?style=flat-square&colorB=b73732
|
||||||
149
vendor/audiopus_sys/build.rs
vendored
Normal file
149
vendor/audiopus_sys/build.rs
vendored
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#![deny(rust_2018_idioms)]
|
||||||
|
|
||||||
|
#[cfg(feature = "generate_binding")]
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, fmt::Display, path::Path};
|
||||||
|
|
||||||
|
/// Outputs the library-file's prefix as word usable for actual arguments on
|
||||||
|
/// commands or paths.
|
||||||
|
const fn rustc_linking_word(is_static_link: bool) -> &'static str {
|
||||||
|
if is_static_link {
|
||||||
|
"static"
|
||||||
|
} else {
|
||||||
|
"dylib"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a new binding at `src/lib.rs` using `src/wrapper.h`.
|
||||||
|
#[cfg(feature = "generate_binding")]
|
||||||
|
fn generate_binding() {
|
||||||
|
const ALLOW_UNCONVENTIONALS: &'static str = "#![allow(non_upper_case_globals)]\n\
|
||||||
|
#![allow(non_camel_case_types)]\n\
|
||||||
|
#![allow(non_snake_case)]\n";
|
||||||
|
|
||||||
|
let bindings = bindgen::Builder::default()
|
||||||
|
.header("src/wrapper.h")
|
||||||
|
.raw_line(ALLOW_UNCONVENTIONALS)
|
||||||
|
.generate()
|
||||||
|
.expect("Unable to generate binding");
|
||||||
|
|
||||||
|
let binding_target_path = PathBuf::new().join("src").join("lib.rs");
|
||||||
|
|
||||||
|
bindings
|
||||||
|
.write_to_file(binding_target_path)
|
||||||
|
.expect("Could not write binding to the file at `src/lib.rs`");
|
||||||
|
|
||||||
|
println!("cargo:info=Successfully generated binding.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_opus(is_static: bool) {
|
||||||
|
let opus_path = Path::new("opus");
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:info=Opus source path used: {:?}.",
|
||||||
|
opus_path
|
||||||
|
.canonicalize()
|
||||||
|
.expect("Could not canonicalise to absolute path")
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("cargo:info=Building Opus via CMake.");
|
||||||
|
let opus_build_dir = cmake::build(opus_path);
|
||||||
|
link_opus(is_static, opus_build_dir.display())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn link_opus(is_static: bool, opus_build_dir: impl Display) {
|
||||||
|
let is_static_text = rustc_linking_word(is_static);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:info=Linking Opus as {} lib: {}",
|
||||||
|
is_static_text, opus_build_dir
|
||||||
|
);
|
||||||
|
println!("cargo:rustc-link-lib={}=opus", is_static_text);
|
||||||
|
println!("cargo:rustc-link-search=native={}/lib", opus_build_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_env = "gnu"))]
|
||||||
|
fn find_via_pkg_config(is_static: bool) -> bool {
|
||||||
|
pkg_config::Config::new()
|
||||||
|
.statik(is_static)
|
||||||
|
.probe("opus")
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Based on the OS or target environment we are building for,
|
||||||
|
/// this function will return an expected default library linking method.
|
||||||
|
///
|
||||||
|
/// If we build for Windows, MacOS, or Linux with musl, we will link statically.
|
||||||
|
/// However, if you build for Linux without musl, we will link dynamically.
|
||||||
|
///
|
||||||
|
/// **Info**:
|
||||||
|
/// This is a helper-function and may not be called if
|
||||||
|
/// if the `static`-feature is enabled, the environment variable
|
||||||
|
/// `LIBOPUS_STATIC` or `OPUS_STATIC` is set.
|
||||||
|
fn default_library_linking() -> bool {
|
||||||
|
#[cfg(any(windows, target_os = "macos", target_env = "musl"))]
|
||||||
|
{
|
||||||
|
true
|
||||||
|
}
|
||||||
|
#[cfg(any(target_os = "freebsd", all(unix, target_env = "gnu")))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_installed_opus() -> Option<String> {
|
||||||
|
if let Ok(lib_directory) = env::var("LIBOPUS_LIB_DIR") {
|
||||||
|
Some(lib_directory)
|
||||||
|
} else if let Ok(lib_directory) = env::var("OPUS_LIB_DIR") {
|
||||||
|
Some(lib_directory)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_static_build() -> bool {
|
||||||
|
if cfg!(feature = "static") && cfg!(feature = "dynamic") {
|
||||||
|
default_library_linking()
|
||||||
|
} else if cfg!(feature = "static")
|
||||||
|
|| env::var("LIBOPUS_STATIC").is_ok()
|
||||||
|
|| env::var("OPUS_STATIC").is_ok()
|
||||||
|
{
|
||||||
|
println!("cargo:info=Static feature or environment variable found.");
|
||||||
|
|
||||||
|
true
|
||||||
|
} else if cfg!(feature = "dynamic") {
|
||||||
|
println!("cargo:info=Dynamic feature enabled.");
|
||||||
|
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
println!("cargo:info=No feature or environment variable found, linking by default.");
|
||||||
|
|
||||||
|
default_library_linking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(feature = "generate_binding")]
|
||||||
|
generate_binding();
|
||||||
|
|
||||||
|
let is_static = is_static_build();
|
||||||
|
|
||||||
|
#[cfg(any(unix, target_env = "gnu"))]
|
||||||
|
{
|
||||||
|
if std::env::var("LIBOPUS_NO_PKG").is_ok() || std::env::var("OPUS_NO_PKG").is_ok() {
|
||||||
|
println!("cargo:info=Bypassed `pkg-config`.");
|
||||||
|
} else if find_via_pkg_config(is_static) {
|
||||||
|
println!("cargo:info=Found `Opus` via `pkg_config`.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
println!("cargo:info=`pkg_config` could not find `Opus`.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(installed_opus) = find_installed_opus() {
|
||||||
|
link_opus(is_static, installed_opus);
|
||||||
|
} else {
|
||||||
|
build_opus(is_static);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
vendor/audiopus_sys/opus/.appveyor.yml
vendored
Normal file
37
vendor/audiopus_sys/opus/.appveyor.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
image: Visual Studio 2015
|
||||||
|
configuration:
|
||||||
|
- Debug
|
||||||
|
- DebugDLL
|
||||||
|
- DebugDLL_fixed
|
||||||
|
- Release
|
||||||
|
- ReleaseDLL
|
||||||
|
- ReleaseDLL_fixed
|
||||||
|
|
||||||
|
platform:
|
||||||
|
- Win32
|
||||||
|
- x64
|
||||||
|
|
||||||
|
environment:
|
||||||
|
api_key:
|
||||||
|
secure: kR3Ac0NjGwFnTmXdFrR8d6VXjdk5F7L4F/BilC4nvaM=
|
||||||
|
|
||||||
|
build:
|
||||||
|
project: win32\VS2015\opus.sln
|
||||||
|
parallel: true
|
||||||
|
verbosity: minimal
|
||||||
|
|
||||||
|
after_build:
|
||||||
|
- cd %APPVEYOR_BUILD_FOLDER%
|
||||||
|
- 7z a opus.zip win32\VS2015\%PLATFORM%\%CONFIGURATION%\opus.??? include\*.h
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- cd %APPVEYOR_BUILD_FOLDER%\win32\VS2015\%PLATFORM%\%CONFIGURATION%
|
||||||
|
- test_opus_api.exe
|
||||||
|
- test_opus_decode.exe
|
||||||
|
- test_opus_encode.exe
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
- path: opus.zip
|
||||||
|
|
||||||
|
on_success:
|
||||||
|
- ps: if ($env:api_key -and "$env:configuration/$env:platform" -eq "ReleaseDLL_fixed/x64") { Start-AppveyorBuild -ApiKey $env:api_key -ProjectSlug 'opus-tools' }
|
||||||
10
vendor/audiopus_sys/opus/.gitattributes
vendored
Normal file
10
vendor/audiopus_sys/opus/.gitattributes
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.gitignore export-ignore
|
||||||
|
.gitattributes export-ignore
|
||||||
|
|
||||||
|
update_version export-ignore
|
||||||
|
|
||||||
|
*.bat eol=crlf
|
||||||
|
*.sln eol=crlf
|
||||||
|
*.vcxproj eol=crlf
|
||||||
|
*.vcxproj.filters eol=crlf
|
||||||
|
common.props eol=crlf
|
||||||
90
vendor/audiopus_sys/opus/.gitignore
vendored
Normal file
90
vendor/audiopus_sys/opus/.gitignore
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
Doxyfile
|
||||||
|
Makefile
|
||||||
|
Makefile.in
|
||||||
|
TAGS
|
||||||
|
aclocal.m4
|
||||||
|
autom4te.cache
|
||||||
|
*.kdevelop.pcs
|
||||||
|
*.kdevses
|
||||||
|
compile
|
||||||
|
config.guess
|
||||||
|
config.h
|
||||||
|
config.h.in
|
||||||
|
config.log
|
||||||
|
config.status
|
||||||
|
config.sub
|
||||||
|
configure
|
||||||
|
depcomp
|
||||||
|
INSTALL
|
||||||
|
install-sh
|
||||||
|
.deps
|
||||||
|
.libs
|
||||||
|
.dirstamp
|
||||||
|
*.a
|
||||||
|
*.exe
|
||||||
|
*.la
|
||||||
|
*-gnu.S
|
||||||
|
testcelt
|
||||||
|
libtool
|
||||||
|
ltmain.sh
|
||||||
|
missing
|
||||||
|
m4/libtool.m4
|
||||||
|
m4/ltoptions.m4
|
||||||
|
m4/ltsugar.m4
|
||||||
|
m4/ltversion.m4
|
||||||
|
m4/lt~obsolete.m4
|
||||||
|
opus_compare
|
||||||
|
opus_demo
|
||||||
|
repacketizer_demo
|
||||||
|
stamp-h1
|
||||||
|
test-driver
|
||||||
|
trivial_example
|
||||||
|
*.sw*
|
||||||
|
*.o
|
||||||
|
*.lo
|
||||||
|
*.pc
|
||||||
|
*.tar.gz
|
||||||
|
*~
|
||||||
|
tests/*test
|
||||||
|
tests/test_opus_api
|
||||||
|
tests/test_opus_decode
|
||||||
|
tests/test_opus_encode
|
||||||
|
tests/test_opus_padding
|
||||||
|
tests/test_opus_projection
|
||||||
|
celt/arm/armopts.s
|
||||||
|
celt/dump_modes/dump_modes
|
||||||
|
celt/tests/test_unit_cwrs32
|
||||||
|
celt/tests/test_unit_dft
|
||||||
|
celt/tests/test_unit_entropy
|
||||||
|
celt/tests/test_unit_laplace
|
||||||
|
celt/tests/test_unit_mathops
|
||||||
|
celt/tests/test_unit_mdct
|
||||||
|
celt/tests/test_unit_rotation
|
||||||
|
celt/tests/test_unit_types
|
||||||
|
doc/doxygen_sqlite3.db
|
||||||
|
doc/doxygen-build.stamp
|
||||||
|
doc/html
|
||||||
|
doc/latex
|
||||||
|
doc/man
|
||||||
|
package_version
|
||||||
|
version.h
|
||||||
|
celt/Debug
|
||||||
|
celt/Release
|
||||||
|
celt/x64
|
||||||
|
silk/Debug
|
||||||
|
silk/Release
|
||||||
|
silk/x64
|
||||||
|
silk/fixed/Debug
|
||||||
|
silk/fixed/Release
|
||||||
|
silk/fixed/x64
|
||||||
|
silk/float/Debug
|
||||||
|
silk/float/Release
|
||||||
|
silk/float/x64
|
||||||
|
silk/tests/test_unit_LPC_inv_pred_gain
|
||||||
|
src/Debug
|
||||||
|
src/Release
|
||||||
|
src/x64
|
||||||
|
/*[Bb]uild*/
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
CMakeSettings.json
|
||||||
61
vendor/audiopus_sys/opus/.gitlab-ci.yml
vendored
Normal file
61
vendor/audiopus_sys/opus/.gitlab-ci.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
include:
|
||||||
|
- template: 'Workflows/Branch-Pipelines.gitlab-ci.yml'
|
||||||
|
|
||||||
|
default:
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
# Image from https://hub.docker.com/_/gcc/ based on Debian
|
||||||
|
image: gcc:9
|
||||||
|
|
||||||
|
whitespace:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- git diff-tree --check origin/master HEAD
|
||||||
|
|
||||||
|
autoconf:
|
||||||
|
stage: build
|
||||||
|
before_script:
|
||||||
|
- apt-get update &&
|
||||||
|
apt-get install -y zip doxygen
|
||||||
|
script:
|
||||||
|
- ./autogen.sh
|
||||||
|
- ./configure
|
||||||
|
- make -j4
|
||||||
|
- make distcheck
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- "src/*.o"
|
||||||
|
- "src/.libs/*.o"
|
||||||
|
- "silk/*.o"
|
||||||
|
- "silk/.libs/*.o"
|
||||||
|
- "celt/*.o"
|
||||||
|
- "celt/.libs/*.o"
|
||||||
|
|
||||||
|
cmake:
|
||||||
|
stage: build
|
||||||
|
before_script:
|
||||||
|
- apt-get update &&
|
||||||
|
apt-get install -y cmake ninja-build
|
||||||
|
script:
|
||||||
|
- mkdir build
|
||||||
|
- cmake -S . -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DOPUS_BUILD_TESTING=ON -DOPUS_BUILD_PROGRAMS=ON
|
||||||
|
- cmake --build build
|
||||||
|
- cd build && ctest --output-on-failure
|
||||||
|
|
||||||
|
meson:
|
||||||
|
stage: build
|
||||||
|
before_script:
|
||||||
|
- apt-get update &&
|
||||||
|
apt-get install -y python3-pip ninja-build doxygen
|
||||||
|
- export XDG_CACHE_HOME=$PWD/pip-cache
|
||||||
|
- pip3 install --user meson
|
||||||
|
script:
|
||||||
|
- export PATH=$PATH:$HOME/.local/bin
|
||||||
|
- mkdir builddir
|
||||||
|
- meson setup --werror -Dtests=enabled -Ddocs=enabled -Dbuildtype=release builddir
|
||||||
|
- meson compile -C builddir
|
||||||
|
- meson test -C builddir
|
||||||
|
#- meson dist --no-tests -C builddir
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- 'pip-cache/*'
|
||||||
21
vendor/audiopus_sys/opus/.travis.yml
vendored
Normal file
21
vendor/audiopus_sys/opus/.travis.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
language: c
|
||||||
|
|
||||||
|
compiler:
|
||||||
|
- gcc
|
||||||
|
- clang
|
||||||
|
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
- osx
|
||||||
|
|
||||||
|
env:
|
||||||
|
- CONFIG=""
|
||||||
|
- CONFIG="--enable-assertions"
|
||||||
|
- CONFIG="--enable-fixed-point"
|
||||||
|
- CONFIG="--enable-fixed-point --disable-float-api"
|
||||||
|
- CONFIG="--enable-fixed-point --enable-assertions"
|
||||||
|
|
||||||
|
script:
|
||||||
|
- ./autogen.sh
|
||||||
|
- ./configure $CONFIG
|
||||||
|
- make distcheck
|
||||||
6
vendor/audiopus_sys/opus/AUTHORS
vendored
Normal file
6
vendor/audiopus_sys/opus/AUTHORS
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Jean-Marc Valin (jmvalin@jmvalin.ca)
|
||||||
|
Koen Vos (koenvos74@gmail.com)
|
||||||
|
Timothy Terriberry (tterribe@xiph.org)
|
||||||
|
Karsten Vandborg Sorensen (karsten.vandborg.sorensen@skype.net)
|
||||||
|
Soren Skak Jensen (ssjensen@gn.com)
|
||||||
|
Gregory Maxwell (greg@xiph.org)
|
||||||
643
vendor/audiopus_sys/opus/CMakeLists.txt
vendored
Normal file
643
vendor/audiopus_sys/opus/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.1)
|
||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||||
|
|
||||||
|
include(OpusPackageVersion)
|
||||||
|
get_package_version(PACKAGE_VERSION PROJECT_VERSION)
|
||||||
|
|
||||||
|
project(Opus LANGUAGES C VERSION ${PROJECT_VERSION})
|
||||||
|
|
||||||
|
include(OpusFunctions)
|
||||||
|
include(OpusBuildtype)
|
||||||
|
include(OpusConfig)
|
||||||
|
include(OpusSources)
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
include(CMakeDependentOption)
|
||||||
|
include(FeatureSummary)
|
||||||
|
|
||||||
|
set(OPUS_BUILD_SHARED_LIBRARY_HELP_STR "build shared library.")
|
||||||
|
option(OPUS_BUILD_SHARED_LIBRARY ${OPUS_BUILD_SHARED_LIBRARY_HELP_STR} OFF)
|
||||||
|
if(OPUS_BUILD_SHARED_LIBRARY OR BUILD_SHARED_LIBS OR OPUS_BUILD_FRAMEWORK)
|
||||||
|
# Global flag to cause add_library() to create shared libraries if on.
|
||||||
|
set(BUILD_SHARED_LIBS ON)
|
||||||
|
set(OPUS_BUILD_SHARED_LIBRARY ON)
|
||||||
|
endif()
|
||||||
|
add_feature_info(OPUS_BUILD_SHARED_LIBRARY OPUS_BUILD_SHARED_LIBRARY ${OPUS_BUILD_SHARED_LIBRARY_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_BUILD_TESTING_HELP_STR "build tests.")
|
||||||
|
option(OPUS_BUILD_TESTING ${OPUS_BUILD_TESTING_HELP_STR} OFF)
|
||||||
|
if(OPUS_BUILD_TESTING OR BUILD_TESTING)
|
||||||
|
set(OPUS_BUILD_TESTING ON)
|
||||||
|
set(BUILD_TESTING ON)
|
||||||
|
endif()
|
||||||
|
add_feature_info(OPUS_BUILD_TESTING OPUS_BUILD_TESTING ${OPUS_BUILD_TESTING_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_CUSTOM_MODES_HELP_STR "enable non-Opus modes, e.g. 44.1 kHz & 2^n frames.")
|
||||||
|
option(OPUS_CUSTOM_MODES ${OPUS_CUSTOM_MODES_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_CUSTOM_MODES OPUS_CUSTOM_MODES ${OPUS_CUSTOM_MODES_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_BUILD_PROGRAMS_HELP_STR "build programs.")
|
||||||
|
option(OPUS_BUILD_PROGRAMS ${OPUS_BUILD_PROGRAMS_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_BUILD_PROGRAMS OPUS_BUILD_PROGRAMS ${OPUS_BUILD_PROGRAMS_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_DISABLE_INTRINSICS_HELP_STR "disable all intrinsics optimizations.")
|
||||||
|
option(OPUS_DISABLE_INTRINSICS ${OPUS_DISABLE_INTRINSICS_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_DISABLE_INTRINSICS OPUS_DISABLE_INTRINSICS ${OPUS_DISABLE_INTRINSICS_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_FIXED_POINT_HELP_STR "compile as fixed-point (for machines without a fast enough FPU).")
|
||||||
|
option(OPUS_FIXED_POINT ${OPUS_FIXED_POINT_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_FIXED_POINT OPUS_FIXED_POINT ${OPUS_FIXED_POINT_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_ENABLE_FLOAT_API_HELP_STR "compile with the floating point API (for machines with float library).")
|
||||||
|
option(OPUS_ENABLE_FLOAT_API ${OPUS_ENABLE_FLOAT_API_HELP_STR} ON)
|
||||||
|
add_feature_info(OPUS_ENABLE_FLOAT_API OPUS_ENABLE_FLOAT_API ${OPUS_ENABLE_FLOAT_API_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_FLOAT_APPROX_HELP_STR "enable floating point approximations (Ensure your platform supports IEEE 754 before enabling).")
|
||||||
|
option(OPUS_FLOAT_APPROX ${OPUS_FLOAT_APPROX_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_FLOAT_APPROX OPUS_FLOAT_APPROX ${OPUS_FLOAT_APPROX_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_ASSERTIONS_HELP_STR "additional software error checking.")
|
||||||
|
option(OPUS_ASSERTIONS ${OPUS_ASSERTIONS_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_ASSERTIONS OPUS_ASSERTIONS ${OPUS_ASSERTIONS_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_HARDENING_HELP_STR "run-time checks that are cheap and safe for use in production.")
|
||||||
|
option(OPUS_HARDENING ${OPUS_HARDENING_HELP_STR} ON)
|
||||||
|
add_feature_info(OPUS_HARDENING OPUS_HARDENING ${OPUS_HARDENING_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_FUZZING_HELP_STR "causes the encoder to make random decisions (do not use in production).")
|
||||||
|
option(OPUS_FUZZING ${OPUS_FUZZING_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_FUZZING OPUS_FUZZING ${OPUS_FUZZING_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_CHECK_ASM_HELP_STR "enable bit-exactness checks between optimized and c implementations.")
|
||||||
|
option(OPUS_CHECK_ASM ${OPUS_CHECK_ASM_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_CHECK_ASM OPUS_CHECK_ASM ${OPUS_CHECK_ASM_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR "install pkg-config module.")
|
||||||
|
option(OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR} ON)
|
||||||
|
add_feature_info(OPUS_INSTALL_PKG_CONFIG_MODULE OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR "install CMake package config module.")
|
||||||
|
option(OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR} ON)
|
||||||
|
add_feature_info(OPUS_INSTALL_CMAKE_CONFIG_MODULE OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR})
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
set(OPUS_BUILD_FRAMEWORK_HELP_STR "build Framework bundle for Apple systems.")
|
||||||
|
option(OPUS_BUILD_FRAMEWORK ${OPUS_BUILD_FRAMEWORK_HELP_STR} OFF)
|
||||||
|
add_feature_info(OPUS_BUILD_FRAMEWORK OPUS_BUILD_FRAMEWORK ${OPUS_BUILD_FRAMEWORK_HELP_STR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(OPUS_FIXED_POINT_DEBUG_HELP_STR "debug fixed-point implementation.")
|
||||||
|
cmake_dependent_option(OPUS_FIXED_POINT_DEBUG
|
||||||
|
${OPUS_FIXED_POINT_DEBUG_HELP_STR}
|
||||||
|
ON
|
||||||
|
"OPUS_FIXED_POINT; OPUS_FIXED_POINT_DEBUG"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_FIXED_POINT_DEBUG OPUS_FIXED_POINT_DEBUG ${OPUS_FIXED_POINT_DEBUG_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_VAR_ARRAYS_HELP_STR "use variable length arrays for stack arrays.")
|
||||||
|
cmake_dependent_option(OPUS_VAR_ARRAYS
|
||||||
|
${OPUS_VAR_ARRAYS_HELP_STR}
|
||||||
|
ON
|
||||||
|
"VLA_SUPPORTED; NOT OPUS_USE_ALLOCA; NOT OPUS_NONTHREADSAFE_PSEUDOSTACK"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_VAR_ARRAYS OPUS_VAR_ARRAYS ${OPUS_VAR_ARRAYS_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_USE_ALLOCA_HELP_STR "use alloca for stack arrays (on non-C99 compilers).")
|
||||||
|
cmake_dependent_option(OPUS_USE_ALLOCA
|
||||||
|
${OPUS_USE_ALLOCA_HELP_STR}
|
||||||
|
ON
|
||||||
|
"USE_ALLOCA_SUPPORTED; NOT OPUS_VAR_ARRAYS; NOT OPUS_NONTHREADSAFE_PSEUDOSTACK"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_USE_ALLOCA OPUS_USE_ALLOCA ${OPUS_USE_ALLOCA_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_NONTHREADSAFE_PSEUDOSTACK_HELP_STR "use a non threadsafe pseudostack when neither variable length arrays or alloca is supported.")
|
||||||
|
cmake_dependent_option(OPUS_NONTHREADSAFE_PSEUDOSTACK
|
||||||
|
${OPUS_NONTHREADSAFE_PSEUDOSTACK_HELP_STR}
|
||||||
|
ON
|
||||||
|
"NOT OPUS_VAR_ARRAYS; NOT OPUS_USE_ALLOCA"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_NONTHREADSAFE_PSEUDOSTACK OPUS_NONTHREADSAFE_PSEUDOSTACK ${OPUS_NONTHREADSAFE_PSEUDOSTACK_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_FAST_MATH_HELP_STR "enable fast math (unsupported and discouraged use, as code is not well tested with this build option).")
|
||||||
|
cmake_dependent_option(OPUS_FAST_MATH
|
||||||
|
${OPUS_FAST_MATH_HELP_STR}
|
||||||
|
ON
|
||||||
|
"OPUS_FLOAT_APPROX; OPUS_FAST_MATH; FAST_MATH_SUPPORTED"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_FAST_MATH OPUS_FAST_MATH ${OPUS_FAST_MATH_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_STACK_PROTECTOR_HELP_STR "use stack protection.")
|
||||||
|
cmake_dependent_option(OPUS_STACK_PROTECTOR
|
||||||
|
${OPUS_STACK_PROTECTOR_HELP_STR}
|
||||||
|
ON
|
||||||
|
"STACK_PROTECTOR_SUPPORTED"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_STACK_PROTECTOR OPUS_STACK_PROTECTOR ${OPUS_STACK_PROTECTOR_HELP_STR})
|
||||||
|
|
||||||
|
if(NOT MSVC)
|
||||||
|
set(OPUS_FORTIFY_SOURCE_HELP_STR "add protection against buffer overflows.")
|
||||||
|
cmake_dependent_option(OPUS_FORTIFY_SOURCE
|
||||||
|
${OPUS_FORTIFY_SOURCE_HELP_STR}
|
||||||
|
ON
|
||||||
|
"FORTIFY_SOURCE_SUPPORTED"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_FORTIFY_SOURCE OPUS_FORTIFY_SOURCE ${OPUS_FORTIFY_SOURCE_HELP_STR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MINGW AND (OPUS_FORTIFY_SOURCE OR OPUS_STACK_PROTECTOR))
|
||||||
|
# ssp lib is needed for security features for MINGW
|
||||||
|
list(APPEND OPUS_REQUIRED_LIBRARIES ssp)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_CPU_X86 OR OPUS_CPU_X64)
|
||||||
|
set(OPUS_X86_MAY_HAVE_SSE_HELP_STR "does runtime check for SSE1 support.")
|
||||||
|
cmake_dependent_option(OPUS_X86_MAY_HAVE_SSE
|
||||||
|
${OPUS_X86_MAY_HAVE_SSE_HELP_STR}
|
||||||
|
ON
|
||||||
|
"SSE1_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_X86_MAY_HAVE_SSE OPUS_X86_MAY_HAVE_SSE ${OPUS_X86_MAY_HAVE_SSE_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_X86_MAY_HAVE_SSE2_HELP_STR "does runtime check for SSE2 support.")
|
||||||
|
cmake_dependent_option(OPUS_X86_MAY_HAVE_SSE2
|
||||||
|
${OPUS_X86_MAY_HAVE_SSE2_HELP_STR}
|
||||||
|
ON
|
||||||
|
"SSE2_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_X86_MAY_HAVE_SSE2 OPUS_X86_MAY_HAVE_SSE2 ${OPUS_X86_MAY_HAVE_SSE2_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_X86_MAY_HAVE_SSE4_1_HELP_STR "does runtime check for SSE4.1 support.")
|
||||||
|
cmake_dependent_option(OPUS_X86_MAY_HAVE_SSE4_1
|
||||||
|
${OPUS_X86_MAY_HAVE_SSE4_1_HELP_STR}
|
||||||
|
ON
|
||||||
|
"SSE4_1_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_X86_MAY_HAVE_SSE4_1 OPUS_X86_MAY_HAVE_SSE4_1 ${OPUS_X86_MAY_HAVE_SSE4_1_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_X86_MAY_HAVE_AVX_HELP_STR "does runtime check for AVX support.")
|
||||||
|
cmake_dependent_option(OPUS_X86_MAY_HAVE_AVX
|
||||||
|
${OPUS_X86_MAY_HAVE_AVX_HELP_STR}
|
||||||
|
ON
|
||||||
|
"AVX_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_X86_MAY_HAVE_AVX OPUS_X86_MAY_HAVE_AVX ${OPUS_X86_MAY_HAVE_AVX_HELP_STR})
|
||||||
|
|
||||||
|
# PRESUME depends on MAY HAVE, but PRESUME will override runtime detection
|
||||||
|
set(OPUS_X86_PRESUME_SSE_HELP_STR "assume target CPU has SSE1 support (override runtime check).")
|
||||||
|
set(OPUS_X86_PRESUME_SSE2_HELP_STR "assume target CPU has SSE2 support (override runtime check).")
|
||||||
|
if(OPUS_CPU_X64) # Assume x86_64 has up to SSE2 support
|
||||||
|
cmake_dependent_option(OPUS_X86_PRESUME_SSE
|
||||||
|
${OPUS_X86_PRESUME_SSE_HELP_STR}
|
||||||
|
ON
|
||||||
|
"OPUS_X86_MAY_HAVE_SSE; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
|
||||||
|
cmake_dependent_option(OPUS_X86_PRESUME_SSE2
|
||||||
|
${OPUS_X86_PRESUME_SSE2_HELP_STR}
|
||||||
|
ON
|
||||||
|
"OPUS_X86_MAY_HAVE_SSE2; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
else()
|
||||||
|
cmake_dependent_option(OPUS_X86_PRESUME_SSE
|
||||||
|
${OPUS_X86_PRESUME_SSE_HELP_STR}
|
||||||
|
OFF
|
||||||
|
"OPUS_X86_MAY_HAVE_SSE; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
|
||||||
|
cmake_dependent_option(OPUS_X86_PRESUME_SSE2
|
||||||
|
${OPUS_X86_PRESUME_SSE2_HELP_STR}
|
||||||
|
OFF
|
||||||
|
"OPUS_X86_MAY_HAVE_SSE2; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
endif()
|
||||||
|
add_feature_info(OPUS_X86_PRESUME_SSE OPUS_X86_PRESUME_SSE ${OPUS_X86_PRESUME_SSE_HELP_STR})
|
||||||
|
add_feature_info(OPUS_X86_PRESUME_SSE2 OPUS_X86_PRESUME_SSE2 ${OPUS_X86_PRESUME_SSE2_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_X86_PRESUME_SSE4_1_HELP_STR "assume target CPU has SSE4.1 support (override runtime check).")
|
||||||
|
cmake_dependent_option(OPUS_X86_PRESUME_SSE4_1
|
||||||
|
${OPUS_X86_PRESUME_SSE4_1_HELP_STR}
|
||||||
|
OFF
|
||||||
|
"OPUS_X86_MAY_HAVE_SSE4_1; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_X86_PRESUME_SSE4_1 OPUS_X86_PRESUME_SSE4_1 ${OPUS_X86_PRESUME_SSE4_1_HELP_STR})
|
||||||
|
|
||||||
|
set(OPUS_X86_PRESUME_AVX_HELP_STR "assume target CPU has AVX support (override runtime check).")
|
||||||
|
cmake_dependent_option(OPUS_X86_PRESUME_AVX
|
||||||
|
${OPUS_X86_PRESUME_AVX_HELP_STR}
|
||||||
|
OFF
|
||||||
|
"OPUS_X86_MAY_HAVE_AVX; NOT OPUS_DISABLE_INTRINSICS"
|
||||||
|
OFF)
|
||||||
|
add_feature_info(OPUS_X86_PRESUME_AVX OPUS_X86_PRESUME_AVX ${OPUS_X86_PRESUME_AVX_HELP_STR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
feature_summary(WHAT ALL)
|
||||||
|
|
||||||
|
set_package_properties(Git
|
||||||
|
PROPERTIES
|
||||||
|
TYPE
|
||||||
|
REQUIRED
|
||||||
|
DESCRIPTION
|
||||||
|
"fast, scalable, distributed revision control system"
|
||||||
|
URL
|
||||||
|
"https://git-scm.com/"
|
||||||
|
PURPOSE
|
||||||
|
"required to set up package version")
|
||||||
|
|
||||||
|
set(Opus_PUBLIC_HEADER
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/opus.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_defines.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_multistream.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_projection.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_types.h)
|
||||||
|
|
||||||
|
if(OPUS_CUSTOM_MODES)
|
||||||
|
list(APPEND Opus_PUBLIC_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/include/opus_custom.h)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_library(opus ${opus_headers} ${opus_sources} ${opus_sources_float} ${Opus_PUBLIC_HEADER})
|
||||||
|
add_library(Opus::opus ALIAS opus)
|
||||||
|
|
||||||
|
get_library_version(OPUS_LIBRARY_VERSION OPUS_LIBRARY_VERSION_MAJOR)
|
||||||
|
message(DEBUG "Opus library version: ${OPUS_LIBRARY_VERSION}")
|
||||||
|
|
||||||
|
set_target_properties(opus
|
||||||
|
PROPERTIES SOVERSION
|
||||||
|
${OPUS_LIBRARY_VERSION_MAJOR}
|
||||||
|
VERSION
|
||||||
|
${OPUS_LIBRARY_VERSION}
|
||||||
|
PUBLIC_HEADER
|
||||||
|
"${Opus_PUBLIC_HEADER}")
|
||||||
|
|
||||||
|
target_include_directories(
|
||||||
|
opus
|
||||||
|
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||||
|
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/opus>
|
||||||
|
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
celt
|
||||||
|
silk)
|
||||||
|
|
||||||
|
target_link_libraries(opus PRIVATE ${OPUS_REQUIRED_LIBRARIES})
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_BUILD)
|
||||||
|
|
||||||
|
if(OPUS_FIXED_POINT_DEBUG)
|
||||||
|
target_compile_definitions(opus PRIVATE FIXED_DEBUG)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_FORTIFY_SOURCE AND NOT MSVC)
|
||||||
|
target_compile_definitions(opus PRIVATE
|
||||||
|
$<$<NOT:$<CONFIG:debug>>:_FORTIFY_SOURCE=2>)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_FLOAT_APPROX)
|
||||||
|
target_compile_definitions(opus PRIVATE FLOAT_APPROX)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_ASSERTIONS)
|
||||||
|
target_compile_definitions(opus PRIVATE ENABLE_ASSERTIONS)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_HARDENING)
|
||||||
|
target_compile_definitions(opus PRIVATE ENABLE_HARDENING)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_FUZZING)
|
||||||
|
target_compile_definitions(opus PRIVATE FUZZING)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_CHECK_ASM)
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_CHECK_ASM)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_VAR_ARRAYS)
|
||||||
|
target_compile_definitions(opus PRIVATE VAR_ARRAYS)
|
||||||
|
elseif(OPUS_USE_ALLOCA)
|
||||||
|
target_compile_definitions(opus PRIVATE USE_ALLOCA)
|
||||||
|
elseif(OPUS_NONTHREADSAFE_PSEUDOSTACK)
|
||||||
|
target_compile_definitions(opus PRIVATE NONTHREADSAFE_PSEUDOSTACK)
|
||||||
|
else()
|
||||||
|
message(ERROR "Need to set a define for stack allocation")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_CUSTOM_MODES)
|
||||||
|
target_compile_definitions(opus PRIVATE CUSTOM_MODES)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_FAST_MATH)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(opus PRIVATE /fp:fast)
|
||||||
|
else()
|
||||||
|
target_compile_options(opus PRIVATE -ffast-math)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_STACK_PROTECTOR)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(opus PRIVATE /GS)
|
||||||
|
else()
|
||||||
|
target_compile_options(opus PRIVATE -fstack-protector-strong)
|
||||||
|
endif()
|
||||||
|
elseif(STACK_PROTECTOR_DISABLED_SUPPORTED)
|
||||||
|
target_compile_options(opus PRIVATE /GS-)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(BUILD_SHARED_LIBS)
|
||||||
|
if(WIN32)
|
||||||
|
target_compile_definitions(opus PRIVATE DLL_EXPORT)
|
||||||
|
elseif(HIDDEN_VISIBILITY_SUPPORTED)
|
||||||
|
set_target_properties(opus PROPERTIES C_VISIBILITY_PRESET hidden)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_sources_group(opus silk ${silk_headers} ${silk_sources})
|
||||||
|
add_sources_group(opus celt ${celt_headers} ${celt_sources})
|
||||||
|
|
||||||
|
if(OPUS_FIXED_POINT)
|
||||||
|
add_sources_group(opus silk ${silk_sources_fixed})
|
||||||
|
target_include_directories(opus PRIVATE silk/fixed)
|
||||||
|
target_compile_definitions(opus PRIVATE FIXED_POINT=1)
|
||||||
|
else()
|
||||||
|
add_sources_group(opus silk ${silk_sources_float})
|
||||||
|
target_include_directories(opus PRIVATE silk/float)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT OPUS_ENABLE_FLOAT_API)
|
||||||
|
target_compile_definitions(opus PRIVATE DISABLE_FLOAT_API)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# WZP: distinguish real cl.exe from clang-cl. libopus uses `if(NOT MSVC)`
|
||||||
|
# to guard per-file `-msse4.1` / `-mssse3` / `-msse2` flags that GCC and
|
||||||
|
# clang (GNU driver) accept. Under clang-cl (Clang running in MSVC driver
|
||||||
|
# mode, as used by cargo-xwin cross-compiles), CMake sets MSVC=1 via
|
||||||
|
# Platform/Windows-MSVC.cmake, so those guards become false and the
|
||||||
|
# SIMD source files end up compiled WITHOUT the required target feature
|
||||||
|
# — which then explodes in silk/x86/NSQ_sse4_1.c with
|
||||||
|
# "always_inline function '_mm_cvtepi16_epi32' requires target feature
|
||||||
|
# 'sse4.1'" errors. clang-cl, unlike real cl.exe, still honors Clang's
|
||||||
|
# target-feature system, and accepts `-msse4.1` (LLVM 14+) to enable it.
|
||||||
|
#
|
||||||
|
# Split real cl.exe (which genuinely doesn't need per-feature gating
|
||||||
|
# because its SIMD intrinsic headers are unconditionally available) from
|
||||||
|
# clang-cl (which does need gating) using CMAKE_C_COMPILER_ID. Then the
|
||||||
|
# `if(NOT MSVC)` guards below become `if(NOT MSVC_CL)` so clang-cl gets
|
||||||
|
# the GCC-style per-file flags, and the `if(MSVC)` global /arch block at
|
||||||
|
# the bottom becomes `if(MSVC_CL)` so only real cl.exe applies `/arch:AVX`
|
||||||
|
# / `/arch:SSE2` globally (clang-cl relies on per-file `-msse` instead).
|
||||||
|
#
|
||||||
|
# Upstream tracking: xiph/opus#256, xiph/opus PR #257 (stale).
|
||||||
|
set(MSVC_CL OFF)
|
||||||
|
if(MSVC AND CMAKE_C_COMPILER_ID STREQUAL "MSVC")
|
||||||
|
set(MSVC_CL ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT OPUS_DISABLE_INTRINSICS)
|
||||||
|
if((OPUS_X86_MAY_HAVE_SSE AND NOT OPUS_X86_PRESUME_SSE) OR
|
||||||
|
(OPUS_X86_MAY_HAVE_SSE2 AND NOT OPUS_X86_PRESUME_SSE2) OR
|
||||||
|
(OPUS_X86_MAY_HAVE_SSE4_1 AND NOT OPUS_X86_PRESUME_SSE4_1) OR
|
||||||
|
(OPUS_X86_MAY_HAVE_AVX AND NOT OPUS_X86_PRESUME_AVX))
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_HAVE_RTCD)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(SSE1_SUPPORTED)
|
||||||
|
if(OPUS_X86_MAY_HAVE_SSE)
|
||||||
|
add_sources_group(opus celt ${celt_sources_sse})
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_SSE)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
set_source_files_properties(${celt_sources_sse} PROPERTIES COMPILE_FLAGS -msse)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(OPUS_X86_PRESUME_SSE)
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
target_compile_options(opus PRIVATE -msse)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(SSE2_SUPPORTED)
|
||||||
|
if(OPUS_X86_MAY_HAVE_SSE2)
|
||||||
|
add_sources_group(opus celt ${celt_sources_sse2})
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_SSE2)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
set_source_files_properties(${celt_sources_sse2} PROPERTIES COMPILE_FLAGS -msse2)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(OPUS_X86_PRESUME_SSE2)
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE2)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
target_compile_options(opus PRIVATE -msse2)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(SSE4_1_SUPPORTED)
|
||||||
|
if(OPUS_X86_MAY_HAVE_SSE4_1)
|
||||||
|
add_sources_group(opus celt ${celt_sources_sse4_1})
|
||||||
|
add_sources_group(opus silk ${silk_sources_sse4_1})
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_SSE4_1)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
set_source_files_properties(${celt_sources_sse4_1} ${silk_sources_sse4_1} PROPERTIES COMPILE_FLAGS -msse4.1)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_FIXED_POINT)
|
||||||
|
add_sources_group(opus silk ${silk_sources_fixed_sse4_1})
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
set_source_files_properties(${silk_sources_fixed_sse4_1} PROPERTIES COMPILE_FLAGS -msse4.1)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
if(OPUS_X86_PRESUME_SSE4_1)
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE4_1)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
target_compile_options(opus PRIVATE -msse4.1)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(AVX_SUPPORTED)
|
||||||
|
# mostly placeholder in case of avx intrinsics is added
|
||||||
|
if(OPUS_X86_MAY_HAVE_AVX)
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_AVX)
|
||||||
|
endif()
|
||||||
|
if(OPUS_X86_PRESUME_AVX)
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_AVX)
|
||||||
|
if(NOT MSVC_CL)
|
||||||
|
target_compile_options(opus PRIVATE -mavx)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(MSVC_CL)
|
||||||
|
if(AVX_SUPPORTED AND OPUS_X86_PRESUME_AVX) # on 64 bit and 32 bits
|
||||||
|
add_definitions(/arch:AVX)
|
||||||
|
elseif(OPUS_CPU_X86) # if AVX not supported then set SSE flag
|
||||||
|
if((SSE4_1_SUPPORTED AND OPUS_X86_PRESUME_SSE4_1)
|
||||||
|
OR (SSE2_SUPPORTED AND OPUS_X86_PRESUME_SSE2))
|
||||||
|
target_compile_definitions(opus PRIVATE /arch:SSE2)
|
||||||
|
elseif(SSE1_SUPPORTED AND OPUS_X86_PRESUME_SSE)
|
||||||
|
target_compile_definitions(opus PRIVATE /arch:SSE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CMAKE_SYSTEM_PROCESSOR MATCHES "(arm|aarch64)")
|
||||||
|
add_sources_group(opus celt ${celt_sources_arm})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(COMPILER_SUPPORT_NEON)
|
||||||
|
if(OPUS_MAY_HAVE_NEON)
|
||||||
|
if(RUNTIME_CPU_CAPABILITY_DETECTION)
|
||||||
|
message(STATUS "OPUS_MAY_HAVE_NEON enabling runtime detection")
|
||||||
|
target_compile_definitions(opus PRIVATE OPUS_HAVE_RTCD)
|
||||||
|
else()
|
||||||
|
message(ERROR "Runtime cpu capability detection needed for MAY_HAVE_NEON")
|
||||||
|
endif()
|
||||||
|
# Do runtime check for NEON
|
||||||
|
target_compile_definitions(opus
|
||||||
|
PRIVATE
|
||||||
|
OPUS_ARM_MAY_HAVE_NEON
|
||||||
|
OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_sources_group(opus celt ${celt_sources_arm_neon_intr})
|
||||||
|
add_sources_group(opus silk ${silk_sources_arm_neon_intr})
|
||||||
|
|
||||||
|
# silk arm neon depends on main_Fix.h
|
||||||
|
target_include_directories(opus PRIVATE silk/fixed)
|
||||||
|
|
||||||
|
if(OPUS_FIXED_POINT)
|
||||||
|
add_sources_group(opus silk ${silk_sources_fixed_arm_neon_intr})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_PRESUME_NEON)
|
||||||
|
target_compile_definitions(opus
|
||||||
|
PRIVATE
|
||||||
|
OPUS_ARM_PRESUME_NEON
|
||||||
|
OPUS_ARM_PRESUME_NEON_INTR)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_compile_definitions(opus
|
||||||
|
PRIVATE
|
||||||
|
$<$<BOOL:${HAVE_LRINT}>:HAVE_LRINT>
|
||||||
|
$<$<BOOL:${HAVE_LRINTF}>:HAVE_LRINTF>)
|
||||||
|
|
||||||
|
if(OPUS_BUILD_FRAMEWORK)
|
||||||
|
set_target_properties(opus PROPERTIES
|
||||||
|
FRAMEWORK TRUE
|
||||||
|
FRAMEWORK_VERSION ${PROJECT_VERSION}
|
||||||
|
MACOSX_FRAMEWORK_IDENTIFIER org.xiph.opus
|
||||||
|
MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PROJECT_VERSION}
|
||||||
|
MACOSX_FRAMEWORK_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||||
|
XCODE_ATTRIBUTE_INSTALL_PATH "@rpath"
|
||||||
|
OUTPUT_NAME Opus)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
install(TARGETS opus
|
||||||
|
EXPORT OpusTargets
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
FRAMEWORK DESTINATION ${CMAKE_INSTALL_PREFIX}
|
||||||
|
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/opus)
|
||||||
|
|
||||||
|
if(OPUS_INSTALL_PKG_CONFIG_MODULE)
|
||||||
|
set(prefix ${CMAKE_INSTALL_PREFIX})
|
||||||
|
set(exec_prefix ${CMAKE_INSTALL_PREFIX})
|
||||||
|
set(libdir ${CMAKE_INSTALL_FULL_LIBDIR})
|
||||||
|
set(includedir ${CMAKE_INSTALL_FULL_INCLUDEDIR})
|
||||||
|
set(VERSION ${PACKAGE_VERSION})
|
||||||
|
if(HAVE_LIBM)
|
||||||
|
set(LIBM "-lm")
|
||||||
|
endif()
|
||||||
|
configure_file(opus.pc.in opus.pc)
|
||||||
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/opus.pc
|
||||||
|
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_INSTALL_CMAKE_CONFIG_MODULE)
|
||||||
|
set(CPACK_GENERATOR TGZ)
|
||||||
|
include(CPack)
|
||||||
|
set(CMAKE_INSTALL_PACKAGEDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
|
||||||
|
install(EXPORT OpusTargets
|
||||||
|
NAMESPACE Opus::
|
||||||
|
DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
|
||||||
|
|
||||||
|
include(CMakePackageConfigHelpers)
|
||||||
|
|
||||||
|
set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
|
configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/OpusConfig.cmake.in
|
||||||
|
OpusConfig.cmake
|
||||||
|
INSTALL_DESTINATION
|
||||||
|
${CMAKE_INSTALL_PACKAGEDIR}
|
||||||
|
PATH_VARS
|
||||||
|
INCLUDE_INSTALL_DIR
|
||||||
|
INSTALL_PREFIX
|
||||||
|
${CMAKE_INSTALL_PREFIX})
|
||||||
|
write_basic_package_version_file(OpusConfigVersion.cmake
|
||||||
|
VERSION ${PROJECT_VERSION}
|
||||||
|
COMPATIBILITY SameMajorVersion)
|
||||||
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/OpusConfig.cmake
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/OpusConfigVersion.cmake
|
||||||
|
DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(OPUS_BUILD_PROGRAMS)
|
||||||
|
# demo
|
||||||
|
if(OPUS_CUSTOM_MODES)
|
||||||
|
add_executable(opus_custom_demo ${opus_custom_demo_sources})
|
||||||
|
target_include_directories(opus_custom_demo
|
||||||
|
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(opus_custom_demo PRIVATE opus)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(opus_demo ${opus_demo_sources})
|
||||||
|
target_include_directories(opus_demo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_include_directories(opus_demo PRIVATE silk) # debug.h
|
||||||
|
target_include_directories(opus_demo PRIVATE celt) # arch.h
|
||||||
|
target_link_libraries(opus_demo PRIVATE opus ${OPUS_REQUIRED_LIBRARIES})
|
||||||
|
|
||||||
|
# compare
|
||||||
|
add_executable(opus_compare ${opus_compare_sources})
|
||||||
|
target_include_directories(opus_compare PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(opus_compare PRIVATE opus ${OPUS_REQUIRED_LIBRARIES})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
enable_testing()
|
||||||
|
|
||||||
|
# tests
|
||||||
|
add_executable(test_opus_decode ${test_opus_decode_sources})
|
||||||
|
target_include_directories(test_opus_decode
|
||||||
|
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(test_opus_decode PRIVATE opus)
|
||||||
|
if(OPUS_FIXED_POINT)
|
||||||
|
target_compile_definitions(test_opus_decode PRIVATE DISABLE_FLOAT_API)
|
||||||
|
endif()
|
||||||
|
add_test(NAME test_opus_decode COMMAND $<TARGET_FILE:test_opus_decode> WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
add_executable(test_opus_padding ${test_opus_padding_sources})
|
||||||
|
target_include_directories(test_opus_padding
|
||||||
|
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
target_link_libraries(test_opus_padding PRIVATE opus)
|
||||||
|
add_test(NAME test_opus_padding COMMAND $<TARGET_FILE:test_opus_padding> WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
if(NOT BUILD_SHARED_LIBS)
|
||||||
|
# disable tests that depends on private API when building shared lib
|
||||||
|
add_executable(test_opus_api ${test_opus_api_sources})
|
||||||
|
target_include_directories(test_opus_api
|
||||||
|
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} celt)
|
||||||
|
target_link_libraries(test_opus_api PRIVATE opus)
|
||||||
|
if(OPUS_FIXED_POINT)
|
||||||
|
target_compile_definitions(test_opus_api PRIVATE DISABLE_FLOAT_API)
|
||||||
|
endif()
|
||||||
|
add_test(NAME test_opus_api COMMAND $<TARGET_FILE:test_opus_api> WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
add_executable(test_opus_encode ${test_opus_encode_sources})
|
||||||
|
target_include_directories(test_opus_encode
|
||||||
|
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} celt)
|
||||||
|
target_link_libraries(test_opus_encode PRIVATE opus)
|
||||||
|
add_test(NAME test_opus_encode COMMAND $<TARGET_FILE:test_opus_encode> WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
44
vendor/audiopus_sys/opus/COPYING
vendored
Normal file
44
vendor/audiopus_sys/opus/COPYING
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
|
||||||
|
Jean-Marc Valin, Timothy B. Terriberry,
|
||||||
|
CSIRO, Gregory Maxwell, Mark Borgerding,
|
||||||
|
Erik de Castro Lopo
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
- Neither the name of Internet Society, IETF or IETF Trust, nor the
|
||||||
|
names of specific contributors, may be used to endorse or promote
|
||||||
|
products derived from this software without specific prior written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Opus is subject to the royalty-free patent licenses which are
|
||||||
|
specified at:
|
||||||
|
|
||||||
|
Xiph.Org Foundation:
|
||||||
|
https://datatracker.ietf.org/ipr/1524/
|
||||||
|
|
||||||
|
Microsoft Corporation:
|
||||||
|
https://datatracker.ietf.org/ipr/1914/
|
||||||
|
|
||||||
|
Broadcom Corporation:
|
||||||
|
https://datatracker.ietf.org/ipr/1526/
|
||||||
0
vendor/audiopus_sys/opus/ChangeLog
vendored
Normal file
0
vendor/audiopus_sys/opus/ChangeLog
vendored
Normal file
22
vendor/audiopus_sys/opus/LICENSE_PLEASE_READ.txt
vendored
Normal file
22
vendor/audiopus_sys/opus/LICENSE_PLEASE_READ.txt
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Contributions to the collaboration shall not be considered confidential.
|
||||||
|
|
||||||
|
Each contributor represents and warrants that it has the right and
|
||||||
|
authority to license copyright in its contributions to the collaboration.
|
||||||
|
|
||||||
|
Each contributor agrees to license the copyright in the contributions
|
||||||
|
under the Modified (2-clause or 3-clause) BSD License or the Clear BSD License.
|
||||||
|
|
||||||
|
Please see the IPR statements submitted to the IETF for the complete
|
||||||
|
patent licensing details:
|
||||||
|
|
||||||
|
Xiph.Org Foundation:
|
||||||
|
https://datatracker.ietf.org/ipr/1524/
|
||||||
|
|
||||||
|
Microsoft Corporation:
|
||||||
|
https://datatracker.ietf.org/ipr/1914/
|
||||||
|
|
||||||
|
Skype Limited:
|
||||||
|
https://datatracker.ietf.org/ipr/1602/
|
||||||
|
|
||||||
|
Broadcom Corporation:
|
||||||
|
https://datatracker.ietf.org/ipr/1526/
|
||||||
371
vendor/audiopus_sys/opus/Makefile.am
vendored
Normal file
371
vendor/audiopus_sys/opus/Makefile.am
vendored
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
# Provide the full test output for failed tests when using the parallel
|
||||||
|
# test suite (which is enabled by default with automake 1.13+).
|
||||||
|
export VERBOSE = yes
|
||||||
|
|
||||||
|
AUTOMAKE_OPTIONS = subdir-objects
|
||||||
|
ACLOCAL_AMFLAGS = -I m4
|
||||||
|
|
||||||
|
lib_LTLIBRARIES = libopus.la
|
||||||
|
|
||||||
|
DIST_SUBDIRS = doc
|
||||||
|
|
||||||
|
AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_srcdir)/celt -I$(top_srcdir)/silk \
|
||||||
|
-I$(top_srcdir)/silk/float -I$(top_srcdir)/silk/fixed $(NE10_CFLAGS)
|
||||||
|
|
||||||
|
include celt_sources.mk
|
||||||
|
include silk_sources.mk
|
||||||
|
include opus_sources.mk
|
||||||
|
|
||||||
|
if FIXED_POINT
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FIXED)
|
||||||
|
if HAVE_SSE4_1
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_SSE4_1) $(SILK_SOURCES_FIXED_SSE4_1)
|
||||||
|
endif
|
||||||
|
if HAVE_ARM_NEON_INTR
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FIXED_ARM_NEON_INTR)
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FLOAT)
|
||||||
|
if HAVE_SSE4_1
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_SSE4_1)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
if DISABLE_FLOAT_API
|
||||||
|
else
|
||||||
|
OPUS_SOURCES += $(OPUS_SOURCES_FLOAT)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if HAVE_SSE
|
||||||
|
CELT_SOURCES += $(CELT_SOURCES_SSE)
|
||||||
|
endif
|
||||||
|
if HAVE_SSE2
|
||||||
|
CELT_SOURCES += $(CELT_SOURCES_SSE2)
|
||||||
|
endif
|
||||||
|
if HAVE_SSE4_1
|
||||||
|
CELT_SOURCES += $(CELT_SOURCES_SSE4_1)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if CPU_ARM
|
||||||
|
CELT_SOURCES += $(CELT_SOURCES_ARM)
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_ARM)
|
||||||
|
|
||||||
|
if HAVE_ARM_NEON_INTR
|
||||||
|
CELT_SOURCES += $(CELT_SOURCES_ARM_NEON_INTR)
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_ARM_NEON_INTR)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if HAVE_ARM_NE10
|
||||||
|
CELT_SOURCES += $(CELT_SOURCES_ARM_NE10)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
noinst_LTLIBRARIES = libarmasm.la
|
||||||
|
libarmasm_la_SOURCES = $(CELT_SOURCES_ARM_ASM:.s=-gnu.S)
|
||||||
|
BUILT_SOURCES = $(CELT_SOURCES_ARM_ASM:.s=-gnu.S) \
|
||||||
|
$(CELT_AM_SOURCES_ARM_ASM:.s.in=.s) \
|
||||||
|
$(CELT_AM_SOURCES_ARM_ASM:.s.in=-gnu.S)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
CLEANFILES = $(CELT_SOURCES_ARM_ASM:.s=-gnu.S) \
|
||||||
|
$(CELT_AM_SOURCES_ARM_ASM:.s.in=-gnu.S)
|
||||||
|
|
||||||
|
include celt_headers.mk
|
||||||
|
include silk_headers.mk
|
||||||
|
include opus_headers.mk
|
||||||
|
|
||||||
|
libopus_la_SOURCES = $(CELT_SOURCES) $(SILK_SOURCES) $(OPUS_SOURCES)
|
||||||
|
libopus_la_LDFLAGS = -no-undefined -version-info @OPUS_LT_CURRENT@:@OPUS_LT_REVISION@:@OPUS_LT_AGE@
|
||||||
|
libopus_la_LIBADD = $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
libopus_la_LIBADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
pkginclude_HEADERS = include/opus.h include/opus_multistream.h include/opus_types.h include/opus_defines.h include/opus_projection.h
|
||||||
|
|
||||||
|
noinst_HEADERS = $(OPUS_HEAD) $(SILK_HEAD) $(CELT_HEAD)
|
||||||
|
|
||||||
|
if EXTRA_PROGRAMS
|
||||||
|
noinst_PROGRAMS = celt/tests/test_unit_cwrs32 \
|
||||||
|
celt/tests/test_unit_dft \
|
||||||
|
celt/tests/test_unit_entropy \
|
||||||
|
celt/tests/test_unit_laplace \
|
||||||
|
celt/tests/test_unit_mathops \
|
||||||
|
celt/tests/test_unit_mdct \
|
||||||
|
celt/tests/test_unit_rotation \
|
||||||
|
celt/tests/test_unit_types \
|
||||||
|
opus_compare \
|
||||||
|
opus_demo \
|
||||||
|
repacketizer_demo \
|
||||||
|
silk/tests/test_unit_LPC_inv_pred_gain \
|
||||||
|
tests/test_opus_api \
|
||||||
|
tests/test_opus_decode \
|
||||||
|
tests/test_opus_encode \
|
||||||
|
tests/test_opus_padding \
|
||||||
|
tests/test_opus_projection \
|
||||||
|
trivial_example
|
||||||
|
|
||||||
|
TESTS = celt/tests/test_unit_cwrs32 \
|
||||||
|
celt/tests/test_unit_dft \
|
||||||
|
celt/tests/test_unit_entropy \
|
||||||
|
celt/tests/test_unit_laplace \
|
||||||
|
celt/tests/test_unit_mathops \
|
||||||
|
celt/tests/test_unit_mdct \
|
||||||
|
celt/tests/test_unit_rotation \
|
||||||
|
celt/tests/test_unit_types \
|
||||||
|
silk/tests/test_unit_LPC_inv_pred_gain \
|
||||||
|
tests/test_opus_api \
|
||||||
|
tests/test_opus_decode \
|
||||||
|
tests/test_opus_encode \
|
||||||
|
tests/test_opus_padding \
|
||||||
|
tests/test_opus_projection
|
||||||
|
|
||||||
|
opus_demo_SOURCES = src/opus_demo.c
|
||||||
|
|
||||||
|
opus_demo_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
|
||||||
|
|
||||||
|
repacketizer_demo_SOURCES = src/repacketizer_demo.c
|
||||||
|
|
||||||
|
repacketizer_demo_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
|
||||||
|
|
||||||
|
opus_compare_SOURCES = src/opus_compare.c
|
||||||
|
opus_compare_LDADD = $(LIBM)
|
||||||
|
|
||||||
|
trivial_example_SOURCES = doc/trivial_example.c
|
||||||
|
trivial_example_LDADD = libopus.la $(LIBM)
|
||||||
|
|
||||||
|
tests_test_opus_api_SOURCES = tests/test_opus_api.c tests/test_opus_common.h
|
||||||
|
tests_test_opus_api_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
|
||||||
|
|
||||||
|
tests_test_opus_encode_SOURCES = tests/test_opus_encode.c tests/opus_encode_regressions.c tests/test_opus_common.h
|
||||||
|
tests_test_opus_encode_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
|
||||||
|
|
||||||
|
tests_test_opus_decode_SOURCES = tests/test_opus_decode.c tests/test_opus_common.h
|
||||||
|
tests_test_opus_decode_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
|
||||||
|
|
||||||
|
tests_test_opus_padding_SOURCES = tests/test_opus_padding.c tests/test_opus_common.h
|
||||||
|
tests_test_opus_padding_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
|
||||||
|
|
||||||
|
CELT_OBJ = $(CELT_SOURCES:.c=.lo)
|
||||||
|
SILK_OBJ = $(SILK_SOURCES:.c=.lo)
|
||||||
|
OPUS_OBJ = $(OPUS_SOURCES:.c=.lo)
|
||||||
|
|
||||||
|
tests_test_opus_projection_SOURCES = tests/test_opus_projection.c tests/test_opus_common.h
|
||||||
|
tests_test_opus_projection_LDADD = $(OPUS_OBJ) $(SILK_OBJ) $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
tests_test_opus_projection_LDADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
silk_tests_test_unit_LPC_inv_pred_gain_SOURCES = silk/tests/test_unit_LPC_inv_pred_gain.c
|
||||||
|
silk_tests_test_unit_LPC_inv_pred_gain_LDADD = $(SILK_OBJ) $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
silk_tests_test_unit_LPC_inv_pred_gain_LDADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
celt_tests_test_unit_cwrs32_SOURCES = celt/tests/test_unit_cwrs32.c
|
||||||
|
celt_tests_test_unit_cwrs32_LDADD = $(LIBM)
|
||||||
|
|
||||||
|
celt_tests_test_unit_dft_SOURCES = celt/tests/test_unit_dft.c
|
||||||
|
celt_tests_test_unit_dft_LDADD = $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
celt_tests_test_unit_dft_LDADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
celt_tests_test_unit_entropy_SOURCES = celt/tests/test_unit_entropy.c
|
||||||
|
celt_tests_test_unit_entropy_LDADD = $(LIBM)
|
||||||
|
|
||||||
|
celt_tests_test_unit_laplace_SOURCES = celt/tests/test_unit_laplace.c
|
||||||
|
celt_tests_test_unit_laplace_LDADD = $(LIBM)
|
||||||
|
|
||||||
|
celt_tests_test_unit_mathops_SOURCES = celt/tests/test_unit_mathops.c
|
||||||
|
celt_tests_test_unit_mathops_LDADD = $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
celt_tests_test_unit_mathops_LDADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
celt_tests_test_unit_mdct_SOURCES = celt/tests/test_unit_mdct.c
|
||||||
|
celt_tests_test_unit_mdct_LDADD = $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
celt_tests_test_unit_mdct_LDADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
celt_tests_test_unit_rotation_SOURCES = celt/tests/test_unit_rotation.c
|
||||||
|
celt_tests_test_unit_rotation_LDADD = $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
|
||||||
|
if OPUS_ARM_EXTERNAL_ASM
|
||||||
|
celt_tests_test_unit_rotation_LDADD += libarmasm.la
|
||||||
|
endif
|
||||||
|
|
||||||
|
celt_tests_test_unit_types_SOURCES = celt/tests/test_unit_types.c
|
||||||
|
celt_tests_test_unit_types_LDADD = $(LIBM)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if CUSTOM_MODES
|
||||||
|
pkginclude_HEADERS += include/opus_custom.h
|
||||||
|
if EXTRA_PROGRAMS
|
||||||
|
noinst_PROGRAMS += opus_custom_demo
|
||||||
|
opus_custom_demo_SOURCES = celt/opus_custom_demo.c
|
||||||
|
opus_custom_demo_LDADD = libopus.la $(LIBM)
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
EXTRA_DIST = opus.pc.in \
|
||||||
|
opus-uninstalled.pc.in \
|
||||||
|
opus.m4 \
|
||||||
|
Makefile.mips \
|
||||||
|
Makefile.unix \
|
||||||
|
CMakeLists.txt \
|
||||||
|
cmake/CFeatureCheck.cmake \
|
||||||
|
cmake/OpusBuildtype.cmake \
|
||||||
|
cmake/OpusConfig.cmake \
|
||||||
|
cmake/OpusConfig.cmake.in \
|
||||||
|
cmake/OpusFunctions.cmake \
|
||||||
|
cmake/OpusPackageVersion.cmake \
|
||||||
|
cmake/OpusSources.cmake \
|
||||||
|
cmake/config.h.cmake.in \
|
||||||
|
cmake/vla.c \
|
||||||
|
meson/get-version.py \
|
||||||
|
meson/read-sources-list.py \
|
||||||
|
meson.build \
|
||||||
|
meson_options.txt \
|
||||||
|
include/meson.build \
|
||||||
|
celt/meson.build \
|
||||||
|
celt/tests/meson.build \
|
||||||
|
silk/meson.build \
|
||||||
|
silk/tests/meson.build \
|
||||||
|
src/meson.build \
|
||||||
|
tests/meson.build \
|
||||||
|
doc/meson.build \
|
||||||
|
tests/run_vectors.sh \
|
||||||
|
celt/arm/arm2gnu.pl \
|
||||||
|
celt/arm/celt_pitch_xcorr_arm.s \
|
||||||
|
win32/VS2015/opus.vcxproj \
|
||||||
|
win32/VS2015/test_opus_encode.vcxproj.filters \
|
||||||
|
win32/VS2015/test_opus_encode.vcxproj \
|
||||||
|
win32/VS2015/opus_demo.vcxproj \
|
||||||
|
win32/VS2015/test_opus_api.vcxproj.filters \
|
||||||
|
win32/VS2015/test_opus_api.vcxproj \
|
||||||
|
win32/VS2015/test_opus_decode.vcxproj.filters \
|
||||||
|
win32/VS2015/opus_demo.vcxproj.filters \
|
||||||
|
win32/VS2015/opus.vcxproj.filters \
|
||||||
|
win32/VS2015/test_opus_decode.vcxproj \
|
||||||
|
win32/VS2015/opus.sln \
|
||||||
|
win32/VS2015/common.props \
|
||||||
|
win32/genversion.bat \
|
||||||
|
win32/config.h
|
||||||
|
|
||||||
|
pkgconfigdir = $(libdir)/pkgconfig
|
||||||
|
pkgconfig_DATA = opus.pc
|
||||||
|
|
||||||
|
m4datadir = $(datadir)/aclocal
|
||||||
|
m4data_DATA = opus.m4
|
||||||
|
|
||||||
|
# Targets to build and install just the library without the docs
|
||||||
|
opus check-opus install-opus: export NO_DOXYGEN = 1
|
||||||
|
|
||||||
|
opus: all
|
||||||
|
check-opus: check
|
||||||
|
install-opus: install
|
||||||
|
|
||||||
|
|
||||||
|
# Or just the docs
|
||||||
|
docs:
|
||||||
|
( cd doc && $(MAKE) $(AM_MAKEFLAGS) )
|
||||||
|
|
||||||
|
install-docs:
|
||||||
|
( cd doc && $(MAKE) $(AM_MAKEFLAGS) install )
|
||||||
|
|
||||||
|
|
||||||
|
# Or everything (by default)
|
||||||
|
all-local:
|
||||||
|
@[ -n "$(NO_DOXYGEN)" ] || ( cd doc && $(MAKE) $(AM_MAKEFLAGS) )
|
||||||
|
|
||||||
|
install-data-local:
|
||||||
|
@[ -n "$(NO_DOXYGEN)" ] || ( cd doc && $(MAKE) $(AM_MAKEFLAGS) install )
|
||||||
|
|
||||||
|
clean-local:
|
||||||
|
-( cd doc && $(MAKE) $(AM_MAKEFLAGS) clean )
|
||||||
|
|
||||||
|
uninstall-local:
|
||||||
|
( cd doc && $(MAKE) $(AM_MAKEFLAGS) uninstall )
|
||||||
|
|
||||||
|
|
||||||
|
# We check this every time make is run, with configure.ac being touched to
|
||||||
|
# trigger an update of the build system files if update_version changes the
|
||||||
|
# current PACKAGE_VERSION (or if package_version was modified manually by a
|
||||||
|
# user with either AUTO_UPDATE=no or no update_version script present - the
|
||||||
|
# latter being the normal case for tarball releases).
|
||||||
|
#
|
||||||
|
# We can't just add the package_version file to CONFIGURE_DEPENDENCIES since
|
||||||
|
# simply running autoconf will not actually regenerate configure for us when
|
||||||
|
# the content of that file changes (due to autoconf dependency checking not
|
||||||
|
# knowing about that without us creating yet another file for it to include).
|
||||||
|
#
|
||||||
|
# The MAKECMDGOALS check is a gnu-make'ism, but will degrade 'gracefully' for
|
||||||
|
# makes that don't support it. The only loss of functionality is not forcing
|
||||||
|
# an update of package_version for `make dist` if AUTO_UPDATE=no, but that is
|
||||||
|
# unlikely to be a real problem for any real user.
|
||||||
|
$(top_srcdir)/configure.ac: force
|
||||||
|
@case "$(MAKECMDGOALS)" in \
|
||||||
|
dist-hook) exit 0 ;; \
|
||||||
|
dist-* | dist | distcheck | distclean) _arg=release ;; \
|
||||||
|
esac; \
|
||||||
|
if ! $(top_srcdir)/update_version $$_arg 2> /dev/null; then \
|
||||||
|
if [ ! -e $(top_srcdir)/package_version ]; then \
|
||||||
|
echo 'PACKAGE_VERSION="unknown"' > $(top_srcdir)/package_version; \
|
||||||
|
fi; \
|
||||||
|
. $(top_srcdir)/package_version || exit 1; \
|
||||||
|
[ "$(PACKAGE_VERSION)" != "$$PACKAGE_VERSION" ] || exit 0; \
|
||||||
|
fi; \
|
||||||
|
touch $@
|
||||||
|
|
||||||
|
force:
|
||||||
|
|
||||||
|
# Create a minimal package_version file when make dist is run.
|
||||||
|
dist-hook:
|
||||||
|
echo 'PACKAGE_VERSION="$(PACKAGE_VERSION)"' > $(top_distdir)/package_version
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: opus check-opus install-opus docs install-docs
|
||||||
|
|
||||||
|
# automake doesn't do dependency tracking for asm files, that I can tell
|
||||||
|
$(CELT_SOURCES_ARM_ASM:%.s=%-gnu.S): celt/arm/armopts-gnu.S
|
||||||
|
$(CELT_SOURCES_ARM_ASM:%.s=%-gnu.S): $(top_srcdir)/celt/arm/arm2gnu.pl
|
||||||
|
|
||||||
|
# convert ARM asm to GNU as format
|
||||||
|
%-gnu.S: $(top_srcdir)/%.s
|
||||||
|
$(top_srcdir)/celt/arm/arm2gnu.pl @ARM2GNU_PARAMS@ < $< > $@
|
||||||
|
# For autoconf-modified sources (e.g., armopts.s)
|
||||||
|
%-gnu.S: %.s
|
||||||
|
$(top_srcdir)/celt/arm/arm2gnu.pl @ARM2GNU_PARAMS@ < $< > $@
|
||||||
|
|
||||||
|
OPT_UNIT_TEST_OBJ = $(celt_tests_test_unit_mathops_SOURCES:.c=.o) \
|
||||||
|
$(celt_tests_test_unit_rotation_SOURCES:.c=.o) \
|
||||||
|
$(celt_tests_test_unit_mdct_SOURCES:.c=.o) \
|
||||||
|
$(celt_tests_test_unit_dft_SOURCES:.c=.o) \
|
||||||
|
$(silk_tests_test_unit_LPC_inv_pred_gain_SOURCES:.c=.o)
|
||||||
|
|
||||||
|
if HAVE_SSE
|
||||||
|
SSE_OBJ = $(CELT_SOURCES_SSE:.c=.lo)
|
||||||
|
$(SSE_OBJ): CFLAGS += $(OPUS_X86_SSE_CFLAGS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if HAVE_SSE2
|
||||||
|
SSE2_OBJ = $(CELT_SOURCES_SSE2:.c=.lo)
|
||||||
|
$(SSE2_OBJ): CFLAGS += $(OPUS_X86_SSE2_CFLAGS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if HAVE_SSE4_1
|
||||||
|
SSE4_1_OBJ = $(CELT_SOURCES_SSE4_1:.c=.lo) \
|
||||||
|
$(SILK_SOURCES_SSE4_1:.c=.lo) \
|
||||||
|
$(SILK_SOURCES_FIXED_SSE4_1:.c=.lo)
|
||||||
|
$(SSE4_1_OBJ): CFLAGS += $(OPUS_X86_SSE4_1_CFLAGS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if HAVE_ARM_NEON_INTR
|
||||||
|
ARM_NEON_INTR_OBJ = $(CELT_SOURCES_ARM_NEON_INTR:.c=.lo) \
|
||||||
|
$(SILK_SOURCES_ARM_NEON_INTR:.c=.lo) \
|
||||||
|
$(SILK_SOURCES_FIXED_ARM_NEON_INTR:.c=.lo)
|
||||||
|
$(ARM_NEON_INTR_OBJ): CFLAGS += \
|
||||||
|
$(OPUS_ARM_NEON_INTR_CFLAGS) $(NE10_CFLAGS)
|
||||||
|
endif
|
||||||
161
vendor/audiopus_sys/opus/Makefile.mips
vendored
Normal file
161
vendor/audiopus_sys/opus/Makefile.mips
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#################### COMPILE OPTIONS #######################
|
||||||
|
|
||||||
|
# Uncomment this for fixed-point build
|
||||||
|
FIXED_POINT=1
|
||||||
|
|
||||||
|
# It is strongly recommended to uncomment one of these
|
||||||
|
# VAR_ARRAYS: Use C99 variable-length arrays for stack allocation
|
||||||
|
# USE_ALLOCA: Use alloca() for stack allocation
|
||||||
|
# If none is defined, then the fallback is a non-threadsafe global array
|
||||||
|
CFLAGS := -DUSE_ALLOCA $(CFLAGS)
|
||||||
|
#CFLAGS := -DVAR_ARRAYS $(CFLAGS)
|
||||||
|
|
||||||
|
# These options affect performance
|
||||||
|
# HAVE_LRINTF: Use C99 intrinsics to speed up float-to-int conversion
|
||||||
|
CFLAGS := -DHAVE_LRINTF $(CFLAGS)
|
||||||
|
|
||||||
|
###################### END OF OPTIONS ######################
|
||||||
|
|
||||||
|
-include package_version
|
||||||
|
|
||||||
|
include silk_sources.mk
|
||||||
|
include celt_sources.mk
|
||||||
|
include opus_sources.mk
|
||||||
|
|
||||||
|
ifdef FIXED_POINT
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FIXED)
|
||||||
|
else
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FLOAT)
|
||||||
|
OPUS_SOURCES += $(OPUS_SOURCES_FLOAT)
|
||||||
|
endif
|
||||||
|
|
||||||
|
EXESUFFIX =
|
||||||
|
LIBPREFIX = lib
|
||||||
|
LIBSUFFIX = .a
|
||||||
|
OBJSUFFIX = .o
|
||||||
|
|
||||||
|
CC = $(TOOLCHAIN_PREFIX)cc$(TOOLCHAIN_SUFFIX)
|
||||||
|
AR = $(TOOLCHAIN_PREFIX)ar
|
||||||
|
RANLIB = $(TOOLCHAIN_PREFIX)ranlib
|
||||||
|
CP = $(TOOLCHAIN_PREFIX)cp
|
||||||
|
|
||||||
|
cppflags-from-defines = $(addprefix -D,$(1))
|
||||||
|
cppflags-from-includes = $(addprefix -I,$(1))
|
||||||
|
ldflags-from-ldlibdirs = $(addprefix -L,$(1))
|
||||||
|
ldlibs-from-libs = $(addprefix -l,$(1))
|
||||||
|
|
||||||
|
WARNINGS = -Wall -W -Wstrict-prototypes -Wextra -Wcast-align -Wnested-externs -Wshadow
|
||||||
|
|
||||||
|
CFLAGS += -mips32r2 -mno-mips16 -std=gnu99 -O2 -g $(WARNINGS) -DENABLE_ASSERTIONS -DMIPSr1_ASM -DOPUS_BUILD -mdspr2 -march=74kc -mtune=74kc -mmt -mgp32
|
||||||
|
|
||||||
|
CINCLUDES = include silk celt
|
||||||
|
|
||||||
|
ifdef FIXED_POINT
|
||||||
|
CFLAGS += -DFIXED_POINT=1 -DDISABLE_FLOAT_API
|
||||||
|
CINCLUDES += silk/fixed
|
||||||
|
else
|
||||||
|
CINCLUDES += silk/float
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
LIBS = m
|
||||||
|
|
||||||
|
LDLIBDIRS = ./
|
||||||
|
|
||||||
|
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
||||||
|
CFLAGS += $(call cppflags-from-includes,$(CINCLUDES))
|
||||||
|
LDFLAGS += $(call ldflags-from-ldlibdirs,$(LDLIBDIRS))
|
||||||
|
LDLIBS += $(call ldlibs-from-libs,$(LIBS))
|
||||||
|
|
||||||
|
COMPILE.c.cmdline = $(CC) -c $(CFLAGS) -o $@ $<
|
||||||
|
LINK.o = $(CC) $(LDPREFLAGS) $(LDFLAGS)
|
||||||
|
LINK.o.cmdline = $(LINK.o) $^ $(LDLIBS) -o $@$(EXESUFFIX)
|
||||||
|
|
||||||
|
ARCHIVE.cmdline = $(AR) $(ARFLAGS) $@ $^ && $(RANLIB) $@
|
||||||
|
|
||||||
|
%$(OBJSUFFIX):%.c
|
||||||
|
$(COMPILE.c.cmdline)
|
||||||
|
|
||||||
|
%$(OBJSUFFIX):%.cpp
|
||||||
|
$(COMPILE.cpp.cmdline)
|
||||||
|
|
||||||
|
# Directives
|
||||||
|
|
||||||
|
|
||||||
|
# Variable definitions
|
||||||
|
LIB_NAME = opus
|
||||||
|
TARGET = $(LIBPREFIX)$(LIB_NAME)$(LIBSUFFIX)
|
||||||
|
|
||||||
|
SRCS_C = $(SILK_SOURCES) $(CELT_SOURCES) $(OPUS_SOURCES)
|
||||||
|
|
||||||
|
OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(SRCS_C))
|
||||||
|
|
||||||
|
OPUSDEMO_SRCS_C = src/opus_demo.c
|
||||||
|
OPUSDEMO_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSDEMO_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSAPI_SRCS_C = tests/test_opus_api.c
|
||||||
|
TESTOPUSAPI_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSAPI_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSDECODE_SRCS_C = tests/test_opus_decode.c
|
||||||
|
TESTOPUSDECODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSDECODE_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSENCODE_SRCS_C = tests/test_opus_encode.c tests/opus_encode_regressions.c
|
||||||
|
TESTOPUSENCODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSENCODE_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSPADDING_SRCS_C = tests/test_opus_padding.c
|
||||||
|
TESTOPUSPADDING_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSPADDING_SRCS_C))
|
||||||
|
|
||||||
|
OPUSCOMPARE_SRCS_C = src/opus_compare.c
|
||||||
|
OPUSCOMPARE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSCOMPARE_SRCS_C))
|
||||||
|
|
||||||
|
TESTS := test_opus_api test_opus_decode test_opus_encode test_opus_padding
|
||||||
|
|
||||||
|
# Rules
|
||||||
|
all: lib opus_demo opus_compare $(TESTS)
|
||||||
|
|
||||||
|
lib: $(TARGET)
|
||||||
|
|
||||||
|
check: all
|
||||||
|
for test in $(TESTS); do ./$$test; done
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS)
|
||||||
|
$(ARCHIVE.cmdline)
|
||||||
|
|
||||||
|
opus_demo$(EXESUFFIX): $(OPUSDEMO_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_api$(EXESUFFIX): $(TESTOPUSAPI_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_decode$(EXESUFFIX): $(TESTOPUSDECODE_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_encode$(EXESUFFIX): $(TESTOPUSENCODE_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_padding$(EXESUFFIX): $(TESTOPUSPADDING_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
opus_compare$(EXESUFFIX): $(OPUSCOMPARE_OBJS)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
celt/celt.o: CFLAGS += -DPACKAGE_VERSION='$(PACKAGE_VERSION)'
|
||||||
|
celt/celt.o: package_version
|
||||||
|
|
||||||
|
package_version: force
|
||||||
|
@if [ -x ./update_version ]; then \
|
||||||
|
./update_version || true; \
|
||||||
|
elif [ ! -e ./package_version ]; then \
|
||||||
|
echo 'PACKAGE_VERSION="unknown"' > ./package_version; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
force:
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f opus_demo$(EXESUFFIX) opus_compare$(EXESUFFIX) $(TARGET) \
|
||||||
|
test_opus_api$(EXESUFFIX) test_opus_decode$(EXESUFFIX) \
|
||||||
|
test_opus_encode$(EXESUFFIX) test_opus_padding$(EXESUFFIX) \
|
||||||
|
$(OBJS) $(OPUSDEMO_OBJS) $(OPUSCOMPARE_OBJS) $(TESTOPUSAPI_OBJS) \
|
||||||
|
$(TESTOPUSDECODE_OBJS) $(TESTOPUSENCODE_OBJS) $(TESTOPUSPADDING_OBJS)
|
||||||
|
|
||||||
|
.PHONY: all lib clean force check
|
||||||
159
vendor/audiopus_sys/opus/Makefile.unix
vendored
Normal file
159
vendor/audiopus_sys/opus/Makefile.unix
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
#################### COMPILE OPTIONS #######################
|
||||||
|
|
||||||
|
# Uncomment this for fixed-point build
|
||||||
|
#FIXED_POINT=1
|
||||||
|
|
||||||
|
# It is strongly recommended to uncomment one of these
|
||||||
|
# VAR_ARRAYS: Use C99 variable-length arrays for stack allocation
|
||||||
|
# USE_ALLOCA: Use alloca() for stack allocation
|
||||||
|
# If none is defined, then the fallback is a non-threadsafe global array
|
||||||
|
CFLAGS := -DUSE_ALLOCA $(CFLAGS)
|
||||||
|
#CFLAGS := -DVAR_ARRAYS $(CFLAGS)
|
||||||
|
|
||||||
|
# These options affect performance
|
||||||
|
# HAVE_LRINTF: Use C99 intrinsics to speed up float-to-int conversion
|
||||||
|
#CFLAGS := -DHAVE_LRINTF $(CFLAGS)
|
||||||
|
|
||||||
|
###################### END OF OPTIONS ######################
|
||||||
|
|
||||||
|
-include package_version
|
||||||
|
|
||||||
|
include silk_sources.mk
|
||||||
|
include celt_sources.mk
|
||||||
|
include opus_sources.mk
|
||||||
|
|
||||||
|
ifdef FIXED_POINT
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FIXED)
|
||||||
|
else
|
||||||
|
SILK_SOURCES += $(SILK_SOURCES_FLOAT)
|
||||||
|
OPUS_SOURCES += $(OPUS_SOURCES_FLOAT)
|
||||||
|
endif
|
||||||
|
|
||||||
|
EXESUFFIX =
|
||||||
|
LIBPREFIX = lib
|
||||||
|
LIBSUFFIX = .a
|
||||||
|
OBJSUFFIX = .o
|
||||||
|
|
||||||
|
CC = $(TOOLCHAIN_PREFIX)cc$(TOOLCHAIN_SUFFIX)
|
||||||
|
AR = $(TOOLCHAIN_PREFIX)ar
|
||||||
|
RANLIB = $(TOOLCHAIN_PREFIX)ranlib
|
||||||
|
CP = $(TOOLCHAIN_PREFIX)cp
|
||||||
|
|
||||||
|
cppflags-from-defines = $(addprefix -D,$(1))
|
||||||
|
cppflags-from-includes = $(addprefix -I,$(1))
|
||||||
|
ldflags-from-ldlibdirs = $(addprefix -L,$(1))
|
||||||
|
ldlibs-from-libs = $(addprefix -l,$(1))
|
||||||
|
|
||||||
|
WARNINGS = -Wall -W -Wstrict-prototypes -Wextra -Wcast-align -Wnested-externs -Wshadow
|
||||||
|
CFLAGS += -O2 -g $(WARNINGS) -DOPUS_BUILD
|
||||||
|
CINCLUDES = include silk celt
|
||||||
|
|
||||||
|
ifdef FIXED_POINT
|
||||||
|
CFLAGS += -DFIXED_POINT=1 -DDISABLE_FLOAT_API
|
||||||
|
CINCLUDES += silk/fixed
|
||||||
|
else
|
||||||
|
CINCLUDES += silk/float
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
LIBS = m
|
||||||
|
|
||||||
|
LDLIBDIRS = ./
|
||||||
|
|
||||||
|
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
||||||
|
CFLAGS += $(call cppflags-from-includes,$(CINCLUDES))
|
||||||
|
LDFLAGS += $(call ldflags-from-ldlibdirs,$(LDLIBDIRS))
|
||||||
|
LDLIBS += $(call ldlibs-from-libs,$(LIBS))
|
||||||
|
|
||||||
|
COMPILE.c.cmdline = $(CC) -c $(CFLAGS) -o $@ $<
|
||||||
|
LINK.o = $(CC) $(LDPREFLAGS) $(LDFLAGS)
|
||||||
|
LINK.o.cmdline = $(LINK.o) $^ $(LDLIBS) -o $@$(EXESUFFIX)
|
||||||
|
|
||||||
|
ARCHIVE.cmdline = $(AR) $(ARFLAGS) $@ $^ && $(RANLIB) $@
|
||||||
|
|
||||||
|
%$(OBJSUFFIX):%.c
|
||||||
|
$(COMPILE.c.cmdline)
|
||||||
|
|
||||||
|
%$(OBJSUFFIX):%.cpp
|
||||||
|
$(COMPILE.cpp.cmdline)
|
||||||
|
|
||||||
|
# Directives
|
||||||
|
|
||||||
|
|
||||||
|
# Variable definitions
|
||||||
|
LIB_NAME = opus
|
||||||
|
TARGET = $(LIBPREFIX)$(LIB_NAME)$(LIBSUFFIX)
|
||||||
|
|
||||||
|
SRCS_C = $(SILK_SOURCES) $(CELT_SOURCES) $(OPUS_SOURCES)
|
||||||
|
|
||||||
|
OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(SRCS_C))
|
||||||
|
|
||||||
|
OPUSDEMO_SRCS_C = src/opus_demo.c
|
||||||
|
OPUSDEMO_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSDEMO_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSAPI_SRCS_C = tests/test_opus_api.c
|
||||||
|
TESTOPUSAPI_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSAPI_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSDECODE_SRCS_C = tests/test_opus_decode.c
|
||||||
|
TESTOPUSDECODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSDECODE_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSENCODE_SRCS_C = tests/test_opus_encode.c tests/opus_encode_regressions.c
|
||||||
|
TESTOPUSENCODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSENCODE_SRCS_C))
|
||||||
|
|
||||||
|
TESTOPUSPADDING_SRCS_C = tests/test_opus_padding.c
|
||||||
|
TESTOPUSPADDING_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSPADDING_SRCS_C))
|
||||||
|
|
||||||
|
OPUSCOMPARE_SRCS_C = src/opus_compare.c
|
||||||
|
OPUSCOMPARE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSCOMPARE_SRCS_C))
|
||||||
|
|
||||||
|
TESTS := test_opus_api test_opus_decode test_opus_encode test_opus_padding
|
||||||
|
|
||||||
|
# Rules
|
||||||
|
all: lib opus_demo opus_compare $(TESTS)
|
||||||
|
|
||||||
|
lib: $(TARGET)
|
||||||
|
|
||||||
|
check: all
|
||||||
|
for test in $(TESTS); do ./$$test; done
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS)
|
||||||
|
$(ARCHIVE.cmdline)
|
||||||
|
|
||||||
|
opus_demo$(EXESUFFIX): $(OPUSDEMO_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_api$(EXESUFFIX): $(TESTOPUSAPI_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_decode$(EXESUFFIX): $(TESTOPUSDECODE_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_encode$(EXESUFFIX): $(TESTOPUSENCODE_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
test_opus_padding$(EXESUFFIX): $(TESTOPUSPADDING_OBJS) $(TARGET)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
opus_compare$(EXESUFFIX): $(OPUSCOMPARE_OBJS)
|
||||||
|
$(LINK.o.cmdline)
|
||||||
|
|
||||||
|
celt/celt.o: CFLAGS += -DPACKAGE_VERSION='$(PACKAGE_VERSION)'
|
||||||
|
celt/celt.o: package_version
|
||||||
|
|
||||||
|
package_version: force
|
||||||
|
@if [ -x ./update_version ]; then \
|
||||||
|
./update_version || true; \
|
||||||
|
elif [ ! -e ./package_version ]; then \
|
||||||
|
echo 'PACKAGE_VERSION="unknown"' > ./package_version; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
force:
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f opus_demo$(EXESUFFIX) opus_compare$(EXESUFFIX) $(TARGET) \
|
||||||
|
test_opus_api$(EXESUFFIX) test_opus_decode$(EXESUFFIX) \
|
||||||
|
test_opus_encode$(EXESUFFIX) test_opus_padding$(EXESUFFIX) \
|
||||||
|
$(OBJS) $(OPUSDEMO_OBJS) $(OPUSCOMPARE_OBJS) $(TESTOPUSAPI_OBJS) \
|
||||||
|
$(TESTOPUSDECODE_OBJS) $(TESTOPUSENCODE_OBJS) $(TESTOPUSPADDING_OBJS)
|
||||||
|
|
||||||
|
.PHONY: all lib clean force check
|
||||||
0
vendor/audiopus_sys/opus/NEWS
vendored
Normal file
0
vendor/audiopus_sys/opus/NEWS
vendored
Normal file
161
vendor/audiopus_sys/opus/README
vendored
Normal file
161
vendor/audiopus_sys/opus/README
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
== Opus audio codec ==
|
||||||
|
|
||||||
|
Opus is a codec for interactive speech and audio transmission over the Internet.
|
||||||
|
|
||||||
|
Opus can handle a wide range of interactive audio applications, including
|
||||||
|
Voice over IP, videoconferencing, in-game chat, and even remote live music
|
||||||
|
performances. It can scale from low bit-rate narrowband speech to very high
|
||||||
|
quality stereo music.
|
||||||
|
|
||||||
|
Opus, when coupled with an appropriate container format, is also suitable
|
||||||
|
for non-realtime stored-file applications such as music distribution, game
|
||||||
|
soundtracks, portable music players, jukeboxes, and other applications that
|
||||||
|
have historically used high latency formats such as MP3, AAC, or Vorbis.
|
||||||
|
|
||||||
|
Opus is specified by IETF RFC 6716:
|
||||||
|
https://tools.ietf.org/html/rfc6716
|
||||||
|
|
||||||
|
The Opus format and this implementation of it are subject to the royalty-
|
||||||
|
free patent and copyright licenses specified in the file COPYING.
|
||||||
|
|
||||||
|
This package implements a shared library for encoding and decoding raw Opus
|
||||||
|
bitstreams. Raw Opus bitstreams should be used over RTP according to
|
||||||
|
https://tools.ietf.org/html/rfc7587
|
||||||
|
|
||||||
|
The package also includes a number of test tools used for testing the
|
||||||
|
correct operation of the library. The bitstreams read/written by these
|
||||||
|
tools should not be used for Opus file distribution: They include
|
||||||
|
additional debugging data and cannot support seeking.
|
||||||
|
|
||||||
|
Opus stored in files should use the Ogg encapsulation for Opus which is
|
||||||
|
described at:
|
||||||
|
https://tools.ietf.org/html/rfc7845
|
||||||
|
|
||||||
|
An opus-tools package is available which provides encoding and decoding of
|
||||||
|
Ogg encapsulated Opus files and includes a number of useful features.
|
||||||
|
|
||||||
|
Opus-tools can be found at:
|
||||||
|
https://gitlab.xiph.org/xiph/opus-tools.git
|
||||||
|
or on the main Opus website:
|
||||||
|
https://opus-codec.org/
|
||||||
|
|
||||||
|
== Compiling libopus ==
|
||||||
|
|
||||||
|
To build from a distribution tarball, you only need to do the following:
|
||||||
|
|
||||||
|
% ./configure
|
||||||
|
% make
|
||||||
|
|
||||||
|
To build from the git repository, the following steps are necessary:
|
||||||
|
|
||||||
|
0) Set up a development environment:
|
||||||
|
|
||||||
|
On an Ubuntu or Debian family Linux distribution:
|
||||||
|
|
||||||
|
% sudo apt-get install git autoconf automake libtool gcc make
|
||||||
|
|
||||||
|
On a Fedora/Redhat based Linux:
|
||||||
|
|
||||||
|
% sudo dnf install git autoconf automake libtool gcc make
|
||||||
|
|
||||||
|
Or for older Redhat/Centos Linux releases:
|
||||||
|
|
||||||
|
% sudo yum install git autoconf automake libtool gcc make
|
||||||
|
|
||||||
|
On Apple macOS, install Xcode and brew.sh, then in the Terminal enter:
|
||||||
|
|
||||||
|
% brew install autoconf automake libtool
|
||||||
|
|
||||||
|
1) Clone the repository:
|
||||||
|
|
||||||
|
% git clone https://gitlab.xiph.org/xiph/opus.git
|
||||||
|
% cd opus
|
||||||
|
|
||||||
|
2) Compiling the source
|
||||||
|
|
||||||
|
% ./autogen.sh
|
||||||
|
% ./configure
|
||||||
|
% make
|
||||||
|
|
||||||
|
3) Install the codec libraries (optional)
|
||||||
|
|
||||||
|
% sudo make install
|
||||||
|
|
||||||
|
Once you have compiled the codec, there will be a opus_demo executable
|
||||||
|
in the top directory.
|
||||||
|
|
||||||
|
Usage: opus_demo [-e] <application> <sampling rate (Hz)> <channels (1/2)>
|
||||||
|
<bits per second> [options] <input> <output>
|
||||||
|
opus_demo -d <sampling rate (Hz)> <channels (1/2)> [options]
|
||||||
|
<input> <output>
|
||||||
|
|
||||||
|
mode: voip | audio | restricted-lowdelay
|
||||||
|
options:
|
||||||
|
-e : only runs the encoder (output the bit-stream)
|
||||||
|
-d : only runs the decoder (reads the bit-stream as input)
|
||||||
|
-cbr : enable constant bitrate; default: variable bitrate
|
||||||
|
-cvbr : enable constrained variable bitrate; default:
|
||||||
|
unconstrained
|
||||||
|
-bandwidth <NB|MB|WB|SWB|FB>
|
||||||
|
: audio bandwidth (from narrowband to fullband);
|
||||||
|
default: sampling rate
|
||||||
|
-framesize <2.5|5|10|20|40|60>
|
||||||
|
: frame size in ms; default: 20
|
||||||
|
-max_payload <bytes>
|
||||||
|
: maximum payload size in bytes, default: 1024
|
||||||
|
-complexity <comp>
|
||||||
|
: complexity, 0 (lowest) ... 10 (highest); default: 10
|
||||||
|
-inbandfec : enable SILK inband FEC
|
||||||
|
-forcemono : force mono encoding, even for stereo input
|
||||||
|
-dtx : enable SILK DTX
|
||||||
|
-loss <perc> : simulate packet loss, in percent (0-100); default: 0
|
||||||
|
|
||||||
|
input and output are little-endian signed 16-bit PCM files or opus
|
||||||
|
bitstreams with simple opus_demo proprietary framing.
|
||||||
|
|
||||||
|
== Testing ==
|
||||||
|
|
||||||
|
This package includes a collection of automated unit and system tests
|
||||||
|
which SHOULD be run after compiling the package especially the first
|
||||||
|
time it is run on a new platform.
|
||||||
|
|
||||||
|
To run the integrated tests:
|
||||||
|
|
||||||
|
% make check
|
||||||
|
|
||||||
|
There is also collection of standard test vectors which are not
|
||||||
|
included in this package for size reasons but can be obtained from:
|
||||||
|
https://opus-codec.org/docs/opus_testvectors-rfc8251.tar.gz
|
||||||
|
|
||||||
|
To run compare the code to these test vectors:
|
||||||
|
|
||||||
|
% curl -OL https://opus-codec.org/docs/opus_testvectors-rfc8251.tar.gz
|
||||||
|
% tar -zxf opus_testvectors-rfc8251.tar.gz
|
||||||
|
% ./tests/run_vectors.sh ./ opus_newvectors 48000
|
||||||
|
|
||||||
|
== Portability notes ==
|
||||||
|
|
||||||
|
This implementation uses floating-point by default but can be compiled to
|
||||||
|
use only fixed-point arithmetic by setting --enable-fixed-point (if using
|
||||||
|
autoconf) or by defining the FIXED_POINT macro (if building manually).
|
||||||
|
The fixed point implementation has somewhat lower audio quality and is
|
||||||
|
slower on platforms with fast FPUs, it is normally only used in embedded
|
||||||
|
environments.
|
||||||
|
|
||||||
|
The implementation can be compiled with either a C89 or a C99 compiler.
|
||||||
|
While it does not rely on any _undefined behavior_ as defined by C89 or
|
||||||
|
C99, it relies on common _implementation-defined behavior_ for two's
|
||||||
|
complement architectures:
|
||||||
|
|
||||||
|
o Right shifts of negative values are consistent with two's
|
||||||
|
complement arithmetic, so that a>>b is equivalent to
|
||||||
|
floor(a/(2^b)),
|
||||||
|
|
||||||
|
o For conversion to a signed integer of N bits, the value is reduced
|
||||||
|
modulo 2^N to be within range of the type,
|
||||||
|
|
||||||
|
o The result of integer division of a negative value is truncated
|
||||||
|
towards zero, and
|
||||||
|
|
||||||
|
o The compiler provides a 64-bit integer type (a C99 requirement
|
||||||
|
which is supported by most C89 compilers).
|
||||||
54
vendor/audiopus_sys/opus/README.draft
vendored
Normal file
54
vendor/audiopus_sys/opus/README.draft
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
To build this source code, simply type:
|
||||||
|
|
||||||
|
% make
|
||||||
|
|
||||||
|
If this does not work, or if you want to change the default configuration
|
||||||
|
(e.g., to compile for a fixed-point architecture), simply edit the options
|
||||||
|
in the Makefile.
|
||||||
|
|
||||||
|
An up-to-date implementation conforming to this standard is available in a
|
||||||
|
Git repository at https://gitlab.xiph.org/xiph/opus.git or on a website at:
|
||||||
|
https://opus-codec.org/
|
||||||
|
However, although that implementation is expected to remain conformant
|
||||||
|
with the standard, it is the code in this RFC that shall remain normative.
|
||||||
|
To build from the git repository instead of using this RFC, follow these
|
||||||
|
steps:
|
||||||
|
|
||||||
|
1) Clone the repository (latest implementation of this standard at the time
|
||||||
|
of publication)
|
||||||
|
|
||||||
|
% git clone https://gitlab.xiph.org/xiph/opus.git
|
||||||
|
% cd opus
|
||||||
|
|
||||||
|
2) Compile
|
||||||
|
|
||||||
|
% ./autogen.sh
|
||||||
|
% ./configure
|
||||||
|
% make
|
||||||
|
|
||||||
|
Once you have compiled the codec, there will be a opus_demo executable in
|
||||||
|
the top directory.
|
||||||
|
|
||||||
|
Usage: opus_demo [-e] <application> <sampling rate (Hz)> <channels (1/2)>
|
||||||
|
<bits per second> [options] <input> <output>
|
||||||
|
opus_demo -d <sampling rate (Hz)> <channels (1/2)> [options]
|
||||||
|
<input> <output>
|
||||||
|
|
||||||
|
mode: voip | audio | restricted-lowdelay
|
||||||
|
options:
|
||||||
|
-e : only runs the encoder (output the bit-stream)
|
||||||
|
-d : only runs the decoder (reads the bit-stream as input)
|
||||||
|
-cbr : enable constant bitrate; default: variable bitrate
|
||||||
|
-cvbr : enable constrained variable bitrate; default: unconstrained
|
||||||
|
-bandwidth <NB|MB|WB|SWB|FB> : audio bandwidth (from narrowband to fullband);
|
||||||
|
default: sampling rate
|
||||||
|
-framesize <2.5|5|10|20|40|60> : frame size in ms; default: 20
|
||||||
|
-max_payload <bytes> : maximum payload size in bytes, default: 1024
|
||||||
|
-complexity <comp> : complexity, 0 (lowest) ... 10 (highest); default: 10
|
||||||
|
-inbandfec : enable SILK inband FEC
|
||||||
|
-forcemono : force mono encoding, even for stereo input
|
||||||
|
-dtx : enable SILK DTX
|
||||||
|
-loss <perc> : simulate packet loss, in percent (0-100); default: 0
|
||||||
|
|
||||||
|
input and output are little endian signed 16-bit PCM files or opus bitstreams
|
||||||
|
with simple opus_demo proprietary framing.
|
||||||
14
vendor/audiopus_sys/opus/autogen.sh
vendored
Executable file
14
vendor/audiopus_sys/opus/autogen.sh
vendored
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Copyright (c) 2010-2015 Xiph.Org Foundation and contributors.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the COPYING file.
|
||||||
|
|
||||||
|
# Run this to set up the build system: configure, makefiles, etc.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
srcdir=`dirname $0`
|
||||||
|
test -n "$srcdir" && cd "$srcdir"
|
||||||
|
|
||||||
|
echo "Updating build configuration files, please wait...."
|
||||||
|
|
||||||
|
autoreconf -isf
|
||||||
182
vendor/audiopus_sys/opus/celt/_kiss_fft_guts.h
vendored
Normal file
182
vendor/audiopus_sys/opus/celt/_kiss_fft_guts.h
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/*Copyright (c) 2003-2004, Mark Borgerding
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.*/
|
||||||
|
|
||||||
|
#ifndef KISS_FFT_GUTS_H
|
||||||
|
#define KISS_FFT_GUTS_H
|
||||||
|
|
||||||
|
#define MIN(a,b) ((a)<(b) ? (a):(b))
|
||||||
|
#define MAX(a,b) ((a)>(b) ? (a):(b))
|
||||||
|
|
||||||
|
/* kiss_fft.h
|
||||||
|
defines kiss_fft_scalar as either short or a float type
|
||||||
|
and defines
|
||||||
|
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
|
||||||
|
#include "kiss_fft.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Explanation of macros dealing with complex math:
|
||||||
|
|
||||||
|
C_MUL(m,a,b) : m = a*b
|
||||||
|
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
|
||||||
|
C_SUB( res, a,b) : res = a - b
|
||||||
|
C_SUBFROM( res , a) : res -= a
|
||||||
|
C_ADDTO( res , a) : res += a
|
||||||
|
* */
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
#include "arch.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define SAMP_MAX 2147483647
|
||||||
|
#define TWID_MAX 32767
|
||||||
|
#define TRIG_UPSCALE 1
|
||||||
|
|
||||||
|
#define SAMP_MIN -SAMP_MAX
|
||||||
|
|
||||||
|
|
||||||
|
# define S_MUL(a,b) MULT16_32_Q15(b, a)
|
||||||
|
|
||||||
|
# define C_MUL(m,a,b) \
|
||||||
|
do{ (m).r = SUB32_ovflw(S_MUL((a).r,(b).r) , S_MUL((a).i,(b).i)); \
|
||||||
|
(m).i = ADD32_ovflw(S_MUL((a).r,(b).i) , S_MUL((a).i,(b).r)); }while(0)
|
||||||
|
|
||||||
|
# define C_MULC(m,a,b) \
|
||||||
|
do{ (m).r = ADD32_ovflw(S_MUL((a).r,(b).r) , S_MUL((a).i,(b).i)); \
|
||||||
|
(m).i = SUB32_ovflw(S_MUL((a).i,(b).r) , S_MUL((a).r,(b).i)); }while(0)
|
||||||
|
|
||||||
|
# define C_MULBYSCALAR( c, s ) \
|
||||||
|
do{ (c).r = S_MUL( (c).r , s ) ;\
|
||||||
|
(c).i = S_MUL( (c).i , s ) ; }while(0)
|
||||||
|
|
||||||
|
# define DIVSCALAR(x,k) \
|
||||||
|
(x) = S_MUL( x, (TWID_MAX-((k)>>1))/(k)+1 )
|
||||||
|
|
||||||
|
# define C_FIXDIV(c,div) \
|
||||||
|
do { DIVSCALAR( (c).r , div); \
|
||||||
|
DIVSCALAR( (c).i , div); }while (0)
|
||||||
|
|
||||||
|
#define C_ADD( res, a,b)\
|
||||||
|
do {(res).r=ADD32_ovflw((a).r,(b).r); (res).i=ADD32_ovflw((a).i,(b).i); \
|
||||||
|
}while(0)
|
||||||
|
#define C_SUB( res, a,b)\
|
||||||
|
do {(res).r=SUB32_ovflw((a).r,(b).r); (res).i=SUB32_ovflw((a).i,(b).i); \
|
||||||
|
}while(0)
|
||||||
|
#define C_ADDTO( res , a)\
|
||||||
|
do {(res).r = ADD32_ovflw((res).r, (a).r); (res).i = ADD32_ovflw((res).i,(a).i);\
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
#define C_SUBFROM( res , a)\
|
||||||
|
do {(res).r = ADD32_ovflw((res).r,(a).r); (res).i = SUB32_ovflw((res).i,(a).i); \
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
#if defined(OPUS_ARM_INLINE_ASM)
|
||||||
|
#include "arm/kiss_fft_armv4.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(OPUS_ARM_INLINE_EDSP)
|
||||||
|
#include "arm/kiss_fft_armv5e.h"
|
||||||
|
#endif
|
||||||
|
#if defined(MIPSr1_ASM)
|
||||||
|
#include "mips/kiss_fft_mipsr1.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else /* not FIXED_POINT*/
|
||||||
|
|
||||||
|
# define S_MUL(a,b) ( (a)*(b) )
|
||||||
|
#define C_MUL(m,a,b) \
|
||||||
|
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
|
||||||
|
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
|
||||||
|
#define C_MULC(m,a,b) \
|
||||||
|
do{ (m).r = (a).r*(b).r + (a).i*(b).i;\
|
||||||
|
(m).i = (a).i*(b).r - (a).r*(b).i; }while(0)
|
||||||
|
|
||||||
|
#define C_MUL4(m,a,b) C_MUL(m,a,b)
|
||||||
|
|
||||||
|
# define C_FIXDIV(c,div) /* NOOP */
|
||||||
|
# define C_MULBYSCALAR( c, s ) \
|
||||||
|
do{ (c).r *= (s);\
|
||||||
|
(c).i *= (s); }while(0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CHECK_OVERFLOW_OP
|
||||||
|
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef C_ADD
|
||||||
|
#define C_ADD( res, a,b)\
|
||||||
|
do { \
|
||||||
|
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
|
||||||
|
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
|
||||||
|
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
|
||||||
|
}while(0)
|
||||||
|
#define C_SUB( res, a,b)\
|
||||||
|
do { \
|
||||||
|
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
|
||||||
|
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
|
||||||
|
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
|
||||||
|
}while(0)
|
||||||
|
#define C_ADDTO( res , a)\
|
||||||
|
do { \
|
||||||
|
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
|
||||||
|
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
|
||||||
|
(res).r += (a).r; (res).i += (a).i;\
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
#define C_SUBFROM( res , a)\
|
||||||
|
do {\
|
||||||
|
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
|
||||||
|
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
|
||||||
|
(res).r -= (a).r; (res).i -= (a).i; \
|
||||||
|
}while(0)
|
||||||
|
#endif /* C_ADD defined */
|
||||||
|
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
/*# define KISS_FFT_COS(phase) TRIG_UPSCALE*floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase))))
|
||||||
|
# define KISS_FFT_SIN(phase) TRIG_UPSCALE*floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase))))*/
|
||||||
|
# define KISS_FFT_COS(phase) floor(.5+TWID_MAX*cos (phase))
|
||||||
|
# define KISS_FFT_SIN(phase) floor(.5+TWID_MAX*sin (phase))
|
||||||
|
# define HALF_OF(x) ((x)>>1)
|
||||||
|
#elif defined(USE_SIMD)
|
||||||
|
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
|
||||||
|
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
|
||||||
|
# define HALF_OF(x) ((x)*_mm_set1_ps(.5f))
|
||||||
|
#else
|
||||||
|
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
|
||||||
|
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
|
||||||
|
# define HALF_OF(x) ((x)*.5f)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define kf_cexp(x,phase) \
|
||||||
|
do{ \
|
||||||
|
(x)->r = KISS_FFT_COS(phase);\
|
||||||
|
(x)->i = KISS_FFT_SIN(phase);\
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
#define kf_cexp2(x,phase) \
|
||||||
|
do{ \
|
||||||
|
(x)->r = TRIG_UPSCALE*celt_cos_norm((phase));\
|
||||||
|
(x)->i = TRIG_UPSCALE*celt_cos_norm((phase)-32768);\
|
||||||
|
}while(0)
|
||||||
|
|
||||||
|
#endif /* KISS_FFT_GUTS_H */
|
||||||
291
vendor/audiopus_sys/opus/celt/arch.h
vendored
Normal file
291
vendor/audiopus_sys/opus/celt/arch.h
vendored
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
/* Copyright (c) 2003-2008 Jean-Marc Valin
|
||||||
|
Copyright (c) 2007-2008 CSIRO
|
||||||
|
Copyright (c) 2007-2009 Xiph.Org Foundation
|
||||||
|
Written by Jean-Marc Valin */
|
||||||
|
/**
|
||||||
|
@file arch.h
|
||||||
|
@brief Various architecture definitions for CELT
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ARCH_H
|
||||||
|
#define ARCH_H
|
||||||
|
|
||||||
|
#include "opus_types.h"
|
||||||
|
#include "opus_defines.h"
|
||||||
|
|
||||||
|
# if !defined(__GNUC_PREREQ)
|
||||||
|
# if defined(__GNUC__)&&defined(__GNUC_MINOR__)
|
||||||
|
# define __GNUC_PREREQ(_maj,_min) \
|
||||||
|
((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min))
|
||||||
|
# else
|
||||||
|
# define __GNUC_PREREQ(_maj,_min) 0
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#if OPUS_GNUC_PREREQ(3, 0)
|
||||||
|
#define opus_likely(x) (__builtin_expect(!!(x), 1))
|
||||||
|
#define opus_unlikely(x) (__builtin_expect(!!(x), 0))
|
||||||
|
#else
|
||||||
|
#define opus_likely(x) (!!(x))
|
||||||
|
#define opus_unlikely(x) (!!(x))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CELT_SIG_SCALE 32768.f
|
||||||
|
|
||||||
|
#define CELT_FATAL(str) celt_fatal(str, __FILE__, __LINE__);
|
||||||
|
|
||||||
|
#if defined(ENABLE_ASSERTIONS) || defined(ENABLE_HARDENING)
|
||||||
|
#ifdef __GNUC__
|
||||||
|
__attribute__((noreturn))
|
||||||
|
#endif
|
||||||
|
void celt_fatal(const char *str, const char *file, int line);
|
||||||
|
|
||||||
|
#if defined(CELT_C) && !defined(OVERRIDE_celt_fatal)
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#ifdef __GNUC__
|
||||||
|
__attribute__((noreturn))
|
||||||
|
#endif
|
||||||
|
void celt_fatal(const char *str, const char *file, int line)
|
||||||
|
{
|
||||||
|
fprintf (stderr, "Fatal (internal) error in %s, line %d: %s\n", file, line, str);
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
_set_abort_behavior( 0, _WRITE_ABORT_MSG);
|
||||||
|
#endif
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define celt_assert(cond) {if (!(cond)) {CELT_FATAL("assertion failed: " #cond);}}
|
||||||
|
#define celt_assert2(cond, message) {if (!(cond)) {CELT_FATAL("assertion failed: " #cond "\n" message);}}
|
||||||
|
#define MUST_SUCCEED(call) celt_assert((call) == OPUS_OK)
|
||||||
|
#else
|
||||||
|
#define celt_assert(cond)
|
||||||
|
#define celt_assert2(cond, message)
|
||||||
|
#define MUST_SUCCEED(call) do {if((call) != OPUS_OK) {RESTORE_STACK; return OPUS_INTERNAL_ERROR;} } while (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(ENABLE_ASSERTIONS)
|
||||||
|
#define celt_sig_assert(cond) {if (!(cond)) {CELT_FATAL("signal assertion failed: " #cond);}}
|
||||||
|
#else
|
||||||
|
#define celt_sig_assert(cond)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define IMUL32(a,b) ((a)*(b))
|
||||||
|
|
||||||
|
#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum 16-bit value. */
|
||||||
|
#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */
|
||||||
|
#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum 32-bit value. */
|
||||||
|
#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */
|
||||||
|
#define IMIN(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum int value. */
|
||||||
|
#define IMAX(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum int value. */
|
||||||
|
#define UADD32(a,b) ((a)+(b))
|
||||||
|
#define USUB32(a,b) ((a)-(b))
|
||||||
|
|
||||||
|
/* Set this if opus_int64 is a native type of the CPU. */
|
||||||
|
/* Assume that all LP64 architectures have fast 64-bit types; also x86_64
|
||||||
|
(which can be ILP32 for x32) and Win64 (which is LLP64). */
|
||||||
|
#if defined(__x86_64__) || defined(__LP64__) || defined(_WIN64)
|
||||||
|
#define OPUS_FAST_INT64 1
|
||||||
|
#else
|
||||||
|
#define OPUS_FAST_INT64 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PRINT_MIPS(file)
|
||||||
|
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
|
||||||
|
typedef opus_int16 opus_val16;
|
||||||
|
typedef opus_int32 opus_val32;
|
||||||
|
typedef opus_int64 opus_val64;
|
||||||
|
|
||||||
|
typedef opus_val32 celt_sig;
|
||||||
|
typedef opus_val16 celt_norm;
|
||||||
|
typedef opus_val32 celt_ener;
|
||||||
|
|
||||||
|
#define celt_isnan(x) 0
|
||||||
|
|
||||||
|
#define Q15ONE 32767
|
||||||
|
|
||||||
|
#define SIG_SHIFT 12
|
||||||
|
/* Safe saturation value for 32-bit signals. Should be less than
|
||||||
|
2^31*(1-0.85) to avoid blowing up on DC at deemphasis.*/
|
||||||
|
#define SIG_SAT (300000000)
|
||||||
|
|
||||||
|
#define NORM_SCALING 16384
|
||||||
|
|
||||||
|
#define DB_SHIFT 10
|
||||||
|
|
||||||
|
#define EPSILON 1
|
||||||
|
#define VERY_SMALL 0
|
||||||
|
#define VERY_LARGE16 ((opus_val16)32767)
|
||||||
|
#define Q15_ONE ((opus_val16)32767)
|
||||||
|
|
||||||
|
#define SCALEIN(a) (a)
|
||||||
|
#define SCALEOUT(a) (a)
|
||||||
|
|
||||||
|
#define ABS16(x) ((x) < 0 ? (-(x)) : (x))
|
||||||
|
#define ABS32(x) ((x) < 0 ? (-(x)) : (x))
|
||||||
|
|
||||||
|
static OPUS_INLINE opus_int16 SAT16(opus_int32 x) {
|
||||||
|
return x > 32767 ? 32767 : x < -32768 ? -32768 : (opus_int16)x;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef FIXED_DEBUG
|
||||||
|
#include "fixed_debug.h"
|
||||||
|
#else
|
||||||
|
|
||||||
|
#include "fixed_generic.h"
|
||||||
|
|
||||||
|
#ifdef OPUS_ARM_PRESUME_AARCH64_NEON_INTR
|
||||||
|
#include "arm/fixed_arm64.h"
|
||||||
|
#elif defined (OPUS_ARM_INLINE_EDSP)
|
||||||
|
#include "arm/fixed_armv5e.h"
|
||||||
|
#elif defined (OPUS_ARM_INLINE_ASM)
|
||||||
|
#include "arm/fixed_armv4.h"
|
||||||
|
#elif defined (BFIN_ASM)
|
||||||
|
#include "fixed_bfin.h"
|
||||||
|
#elif defined (TI_C5X_ASM)
|
||||||
|
#include "fixed_c5x.h"
|
||||||
|
#elif defined (TI_C6X_ASM)
|
||||||
|
#include "fixed_c6x.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#else /* FIXED_POINT */
|
||||||
|
|
||||||
|
typedef float opus_val16;
|
||||||
|
typedef float opus_val32;
|
||||||
|
typedef float opus_val64;
|
||||||
|
|
||||||
|
typedef float celt_sig;
|
||||||
|
typedef float celt_norm;
|
||||||
|
typedef float celt_ener;
|
||||||
|
|
||||||
|
#ifdef FLOAT_APPROX
|
||||||
|
/* This code should reliably detect NaN/inf even when -ffast-math is used.
|
||||||
|
Assumes IEEE 754 format. */
|
||||||
|
static OPUS_INLINE int celt_isnan(float x)
|
||||||
|
{
|
||||||
|
union {float f; opus_uint32 i;} in;
|
||||||
|
in.f = x;
|
||||||
|
return ((in.i>>23)&0xFF)==0xFF && (in.i&0x007FFFFF)!=0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#ifdef __FAST_MATH__
|
||||||
|
#error Cannot build libopus with -ffast-math unless FLOAT_APPROX is defined. This could result in crashes on extreme (e.g. NaN) input
|
||||||
|
#endif
|
||||||
|
#define celt_isnan(x) ((x)!=(x))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define Q15ONE 1.0f
|
||||||
|
|
||||||
|
#define NORM_SCALING 1.f
|
||||||
|
|
||||||
|
#define EPSILON 1e-15f
|
||||||
|
#define VERY_SMALL 1e-30f
|
||||||
|
#define VERY_LARGE16 1e15f
|
||||||
|
#define Q15_ONE ((opus_val16)1.f)
|
||||||
|
|
||||||
|
/* This appears to be the same speed as C99's fabsf() but it's more portable. */
|
||||||
|
#define ABS16(x) ((float)fabs(x))
|
||||||
|
#define ABS32(x) ((float)fabs(x))
|
||||||
|
|
||||||
|
#define QCONST16(x,bits) (x)
|
||||||
|
#define QCONST32(x,bits) (x)
|
||||||
|
|
||||||
|
#define NEG16(x) (-(x))
|
||||||
|
#define NEG32(x) (-(x))
|
||||||
|
#define NEG32_ovflw(x) (-(x))
|
||||||
|
#define EXTRACT16(x) (x)
|
||||||
|
#define EXTEND32(x) (x)
|
||||||
|
#define SHR16(a,shift) (a)
|
||||||
|
#define SHL16(a,shift) (a)
|
||||||
|
#define SHR32(a,shift) (a)
|
||||||
|
#define SHL32(a,shift) (a)
|
||||||
|
#define PSHR32(a,shift) (a)
|
||||||
|
#define VSHR32(a,shift) (a)
|
||||||
|
|
||||||
|
#define PSHR(a,shift) (a)
|
||||||
|
#define SHR(a,shift) (a)
|
||||||
|
#define SHL(a,shift) (a)
|
||||||
|
#define SATURATE(x,a) (x)
|
||||||
|
#define SATURATE16(x) (x)
|
||||||
|
|
||||||
|
#define ROUND16(a,shift) (a)
|
||||||
|
#define SROUND16(a,shift) (a)
|
||||||
|
#define HALF16(x) (.5f*(x))
|
||||||
|
#define HALF32(x) (.5f*(x))
|
||||||
|
|
||||||
|
#define ADD16(a,b) ((a)+(b))
|
||||||
|
#define SUB16(a,b) ((a)-(b))
|
||||||
|
#define ADD32(a,b) ((a)+(b))
|
||||||
|
#define SUB32(a,b) ((a)-(b))
|
||||||
|
#define ADD32_ovflw(a,b) ((a)+(b))
|
||||||
|
#define SUB32_ovflw(a,b) ((a)-(b))
|
||||||
|
#define MULT16_16_16(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16(a,b) ((opus_val32)(a)*(opus_val32)(b))
|
||||||
|
#define MAC16_16(c,a,b) ((c)+(opus_val32)(a)*(opus_val32)(b))
|
||||||
|
|
||||||
|
#define MULT16_32_Q15(a,b) ((a)*(b))
|
||||||
|
#define MULT16_32_Q16(a,b) ((a)*(b))
|
||||||
|
|
||||||
|
#define MULT32_32_Q31(a,b) ((a)*(b))
|
||||||
|
|
||||||
|
#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b))
|
||||||
|
#define MAC16_32_Q16(c,a,b) ((c)+(a)*(b))
|
||||||
|
|
||||||
|
#define MULT16_16_Q11_32(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_Q11(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_Q13(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_Q14(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_Q15(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_P15(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_P13(a,b) ((a)*(b))
|
||||||
|
#define MULT16_16_P14(a,b) ((a)*(b))
|
||||||
|
#define MULT16_32_P16(a,b) ((a)*(b))
|
||||||
|
|
||||||
|
#define DIV32_16(a,b) (((opus_val32)(a))/(opus_val16)(b))
|
||||||
|
#define DIV32(a,b) (((opus_val32)(a))/(opus_val32)(b))
|
||||||
|
|
||||||
|
#define SCALEIN(a) ((a)*CELT_SIG_SCALE)
|
||||||
|
#define SCALEOUT(a) ((a)*(1/CELT_SIG_SCALE))
|
||||||
|
|
||||||
|
#define SIG2WORD16(x) (x)
|
||||||
|
|
||||||
|
#endif /* !FIXED_POINT */
|
||||||
|
|
||||||
|
#ifndef GLOBAL_STACK_SIZE
|
||||||
|
#ifdef FIXED_POINT
|
||||||
|
#define GLOBAL_STACK_SIZE 120000
|
||||||
|
#else
|
||||||
|
#define GLOBAL_STACK_SIZE 120000
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* ARCH_H */
|
||||||
353
vendor/audiopus_sys/opus/celt/arm/arm2gnu.pl
vendored
Executable file
353
vendor/audiopus_sys/opus/celt/arm/arm2gnu.pl
vendored
Executable file
@@ -0,0 +1,353 @@
|
|||||||
|
#!/usr/bin/perl
|
||||||
|
# Copyright (C) 2002-2013 Xiph.org Foundation
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions
|
||||||
|
# are met:
|
||||||
|
#
|
||||||
|
# - Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# - Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
my $bigend; # little/big endian
|
||||||
|
my $nxstack;
|
||||||
|
my $apple = 0;
|
||||||
|
my $symprefix = "";
|
||||||
|
|
||||||
|
$nxstack = 0;
|
||||||
|
|
||||||
|
eval 'exec /usr/local/bin/perl -S $0 ${1+"$@"}'
|
||||||
|
if $running_under_some_shell;
|
||||||
|
|
||||||
|
while ($ARGV[0] =~ /^-/) {
|
||||||
|
$_ = shift;
|
||||||
|
last if /^--$/;
|
||||||
|
if (/^-n$/) {
|
||||||
|
$nflag++;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
if (/^--apple$/) {
|
||||||
|
$apple = 1;
|
||||||
|
$symprefix = "_";
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
die "I don't recognize this switch: $_\\n";
|
||||||
|
}
|
||||||
|
$printit++ unless $nflag;
|
||||||
|
|
||||||
|
$\ = "\n"; # automatically add newline on print
|
||||||
|
$n=0;
|
||||||
|
|
||||||
|
$thumb = 0; # ARM mode by default, not Thumb.
|
||||||
|
@proc_stack = ();
|
||||||
|
|
||||||
|
printf (" .syntax unified\n");
|
||||||
|
|
||||||
|
LINE:
|
||||||
|
while (<>) {
|
||||||
|
|
||||||
|
# For ADRLs we need to add a new line after the substituted one.
|
||||||
|
$addPadding = 0;
|
||||||
|
|
||||||
|
# First, we do not dare to touch *anything* inside double quotes, do we?
|
||||||
|
# Second, if you want a dollar character in the string,
|
||||||
|
# insert two of them -- that's how ARM C and assembler treat strings.
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+DCB[ \t]*\"/$1: .ascii \"/ && do { s/\$\$/\$/g; next };
|
||||||
|
s/\bDCB\b[ \t]*\"/.ascii \"/ && do { s/\$\$/\$/g; next };
|
||||||
|
s/^(\S+)\s+RN\s+(\S+)/$1 .req r$2/ && do { s/\$\$/\$/g; next };
|
||||||
|
# If there's nothing on a line but a comment, don't try to apply any further
|
||||||
|
# substitutions (this is a cheap hack to avoid mucking up the license header)
|
||||||
|
s/^([ \t]*);/$1@/ && do { s/\$\$/\$/g; next };
|
||||||
|
# If substituted -- leave immediately !
|
||||||
|
|
||||||
|
s/@/,:/;
|
||||||
|
s/;/@/;
|
||||||
|
while ( /@.*'/ ) {
|
||||||
|
s/(@.*)'/$1/g;
|
||||||
|
}
|
||||||
|
s/\{FALSE\}/0/g;
|
||||||
|
s/\{TRUE\}/1/g;
|
||||||
|
s/\{(\w\w\w\w+)\}/$1/g;
|
||||||
|
s/\bINCLUDE[ \t]*([^ \t\n]+)/.include \"$1\"/;
|
||||||
|
s/\bGET[ \t]*([^ \t\n]+)/.include \"${ my $x=$1; $x =~ s|\.s|-gnu.S|; \$x }\"/;
|
||||||
|
s/\bIMPORT\b/.extern/;
|
||||||
|
s/\bEXPORT\b\s*/.global $symprefix/;
|
||||||
|
s/^(\s+)\[/$1IF/;
|
||||||
|
s/^(\s+)\|/$1ELSE/;
|
||||||
|
s/^(\s+)\]/$1ENDIF/;
|
||||||
|
s/IF *:DEF:/ .ifdef/;
|
||||||
|
s/IF *:LNOT: *:DEF:/ .ifndef/;
|
||||||
|
s/ELSE/ .else/;
|
||||||
|
s/ENDIF/ .endif/;
|
||||||
|
|
||||||
|
if( /\bIF\b/ ) {
|
||||||
|
s/\bIF\b/ .if/;
|
||||||
|
s/=/==/;
|
||||||
|
}
|
||||||
|
if ( $n == 2) {
|
||||||
|
s/\$/\\/g;
|
||||||
|
}
|
||||||
|
if ($n == 1) {
|
||||||
|
s/\$//g;
|
||||||
|
s/label//g;
|
||||||
|
$n = 2;
|
||||||
|
}
|
||||||
|
if ( /MACRO/ ) {
|
||||||
|
s/MACRO *\n/.macro/;
|
||||||
|
$n=1;
|
||||||
|
}
|
||||||
|
if ( /\bMEND\b/ ) {
|
||||||
|
s/\bMEND\b/.endm/;
|
||||||
|
$n=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ".rdata" doesn't work in 'as' version 2.13.2, as it is ".rodata" there.
|
||||||
|
#
|
||||||
|
if ( /\bAREA\b/ ) {
|
||||||
|
my $align;
|
||||||
|
$align = "2";
|
||||||
|
if ( /ALIGN=(\d+)/ ) {
|
||||||
|
$align = $1;
|
||||||
|
}
|
||||||
|
if ( /CODE/ ) {
|
||||||
|
$nxstack = 1;
|
||||||
|
}
|
||||||
|
s/^(.+)CODE(.+)READONLY(.*)/ .text/;
|
||||||
|
s/^(.+)DATA(.+)READONLY(.*)/ .section .rdata/;
|
||||||
|
s/^(.+)\|\|\.data\|\|(.+)/ .data/;
|
||||||
|
s/^(.+)\|\|\.bss\|\|(.+)/ .bss/;
|
||||||
|
s/$/; .p2align $align/;
|
||||||
|
# Enable NEON instructions but don't produce a binary that requires
|
||||||
|
# ARMv7. RVCT does not have equivalent directives, so we just do this
|
||||||
|
# for all CODE areas.
|
||||||
|
if ( /.text/ ) {
|
||||||
|
# Separating .arch, .fpu, etc., by semicolons does not work (gas
|
||||||
|
# thinks the semicolon is part of the arch name, even when there's
|
||||||
|
# whitespace separating them). Sadly this means our line numbers
|
||||||
|
# won't match the original source file (we could use the .line
|
||||||
|
# directive, which is documented to be obsolete, but then gdb will
|
||||||
|
# show the wrong line in the translated source file).
|
||||||
|
s/$/; .arch armv7-a\n .fpu neon\n .object_arch armv4t/ unless ($apple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s/\|\|\.constdata\$(\d+)\|\|/.L_CONST$1/; # ||.constdata$3||
|
||||||
|
s/\|\|\.bss\$(\d+)\|\|/.L_BSS$1/; # ||.bss$2||
|
||||||
|
s/\|\|\.data\$(\d+)\|\|/.L_DATA$1/; # ||.data$2||
|
||||||
|
s/\|\|([a-zA-Z0-9_]+)\@([a-zA-Z0-9_]+)\|\|/@ $&/;
|
||||||
|
s/^(\s+)\%(\s)/ .space $1/;
|
||||||
|
|
||||||
|
s/\|(.+)\.(\d+)\|/\.$1_$2/; # |L80.123| -> .L80_123
|
||||||
|
s/\bCODE32\b/.code 32/ && do {$thumb = 0};
|
||||||
|
s/\bCODE16\b/.code 16/ && do {$thumb = 1};
|
||||||
|
if (/\bPROC\b/)
|
||||||
|
{
|
||||||
|
my $prefix;
|
||||||
|
my $proc;
|
||||||
|
/^([A-Za-z_\.]\w+)\b/;
|
||||||
|
$proc = $1;
|
||||||
|
$prefix = "";
|
||||||
|
if ($proc)
|
||||||
|
{
|
||||||
|
$prefix = $prefix.sprintf("\t.type\t%s, %%function", $proc) unless ($apple);
|
||||||
|
# Make sure we $prefix isn't empty here (for the $apple case).
|
||||||
|
# We handle mangling the label here, make sure it doesn't match
|
||||||
|
# the label handling below (if $prefix would be empty).
|
||||||
|
$prefix = $prefix."; ";
|
||||||
|
push(@proc_stack, $proc);
|
||||||
|
s/^[A-Za-z_\.]\w+/$symprefix$&:/;
|
||||||
|
}
|
||||||
|
$prefix = $prefix."\t.thumb_func; " if ($thumb);
|
||||||
|
s/\bPROC\b/@ $&/;
|
||||||
|
$_ = $prefix.$_;
|
||||||
|
}
|
||||||
|
s/^(\s*)(S|Q|SH|U|UQ|UH)ASX\b/$1$2ADDSUBX/;
|
||||||
|
s/^(\s*)(S|Q|SH|U|UQ|UH)SAX\b/$1$2SUBADDX/;
|
||||||
|
if (/\bENDP\b/)
|
||||||
|
{
|
||||||
|
my $proc;
|
||||||
|
s/\bENDP\b/@ $&/;
|
||||||
|
$proc = pop(@proc_stack);
|
||||||
|
$_ = "\t.size $proc, .-$proc".$_ if ($proc && !$apple);
|
||||||
|
}
|
||||||
|
s/\bSUBT\b/@ $&/;
|
||||||
|
s/\bDATA\b/@ $&/; # DATA directive is deprecated -- Asm guide, p.7-25
|
||||||
|
s/\bKEEP\b/@ $&/;
|
||||||
|
s/\bEXPORTAS\b/@ $&/;
|
||||||
|
s/\|\|(.)+\bEQU\b/@ $&/;
|
||||||
|
s/\|\|([\w\$]+)\|\|/$1/;
|
||||||
|
s/\bENTRY\b/@ $&/;
|
||||||
|
s/\bASSERT\b/@ $&/;
|
||||||
|
s/\bGBLL\b/@ $&/;
|
||||||
|
s/\bGBLA\b/@ $&/;
|
||||||
|
s/^\W+OPT\b/@ $&/;
|
||||||
|
s/:OR:/|/g;
|
||||||
|
s/:SHL:/<</g;
|
||||||
|
s/:SHR:/>>/g;
|
||||||
|
s/:AND:/&/g;
|
||||||
|
s/:LAND:/&&/g;
|
||||||
|
s/CPSR/cpsr/;
|
||||||
|
s/SPSR/spsr/;
|
||||||
|
s/ALIGN$/.balign 4/;
|
||||||
|
s/ALIGN\s+([0-9x]+)$/.balign $1/;
|
||||||
|
s/psr_cxsf/psr_all/;
|
||||||
|
s/LTORG/.ltorg/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+EQU/ .set $1,/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+SETL/ .set $1,/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+SETA/ .set $1,/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+\*/ .set $1,/;
|
||||||
|
|
||||||
|
# {PC} + 0xdeadfeed --> . + 0xdeadfeed
|
||||||
|
s/\{PC\} \+/ \. +/;
|
||||||
|
|
||||||
|
# Single hex constant on the line !
|
||||||
|
#
|
||||||
|
# >>> NOTE <<<
|
||||||
|
# Double-precision floats in gcc are always mixed-endian, which means
|
||||||
|
# bytes in two words are little-endian, but words are big-endian.
|
||||||
|
# So, 0x0000deadfeed0000 would be stored as 0x0000dead at low address
|
||||||
|
# and 0xfeed0000 at high address.
|
||||||
|
#
|
||||||
|
s/\bDCFD\b[ \t]+0x([a-fA-F0-9]{8})([a-fA-F0-9]{8})/.long 0x$1, 0x$2/;
|
||||||
|
# Only decimal constants on the line, no hex !
|
||||||
|
s/\bDCFD\b[ \t]+([0-9\.\-]+)/.double $1/;
|
||||||
|
|
||||||
|
# Single hex constant on the line !
|
||||||
|
# s/\bDCFS\b[ \t]+0x([a-f0-9]{8})([a-f0-9]{8})/.long 0x$1, 0x$2/;
|
||||||
|
# Only decimal constants on the line, no hex !
|
||||||
|
# s/\bDCFS\b[ \t]+([0-9\.\-]+)/.double $1/;
|
||||||
|
s/\bDCFS[ \t]+0x/.word 0x/;
|
||||||
|
s/\bDCFS\b/.float/;
|
||||||
|
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+DCD/$1 .word/;
|
||||||
|
s/\bDCD\b/.word/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+DCW/$1 .short/;
|
||||||
|
s/\bDCW\b/.short/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+DCB/$1 .byte/;
|
||||||
|
s/\bDCB\b/.byte/;
|
||||||
|
s/^([A-Za-z_]\w*)[ \t]+\%/.comm $1,/;
|
||||||
|
s/^[A-Za-z_\.]\w+/$&:/;
|
||||||
|
s/^(\d+)/$1:/;
|
||||||
|
s/\%(\d+)/$1b_or_f/;
|
||||||
|
s/\%[Bb](\d+)/$1b/;
|
||||||
|
s/\%[Ff](\d+)/$1f/;
|
||||||
|
s/\%[Ff][Tt](\d+)/$1f/;
|
||||||
|
s/&([\dA-Fa-f]+)/0x$1/;
|
||||||
|
if ( /\b2_[01]+\b/ ) {
|
||||||
|
s/\b2_([01]+)\b/conv$1&&&&/g;
|
||||||
|
while ( /[01][01][01][01]&&&&/ ) {
|
||||||
|
s/0000&&&&/&&&&0/g;
|
||||||
|
s/0001&&&&/&&&&1/g;
|
||||||
|
s/0010&&&&/&&&&2/g;
|
||||||
|
s/0011&&&&/&&&&3/g;
|
||||||
|
s/0100&&&&/&&&&4/g;
|
||||||
|
s/0101&&&&/&&&&5/g;
|
||||||
|
s/0110&&&&/&&&&6/g;
|
||||||
|
s/0111&&&&/&&&&7/g;
|
||||||
|
s/1000&&&&/&&&&8/g;
|
||||||
|
s/1001&&&&/&&&&9/g;
|
||||||
|
s/1010&&&&/&&&&A/g;
|
||||||
|
s/1011&&&&/&&&&B/g;
|
||||||
|
s/1100&&&&/&&&&C/g;
|
||||||
|
s/1101&&&&/&&&&D/g;
|
||||||
|
s/1110&&&&/&&&&E/g;
|
||||||
|
s/1111&&&&/&&&&F/g;
|
||||||
|
}
|
||||||
|
s/000&&&&/&&&&0/g;
|
||||||
|
s/001&&&&/&&&&1/g;
|
||||||
|
s/010&&&&/&&&&2/g;
|
||||||
|
s/011&&&&/&&&&3/g;
|
||||||
|
s/100&&&&/&&&&4/g;
|
||||||
|
s/101&&&&/&&&&5/g;
|
||||||
|
s/110&&&&/&&&&6/g;
|
||||||
|
s/111&&&&/&&&&7/g;
|
||||||
|
s/00&&&&/&&&&0/g;
|
||||||
|
s/01&&&&/&&&&1/g;
|
||||||
|
s/10&&&&/&&&&2/g;
|
||||||
|
s/11&&&&/&&&&3/g;
|
||||||
|
s/0&&&&/&&&&0/g;
|
||||||
|
s/1&&&&/&&&&1/g;
|
||||||
|
s/conv&&&&/0x/g;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( /commandline/)
|
||||||
|
{
|
||||||
|
if( /-bigend/)
|
||||||
|
{
|
||||||
|
$bigend=1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( /\bDCDU\b/ )
|
||||||
|
{
|
||||||
|
my $cmd=$_;
|
||||||
|
my $value;
|
||||||
|
my $prefix;
|
||||||
|
my $w1;
|
||||||
|
my $w2;
|
||||||
|
my $w3;
|
||||||
|
my $w4;
|
||||||
|
|
||||||
|
s/\s+DCDU\b/@ $&/;
|
||||||
|
|
||||||
|
$cmd =~ /\bDCDU\b\s+0x(\d+)/;
|
||||||
|
$value = $1;
|
||||||
|
$value =~ /(\w\w)(\w\w)(\w\w)(\w\w)/;
|
||||||
|
$w1 = $1;
|
||||||
|
$w2 = $2;
|
||||||
|
$w3 = $3;
|
||||||
|
$w4 = $4;
|
||||||
|
|
||||||
|
if( $bigend ne "")
|
||||||
|
{
|
||||||
|
# big endian
|
||||||
|
$prefix = "\t.byte\t0x".$w1.";".
|
||||||
|
"\t.byte\t0x".$w2.";".
|
||||||
|
"\t.byte\t0x".$w3.";".
|
||||||
|
"\t.byte\t0x".$w4."; ";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
# little endian
|
||||||
|
$prefix = "\t.byte\t0x".$w4.";".
|
||||||
|
"\t.byte\t0x".$w3.";".
|
||||||
|
"\t.byte\t0x".$w2.";".
|
||||||
|
"\t.byte\t0x".$w1."; ";
|
||||||
|
}
|
||||||
|
$_=$prefix.$_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( /\badrl\b/i )
|
||||||
|
{
|
||||||
|
s/\badrl\s+(\w+)\s*,\s*(\w+)/ldr $1,=$2/i;
|
||||||
|
$addPadding = 1;
|
||||||
|
}
|
||||||
|
s/\bEND\b/@ END/;
|
||||||
|
} continue {
|
||||||
|
printf ("%s", $_) if $printit;
|
||||||
|
if ($addPadding != 0)
|
||||||
|
{
|
||||||
|
printf (" mov r0,r0\n");
|
||||||
|
$addPadding = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#If we had a code section, mark that this object doesn't need an executable
|
||||||
|
# stack.
|
||||||
|
if ($nxstack && !$apple) {
|
||||||
|
printf (" .section\t.note.GNU-stack,\"\",\%\%progbits\n");
|
||||||
|
}
|
||||||
160
vendor/audiopus_sys/opus/celt/arm/arm_celt_map.c
vendored
Normal file
160
vendor/audiopus_sys/opus/celt/arm/arm_celt_map.c
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/* Copyright (c) 2010 Xiph.Org Foundation
|
||||||
|
* Copyright (c) 2013 Parrot */
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "pitch.h"
|
||||||
|
#include "kiss_fft.h"
|
||||||
|
#include "mdct.h"
|
||||||
|
|
||||||
|
#if defined(OPUS_HAVE_RTCD)
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_NEON_INTR) && !defined(OPUS_ARM_PRESUME_NEON_INTR)
|
||||||
|
opus_val32 (*const CELT_INNER_PROD_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *x, const opus_val16 *y, int N) = {
|
||||||
|
celt_inner_prod_c, /* ARMv4 */
|
||||||
|
celt_inner_prod_c, /* EDSP */
|
||||||
|
celt_inner_prod_c, /* Media */
|
||||||
|
celt_inner_prod_neon /* NEON */
|
||||||
|
};
|
||||||
|
|
||||||
|
void (*const DUAL_INNER_PROD_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *x, const opus_val16 *y01, const opus_val16 *y02,
|
||||||
|
int N, opus_val32 *xy1, opus_val32 *xy2) = {
|
||||||
|
dual_inner_prod_c, /* ARMv4 */
|
||||||
|
dual_inner_prod_c, /* EDSP */
|
||||||
|
dual_inner_prod_c, /* Media */
|
||||||
|
dual_inner_prod_neon /* NEON */
|
||||||
|
};
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(FIXED_POINT)
|
||||||
|
# if ((defined(OPUS_ARM_MAY_HAVE_NEON) && !defined(OPUS_ARM_PRESUME_NEON)) || \
|
||||||
|
(defined(OPUS_ARM_MAY_HAVE_MEDIA) && !defined(OPUS_ARM_PRESUME_MEDIA)) || \
|
||||||
|
(defined(OPUS_ARM_MAY_HAVE_EDSP) && !defined(OPUS_ARM_PRESUME_EDSP)))
|
||||||
|
opus_val32 (*const CELT_PITCH_XCORR_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *,
|
||||||
|
const opus_val16 *, opus_val32 *, int, int, int) = {
|
||||||
|
celt_pitch_xcorr_c, /* ARMv4 */
|
||||||
|
MAY_HAVE_EDSP(celt_pitch_xcorr), /* EDSP */
|
||||||
|
MAY_HAVE_MEDIA(celt_pitch_xcorr), /* Media */
|
||||||
|
MAY_HAVE_NEON(celt_pitch_xcorr) /* NEON */
|
||||||
|
};
|
||||||
|
|
||||||
|
# endif
|
||||||
|
# else /* !FIXED_POINT */
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_NEON_INTR) && !defined(OPUS_ARM_PRESUME_NEON_INTR)
|
||||||
|
void (*const CELT_PITCH_XCORR_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *,
|
||||||
|
const opus_val16 *, opus_val32 *, int, int, int) = {
|
||||||
|
celt_pitch_xcorr_c, /* ARMv4 */
|
||||||
|
celt_pitch_xcorr_c, /* EDSP */
|
||||||
|
celt_pitch_xcorr_c, /* Media */
|
||||||
|
celt_pitch_xcorr_float_neon /* Neon */
|
||||||
|
};
|
||||||
|
# endif
|
||||||
|
# endif /* FIXED_POINT */
|
||||||
|
|
||||||
|
#if defined(FIXED_POINT) && defined(OPUS_HAVE_RTCD) && \
|
||||||
|
defined(OPUS_ARM_MAY_HAVE_NEON_INTR) && !defined(OPUS_ARM_PRESUME_NEON_INTR)
|
||||||
|
|
||||||
|
void (*const XCORR_KERNEL_IMPL[OPUS_ARCHMASK + 1])(
|
||||||
|
const opus_val16 *x,
|
||||||
|
const opus_val16 *y,
|
||||||
|
opus_val32 sum[4],
|
||||||
|
int len
|
||||||
|
) = {
|
||||||
|
xcorr_kernel_c, /* ARMv4 */
|
||||||
|
xcorr_kernel_c, /* EDSP */
|
||||||
|
xcorr_kernel_c, /* Media */
|
||||||
|
xcorr_kernel_neon_fixed, /* Neon */
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
# if defined(HAVE_ARM_NE10)
|
||||||
|
# if defined(CUSTOM_MODES)
|
||||||
|
int (*const OPUS_FFT_ALLOC_ARCH_IMPL[OPUS_ARCHMASK+1])(kiss_fft_state *st) = {
|
||||||
|
opus_fft_alloc_arch_c, /* ARMv4 */
|
||||||
|
opus_fft_alloc_arch_c, /* EDSP */
|
||||||
|
opus_fft_alloc_arch_c, /* Media */
|
||||||
|
opus_fft_alloc_arm_neon /* Neon with NE10 library support */
|
||||||
|
};
|
||||||
|
|
||||||
|
void (*const OPUS_FFT_FREE_ARCH_IMPL[OPUS_ARCHMASK+1])(kiss_fft_state *st) = {
|
||||||
|
opus_fft_free_arch_c, /* ARMv4 */
|
||||||
|
opus_fft_free_arch_c, /* EDSP */
|
||||||
|
opus_fft_free_arch_c, /* Media */
|
||||||
|
opus_fft_free_arm_neon /* Neon with NE10 */
|
||||||
|
};
|
||||||
|
# endif /* CUSTOM_MODES */
|
||||||
|
|
||||||
|
void (*const OPUS_FFT[OPUS_ARCHMASK+1])(const kiss_fft_state *cfg,
|
||||||
|
const kiss_fft_cpx *fin,
|
||||||
|
kiss_fft_cpx *fout) = {
|
||||||
|
opus_fft_c, /* ARMv4 */
|
||||||
|
opus_fft_c, /* EDSP */
|
||||||
|
opus_fft_c, /* Media */
|
||||||
|
opus_fft_neon /* Neon with NE10 */
|
||||||
|
};
|
||||||
|
|
||||||
|
void (*const OPUS_IFFT[OPUS_ARCHMASK+1])(const kiss_fft_state *cfg,
|
||||||
|
const kiss_fft_cpx *fin,
|
||||||
|
kiss_fft_cpx *fout) = {
|
||||||
|
opus_ifft_c, /* ARMv4 */
|
||||||
|
opus_ifft_c, /* EDSP */
|
||||||
|
opus_ifft_c, /* Media */
|
||||||
|
opus_ifft_neon /* Neon with NE10 */
|
||||||
|
};
|
||||||
|
|
||||||
|
void (*const CLT_MDCT_FORWARD_IMPL[OPUS_ARCHMASK+1])(const mdct_lookup *l,
|
||||||
|
kiss_fft_scalar *in,
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT out,
|
||||||
|
const opus_val16 *window,
|
||||||
|
int overlap, int shift,
|
||||||
|
int stride, int arch) = {
|
||||||
|
clt_mdct_forward_c, /* ARMv4 */
|
||||||
|
clt_mdct_forward_c, /* EDSP */
|
||||||
|
clt_mdct_forward_c, /* Media */
|
||||||
|
clt_mdct_forward_neon /* Neon with NE10 */
|
||||||
|
};
|
||||||
|
|
||||||
|
void (*const CLT_MDCT_BACKWARD_IMPL[OPUS_ARCHMASK+1])(const mdct_lookup *l,
|
||||||
|
kiss_fft_scalar *in,
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT out,
|
||||||
|
const opus_val16 *window,
|
||||||
|
int overlap, int shift,
|
||||||
|
int stride, int arch) = {
|
||||||
|
clt_mdct_backward_c, /* ARMv4 */
|
||||||
|
clt_mdct_backward_c, /* EDSP */
|
||||||
|
clt_mdct_backward_c, /* Media */
|
||||||
|
clt_mdct_backward_neon /* Neon with NE10 */
|
||||||
|
};
|
||||||
|
|
||||||
|
# endif /* HAVE_ARM_NE10 */
|
||||||
|
# endif /* OPUS_ARM_MAY_HAVE_NEON_INTR */
|
||||||
|
|
||||||
|
#endif /* OPUS_HAVE_RTCD */
|
||||||
187
vendor/audiopus_sys/opus/celt/arm/armcpu.c
vendored
Normal file
187
vendor/audiopus_sys/opus/celt/arm/armcpu.c
vendored
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/* Copyright (c) 2010 Xiph.Org Foundation
|
||||||
|
* Copyright (c) 2013 Parrot */
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Original code from libtheora modified to suit to Opus */
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef OPUS_HAVE_RTCD
|
||||||
|
|
||||||
|
#include "armcpu.h"
|
||||||
|
#include "cpu_support.h"
|
||||||
|
#include "os_support.h"
|
||||||
|
#include "opus_types.h"
|
||||||
|
#include "arch.h"
|
||||||
|
|
||||||
|
#define OPUS_CPU_ARM_V4_FLAG (1<<OPUS_ARCH_ARM_V4)
|
||||||
|
#define OPUS_CPU_ARM_EDSP_FLAG (1<<OPUS_ARCH_ARM_EDSP)
|
||||||
|
#define OPUS_CPU_ARM_MEDIA_FLAG (1<<OPUS_ARCH_ARM_MEDIA)
|
||||||
|
#define OPUS_CPU_ARM_NEON_FLAG (1<<OPUS_ARCH_ARM_NEON)
|
||||||
|
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
/*For GetExceptionCode() and EXCEPTION_ILLEGAL_INSTRUCTION.*/
|
||||||
|
# define WIN32_LEAN_AND_MEAN
|
||||||
|
# define WIN32_EXTRA_LEAN
|
||||||
|
# include <windows.h>
|
||||||
|
|
||||||
|
static OPUS_INLINE opus_uint32 opus_cpu_capabilities(void){
|
||||||
|
opus_uint32 flags;
|
||||||
|
flags=0;
|
||||||
|
/* MSVC has no OPUS_INLINE __asm support for ARM, but it does let you __emit
|
||||||
|
* instructions via their assembled hex code.
|
||||||
|
* All of these instructions should be essentially nops. */
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_EDSP) || defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|
||||||
|
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
__try{
|
||||||
|
/*PLD [r13]*/
|
||||||
|
__emit(0xF5DDF000);
|
||||||
|
flags|=OPUS_CPU_ARM_EDSP_FLAG;
|
||||||
|
}
|
||||||
|
__except(GetExceptionCode()==EXCEPTION_ILLEGAL_INSTRUCTION){
|
||||||
|
/*Ignore exception.*/
|
||||||
|
}
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|
||||||
|
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
__try{
|
||||||
|
/*SHADD8 r3,r3,r3*/
|
||||||
|
__emit(0xE6333F93);
|
||||||
|
flags|=OPUS_CPU_ARM_MEDIA_FLAG;
|
||||||
|
}
|
||||||
|
__except(GetExceptionCode()==EXCEPTION_ILLEGAL_INSTRUCTION){
|
||||||
|
/*Ignore exception.*/
|
||||||
|
}
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
__try{
|
||||||
|
/*VORR q0,q0,q0*/
|
||||||
|
__emit(0xF2200150);
|
||||||
|
flags|=OPUS_CPU_ARM_NEON_FLAG;
|
||||||
|
}
|
||||||
|
__except(GetExceptionCode()==EXCEPTION_ILLEGAL_INSTRUCTION){
|
||||||
|
/*Ignore exception.*/
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
# endif
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(__linux__)
|
||||||
|
/* Linux based */
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
opus_uint32 opus_cpu_capabilities(void)
|
||||||
|
{
|
||||||
|
opus_uint32 flags = 0;
|
||||||
|
FILE *cpuinfo;
|
||||||
|
|
||||||
|
/* Reading /proc/self/auxv would be easier, but that doesn't work reliably on
|
||||||
|
* Android */
|
||||||
|
cpuinfo = fopen("/proc/cpuinfo", "r");
|
||||||
|
|
||||||
|
if(cpuinfo != NULL)
|
||||||
|
{
|
||||||
|
/* 512 should be enough for anybody (it's even enough for all the flags that
|
||||||
|
* x86 has accumulated... so far). */
|
||||||
|
char buf[512];
|
||||||
|
|
||||||
|
while(fgets(buf, 512, cpuinfo) != NULL)
|
||||||
|
{
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_EDSP) || defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|
||||||
|
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
/* Search for edsp and neon flag */
|
||||||
|
if(memcmp(buf, "Features", 8) == 0)
|
||||||
|
{
|
||||||
|
char *p;
|
||||||
|
p = strstr(buf, " edsp");
|
||||||
|
if(p != NULL && (p[5] == ' ' || p[5] == '\n'))
|
||||||
|
flags |= OPUS_CPU_ARM_EDSP_FLAG;
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
p = strstr(buf, " neon");
|
||||||
|
if(p != NULL && (p[5] == ' ' || p[5] == '\n'))
|
||||||
|
flags |= OPUS_CPU_ARM_NEON_FLAG;
|
||||||
|
# endif
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|
||||||
|
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
|
||||||
|
/* Search for media capabilities (>= ARMv6) */
|
||||||
|
if(memcmp(buf, "CPU architecture:", 17) == 0)
|
||||||
|
{
|
||||||
|
int version;
|
||||||
|
version = atoi(buf+17);
|
||||||
|
|
||||||
|
if(version >= 6)
|
||||||
|
flags |= OPUS_CPU_ARM_MEDIA_FLAG;
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(cpuinfo);
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/* The feature registers which can tell us what the processor supports are
|
||||||
|
* accessible in priveleged modes only, so we can't have a general user-space
|
||||||
|
* detection method like on x86.*/
|
||||||
|
# error "Configured to use ARM asm but no CPU detection method available for " \
|
||||||
|
"your platform. Reconfigure with --disable-rtcd (or send patches)."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int opus_select_arch(void)
|
||||||
|
{
|
||||||
|
opus_uint32 flags = opus_cpu_capabilities();
|
||||||
|
int arch = 0;
|
||||||
|
|
||||||
|
if(!(flags & OPUS_CPU_ARM_EDSP_FLAG)) {
|
||||||
|
/* Asserts ensure arch values are sequential */
|
||||||
|
celt_assert(arch == OPUS_ARCH_ARM_V4);
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
|
arch++;
|
||||||
|
|
||||||
|
if(!(flags & OPUS_CPU_ARM_MEDIA_FLAG)) {
|
||||||
|
celt_assert(arch == OPUS_ARCH_ARM_EDSP);
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
|
arch++;
|
||||||
|
|
||||||
|
if(!(flags & OPUS_CPU_ARM_NEON_FLAG)) {
|
||||||
|
celt_assert(arch == OPUS_ARCH_ARM_MEDIA);
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
|
arch++;
|
||||||
|
|
||||||
|
celt_assert(arch == OPUS_ARCH_ARM_NEON);
|
||||||
|
return arch;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
77
vendor/audiopus_sys/opus/celt/arm/armcpu.h
vendored
Normal file
77
vendor/audiopus_sys/opus/celt/arm/armcpu.h
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/* Copyright (c) 2010 Xiph.Org Foundation
|
||||||
|
* Copyright (c) 2013 Parrot */
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if !defined(ARMCPU_H)
|
||||||
|
# define ARMCPU_H
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_EDSP)
|
||||||
|
# define MAY_HAVE_EDSP(name) name ## _edsp
|
||||||
|
# else
|
||||||
|
# define MAY_HAVE_EDSP(name) name ## _c
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_MEDIA)
|
||||||
|
# define MAY_HAVE_MEDIA(name) name ## _media
|
||||||
|
# else
|
||||||
|
# define MAY_HAVE_MEDIA(name) MAY_HAVE_EDSP(name)
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_MAY_HAVE_NEON)
|
||||||
|
# define MAY_HAVE_NEON(name) name ## _neon
|
||||||
|
# else
|
||||||
|
# define MAY_HAVE_NEON(name) MAY_HAVE_MEDIA(name)
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_PRESUME_EDSP)
|
||||||
|
# define PRESUME_EDSP(name) name ## _edsp
|
||||||
|
# else
|
||||||
|
# define PRESUME_EDSP(name) name ## _c
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_PRESUME_MEDIA)
|
||||||
|
# define PRESUME_MEDIA(name) name ## _media
|
||||||
|
# else
|
||||||
|
# define PRESUME_MEDIA(name) PRESUME_EDSP(name)
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_ARM_PRESUME_NEON)
|
||||||
|
# define PRESUME_NEON(name) name ## _neon
|
||||||
|
# else
|
||||||
|
# define PRESUME_NEON(name) PRESUME_MEDIA(name)
|
||||||
|
# endif
|
||||||
|
|
||||||
|
# if defined(OPUS_HAVE_RTCD)
|
||||||
|
int opus_select_arch(void);
|
||||||
|
|
||||||
|
#define OPUS_ARCH_ARM_V4 (0)
|
||||||
|
#define OPUS_ARCH_ARM_EDSP (1)
|
||||||
|
#define OPUS_ARCH_ARM_MEDIA (2)
|
||||||
|
#define OPUS_ARCH_ARM_NEON (3)
|
||||||
|
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#endif
|
||||||
37
vendor/audiopus_sys/opus/celt/arm/armopts.s.in
vendored
Normal file
37
vendor/audiopus_sys/opus/celt/arm/armopts.s.in
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* Copyright (C) 2013 Mozilla Corporation */
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
; Set the following to 1 if we have EDSP instructions
|
||||||
|
; (LDRD/STRD, etc., ARMv5E and later).
|
||||||
|
OPUS_ARM_MAY_HAVE_EDSP * @OPUS_ARM_MAY_HAVE_EDSP@
|
||||||
|
|
||||||
|
; Set the following to 1 if we have ARMv6 media instructions.
|
||||||
|
OPUS_ARM_MAY_HAVE_MEDIA * @OPUS_ARM_MAY_HAVE_MEDIA@
|
||||||
|
|
||||||
|
; Set the following to 1 if we have NEON (some ARMv7)
|
||||||
|
OPUS_ARM_MAY_HAVE_NEON * @OPUS_ARM_MAY_HAVE_NEON@
|
||||||
|
|
||||||
|
END
|
||||||
173
vendor/audiopus_sys/opus/celt/arm/celt_fft_ne10.c
vendored
Normal file
173
vendor/audiopus_sys/opus/celt/arm/celt_fft_ne10.c
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
/* Copyright (c) 2015 Xiph.Org Foundation
|
||||||
|
Written by Viswanath Puttagunta */
|
||||||
|
/**
|
||||||
|
@file celt_fft_ne10.c
|
||||||
|
@brief ARM Neon optimizations for fft using NE10 library
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SKIP_CONFIG_H
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <NE10_dsp.h>
|
||||||
|
#include "os_support.h"
|
||||||
|
#include "kiss_fft.h"
|
||||||
|
#include "stack_alloc.h"
|
||||||
|
|
||||||
|
#if !defined(FIXED_POINT)
|
||||||
|
# define NE10_FFT_ALLOC_C2C_TYPE_NEON ne10_fft_alloc_c2c_float32_neon
|
||||||
|
# define NE10_FFT_CFG_TYPE_T ne10_fft_cfg_float32_t
|
||||||
|
# define NE10_FFT_STATE_TYPE_T ne10_fft_state_float32_t
|
||||||
|
# define NE10_FFT_DESTROY_C2C_TYPE ne10_fft_destroy_c2c_float32
|
||||||
|
# define NE10_FFT_CPX_TYPE_T ne10_fft_cpx_float32_t
|
||||||
|
# define NE10_FFT_C2C_1D_TYPE_NEON ne10_fft_c2c_1d_float32_neon
|
||||||
|
#else
|
||||||
|
# define NE10_FFT_ALLOC_C2C_TYPE_NEON(nfft) ne10_fft_alloc_c2c_int32_neon(nfft)
|
||||||
|
# define NE10_FFT_CFG_TYPE_T ne10_fft_cfg_int32_t
|
||||||
|
# define NE10_FFT_STATE_TYPE_T ne10_fft_state_int32_t
|
||||||
|
# define NE10_FFT_DESTROY_C2C_TYPE ne10_fft_destroy_c2c_int32
|
||||||
|
# define NE10_FFT_DESTROY_C2C_TYPE ne10_fft_destroy_c2c_int32
|
||||||
|
# define NE10_FFT_CPX_TYPE_T ne10_fft_cpx_int32_t
|
||||||
|
# define NE10_FFT_C2C_1D_TYPE_NEON ne10_fft_c2c_1d_int32_neon
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(CUSTOM_MODES)
|
||||||
|
|
||||||
|
/* nfft lengths in NE10 that support scaled fft */
|
||||||
|
# define NE10_FFTSCALED_SUPPORT_MAX 4
|
||||||
|
static const int ne10_fft_scaled_support[NE10_FFTSCALED_SUPPORT_MAX] = {
|
||||||
|
480, 240, 120, 60
|
||||||
|
};
|
||||||
|
|
||||||
|
int opus_fft_alloc_arm_neon(kiss_fft_state *st)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
size_t memneeded = sizeof(struct arch_fft_state);
|
||||||
|
|
||||||
|
st->arch_fft = (arch_fft_state *)opus_alloc(memneeded);
|
||||||
|
if (!st->arch_fft)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
for (i = 0; i < NE10_FFTSCALED_SUPPORT_MAX; i++) {
|
||||||
|
if(st->nfft == ne10_fft_scaled_support[i])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i == NE10_FFTSCALED_SUPPORT_MAX) {
|
||||||
|
/* This nfft length (scaled fft) is not supported in NE10 */
|
||||||
|
st->arch_fft->is_supported = 0;
|
||||||
|
st->arch_fft->priv = NULL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
st->arch_fft->is_supported = 1;
|
||||||
|
st->arch_fft->priv = (void *)NE10_FFT_ALLOC_C2C_TYPE_NEON(st->nfft);
|
||||||
|
if (st->arch_fft->priv == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void opus_fft_free_arm_neon(kiss_fft_state *st)
|
||||||
|
{
|
||||||
|
NE10_FFT_CFG_TYPE_T cfg;
|
||||||
|
|
||||||
|
if (!st->arch_fft)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cfg = (NE10_FFT_CFG_TYPE_T)st->arch_fft->priv;
|
||||||
|
if (cfg)
|
||||||
|
NE10_FFT_DESTROY_C2C_TYPE(cfg);
|
||||||
|
opus_free(st->arch_fft);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void opus_fft_neon(const kiss_fft_state *st,
|
||||||
|
const kiss_fft_cpx *fin,
|
||||||
|
kiss_fft_cpx *fout)
|
||||||
|
{
|
||||||
|
NE10_FFT_STATE_TYPE_T state;
|
||||||
|
NE10_FFT_CFG_TYPE_T cfg = &state;
|
||||||
|
VARDECL(NE10_FFT_CPX_TYPE_T, buffer);
|
||||||
|
SAVE_STACK;
|
||||||
|
ALLOC(buffer, st->nfft, NE10_FFT_CPX_TYPE_T);
|
||||||
|
|
||||||
|
if (!st->arch_fft->is_supported) {
|
||||||
|
/* This nfft length (scaled fft) not supported in NE10 */
|
||||||
|
opus_fft_c(st, fin, fout);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
memcpy((void *)cfg, st->arch_fft->priv, sizeof(NE10_FFT_STATE_TYPE_T));
|
||||||
|
state.buffer = (NE10_FFT_CPX_TYPE_T *)&buffer[0];
|
||||||
|
#if !defined(FIXED_POINT)
|
||||||
|
state.is_forward_scaled = 1;
|
||||||
|
|
||||||
|
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
|
||||||
|
(NE10_FFT_CPX_TYPE_T *)fin,
|
||||||
|
cfg, 0);
|
||||||
|
#else
|
||||||
|
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
|
||||||
|
(NE10_FFT_CPX_TYPE_T *)fin,
|
||||||
|
cfg, 0, 1);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
RESTORE_STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void opus_ifft_neon(const kiss_fft_state *st,
|
||||||
|
const kiss_fft_cpx *fin,
|
||||||
|
kiss_fft_cpx *fout)
|
||||||
|
{
|
||||||
|
NE10_FFT_STATE_TYPE_T state;
|
||||||
|
NE10_FFT_CFG_TYPE_T cfg = &state;
|
||||||
|
VARDECL(NE10_FFT_CPX_TYPE_T, buffer);
|
||||||
|
SAVE_STACK;
|
||||||
|
ALLOC(buffer, st->nfft, NE10_FFT_CPX_TYPE_T);
|
||||||
|
|
||||||
|
if (!st->arch_fft->is_supported) {
|
||||||
|
/* This nfft length (scaled fft) not supported in NE10 */
|
||||||
|
opus_ifft_c(st, fin, fout);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
memcpy((void *)cfg, st->arch_fft->priv, sizeof(NE10_FFT_STATE_TYPE_T));
|
||||||
|
state.buffer = (NE10_FFT_CPX_TYPE_T *)&buffer[0];
|
||||||
|
#if !defined(FIXED_POINT)
|
||||||
|
state.is_backward_scaled = 0;
|
||||||
|
|
||||||
|
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
|
||||||
|
(NE10_FFT_CPX_TYPE_T *)fin,
|
||||||
|
cfg, 1);
|
||||||
|
#else
|
||||||
|
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
|
||||||
|
(NE10_FFT_CPX_TYPE_T *)fin,
|
||||||
|
cfg, 1, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
RESTORE_STACK;
|
||||||
|
}
|
||||||
258
vendor/audiopus_sys/opus/celt/arm/celt_mdct_ne10.c
vendored
Normal file
258
vendor/audiopus_sys/opus/celt/arm/celt_mdct_ne10.c
vendored
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
/* Copyright (c) 2015 Xiph.Org Foundation
|
||||||
|
Written by Viswanath Puttagunta */
|
||||||
|
/**
|
||||||
|
@file celt_mdct_ne10.c
|
||||||
|
@brief ARM Neon optimizations for mdct using NE10 library
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
- Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
- Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||||
|
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||||
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SKIP_CONFIG_H
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "kiss_fft.h"
|
||||||
|
#include "_kiss_fft_guts.h"
|
||||||
|
#include "mdct.h"
|
||||||
|
#include "stack_alloc.h"
|
||||||
|
|
||||||
|
void clt_mdct_forward_neon(const mdct_lookup *l,
|
||||||
|
kiss_fft_scalar *in,
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT out,
|
||||||
|
const opus_val16 *window,
|
||||||
|
int overlap, int shift, int stride, int arch)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
int N, N2, N4;
|
||||||
|
VARDECL(kiss_fft_scalar, f);
|
||||||
|
VARDECL(kiss_fft_cpx, f2);
|
||||||
|
const kiss_fft_state *st = l->kfft[shift];
|
||||||
|
const kiss_twiddle_scalar *trig;
|
||||||
|
|
||||||
|
SAVE_STACK;
|
||||||
|
|
||||||
|
N = l->n;
|
||||||
|
trig = l->trig;
|
||||||
|
for (i=0;i<shift;i++)
|
||||||
|
{
|
||||||
|
N >>= 1;
|
||||||
|
trig += N;
|
||||||
|
}
|
||||||
|
N2 = N>>1;
|
||||||
|
N4 = N>>2;
|
||||||
|
|
||||||
|
ALLOC(f, N2, kiss_fft_scalar);
|
||||||
|
ALLOC(f2, N4, kiss_fft_cpx);
|
||||||
|
|
||||||
|
/* Consider the input to be composed of four blocks: [a, b, c, d] */
|
||||||
|
/* Window, shuffle, fold */
|
||||||
|
{
|
||||||
|
/* Temp pointers to make it really clear to the compiler what we're doing */
|
||||||
|
const kiss_fft_scalar * OPUS_RESTRICT xp1 = in+(overlap>>1);
|
||||||
|
const kiss_fft_scalar * OPUS_RESTRICT xp2 = in+N2-1+(overlap>>1);
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT yp = f;
|
||||||
|
const opus_val16 * OPUS_RESTRICT wp1 = window+(overlap>>1);
|
||||||
|
const opus_val16 * OPUS_RESTRICT wp2 = window+(overlap>>1)-1;
|
||||||
|
for(i=0;i<((overlap+3)>>2);i++)
|
||||||
|
{
|
||||||
|
/* Real part arranged as -d-cR, Imag part arranged as -b+aR*/
|
||||||
|
*yp++ = MULT16_32_Q15(*wp2, xp1[N2]) + MULT16_32_Q15(*wp1,*xp2);
|
||||||
|
*yp++ = MULT16_32_Q15(*wp1, *xp1) - MULT16_32_Q15(*wp2, xp2[-N2]);
|
||||||
|
xp1+=2;
|
||||||
|
xp2-=2;
|
||||||
|
wp1+=2;
|
||||||
|
wp2-=2;
|
||||||
|
}
|
||||||
|
wp1 = window;
|
||||||
|
wp2 = window+overlap-1;
|
||||||
|
for(;i<N4-((overlap+3)>>2);i++)
|
||||||
|
{
|
||||||
|
/* Real part arranged as a-bR, Imag part arranged as -c-dR */
|
||||||
|
*yp++ = *xp2;
|
||||||
|
*yp++ = *xp1;
|
||||||
|
xp1+=2;
|
||||||
|
xp2-=2;
|
||||||
|
}
|
||||||
|
for(;i<N4;i++)
|
||||||
|
{
|
||||||
|
/* Real part arranged as a-bR, Imag part arranged as -c-dR */
|
||||||
|
*yp++ = -MULT16_32_Q15(*wp1, xp1[-N2]) + MULT16_32_Q15(*wp2, *xp2);
|
||||||
|
*yp++ = MULT16_32_Q15(*wp2, *xp1) + MULT16_32_Q15(*wp1, xp2[N2]);
|
||||||
|
xp1+=2;
|
||||||
|
xp2-=2;
|
||||||
|
wp1+=2;
|
||||||
|
wp2-=2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Pre-rotation */
|
||||||
|
{
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT yp = f;
|
||||||
|
const kiss_twiddle_scalar *t = &trig[0];
|
||||||
|
for(i=0;i<N4;i++)
|
||||||
|
{
|
||||||
|
kiss_fft_cpx yc;
|
||||||
|
kiss_twiddle_scalar t0, t1;
|
||||||
|
kiss_fft_scalar re, im, yr, yi;
|
||||||
|
t0 = t[i];
|
||||||
|
t1 = t[N4+i];
|
||||||
|
re = *yp++;
|
||||||
|
im = *yp++;
|
||||||
|
yr = S_MUL(re,t0) - S_MUL(im,t1);
|
||||||
|
yi = S_MUL(im,t0) + S_MUL(re,t1);
|
||||||
|
yc.r = yr;
|
||||||
|
yc.i = yi;
|
||||||
|
f2[i] = yc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opus_fft(st, f2, (kiss_fft_cpx *)f, arch);
|
||||||
|
|
||||||
|
/* Post-rotate */
|
||||||
|
{
|
||||||
|
/* Temp pointers to make it really clear to the compiler what we're doing */
|
||||||
|
const kiss_fft_cpx * OPUS_RESTRICT fp = (kiss_fft_cpx *)f;
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT yp1 = out;
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT yp2 = out+stride*(N2-1);
|
||||||
|
const kiss_twiddle_scalar *t = &trig[0];
|
||||||
|
/* Temp pointers to make it really clear to the compiler what we're doing */
|
||||||
|
for(i=0;i<N4;i++)
|
||||||
|
{
|
||||||
|
kiss_fft_scalar yr, yi;
|
||||||
|
yr = S_MUL(fp->i,t[N4+i]) - S_MUL(fp->r,t[i]);
|
||||||
|
yi = S_MUL(fp->r,t[N4+i]) + S_MUL(fp->i,t[i]);
|
||||||
|
*yp1 = yr;
|
||||||
|
*yp2 = yi;
|
||||||
|
fp++;
|
||||||
|
yp1 += 2*stride;
|
||||||
|
yp2 -= 2*stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESTORE_STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clt_mdct_backward_neon(const mdct_lookup *l,
|
||||||
|
kiss_fft_scalar *in,
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT out,
|
||||||
|
const opus_val16 * OPUS_RESTRICT window,
|
||||||
|
int overlap, int shift, int stride, int arch)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
int N, N2, N4;
|
||||||
|
VARDECL(kiss_fft_scalar, f);
|
||||||
|
const kiss_twiddle_scalar *trig;
|
||||||
|
const kiss_fft_state *st = l->kfft[shift];
|
||||||
|
|
||||||
|
N = l->n;
|
||||||
|
trig = l->trig;
|
||||||
|
for (i=0;i<shift;i++)
|
||||||
|
{
|
||||||
|
N >>= 1;
|
||||||
|
trig += N;
|
||||||
|
}
|
||||||
|
N2 = N>>1;
|
||||||
|
N4 = N>>2;
|
||||||
|
|
||||||
|
ALLOC(f, N2, kiss_fft_scalar);
|
||||||
|
|
||||||
|
/* Pre-rotate */
|
||||||
|
{
|
||||||
|
/* Temp pointers to make it really clear to the compiler what we're doing */
|
||||||
|
const kiss_fft_scalar * OPUS_RESTRICT xp1 = in;
|
||||||
|
const kiss_fft_scalar * OPUS_RESTRICT xp2 = in+stride*(N2-1);
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT yp = f;
|
||||||
|
const kiss_twiddle_scalar * OPUS_RESTRICT t = &trig[0];
|
||||||
|
for(i=0;i<N4;i++)
|
||||||
|
{
|
||||||
|
kiss_fft_scalar yr, yi;
|
||||||
|
yr = S_MUL(*xp2, t[i]) + S_MUL(*xp1, t[N4+i]);
|
||||||
|
yi = S_MUL(*xp1, t[i]) - S_MUL(*xp2, t[N4+i]);
|
||||||
|
yp[2*i] = yr;
|
||||||
|
yp[2*i+1] = yi;
|
||||||
|
xp1+=2*stride;
|
||||||
|
xp2-=2*stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opus_ifft(st, (kiss_fft_cpx *)f, (kiss_fft_cpx*)(out+(overlap>>1)), arch);
|
||||||
|
|
||||||
|
/* Post-rotate and de-shuffle from both ends of the buffer at once to make
|
||||||
|
it in-place. */
|
||||||
|
{
|
||||||
|
kiss_fft_scalar * yp0 = out+(overlap>>1);
|
||||||
|
kiss_fft_scalar * yp1 = out+(overlap>>1)+N2-2;
|
||||||
|
const kiss_twiddle_scalar *t = &trig[0];
|
||||||
|
/* Loop to (N4+1)>>1 to handle odd N4. When N4 is odd, the
|
||||||
|
middle pair will be computed twice. */
|
||||||
|
for(i=0;i<(N4+1)>>1;i++)
|
||||||
|
{
|
||||||
|
kiss_fft_scalar re, im, yr, yi;
|
||||||
|
kiss_twiddle_scalar t0, t1;
|
||||||
|
re = yp0[0];
|
||||||
|
im = yp0[1];
|
||||||
|
t0 = t[i];
|
||||||
|
t1 = t[N4+i];
|
||||||
|
/* We'd scale up by 2 here, but instead it's done when mixing the windows */
|
||||||
|
yr = S_MUL(re,t0) + S_MUL(im,t1);
|
||||||
|
yi = S_MUL(re,t1) - S_MUL(im,t0);
|
||||||
|
re = yp1[0];
|
||||||
|
im = yp1[1];
|
||||||
|
yp0[0] = yr;
|
||||||
|
yp1[1] = yi;
|
||||||
|
|
||||||
|
t0 = t[(N4-i-1)];
|
||||||
|
t1 = t[(N2-i-1)];
|
||||||
|
/* We'd scale up by 2 here, but instead it's done when mixing the windows */
|
||||||
|
yr = S_MUL(re,t0) + S_MUL(im,t1);
|
||||||
|
yi = S_MUL(re,t1) - S_MUL(im,t0);
|
||||||
|
yp1[0] = yr;
|
||||||
|
yp0[1] = yi;
|
||||||
|
yp0 += 2;
|
||||||
|
yp1 -= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirror on both sides for TDAC */
|
||||||
|
{
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT xp1 = out+overlap-1;
|
||||||
|
kiss_fft_scalar * OPUS_RESTRICT yp1 = out;
|
||||||
|
const opus_val16 * OPUS_RESTRICT wp1 = window;
|
||||||
|
const opus_val16 * OPUS_RESTRICT wp2 = window+overlap-1;
|
||||||
|
|
||||||
|
for(i = 0; i < overlap/2; i++)
|
||||||
|
{
|
||||||
|
kiss_fft_scalar x1, x2;
|
||||||
|
x1 = *xp1;
|
||||||
|
x2 = *yp1;
|
||||||
|
*yp1++ = MULT16_32_Q15(*wp2, x2) - MULT16_32_Q15(*wp1, x1);
|
||||||
|
*xp1-- = MULT16_32_Q15(*wp1, x2) + MULT16_32_Q15(*wp2, x1);
|
||||||
|
wp1++;
|
||||||
|
wp2--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESTORE_STACK;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user