mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2025-01-11 19:48: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()
|
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() {
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
3
run.go
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user