Skip to content

Commit 6c8d2eb

Browse files
committed
refactor: move internal audio source code to a separate package
1 parent 3f14410 commit 6c8d2eb

File tree

8 files changed

+184
-178
lines changed

8 files changed

+184
-178
lines changed

piebiten/internal/audio/backend.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2025 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package audio
5+
6+
import (
7+
"github.com/elgopher/pi/piaudio"
8+
"github.com/elgopher/pi/pimath"
9+
"github.com/hajimehoshi/ebiten/v2/audio"
10+
"time"
11+
)
12+
13+
const CtxSampleRate = 44100
14+
15+
func StartAudioBackend(ctx *audio.Context) *Backend {
16+
timeFromPlayer := make(chan float64, 100)
17+
18+
thePlayer := newPlayer(timeFromPlayer)
19+
ebitenPlayer, err := ctx.NewPlayer(thePlayer)
20+
if err != nil {
21+
panic("failed to create Ebitengine player: " + err.Error())
22+
}
23+
ebitenPlayer.SetBufferSize(time.Duration(audioBufferSizeInSeconds * float64(time.Second)))
24+
25+
b := &Backend{
26+
ctx: ctx,
27+
timeFromPlayer: timeFromPlayer,
28+
player: thePlayer,
29+
ebitenPlayer: ebitenPlayer,
30+
}
31+
32+
return b
33+
}
34+
35+
// Backend also works in browsers but may glitch if the garbage collector
36+
// blocks the main thread for too long. The backend should ideally use the
37+
// AudioWorklet API to avoid audio glitches.
38+
type Backend struct {
39+
ctx *audio.Context
40+
timeFromPlayer chan float64
41+
commands []command
42+
currentTime float64
43+
player *player
44+
ebitenPlayer *audio.Player
45+
}
46+
47+
func (b *Backend) LoadSample(sample *piaudio.Sample) {
48+
b.player.LoadSample(sample)
49+
}
50+
51+
func (b *Backend) UnloadSample(sample *piaudio.Sample) {
52+
b.player.UnloadSample(sample)
53+
}
54+
55+
func (b *Backend) scheduleTime(delay float64) float64 {
56+
return b.currentTime + delay + audioBufferSizeInSeconds
57+
}
58+
59+
func (b *Backend) SetSample(ch piaudio.Chan, sample *piaudio.Sample, offset int, delay float64) {
60+
b.commands = append(b.commands,
61+
command{
62+
kind: cmdKindSetSample,
63+
ch: ch,
64+
sampleAddr: getPointerAddr(sample),
65+
offset: offset,
66+
time: b.scheduleTime(delay),
67+
},
68+
)
69+
}
70+
71+
type loop struct {
72+
start, stop int
73+
loopType piaudio.LoopType
74+
}
75+
76+
func (b *Backend) SetLoop(ch piaudio.Chan, start, length int, loopType piaudio.LoopType, delay float64) {
77+
b.commands = append(b.commands,
78+
command{
79+
kind: cmdKindSetLoop,
80+
ch: ch,
81+
loop: loop{
82+
start: start,
83+
stop: start + length - 1,
84+
loopType: loopType,
85+
},
86+
time: b.scheduleTime(delay),
87+
},
88+
)
89+
}
90+
91+
func (b *Backend) ClearChan(ch piaudio.Chan, delay float64) {
92+
b.commands = append(b.commands,
93+
command{
94+
kind: cmdKindClearChan,
95+
ch: ch,
96+
time: b.scheduleTime(delay),
97+
},
98+
)
99+
}
100+
101+
func (b *Backend) SetPitch(ch piaudio.Chan, pitch float64, delay float64) {
102+
if pitch < 0 {
103+
pitch = 0
104+
}
105+
b.commands = append(b.commands,
106+
command{
107+
kind: cmdKindSetPitch,
108+
ch: ch,
109+
pitch: pitch,
110+
time: b.scheduleTime(delay),
111+
},
112+
)
113+
}
114+
115+
func (b *Backend) SetVolume(ch piaudio.Chan, vol float64, delay float64) {
116+
vol = pimath.Clamp(vol, 0, 1)
117+
118+
b.commands = append(b.commands,
119+
command{
120+
kind: cmdKindSetVolume,
121+
ch: ch,
122+
vol: vol,
123+
time: b.scheduleTime(delay),
124+
},
125+
)
126+
}
127+
128+
type cmdKind string
129+
130+
const (
131+
cmdKindSetSample cmdKind = "setSample"
132+
cmdKindSetLoop cmdKind = "setLoop"
133+
cmdKindClearChan cmdKind = "clearChan"
134+
cmdKindSetPitch cmdKind = "setPitch"
135+
cmdKindSetVolume cmdKind = "setVolume"
136+
)
137+
138+
type command struct {
139+
kind cmdKind
140+
ch piaudio.Chan
141+
sampleAddr uintptr
142+
offset int
143+
pitch float64
144+
time float64
145+
vol float64
146+
loop loop
147+
}
148+
149+
func (b *Backend) OnBeforeUpdate() {
150+
if !b.ebitenPlayer.IsPlaying() {
151+
b.ebitenPlayer.Play()
152+
}
153+
154+
for {
155+
select {
156+
case t := <-b.timeFromPlayer:
157+
b.currentTime = t
158+
piaudio.Time = t
159+
default:
160+
return
161+
}
162+
}
163+
}
164+
165+
func (b *Backend) OnAfterUpdate() {
166+
b.player.SendCommands(b.commands)
167+
b.commands = b.commands[:0]
168+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2025 Jacek Olszak
22
// This code is licensed under MIT license (see LICENSE for details)
33

4-
package internal
4+
package audio
55

66
const audioBufferSizeInSeconds = 0.04 // 40ms
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2025 Jacek Olszak
22
// This code is licensed under MIT license (see LICENSE for details)
33

4-
package internal
4+
package audio
55

66
const audioBufferSizeInSeconds = 0.06 // 60ms
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
//go:build freebsd || linux || netbsd || openbsd
55

6-
package internal
6+
package audio
77

88
const audioBufferSizeInSeconds = 0.02 // 20ms
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2025 Jacek Olszak
22
// This code is licensed under MIT license (see LICENSE for details)
33

4-
package internal
4+
package audio
55

66
const audioBufferSizeInSeconds = 0.02 // 20ms
Lines changed: 3 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,19 @@
11
// Copyright 2025 Jacek Olszak
22
// This code is licensed under MIT license (see LICENSE for details)
33

4-
package internal
4+
package audio
55

66
import (
77
"github.com/elgopher/pi/piaudio"
88
"github.com/elgopher/pi/pimath"
9-
"github.com/hajimehoshi/ebiten/v2/audio"
109
"log"
1110
"math"
1211
"slices"
1312
"sort"
1413
"sync"
15-
"time"
1614
"unsafe"
1715
)
1816

19-
const CtxSampleRate = 44100
20-
21-
func StartAudioBackend(ctx *audio.Context) *AudioBackend {
22-
timeFromPlayer := make(chan float64, 100)
23-
24-
thePlayer := newPlayer(timeFromPlayer)
25-
ebitenPlayer, err := ctx.NewPlayer(thePlayer)
26-
if err != nil {
27-
panic("failed to create Ebitengine player: " + err.Error())
28-
}
29-
ebitenPlayer.SetBufferSize(time.Duration(audioBufferSizeInSeconds * float64(time.Second)))
30-
31-
b := &AudioBackend{
32-
ctx: ctx,
33-
timeFromPlayer: timeFromPlayer,
34-
player: thePlayer,
35-
ebitenPlayer: ebitenPlayer,
36-
}
37-
38-
return b
39-
}
40-
41-
// AudioBackend also works in browsers but may glitch if the garbage collector
42-
// blocks the main thread for too long. The backend should ideally use the
43-
// AudioWorklet API to avoid audio glitches.
44-
type AudioBackend struct {
45-
ctx *audio.Context
46-
timeFromPlayer chan float64
47-
commands []command
48-
currentTime float64
49-
player *player
50-
ebitenPlayer *audio.Player
51-
}
52-
53-
func (b *AudioBackend) LoadSample(sample *piaudio.Sample) {
54-
b.player.LoadSample(sample)
55-
}
56-
57-
func (b *AudioBackend) UnloadSample(sample *piaudio.Sample) {
58-
b.player.UnloadSample(sample)
59-
}
60-
61-
func (b *AudioBackend) scheduleTime(delay float64) float64 {
62-
return b.currentTime + delay + audioBufferSizeInSeconds
63-
}
64-
65-
func (b *AudioBackend) SetSample(ch piaudio.Chan, sample *piaudio.Sample, offset int, delay float64) {
66-
b.commands = append(b.commands,
67-
command{
68-
kind: cmdKindSetSample,
69-
ch: ch,
70-
sampleAddr: getPointerAddr(sample),
71-
offset: offset,
72-
time: b.scheduleTime(delay),
73-
},
74-
)
75-
}
76-
77-
type loop struct {
78-
start, stop int
79-
loopType piaudio.LoopType
80-
}
81-
82-
func (b *AudioBackend) SetLoop(ch piaudio.Chan, start, length int, loopType piaudio.LoopType, delay float64) {
83-
b.commands = append(b.commands,
84-
command{
85-
kind: cmdKindSetLoop,
86-
ch: ch,
87-
loop: loop{
88-
start: start,
89-
stop: start + length - 1,
90-
loopType: loopType,
91-
},
92-
time: b.scheduleTime(delay),
93-
},
94-
)
95-
}
96-
97-
func (b *AudioBackend) ClearChan(ch piaudio.Chan, delay float64) {
98-
b.commands = append(b.commands,
99-
command{
100-
kind: cmdKindClearChan,
101-
ch: ch,
102-
time: b.scheduleTime(delay),
103-
},
104-
)
105-
}
106-
107-
func (b *AudioBackend) SetPitch(ch piaudio.Chan, pitch float64, delay float64) {
108-
if pitch < 0 {
109-
pitch = 0
110-
}
111-
b.commands = append(b.commands,
112-
command{
113-
kind: cmdKindSetPitch,
114-
ch: ch,
115-
pitch: pitch,
116-
time: b.scheduleTime(delay),
117-
},
118-
)
119-
}
120-
121-
func (b *AudioBackend) SetVolume(ch piaudio.Chan, vol float64, delay float64) {
122-
vol = pimath.Clamp(vol, 0, 1)
123-
124-
b.commands = append(b.commands,
125-
command{
126-
kind: cmdKindSetVolume,
127-
ch: ch,
128-
vol: vol,
129-
time: b.scheduleTime(delay),
130-
},
131-
)
132-
}
133-
134-
type cmdKind string
135-
136-
const (
137-
cmdKindSetSample cmdKind = "setSample"
138-
cmdKindSetLoop cmdKind = "setLoop"
139-
cmdKindClearChan cmdKind = "clearChan"
140-
cmdKindSetPitch cmdKind = "setPitch"
141-
cmdKindSetVolume cmdKind = "setVolume"
142-
)
143-
144-
type command struct {
145-
kind cmdKind
146-
ch piaudio.Chan
147-
sampleAddr uintptr
148-
offset int
149-
pitch float64
150-
time float64
151-
vol float64
152-
loop loop
153-
}
154-
155-
func (b *AudioBackend) OnBeforeUpdate() {
156-
for {
157-
select {
158-
case t := <-b.timeFromPlayer:
159-
b.currentTime = t
160-
piaudio.Time = t
161-
default:
162-
return
163-
}
164-
}
165-
}
166-
167-
func (b *AudioBackend) OnAfterUpdate() {
168-
b.player.SendCommands(commandBatch{
169-
time: b.currentTime,
170-
cmds: b.commands,
171-
})
172-
b.commands = b.commands[:0]
173-
}
174-
17517
func newPlayer(timeFromPlayer chan float64) *player {
17618
defaultChannel := channel{
17719
pitch: 1.0,
@@ -367,16 +209,11 @@ func writeInt16LE(out []byte, val float64) {
367209
out[1] = byte(sample >> 8)
368210
}
369211

370-
type commandBatch struct {
371-
time float64
372-
cmds []command
373-
}
374-
375-
func (p *player) SendCommands(batch commandBatch) {
212+
func (p *player) SendCommands(cmds []command) {
376213
p.mutex.Lock()
377214
defer p.mutex.Unlock()
378215

379-
for _, cmd := range batch.cmds {
216+
for _, cmd := range cmds {
380217
if cmd.time < p.currentTime {
381218
log.Printf("Discarding late audio command with time %f, but current time is %f", cmd.time, p.currentTime)
382219
continue

0 commit comments

Comments
 (0)