detached 958d550

refactor: split player RT helpers into dedicated file

commit
958d5504e5c5da6dab06271b1e4e8607384100d3
author
brice · · 2026-04-09 15:39 UTC
parent
175a2c8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +145 -145
M internal/core/player.go +0 -145
@@ -2,7 +2,6 @@ package core
22
33 import (
44 "errors"
5- "io"
65 "sync"
76 "sync/atomic"
87 )
@@ -12,55 +11,6 @@ var (
1211 ErrPlayerClosed = errors.New("player is closed")
1312 )
1413
15-// UnderrunPolicy determines how to handle underruns
16-type UnderrunPolicy int
17-
18-const (
19- UnderrunFillSilence UnderrunPolicy = iota
20- UnderrunFailFast
21-)
22-
23-// PCMBuffer represents a PCM audio buffer with deinterleaved samples
24-// Samples[channel][frame] format for cache efficiency
25-type PCMBuffer struct {
26- Frames int
27- Channels int
28- Stride int
29- Samples [][]float32
30-}
31-
32-// allocate allocates the Samples slice structure
33-func (b *PCMBuffer) allocate() {
34- if b.Samples == nil {
35- b.Samples = make([][]float32, b.Channels)
36- for ch := 0; ch < b.Channels; ch++ {
37- b.Samples[ch] = make([]float32, b.Frames)
38- }
39- }
40-}
41-
42-// PlayerConfig holds configuration for the player
43-type PlayerConfig struct {
44- FramesPerBuffer int
45- Channels int
46- UnderrunPolicy UnderrunPolicy
47-}
48-
49-// PlayerCallbacks holds callbacks for player events
50-type PlayerCallbacks struct {
51- OnStateChange func(PlayerState)
52- Fill func(*PCMBuffer) (int, error)
53- OnUnderrun func(int)
54- OnDrain func()
55- OnError func(error)
56-}
57-
58-// playerConfig is the internal alias for PlayerConfig
59-type playerConfig = PlayerConfig
60-
61-// playerCallbacks is the internal alias for PlayerCallbacks
62-type playerCallbacks = PlayerCallbacks
63-
6414 // player is the internal player shell with state machine
6515 type player struct {
6616 config playerConfig
@@ -91,11 +41,6 @@ func (p *player) setState(next PlayerState) {
9141 p.state.Store(int32(next))
9242 }
9343
94-// isPaused returns true if the player is paused
95-func (p *player) isPaused() bool {
96- return p.paused.Load()
97-}
98-
9944 // transition attempts to transition to the next state
10045 func (p *player) transition(next PlayerState) error {
10146 p.mu.Lock()
@@ -259,96 +204,6 @@ func (p *player) teardown() {
259204 // Minimal placeholder - will be implemented in future tasks
260205 }
261206
262-// processPCM processes PCM audio data for the given buffer
263-// Returns the number of frames processed, or an error
264-func (p *player) processPCM(buf *PCMBuffer) (int, error) {
265- // Ensure buffer is allocated
266- buf.allocate()
267-
268- state := p.State()
269-
270- // If paused or stopped, fill with silence and return
271- if state == PlayerStatePaused || state == PlayerStateStopped || state == PlayerStateIdle {
272- p.fillSilence(buf, 0, buf.Frames)
273- return buf.Frames, nil
274- }
275-
276- // If not playing, fill with silence
277- if state != PlayerStatePlaying {
278- p.fillSilence(buf, 0, buf.Frames)
279- return buf.Frames, nil
280- }
281-
282- // Call Fill callback
283- if p.callbacks.Fill == nil {
284- p.fillSilence(buf, 0, buf.Frames)
285- return buf.Frames, nil
286- }
287-
288- frames, err := p.callbacks.Fill(buf)
289- if err != nil {
290- if errors.Is(err, io.EOF) {
291- // Drain condition - emit drain callback and return frames
292- p.emitDrain()
293- return frames, nil
294- }
295- // Other error - fail the player
296- p.fail(err)
297- return 0, err
298- }
299-
300- // Handle underrun if Fill returned fewer frames than requested
301- if frames < buf.Frames {
302- return p.handleUnderrun(buf, frames)
303- }
304-
305- return frames, nil
306-}
307-
308-// fillSilence fills the buffer with silence (0.0) from startFrame to endFrame
309-func (p *player) fillSilence(buf *PCMBuffer, startFrame, endFrame int) {
310- for ch := 0; ch < buf.Channels; ch++ {
311- for frame := startFrame; frame < endFrame && frame < len(buf.Samples[ch]); frame++ {
312- buf.Samples[ch][frame] = 0.0
313- }
314- }
315-}
316-
317-// handleUnderrun applies the underrun policy when Fill returns fewer frames than requested
318-func (p *player) handleUnderrun(buf *PCMBuffer, filledFrames int) (int, error) {
319- // Emit underrun callback
320- if p.callbacks.OnUnderrun != nil {
321- p.callbacks.OnUnderrun(buf.Frames - filledFrames)
322- }
323-
324- switch p.config.UnderrunPolicy {
325- case UnderrunFailFast:
326- err := errors.New("underrun occurred")
327- p.fail(err)
328- return 0, err
329- case UnderrunFillSilence:
330- fallthrough
331- default:
332- p.fillSilence(buf, filledFrames, buf.Frames)
333- return buf.Frames, nil
334- }
335-}
336-
337-// emitDrain invokes the OnDrain callback
338-func (p *player) emitDrain() {
339- if p.callbacks.OnDrain != nil {
340- p.callbacks.OnDrain()
341- }
342-}
343-
344-// fail transitions the player to error state and invokes OnError
345-func (p *player) fail(err error) {
346- p.transition(PlayerStateError)
347- if p.callbacks.OnError != nil {
348- p.callbacks.OnError(err)
349- }
350-}
351-
352207 // Player is the exported wrapper for the internal player
353208 type Player struct {
354209 p *player
@@ -2,7 +2,6 @@ package core
2 2
3 import (3 import (
4 "errors"4 "errors"
5- "io"
6 "sync"5 "sync"
7 "sync/atomic"6 "sync/atomic"
8 )7 )
@@ -12,55 +11,6 @@ var (
12 ErrPlayerClosed = errors.New("player is closed")11 ErrPlayerClosed = errors.New("player is closed")
13 )12 )
14 13
15-// UnderrunPolicy determines how to handle underruns
16-type UnderrunPolicy int
17-
18-const (
19- UnderrunFillSilence UnderrunPolicy = iota
20- UnderrunFailFast
21-)
22-
23-// PCMBuffer represents a PCM audio buffer with deinterleaved samples
24-// Samples[channel][frame] format for cache efficiency
25-type PCMBuffer struct {
26- Frames int
27- Channels int
28- Stride int
29- Samples [][]float32
30-}
31-
32-// allocate allocates the Samples slice structure
33-func (b *PCMBuffer) allocate() {
34- if b.Samples == nil {
35- b.Samples = make([][]float32, b.Channels)
36- for ch := 0; ch < b.Channels; ch++ {
37- b.Samples[ch] = make([]float32, b.Frames)
38- }
39- }
40-}
41-
42-// PlayerConfig holds configuration for the player
43-type PlayerConfig struct {
44- FramesPerBuffer int
45- Channels int
46- UnderrunPolicy UnderrunPolicy
47-}
48-
49-// PlayerCallbacks holds callbacks for player events
50-type PlayerCallbacks struct {
51- OnStateChange func(PlayerState)
52- Fill func(*PCMBuffer) (int, error)
53- OnUnderrun func(int)
54- OnDrain func()
55- OnError func(error)
56-}
57-
58-// playerConfig is the internal alias for PlayerConfig
59-type playerConfig = PlayerConfig
60-
61-// playerCallbacks is the internal alias for PlayerCallbacks
62-type playerCallbacks = PlayerCallbacks
63-
64 // player is the internal player shell with state machine14 // player is the internal player shell with state machine
65 type player struct {15 type player struct {
66 config playerConfig16 config playerConfig
@@ -91,11 +41,6 @@ func (p *player) setState(next PlayerState) {
91 p.state.Store(int32(next))41 p.state.Store(int32(next))
92 }42 }
93 43
94-// isPaused returns true if the player is paused
95-func (p *player) isPaused() bool {
96- return p.paused.Load()
97-}
98-
99 // transition attempts to transition to the next state44 // transition attempts to transition to the next state
100 func (p *player) transition(next PlayerState) error {45 func (p *player) transition(next PlayerState) error {
101 p.mu.Lock()46 p.mu.Lock()
@@ -259,96 +204,6 @@ func (p *player) teardown() {
259 // Minimal placeholder - will be implemented in future tasks204 // Minimal placeholder - will be implemented in future tasks
260 }205 }
261 206
262-// processPCM processes PCM audio data for the given buffer
263-// Returns the number of frames processed, or an error
264-func (p *player) processPCM(buf *PCMBuffer) (int, error) {
265- // Ensure buffer is allocated
266- buf.allocate()
267-
268- state := p.State()
269-
270- // If paused or stopped, fill with silence and return
271- if state == PlayerStatePaused || state == PlayerStateStopped || state == PlayerStateIdle {
272- p.fillSilence(buf, 0, buf.Frames)
273- return buf.Frames, nil
274- }
275-
276- // If not playing, fill with silence
277- if state != PlayerStatePlaying {
278- p.fillSilence(buf, 0, buf.Frames)
279- return buf.Frames, nil
280- }
281-
282- // Call Fill callback
283- if p.callbacks.Fill == nil {
284- p.fillSilence(buf, 0, buf.Frames)
285- return buf.Frames, nil
286- }
287-
288- frames, err := p.callbacks.Fill(buf)
289- if err != nil {
290- if errors.Is(err, io.EOF) {
291- // Drain condition - emit drain callback and return frames
292- p.emitDrain()
293- return frames, nil
294- }
295- // Other error - fail the player
296- p.fail(err)
297- return 0, err
298- }
299-
300- // Handle underrun if Fill returned fewer frames than requested
301- if frames < buf.Frames {
302- return p.handleUnderrun(buf, frames)
303- }
304-
305- return frames, nil
306-}
307-
308-// fillSilence fills the buffer with silence (0.0) from startFrame to endFrame
309-func (p *player) fillSilence(buf *PCMBuffer, startFrame, endFrame int) {
310- for ch := 0; ch < buf.Channels; ch++ {
311- for frame := startFrame; frame < endFrame && frame < len(buf.Samples[ch]); frame++ {
312- buf.Samples[ch][frame] = 0.0
313- }
314- }
315-}
316-
317-// handleUnderrun applies the underrun policy when Fill returns fewer frames than requested
318-func (p *player) handleUnderrun(buf *PCMBuffer, filledFrames int) (int, error) {
319- // Emit underrun callback
320- if p.callbacks.OnUnderrun != nil {
321- p.callbacks.OnUnderrun(buf.Frames - filledFrames)
322- }
323-
324- switch p.config.UnderrunPolicy {
325- case UnderrunFailFast:
326- err := errors.New("underrun occurred")
327- p.fail(err)
328- return 0, err
329- case UnderrunFillSilence:
330- fallthrough
331- default:
332- p.fillSilence(buf, filledFrames, buf.Frames)
333- return buf.Frames, nil
334- }
335-}
336-
337-// emitDrain invokes the OnDrain callback
338-func (p *player) emitDrain() {
339- if p.callbacks.OnDrain != nil {
340- p.callbacks.OnDrain()
341- }
342-}
343-
344-// fail transitions the player to error state and invokes OnError
345-func (p *player) fail(err error) {
346- p.transition(PlayerStateError)
347- if p.callbacks.OnError != nil {
348- p.callbacks.OnError(err)
349- }
350-}
351-
352 // Player is the exported wrapper for the internal player207 // Player is the exported wrapper for the internal player
353 type Player struct {208 type Player struct {
354 p *player209 p *player
A internal/core/player_rt.go +145 -0
@@ -0,0 +1,145 @@
1+package core
2+
3+import (
4+ "errors"
5+ "io"
6+)
7+
8+// UnderrunPolicy determines how to handle underruns
9+type UnderrunPolicy int
10+
11+const (
12+ UnderrunFillSilence UnderrunPolicy = iota
13+ UnderrunFailFast
14+)
15+
16+// PCMBuffer represents a PCM audio buffer with deinterleaved samples
17+// Samples[channel][frame] format for cache efficiency
18+type PCMBuffer struct {
19+ Frames int
20+ Channels int
21+ Stride int
22+ Samples [][]float32
23+}
24+
25+// allocate allocates the Samples slice structure
26+func (b *PCMBuffer) allocate() {
27+ if b.Samples == nil {
28+ b.Samples = make([][]float32, b.Channels)
29+ for ch := 0; ch < b.Channels; ch++ {
30+ b.Samples[ch] = make([]float32, b.Frames)
31+ }
32+ }
33+}
34+
35+// PlayerConfig holds configuration for the player
36+type PlayerConfig struct {
37+ FramesPerBuffer int
38+ Channels int
39+ UnderrunPolicy UnderrunPolicy
40+}
41+
42+// PlayerCallbacks holds callbacks for player events
43+type PlayerCallbacks struct {
44+ OnStateChange func(PlayerState)
45+ Fill func(*PCMBuffer) (int, error)
46+ OnUnderrun func(int)
47+ OnDrain func()
48+ OnError func(error)
49+}
50+
51+// playerConfig is the internal alias for PlayerConfig
52+type playerConfig = PlayerConfig
53+
54+// playerCallbacks is the internal alias for PlayerCallbacks
55+type playerCallbacks = PlayerCallbacks
56+
57+// processPCM processes PCM audio data for the given buffer
58+// Returns the number of frames processed, or an error
59+func (p *player) processPCM(buf *PCMBuffer) (int, error) {
60+ // Ensure buffer is allocated
61+ buf.allocate()
62+
63+ state := p.State()
64+
65+ // If paused or stopped, fill with silence and return
66+ if state == PlayerStatePaused || state == PlayerStateStopped || state == PlayerStateIdle {
67+ p.fillSilence(buf, 0, buf.Frames)
68+ return buf.Frames, nil
69+ }
70+
71+ // If not playing, fill with silence
72+ if state != PlayerStatePlaying {
73+ p.fillSilence(buf, 0, buf.Frames)
74+ return buf.Frames, nil
75+ }
76+
77+ // Call Fill callback
78+ if p.callbacks.Fill == nil {
79+ p.fillSilence(buf, 0, buf.Frames)
80+ return buf.Frames, nil
81+ }
82+
83+ frames, err := p.callbacks.Fill(buf)
84+ if err != nil {
85+ if errors.Is(err, io.EOF) {
86+ // Drain condition - emit drain callback and return frames
87+ p.emitDrain()
88+ return frames, nil
89+ }
90+ // Other error - fail the player
91+ p.fail(err)
92+ return 0, err
93+ }
94+
95+ // Handle underrun if Fill returned fewer frames than requested
96+ if frames < buf.Frames {
97+ return p.handleUnderrun(buf, frames)
98+ }
99+
100+ return frames, nil
101+}
102+
103+// fillSilence fills the buffer with silence (0.0) from startFrame to endFrame
104+func (p *player) fillSilence(buf *PCMBuffer, startFrame, endFrame int) {
105+ for ch := 0; ch < buf.Channels; ch++ {
106+ for frame := startFrame; frame < endFrame && frame < len(buf.Samples[ch]); frame++ {
107+ buf.Samples[ch][frame] = 0.0
108+ }
109+ }
110+}
111+
112+// handleUnderrun applies the underrun policy when Fill returns fewer frames than requested
113+func (p *player) handleUnderrun(buf *PCMBuffer, filledFrames int) (int, error) {
114+ // Emit underrun callback
115+ if p.callbacks.OnUnderrun != nil {
116+ p.callbacks.OnUnderrun(buf.Frames - filledFrames)
117+ }
118+
119+ switch p.config.UnderrunPolicy {
120+ case UnderrunFailFast:
121+ err := errors.New("underrun occurred")
122+ p.fail(err)
123+ return 0, err
124+ case UnderrunFillSilence:
125+ fallthrough
126+ default:
127+ p.fillSilence(buf, filledFrames, buf.Frames)
128+ return buf.Frames, nil
129+ }
130+}
131+
132+// emitDrain invokes the OnDrain callback
133+func (p *player) emitDrain() {
134+ if p.callbacks.OnDrain != nil {
135+ p.callbacks.OnDrain()
136+ }
137+}
138+
139+// fail transitions the player to error state and invokes OnError
140+func (p *player) fail(err error) {
141+ p.transition(PlayerStateError)
142+ if p.callbacks.OnError != nil {
143+ p.callbacks.OnError(err)
144+ }
145+}
@@ -0,0 +1,145 @@
1+package core
2+
3+import (
4+ "errors"
5+ "io"
6+)
7+
8+// UnderrunPolicy determines how to handle underruns
9+type UnderrunPolicy int
10+
11+const (
12+ UnderrunFillSilence UnderrunPolicy = iota
13+ UnderrunFailFast
14+)
15+
16+// PCMBuffer represents a PCM audio buffer with deinterleaved samples
17+// Samples[channel][frame] format for cache efficiency
18+type PCMBuffer struct {
19+ Frames int
20+ Channels int
21+ Stride int
22+ Samples [][]float32
23+}
24+
25+// allocate allocates the Samples slice structure
26+func (b *PCMBuffer) allocate() {
27+ if b.Samples == nil {
28+ b.Samples = make([][]float32, b.Channels)
29+ for ch := 0; ch < b.Channels; ch++ {
30+ b.Samples[ch] = make([]float32, b.Frames)
31+ }
32+ }
33+}
34+
35+// PlayerConfig holds configuration for the player
36+type PlayerConfig struct {
37+ FramesPerBuffer int
38+ Channels int
39+ UnderrunPolicy UnderrunPolicy
40+}
41+
42+// PlayerCallbacks holds callbacks for player events
43+type PlayerCallbacks struct {
44+ OnStateChange func(PlayerState)
45+ Fill func(*PCMBuffer) (int, error)
46+ OnUnderrun func(int)
47+ OnDrain func()
48+ OnError func(error)
49+}
50+
51+// playerConfig is the internal alias for PlayerConfig
52+type playerConfig = PlayerConfig
53+
54+// playerCallbacks is the internal alias for PlayerCallbacks
55+type playerCallbacks = PlayerCallbacks
56+
57+// processPCM processes PCM audio data for the given buffer
58+// Returns the number of frames processed, or an error
59+func (p *player) processPCM(buf *PCMBuffer) (int, error) {
60+ // Ensure buffer is allocated
61+ buf.allocate()
62+
63+ state := p.State()
64+
65+ // If paused or stopped, fill with silence and return
66+ if state == PlayerStatePaused || state == PlayerStateStopped || state == PlayerStateIdle {
67+ p.fillSilence(buf, 0, buf.Frames)
68+ return buf.Frames, nil
69+ }
70+
71+ // If not playing, fill with silence
72+ if state != PlayerStatePlaying {
73+ p.fillSilence(buf, 0, buf.Frames)
74+ return buf.Frames, nil
75+ }
76+
77+ // Call Fill callback
78+ if p.callbacks.Fill == nil {
79+ p.fillSilence(buf, 0, buf.Frames)
80+ return buf.Frames, nil
81+ }
82+
83+ frames, err := p.callbacks.Fill(buf)
84+ if err != nil {
85+ if errors.Is(err, io.EOF) {
86+ // Drain condition - emit drain callback and return frames
87+ p.emitDrain()
88+ return frames, nil
89+ }
90+ // Other error - fail the player
91+ p.fail(err)
92+ return 0, err
93+ }
94+
95+ // Handle underrun if Fill returned fewer frames than requested
96+ if frames < buf.Frames {
97+ return p.handleUnderrun(buf, frames)
98+ }
99+
100+ return frames, nil
101+}
102+
103+// fillSilence fills the buffer with silence (0.0) from startFrame to endFrame
104+func (p *player) fillSilence(buf *PCMBuffer, startFrame, endFrame int) {
105+ for ch := 0; ch < buf.Channels; ch++ {
106+ for frame := startFrame; frame < endFrame && frame < len(buf.Samples[ch]); frame++ {
107+ buf.Samples[ch][frame] = 0.0
108+ }
109+ }
110+}
111+
112+// handleUnderrun applies the underrun policy when Fill returns fewer frames than requested
113+func (p *player) handleUnderrun(buf *PCMBuffer, filledFrames int) (int, error) {
114+ // Emit underrun callback
115+ if p.callbacks.OnUnderrun != nil {
116+ p.callbacks.OnUnderrun(buf.Frames - filledFrames)
117+ }
118+
119+ switch p.config.UnderrunPolicy {
120+ case UnderrunFailFast:
121+ err := errors.New("underrun occurred")
122+ p.fail(err)
123+ return 0, err
124+ case UnderrunFillSilence:
125+ fallthrough
126+ default:
127+ p.fillSilence(buf, filledFrames, buf.Frames)
128+ return buf.Frames, nil
129+ }
130+}
131+
132+// emitDrain invokes the OnDrain callback
133+func (p *player) emitDrain() {
134+ if p.callbacks.OnDrain != nil {
135+ p.callbacks.OnDrain()
136+ }
137+}
138+
139+// fail transitions the player to error state and invokes OnError
140+func (p *player) fail(err error) {
141+ p.transition(PlayerStateError)
142+ if p.callbacks.OnError != nil {
143+ p.callbacks.OnError(err)
144+ }
145+}

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