use alpha2_audio::{LoudnessMeter, LoudnessNormalization, LoudnessReport};
use wasm_bindgen::prelude::*;
use crate::web::invalid_input;
pub(crate) fn report_json(report: &LoudnessReport) -> String {
report_value(report).to_string()
}
fn report_value(report: &LoudnessReport) -> serde_json::Value {
serde_json::json!({
"integratedLufs": report.integrated_lufs,
"loudnessRangeLu": report.loudness_range_lu,
"truePeakDbtp": report.true_peak_dbtp,
"samplePeakDbfs": report.sample_peak_dbfs,
"momentaryMaxLufs": report.momentary_max_lufs,
"shortTermMaxLufs": report.short_term_max_lufs,
"frames": report.frames,
})
}
pub(crate) fn mix_report_json(report: &LoudnessReport, limiter_threshold: Option<f64>) -> String {
let mut value = report_value(report);
value["limiterThreshold"] = serde_json::json!(limiter_threshold);
value.to_string()
}
fn report_from_json(json: &str) -> Result<(LoudnessReport, Option<f64>), JsValue> {
let value: serde_json::Value = serde_json::from_str(json).map_err(invalid_input)?;
let Some(report) = value.as_object() else {
return Err(invalid_input(
"a loudness report is a JSON object; parse loudnessReport() before planning, and \
measure with metering on",
));
};
let Some(frames) = report.get("frames").and_then(serde_json::Value::as_u64) else {
return Err(invalid_input(
"a loudness report states the frames it measured",
));
};
let limiter_threshold = match report.get("limiterThreshold") {
None | Some(serde_json::Value::Null) => None,
Some(threshold) => match threshold.as_f64() {
Some(threshold) if threshold.is_finite() && threshold > 0.0 => Some(threshold),
_ => {
return Err(invalid_input(
"a loudness report's limiterThreshold is a positive number or null",
));
}
},
};
let number = |key: &str| report.get(key).and_then(serde_json::Value::as_f64);
Ok((
LoudnessReport {
integrated_lufs: number("integratedLufs"),
loudness_range_lu: number("loudnessRangeLu"),
true_peak_dbtp: number("truePeakDbtp"),
sample_peak_dbfs: number("samplePeakDbfs"),
momentary_max_lufs: number("momentaryMaxLufs"),
short_term_max_lufs: number("shortTermMaxLufs"),
frames,
},
limiter_threshold,
))
}
#[wasm_bindgen(js_name = planLoudnessNormalization)]
pub fn plan_loudness_normalization(
report_json: &str,
target_lufs: f64,
ceiling_dbtp: f64,
) -> Result<String, JsValue> {
let (report, limiter_threshold) = report_from_json(report_json)?;
Ok(
LoudnessNormalization::plan(&report, target_lufs, ceiling_dbtp, limiter_threshold)
.map_or_else(
|| "null".to_owned(),
|plan| {
serde_json::json!({
"gainDb": plan.gain_db,
"integratedLufs": plan.integrated_lufs,
"truePeakDbtp": plan.true_peak_dbtp,
"peakLimited": plan.peak_limited,
"limiterLimited": plan.limiter_limited,
})
.to_string()
},
),
)
}
#[wasm_bindgen(js_name = LoudnessMeter)]
pub struct WasmLoudnessMeter {
meter: LoudnessMeter,
channels: usize,
}
#[wasm_bindgen(js_class = LoudnessMeter)]
impl WasmLoudnessMeter {
#[wasm_bindgen(constructor)]
pub fn new(sample_rate: u32, channels: usize) -> Result<Self, JsValue> {
let meter = LoudnessMeter::new(sample_rate, channels).map_err(invalid_input)?;
Ok(Self { meter, channels })
}
pub fn push(&mut self, planar: &[f32]) -> Result<(), JsValue> {
if !planar.len().is_multiple_of(self.channels) {
return Err(invalid_input(
"a planar block holds the same number of samples per channel",
));
}
let frames = planar.len() / self.channels;
if frames == 0 {
return Ok(());
}
let planes = planar.chunks(frames).collect::<Vec<_>>();
self.meter.push(&planes).map_err(invalid_input)
}
#[must_use]
pub fn report(&self) -> String {
report_json(&self.meter.report())
}
#[wasm_bindgen(getter, js_name = momentaryLufs)]
#[must_use]
pub fn momentary_lufs(&self) -> Option<f64> {
self.meter.momentary_lufs()
}
#[wasm_bindgen(getter, js_name = shortTermLufs)]
#[must_use]
pub fn short_term_lufs(&self) -> Option<f64> {
self.meter.short_term_lufs()
}
}