NetDesk 1.5.0 — source release
This commit is contained in:
commit
40318aafdd
227 files changed
+80798
No files matched your search
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "netdesk-opus"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "Opus (libopus) encode/decode for NetDesk audio. Developed by Akronlogic."
|
||||
links = "opus"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "=0.65.1"
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Link the vendored static libopus (UCRT MinGW ABI) + generate FFI bindings
|
||||
//! from the vendored opus headers via bindgen (libclang). Same vendoring
|
||||
//! approach as netdesk-vpx: a UCRT-ABI libopus.a so there is no vcpkg/MSYS2 or
|
||||
//! runtime DLL dependency, and no fragile build-from-source (audiopus_sys's
|
||||
//! build script mangles extended-length paths on this toolchain).
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let tp = manifest.join("..").join("third_party").join("ucrt64");
|
||||
let lib = tp.join("lib");
|
||||
let inc = tp.join("include");
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", lib.display());
|
||||
println!("cargo:rustc-link-lib=static=opus");
|
||||
println!("cargo:rerun-if-changed=wrapper.h");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed={}", lib.join("libopus.a").display());
|
||||
|
||||
if std::env::var("LIBCLANG_PATH").is_err() {
|
||||
for p in ["C:/Program Files/LLVM/bin", "C:/Program Files (x86)/LLVM/bin"] {
|
||||
if PathBuf::from(p).join("libclang.dll").exists() {
|
||||
std::env::set_var("LIBCLANG_PATH", p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpusEncoder / OpusDecoder are opaque (used only through pointers), so
|
||||
// bindgen's default opaque representation is exactly right here — unlike
|
||||
// libvpx's config structs, we never set their fields.
|
||||
let regex = "^(opus|Opus|OPUS).*";
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("wrapper.h")
|
||||
.clang_arg(format!("-I{}", inc.display()))
|
||||
.allowlist_type(regex)
|
||||
.allowlist_var(regex)
|
||||
.allowlist_function(regex)
|
||||
.rustified_enum(regex)
|
||||
.trust_clang_mangling(false)
|
||||
.layout_tests(false)
|
||||
.generate_comments(false)
|
||||
.generate()
|
||||
.expect("bindgen libopus");
|
||||
bindings
|
||||
.write_to_file(PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("opus_ffi.rs"))
|
||||
.expect("write opus_ffi.rs");
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
//! Opus audio encode/decode over the vendored static libopus (UCRT MinGW).
|
||||
//!
|
||||
//! The raw FFI is generated by bindgen from the vendored headers (build.rs).
|
||||
//! `OpusEncoder`/`OpusDecoder` are opaque (used only through pointers), so
|
||||
//! bindgen's default handling is correct — no hand-written structs needed,
|
||||
//! unlike netdesk-vpx's config structs.
|
||||
//!
|
||||
//! NetDesk audio is 48 kHz stereo, one 20 ms frame (960 samples/channel) per
|
||||
//! Opus packet, RESTRICTED_LOWDELAY — matching what the web viewer sends/decodes
|
||||
//! and what the host system-audio capture produces.
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub mod ffi {
|
||||
#![allow(
|
||||
non_camel_case_types,
|
||||
non_snake_case,
|
||||
non_upper_case_globals,
|
||||
dead_code,
|
||||
improper_ctypes
|
||||
)]
|
||||
include!(concat!(env!("OUT_DIR"), "/opus_ffi.rs"));
|
||||
}
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_int;
|
||||
|
||||
/// 48 kHz — the Opus "fullband" rate NetDesk uses end to end.
|
||||
pub const SAMPLE_RATE: u32 = 48_000;
|
||||
/// 20 ms frame at 48 kHz = 960 samples per channel.
|
||||
pub const FRAME_SIZE: usize = 960;
|
||||
/// Opus can emit up to 120 ms per packet = 5760 samples/channel; size any decode
|
||||
/// scratch for the max so a decode can never overflow.
|
||||
pub const MAX_FRAME_SIZE: usize = 5760;
|
||||
|
||||
/// libopus version string, e.g. "libopus 1.5.2".
|
||||
pub fn version() -> String {
|
||||
unsafe {
|
||||
let p = ffi::opus_get_version_string();
|
||||
if p.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
CStr::from_ptr(p).to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Opus encoder for system audio: RESTRICTED_LOWDELAY (lowest algorithmic delay,
|
||||
/// which is what a screen-share wants). Input PCM is interleaved i16.
|
||||
pub struct Encoder {
|
||||
enc: *mut ffi::OpusEncoder,
|
||||
channels: usize,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(sample_rate: u32, channels: u32) -> Result<Self> {
|
||||
let mut err: c_int = 0;
|
||||
let enc = unsafe {
|
||||
ffi::opus_encoder_create(
|
||||
sample_rate as i32,
|
||||
channels as i32,
|
||||
ffi::OPUS_APPLICATION_RESTRICTED_LOWDELAY as i32,
|
||||
&mut err,
|
||||
)
|
||||
};
|
||||
if enc.is_null() || err != 0 {
|
||||
bail!("opus_encoder_create failed: {err}");
|
||||
}
|
||||
// Quality tuning for crystal-clear system/mic audio (defaults are
|
||||
// conservative). 128 kbps VBR, max complexity, music-optimised — the
|
||||
// audio stream is a tiny fraction of the video bitrate, so this costs
|
||||
// almost nothing on the wire but is near-transparent for music/voice.
|
||||
unsafe {
|
||||
ffi::opus_encoder_ctl(enc, ffi::OPUS_SET_BITRATE_REQUEST as c_int, 128_000 as c_int);
|
||||
ffi::opus_encoder_ctl(enc, ffi::OPUS_SET_VBR_REQUEST as c_int, 1 as c_int);
|
||||
ffi::opus_encoder_ctl(enc, ffi::OPUS_SET_COMPLEXITY_REQUEST as c_int, 10 as c_int);
|
||||
ffi::opus_encoder_ctl(
|
||||
enc,
|
||||
ffi::OPUS_SET_SIGNAL_REQUEST as c_int,
|
||||
ffi::OPUS_SIGNAL_MUSIC as c_int,
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
enc,
|
||||
channels: channels as usize,
|
||||
})
|
||||
}
|
||||
|
||||
/// Encode one frame of interleaved i16 PCM into a single Opus packet.
|
||||
/// `frame_size` is samples PER CHANNEL (e.g. [`FRAME_SIZE`]).
|
||||
pub fn encode(&mut self, pcm: &[i16], frame_size: usize) -> Result<Vec<u8>> {
|
||||
if pcm.len() < frame_size * self.channels {
|
||||
bail!(
|
||||
"pcm too short: {} < {}",
|
||||
pcm.len(),
|
||||
frame_size * self.channels
|
||||
);
|
||||
}
|
||||
let mut out = vec![0u8; 4000]; // max sensible Opus packet
|
||||
let n = unsafe {
|
||||
ffi::opus_encode(
|
||||
self.enc,
|
||||
pcm.as_ptr(),
|
||||
frame_size as i32,
|
||||
out.as_mut_ptr(),
|
||||
out.len() as i32,
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
bail!("opus_encode failed: {n}");
|
||||
}
|
||||
out.truncate(n as usize);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Encoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::opus_encoder_destroy(self.enc) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Opus decoder. Output PCM is interleaved i16.
|
||||
pub struct Decoder {
|
||||
dec: *mut ffi::OpusDecoder,
|
||||
channels: usize,
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
pub fn new(sample_rate: u32, channels: u32) -> Result<Self> {
|
||||
let mut err: c_int = 0;
|
||||
let dec =
|
||||
unsafe { ffi::opus_decoder_create(sample_rate as i32, channels as i32, &mut err) };
|
||||
if dec.is_null() || err != 0 {
|
||||
bail!("opus_decoder_create failed: {err}");
|
||||
}
|
||||
Ok(Self {
|
||||
dec,
|
||||
channels: channels as usize,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode one Opus packet into interleaved i16 PCM. Returns
|
||||
/// `samples_per_channel * channels` samples.
|
||||
pub fn decode(&mut self, data: &[u8]) -> Result<Vec<i16>> {
|
||||
let mut pcm = vec![0i16; MAX_FRAME_SIZE * self.channels];
|
||||
let n = unsafe {
|
||||
ffi::opus_decode(
|
||||
self.dec,
|
||||
data.as_ptr(),
|
||||
data.len() as i32,
|
||||
pcm.as_mut_ptr(),
|
||||
MAX_FRAME_SIZE as i32,
|
||||
0,
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
bail!("opus_decode failed: {n}");
|
||||
}
|
||||
pcm.truncate(n as usize * self.channels);
|
||||
Ok(pcm)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Decoder {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ffi::opus_decoder_destroy(self.dec) };
|
||||
}
|
||||
}
|
||||
|
||||
// Opus contexts hold raw pointers; they are not shared (no Sync) but a single
|
||||
// owner may move them between threads (the host capture thread owns the Encoder,
|
||||
// the viewer audio thread owns the Decoder).
|
||||
unsafe impl Send for Encoder {}
|
||||
unsafe impl Send for Decoder {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn links_libopus() {
|
||||
let v = version();
|
||||
assert!(v.to_lowercase().contains("opus"), "opus version: {v}");
|
||||
println!("OPUS_LINK_OK {v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opus_roundtrip_48k_stereo() {
|
||||
let mut enc = Encoder::new(SAMPLE_RATE, 2).unwrap();
|
||||
let mut dec = Decoder::new(SAMPLE_RATE, 2).unwrap();
|
||||
|
||||
// 5 frames of a full-scale 440 Hz stereo sine.
|
||||
let mut max_abs = 0i32;
|
||||
let mut total_out = 0usize;
|
||||
let mut packet_bytes = 0usize;
|
||||
for frame in 0..5 {
|
||||
let mut pcm = vec![0i16; FRAME_SIZE * 2];
|
||||
for i in 0..FRAME_SIZE {
|
||||
let t = (frame * FRAME_SIZE + i) as f32 / SAMPLE_RATE as f32;
|
||||
let s = (2.0 * std::f32::consts::PI * 440.0 * t).sin();
|
||||
let v = (s * 30000.0) as i16;
|
||||
pcm[i * 2] = v;
|
||||
pcm[i * 2 + 1] = v;
|
||||
}
|
||||
let pkt = enc.encode(&pcm, FRAME_SIZE).unwrap();
|
||||
assert!(!pkt.is_empty(), "empty opus packet");
|
||||
packet_bytes += pkt.len();
|
||||
let out = dec.decode(&pkt).unwrap();
|
||||
assert_eq!(out.len(), FRAME_SIZE * 2, "decoded frame size (stereo)");
|
||||
total_out += out.len();
|
||||
for &s in &out {
|
||||
max_abs = max_abs.max((s as i32).abs());
|
||||
}
|
||||
}
|
||||
assert_eq!(total_out, 5 * FRAME_SIZE * 2);
|
||||
// After the encoder's lookahead delay, the sine energy must come through
|
||||
// (proves the codec actually ran, not just returned silence).
|
||||
assert!(max_abs > 1000, "decoded audio is silent (max_abs={max_abs})");
|
||||
println!("OPUS_ROUNDTRIP_OK 48k/stereo 5x20ms total_pkt_bytes={packet_bytes} peak={max_abs}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#include <opus/opus.h>
|
||||
Reference in new issue
Block a user