ebur128_stream 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -0
- data/LICENSE-APACHE +201 -0
- data/LICENSE-MIT +21 -0
- data/README.md +256 -0
- data/Rakefile +31 -0
- data/ebur128_stream.gemspec +50 -0
- data/ext/ebur128_stream/Cargo.lock +359 -0
- data/ext/ebur128_stream/Cargo.toml +21 -0
- data/ext/ebur128_stream/build.rs +5 -0
- data/ext/ebur128_stream/src/analyzer.rs +159 -0
- data/ext/ebur128_stream/src/error.rs +57 -0
- data/ext/ebur128_stream/src/lib.rs +123 -0
- data/ext/ebur128_stream/src/normalize.rs +121 -0
- data/ext/ebur128_stream/src/report.rs +52 -0
- data/ext/ebur128_stream/src/samples.rs +209 -0
- data/ext/ebur128_stream/src/snapshot.rs +49 -0
- data/lib/ebur128_stream/version.rb +5 -0
- data/lib/ebur128_stream.rb +68 -0
- data/sample/analyze-microphone.rb +110 -0
- data/sample/analyze-planar-data.rb +38 -0
- data/sample/analyze-wavefile.rb +77 -0
- data/sample/normalize.rb +35 -0
- data/sig/ebur128_stream.rbs +69 -0
- data/test/helper.rb +18 -0
- data/test/test_analizer.rb +114 -0
- data/test/test_ebur128_stream.rb +13 -0
- data/test/test_normalize_report.rb +35 -0
- data/test/test_normalizer.rb +49 -0
- data/test/test_report.rb +34 -0
- data/test/test_snapshot.rb +36 -0
- metadata +177 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require "ebur128_stream"
|
|
2
|
+
require "gstreamer"
|
|
3
|
+
require "numo/narray/alt"
|
|
4
|
+
require "ndav/numo/narray"
|
|
5
|
+
require "io/console"
|
|
6
|
+
|
|
7
|
+
CHANNELS = [:left, :right]
|
|
8
|
+
RATE = 48_000
|
|
9
|
+
HEIGHT = $stdout.winsize[0]
|
|
10
|
+
WIDTH = $stdout.winsize[1]
|
|
11
|
+
PADDING_BLOCK_START = HEIGHT / 2 - 2
|
|
12
|
+
PADDING_INLINE = WIDTH / 5
|
|
13
|
+
AREA_WIDTH = WIDTH - PADDING_INLINE * 2
|
|
14
|
+
LIMIT = -70
|
|
15
|
+
|
|
16
|
+
include NDAV::Converter
|
|
17
|
+
|
|
18
|
+
def main(argv)
|
|
19
|
+
analyser = setup_ebur128_stream
|
|
20
|
+
|
|
21
|
+
print "\e[2J"
|
|
22
|
+
|
|
23
|
+
setup_gstreamer do |sample|
|
|
24
|
+
# GStreamer's Gst::Sample is 2-D but EBUR128Stream requires 1-D.
|
|
25
|
+
# Reshapes it using Numo::NArray
|
|
26
|
+
samples = NumoNArray(sample)
|
|
27
|
+
samples.reshape!(*samples.shape.reduce(:*))
|
|
28
|
+
|
|
29
|
+
analyser.push_interleaved samples
|
|
30
|
+
analyser.snapshot => {momentary_lufs:}
|
|
31
|
+
next unless momentary_lufs
|
|
32
|
+
|
|
33
|
+
render_loudness momentary_lufs
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def setup_ebur128_stream
|
|
38
|
+
EBUR128Stream::Analyzer.new(channels: CHANNELS, sample_rate: RATE, modes: [:momentary])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def setup_gstreamer
|
|
42
|
+
pipeline = Gst::Pipeline.new("ebur128-stream")
|
|
43
|
+
src = Gst::ElementFactory.make("autoaudiosrc", nil)
|
|
44
|
+
convert = Gst::ElementFactory.make("audioconvert", nil)
|
|
45
|
+
resample = Gst::ElementFactory.make("audioresample", nil)
|
|
46
|
+
sink = Gst::ElementFactory.make("appsink", nil)
|
|
47
|
+
|
|
48
|
+
caps = Gst::Caps.new("audio/x-raw")
|
|
49
|
+
caps["format"] = "F32LE" # F32 doesn't work for macOS
|
|
50
|
+
caps["rate", :int] = RATE
|
|
51
|
+
caps["channels", :int] = CHANNELS.length
|
|
52
|
+
caps["layout"] = "interleaved"
|
|
53
|
+
sink.caps = caps
|
|
54
|
+
|
|
55
|
+
sink.emit_signals = true
|
|
56
|
+
sink.signal_connect :new_sample do |_|
|
|
57
|
+
begin
|
|
58
|
+
yield sink.pull_sample
|
|
59
|
+
rescue => err
|
|
60
|
+
$stderr.puts err
|
|
61
|
+
end
|
|
62
|
+
Gst::FlowReturn::OK
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
pipeline << src << convert << resample << sink
|
|
66
|
+
src >> convert >> resample >> sink
|
|
67
|
+
|
|
68
|
+
loop = GLib::MainLoop.new
|
|
69
|
+
|
|
70
|
+
bus = pipeline.bus
|
|
71
|
+
bus.add_watch do |bus, message|
|
|
72
|
+
case message.type
|
|
73
|
+
when Gst::MessageType::EOS
|
|
74
|
+
loop.quit
|
|
75
|
+
when Gst::MessageType::ERROR
|
|
76
|
+
error, debug = message.parse_error
|
|
77
|
+
$stderr.puts error
|
|
78
|
+
$stderr.puts debug
|
|
79
|
+
loop.quit
|
|
80
|
+
end
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
pipeline.play
|
|
85
|
+
begin
|
|
86
|
+
loop.run
|
|
87
|
+
rescue Interrupt
|
|
88
|
+
pp :Interrupt
|
|
89
|
+
rescue err
|
|
90
|
+
$stderr.puts err
|
|
91
|
+
ensure
|
|
92
|
+
pipeline.stop
|
|
93
|
+
GC.start
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render_loudness(loudness)
|
|
98
|
+
len = (-LIMIT + loudness) * (-AREA_WIDTH / LIMIT)
|
|
99
|
+
volume = "|" * len
|
|
100
|
+
ws = " " * (AREA_WIDTH - len)
|
|
101
|
+
print "\e[#{PADDING_BLOCK_START};#{PADDING_INLINE}H"
|
|
102
|
+
print "\e[#{PADDING_INLINE}G"
|
|
103
|
+
puts "#{volume}#{ws}"
|
|
104
|
+
puts
|
|
105
|
+
digits = "%.3f" % loudness
|
|
106
|
+
print "\e[#{WIDTH - PADDING_INLINE - digits.to_s.length}G"
|
|
107
|
+
print digits
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
main ARGV
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "ebur128_stream"
|
|
2
|
+
require "torchaudio"
|
|
3
|
+
require "ndav/torch/tensor"
|
|
4
|
+
|
|
5
|
+
def main(argv)
|
|
6
|
+
waveform, sample_rate = TorchAudio.load(argv.shift)
|
|
7
|
+
|
|
8
|
+
# TorchAudio.load returns planar waveform:
|
|
9
|
+
# [[L1, L2, L3, ...], [R1, R2, R3, ...]]
|
|
10
|
+
# which is suitable for push_panar
|
|
11
|
+
analyzer = EBUR128Stream::Analyzer.new(sample_rate:, channels: [:left, :right])
|
|
12
|
+
analyzer.push_planar waveform
|
|
13
|
+
report = analyzer.finalize
|
|
14
|
+
|
|
15
|
+
puts format_report(report)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format_report(report)
|
|
19
|
+
template = <<~EOS
|
|
20
|
+
=== Report ===
|
|
21
|
+
duration: %<dur>.2f seconds
|
|
22
|
+
integrated: %<int>.2f LUFS
|
|
23
|
+
LRA: %<lra>.2f LU
|
|
24
|
+
true peak: %<tp>.2f dBTP
|
|
25
|
+
momentary max: %<mom>.2f LUFS
|
|
26
|
+
short term max: %<st>.2f LUFS
|
|
27
|
+
EOS
|
|
28
|
+
template % {
|
|
29
|
+
dur: report.programme_duration_seconds,
|
|
30
|
+
int: report.integrated_lufs,
|
|
31
|
+
lra: report.loudness_range_lu,
|
|
32
|
+
tp: report.true_peak_dbtp,
|
|
33
|
+
mom: report.momentary_max_lufs,
|
|
34
|
+
st: report.short_term_max_lufs,
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
main ARGV
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require "ebur128_stream"
|
|
2
|
+
require "wavefile"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
SAMPLE_RATE = 48_000
|
|
7
|
+
FORMAT = WaveFile::Format.new(:stereo, :float, SAMPLE_RATE)
|
|
8
|
+
|
|
9
|
+
def main(argv)
|
|
10
|
+
audio_path = argv.shift || make_fixture_audio
|
|
11
|
+
|
|
12
|
+
analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], sample_rate: SAMPLE_RATE)
|
|
13
|
+
WaveFile::Reader.new(audio_path).each_buffer do |buffer|
|
|
14
|
+
# WaveFile::Buffer#samples returns 2-D array:
|
|
15
|
+
# [[L1, R2], [L2, R2], [L3, R3], ...]
|
|
16
|
+
# EBUR128Stream::Analyzer#push_interleaved requires flat array:
|
|
17
|
+
# [L1, R1, L2, R2, L3, R3, ...]
|
|
18
|
+
analyzer.push_interleaved buffer.samples.flatten
|
|
19
|
+
snapshot = analyzer.snapshot
|
|
20
|
+
|
|
21
|
+
puts format_snapshot(snapshot)
|
|
22
|
+
end
|
|
23
|
+
report = analyzer.finalize
|
|
24
|
+
|
|
25
|
+
puts
|
|
26
|
+
puts format_report(report)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format_snapshot(snapshot)
|
|
30
|
+
template = "[%<dur>f] momentary: %{mom}, short term: %{st}, integrated: %{int}, true peak: %{tp}"
|
|
31
|
+
template % {
|
|
32
|
+
dur: snapshot.programme_duration_seconds,
|
|
33
|
+
mom: (snapshot.momentary_lufs&.to_s || "N/A")[..3],
|
|
34
|
+
st: (snapshot.short_term_lufs&.to_s || "N/A")[..3],
|
|
35
|
+
int: (snapshot.integrated_lufs&.to_s || "N/A")[..3],
|
|
36
|
+
tp: (snapshot.true_peak_dbtp&.to_s || "N/A")[..3],
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def format_report(report)
|
|
41
|
+
template = <<~EOS
|
|
42
|
+
=== Report ===
|
|
43
|
+
duration: %<dur>.2f seconds
|
|
44
|
+
integrated: %<int>.2f LUFS
|
|
45
|
+
LRA: %<lra>.2f LU
|
|
46
|
+
true peak: %<tp>.2f dBTP
|
|
47
|
+
momentary max: %<mom>.2f LUFS
|
|
48
|
+
short term max: %<st>.2f LUFS
|
|
49
|
+
EOS
|
|
50
|
+
template % {
|
|
51
|
+
dur: report.programme_duration_seconds,
|
|
52
|
+
int: report.integrated_lufs,
|
|
53
|
+
lra: report.loudness_range_lu,
|
|
54
|
+
tp: report.true_peak_dbtp,
|
|
55
|
+
mom: report.momentary_max_lufs,
|
|
56
|
+
st: report.short_term_max_lufs,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def make_fixture_audio
|
|
61
|
+
file = Tempfile.new(["", ".wav"])
|
|
62
|
+
|
|
63
|
+
freq = 440
|
|
64
|
+
length = 5 # seconds
|
|
65
|
+
WaveFile::Writer.new file.to_path, FORMAT do |writer|
|
|
66
|
+
(SAMPLE_RATE * length).times do |n|
|
|
67
|
+
phase = 2 * Math::PI * n / (SAMPLE_RATE / freq)
|
|
68
|
+
value = Math.sin(phase)
|
|
69
|
+
buffer = WaveFile::Buffer.new([[value, value]], FORMAT) # left, right
|
|
70
|
+
writer.write buffer
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
file.to_path
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
main ARGV
|
data/sample/normalize.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "ebur128_stream"
|
|
2
|
+
require "torchaudio"
|
|
3
|
+
require "ndav/torch/tensor"
|
|
4
|
+
|
|
5
|
+
def main(argv)
|
|
6
|
+
input = argv.shift
|
|
7
|
+
output = argv.shift
|
|
8
|
+
unless output
|
|
9
|
+
abort "Usage: ruby #{$PROGRAM_NAME} INPUT OUTPUT"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
waveform, sample_rate = TorchAudio.load(input)
|
|
13
|
+
|
|
14
|
+
# TorchAudio returns a planar samples
|
|
15
|
+
# but, EBUR128Stream requires an interleaved samples for normalization.
|
|
16
|
+
# We need to reshape the waveform.
|
|
17
|
+
samples = waveform.transpose(1, 0)
|
|
18
|
+
shape = samples.shape
|
|
19
|
+
|
|
20
|
+
# Currently 2-D array. Reshapes to 1-D.
|
|
21
|
+
samples = samples.reshape(shape.reduce(&:*))
|
|
22
|
+
|
|
23
|
+
normalizer = EBUR128Stream::Normalizer.new(
|
|
24
|
+
sample_rate:,
|
|
25
|
+
channels: [:left, :right],
|
|
26
|
+
target_lufs: -14.0
|
|
27
|
+
)
|
|
28
|
+
normalize_report = normalizer.normalize_in_place(samples)
|
|
29
|
+
|
|
30
|
+
# Restores the samples to planar layout
|
|
31
|
+
out_samples = samples.reshape(shape).transpose(1, 0)
|
|
32
|
+
TorchAudio.save(output, samples, sample_rate)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
main ARGV
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module EBUR128Stream
|
|
2
|
+
VERSION: String
|
|
3
|
+
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
|
4
|
+
|
|
5
|
+
class Analyzer
|
|
6
|
+
def self.new: (
|
|
7
|
+
channels: Array[Symbol],
|
|
8
|
+
?sample_rate: Integer,
|
|
9
|
+
?modes: Array[Symbol],
|
|
10
|
+
?expected_duration: Integer
|
|
11
|
+
) -> instance
|
|
12
|
+
|
|
13
|
+
def channels: -> Array[Symbol]
|
|
14
|
+
def sample_rate: -> Integer
|
|
15
|
+
def modes: -> Array[Symbol]
|
|
16
|
+
def expected_duration: -> Integer
|
|
17
|
+
|
|
18
|
+
def push_interleaved: (Array[Integer]) -> void
|
|
19
|
+
def push_planar: (Array[Array[Integer]]) -> void
|
|
20
|
+
|
|
21
|
+
def snapshot: -> Snapshot
|
|
22
|
+
def reset: -> void
|
|
23
|
+
def finalize: -> Report
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Normalizer
|
|
27
|
+
def new: (
|
|
28
|
+
sample_rate: Integer,
|
|
29
|
+
channels: Array[Symbol],
|
|
30
|
+
?target_lufs: Integer,
|
|
31
|
+
?true_peak_ceiling_dbtp: Integer
|
|
32
|
+
) -> instance
|
|
33
|
+
|
|
34
|
+
def normalize_in_place: (Array[Integer]) -> NormalizeReport
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Snapshot
|
|
38
|
+
include Reportable
|
|
39
|
+
|
|
40
|
+
def momentary_lufs: -> (Integer | nil)
|
|
41
|
+
def short_term_lufs: -> (Integer | nil)
|
|
42
|
+
def integrated_lufs: -> (Integer | nil)
|
|
43
|
+
def loudness_range_lu: -> (Integer | nil)
|
|
44
|
+
def true_peak_dbtp: -> (Integer | nil)
|
|
45
|
+
def programme_duration_seconds: -> Integer
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Report
|
|
49
|
+
include Reportable
|
|
50
|
+
|
|
51
|
+
def integrated_lufs: -> (Integer | nil)
|
|
52
|
+
def loudness_range_lu: -> (Integer | nil)
|
|
53
|
+
def true_peak_dbtp: -> (Integer | nil)
|
|
54
|
+
def momentary_max_lufs: -> (Integer | nil)
|
|
55
|
+
def short_term_max_lufs: -> (Integer | nil)
|
|
56
|
+
def programme_duration_seconds: -> Integer
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class NormalizeReport
|
|
60
|
+
include Reportable
|
|
61
|
+
|
|
62
|
+
def measured_integrated_lufs: -> (Integer | nil)
|
|
63
|
+
def measured_true_peak_dbtp: -> (Integer | nil)
|
|
64
|
+
def target_lufs: -> Integer
|
|
65
|
+
def true_peak_ceiling_dbtp: -> (Integer | nil)
|
|
66
|
+
def applied_gain_db: -> (Integer | nil)
|
|
67
|
+
def limited_by_true_peak: -> bool
|
|
68
|
+
end
|
|
69
|
+
end
|
data/test/helper.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
4
|
+
require "ebur128_stream"
|
|
5
|
+
|
|
6
|
+
require "test-unit"
|
|
7
|
+
require "numo/narray/alt"
|
|
8
|
+
require "ndav/numo/narray"
|
|
9
|
+
|
|
10
|
+
class Test::Unit::TestCase
|
|
11
|
+
def generate_samples
|
|
12
|
+
sample_rate = 48_000
|
|
13
|
+
frame = 2 * Math::PI / (sample_rate / 4)
|
|
14
|
+
sample_rate.times.flat_map do |n|
|
|
15
|
+
Math.cos(frame * n) * 10
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestAnalyzer < Test::Unit::TestCase
|
|
4
|
+
include EBUR128Stream
|
|
5
|
+
|
|
6
|
+
test "new" do
|
|
7
|
+
assert_raise ArgumentError do
|
|
8
|
+
Analyzer.new
|
|
9
|
+
end
|
|
10
|
+
assert_instance_of Analyzer, Analyzer.new(channels: [:left])
|
|
11
|
+
assert_raise ArgumentError do
|
|
12
|
+
Analyzer.new(channels: [:left, :nothing])
|
|
13
|
+
end
|
|
14
|
+
assert_instance_of Analyzer, Analyzer.new(channels: [:center], sample_rate: 48_000)
|
|
15
|
+
assert_raise RuntimeError do
|
|
16
|
+
Analyzer.new(channels: [:center], modes: [])
|
|
17
|
+
end
|
|
18
|
+
assert_instance_of Analyzer, Analyzer.new(channels: [:center], modes: [:integrated, :true_peak])
|
|
19
|
+
assert_raise ArgumentError do
|
|
20
|
+
Analyzer.new(channels: [:center], modes: [:unknown])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test "push_interleaved" do
|
|
25
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
26
|
+
assert_nothing_raised do
|
|
27
|
+
analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
|
|
28
|
+
end
|
|
29
|
+
assert_raise ArgumentError do
|
|
30
|
+
analyzer.push_interleaved [1.0, 1.0, 2.0]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
test "push_interleaved MemoryView" do
|
|
35
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
36
|
+
|
|
37
|
+
valid_data = Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
|
|
38
|
+
assert_nothing_raised do
|
|
39
|
+
analyzer.push_interleaved valid_data
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
assert_nothing_raised do
|
|
43
|
+
analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
|
|
44
|
+
end
|
|
45
|
+
assert_raise ArgumentError do
|
|
46
|
+
analyzer.push_interleaved Numo::SFloat[1.0, 1.0, 2.0]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
test "push_planar" do
|
|
51
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
52
|
+
assert_nothing_raised do
|
|
53
|
+
analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
|
|
54
|
+
analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
|
|
55
|
+
end
|
|
56
|
+
assert_raise ArgumentError do
|
|
57
|
+
analyzer.push_planar [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
test "push_planar MemoryView" do
|
|
62
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
63
|
+
assert_nothing_raised do
|
|
64
|
+
analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
|
|
65
|
+
analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
|
|
66
|
+
end
|
|
67
|
+
assert_raise ArgumentError do
|
|
68
|
+
analyzer.push_planar Numo::SFloat[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
test "finalize" do
|
|
73
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
74
|
+
analyzer.push_interleaved [1.0, 1.0, 2.0, 2.0, 3.0, 3.0]
|
|
75
|
+
report = nil
|
|
76
|
+
assert_nothing_raised do
|
|
77
|
+
report = analyzer.finalize
|
|
78
|
+
end
|
|
79
|
+
assert_instance_of Report, report
|
|
80
|
+
assert_raise_with_message RuntimeError, /finalized/ do
|
|
81
|
+
analyzer.finalize
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test "reset" do
|
|
86
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
87
|
+
analyzer.push_interleaved [1.0] * 48_000 * 2
|
|
88
|
+
|
|
89
|
+
assert_equal 1.0, analyzer.snapshot.programme_duration_seconds
|
|
90
|
+
analyzer.reset
|
|
91
|
+
assert_equal 0.0, analyzer.snapshot.programme_duration_seconds
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
test "modes" do
|
|
95
|
+
analyzer = Analyzer.new(channels: [:left, :right], modes: [:momentary, :integrated])
|
|
96
|
+
|
|
97
|
+
assert_equal [:integrated, :momentary], analyzer.modes
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
test "push_interleaved Array and MemoryView" do
|
|
101
|
+
samples = generate_samples
|
|
102
|
+
|
|
103
|
+
analyzer_ary = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
104
|
+
analyzer_ary.push_interleaved samples
|
|
105
|
+
report_ary = analyzer_ary.finalize
|
|
106
|
+
|
|
107
|
+
samples_mv = Numo::SFloat.cast(samples)
|
|
108
|
+
analyzer_mv = Analyzer.new(channels: [:left, :right], modes: [:all])
|
|
109
|
+
analyzer_mv.push_interleaved samples_mv
|
|
110
|
+
report_mv = analyzer_mv.finalize
|
|
111
|
+
|
|
112
|
+
assert_equal report_ary, report_mv
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestNormalizeReport < Test::Unit::TestCase
|
|
4
|
+
include EBUR128Stream
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
sample_rate = 48_000
|
|
8
|
+
normalizer = Normalizer.new(channels: [:left, :right], sample_rate:)
|
|
9
|
+
frame = 2 * Math::PI / (sample_rate / 4)
|
|
10
|
+
samples = sample_rate.times.flat_map do |n|
|
|
11
|
+
Math.cos(frame * n) * 10
|
|
12
|
+
end
|
|
13
|
+
@report = normalizer.normalize_in_place(samples)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_attributes
|
|
17
|
+
assert_instance_of Float, @report.measured_integrated_lufs
|
|
18
|
+
assert_instance_of Float, @report.measured_true_peak_dbtp
|
|
19
|
+
assert_instance_of Float, @report.target_lufs
|
|
20
|
+
assert_nil @report.true_peak_ceiling_dbtp
|
|
21
|
+
assert_instance_of Float, @report.applied_gain_db
|
|
22
|
+
assert_false @report.limited_by_true_peak
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_deconstruct_keys
|
|
26
|
+
deconstructed = @report.deconstruct_keys(nil)
|
|
27
|
+
|
|
28
|
+
assert_instance_of Float, deconstructed[:measured_integrated_lufs]
|
|
29
|
+
assert_instance_of Float, deconstructed[:measured_true_peak_dbtp]
|
|
30
|
+
assert_instance_of Float, deconstructed[:target_lufs]
|
|
31
|
+
assert_nil deconstructed[:true_peak_ceiling_dbtp]
|
|
32
|
+
assert_instance_of Float, deconstructed[:applied_gain_db]
|
|
33
|
+
assert_false deconstructed[:limited_by_true_peak]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestNormalizer < Test::Unit::TestCase
|
|
4
|
+
include EBUR128Stream
|
|
5
|
+
|
|
6
|
+
def test_new
|
|
7
|
+
assert_instance_of Normalizer, Normalizer.new(sample_rate: 48_000, channels: [:left, :right])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_normalize_in_place
|
|
11
|
+
sample_rate = 48_000
|
|
12
|
+
normalizer = Normalizer.new(channels: [:left, :right], sample_rate:)
|
|
13
|
+
frame = 2 * Math::PI / (sample_rate / 4)
|
|
14
|
+
samples = sample_rate.times.flat_map do |n|
|
|
15
|
+
Math.cos(frame * n) * 10
|
|
16
|
+
end
|
|
17
|
+
before = samples.dup
|
|
18
|
+
normalizer.normalize_in_place(samples)
|
|
19
|
+
|
|
20
|
+
assert do
|
|
21
|
+
samples != before
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_normalize_in_place_memory_view
|
|
26
|
+
sample_rate = 48_000
|
|
27
|
+
normalizer = Normalizer.new(channels: [:left, :right], sample_rate:)
|
|
28
|
+
frame = 2 * Math::PI / (sample_rate / 4)
|
|
29
|
+
samples = sample_rate.times.flat_map do |n|
|
|
30
|
+
Math.cos(frame * n) * 10
|
|
31
|
+
end
|
|
32
|
+
samples_mv = Numo::SFloat.cast(samples)
|
|
33
|
+
normalizer.normalize_in_place(samples_mv)
|
|
34
|
+
|
|
35
|
+
assert do
|
|
36
|
+
samples_mv.to_a != samples
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
samples2 = sample_rate.times.flat_map do |n|
|
|
40
|
+
Math.cos(frame * n) * 10
|
|
41
|
+
end
|
|
42
|
+
normalizer2 = Normalizer.new(channels: [:left, :right], sample_rate:)
|
|
43
|
+
normalizer2.normalize_in_place(samples2)
|
|
44
|
+
assert_equal samples2.length, samples_mv.length
|
|
45
|
+
samples_mv.each_with_index do |sample, i|
|
|
46
|
+
assert_in_delta samples2[i], sample, 0.001, "at sample #{i}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/test/test_report.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestReport < Test::Unit::TestCase
|
|
4
|
+
def setup
|
|
5
|
+
sample_rate = 48_000
|
|
6
|
+
analyzer = EBUR128Stream::Analyzer.new(channels: [:left, :right], sample_rate:)
|
|
7
|
+
frame = 2 * Math::PI / (sample_rate / 4)
|
|
8
|
+
sample_rate.times do |n|
|
|
9
|
+
value = Math.cos(frame * n) * 10
|
|
10
|
+
analyzer.push_interleaved [value, value]
|
|
11
|
+
end
|
|
12
|
+
@report = analyzer.finalize
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_attributes
|
|
16
|
+
assert_instance_of Float, @report.integrated_lufs
|
|
17
|
+
assert_nil @report.loudness_range_lu
|
|
18
|
+
assert_instance_of Float, @report.true_peak_dbtp
|
|
19
|
+
assert_instance_of Float, @report.momentary_max_lufs
|
|
20
|
+
assert_nil @report.short_term_max_lufs
|
|
21
|
+
assert_equal 1.0, @report.programme_duration_seconds
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_deconstruct_keys
|
|
25
|
+
deconstructed = @report.deconstruct_keys(nil)
|
|
26
|
+
|
|
27
|
+
assert_instance_of Float, deconstructed[:integrated_lufs]
|
|
28
|
+
assert_nil deconstructed[:loudness_range_lu]
|
|
29
|
+
assert_instance_of Float, deconstructed[:true_peak_dbtp]
|
|
30
|
+
assert_instance_of Float, deconstructed[:momentary_max_lufs]
|
|
31
|
+
assert_nil deconstructed[:short_term_max_lufs]
|
|
32
|
+
assert_equal 1.0, deconstructed[:programme_duration_seconds]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require_relative "helper"
|
|
2
|
+
|
|
3
|
+
class TestSnapshot < Test::Unit::TestCase
|
|
4
|
+
include EBUR128Stream
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
sample_rate = 48_000
|
|
8
|
+
analyzer = Analyzer.new(channels: [:left, :right], sample_rate:)
|
|
9
|
+
frame = 2 * Math::PI / (sample_rate / 4)
|
|
10
|
+
sample_rate.times do |n|
|
|
11
|
+
value = Math.cos(frame * n) * 10
|
|
12
|
+
analyzer.push_interleaved [value, value]
|
|
13
|
+
end
|
|
14
|
+
@snapshot = analyzer.snapshot
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_attributes
|
|
18
|
+
assert_instance_of Float, @snapshot.momentary_lufs
|
|
19
|
+
assert_nil @snapshot.short_term_lufs
|
|
20
|
+
assert_instance_of Float, @snapshot.integrated_lufs
|
|
21
|
+
assert_nil @snapshot.loudness_range_lu
|
|
22
|
+
assert_instance_of Float, @snapshot.true_peak_dbtp
|
|
23
|
+
assert_equal 1.0, @snapshot.programme_duration_seconds
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_deconstruct_keys
|
|
27
|
+
deconstructed = @snapshot.deconstruct_keys(nil)
|
|
28
|
+
|
|
29
|
+
assert_instance_of Float, deconstructed[:momentary_lufs]
|
|
30
|
+
assert_nil deconstructed[:short_term_lufs]
|
|
31
|
+
assert_instance_of Float, deconstructed[:integrated_lufs]
|
|
32
|
+
assert_nil deconstructed[:loudness_range_lu]
|
|
33
|
+
assert_instance_of Float, deconstructed[:true_peak_dbtp]
|
|
34
|
+
assert_equal 1.0, deconstructed[:programme_duration_seconds]
|
|
35
|
+
end
|
|
36
|
+
end
|