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