class Normalizer
Normalizes recording loudness using measured gain and peak limiting.
Short spoken recordings can have isolated peaks which prevent FFmpeg's
two-pass loudnorm filter from reaching its integrated loudness target.
This implementation measures the result of a gain and limiter filter,
adjusting the gain until the complete recording reaches the target. The
limiter retains headroom for peaks introduced by Opus encoding.
Nested Classes and Modules
class ErrorRaised when FFmpeg cannot analyze or normalize a recording.
Definitions
INTEGRATED_LOUDNESS = -16.0
The default integrated loudness target for spoken web content.
TRUE_PEAK = -1.5
The default maximum true peak, in dBTP.
LOUDNESS_RANGE = 11.0
The default loudness range target.
BITRATE = "128k"
The default Opus bitrate used for normalized recordings.
ENCODER_HEADROOM = 0.75
Additional headroom retained before lossy encoding, in dB.
LOUDNESS_TOLERANCE = 0.05
Maximum acceptable difference from the integrated loudness target.
MAXIMUM_ITERATIONS = 12
Maximum gain refinements performed before encoding.
MAXIMUM_ENCODINGS = 4
Maximum encodings performed while accounting for codec peak overshoot.
PEAK_CORRECTION_MARGIN = 0.1
Additional headroom added when an encoded file exceeds the peak target.
def initialize(command: "ffmpeg")
Initialize the normalizer.
Signature
-
parameter
commandString The FFmpeg executable.
Implementation
def initialize(command: "ffmpeg")
@command = command
end
def normalize(input_path, output_path, integrated_loudness: INTEGRATED_LOUDNESS, true_peak: TRUE_PEAK, loudness_range: LOUDNESS_RANGE, bitrate: BITRATE)
Normalize one recording to a separate output path.
Signature
-
parameter
input_pathString Source WebM recording.
-
parameter
output_pathString Destination WebM recording.
-
parameter
integrated_loudnessNumeric Target integrated loudness in LUFS.
-
parameter
true_peakNumeric Maximum true peak in dBTP.
-
parameter
loudness_rangeNumeric Target loudness range in LU.
-
parameter
bitrateString Output Opus bitrate.
-
returns
Hash Input and normalized loudness measurements.
Implementation
def normalize(input_path, output_path, integrated_loudness: INTEGRATED_LOUDNESS, true_peak: TRUE_PEAK, loudness_range: LOUDNESS_RANGE, bitrate: BITRATE)
input_path = File.expand_path(input_path)
output_path = File.expand_path(output_path)
input_measurements = measure(input_path, integrated_loudness, true_peak, loudness_range)
limiter_peak = true_peak - ENCODER_HEADROOM
FileUtils.mkdir_p(File.dirname(output_path))
Tempfile.create(["presently-normalized", ".webm"], File.dirname(output_path), binmode: true) do |temporary|
temporary.close
MAXIMUM_ENCODINGS.times do
filter, gain = normalization_filter(input_path, integrated_loudness, limiter_peak, loudness_range, input_measurements)
run(
"-i", input_path,
"-map", "0:a:0",
"-af", filter,
"-c:a", "libopus",
"-b:a", bitrate,
"-vbr", "on",
"-vn",
"-f", "webm",
"-y", temporary.path,
)
output_measurements = measure(temporary.path, integrated_loudness, true_peak, loudness_range)
output_peak = Float(output_measurements.fetch("input_tp"))
if output_peak <= true_peak
File.rename(temporary.path, output_path)
return {
input: input_measurements,
output: output_measurements,
gain: gain,
}
end
limiter_peak -= output_peak - true_peak + PEAK_CORRECTION_MARGIN
end
raise Error, "Could not constrain the encoded true peak to #{true_peak} dBTP for #{input_path}!"
end
end