Documentation
¶
Overview ¶
Package voicegoio is an offline ATC voice input/output library.
It provides controller speech out and pilot speech in for any application that needs an air traffic control radio: a flight simulator add-on, a training tool, a controller trainer. The library is aviation-specific — the normaliser, the grammar, the radio chain and the voice pool are all built around ATC phraseology — but it is simulator-agnostic. Nothing here knows about SimConnect, or about where a transmission came from.
It is the compatibility surface of the module: this file declares the interfaces and value types the consuming application sees. Everything else in the module is an implementation of one of these interfaces, in a subpackage that imports this one. Nothing in this package imports a subpackage, so the application can compile against the API long before a given backend exists.
Design constraints (see docs/SPEC.md):
- no third party Go modules: go.mod has zero requires, stdlib only;
- no cgo: every build is CGO_ENABLED=0, native APIs are reached through syscall (Windows) or subprocesses;
- runtime target is Windows 10/11 x64, because the first consumer is an MSFS add-on and MSFS is Windows-only; development happens on macOS.
The application deals in text and semantic tags only. It never sees PCM, grammars or voice model files.
Example (Devices) ¶
Example_devices is what a settings screen needs.
package main
import (
"fmt"
"log"
"github.com/mrlm-net/voice-goio/audio"
)
func main() {
player, err := audio.NewPlayer(audio.Options{})
if err != nil {
log.Fatal(err)
}
defer player.Close()
devices, err := player.Devices()
if err != nil {
log.Fatal(err)
}
for _, d := range devices {
fmt.Printf("%-8s %s (default=%v)\n", d.ID, d.Name, d.Default)
}
// Selecting a device is a no-op on platforms that cannot route audio, so
// this needs no platform switch.
if len(devices) > 0 {
if err := player.SetDevice(devices[0].ID); err != nil {
log.Fatal(err)
}
}
}
Output:
Example (Listen) ¶
Example_listen assembles the input half. The Windows application swaps fake.FromStdin for sapi.New; nothing else changes, because both satisfy voicegoio.STT.
package main
import (
"fmt"
"log"
voicegoio "github.com/mrlm-net/voice-goio"
"github.com/mrlm-net/voice-goio/stt/fake"
)
func main() {
var rec voicegoio.STT = fake.FromStdin()
defer rec.Close()
// Resend on every roster change: restricting the grammar to the aircraft
// on frequency is the single largest accuracy lever there is.
if err := rec.SetCallsigns([]voicegoio.Callsign{
{ICAO: "BAW123"},
{ICAO: "OK-ABC"},
}); err != nil {
log.Fatal(err)
}
if err := rec.Start(); err != nil { // push to talk pressed
log.Fatal(err)
}
if err := rec.Stop(); err != nil { // released
log.Fatal(err)
}
got := <-rec.Results() // exactly one result per cycle
switch got.Tags[voicegoio.TagIntent] {
case "readback_altitude":
fmt.Printf("%s read back %s\n", got.Tags[voicegoio.TagCallsign], got.Tags[voicegoio.TagValue])
case "mayday", "pan_pan":
fmt.Printf("%s declared an emergency: %s\n", got.Tags[voicegoio.TagCallsign], got.Tags[voicegoio.TagValue])
case voicegoio.IntentSayAgain:
fmt.Println("not understood; the controller should say again")
}
}
Output:
Example (Speak) ¶
Example_speak assembles the output half and speaks one transmission.
package main
import (
"context"
"fmt"
"log"
voicegoio "github.com/mrlm-net/voice-goio"
"github.com/mrlm-net/voice-goio/audio"
"github.com/mrlm-net/voice-goio/audio/radio"
"github.com/mrlm-net/voice-goio/normalise"
"github.com/mrlm-net/voice-goio/tts"
"github.com/mrlm-net/voice-goio/voices"
)
func main() {
const sessionSeed = 20260928
engine, backend, err := tts.Open(tts.Options{})
if err != nil {
log.Fatal(err)
}
defer engine.Close()
fmt.Println("tts backend:", backend)
man, err := voices.LoadDefault()
if err != nil {
log.Fatal(err)
}
pool := voices.NewPool(man, voices.PoolOptions{Seed: sessionSeed, AllowUnaudited: true})
chain := radio.Default()
norm := normalise.New()
player, err := audio.NewPlayer(audio.Options{})
if err != nil {
log.Fatal(err)
}
defer player.Close()
// The application must drain playback events: Finished is how it knows the
// frequency is clear for the next call.
go func() {
for ev := range player.Events() {
if !ev.Started {
fmt.Printf("%s finished on %s\n", ev.ControllerID, ev.Frequency)
}
}
}()
// A controller keeps this voice for the whole session.
voice := pool.Assign("EGLL", voicegoio.Tower)
tx := voicegoio.Transmission{
Frequency: "118.500",
ControllerID: "EGLL_TWR",
Phraseology: voicegoio.ICAO,
Text: "BAW123 RWY 27L cleared to land, wind 250 degrees 8 kt",
}
pcm, err := engine.Synthesize(context.Background(), voice, norm.Spoken(tx.Text, tx.Phraseology))
if err != nil {
log.Fatal(err)
}
out := chain.Apply(pcm, engine.SampleRate(voice), voice.Radio, player.SampleRate(), sessionSeed)
if err := player.Play(tx, out, player.SampleRate()); err != nil {
log.Fatal(err)
}
}
Output:
Index ¶
Examples ¶
Constants ¶
const ( TagIntent = "intent" TagCallsign = "callsign" TagValue = "value" TagUnit = "unit" // TagReason is set alongside IntentSayAgain when the recogniser failed to // understand a transmission, and says which way it failed. It is absent // when the pilot themselves said "say again", which is a recognised // instruction rather than a failure. // // The distinction matters because the controller's reply differs. A // transmission whose callsign was identified deserves to be answered by // name — "Speedbird one two three, say again" — and answering it with // "station calling, say again your callsign" when the callsign is sitting // in the tags reads as a machine that is not listening. TagReason = "reason" IntentSayAgain = "say_again" )
Well known tag keys and the intent used for unusable input.
const ( // ReasonNoCallsign: no aircraft on frequency was identified. Ask who is // calling. ReasonNoCallsign = "no_callsign" // ReasonOffGrammar: the aircraft was identified but what it said is not // phraseology the grammar covers. TagCallsign is set. Ask that aircraft to // say again. ReasonOffGrammar = "off_grammar" // ReasonLowConfidence: the engine matched the grammar but below the // configured threshold, so the result was discarded rather than acted on. ReasonLowConfidence = "low_confidence" )
Reasons reported in TagReason.
Variables ¶
var ErrClosed = errors.New("voicegoio: component is closed")
ErrClosed is returned once a component has been closed.
var ErrNotImplemented = errors.New("voicegoio: not implemented on this platform")
ErrNotImplemented is returned by a backend that exists on this platform only as a compiling stub. Callers should treat it as "pick another backend", not as a failure of the transmission.
Functions ¶
This section is empty.
Types ¶
type Callsign ¶
type Callsign struct {
ICAO string // "BAW123", "OK-ABC"
Spoken string // "Speedbird one two three"
}
Callsign describes one aircraft on frequency. Spoken is the phonetic form fed to the recogniser's dynamic rule; when empty the library fills it from the telephony table.
type ControllerKind ¶
type ControllerKind string
ControllerKind identifies the position a controller works, which selects a radio profile and weights voice assignment.
const ( Tower ControllerKind = "tower" Ground ControllerKind = "ground" Approach ControllerKind = "approach" Center ControllerKind = "center" ATIS ControllerKind = "atis" )
type Normaliser ¶
type Normaliser interface {
Spoken(text string, ph Phraseology) string
SpokenCallsign(icao string) string
}
Normaliser converts application text into spoken form. The concrete implementation lives in normalise; the interface is here so the pipeline can be assembled from this package alone.
type Phraseology ¶
type Phraseology string
Phraseology selects the spoken conventions used by the normaliser and accepted by the recogniser.
const ( // ICAO phraseology: "tree", "fife", "niner", "decimal", QNH in hectopascals. ICAO Phraseology = "icao" // FAA phraseology: "three", "five", "niner", "point", altimeter in inHg. FAA Phraseology = "faa" )
type PlaybackEvent ¶
type PlaybackEvent struct {
Frequency string
ControllerID string
Started bool // true = started, false = finished
}
PlaybackEvent brackets one transmission so the application can hold the next call until the frequency is clear.
type Player ¶
type Player interface {
Devices() ([]Device, error)
SetDevice(id string) error
// Play enqueues pcm behind anything already queued for t.Frequency.
Play(t Transmission, pcm []int16, sampleRate int) error
Events() <-chan PlaybackEvent
Close() error
}
Player owns the output device and one queue per frequency.
type Recognition ¶
Recognition is one result per push-to-talk cycle.
Tags carries the semantic properties extracted by the grammar. Well known keys are "intent", "callsign" and "value"; an unrecognised or low confidence transmission is reported as intent "say_again".
type STT ¶
type STT interface {
// SetCallsigns rebuilds the dynamic callsign rule. Call on every roster
// change; restricting the grammar is the main accuracy lever.
SetCallsigns(cs []Callsign) error
// Start is called when PTT is pressed.
Start() error
// Stop is called when PTT is released. Exactly one Recognition follows on
// Results, possibly with intent "say_again".
Stop() error
Results() <-chan Recognition
Close() error
}
STT is a push-to-talk recogniser restricted to the aircraft on frequency.
type TTS ¶
type TTS interface {
// Synthesize returns 16 bit mono PCM at SampleRate(profile).
Synthesize(ctx context.Context, profile VoiceProfile, spokenText string) ([]int16, error)
// SampleRate reports the native rate of the model behind profile.
SampleRate(profile VoiceProfile) int
Close() error
}
TTS synthesises already normalised text into mono 16 bit PCM.
Implementations are safe for concurrent use; the piper backend multiplexes onto a pool of sidecar processes.
type Transmission ¶
type Transmission struct {
Frequency string // "127.450"
ControllerID string // "LKPR_TWR"
Phraseology Phraseology
Text string
}
Transmission is one ATC message handed to the library by the application.
Text carries the application's normal tokens ("BAW123 climb FL350, QNH 1013") and is normalised by the library before synthesis. Frequency and ControllerID are opaque to the library except as a queue key and as the identity reported back on PlaybackEvent.
type VoiceProfile ¶
type VoiceProfile struct {
Model string // "en_GB-vctk-medium"
SpeakerID int // multi speaker models only
LengthScale float32 // 0.80-0.95, lower is faster
NoiseScale float32 // 0.5-0.7
NoiseW float32 // 0.6-0.9
Radio string // radio profile name: tower|ground|approach|center|atis
// Accent is the accent this voice was assigned for, such as "en-GB" or
// "en-IN". It is informational for the application, and backends that do
// not use piper models use it to choose an equivalent local voice.
Accent string
}
VoiceProfile identifies one controller voice: a piper model, a speaker inside that model, its prosody, and the radio profile applied after synthesis.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package audio owns the output device and the per frequency playback queues.
|
Package audio owns the output device and the per frequency playback queues. |
|
radio
Package radio turns clean synthesised speech into something that sounds like it arrived over a VHF aviation radio.
|
Package radio turns clean synthesised speech into something that sounds like it arrived over a VHF aviation radio. |
|
cmd
|
|
|
demo
command
Command demo runs the whole voice-goio pipeline end to end so it can be heard, not just tested.
|
Command demo runs the whole voice-goio pipeline end to end so it can be heard, not just tested. |
|
voicecheck
command
Command voicecheck is the regression and audit tool for voice-goio.
|
Command voicecheck is the regression and audit tool for voice-goio. |
|
Package data embeds the library's static lookup tables.
|
Package data embeds the library's static lookup tables. |
|
Package grammar embeds the SRGS grammar used by the Windows SAPI5 recogniser.
|
Package grammar embeds the SRGS grammar used by the Windows SAPI5 recogniser. |
|
internal
|
|
|
dsp
Package dsp holds the signal processing primitives the radio chain is built from: RBJ biquads, sample rate conversion and level helpers.
|
Package dsp holds the signal processing primitives the radio chain is built from: RBJ biquads, sample rate conversion and level helpers. |
|
jsonl
Package jsonl writes the JSON-lines protocol the piper sidecar reads on stdin: one compact object per line, flushed immediately so the child starts synthesising without waiting for a buffer to fill.
|
Package jsonl writes the JSON-lines protocol the piper sidecar reads on stdin: one compact object per line, flushed immediately so the child starts synthesising without waiting for a buffer to fill. |
|
userdir
Package userdir resolves the per-user directory voice models are stored in.
|
Package userdir resolves the per-user directory voice models are stored in. |
|
wav
Package wav reads and writes the small subset of RIFF/WAVE the library needs: uncompressed 16 bit PCM.
|
Package wav reads and writes the small subset of RIFF/WAVE the library needs: uncompressed 16 bit PCM. |
|
Package normalise converts ATC message text written in the application's normal tokens ("BAW123 climb FL350, QNH 1013") into the spoken form a text to speech engine can read out ("Speedbird one two tree, climb flight level tree fife zero, Q N H one zero one tree").
|
Package normalise converts ATC message text written in the application's normal tokens ("BAW123 climb FL350, QNH 1013") into the spoken form a text to speech engine can read out ("Speedbird one two tree, climb flight level tree fife zero, Q N H one zero one tree"). |
|
stt
|
|
|
fake
Package fake provides the non Windows speech recognition backends: a parser over spoken ATC text, a stdin driven recogniser for development, and a script driven one for regression runs.
|
Package fake provides the non Windows speech recognition backends: a parser over spoken ATC text, a stdin driven recogniser for development, and a script driven one for regression runs. |
|
sapi
Package sapi is the Windows speech recognition backend.
|
Package sapi is the Windows speech recognition backend. |
|
Package tts selects a synthesis backend.
|
Package tts selects a synthesis backend. |
|
fake
Package fake is a deterministic TTS backend that needs nothing installed.
|
Package fake is a deterministic TTS backend that needs nothing installed. |
|
piper
Package piper drives the piper neural TTS binary as a sidecar process.
|
Package piper drives the piper neural TTS binary as a sidecar process. |
|
say
Package say is a development only TTS backend built on the macOS `say` command.
|
Package say is a development only TTS backend built on the macOS `say` command. |
|
Package voices manages the piper voice pool: the manifest of models and speakers, downloading them on first run, and assigning a voice to a controller.
|
Package voices manages the piper voice pool: the manifest of models and speakers, downloading them on first run, and assigning a voice to a controller. |