Skip to main content

sva_core/
answer.rs

1// Concern: writes one reading and the render around it as the `data` object | Non-concern: the error envelope (output.rs), subcommand JSON (sva-cli) | IO: (Answer) -> a JSON string
2
3use 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
13/// Past this a caller reading stdout wants a later interval start, not a wall of JSON.
14pub 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
282/// One object per component: a buffer is planar, and the component IS the channel.
283fn 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
320/// A cost tree, deepest row last, each already folded to the share it is worth printing.
321fn 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
340/// `limit` caps the arrays a stdout reader scrolls past; `None` writes every value. `skim`
341/// only changes the ledger.
342pub 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
395/// FORMAT 14.3: every answer says which reading ran, under which profile, and at what rate.
396/// A reading that truncated a series names what it left out, so the kept list is never read
397/// as the whole of it.
398pub 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
416/// What one row of the collapse table states beyond its name, as JSON fields.
417fn 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
454/// The collapse label beside the reading it belongs to, per FORMAT 9.3. `detail` is the one
455/// place a key may be absent: each `rule` is its own shape, and its fields belong to it.
456pub 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
475/// Whole counts every one; `waves` is null where a node's go uncounted.
476pub 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
484/// `computed` counts every miss, `stored` the misses the store kept and `replaced` the volatile
485/// misses kept in place of their last value; the store's figures are as the render left it.
486pub fn stats_json(stats: &CacheStats) -> String {
487    stats_with(stats, "")
488}
489
490/// `stats_json`, and `dropped`: each node a live stream's edits started silent.
491pub 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
534/// One answer as a report prints it.
535pub struct Printed {
536    pub name: String,
537    pub answer: Answer,
538    /// `ledger(skim=1)`: each row's level alone.
539    pub skim: bool,
540}
541
542/// What a render answered with, and where anything too big for the object went instead.
543pub struct Report<'a> {
544    pub target: &'a str,
545    pub rate: u32,
546    /// The precision every sample was written to; `None` for a file read back.
547    pub bits: Option<i32>,
548    /// The seconds the readings were taken over; `None` where none read samples.
549    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    /// Readings a crate outside this pipeline answered, each already a JSON value: this
555    /// envelope only says which reading ran, under which profile, and at what rate.
556    pub analyses: &'a [(String, String)],
557    pub limit: Option<usize>,
558}
559
560/// `written` names every reading that went to a file rather than into `representations`.
561pub 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}