#![forbid(unsafe_code)]
use crate::frame::AudioFrame;
use std::collections::VecDeque;
pub struct DialogueDetector {
sample_rate: f64,
channels: usize,
speech_detector: SpeechDetector,
dialogue_gate: DialogueGate,
dialogue_accumulator: f64,
dialogue_samples: usize,
}
struct SpeechDetector {
energy_threshold: f64,
flatness_threshold: f64,
zcr_threshold: f64,
frame_buffer: VecDeque<f64>,
frame_size: usize,
hop_size: usize,
sample_count: usize,
}
impl SpeechDetector {
fn new(sample_rate: f64) -> Self {
let frame_size = (sample_rate * 0.025) as usize; let hop_size = frame_size / 2;
Self {
energy_threshold: 0.001,
flatness_threshold: 0.3,
zcr_threshold: 0.15,
frame_buffer: VecDeque::with_capacity(frame_size),
frame_size,
hop_size,
sample_count: 0,
}
}
fn process(&mut self, sample: f64) -> Option<bool> {
self.frame_buffer.push_back(sample);
self.sample_count += 1;
if self.frame_buffer.len() > self.frame_size {
self.frame_buffer.pop_front();
}
if self.sample_count >= self.hop_size && self.frame_buffer.len() == self.frame_size {
self.sample_count = 0;
let is_speech = self.detect_speech();
return Some(is_speech);
}
None
}
fn detect_speech(&self) -> bool {
let energy: f64 = self
.frame_buffer
.iter()
.map(|&s| s * s)
.sum::<f64>()
/ self.frame_size as f64;
if energy < self.energy_threshold {
return false; }
let mut zcr = 0;
for i in 1..self.frame_buffer.len() {
if self.frame_buffer[i - 1] * self.frame_buffer[i] < 0.0 {
zcr += 1;
}
}
let zcr_rate = zcr as f64 / self.frame_size as f64;
let flatness = self.calculate_spectral_flatness();
energy > self.energy_threshold
&& zcr_rate > self.zcr_threshold
&& flatness < self.flatness_threshold
}
fn calculate_spectral_flatness(&self) -> f64 {
let samples: Vec<f64> = self.frame_buffer.iter().copied().collect();
let geometric_mean = self.geometric_mean(&samples);
let arithmetic_mean = self.arithmetic_mean(&samples);
if arithmetic_mean > 0.0 {
geometric_mean / arithmetic_mean
} else {
0.0
}
}
fn geometric_mean(&self, values: &[f64]) -> f64 {
let product: f64 = values.iter().map(|&x| x.abs() + 1e-10).product();
product.powf(1.0 / values.len() as f64)
}
fn arithmetic_mean(&self, values: &[f64]) -> f64 {
values.iter().map(|&x| x.abs()).sum::<f64>() / values.len() as f64
}
fn reset(&mut self) {
self.frame_buffer.clear();
self.sample_count = 0;
}
}
struct DialogueGate {
gate_threshold: f64,
max_level: f64,
confidence_history: VecDeque<bool>,
history_size: usize,
}
impl DialogueGate {
fn new() -> Self {
Self {
gate_threshold: -10.0, max_level: 0.0,
confidence_history: VecDeque::with_capacity(50),
history_size: 50,
}
}
fn is_dialogue(&mut self, level: f64, is_speech: bool) -> bool {
self.max_level = self.max_level.max(level);
self.confidence_history.push_back(is_speech);
if self.confidence_history.len() > self.history_size {
self.confidence_history.pop_front();
}
let confidence = self
.confidence_history
.iter()
.filter(|&&x| x)
.count() as f64
/ self.confidence_history.len() as f64;
let level_db = if level > 0.0 {
20.0 * level.log10()
} else {
-100.0
};
let max_db = if self.max_level > 0.0 {
20.0 * self.max_level.log10()
} else {
-100.0
};
level_db > max_db + self.gate_threshold && confidence > 0.6
}
fn reset(&mut self) {
self.max_level = 0.0;
self.confidence_history.clear();
}
}
impl DialogueDetector {
pub fn new(sample_rate: f64, channels: usize) -> Self {
Self {
sample_rate,
channels,
speech_detector: SpeechDetector::new(sample_rate),
dialogue_gate: DialogueGate::new(),
dialogue_accumulator: 0.0,
dialogue_samples: 0,
}
}
pub fn process(&mut self, samples: &[f64]) {
let frames = samples.len() / self.channels;
for frame_idx in 0..frames {
let mut frame_energy = 0.0;
for ch in 0..self.channels {
let idx = frame_idx * self.channels + ch;
if idx < samples.len() {
frame_energy += samples[idx].abs();
}
}
frame_energy /= self.channels as f64;
if let Some(is_speech) = self.speech_detector.process(frame_energy) {
if self.dialogue_gate.is_dialogue(frame_energy, is_speech) {
self.dialogue_accumulator += frame_energy * frame_energy;
self.dialogue_samples += 1;
}
}
}
}
pub fn dialogue_level(&self) -> f64 {
if self.dialogue_samples > 0 {
let rms = (self.dialogue_accumulator / self.dialogue_samples as f64).sqrt();
if rms > 0.0 {
20.0 * rms.log10()
} else {
-100.0
}
} else {
-100.0
}
}
pub fn dialogue_percentage(&self) -> f64 {
if self.dialogue_gate.confidence_history.is_empty() {
return 0.0;
}
self.dialogue_gate
.confidence_history
.iter()
.filter(|&&x| x)
.count() as f64
/ self.dialogue_gate.confidence_history.len() as f64
* 100.0
}
pub fn calculate_dialnorm(&self) -> i32 {
let level = self.dialogue_level();
let clamped = level.max(-31.0).min(0.0);
clamped.round() as i32
}
pub fn reset(&mut self) {
self.speech_detector.reset();
self.dialogue_gate.reset();
self.dialogue_accumulator = 0.0;
self.dialogue_samples = 0;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DrcProfile {
FilmStandard,
FilmLight,
MusicStandard,
MusicLight,
Speech,
None,
}
impl DrcProfile {
pub fn compression_ratio(&self) -> f64 {
match self {
Self::FilmStandard => 1.0, Self::FilmLight => 2.0, Self::MusicStandard => 1.5, Self::MusicLight => 2.5, Self::Speech => 3.0, Self::None => 1.0,
}
}
pub fn compr_value(&self) -> u8 {
match self {
Self::FilmStandard => 0x00,
Self::FilmLight => 0x20,
Self::MusicStandard => 0x10,
Self::MusicLight => 0x30,
Self::Speech => 0x40,
Self::None => 0xFF,
}
}
pub fn dynrng_value(&self) -> u8 {
match self {
Self::FilmStandard => 0x00,
Self::FilmLight => 0x20,
Self::MusicStandard => 0x10,
Self::MusicLight => 0x30,
Self::Speech => 0x40,
Self::None => 0xFF,
}
}
}
#[derive(Clone, Debug)]
pub struct DolbyMetadata {
pub dialnorm: i32,
pub drc_profile: DrcProfile,
pub lfe_level: f64,
pub room_type: RoomType,
pub mixing_level: f64,
pub copyright: bool,
pub original_bitstream: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RoomType {
NotIndicated,
LargeRoom,
SmallRoom,
}
impl RoomType {
pub fn code(&self) -> u8 {
match self {
Self::NotIndicated => 0,
Self::LargeRoom => 1,
Self::SmallRoom => 2,
}
}
}
impl Default for DolbyMetadata {
fn default() -> Self {
Self {
dialnorm: -27,
drc_profile: DrcProfile::FilmStandard,
lfe_level: 0.0,
room_type: RoomType::NotIndicated,
mixing_level: 85.0,
copyright: false,
original_bitstream: true,
}
}
}
impl DolbyMetadata {
pub fn new(dialnorm: i32) -> Self {
let clamped_dialnorm = dialnorm.max(-31).min(0);
Self {
dialnorm: clamped_dialnorm,
..Default::default()
}
}
pub fn with_drc_profile(mut self, profile: DrcProfile) -> Self {
self.drc_profile = profile;
self
}
pub fn with_lfe_level(mut self, level: f64) -> Self {
self.lfe_level = level;
self
}
pub fn with_room_type(mut self, room_type: RoomType) -> Self {
self.room_type = room_type;
self
}
pub fn with_mixing_level(mut self, level: f64) -> Self {
self.mixing_level = level;
self
}
pub fn validate(&self) -> Result<(), String> {
if self.dialnorm < -31 || self.dialnorm > 0 {
return Err(format!(
"Invalid dialnorm: {} (must be -31 to 0)",
self.dialnorm
));
}
if self.mixing_level < 80.0 || self.mixing_level > 111.0 {
return Err(format!(
"Invalid mixing level: {} (must be 80-111 dB SPL)",
self.mixing_level
));
}
Ok(())
}
}
pub struct LeqAMeter {
sample_rate: f64,
channels: usize,
a_weight: AWeightingFilter,
accumulator: f64,
sample_count: usize,
}
struct AWeightingFilter {
b: [f64; 7],
a: [f64; 7],
states: Vec<Vec<f64>>,
}
impl AWeightingFilter {
fn new(sample_rate: f64, channels: usize) -> Self {
let b = [
0.169994948147430,
0.0,
-0.509984844442290,
0.0,
0.509984844442290,
0.0,
-0.169994948147430,
];
let a = [
1.0,
-2.12979364760736,
0.42996125885751,
1.62132698199721,
-0.96669962900954,
0.00121015844426,
0.04400300696788,
];
Self {
b,
a,
states: vec![vec![0.0; 7]; channels],
}
}
fn process(&mut self, sample: f64, channel: usize) -> f64 {
let state = &mut self.states[channel];
let mut y = self.b[0] * sample + state[0];
for i in 0..6 {
state[i] = self.b[i + 1] * sample - self.a[i + 1] * y + state[i + 1];
}
y
}
fn reset(&mut self) {
for state in &mut self.states {
state.fill(0.0);
}
}
}
impl LeqAMeter {
pub fn new(sample_rate: f64, channels: usize) -> Self {
Self {
sample_rate,
channels,
a_weight: AWeightingFilter::new(sample_rate, channels),
accumulator: 0.0,
sample_count: 0,
}
}
pub fn process(&mut self, samples: &[f64]) {
let frames = samples.len() / self.channels;
for frame_idx in 0..frames {
for ch in 0..self.channels {
let idx = frame_idx * self.channels + ch;
if idx < samples.len() {
let weighted = self.a_weight.process(samples[idx], ch);
self.accumulator += weighted * weighted;
self.sample_count += 1;
}
}
}
}
pub fn leq_a(&self) -> f64 {
if self.sample_count > 0 {
let rms = (self.accumulator / self.sample_count as f64).sqrt();
94.0 + 20.0 * rms.log10()
} else {
f64::NEG_INFINITY
}
}
pub fn reset(&mut self) {
self.a_weight.reset();
self.accumulator = 0.0;
self.sample_count = 0;
}
}
pub struct LeqMMeter {
sample_rate: f64,
channels: usize,
m_weight: MWeightingFilter,
accumulator: f64,
sample_count: usize,
}
struct MWeightingFilter {
hp_state: Vec<(f64, f64, f64, f64)>,
lp_state: Vec<(f64, f64, f64, f64)>,
channels: usize,
}
impl MWeightingFilter {
fn new(_sample_rate: f64, channels: usize) -> Self {
Self {
hp_state: vec![(0.0, 0.0, 0.0, 0.0); channels],
lp_state: vec![(0.0, 0.0, 0.0, 0.0); channels],
channels,
}
}
fn process(&mut self, sample: f64, channel: usize) -> f64 {
let hp = self.high_pass(sample, channel);
self.low_pass(hp, channel)
}
fn high_pass(&mut self, x: f64, ch: usize) -> f64 {
let (x1, x2, y1, y2) = self.hp_state[ch];
let b0 = 0.998;
let b1 = -1.996;
let b2 = 0.998;
let a1 = -1.996;
let a2 = 0.996;
let y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
self.hp_state[ch] = (x, x1, y, y1);
y
}
fn low_pass(&mut self, x: f64, ch: usize) -> f64 {
let (x1, x2, y1, y2) = self.lp_state[ch];
let b0 = 0.01;
let b1 = 0.02;
let b2 = 0.01;
let a1 = -1.5;
let a2 = 0.6;
let y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
self.lp_state[ch] = (x, x1, y, y1);
y
}
fn reset(&mut self) {
self.hp_state.fill((0.0, 0.0, 0.0, 0.0));
self.lp_state.fill((0.0, 0.0, 0.0, 0.0));
}
}
impl LeqMMeter {
pub fn new(sample_rate: f64, channels: usize) -> Self {
Self {
sample_rate,
channels,
m_weight: MWeightingFilter::new(sample_rate, channels),
accumulator: 0.0,
sample_count: 0,
}
}
pub fn process(&mut self, samples: &[f64]) {
let frames = samples.len() / self.channels;
for frame_idx in 0..frames {
for ch in 0..self.channels {
let idx = frame_idx * self.channels + ch;
if idx < samples.len() {
let weighted = self.m_weight.process(samples[idx], ch);
self.accumulator += weighted * weighted;
self.sample_count += 1;
}
}
}
}
pub fn leq_m(&self) -> f64 {
if self.sample_count > 0 {
let rms = (self.accumulator / self.sample_count as f64).sqrt();
85.0 + 20.0 * rms.log10() } else {
f64::NEG_INFINITY
}
}
pub fn reset(&mut self) {
self.m_weight.reset();
self.accumulator = 0.0;
self.sample_count = 0;
}
}
pub struct DolbyMeter {
sample_rate: f64,
channels: usize,
dialogue_detector: DialogueDetector,
leq_a: LeqAMeter,
leq_m: LeqMMeter,
metadata: DolbyMetadata,
}
impl DolbyMeter {
pub fn new(sample_rate: f64, channels: usize) -> Self {
Self {
sample_rate,
channels,
dialogue_detector: DialogueDetector::new(sample_rate, channels),
leq_a: LeqAMeter::new(sample_rate, channels),
leq_m: LeqMMeter::new(sample_rate, channels),
metadata: DolbyMetadata::default(),
}
}
pub fn process(&mut self, frame: &AudioFrame) {
let samples = extract_samples_f64(frame);
self.dialogue_detector.process(&samples);
self.leq_a.process(&samples);
self.leq_m.process(&samples);
}
pub fn get_metrics(&mut self) -> DolbyMetrics {
self.metadata.dialnorm = self.dialogue_detector.calculate_dialnorm();
DolbyMetrics {
dialogue_level: self.dialogue_detector.dialogue_level(),
dialogue_percentage: self.dialogue_detector.dialogue_percentage(),
dialnorm: self.metadata.dialnorm,
leq_a: self.leq_a.leq_a(),
leq_m: self.leq_m.leq_m(),
metadata: self.metadata.clone(),
}
}
pub fn get_metadata(&self) -> &DolbyMetadata {
&self.metadata
}
pub fn set_drc_profile(&mut self, profile: DrcProfile) {
self.metadata.drc_profile = profile;
}
pub fn set_room_type(&mut self, room_type: RoomType) {
self.metadata.room_type = room_type;
}
pub fn set_mixing_level(&mut self, level: f64) {
self.metadata.mixing_level = level;
}
pub fn reset(&mut self) {
self.dialogue_detector.reset();
self.leq_a.reset();
self.leq_m.reset();
}
}
#[derive(Clone, Debug)]
pub struct DolbyMetrics {
pub dialogue_level: f64,
pub dialogue_percentage: f64,
pub dialnorm: i32,
pub leq_a: f64,
pub leq_m: f64,
pub metadata: DolbyMetadata,
}
pub struct AtmosDialogueDetector {
base_detector: DialogueDetector,
objects: Vec<ObjectChannel>,
}
struct ObjectChannel {
id: usize,
position: (f64, f64, f64),
dialogue_confidence: f64,
}
impl AtmosDialogueDetector {
pub fn new(sample_rate: f64, channels: usize, num_objects: usize) -> Self {
let mut objects = Vec::with_capacity(num_objects);
for i in 0..num_objects {
objects.push(ObjectChannel {
id: i,
position: (0.0, 0.0, 0.0),
dialogue_confidence: 0.0,
});
}
Self {
base_detector: DialogueDetector::new(sample_rate, channels),
objects,
}
}
pub fn process(&mut self, samples: &[f64]) {
self.base_detector.process(samples);
for obj in &mut self.objects {
obj.dialogue_confidence = self.base_detector.dialogue_percentage() / 100.0;
}
}
pub fn object_dialogue_level(&self, object_id: usize) -> Option<f64> {
self.objects
.get(object_id)
.map(|obj| self.base_detector.dialogue_level() * obj.dialogue_confidence)
}
pub fn dialogue_level(&self) -> f64 {
self.base_detector.dialogue_level()
}
pub fn reset(&mut self) {
self.base_detector.reset();
for obj in &mut self.objects {
obj.dialogue_confidence = 0.0;
}
}
}
fn extract_samples_f64(frame: &AudioFrame) -> Vec<f64> {
match &frame.samples {
crate::frame::AudioBuffer::Interleaved(data) => {
let sample_count = data.len() / 4;
let mut samples = Vec::with_capacity(sample_count);
for i in 0..sample_count {
let offset = i * 4;
if offset + 4 <= data.len() {
let bytes_array = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
let sample = f32::from_le_bytes(bytes_array);
samples.push(f64::from(sample));
}
}
samples
}
crate::frame::AudioBuffer::Planar(planes) => {
if planes.is_empty() {
return Vec::new();
}
let channels = planes.len();
let sample_size = std::mem::size_of::<f32>();
let frames = planes[0].len() / sample_size;
let mut interleaved = Vec::with_capacity(frames * channels);
for frame_idx in 0..frames {
for plane in planes {
let offset = frame_idx * sample_size;
if offset + 4 <= plane.len() {
let bytes_array = [
plane[offset],
plane[offset + 1],
plane[offset + 2],
plane[offset + 3],
];
let sample = f32::from_le_bytes(bytes_array);
interleaved.push(f64::from(sample));
}
}
}
interleaved
}
}
}