141 lines
3.9 KiB
Go
141 lines
3.9 KiB
Go
// Copyright 2021 The Oto Authors
|
|
//
|
|
// 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 oto
|
|
|
|
import (
|
|
"errors"
|
|
"runtime"
|
|
"syscall/js"
|
|
"unsafe"
|
|
|
|
"github.com/ebitengine/oto/v3/internal/mux"
|
|
)
|
|
|
|
type context struct {
|
|
audioContext js.Value
|
|
scriptProcessor js.Value
|
|
scriptProcessorCallback js.Func
|
|
ready bool
|
|
callbacks map[string]js.Func
|
|
|
|
mux *mux.Mux
|
|
}
|
|
|
|
func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) {
|
|
ready := make(chan struct{})
|
|
|
|
class := js.Global().Get("AudioContext")
|
|
if !class.Truthy() {
|
|
class = js.Global().Get("webkitAudioContext")
|
|
}
|
|
if !class.Truthy() {
|
|
return nil, nil, errors.New("oto: AudioContext or webkitAudioContext was not found")
|
|
}
|
|
options := js.Global().Get("Object").New()
|
|
options.Set("sampleRate", sampleRate)
|
|
|
|
d := &context{
|
|
audioContext: class.New(options),
|
|
mux: mux.New(sampleRate, channelCount, format),
|
|
}
|
|
|
|
if bufferSizeInBytes == 0 {
|
|
// 4096 was not great at least on Safari 15.
|
|
bufferSizeInBytes = 8192 * channelCount
|
|
}
|
|
|
|
buf32 := make([]float32, bufferSizeInBytes/4)
|
|
chBuf32 := make([][]float32, channelCount)
|
|
for i := range chBuf32 {
|
|
chBuf32[i] = make([]float32, len(buf32)/channelCount)
|
|
}
|
|
|
|
// TODO: Use AudioWorklet if available.
|
|
sp := d.audioContext.Call("createScriptProcessor", bufferSizeInBytes/4/channelCount, 0, channelCount)
|
|
f := js.FuncOf(func(this js.Value, arguments []js.Value) any {
|
|
d.mux.ReadFloat32s(buf32)
|
|
for i := 0; i < channelCount; i++ {
|
|
for j := range chBuf32[i] {
|
|
chBuf32[i][j] = buf32[j*channelCount+i]
|
|
}
|
|
}
|
|
|
|
buf := arguments[0].Get("outputBuffer")
|
|
if buf.Get("copyToChannel").Truthy() {
|
|
for i := 0; i < channelCount; i++ {
|
|
buf.Call("copyToChannel", float32SliceToTypedArray(chBuf32[i]), i, 0)
|
|
}
|
|
} else {
|
|
// copyToChannel is not defined on Safari 11.
|
|
for i := 0; i < channelCount; i++ {
|
|
buf.Call("getChannelData", i).Call("set", float32SliceToTypedArray(chBuf32[i]))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
sp.Call("addEventListener", "audioprocess", f)
|
|
d.scriptProcessor = sp
|
|
d.scriptProcessorCallback = f
|
|
|
|
sp.Call("connect", d.audioContext.Get("destination"))
|
|
|
|
setCallback := func(event string) js.Func {
|
|
var f js.Func
|
|
f = js.FuncOf(func(this js.Value, arguments []js.Value) any {
|
|
if !d.ready {
|
|
d.audioContext.Call("resume")
|
|
d.ready = true
|
|
close(ready)
|
|
}
|
|
js.Global().Get("document").Call("removeEventListener", event, f)
|
|
return nil
|
|
})
|
|
js.Global().Get("document").Call("addEventListener", event, f)
|
|
d.callbacks[event] = f
|
|
return f
|
|
}
|
|
|
|
// Browsers require user interaction to start the audio.
|
|
// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio
|
|
d.callbacks = map[string]js.Func{}
|
|
setCallback("touchend")
|
|
setCallback("keyup")
|
|
setCallback("mouseup")
|
|
|
|
return d, ready, nil
|
|
}
|
|
|
|
func (c *context) Suspend() error {
|
|
c.audioContext.Call("suspend")
|
|
return nil
|
|
}
|
|
|
|
func (c *context) Resume() error {
|
|
c.audioContext.Call("resume")
|
|
return nil
|
|
}
|
|
|
|
func (c *context) Err() error {
|
|
return nil
|
|
}
|
|
|
|
func float32SliceToTypedArray(s []float32) js.Value {
|
|
bs := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*4)
|
|
a := js.Global().Get("Uint8Array").New(len(bs))
|
|
js.CopyBytesToJS(a, bs)
|
|
runtime.KeepAlive(s)
|
|
buf := a.Get("buffer")
|
|
return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4)
|
|
}
|