From 355da1bcbc76745272d5421185fcabc982de1e0c Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 11 Jan 2015 01:23:43 +0900 Subject: [PATCH 1/7] Add Experimental Audio API --- _docs/public/example/blocks.content.html | 2 +- _docs/public/example/hue.content.html | 2 +- _docs/public/example/keyboard.content.html | 2 +- _docs/public/example/mosaic.content.html | 2 +- _docs/public/example/paint.content.html | 2 +- _docs/public/example/perspective.content.html | 2 +- _docs/public/example/rotate.content.html | 2 +- audio.go | 23 +++++ example/audio/main.go | 87 +++++++++++++++++++ example/server/main.go | 2 +- internal/audio/audio.go | 29 +++++++ internal/audio/audio_js.go | 78 +++++++++++++++++ internal/ui/ui_glfw.go | 6 ++ internal/ui/ui_js.go | 9 ++ 14 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 audio.go create mode 100644 example/audio/main.go create mode 100644 internal/audio/audio.go create mode 100644 internal/audio/audio_js.go diff --git a/_docs/public/example/blocks.content.html b/_docs/public/example/blocks.content.html index 9ba0f8567..8083b265f 100644 --- a/_docs/public/example/blocks.content.html +++ b/_docs/public/example/blocks.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'blocks.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/_docs/public/example/hue.content.html b/_docs/public/example/hue.content.html index 66fecdf3f..c5dcb62c6 100644 --- a/_docs/public/example/hue.content.html +++ b/_docs/public/example/hue.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'hue.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/_docs/public/example/keyboard.content.html b/_docs/public/example/keyboard.content.html index b68a5b8b6..c35a01574 100644 --- a/_docs/public/example/keyboard.content.html +++ b/_docs/public/example/keyboard.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'keyboard.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/_docs/public/example/mosaic.content.html b/_docs/public/example/mosaic.content.html index 581739780..87ae921b5 100644 --- a/_docs/public/example/mosaic.content.html +++ b/_docs/public/example/mosaic.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'mosaic.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/_docs/public/example/paint.content.html b/_docs/public/example/paint.content.html index 2d7fb4dd8..66b75e748 100644 --- a/_docs/public/example/paint.content.html +++ b/_docs/public/example/paint.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'paint.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/_docs/public/example/perspective.content.html b/_docs/public/example/perspective.content.html index fb89faec9..7deccf0ca 100644 --- a/_docs/public/example/perspective.content.html +++ b/_docs/public/example/perspective.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'perspective.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/_docs/public/example/rotate.content.html b/_docs/public/example/rotate.content.html index d0aaa42c3..e9c53ecae 100644 --- a/_docs/public/example/rotate.content.html +++ b/_docs/public/example/rotate.content.html @@ -31,7 +31,7 @@ window.addEventListener('load', function() { var s = document.createElement('script'); var src = 'rotate.js'; if (isProduction()) { - src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; + src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src; } s.src = src; document.body.appendChild(s); diff --git a/audio.go b/audio.go new file mode 100644 index 000000000..b0bda376d --- /dev/null +++ b/audio.go @@ -0,0 +1,23 @@ +// Copyright 2015 Hajime Hoshi +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ebiten + +import ( + "github.com/hajimehoshi/ebiten/internal/audio" +) + +func AppendAudioBuffer(l []float32, r []float32) { + audio.Append(l, r) +} diff --git a/example/audio/main.go b/example/audio/main.go new file mode 100644 index 000000000..843ca2196 --- /dev/null +++ b/example/audio/main.go @@ -0,0 +1,87 @@ +// Copyright 2015 Hajime Hoshi +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/ebitenutil" + "log" + "math/rand" +) + +const ( + screenWidth = 320 + screenHeight = 240 +) + +var frames = 0 + +const ( + hzA = 440.0 + hzAS = 466.2 + hzB = 493.9 + hzC = 523.3 + hzCS = 554.4 + hzD = 587.3 + hzDS = 622.3 + hzE = 659.3 + hzF = 698.5 + hzFS = 740.0 + hzG = 784.0 + hzGS = 830.6 +) + +// TODO: Need API to get sample rate? +const sampleRate = 44100 + +func square(out []float32, hz float64, sequence float64) { + length := int(sampleRate / hz) + if length == 0 { + panic("invalid hz") + } + for i := 0; i < len(out); i++ { + a := float32(1.0) + if i%length < int(float64(length)*sequence) { + a = 0 + } + out[i] = a + } +} + +func update(screen *ebiten.Image) error { + defer func() { + frames++ + }() + + const size = sampleRate / 60 // 3600 BPM + notes := []float64{hzA, hzB, hzC, hzD, hzE, hzF, hzG, hzA * 2} + if frames%30 == 0 { + l := make([]float32, size*30) + r := make([]float32, size*30) + note := notes[rand.Intn(len(notes))] + square(l, note, 0.5) + square(r, note, 0.5) + ebiten.AppendAudioBuffer(l, r) + } + ebitenutil.DebugPrint(screen, fmt.Sprintf("%0.2f", ebiten.CurrentFPS())) + return nil +} + +func main() { + if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Rotate (Ebiten Demo)"); err != nil { + log.Fatal(err) + } +} diff --git a/example/server/main.go b/example/server/main.go index 31ad16aff..e87fe0cf3 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -61,7 +61,7 @@ func createJSIfNeeded(name string) (string, error) { if err != nil && !os.IsNotExist(err) { return "", err } - if (err != nil && os.IsNotExist(err)) || time.Now().Sub(stat.ModTime()) > 10*time.Second { + if (err != nil && os.IsNotExist(err)) || time.Now().Sub(stat.ModTime()) > 5*time.Second { target := "github.com/hajimehoshi/ebiten/example/" + name out, err := exec.Command("gopherjs", "build", "-o", out, target).CombinedOutput() if err != nil { diff --git a/internal/audio/audio.go b/internal/audio/audio.go new file mode 100644 index 000000000..78eedcf83 --- /dev/null +++ b/internal/audio/audio.go @@ -0,0 +1,29 @@ +// Copyright 2015 Hajime Hoshi +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !js + +package audio + +func Init() { + // TODO: Implement +} + +func Start() { + // TODO: Implement +} + +func Append(l []float32, r []float32) { + // TODO: Implement +} diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go new file mode 100644 index 000000000..d9e2b0c63 --- /dev/null +++ b/internal/audio/audio_js.go @@ -0,0 +1,78 @@ +// Copyright 2015 Hajime Hoshi +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build js + +package audio + +import ( + "github.com/gopherjs/gopherjs/js" +) + +// Keep this so as not to be destroyed by GC. +var node js.Object +var context js.Object + +const bufferSize = 1024 + +var ( + bufferL = make([]float32, 0) + bufferR = make([]float32, 0) +) + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func Init() { + context = js.Global.Get("AudioContext").New() + // TODO: ScriptProcessorNode will be replaced Audio WebWorker. + // https://developer.mozilla.org/ja/docs/Web/API/ScriptProcessorNode + const bufLen = 1024 + node = context.Call("createScriptProcessor", bufLen, 0, 2) + node.Call("addEventListener", "audioprocess", func(e js.Object) { + l := e.Get("outputBuffer").Call("getChannelData", 0) + r := e.Get("outputBuffer").Call("getChannelData", 1) + for i := 0; i < bufLen; i++ { + // TODO: Use copyFromChannel? + if len(bufferL) <= i { + l.SetIndex(i, 0) + r.SetIndex(i, 0) + continue + } + l.SetIndex(i, bufferL[i]) + r.SetIndex(i, bufferR[i]) + } + // TODO: Will the array heads be released properly on GopherJS? + usedLen := min(bufLen, len(bufferL)) + bufferL = bufferL[usedLen:] + bufferR = bufferR[usedLen:] + }) +} + +func Start() { + // TODO: For iOS, node should be connected with a buffer node. + node.Call("connect", context.Get("destination")) +} + +func Append(l []float32, r []float32) { + if len(l) != len(r) { + panic("len(l) must equal to len(r)") + } + bufferL = append(bufferL, l...) + bufferR = append(bufferR, r...) +} diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index ea589e4a9..2ae6ffceb 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -19,6 +19,7 @@ package ui import ( "fmt" glfw "github.com/go-gl/glfw3" + "github.com/hajimehoshi/ebiten/internal/audio" "github.com/hajimehoshi/ebiten/internal/opengl" "runtime" "time" @@ -81,6 +82,9 @@ func init() { f() } }() + + audio.Init() + current = u } @@ -134,6 +138,8 @@ func Start(width, height, scale int, title string) (actualScale int, err error) windowWidth, _ := window.GetFramebufferSize() actualScale = windowWidth / width + audio.Start() + return actualScale, nil } diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index 15774d8b6..018b4021e 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -19,6 +19,7 @@ package ui import ( "github.com/gopherjs/gopherjs/js" "github.com/gopherjs/webgl" + "github.com/hajimehoshi/ebiten/internal/audio" "github.com/hajimehoshi/ebiten/internal/opengl" "strconv" ) @@ -26,6 +27,8 @@ import ( var canvas js.Object var context *opengl.Context +// TODO: This returns true even when the browser is not active. +// The current behavior causes sound noise... func shown() bool { return !js.Global.Get("document").Get("hidden").Bool() } @@ -36,6 +39,8 @@ func Use(f func(*opengl.Context)) { func vsync() { ch := make(chan struct{}) + // TODO: In iOS8, this is called at every 1/30[sec] frame. + // Can we use DOMHighResTimeStamp? js.Global.Get("window").Call("requestAnimationFrame", func() { close(ch) }) @@ -149,6 +154,8 @@ func init() { canvas.Call("addEventListener", "contextmenu", func(e js.Object) bool { return false }) + + audio.Init() } func devicePixelRatio() int { @@ -185,5 +192,7 @@ func Start(width, height, scale int, title string) (actualScale int, err error) }) canvas.Call("focus") + audio.Start() + return actualScale, nil } From a22025171603db181887a86edbc162c4aa60e205 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 11 Jan 2015 19:52:11 +0900 Subject: [PATCH 2/7] Play music at example/audio --- audio.go | 6 ++- example/audio/main.go | 80 +++++++++++++++++++++++++------------- internal/audio/audio_js.go | 16 ++++++++ 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/audio.go b/audio.go index b0bda376d..5382d5714 100644 --- a/audio.go +++ b/audio.go @@ -18,6 +18,10 @@ import ( "github.com/hajimehoshi/ebiten/internal/audio" ) -func AppendAudioBuffer(l []float32, r []float32) { +func AppendToAudioBuffer(l []float32, r []float32) { audio.Append(l, r) } + +func AddToAudioBuffer(l []float32, r []float32) { + audio.Add(l, r) +} diff --git a/example/audio/main.go b/example/audio/main.go index 843ca2196..276fa49c9 100644 --- a/example/audio/main.go +++ b/example/audio/main.go @@ -19,7 +19,6 @@ import ( "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" "log" - "math/rand" ) const ( @@ -30,30 +29,34 @@ const ( var frames = 0 const ( - hzA = 440.0 - hzAS = 466.2 - hzB = 493.9 - hzC = 523.3 - hzCS = 554.4 - hzD = 587.3 - hzDS = 622.3 - hzE = 659.3 - hzF = 698.5 - hzFS = 740.0 - hzG = 784.0 - hzGS = 830.6 + freqA = 440.0 + freqAS = 466.2 + freqB = 493.9 + freqC = 523.3 + freqCS = 554.4 + freqD = 587.3 + freqDS = 622.3 + freqE = 659.3 + freqF = 698.5 + freqFS = 740.0 + freqG = 784.0 + freqGS = 830.6 ) // TODO: Need API to get sample rate? const sampleRate = 44100 -func square(out []float32, hz float64, sequence float64) { - length := int(sampleRate / hz) +const score = `CCGGAAGR FFEEDDCR GGFFEEDR GGFFEEDR CCGGAAGR FFEEDDCR` + +var scoreIndex = 0 + +func square(out []float32, volume float64, freq float64, sequence float64) { + length := int(sampleRate / freq) if length == 0 { - panic("invalid hz") + panic("invalid freq") } for i := 0; i < len(out); i++ { - a := float32(1.0) + a := float32(volume) if i%length < int(float64(length)*sequence) { a = 0 } @@ -61,22 +64,45 @@ func square(out []float32, hz float64, sequence float64) { } } +func addNote() { + const size = sampleRate / 60 + notes := []float64{freqC, freqD, freqE, freqF, freqG, freqA * 2, freqB * 2} + + defer func() { + scoreIndex++ + scoreIndex %= len(score) + }() + l := make([]float32, size*30) + r := make([]float32, size*30) + note := score[scoreIndex] + for note == ' ' { + scoreIndex++ + scoreIndex %= len(score) + note = score[scoreIndex] + } + freq := 0.0 + switch { + case note == 'R': + return + case note <= 'B': + freq = notes[int(note)+len(notes)-int('C')] + default: + freq = notes[note-'C'] + } + vol := 1.0 / 32.0 + square(l, vol, freq, 0.5) + square(r, vol, freq, 0.5) + ebiten.AddToAudioBuffer(l, r) +} + func update(screen *ebiten.Image) error { defer func() { frames++ }() - - const size = sampleRate / 60 // 3600 BPM - notes := []float64{hzA, hzB, hzC, hzD, hzE, hzF, hzG, hzA * 2} if frames%30 == 0 { - l := make([]float32, size*30) - r := make([]float32, size*30) - note := notes[rand.Intn(len(notes))] - square(l, note, 0.5) - square(r, note, 0.5) - ebiten.AppendAudioBuffer(l, r) + addNote() } - ebitenutil.DebugPrint(screen, fmt.Sprintf("%0.2f", ebiten.CurrentFPS())) + ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f", ebiten.CurrentFPS())) return nil } diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go index d9e2b0c63..d36804349 100644 --- a/internal/audio/audio_js.go +++ b/internal/audio/audio_js.go @@ -76,3 +76,19 @@ func Append(l []float32, r []float32) { bufferL = append(bufferL, l...) bufferR = append(bufferR, r...) } + +func Add(l []float32, r []float32) { + // TODO: Adjust timing for frame? + if len(l) != len(r) { + panic("len(l) must equal to len(r)") + } + m := min(len(l), len(bufferL)) + for i := 0; i < m; i++ { + bufferL[i] += l[i] + bufferR[i] += r[i] + } + if m < len(l) { + bufferL = append(bufferL, l[m:]...) + bufferR = append(bufferR, r[m:]...) + } +} From ba3a612ce43e5a51e3f1d637584cd72a14fc72e8 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Wed, 21 Jan 2015 10:37:15 +0900 Subject: [PATCH 3/7] Bug fix: Add 'Add' method for non-js environment --- internal/audio/audio.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 78eedcf83..e1c74f8cc 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -27,3 +27,7 @@ func Start() { func Append(l []float32, r []float32) { // TODO: Implement } + +func Add(l []float32, r []float32) { + // TODO: Implement +} From 4b744119224a3fc8b0150aa4e91b83666756f068 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 23 Jan 2015 03:02:23 +0900 Subject: [PATCH 4/7] audio: Introduce channels --- audio.go | 13 +++-- example/audio/main.go | 13 ++--- internal/audio/audio.go | 8 ++- internal/audio/audio_js.go | 101 ++++++++++++++++++++++++++----------- 4 files changed, 91 insertions(+), 44 deletions(-) diff --git a/audio.go b/audio.go index 5382d5714..4cbfe08cb 100644 --- a/audio.go +++ b/audio.go @@ -18,10 +18,15 @@ import ( "github.com/hajimehoshi/ebiten/internal/audio" ) -func AppendToAudioBuffer(l []float32, r []float32) { - audio.Append(l, r) +func AudioSampleRate() int { + return audio.SampleRate } -func AddToAudioBuffer(l []float32, r []float32) { - audio.Add(l, r) +func AppendToAudioBuffer(channel int, l []float32, r []float32) bool { + return audio.Append(channel, l, r) +} + +// TODO: better name +func CurrentAudioTime() int { + return audio.CurrentBytes() } diff --git a/example/audio/main.go b/example/audio/main.go index 276fa49c9..75eacb9a1 100644 --- a/example/audio/main.go +++ b/example/audio/main.go @@ -43,15 +43,12 @@ const ( freqGS = 830.6 ) -// TODO: Need API to get sample rate? -const sampleRate = 44100 - const score = `CCGGAAGR FFEEDDCR GGFFEEDR GGFFEEDR CCGGAAGR FFEEDDCR` var scoreIndex = 0 func square(out []float32, volume float64, freq float64, sequence float64) { - length := int(sampleRate / freq) + length := int(float64(ebiten.AudioSampleRate()) / freq) if length == 0 { panic("invalid freq") } @@ -65,15 +62,15 @@ func square(out []float32, volume float64, freq float64, sequence float64) { } func addNote() { - const size = sampleRate / 60 + size := ebiten.AudioSampleRate() / 60 notes := []float64{freqC, freqD, freqE, freqF, freqG, freqA * 2, freqB * 2} defer func() { scoreIndex++ scoreIndex %= len(score) }() - l := make([]float32, size*30) - r := make([]float32, size*30) + l := make([]float32, size*30*2) + r := make([]float32, size*30*2) note := score[scoreIndex] for note == ' ' { scoreIndex++ @@ -92,7 +89,7 @@ func addNote() { vol := 1.0 / 32.0 square(l, vol, freq, 0.5) square(r, vol, freq, 0.5) - ebiten.AddToAudioBuffer(l, r) + ebiten.AppendToAudioBuffer(-1, l, r) } func update(screen *ebiten.Image) error { diff --git a/internal/audio/audio.go b/internal/audio/audio.go index e1c74f8cc..806cb9b5a 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -16,6 +16,8 @@ package audio +const SampleRate = 44100 + func Init() { // TODO: Implement } @@ -24,10 +26,12 @@ func Start() { // TODO: Implement } -func Append(l []float32, r []float32) { +func Append(channel int, l []float32, r []float32) bool { // TODO: Implement + return false } -func Add(l []float32, r []float32) { +func CurrentBytes() int { // TODO: Implement + return 0 } diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go index d36804349..90a56c279 100644 --- a/internal/audio/audio_js.go +++ b/internal/audio/audio_js.go @@ -25,11 +25,23 @@ var node js.Object var context js.Object const bufferSize = 1024 +const SampleRate = 44100 -var ( - bufferL = make([]float32, 0) - bufferR = make([]float32, 0) -) +type channel struct { + l []float32 + r []float32 +} + +var channels = make([]*channel, 16) + +func init() { + for i, _ := range channels { + channels[i] = &channel{ + l: []float32{}, + r: []float32{}, + } + } +} func min(a, b int) int { if a < b { @@ -38,29 +50,50 @@ func min(a, b int) int { return b } +var currentBytes = 0 + +func CurrentBytes() int { + return currentBytes +} + func Init() { context = js.Global.Get("AudioContext").New() - // TODO: ScriptProcessorNode will be replaced Audio WebWorker. + // TODO: ScriptProcessorNode will be replaced with Audio WebWorker. // https://developer.mozilla.org/ja/docs/Web/API/ScriptProcessorNode - const bufLen = 1024 - node = context.Call("createScriptProcessor", bufLen, 0, 2) + node = context.Call("createScriptProcessor", bufferSize, 0, 2) node.Call("addEventListener", "audioprocess", func(e js.Object) { + defer func() { + currentBytes += bufferSize + }() + l := e.Get("outputBuffer").Call("getChannelData", 0) r := e.Get("outputBuffer").Call("getChannelData", 1) - for i := 0; i < bufLen; i++ { + inputL := make([]float32, bufferSize) + inputR := make([]float32, bufferSize) + for _, ch := range channels { + if len(ch.l) == 0 { + continue + } + l := min(len(ch.l), bufferSize) + for i := 0; i < l; i++ { + inputL[i] += ch.l[i] + inputR[i] += ch.r[i] + } // TODO: Use copyFromChannel? - if len(bufferL) <= i { + usedLen := min(bufferSize, len(ch.l)) + ch.l = ch.l[usedLen:] + ch.r = ch.r[usedLen:] + } + for i := 0; i < bufferSize; i++ { + // TODO: Use copyFromChannel? + if len(inputL) <= i { l.SetIndex(i, 0) r.SetIndex(i, 0) continue } - l.SetIndex(i, bufferL[i]) - r.SetIndex(i, bufferR[i]) + l.SetIndex(i, inputL[i]) + r.SetIndex(i, inputR[i]) } - // TODO: Will the array heads be released properly on GopherJS? - usedLen := min(bufLen, len(bufferL)) - bufferL = bufferL[usedLen:] - bufferR = bufferR[usedLen:] }) } @@ -69,26 +102,34 @@ func Start() { node.Call("connect", context.Get("destination")) } -func Append(l []float32, r []float32) { - if len(l) != len(r) { - panic("len(l) must equal to len(r)") +func channelAt(i int) *channel { + if i == -1 { + for _, ch := range channels { + if 0 < len(ch.l) { + continue + } + return ch + } + return nil } - bufferL = append(bufferL, l...) - bufferR = append(bufferR, r...) + ch := channels[i] + // TODO: Can we append even though all data is not consumed? Need game timer? + if 0 < len(ch.l) { + return nil + } + return ch } -func Add(l []float32, r []float32) { - // TODO: Adjust timing for frame? +func Append(i int, l []float32, r []float32) bool { + // TODO: Mutex (especially for OpenAL) if len(l) != len(r) { panic("len(l) must equal to len(r)") } - m := min(len(l), len(bufferL)) - for i := 0; i < m; i++ { - bufferL[i] += l[i] - bufferR[i] += r[i] - } - if m < len(l) { - bufferL = append(bufferL, l[m:]...) - bufferR = append(bufferR, r[m:]...) + ch := channelAt(i) + if ch == nil { + return false } + ch.l = append(ch.l, l...) + ch.r = append(ch.r, r...) + return true } From 8d250c6b25e445c3c658862a2f584230f954646c Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 23 Jan 2015 10:58:18 +0900 Subject: [PATCH 5/7] audio: Add channel.nextInsertion --- example/audio/main.go | 14 ++++++++++---- internal/audio/audio.go | 4 ++++ internal/audio/audio_js.go | 30 +++++++++++++++++++++--------- run.go | 3 +++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/example/audio/main.go b/example/audio/main.go index 75eacb9a1..40ccf11bb 100644 --- a/example/audio/main.go +++ b/example/audio/main.go @@ -48,6 +48,12 @@ const score = `CCGGAAGR FFEEDDCR GGFFEEDR GGFFEEDR CCGGAAGR FFEEDDCR` var scoreIndex = 0 func square(out []float32, volume float64, freq float64, sequence float64) { + if freq == 0 { + for i := 0; i < len(out); i++ { + out[i] = 0 + } + return + } length := int(float64(ebiten.AudioSampleRate()) / freq) if length == 0 { panic("invalid freq") @@ -69,8 +75,8 @@ func addNote() { scoreIndex++ scoreIndex %= len(score) }() - l := make([]float32, size*30*2) - r := make([]float32, size*30*2) + l := make([]float32, size*30) + r := make([]float32, size*30) note := score[scoreIndex] for note == ' ' { scoreIndex++ @@ -80,7 +86,7 @@ func addNote() { freq := 0.0 switch { case note == 'R': - return + freq = 0 case note <= 'B': freq = notes[int(note)+len(notes)-int('C')] default: @@ -89,7 +95,7 @@ func addNote() { vol := 1.0 / 32.0 square(l, vol, freq, 0.5) square(r, vol, freq, 0.5) - ebiten.AppendToAudioBuffer(-1, l, r) + ebiten.AppendToAudioBuffer(0, l, r) } func update(screen *ebiten.Image) error { diff --git a/internal/audio/audio.go b/internal/audio/audio.go index 806cb9b5a..2d6093e24 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -35,3 +35,7 @@ func CurrentBytes() int { // TODO: Implement return 0 } + +func Update() { + // TODO: Implement +} diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go index 90a56c279..6bbc8636f 100644 --- a/internal/audio/audio_js.go +++ b/internal/audio/audio_js.go @@ -28,8 +28,9 @@ const bufferSize = 1024 const SampleRate = 44100 type channel struct { - l []float32 - r []float32 + l []float32 + r []float32 + nextInsertion int } var channels = make([]*channel, 16) @@ -83,6 +84,7 @@ func Init() { usedLen := min(bufferSize, len(ch.l)) ch.l = ch.l[usedLen:] ch.r = ch.r[usedLen:] + ch.nextInsertion -= min(bufferSize, ch.nextInsertion) } for i := 0; i < bufferSize; i++ { // TODO: Use copyFromChannel? @@ -97,6 +99,15 @@ func Init() { }) } +func Update() { + for _, ch := range channels { + if len(ch.l) == 0 { + continue + } + ch.nextInsertion += SampleRate / 60 + } +} + func Start() { // TODO: For iOS, node should be connected with a buffer node. node.Call("connect", context.Get("destination")) @@ -105,19 +116,17 @@ func Start() { func channelAt(i int) *channel { if i == -1 { for _, ch := range channels { - if 0 < len(ch.l) { - continue + if len(ch.l) <= ch.nextInsertion { + return ch } - return ch } return nil } ch := channels[i] - // TODO: Can we append even though all data is not consumed? Need game timer? - if 0 < len(ch.l) { - return nil + if len(ch.l) <= ch.nextInsertion { + return ch } - return ch + return nil } func Append(i int, l []float32, r []float32) bool { @@ -129,6 +138,9 @@ func Append(i int, l []float32, r []float32) bool { if ch == nil { return false } + print(ch.nextInsertion) + ch.l = append(ch.l, make([]float32, ch.nextInsertion-len(ch.l))...) + ch.r = append(ch.r, make([]float32, ch.nextInsertion-len(ch.r))...) ch.l = append(ch.l, l...) ch.r = append(ch.r, r...) return true diff --git a/run.go b/run.go index dbb9fd793..ebcab0d0a 100644 --- a/run.go +++ b/run.go @@ -15,6 +15,7 @@ package ebiten import ( + "github.com/hajimehoshi/ebiten/internal/audio" "github.com/hajimehoshi/ebiten/internal/opengl" "github.com/hajimehoshi/ebiten/internal/ui" "time" @@ -69,6 +70,8 @@ func Run(f func(*Image) error, width, height, scale int, title string) error { if err := graphicsContext.postUpdate(); err != nil { return err } + // TODO: I'm not sure this is 'Update'. Is 'Tick' better? + audio.Update() ui.SwapBuffers() if err != nil { return err From 88ac129dd6814af3d2a5b3dc849466603eb494a1 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 23 Jan 2015 21:51:21 +0900 Subject: [PATCH 6/7] Refactoring: only one nextInsertion is needed --- internal/audio/audio_js.go | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/internal/audio/audio_js.go b/internal/audio/audio_js.go index 6bbc8636f..a0dc7aac7 100644 --- a/internal/audio/audio_js.go +++ b/internal/audio/audio_js.go @@ -27,10 +27,11 @@ var context js.Object const bufferSize = 1024 const SampleRate = 44100 +var nextInsertion = 0 + type channel struct { - l []float32 - r []float32 - nextInsertion int + l []float32 + r []float32 } var channels = make([]*channel, 16) @@ -84,8 +85,8 @@ func Init() { usedLen := min(bufferSize, len(ch.l)) ch.l = ch.l[usedLen:] ch.r = ch.r[usedLen:] - ch.nextInsertion -= min(bufferSize, ch.nextInsertion) } + nextInsertion -= min(bufferSize, nextInsertion) for i := 0; i < bufferSize; i++ { // TODO: Use copyFromChannel? if len(inputL) <= i { @@ -100,12 +101,7 @@ func Init() { } func Update() { - for _, ch := range channels { - if len(ch.l) == 0 { - continue - } - ch.nextInsertion += SampleRate / 60 - } + nextInsertion += SampleRate / 60 } func Start() { @@ -116,14 +112,14 @@ func Start() { func channelAt(i int) *channel { if i == -1 { for _, ch := range channels { - if len(ch.l) <= ch.nextInsertion { + if len(ch.l) <= nextInsertion { return ch } } return nil } ch := channels[i] - if len(ch.l) <= ch.nextInsertion { + if len(ch.l) <= nextInsertion { return ch } return nil @@ -138,9 +134,8 @@ func Append(i int, l []float32, r []float32) bool { if ch == nil { return false } - print(ch.nextInsertion) - ch.l = append(ch.l, make([]float32, ch.nextInsertion-len(ch.l))...) - ch.r = append(ch.r, make([]float32, ch.nextInsertion-len(ch.r))...) + ch.l = append(ch.l, make([]float32, nextInsertion-len(ch.l))...) + ch.r = append(ch.r, make([]float32, nextInsertion-len(ch.r))...) ch.l = append(ch.l, l...) ch.r = append(ch.r, r...) return true From d020824b3a1e6d5b0c327ab60bcef59fe605e187 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 23 Jan 2015 23:04:56 +0900 Subject: [PATCH 7/7] audio: Move to exa/audio --- example/audio/main.go | 13 +++++++------ audio.go => exp/audio/audio.go | 8 ++++---- 2 files changed, 11 insertions(+), 10 deletions(-) rename audio.go => exp/audio/audio.go (84%) diff --git a/example/audio/main.go b/example/audio/main.go index 40ccf11bb..2b67b240f 100644 --- a/example/audio/main.go +++ b/example/audio/main.go @@ -18,6 +18,7 @@ import ( "fmt" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" + "github.com/hajimehoshi/ebiten/exp/audio" "log" ) @@ -54,7 +55,7 @@ func square(out []float32, volume float64, freq float64, sequence float64) { } return } - length := int(float64(ebiten.AudioSampleRate()) / freq) + length := int(float64(audio.SampleRate()) / freq) if length == 0 { panic("invalid freq") } @@ -68,7 +69,7 @@ func square(out []float32, volume float64, freq float64, sequence float64) { } func addNote() { - size := ebiten.AudioSampleRate() / 60 + size := audio.SampleRate() / 60 notes := []float64{freqC, freqD, freqE, freqF, freqG, freqA * 2, freqB * 2} defer func() { @@ -92,10 +93,10 @@ func addNote() { default: freq = notes[note-'C'] } - vol := 1.0 / 32.0 - square(l, vol, freq, 0.5) - square(r, vol, freq, 0.5) - ebiten.AppendToAudioBuffer(0, l, r) + vol := 1.0 / 16.0 + square(l, vol, freq, 0.25) + square(r, vol, freq, 0.25) + audio.AppendToBuffer(0, l, r) } func update(screen *ebiten.Image) error { diff --git a/audio.go b/exp/audio/audio.go similarity index 84% rename from audio.go rename to exp/audio/audio.go index 4cbfe08cb..5c4822e98 100644 --- a/audio.go +++ b/exp/audio/audio.go @@ -12,21 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ebiten +package audio import ( "github.com/hajimehoshi/ebiten/internal/audio" ) -func AudioSampleRate() int { +func SampleRate() int { return audio.SampleRate } -func AppendToAudioBuffer(channel int, l []float32, r []float32) bool { +func AppendToBuffer(channel int, l []float32, r []float32) bool { return audio.Append(channel, l, r) } // TODO: better name -func CurrentAudioTime() int { +func CurrentTime() int { return audio.CurrentBytes() }