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