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