Skip to content

Commit 7dca670

Browse files
committed
feat: add an interactive audio synthesizer
1 parent 070eeb7 commit 7dca670

15 files changed

Lines changed: 2226 additions & 14 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.octmp

README.md

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Pirate Audio Go Module
22

3-
Go module to control Pimoroni's Pirate Audio LCD and buttons.
3+
Go module to control Pimoroni's Pirate Audio sound output, LCD, and buttons.
44

55
⚠️ Highly Experimental, API subject to change, may set your device on fire ⚠️
66

@@ -24,9 +24,25 @@ ls -l /dev/spidev0.1
2424

2525
If configuring SPI manually, add `dtparam=spi=on` to
2626
`/boot/firmware/config.txt` on current Raspberry Pi OS releases, or
27-
`/boot/config.txt` on older releases. See the
27+
`/boot/config.txt` on older releases.
28+
29+
The DAC is exposed as an ALSA device by the `hifiberry-dac` overlay. Add the
30+
following lines to the same configuration file and reboot:
31+
32+
```ini
33+
dtoverlay=hifiberry-dac
34+
gpio=25=op,dh
35+
dtparam=audio=off
36+
```
37+
38+
`dtparam=audio=off` is optional, but prevents the built-in Raspberry Pi audio
39+
device from being selected as the default output. See the
2840
[official Pirate Audio repository](https://github.com/pimoroni/pirate-audio)
29-
for the remaining hardware configuration.
41+
for more hardware configuration details.
42+
43+
The `audio` package accesses ALSA directly without cgo or development headers.
44+
It finds the `sndrpihifiberry` card automatically and uses device `0`. To select
45+
a card number explicitly, clear `audio.Options.CardName` and set `Card`.
3046

3147
The `textview` package loads Roboto Medium or falls back to DejaVu Sans Mono
3248
and DejaVu Sans. Install the packages that provide those fonts on Raspberry Pi
@@ -39,6 +55,85 @@ sudo apt install fonts-roboto-unhinted fonts-dejavu-core
3955

4056
Alternatively, set `textview.Options.FontPath` to another TrueType font file.
4157

58+
## Audio
59+
60+
The `audio` package plays [`beep.Streamer`](https://pkg.go.dev/github.com/gopxl/beep/v2#Streamer)
61+
values and provides oscillators, white noise, and ADSR envelopes. Use Beep for
62+
gain, panning, mixing, sequencing, duration limits, and decoded audio. The
63+
engine opens the selected ALSA hardware device at 48 kHz, S32_LE stereo. Only
64+
one engine may be created at a time because direct hardware access is exclusive.
65+
66+
```Go
67+
package main
68+
69+
import (
70+
"log"
71+
"time"
72+
73+
"github.com/gopxl/beep/v2/effects"
74+
"github.com/rubiojr/go-pirateaudio/audio"
75+
)
76+
77+
func main() {
78+
engine, err := audio.New()
79+
if err != nil {
80+
log.Fatal(err)
81+
}
82+
defer engine.Close()
83+
84+
tone, err := audio.Saw(engine.SampleRate(), 220)
85+
if err != nil {
86+
log.Fatal(err)
87+
}
88+
sound, err := audio.Envelope(
89+
&effects.Gain{Streamer: tone, Gain: -0.5},
90+
engine.SampleRate(),
91+
300*time.Millisecond,
92+
audio.ADSR{
93+
Attack: 10 * time.Millisecond,
94+
Decay: 80 * time.Millisecond,
95+
Sustain: 0.5,
96+
Release: 200 * time.Millisecond,
97+
},
98+
)
99+
if err != nil {
100+
log.Fatal(err)
101+
}
102+
voice, err := engine.Play(sound)
103+
if err != nil {
104+
log.Fatal(err)
105+
}
106+
if err := voice.Wait(); err != nil {
107+
log.Fatal(err)
108+
}
109+
}
110+
```
111+
112+
Beep streamers are stateful. Construct a fresh oscillator, envelope, decoder,
113+
or other stream graph for every call to `Engine.Play`; do not replay the same
114+
`Streamer` value.
115+
116+
Streaming runs under Beep's mixer lock. A `Streamer` or `beep.Callback` must
117+
not call `Engine` or `Voice` methods synchronously; hand that work to another
118+
goroutine instead. Custom `Streamer.Stream` methods must also return promptly
119+
so they cannot stall the real-time mixer or shutdown.
120+
121+
The default master volume is deliberately limited to 20 percent. Copy and
122+
modify `audio.DefaultOptions`, then call `audio.Open(options)` to change it. The
123+
same options expose `CardName`, `Card`, `Device`, and `BufferSize` for ALSA
124+
configuration. Mixing many simultaneous voices can exceed the normalized range;
125+
the final PCM output is hard-clipped to protect the DAC from numeric overflow.
126+
127+
Run the four-button synthesizer with:
128+
129+
```shell
130+
go run ./examples/synth
131+
```
132+
133+
The display shows a live triangle-wave scope and four color-coded note pads.
134+
Button callbacks enqueue notes, audio starts independently of display updates,
135+
and the display redraws only while a note animation is active.
136+
42137
## ST7789
43138

44139
The driver package for the 240x240px [Pirate Audio display](https://shop.pimoroni.com/products/pirate-audio-headphone-amp).

audio/audio_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package audio
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gopxl/beep/v2"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func collectSamples(t *testing.T, stream beep.Streamer, bufferSize int) [][2]float64 {
11+
t.Helper()
12+
var samples [][2]float64
13+
buffer := make([][2]float64, bufferSize)
14+
for {
15+
n, ok := stream.Stream(buffer)
16+
require.GreaterOrEqual(t, n, 0)
17+
require.LessOrEqual(t, n, len(buffer))
18+
samples = append(samples, buffer[:n]...)
19+
if !ok || n < len(buffer) {
20+
require.NoError(t, stream.Err())
21+
return samples
22+
}
23+
require.NotZero(t, n, "stream made no progress")
24+
}
25+
}
26+
27+
func constant(sample [2]float64) beep.Streamer {
28+
return beep.StreamerFunc(func(samples [][2]float64) (int, bool) {
29+
for i := range samples {
30+
samples[i] = sample
31+
}
32+
return len(samples), true
33+
})
34+
}

0 commit comments

Comments
 (0)