1use std::path::Path;
4
5use sva_engine::{
6 Alias, AliasBand, Answer, Arguments, BandCrest, BandTrack, Bands, Binding, Buffer, CacheStats,
7 Cost, Counters, Crest, Detail, EnvelopeFrame, FormantFrame, Label, LedgerEntry, Loudness,
8 LoudnessFrame, Onsets, Outcome, Output, PayloadKind, Pruned, Source, SpectralSum, Spectrum,
9 StereoFrame, StereoImage, Work,
10};
11
12use crate::json::{NONE, capped, escape, latest, list, num};
13
14pub const SAMPLE_LIMIT: usize = 4096;
16
17fn maybe(v: Option<f64>) -> String {
18 v.map(num).unwrap_or_else(|| NONE.to_string())
19}
20
21fn counted(v: Option<usize>) -> String {
22 v.map_or_else(|| NONE.to_string(), |c| c.to_string())
23}
24
25fn ledger_json(entries: &[LedgerEntry], skim: bool) -> String {
26 list(entries, |e| {
27 let clipped = e
28 .clipped
29 .map_or_else(|| NONE.to_string(), |c| c.to_string());
30 let channel = counted(e.channel);
31 if skim {
32 return format!(
33 "\n {{ \"node\": \"{}\", \"channel\": {channel}, \"rms\": {}, \"peak\": {}, \"clipped\": {clipped} }}",
34 escape(&e.node),
35 num(e.rms),
36 num(e.peak)
37 );
38 }
39 format!(
40 "\n {{ \"node\": \"{}\", \"channel\": {channel}, \"depth\": {}, \"unit\": \"{}\", \
41 \"control\": {}, \"rms\": {}, \"peak\": {}, \"min\": {}, \"max\": {}, \
42 \"share\": {}, \"clipped\": {clipped} }}",
43 escape(&e.node),
44 e.depth,
45 e.kind.unit().name(),
46 !e.kind.is_audio(),
47 num(e.rms),
48 num(e.peak),
49 num(e.min),
50 num(e.max),
51 maybe(e.share)
52 )
53 })
54}
55
56fn spectrum_json(s: &Spectrum) -> String {
57 format!(
58 "{{ \"frame_size\": {}, \"frames\": {}, \"resolution_hz\": {}, \"rms\": {}, \
59 \"centroid_hz\": {}, \"rolloff85_hz\": {}, \"peaks\": {}, \"bands\": {} }}",
60 s.frame_size,
61 s.frames,
62 num(s.resolution_hz),
63 num(s.rms),
64 num(s.centroid_hz),
65 num(s.rolloff85_hz),
66 list(&s.peaks, |p| format!(
67 "{{ \"hz\": {}, \"db\": {} }}",
68 num(p.hz),
69 num(p.db)
70 )),
71 list(&s.bands, |b| format!(
72 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"db\": {} }}",
73 num(b.lo_hz),
74 num(b.hi_hz),
75 num(b.db)
76 ))
77 )
78}
79
80fn stereo_json(image: &StereoImage) -> String {
81 let frame = |f: &StereoFrame| {
82 format!(
83 "\n {{ \"t\": {}, \"correlation\": {}, \"mid_rms\": {}, \"side_rms\": {}, \
84 \"width\": {}, \"balance_db\": {}, \"mono_db\": {} }}",
85 num(f.t_secs),
86 num(f.correlation),
87 num(f.mid_rms),
88 num(f.side_rms),
89 num(f.width),
90 num(f.balance_db),
91 num(f.mono_db)
92 )
93 };
94 format!(
95 "{{ \"channels\": {}, \"overall\": {}, \"frames\": {} }}",
96 image.channels,
97 frame(&image.overall),
98 list(&image.frames, frame)
99 )
100}
101
102fn formants_json(f: &FormantFrame) -> String {
103 format!(
104 "\n {{ \"t\": {}, \"order\": {}, \"energy\": {}, \"residual\": {}, \"formants\": {} }}",
105 num(f.t_secs),
106 f.order,
107 num(f.energy),
108 num(f.residual),
109 list(&f.formants, |v| format!(
110 "{{ \"hz\": {}, \"bandwidth_hz\": {}, \"db\": {} }}",
111 num(v.hz),
112 num(v.bandwidth_hz),
113 num(v.db)
114 ))
115 )
116}
117
118fn band_json(b: &BandTrack, start_secs: f64, rate_hz: f64, limit: Option<usize>) -> String {
119 let shown = limit.map_or(b.rms.len(), |cap| b.rms.len().min(cap));
120 format!(
121 "\n {{ \"centre_hz\": {}, \"cam\": {}, \"erb_hz\": {}, \"q\": {}, \"peak\": {}, \
122 \"time_to_peak_secs\": {}, \"rise_10_90_secs\": {}, \
123 \"floor\": {{ \"peak\": {}, \"time_to_peak_secs\": {}, \"rise_10_90_secs\": {} }}, \
124 \"rms\": {} }}",
125 num(b.centre_hz),
126 num(b.cam),
127 num(b.erb_hz),
128 num(b.q),
129 num(b.peak),
130 maybe(b.time_to_peak_secs),
131 maybe(b.rise_10_90_secs),
132 num(b.floor.peak),
133 num(b.floor.time_to_peak_secs),
134 maybe(b.floor.rise_10_90_secs),
135 capped(
136 &b.rms,
137 shown,
138 |n| start_secs + n as f64 / rate_hz,
139 |v| num(*v),
140 )
141 )
142}
143
144fn bands_json(b: &Bands, limit: Option<usize>) -> String {
145 format!(
146 "{{ \"rate_hz\": {}, \"start_secs\": {}, \"bands\": {} }}",
147 num(b.rate_hz),
148 num(b.start_secs),
149 list(&b.bands, |band| band_json(
150 band,
151 b.start_secs,
152 b.rate_hz,
153 limit
154 ))
155 )
156}
157
158fn loudness_json(l: &Loudness, limit: Option<usize>) -> String {
159 let frames = |fs: &[LoudnessFrame]| {
160 let shown = limit.map_or(fs.len(), |cap| fs.len().min(cap));
161 capped(
162 fs,
163 shown,
164 |n| fs[n].t,
165 |f| format!("\n {{ \"t\": {}, \"lufs\": {} }}", num(f.t), num(f.lufs)),
166 )
167 };
168 format!(
169 "{{ \"integrated_lufs\": {}, \"range_lu\": {}, \"momentary_max_lufs\": {}, \
170 \"short_term_max_lufs\": {}, \"sample_peak\": {}, \"sample_peak_dbfs\": {}, \
171 \"peak_note\": \"{}\", \"momentary\": {}, \"short_term\": {} }}",
172 maybe(l.integrated_lufs),
173 maybe(l.range_lu),
174 maybe(l.momentary_max_lufs),
175 maybe(l.short_term_max_lufs),
176 num(l.sample_peak),
177 maybe(l.sample_peak_dbfs),
178 escape(l.peak_note),
179 frames(&l.momentary),
180 frames(&l.short_term)
181 )
182}
183
184fn crest_json(c: &Crest) -> String {
185 let band = |b: &BandCrest| {
186 format!(
187 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"centre_hz\": {}, \"peak\": {}, \
188 \"rms\": {}, \"crest_db\": {}, \"counted\": {} }}",
189 num(b.lo_hz),
190 num(b.hi_hz),
191 num(b.centre_hz),
192 num(b.peak),
193 num(b.rms),
194 num(b.crest_db),
195 b.counted
196 )
197 };
198 format!(
199 "{{ \"broadband_crest_db\": {}, \"spread_db\": {}, \"widest_band_hz\": {}, \
200 \"tightest_band_hz\": {}, \"counted_under_db\": {}, \"bands\": {} }}",
201 num(c.broadband_crest_db),
202 maybe(c.spread_db),
203 maybe(c.widest_band_hz),
204 maybe(c.tightest_band_hz),
205 num(c.counted_under_db),
206 list(&c.bands, band)
207 )
208}
209
210fn onsets_json(o: &Onsets) -> String {
211 format!(
212 "{{ \"onsets\": {}, \"resolution_secs\": {}, \"ioi_histogram\": {} }}",
213 list(&o.onsets, |t| format!(
214 "{{ \"t_secs\": {}, \"strength\": {} }}",
215 num(t.t_secs),
216 num(t.strength)
217 )),
218 num(o.resolution_secs),
219 list(&o.ioi_histogram, |b| format!(
220 "{{ \"lo_secs\": {}, \"hi_secs\": {}, \"count\": {} }}",
221 num(b.lo_secs),
222 num(b.hi_secs),
223 b.count
224 )),
225 )
226}
227
228fn alias_json(a: &Alias) -> String {
229 let band = |b: &AliasBand| {
230 format!(
231 "\n {{ \"lo_hz\": {}, \"hi_hz\": {}, \"signal_db\": {}, \"alias_db\": {}, \
232 \"nmr_db\": {} }}",
233 num(b.lo_hz),
234 num(b.hi_hz),
235 num(b.signal_db),
236 num(b.alias_db),
237 num(b.nmr_db)
238 )
239 };
240 format!(
241 "{{ \"oversample\": {}, \"sample_rate\": {}, \"frame_size\": {}, \"frames\": {}, \
242 \"scored_frames\": {}, \"playback_db_spl\": {}, \"asr_db\": {}, \"nmr_db\": {}, \
243 \"nmr_peak_db\": {}, \"peak_at_secs\": {}, \"audible\": {}, \
244 \"instances\": {}, \"bands\": {} }}",
245 a.oversample,
246 num(a.sample_rate),
247 a.frame_size,
248 a.frames,
249 a.scored_frames,
250 num(a.playback_db_spl),
251 num(a.asr_db),
252 num(a.nmr_db),
253 num(a.nmr_peak_db),
254 num(a.peak_at_secs),
255 a.audible,
256 a.instances,
257 list(&a.bands, band)
258 )
259}
260
261fn binding_json(b: &Binding) -> String {
262 format!(
263 "\n {{ \"name\": \"{}\", \"source\": \"{}\" }}",
264 escape(&b.name),
265 escape(&b.source)
266 )
267}
268
269fn arguments_json(a: &Arguments) -> String {
270 let at = |s: sva_ast::ByteSpan| format!("{{ \"start\": {}, \"end\": {} }}", s.start, s.end);
271 let calls = list(&a.calls, |c| {
272 let arguments = list(&c.arguments, |x| {
273 format!(
274 "{{ \"name\": \"{}\", \"value\": {}, \"written\": {} }}",
275 escape(&x.name),
276 num(x.value),
277 x.written
278 )
279 });
280 format!(
281 "\n {{ \"name\": \"{}\", \"at\": {}, \"arguments\": {arguments} }}",
282 escape(&c.name),
283 at(c.at)
284 )
285 });
286 let chosen = list(&a.chosen, |c| {
287 format!(
288 "\n {{ \"name\": \"{}\", \"at\": {}, \"operands\": {}, \"chosen\": {} }}",
289 escape(&c.name),
290 at(c.at),
291 list(&c.operands, |v| num(*v)),
292 c.chosen
293 )
294 });
295 format!(
296 "\n {{ \"node\": \"{}\", \"calls\": {calls}, \"chosen\": {chosen} }}",
297 escape(&a.node)
298 )
299}
300
301fn samples_json(b: &Buffer, limit: Option<usize>) -> String {
303 let component = |c: usize| {
304 let plane = b.plane(c);
305 let shown = limit.map_or(plane.len(), |cap| plane.len().min(cap));
306 format!(
307 "\n {{ \"channel\": {c}, \"values\": {} }}",
308 capped(
309 plane,
310 shown,
311 |n| b.origin_secs() + n as f64 / f64::from(b.rate),
312 |v| num(*v)
313 )
314 )
315 };
316 let components: Vec<usize> = (0..b.width).collect();
317 format!(
318 "{{ \"rate\": {}, \"origin_secs\": {}, \"width\": {}, \"components\": {} }}",
319 b.rate,
320 num(b.origin_secs()),
321 b.width,
322 list(&components, |c| component(*c))
323 )
324}
325
326fn symbolic_json(n: &SpectralSum) -> String {
327 let terms: Vec<String> = n.atoms().map(sva_engine::sketch_atom).collect();
328 format!(
329 "{{ \"var\": \"{}\", \"lanes\": {}, \"terms\": {} }}",
330 match n.var {
331 sva_engine::Var::T => "t",
332 sva_engine::Var::F => "f",
333 },
334 n.lanes.len(),
335 crate::json::strings(&terms)
336 )
337}
338
339fn flops_json(tree: &sva_engine::FlopTree) -> String {
341 format!(
342 "{{ \"total\": {}, \"budget\": {}, \"rows\": {} }}",
343 tree.total,
344 tree.budget,
345 list(&tree.rows, |r: &sva_engine::FlopRow| format!(
346 "\n {{ \"depth\": {}, \"node\": \"{}\", \"own\": {}, \"subtree\": {}, \
347 \"percent\": {}, \"route\": \"{}\", \"shared\": {} }}",
348 r.depth,
349 escape(&r.node),
350 r.own,
351 r.subtree,
352 num(r.percent),
353 escape(r.route),
354 r.shared
355 ))
356 )
357}
358
359pub fn value_json(output: &Output, limit: Option<usize>, skim: bool) -> String {
362 match output {
363 Output::Lines(lines) => list(lines, line_json),
364 Output::Atoms(sketches) => crate::json::strings(sketches),
365 Output::Symbolic(sum) => symbolic_json(sum),
366 Output::Samples(buffer) => samples_json(buffer, limit),
367 Output::Ledger(entries) => ledger_json(entries, skim),
368 Output::Spectrum(s) => spectrum_json(s),
369 Output::Stereo(image) => stereo_json(image),
370 Output::Bands(b) => bands_json(b, limit),
371 Output::Loudness(l) => loudness_json(l, limit),
372 Output::Crest(c) => crest_json(c),
373 Output::Onsets(o) => onsets_json(o),
374 Output::Alias(a) => alias_json(a),
375 Output::Bindings(b) => list(b, binding_json),
376 Output::Arguments(a) => list(a, arguments_json),
377 Output::Flops(tree) => flops_json(tree),
378 Output::Envelope(frames) => list(frames, |f: &EnvelopeFrame| {
379 format!(
380 "\n {{ \"t\": {}, \"rms\": {}, \"peak\": {} }}",
381 num(f.t_secs),
382 num(f.rms),
383 num(f.peak)
384 )
385 }),
386 Output::Pitch(frames) => list(frames, |f| {
387 format!(
388 "\n {{ \"t\": {}, \"notes\": {} }}",
389 num(f.t_secs),
390 list(&f.notes, |n| format!(
391 "{{ \"note\": \"{}\", \"hz\": {}, \"cents\": {}, \"db\": {}, \
392 \"harmonic_of\": {} }}",
393 escape(&n.name),
394 num(n.hz),
395 num(n.cents),
396 num(n.db),
397 maybe(n.harmonic_of)
398 ))
399 )
400 }),
401 Output::Formants(frames) => list(frames, formants_json),
402 }
403}
404
405fn line_json(l: &sva_engine::Line) -> String {
406 format!(
407 "\n {{ \"hz\": {}, \"re\": {}, \"im\": {}, \"db\": {} }}",
408 num(l.hz),
409 num(l.amp.re),
410 num(l.amp.im),
411 num(20.0 * l.amp.abs().log10())
412 )
413}
414
415pub fn answer_json(answer: &Answer, limit: Option<usize>, skim: bool) -> String {
419 format!(
420 "{{ \"source\": \"{}\", \"profile\": \"{}\", \"rate\": {}, \"value\": {}, \
421 \"dropped\": {}, \"tail_db\": {} }}",
422 match answer.source {
423 Source::Exact => "exact",
424 Source::Measured => "measured",
425 },
426 escape(answer.profile),
427 answer
428 .rate
429 .map_or_else(|| NONE.to_string(), |r| r.to_string()),
430 value_json(&answer.value, limit, skim),
431 list(&answer.dropped, line_json),
432 maybe(answer.tail_db)
433 )
434}
435
436fn detail_json(detail: &Detail) -> String {
438 match detail {
439 Detail::Lines {
440 placed,
441 summed,
442 dropped,
443 dropped_more,
444 terms,
445 tail_db,
446 ..
447 } => format!(
448 ", \"placed\": {placed}, \"summed\": {summed}, \"dropped\": {}, \
449 \"dropped_more\": {dropped_more}, \"terms\": {}, \"tail_db\": {}",
450 list(dropped, |d| format!(
451 "{{ \"hz\": {}, \"db\": {} }}",
452 num(d.hz),
453 num(d.db)
454 )),
455 counted(*terms),
456 maybe(*tail_db)
457 ),
458 Detail::Cropped { tail_db, .. } => format!(", \"tail_db\": {}", maybe(*tail_db)),
459 Detail::Point {
460 alias_db, tail_db, ..
461 } => format!(
462 ", \"alias_db\": {}, \"tail_db\": {}",
463 maybe(*alias_db),
464 maybe(*tail_db)
465 ),
466 Detail::Spectrum { wrap_db, .. } => format!(", \"wrap_db\": {}", num(*wrap_db)),
467 Detail::Roundtrip { edited, .. } => format!(", \"edited\": {edited}"),
468 Detail::Continuous { .. } | Detail::Reading { .. } => String::new(),
469 Detail::Added { parts } => format!(
470 ", \"addends\": {}",
471 list(parts, |part| format!(
472 "{{ \"rule\": \"{}\"{} }}",
473 part.rule().as_str(),
474 detail_json(part)
475 ))
476 ),
477 }
478}
479
480pub fn label_json(label: &Label) -> String {
483 let detail = detail_json(&label.detail);
484 let cost = match label.cost {
485 Some(Cost { flops, budget }) => format!(", \"flops\": {flops}, \"flop_budget\": {budget}"),
486 None => format!(", \"flops\": {NONE}, \"flop_budget\": {NONE}"),
487 };
488 let pruned = label.pruned.as_ref().map_or(NONE.to_string(), pruned_json);
489 let cost = format!(
490 "{cost}, \"moved_s\": {}, \"pruned\": {pruned}",
491 maybe(label.moved)
492 );
493 format!(
494 "{{ \"source\": \"{}\", \"profile\": \"{}\", \"rate\": {}, \"rule\": \"{}\"{detail}{cost} }}",
495 match label.source {
496 Source::Exact => "exact",
497 Source::Measured => "measured",
498 },
499 escape(label.profile),
500 label.rate,
501 escape(label.rule().as_str())
502 )
503}
504
505pub fn pruned_json(pruned: &Pruned) -> String {
508 let cuts = list(&pruned.cuts, |(node, from)| {
509 format!("{{ \"node\": \"{}\", \"from\": {from} }}", escape(node))
510 });
511 format!("{{ \"db\": {}, \"cuts\": {cuts} }}", num(pruned.db))
512}
513
514pub fn work_json(work: &Work) -> String {
516 let waves = work.waves.map_or(NONE.to_string(), |w| w.to_string());
517 format!(
518 "{{ \"samples\": {}, \"priced_flops\": {}, \"waves\": {waves} }}",
519 work.samples, work.priced_flops
520 )
521}
522
523pub fn stats_json(stats: &CacheStats) -> String {
527 stats_with(stats, "")
528}
529
530pub fn stream_stats_json(
534 stats: &CacheStats,
535 (dropped, made): (&[&str], usize),
536 pruned: &Pruned,
537) -> String {
538 let dropped = latest(dropped, made, |name| format!("\"{}\"", escape(name)));
539 let pruned = pruned_json(pruned);
540 stats_with(
541 stats,
542 &format!(", \"dropped\": {dropped}, \"pruned\": {pruned}"),
543 )
544}
545
546fn stats_with(stats: &CacheStats, extra: &str) -> String {
547 let made = stats.shed + stats.lookups.len();
548 let lookups = latest(&stats.lookups, made, |l| {
549 let outcome = match l.outcome {
550 Outcome::Hit => "hit",
551 Outcome::ComputedStored => "computed_stored",
552 Outcome::ComputedNotStored => "computed_not_stored",
553 Outcome::ComputedReplaced => "computed_replaced",
554 Outcome::Extended => "extended",
555 Outcome::Prefix => "prefix",
556 };
557 format!(
558 "{{ \"node\": \"{}\", \"key\": \"{}\", \"kind\": \"{}\", \"outcome\": \"{outcome}\" }}",
559 escape(&l.node),
560 l.key,
561 match l.kind {
562 PayloadKind::Segments => "segments",
563 PayloadKind::Frames => "frames",
564 PayloadKind::Run => "run",
565 }
566 )
567 });
568 format!(
569 "{{ \"nodes\": {}, \"hits\": {}, \"computed\": {}, \"stored\": {}, \"replaced\": {}, \"extended\": {}, \
570 \"bytes\": {}, \"max_bytes\": {}, \"entries\": {}, \"evictions\": {}, \
571 \"tier\": {}, \"lookups\": {lookups}{extra} }}",
572 stats.nodes(),
573 stats.hits(),
574 stats.computed(),
575 stats.stored(),
576 stats.replaced(),
577 stats.extended(),
578 stats.bytes,
579 stats.max_bytes,
580 stats.entries,
581 stats.evictions,
582 counters_json(&stats.tier)
583 )
584}
585
586pub fn counters_json(counters: &Counters) -> String {
588 format!(
589 "{{ \"disk_lookups\": {}, \"disk_reads\": {}, \"disk_read_bytes\": {}, \
590 \"promotions\": {}, \"writebacks\": {}, \"hits\": {}, \"evictions\": {}, \
591 \"probation_evictions\": {}, \"protected_evictions\": {} }}",
592 counters.disk_lookups,
593 counters.disk_reads,
594 counters.disk_read_bytes,
595 counters.promotions,
596 counters.writebacks,
597 counters.hits,
598 counters.evictions(),
599 counters.probation_evictions,
600 counters.protected_evictions
601 )
602}
603
604pub struct Printed {
606 pub name: String,
607 pub answer: Answer,
608 pub skim: bool,
610}
611
612pub struct Report<'a> {
614 pub target: &'a str,
615 pub rate: u32,
616 pub bits: Option<i32>,
618 pub interval: Option<(f64, f64)>,
620 pub profile: &'a str,
621 pub label: Option<&'a Label>,
622 pub written: &'a [(String, &'a Path)],
623 pub answers: &'a [Printed],
624 pub limit: Option<usize>,
625}
626
627pub fn query_data(report: &Report) -> String {
629 let written = list(report.written, |(name, path)| {
630 format!(
631 "{{ \"representation\": \"{}\", \"path\": \"{}\" }}",
632 escape(name),
633 escape(&path.display().to_string())
634 )
635 });
636 let label = report
637 .label
638 .map(label_json)
639 .unwrap_or_else(|| NONE.to_string());
640 let reads = report
641 .answers
642 .iter()
643 .map(|printed| {
644 format!(
645 "\"{}\": {}",
646 escape(&printed.name),
647 answer_json(&printed.answer, report.limit, printed.skim)
648 )
649 })
650 .collect::<Vec<_>>()
651 .join(",\n ");
652 let interval = report.interval.map_or(NONE.to_string(), |(start, end)| {
653 format!(
654 "{{ \"start_secs\": {}, \"end_secs\": {} }}",
655 num(start),
656 num(end)
657 )
658 });
659 format!(
660 "{{\n \"target\": \"{}\",\n \"sample_rate\": {},\n \"bits\": {},\n \
661 \"profile\": \"{}\",\n \"interval\": {interval},\n \"label\": {label},\n \
662 \"written\": {written},\n \"representations\": {{\n {reads}\n }}\n}}",
663 escape(report.target),
664 report.rate,
665 report.bits.map_or(NONE.to_string(), |b| b.to_string()),
666 escape(report.profile),
667 )
668}