NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 02:02:46 +00:00
commit 40318aafdd
227 files changed
+80798

No files matched your search

+15
View File
@@ -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"
+48
View File
@@ -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");
}
+225
View File
@@ -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}");
}
}
+1
View File
@@ -0,0 +1 @@
#include <opus/opus.h>