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