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