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.
@@ -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
@@ -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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "helper"
4
+
5
+ class EBUR128StreamTest < Test::Unit::TestCase
6
+ include EBUR128Stream
7
+
8
+ test "VERSION" do
9
+ assert do
10
+ ::EBUR128Stream.const_defined?(:VERSION)
11
+ end
12
+ end
13
+ 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
@@ -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