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/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/example/audio/main.go b/example/audio/main.go new file mode 100644 index 000000000..2b67b240f --- /dev/null +++ b/example/audio/main.go @@ -0,0 +1,117 @@ +// 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" + "github.com/hajimehoshi/ebiten/exp/audio" + "log" +) + +const ( + screenWidth = 320 + screenHeight = 240 +) + +var frames = 0 + +const ( + 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 +) + +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(audio.SampleRate()) / freq) + if length == 0 { + panic("invalid freq") + } + for i := 0; i < len(out); i++ { + a := float32(volume) + if i%length < int(float64(length)*sequence) { + a = 0 + } + out[i] = a + } +} + +func addNote() { + size := audio.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': + freq = 0 + case note <= 'B': + freq = notes[int(note)+len(notes)-int('C')] + default: + freq = notes[note-'C'] + } + 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 { + defer func() { + frames++ + }() + if frames%30 == 0 { + addNote() + } + ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %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/exp/audio/audio.go b/exp/audio/audio.go new file mode 100644 index 000000000..5c4822e98 --- /dev/null +++ b/exp/audio/audio.go @@ -0,0 +1,32 @@ +// 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 audio + +import ( + "github.com/hajimehoshi/ebiten/internal/audio" +) + +func SampleRate() int { + return audio.SampleRate +} + +func AppendToBuffer(channel int, l []float32, r []float32) bool { + return audio.Append(channel, l, r) +} + +// TODO: better name +func CurrentTime() int { + return audio.CurrentBytes() +} diff --git a/internal/audio/audio.go b/internal/audio/audio.go new file mode 100644 index 000000000..2d6093e24 --- /dev/null +++ b/internal/audio/audio.go @@ -0,0 +1,41 @@ +// 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 + +const SampleRate = 44100 + +func Init() { + // TODO: Implement +} + +func Start() { + // TODO: Implement +} + +func Append(channel int, l []float32, r []float32) bool { + // TODO: Implement + return false +} + +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 new file mode 100644 index 000000000..a0dc7aac7 --- /dev/null +++ b/internal/audio/audio_js.go @@ -0,0 +1,142 @@ +// 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 +const SampleRate = 44100 + +var nextInsertion = 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 { + return a + } + return b +} + +var currentBytes = 0 + +func CurrentBytes() int { + return currentBytes +} + +func Init() { + 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) { + defer func() { + currentBytes += bufferSize + }() + + l := e.Get("outputBuffer").Call("getChannelData", 0) + r := e.Get("outputBuffer").Call("getChannelData", 1) + 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? + usedLen := min(bufferSize, len(ch.l)) + ch.l = ch.l[usedLen:] + ch.r = ch.r[usedLen:] + } + nextInsertion -= min(bufferSize, nextInsertion) + 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, inputL[i]) + r.SetIndex(i, inputR[i]) + } + }) +} + +func Update() { + nextInsertion += SampleRate / 60 +} + +func Start() { + // TODO: For iOS, node should be connected with a buffer node. + node.Call("connect", context.Get("destination")) +} + +func channelAt(i int) *channel { + if i == -1 { + for _, ch := range channels { + if len(ch.l) <= nextInsertion { + return ch + } + } + return nil + } + ch := channels[i] + if len(ch.l) <= nextInsertion { + return ch + } + return nil +} + +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)") + } + ch := channelAt(i) + if ch == nil { + return false + } + 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 +} diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index 893e48e62..60398deb9 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 } @@ -133,6 +137,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 8af9d97a6..4f339f13d 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) }) @@ -178,6 +183,8 @@ func init() { window.Call("addEventListener", "gamepadconnected", func(e js.Object) { // Do nothing. }) + + audio.Init() } func setMouseCursorFromEvent(e js.Object) { @@ -217,5 +224,7 @@ func Start(width, height, scale int, title string) (actualScale int, err error) canvas.Call("focus") + audio.Start() + return actualScale, nil } 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