audio: Reimplement audio for JS with AudioBuffer (#146)

This commit is contained in:
Hajime Hoshi 2016-02-09 22:35:55 +09:00
parent 21e2b1ed7b
commit 34691dabbf
4 changed files with 59 additions and 45 deletions

View File

@ -50,11 +50,6 @@ func withChannels(f func()) {
f()
}
func isPlaying(channel int) bool {
ch := channels[channel]
return 0 < len(ch.buffer)
}
func channelAt(i int) *channel {
if i == -1 {
for i, _ := range channels {
@ -70,6 +65,11 @@ func channelAt(i int) *channel {
return nil
}
func Tick() {
tick()
}
// TODO: Accept sample rate
func Queue(channel int, data []byte) bool {
result := true
withChannels(func() {

View File

@ -20,25 +20,20 @@ import (
"github.com/gopherjs/gopherjs/js"
)
// Known issue (#146): We can't change the sample rate of a AudioContext.
// This means that if AudioContext's sample rate is 48000,
// there is no way to play 44.1 kHz PCM properly without resampling.
// Let's wait for the new spec and browser implementation:
// https://github.com/WebAudio/web-audio-api/issues/300
// Keep this so as not to be destroyed by GC.
// Keep this as global so as not to be destroyed by GC.
var (
nodes = []*js.Object{}
dummies = []*js.Object{} // Dummy source nodes for iOS.
context *js.Object
)
const bufferSize = 1024
type audioProcessor struct {
channel int
channel int
position float64
}
var audioProcessors [MaxChannel]*audioProcessor
func toLR(data []byte) ([]int16, []int16) {
l := make([]int16, len(data)/4)
r := make([]int16, len(data)/4)
@ -49,22 +44,42 @@ func toLR(data []byte) ([]int16, []int16) {
return l, r
}
func (a *audioProcessor) Process(e *js.Object) {
// Can't use goroutines here. Probably it may cause race conditions.
b := e.Get("outputBuffer")
func (a *audioProcessor) playChunk(buf []byte, sampleRate int) {
if len(buf) == 0 {
return
}
const channelNum = 2
const bytesPerSample = channelNum * 16 / 8
b := context.Call("createBuffer", channelNum, len(buf)/bytesPerSample, sampleRate)
l := b.Call("getChannelData", 0)
r := b.Call("getChannelData", 1)
inputL, inputR := toLR(loadChannelBuffer(a.channel, bufferSize*4))
il, ir := toLR(buf)
const max = 1 << 15
for i := 0; i < bufferSize; i++ {
// TODO: Use copyToChannel?
if i < len(inputL) {
l.SetIndex(i, float64(inputL[i])/max)
r.SetIndex(i, float64(inputR[i])/max)
} else {
l.SetIndex(i, 0)
r.SetIndex(i, 0)
}
for i := 0; i < len(il); i++ {
l.SetIndex(i, float64(il[i])/max)
r.SetIndex(i, float64(ir[i])/max)
}
s := context.Call("createBufferSource")
s.Set("buffer", b)
s.Call("connect", context.Get("destination"))
c := context.Get("currentTime").Float()
if a.position < c {
a.position = c
}
s.Call("start", a.position)
a.position += float64(len(il)) / float64(sampleRate)
}
func isPlaying(channel int) bool {
c := context.Get("currentTime").Float()
return c < audioProcessors[channel].position
}
func tick() {
const bufferSize = 1024
const sampleRate = 44100 // TODO: This should be changeable
for _, a := range audioProcessors {
a.playChunk(loadChannelBuffer(a.channel, bufferSize*4), sampleRate)
}
}
@ -82,23 +97,11 @@ func initialize() {
return
}
context = class.New()
// TODO: ScriptProcessorNode will be replaced with Audio WebWorker.
// https://developer.mozilla.org/ja/docs/Web/API/ScriptProcessorNode
for i := 0; i < MaxChannel; i++ {
node := context.Call("createScriptProcessor", bufferSize, 0, 2)
processor := &audioProcessor{i}
node.Call("addEventListener", "audioprocess", processor.Process)
nodes = append(nodes, node)
dummy := context.Call("createBufferSource")
dummies = append(dummies, dummy)
}
audioEnabled = true
destination := context.Get("destination")
for i, node := range nodes {
dummy := dummies[i]
dummy.Call("connect", node)
node.Call("connect", destination)
for i := 0; i < len(audioProcessors); i++ {
audioProcessors[i] = &audioProcessor{
channel: i,
position: 0,
}
}
}

View File

@ -24,6 +24,14 @@ import (
"golang.org/x/mobile/exp/audio/al"
)
func isPlaying(channel int) bool {
ch := channels[channel]
return 0 < len(ch.buffer)
}
func tick() {
}
func initialize() {
// Creating OpenAL device must be done after initializing UI. I'm not sure the reason.
ch := make(chan struct{})

3
run.go
View File

@ -17,6 +17,7 @@ package ebiten
import (
"time"
"github.com/hajimehoshi/ebiten/internal/audio"
"github.com/hajimehoshi/ebiten/internal/ui"
)
@ -92,6 +93,8 @@ func Run(f func(*Image) error, width, height, scale int, title string) error {
runContext.newScreenHeight = 0
runContext.newScreenScale = 0
audio.Tick()
if err := ui.DoEvents(); err != nil {
return err
}