Program Listing for File voice_result.hpp#
↰ Return to documentation for file (src/analysis/voice_result.hpp)
#pragma once
#include <QString>
#include <QVector>
#include <cstdint>
namespace mosaic {
/// Schema version this build understands. A file declaring anything else is
/// rejected outright rather than parsed on a hope: the fields here describe how
/// to interpret an image's pixels, and quietly misreading them produces a
/// picture that looks entirely plausible and is wrong.
inline constexpr int k_voice_schema_version = 1;
/// How to read the spectrogram PNG that accompanies a .voice.json.
struct VoiceSpectrogramMeta {
/// Absolute path, resolved next to the JSON. Empty when the file declared
/// no image, which is legal — tracks alone are still worth drawing.
QString imagePath;
int width = 0;
int height = 0;
/// Time span the image covers. Deliberately separate from the clip's own
/// duration: parselmouth, the WAV header and QMediaPlayer can disagree by
/// tens of milliseconds, so the image is drawn into *its* slice of the
/// widget's timeline rather than being allowed to define that timeline.
double t0Ms = 0.0;
double t1Ms = 0.0;
double f0Hz = 0.0;
double f1Hz = 0.0;
/// The dB values that image intensities 0 and 255 correspond to. Kept so a
/// readout or colour bar can name real units later.
double dbMin = 0.0;
double dbMax = 0.0;
[[nodiscard]] bool has_image() const { return !imagePath.isEmpty() && width > 0 && height > 0; }
};
/// A uniformly-sampled curve: value i is at t0Ms + i * dtMs.
///
/// Uniform rather than (time, value) pairs because Praat's own output already
/// is, so nothing is lost, the file is half the size, and lookup is arithmetic
/// instead of a search.
struct VoiceTrack {
double t0Ms = 0.0;
double dtMs = 0.0;
QVector<float> values;
/// dtMs > 0 is load-bearing, not cosmetic: every lookup divides by it.
[[nodiscard]] bool is_valid() const { return dtMs > 0.0 && !values.isEmpty(); }
/// Nearest sample to `ms`, or `fallback` outside the track's span.
[[nodiscard]] float at(double ms, float fallback) const;
};
/// Parses a "<name>.voice.json" written by analysis/run_voice.py — the
/// spectrogram, pitch track and intensity contour drawn beneath the waveform
/// on the Speaker Diarization page.
///
/// QtCore-only on purpose. The PNG is located but never loaded here, so this
/// stays free of QtGui and can be compiled into mosaic_tests, which links only
/// Qt6::Core and Qt6::Network. Everything that decides how the image is
/// *interpreted* therefore has test coverage; only the drawing does not.
class VoiceResult {
public:
VoiceResult() = default;
/// Returns a default-constructed (is_valid() == false) result if the file
/// is missing, malformed, or declares a schema this build cannot read.
static VoiceResult load(const QString& jsonPath);
[[nodiscard]] bool is_valid() const { return valid_; }
[[nodiscard]] const QString& source_audio() const { return sourceAudio_; }
[[nodiscard]] double duration_ms() const { return durationMs_; }
[[nodiscard]] const VoiceSpectrogramMeta& spectrogram() const { return spectrogram_; }
/// Hz per frame; 0 means unvoiced, which is a value and not an absence —
/// the widget must break its line there rather than interpolate across it.
[[nodiscard]] const VoiceTrack& pitch() const { return pitch_; }
[[nodiscard]] double pitch_floor_hz() const { return pitchFloorHz_; }
[[nodiscard]] double pitch_ceiling_hz() const { return pitchCeilingHz_; }
[[nodiscard]] const VoiceTrack& intensity() const { return intensity_; }
private:
bool valid_ = false;
QString sourceAudio_;
double durationMs_ = 0.0;
VoiceSpectrogramMeta spectrogram_;
VoiceTrack pitch_;
double pitchFloorHz_ = 0.0;
double pitchCeilingHz_ = 0.0;
VoiceTrack intensity_;
};
} // namespace mosaic