86 lines
3.8 KiB
Go
86 lines
3.8 KiB
Go
// Package audio is maven's shared audio types.
|
|
//
|
|
// Audio crosses two boundaries in maven, and only two:
|
|
//
|
|
// - client → core (the PushToTalk payload over internal/voice): raw audio
|
|
// bytes the client captured after wake-word + VAD (per the spec's "clients
|
|
// do capture, server transcribes" decision). one clean blob per utterance.
|
|
// - core → worker module (/internal/worker for stt + tts): raw bytes the
|
|
// server ships to / from the transcription + synthesis modules. core is
|
|
// the worker's client here; the worker never reaches back.
|
|
//
|
|
// Format is fixed across both surfaces at scaffold time: 16 kHz mono int16
|
|
// little-endian PCM. faster-whisper, vosk, silero, and piper all work with
|
|
// 16 kHz mono; picking one shape up-front keeps the wire a single format
|
|
// field rather than a negotiation. A different rate upstream (e.g. an 8 kHz
|
|
// phone codec) downmixes at the client before send, never on the server —
|
|
// resampling on the always-on box is wasted work.
|
|
//
|
|
// Bytes are raw PCM, NOT a container (no WAV header on the wire). The client
|
|
// strips/generates WAV headers locally so the server's worker modules get
|
|
// exactly the bytes their model expects — there is no useful reason to ship
|
|
// a 44-byte header through a length-prefixed JSON frame. internal/pcmwav
|
|
// (a tiny helper below) is the WAV ⇄ PCM pair the reference client uses.
|
|
package audio
|
|
|
|
// Format describes raw PCM audio. Fixed at scaffold time; the wire carries
|
|
// it but today only one value is meaningful. Future: a negotiated registry
|
|
// if a second codec lands (e.g. opus, for a low-bitrate phone PWA path).
|
|
type Format struct {
|
|
SampleRate int `json:"sample_rate"` // samples/sec; default 16000
|
|
Channels int `json:"channels"` // 1 = mono
|
|
SampleBits int `json:"sample_bits"` // 16 ⇒ int16 little-endian PCM
|
|
Encoding string `json:"encoding"` // "pcm_s16le" is the only value today
|
|
}
|
|
|
|
// PCM16kMono — the canonical maven audio shape. faster-whisper, silero,
|
|
// vosk, piper all consume it. Set as the default at every seam; the wire
|
|
// carries the explicit fields so a second format doesn't need a protocol
|
|
// version bump when it lands, just a new value here.
|
|
var PCM16kMono = Format{
|
|
SampleRate: 16000,
|
|
Channels: 1,
|
|
SampleBits: 16,
|
|
Encoding: "pcm_s16le",
|
|
}
|
|
|
|
// Audio — one blob. Bytes is raw PCM in Format (no container header). The
|
|
// caller that built it (client capture, TTS synthesis output, a stub) knows
|
|
// the duration from len(Bytes) / Format.bytesPerSample() / SampleRate.
|
|
type Audio struct {
|
|
Format Format `json:"format"`
|
|
Bytes []byte `json:"bytes"` // raw PCM; base64 over the wire via worker/voice JSON marshal
|
|
}
|
|
|
|
// Duration returns the playback duration implied by Bytes + Format. Returns
|
|
// 0 for empty Bytes or an unknown encoding. A sanity helper, not a contract:
|
|
// callers that need the duration for display use this; callers that need it
|
|
// for real (e.g. cooldown math) read it from the facts table, not the audio.
|
|
func (a Audio) Duration() float64 {
|
|
if len(a.Bytes) == 0 {
|
|
return 0
|
|
}
|
|
if a.Format.SampleRate <= 0 || a.Format.Channels <= 0 || a.Format.SampleBits <= 0 {
|
|
return 0
|
|
}
|
|
if a.Format.Encoding != "pcm_s16le" {
|
|
return 0
|
|
}
|
|
bytesPerSample := a.Format.SampleBits / 8
|
|
if bytesPerSample == 0 {
|
|
return 0
|
|
}
|
|
samples := float64(len(a.Bytes)) / float64(bytesPerSample*a.Format.Channels)
|
|
return samples / float64(a.Format.SampleRate)
|
|
}
|
|
|
|
// IsValid reports whether f is a format maven can route today. Returns true
|
|
// only for the single canonical shape; a value with a different encoding is
|
|
// refused at the seam rather than mis-routed to a model that expects
|
|
// something else.
|
|
func (f Format) IsValid() bool {
|
|
return f.SampleRate == PCM16kMono.SampleRate &&
|
|
f.Channels == PCM16kMono.Channels &&
|
|
f.SampleBits == PCM16kMono.SampleBits &&
|
|
f.Encoding == PCM16kMono.Encoding
|
|
} |