1use std::path::Path;
4
5use sva_engine::{
6 Alias, AliasBand, Answer, Arguments, BandCrest, BandTrack, Bands, Binding, Buffer, CacheStats,
7 Counters, Crest, CuttingBelowSilenceThreshold, Detail, EnvelopeFrame, FormantFrame, Label,
8 LedgerEntry, Loudness, LoudnessFrame, Onsets, Outcome, Output, PayloadKind, Source,
9 SpectralSum, Spectrum, StereoFrame, StereoImage, Work,
10};
11
12use sva_engine::harmony::Framing;
13use sva_engine::harmony::chords::{self, Chord};
14use sva_engine::harmony::dissonance::{self, Dissonance, Extreme};
15use sva_engine::harmony::inharmonicity::{self, Inharmonicity, Series, Weighted};
16use sva_engine::harmony::tonnetz::{self, Quality, TonalDistance, Triad};
17use sva_engine::harmony::voice_leading::{self, VoiceLeading};
18use sva_engine::modulation::{self, Modulation};
19use sva_engine::periodicity::{self, Periodicity, Track, Window};
20use sva_engine::pitch as pitch_names;
21use sva_engine::self_similarity::{self, SelfSimilarity};
22use sva_engine::sliding::Stats;
23
24use crate::json::{NONE, capped, escape, latest, list, num};
25
26pub const SAMPLE_LIMIT: usize = 4096;
28
29fn maybe(v: Option<f64>) -> String {
30 v.map(num).unwrap_or_else(|| NONE.to_string())
31}
32
33fn counted(v: Option<usize>) -> String {
34 v.map_or_else(|| NONE.to_string(), |c| c.to_string())
35}
36
37fn ledger_json(entries: &[LedgerEntry], skim: bool) -> String {
38 list(entries, |e| {
39 let clipped = e
40 .clipped
41 .map_or_else(|| NONE.to_string(), |c| c.to_string());
42 let channel = counted(e.channel);
43 if skim {
44 return format!(
45 "\n {{ \"node\": \"{}\", \"channel\": {channel}, \"rms\": {}, \"peak\": {}, \"clipped\": {clipped} }}",
46 escape(&e.node),
47 num(e.rms),
48 num(e.peak)
49 );
50 }
51 format!(
52 "\n {{ \"node\": \"{}\", \"channel\": {channel}, \"depth\": {}, \"unit\": \"{}\", \
53 \"control\": {}, \"rms\": {}, \"peak\": {}, \"min\": {}, \"max\": {}, \
54 \"share\": {}, \"clipped\": {clipped} }}",
55 escape(&e.node),
56 e.depth,
57 e.kind.unit().name(),
58 !e.kind.is_audio(),
59 num(e.rms),
60 num(e.peak),
61 num(e.min),
62 num(e.max),
63 maybe(e.share)
64 )
65 })
66}
67
68fn spectrum_json(s: &Spectrum) -> String {
69 format!(
70 "{{ \"frame_size\": {}, \"frames\": {}, \"resolution_hz\": {}, \"rms\": {}, \
71 \"centroid_hz\": {}, \"rolloff85_hz\": {}, \"peaks\": {}, \"bands\": {} }}",
72 s.frame_size,
73 s.frames,
74 num(s.resolution_hz),
75 num(s.rms),
76 num(s.centroid_hz),
77 num(s.rolloff85_hz),
78 list(&s.peaks, |p| format!(
79 "{{ \"hz\": {}, \"db\": {} }}",
80 num(p.hz),
81 num(p.db)
82 )),
83 list(&s.bands, |b| format!(
84 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"db\": {} }}",
85 num(b.lo_hz),
86 num(b.hi_hz),
87 num(b.db)
88 ))
89 )
90}
91
92fn stereo_json(image: &StereoImage) -> String {
93 let frame = |f: &StereoFrame| {
94 format!(
95 "\n {{ \"t\": {}, \"correlation\": {}, \"mid_rms\": {}, \"side_rms\": {}, \
96 \"width\": {}, \"balance_db\": {}, \"mono_db\": {} }}",
97 num(f.t_secs),
98 num(f.correlation),
99 num(f.mid_rms),
100 num(f.side_rms),
101 num(f.width),
102 num(f.balance_db),
103 num(f.mono_db)
104 )
105 };
106 format!(
107 "{{ \"channels\": {}, \"overall\": {}, \"frames\": {} }}",
108 image.channels,
109 frame(&image.overall),
110 list(&image.frames, frame)
111 )
112}
113
114fn formants_json(f: &FormantFrame) -> String {
115 format!(
116 "\n {{ \"t\": {}, \"order\": {}, \"energy\": {}, \"residual\": {}, \"formants\": {} }}",
117 num(f.t_secs),
118 f.order,
119 num(f.energy),
120 num(f.residual),
121 list(&f.formants, |v| format!(
122 "{{ \"hz\": {}, \"bandwidth_hz\": {}, \"db\": {} }}",
123 num(v.hz),
124 num(v.bandwidth_hz),
125 num(v.db)
126 ))
127 )
128}
129
130fn band_json(b: &BandTrack, start_secs: f64, rate_hz: f64, limit: Option<usize>) -> String {
131 let shown = limit.map_or(b.rms.len(), |cap| b.rms.len().min(cap));
132 format!(
133 "\n {{ \"centre_hz\": {}, \"cam\": {}, \"erb_hz\": {}, \"q\": {}, \"peak\": {}, \
134 \"time_to_peak_secs\": {}, \"rise_10_90_secs\": {}, \
135 \"floor\": {{ \"peak\": {}, \"time_to_peak_secs\": {}, \"rise_10_90_secs\": {} }}, \
136 \"rms\": {} }}",
137 num(b.centre_hz),
138 num(b.cam),
139 num(b.erb_hz),
140 num(b.q),
141 num(b.peak),
142 maybe(b.time_to_peak_secs),
143 maybe(b.rise_10_90_secs),
144 num(b.floor.peak),
145 num(b.floor.time_to_peak_secs),
146 maybe(b.floor.rise_10_90_secs),
147 capped(
148 &b.rms,
149 shown,
150 |n| start_secs + n as f64 / rate_hz,
151 |v| num(*v),
152 )
153 )
154}
155
156fn bands_json(b: &Bands, limit: Option<usize>) -> String {
157 format!(
158 "{{ \"rate_hz\": {}, \"start_secs\": {}, \"bands\": {} }}",
159 num(b.rate_hz),
160 num(b.start_secs),
161 list(&b.bands, |band| band_json(
162 band,
163 b.start_secs,
164 b.rate_hz,
165 limit
166 ))
167 )
168}
169
170fn loudness_json(l: &Loudness, limit: Option<usize>) -> String {
171 let frames = |fs: &[LoudnessFrame]| {
172 let shown = limit.map_or(fs.len(), |cap| fs.len().min(cap));
173 capped(
174 fs,
175 shown,
176 |n| fs[n].t,
177 |f| format!("\n {{ \"t\": {}, \"lufs\": {} }}", num(f.t), num(f.lufs)),
178 )
179 };
180 format!(
181 "{{ \"integrated_lufs\": {}, \"range_lu\": {}, \"momentary_max_lufs\": {}, \
182 \"short_term_max_lufs\": {}, \"sample_peak\": {}, \"sample_peak_dbfs\": {}, \
183 \"peak_note\": \"{}\", \"momentary\": {}, \"short_term\": {} }}",
184 maybe(l.integrated_lufs),
185 maybe(l.range_lu),
186 maybe(l.momentary_max_lufs),
187 maybe(l.short_term_max_lufs),
188 num(l.sample_peak),
189 maybe(l.sample_peak_dbfs),
190 escape(l.peak_note),
191 frames(&l.momentary),
192 frames(&l.short_term)
193 )
194}
195
196fn crest_json(c: &Crest) -> String {
197 let band = |b: &BandCrest| {
198 format!(
199 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"centre_hz\": {}, \"peak\": {}, \
200 \"rms\": {}, \"crest_db\": {}, \"counted\": {} }}",
201 num(b.lo_hz),
202 num(b.hi_hz),
203 num(b.centre_hz),
204 num(b.peak),
205 num(b.rms),
206 num(b.crest_db),
207 b.counted
208 )
209 };
210 format!(
211 "{{ \"broadband_crest_db\": {}, \"spread_db\": {}, \"widest_band_hz\": {}, \
212 \"tightest_band_hz\": {}, \"counted_under_db\": {}, \"bands\": {} }}",
213 num(c.broadband_crest_db),
214 maybe(c.spread_db),
215 maybe(c.widest_band_hz),
216 maybe(c.tightest_band_hz),
217 num(c.counted_under_db),
218 list(&c.bands, band)
219 )
220}
221
222fn onsets_json(o: &Onsets) -> String {
223 format!(
224 "{{ \"onsets\": {}, \"resolution_secs\": {}, \"ioi_histogram\": {} }}",
225 list(&o.onsets, |t| format!(
226 "{{ \"t_secs\": {}, \"strength\": {} }}",
227 num(t.t_secs),
228 num(t.strength)
229 )),
230 num(o.resolution_secs),
231 list(&o.ioi_histogram, |b| format!(
232 "{{ \"lo_secs\": {}, \"hi_secs\": {}, \"count\": {} }}",
233 num(b.lo_secs),
234 num(b.hi_secs),
235 b.count
236 )),
237 )
238}
239
240fn alias_json(a: &Alias) -> String {
241 let band = |b: &AliasBand| {
242 format!(
243 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"signal_db\": {}, \"alias_db\": {}, \
244 \"nmr_db\": {} }}",
245 num(b.lo_hz),
246 num(b.hi_hz),
247 num(b.signal_db),
248 num(b.alias_db),
249 num(b.nmr_db)
250 )
251 };
252 format!(
253 "{{ \"oversample\": {}, \"sample_rate\": {}, \"frame_size\": {}, \"frames\": {}, \
254 \"scored_frames\": {}, \"playback_db_spl\": {}, \"asr_db\": {}, \"nmr_db\": {}, \
255 \"nmr_peak_db\": {}, \"peak_at_secs\": {}, \"audible\": {}, \
256 \"instances\": {}, \"bands\": {} }}",
257 a.oversample,
258 num(a.sample_rate),
259 a.frame_size,
260 a.frames,
261 a.scored_frames,
262 num(a.playback_db_spl),
263 num(a.asr_db),
264 num(a.nmr_db),
265 num(a.nmr_peak_db),
266 num(a.peak_at_secs),
267 a.audible,
268 a.instances,
269 list(&a.bands, band)
270 )
271}
272
273fn framing_json(framing: &Framing, floor_db: f64) -> String {
276 let window = framing.window.map_or(NONE.to_string(), |w| {
277 format!(
278 "{{ \"size\": {}, \"secs\": {}, \"resolution_hz\": {} }}",
279 w.size,
280 num(w.secs),
281 num(w.resolution_hz)
282 )
283 });
284 format!(
285 "\"partials\": \"{}\", \"floor_db\": {}, \"hop_secs\": {}, \"window\": {window}",
286 escape(framing.method()),
287 num(floor_db),
288 num(framing.hop_secs)
289 )
290}
291
292fn extreme_json(e: Option<Extreme>) -> String {
293 e.map_or(NONE.to_string(), |e| {
294 format!(
295 "{{ \"value\": {}, \"t\": {} }}",
296 num(e.value),
297 num(e.t_secs)
298 )
299 })
300}
301
302fn dissonance_json(d: &Dissonance) -> String {
303 format!(
304 "{{ \"model\": \"{}\", \"constants\": {{ \"a\": {}, \"b\": {}, \"d_star\": {}, \
305 \"s1\": {}, \"s2\": {} }}, \"amplitude\": \"product\", {}, \"mean\": {}, \
306 \"max\": {}, \"min\": {}, \"curve\": {} }}",
307 escape(dissonance::MODEL),
308 num(dissonance::A),
309 num(dissonance::B),
310 num(dissonance::D_STAR),
311 num(dissonance::S1),
312 num(dissonance::S2),
313 framing_json(&d.framing, d.floor_db),
314 maybe(d.mean),
315 extreme_json(d.max),
316 extreme_json(d.min),
317 list(&d.frames, |f| format!(
318 "\n {{ \"t\": {}, \"roughness\": {}, \"roughness_bounds\": [{}, {}], \
319 \"partials\": {} }}",
320 num(f.t_secs),
321 num(f.roughness),
322 num(f.roughness_bounds.0),
323 num(f.roughness_bounds.1),
324 f.partials
325 ))
326 )
327}
328
329fn notes_json(notes: &[i64]) -> String {
330 list(notes, |n| format!("\"{}\"", pitch_names::note_name(*n)))
331}
332
333fn chord_fields(c: &Chord) -> String {
334 format!(
335 "\"start_secs\": {}, \"end_secs\": {}, \"notes\": {}, \"unresolved\": {}",
336 num(c.start_secs),
337 num(c.end_secs),
338 notes_json(&c.notes),
339 notes_json(&c.unresolved)
340 )
341}
342
343fn voice_leading_json(v: &VoiceLeading) -> String {
344 format!(
345 "{{ \"model\": \"{}\", \"norm\": \"{}\", \"chords_by\": \"{}\", {}, \
346 \"total\": {}, \"chords\": {}, \"transitions\": {} }}",
347 escape(voice_leading::MODEL),
348 v.norm.key(),
349 escape(chords::METHOD),
350 framing_json(&v.framing, v.floor_db),
351 num(v.total),
352 list(&v.chords, |c| format!("\n {{ {} }}", chord_fields(c))),
353 list(&v.transitions, |t| format!(
354 "\n {{ \"t\": {}, \"from\": {}, \"to\": {}, \"distance\": {}, \
355 \"mapping\": {} }}",
356 num(t.at_secs),
357 notes_json(&t.from),
358 notes_json(&t.to),
359 num(t.distance),
360 list(&t.mapping, |(from, to)| format!(
361 "{{ \"from\": \"{}\", \"to\": \"{}\" }}",
362 pitch_names::note_name(*from),
363 pitch_names::note_name(*to)
364 ))
365 ))
366 )
367}
368
369fn triad_json(t: Option<Triad>) -> String {
370 t.map_or(NONE.to_string(), |t| {
371 format!(
372 "{{ \"root\": \"{}\", \"quality\": \"{}\" }}",
373 pitch_names::class_name(i64::from(t.root)),
374 match t.quality {
375 Quality::Major => "major",
376 Quality::Minor => "minor",
377 }
378 )
379 })
380}
381
382fn tonal_distance_json(d: &TonalDistance) -> String {
383 let tonic = d.tonic.map_or(NONE.to_string(), |(triad, t)| {
384 format!(
385 "{{ \"triad\": {}, \"t\": {} }}",
386 triad_json(Some(triad)),
387 num(t)
388 )
389 });
390 format!(
391 "{{ \"model\": \"{}\", \"chords_by\": \"{}\", {}, \"tonic\": {tonic}, \
392 \"chords\": {}, \"transitions\": {} }}",
393 escape(tonnetz::MODEL),
394 escape(chords::METHOD),
395 framing_json(&d.framing, d.floor_db),
396 list(&d.chords, |c| format!(
397 "\n {{ {}, \"triad\": {}, \"tension\": {} }}",
398 chord_fields(&c.chord),
399 triad_json(c.triad),
400 counted(c.tension)
401 )),
402 list(&d.transitions, |t| format!(
403 "\n {{ \"t\": {}, \"from\": {}, \"to\": {}, \"distance\": {}, \
404 \"path\": {} }}",
405 num(t.at_secs),
406 triad_json(t.from),
407 triad_json(t.to),
408 counted(t.path.as_ref().map(Vec::len)),
409 t.path.as_ref().map_or(NONE.to_string(), |p| format!(
410 "\"{}\"",
411 p.iter().map(|s| s.letter()).collect::<String>()
412 ))
413 ))
414 )
415}
416
417fn weighted_json(w: Option<Weighted>) -> String {
418 w.map_or(NONE.to_string(), |w| {
419 format!(
420 "{{ \"value\": {}, \"bounds\": [{}, {}] }}",
421 num(w.value),
422 num(w.bounds.0),
423 num(w.bounds.1)
424 )
425 })
426}
427
428fn bounds_json(b: Option<(f64, f64)>) -> String {
429 b.map_or(NONE.to_string(), |(lo, hi)| {
430 format!("[{}, {}]", num(lo), num(hi))
431 })
432}
433
434fn series_json(s: &Series) -> String {
435 let fit = s.fit.map_or(NONE.to_string(), |f| {
436 format!(
437 "{{ \"b\": {}, \"b_bounds\": {}, \"f0\": {}, \"f0_bounds\": {} }}",
438 num(f.b),
439 bounds_json(f.b_bounds),
440 num(f.f0_hz),
441 bounds_json(f.f0_bounds)
442 )
443 });
444 format!(
445 "{{ \"f1\": {}, \"cents\": {}, \"fit\": {fit}, \"members\": {} }}",
446 num(s.members[0].partial.hz),
447 weighted_json(s.cents),
448 list(&s.members, |m| format!(
449 "{{ \"n\": {}, \"hz\": {}, \"hz_bounds\": {}, \"amplitude\": {}, \
450 \"amplitude_bounds\": {}, \"cents\": {}, \"cents_bounds\": {} }}",
451 m.n,
452 num(m.partial.hz),
453 bounds_json(Some(m.partial.hz_bounds)),
454 num(m.partial.amplitude),
455 bounds_json(Some(m.partial.amplitude_bounds)),
456 num(m.cents),
457 bounds_json(Some(m.cents_bounds))
458 ))
459 )
460}
461
462fn inharmonicity_json(r: &Inharmonicity) -> String {
463 format!(
464 "{{ \"model\": \"{}\", {}, \"cents_mean\": {}, \"cents_max\": {}, \
465 \"unexplained_mean\": {}, \"curve\": {} }}",
466 escape(inharmonicity::MODEL),
467 framing_json(&r.framing, r.floor_db),
468 maybe(r.cents_mean),
469 extreme_json(r.cents_max),
470 maybe(r.unexplained_mean),
471 list(&r.frames, |f| format!(
472 "\n {{ \"t\": {}, \"cents\": {}, \"unexplained\": {}, \"series\": {} }}",
473 num(f.t_secs),
474 weighted_json(f.cents),
475 weighted_json(f.unexplained),
476 list(&f.series, series_json)
477 ))
478 )
479}
480
481fn spread_json(s: Option<Stats>) -> String {
482 s.map_or(NONE.to_string(), |s| {
483 format!(
484 "{{ \"mean\": {}, \"min\": {}, \"max\": {} }}",
485 num(s.mean),
486 extreme_json(Some(s.min)),
487 extreme_json(Some(s.max))
488 )
489 })
490}
491
492fn periodicity_window_json(w: &Window) -> String {
495 let curve = w.curve.as_ref().map_or(String::new(), |c| {
496 let heights: Vec<String> = c.iter().map(|h| num(*h)).collect();
497 format!(", \"curve\": [{}]", heights.join(", "))
498 });
499 format!(
500 "\n {{ \"t\": {}, \"from\": {}, \"to\": {}, \"drift_bpm\": {}, \"peaks\": {}{curve} }}",
501 num(w.t_secs),
502 num(w.from_secs),
503 num(w.to_secs),
504 maybe(w.drift_bpm),
505 list(&w.peaks, |p| format!(
506 "{{ \"period_secs\": {}, \"period_bounds_secs\": {}, \"bpm\": {}, \
507 \"bpm_bounds\": {}, \"height\": {} }}",
508 num(p.period_secs),
509 bounds_json(p.period_bounds_secs),
510 num(p.bpm),
511 bounds_json(p.bpm_bounds),
512 num(p.height)
513 ))
514 )
515}
516
517fn periodicity_track_json(t: &Track) -> String {
518 let centres: Vec<String> = t.centres_hz.iter().map(|hz| num(*hz)).collect();
519 format!(
520 "\n {{ \"group\": \"{}\", \"centres_hz\": [{}], \"summary\": {{ \
521 \"windows_with_peak\": {}, \"bpm\": {}, \"height\": {}, \"drift_bpm\": {} }}, \
522 \"windows\": {} }}",
523 t.group.key(),
524 centres.join(", "),
525 t.summary.windows_with_peak,
526 spread_json(t.summary.bpm),
527 spread_json(t.summary.height),
528 spread_json(t.summary.drift_bpm),
529 list(&t.windows, periodicity_window_json)
530 )
531}
532
533fn periodicity_json(p: &Periodicity) -> String {
534 format!(
535 "{{ \"model\": \"{}\", \"hop_secs\": {}, \"window_secs\": {}, \"envelope_hz\": {}, \
536 \"lag_step_secs\": {}, \"min_period_secs\": {}, \"max_period_secs\": {}, \
537 \"split_hz\": {}, \"smoothing\": {{ \"hz\": {}, \"poles\": {} }}, \"tracks\": {} }}",
538 escape(periodicity::MODEL),
539 num(p.hop_secs),
540 num(p.window_secs),
541 num(p.envelope_hz),
542 num(p.lag_step_secs),
543 num(p.min_period_secs),
544 num(p.max_period_secs),
545 num(periodicity::SPLIT_HZ),
546 maybe(periodicity::SMOOTHING.hz),
547 periodicity::SMOOTHING.poles,
548 list(&p.tracks, periodicity_track_json)
549 )
550}
551
552fn modulation_peak_json(p: &modulation::Peak, depth_key: &str) -> String {
555 let beats = p.period_beats.map_or(String::new(), |b| {
556 format!(
557 ", \"period_beats\": {}, \"period_beats_bounds\": {}",
558 num(b),
559 bounds_json(p.period_beats_bounds)
560 )
561 });
562 format!(
563 "{{ \"hz\": {}, \"hz_bounds\": {}{beats}, \"{depth_key}\": {}, \
564 \"{depth_key}_bounds\": {} }}",
565 num(p.hz),
566 bounds_json(p.hz_bounds),
567 num(p.depth),
568 bounds_json(p.depth_bounds)
569 )
570}
571
572fn modulation_window_json(w: &modulation::Window) -> String {
573 let centroid = w.centroid.as_ref().map_or(NONE.to_string(), |c| {
574 format!(
575 "{{ \"mean_hz\": {}, \"peaks\": {} }}",
576 num(c.mean_hz),
577 list(&c.peaks, |p| modulation_peak_json(p, "depth_hz"))
578 )
579 });
580 format!(
581 "\n {{ \"t\": {}, \"from\": {}, \"to\": {}, \"level\": {}, \"peaks\": {}, \
582 \"centroid\": {centroid} }}",
583 num(w.t_secs),
584 num(w.from_secs),
585 num(w.to_secs),
586 num(w.level),
587 list(&w.peaks, |p| modulation_peak_json(p, "depth"))
588 )
589}
590
591fn modulation_track_json(t: &modulation::Track) -> String {
592 let centres: Vec<String> = t.centres_hz.iter().map(|hz| num(*hz)).collect();
593 format!(
594 "\n {{ \"group\": \"{}\", \"centres_hz\": [{}], \"summary\": {{ \
595 \"windows_with_peak\": {}, \"hz\": {}, \"depth\": {}, \"centroid_hz\": {}, \
596 \"centroid_depth_hz\": {} }}, \"windows\": {} }}",
597 t.group.key(),
598 centres.join(", "),
599 t.summary.windows_with_peak,
600 spread_json(t.summary.hz),
601 spread_json(t.summary.depth),
602 spread_json(t.summary.centroid_hz),
603 spread_json(t.summary.centroid_depth_hz),
604 list(&t.windows, modulation_window_json)
605 )
606}
607
608fn modulation_json(m: &Modulation) -> String {
609 let splits: Vec<String> = modulation::SPLITS_HZ.iter().map(|hz| num(*hz)).collect();
610 format!(
611 "{{ \"model\": \"{}\", \"hop_secs\": {}, \"window_secs\": {}, \"envelope_hz\": {}, \
612 \"bin_step_hz\": {}, \"resolution_hz\": {}, \"min_hz\": {}, \"max_hz\": {}, \
613 \"bpm\": {}, \"splits_hz\": [{}], \"smoothing\": {{ \"hz\": {}, \"poles\": {} }}, \
614 \"passband_gain_at_max_hz\": {}, \"tracks\": {} }}",
615 escape(modulation::MODEL),
616 num(m.hop_secs),
617 num(m.window_secs),
618 num(m.envelope_hz),
619 num(m.bin_step_hz),
620 num(m.resolution_hz),
621 num(m.min_hz),
622 num(m.max_hz),
623 maybe(m.bpm),
624 splits.join(", "),
625 maybe(modulation::SMOOTHING.hz),
626 modulation::SMOOTHING.poles,
627 num(m.passband_gain_at_max_hz),
628 list(&m.tracks, modulation_track_json)
629 )
630}
631
632fn span_json(span: (f64, f64)) -> String {
633 format!("{{ \"from\": {}, \"to\": {} }}", num(span.0), num(span.1))
634}
635
636fn numbers(values: &[f64]) -> String {
637 let held: Vec<String> = values.iter().map(|v| num(*v)).collect();
638 format!("[{}]", held.join(", "))
639}
640
641fn self_similarity_cell_json(c: &self_similarity::Cell) -> String {
642 format!(
643 "\n {{ \"t\": {}, \"from\": {}, \"to\": {}, \"chroma\": {}, \"levels_db\": {} }}",
644 num(c.t_secs),
645 num(c.from_secs),
646 num(c.to_secs),
647 numbers(&c.chroma),
648 numbers(&c.levels_db)
649 )
650}
651
652fn self_similarity_view_json(v: &self_similarity::Reading, from_secs: f64, step: f64) -> String {
654 let matrix = v.matrix.as_ref().map_or(String::new(), |rows| {
655 let rows: Vec<String> = rows
656 .iter()
657 .map(|r| format!("\n {}", numbers(r)))
658 .collect();
659 format!(", \"matrix\": [{}]", rows.join(","))
660 });
661 format!(
662 "\n {{ \"view\": \"{}\", \"stripes_found\": {}, \"stripes\": {}, \
663 \"novelty\": {{ \"from_secs\": {}, \"step_secs\": {}, \"values\": {} }}, \
664 \"peaks_found\": {}, \"peaks\": {}{matrix} }}",
665 v.view.key(),
666 v.stripes_found,
667 list(&v.stripes, |s| format!(
668 "\n {{ \"shift_semitones\": {}, \"lag_secs\": {}, \"earlier\": {}, \
669 \"later\": {}, \"cells\": {}, \"similarity_mean\": {}, \"similarity_min\": {} }}",
670 s.shift_semitones,
671 num(s.lag_secs),
672 span_json(s.earlier),
673 span_json(s.later),
674 s.cells,
675 num(s.similarity_mean),
676 num(s.similarity_min)
677 )),
678 num(from_secs),
679 num(step),
680 numbers(&v.novelty),
681 v.peaks_found,
682 list(&v.peaks, |p| format!(
683 "{{ \"t\": {}, \"novelty\": {} }}",
684 num(p.t_secs),
685 num(p.novelty)
686 ))
687 )
688}
689
690fn self_similarity_json(s: &SelfSimilarity) -> String {
692 let cells = s.cells.as_ref().map_or(String::new(), |c| {
693 format!(", \"cells\": {}", list(c, self_similarity_cell_json))
694 });
695 format!(
696 "{{ \"model\": \"{}\", \"hop_secs\": {}, \"frame_window_secs\": {}, \
697 \"resolution_hz\": {}, \"cell_secs\": {}, \"frames_per_cell\": {}, \
698 \"cell_count\": {}, \"max_cells\": {}, \"smooth_cells\": {}, \"kernel_cells\": {}, \
699 \"floor_db\": {}, \"listed\": {}, \"min_hz\": {}, \"max_hz\": {}, \
700 \"centres_hz\": {}{cells}, \"views\": {} }}",
701 escape(self_similarity::MODEL),
702 num(s.hop_secs),
703 num(s.frame_window_secs),
704 num(s.resolution_hz),
705 num(s.cell_secs),
706 s.frames_per_cell,
707 s.cell_count,
708 self_similarity::MAX_CELLS,
709 s.smooth_cells,
710 s.kernel_cells,
711 num(s.floor_db),
712 s.listed,
713 num(s.min_hz),
714 num(s.max_hz),
715 numbers(&s.centres_hz),
716 list(&s.views, |v| self_similarity_view_json(
717 v,
718 s.novelty_from_secs,
719 s.cell_secs
720 ))
721 )
722}
723
724fn binding_json(b: &Binding) -> String {
725 format!(
726 "\n {{ \"name\": \"{}\", \"source\": \"{}\" }}",
727 escape(&b.name),
728 escape(&b.source)
729 )
730}
731
732fn arguments_json(a: &Arguments) -> String {
733 let at = |s: sva_ast::ByteSpan| format!("{{ \"start\": {}, \"end\": {} }}", s.start, s.end);
734 let calls = list(&a.calls, |c| {
735 let arguments = list(&c.arguments, |x| {
736 format!(
737 "{{ \"name\": \"{}\", \"value\": {}, \"written\": {} }}",
738 escape(&x.name),
739 num(x.value),
740 x.written
741 )
742 });
743 format!(
744 "\n {{ \"name\": \"{}\", \"at\": {}, \"arguments\": {arguments} }}",
745 escape(&c.name),
746 at(c.at)
747 )
748 });
749 let chosen = list(&a.chosen, |c| {
750 format!(
751 "\n {{ \"name\": \"{}\", \"at\": {}, \"operands\": {}, \"chosen\": {} }}",
752 escape(&c.name),
753 at(c.at),
754 list(&c.operands, |v| num(*v)),
755 c.chosen
756 )
757 });
758 format!(
759 "\n {{ \"node\": \"{}\", \"calls\": {calls}, \"chosen\": {chosen} }}",
760 escape(&a.node)
761 )
762}
763
764fn samples_json(b: &Buffer, limit: Option<usize>) -> String {
766 let component = |c: usize| {
767 let plane = b.plane(c);
768 let shown = limit.map_or(plane.len(), |cap| plane.len().min(cap));
769 format!(
770 "\n {{ \"channel\": {c}, \"values\": {} }}",
771 capped(
772 plane,
773 shown,
774 |n| b.origin_secs() + n as f64 / f64::from(b.rate),
775 |v| num(*v)
776 )
777 )
778 };
779 let components: Vec<usize> = (0..b.width()).collect();
780 format!(
781 "{{ \"rate\": {}, \"origin_secs\": {}, \"width\": {}, \"components\": {} }}",
782 b.rate,
783 num(b.origin_secs()),
784 b.width(),
785 list(&components, |c| component(*c))
786 )
787}
788
789fn symbolic_json(n: &SpectralSum) -> String {
790 let terms: Vec<String> = n.atoms().map(sva_engine::sketch_atom).collect();
791 format!(
792 "{{ \"var\": \"{}\", \"lanes\": {}, \"terms\": {} }}",
793 match n.var {
794 sva_engine::Var::T => "t",
795 sva_engine::Var::F => "f",
796 },
797 n.lanes.len(),
798 crate::json::strings(&terms)
799 )
800}
801
802pub fn value_json(output: &Output, limit: Option<usize>, skim: bool) -> String {
805 match output {
806 Output::Lines(lines) => list(lines, line_json),
807 Output::Atoms(atoms) => list(atoms, atom_json),
808 Output::Symbolic(sum) => symbolic_json(sum),
809 Output::Samples(buffer) => samples_json(buffer, limit),
810 Output::Ledger(entries) => ledger_json(entries, skim),
811 Output::Spectrum(s) => spectrum_json(s),
812 Output::Stereo(image) => stereo_json(image),
813 Output::Bands(b) => bands_json(b, limit),
814 Output::Loudness(l) => loudness_json(l, limit),
815 Output::Crest(c) => crest_json(c),
816 Output::Onsets(o) => onsets_json(o),
817 Output::Alias(a) => alias_json(a),
818 Output::Dissonance(d) => dissonance_json(d),
819 Output::VoiceLeading(v) => voice_leading_json(v),
820 Output::TonalDistance(d) => tonal_distance_json(d),
821 Output::Inharmonicity(r) => inharmonicity_json(r),
822 Output::Periodicity(p) => periodicity_json(p),
823 Output::Modulation(m) => modulation_json(m),
824 Output::SelfSimilarity(s) => self_similarity_json(s),
825 Output::Bindings(b) => list(b, binding_json),
826 Output::Arguments(a) => list(a, arguments_json),
827 Output::Envelope(frames) => list(frames, |f: &EnvelopeFrame| {
828 format!(
829 "\n {{ \"t\": {}, \"rms\": {}, \"peak\": {} }}",
830 num(f.t_secs),
831 num(f.rms),
832 num(f.peak)
833 )
834 }),
835 Output::Pitch(frames) => list(frames, |f| {
836 format!(
837 "\n {{ \"t\": {}, \"notes\": {} }}",
838 num(f.t_secs),
839 list(&f.notes, |n| format!(
840 "{{ \"note\": \"{}\", \"hz\": {}, \"cents\": {}, \"db\": {}, \
841 \"harmonic_of\": {} }}",
842 escape(&n.name),
843 num(n.hz),
844 num(n.cents),
845 num(n.db),
846 maybe(n.harmonic_of)
847 ))
848 )
849 }),
850 Output::Formants(frames) => list(frames, formants_json),
851 }
852}
853
854fn atom_json(a: &sva_engine::Atom) -> String {
857 let (at, per, turns) = match a.var {
858 sva_engine::Var::T => ("secs", "sec", "hz"),
859 sva_engine::Var::F => ("hz", "hz", "secs"),
860 };
861 format!(
862 "\n {{ \"sketch\": \"{}\", \"{turns}\": {}, \"re\": {}, \"im\": {}, \"db\": {}, \
863 \"decay_per_{per}\": {}, \"decay_from_{at}\": {}, \"degree\": {}, \
864 \"polynomial_at_{at}\": {}, \"start_{at}\": {}, \"end_{at}\": {} }}",
865 escape(&a.sketch),
866 num(a.turns),
867 num(a.amp.re),
868 num(a.amp.im),
869 num(20.0 * a.amp.abs().log10()),
870 num(a.decay),
871 num(a.decay_from),
872 a.degree,
873 num(a.polynomial_at),
874 maybe(a.start),
875 maybe(a.end)
876 )
877}
878
879fn line_json(l: &sva_engine::Line) -> String {
880 format!(
881 "\n {{ \"hz\": {}, \"re\": {}, \"im\": {}, \"db\": {} }}",
882 num(l.hz),
883 num(l.amp.re),
884 num(l.amp.im),
885 num(20.0 * l.amp.abs().log10())
886 )
887}
888
889pub fn answer_json(answer: &Answer, limit: Option<usize>, skim: bool) -> String {
893 format!(
894 "{{ \"source\": \"{}\", \"profile\": \"{}\", \"rate\": {}, \"value\": {}, \
895 \"dropped\": {}, \"tail_db\": {} }}",
896 match answer.source {
897 Source::Exact => "exact",
898 Source::Measured => "measured",
899 },
900 escape(answer.profile),
901 answer
902 .rate
903 .map_or_else(|| NONE.to_string(), |r| r.to_string()),
904 value_json(&answer.value, limit, skim),
905 list(&answer.dropped, line_json),
906 maybe(answer.tail_db)
907 )
908}
909
910fn detail_json(detail: &Detail) -> String {
912 match detail {
913 Detail::Lines {
914 summed,
915 dropped,
916 dropped_more,
917 tail_db,
918 ..
919 } => format!(
920 ", \"summed\": {summed}, \"dropped\": {}, \"dropped_more\": {dropped_more}, \
921 \"tail_db\": {}",
922 list(dropped, |d| format!(
923 "{{ \"hz\": {}, \"db\": {} }}",
924 num(d.hz),
925 num(d.db)
926 )),
927 maybe(*tail_db)
928 ),
929 Detail::Cropped { tail_db, .. } => format!(", \"tail_db\": {}", maybe(*tail_db)),
930 Detail::Point {
931 alias_db, tail_db, ..
932 } => format!(
933 ", \"alias_db\": {}, \"tail_db\": {}",
934 maybe(*alias_db),
935 maybe(*tail_db)
936 ),
937 Detail::Roundtrip { edited, .. } => format!(", \"edited\": {edited}"),
938 Detail::Continuous { .. } | Detail::Reading { .. } => String::new(),
939 Detail::Added { parts } => format!(
940 ", \"addends\": {}",
941 list(parts, |part| format!(
942 "{{ \"rule\": \"{}\"{} }}",
943 part.rule().as_str(),
944 detail_json(part)
945 ))
946 ),
947 }
948}
949
950pub fn label_json(label: &Label) -> String {
953 let detail = detail_json(&label.detail);
954 let cutting = label
955 .cutting_below_silence_threshold
956 .as_ref()
957 .map_or(NONE.to_string(), cutting_below_silence_threshold_json);
958 let rest = format!(
959 ", \"moved_s\": {}, \"pruned\": {cutting}",
960 maybe(label.moved)
961 );
962 format!(
963 "{{ \"source\": \"{}\", \"profile\": \"{}\", \"rate\": {}, \"rule\": \"{}\"{detail}{rest} }}",
964 match label.source {
965 Source::Exact => "exact",
966 Source::Measured => "measured",
967 },
968 escape(label.profile),
969 label.rate,
970 escape(label.rule().as_str())
971 )
972}
973
974pub fn cutting_below_silence_threshold_json(cutting: &CuttingBelowSilenceThreshold) -> String {
977 let cuts = list(&cutting.treated_as_silent_from_sample, |(node, from)| {
978 format!("{{ \"node\": \"{}\", \"from\": {from} }}", escape(node))
979 });
980 format!(
981 "{{ \"db\": {}, \"cuts\": {cuts} }}",
982 num(cutting.silence_threshold_dbfs)
983 )
984}
985
986pub fn work_json(work: &Work) -> String {
987 format!(
988 "{{ \"samples\": {}, \"computed_samples\": {} }}",
989 work.samples, work.computed_samples
990 )
991}
992
993pub fn stats_json(stats: &CacheStats) -> String {
997 stats_with(stats, "")
998}
999
1000pub fn stream_stats_json(
1004 stats: &CacheStats,
1005 (dropped, made): (&[&str], usize),
1006 cutting: &CuttingBelowSilenceThreshold,
1007) -> String {
1008 let dropped = latest(dropped, made, |name| format!("\"{}\"", escape(name)));
1009 let pruned = cutting_below_silence_threshold_json(cutting);
1010 stats_with(
1011 stats,
1012 &format!(", \"dropped\": {dropped}, \"pruned\": {pruned}"),
1013 )
1014}
1015
1016fn stats_with(stats: &CacheStats, extra: &str) -> String {
1017 let made = stats.shed + stats.lookups.len();
1018 let lookups = latest(&stats.lookups, made, |l| {
1019 let outcome = match l.outcome {
1020 Outcome::Hit => "hit",
1021 Outcome::ComputedStored => "computed_stored",
1022 Outcome::ComputedNotStored => "computed_not_stored",
1023 Outcome::ComputedReplaced => "computed_replaced",
1024 Outcome::Extended => "extended",
1025 Outcome::Prefix => "prefix",
1026 Outcome::Reused => "reused",
1027 };
1028 format!(
1029 "{{ \"node\": \"{}\", \"key\": \"{}\", \"kind\": \"{}\", \"outcome\": \"{outcome}\" }}",
1030 escape(&l.node),
1031 l.key,
1032 match l.kind {
1033 PayloadKind::Segments => "segments",
1034 PayloadKind::Frames => "frames",
1035 PayloadKind::Run => "run",
1036 }
1037 )
1038 });
1039 format!(
1040 "{{ \"nodes\": {}, \"hits\": {}, \"computed\": {}, \"stored\": {}, \"replaced\": {}, \"extended\": {}, \"reused\": {}, \
1041 \"bytes\": {}, \"max_bytes\": {}, \"entries\": {}, \"evictions\": {}, \
1042 \"tier\": {}, \"lookups\": {lookups}{extra} }}",
1043 stats.nodes(),
1044 stats.hits(),
1045 stats.computed(),
1046 stats.stored(),
1047 stats.replaced(),
1048 stats.extended(),
1049 stats.reused(),
1050 stats.bytes,
1051 stats.max_bytes,
1052 stats.entries,
1053 stats.evictions,
1054 counters_json(&stats.tier)
1055 )
1056}
1057
1058pub fn counters_json(counters: &Counters) -> String {
1060 format!(
1061 "{{ \"disk_lookups\": {}, \"disk_reads\": {}, \"disk_read_bytes\": {}, \
1062 \"promotions\": {}, \"writebacks\": {}, \"hits\": {}, \"evictions\": {}, \
1063 \"probation_evictions\": {}, \"protected_evictions\": {} }}",
1064 counters.disk_lookups,
1065 counters.disk_reads,
1066 counters.disk_read_bytes,
1067 counters.promotions,
1068 counters.writebacks,
1069 counters.hits,
1070 counters.evictions(),
1071 counters.probation_evictions,
1072 counters.protected_evictions
1073 )
1074}
1075
1076pub struct Printed {
1078 pub name: String,
1079 pub answer: Answer,
1080 pub skim: bool,
1082}
1083
1084pub struct Report<'a> {
1086 pub target: &'a str,
1087 pub rate: u32,
1088 pub bits: Option<i32>,
1090 pub interval: Option<(f64, f64)>,
1092 pub profile: &'a str,
1093 pub label: Option<&'a Label>,
1094 pub written: &'a [(String, &'a Path)],
1095 pub played: Option<&'a Played>,
1097 pub answers: &'a [Printed],
1098 pub limit: Option<usize>,
1099}
1100
1101#[derive(Clone, Debug, PartialEq)]
1103pub struct Played {
1104 pub representation: String,
1105 pub device: String,
1107 pub seconds: f64,
1108 pub rate: u32,
1109 pub channels: usize,
1110 pub format: &'static str,
1112 pub buffer_secs: f64,
1113 pub granted_buffer_secs: Option<f64>,
1115 pub underruns: usize,
1116 pub underruns_seen_by: &'static str,
1118}
1119
1120fn played_json(played: &Played) -> String {
1121 format!(
1122 "{{ \"representation\": \"{}\", \"device\": \"{}\", \"seconds\": {}, \
1123 \"sample_rate\": {}, \"channels\": {}, \"format\": \"{}\", \"buffer\": {{ \
1124 \"asked_secs\": {}, \"granted_secs\": {} }}, \"underruns\": {{ \"count\": {}, \
1125 \"seen_by\": \"{}\" }} }}",
1126 escape(&played.representation),
1127 escape(&played.device),
1128 num(played.seconds),
1129 played.rate,
1130 played.channels,
1131 played.format,
1132 num(played.buffer_secs),
1133 maybe(played.granted_buffer_secs),
1134 played.underruns,
1135 escape(played.underruns_seen_by),
1136 )
1137}
1138
1139pub fn query_data(report: &Report) -> String {
1141 let written = list(report.written, |(name, path)| {
1142 format!(
1143 "{{ \"representation\": \"{}\", \"path\": \"{}\" }}",
1144 escape(name),
1145 escape(&path.display().to_string())
1146 )
1147 });
1148 let label = report
1149 .label
1150 .map(label_json)
1151 .unwrap_or_else(|| NONE.to_string());
1152 let reads = report
1153 .answers
1154 .iter()
1155 .map(|printed| {
1156 format!(
1157 "\"{}\": {}",
1158 escape(&printed.name),
1159 answer_json(&printed.answer, report.limit, printed.skim)
1160 )
1161 })
1162 .collect::<Vec<_>>()
1163 .join(",\n ");
1164 let interval = report.interval.map_or(NONE.to_string(), |(start, end)| {
1165 format!(
1166 "{{ \"start_secs\": {}, \"end_secs\": {} }}",
1167 num(start),
1168 num(end)
1169 )
1170 });
1171 let played = report.played.map_or(String::new(), |played| {
1172 format!("\n \"played\": {},", played_json(played))
1173 });
1174 format!(
1175 "{{\n \"target\": \"{}\",\n \"sample_rate\": {},\n \"bits\": {},\n \
1176 \"profile\": \"{}\",\n \"interval\": {interval},\n \"label\": {label},\n \
1177 \"written\": {written},{played}\n \"representations\": {{\n {reads}\n }}\n}}",
1178 escape(report.target),
1179 report.rate,
1180 report.bits.map_or(NONE.to_string(), |b| b.to_string()),
1181 escape(report.profile),
1182 )
1183}