Files
oak-editor/crates/oakaudio/src/params.rs
T
Mike-Solar 013a175707 refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
2026-08-10 20:24:25 +08:00

149 lines
5.2 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Audio value types: `AudioParams` and the `SampleFormat` enum
//! (re-exported from oakcore-rs; planar-first ordering, values identical
//! to `olive::core::SampleFormat::Format` — `// CPP-PARITY:
//! core/include/olive/core/render/sampleformat.h:33`).
//!
//! These integer values cross the C ABI as `int`, so they MUST match the
//! authoritative C++ enums bit-for-bit.
use oakcore_rs::Rational;
/// Audio sample format: re-exported from oakcore-rs (planar-first,
/// values identical to `olive::core::SampleFormat::Format`).
/// `// CPP-PARITY: core/include/olive/core/render/sampleformat.h:33`
pub use oakcore_rs::SampleFormat;
/// Audio stream parameters, mirroring `olive::core::AudioParams`
/// (core/include/olive/core/render/audioparams.h). A plain value type;
/// never bridged through a C ABI handle — liboakcore owns the matching
/// `oakcore_audioparams_*` wrapper and is out of scope here.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AudioParams {
/// Sample rate in Hz.
pub sample_rate: i32,
/// ffmpeg-style channel layout mask (0 = unknown/unspecified).
pub channel_layout: u64,
/// Sample format (see [`SampleFormat`]).
pub format: SampleFormat,
}
impl AudioParams {
/// Channel count of the current layout mask (popcount).
///
/// `// CPP-PARITY: core/src/render/audioparams.cpp:150`
/// (`AudioParams::calculate_channel_count` —
/// `channel_layout_mask_channel_count`); a layout mask of 0 yields 0,
/// NOT the stereo fallback (the fallback lives in the processor's
/// `fix_channel_layout`).
pub fn channel_count(&self) -> i32 {
self.channel_layout.count_ones() as i32
}
/// Bytes per sample per channel for the format.
///
/// `// CPP-PARITY: core/src/render/audioparams.cpp`
/// (`AudioParams::bytes_per_sample_per_channel`).
pub fn bytes_per_sample_per_channel(&self) -> i64 {
self.format.bytes_per_sample() as i64
}
/// Byte count of `samples` frames across all channels.
///
/// `// CPP-PARITY: core/src/render/audioparams.cpp:93`
/// (`AudioParams::samples_to_bytes`).
pub fn samples_to_bytes(&self, samples: i64) -> i64 {
samples * self.bytes_per_sample_per_channel() * i64::from(self.channel_count())
}
}
/// Rebuild a [`SampleFormat`] from the `int` that crossed the C ABI.
///
/// `// CPP-PARITY: src/audio/c_api/manager.cpp:130` — the C++ layers cast
/// the raw `int` straight onto `SampleFormat::Format`, which is UB for
/// out-of-range values but in practice wraps to whatever the enum width
/// holds. The Rust side maps unknown values to `Invalid` (a safe
/// equivalent; no contract test pins the wrapped value).
pub fn sample_format_from_i32(value: i32) -> SampleFormat {
match value {
0 => SampleFormat::U8Planar,
1 => SampleFormat::S16Planar,
2 => SampleFormat::S32Planar,
3 => SampleFormat::S64Planar,
4 => SampleFormat::F32Planar,
5 => SampleFormat::F64Planar,
6 => SampleFormat::U8,
7 => SampleFormat::S16,
8 => SampleFormat::S32,
9 => SampleFormat::S64,
10 => SampleFormat::F32,
11 => SampleFormat::F64,
_ => SampleFormat::Invalid,
}
}
/// Seconds elapsed at `frames` frames given `sample_rate`
/// (`frames/sample_rate` as a rational).
pub fn frames_to_rational(frames: i64, sample_rate: i32) -> Rational {
Rational::new(frames, i64::from(sample_rate))
}
/// Sample index of `time` seconds at `sample_rate` (rounded to nearest,
/// half away from zero).
///
/// `// CPP-PARITY: core/src/render/audioparams.cpp:81`
/// (`AudioParams::time_to_samples` uses `std::round`, not truncation).
pub fn rational_to_samples(time: Rational, sample_rate: i32) -> i64 {
(time.to_f64() * f64::from(sample_rate)).round() as i64
}
/// Continued-fraction double → rational conversion.
///
/// `// CPP-PARITY: core/src/util/rational.cpp:39`
/// (`Rational::from_double`): NaN/out-of-range → the null sentinel (0/0);
/// tiny values retried at INT64_MAX precision.
pub fn rational_from_double(flt: f64) -> Rational {
if flt.is_nan() {
return Rational::NULL;
}
if flt.abs() > f64::from(i32::MAX) + 3.0 {
return Rational::NULL;
}
// frexp: flt = f * 2^exp with f in [0.5, 1)
let (mut exponent, _frac) = {
if flt == 0.0 {
(0i32, 0.0)
} else {
let bits = flt.abs().to_bits();
let e = (((bits >> 52) & 0x7ff) as i32) - 1022;
(e, flt)
}
};
exponent = (exponent - 1).max(0);
let den: i64 = 1i64 << (62 - exponent);
let num: i64 = (flt * den as f64 + 0.5).floor() as i64;
let mut r = Rational::new(num, den);
if r.is_null() && flt != 0.0 {
// Too small to represent above; retry with maximum precision.
r = Rational::new((flt * i64::MAX as f64) as i64, i64::MAX);
}
r
}