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,123 @@
|
|
|
1
|
+
mod analyzer;
|
|
2
|
+
mod error;
|
|
3
|
+
mod normalize;
|
|
4
|
+
mod report;
|
|
5
|
+
mod samples;
|
|
6
|
+
mod snapshot;
|
|
7
|
+
|
|
8
|
+
use crate::{
|
|
9
|
+
error::Error,
|
|
10
|
+
report::Report,
|
|
11
|
+
samples::{InterleavedSamples, PlanarSamples},
|
|
12
|
+
snapshot::Snapshot,
|
|
13
|
+
};
|
|
14
|
+
use ebur128_stream_rs as engine;
|
|
15
|
+
use magnus::{RArray, Ruby, Symbol, TryConvert, Value};
|
|
16
|
+
use std::ops::Deref;
|
|
17
|
+
|
|
18
|
+
struct Channel(engine::Channel);
|
|
19
|
+
|
|
20
|
+
impl From<Channel> for engine::Channel {
|
|
21
|
+
fn from(value: Channel) -> Self {
|
|
22
|
+
value.0
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl TryConvert for Channel {
|
|
27
|
+
fn try_convert(val: Value) -> Result<Self, magnus::Error> {
|
|
28
|
+
let channel = Symbol::try_convert(val)?;
|
|
29
|
+
Ok(match channel.name()?.as_ref() {
|
|
30
|
+
"left" => Self(engine::Channel::Left),
|
|
31
|
+
"right" => Self(engine::Channel::Right),
|
|
32
|
+
"center" => Self(engine::Channel::Center),
|
|
33
|
+
"left_surround" => Self(engine::Channel::LeftSurround),
|
|
34
|
+
"right_surround" => Self(engine::Channel::RightSurround),
|
|
35
|
+
"lfe" => Self(engine::Channel::Lfe),
|
|
36
|
+
"other" => Self(engine::Channel::Other),
|
|
37
|
+
_ => {
|
|
38
|
+
let ruby = Ruby::get_with(val);
|
|
39
|
+
return Err(magnus::Error::new(
|
|
40
|
+
ruby.exception_arg_error(),
|
|
41
|
+
format!("unknown channel: {val}"),
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub(crate) struct Channels {
|
|
49
|
+
inner: Vec<engine::Channel>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl Deref for Channels {
|
|
53
|
+
type Target = Vec<engine::Channel>;
|
|
54
|
+
|
|
55
|
+
fn deref(&self) -> &Self::Target {
|
|
56
|
+
&self.inner
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl<'a> From<&'a [engine::Channel]> for Channels {
|
|
61
|
+
fn from(value: &'a [engine::Channel]) -> Self {
|
|
62
|
+
Self {
|
|
63
|
+
inner: value.to_vec(),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
impl TryConvert for Channels {
|
|
69
|
+
fn try_convert(val: Value) -> Result<Self, magnus::Error> {
|
|
70
|
+
Ok(Self {
|
|
71
|
+
inner: RArray::try_convert(val)?
|
|
72
|
+
.into_iter()
|
|
73
|
+
.map(|value| Ok(Channel::try_convert(value)?.into()))
|
|
74
|
+
.collect::<Result<Vec<engine::Channel>, magnus::Error>>()?,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl Channels {
|
|
80
|
+
fn try_into_rarray(&self, ruby: &Ruby) -> Result<RArray, magnus::Error> {
|
|
81
|
+
let syms = self
|
|
82
|
+
.inner
|
|
83
|
+
.iter()
|
|
84
|
+
.map(|channel| {
|
|
85
|
+
use engine::Channel::*;
|
|
86
|
+
|
|
87
|
+
let str = match channel {
|
|
88
|
+
Left => "left",
|
|
89
|
+
Right => "right",
|
|
90
|
+
Center => "center",
|
|
91
|
+
LeftSurround => "left_surround",
|
|
92
|
+
RightSurround => "right_surround",
|
|
93
|
+
Lfe => "lfe",
|
|
94
|
+
Other => "other",
|
|
95
|
+
_ => {
|
|
96
|
+
return Err(magnus::Error::new(
|
|
97
|
+
ruby.exception_runtime_error(),
|
|
98
|
+
"couldn't convert to Symbol: {channel}",
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
Ok(ruby.to_symbol(str))
|
|
103
|
+
})
|
|
104
|
+
.collect::<Result<Vec<Symbol>, magnus::Error>>()?;
|
|
105
|
+
Ok(ruby.ary_new_from_values(&syms))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn into_boxed_slice(self) -> Box<[engine::Channel]> {
|
|
109
|
+
self.inner.into_boxed_slice()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[magnus::init]
|
|
114
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
115
|
+
let ebur128_stream = ruby.define_module("EBUR128Stream")?;
|
|
116
|
+
|
|
117
|
+
analyzer::init(ruby, &ebur128_stream)?;
|
|
118
|
+
snapshot::init(ruby, &ebur128_stream)?;
|
|
119
|
+
report::init(ruby, &ebur128_stream)?;
|
|
120
|
+
normalize::init(ruby, &ebur128_stream)?;
|
|
121
|
+
|
|
122
|
+
Ok(())
|
|
123
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
use crate::{Channels, error::Error, samples::WritableInterleavedSamples};
|
|
2
|
+
use ebur128_stream_rs as engine;
|
|
3
|
+
use magnus::{
|
|
4
|
+
RModule, Ruby, Value, function, method,
|
|
5
|
+
prelude::*,
|
|
6
|
+
scan_args::{get_kwargs, scan_args},
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Members are the same to engine::normalize::Normalizer
|
|
10
|
+
#[magnus::wrap(class = "EBUR128Stream::Normalizer")]
|
|
11
|
+
struct Normalizer {
|
|
12
|
+
sample_rate: u32,
|
|
13
|
+
channels: Box<[engine::Channel]>,
|
|
14
|
+
target_lufs: Option<f64>,
|
|
15
|
+
true_peak_ceiling_dbtp: Option<f64>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl Normalizer {
|
|
19
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
20
|
+
let args = scan_args::<(), (), (), (), _, ()>(args)?;
|
|
21
|
+
let kws = get_kwargs::<_, (u32, Channels), (Option<f64>, Option<f64>), ()>(
|
|
22
|
+
args.keywords,
|
|
23
|
+
&["sample_rate", "channels"],
|
|
24
|
+
&["target_lufs", "true_peak_ceiling_dbtp"],
|
|
25
|
+
)?;
|
|
26
|
+
let (sample_rate, channels) = kws.required;
|
|
27
|
+
let (target_lufs, true_peak_ceiling_dbtp) = kws.optional;
|
|
28
|
+
|
|
29
|
+
Ok(Self {
|
|
30
|
+
sample_rate,
|
|
31
|
+
channels: channels.into_boxed_slice(),
|
|
32
|
+
target_lufs,
|
|
33
|
+
true_peak_ceiling_dbtp,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn normalize_in_place(
|
|
38
|
+
&self,
|
|
39
|
+
mut samples: WritableInterleavedSamples,
|
|
40
|
+
) -> Result<NormalizeReport, Error> {
|
|
41
|
+
let mut normalizer = engine::normalize::Normalizer::new(self.sample_rate, &self.channels);
|
|
42
|
+
if let Some(target_lufs) = self.target_lufs {
|
|
43
|
+
normalizer = normalizer.target_lufs(target_lufs);
|
|
44
|
+
}
|
|
45
|
+
if let Some(true_peak_ceiling_dbtp) = self.true_peak_ceiling_dbtp {
|
|
46
|
+
normalizer = normalizer.true_peak_ceiling_dbtp(true_peak_ceiling_dbtp);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let report = normalizer
|
|
50
|
+
.normalize_in_place(samples.as_mut_slice())
|
|
51
|
+
.map_err(Error::runtime)?;
|
|
52
|
+
samples.write_back_in_place()?;
|
|
53
|
+
|
|
54
|
+
Ok(NormalizeReport { report })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[magnus::wrap(class = "EBUR128Stream::NormalizeReport")]
|
|
59
|
+
struct NormalizeReport {
|
|
60
|
+
report: engine::normalize::NormalizeReport,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl NormalizeReport {
|
|
64
|
+
fn measured_integrated_lufs(&self) -> Option<f64> {
|
|
65
|
+
self.report.measured_integrated_lufs
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn measured_true_peak_dbtp(&self) -> Option<f64> {
|
|
69
|
+
self.report.measured_true_peak_dbtp
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn target_lufs(&self) -> f64 {
|
|
73
|
+
self.report.target_lufs
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn true_peak_ceiling_dbtp(&self) -> Option<f64> {
|
|
77
|
+
self.report.true_peak_ceiling_dbtp
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn applied_gain_db(&self) -> f64 {
|
|
81
|
+
self.report.applied_gain_db
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn limited_by_true_peak(&self) -> bool {
|
|
85
|
+
self.report.limited_by_true_peak
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
90
|
+
let normalizer = module.define_class("Normalizer", ruby.class_object())?;
|
|
91
|
+
normalizer.define_singleton_method("new", function!(Normalizer::new, -1))?;
|
|
92
|
+
normalizer.define_method(
|
|
93
|
+
"normalize_in_place",
|
|
94
|
+
method!(Normalizer::normalize_in_place, 1),
|
|
95
|
+
)?;
|
|
96
|
+
|
|
97
|
+
let normalize_report = module.define_class("NormalizeReport", ruby.class_object())?;
|
|
98
|
+
normalize_report.define_method(
|
|
99
|
+
"measured_integrated_lufs",
|
|
100
|
+
method!(NormalizeReport::measured_integrated_lufs, 0),
|
|
101
|
+
)?;
|
|
102
|
+
normalize_report.define_method(
|
|
103
|
+
"measured_true_peak_dbtp",
|
|
104
|
+
method!(NormalizeReport::measured_true_peak_dbtp, 0),
|
|
105
|
+
)?;
|
|
106
|
+
normalize_report.define_method("target_lufs", method!(NormalizeReport::target_lufs, 0))?;
|
|
107
|
+
normalize_report.define_method(
|
|
108
|
+
"true_peak_ceiling_dbtp",
|
|
109
|
+
method!(NormalizeReport::true_peak_ceiling_dbtp, 0),
|
|
110
|
+
)?;
|
|
111
|
+
normalize_report.define_method(
|
|
112
|
+
"applied_gain_db",
|
|
113
|
+
method!(NormalizeReport::applied_gain_db, 0),
|
|
114
|
+
)?;
|
|
115
|
+
normalize_report.define_method(
|
|
116
|
+
"limited_by_true_peak",
|
|
117
|
+
method!(NormalizeReport::limited_by_true_peak, 0),
|
|
118
|
+
)?;
|
|
119
|
+
|
|
120
|
+
Ok(())
|
|
121
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
use crate::error::Error;
|
|
2
|
+
use ebur128_stream_rs as engine;
|
|
3
|
+
use magnus::{RModule, Ruby, method, prelude::*};
|
|
4
|
+
|
|
5
|
+
#[magnus::wrap(class = "EBUR128Stream::Report")]
|
|
6
|
+
pub(crate) struct Report {
|
|
7
|
+
pub(crate) report: engine::Report,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl Report {
|
|
11
|
+
fn integrated_lufs(&self) -> Option<f64> {
|
|
12
|
+
self.report.integrated_lufs()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn loudness_range_lu(&self) -> Option<f64> {
|
|
16
|
+
self.report.loudness_range_lu()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn true_peak_dbtp(&self) -> Option<f64> {
|
|
20
|
+
self.report.true_peak_dbtp()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn momentary_max_lufs(&self) -> Option<f64> {
|
|
24
|
+
self.report.momentary_max_lufs()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn short_term_max_lufs(&self) -> Option<f64> {
|
|
28
|
+
self.report.short_term_max_lufs()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn programme_duration_seconds(&self) -> f64 {
|
|
32
|
+
self.report.programme_duration_seconds()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
37
|
+
let report = module.define_class("Report", ruby.class_object())?;
|
|
38
|
+
report.define_method("integrated_lufs", method!(Report::integrated_lufs, 0))?;
|
|
39
|
+
report.define_method("loudness_range_lu", method!(Report::loudness_range_lu, 0))?;
|
|
40
|
+
report.define_method("true_peak_dbtp", method!(Report::true_peak_dbtp, 0))?;
|
|
41
|
+
report.define_method("momentary_max_lufs", method!(Report::momentary_max_lufs, 0))?;
|
|
42
|
+
report.define_method(
|
|
43
|
+
"short_term_max_lufs",
|
|
44
|
+
method!(Report::short_term_max_lufs, 0),
|
|
45
|
+
)?;
|
|
46
|
+
report.define_method(
|
|
47
|
+
"programme_duration_seconds",
|
|
48
|
+
method!(Report::programme_duration_seconds, 0),
|
|
49
|
+
)?;
|
|
50
|
+
|
|
51
|
+
Ok(())
|
|
52
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
use crate::error::Error;
|
|
2
|
+
use grey_knights::memory_view::{Flags, FlagsChainable, ItemComponent, ValidatedMemoryView};
|
|
3
|
+
use magnus::{RArray, Ruby, TryConvert, Value, error::IntoError};
|
|
4
|
+
|
|
5
|
+
fn is_acceptable_component(component: ItemComponent) -> bool {
|
|
6
|
+
component.offset == 0 && component.repeat == 1 && is_acceptable_format(component.format)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fn is_acceptable_format(format: char) -> bool {
|
|
10
|
+
match format {
|
|
11
|
+
'f' => true,
|
|
12
|
+
|
|
13
|
+
#[cfg(target_endian = "little")]
|
|
14
|
+
'e' => true,
|
|
15
|
+
|
|
16
|
+
#[cfg(target_endian = "big")]
|
|
17
|
+
'g' => true,
|
|
18
|
+
|
|
19
|
+
_ => false,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub(crate) enum InterleavedSamples {
|
|
24
|
+
Array { samples: Vec<f32> },
|
|
25
|
+
MemoryView { view: ValidatedMemoryView<f32> },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl TryConvert for InterleavedSamples {
|
|
29
|
+
fn try_convert(val: Value) -> Result<Self, magnus::Error> {
|
|
30
|
+
if let Some(view) = Self::consume_memory_view(val) {
|
|
31
|
+
Ok(Self::MemoryView { view })
|
|
32
|
+
} else if let Some(obj) = RArray::from_value(val) {
|
|
33
|
+
Ok(Self::Array {
|
|
34
|
+
samples: obj.to_vec()?,
|
|
35
|
+
})
|
|
36
|
+
} else {
|
|
37
|
+
Err(Error::argument(format!("unsupported samples type: {val}"))
|
|
38
|
+
.into_error(&Ruby::get_with(val)))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl InterleavedSamples {
|
|
44
|
+
pub(crate) fn as_slice(&self) -> &[f32] {
|
|
45
|
+
match self {
|
|
46
|
+
Self::Array { samples } => samples,
|
|
47
|
+
Self::MemoryView { view } => view.data(),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn consume_memory_view(val: Value) -> Option<ValidatedMemoryView<f32>> {
|
|
52
|
+
let view = ValidatedMemoryView::<f32>::new(val, Flags::any_contiguous());
|
|
53
|
+
if let Ok(mut view) = view {
|
|
54
|
+
if Self::is_acceptable(&mut view).unwrap_or(false) {
|
|
55
|
+
return Some(view);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let view = ValidatedMemoryView::<f32>::new(val, Flags::simple());
|
|
59
|
+
if let Ok(mut view) = view {
|
|
60
|
+
if Self::is_acceptable(&mut view).unwrap_or(false) {
|
|
61
|
+
return Some(view);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
None
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// TODO: Check format more strictly(size, other expression)
|
|
68
|
+
fn is_acceptable(view: &mut ValidatedMemoryView<f32>) -> Result<bool, Error> {
|
|
69
|
+
let item_desc = view.item_desc()?;
|
|
70
|
+
Ok(view.ndim() == 1
|
|
71
|
+
&& item_desc.len() == 1
|
|
72
|
+
&& item_desc
|
|
73
|
+
.into_iter()
|
|
74
|
+
.next()
|
|
75
|
+
.is_some_and(is_acceptable_component))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub(crate) enum WritableInterleavedSamples {
|
|
80
|
+
Array { obj: RArray, samples: Vec<f32> },
|
|
81
|
+
MemoryView { view: ValidatedMemoryView<f32> },
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl TryConvert for WritableInterleavedSamples {
|
|
85
|
+
fn try_convert(val: Value) -> Result<Self, magnus::Error> {
|
|
86
|
+
if let Some(view) = Self::consume_memory_view(val) {
|
|
87
|
+
Ok(Self::MemoryView { view })
|
|
88
|
+
} else if let Some(obj) = RArray::from_value(val) {
|
|
89
|
+
Ok(Self::Array {
|
|
90
|
+
obj,
|
|
91
|
+
samples: obj.to_vec()?,
|
|
92
|
+
})
|
|
93
|
+
} else {
|
|
94
|
+
Err(Error::argument(format!("unsupported samples type: {val}"))
|
|
95
|
+
.into_error(&Ruby::get_with(val)))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl WritableInterleavedSamples {
|
|
101
|
+
pub(crate) fn as_mut_slice(&mut self) -> &mut [f32] {
|
|
102
|
+
match self {
|
|
103
|
+
Self::Array { obj: _, samples } => samples,
|
|
104
|
+
Self::MemoryView { view } => view.data_as_mut(),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub(crate) fn write_back_in_place(self) -> Result<(), Error> {
|
|
109
|
+
match self {
|
|
110
|
+
Self::Array { obj, samples } => {
|
|
111
|
+
let ruby = Ruby::get_with(obj);
|
|
112
|
+
obj.replace(ruby.ary_from_vec(samples))?;
|
|
113
|
+
}
|
|
114
|
+
Self::MemoryView { view: _ } => {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Ok(())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn consume_memory_view(val: Value) -> Option<ValidatedMemoryView<f32>> {
|
|
121
|
+
let view = ValidatedMemoryView::<f32>::new(val, Flags::writable().any_contiguous());
|
|
122
|
+
if let Ok(mut view) = view {
|
|
123
|
+
if Self::is_acceptable(&mut view).unwrap_or(false) {
|
|
124
|
+
return Some(view);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let view = ValidatedMemoryView::<f32>::new(val, Flags::simple());
|
|
128
|
+
if let Ok(mut view) = view {
|
|
129
|
+
if !view.is_readonly() && Self::is_acceptable(&mut view).unwrap_or(false) {
|
|
130
|
+
return Some(view);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
None
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn is_acceptable(view: &mut ValidatedMemoryView<f32>) -> Result<bool, Error> {
|
|
137
|
+
let item_desc = view.item_desc()?;
|
|
138
|
+
Ok(view.ndim() == 1
|
|
139
|
+
&& item_desc.len() == 1
|
|
140
|
+
&& item_desc
|
|
141
|
+
.into_iter()
|
|
142
|
+
.next()
|
|
143
|
+
.is_some_and(is_acceptable_component))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub(crate) enum PlanarSamples {
|
|
148
|
+
Array { samples: Vec<Vec<f32>> },
|
|
149
|
+
MemoryView { view: ValidatedMemoryView<f32> },
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
impl TryConvert for PlanarSamples {
|
|
153
|
+
fn try_convert(val: Value) -> Result<Self, magnus::Error> {
|
|
154
|
+
if let Some(view) = Self::consume_memory_view(val) {
|
|
155
|
+
Ok(Self::MemoryView { view })
|
|
156
|
+
} else if let Some(obj) = RArray::from_value(val) {
|
|
157
|
+
Ok(Self::Array {
|
|
158
|
+
samples: obj.to_vec()?,
|
|
159
|
+
})
|
|
160
|
+
} else {
|
|
161
|
+
Err(Error::argument(format!("unsupported samples type: {val}"))
|
|
162
|
+
.into_error(&Ruby::get_with(val)))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
impl PlanarSamples {
|
|
168
|
+
pub fn channel_slices(&self) -> Vec<&[f32]> {
|
|
169
|
+
match self {
|
|
170
|
+
Self::Array { samples } => samples.iter().map(Vec::as_slice).collect(),
|
|
171
|
+
Self::MemoryView { view } => {
|
|
172
|
+
let shape = view.shape().expect("ndim > 1 is checked when calling ");
|
|
173
|
+
let n_channels = shape[0];
|
|
174
|
+
let channel_len = shape[1];
|
|
175
|
+
if channel_len == 0 {
|
|
176
|
+
(0..n_channels).map(|_| &view.data()[..0]).collect()
|
|
177
|
+
} else {
|
|
178
|
+
view.data().chunks_exact(channel_len).collect()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn consume_memory_view(val: Value) -> Option<ValidatedMemoryView<f32>> {
|
|
185
|
+
let view = ValidatedMemoryView::<f32>::new(val, Flags::row_major());
|
|
186
|
+
if let Ok(mut view) = view {
|
|
187
|
+
if Self::is_acceptable(&mut view).unwrap_or(false) {
|
|
188
|
+
return Some(view);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let view = ValidatedMemoryView::<f32>::new(val, Flags::simple());
|
|
192
|
+
if let Ok(mut view) = view {
|
|
193
|
+
if Self::is_acceptable(&mut view).unwrap_or(false) {
|
|
194
|
+
return Some(view);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
None
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn is_acceptable(view: &mut ValidatedMemoryView<f32>) -> Result<bool, Error> {
|
|
201
|
+
let item_desc = view.item_desc()?;
|
|
202
|
+
Ok(view.ndim() == 2
|
|
203
|
+
&& item_desc.len() == 1
|
|
204
|
+
&& item_desc
|
|
205
|
+
.into_iter()
|
|
206
|
+
.next()
|
|
207
|
+
.is_some_and(is_acceptable_component))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
use crate::Error;
|
|
2
|
+
use ebur128_stream_rs as engine;
|
|
3
|
+
use magnus::{RModule, Ruby, method, prelude::*};
|
|
4
|
+
|
|
5
|
+
#[magnus::wrap(class = "EBUR128Stream::Snapshot")]
|
|
6
|
+
pub(crate) struct Snapshot {
|
|
7
|
+
pub(crate) snapshot: engine::Snapshot,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl Snapshot {
|
|
11
|
+
fn momentary_lufs(&self) -> Option<f64> {
|
|
12
|
+
self.snapshot.momentary_lufs()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn short_term_lufs(&self) -> Option<f64> {
|
|
16
|
+
self.snapshot.short_term_lufs()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn integrated_lufs(&self) -> Option<f64> {
|
|
20
|
+
self.snapshot.integrated_lufs()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn true_peak_dbtp(&self) -> Option<f64> {
|
|
24
|
+
self.snapshot.true_peak_dbtp()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn loudness_range_lu(&self) -> Option<f64> {
|
|
28
|
+
self.snapshot.loudness_range_lu()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn programme_duration_seconds(&self) -> f64 {
|
|
32
|
+
self.snapshot.programme_duration_seconds()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub(crate) fn init(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
37
|
+
let snapshot = module.define_class("Snapshot", ruby.class_object())?;
|
|
38
|
+
snapshot.define_method("momentary_lufs", method!(Snapshot::momentary_lufs, 0))?;
|
|
39
|
+
snapshot.define_method("short_term_lufs", method!(Snapshot::short_term_lufs, 0))?;
|
|
40
|
+
snapshot.define_method("integrated_lufs", method!(Snapshot::integrated_lufs, 0))?;
|
|
41
|
+
snapshot.define_method("loudness_range_lu", method!(Snapshot::loudness_range_lu, 0))?;
|
|
42
|
+
snapshot.define_method("true_peak_dbtp", method!(Snapshot::true_peak_dbtp, 0))?;
|
|
43
|
+
snapshot.define_method(
|
|
44
|
+
"programme_duration_seconds",
|
|
45
|
+
method!(Snapshot::programme_duration_seconds, 0),
|
|
46
|
+
)?;
|
|
47
|
+
|
|
48
|
+
Ok(())
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ebur128_stream/version"
|
|
4
|
+
require "ebur128_stream/ebur128_stream"
|
|
5
|
+
|
|
6
|
+
module EBUR128Stream
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
module Reportable
|
|
10
|
+
def deconstruct_keys(keys = nil)
|
|
11
|
+
keys = self.class::ATTRS if keys.nil?
|
|
12
|
+
(keys & self.class::ATTRS).inject({}) {|deconstructed, attr|
|
|
13
|
+
deconstructed[attr] = send(attr)
|
|
14
|
+
deconstructed
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inspect
|
|
19
|
+
"#<%{class} %{attrs}>" % {
|
|
20
|
+
class: self.class,
|
|
21
|
+
attrs: self.class::ATTRS.collect {|attr| "#{attr}=#{send(attr).inspect}"}.join(" ")
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ==(other)
|
|
26
|
+
deconstruct_keys(nil) == other.deconstruct_keys(nil)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Snapshot
|
|
31
|
+
include Reportable
|
|
32
|
+
|
|
33
|
+
ATTRS = [
|
|
34
|
+
:momentary_lufs,
|
|
35
|
+
:short_term_lufs,
|
|
36
|
+
:integrated_lufs,
|
|
37
|
+
:loudness_range_lu,
|
|
38
|
+
:true_peak_dbtp,
|
|
39
|
+
:programme_duration_seconds
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Report
|
|
44
|
+
include Reportable
|
|
45
|
+
|
|
46
|
+
ATTRS = [
|
|
47
|
+
:integrated_lufs,
|
|
48
|
+
:loudness_range_lu,
|
|
49
|
+
:true_peak_dbtp,
|
|
50
|
+
:momentary_max_lufs,
|
|
51
|
+
:short_term_max_lufs,
|
|
52
|
+
:programme_duration_seconds
|
|
53
|
+
]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class NormalizeReport
|
|
57
|
+
include Reportable
|
|
58
|
+
|
|
59
|
+
ATTRS = [
|
|
60
|
+
:measured_integrated_lufs,
|
|
61
|
+
:measured_true_peak_dbtp,
|
|
62
|
+
:target_lufs,
|
|
63
|
+
:true_peak_ceiling_dbtp,
|
|
64
|
+
:applied_gain_db,
|
|
65
|
+
:limited_by_true_peak,
|
|
66
|
+
]
|
|
67
|
+
end
|
|
68
|
+
end
|