Files
oak-editor/crates/oak-audio/src/levelmeter.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

157 lines
4.8 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/>.
//! Loudness analysis (`olive::AudioLevelMeter`). A static helper producing
//! per-channel peak/RMS/VU statistics plus an overall LUFS-integrated
//! summary. Pure function module in Rust; feeds the level-meter UI widget.
/// dB floor for all decibel readings.
///
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.cpp:30`
/// (`k_decibel_minimum`, inlined from engine/common/decibel.h).
const DECIBEL_MINIMUM: f64 = -200.0;
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.cpp:32`
/// (`decibel_from_linear`): -inf clamps to the floor.
fn decibel_from_linear(linear: f64) -> f64 {
let v = 20.0 * linear.log10();
if v.is_infinite() {
return DECIBEL_MINIMUM;
}
v
}
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.cpp:99`
/// (`AudioLevelMeter::linear_to_db`).
fn linear_to_db(linear: f64) -> f64 {
if linear <= 0.0 {
return DECIBEL_MINIMUM;
}
decibel_from_linear(linear)
}
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.cpp:107`
/// (`AudioLevelMeter::power_to_lufs`): BS.1770-compatible unit, no
/// K-weighting.
fn power_to_lufs(mean_square: f64) -> f64 {
if mean_square <= 0.0 {
return DECIBEL_MINIMUM;
}
-0.691 + 10.0 * mean_square.log10()
}
/// Per-channel statistics for a single analysis pass.
///
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.h`
/// (`AudioLevelMeter::ChannelStats`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ChannelStats {
/// Peak amplitude, linear scale. `0.0` when silent.
pub peak_linear: f64,
/// Peak amplitude, decibel scale. `-200.0` when silent.
pub peak_db: f64,
/// Root-mean-square level, linear scale.
pub rms_linear: f64,
/// Root-mean-square level, decibel scale. `-200.0` when silent.
pub rms_db: f64,
/// VU-meter ballistics reading, decibel scale.
pub vu_db: f64,
}
/// Aggregate statistics over all analyzed channels.
///
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.h`
/// (`AudioLevelMeter::Stats`).
#[derive(Debug, Clone, PartialEq)]
pub struct Stats {
/// Per-channel statistics, indexed by channel.
pub channels: Vec<ChannelStats>,
/// Maximum peak across all channels, linear scale.
pub max_peak_linear: f64,
/// Integrated loudness (EBU R128 LUFS). `-200.0` for silence.
pub integrated_lufs: f64,
/// Whether every channel was silent below the noise gate.
pub silence: bool,
}
/// Compute per-channel and summary statistics for a planar sample buffer.
///
/// `planar` holds one f32 slice per channel; every channel slice is assumed to
/// have the same length. Mirrors `AudioLevelMeter::analyze_sample_buffer`.
///
/// `// CPP-PARITY: src/audio/src/audiolevelmeter.cpp:42`
/// (`AudioLevelMeter::analyze_sample_buffer`): VU == RMS dB (no separate
/// ballistics); the silence gate is `qFuzzyIsNull` (|x| < 1e-12) on the
/// max peak; LUFS uses the mean square over ALL channels' samples.
pub fn analyze_sample_buffer(planar: &[&[f32]]) -> Stats {
let mut stats = Stats {
channels: vec![
ChannelStats {
peak_linear: 0.0,
peak_db: DECIBEL_MINIMUM,
rms_linear: 0.0,
rms_db: DECIBEL_MINIMUM,
vu_db: DECIBEL_MINIMUM,
};
planar.len()
],
max_peak_linear: 0.0,
integrated_lufs: DECIBEL_MINIMUM,
silence: true,
};
if planar.is_empty() || planar[0].is_empty() {
return stats;
}
let sample_count = planar[0].len();
let mut total_square = 0.0f64;
let mut total_samples = 0usize;
for (channel, data) in planar.iter().enumerate() {
let mut peak = 0.0f64;
let mut square_sum = 0.0f64;
for &s in data.iter() {
let value = f64::from(s);
peak = peak.max(value.abs());
square_sum += value * value;
}
let mean_square = square_sum / sample_count as f64;
let rms = mean_square.sqrt();
let rms_db = linear_to_db(rms);
stats.channels[channel] = ChannelStats {
peak_linear: peak,
peak_db: linear_to_db(peak),
rms_linear: rms,
rms_db,
vu_db: rms_db,
};
stats.max_peak_linear = stats.max_peak_linear.max(peak);
total_square += square_sum;
total_samples += sample_count;
}
// qFuzzyIsNull(double): |x| < 1e-12
stats.silence = stats.max_peak_linear.abs() < 1e-12;
stats.integrated_lufs = power_to_lufs(total_square / total_samples as f64);
stats
}