detached ae5412a

refactor: tighten temporary playback format contract

commit
ae5412aae6e54b0b6bd5e169f9fb319e5ab4b7f7
author
brice · · 2026-04-10 12:51 UTC
parent
d43cf59
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +24 -19
M internal/capi/stream_ops.go +9 -2
@@ -1,6 +1,7 @@
11 package capi
22
33 import (
4+ "errors"
45 "fmt"
56 "sync"
67 "unsafe"
@@ -18,6 +19,11 @@ func (e *PWError) Error() string {
1819 return fmt.Sprintf("%s failed: %d", e.Func, e.Code)
1920 }
2021
22+// ErrInvalidPlaybackFormat is returned by ConnectPlaybackStream when the
23+// provided PlaybackFormat has zero or negative fields. Callers can use
24+// errors.Is to check for this condition specifically.
25+var ErrInvalidPlaybackFormat = errors.New("invalid playback format")
26+
2127 var (
2228 errStreamCreate = &PWError{Func: "pw_stream_new_simple", Code: 0}
2329 errMainLoopCreate = &PWError{Func: "pw_main_loop_new", Code: 0}
@@ -90,9 +96,10 @@ func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer, format p
9096 // NOTE: The format fields are not yet forwarded to PipeWire as SPA params.
9197 // This validation is a temporary guard that will be superseded once SPA
9298 // param building is wired in a subsequent task.
99+ // Returns ErrInvalidPlaybackFormat (wrappable via errors.Is) for bad values.
93100 if format.SampleRate <= 0 || format.Channels <= 0 || format.FramesPerBuffer <= 0 {
94- return fmt.Errorf("invalid PlaybackFormat: SampleRate=%d, Channels=%d, FramesPerBuffer=%d",
95- format.SampleRate, format.Channels, format.FramesPerBuffer)
101+ return fmt.Errorf("%w: SampleRate=%d, Channels=%d, FramesPerBuffer=%d",
102+ ErrInvalidPlaybackFormat, format.SampleRate, format.Channels, format.FramesPerBuffer)
96103 }
97104 // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff
98105 // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
@@ -1,6 +1,7 @@
1 package capi1 package capi
2 2
3 import (3 import (
4+ "errors"
4 "fmt"5 "fmt"
5 "sync"6 "sync"
6 "unsafe"7 "unsafe"
@@ -18,6 +19,11 @@ func (e *PWError) Error() string {
18 return fmt.Sprintf("%s failed: %d", e.Func, e.Code)19 return fmt.Sprintf("%s failed: %d", e.Func, e.Code)
19 }20 }
20 21
22+// ErrInvalidPlaybackFormat is returned by ConnectPlaybackStream when the
23+// provided PlaybackFormat has zero or negative fields. Callers can use
24+// errors.Is to check for this condition specifically.
25+var ErrInvalidPlaybackFormat = errors.New("invalid playback format")
26+
21 var (27 var (
22 errStreamCreate = &PWError{Func: "pw_stream_new_simple", Code: 0}28 errStreamCreate = &PWError{Func: "pw_stream_new_simple", Code: 0}
23 errMainLoopCreate = &PWError{Func: "pw_main_loop_new", Code: 0}29 errMainLoopCreate = &PWError{Func: "pw_main_loop_new", Code: 0}
@@ -90,9 +96,10 @@ func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer, format p
90 // NOTE: The format fields are not yet forwarded to PipeWire as SPA params.96 // NOTE: The format fields are not yet forwarded to PipeWire as SPA params.
91 // This validation is a temporary guard that will be superseded once SPA97 // This validation is a temporary guard that will be superseded once SPA
92 // param building is wired in a subsequent task.98 // param building is wired in a subsequent task.
99+ // Returns ErrInvalidPlaybackFormat (wrappable via errors.Is) for bad values.
93 if format.SampleRate <= 0 || format.Channels <= 0 || format.FramesPerBuffer <= 0 {100 if format.SampleRate <= 0 || format.Channels <= 0 || format.FramesPerBuffer <= 0 {
94- return fmt.Errorf("invalid PlaybackFormat: SampleRate=%d, Channels=%d, FramesPerBuffer=%d",101+ return fmt.Errorf("%w: SampleRate=%d, Channels=%d, FramesPerBuffer=%d",
95- format.SampleRate, format.Channels, format.FramesPerBuffer)102+ ErrInvalidPlaybackFormat, format.SampleRate, format.Channels, format.FramesPerBuffer)
96 }103 }
97 // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff104 // PW_DIRECTION_OUTPUT = 1, PW_ID_ANY = 0xffffffff
98 // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04105 // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
M internal/capi/stream_ops_test.go +10 -7
@@ -268,9 +268,9 @@ func TestDisconnectStreamReturnsNilOnSuccess(t *testing.T) {
268268 }
269269
270270 // TestConnectPlaybackStreamRejectsMissingFormat verifies that ConnectPlaybackStream
271-// rejects a zero-valued PlaybackFormat at the StreamOps layer. This is a temporary
272-// guard; once SPA params are wired, the rejection will also cover missing/invalid
273-// SPA param construction.
271+// rejects a zero-valued PlaybackFormat at the StreamOps layer by returning
272+// ErrInvalidPlaybackFormat. This is a temporary guard; once SPA params are
273+// wired, the rejection will also cover missing/invalid SPA param construction.
274274 func TestConnectPlaybackStreamRejectsMissingFormat(t *testing.T) {
275275 origConnect := pw_stream_connect
276276 t.Cleanup(func() { pw_stream_connect = origConnect })
@@ -290,14 +290,17 @@ func TestConnectPlaybackStreamRejectsMissingFormat(t *testing.T) {
290290 if err == nil {
291291 t.Fatal("expected error for zero-valued PlaybackFormat, got nil")
292292 }
293+ if !errors.Is(err, ErrInvalidPlaybackFormat) {
294+ t.Fatalf("expected error wrapping ErrInvalidPlaybackFormat, got: %v", err)
295+ }
293296 }
294297
295-// TestConnectPlaybackStreamBuildsParamsFromFormat verifies that ConnectPlaybackStream
298+// TestConnectPlaybackStreamAcceptsValidFormat verifies that ConnectPlaybackStream
296299 // accepts a valid PlaybackFormat and calls pw_stream_connect successfully.
297300 // NOTE: The format is not yet forwarded to PipeWire as SPA params; this test
298-// only confirms that the format is accepted at the seam. A subsequent task
299-// will add tests verifying that the format values reach the C call.
300-func TestConnectPlaybackStreamBuildsParamsFromFormat(t *testing.T) {
301+// only confirms that the format is accepted at the validation seam. A subsequent
302+// task will add tests verifying that the format values reach the C call.
303+func TestConnectPlaybackStreamAcceptsValidFormat(t *testing.T) {
301304 origConnect := pw_stream_connect
302305 t.Cleanup(func() { pw_stream_connect = origConnect })
303306
@@ -268,9 +268,9 @@ func TestDisconnectStreamReturnsNilOnSuccess(t *testing.T) {
268 }268 }
269 269
270 // TestConnectPlaybackStreamRejectsMissingFormat verifies that ConnectPlaybackStream270 // TestConnectPlaybackStreamRejectsMissingFormat verifies that ConnectPlaybackStream
271-// rejects a zero-valued PlaybackFormat at the StreamOps layer. This is a temporary271+// rejects a zero-valued PlaybackFormat at the StreamOps layer by returning
272-// guard; once SPA params are wired, the rejection will also cover missing/invalid272+// ErrInvalidPlaybackFormat. This is a temporary guard; once SPA params are
273-// SPA param construction.273+// wired, the rejection will also cover missing/invalid SPA param construction.
274 func TestConnectPlaybackStreamRejectsMissingFormat(t *testing.T) {274 func TestConnectPlaybackStreamRejectsMissingFormat(t *testing.T) {
275 origConnect := pw_stream_connect275 origConnect := pw_stream_connect
276 t.Cleanup(func() { pw_stream_connect = origConnect })276 t.Cleanup(func() { pw_stream_connect = origConnect })
@@ -290,14 +290,17 @@ func TestConnectPlaybackStreamRejectsMissingFormat(t *testing.T) {
290 if err == nil {290 if err == nil {
291 t.Fatal("expected error for zero-valued PlaybackFormat, got nil")291 t.Fatal("expected error for zero-valued PlaybackFormat, got nil")
292 }292 }
293+ if !errors.Is(err, ErrInvalidPlaybackFormat) {
294+ t.Fatalf("expected error wrapping ErrInvalidPlaybackFormat, got: %v", err)
295+ }
293 }296 }
294 297
295-// TestConnectPlaybackStreamBuildsParamsFromFormat verifies that ConnectPlaybackStream298+// TestConnectPlaybackStreamAcceptsValidFormat verifies that ConnectPlaybackStream
296 // accepts a valid PlaybackFormat and calls pw_stream_connect successfully.299 // accepts a valid PlaybackFormat and calls pw_stream_connect successfully.
297 // NOTE: The format is not yet forwarded to PipeWire as SPA params; this test300 // NOTE: The format is not yet forwarded to PipeWire as SPA params; this test
298-// only confirms that the format is accepted at the seam. A subsequent task301+// only confirms that the format is accepted at the validation seam. A subsequent
299-// will add tests verifying that the format values reach the C call.302+// task will add tests verifying that the format values reach the C call.
300-func TestConnectPlaybackStreamBuildsParamsFromFormat(t *testing.T) {303+func TestConnectPlaybackStreamAcceptsValidFormat(t *testing.T) {
301 origConnect := pw_stream_connect304 origConnect := pw_stream_connect
302 t.Cleanup(func() { pw_stream_connect = origConnect })305 t.Cleanup(func() { pw_stream_connect = origConnect })
303 306
M internal/ports/out/capi.go +2 -7
@@ -1,11 +1,6 @@
11 // Temporary manual composition of the outbound CAPI interface.
22 // This file bridges the gap until the generator emits a composite CAPI interface.
3+// NOTE: The CAPI type is now generated in capi_gen.go — this file only holds
4+// the narrative comment. The actual composite interface is capi_gen.go CAPI.
35
46 package out
5-
6-// CAPI is a composite interface combining all PipeWire API groups.
7-// It embeds InitAPI and LoopAPI for unified access to all bound functions.
8-type CAPI interface {
9- InitAPI
10- LoopAPI
11-}
@@ -1,11 +1,6 @@
1 // Temporary manual composition of the outbound CAPI interface.1 // Temporary manual composition of the outbound CAPI interface.
2 // This file bridges the gap until the generator emits a composite CAPI interface.2 // This file bridges the gap until the generator emits a composite CAPI interface.
3+// NOTE: The CAPI type is now generated in capi_gen.go — this file only holds
4+// the narrative comment. The actual composite interface is capi_gen.go CAPI.
3 5
4 package out6 package out
5-
6-// CAPI is a composite interface combining all PipeWire API groups.
7-// It embeds InitAPI and LoopAPI for unified access to all bound functions.
8-type CAPI interface {
9- InitAPI
10- LoopAPI
11-}
M internal/ports/out/stream_ops.go +3 -3
@@ -22,9 +22,9 @@ type StreamOps interface {
2222 // stream needs more audio data. Returns the stream pointer or an error.
2323 CreatePlaybackStream(loopPtr unsafe.Pointer, name string, onProcess func()) (streamPtr unsafe.Pointer, err error)
2424
25- // ConnectPlaybackStream connects the stream for playback output using the
26- // given format. The format is currently validated but not yet forwarded to
27- // PipeWire as SPA params; that wiring will happen in a subsequent task.
25+ // ConnectPlaybackStream connects the stream for playback output.
26+ // The format is validated but not yet forwarded to PipeWire as SPA params;
27+ // that wiring will happen in a subsequent task.
2828 ConnectPlaybackStream(streamPtr unsafe.Pointer, format PlaybackFormat) error
2929
3030 // SetStreamActive activates or deactivates the stream.
@@ -22,9 +22,9 @@ type StreamOps interface {
22 // stream needs more audio data. Returns the stream pointer or an error.22 // stream needs more audio data. Returns the stream pointer or an error.
23 CreatePlaybackStream(loopPtr unsafe.Pointer, name string, onProcess func()) (streamPtr unsafe.Pointer, err error)23 CreatePlaybackStream(loopPtr unsafe.Pointer, name string, onProcess func()) (streamPtr unsafe.Pointer, err error)
24 24
25- // ConnectPlaybackStream connects the stream for playback output using the25+ // ConnectPlaybackStream connects the stream for playback output.
26- // given format. The format is currently validated but not yet forwarded to26+ // The format is validated but not yet forwarded to PipeWire as SPA params;
27- // PipeWire as SPA params; that wiring will happen in a subsequent task.27+ // that wiring will happen in a subsequent task.
28 ConnectPlaybackStream(streamPtr unsafe.Pointer, format PlaybackFormat) error28 ConnectPlaybackStream(streamPtr unsafe.Pointer, format PlaybackFormat) error
29 29
30 // SetStreamActive activates or deactivates the stream.30 // SetStreamActive activates or deactivates the stream.

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