detached 73b2a2a

feat: wire public player wrapper to internal core

commit
73b2a2aa61a0deca82c46ab82ea08da210f000f8
author
brice · · 2026-04-09 15:45 UTC
parent
958d550
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +148 -3
M internal/core/player.go +1 -1
@@ -73,7 +73,7 @@ func (p *player) transition(next PlayerState) error {
7373 func isValidTransition(current, next PlayerState) bool {
7474 switch current {
7575 case PlayerStateIdle:
76- return next == PlayerStateStarting
76+ return next == PlayerStateStarting || next == PlayerStateClosing
7777 case PlayerStateStarting:
7878 return next == PlayerStatePlaying || next == PlayerStateError
7979 case PlayerStatePlaying:
@@ -73,7 +73,7 @@ func (p *player) transition(next PlayerState) error {
73 func isValidTransition(current, next PlayerState) bool {73 func isValidTransition(current, next PlayerState) bool {
74 switch current {74 switch current {
75 case PlayerStateIdle:75 case PlayerStateIdle:
76- return next == PlayerStateStarting76+ return next == PlayerStateStarting || next == PlayerStateClosing
77 case PlayerStateStarting:77 case PlayerStateStarting:
78 return next == PlayerStatePlaying || next == PlayerStateError78 return next == PlayerStatePlaying || next == PlayerStateError
79 case PlayerStatePlaying:79 case PlayerStatePlaying:
M pipewire/player.go +44 -2
@@ -154,6 +154,15 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) {
154154 return nil, ErrInvalidPlayerConfig
155155 }
156156
157+ // Convert public UnderrunPolicy to internal UnderrunPolicy
158+ var coreUnderrunPolicy core.UnderrunPolicy
159+ switch config.UnderrunPolicy {
160+ case UnderrunFail:
161+ coreUnderrunPolicy = core.UnderrunFailFast
162+ default:
163+ coreUnderrunPolicy = core.UnderrunFillSilence
164+ }
165+
157166 // Convert public callbacks to internal callbacks
158167 coreCallbacks := core.PlayerCallbacks{
159168 OnStateChange: func(state core.PlayerState) {
@@ -161,10 +170,43 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) {
161170 callbacks.OnStateChange(PlayerState(state))
162171 }
163172 },
173+ Fill: func(buf *core.PCMBuffer) (int, error) {
174+ if callbacks.Fill == nil {
175+ return 0, nil
176+ }
177+ // Adapt internal PCMBuffer to public PCMBuffer
178+ publicBuf := &PCMBuffer{
179+ Frames: buf.Frames,
180+ Channels: buf.Channels,
181+ Stride: buf.Stride,
182+ Samples: buf.Samples,
183+ }
184+ return callbacks.Fill(publicBuf)
185+ },
186+ OnUnderrun: func(frames int) {
187+ if callbacks.OnUnderrun != nil {
188+ callbacks.OnUnderrun(frames)
189+ }
190+ },
191+ OnDrain: func() {
192+ if callbacks.OnDrain != nil {
193+ callbacks.OnDrain()
194+ }
195+ },
196+ OnError: func(err error) {
197+ if callbacks.OnError != nil {
198+ callbacks.OnError(err)
199+ }
200+ },
164201 }
165202
166- // Create internal player
167- internalPlayer := core.NewPlayer(core.PlayerConfig{}, coreCallbacks)
203+ // Create internal player with mapped config
204+ coreConfig := core.PlayerConfig{
205+ FramesPerBuffer: config.FramesPerBuffer,
206+ Channels: config.Channels,
207+ UnderrunPolicy: coreUnderrunPolicy,
208+ }
209+ internalPlayer := core.NewPlayer(coreConfig, coreCallbacks)
168210
169211 return &playerImpl{
170212 config: config,
@@ -154,6 +154,15 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) {
154 return nil, ErrInvalidPlayerConfig154 return nil, ErrInvalidPlayerConfig
155 }155 }
156 156
157+ // Convert public UnderrunPolicy to internal UnderrunPolicy
158+ var coreUnderrunPolicy core.UnderrunPolicy
159+ switch config.UnderrunPolicy {
160+ case UnderrunFail:
161+ coreUnderrunPolicy = core.UnderrunFailFast
162+ default:
163+ coreUnderrunPolicy = core.UnderrunFillSilence
164+ }
165+
157 // Convert public callbacks to internal callbacks166 // Convert public callbacks to internal callbacks
158 coreCallbacks := core.PlayerCallbacks{167 coreCallbacks := core.PlayerCallbacks{
159 OnStateChange: func(state core.PlayerState) {168 OnStateChange: func(state core.PlayerState) {
@@ -161,10 +170,43 @@ func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) {
161 callbacks.OnStateChange(PlayerState(state))170 callbacks.OnStateChange(PlayerState(state))
162 }171 }
163 },172 },
173+ Fill: func(buf *core.PCMBuffer) (int, error) {
174+ if callbacks.Fill == nil {
175+ return 0, nil
176+ }
177+ // Adapt internal PCMBuffer to public PCMBuffer
178+ publicBuf := &PCMBuffer{
179+ Frames: buf.Frames,
180+ Channels: buf.Channels,
181+ Stride: buf.Stride,
182+ Samples: buf.Samples,
183+ }
184+ return callbacks.Fill(publicBuf)
185+ },
186+ OnUnderrun: func(frames int) {
187+ if callbacks.OnUnderrun != nil {
188+ callbacks.OnUnderrun(frames)
189+ }
190+ },
191+ OnDrain: func() {
192+ if callbacks.OnDrain != nil {
193+ callbacks.OnDrain()
194+ }
195+ },
196+ OnError: func(err error) {
197+ if callbacks.OnError != nil {
198+ callbacks.OnError(err)
199+ }
200+ },
164 }201 }
165 202
166- // Create internal player203+ // Create internal player with mapped config
167- internalPlayer := core.NewPlayer(core.PlayerConfig{}, coreCallbacks)204+ coreConfig := core.PlayerConfig{
205+ FramesPerBuffer: config.FramesPerBuffer,
206+ Channels: config.Channels,
207+ UnderrunPolicy: coreUnderrunPolicy,
208+ }
209+ internalPlayer := core.NewPlayer(coreConfig, coreCallbacks)
168 210
169 return &playerImpl{211 return &playerImpl{
170 config: config,212 config: config,
M pipewire/player_test.go +103 -0
@@ -31,6 +31,22 @@ func TestNewPlayerAcceptsValidConfig(t *testing.T) {
3131 }
3232 }
3333
34+func TestPlayerStateReflectsLifecycle(t *testing.T) {
35+ config := PlayerConfig{
36+ SampleRate: 44100,
37+ Channels: 2,
38+ FramesPerBuffer: 256,
39+ SampleFormat: SampleFormatF32,
40+ }
41+ player, err := NewPlayer(config, PlayerCallbacks{})
42+ if err != nil {
43+ t.Fatalf("NewPlayer returned error: %v", err)
44+ }
45+ if player.State() != PlayerStateIdle {
46+ t.Fatalf("expected initial state PlayerStateIdle, got %v", player.State())
47+ }
48+}
49+
3450 func TestPlayerStateReflectsInitialState(t *testing.T) {
3551 config := PlayerConfig{
3652 SampleRate: 44100,
@@ -138,3 +154,90 @@ func TestPlayerCloseIsIdempotent(t *testing.T) {
138154 t.Fatalf("expected Closed state after second close, got %v", player.State())
139155 }
140156 }
157+
158+func TestPlayerCallbacksAreWired(t *testing.T) {
159+ var stateChanges []PlayerState
160+ var fillCalled bool
161+ var underrunFrames int
162+ var drainCalled bool
163+
164+ config := PlayerConfig{
165+ SampleRate: 44100,
166+ Channels: 2,
167+ FramesPerBuffer: 256,
168+ SampleFormat: SampleFormatF32,
169+ UnderrunPolicy: UnderrunFillSilence,
170+ }
171+
172+ callbacks := PlayerCallbacks{
173+ Fill: func(buf *PCMBuffer) (int, error) {
174+ fillCalled = true
175+ _ = fillCalled // Suppress unused warning for now
176+ return buf.Frames, nil
177+ },
178+ OnUnderrun: func(frames int) {
179+ underrunFrames = frames
180+ _ = underrunFrames // Suppress unused warning for now
181+ },
182+ OnDrain: func() {
183+ drainCalled = true
184+ _ = drainCalled // Suppress unused warning for now
185+ },
186+ OnStateChange: func(state PlayerState) {
187+ stateChanges = append(stateChanges, state)
188+ },
189+ }
190+
191+ player, err := NewPlayer(config, callbacks)
192+ if err != nil {
193+ t.Fatalf("NewPlayer returned error: %v", err)
194+ }
195+
196+ // Verify initial state
197+ if player.State() != PlayerStateIdle {
198+ t.Fatalf("expected PlayerStateIdle, got %v", player.State())
199+ }
200+
201+ // Start should trigger state change callback
202+ if err := player.Start(); err != nil {
203+ t.Fatalf("Start failed: %v", err)
204+ }
205+
206+ // Verify we got state change callbacks
207+ if len(stateChanges) == 0 {
208+ t.Fatal("expected OnStateChange callbacks, got none")
209+ }
210+
211+ // Clean up
212+ if err := player.Close(); err != nil {
213+ t.Fatalf("Close failed: %v", err)
214+ }
215+}
216+
217+func TestPlayerConfigMapsToCore(t *testing.T) {
218+ config := PlayerConfig{
219+ SampleRate: 48000,
220+ Channels: 4,
221+ FramesPerBuffer: 512,
222+ SampleFormat: SampleFormatF32,
223+ UnderrunPolicy: UnderrunFail,
224+ }
225+
226+ player, err := NewPlayer(config, PlayerCallbacks{})
227+ if err != nil {
228+ t.Fatalf("NewPlayer returned error: %v", err)
229+ }
230+ if player == nil {
231+ t.Fatal("NewPlayer returned nil player")
232+ }
233+
234+ // Player should be usable after creation
235+ if player.State() != PlayerStateIdle {
236+ t.Fatalf("expected PlayerStateIdle, got %v", player.State())
237+ }
238+
239+ // Clean up
240+ if err := player.Close(); err != nil {
241+ t.Fatalf("Close failed: %v", err)
242+ }
243+}
@@ -31,6 +31,22 @@ func TestNewPlayerAcceptsValidConfig(t *testing.T) {
31 }31 }
32 }32 }
33 33
34+func TestPlayerStateReflectsLifecycle(t *testing.T) {
35+ config := PlayerConfig{
36+ SampleRate: 44100,
37+ Channels: 2,
38+ FramesPerBuffer: 256,
39+ SampleFormat: SampleFormatF32,
40+ }
41+ player, err := NewPlayer(config, PlayerCallbacks{})
42+ if err != nil {
43+ t.Fatalf("NewPlayer returned error: %v", err)
44+ }
45+ if player.State() != PlayerStateIdle {
46+ t.Fatalf("expected initial state PlayerStateIdle, got %v", player.State())
47+ }
48+}
49+
34 func TestPlayerStateReflectsInitialState(t *testing.T) {50 func TestPlayerStateReflectsInitialState(t *testing.T) {
35 config := PlayerConfig{51 config := PlayerConfig{
36 SampleRate: 44100,52 SampleRate: 44100,
@@ -138,3 +154,90 @@ func TestPlayerCloseIsIdempotent(t *testing.T) {
138 t.Fatalf("expected Closed state after second close, got %v", player.State())154 t.Fatalf("expected Closed state after second close, got %v", player.State())
139 }155 }
140 }156 }
157+
158+func TestPlayerCallbacksAreWired(t *testing.T) {
159+ var stateChanges []PlayerState
160+ var fillCalled bool
161+ var underrunFrames int
162+ var drainCalled bool
163+
164+ config := PlayerConfig{
165+ SampleRate: 44100,
166+ Channels: 2,
167+ FramesPerBuffer: 256,
168+ SampleFormat: SampleFormatF32,
169+ UnderrunPolicy: UnderrunFillSilence,
170+ }
171+
172+ callbacks := PlayerCallbacks{
173+ Fill: func(buf *PCMBuffer) (int, error) {
174+ fillCalled = true
175+ _ = fillCalled // Suppress unused warning for now
176+ return buf.Frames, nil
177+ },
178+ OnUnderrun: func(frames int) {
179+ underrunFrames = frames
180+ _ = underrunFrames // Suppress unused warning for now
181+ },
182+ OnDrain: func() {
183+ drainCalled = true
184+ _ = drainCalled // Suppress unused warning for now
185+ },
186+ OnStateChange: func(state PlayerState) {
187+ stateChanges = append(stateChanges, state)
188+ },
189+ }
190+
191+ player, err := NewPlayer(config, callbacks)
192+ if err != nil {
193+ t.Fatalf("NewPlayer returned error: %v", err)
194+ }
195+
196+ // Verify initial state
197+ if player.State() != PlayerStateIdle {
198+ t.Fatalf("expected PlayerStateIdle, got %v", player.State())
199+ }
200+
201+ // Start should trigger state change callback
202+ if err := player.Start(); err != nil {
203+ t.Fatalf("Start failed: %v", err)
204+ }
205+
206+ // Verify we got state change callbacks
207+ if len(stateChanges) == 0 {
208+ t.Fatal("expected OnStateChange callbacks, got none")
209+ }
210+
211+ // Clean up
212+ if err := player.Close(); err != nil {
213+ t.Fatalf("Close failed: %v", err)
214+ }
215+}
216+
217+func TestPlayerConfigMapsToCore(t *testing.T) {
218+ config := PlayerConfig{
219+ SampleRate: 48000,
220+ Channels: 4,
221+ FramesPerBuffer: 512,
222+ SampleFormat: SampleFormatF32,
223+ UnderrunPolicy: UnderrunFail,
224+ }
225+
226+ player, err := NewPlayer(config, PlayerCallbacks{})
227+ if err != nil {
228+ t.Fatalf("NewPlayer returned error: %v", err)
229+ }
230+ if player == nil {
231+ t.Fatal("NewPlayer returned nil player")
232+ }
233+
234+ // Player should be usable after creation
235+ if player.State() != PlayerStateIdle {
236+ t.Fatalf("expected PlayerStateIdle, got %v", player.State())
237+ }
238+
239+ // Clean up
240+ if err := player.Close(); err != nil {
241+ t.Fatalf("Close failed: %v", err)
242+ }
243+}

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