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

View File

@ -20,25 +20,20 @@ import (
"github.com/gopherjs/gopherjs/js" "github.com/gopherjs/gopherjs/js"
) )
// Known issue (#146): We can't change the sample rate of a AudioContext. // Keep this as global so as not to be destroyed by GC.
// 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.
var ( var (
nodes = []*js.Object{} nodes = []*js.Object{}
dummies = []*js.Object{} // Dummy source nodes for iOS. dummies = []*js.Object{} // Dummy source nodes for iOS.
context *js.Object context *js.Object
) )
const bufferSize = 1024
type audioProcessor struct { type audioProcessor struct {
channel int channel int
position float64
} }
var audioProcessors [MaxChannel]*audioProcessor
func toLR(data []byte) ([]int16, []int16) { func toLR(data []byte) ([]int16, []int16) {
l := make([]int16, len(data)/4) l := make([]int16, len(data)/4)
r := make([]int16, len(data)/4) r := make([]int16, len(data)/4)
@ -49,22 +44,42 @@ func toLR(data []byte) ([]int16, []int16) {
return l, r return l, r
} }
func (a *audioProcessor) Process(e *js.Object) { func (a *audioProcessor) playChunk(buf []byte, sampleRate int) {
// Can't use goroutines here. Probably it may cause race conditions. if len(buf) == 0 {
b := e.Get("outputBuffer") return
}
const channelNum = 2
const bytesPerSample = channelNum * 16 / 8
b := context.Call("createBuffer", channelNum, len(buf)/bytesPerSample, sampleRate)
l := b.Call("getChannelData", 0) l := b.Call("getChannelData", 0)
r := b.Call("getChannelData", 1) r := b.Call("getChannelData", 1)
inputL, inputR := toLR(loadChannelBuffer(a.channel, bufferSize*4)) il, ir := toLR(buf)
const max = 1 << 15 const max = 1 << 15
for i := 0; i < bufferSize; i++ { for i := 0; i < len(il); i++ {
// TODO: Use copyToChannel? l.SetIndex(i, float64(il[i])/max)
if i < len(inputL) { r.SetIndex(i, float64(ir[i])/max)
l.SetIndex(i, float64(inputL[i])/max)
r.SetIndex(i, float64(inputR[i])/max)
} else {
l.SetIndex(i, 0)
r.SetIndex(i, 0)
} }
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 return
} }
context = class.New() 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 audioEnabled = true
for i := 0; i < len(audioProcessors); i++ {
destination := context.Get("destination") audioProcessors[i] = &audioProcessor{
for i, node := range nodes { channel: i,
dummy := dummies[i] position: 0,
dummy.Call("connect", node) }
node.Call("connect", destination)
} }
} }

View File

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

3
run.go
View File

@ -17,6 +17,7 @@ package ebiten
import ( import (
"time" "time"
"github.com/hajimehoshi/ebiten/internal/audio"
"github.com/hajimehoshi/ebiten/internal/ui" "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.newScreenHeight = 0
runContext.newScreenScale = 0 runContext.newScreenScale = 0
audio.Tick()
if err := ui.DoEvents(); err != nil { if err := ui.DoEvents(); err != nil {
return err return err
} }