Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ jobs:
with:
components: rustfmt, clippy

- name: Set optional environment variables (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: Add-Content -Path $env:GITHUB_ENV -Value "VCPKG_ROOT=C:\vcpkg\"

- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
shell: bash
Expand All @@ -51,6 +56,7 @@ jobs:
libclang-dev \
ocl-icd-opencl-dev \
libclfft-dev \
libopus-dev \
|| true

# Some runner images may not ship libclfft-dev. Fall back to building from source so the
Expand All @@ -64,6 +70,10 @@ jobs:
sudo ldconfig || true
fi

- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
run: C:\vcpkg\vcpkg install opus --vcpkg-root C:\vcpkg

- name: Format
run: cargo fmt --check

Expand Down
13 changes: 12 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
# The frontend is a git submodule and ships with prebuilt artifacts in frontend/dist/.

- name: Set optional environment variables (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: Add-Content -Path $env:GITHUB_ENV -Value "VCPKG_ROOT=C:\vcpkg\"

- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
shell: bash
Expand All @@ -70,6 +75,7 @@ jobs:
soapysdr-tools \
ocl-icd-opencl-dev \
libclfft-dev \
libopus-dev \
|| true

# If libclfft-dev isn't available on the runner image, build clFFT from source so the
Expand All @@ -89,7 +95,11 @@ jobs:
run: |
set -euo pipefail
brew update
brew install pkg-config soapysdr
brew install pkg-config soapysdr opus

- name: Install system dependencies (Windows)
if: runner.os == 'Windows'
run: C:\vcpkg\vcpkg install opus --vcpkg-root C:\vcpkg

- name: Verify frontend/dist exists
shell: bash
Expand Down Expand Up @@ -210,6 +220,7 @@ jobs:
apt-get install -y --no-install-recommends \
ocl-icd-opencl-dev \
libclfft-dev \
libopus-dev \
|| true

# Build SoapySDR from source for build-time linking (we do not ship/bundle it).
Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/novasdr-core",
"crates/novasdr-server",
"crates/ws_probe",
"crates/interop",
]

[workspace.package]
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ sudo apt-get update && sudo apt-get install -y --no-install-recommends \
nodejs npm \
ocl-icd-opencl-dev ocl-icd-libopencl1 \
libclfft-dev \
libusb-1.0-0-dev
libusb-1.0-0-dev \
libopus-dev
```

</details>
Expand All @@ -79,7 +80,8 @@ sudo dnf install -y \
swig python3 python3-devel python3-numpy \
nodejs npm \
ocl-icd ocl-icd-devel \
libusb1-devel
libusb1-devel \
opus-devel
```

</details>
Expand All @@ -95,7 +97,8 @@ sudo pacman -Sy --noconfirm --needed \
swig python python-numpy \
nodejs npm \
ocl-icd opencl-headers \
libusb
libusb \
opus
```

</details>
Expand All @@ -111,7 +114,8 @@ sudo zypper --non-interactive refresh && sudo zypper --non-interactive install -
swig python3 python3-devel python3-numpy \
nodejs npm \
OpenCL-Headers ocl-icd-devel \
libusb-1_0-devel
libusb-1_0-devel \
libopus-devel
```

</details>
Expand All @@ -125,7 +129,8 @@ brew update && brew install \
llvm \
swig python \
node \
libusb
libusb \
opus
```

</details>
Expand Down
19 changes: 19 additions & 0 deletions crates/interop/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "interop"
version = "0.1.0"
edition = "2021"
authors = ["Alexander Sholokhov <ra9yer@yahoo.com>"]
keywords = ["ffi", "sdr"]
build = "build.rs"
license = "GPL-3.0-only"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

[build-dependencies]
bindgen = { version = "0.66.1", default-features = false, features = ["runtime"] }
cc = "1.0"
pkg-config = "0.3.9"
vcpkg = "0.2.15"

89 changes: 89 additions & 0 deletions crates/interop/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::env;
use std::path::PathBuf;

fn probe_opus_pkg_config() -> Option<Vec<PathBuf>> {
match pkg_config::Config::new()
.atleast_version("1.3")
.probe("opus")
{
Err(e) => {
eprintln!("pkg_config: {}", e);
None
}
Ok(lib) => Some(lib.include_paths),
}
}

fn probe_opus_path_patch(paths: Vec<PathBuf>) -> Vec<PathBuf> {
// MacOs homebrew Opus specific patch
// we get path like /opt/homebrew/Cellar/opus/1.6.1/include/opus ,
// but it should be /opt/homebrew/Cellar/opus/1.6.1/include

for path in paths.iter() {
if path.join("opus/opus.h").exists() {
return paths;
}
}

let mut op_extra_path: Option<PathBuf> = None;
for path in paths.iter() {
if let Some(parent) = path.parent() {
if parent.join("opus/opus.h").exists() {
op_extra_path = Some(parent.into());
break;
}
}
}

let Some(extra_path) = op_extra_path else {
return paths;
};

let mut paths = paths;
paths.push(extra_path);
paths
}

fn do_opus() {
println!("cargo:rerun-if-changed=./interop/opus_wrapper.h");
println!("cargo:rerun-if-changed=./interop/opus_wrapper.c");

let include_paths = if env::var_os("VCPKG_ROOT").is_some() {
env::set_var("VCPKGRS_DYNAMIC", "1");
let pkg = vcpkg::Config::new()
.find_package("opus")
.expect("cant't find opus package");
pkg.include_paths
} else {
let path = probe_opus_pkg_config().expect("Couldn't find opus");
probe_opus_path_patch(path)
};

let bindgen_builder = bindgen::Builder::default()
.trust_clang_mangling(false)
.size_t_is_usize(true)
.header("opus_wrapper.h");

let mut cc_builder = cc::Build::new();
cc_builder.file("opus_wrapper.c");

for elm in include_paths.iter() {
cc_builder.include(elm);
}

let bindings = bindgen_builder
.allowlist_function("_opus_.*")
.generate()
.expect("Unable to generate bindings");

let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("opus_bindings.rs"))
.expect("Couldn't write opus_bindings!");

cc_builder.compile("opus-wrapper")
}

fn main() {
do_opus();
}
101 changes: 101 additions & 0 deletions crates/interop/opus_wrapper.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include "opus_wrapper.h"
#include <opus/opus.h>

int32_t _opus_application_audio()
{
return OPUS_APPLICATION_AUDIO;
}

int32_t _opus_application_voip()
{
return OPUS_APPLICATION_VOIP;
}

int32_t _opus_application_lowdelay()
{
return OPUS_APPLICATION_RESTRICTED_LOWDELAY;
}

int32_t _opus_bitrate_max()
{
return OPUS_BITRATE_MAX;
}

int32_t _opus_bitrate_auto()
{
return OPUS_AUTO;
}

void *_opus_encoder_create(int32_t fs, int32_t channels, int32_t application, int32_t *err)
{
// allowed fs: 48000, 24000, 16000, 12000, 8000
// allowed channels: 1, 2
int local_err = 0;
void *res = opus_encoder_create(fs, channels, application, &local_err);
*err = local_err; // safe translation C-int to Rust's i32
return res;
}

void _opus_encoder_destroy(void *enc)
{
opus_encoder_destroy(enc);
}

int32_t _opus_set_bitrate(void *enc, int32_t bitrate)
{
// Rates from 500 to 512000 bits per second are meaningful
return opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate));
}

int32_t _opus_set_complexity(void *enc, int32_t complexity)
{
return opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
}

int32_t _opus_encode_i16(void *enc, const int16_t *pcm, size_t frame_size, uint8_t *data, size_t max_data_bytes)
{
// To encode a frame, opus_encode() or opus_encode_float() must be called with exactly one frame (2.5, 5, 10, 20, 40 or 60 ms) of audio data:
return opus_encode(enc, pcm, frame_size, data, max_data_bytes);
}

int32_t _opus_encode_float(void *enc, const float *pcm, size_t frame_size, uint8_t *data, size_t max_data_bytes)
{
// To encode a frame, opus_encode() or opus_encode_float() must be called with exactly one frame (2.5, 5, 10, 20, 40 or 60 ms) of audio data:
return opus_encode_float(enc, pcm, frame_size, data, max_data_bytes);
}

void *_opus_decoder_create(int32_t fs, int32_t channels, int32_t *error)
{
// allowed fs: 48000, 24000, 16000, 12000, 8000
// allowed channels: 1, 2
int local_err = 0;
void *res = opus_decoder_create(fs, channels, &local_err);
*error = local_err;
return res;
}

int32_t _opus_decode_i16(void *dec, const uint8_t *data, size_t len, int16_t *pcm, size_t frame_size, int32_t decode_fec)
{
// decode_fec: Flag (0 or 1) to request that any in-band forward error correction data be
return opus_decode(dec, data, len, pcm, frame_size, decode_fec);
}

int32_t _opus_decode_float(void *dec, const uint8_t *data, size_t len, float *pcm, size_t frame_size, int32_t decode_fec)
{
return opus_decode_float(dec, data, len, pcm, frame_size, decode_fec);
}

void _opus_decoder_destroy(void *dec)
{
opus_decoder_destroy(dec);
}

const char *_opus_get_version_string(void)
{
return opus_get_version_string();
}

const char *_opus_strerror(int32_t error)
{
return opus_strerror(error);
}
Loading