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
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,46 @@ jobs:
run: choco install ffmpeg --no-progress -y
- name: cargo test
run: cargo test

avx-presume:
name: AVX presume feature
runs-on: ubuntu-latest
needs: [fmt, clippy, check, tests]
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Verify AVX presume gating
run: python scripts/verify_avx.py

system-lib:
name: System Library
runs-on: ubuntu-latest
needs: [fmt, clippy, check, tests]
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y pkg-config ffmpeg curl build-essential autoconf automake libtool cmake
- name: Verify system lib build and tests
run: python scripts/verify_system_lib.py

dred:
name: DRED (bundled)
runs-on: ubuntu-latest
needs: [fmt, clippy, check, tests]
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install deps (ffmpeg + wget for model download)
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg wget
- name: Build with dred
run: cargo build --features dred
- name: Test with dred
run: cargo test --features dred
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
.vscode/

__pycache__/
*/__pycache__/
10 changes: 5 additions & 5 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opus-codec"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
authors = ["Denis Avvakumov"]
description = "Safe Rust bindings for the Opus audio codec"
Expand All @@ -22,6 +22,7 @@ pkg-config = "0.3"
default = []
dred = []
system-lib = []
presume-avx2 = []

[dev-dependencies]
tempfile = "3.23.0"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Safe Rust wrappers around libopus for encoding/decoding Opus audio, with tests that validate core functionality against ffmpeg.

## Features

- `presume-avx2`: Build the bundled libopus with `OPUS_X86_PRESUME_AVX2` on x86/x86_64 targets, assuming AVX/AVX2/FMA support. Ignored when linking against a system libopus.
- `dred`: Enable libopus DRED support (downloads the model when building the bundled library). The bundled DRED build currently assumes a Unix-like host with `sh`, `wget`, and `tar`, it is not supported on Windows.
- `system-lib`: Link against a system-provided libopus instead of the bundled sources.

## License

This crate is licensed under either of
Expand Down
91 changes: 73 additions & 18 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
use std::env;

fn main() {
emit_rerun_directives();
let opts = BuildOptions::from_env();

if opts.use_system_lib {
handle_system_lib(&opts);
} else {
build_bundled_and_link(&opts);
}

generate_bindings();
}

struct BuildOptions {
use_system_lib: bool,
dred_enabled: bool,
presume_avx: bool,
target_arch: String,
avx_allowed: bool,
}

impl BuildOptions {
fn from_env() -> Self {
let use_system_lib = env::var("CARGO_FEATURE_SYSTEM_LIB").is_ok();
let dred_enabled = env::var("CARGO_FEATURE_DRED").is_ok();
let presume_avx = env::var("CARGO_FEATURE_PRESUME_AVX2").is_ok();
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let avx_allowed = presume_avx && matches!(target_arch.as_str(), "x86" | "x86_64");

Self {
use_system_lib,
dred_enabled,
presume_avx,
target_arch,
avx_allowed,
}
}
}

fn emit_rerun_directives() {
println!("cargo:rerun-if-changed=opus/include/opus.h");
println!("cargo:rerun-if-changed=opus/include/opus_defines.h");
println!("cargo:rerun-if-changed=opus/include/opus_types.h");
Expand All @@ -9,30 +48,40 @@ fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=opus/dnn/download_model.sh");
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_SYSTEM_LIB");
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_PRESUME_AVX2");
}

let use_system_lib = env::var("CARGO_FEATURE_SYSTEM_LIB").is_ok();
let dred_enabled = env::var("CARGO_FEATURE_DRED").is_ok();
fn handle_system_lib(opts: &BuildOptions) {
if opts.dred_enabled {
println!(
"cargo:warning=system-lib feature enabled; ensure the system libopus includes DRED support"
);
}
if opts.presume_avx {
println!(
"cargo:warning=presume-avx2 feature enabled; ensure the system libopus was built with OPUS_X86_PRESUME_AVX2"
);
}
link_system_lib();
}

if use_system_lib {
if dred_enabled {
println!(
"cargo:warning=system-lib feature enabled; ensure the system libopus includes DRED support"
);
}
link_system_lib();
} else {
if dred_enabled {
ensure_dred_assets();
}
let dst = build_bundled(dred_enabled);
println!("cargo:rustc-link-search=native={}/lib", dst.display());
println!("cargo:rustc-link-lib=static=opus");
fn build_bundled_and_link(opts: &BuildOptions) {
if opts.dred_enabled {
ensure_dred_assets();
}
if opts.presume_avx && !opts.avx_allowed {
println!(
"cargo:warning=presume-avx2 feature only applies to x86/x86_64 targets; ignoring for {}",
opts.target_arch
);
}

generate_bindings();
let dst = build_bundled(opts.dred_enabled, opts.avx_allowed);
println!("cargo:rustc-link-search=native={}/lib", dst.display());
println!("cargo:rustc-link-lib=static=opus");
}

fn build_bundled(dred_enabled: bool) -> std::path::PathBuf {
fn build_bundled(dred_enabled: bool, presume_avx: bool) -> std::path::PathBuf {
let mut config = cmake::Config::new("opus");

config.profile("Release");
Expand All @@ -56,6 +105,12 @@ fn build_bundled(dred_enabled: bool) -> std::path::PathBuf {
.define("OPUS_DISABLE_INTRINSICS", "OFF")
.define("CMAKE_POSITION_INDEPENDENT_CODE", "ON");

if presume_avx {
config
.define("OPUS_X86_PRESUME_AVX2", "ON")
.define("OPUS_X86_MAY_HAVE_AVX2", "ON");
}

config.build()
}

Expand Down
59 changes: 59 additions & 0 deletions scripts/ci_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
import subprocess
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Dict, List, Optional, Union

@contextmanager
def group(name: str):
"""Group output in GitHub Actions."""
# Only print group markers if running in GitHub Actions or if forced
# But for now, we'll always print them as they are harmless in local terminals
print(f"::group::{name}")
try:
yield
finally:
print("::endgroup::")

def run(
cmd: List[str],
env: Optional[Dict[str, str]] = None,
cwd: Optional[Union[str, Path]] = None,
check: bool = True,
capture_output: bool = False,
) -> subprocess.CompletedProcess:
"""Run a command with optional grouping and error handling."""
cmd_str = " ".join(str(c) for c in cmd)

# Don't group if capturing output, as it's likely an internal check
should_group = not capture_output

if should_group:
print(f"::group::{cmd_str}")

try:
run_env = os.environ.copy()
if env:
run_env.update(env)

result = subprocess.run(
cmd,
env=run_env,
cwd=cwd,
check=check,
text=True,
capture_output=capture_output
)
return result
except subprocess.CalledProcessError as e:
if should_group:
print(f"Command failed with exit code {e.returncode}")
raise
finally:
if should_group:
print("::endgroup::")

def fail(msg: str) -> None:
"""Exit with an error message."""
sys.exit(f"Error: {msg}")
85 changes: 85 additions & 0 deletions scripts/verify_avx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Build and verify AVX-presume gating for the bundled opus library.

- Builds a generic target (no presume feature) and expects no AVX flag/instructions.
- Builds a presume target (presume-avx2 feature) and expects AVX flag/instructions.
"""

from pathlib import Path
from typing import Optional, Tuple
import ci_utils


PRESUME_FLAG = "OPUS_X86_PRESUME_AVX2:BOOL=ON"


def newest_build_dir(target_dir: Path) -> Optional[Path]:
build_root = target_dir / "release" / "build"
if not build_root.exists():
return None
# Find all opus-codec-* directories
candidates = [p for p in build_root.glob("opus-codec-*") if p.is_dir()]
if not candidates:
return None
# Return the most recently modified one
return max(candidates, key=lambda p: p.stat().st_mtime)


def find_artifacts(base: Path) -> Tuple[Optional[Path], Optional[Path]]:
# Recursive search for artifacts
caches = list(base.rglob("CMakeCache.txt"))
objs = list(base.rglob("bands.c.o"))
return (caches[0] if caches else None), (objs[0] if objs else None)


def verify(target_dir: str, features: str, expect_flag: bool, expect_avx: bool) -> None:
# Build
cmd = ["cargo", "build", "--release"]
if features:
cmd += ["--features", features]

ci_utils.run(cmd, env={"CARGO_TARGET_DIR": target_dir})

# Verify
target = Path(target_dir)
build_dir = newest_build_dir(target)
if not build_dir:
ci_utils.fail(f"build dir not found under {target_dir}")

print(f"Checking build dir: {build_dir}")

cache, obj = find_artifacts(target)
if not cache or not obj:
print(f"Artifacts missing for {target_dir}")
print("Found CMakeCache.txt:", [str(p) for p in target.rglob("CMakeCache.txt")])
print("Found bands.c.o:", [str(p) for p in target.rglob("bands.c.o")])
ci_utils.fail("Missing required build artifacts")

# Check CMake cache for flag
cache_content = cache.read_text()
flag_present = PRESUME_FLAG in cache_content
if flag_present != expect_flag:
ci_utils.fail(
f"AVX presume flag mismatch in {cache}: expected={expect_flag}, got={flag_present}"
)

# Check object file for AVX instructions
disasm = ci_utils.run(
["objdump", "-d", str(obj)], capture_output=True
).stdout

has_avx = "ymm" in disasm
if has_avx != expect_avx:
ci_utils.fail(
f"AVX instructions mismatch in {obj}: expected={expect_avx}, got={has_avx}"
)


def main() -> None:
verify("target/ci-generic", "", False, False)
verify("target/ci-presume", "presume-avx2", True, True)


if __name__ == "__main__":
main()
Loading