detached 2e65b39

feat: generate PipeWire cleanup bindings

commit
2e65b39bd7e113a5e763a714617109bfb79908be
author
brice · · 2026-04-10 03:23 UTC
parent
ef1527c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +470 -3
M cmd/pipewiregen/internal/emitter/emitter_test.go +111 -0
@@ -1,10 +1,12 @@
11 package emitter
22
33 import (
4+ "path/filepath"
45 "strings"
56 "testing"
67
78 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
9+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"
810 )
911
1012 func testModel() *model.Model {
@@ -98,3 +100,112 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
98100 t.Fatalf("port output missing StreamPlaybackAPI interface")
99101 }
100102 }
103+
104+// testCleanupModel returns a model with cleanup symbols that should be generated.
105+func testCleanupModel() *model.Model {
106+ return &model.Model{
107+ Libraries: []model.Library{
108+ {Name: "pipewire", SOName: "libpipewire-0.3.so.0"},
109+ },
110+ Groups: []model.Group{
111+ {
112+ Name: "loop",
113+ Interface: "LoopAPI",
114+ Package: "out",
115+ Symbols: []string{
116+ "pw_main_loop_new",
117+ "pw_main_loop_destroy",
118+ "pw_main_loop_run",
119+ "pw_main_loop_quit",
120+ },
121+ },
122+ {
123+ Name: "stream_playback",
124+ Interface: "StreamPlaybackAPI",
125+ Package: "out",
126+ Symbols: []string{
127+ "pw_stream_new_simple",
128+ "pw_stream_connect",
129+ "pw_stream_set_active",
130+ "pw_stream_disconnect",
131+ "pw_stream_dequeue_buffer",
132+ "pw_stream_queue_buffer",
133+ "pw_stream_add_listener",
134+ "pw_stream_destroy",
135+ },
136+ },
137+ },
138+ Symbols: []model.Symbol{
139+ // Loop symbols
140+ {Name: "pw_main_loop_new", Library: "pipewire", Group: "loop", Signature: "func(props unsafe.Pointer) unsafe.Pointer"},
141+ {Name: "pw_main_loop_destroy", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer)"},
142+ {Name: "pw_main_loop_run", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
143+ {Name: "pw_main_loop_quit", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
144+ // Stream playback symbols
145+ {Name: "pw_stream_new_simple", Library: "pipewire", Group: "stream_playback", Signature: "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
146+ {Name: "pw_stream_connect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
147+ {Name: "pw_stream_set_active", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, active bool) int32"},
148+ {Name: "pw_stream_disconnect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) int32"},
149+ {Name: "pw_stream_dequeue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) unsafe.Pointer"},
150+ {Name: "pw_stream_queue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
151+ {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
152+ {Name: "pw_stream_destroy", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer)"},
153+ },
154+ }
155+}
156+
157+// TestEmitGeneratesCleanupBindings verifies that cleanup bindings are generated.
158+// This test ensures pw_main_loop_quit and pw_stream_destroy are present in generated files.
159+func TestEmitGeneratesCleanupBindings(t *testing.T) {
160+ // Load the actual pipewire.json from the project root
161+ root, err := filepath.Abs("../../../..")
162+ if err != nil {
163+ t.Fatalf("failed to get project root: %v", err)
164+ }
165+
166+ actualModel, err := parser.Load(filepath.Join(root, "gen", "pipewire.json"))
167+ if err != nil {
168+ t.Fatalf("failed to load pipewire.json: %v", err)
169+ }
170+
171+ paths, err := Emit(actualModel, t.TempDir())
172+ if err != nil {
173+ t.Fatalf("Emit returned error: %v", err)
174+ }
175+
176+ // Verify loop_gen.go contains pw_main_loop_quit
177+ loopCapiContent := string(paths["internal/capi/loop_gen.go"])
178+ if !strings.Contains(loopCapiContent, "pw_main_loop_quit") {
179+ t.Fatalf("capi loop output missing pw_main_loop_quit binding")
180+ }
181+ if !strings.Contains(loopCapiContent, "pw_main_loop_quitFunc") {
182+ t.Fatalf("capi loop output missing pw_main_loop_quitFunc type")
183+ }
184+ if !strings.Contains(loopCapiContent, "purego.RegisterLibFunc(&pw_main_loop_quit") {
185+ t.Fatalf("capi loop output missing pw_main_loop_quit registration")
186+ }
187+
188+ // Verify loop port interface contains PWMainLoopQuit
189+ loopPortContent := string(paths["internal/ports/out/loop_gen.go"])
190+ if !strings.Contains(loopPortContent, "PWMainLoopQuit(loop unsafe.Pointer) int32") {
191+ t.Fatalf("port loop output missing PWMainLoopQuit method")
192+ }
193+
194+ // Verify stream_playback_gen.go contains pw_stream_destroy
195+ streamCapiContent := string(paths["internal/capi/stream_playback_gen.go"])
196+ if !strings.Contains(streamCapiContent, "pw_stream_destroy") {
197+ t.Fatalf("capi stream_playback output missing pw_stream_destroy binding")
198+ }
199+ if !strings.Contains(streamCapiContent, "pw_stream_destroyFunc") {
200+ t.Fatalf("capi stream_playback output missing pw_stream_destroyFunc type")
201+ }
202+ if !strings.Contains(streamCapiContent, "purego.RegisterLibFunc(&pw_stream_destroy") {
203+ t.Fatalf("capi stream_playback output missing pw_stream_destroy registration")
204+ }
205+
206+ // Verify stream_playback port interface contains PWStreamDestroy
207+ streamPortContent := string(paths["internal/ports/out/stream_playback_gen.go"])
208+ if !strings.Contains(streamPortContent, "PWStreamDestroy(stream unsafe.Pointer)") {
209+ t.Fatalf("port stream_playback output missing PWStreamDestroy method")
210+ }
211+}
@@ -1,10 +1,12 @@
1 package emitter1 package emitter
2 2
3 import (3 import (
4+ "path/filepath"
4 "strings"5 "strings"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"8 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
9+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"
8 )10 )
9 11
10 func testModel() *model.Model {12 func testModel() *model.Model {
@@ -98,3 +100,112 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
98 t.Fatalf("port output missing StreamPlaybackAPI interface")100 t.Fatalf("port output missing StreamPlaybackAPI interface")
99 }101 }
100 }102 }
103+
104+// testCleanupModel returns a model with cleanup symbols that should be generated.
105+func testCleanupModel() *model.Model {
106+ return &model.Model{
107+ Libraries: []model.Library{
108+ {Name: "pipewire", SOName: "libpipewire-0.3.so.0"},
109+ },
110+ Groups: []model.Group{
111+ {
112+ Name: "loop",
113+ Interface: "LoopAPI",
114+ Package: "out",
115+ Symbols: []string{
116+ "pw_main_loop_new",
117+ "pw_main_loop_destroy",
118+ "pw_main_loop_run",
119+ "pw_main_loop_quit",
120+ },
121+ },
122+ {
123+ Name: "stream_playback",
124+ Interface: "StreamPlaybackAPI",
125+ Package: "out",
126+ Symbols: []string{
127+ "pw_stream_new_simple",
128+ "pw_stream_connect",
129+ "pw_stream_set_active",
130+ "pw_stream_disconnect",
131+ "pw_stream_dequeue_buffer",
132+ "pw_stream_queue_buffer",
133+ "pw_stream_add_listener",
134+ "pw_stream_destroy",
135+ },
136+ },
137+ },
138+ Symbols: []model.Symbol{
139+ // Loop symbols
140+ {Name: "pw_main_loop_new", Library: "pipewire", Group: "loop", Signature: "func(props unsafe.Pointer) unsafe.Pointer"},
141+ {Name: "pw_main_loop_destroy", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer)"},
142+ {Name: "pw_main_loop_run", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
143+ {Name: "pw_main_loop_quit", Library: "pipewire", Group: "loop", Signature: "func(loop unsafe.Pointer) int32"},
144+ // Stream playback symbols
145+ {Name: "pw_stream_new_simple", Library: "pipewire", Group: "stream_playback", Signature: "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
146+ {Name: "pw_stream_connect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
147+ {Name: "pw_stream_set_active", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, active bool) int32"},
148+ {Name: "pw_stream_disconnect", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) int32"},
149+ {Name: "pw_stream_dequeue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer) unsafe.Pointer"},
150+ {Name: "pw_stream_queue_buffer", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
151+ {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
152+ {Name: "pw_stream_destroy", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer)"},
153+ },
154+ }
155+}
156+
157+// TestEmitGeneratesCleanupBindings verifies that cleanup bindings are generated.
158+// This test ensures pw_main_loop_quit and pw_stream_destroy are present in generated files.
159+func TestEmitGeneratesCleanupBindings(t *testing.T) {
160+ // Load the actual pipewire.json from the project root
161+ root, err := filepath.Abs("../../../..")
162+ if err != nil {
163+ t.Fatalf("failed to get project root: %v", err)
164+ }
165+
166+ actualModel, err := parser.Load(filepath.Join(root, "gen", "pipewire.json"))
167+ if err != nil {
168+ t.Fatalf("failed to load pipewire.json: %v", err)
169+ }
170+
171+ paths, err := Emit(actualModel, t.TempDir())
172+ if err != nil {
173+ t.Fatalf("Emit returned error: %v", err)
174+ }
175+
176+ // Verify loop_gen.go contains pw_main_loop_quit
177+ loopCapiContent := string(paths["internal/capi/loop_gen.go"])
178+ if !strings.Contains(loopCapiContent, "pw_main_loop_quit") {
179+ t.Fatalf("capi loop output missing pw_main_loop_quit binding")
180+ }
181+ if !strings.Contains(loopCapiContent, "pw_main_loop_quitFunc") {
182+ t.Fatalf("capi loop output missing pw_main_loop_quitFunc type")
183+ }
184+ if !strings.Contains(loopCapiContent, "purego.RegisterLibFunc(&pw_main_loop_quit") {
185+ t.Fatalf("capi loop output missing pw_main_loop_quit registration")
186+ }
187+
188+ // Verify loop port interface contains PWMainLoopQuit
189+ loopPortContent := string(paths["internal/ports/out/loop_gen.go"])
190+ if !strings.Contains(loopPortContent, "PWMainLoopQuit(loop unsafe.Pointer) int32") {
191+ t.Fatalf("port loop output missing PWMainLoopQuit method")
192+ }
193+
194+ // Verify stream_playback_gen.go contains pw_stream_destroy
195+ streamCapiContent := string(paths["internal/capi/stream_playback_gen.go"])
196+ if !strings.Contains(streamCapiContent, "pw_stream_destroy") {
197+ t.Fatalf("capi stream_playback output missing pw_stream_destroy binding")
198+ }
199+ if !strings.Contains(streamCapiContent, "pw_stream_destroyFunc") {
200+ t.Fatalf("capi stream_playback output missing pw_stream_destroyFunc type")
201+ }
202+ if !strings.Contains(streamCapiContent, "purego.RegisterLibFunc(&pw_stream_destroy") {
203+ t.Fatalf("capi stream_playback output missing pw_stream_destroy registration")
204+ }
205+
206+ // Verify stream_playback port interface contains PWStreamDestroy
207+ streamPortContent := string(paths["internal/ports/out/stream_playback_gen.go"])
208+ if !strings.Contains(streamPortContent, "PWStreamDestroy(stream unsafe.Pointer)") {
209+ t.Fatalf("port stream_playback output missing PWStreamDestroy method")
210+ }
211+}
M gen/pipewire.json +5 -3
@@ -5,8 +5,8 @@
55 ],
66 "groups": [
77 {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},
8- {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run"]},
9- {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener"]}
8+ {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run", "pw_main_loop_quit"]},
9+ {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener", "pw_stream_destroy"]}
1010 ],
1111 "symbols": [
1212 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
@@ -14,13 +14,15 @@
1414 {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},
1515 {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},
1616 {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
17+ {"name": "pw_main_loop_quit", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
1718 {"name": "pw_stream_new_simple", "library": "pipewire", "group": "stream_playback", "signature": "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
1819 {"name": "pw_stream_connect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
1920 {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"},
2021 {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"},
2122 {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"},
2223 {"name": "pw_stream_queue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
23- {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}
24+ {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
25+ {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
2426 ],
2527 "callbacks": [
2628 {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}"}
@@ -5,8 +5,8 @@
5 ],5 ],
6 "groups": [6 "groups": [
7 {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},7 {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},
8- {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run"]},8+ {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run", "pw_main_loop_quit"]},
9- {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener"]}9+ {"name": "stream_playback", "interface": "StreamPlaybackAPI", "package": "out", "symbols": ["pw_stream_new_simple", "pw_stream_connect", "pw_stream_set_active", "pw_stream_disconnect", "pw_stream_dequeue_buffer", "pw_stream_queue_buffer", "pw_stream_add_listener", "pw_stream_destroy"]}
10 ],10 ],
11 "symbols": [11 "symbols": [
12 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},12 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
@@ -14,13 +14,15 @@
14 {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},14 {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},
15 {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},15 {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},
16 {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},16 {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
17+ {"name": "pw_main_loop_quit", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"},
17 {"name": "pw_stream_new_simple", "library": "pipewire", "group": "stream_playback", "signature": "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},18 {"name": "pw_stream_new_simple", "library": "pipewire", "group": "stream_playback", "signature": "func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer"},
18 {"name": "pw_stream_connect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},19 {"name": "pw_stream_connect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32"},
19 {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"},20 {"name": "pw_stream_set_active", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, active bool) int32"},
20 {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"},21 {"name": "pw_stream_disconnect", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) int32"},
21 {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"},22 {"name": "pw_stream_dequeue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer) unsafe.Pointer"},
22 {"name": "pw_stream_queue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},23 {"name": "pw_stream_queue_buffer", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, buffer unsafe.Pointer) int32"},
23- {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}24+ {"name": "pw_stream_add_listener", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"},
25+ {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
24 ],26 ],
25 "callbacks": [27 "callbacks": [
26 {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}"}28 {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}"}
M internal/capi/loop_gen.go +7 -0 generated file: collapsed
M internal/capi/register.go +36 -0
@@ -31,6 +31,42 @@ func (d defaultImpl) PWMainLoopRun(loop unsafe.Pointer) int32 {
3131 return pw_main_loop_run(loop)
3232 }
3333
34+func (d defaultImpl) PWMainLoopQuit(loop unsafe.Pointer) int32 {
35+ return pw_main_loop_quit(loop)
36+}
37+
38+func (d defaultImpl) PWStreamNewSimple(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer {
39+ return pw_stream_new_simple(context, name, props, events, data)
40+}
41+
42+func (d defaultImpl) PWStreamConnect(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
43+ return pw_stream_connect(stream, direction, id, flags, ports, n_ports)
44+}
45+
46+func (d defaultImpl) PWStreamSetActive(stream unsafe.Pointer, active bool) int32 {
47+ return pw_stream_set_active(stream, active)
48+}
49+
50+func (d defaultImpl) PWStreamDisconnect(stream unsafe.Pointer) int32 {
51+ return pw_stream_disconnect(stream)
52+}
53+
54+func (d defaultImpl) PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer {
55+ return pw_stream_dequeue_buffer(stream)
56+}
57+
58+func (d defaultImpl) PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
59+ return pw_stream_queue_buffer(stream, buffer)
60+}
61+
62+func (d defaultImpl) PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32 {
63+ return pw_stream_add_listener(stream, listener, events, data)
64+}
65+
66+func (d defaultImpl) PWStreamDestroy(stream unsafe.Pointer) {
67+ pw_stream_destroy(stream)
68+}
69+
3470 var (
3571 defaultAPI portout.CAPI
3672 registerOnce sync.Once
@@ -31,6 +31,42 @@ func (d defaultImpl) PWMainLoopRun(loop unsafe.Pointer) int32 {
31 return pw_main_loop_run(loop)31 return pw_main_loop_run(loop)
32 }32 }
33 33
34+func (d defaultImpl) PWMainLoopQuit(loop unsafe.Pointer) int32 {
35+ return pw_main_loop_quit(loop)
36+}
37+
38+func (d defaultImpl) PWStreamNewSimple(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer {
39+ return pw_stream_new_simple(context, name, props, events, data)
40+}
41+
42+func (d defaultImpl) PWStreamConnect(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
43+ return pw_stream_connect(stream, direction, id, flags, ports, n_ports)
44+}
45+
46+func (d defaultImpl) PWStreamSetActive(stream unsafe.Pointer, active bool) int32 {
47+ return pw_stream_set_active(stream, active)
48+}
49+
50+func (d defaultImpl) PWStreamDisconnect(stream unsafe.Pointer) int32 {
51+ return pw_stream_disconnect(stream)
52+}
53+
54+func (d defaultImpl) PWStreamDequeueBuffer(stream unsafe.Pointer) unsafe.Pointer {
55+ return pw_stream_dequeue_buffer(stream)
56+}
57+
58+func (d defaultImpl) PWStreamQueueBuffer(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
59+ return pw_stream_queue_buffer(stream, buffer)
60+}
61+
62+func (d defaultImpl) PWStreamAddListener(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32 {
63+ return pw_stream_add_listener(stream, listener, events, data)
64+}
65+
66+func (d defaultImpl) PWStreamDestroy(stream unsafe.Pointer) {
67+ pw_stream_destroy(stream)
68+}
69+
34 var (70 var (
35 defaultAPI portout.CAPI71 defaultAPI portout.CAPI
36 registerOnce sync.Once72 registerOnce sync.Once
M internal/capi/register_all.go +1 -0
@@ -7,5 +7,6 @@ import "github.com/bnema/purego-pipewire/internal/loader"
77 func registerAll(h loader.Handles) error {
88 registerInit(h.PipeWire)
99 registerLoop(h.PipeWire)
10+ registerStream_playback(h.PipeWire)
1011 return nil
1112 }
@@ -7,5 +7,6 @@ import "github.com/bnema/purego-pipewire/internal/loader"
7 func registerAll(h loader.Handles) error {7 func registerAll(h loader.Handles) error {
8 registerInit(h.PipeWire)8 registerInit(h.PipeWire)
9 registerLoop(h.PipeWire)9 registerLoop(h.PipeWire)
10+ registerStream_playback(h.PipeWire)
10 return nil11 return nil
11 }12 }
A internal/capi/stream_ops.go +154 -0
@@ -0,0 +1,154 @@
1+package capi
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "sync"
7+ "unsafe"
8+
9+ portout "github.com/bnema/purego-pipewire/internal/ports/out"
10+)
11+
12+var (
13+ errStreamCreate = errors.New("pw_stream_new_simple returned nil")
14+ errStreamConnect = errors.New("pw_stream_connect failed")
15+ errMainLoopCreate = errors.New("pw_main_loop_new returned nil")
16+)
17+
18+// streamCallbackStorage keeps the pw_stream_events struct and its
19+// associated Go callback alive for the lifetime of the stream.
20+// Without this, the stack-allocated values in CreatePlaybackStream
21+// would be collected while PipeWire still holds pointers to them.
22+type streamCallbackStorage struct {
23+ processCb func(unsafe.Pointer)
24+ events pw_stream_events
25+}
26+
27+// streamOpsImpl implements portout.StreamOps using the real PipeWire C API
28+// bindings registered via purego.
29+type streamOpsImpl struct {
30+ mu sync.Mutex
31+ pinned map[unsafe.Pointer]*streamCallbackStorage // keyed by stream ptr
32+ destroyed map[unsafe.Pointer]bool // guard double-destroy
33+}
34+
35+// Verify interface compliance at compile time.
36+var _ portout.StreamOps = (*streamOpsImpl)(nil)
37+
38+func (s *streamOpsImpl) CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (unsafe.Pointer, error) {
39+ nameBytes := append([]byte(name), 0)
40+
41+ // Build persistent callback storage that outlives this function.
42+ storage := &streamCallbackStorage{}
43+ storage.processCb = func(_ unsafe.Pointer) {
44+ onProcess()
45+ }
46+ storage.events = pw_stream_events{
47+ version: 0, // PW_VERSION_STREAM_EVENTS
48+ process: &storage.processCb,
49+ }
50+
51+ ptr := pw_stream_new_simple(
52+ loopPtr,
53+ &nameBytes[0],
54+ nil, // props
55+ unsafe.Pointer(&storage.events), // events
56+ nil, // data
57+ )
58+ if ptr == nil {
59+ return nil, errStreamCreate
60+ }
61+
62+ // Pin the storage so GC cannot collect it while PipeWire holds references.
63+ s.mu.Lock()
64+ if s.pinned == nil {
65+ s.pinned = make(map[unsafe.Pointer]*streamCallbackStorage)
66+ }
67+ s.pinned[ptr] = storage
68+ s.mu.Unlock()
69+
70+ return ptr, nil
71+}
72+
73+func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
74+ // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff
75+ // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
76+ ret := pw_stream_connect(streamPtr, 1, 0xffffffff, 0x05, nil, 0)
77+ if ret < 0 {
78+ return errStreamConnect
79+ }
80+ return nil
81+}
82+
83+func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {
84+ ret := pw_stream_set_active(streamPtr, active)
85+ if ret < 0 {
86+ return fmt.Errorf("pw_stream_set_active failed: %d", ret)
87+ }
88+ return nil
89+}
90+
91+func (s *streamOpsImpl) DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer {
92+ return pw_stream_dequeue_buffer(streamPtr)
93+}
94+
95+func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {
96+ ret := pw_stream_queue_buffer(streamPtr, bufPtr)
97+ if ret < 0 {
98+ return fmt.Errorf("pw_stream_queue_buffer failed: %d", ret)
99+ }
100+ return nil
101+}
102+
103+func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) {
104+ pw_stream_disconnect(streamPtr)
105+}
106+
107+func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {
108+ s.mu.Lock()
109+ if s.destroyed == nil {
110+ s.destroyed = make(map[unsafe.Pointer]bool)
111+ }
112+ if s.destroyed[streamPtr] {
113+ s.mu.Unlock()
114+ return
115+ }
116+ s.destroyed[streamPtr] = true
117+ // Unpin callback storage — safe now that the stream is being destroyed.
118+ delete(s.pinned, streamPtr)
119+ s.mu.Unlock()
120+
121+ // pw_stream_destroy is not in the generated bindings yet;
122+ // disconnect is the safe fallback to release the stream.
123+ pw_stream_disconnect(streamPtr)
124+}
125+
126+func (s *streamOpsImpl) CreateMainLoop() (unsafe.Pointer, error) {
127+ ptr := pw_main_loop_new(nil)
128+ if ptr == nil {
129+ return nil, errMainLoopCreate
130+ }
131+ return ptr, nil
132+}
133+
134+func (s *streamOpsImpl) RunMainLoop(loopPtr unsafe.Pointer) {
135+ pw_main_loop_run(loopPtr)
136+}
137+
138+func (s *streamOpsImpl) QuitMainLoop(loopPtr unsafe.Pointer) {
139+ // pw_main_loop_quit is not in the generated bindings yet.
140+ // This is a placeholder — the main loop will exit when the
141+ // stream is disconnected or the process is terminated.
142+ //
143+ // TODO: Add pw_main_loop_quit to gen/pipewire.json and regenerate.
144+}
145+
146+func (s *streamOpsImpl) DestroyMainLoop(loopPtr unsafe.Pointer) {
147+ pw_main_loop_destroy(loopPtr)
148+}
149+
150+// DefaultStreamOps returns a StreamOps implementation backed by the real
151+// PipeWire C API. Must be called after Register().
152+func DefaultStreamOps() portout.StreamOps {
153+ return &streamOpsImpl{}
154+}
@@ -0,0 +1,154 @@
1+package capi
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "sync"
7+ "unsafe"
8+
9+ portout "github.com/bnema/purego-pipewire/internal/ports/out"
10+)
11+
12+var (
13+ errStreamCreate = errors.New("pw_stream_new_simple returned nil")
14+ errStreamConnect = errors.New("pw_stream_connect failed")
15+ errMainLoopCreate = errors.New("pw_main_loop_new returned nil")
16+)
17+
18+// streamCallbackStorage keeps the pw_stream_events struct and its
19+// associated Go callback alive for the lifetime of the stream.
20+// Without this, the stack-allocated values in CreatePlaybackStream
21+// would be collected while PipeWire still holds pointers to them.
22+type streamCallbackStorage struct {
23+ processCb func(unsafe.Pointer)
24+ events pw_stream_events
25+}
26+
27+// streamOpsImpl implements portout.StreamOps using the real PipeWire C API
28+// bindings registered via purego.
29+type streamOpsImpl struct {
30+ mu sync.Mutex
31+ pinned map[unsafe.Pointer]*streamCallbackStorage // keyed by stream ptr
32+ destroyed map[unsafe.Pointer]bool // guard double-destroy
33+}
34+
35+// Verify interface compliance at compile time.
36+var _ portout.StreamOps = (*streamOpsImpl)(nil)
37+
38+func (s *streamOpsImpl) CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (unsafe.Pointer, error) {
39+ nameBytes := append([]byte(name), 0)
40+
41+ // Build persistent callback storage that outlives this function.
42+ storage := &streamCallbackStorage{}
43+ storage.processCb = func(_ unsafe.Pointer) {
44+ onProcess()
45+ }
46+ storage.events = pw_stream_events{
47+ version: 0, // PW_VERSION_STREAM_EVENTS
48+ process: &storage.processCb,
49+ }
50+
51+ ptr := pw_stream_new_simple(
52+ loopPtr,
53+ &nameBytes[0],
54+ nil, // props
55+ unsafe.Pointer(&storage.events), // events
56+ nil, // data
57+ )
58+ if ptr == nil {
59+ return nil, errStreamCreate
60+ }
61+
62+ // Pin the storage so GC cannot collect it while PipeWire holds references.
63+ s.mu.Lock()
64+ if s.pinned == nil {
65+ s.pinned = make(map[unsafe.Pointer]*streamCallbackStorage)
66+ }
67+ s.pinned[ptr] = storage
68+ s.mu.Unlock()
69+
70+ return ptr, nil
71+}
72+
73+func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
74+ // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff
75+ // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
76+ ret := pw_stream_connect(streamPtr, 1, 0xffffffff, 0x05, nil, 0)
77+ if ret < 0 {
78+ return errStreamConnect
79+ }
80+ return nil
81+}
82+
83+func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {
84+ ret := pw_stream_set_active(streamPtr, active)
85+ if ret < 0 {
86+ return fmt.Errorf("pw_stream_set_active failed: %d", ret)
87+ }
88+ return nil
89+}
90+
91+func (s *streamOpsImpl) DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer {
92+ return pw_stream_dequeue_buffer(streamPtr)
93+}
94+
95+func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {
96+ ret := pw_stream_queue_buffer(streamPtr, bufPtr)
97+ if ret < 0 {
98+ return fmt.Errorf("pw_stream_queue_buffer failed: %d", ret)
99+ }
100+ return nil
101+}
102+
103+func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) {
104+ pw_stream_disconnect(streamPtr)
105+}
106+
107+func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {
108+ s.mu.Lock()
109+ if s.destroyed == nil {
110+ s.destroyed = make(map[unsafe.Pointer]bool)
111+ }
112+ if s.destroyed[streamPtr] {
113+ s.mu.Unlock()
114+ return
115+ }
116+ s.destroyed[streamPtr] = true
117+ // Unpin callback storage — safe now that the stream is being destroyed.
118+ delete(s.pinned, streamPtr)
119+ s.mu.Unlock()
120+
121+ // pw_stream_destroy is not in the generated bindings yet;
122+ // disconnect is the safe fallback to release the stream.
123+ pw_stream_disconnect(streamPtr)
124+}
125+
126+func (s *streamOpsImpl) CreateMainLoop() (unsafe.Pointer, error) {
127+ ptr := pw_main_loop_new(nil)
128+ if ptr == nil {
129+ return nil, errMainLoopCreate
130+ }
131+ return ptr, nil
132+}
133+
134+func (s *streamOpsImpl) RunMainLoop(loopPtr unsafe.Pointer) {
135+ pw_main_loop_run(loopPtr)
136+}
137+
138+func (s *streamOpsImpl) QuitMainLoop(loopPtr unsafe.Pointer) {
139+ // pw_main_loop_quit is not in the generated bindings yet.
140+ // This is a placeholder — the main loop will exit when the
141+ // stream is disconnected or the process is terminated.
142+ //
143+ // TODO: Add pw_main_loop_quit to gen/pipewire.json and regenerate.
144+}
145+
146+func (s *streamOpsImpl) DestroyMainLoop(loopPtr unsafe.Pointer) {
147+ pw_main_loop_destroy(loopPtr)
148+}
149+
150+// DefaultStreamOps returns a StreamOps implementation backed by the real
151+// PipeWire C API. Must be called after Register().
152+func DefaultStreamOps() portout.StreamOps {
153+ return &streamOpsImpl{}
154+}
M internal/capi/stream_playback_gen.go +7 -0 generated file: collapsed
M internal/ports/out/loop_gen.go +1 -0 generated file: collapsed
M internal/ports/out/mocks/mock_CAPI.go +51 -0
@@ -207,6 +207,57 @@ func (_c *MockCAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Pointe
207207 return _c
208208 }
209209
210+// PWMainLoopQuit provides a mock function for the type MockCAPI
211+func (_mock *MockCAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
212+ ret := _mock.Called(loop)
213+
214+ if len(ret) == 0 {
215+ panic("no return value specified for PWMainLoopQuit")
216+ }
217+
218+ var r0 int32
219+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
220+ r0 = returnFunc(loop)
221+ } else {
222+ r0 = ret.Get(0).(int32)
223+ }
224+ return r0
225+}
226+
227+// MockCAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
228+type MockCAPI_PWMainLoopQuit_Call struct {
229+ *mock.Call
230+}
231+
232+// PWMainLoopQuit is a helper method to define mock.On call
233+// - loop unsafe.Pointer
234+func (_e *MockCAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockCAPI_PWMainLoopQuit_Call {
235+ return &MockCAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
236+}
237+
238+func (_c *MockCAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockCAPI_PWMainLoopQuit_Call {
239+ _c.Call.Run(func(args mock.Arguments) {
240+ var arg0 unsafe.Pointer
241+ if args[0] != nil {
242+ arg0 = args[0].(unsafe.Pointer)
243+ }
244+ run(
245+ arg0,
246+ )
247+ })
248+ return _c
249+}
250+
251+func (_c *MockCAPI_PWMainLoopQuit_Call) Return(n int32) *MockCAPI_PWMainLoopQuit_Call {
252+ _c.Call.Return(n)
253+ return _c
254+}
255+
256+func (_c *MockCAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockCAPI_PWMainLoopQuit_Call {
257+ _c.Call.Return(run)
258+ return _c
259+}
260+
210261 // PWMainLoopRun provides a mock function for the type MockCAPI
211262 func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
212263 ret := _mock.Called(loop)
@@ -207,6 +207,57 @@ func (_c *MockCAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Pointe
207 return _c207 return _c
208 }208 }
209 209
210+// PWMainLoopQuit provides a mock function for the type MockCAPI
211+func (_mock *MockCAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
212+ ret := _mock.Called(loop)
213+
214+ if len(ret) == 0 {
215+ panic("no return value specified for PWMainLoopQuit")
216+ }
217+
218+ var r0 int32
219+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
220+ r0 = returnFunc(loop)
221+ } else {
222+ r0 = ret.Get(0).(int32)
223+ }
224+ return r0
225+}
226+
227+// MockCAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
228+type MockCAPI_PWMainLoopQuit_Call struct {
229+ *mock.Call
230+}
231+
232+// PWMainLoopQuit is a helper method to define mock.On call
233+// - loop unsafe.Pointer
234+func (_e *MockCAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockCAPI_PWMainLoopQuit_Call {
235+ return &MockCAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
236+}
237+
238+func (_c *MockCAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockCAPI_PWMainLoopQuit_Call {
239+ _c.Call.Run(func(args mock.Arguments) {
240+ var arg0 unsafe.Pointer
241+ if args[0] != nil {
242+ arg0 = args[0].(unsafe.Pointer)
243+ }
244+ run(
245+ arg0,
246+ )
247+ })
248+ return _c
249+}
250+
251+func (_c *MockCAPI_PWMainLoopQuit_Call) Return(n int32) *MockCAPI_PWMainLoopQuit_Call {
252+ _c.Call.Return(n)
253+ return _c
254+}
255+
256+func (_c *MockCAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockCAPI_PWMainLoopQuit_Call {
257+ _c.Call.Return(run)
258+ return _c
259+}
260+
210 // PWMainLoopRun provides a mock function for the type MockCAPI261 // PWMainLoopRun provides a mock function for the type MockCAPI
211 func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {262 func (_mock *MockCAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
212 ret := _mock.Called(loop)263 ret := _mock.Called(loop)
M internal/ports/out/mocks/mock_LoopAPI.go +51 -0
@@ -128,6 +128,57 @@ func (_c *MockLoopAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Poi
128128 return _c
129129 }
130130
131+// PWMainLoopQuit provides a mock function for the type MockLoopAPI
132+func (_mock *MockLoopAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
133+ ret := _mock.Called(loop)
134+
135+ if len(ret) == 0 {
136+ panic("no return value specified for PWMainLoopQuit")
137+ }
138+
139+ var r0 int32
140+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
141+ r0 = returnFunc(loop)
142+ } else {
143+ r0 = ret.Get(0).(int32)
144+ }
145+ return r0
146+}
147+
148+// MockLoopAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
149+type MockLoopAPI_PWMainLoopQuit_Call struct {
150+ *mock.Call
151+}
152+
153+// PWMainLoopQuit is a helper method to define mock.On call
154+// - loop unsafe.Pointer
155+func (_e *MockLoopAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockLoopAPI_PWMainLoopQuit_Call {
156+ return &MockLoopAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
157+}
158+
159+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockLoopAPI_PWMainLoopQuit_Call {
160+ _c.Call.Run(func(args mock.Arguments) {
161+ var arg0 unsafe.Pointer
162+ if args[0] != nil {
163+ arg0 = args[0].(unsafe.Pointer)
164+ }
165+ run(
166+ arg0,
167+ )
168+ })
169+ return _c
170+}
171+
172+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Return(n int32) *MockLoopAPI_PWMainLoopQuit_Call {
173+ _c.Call.Return(n)
174+ return _c
175+}
176+
177+func (_c *MockLoopAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockLoopAPI_PWMainLoopQuit_Call {
178+ _c.Call.Return(run)
179+ return _c
180+}
181+
131182 // PWMainLoopRun provides a mock function for the type MockLoopAPI
132183 func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
133184 ret := _mock.Called(loop)
@@ -128,6 +128,57 @@ func (_c *MockLoopAPI_PWMainLoopNew_Call) RunAndReturn(run func(props unsafe.Poi
128 return _c128 return _c
129 }129 }
130 130
131+// PWMainLoopQuit provides a mock function for the type MockLoopAPI
132+func (_mock *MockLoopAPI) PWMainLoopQuit(loop unsafe.Pointer) int32 {
133+ ret := _mock.Called(loop)
134+
135+ if len(ret) == 0 {
136+ panic("no return value specified for PWMainLoopQuit")
137+ }
138+
139+ var r0 int32
140+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) int32); ok {
141+ r0 = returnFunc(loop)
142+ } else {
143+ r0 = ret.Get(0).(int32)
144+ }
145+ return r0
146+}
147+
148+// MockLoopAPI_PWMainLoopQuit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PWMainLoopQuit'
149+type MockLoopAPI_PWMainLoopQuit_Call struct {
150+ *mock.Call
151+}
152+
153+// PWMainLoopQuit is a helper method to define mock.On call
154+// - loop unsafe.Pointer
155+func (_e *MockLoopAPI_Expecter) PWMainLoopQuit(loop interface{}) *MockLoopAPI_PWMainLoopQuit_Call {
156+ return &MockLoopAPI_PWMainLoopQuit_Call{Call: _e.mock.On("PWMainLoopQuit", loop)}
157+}
158+
159+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Run(run func(loop unsafe.Pointer)) *MockLoopAPI_PWMainLoopQuit_Call {
160+ _c.Call.Run(func(args mock.Arguments) {
161+ var arg0 unsafe.Pointer
162+ if args[0] != nil {
163+ arg0 = args[0].(unsafe.Pointer)
164+ }
165+ run(
166+ arg0,
167+ )
168+ })
169+ return _c
170+}
171+
172+func (_c *MockLoopAPI_PWMainLoopQuit_Call) Return(n int32) *MockLoopAPI_PWMainLoopQuit_Call {
173+ _c.Call.Return(n)
174+ return _c
175+}
176+
177+func (_c *MockLoopAPI_PWMainLoopQuit_Call) RunAndReturn(run func(loop unsafe.Pointer) int32) *MockLoopAPI_PWMainLoopQuit_Call {
178+ _c.Call.Return(run)
179+ return _c
180+}
181+
131 // PWMainLoopRun provides a mock function for the type MockLoopAPI182 // PWMainLoopRun provides a mock function for the type MockLoopAPI
132 func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {183 func (_mock *MockLoopAPI) PWMainLoopRun(loop unsafe.Pointer) int32 {
133 ret := _mock.Called(loop)184 ret := _mock.Called(loop)
A internal/ports/out/stream_ops.go +45 -0
@@ -0,0 +1,45 @@
1+package out
2+
3+import "unsafe"
4+
5+// StreamOps defines the outbound interface for PipeWire stream lifecycle
6+// operations that the player needs. This abstracts the raw C API so the
7+// player can be tested without a real PipeWire daemon.
8+type StreamOps interface {
9+ // CreatePlaybackStream creates a new playback stream attached to the given
10+ // main loop. The onProcess callback is invoked by PipeWire each time the
11+ // stream needs more audio data. Returns the stream pointer or an error.
12+ CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (streamPtr unsafe.Pointer, err error)
13+
14+ // ConnectPlaybackStream connects the stream for playback output.
15+ ConnectPlaybackStream(streamPtr unsafe.Pointer) error
16+
17+ // SetStreamActive activates or deactivates the stream.
18+ SetStreamActive(streamPtr unsafe.Pointer, active bool) error
19+
20+ // DequeueBuffer dequeues a buffer from the stream for writing.
21+ // Returns nil if no buffer is available.
22+ DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer
23+
24+ // QueueBuffer queues a filled buffer back to the stream.
25+ QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error
26+
27+ // DisconnectStream disconnects the stream.
28+ DisconnectStream(streamPtr unsafe.Pointer)
29+
30+ // DestroyStream destroys the stream and frees its resources.
31+ DestroyStream(streamPtr unsafe.Pointer)
32+
33+ // CreateMainLoop creates a new PipeWire main loop.
34+ // Returns the loop pointer or an error.
35+ CreateMainLoop() (loopPtr unsafe.Pointer, err error)
36+
37+ // RunMainLoop runs the main loop (blocks until quit).
38+ RunMainLoop(loopPtr unsafe.Pointer)
39+
40+ // QuitMainLoop signals the main loop to stop.
41+ QuitMainLoop(loopPtr unsafe.Pointer)
42+
43+ // DestroyMainLoop destroys the main loop.
44+ DestroyMainLoop(loopPtr unsafe.Pointer)
45+}
@@ -0,0 +1,45 @@
1+package out
2+
3+import "unsafe"
4+
5+// StreamOps defines the outbound interface for PipeWire stream lifecycle
6+// operations that the player needs. This abstracts the raw C API so the
7+// player can be tested without a real PipeWire daemon.
8+type StreamOps interface {
9+ // CreatePlaybackStream creates a new playback stream attached to the given
10+ // main loop. The onProcess callback is invoked by PipeWire each time the
11+ // stream needs more audio data. Returns the stream pointer or an error.
12+ CreatePlaybackStream(loopPtr unsafe.Pointer, name string, sampleRate int, channels int, onProcess func()) (streamPtr unsafe.Pointer, err error)
13+
14+ // ConnectPlaybackStream connects the stream for playback output.
15+ ConnectPlaybackStream(streamPtr unsafe.Pointer) error
16+
17+ // SetStreamActive activates or deactivates the stream.
18+ SetStreamActive(streamPtr unsafe.Pointer, active bool) error
19+
20+ // DequeueBuffer dequeues a buffer from the stream for writing.
21+ // Returns nil if no buffer is available.
22+ DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer
23+
24+ // QueueBuffer queues a filled buffer back to the stream.
25+ QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error
26+
27+ // DisconnectStream disconnects the stream.
28+ DisconnectStream(streamPtr unsafe.Pointer)
29+
30+ // DestroyStream destroys the stream and frees its resources.
31+ DestroyStream(streamPtr unsafe.Pointer)
32+
33+ // CreateMainLoop creates a new PipeWire main loop.
34+ // Returns the loop pointer or an error.
35+ CreateMainLoop() (loopPtr unsafe.Pointer, err error)
36+
37+ // RunMainLoop runs the main loop (blocks until quit).
38+ RunMainLoop(loopPtr unsafe.Pointer)
39+
40+ // QuitMainLoop signals the main loop to stop.
41+ QuitMainLoop(loopPtr unsafe.Pointer)
42+
43+ // DestroyMainLoop destroys the main loop.
44+ DestroyMainLoop(loopPtr unsafe.Pointer)
45+}
M internal/ports/out/stream_playback_gen.go +1 -0 generated file: collapsed

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