Skip to content

Commit 493d79b

Browse files
committed
performance: Speed up piaudio.ClearChan()
Improve the performance of piebiten's ClearChan() command. Before the change the complexity of command was O(n^2) in the worst case. Now it is O(n).
1 parent adb4ce6 commit 493d79b

File tree

3 files changed

+70
-70
lines changed

3 files changed

+70
-70
lines changed

_examples/audio/lowlevel/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ package main
99

1010
import (
1111
_ "embed"
12+
"log"
13+
1214
"github.com/elgopher/pi"
1315
"github.com/elgopher/pi/piaudio"
1416
"github.com/elgopher/pi/piebiten"
1517
"github.com/elgopher/pi/pievent"
1618
"github.com/elgopher/pi/piloop"
1719
"github.com/elgopher/pi/pimouse"
18-
"log"
1920
)
2021

2122
//go:embed "wave.wav"

_examples/audio/piano/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ package main
88

99
import (
1010
_ "embed"
11+
"math"
12+
"slices"
13+
1114
"github.com/elgopher/pi"
1215
"github.com/elgopher/pi/piaudio"
1316
"github.com/elgopher/pi/picofont"
1417
"github.com/elgopher/pi/piebiten"
1518
"github.com/elgopher/pi/pikey"
16-
"math"
17-
"slices"
1819
)
1920

2021
var (

piebiten/internal/audio/player.go

Lines changed: 65 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
package audio
55

66
import (
7-
"github.com/elgopher/pi/piaudio"
8-
"github.com/elgopher/pi/pimath"
97
"log"
108
"math"
119
"slices"
1210
"sort"
1311
"sync"
1412
"unsafe"
13+
14+
"github.com/elgopher/pi/piaudio"
15+
"github.com/elgopher/pi/pimath"
1516
)
1617

18+
const chanLen = 4
19+
1720
func newPlayer() *player {
1821
defaultChannel := channel{
1922
pitch: 1.0,
@@ -25,7 +28,7 @@ func newPlayer() *player {
2528
}
2629
return &player{
2730
samplesByAddr: map[uintptr]*piaudio.Sample{},
28-
channels: [4]channel{
31+
channels: [chanLen]channel{
2932
defaultChannel, defaultChannel, defaultChannel, defaultChannel,
3033
},
3134
}
@@ -34,9 +37,9 @@ func newPlayer() *player {
3437
type player struct {
3538
mutex sync.Mutex
3639
samplesByAddr map[uintptr]*piaudio.Sample
37-
channels [4]channel
40+
channels [chanLen]channel
3841

39-
commandsByTime []command // all planned commands sorted by time
42+
commandsByTime [chanLen][]command // each channel's planned commands sorted by time
4043

4144
currentTime float64
4245
}
@@ -115,51 +118,48 @@ func (p *player) Read(out []byte) (n int, err error) {
115118
}
116119

117120
func (p *player) runCommands() {
118-
processed := 0
121+
for i := 0; i < chanLen; i++ {
122+
selectedChan := &p.channels[i]
119123

120-
for _, cmd := range p.commandsByTime {
121-
if cmd.time > p.currentTime {
122-
break
123-
}
124+
processed := 0
124125

125-
for i := 0; i < 4; i++ {
126-
selectedChan := &p.channels[i]
127-
chanNum := piaudio.Chan(1 << i)
128-
// a single command can be executed on multiple channels at once
129-
if cmd.ch&chanNum == chanNum {
130-
switch cmd.kind {
131-
case cmdKindSetSample:
132-
switch {
133-
case cmd.sampleAddr == 0:
134-
selectedChan.active = false
135-
selectedChan.sampleData = nil
136-
case p.samplesByAddr[cmd.sampleAddr] == nil:
137-
log.Printf("[piaudio] SetSample failed: Sample not found, addr: 0x%x", cmd.sampleAddr)
138-
selectedChan.active = false
139-
selectedChan.sampleData = nil
140-
default:
141-
selectedChan.active = true
142-
sample := p.samplesByAddr[cmd.sampleAddr]
143-
selectedChan.sampleData = sample.Data()
144-
selectedChan.sampleRate = sample.SampleRate()
145-
}
146-
selectedChan.position = float64(cmd.offset)
147-
case cmdKindSetLoop:
148-
selectedChan.loop = cmd.loop
149-
case cmdKindSetPitch:
150-
selectedChan.pitch = cmd.pitch
151-
case cmdKindSetVolume:
152-
selectedChan.volume = cmd.vol
153-
case cmdKindClearChan:
154-
// ClearChan was already called in SendCommands
126+
for _, cmd := range p.commandsByTime[i] {
127+
if cmd.time > p.currentTime {
128+
break
129+
}
130+
131+
switch cmd.kind {
132+
case cmdKindSetSample:
133+
switch {
134+
case cmd.sampleAddr == 0:
135+
selectedChan.active = false
136+
selectedChan.sampleData = nil
137+
case p.samplesByAddr[cmd.sampleAddr] == nil:
138+
log.Printf("[piaudio] SetSample failed: Sample not found, addr: 0x%x", cmd.sampleAddr)
139+
selectedChan.active = false
140+
selectedChan.sampleData = nil
141+
default:
142+
selectedChan.active = true
143+
sample := p.samplesByAddr[cmd.sampleAddr]
144+
selectedChan.sampleData = sample.Data()
145+
selectedChan.sampleRate = sample.SampleRate()
155146
}
147+
selectedChan.position = float64(cmd.offset)
148+
case cmdKindSetLoop:
149+
selectedChan.loop = cmd.loop
150+
case cmdKindSetPitch:
151+
selectedChan.pitch = cmd.pitch
152+
case cmdKindSetVolume:
153+
selectedChan.volume = cmd.vol
154+
case cmdKindClearChan:
155+
// ClearChan was already called in SendCommands
156156
}
157+
processed++
157158
}
158-
processed++
159-
}
160159

161-
copy(p.commandsByTime, p.commandsByTime[processed:])
162-
p.commandsByTime = p.commandsByTime[:len(p.commandsByTime)-processed]
160+
copy(p.commandsByTime[i], p.commandsByTime[i][processed:])
161+
p.commandsByTime[i] = p.commandsByTime[i][:len(p.commandsByTime[i])-processed]
162+
}
163163
}
164164

165165
func (p *player) read(out []byte) {
@@ -215,34 +215,32 @@ func (p *player) SendCommands(cmds []command) {
215215
p.clearChan(cmd.ch, cmd.time)
216216
continue
217217
}
218-
p.commandsByTime = append(p.commandsByTime, cmd)
218+
for i := 0; i < chanLen; i++ {
219+
chanNum := piaudio.Chan(1 << i)
220+
// a single command can be executed on multiple channels at once
221+
if cmd.ch&chanNum != 0 {
222+
p.commandsByTime[i] = append(p.commandsByTime[i], cmd)
223+
}
224+
}
219225
}
220226

221-
// sort again by time, because new commands may have been inserted between existing ones
222-
sort.SliceStable(p.commandsByTime, func(i, j int) bool {
223-
return p.commandsByTime[i].time < p.commandsByTime[j].time
224-
})
227+
for _, commands := range p.commandsByTime {
228+
// sort again by time, because new commands may have been inserted between existing ones
229+
sort.SliceStable(commands, func(i, j int) bool {
230+
return commands[i].time < commands[j].time
231+
})
232+
}
225233
}
226234

227-
// clearChan is O(n^2).
228-
// It could be optimized to use a separate command list for each channel.
229-
// Then complexity will be O(n)
230235
func (p *player) clearChan(ch piaudio.Chan, time float64) {
231-
for j := len(p.commandsByTime) - 1; j >= 0; j-- {
232-
cmd := p.commandsByTime[j]
233-
noMoreCommands := cmd.time < time
234-
if noMoreCommands {
235-
return
236-
}
237-
if cmd.ch&ch != 0 {
238-
remaining := cmd.ch &^ ch
239-
if remaining == 0 {
240-
// remove cmd
241-
copy(p.commandsByTime[j:], p.commandsByTime[j+1:])
242-
p.commandsByTime = p.commandsByTime[:len(p.commandsByTime)-1]
243-
} else {
244-
// update cmd to apply only to the remaining channels
245-
p.commandsByTime[j].ch = remaining
236+
for i := 0; i < chanLen; i++ {
237+
chanNum := piaudio.Chan(1 << i)
238+
if ch&chanNum != 0 {
239+
idx := slices.IndexFunc(p.commandsByTime[i], func(c command) bool {
240+
return c.time >= time
241+
})
242+
if idx != -1 {
243+
p.commandsByTime[i] = p.commandsByTime[i][:idx]
246244
}
247245
}
248246
}

0 commit comments

Comments
 (0)