v0.1.3 290220c

README.md RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close