From 9e7dfa2f16a3c5625f547151abc5ff2272b30dec Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 26 Jan 2015 01:20:56 +0900 Subject: [PATCH] audio: Bug fix: Multi source (or nodes) should be used to play multi sounds --- example/piano/main.go | 4 +-- internal/audio/audio.go | 44 +++++++++++++++++++++------ internal/audio/audio_js.go | 30 ++++++++++++------- internal/audio/audio_openal.go | 55 +++++++++++++++++++--------------- 4 files changed, 87 insertions(+), 46 deletions(-) diff --git a/example/piano/main.go b/example/piano/main.go index b09f69c9c..93908e6ff 100644 --- a/example/piano/main.go +++ b/example/piano/main.go @@ -43,7 +43,7 @@ func init() { a := amp[j] * math.Exp(-5*float64(i)/(x[j]*s)) v += a * math.Sin(float64(i)*twoPiF*float64(j+1)/s) } - pcm[i] = v + pcm[i] = v / 5.0 } } @@ -112,7 +112,7 @@ func update(screen *ebiten.Image) error { if keyStates[key] != 1 { continue } - addNote(220*math.Exp2(float64(i)/12.0), 1.0/8.0) + addNote(220*math.Exp2(float64(i)/12.0), 1.0) } ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f", ebiten.CurrentFPS())) return nil diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 5cbd16b4b..59083f4ff 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -23,12 +23,12 @@ var audioEnabled = false const bufferSize = 1024 const SampleRate = 44100 -var nextInsertionPosition = 0 var currentPosition = 0 type channel struct { - l []int16 - r []int16 + l []int16 + r []int16 + nextInsertionPosition int } var MaxChannel = 32 @@ -55,7 +55,7 @@ func Start() { func isPlaying(channel int) bool { ch := channels[channel] - return nextInsertionPosition < len(ch.l) + return ch.nextInsertionPosition < len(ch.l) } func channelAt(i int) *channel { @@ -88,8 +88,8 @@ func Play(channel int, l []int16, r []int16) bool { if ch == nil { return false } - ch.l = append(ch.l, make([]int16, nextInsertionPosition-len(ch.l))...) - ch.r = append(ch.r, make([]int16, nextInsertionPosition-len(ch.r))...) + ch.l = append(ch.l, make([]int16, ch.nextInsertionPosition-len(ch.l))...) + ch.r = append(ch.r, make([]int16, ch.nextInsertionPosition-len(ch.r))...) ch.l = append(ch.l, l...) ch.r = append(ch.r, r...) return true @@ -112,7 +112,9 @@ func Queue(channel int, l []int16, r []int16) { } func Update() { - nextInsertionPosition += SampleRate / 60 + for _, ch := range channels { + ch.nextInsertionPosition += SampleRate / 60 + } } func min(a, b int) int { @@ -138,7 +140,31 @@ func isChannelsEmpty() bool { return true } -func loadChannelBuffers() (l, r []int16) { +func loadChannelBuffer(channel int) (l, r []int16) { + channelsLock.Lock() + defer channelsLock.Unlock() + + if !audioEnabled { + return nil, nil + } + + ch := channels[channel] + inputL := make([]int16, bufferSize) + inputR := make([]int16, bufferSize) + length := min(len(ch.l), bufferSize) + for i := 0; i < length; i++ { + inputL[i] = ch.l[i] + inputR[i] = ch.r[i] + } + usedLen := min(bufferSize, len(ch.l)) + ch.l = ch.l[usedLen:] + ch.r = ch.r[usedLen:] + + ch.nextInsertionPosition -= min(bufferSize, ch.nextInsertionPosition) + return inputL, inputR +} + +/*func loadChannelBuffers() (l, r []int16) { channelsLock.Lock() defer channelsLock.Unlock() @@ -162,7 +188,7 @@ func loadChannelBuffers() (l, r []int16) { ch.r = ch.r[usedLen:] } return inputL, inputR -} +}*/ func IsPlaying(channel int) bool { channelsLock.Lock() diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go index 3ba89dcf3..908085e93 100644 --- a/internal/audio/audio_js.go +++ b/internal/audio/audio_js.go @@ -21,23 +21,18 @@ import ( ) // Keep this so as not to be destroyed by GC. -var node js.Object +var nodes = []js.Object{} var context js.Object -func initialize() { - context = js.Global.Get("AudioContext").New() - // TODO: ScriptProcessorNode will be replaced with Audio WebWorker. - // https://developer.mozilla.org/ja/docs/Web/API/ScriptProcessorNode - node = context.Call("createScriptProcessor", bufferSize, 0, 2) - node.Call("addEventListener", "audioprocess", func(e js.Object) { +func audioProcess(channel int) func(e js.Object) { + return func(e js.Object) { defer func() { currentPosition += bufferSize }() l := e.Get("outputBuffer").Call("getChannelData", 0) r := e.Get("outputBuffer").Call("getChannelData", 1) - inputL, inputR := loadChannelBuffers() - nextInsertionPosition -= min(bufferSize, nextInsertionPosition) + inputL, inputR := loadChannelBuffer(channel) const max = 1 << 15 for i := 0; i < bufferSize; i++ { // TODO: Use copyFromChannel? @@ -49,11 +44,24 @@ func initialize() { l.SetIndex(i, float64(inputL[i])/max) r.SetIndex(i, float64(inputR[i])/max) } - }) + } +} + +func initialize() { + context = js.Global.Get("AudioContext").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) + node.Call("addEventListener", "audioprocess", audioProcess(i)) + nodes = append(nodes, node) + } audioEnabled = true } func start() { // TODO: For iOS, node should be connected with a buffer node. - node.Call("connect", context.Get("destination")) + for _, node := range nodes { + node.Call("connect", context.Get("destination")) + } } diff --git a/internal/audio/audio_openal.go b/internal/audio/audio_openal.go index 1ddf4bc92..a6150bc5e 100644 --- a/internal/audio/audio_openal.go +++ b/internal/audio/audio_openal.go @@ -60,39 +60,46 @@ func initialize() { } audioEnabled = true - source := openal.NewSource() + sources := openal.NewSources(MaxChannel) close(ch) emptyBytes := make([]byte, 4*bufferSize) - const bufferNum = 16 - buffers := openal.NewBuffers(bufferNum) - for _, buffer := range buffers { - buffer.SetData(openal.FormatStereo16, emptyBytes, SampleRate) - source.QueueBuffer(buffer) - } - source.Play() - if alErr := openal.GetError(); alErr != 0 { - panic(fmt.Sprintf("OpenAL error: %d", alErr)) + for _, source := range sources { + const bufferNum = 16 + buffers := openal.NewBuffers(bufferNum) + for _, buffer := range buffers { + buffer.SetData(openal.FormatStereo16, emptyBytes, SampleRate) + source.QueueBuffer(buffer) + } + source.Play() + if alErr := openal.GetError(); alErr != 0 { + panic(fmt.Sprintf("OpenAL error: %d", alErr)) + } } for { - if source.State() != openal.Playing { - panic(fmt.Sprintf("invalid source state: %d (0x%[1]x)", source.State())) + oneProcessed := false + for channel, source := range sources { + if source.State() != openal.Playing { + panic(fmt.Sprintf("invalid source state: %d (0x%[1]x)", source.State())) + } + processed := source.BuffersProcessed() + if processed == 0 { + continue + } + oneProcessed = true + buffers := make([]openal.Buffer, processed) + source.UnqueueBuffers(buffers) + for _, buffer := range buffers { + l, r := loadChannelBuffer(channel) + b := toBytes(l, r) + buffer.SetData(openal.FormatStereo16, b, SampleRate) + source.QueueBuffer(buffer) + } } - processed := source.BuffersProcessed() - if processed == 0 { + if !oneProcessed { time.Sleep(1) - continue - } - buffers := make([]openal.Buffer, processed) - source.UnqueueBuffers(buffers) - for _, buffer := range buffers { - l, r := loadChannelBuffers() - b := toBytes(l, r) - buffer.SetData(openal.FormatStereo16, b, SampleRate) - source.QueueBuffer(buffer) - nextInsertionPosition -= min(bufferSize, nextInsertionPosition) } } }()