// 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 }