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