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 Error

Raised 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 command String

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_path String

Source WebM recording.

parameter output_path String

Destination WebM recording.

parameter integrated_loudness Numeric

Target integrated loudness in LUFS.

parameter true_peak Numeric

Maximum true peak in dBTP.

parameter loudness_range Numeric

Target loudness range in LU.

parameter bitrate String

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