-
Notifications
You must be signed in to change notification settings - Fork 0
/
audioengine.go
123 lines (106 loc) · 3.81 KB
/
audioengine.go
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
/*
The audioengine package creates a portaudio stream and synthesises audio samples into the stream using Voices
it receives.
*/
package audioengine
import (
"sync"
"github.com/crnbaker/gostringsynth/errors"
"github.com/gordonklaus/portaudio"
)
type SynthVoice interface {
ShouldDie() bool
KillOnNextCycle()
SynthesizeSample() float32
IncrementAgeInSamples()
}
// VoiceController provides a portaudio output stream and attributes for keeping track of currently enabled voices.
type VoiceController struct {
*portaudio.Stream
activeVoices []SynthVoice
stagedVoices []SynthVoice
}
// setStream sets a portaudio stream to a VoiceController
func (vc *VoiceController) setStream(stream *portaudio.Stream) {
vc.Stream = stream
}
// stageVoice adds a Voice to the list of voices that will be enabled at the beginning of the next iteration of
// the audio output loop
func (vc *VoiceController) stageVoice(voice SynthVoice) {
vc.stagedVoices = append(vc.stagedVoices, voice)
}
// addVoice adds a voice to the list of currently active voices
func (vc *VoiceController) addVoice(voice SynthVoice) {
vc.activeVoices = append(vc.activeVoices, voice)
}
// activateStagedVoices activates all voices in the staged voices list
func (vc *VoiceController) activateStagedVoices() {
for _, voice := range vc.stagedVoices {
vc.addVoice(voice)
vc.stagedVoices = vc.stagedVoices[1:]
}
}
// killVoice deletes a voice from the list of active voices using its index
func (vc *VoiceController) killVoice(i int) {
vc.activeVoices = append(vc.activeVoices[:i], vc.activeVoices[i+1:]...)
}
// output is provided to portaudio as the audio generation callback function. It generates audio samples by
// summing the samples provided by the synthesis functions of the currently active Voices.
func (vc *VoiceController) output(out [][]float32) {
// Kill voices that are past their lifetime or have been "stolen"
numKilled := 0
for i, f := range vc.activeVoices {
if f.ShouldDie() {
vc.killVoice(i - numKilled)
numKilled++
}
}
// Activate new voices that have been staged for activation
vc.activateStagedVoices()
// Initialise buffer with zeros
for i := range out[0] {
out[0][i] = 0
out[1][i] = 0
}
// Add samples values synthesized by currently active voices
for i := range out[0] {
for j, f := range vc.activeVoices {
newSample := f.SynthesizeSample()
out[0][i] += newSample
out[1][i] += newSample
vc.activeVoices[j].IncrementAgeInSamples() // Use index because f is a copy
}
}
// Get current CPU load and kill oldest voice if it's too high
if vc.Stream.CpuLoad() > 0.7 { // portaudio docs say reasonable to expect to use 70% or more of CPU
vc.killVoice(0)
}
}
// newVoiceController constructs a VoiceController with a portaudio stream configured to use the output function
// as the audio generationo callback.
func newVoiceController(sampleRate float64) *VoiceController {
activeVoices := make([]SynthVoice, 0)
stagedVoices := make([]SynthVoice, 0)
engine := &VoiceController{nil, activeVoices, stagedVoices}
stream, err := portaudio.OpenDefaultStream(0, 2, sampleRate, portaudio.FramesPerBufferUnspecified, engine.output)
errors.Chk(err)
engine.setStream(stream)
return engine
}
// ControlVoices receives voices from the voiceReceiveChan and stages them for activation by the voice controller.
// It also implements voice stealing by marking the oldest voice for death if a maximum number of activated voices
// is exceeded.
func ControlVoices(waitGroup *sync.WaitGroup, voiceReceiveChan chan SynthVoice, sampleRate float64, voiceLimit int) {
defer waitGroup.Done()
portaudio.Initialize()
engine := newVoiceController(sampleRate)
engine.Start()
for f := range voiceReceiveChan {
if len(engine.activeVoices) == voiceLimit {
engine.activeVoices[0].KillOnNextCycle()
}
engine.stageVoice(f)
}
engine.Stop()
portaudio.Terminate()
}