Add Experimental Audio API

This commit is contained in:
Hajime Hoshi 2015-01-11 01:23:43 +09:00
parent 9d5ab644a4
commit 355da1bcbc
14 changed files with 240 additions and 8 deletions

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'blocks.js'; var src = 'blocks.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'hue.js'; var src = 'hue.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'keyboard.js'; var src = 'keyboard.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'mosaic.js'; var src = 'mosaic.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'paint.js'; var src = 'paint.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'perspective.js'; var src = 'perspective.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

View File

@ -31,7 +31,7 @@ window.addEventListener('load', function() {
var s = document.createElement('script'); var s = document.createElement('script');
var src = 'rotate.js'; var src = 'rotate.js';
if (isProduction()) { if (isProduction()) {
src = 'http://hajimehoshi.github.io/ebiten.pagestorage/master/' + src; src = 'http://hajimehoshi.github.io/ebiten.pagestorage/audio/' + src;
} }
s.src = src; s.src = src;
document.body.appendChild(s); document.body.appendChild(s);

23
audio.go Normal file
View File

@ -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)
}

87
example/audio/main.go Normal file
View File

@ -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)
}
}

View File

@ -61,7 +61,7 @@ func createJSIfNeeded(name string) (string, error) {
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return "", 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 target := "github.com/hajimehoshi/ebiten/example/" + name
out, err := exec.Command("gopherjs", "build", "-o", out, target).CombinedOutput() out, err := exec.Command("gopherjs", "build", "-o", out, target).CombinedOutput()
if err != nil { if err != nil {

29
internal/audio/audio.go Normal file
View File

@ -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
}

View File

@ -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...)
}

View File

@ -19,6 +19,7 @@ package ui
import ( import (
"fmt" "fmt"
glfw "github.com/go-gl/glfw3" glfw "github.com/go-gl/glfw3"
"github.com/hajimehoshi/ebiten/internal/audio"
"github.com/hajimehoshi/ebiten/internal/opengl" "github.com/hajimehoshi/ebiten/internal/opengl"
"runtime" "runtime"
"time" "time"
@ -81,6 +82,9 @@ func init() {
f() f()
} }
}() }()
audio.Init()
current = u current = u
} }
@ -134,6 +138,8 @@ func Start(width, height, scale int, title string) (actualScale int, err error)
windowWidth, _ := window.GetFramebufferSize() windowWidth, _ := window.GetFramebufferSize()
actualScale = windowWidth / width actualScale = windowWidth / width
audio.Start()
return actualScale, nil return actualScale, nil
} }

View File

@ -19,6 +19,7 @@ package ui
import ( import (
"github.com/gopherjs/gopherjs/js" "github.com/gopherjs/gopherjs/js"
"github.com/gopherjs/webgl" "github.com/gopherjs/webgl"
"github.com/hajimehoshi/ebiten/internal/audio"
"github.com/hajimehoshi/ebiten/internal/opengl" "github.com/hajimehoshi/ebiten/internal/opengl"
"strconv" "strconv"
) )
@ -26,6 +27,8 @@ import (
var canvas js.Object var canvas js.Object
var context *opengl.Context var context *opengl.Context
// TODO: This returns true even when the browser is not active.
// The current behavior causes sound noise...
func shown() bool { func shown() bool {
return !js.Global.Get("document").Get("hidden").Bool() return !js.Global.Get("document").Get("hidden").Bool()
} }
@ -36,6 +39,8 @@ func Use(f func(*opengl.Context)) {
func vsync() { func vsync() {
ch := make(chan struct{}) 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() { js.Global.Get("window").Call("requestAnimationFrame", func() {
close(ch) close(ch)
}) })
@ -149,6 +154,8 @@ func init() {
canvas.Call("addEventListener", "contextmenu", func(e js.Object) bool { canvas.Call("addEventListener", "contextmenu", func(e js.Object) bool {
return false return false
}) })
audio.Init()
} }
func devicePixelRatio() int { func devicePixelRatio() int {
@ -185,5 +192,7 @@ func Start(width, height, scale int, title string) (actualScale int, err error)
}) })
canvas.Call("focus") canvas.Call("focus")
audio.Start()
return actualScale, nil return actualScale, nil
} }