No description
Find a file
2026-06-27 19:51:28 +00:00
.github/workflows Update GitHub Actions workflows 2026-06-27 15:34:42 -04:00
cmd Update GitHub Actions workflows 2026-06-27 15:34:42 -04:00
examples Work on mixer 2026-06-26 17:38:43 -04:00
lxst Bump version to 0.5.0 [skip ci] 2026-06-27 19:51:28 +00:00
scripts Fix all tests 2026-06-13 16:43:01 -04:00
test/integration Work on mixer 2026-06-26 17:38:43 -04:00
testutils Work on mixer 2026-06-26 17:38:43 -04:00
.errcheck_excludes Fix all tests 2026-06-13 16:43:01 -04:00
.gitignore Fix panic on Windows 2026-06-21 11:11:29 -04:00
ARCHITECTURE.md Fix Channels bug 2026-06-26 18:53:02 -04:00
go.mod Work on test cleanup 2026-06-25 20:12:04 -04:00
go.sum Work on test cleanup 2026-06-25 20:12:04 -04:00
LICENSE Initial commit 2026-06-05 17:53:43 -04:00
pull.sh Add pull.sh 2026-06-27 15:38:58 -04:00
README.md Fix Channels bug 2026-06-26 18:53:02 -04:00
run-all-tests.sh Fix errcheck 2026-06-15 16:39:05 -04:00

go-lxst — Lightweight Extensible Signal Transport (Go)

A complete Go port of the LXST real-time audio streaming library, built on top of Reticulum.

Features

  • Pure Go — no CGO required for default build; builds with go build ./...
  • Cross-platform — works on Linux, macOS, Windows, and Android
  • Audio codecs — Opus (CGO required for encode/decode), Codec2 (stub), Raw PCM, FLAC, MP3, Vorbis
  • Real-time filters — HighPass, LowPass, BandPass, AGC (parity-verified against C reference)
  • Signal processing — RMS, Peak, VAD, Normalize, Resample, Channel conversion
  • Audio I/O — PortAudio via purego (recording + playback, requires system libportaudio); oto fallback (playback only); malgo optional via CGO
  • Pipeline architecture — Source → Filter → Codec → Sink with staged routing
  • Audio mixing — N-source mixer with per-source gain control
  • Tone generation — Configurable frequency, gain, and easing
  • Embedded sounds — Built-in ringer and soft tones
  • Hardware support — Mock keypad/display interfaces (GPIO/I2C planned)

CGO Requirements

The library itself compiles without CGO. However, gornphone and gornphone-echo require CGO (CGO_ENABLED=1) to function — the Opus codec (used by all audio profiles) needs the C libopus library via CGO. Without CGO, Opus silently returns empty data: calls connect but transfer no audio. Codec2 profiles are currently stubs regardless of CGO.

# Building the CLI tools with CGO enabled:
CGO_ENABLED=1 go install github.com/gmlewis/go-lxst/cmd/gornphone@latest
CGO_ENABLED=1 go install github.com/gmlewis/go-lxst/cmd/gornphone-echo@latest

Install libopus on your system first:

# macOS
brew install opus

# Debian/Ubuntu
sudo apt install libopus-dev

Installation

Library

go get github.com/gmlewis/go-lxst

gornphone CLI

Requires CGO and libopus. Install from GitHub — no clone needed:

CGO_ENABLED=1 go install github.com/gmlewis/go-lxst/cmd/gornphone@latest

This puts the gornphone binary on your $GOPATH/bin (or $GOBIN). Make sure that directory is on your PATH.

gornphone-echo (audio echo test service)

A headless echo/debugging service that auto-answers calls, generates a sine-wave test tone, and echoes back all received audio. No audio hardware needed — runs entirely in memory. Useful for testing call setup, signalling, and audio pipeline correctness. Also requires CGO and libopus.

CGO_ENABLED=1 go install github.com/gmlewis/go-lxst/cmd/gornphone-echo@latest
# Echo server (auto-answers, echoes audio with 0.5s delay, 440Hz tone)
gornphone-echo

# Custom settings
gornphone-echo -freq 1000 -gain 0.2 -delay 0.3

# Standalone mode (two instances on same machine)
gornphone-echo --standalone --listen :4242
gornphone-echo --standalone --connect localhost:4242

Quick Start

Play a Tone

package main

import (
    "time"
    "github.com/gmlewis/go-lxst/lxst/generators"
    "github.com/gmlewis/go-lxst/lxst/sinks"
)

func main() {
    sink := sinks.NewLineSink("", true, false, 48000)
    tone := generators.NewToneSource(440.0, 0.1, true, 20.0, 20.0, nil, sink, 1)
    tone.Start()
    time.Sleep(2 * time.Second)
    tone.Stop()
    sink.Stop()
}

Apply Filters

package main

import (
    "math"
    "github.com/gmlewis/go-lxst/lxst/filters"
)

func main() {
    hp := filters.NewHighPass(300)
    lp := filters.NewLowPass(3400)
    agc := filters.NewAGC(-12.0, 12.0, 0.0001, 0.002, 0.001)

    frame := make([][]float32, 480)
    for i := range frame {
        frame[i] = []float32{float32(math.Sin(2*math.Pi*440.0*float64(i)/48000))}
    }

    filtered := hp.HandleFrame(frame, 48000)
    filtered = lp.HandleFrame(filtered, 48000)
    filtered = agc.HandleFrame(filtered, 48000)
}

Encode/Decode Audio

package main

import (
    "github.com/gmlewis/go-lxst/lxst/codecs"
    raw "github.com/gmlewis/go-lxst/lxst/codecs/raw"
)

func main() {
    codec := raw.NewRaw(1, 16)
    frame := make([][]float32, 160)
    // ... fill frame ...
    encoded := codec.Encode(frame)
    decoded := codec.Decode(encoded, 1)
}

Package Overview

Package Description
lxst/filters HighPass, LowPass, BandPass, AGC filters
lxst/generators ToneSource signal generator
lxst/mixer Multi-source audio mixer
lxst/pipeline Source → Codec → Sink pipeline
lxst/processing RMS, Peak, VAD, Normalize, Resample, etc.
lxst/sources LineSource, OpusFileSource, Loopback
lxst/sinks LineSink, OpusFileSink
lxst/codecs Codec interface, Resample utilities
lxst/codecs/opus Opus codec (CGO via gopus for encode/decode; stub without CGO)
lxst/codecs/raw Raw PCM codec
lxst/codecs/codec2 Codec2 codec (stub, CGO needed for full impl)
lxst/codecs/flac FLAC file decoder (pure Go)
lxst/codecs/mp3 MP3 file decoder (pure Go)
lxst/codecs/vorbis Vorbis file decoder (pure Go)
lxst/platforms Audio backends: PortAudio (purego), oto (pure Go fallback), Null; malgo via CGO
lxst/sounds Embedded audio resources (ringer, soft)
lxst/call Telephony call endpoint management
lxst/network Reticulum audio streaming
lxst/primitives/hardware Keypad and display interfaces
lxst/primitives/players File playback primitives
lxst/primitives/recorders File recording primitives
lxst/primitives/telephony DTMF, tones, dialing state machines

Testing

# Run all tests
go test ./...

# Run integration/parity tests
go test -tags=integration ./...

# Run benchmarks
go test -bench=. ./lxst/filters/... ./lxst/codecs/... ./lxst/mixer/... ./lxst/processing/...

Parity with Python LXST

The Go implementation has been verified against the Python LXST C native filter implementation using integration tests (//go:build integration). Key findings:

  • HighPass, LowPass, BandPass: Full parity with C native implementation
  • AGC: Full parity with C native implementation (block processing, peak limiting)
  • Python fallback has a bug (line 102 double-applies alpha) — Go matches C, not Python fallback

Architecture

Source → [Filter] → [Mixer] → Codec → Network → Codec → [Filter] → Sink
                              ↕
                          Loopback

Sources produce audio frames, filters process them, mixers combine multiple sources, codecs encode/decode for transport, and sinks consume the final output.

Making Phone Calls Over Reticulum

gornphone is a wire-compatible Go port of the Python rnphone.py from the LXST repository. Both use the same lxst.telephony RNS destination name and signalling protocol, so a Go gornphone can call a Python rnphone and vice versa.

Prerequisites

On the Go side — install gornphone:

go install github.com/gmlewis/go-lxst/cmd/gornphone@latest

On the Python side — install LXST (includes rnphone):

pip install LXST

Both sides need a go-reticulum or Reticulum transport configured so the two machines can reach each other.

Step 1: Configure RNS Transport

Reticulum uses ~/.reticulum/config to define how nodes reach each other. If your system already has a working RNS config (for example, one that connects to an RNS testnet or another RNS node), you can skip this step — gornphone and rnphone will use whatever transport Reticulum already has. Any two Reticulum nodes that can reach each other through the network can make phone calls.

If you don't have an existing RNS config, the simplest setup for two machines on the same LAN is a direct TCP connection.

On Machine A (the machine running gornphone), create ~/.reticulum/config:

[[TCPServer]]
    tcp_address = 0.0.0.0
    tcp_port = 2222

On Machine B (the machine running Python rnphone), create ~/.reticulum/config:

[[TCPClient]]
    target_host = <Machine A's LAN IP address>
    target_port = 2222

For other transport options (RNode LoRa, AutoInterface, etc.), see the Reticulum docs.

Step 2: Start gornphone (Go side)

gornphone

On first run, gornphone creates ~/.rnphone/config and ~/.rnphone/identity. Note the identity hash printed on startup — this is your phone number that you share with the other person.

Step 3: Start rnphone (Python side)

rnphone

On first run, rnphone creates ~/.rnphone/config and ~/.rnphone/identity. Note the identity hash printed on startup.

Step 4: Make a Call

Once both machines are running and have discovered each other's paths (either through announces or by entering the identity hash directly):

# On the Go phone, dial the Python phone's 32-char hex identity hash:
> <32-char identity hash>

# Or use the phonebook — add to ~/.rnphone/config:
# [phonebook]
#     Alice = <32-char identity hash>

# Then dial by name:
> alice

On the Python side, the incoming call will appear with a prompt to answer (press Enter) or reject (press r).

Interactive Commands

When gornphone is in the available state:

Key Command Description
<hash> Dial a 32-char hex identity hash
<name> Dial a phonebook entry by name
p phonebook Show phonebook entries
r redial Redial the last called identity
i identity Show identity hash (share this with others to call you)
d desthash Show destination hash (for RNS path/announce)
a announce Send an announce on the network
x hangup Force hangup (works even when call state is unknown)
q quit Exit gornphone
h help Show help

When ringing (incoming call): press Enter to answer, any other key to reject. When in a call: press Enter to hang up.

Audio Architecture

┌──────────────┐                          ┌────────────────┐
│  Go gornphone│                          │ Python rnphone │
│              │                          │                │
│ Mic → Enc ───┼──────────────────────────┼──→ Dec → Spk   │
│ Spk ← Dec ───┼──────────────────────────┼──← Enc ← Mic   │
│              │      ◄── RNS Link ──►    │                │
└──────────────┘       (Opus/Codec2)      └────────────────┘

Transmit: LineSource → Mixer → Codec → Packetizer → RNS Link
Receive:  RNS Link → LinkSource → Mixer → Codec → LineSink

The TelephoneEndpoint in cmd/gornphone/rns.go wires the audio pipeline automatically when a link is established — both for incoming calls (via incomingLinkEstablished) and outgoing calls (via Call).

Audio Profile Selection

gornphone supports the same audio profiles as Python rnphone:

Profile Code Codec Frame Time Use Case
Ultra Low Bandwidth 0x10 Codec2 700C 400ms Weak links
Very Low Bandwidth 0x20 Codec2 1600 320ms Narrow links
Low Bandwidth 0x30 Codec2 3200 200ms Moderate links
Medium Quality 0x40 Opus 60ms Default
High Quality 0x50 Opus 60ms Good links
Super High Quality 0x60 Opus 60ms Excellent links
Low Latency 0x70 Opus 20ms Real-time
Ultra Low Latency 0x80 Opus 10ms Ultra real-time

Select a profile at startup:

gornphone -profile 0x50

Phonebook Configuration

Add entries to ~/.rnphone/config to dial by name or numerical alias:

[phonebook]
    Alice = <32-char hex identity hash>
    Bob = <32-char hex identity hash>, 42

Then dial with alice, bob, or the alias 42.

Caller Access Control

Configure who can call you in ~/.rnphone/config:

[telephone]
    # Allow everyone (default)
    allowed_callers = all

    # Block everyone
    allowed_callers = none

    # Only allow phonebook entries
    allowed_callers = phonebook

    # Only allow specific identity hashes
    allowed_callers = b8d80b1b7a9d3147880b366995422a45, fcfb80d4cd3aab7c8710541fb2317974

    # Block specific callers (overrides allow list)
    blocked_callers = f3e8c3359b39d36f3baff0a616a73d3e

Local Testing (Two gornphones on the Same Machine)

When running two gornphone instances on the same machine, each needs its own standalone RNS stack (the default shared-instance mode can't route link requests to the correct destination). Use --standalone to force share_instance = No, and --listen/--connect to establish a direct local TCP connection between them.

Terminal 1 — phone-a (listens for incoming connections):

gornphone --config /tmp/gornphone-a --standalone --listen :4242

Terminal 2 — phone-b (connects to phone-a):

gornphone --config /tmp/gornphone-b --standalone --connect localhost:4242

The --config flag is critical — each instance needs its own config and identity directory. Without it, both would share ~/.rnphone/ and collide on the identity file. The --standalone flag ensures each instance runs its own independent RNS stack instead of connecting to a shared rnsd.

After both start up, note phone-a's identity hash (shown on startup), then dial it from phone-b:

> <phone-a's 32-char identity hash>

Phone-a will show the incoming call and you can answer it. The --listen flag starts a TCP server interface on the given address; --connect starts a TCP client interface to the given address. Both flags can be combined with other RNS config interfaces if needed.

Verbosity

The -v flag controls log verbosity. By default, gornphone logs at Notice level (compact — only application messages and RNS errors). Add -v flags for more detail:

Flag RNS Log Level What's included
(none) Notice gornphone app messages, RNS errors
-v Info adds RNS internal info
-vv Verbose adds RNS verbose messages
-vvv Debug adds RNS debug (still no TCP frame noise)
-vvvv Extreme everything including TCP HDLC frame traces

Bluetooth Audio on macOS

On macOS with Bluetooth earbuds, the PortAudio backend (loaded via purego) uses CoreAudio which automatically routes to the system's default audio device. To select specific devices:

# List available devices
gornphone -l

# Use a specific speaker/mic
gornphone --speaker "AirPods Pro" --mic "AirPods Pro"

Running as a Service

gornphone can run as a headless service that auto-answers incoming calls:

gornphone --service

To install as a systemd service on Linux:

gornphone --systemd    # prints a systemd unit file

License

Reticulum License — see LICENSE for details.