// Copyright (C) 2019-2020 Sebastian Dröge // // Audio processing part of this file ported from ffmpeg/libavfilter/af_loudnorm.c // // Original C code // Copyright (c) 2016 Kyle Swanson // licensed under the LGPL-2.1+ and generously relicensed to MPL-2.0. // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; use gst::prelude::*; use gst::subclass::prelude::*; use std::mem; use std::sync::Mutex; use std::u64; use byte_slice_cast::*; use once_cell::sync::Lazy; use atomic_refcell::AtomicRefCell; static CAT: Lazy = Lazy::new(|| { gst::DebugCategory::new( "audioloudnorm", gst::DebugColorFlags::empty(), Some("Audio Loudless Normalization Filter"), ) }); const DEFAULT_LOUDNESS_TARGET: f64 = -24.0; const DEFAULT_LOUDNESS_RANGE_TARGET: f64 = 7.0; const DEFAULT_MAX_TRUE_PEAK: f64 = -2.0; const DEFAULT_OFFSET: f64 = 0.0; #[derive(Debug, Clone, Copy)] struct Settings { pub loudness_target: f64, pub loudness_range_target: f64, pub max_true_peak: f64, pub offset: f64, } impl Default for Settings { fn default() -> Self { Settings { loudness_target: DEFAULT_LOUDNESS_TARGET, loudness_range_target: DEFAULT_LOUDNESS_RANGE_TARGET, max_true_peak: DEFAULT_MAX_TRUE_PEAK, offset: DEFAULT_OFFSET, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FrameType { First, Inner, Final, Linear, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LimiterState { Out, Attack, Sustain, Release, } struct State { info: gst_audio::AudioInfo, adapter: gst_base::UniqueAdapter, // Current amount of sample we consume per iteration: for the first frame 3s, afterwards 100ms current_samples_per_frame: usize, // Settings during setup offset: f64, target_i: f64, target_lra: f64, target_tp: f64, // Input ringbuffer for loudness analysis // TODO: Convert to a proper ringbuffer buf: Box<[f64]>, // read index buf_index: usize, // write index (always 210ms from buf_index) prev_buf_index: usize, // Gaussian filter for gains // TODO: These are actually constant. Once `for` is allowed // in `const fn` we can make them proper constants. weights: [f64; 21], // TODO: Convert to a proper ringbuffer delta: [f64; 30], index: usize, prev_delta: f64, // Limiter gain_reduction: [f64; 2], // TODO: Convert to a proper ringbuffer limiter_buf: Box<[f64]>, // Read/write index, depending on context limiter_buf_index: usize, // Previous sample (potentially of the previous frame) used for detecting peaks in the limiter prev_smp: Box<[f64]>, limiter_state: LimiterState, // During attack/release state, the position in the corresponding window env_cnt: usize, // Number of samples to sustain at the beginning of the sustain state, if any sustain_cnt: Option, frame_type: FrameType, above_threshold: bool, // Input loudness calculation r128_in: ebur128::EbuR128, // Actual output loudness calculation r128_out: ebur128::EbuR128, } impl State { fn new(settings: &Settings, info: gst_audio::AudioInfo) -> Self { let r128_in = ebur128::EbuR128::new( info.channels(), info.rate(), ebur128::Mode::HISTOGRAM | ebur128::Mode::I | ebur128::Mode::S | ebur128::Mode::LRA | ebur128::Mode::SAMPLE_PEAK, ) .unwrap(); let r128_out = ebur128::EbuR128::new( info.channels(), info.rate(), ebur128::Mode::HISTOGRAM | ebur128::Mode::I | ebur128::Mode::S | ebur128::Mode::LRA | ebur128::Mode::SAMPLE_PEAK, ) .unwrap(); let buf_size = GAIN_LOOKAHEAD * info.channels() as usize; let buf = vec![0.0; buf_size].into_boxed_slice(); let limiter_buf_size = (2 * FRAME_SIZE + LIMITER_LOOKAHEAD) * info.channels() as usize; let limiter_buf = vec![0.0; limiter_buf_size].into_boxed_slice(); let prev_smp = vec![0.0; info.channels() as usize].into_boxed_slice(); let current_samples_per_frame = GAIN_LOOKAHEAD; let buf_index = 0; let prev_buf_index = 0; let limiter_buf_index = 0; let index = 1; let limiter_state = LimiterState::Out; let offset = f64::powf(10., settings.offset / 20.); let target_tp = f64::powf(10., settings.max_true_peak / 20.); State { info, adapter: gst_base::UniqueAdapter::new(), current_samples_per_frame, offset, target_i: settings.loudness_target, target_lra: settings.loudness_range_target, target_tp, buf, buf_index, prev_buf_index, delta: [0.0; 30], weights: init_gaussian_filter(), prev_delta: 0.0, index, gain_reduction: [0.0; 2], limiter_buf, prev_smp, limiter_buf_index, limiter_state, env_cnt: 0, sustain_cnt: None, frame_type: FrameType::First, above_threshold: false, r128_in, r128_out, } } } pub struct AudioLoudNorm { srcpad: gst::Pad, sinkpad: gst::Pad, settings: Mutex, state: AtomicRefCell>, } // Gain analysis parameters const GAIN_LOOKAHEAD: usize = 3 * 192_000; // 3s const FRAME_SIZE: usize = 19_200; // 100ms // Limiter parameters const LIMITER_ATTACK_WINDOW: usize = 1920; // 10ms const LIMITER_RELEASE_WINDOW: usize = 19_200; // 100ms const LIMITER_LOOKAHEAD: usize = 1920; // 10ms impl State { // Drains all full frames that are currently in the adapter fn drain_full_frames( &mut self, imp: &AudioLoudNorm, ) -> Result, gst::FlowError> { let mut outbufs = vec![]; while self.adapter.available() >= self.info.bpf() as usize * self.current_samples_per_frame { let (pts, distance) = self.adapter.prev_pts(); let distance_samples = distance / self.info.bpf() as u64; let distance_ts = distance_samples .mul_div_floor(*gst::ClockTime::SECOND, self.info.rate() as u64) .map(gst::ClockTime::from_nseconds); let pts = pts .opt_checked_add(distance_ts) .map_err(|_| gst::FlowError::Error)?; let inbuf = self .adapter .take_buffer(self.info.bpf() as usize * self.current_samples_per_frame) .unwrap(); let src = inbuf.map_readable().map_err(|_| gst::FlowError::Error)?; let src = src .as_slice_of::() .map_err(|_| gst::FlowError::Error)?; let (mut outbuf, pts) = self.process(imp, src, pts)?; { let outbuf = outbuf.get_mut().unwrap(); outbuf.set_pts(pts); outbuf.set_duration( (outbuf.size() as u64) .mul_div_floor( *gst::ClockTime::SECOND, (self.info.bpf() * self.info.rate()) as u64, ) .map(gst::ClockTime::from_nseconds), ); } outbufs.push(outbuf); } Ok(outbufs) } // Drains everything fn drain(&mut self, imp: &AudioLoudNorm) -> Result { gst::debug!(CAT, imp: imp, "Draining"); let (pts, distance) = self.adapter.prev_pts(); let distance_samples = distance / self.info.bpf() as u64; let distance_ts = distance_samples .mul_div_floor(*gst::ClockTime::SECOND, self.info.rate() as u64) .map(gst::ClockTime::from_nseconds); let pts = pts .opt_checked_add(distance_ts) .map_err(|_| gst::FlowError::Error)?; let mut _mapped_inbuf = None; let src = if self.adapter.available() > 0 { let inbuf = self.adapter.take_buffer(self.adapter.available()).unwrap(); let inbuf = inbuf .into_mapped_buffer_readable() .map_err(|_| gst::FlowError::Error)?; _mapped_inbuf = Some(inbuf); _mapped_inbuf .as_ref() .unwrap() .as_slice_of::() .map_err(|_| gst::FlowError::Error)? } else { &[] }; // If we already output something before then we go into final frame processing, otherwise // we drain any data we still have by doing linear processing. if self.current_samples_per_frame == FRAME_SIZE { self.frame_type = FrameType::Final; } else if src.is_empty() { // Nothing to drain at all gst::debug!(CAT, imp: imp, "No data to drain"); return Err(gst::FlowError::Eos); } let (mut outbuf, pts) = self.process(imp, src, pts)?; { let outbuf = outbuf.get_mut().unwrap(); outbuf.set_pts(pts); outbuf.set_duration( (outbuf.size() as u64) .mul_div_floor( *gst::ClockTime::SECOND, (self.info.bpf() * self.info.rate()) as u64, ) .map(gst::ClockTime::from_nseconds), ); } Ok(outbuf) } fn process_first_frame_is_last(&mut self, imp: &AudioLoudNorm) -> Result<(), gst::FlowError> { // Calculated loudness in LUFS let global = self .r128_in .loudness_global() .map_err(|_| gst::FlowError::Error)?; // Peak sample value for all changes let mut true_peak = 0.0; for c in 0..(self.info.channels()) { let peak = self .r128_in .sample_peak(c) .map_err(|_| gst::FlowError::Error)?; if c == 0 || peak > true_peak { true_peak = peak; } } gst::debug!( CAT, imp: imp, "Calculated global loudness for first frame {} with peak {}", global, true_peak ); // Difference between targetted and calculated LUFS loudness as a linear scalefactor. let offset = f64::powf(10., (self.target_i - global) / 20.); // What the new peak would be after adjusting for the targetted loudness. let offset_tp = true_peak * offset; // If the new peak would be more quiet than targeted one, take it. Otherwise only go as // high as the true peak allows. self.offset = if offset_tp < self.target_tp { offset } else { self.target_tp / true_peak }; self.frame_type = FrameType::Linear; Ok(()) } fn process_first_frame( &mut self, imp: &AudioLoudNorm, src: &[f64], pts: impl Into>, ) -> Result<(gst::Buffer, Option), gst::FlowError> { // Fill our whole buffer here with the initial input, i.e. 3000ms of samples. self.buf.copy_from_slice(src); // Calculate the shortterm loudness in LUFS. let shortterm = self .r128_in .loudness_shortterm() .map_err(|_| gst::FlowError::Error)?; let env_shortterm = if shortterm < -70.0 { self.above_threshold = false; 0. } else { self.above_threshold = true; self.target_i - shortterm }; // Initialize with linear scale factor for reaching the target loudness. for delta in self.delta.iter_mut() { *delta = f64::powf(10.0, env_shortterm / 20.); } self.prev_delta = self.delta[self.index]; gst::debug!( CAT, imp: imp, "Initializing for first frame with gain adjustment of {}", self.prev_delta ); // Fill the whole limiter_buf with the gain corrected first part of the buffered // input, i.e. 210ms. 100ms for the current frame plus 100ms lookahead for the // limiter with the next frame plus 10ms in addition because the limiter would // look a few samples further when detecting a peak to make sure no higher values // are following. for (limiter_buf, sample) in self.limiter_buf.iter_mut().zip(self.buf.iter()) { *limiter_buf = sample * self.prev_delta * self.offset; } // Read position of the buffer is now advanced. self.buf_index = self.limiter_buf.len(); // Write position of the limiter_buf is at the beginning still. We consume // the first 100ms of it below directly so that the next iteration will // overwrite these 100ms directly. self.limiter_buf_index = 0; let mut outbuf = gst::Buffer::with_size(FRAME_SIZE * self.info.bpf() as usize) .map_err(|_| gst::FlowError::Error)?; { let outbuf = outbuf.get_mut().unwrap(); let mut dst = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?; let dst = dst .as_mut_slice_of::() .map_err(|_| gst::FlowError::Error)?; // This now consumes the first 100ms of limiter_buf for the output. self.true_peak_limiter(imp, dst); self.r128_out .add_frames_f64(dst) .map_err(|_| gst::FlowError::Error)?; } // From now on we consume 100ms input frames and output 100ms. self.current_samples_per_frame = FRAME_SIZE; self.frame_type = FrameType::Inner; // PTS is the input PTS for the first frame, we output the first 100ms of the input // buffer here Ok((outbuf, pts.into())) } fn process_fill_inner_frame(&mut self, imp: &AudioLoudNorm, src: &[f64]) { // Get gain for this and the next 100ms frame based the delta array // and smoothened with a gaussian filter. let gain = self.gaussian_filter(if self.index + 10 < 30 { self.index + 10 } else { self.index + 10 - 30 }); let gain_next = self.gaussian_filter(if self.index + 11 < 30 { self.index + 11 } else { self.index + 11 - 30 }); gst::debug!( CAT, imp: imp, "Applying gain adjustment {}-{}", gain, gain_next ); // Overwrite the first 100ms of the limiter_buf with the gain corrected 100ms of // buf. This is correct because either above (for the first frame) or in the // previous iteration here we already have output these 100ms. // // Also fill 100ms of buf with the 100ms of new input at the same time. let channels = self.info.channels() as usize; assert!(src.len() / channels <= FRAME_SIZE); for (n, samples) in src.chunks_exact(channels).enumerate() { // Safety: Index ranges are checked below and both slices from buf are // guaranteed to be non-overlapping (210ms limiter_buf difference). let (buf_read, buf_write, limiter_buf) = unsafe { let buf = &mut &mut *self.buf as *mut &mut [f64]; let buf_read = (*buf).get_unchecked(self.buf_index..(self.buf_index + channels)); let buf_write = (*buf).get_unchecked_mut(self.prev_buf_index..(self.prev_buf_index + channels)); let limiter_buf = self .limiter_buf .get_unchecked_mut(self.limiter_buf_index..(self.limiter_buf_index + channels)); (buf_read, buf_write, limiter_buf) }; buf_write.copy_from_slice(samples); // Linearly interpolate between the current and next gain for each sample. let current_gain = (gain + ((n as f64 / FRAME_SIZE as f64) * (gain_next - gain))) * self.offset; for (o, i) in limiter_buf.iter_mut().zip(buf_read.iter()) { *o = *i * current_gain; } self.limiter_buf_index += channels; if self.limiter_buf_index >= self.limiter_buf.len() { self.limiter_buf_index -= self.limiter_buf.len(); } self.prev_buf_index += channels; if self.prev_buf_index >= self.buf.len() { self.prev_buf_index -= self.buf.len(); } self.buf_index += channels; if self.buf_index >= self.buf.len() { self.buf_index -= self.buf.len(); } } } fn process_update_gain_inner_frame( &mut self, imp: &AudioLoudNorm, ) -> Result<(), gst::FlowError> { // Calculate global, shortterm loudness and relative threshold in LUFS. let global = self .r128_in .loudness_global() .map_err(|_| gst::FlowError::Error)?; let shortterm = self .r128_in .loudness_shortterm() .map_err(|_| gst::FlowError::Error)?; let relative_threshold = self .r128_in .relative_threshold() .map_err(|_| gst::FlowError::Error)?; gst::debug!( CAT, imp: imp, "Calculated global loudness {}, short term loudness {} and relative threshold {}", global, shortterm, relative_threshold ); // If we were previously not above the threshold but are now above in the // shortterm, slightly increase the scale factor. If the shortterm output was above // the target then also consider this frame above threshold. if !self.above_threshold { if shortterm > -70.0 { self.prev_delta *= 1.0058; } let shortterm_out = self .r128_out .loudness_shortterm() .map_err(|_| gst::FlowError::Error)?; if shortterm_out >= self.target_i { self.above_threshold = true; gst::debug!( CAT, imp: imp, "Above threshold now ({} >= {}, {} > -70)", shortterm_out, self.target_i, shortterm ); } } // If we're still below the threshold, continue using the previous delta. Otherwise // calculate a new one. if shortterm < relative_threshold || shortterm <= -70. || !self.above_threshold { self.delta[self.index] = self.prev_delta; } else { let env_global = if (shortterm - global).abs() < (self.target_lra / 2.) { shortterm - global } else if (self.target_lra / 2.) * (shortterm - global) < 0.0 { -1. } else { 1. }; let env_shortterm = self.target_i - shortterm; self.delta[self.index] = f64::powf(10., (env_global + env_shortterm) / 20.); } self.prev_delta = self.delta[self.index]; gst::debug!( CAT, imp: imp, "Calculated new gain adjustment {}", self.prev_delta ); self.index += 1; if self.index >= 30 { self.index -= 30; } Ok(()) } fn process_inner_frame( &mut self, imp: &AudioLoudNorm, src: &[f64], pts: impl Into>, ) -> Result<(gst::Buffer, Option), gst::FlowError> { // Fill in these 100ms and adjust its gain according to previous measurements, and // at the same time copy 100ms over to the limiter_buf. self.process_fill_inner_frame(imp, src); // limiter_buf_index was 100ms advanced above, which brings us to exactly the // position where we have to start consuming 100ms for the output now, and exactly // the position where we have to start writing the next 100ms in the next // iteration. let mut outbuf = gst::Buffer::with_size( self.current_samples_per_frame as usize * self.info.bpf() as usize, ) .map_err(|_| gst::FlowError::Error)?; { let outbuf = outbuf.get_mut().unwrap(); let mut dst = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?; let dst = dst .as_mut_slice_of::() .map_err(|_| gst::FlowError::Error)?; // This now consumes the next 100ms of limiter_buf for the output. self.true_peak_limiter(imp, dst); self.r128_out .add_frames_f64(dst) .map_err(|_| gst::FlowError::Error)?; } self.process_update_gain_inner_frame(imp)?; // PTS is 2.9s seconds before the input PTS as we buffer 3s of samples and just // outputted here the first 100ms of that. let pts = pts.into().map(|pts| pts + 100.mseconds() - 3.seconds()); Ok((outbuf, pts)) } fn process_fill_final_frame(&mut self, _imp: &AudioLoudNorm, idx: usize, num_samples: usize) { let channels = self.info.channels() as usize; // Get gain for this and the next 100ms frame based the delta array // and smoothened with a gaussian filter. let gain = self.gaussian_filter(if self.index + 10 < 30 { self.index + 10 } else { self.index + 10 - 30 }); let gain_next = self.gaussian_filter(if self.index + 11 < 30 { self.index + 11 } else { self.index + 11 - 30 }); for n in idx..num_samples { // Safety: Index ranges are checked below. let (buf_read, limiter_buf) = unsafe { let buf_read = self .buf .get_unchecked(self.buf_index..(self.buf_index + channels)); let limiter_buf = self .limiter_buf .get_unchecked_mut(self.limiter_buf_index..(self.limiter_buf_index + channels)); (buf_read, limiter_buf) }; // Linearly interpolate between the current and next gain for each sample. let current_gain = (gain + ((n as f64 / num_samples as f64) * (gain_next - gain))) * self.offset; for (o, i) in limiter_buf.iter_mut().zip(buf_read.iter()) { *o = *i * current_gain; } self.limiter_buf_index += channels; if self.limiter_buf_index >= self.limiter_buf.len() { self.limiter_buf_index -= self.limiter_buf.len(); } self.buf_index += channels; if self.buf_index >= self.buf.len() { self.buf_index -= self.buf.len(); } } } fn process_final_frame( &mut self, imp: &AudioLoudNorm, src: &[f64], pts: impl Into>, ) -> Result<(gst::Buffer, Option), gst::FlowError> { let channels = self.info.channels() as usize; let num_samples = src.len() / channels; // First process any new/leftover data we get passed. This is the same // as for inner frames. After this we will have done all gain adjustments // and all samples we ever output are in buf or limiter_buf. self.process_fill_inner_frame(imp, src); // If we got passed less than 100ms in src then limiter_buf_index is now // not yet at the correct read position! Adjust accordingly here so that all // further reads come from the right position by copying over the next samples // from buf. if num_samples != FRAME_SIZE { self.process_fill_final_frame(imp, num_samples, FRAME_SIZE); } // Now repeatadly run the limiter, output the output gain, update the gains, copy further // data from the buf to limiter_buf until we have output everything. // // At this point we have to output 3s - (FRAME_SIZE - num_samples) // buf. let out_num_samples = 30 * FRAME_SIZE - (FRAME_SIZE - num_samples); let mut outbuf = gst::Buffer::with_size(out_num_samples * self.info.bpf() as usize) .map_err(|_| gst::FlowError::Error)?; { let outbuf = outbuf.get_mut().unwrap(); let mut dst = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?; let dst = dst .as_mut_slice_of::() .map_err(|_| gst::FlowError::Error)?; let mut smp_cnt = 0; while smp_cnt < out_num_samples { let frame_size = std::cmp::min(out_num_samples - smp_cnt, FRAME_SIZE); let dst = &mut dst[(smp_cnt * channels)..((smp_cnt + frame_size) * channels)]; // This now consumes the next frame_size samples of limiter_buf for the output. // Note that on the very last call this will read up to 10ms of old limiter_buf // data but as this was already processed it will not find any peak in there and // just pass through. //if frame_size < FRAME_SIZE { // self.limiter_buf_index += FRAME_SIZE - num_samples; //} self.true_peak_limiter(imp, dst); smp_cnt += frame_size; if smp_cnt == out_num_samples { break; } // Update the gain for the next iteration self.r128_out .add_frames_f64(dst) .map_err(|_| gst::FlowError::Error)?; self.process_update_gain_inner_frame(imp)?; // And now copy over the next block of samples from buf to limiter_buf let next_frame_size = std::cmp::min(out_num_samples - smp_cnt, FRAME_SIZE); self.process_fill_final_frame(imp, 0, next_frame_size); // Now for the very last frame we need to update the limiter buffer index by the // amount of samples the last frame is short to reach the correct read position. if next_frame_size < FRAME_SIZE { self.limiter_buf_index += FRAME_SIZE - next_frame_size; if self.limiter_buf_index > self.limiter_buf.len() { self.limiter_buf_index -= self.limiter_buf.len(); } } } } // PTS is 2.9s seconds before the input PTS as we buffer 3s of samples and just // outputted here the first 100ms of that. let pts = pts.into().map(|pts| pts + 100.mseconds() - 3.seconds()); Ok((outbuf, pts)) } fn process_linear_frame( &mut self, imp: &AudioLoudNorm, src: &[f64], pts: impl Into>, ) -> Result<(gst::Buffer, Option), gst::FlowError> { // Apply a linear scale factor to the whole buffer gst::debug!( CAT, imp: imp, "Applying linear gain adjustment of {}", self.offset ); let mut outbuf = gst::Buffer::with_size(src.len() * mem::size_of::()) .map_err(|_| gst::FlowError::Error)?; { let outbuf = outbuf.get_mut().unwrap(); let mut dst = outbuf.map_writable().map_err(|_| gst::FlowError::Error)?; let dst = dst .as_mut_slice_of::() .map_err(|_| gst::FlowError::Error)?; for (o, i) in dst.iter_mut().zip(src.iter()) { *o = *i * self.offset; } self.r128_out .add_frames_f64(dst) .map_err(|_| gst::FlowError::Error)?; } // PTS is input PTS as we just pass through the data without latency. Ok((outbuf, pts.into())) } fn process( &mut self, imp: &AudioLoudNorm, src: &[f64], pts: impl Into>, ) -> Result<(gst::Buffer, Option), gst::FlowError> { self.r128_in .add_frames_f64(src) .map_err(|_| gst::FlowError::Error)?; // If we are at the end and had less than 3s of samples overall, do simple linear volume // adjustment. frame_type should only ever be set to Final at the end if we ended up in // Inner state before. if self.frame_type == FrameType::First && (src.len() / self.info.channels() as usize) < self.current_samples_per_frame as usize { self.process_first_frame_is_last(imp)?; } match self.frame_type { FrameType::First => self.process_first_frame(imp, src, pts), FrameType::Inner => self.process_inner_frame(imp, src, pts), FrameType::Final => self.process_final_frame(imp, src, pts), FrameType::Linear => self.process_linear_frame(imp, src, pts), } } fn true_peak_limiter_out( &mut self, imp: &AudioLoudNorm, mut smp_cnt: usize, nb_samples: usize, ) -> usize { // Default out state, check if we have a new peak to act on in the next frame // and otherwise simply output all samples with the current gain adjustment. let peak = self.detect_peak(smp_cnt, nb_samples - smp_cnt); if let Some((peak_delta, peak_value)) = peak { self.limiter_state = LimiterState::Attack; self.env_cnt = 0; self.sustain_cnt = None; self.gain_reduction[0] = 1.; self.gain_reduction[1] = self.target_tp / peak_value; // Skip all samples that don't have to be adjusted because the peak is far // enough in the future. // Note: peak_delta=0 is LIMITER_LOOKAHEAD in the future and we have to start // LIMITER_ATTACK_WINDOW before the peak position. smp_cnt += LIMITER_LOOKAHEAD + peak_delta - LIMITER_ATTACK_WINDOW; gst::debug!( CAT, imp: imp, "Found peak {} at sample {}, going to attack state at sample {} (gain reduction {}-{})", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, self.gain_reduction[0], self.gain_reduction[1] ); } else { // Process all samples, no peak found smp_cnt = nb_samples; } smp_cnt } fn true_peak_limiter_attack( &mut self, imp: &AudioLoudNorm, mut smp_cnt: usize, nb_samples: usize, ) -> usize { let channels = self.info.channels() as usize; // Attack state, we have a peak in the near future and need to apply gain // reduction smoothly over the next milliseconds to not go over the threshold. // Once env_cnt reaches attack window we're at the peak sample. // // As there might be another, higher peak right afterwards we still need to // check for this and potentially update the gain reduction accordingly. let peak = self.detect_peak(smp_cnt, nb_samples - smp_cnt); let mut new_peak_smp_cnt = None; if let Some((peak_delta, _)) = peak { // If smp_cnt == new_peak_smp we're exactly 10ms before the new, higher // peak and need to increase the slope. new_peak_smp_cnt = Some(smp_cnt + peak_delta); } let mut index = self.limiter_buf_index + smp_cnt * channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } while self.env_cnt < LIMITER_ATTACK_WINDOW && smp_cnt < nb_samples { // Stop once we're exactly 10ms before the new higher peak so we can // restart the attack state. if let Some(new_peak_smp_cnt) = new_peak_smp_cnt { if smp_cnt == new_peak_smp_cnt { break; } } // Linear interpolation between the start and target gain reduction let env = self.gain_reduction[0] - (self.env_cnt as f64 / (LIMITER_ATTACK_WINDOW as f64 - 1.0) * (self.gain_reduction[0] - self.gain_reduction[1])); // Safety: Index checked below let samples = unsafe { self.limiter_buf .get_unchecked_mut(index..(index + channels)) }; for sample in samples { *sample *= env; } index += channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } smp_cnt += 1; self.env_cnt += 1; } if let Some(new_peak_smp) = new_peak_smp_cnt { assert!(smp_cnt < nb_samples); // Sustain until we are exactly 10ms before the new peak in case // we finished the attack window above already. if smp_cnt < new_peak_smp { for _ in smp_cnt..new_peak_smp { // Safety: Index checked below let samples = unsafe { self.limiter_buf .get_unchecked_mut(index..(index + channels)) }; for sample in samples { *sample *= self.gain_reduction[1]; } index += channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } } smp_cnt = new_peak_smp; } assert!(smp_cnt < nb_samples); let (_, peak_value) = peak.unwrap(); let gain_reduction = self.target_tp / peak_value; // If the gain reduction is more than our current target gain reduction we // need to change the attack state. If it less or the same we can simply // contain the current attack state as we will end up at a low enough again // before the new peak. We however have to remember to sustain at least // that long. if gain_reduction < self.gain_reduction[1] { // If we need to change something we need to consider two different // cases based on the slope of the gain reduction. let current_gain_reduction = self.gain_reduction[0] - (self.env_cnt as f64 / (LIMITER_ATTACK_WINDOW as f64 - 1.0) * (self.gain_reduction[0] - self.gain_reduction[1])); // Calculate the slopes. Note the minus! let old_slope = -(self.gain_reduction[0] - self.gain_reduction[1]); let new_slope = -(current_gain_reduction - gain_reduction); if new_slope <= old_slope { // If the slope from our current position to the new gain reduction at // the new peak is higher (we need to reduce gain faster) then we // restart the attack state at this point with the higher slope. We // will then reach the new peak at the end of the attack window. self.limiter_state = LimiterState::Attack; self.gain_reduction[0] = current_gain_reduction; self.gain_reduction[1] = gain_reduction; self.env_cnt = 0; self.sustain_cnt = None; gst::debug!( CAT, imp: imp, "Found new peak {} at sample {}, restarting attack state at sample {} (gain reduction {}-{})", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, self.gain_reduction[0], self.gain_reduction[1], ); } else { // If the slope is lower we can't simply reduce the slope as we would // then have a lower gain reduction than needed at the previous peak. // Instead of continue with the same slope but continue further than // the old peak until we reach the required gain reduction for the new // peak. Just like above we need to remember to sustain from the end of // the attack window until the new peak. // Calculate at which point we would reach the new gain reduction // relative to 0.0 == attack window start, 1.0 attack window end. let new_end = (gain_reduction - self.gain_reduction[0]) / old_slope; let new_end = f64::max(new_end, 1.0); // New start of the window, this will be in the past let new_start = new_end - 1.0; // Gain reduction at the new start. Note the plus as the slope is // negative already here. // // Clippy warning ignored here because this is just incidentally the same as // AssignAdd: we calculate a new adjusted gain reduction here, and override the // previous one. #[allow(clippy::assign_op_pattern)] { self.gain_reduction[0] = self.gain_reduction[0] + new_start * old_slope; } // At env_cnt == ATTACK_WINDOW we need the new gain reduction self.gain_reduction[1] = gain_reduction; // Calculate the current position in the attack window let cur_pos = (current_gain_reduction - self.gain_reduction[0]) / old_slope; let cur_pos = f64::clamp(cur_pos, 0.0, 1.0); self.env_cnt = ((LIMITER_ATTACK_WINDOW as f64 - 1.0) * cur_pos) as usize; // Need to sustain in any case for this many samples to actually // reach the new peak self.sustain_cnt = Some(self.env_cnt); gst::debug!( CAT, imp: imp, "Found new peak {} at sample {}, adjusting attack state at sample {} (gain reduction {}-{})", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, self.gain_reduction[0], self.gain_reduction[1], ); } return smp_cnt; } else { // We're ending the attack state this much before the new peak so need // to ensure that we at least sustain it for that long afterwards. gst::debug!( CAT, imp: imp, "Found new low peak {} at sample {} in attack state at sample {}", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, ); if self.env_cnt < LIMITER_ATTACK_WINDOW { self.sustain_cnt = Some(self.env_cnt); } } } if self.env_cnt == LIMITER_ATTACK_WINDOW && smp_cnt < nb_samples { // If we reached the target gain reduction, go into sustain state. gst::debug!( CAT, imp: imp, "Going to sustain state at sample {} (gain reduction {})", smp_cnt, self.gain_reduction[1] ); self.limiter_state = LimiterState::Sustain; // Keep sustain_cnt as is from above } smp_cnt } fn true_peak_limiter_sustain( &mut self, imp: &AudioLoudNorm, mut smp_cnt: usize, nb_samples: usize, ) -> usize { let channels = self.info.channels() as usize; // Sustain the previous gain reduction as long as a peak is found in the // next frame, otherwise go over to smoothly release. let peak = self.detect_peak(smp_cnt, nb_samples - smp_cnt); // We might have to sustain for a few more samples regardless of any new peak // we find in 10ms because of code above (first frame or ending the attack // state). // If another peak was found afterwards we can start working with that one: if // it's higher than we go into attack state, if it's lower we sustain for now. if let Some(sustain_cnt) = peak.map(|(d, _v)| d).or(self.sustain_cnt) { // Apply the final gain reduction from the previous attack for the next // samples until we're 1920 samples / 10ms before the peak and then either // need to go into attack state if the peak was higher, or stay in sustain // state and check for the next peak. let mut index = self.limiter_buf_index + smp_cnt * channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } // Sustain the current gain reduction until we're exactly 10ms before // the new peak let mut s = 0; while s < sustain_cnt && smp_cnt < nb_samples { // Safety: Index checked below let samples = unsafe { self.limiter_buf .get_unchecked_mut(index..(index + channels)) }; for sample in samples { *sample *= self.gain_reduction[1]; } index += channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } smp_cnt += 1; s += 1; } if let Some((_, peak_value)) = peak { // If a higher peak than before is found in the next frame need to move // into attack state again to reduce the gain smoothly further. // // Otherwise we stay in sustain mode and smp_cnt is now exactly 10ms before // the new peak, i.e. the next call to detect_peak() would find the *next* // peak. let gain_reduction = self.target_tp / peak_value; if gain_reduction < self.gain_reduction[1] { self.limiter_state = LimiterState::Attack; self.env_cnt = 0; self.sustain_cnt = None; self.gain_reduction[0] = self.gain_reduction[1]; self.gain_reduction[1] = gain_reduction; gst::debug!( CAT, imp: imp, "Found new peak {} at sample {}, going back to attack state at sample {} (gain reduction {}-{})", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, self.gain_reduction[0], self.gain_reduction[1], ); } else { gst::debug!( CAT, imp: imp, "Found new peak {} at sample {}, going sustain further at sample {} (gain reduction {})", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, self.gain_reduction[1], ); // We need to sustain until the peak at least self.sustain_cnt = Some(LIMITER_LOOKAHEAD); } } else if let Some(ref mut sustain_cnt) = self.sustain_cnt { *sustain_cnt -= s; if *sustain_cnt == 0 { self.sustain_cnt = None; } } else { unreachable!(); } } else { // If no new peak is found, release smoothly over the next 100ms. self.limiter_state = LimiterState::Release; self.gain_reduction[0] = self.gain_reduction[1]; self.gain_reduction[1] = 1.; self.env_cnt = 0; gst::debug!( CAT, imp: imp, "Going to release state for sample {} at sample {} (gain reduction {}-1.0)", smp_cnt + LIMITER_RELEASE_WINDOW, smp_cnt, self.gain_reduction[0] ); } smp_cnt } fn true_peak_limiter_release( &mut self, imp: &AudioLoudNorm, mut smp_cnt: usize, nb_samples: usize, ) -> usize { let channels = self.info.channels() as usize; // Smoothly release over the duration of 1 frame (100ms, 19200 samples). let mut index = self.limiter_buf_index + smp_cnt * channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } // There might be a new peak during these 100ms, which we will have to detect // and in that case go into attack state again if the gain reduction is higher // than the current gain reduction we have, or go into sustain mode if it is // equal or lower. We don't stay in release mode if a peak is found. let peak = self.detect_peak(smp_cnt, nb_samples - smp_cnt); if let Some((peak_delta, peak_value)) = peak { let gain_reduction = self.target_tp / peak_value; let current_gain_reduction = self.gain_reduction[0] - (self.env_cnt as f64 / (LIMITER_RELEASE_WINDOW as f64 - 1.0) * (self.gain_reduction[1] - self.gain_reduction[0])); if gain_reduction < current_gain_reduction { assert!(smp_cnt + peak_delta < nb_samples); // Sustain the current gain reduction until we're exactly 10ms before // the new peak for _ in 0..peak_delta { // Safety: Index checked below let samples = unsafe { self.limiter_buf .get_unchecked_mut(index..(index + channels)) }; for sample in samples { *sample *= self.gain_reduction[1]; } index += channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } smp_cnt += 1; assert!(smp_cnt < nb_samples); } self.limiter_state = LimiterState::Attack; self.env_cnt = 0; self.sustain_cnt = None; self.gain_reduction[0] = current_gain_reduction; self.gain_reduction[1] = gain_reduction; gst::debug!( CAT, imp: imp, "Found new peak {} at sample {}, going back to attack state at sample {} (gain reduction {}-{})", peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, smp_cnt, self.gain_reduction[0], self.gain_reduction[1], ); } else { self.gain_reduction[1] = current_gain_reduction; gst::debug!( CAT, imp: imp, "Going from release to sustain state at sample {} because of low peak {} at sample {} (gain reduction {})", smp_cnt, peak_value, smp_cnt + LIMITER_ATTACK_WINDOW, self.gain_reduction[1] ); self.limiter_state = LimiterState::Sustain; } return smp_cnt; } while self.env_cnt < LIMITER_RELEASE_WINDOW && smp_cnt < nb_samples { let env = self.gain_reduction[0] - (self.env_cnt as f64 / (LIMITER_RELEASE_WINDOW as f64 - 1.0) * (self.gain_reduction[1] - self.gain_reduction[0])); // Safety: Index checked below let samples = unsafe { self.limiter_buf .get_unchecked_mut(index..(index + channels)) }; for sample in samples { *sample *= env; } index += channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } smp_cnt += 1; self.env_cnt += 1; } // If we're done with the release, go to out state if smp_cnt < nb_samples { self.limiter_state = LimiterState::Out; gst::debug!( CAT, imp: imp, "Leaving release state and going to out state at sample {}", smp_cnt, ); } smp_cnt } fn true_peak_limiter_first_frame(&mut self, imp: &AudioLoudNorm) { let channels = self.info.channels() as usize; assert_eq!(self.limiter_buf_index, 0); let mut max = 0.; for sample in &self.limiter_buf[0..((LIMITER_LOOKAHEAD + 1) * channels)] { if sample.abs() > max { max = *sample; } } // Initialize the previous sample for peak detection with the last sample we looked at // above for (o, i) in self .prev_smp .iter_mut() .zip(self.limiter_buf[(LIMITER_LOOKAHEAD * channels)..].iter()) { *o = i.abs(); } if max > self.target_tp { // Pretend the first peak was at the last sample so that the sustain code can work // as with normal peaks self.limiter_state = LimiterState::Sustain; self.sustain_cnt = Some(LIMITER_LOOKAHEAD); self.gain_reduction[1] = self.target_tp / max; gst::debug!( CAT, imp: imp, "Reducing gain for start of first frame by {} ({} > {}) and going to sustain state", self.gain_reduction[1], max, self.target_tp ); // The sustain code below will already handle the gain reduction and checking for // further peaks. } } fn true_peak_limiter(&mut self, imp: &AudioLoudNorm, dst: &mut [f64]) { let channels = self.info.channels() as usize; let nb_samples = dst.len() / channels; gst::debug!(CAT, imp: imp, "Running limiter for {} samples", nb_samples); // For the first frame we can't adjust the gain before it smoothly anymore so instead // apply the gain reduction immediately if we get above the threshold and move to sustain // state directly. if self.frame_type == FrameType::First { self.true_peak_limiter_first_frame(imp); } let mut smp_cnt = 0; while smp_cnt < nb_samples { match self.limiter_state { LimiterState::Out => { smp_cnt = self.true_peak_limiter_out(imp, smp_cnt, nb_samples); } LimiterState::Attack => { smp_cnt = self.true_peak_limiter_attack(imp, smp_cnt, nb_samples); } LimiterState::Sustain => { smp_cnt = self.true_peak_limiter_sustain(imp, smp_cnt, nb_samples); } LimiterState::Release => { smp_cnt = self.true_peak_limiter_release(imp, smp_cnt, nb_samples); } } } // Copy over the samples into the output buffer, after going through the limiter above. let mut index = self.limiter_buf_index; for dest_samples in dst.chunks_exact_mut(channels) { // Safety: Index checked below let in_samples = unsafe { self.limiter_buf .get_unchecked_mut(index..(index + channels)) }; for (o, i) in dest_samples.iter_mut().zip(in_samples.iter()) { *o = *i; // Clamp to the maximum for rounding errors above if o.abs() > self.target_tp { *o = self.target_tp * o.signum(); } } index += channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } } } // Checks if there is a peak above the threshold 10ms or 1920 samples after the current // sample. Returns the peak delta and its value. The peak delta is relative to // offset + LIMITER_LOOKAHEAD (10ms), i.e. a peak delta of 0 would be 10ms after the offset. // // peak delta 0 is never returned, i.e. it is safe to call this 10ms before a peak and it would // then return the next peak. fn detect_peak(&mut self, offset: usize, samples: usize) -> Option<(usize, f64)> { let channels = self.info.channels() as usize; // Check for a peak 1920 samples / 10ms in the future let mut index = self.limiter_buf_index + (offset + LIMITER_LOOKAHEAD) * channels; if index >= self.limiter_buf.len() { index -= self.limiter_buf.len(); } for n in 0..samples { let mut next_index = index + channels; if next_index >= self.limiter_buf.len() { next_index -= self.limiter_buf.len(); } // Get the current sample for each channel and the next here // Safety: Index checked above let (this, next) = unsafe { ( self.limiter_buf.get_unchecked(index..(index + channels)), self.limiter_buf .get_unchecked(next_index..(next_index + channels)), ) }; let mut detected = false; // Iterate over the previous sample for each channel, the current and the next, i.e. // in each iteration we're looking at channel c for those 3 samples. for (c, (prev_smp, (this, next))) in self .prev_smp .iter_mut() .zip(this.iter().zip(next.iter())) .enumerate() { let this = this.abs(); let next = next.abs(); detected = false; // Check if the current sample is the highest peak if (*prev_smp <= this) && (this >= next) && (this > self.target_tp) && (n > 0) { detected = true; // Check the 12 following samples, if one of them is higher then that would be // the peak. for i in 2..12 { // Safety: Index checked right here let next = unsafe { let mut next_index = index + c + i * channels; if next_index >= self.limiter_buf.len() { next_index -= self.limiter_buf.len(); } self.limiter_buf.get_unchecked(next_index).abs() }; if next > this { detected = false; break; } } if detected { break; } } // Remember as previous sample. *prev_smp = this; } // If this was the highest peak then remember it as the previous sample (as we didn't // just above here because of the break!) and return the peak index and value. if detected { let mut max_peak = 0.0; for (c, (prev_smp, this)) in (self.prev_smp.iter_mut().zip(this.iter())).enumerate() { if c == 0 || this.abs() > max_peak { max_peak = this.abs(); } *prev_smp = this.abs(); } return Some((n, max_peak)); } index = next_index; } None } fn gaussian_filter(&self, index: usize) -> f64 { let mut result = 0.; let index = if index > 10 { index - 10 } else { index + 20 }; // Apply gaussian filter to the gain adjustments for smoothening them. let delta = self.delta[index..].iter().chain(self.delta.iter()); for (weight, delta) in self.weights.iter().zip(delta) { result += delta * weight; } result } } impl AudioLoudNorm { fn sink_chain( &self, _pad: &gst::Pad, buffer: gst::Buffer, ) -> Result { gst::log!(CAT, imp: self, "Handling buffer {:?}", buffer); let mut state_guard = self.state.borrow_mut(); let state = match *state_guard { None => { gst::error!(CAT, imp: self, "Not negotiated yet"); return Err(gst::FlowError::NotNegotiated); } Some(ref mut state) => state, }; let mut outbufs = vec![]; if buffer.flags().contains(gst::BufferFlags::DISCONT) { gst::debug!(CAT, imp: self, "Draining on discontinuity"); match state.drain(self) { Ok(outbuf) => { outbufs.push(outbuf); } Err(gst::FlowError::Eos) => (), Err(err) => return Err(err), } // Need to reset the state now *state = State::new(&self.settings.lock().unwrap(), state.info.clone()); } state.adapter.push(buffer); outbufs.append(&mut state.drain_full_frames(self)?); drop(state_guard); for buffer in outbufs { gst::log!(CAT, imp: self, "Outputting buffer {:?}", buffer); self.srcpad.push(buffer)?; } Ok(gst::FlowSuccess::Ok) } fn sink_event(&self, pad: &gst::Pad, event: gst::Event) -> bool { use gst::EventView; gst::log!(CAT, obj: pad, "Handling event {:?}", event); match event.view() { EventView::Caps(c) => { let caps = c.caps(); gst::info!(CAT, obj: pad, "Got caps {:?}", caps); let info = match gst_audio::AudioInfo::from_caps(caps) { Ok(info) => info, Err(_) => { gst::error!(CAT, obj: pad, "Failed to parse caps"); return false; } }; let mut state = self.state.borrow_mut(); let mut outbuf = None; if let Some(ref mut state) = &mut *state { outbuf = match state.drain(self) { Ok(outbuf) => Some(outbuf), Err(gst::FlowError::Eos) => None, Err(_) => return false, }; } *state = Some(State::new(&self.settings.lock().unwrap(), info)); drop(state); if let Some(outbuf) = outbuf { gst::log!(CAT, imp: self, "Outputting buffer {:?}", outbuf); if let Err(err) = self.srcpad.push(outbuf) { gst::error!(CAT, imp: self, "Failed to push drained data: {}", err); return false; } } } EventView::Eos(_) | EventView::Segment(_) => { let mut state = self.state.borrow_mut(); let mut outbuf = None; if let Some(ref mut state) = &mut *state { outbuf = match state.drain(self) { Ok(outbuf) => Some(outbuf), Err(gst::FlowError::Eos) => None, Err(_) => return false, }; *state = State::new(&self.settings.lock().unwrap(), state.info.clone()); } drop(state); if let Some(outbuf) = outbuf { gst::log!(CAT, imp: self, "Outputting buffer {:?}", outbuf); if let Err(err) = self.srcpad.push(outbuf) { gst::error!( CAT, imp: self, "Failed to push drained data on EOS: {}", err ); return false; } } } EventView::FlushStop(_) => { // Resetting our whole state let mut state = self.state.borrow_mut(); if let Some(info) = state.as_ref().map(|s| s.info.clone()) { let settings = *self.settings.lock().unwrap(); *state = Some(State::new(&settings, info)); } else { *state = None; } } _ => (), } gst::Pad::event_default(pad, Some(&*self.obj()), event) } #[allow(clippy::single_match)] fn src_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool { use gst::QueryViewMut; gst::log!(CAT, obj: pad, "Handling query {:?}", query); match query.view_mut() { QueryViewMut::Latency(q) => { let mut peer_query = gst::query::Latency::new(); if self.sinkpad.peer_query(&mut peer_query) { let (live, min_latency, max_latency) = peer_query.result(); q.set( live, min_latency + 3.seconds(), max_latency.opt_add(3.seconds()), ); true } else { false } } _ => gst::Pad::query_default(pad, Some(&*self.obj()), query), } } } #[glib::object_subclass] impl ObjectSubclass for AudioLoudNorm { const NAME: &'static str = "GstAudioLoudNorm"; type Type = super::AudioLoudNorm; type ParentType = gst::Element; fn with_class(klass: &Self::Class) -> Self { let templ = klass.pad_template("sink").unwrap(); let sinkpad = gst::Pad::builder_with_template(&templ, Some("sink")) .chain_function(|pad, parent, buffer| { Self::catch_panic_pad_function( parent, || Err(gst::FlowError::Error), |this| this.sink_chain(pad, buffer), ) }) .event_function(|pad, parent, event| { Self::catch_panic_pad_function(parent, || false, |this| this.sink_event(pad, event)) }) .flags(gst::PadFlags::PROXY_CAPS) .build(); let templ = klass.pad_template("src").unwrap(); let srcpad = gst::Pad::builder_with_template(&templ, Some("src")) .query_function(|pad, parent, query| { Self::catch_panic_pad_function(parent, || false, |this| this.src_query(pad, query)) }) .flags(gst::PadFlags::PROXY_CAPS) .build(); Self { sinkpad, srcpad, settings: Mutex::new(Default::default()), state: AtomicRefCell::new(None), } } } impl ObjectImpl for AudioLoudNorm { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecDouble::builder("loudness-target") .nick("Loudness Target") .blurb("Loudness target in LUFS") .minimum(-70.0) .maximum(-5.0) .default_value(DEFAULT_LOUDNESS_TARGET) .mutable_ready() .build(), glib::ParamSpecDouble::builder("loudness-range-target") .nick("Loudness Range Target") .blurb("Loudness range target in LU") .minimum(1.0) .maximum(20.0) .default_value(DEFAULT_LOUDNESS_RANGE_TARGET) .mutable_ready() .build(), glib::ParamSpecDouble::builder("max-true-peak") .nick("Maximum True Peak") .blurb("Maximum True Peak in dbTP") .minimum(-9.0) .maximum(0.0) .default_value(DEFAULT_MAX_TRUE_PEAK) .mutable_ready() .build(), glib::ParamSpecDouble::builder("offset") .nick("Offset Gain") .blurb("Offset Gain in LU") .minimum(-99.0) .maximum(99.0) .default_value(DEFAULT_OFFSET) .mutable_ready() .build(), ] }); PROPERTIES.as_ref() } fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); obj.add_pad(&self.sinkpad).unwrap(); obj.add_pad(&self.srcpad).unwrap(); } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { "loudness-target" => { let mut settings = self.settings.lock().unwrap(); settings.loudness_target = value.get().expect("type checked upstream"); } "loudness-range-target" => { let mut settings = self.settings.lock().unwrap(); settings.loudness_range_target = value.get().expect("type checked upstream"); } "max-true-peak" => { let mut settings = self.settings.lock().unwrap(); settings.max_true_peak = value.get().expect("type checked upstream"); } "offset" => { let mut settings = self.settings.lock().unwrap(); settings.offset = value.get().expect("type checked upstream"); } _ => unimplemented!(), } } fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "loudness-target" => { let settings = self.settings.lock().unwrap(); settings.loudness_target.to_value() } "loudness-range-target" => { let settings = self.settings.lock().unwrap(); settings.loudness_range_target.to_value() } "max-true-peak" => { let settings = self.settings.lock().unwrap(); settings.max_true_peak.to_value() } "offset" => { let settings = self.settings.lock().unwrap(); settings.offset.to_value() } _ => unimplemented!(), } } } impl GstObjectImpl for AudioLoudNorm {} impl ElementImpl for AudioLoudNorm { fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { static ELEMENT_METADATA: Lazy = Lazy::new(|| { gst::subclass::ElementMetadata::new( "Audio loudness normalizer", "Filter/Effect/Audio", "Normalizes perceived loudness of an audio stream", "Sebastian Dröge ", ) }); Some(&*ELEMENT_METADATA) } fn pad_templates() -> &'static [gst::PadTemplate] { static PAD_TEMPLATES: Lazy> = Lazy::new(|| { let caps = gst_audio::AudioCapsBuilder::new_interleaved() .format(gst_audio::AUDIO_FORMAT_F64) .rate(192_000) .build(); let src_pad_template = gst::PadTemplate::new( "src", gst::PadDirection::Src, gst::PadPresence::Always, &caps, ) .unwrap(); let sink_pad_template = gst::PadTemplate::new( "sink", gst::PadDirection::Sink, gst::PadPresence::Always, &caps, ) .unwrap(); vec![src_pad_template, sink_pad_template] }); PAD_TEMPLATES.as_ref() } #[allow(clippy::single_match)] fn change_state( &self, transition: gst::StateChange, ) -> Result { let res = self.parent_change_state(transition); match transition { gst::StateChange::PausedToReady => { // Drop state *self.state.borrow_mut() = None; } _ => (), } res } } fn init_gaussian_filter() -> [f64; 21] { let mut weights = [0.0f64; 21]; let mut total_weight = 0.0f64; let sigma = 3.5f64; let offset = 21 / 2; let c1 = 1.0 / (sigma * f64::sqrt(2.0 * std::f64::consts::PI)); let c2 = 2.0 * f64::powf(sigma, 2.0); for (i, weight) in weights.iter_mut().enumerate() { let x = i as f64 - offset as f64; *weight = c1 * f64::exp(-(f64::powf(x, 2.0) / c2)); total_weight += *weight; } let adjust = 1.0 / total_weight; for weight in weights.iter_mut() { *weight *= adjust; } weights }