Files
oak-editor/crates/oakaudio
Mike-Solar 05e42668cb build(ffmpeg): static GPL FFmpeg 8.0 via project script + FFMPEG_DIR
- tooling/ffmpeg/build-ffmpeg.sh builds release/8.0 static+PIC into
  .cache/ffmpeg: GPL/version3, every free-license external codec lib
  probed via pkg-config (enabled when present), per-OS hardware
  acceleration (VideoToolbox/AudioToolbox, VAAPI/VDPAU/libdrm,
  D3D11VA/DXVA2/MediaFoundation, nvenc when ffnvcodec exists)
- tooling/install-deps.sh installs those libraries on Homebrew / MSYS2
  UCRT64 / Debian-Ubuntu / Fedora / Arch; nothing in the build sudo's
- ffmpeg-next's own build feature is unusable (every crate-version to
  FFmpeg-release pairing is broken upstream: 9.0.0->FF9 AVCodec fields,
  8.1.0->FF8.1 new enum variants, 8.0.0->FF8 FF_PROFILE rename), so
  ffmpeg-next 9 + FFmpeg 8.x headers via FFMPEG_DIR it is
- new links-crate oakffmpeg-link emits the static FFmpeg's transitive
  link flags from its .pc files (cargo only propagates them from links
  crates, and rustc prunes the flags unless the rlib is referenced —
  hence the force_link statics)
- docs/build.md updated for the Rust workspace flow
2026-08-11 20:04:44 +08:00
..

oakaudio Rust crate

Status: implemented. The C ABI (include/audio/*.h) is implemented by src/ffi.rs; the contract suite lives in tests/ (all green, ~88% line coverage under tarpaulin). The architecture below mirrors the oaknode/oakrender crate template (FFI discipline, testing layers) from crates/oaknode/README.md and crates/oakrender/README.md.

Scope

Replaces the C++ oakaudio module (src/audio/src, ~50k lines): the PortAudio output/input manager (AudioManager), the real-time resampler/format converter (AudioProcessor), timeline synchronization helpers (AudioSynchronizer, AudioWaveformSync), the level meter (AudioLevelMeter), the visual waveform store (AudioVisualWaveform), the header-only pull buffer (PreviewAudioDevice), and the config bridge (audio_config namespace).

Public contract: include/audio/*.h (5 headers plus error.h, ~45 functions) — frozen, implemented verbatim by src/ffi.rs.

Key architectural decisions (C++ → Rust mapping)

  1. Singleton manager. AudioManager is a process-wide PortAudio singleton. Rust keeps the singleton behind a OnceLock<Mutex<...>> with borrow-only handles: addref/release are no-ops exactly as on the C++ side, and an empty handle reports OAKAUDIO_E_STATE. No destruction ever happens through the handle.
  2. Processor is the only heavy FFI consumer. AudioProcessor wraps the ffmpeg_bridge audio filter graph (fb_audio_graph_*, fb_frame_*); every call funnels through bridge::ffmpeg. The resampler/format-conversion semantics and the always-planar-f32 output (OAKAUDIO_PROCESSOR_OUTPUT_FORMAT = 4) are preserved.
  3. Sync helpers are stateless. AudioSynchronizer and AudioWaveformSync have only static methods in C++; they become plain functions in synchronizer.rs / waveformsync.rs. No handles are involved on the sync headers except by-value arguments.
  4. Value types are local. params.rs defines AudioParams (a plain POD) and a planar-first SampleFormat enum mirroring olive::core::SampleFormat::Format exactly, because these values cross the C ABI as int. See the note in params.rs about why the crate does not reuse oakcore-rs's SampleFormat.
  5. Rational reuses oakcore-rs. core::Rational (used by synchronizer and waveform) comes from oakcore-rs; there is no local copy.
  6. Record path through oakcodec. AudioManager records through the oakcodec encoder C ABI (bridge::codec) and waveform extraction decodes through the oakcodec decoder C ABI — exactly as the C++ does. No direct ffmpeg_bridge use in the record path.
  7. Config via oakcommon. Device names and the output buffer size read through bridge::common (oakcommon_config_*), preserving the audio_config namespace semantics as a config.rs free-function module.

Layout

COVERAGE.md maps every C++ audio class/method to its Rust home. Review that first.

src/
  lib.rs         crate doc + module map
  error.rs       error codes (mirrors include/audio/error.h)
  handle.rs      refcounted-handle scaffolding (same pattern as node)
  params.rs      AudioParams + planar-first SampleFormat value types
  config.rs      audio_config namespace (bridge::common)
  manager.rs     AudioManager singleton (PortAudio I/O, recording)
  processor.rs   AudioProcessor (resampler/converter, bridge::ffmpeg)
  synchronizer.rs AudioSynchronizer placement helpers
  levelmeter.rs  AudioLevelMeter peak/RMS/VU/LUFS analysis
  waveform.rs    AudioVisualWaveform mipmapped store + extraction
  waveformsync.rs AudioWaveformSync envelope offset estimation
  previewdevice.rs PreviewAudioDevice pull buffer
  bridge/        C ABI imports: common.rs, codec.rs, ffmpeg.rs
  ffi.rs         include/audio/*.h export layer
tests/           contract + golden tests (see README test section)

Hard rules for the implementer

  1. Every extern "C" body goes through handle::guard*; no panic crosses FFI. The manager's borrow-only singleton is the one place guard_handle/guard_void are used with no refcount semantics.
  2. SampleFormat and AudioParams integer values MUST match the C++ enums bit-for-bit; // CPP-PARITY: comments mark every load-bearing layout decision.
  3. Behavior parity with C++ is proven by the C ABI test-suite (src/audio/tests, unchanged) plus the golden tests in tests/ (waveform mipmap/channel-interleaved layout, RMS/LUFS thresholds, sync placement).
  4. Where C++ behavior is genuinely load-bearing but ugly, port the behavior, not the aesthetics; leave a // CPP-PARITY: comment with the C++ file:line.
  5. src/plugin/ is frozen and out of scope; no oakaudio code reaches into it.

Dependency policy

Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible) over hand-rolling; register each addition (name + reason) here. Large existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten — they are consumed through their C ABI / bridge layers.