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