A pure-Go, cgo-free implementation of the Wyoming voice-assistant protocol: the wire framing, a typed event model, and both the client and server roles — plus a satellite composition for edge devices and example programs you can run today.
Disclaimer: This works for me — that's the entire guarantee. Built with AI in the loop, so check your own biases before you love it or hate it on principle. Use at your own risk, fork freely, and don't @ me when it explodes. (But do drop me a note if it helps — pay it forward.)
Wyoming is the small peer-to-peer TCP protocol the Open Home Foundation (Home Assistant) uses to wire together voice-pipeline services: wake word, speech-to-text (ASR), text-to-speech (TTS), intent handling, and the microphone/speaker "satellite" edge role. It is newline-delimited JSON headers plus raw PCM audio on one socket — simple by design.
The only maintained reference library is Python. gowyoming
gives Go programs a first-class way to speak Wyoming on both sides of every conversation, without reverse-engineering the wire format:
Client— connect to a Wyoming service, run the describe/info handshake, stream audio for ASR, receive audio from TTS.** Server**— expose a Go ASR/TTS/wake engine as a Wyoming service.** Satellite**— a mic + speaker edge endpoint (e.g. a small speaker like PineVoice) that a pipeline connects to and drives.
- Go 1.22 or newer (developed on 1.26).
- No cgo, no external runtime dependencies.
- The external-command audio backend optionally shells out to
arecord
/aplay
(or any PCM-piping tool); not required for the file backend or the examples.
Build everything and run the tests:
make build
make test # go test -race ./...
wyoming-info
connects to any Wyoming server and prints what it offers:
go run ./cmd/wyoming-info --uri tcp://127.0.0.1:10300
Point a real device (e.g. a PineVoice satellite) at this server; every utterance it streams is written to disk as a playable WAV, with a one-line summary so wrong gain or silence is obvious:
go run ./examples/record-server --uri tcp://0.0.0.0:10300 --out ./out
Two programs that together exercise the whole protocol path — handshake, run-pipeline, audio both directions — with no Python and no hardware:
make loopback # runs the in-process integration test
go run ./examples/loopback/satellite --uri tcp://127.0.0.1:10700 &
go run ./examples/loopback/server --uri tcp://127.0.0.1:10700
A PineVoice runs its factory firmware as a Wyoming satellite server on port 10700 — it listens, and a pipeline (Home Assistant, or a gowyoming driver) connects to it by IP. It does not connect out to an ASR endpoint, so you talk to it as a client. (This is why record-server
, which listens, is not the tool for a PineVoice — see the note below.)
1. Find the device IP. Look in your router's client list, or rely on mDNS. The port is 10700
.
2. Describe it — the simplest end-to-end check that gowyoming talks to the real hardware:
go run ./cmd/wyoming-info --uri tcp://<pinevoice-ip>:10700
3. Capture its microphone to WAV with the driver-recorder. It connects, sends run-pipeline
, and writes each utterance the device streams to a timestamped WAV in --out
:
go run ./examples/satellite-record --uri tcp://<pinevoice-ip>:10700 --out ./out
Then play a captured file back to confirm the device sent the audio you expect:
aplay ./out/utt-*.wav # or open it in any audio player
Each utterance's summary prints peak
and rms
(out of a 32767 full scale), so you can see how hot the signal actually is.
When exactly the device streams depends on its firmware — many satellites stream only after local wake-word detection or a button press, so trigger it after the recorder connects. Stop the recorder with Ctrl-C; any in-progress utterance is still flushed to disk.
gowyoming records the device's PCM byte-for-byte, so a quiet file means the device is sending a low-level signal. In order of preference:
Raise gain at the source. If the PineVoice exposes a microphone gain or volume-multiplier in its settings, increase that first — more input gain lifts the signal without amplifying the noise floor.Normalize on capture. Scale each utterance's peak up to near full scale:
go run ./examples/satellite-record --uri tcp://<ip>:10700 --out ./out --normalize
Apply a fixed digital gain when you want a specific multiplier:
go run ./examples/satellite-record --uri tcp://<ip>:10700 --out ./out --gain 4
--gain
/--normalize
operate on 16-bit PCM and clip at full scale. They amplify noise along with speech, so they are a convenience for inspection, not a substitute for good input gain. The default (--gain 1.0
, no normalize) captures the device's audio unchanged.
Note —record-server
is the inverse topology.record-server
listensand advertises ASR, for a client that connects out to an ASR endpoint and pushes audio. A PineVoice does not do that (it is itself a server), so usesatellite-record
for PineVoice.record-server
remains useful for testing a client/mic service that streams to an ASR endpoint.
The example binaries take flags, not config files:
| Binary | Flag | Default | Meaning |
|---|---|---|---|
wyoming-info |
|||
--uri |
|||
tcp://127.0.0.1:10300 |
|||
| endpoint to describe | |||
satellite-record |
|||
--uri / --out / --gain / --normalize |
|||
tcp://127.0.0.1:10700 / ./out / 1.0 / false |
|||
| satellite to connect to (e.g. a PineVoice) / WAV output dir / digital gain / peak-normalize | |||
record-server |
|||
--uri / --out |
|||
tcp://0.0.0.0:10300 / ./out |
|||
| listen address / WAV output dir | |||
loopback/satellite |
|||
--uri / --audio |
|||
tcp://0.0.0.0:10700 / fixture |
|||
| listen address / WAV to stream as mic | |||
loopback/server |
|||
--uri / --audio |
|||
tcp://127.0.0.1:10700 / fixture |
|||
| satellite to drive / fixture to validate against |
URIs are tcp://host:port
or unix:///path/to/socket
.
In the Home Assistant model the roles are inverted from intuition: the satellite runs the server and Home Assistant connects in and drives it.
flowchart LR
subgraph Edge["Satellite host (edge)"]
MIC[mic] --> SAT[Satellite<br/>listens :10700]
SAT --> SND[speaker]
end
HA[Home Assistant<br/>pipeline driver] -->|connects in<br/>run-pipeline| SAT
HA -->|client| ASR[ASR server]
HA -->|client| TTS[TTS server]
Client — describe and transcribe:
c, _ := client.Dial(ctx, "tcp://127.0.0.1:10300")
defer c.Close()
info, _ := c.Describe(ctx)
transcript, _ := c.Transcribe(ctx, nextChunk) // nextChunk yields wyoming.AudioChunk
Server — expose a Go ASR engine:
type myASR struct{}
func (myASR) Transcribe(ctx context.Context, audio <-chan wyoming.AudioChunk) (wyoming.Transcript, error) {
// consume audio, return text
}
srv := server.New(server.NewASR(info, myASR{}))
srv.ListenAndServe(ctx, "tcp://0.0.0.0:10300")
Raw events (the hybrid model) — the canonical *wyoming.Event
is always available; Decode
gives a typed value when you want to switch:
switch e := must(wyoming.Decode(ev)).(type) {
case wyoming.Transcript: use(e.Text)
case wyoming.AudioChunk: play(e.Audio)
}
wyoming/ core: framing (Event/Reader/Writer), typed events, Decode, Info
transport/ tcp:// and unix:// dial/listen (the only net-using package)
client/ client role: Dial, Describe, Synthesize, Transcribe
server/ server role: Handler, Server, NewASR/NewTTS domain helpers
satellite/ edge composition: mic Source + speaker Sink + pipeline glue
audio/ Source/Sink with WAV-file and external-command backends
cmd/ wyoming-info CLI
examples/ loopback framework, satellite-record (PineVoice capture), record-server, testdata
Design rationale lives in VISION.md; the decision log and API design are in
docs/superpowers/specs
No local wake word.satellite.Config.Wake
is refused explicitly; wake is handled server-side. Local wake is a planned increment.No TLS/auth. The protocol defines none; rely on network trust.No Real Wyoming peers include an optionalversion
header.version
key in the event header; gowyoming omits it on write and ignores it on read. This does not affect interop (the field is optional for readers) but is noted for completeness.No cgo audio backend. Real device I/O goes through the external-command backend (arecord
/aplay
) or your ownSource
/Sink
.
MIT — see LICENSE.