Repository navigation
Expand file tree
/
Copy pathplayer.go
More file actions
129 lines (116 loc) · 3.04 KB
/
Copy pathplayer.go
File metadata and controls
129 lines (116 loc) · 3.04 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package meowcaller
import (
"io"
"sync"
)
// PlayerState is a Player's lifecycle state.
type PlayerState int
const (
// PlayerIdle means no source is playing (none set, stopped, or finished).
PlayerIdle PlayerState = iota
// PlayerPlaying means the active source is streaming into the call.
PlayerPlaying
// PlayerPaused means playback is suspended (silence is sent in the meantime).
PlayerPaused
)
// Player streams an AudioSource into a Call — the discord.js AudioPlayer analogue.
// Attach it with Call.Subscribe (or Call.Play as a shortcut); while Playing, the call
// pulls frames from the source on the codec's 60 ms cadence. When the source reaches
// io.EOF the player returns to PlayerIdle and fires OnFinish (queue the next source
// there for gapless playback).
type Player struct {
mu sync.Mutex
src AudioSource
state PlayerState
onFinish func()
}
// NewPlayer returns an idle Player.
func NewPlayer() *Player {
return &Player{state: PlayerIdle}
}
// Play sets src as the active source and starts playback, replacing (and closing) any
// current source.
func (p *Player) Play(src AudioSource) {
p.mu.Lock()
old := p.src
p.src = src
p.state = PlayerPlaying
p.mu.Unlock()
if old != nil {
_ = old.Close()
}
}
// Pause suspends playback; the call sends silence until Resume.
func (p *Player) Pause() {
p.mu.Lock()
if p.state == PlayerPlaying {
p.state = PlayerPaused
}
p.mu.Unlock()
}
// Resume continues a paused player.
func (p *Player) Resume() {
p.mu.Lock()
if p.state == PlayerPaused {
p.state = PlayerPlaying
}
p.mu.Unlock()
}
// Stop halts playback and closes the active source.
func (p *Player) Stop() {
p.mu.Lock()
old := p.src
p.src = nil
p.state = PlayerIdle
p.mu.Unlock()
if old != nil {
_ = old.Close()
}
}
// State returns the current PlayerState.
func (p *Player) State() PlayerState {
p.mu.Lock()
defer p.mu.Unlock()
return p.state
}
// OnFinish registers a callback fired when the active source is exhausted (the player
// transitions to PlayerIdle). Replaces any previous callback.
func (p *Player) OnFinish(fn func()) {
p.mu.Lock()
p.onFinish = fn
p.mu.Unlock()
}
// nextFrame is pulled by the engine's send loop each frame interval. It returns the
// next source frame while Playing, or nil to send silence (idle/paused/between
// sources). On source EOF it goes Idle and fires OnFinish (outside the lock).
func (p *Player) nextFrame() []float32 {
p.mu.Lock()
if p.state != PlayerPlaying || p.src == nil {
p.mu.Unlock()
return nil
}
src := p.src
p.mu.Unlock()
frame, err := src.ReadFrame()
if err == nil {
return frame
}
// Source exhausted (or errored): go idle, close it, and fire OnFinish once.
p.mu.Lock()
var finish func()
if p.src == src { // still the active source (not replaced concurrently)
p.src = nil
p.state = PlayerIdle
finish = p.onFinish
}
p.mu.Unlock()
_ = src.Close()
if finish != nil {
finish()
}
if err != io.EOF {
// A decode error still ends playback; the frame (if any) is dropped.
return nil
}
return frame // may be a final padded frame or nil
}