Go module to control Pimoroni's Pirate Audio sound output, LCD, and buttons.
The display uses SPI0 with chip select 1 (/dev/spidev0.1). Enable SPI and
reboot before running the examples:
sudo raspi-config nonint do_spi 0
sudo rebootAfter reboot, verify that the required device exists:
ls -l /dev/spidev0.1If configuring SPI manually, add dtparam=spi=on to
/boot/firmware/config.txt on current Raspberry Pi OS releases, or
/boot/config.txt on older releases.
The DAC is exposed as an ALSA device by the hifiberry-dac overlay. Add the
following lines to the same configuration file and reboot:
dtoverlay=hifiberry-dac
gpio=25=op,dh
dtparam=audio=offdtparam=audio=off is optional, but prevents the built-in Raspberry Pi audio
device from being selected as the default output. See the
official Pirate Audio repository
for more hardware configuration details.
The audio package accesses ALSA directly without cgo or development headers.
It finds the sndrpihifiberry card automatically and uses device 0. To select
a card number explicitly, clear audio.Options.CardName and set Card.
The textview package loads Roboto Medium or falls back to DejaVu Sans Mono
and DejaVu Sans. Install the packages that provide those fonts on Raspberry Pi
OS:
sudo apt update
sudo apt install fonts-roboto-unhinted fonts-dejavu-coreAlternatively, set textview.Options.FontPath to another TrueType font file.
The original Raspberry Pi Zero and Zero W use an ARMv6 CPU with hardware floating point. Build specifically for it so waveform and graphics math use the VFP unit instead of software floating point:
GOOS=linux GOARCH=arm GOARM=6 CGO_ENABLED=0 go build -trimpath -o pirate-synth ./examples/synthUse GOARM=5 only for ARM targets without VFP support.
The audio package plays beep.Streamer
values and provides oscillators, white noise, and ADSR envelopes. Use Beep for
gain, panning, mixing, sequencing, duration limits, and decoded audio. The
engine opens the selected ALSA hardware device at 48 kHz, S32_LE stereo. Only
one engine may be created at a time because direct hardware access is exclusive.
package main
import (
"log"
"time"
"github.com/gopxl/beep/v2/effects"
"github.com/rubiojr/go-pirateaudio/audio"
)
func main() {
engine, err := audio.New()
if err != nil {
log.Fatal(err)
}
defer engine.Close()
tone, err := audio.Saw(engine.SampleRate(), 220)
if err != nil {
log.Fatal(err)
}
sound, err := audio.Envelope(
&effects.Gain{Streamer: tone, Gain: -0.5},
engine.SampleRate(),
300*time.Millisecond,
audio.ADSR{
Attack: 10 * time.Millisecond,
Decay: 80 * time.Millisecond,
Sustain: 0.5,
Release: 200 * time.Millisecond,
},
)
if err != nil {
log.Fatal(err)
}
voice, err := engine.Play(sound)
if err != nil {
log.Fatal(err)
}
if err := voice.Wait(); err != nil {
log.Fatal(err)
}
}Beep streamers are stateful. Construct a fresh oscillator, envelope, decoder,
or other stream graph for every call to Engine.Play; do not replay the same
Streamer value.
Streaming runs under Beep's mixer lock. A Streamer or beep.Callback must
not call Engine or Voice methods synchronously; hand that work to another
goroutine instead. Custom Streamer.Stream methods must also return promptly
so they cannot stall the real-time mixer or shutdown.
The default master volume is deliberately limited to 20 percent. Copy and
modify audio.DefaultOptions, then call audio.Open(options) to change it. The
same options expose CardName, Card, Device, and BufferSize for ALSA
configuration. Mixing many simultaneous voices can exceed the normalized range;
the final PCM output is hard-clipped to protect the DAC from numeric overflow.
Run the four-button synthesizer with:
go run ./examples/synthThe display shows a live triangle-wave scope and four color-coded note pads. Button callbacks enqueue notes, audio starts independently of display updates, and the display redraws only while a note animation is active.
The driver package for the 240x240px Pirate Audio display.
The controller and drawing logic is adapted from the current TinyGo ST7789 driver for mainline Go and periph.io. Initialization and wiring use Pimoroni's Pirate Audio defaults.
Also used the Python driver by Philip Howard as a reference.
// Display a rotated image the display
package main
import (
"fmt"
"image/color"
"log"
"os"
"github.com/rubiojr/go-pirateaudio/display"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <img-path>\n", os.Args[0])
os.Exit(1)
}
dsp, err := display.Init()
if err != nil {
log.Fatal(err)
}
defer dsp.Close()
// Set the screen color to white
if err := dsp.FillScreen(color.RGBA{R: 255, G: 255, B: 255, A: 255}); err != nil {
log.Fatal(err)
}
img, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer img.Close()
// Rotate before pushing pixels, so the image appears rotated
if err := dsp.Rotate(display.ROTATION_270); err != nil {
log.Fatal(err)
}
if err := dsp.DrawImage(img); err != nil {
log.Fatal(err)
}
}The interactive example draws a geometric dashboard and uses all four buttons:
- A changes the color palette.
- B changes the number of shapes.
- X rotates the display.
- Y toggles display inversion.
go run ./examples/interactiveButton callbacks only enqueue input; drawing remains serialized in the main event loop.
Button registrations locate their GPIO<n> line across the available GPIO
chips, which supports both pre-Pi 5 and Pi 5 layouts. On systems without named
GPIO lines, pass buttons.WithChip("gpiochip0") (or the appropriate chip).
Hardware debounce defaults to 100 ms; use buttons.WithDebounce(0) to disable
it. Kernels using GPIO uAPI v1 automatically fall back to no debounce.
package main
import (
"fmt"
"log"
"github.com/rubiojr/go-pirateaudio/buttons"
)
func main() {
buttonA, err := buttons.OnButtonAPressed(func() {
fmt.Println("Yo Dawg, A pressed")
})
if err != nil {
log.Fatal(err)
}
defer buttonA.Close()
buttonX, err := buttons.OnButtonXPressed(func() {
fmt.Println("Yo Dawg, X pressed")
})
if err != nil {
log.Fatal(err)
}
defer buttonX.Close()
buttonY, err := buttons.OnButtonYPressed(func() {
fmt.Println("Yo Dawg, Y pressed")
})
if err != nil {
log.Fatal(err)
}
defer buttonY.Close()
buttonB, err := buttons.OnButtonBPressed(func() {
fmt.Println("Yo Dawg, B pressed")
})
if err != nil {
log.Fatal(err)
}
defer buttonB.Close()
select {}
}The rotate example redraws an image whenever A is pressed:
go run ./examples/rotate path/to/240x240.pngpackage main
import (
"log"
"time"
"github.com/rubiojr/go-pirateaudio/textview"
)
func main() {
opts := textview.DefaultOpts
opts.FontSize = 20
opts.FGColor = textview.GREEN
tv, err := textview.OpenWithOptions(opts)
if err != nil {
log.Fatal(err)
}
defer tv.Close()
if err := tv.Draw(""); err != nil {
log.Fatal(err)
}
time.Sleep(3 * time.Second)
if err := tv.DrawChars("Wake up, Neo..."); err != nil {
log.Fatal(err)
}
time.Sleep(3 * time.Second)
if err := tv.DrawChars("The Matrix has you..."); err != nil {
log.Fatal(err)
}
time.Sleep(3 * time.Second)
if err := tv.DrawChars("Follow the white rabbit."); err != nil {
log.Fatal(err)
}
}

