use serde_json::Value;
pub const H13_CANONICAL_SAMPLE_RATES: &[u32] = &[
8_000, 16_000, 22_050, 24_000, 32_000, 44_100, 48_000, 88_200, 96_000,
];
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum AudioBoundsOutcome {
Ok { min: f64, max: f64 },
NotAnObject,
MissingMin,
MissingMax,
MinNotFinite { got: f64 },
MaxNotFinite { got: f64 },
MinBelowFloor { got: f64, floor: f64 },
MaxAboveCeiling { got: f64, ceiling: f64 },
MinExceedsMax { min: f64, max: f64 },
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum AudioSampleRateOutcome {
Ok {
rate: u32,
},
MissingSampleRate,
SampleRateNotPositive {
got: i64,
},
SampleRateOutOfRange {
got: i64,
},
ExpectedRateMismatch {
got: u32,
expected: u32,
},
NonCanonicalRate {
got: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum AudioChannelShapeOutcome {
Ok {
channels: u32,
samples: u64,
},
MissingChannels,
MissingSamples,
ChannelsNotPositive {
got: i64,
},
ChannelsOutOfRange {
got: i64,
},
SamplesNotPositive {
got: i64,
},
ExpectedChannelsMismatch {
got: u32,
expected: u32,
},
}
pub fn classify_amplitude_bounds(body: &Value) -> AudioBoundsOutcome {
let Some(obj) = body.as_object() else {
return AudioBoundsOutcome::NotAnObject;
};
let Some(min) = obj.get("min").and_then(Value::as_f64) else {
return AudioBoundsOutcome::MissingMin;
};
let Some(max) = obj.get("max").and_then(Value::as_f64) else {
return AudioBoundsOutcome::MissingMax;
};
if !min.is_finite() {
return AudioBoundsOutcome::MinNotFinite { got: min };
}
if !max.is_finite() {
return AudioBoundsOutcome::MaxNotFinite { got: max };
}
if min < -1.0 {
return AudioBoundsOutcome::MinBelowFloor {
got: min,
floor: -1.0,
};
}
if max > 1.0 {
return AudioBoundsOutcome::MaxAboveCeiling {
got: max,
ceiling: 1.0,
};
}
if min > max {
return AudioBoundsOutcome::MinExceedsMax { min, max };
}
AudioBoundsOutcome::Ok { min, max }
}
pub fn classify_sample_rate(body: &Value, expected: Option<u32>) -> AudioSampleRateOutcome {
let Some(raw) = body.get("sample_rate").and_then(Value::as_i64) else {
return AudioSampleRateOutcome::MissingSampleRate;
};
if raw <= 0 {
return AudioSampleRateOutcome::SampleRateNotPositive { got: raw };
}
let Ok(rate) = u32::try_from(raw) else {
return AudioSampleRateOutcome::SampleRateOutOfRange { got: raw };
};
if let Some(exp) = expected {
if rate != exp {
return AudioSampleRateOutcome::ExpectedRateMismatch {
got: rate,
expected: exp,
};
}
return AudioSampleRateOutcome::Ok { rate };
}
if !H13_CANONICAL_SAMPLE_RATES.contains(&rate) {
return AudioSampleRateOutcome::NonCanonicalRate { got: rate };
}
AudioSampleRateOutcome::Ok { rate }
}
pub fn classify_channel_shape(
body: &Value,
expected_channels: Option<u32>,
) -> AudioChannelShapeOutcome {
let Some(raw_c) = body.get("channels").and_then(Value::as_i64) else {
return AudioChannelShapeOutcome::MissingChannels;
};
if raw_c <= 0 {
return AudioChannelShapeOutcome::ChannelsNotPositive { got: raw_c };
}
let Some(raw_s) = body.get("samples").and_then(Value::as_i64) else {
return AudioChannelShapeOutcome::MissingSamples;
};
if raw_s <= 0 {
return AudioChannelShapeOutcome::SamplesNotPositive { got: raw_s };
}
let Ok(channels) = u32::try_from(raw_c) else {
return AudioChannelShapeOutcome::ChannelsOutOfRange { got: raw_c };
};
let samples = raw_s as u64;
if let Some(exp) = expected_channels {
if channels != exp {
return AudioChannelShapeOutcome::ExpectedChannelsMismatch {
got: channels,
expected: exp,
};
}
}
AudioChannelShapeOutcome::Ok { channels, samples }
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn good_body() -> Value {
json!({"min": -0.85, "max": 0.92, "sample_rate": 16000, "channels": 2, "samples": 48000})
}
#[test]
fn amplitude_bounds_ok_on_good_body() {
match classify_amplitude_bounds(&good_body()) {
AudioBoundsOutcome::Ok { min, max } => {
assert!((min - -0.85).abs() < 1e-9);
assert!((max - 0.92).abs() < 1e-9);
}
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn amplitude_bounds_rejects_not_object() {
assert_eq!(
classify_amplitude_bounds(&json!([1, 2])),
AudioBoundsOutcome::NotAnObject
);
}
#[test]
fn amplitude_bounds_rejects_missing_min() {
assert_eq!(
classify_amplitude_bounds(&json!({"max": 0.5})),
AudioBoundsOutcome::MissingMin
);
}
#[test]
fn amplitude_bounds_rejects_nan_min() {
let mut body = good_body();
body["min"] = json!(f64::NAN);
body["min"] = json!("nan");
assert_eq!(
classify_amplitude_bounds(&body),
AudioBoundsOutcome::MissingMin
);
}
#[test]
fn amplitude_bounds_rejects_below_floor() {
let mut body = good_body();
body["min"] = json!(-1.5);
assert!(matches!(
classify_amplitude_bounds(&body),
AudioBoundsOutcome::MinBelowFloor { .. }
));
}
#[test]
fn amplitude_bounds_rejects_above_ceiling() {
let mut body = good_body();
body["max"] = json!(1.5);
assert!(matches!(
classify_amplitude_bounds(&body),
AudioBoundsOutcome::MaxAboveCeiling { .. }
));
}
#[test]
fn amplitude_bounds_rejects_min_gt_max() {
let mut body = good_body();
body["min"] = json!(0.5);
body["max"] = json!(-0.5);
assert!(matches!(
classify_amplitude_bounds(&body),
AudioBoundsOutcome::MinExceedsMax { .. }
));
}
#[test]
fn sample_rate_ok_on_16k_canonical() {
match classify_sample_rate(&good_body(), None) {
AudioSampleRateOutcome::Ok { rate } => assert_eq!(rate, 16_000),
other => panic!("expected Ok(16000), got {other:?}"),
}
}
#[test]
fn sample_rate_ok_when_matches_expected() {
assert_eq!(
classify_sample_rate(&good_body(), Some(16_000)),
AudioSampleRateOutcome::Ok { rate: 16_000 }
);
}
#[test]
fn sample_rate_rejects_expected_mismatch() {
match classify_sample_rate(&good_body(), Some(22_050)) {
AudioSampleRateOutcome::ExpectedRateMismatch { got, expected } => {
assert_eq!(got, 16_000);
assert_eq!(expected, 22_050);
}
other => panic!("expected ExpectedRateMismatch, got {other:?}"),
}
}
#[test]
fn sample_rate_rejects_non_canonical_when_no_expected() {
let mut body = good_body();
body["sample_rate"] = json!(12345);
assert!(matches!(
classify_sample_rate(&body, None),
AudioSampleRateOutcome::NonCanonicalRate { got: 12345 }
));
}
#[test]
fn sample_rate_rejects_zero() {
let mut body = good_body();
body["sample_rate"] = json!(0);
assert!(matches!(
classify_sample_rate(&body, None),
AudioSampleRateOutcome::SampleRateNotPositive { got: 0 }
));
}
#[test]
fn channel_shape_ok_on_good_body() {
match classify_channel_shape(&good_body(), None) {
AudioChannelShapeOutcome::Ok { channels, samples } => {
assert_eq!(channels, 2);
assert_eq!(samples, 48_000);
}
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn channel_shape_rejects_missing_channels() {
let body = json!({"samples": 1000});
assert_eq!(
classify_channel_shape(&body, None),
AudioChannelShapeOutcome::MissingChannels
);
}
#[test]
fn channel_shape_rejects_zero_samples() {
let mut body = good_body();
body["samples"] = json!(0);
assert!(matches!(
classify_channel_shape(&body, None),
AudioChannelShapeOutcome::SamplesNotPositive { got: 0 }
));
}
#[test]
fn sample_rate_rejects_value_above_u32_that_aliases_to_16k() {
let mut body = good_body();
body["sample_rate"] = json!(4_294_983_296i64);
assert_eq!(
classify_sample_rate(&body, None),
AudioSampleRateOutcome::SampleRateOutOfRange { got: 4_294_983_296 }
);
}
#[test]
fn sample_rate_out_of_range_is_not_rescued_by_expected_rate() {
let mut body = good_body();
body["sample_rate"] = json!(4_294_983_296i64);
assert_eq!(
classify_sample_rate(&body, Some(16_000)),
AudioSampleRateOutcome::SampleRateOutOfRange { got: 4_294_983_296 }
);
}
#[test]
fn sample_rate_at_exactly_2_pow_32_reports_the_input_not_zero() {
let mut body = good_body();
body["sample_rate"] = json!(4_294_967_296i64);
assert_eq!(
classify_sample_rate(&body, None),
AudioSampleRateOutcome::SampleRateOutOfRange { got: 4_294_967_296 }
);
}
#[test]
fn sample_rate_at_u32_max_is_in_range_and_merely_non_canonical() {
let mut body = good_body();
body["sample_rate"] = json!(4_294_967_295i64);
assert_eq!(
classify_sample_rate(&body, None),
AudioSampleRateOutcome::NonCanonicalRate { got: 4_294_967_295 }
);
}
#[test]
fn channel_shape_rejects_channels_above_u32_that_aliases_to_1() {
let mut body = good_body();
body["channels"] = json!(4_294_967_297i64);
assert_eq!(
classify_channel_shape(&body, None),
AudioChannelShapeOutcome::ChannelsOutOfRange { got: 4_294_967_297 }
);
}
#[test]
fn channel_shape_out_of_range_is_not_rescued_by_expected_channels() {
let mut body = good_body();
body["channels"] = json!(4_294_967_297i64);
assert_eq!(
classify_channel_shape(&body, Some(1)),
AudioChannelShapeOutcome::ChannelsOutOfRange { got: 4_294_967_297 }
);
}
#[test]
fn channel_shape_at_u32_max_is_in_range() {
let mut body = good_body();
body["channels"] = json!(4_294_967_295i64);
assert_eq!(
classify_channel_shape(&body, None),
AudioChannelShapeOutcome::Ok {
channels: 4_294_967_295,
samples: 48_000
}
);
}
#[test]
fn channel_shape_rejects_expected_channels_mismatch() {
match classify_channel_shape(&good_body(), Some(1)) {
AudioChannelShapeOutcome::ExpectedChannelsMismatch { got, expected } => {
assert_eq!(got, 2);
assert_eq!(expected, 1);
}
other => panic!("expected ExpectedChannelsMismatch, got {other:?}"),
}
}
}