purego-pipewire
Go bindings for PipeWire via purego instead of cgo.
Linux only. CGO_ENABLED=0.
[!WARNING]
This is early stage. The generated binding layer is in place, but the handwritten API is still focused on runtime lifecycle and float32 playback. API may change.
Install
go get github.com/bnema/purego-pipewire
Architecture
The binding layer is generated from the checked-in model in gen/pipewire.json. The generator produces:
- purego function bindings for PipeWire init, loop, and stream entry points
- Outbound port interfaces grouped by API surface (
InitAPI, LoopAPI, StreamOps)
- A composite CAPI interface that embeds the generated port groups
- Inbound port interfaces for the handwritten core layer
- Public API wrappers in
pipewire/ and SPA constants/helpers in spa/
A small hand-written core implements runtime init/deinit, player lifecycle, stream setup, and PCM buffer handoff for playback.
Mocks for public interfaces and outbound ports are generated by Mockery v3 and shipped in pipewire/mocks/ and internal/ports/out/mocks/.
Usage
Runtime lifecycle
import "github.com/bnema/purego-pipewire/pipewire"
rt, err := pipewire.Init()
if err != nil {
return err
}
defer rt.Deinit()
loop, err := rt.NewMainLoop()
if err != nil {
return err
}
defer loop.Destroy()
go loop.Run()
Player playback
import "github.com/bnema/purego-pipewire/pipewire"
rt, err := pipewire.Init()
if err != nil {
return err
}
defer rt.Deinit()
player, err := pipewire.NewPlayer(pipewire.PlayerConfig{
SampleRate: 48000,
Channels: 2,
FramesPerBuffer: 256,
SampleFormat: pipewire.SampleFormatF32,
UnderrunPolicy: pipewire.UnderrunFillSilence,
}, pipewire.PlayerCallbacks{
Fill: func(buf *pipewire.PCMBuffer) (int, error) {
for ch := range buf.Samples {
for i := range buf.Samples[ch] {
buf.Samples[ch][i] = 0
}
}
return buf.Frames, nil
},
})
if err != nil {
return err
}
defer player.Close()
if err := player.Start(); err != nil {
return err
}
Testing with mocks
The package ships pre-generated Mockery v3 mocks for the public interfaces in github.com/bnema/purego-pipewire/pipewire/mocks.
Step 1: Accept the interface in your code
package output
import "github.com/bnema/purego-pipewire/pipewire"
type Service struct {
player pipewire.Player
}
func New(player pipewire.Player) *Service {
return &Service{player: player}
}
func (s *Service) Start() error {
return s.player.Start()
}
Step 2: Mock it in tests
package output_test
import (
"testing"
"github.com/bnema/purego-pipewire/pipewire/mocks"
"github.com/stretchr/testify/require"
)
func TestServiceStart(t *testing.T) {
mockPlayer := mocks.NewMockPlayer(t)
mockPlayer.EXPECT().Start().Return(nil)
svc := output.New(mockPlayer)
require.NoError(t, svc.Start())
}
Available mocks in github.com/bnema/purego-pipewire/pipewire/mocks:
| Mock |
Interface |
Key methods |
MockRuntime |
pipewire.Runtime |
Init, Deinit, NewMainLoop |
MockMainLoop |
pipewire.MainLoop |
Run, Destroy |
MockPlayer |
pipewire.Player |
Start, Pause, Stop, Close, State |
All mocks support .EXPECT() for type-safe expectations and .On()/.Return() for classic testify style.
Requirements
- Go 1.26.1+
- Linux with
libpipewire-0.3.so.0
- A running PipeWire daemon for integration tests and real playback
- Override path:
PIPEWIRE_LIB_PATH=/custom/path/libpipewire-0.3.so.0
Building
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go test ./...
Integration tests need a real PipeWire daemon:
go test -tags integration ./integration ./pipewire -v
The player integration suite checks public lifecycle transitions and proves that a live PipeWire process callback invokes the user-supplied Fill callback.
Regenerating bindings
go generate ./...
Or manually:
go run ./cmd/pipewiregen
mockery
Maintainers refreshing the checked-in model can run:
./scripts/sync-headers.sh
go generate ./...
Project layout
pipewire/ public API + mocks
spa/ public PipeWire and SPA constants/helpers
internal/
ports/in/ inbound port interfaces
ports/out/ outbound port interfaces + mocks
core/ runtime and playback logic
capi/ purego bindings + bridge
loader/ dlopen libpipewire-0.3.so.0
cmd/pipewiregen/ binding generator (parser, model, emitter, templates)
gen/ checked-in model data
integration/ integration tests (require a real PipeWire daemon)
License
MIT
source
1# purego-pipewire
2
3Go bindings for [PipeWire](https://pipewire.org/) via [purego](https://github.com/bnema/purego) instead of cgo.
4
5Linux only. `CGO_ENABLED=0`.
6
7> [!WARNING]
8> This is early stage. The generated binding layer is in place, but the handwritten API is still focused on runtime lifecycle and float32 playback. API may change.
9
10## Install
11
12```sh
13go get github.com/bnema/purego-pipewire
14```
15
16## Architecture
17
18The binding layer is **generated** from the checked-in model in `gen/pipewire.json`. The generator produces:
19
20- **purego function bindings** for PipeWire init, loop, and stream entry points
21- **Outbound port interfaces** grouped by API surface (`InitAPI`, `LoopAPI`, `StreamOps`)
22- **A composite CAPI interface** that embeds the generated port groups
23- **Inbound port interfaces** for the handwritten core layer
24- **Public API wrappers** in `pipewire/` and SPA constants/helpers in `spa/`
25
26A small hand-written core implements runtime init/deinit, player lifecycle, stream setup, and PCM buffer handoff for playback.
27
28Mocks for public interfaces and outbound ports are generated by [Mockery v3](https://vektra.github.io/mockery/) and shipped in `pipewire/mocks/` and `internal/ports/out/mocks/`.
29
30## Usage
31
32### Runtime lifecycle
33
34```go
35import "github.com/bnema/purego-pipewire/pipewire"
36
37rt, err := pipewire.Init()
38if err != nil {
39 return err
40}
41defer rt.Deinit()
42
43loop, err := rt.NewMainLoop()
44if err != nil {
45 return err
46}
47defer loop.Destroy()
48
49go loop.Run()
50```
51
52### Player playback
53
54```go
55import "github.com/bnema/purego-pipewire/pipewire"
56
57rt, err := pipewire.Init()
58if err != nil {
59 return err
60}
61defer rt.Deinit()
62
63player, err := pipewire.NewPlayer(pipewire.PlayerConfig{
64 SampleRate: 48000,
65 Channels: 2,
66 FramesPerBuffer: 256,
67 SampleFormat: pipewire.SampleFormatF32,
68 UnderrunPolicy: pipewire.UnderrunFillSilence,
69}, pipewire.PlayerCallbacks{
70 Fill: func(buf *pipewire.PCMBuffer) (int, error) {
71 for ch := range buf.Samples {
72 for i := range buf.Samples[ch] {
73 buf.Samples[ch][i] = 0
74 }
75 }
76 return buf.Frames, nil
77 },
78})
79if err != nil {
80 return err
81}
82defer player.Close()
83
84if err := player.Start(); err != nil {
85 return err
86}
87```
88
89### Testing with mocks
90
91The package ships pre-generated [Mockery v3](https://vektra.github.io/mockery/) mocks for the public interfaces in `github.com/bnema/purego-pipewire/pipewire/mocks`.
92
93**Step 1: Accept the interface in your code**
94
95```go
96package output
97
98import "github.com/bnema/purego-pipewire/pipewire"
99
100type Service struct {
101 player pipewire.Player
102}
103
104func New(player pipewire.Player) *Service {
105 return &Service{player: player}
106}
107
108func (s *Service) Start() error {
109 return s.player.Start()
110}
111```
112
113**Step 2: Mock it in tests**
114
115```go
116package output_test
117
118import (
119 "testing"
120
121 "github.com/bnema/purego-pipewire/pipewire/mocks"
122 "github.com/stretchr/testify/require"
123)
124
125func TestServiceStart(t *testing.T) {
126 mockPlayer := mocks.NewMockPlayer(t)
127 mockPlayer.EXPECT().Start().Return(nil)
128
129 svc := output.New(mockPlayer)
130 require.NoError(t, svc.Start())
131}
132```
133
134**Available mocks** in `github.com/bnema/purego-pipewire/pipewire/mocks`:
135
136| Mock | Interface | Key methods |
137|---|---|---|
138| `MockRuntime` | `pipewire.Runtime` | `Init`, `Deinit`, `NewMainLoop` |
139| `MockMainLoop` | `pipewire.MainLoop` | `Run`, `Destroy` |
140| `MockPlayer` | `pipewire.Player` | `Start`, `Pause`, `Stop`, `Close`, `State` |
141
142All mocks support `.EXPECT()` for type-safe expectations and `.On()`/`.Return()` for classic testify style.
143
144## Requirements
145
146- Go 1.26.1+
147- Linux with `libpipewire-0.3.so.0`
148- A running PipeWire daemon for integration tests and real playback
149- Override path: `PIPEWIRE_LIB_PATH=/custom/path/libpipewire-0.3.so.0`
150
151## Building
152
153```sh
154CGO_ENABLED=0 go build ./...
155CGO_ENABLED=0 go test ./...
156```
157
158Integration tests need a real PipeWire daemon:
159
160```sh
161go test -tags integration ./integration ./pipewire -v
162```
163
164The player integration suite checks public lifecycle transitions and proves that a live PipeWire process callback invokes the user-supplied `Fill` callback.
165
166## Regenerating bindings
167
168```sh
169go generate ./...
170```
171
172Or manually:
173
174```sh
175go run ./cmd/pipewiregen
176mockery
177```
178
179Maintainers refreshing the checked-in model can run:
180
181```sh
182./scripts/sync-headers.sh
183go generate ./...
184```
185
186## Project layout
187
188```text
189pipewire/ public API + mocks
190spa/ public PipeWire and SPA constants/helpers
191internal/
192 ports/in/ inbound port interfaces
193 ports/out/ outbound port interfaces + mocks
194 core/ runtime and playback logic
195 capi/ purego bindings + bridge
196 loader/ dlopen libpipewire-0.3.so.0
197cmd/pipewiregen/ binding generator (parser, model, emitter, templates)
198gen/ checked-in model data
199integration/ integration tests (require a real PipeWire daemon)
200```
201
202## License
203
204MIT