Files
oak-editor/crates/oakaudio
Mike-Solar ab1a2e9c7b refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
2026-08-16 00:33:45 +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.