player.go RawHistoryFolder
1package pipewire
2
3import (
4 "errors"
5
6 "github.com/bnema/purego-pipewire/internal/core"
7)
8
9// SampleFormat represents the audio sample format type.
10type SampleFormat int
11
12const (
13 // SampleFormatF32 is the 32-bit floating point sample format.
14 SampleFormatF32 SampleFormat = iota
15)
16
17// UnderrunPolicy defines the behavior when a buffer underrun occurs.
18type UnderrunPolicy int
19
20const (
21 // UnderrunFillSilence fills the buffer with silence on underrun.
22 UnderrunFillSilence UnderrunPolicy = iota
23 // UnderrunFail fails playback on underrun.
24 UnderrunFail
25)
26
27// PlayerState represents the current state of the audio player.
28type PlayerState int
29
30const (
31 // PlayerStateIdle is the initial state before the player is started.
32 PlayerStateIdle PlayerState = iota
33 // PlayerStateStarting indicates the player is initializing.
34 PlayerStateStarting
35 // PlayerStatePlaying indicates the player is actively outputting audio.
36 PlayerStatePlaying
37 // PlayerStatePaused indicates the player is temporarily stopped.
38 PlayerStatePaused
39 // PlayerStateStopped indicates the player has been stopped.
40 PlayerStateStopped
41 // PlayerStateClosing indicates the player is shutting down.
42 PlayerStateClosing
43 // PlayerStateClosed indicates the player has been fully closed.
44 PlayerStateClosed
45 // PlayerStateError indicates the player encountered an error.
46 PlayerStateError
47)
48
49// Legacy aliases for backwards compatibility (deprecated)
50const (
51 Idle = PlayerStateIdle
52 Starting = PlayerStateStarting
53 Playing = PlayerStatePlaying
54 Paused = PlayerStatePaused
55 Stopped = PlayerStateStopped
56 Closing = PlayerStateClosing
57 Closed = PlayerStateClosed
58 Error = PlayerStateError
59)
60
61// PlayerConfig holds the configuration for a new audio player.
62type PlayerConfig struct {
63 SampleRate int
64 Channels int
65 FramesPerBuffer int
66 SampleFormat SampleFormat
67 UnderrunPolicy UnderrunPolicy
68}
69
70// PCMBuffer represents a buffer of PCM audio data.
71type PCMBuffer struct {
72 Frames int
73 Channels int
74 Stride int
75 Samples [][]float32
76}
77
78// PlayerCallbacks contains all callback functions for player events.
79type PlayerCallbacks struct {
80 Fill func(*PCMBuffer) (int, error)
81 OnUnderrun func(int)
82 OnDrain func()
83 OnError func(error)
84 OnStateChange func(PlayerState)
85}
86
87// Player is the public interface for audio playback.
88type Player interface {
89 Start() error
90 Pause() error
91 Stop() error
92 Close() error
93 State() PlayerState
94}
95
96// ErrInvalidPlayerConfig is returned when the player configuration is invalid.
97var ErrInvalidPlayerConfig = errors.New("invalid player config")
98
99// playerImpl wraps the internal core player
100type playerImpl struct {
101 config PlayerConfig
102 callbacks PlayerCallbacks
103 internal *core.Player
104 scratchBuf PCMBuffer // reusable buffer for Fill callback to avoid allocation
105}
106
107func (p *playerImpl) Start() error {
108 if p.internal == nil {
109 return errors.New("player not initialized")
110 }
111 return p.internal.Start()
112}
113
114func (p *playerImpl) Pause() error {
115 if p.internal == nil {
116 return errors.New("player not initialized")
117 }
118 return p.internal.Pause()
119}
120
121func (p *playerImpl) Stop() error {
122 if p.internal == nil {
123 return errors.New("player not initialized")
124 }
125 return p.internal.Stop()
126}
127
128func (p *playerImpl) Close() error {
129 if p.internal == nil {
130 return nil
131 }
132 return p.internal.Close()
133}
134
135func (p *playerImpl) State() PlayerState {
136 if p.internal == nil {
137 return PlayerStateIdle
138 }
139 return PlayerState(p.internal.State())
140}
141
142// NewPlayer creates a new audio player with the given configuration and callbacks.
143// Returns ErrInvalidPlayerConfig if the configuration is invalid.
144func NewPlayer(config PlayerConfig, callbacks PlayerCallbacks) (Player, error) {
145 if config.SampleRate <= 0 {
146 return nil, ErrInvalidPlayerConfig
147 }
148 if config.Channels <= 0 {
149 return nil, ErrInvalidPlayerConfig
150 }
151 if config.FramesPerBuffer <= 0 {
152 return nil, ErrInvalidPlayerConfig
153 }
154 if config.SampleFormat != SampleFormatF32 {
155 return nil, ErrInvalidPlayerConfig
156 }
157
158 // Convert public UnderrunPolicy to internal UnderrunPolicy
159 var coreUnderrunPolicy core.UnderrunPolicy
160 switch config.UnderrunPolicy {
161 case UnderrunFail:
162 coreUnderrunPolicy = core.UnderrunFailFast
163 default:
164 coreUnderrunPolicy = core.UnderrunFillSilence
165 }
166
167 // Create the player implementation early so we can use its scratch buffer
168 p := &playerImpl{
169 config: config,
170 callbacks: callbacks,
171 }
172
173 // Convert public callbacks to internal callbacks
174 coreCallbacks := core.PlayerCallbacks{
175 OnStateChange: func(state core.PlayerState) {
176 if callbacks.OnStateChange != nil {
177 callbacks.OnStateChange(PlayerState(state))
178 }
179 },
180 Fill: func(buf *core.PCMBuffer) (int, error) {
181 if callbacks.Fill == nil {
182 return buf.Frames, nil
183 }
184 // Reuse scratch buffer by updating its fields
185 p.scratchBuf.Frames = buf.Frames
186 p.scratchBuf.Channels = buf.Channels
187 p.scratchBuf.Stride = buf.Stride
188 p.scratchBuf.Samples = buf.Samples
189 return callbacks.Fill(&p.scratchBuf)
190 },
191 OnUnderrun: func(frames int) {
192 if callbacks.OnUnderrun != nil {
193 callbacks.OnUnderrun(frames)
194 }
195 },
196 OnDrain: func() {
197 if callbacks.OnDrain != nil {
198 callbacks.OnDrain()
199 }
200 },
201 OnError: func(err error) {
202 if callbacks.OnError != nil {
203 callbacks.OnError(err)
204 }
205 },
206 }
207
208 // Create internal player with mapped config
209 coreConfig := core.PlayerConfig{
210 SampleRate: config.SampleRate,
211 FramesPerBuffer: config.FramesPerBuffer,
212 Channels: config.Channels,
213 UnderrunPolicy: coreUnderrunPolicy,
214 }
215 p.internal = core.NewPlayer(coreConfig, coreCallbacks)
216
217 return p, nil
218}