Documentation
¶
Overview ¶
Package voice provides the voice-decoding plumbing that sits between the trunking engine and the audio output / recording layer.
The package layout:
- vocoder.go a Vocoder interface + thread-safe Registry. Default build registers NullVocoder (silence), the pure-Go IMBE decoder from internal/voice/imbe, and the pure-Go AMBE+2 decoder from internal/voice/ambe2.
- wav.go 16-bit PCM mono WAV writer with length-fields patched on Close (so the file is valid even if the daemon dies).
- recorder.go subscribes to CallStart / CallEnd events from the trunking engine, opens a per-call WAV file (and an optional raw-frame sidecar) under a configurable directory tree, and exposes WritePCM / WriteRawFrame for the demod pipeline to push samples into.
IMBE patents have expired; AMBE+2 carries active patents in some jurisdictions and re-implementing it in pure Go does not change that posture. Operators in licence-restrictive jurisdictions should evaluate before deploying. See docs/vocoders.md for the full picture.
Index ¶
- Variables
- func DecodeStream(in io.Reader, vocoderName string, out io.WriteSeeker) (int, error)
- func DecodeStreamWithVocoder(in io.Reader, v Vocoder, out io.WriteSeeker) (int, error)
- func DefaultVocoderForProtocol() map[string]string
- type NullVocoder
- type Recorder
- func (r *Recorder) Close() error
- func (r *Recorder) HasSession(deviceSerial string) bool
- func (r *Recorder) RecordingEnabled() bool
- func (r *Recorder) Run(ctx context.Context) error
- func (r *Recorder) SessionCount() int
- func (r *Recorder) SetRecordingEnabled(enabled bool)
- func (r *Recorder) WritePCM(deviceSerial string, samples []int16) error
- func (r *Recorder) WriteRawFrame(deviceSerial string, frame []byte) error
- type RecorderOptions
- type Registry
- type Vocoder
- type VocoderFactory
- type WavWriter
Constants ¶
This section is empty.
Variables ¶
var DefaultRegistry = NewRegistry()
DefaultRegistry is process-global; init() in subpackages registers here.
var ErrNoVocoder = errors.New("voice: no vocoder registered for that name")
ErrNoVocoder is returned by recorders when a CallStart references a vocoder name that isn't registered in the build.
var ErrPartialFrame = errors.New("voice: input ended mid-frame")
ErrPartialFrame is returned by DecodeStream when the input ends in the middle of a vocoder frame — the trailing bytes don't make up a complete frame for the chosen vocoder. Callers can inspect the byte count returned alongside the error to decide whether the partial trailer is recoverable (typically: it isn't).
Functions ¶
func DecodeStream ¶
DecodeStream reads vocoder frames from in, decodes each via the named vocoder from DefaultRegistry, and writes 8 kHz / 16-bit / mono PCM as a WAV stream to out. Returns the number of frames decoded successfully.
out must be an io.WriteSeeker so the WAV header length fields can be patched on close (file handles satisfy this; in-memory callers can wrap a bytes.Buffer with a seeker shim).
Frame size is determined by the chosen vocoder via FrameSize(). Input must be an exact multiple of that frame size; trailing bytes are reported via ErrPartialFrame after the leading complete frames have been written.
On a per-frame Decode error, DecodeStream stops and returns the number of frames decoded so far + the error. The WAV is closed (length fields patched) before returning so callers get a playable file even on partial decode.
func DecodeStreamWithVocoder ¶
DecodeStreamWithVocoder is the lower-level entry point: callers supply a constructed Vocoder (so they can pin reproducibility via NewWithSeed or tune AGC via NewWithConfig before handing it off). Behaviour matches DecodeStream — see that function's doc for the contract.
The Vocoder is not Reset before use; the caller controls initial state. The caller is responsible for closing v.
func DefaultVocoderForProtocol ¶
DefaultVocoderForProtocol returns the Protocol → vocoder-name mapping NewRecorder uses when RecorderOptions.VocoderForProtocol is nil. The keys match the strings the radio decoders set on Grant.Protocol; the values match factory names registered into voice.DefaultRegistry by the imbe / ambe2 package init()s.
Callers wanting to override one entry should start with a copy of this map (DefaultVocoderForProtocol() returns a fresh map per call) and mutate from there — RecorderOptions.VocoderForProtocol is taken as-is, no merging.
Types ¶
type NullVocoder ¶
type NullVocoder struct {
// contains filtered or unexported fields
}
NullVocoder produces silence. It's the default when no IMBE / AMBE+2 decoder is available, and it is always safe to use because it doesn't touch any patented algorithm.
func NewNullVocoder ¶
func NewNullVocoder(frameSize int) *NullVocoder
NewNullVocoder returns a silent vocoder with the supplied frame size (in bytes). Output is 8 kHz / 20 ms / 160 samples per frame regardless.
func (*NullVocoder) Close ¶
func (n *NullVocoder) Close() error
func (*NullVocoder) FrameSize ¶
func (n *NullVocoder) FrameSize() int
func (*NullVocoder) Name ¶
func (n *NullVocoder) Name() string
func (*NullVocoder) Reset ¶
func (n *NullVocoder) Reset()
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder writes per-call audio + raw-frame files. It subscribes to events.KindCallStart and events.KindCallEnd from the trunking engine, opens a WAV (and optional raw-frame sidecar) for each new call, and closes them on call end. The demod-pipeline composer pushes PCM samples in via WritePCM (analog protocols) and raw vocoder frames in via WriteRawFrame (digital protocols), keyed by device serial.
Layout under OutDir (Trunk-Recorder-style):
<OutDir>/<system>/<talkgroup-or-decimal-id>/<UTC-RFC3339>_src<src>.wav <OutDir>/<system>/<talkgroup-or-decimal-id>/<UTC-RFC3339>_src<src>.raw
The raw sidecar is appended once per WriteRawFrame call. It is intentionally a flat concatenation of frames so users can BYO decoder (external libmbe, DVSI hardware, etc.) without parsing surrounding metadata.
Per-call vocoder: when Grant.Protocol matches an entry in the configured VocoderForProtocol map, the recorder instantiates a fresh Vocoder from voice.DefaultRegistry on CallStart and decodes each WriteRawFrame call through it, writing the resulting PCM into the WAV. This makes captures of P25 / DMR / NXDN voice produce playable WAVs alongside the optional raw sidecar — out-of-band decode via `gophertrunk decode` remains available for operators who want bit-exact mbelib / DSD-FME output.
EDACS ProVoice grants (Grant.ProVoice == true) always force a `.raw` sidecar even when WriteRaw is false. The ProVoice vocoder is patent + trade-secret encumbered so we cannot ship a built-in decoder; the sidecar lets researchers feed frames into an external decoder.
func NewRecorder ¶
func NewRecorder(opts RecorderOptions) (*Recorder, error)
NewRecorder validates options and returns a recorder ready to Run. Like the engine, the recorder subscribes to the bus at construction so that CallStart events published before Run starts are not lost.
func (*Recorder) Close ¶
Close releases the bus subscription, waits for Run (if running) to exit, then closes any outstanding sessions. Safe to call multiple times; second and later calls are no-ops.
func (*Recorder) HasSession ¶
HasSession reports whether a session exists for deviceSerial.
func (*Recorder) RecordingEnabled ¶
RecordingEnabled reports the current gate state.
func (*Recorder) SessionCount ¶
SessionCount returns the number of currently-open recording sessions. Useful in tests; takes the internal lock so it is race-free.
func (*Recorder) SetRecordingEnabled ¶
SetRecordingEnabled toggles the recorder's runtime "create new sessions" gate. When enabled is false, subsequent CallStart events do NOT open .wav / .raw files; in-flight sessions are left alone so the head of a mid-call disable isn't lost on disk. Default (after NewRecorder) is enabled = true.
func (*Recorder) WritePCM ¶
WritePCM appends 16-bit PCM samples for the named device serial. If no session is open for that device the samples are dropped (the demod pipeline can race ahead of the CallStart event).
func (*Recorder) WriteRawFrame ¶
WriteRawFrame consumes a raw vocoder frame for the named device serial. Two outputs are produced when applicable:
- The .raw sidecar (when one was opened — see handleStart). The frame bytes are appended verbatim so external decoders can consume the file with no surrounding metadata.
- The .wav (when a vocoder was instantiated for the call's Grant.Protocol). The frame is decoded into PCM and the samples are appended to the WAV. A per-frame Decode error is logged and the frame is dropped from PCM but still written to the sidecar.
Frames for a session without either output (no sidecar, no vocoder) are dropped silently.
type RecorderOptions ¶
type RecorderOptions struct {
Bus *events.Bus
Log *slog.Logger
OutDir string
SampleRate uint32 // 8000 typical
WriteRaw bool // emit a .raw sidecar alongside each .wav
// VocoderForProtocol maps a Grant.Protocol value to a vocoder
// registry name used to decode raw frames into PCM that's
// written to the call's WAV. nil means "use the package
// defaults" (DefaultVocoderForProtocol). Pass an explicit empty
// (non-nil) map to disable auto-decode entirely; the .raw
// sidecar then becomes the only path for digital voice.
//
// Protocols not in the map produce no decoded audio — typically
// analog protocols (motorola, edacs, ltr, mpt1327) where the
// composer's FM chain feeds WritePCM directly, and ProVoice
// where no in-binary decoder is available.
VocoderForProtocol map[string]string
}
RecorderOptions configure a new Recorder.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the set of vocoders the running daemon has linked in. Drivers register from init(); callers fetch by name from config.
func NewRegistry ¶
func NewRegistry() *Registry
func (*Registry) Register ¶
func (r *Registry) Register(name string, f VocoderFactory)
Register adds (or replaces) a factory by name.
type Vocoder ¶
type Vocoder interface {
Name() string
FrameSize() int // input bytes per frame
Decode(frame []byte) ([]int16, error)
Reset()
Close() error
}
Vocoder is implemented by every voice decoder GopherTrunk can use: pure-Go IMBE, pure-Go AMBE+2, the future DVSI hardware backend, and the NullVocoder used when no decoder is available.
Decode consumes one compressed frame and returns 16-bit PCM samples at 8 kHz mono (one frame is 20 ms = 160 samples for IMBE/AMBE+2). Decoders that need internal state across frames (most vocoders do) keep it on the implementation; they are NOT safe for concurrent calls on the same instance.
type VocoderFactory ¶
VocoderFactory builds a fresh vocoder instance per call. We allocate one per call so vocoders with internal state don't bleed between calls.
type WavWriter ¶
type WavWriter struct {
// contains filtered or unexported fields
}
WavWriter writes a 16-bit PCM mono WAV file. Length fields in the RIFF and data chunks are patched in Close() so that a daemon crash leaves a readable (if length-zero) file behind rather than something most media players reject.
Construction takes any io.WriteSeeker (so tests can use bytes.Buffer wrapped in an in-memory seeker). The dedicated NewFile helper opens a regular file on disk.
func NewWavFile ¶
NewWavFile opens path for write (creating or truncating) and returns a WavWriter that closes the file on Close().
func NewWavWriter ¶
func NewWavWriter(w io.WriteSeeker, sampleRate uint32) (*WavWriter, error)
NewWavWriter wraps an io.WriteSeeker and emits the WAV header. The sample-rate parameter is the PCM rate in Hz (8000 is typical for digital-radio voice).
func (*WavWriter) Close ¶
Close patches the length fields and closes the underlying file (if the writer owns one).
func (*WavWriter) DataBytes ¶ added in v0.1.9
DataBytes returns the number of PCM payload bytes written so far (excluding the 44-byte header). Stays readable after Close so the recorder can tell whether a call captured any audio.
func (*WavWriter) WriteSamples ¶
WriteSamples appends 16-bit PCM samples (little-endian).
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ambe2 is the in-progress pure-Go AMBE+2 2400 bps voice decoder used by P25 Phase 2, DMR (Tier II / III), and NXDN voice frames.
|
Package ambe2 is the in-progress pure-Go AMBE+2 2400 bps voice decoder used by P25 Phase 2, DMR (Tier II / III), and NXDN voice frames. |
|
Package calibrate compares an in-tree Vocoder's PCM output against a reference WAV (typically produced by DSD-FME or OP25) from the same raw vocoder-frame source.
|
Package calibrate compares an in-tree Vocoder's PCM output against a reference WAV (typically produced by DSD-FME or OP25) from the same raw vocoder-frame source. |
|
Package composer bridges the trunking engine's CallStart events to the per-call demod chain that turns IQ samples on a freshly-tuned Voice device into 16-bit PCM the recorder can write.
|
Package composer bridges the trunking engine's CallStart events to the per-call demod chain that turns IQ samples on a freshly-tuned Voice device into 16-bit PCM the recorder can write. |
|
Package dvsi implements the DVSI USB-3000 / AMBE-3003 hardware vocoder backend.
|
Package dvsi implements the DVSI USB-3000 / AMBE-3003 hardware vocoder backend. |
|
Package imbe is the pure-Go IMBE 4400 bps voice decoder used by P25 Phase 1 LDU1 / LDU2 frames.
|
Package imbe is the pure-Go IMBE 4400 bps voice decoder used by P25 Phase 1 LDU1 / LDU2 frames. |
|
Package mbe is the shared Multi-Band Excitation synthesis core used by GopherTrunk's IMBE 4400 (P25 Phase 1) and AMBE+2 2400 (P25 Phase 2 / DMR / NXDN) decoders.
|
Package mbe is the shared Multi-Band Excitation synthesis core used by GopherTrunk's IMBE 4400 (P25 Phase 1) and AMBE+2 2400 (P25 Phase 2 / DMR / NXDN) decoders. |
|
Package mp3 provides a pure-Go MP3 encoder used to compress completed call audio before it is streamed to broadcast aggregators (Broadcastify Calls, RdioScanner, OpenMHz, Icecast).
|
Package mp3 provides a pure-Go MP3 encoder used to compress completed call audio before it is streamed to broadcast aggregators (Broadcastify Calls, RdioScanner, OpenMHz, Icecast). |
|
Package player is the live-audio sink that turns int16 PCM coming out of the per-call composer / conventional scanner into sound out of the host's speakers.
|
Package player is the live-audio sink that turns int16 PCM coming out of the per-call composer / conventional scanner into sound out of the host's speakers. |
|
Package toneout detects fire/EMS paging tones — Two-Tone Sequential (Motorola Quick Call II), single-tone, and DTMF — over the PCM stream produced by the voice composer, and emits events.KindToneAlert when a configured profile matches.
|
Package toneout detects fire/EMS paging tones — Two-Tone Sequential (Motorola Quick Call II), single-tone, and DTMF — over the PCM stream produced by the voice composer, and emits events.KindToneAlert when a configured profile matches. |