use serde_json::Value;
use sva_engine::bands::BandFloor;
use sva_engine::loudness_pattern::{LoudnessPattern, PatternFrame};
use sva_engine::panel::{Axes, Note as PanelNote, Panel};
use sva_engine::pitch::Note;
use sva_engine::score::{Cue, Event, Hit, Kind, Partial, Score, Sound, Tone, Voiced};
use sva_engine::spectrum::{Band, Peak};
use sva_engine::{
BandCrest, BandTrack, Bands, Crest, EnvelopeFrame, Loudness, LoudnessFrame, Output, PEAK_NOTE,
PitchFrame, Representation, Spectrum,
};
struct At<'v> {
value: &'v Value,
path: String,
}
type Read<T> = Result<T, String>;
impl<'v> At<'v> {
fn key(&self, key: &str) -> Read<At<'v>> {
let path = format!("{}.{key}", self.path);
match self.value.get(key) {
Some(value) => Ok(At { value, path }),
None => Err(format!("`{path}` is missing")),
}
}
fn num(&self) -> Read<f64> {
match self.value {
Value::Number(n) => n
.as_f64()
.ok_or_else(|| format!("`{}` is no number", self.path)),
Value::Null => Ok(f64::NAN),
_ => Err(self.not("a number")),
}
}
fn maybe(&self) -> Read<Option<f64>> {
match self.value {
Value::Null => Ok(None),
_ => self.num().map(Some),
}
}
fn count(&self) -> Read<usize> {
let held = self.value.as_u64().and_then(|n| usize::try_from(n).ok());
held.ok_or_else(|| self.not("a count"))
}
fn flag(&self) -> Read<bool> {
self.value
.as_bool()
.ok_or_else(|| self.not("true or false"))
}
fn text(&self) -> Read<String> {
let held = self.value.as_str().map(str::to_string);
held.ok_or_else(|| self.not("a string"))
}
fn numbers(&self) -> Read<Vec<f64>> {
let Value::Array(list) = self.value else {
return Err(self.not("a list"));
};
list.iter()
.enumerate()
.map(|(k, value)| {
At {
value,
path: format!("{}[{k}]", self.path),
}
.num()
})
.collect()
}
fn keyed<T: Copy>(&self, all: &[T], key: fn(T) -> &'static str) -> Read<T> {
let text = self.text()?;
let named = all.iter().copied().find(|k| key(*k) == text);
named.ok_or_else(|| {
let keys: Vec<&str> = all.iter().map(|k| key(*k)).collect();
format!(
"`{}` is `{text}`, not one of {}",
self.path,
keys.join(", ")
)
})
}
fn not(&self, wanted: &str) -> String {
format!("`{}` is {}, not {wanted}", self.path, shape(self.value))
}
fn items<T>(&self, mut each: impl FnMut(At) -> Read<T>) -> Read<Vec<T>> {
let items = self.key("items")?;
let Value::Array(list) = items.value else {
return Err(items.not("a list"));
};
let count = self.key("pagination")?.key("count")?.count()?;
if count != list.len() {
return Err(format!(
"`{}` holds {} of the {count} items it counts, so it was cut short",
items.path,
list.len()
));
}
list.iter()
.enumerate()
.map(|(k, value)| {
each(At {
value,
path: format!("{}[{k}]", items.path),
})
})
.collect()
}
}
pub fn shape(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "a list",
Value::Object(_) => "an object",
}
}
macro_rules! field {
($at:expr, $key:expr) => {
$at.key($key)?
};
}
fn spectrum(at: At) -> Read<Output> {
Ok(Output::Spectrum(Box::new(Spectrum {
frame_size: field!(at, "frame_size").count()?,
frames: field!(at, "frames").count()?,
resolution_hz: field!(at, "resolution_hz").num()?,
rms: field!(at, "rms").num()?,
centroid_hz: field!(at, "centroid_hz").num()?,
rolloff85_hz: field!(at, "rolloff85_hz").num()?,
peaks: field!(at, "peaks").items(|p| {
Ok(Peak {
hz: field!(p, "hz").num()?,
db: field!(p, "db").num()?,
})
})?,
bands: field!(at, "bands").items(|b| {
Ok(Band {
lo_hz: field!(b, "lo_hz").num()?,
hi_hz: field!(b, "hi_hz").num()?,
db: field!(b, "db").num()?,
})
})?,
})))
}
fn envelope(at: At) -> Read<Output> {
Ok(Output::Envelope(at.items(|f| {
Ok(EnvelopeFrame {
t_secs: field!(f, "t").num()?,
rms: field!(f, "rms").num()?,
peak: field!(f, "peak").num()?,
})
})?))
}
fn pitch(at: At) -> Read<Output> {
Ok(Output::Pitch(at.items(|f| {
Ok(PitchFrame {
t_secs: field!(f, "t").num()?,
notes: field!(f, "notes").items(|n| {
Ok(Note {
hz: field!(n, "hz").num()?,
name: field!(n, "note").text()?,
cents: field!(n, "cents").num()?,
db: field!(n, "db").num()?,
harmonic_of: field!(n, "harmonic_of").maybe()?,
n: 0,
})
})?,
})
})?))
}
fn floor(at: At) -> Read<BandFloor> {
Ok(BandFloor {
peak: field!(at, "peak").num()?,
time_to_peak_secs: field!(at, "time_to_peak_secs").num()?,
rise_10_90_secs: field!(at, "rise_10_90_secs").maybe()?,
})
}
fn bands(at: At) -> Read<Output> {
Ok(Output::Bands(Box::new(Bands {
rate_hz: field!(at, "rate_hz").num()?,
start_secs: field!(at, "start_secs").num()?,
bands: field!(at, "bands").items(|b| {
Ok(BandTrack {
centre_hz: field!(b, "centre_hz").num()?,
cam: field!(b, "cam").num()?,
erb_hz: field!(b, "erb_hz").num()?,
q: field!(b, "q").num()?,
peak: field!(b, "peak").num()?,
time_to_peak_secs: field!(b, "time_to_peak_secs").maybe()?,
rise_10_90_secs: field!(b, "rise_10_90_secs").maybe()?,
floor: floor(field!(b, "floor"))?,
rms: field!(b, "rms").items(|v| v.num())?,
})
})?,
})))
}
fn loudness(at: At) -> Read<Output> {
let frames = |at: At| {
at.items(|f| {
Ok(LoudnessFrame {
t: field!(f, "t").num()?,
lufs: field!(f, "lufs").num()?,
})
})
};
Ok(Output::Loudness(Box::new(Loudness {
integrated_lufs: field!(at, "integrated_lufs").maybe()?,
range_lu: field!(at, "range_lu").maybe()?,
momentary_max_lufs: field!(at, "momentary_max_lufs").maybe()?,
short_term_max_lufs: field!(at, "short_term_max_lufs").maybe()?,
sample_peak: field!(at, "sample_peak").num()?,
sample_peak_dbfs: field!(at, "sample_peak_dbfs").maybe()?,
peak_note: PEAK_NOTE,
momentary_secs: field!(at, "momentary_secs").num()?,
hop_secs: field!(at, "hop_secs").num()?,
momentary: frames(field!(at, "momentary"))?,
short_term: frames(field!(at, "short_term"))?,
})))
}
fn crest(at: At) -> Read<Output> {
Ok(Output::Crest(Box::new(Crest {
broadband_crest_db: field!(at, "broadband_crest_db").num()?,
spread_db: field!(at, "spread_db").maybe()?,
widest_band_hz: field!(at, "widest_band_hz").maybe()?,
tightest_band_hz: field!(at, "tightest_band_hz").maybe()?,
counted_under_db: field!(at, "counted_under_db").num()?,
bands: field!(at, "bands").items(|b| {
Ok(BandCrest {
lo_hz: field!(b, "lo_hz").num()?,
hi_hz: field!(b, "hi_hz").num()?,
centre_hz: field!(b, "centre_hz").num()?,
peak: field!(b, "peak").num()?,
rms: field!(b, "rms").num()?,
crest_db: field!(b, "crest_db").num()?,
counted: field!(b, "counted").flag()?,
})
})?,
})))
}
fn score(at: At) -> Read<Output> {
let voice = |v: At| {
Ok(Voiced {
hz: field!(v, "hz").num()?,
tone: field!(v, "tone").keyed(&[Tone::Tonal, Tone::Noisy], Tone::key)?,
new: field!(v, "new").flag()?,
share: field!(v, "share").num()?,
db: field!(v, "db").num()?,
bend_cents: field!(v, "bend_cents").maybe()?,
coherence: field!(v, "coherence").maybe()?,
partials: field!(v, "partials").items(|p| {
Ok(Partial {
hz: field!(p, "hz").num()?,
db: field!(p, "db").num()?,
harmonic: field!(p, "harmonic").count()?,
})
})?,
})
};
let event = |e: At| {
let hit = field!(e, "hit");
let period = match (
field!(e, "period_hz").maybe()?,
field!(e, "periodicity").maybe()?,
) {
(Some(hz), Some(clarity)) => Some((hz, clarity)),
_ => None,
};
let main = field!(e, "main");
Ok(Event {
at_secs: field!(e, "at_secs").num()?,
end_secs: field!(e, "end_secs").num()?,
next_secs: field!(e, "next_secs").num()?,
kind: field!(e, "kind").keyed(&[Kind::New, Kind::Restrike, Kind::Held], Kind::key)?,
cue: field!(e, "cue")
.keyed(&[Cue::Attack, Cue::Partial, Cue::Grid, Cue::Crop], Cue::key)?,
sound: field!(e, "sound").keyed(
&[Sound::Tonal, Sound::NoisyPitched, Sound::Noisy],
Sound::key,
)?,
hit: match hit.value {
Value::Null => None,
_ => Some(hit.keyed(&[Hit::Kick, Hit::Snare, Hit::Hat], Hit::key)?),
},
attack_secs: field!(e, "attack_secs").num()?,
level_db: field!(e, "level_db").num()?,
period,
main: match main.value {
Value::Null => None,
_ => Some(main.count()?),
},
voices: field!(e, "voices").items(voice)?,
})
};
Ok(Output::Score(Box::new(Score {
resolution_secs: field!(at, "resolution_secs").num()?,
events: field!(at, "events").items(event)?,
})))
}
fn loudness_pattern(at: At) -> Read<Output> {
Ok(Output::LoudnessPattern(Box::new(LoudnessPattern {
frame_secs: field!(at, "frame_secs").num()?,
window_secs: field!(at, "window_secs").num()?,
spl_db: field!(at, "spl_db").num()?,
centres_hz: field!(at, "centres_hz").numbers()?,
threshold_sone: field!(at, "threshold_sone").num()?,
frames: field!(at, "frames").items(|f| {
Ok(PatternFrame {
t_secs: field!(f, "t_secs").num()?,
total_sone: field!(f, "total_sone").num()?,
specific: field!(f, "specific").numbers()?,
})
})?,
})))
}
fn axes(at: &At) -> Read<Axes> {
Ok(Axes {
sharpness_acum: field!(at, "sharpness_acum").maybe()?,
decay_phon_per_sec: field!(at, "decay_phon_per_sec").maybe()?,
beating_phon: field!(at, "beating_phon").maybe()?,
attack_secs: field!(at, "attack_secs").maybe()?,
})
}
fn panel(at: At) -> Read<Output> {
Ok(Output::Panel(Box::new(Panel {
spl_db: field!(at, "spl_db").num()?,
whole: axes(&field!(at, "whole"))?,
notes: field!(at, "notes").items(|n| {
Ok(PanelNote {
at_secs: field!(n, "at_secs").num()?,
length_secs: field!(n, "length_secs").num()?,
pitches_hz: field!(n, "pitches_hz").numbers()?,
axes: axes(&n)?,
})
})?,
})))
}
pub fn reading(representation: Representation, value: &Value, path: &str) -> Read<Output> {
let at = At {
value,
path: path.to_string(),
};
match representation {
Representation::Spectrum { .. } => spectrum(at),
Representation::Envelope { .. } => envelope(at),
Representation::Pitch { .. } => pitch(at),
Representation::Bands => bands(at),
Representation::Loudness { .. } => loudness(at),
Representation::Crest => crest(at),
Representation::Score => score(at),
Representation::LoudnessPattern { .. } => loudness_pattern(at),
Representation::Panel { .. } => panel(at),
other => Err(format!("`{other:?}` has no JSON reader")),
}
}