mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 18:58:54 +01:00
audio: Reimplement audio for JS with AudioBuffer (#146)
This commit is contained in:
parent
21e2b1ed7b
commit
34691dabbf
@ -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() {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
3
run.go
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user