Commit 666ec7d5 authored by Sebastian Dröge's avatar Sebastian Dröge 🍵
Browse files

audiofx: Add audioloudnorm filter based on the ffmpeg af_loudnorm filter

This normalizes the loudness of an audio stream to a target loudness
with a given maximum peak based on EBU R128.

Conceptually it keeps a 3s lookahead for calculating the perceived
loudness and based on that calculates the gain required to reach the
target loudness. The calculated gains then go through a gaussian filter
for smoothening and are then applied to the audio in 100ms blocks. Each
of the 100ms blocks is then passed to a limiter filter to prevent going
above the maximum peak.

See http://k.ylo.ph/2016/04/04/loudnorm.html for some more details about
the algorithm.

It introduces 3s of latency and currently only works on 192kHz audio.
Using it with a different sample rate requires resampling before and
afterwards. The upsampling is required to calculate the true peak.

Other than the ffmpeg filter it currently does not support two-pass
processing but only one-pass/live processing.

Compared to the ffmpeg filter this code was refactored considerably and
the limiter implementation was fixed to actually work, as well as
various other bugs in different places that were fixed.
parent 4bfd1f76
Pipeline #127507 passed with stages
in 14 minutes and 59 seconds
......@@ -5,6 +5,7 @@ authors = ["Sebastian Dröge <sebastian@centricular.com>"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MIT/Apache-2.0"
description = "Rust AudioFx Plugin"
edition = "2018"
[dependencies]
glib = { git = "https://github.com/gtk-rs/glib" }
......@@ -14,11 +15,16 @@ gstreamer-audio = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs
byte-slice-cast = "0.3"
num-traits = "0.2"
lazy_static = "1.0"
ebur128 = "0.1"
[lib]
name = "gstrsaudiofx"
crate-type = ["cdylib"]
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[dev-dependencies]
gstreamer-check = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gstreamer-app = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
[build-dependencies]
gst-plugin-version-helper = { path="../gst-plugin-version-helper" }
This diff is collapsed.
......@@ -21,9 +21,11 @@ extern crate num_traits;
extern crate lazy_static;
mod audioecho;
mod audioloudnorm;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
audioecho::register(plugin)
audioecho::register(plugin)?;
audioloudnorm::register(plugin)
}
gst_plugin_define!(
......
// Copyright (C) 2020 Sebastian Dröge <sebastian@centricular.com>
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library 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
// Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the
// Free Software Foundation, Inc., 51 Franklin Street, Suite 500,
// Boston, MA 02110-1335, USA.
use gstrsaudiofx;
use glib;
extern crate gstreamer as gst;
extern crate gstreamer_app as gst_app;
extern crate gstreamer_audio as gst_audio;
extern crate gstreamer_check as gst_check;
use glib::prelude::*;
use gst::prelude::*;
use byte_slice_cast::*;
use std::sync::{Arc, Mutex};
fn init() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
gst::init().unwrap();
gstrsaudiofx::plugin_register_static().expect("Failed to register rsaudiofx plugin");
});
}
fn run_test(wave: &str, num_buffers: u32, samples_per_buffer: u32, channels: u32) {
init();
let pipeline = gst::parse_launch(&format!(
"audiotestsrc wave={} num-buffers={} samplesperbuffer={} ! rsaudioloudnorm ! appsink name=sink",
wave, num_buffers, samples_per_buffer
))
.unwrap()
.downcast::<gst::Pipeline>()
.unwrap();
let sink = pipeline
.get_by_name("sink")
.unwrap()
.downcast::<gst_app::AppSink>()
.unwrap();
sink.set_property("sync", &false).unwrap();
let caps = gst_audio::AudioInfo::new(gst_audio::AUDIO_FORMAT_F64, 192_000, channels)
.build()
.unwrap()
.to_caps()
.unwrap();
sink.set_caps(Some(&caps));
let samples = Arc::new(Mutex::new(Vec::new()));
let samples_clone = samples.clone();
sink.set_callbacks(
gst_app::AppSinkCallbacks::new()
.new_sample(move |sink| {
let sample = sink.pull_sample().unwrap();
let mut samples = samples_clone.lock().unwrap();
samples.push(sample);
Ok(gst::FlowSuccess::Ok)
})
.build(),
);
pipeline.set_state(gst::State::Playing).unwrap();
let mut eos = false;
let bus = pipeline.get_bus().unwrap();
while let Some(msg) = bus.timed_pop(gst::CLOCK_TIME_NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => {
eos = true;
break;
}
MessageView::Error(..) => unreachable!(),
_ => (),
}
}
pipeline.set_state(gst::State::Null).unwrap();
assert!(eos);
let samples = samples.lock().unwrap();
let mut r128 = ebur128::EbuR128::new(
channels,
192_000,
ebur128::Mode::I | ebur128::Mode::SAMPLE_PEAK,
)
.unwrap();
let mut num_samples = 0;
let mut expected_ts = gst::ClockTime::from(0);
for sample in samples.iter() {
let buf = sample.get_buffer().unwrap();
let ts = buf.get_pts();
if ts > expected_ts {
assert!(
ts - expected_ts <= gst::ClockTime::from(1),
"TS is {} instead of {}",
ts,
expected_ts
);
} else if ts < expected_ts {
assert!(
expected_ts - ts <= gst::ClockTime::from(1),
"TS is {} instead of {}",
ts,
expected_ts
);
}
let map = buf.map_readable().unwrap();
let data = map.as_slice_of::<f64>().unwrap();
num_samples += data.len() / channels as usize;
r128.add_frames_f64(data).unwrap();
expected_ts +=
gst::ClockTime::from((data.len() as u64 / channels as u64) * gst::SECOND_VAL / 192_000);
}
assert_eq!(
num_samples,
num_buffers as usize * samples_per_buffer as usize
);
let loudness = r128.loudness_global().unwrap();
assert!(
f64::abs(loudness + 24.0) < 1.0,
"Loudness is {} instead of -24.0",
loudness
);
for c in 0..channels {
let peak = 20.0 * f64::log10(r128.sample_peak(c).unwrap());
assert!(peak < -2.0, "Peak {} for channel {} is above -2.0", c, peak,);
}
}
#[test]
fn basic() {
run_test("sine", 1000, 1920, 1);
}
#[test]
fn basic_white_noise() {
run_test("white-noise", 1000, 1920, 1);
}
#[test]
fn remaining_at_eos() {
run_test("sine", 1000, 1024, 1);
}
#[test]
fn short_input() {
run_test("sine", 100, 1024, 1);
}
#[test]
fn basic_two_channels() {
run_test("sine", 1000, 1920, 2);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment