# PineVoice – Golang Wyoming Protocol

> Source: <https://github.com/emergingrobotics/gowyoming>
> Published: 2026-07-15 12:35:10+00:00

A pure-Go, cgo-free implementation of the [Wyoming voice-assistant protocol](https://github.com/OHF-Voice/wyoming): 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
# utt-20260714-150405.wav  1.28s  16000Hz/16bit/1ch  peak=8123 rms=1580
```

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

# or as two real processes:
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](https://wiki.pine64.org/wiki/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
# prints the satellite name and its advertised mic/snd formats
```

**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
# trigger the device (wake word or button); after it streams audio you'll see:
# utt-20260714-150405.wav  1.36s  16000Hz/16bit/1ch  34560 bytes
```

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 use`satellite-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.

``` php
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](/emergingrobotics/gowyoming/blob/main/VISION.md); the decision log and API design are in

[.](/emergingrobotics/gowyoming/blob/main/docs/superpowers/specs)

`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 optional`version`

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 own`Source`

/`Sink`

.

MIT — see [LICENSE](/emergingrobotics/gowyoming/blob/main/LICENSE).
