mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 03:08:54 +01:00
audio: Make 'oto' package and use that (#351)
This commit is contained in:
parent
951e5bccef
commit
766072cdbb
@ -35,7 +35,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
"github.com/hajimehoshi/ebiten/audio/internal/driver"
|
"github.com/hajimehoshi/oto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type players struct {
|
type players struct {
|
||||||
@ -188,7 +188,7 @@ func (p *players) hasSource(src ReadSeekCloser) bool {
|
|||||||
// In this case, audio goes on even when the game stops e.g. by diactivating the screen.
|
// In this case, audio goes on even when the game stops e.g. by diactivating the screen.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
players *players
|
players *players
|
||||||
driver *driver.Player
|
driver *oto.Player
|
||||||
sampleRate int
|
sampleRate int
|
||||||
frames int64
|
frames int64
|
||||||
writtenBytes int64
|
writtenBytes int64
|
||||||
@ -239,7 +239,7 @@ func (c *Context) Update() error {
|
|||||||
// e.g. a variable for JVM on Android might not be set.
|
// e.g. a variable for JVM on Android might not be set.
|
||||||
if c.driver == nil {
|
if c.driver == nil {
|
||||||
// TODO: Rename this other than player
|
// TODO: Rename this other than player
|
||||||
p, err := driver.NewPlayer(c.sampleRate, channelNum, bytesPerSample)
|
p, err := oto.NewPlayer(c.sampleRate, channelNum, bytesPerSample)
|
||||||
c.driver = p
|
c.driver = p
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1,269 +0,0 @@
|
|||||||
// Copyright 2016 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 driver
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#include <jni.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
static jclass android_media_AudioFormat;
|
|
||||||
static jclass android_media_AudioManager;
|
|
||||||
static jclass android_media_AudioTrack;
|
|
||||||
|
|
||||||
static char* initAudioTrack(uintptr_t java_vm, uintptr_t jni_env,
|
|
||||||
int sampleRate, int channelNum, int bytesPerSample, jobject* audioTrack, int* bufferSize) {
|
|
||||||
*bufferSize = 0;
|
|
||||||
JavaVM* vm = (JavaVM*)java_vm;
|
|
||||||
JNIEnv* env = (JNIEnv*)jni_env;
|
|
||||||
|
|
||||||
jclass local = (*env)->FindClass(env, "android/media/AudioFormat");
|
|
||||||
android_media_AudioFormat = (*env)->NewGlobalRef(env, local);
|
|
||||||
(*env)->DeleteLocalRef(env, local);
|
|
||||||
|
|
||||||
local = (*env)->FindClass(env, "android/media/AudioManager");
|
|
||||||
android_media_AudioManager = (*env)->NewGlobalRef(env, local);
|
|
||||||
(*env)->DeleteLocalRef(env, local);
|
|
||||||
|
|
||||||
local = (*env)->FindClass(env, "android/media/AudioTrack");
|
|
||||||
android_media_AudioTrack = (*env)->NewGlobalRef(env, local);
|
|
||||||
(*env)->DeleteLocalRef(env, local);
|
|
||||||
|
|
||||||
const jint android_media_AudioManager_STREAM_MUSIC =
|
|
||||||
(*env)->GetStaticIntField(
|
|
||||||
env, android_media_AudioManager,
|
|
||||||
(*env)->GetStaticFieldID(env, android_media_AudioManager, "STREAM_MUSIC", "I"));
|
|
||||||
const jint android_media_AudioTrack_MODE_STREAM =
|
|
||||||
(*env)->GetStaticIntField(
|
|
||||||
env, android_media_AudioTrack,
|
|
||||||
(*env)->GetStaticFieldID(env, android_media_AudioTrack, "MODE_STREAM", "I"));
|
|
||||||
const jint android_media_AudioFormat_CHANNEL_OUT_MONO =
|
|
||||||
(*env)->GetStaticIntField(
|
|
||||||
env, android_media_AudioFormat,
|
|
||||||
(*env)->GetStaticFieldID(env, android_media_AudioFormat, "CHANNEL_OUT_MONO", "I"));
|
|
||||||
const jint android_media_AudioFormat_CHANNEL_OUT_STEREO =
|
|
||||||
(*env)->GetStaticIntField(
|
|
||||||
env, android_media_AudioFormat,
|
|
||||||
(*env)->GetStaticFieldID(env, android_media_AudioFormat, "CHANNEL_OUT_STEREO", "I"));
|
|
||||||
const jint android_media_AudioFormat_ENCODING_PCM_8BIT =
|
|
||||||
(*env)->GetStaticIntField(
|
|
||||||
env, android_media_AudioFormat,
|
|
||||||
(*env)->GetStaticFieldID(env, android_media_AudioFormat, "ENCODING_PCM_8BIT", "I"));
|
|
||||||
const jint android_media_AudioFormat_ENCODING_PCM_16BIT =
|
|
||||||
(*env)->GetStaticIntField(
|
|
||||||
env, android_media_AudioFormat,
|
|
||||||
(*env)->GetStaticFieldID(env, android_media_AudioFormat, "ENCODING_PCM_16BIT", "I"));
|
|
||||||
|
|
||||||
jint channel = android_media_AudioFormat_CHANNEL_OUT_MONO;
|
|
||||||
switch (channelNum) {
|
|
||||||
case 1:
|
|
||||||
channel = android_media_AudioFormat_CHANNEL_OUT_MONO;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
channel = android_media_AudioFormat_CHANNEL_OUT_STEREO;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return "invalid channel";
|
|
||||||
}
|
|
||||||
|
|
||||||
jint encoding = android_media_AudioFormat_ENCODING_PCM_8BIT;
|
|
||||||
switch (bytesPerSample) {
|
|
||||||
case 1:
|
|
||||||
encoding = android_media_AudioFormat_ENCODING_PCM_8BIT;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
encoding = android_media_AudioFormat_ENCODING_PCM_16BIT;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return "invalid bytesPerSample";
|
|
||||||
}
|
|
||||||
|
|
||||||
*bufferSize =
|
|
||||||
(*env)->CallStaticIntMethod(
|
|
||||||
env, android_media_AudioTrack,
|
|
||||||
(*env)->GetStaticMethodID(env, android_media_AudioTrack, "getMinBufferSize", "(III)I"),
|
|
||||||
sampleRate, channel, encoding);
|
|
||||||
|
|
||||||
const jobject tmpAudioTrack =
|
|
||||||
(*env)->NewObject(
|
|
||||||
env, android_media_AudioTrack,
|
|
||||||
(*env)->GetMethodID(env, android_media_AudioTrack, "<init>", "(IIIIII)V"),
|
|
||||||
android_media_AudioManager_STREAM_MUSIC,
|
|
||||||
sampleRate, channel, encoding, *bufferSize,
|
|
||||||
android_media_AudioTrack_MODE_STREAM);
|
|
||||||
// Note that *audioTrack will never be released.
|
|
||||||
*audioTrack = (*env)->NewGlobalRef(env, tmpAudioTrack);
|
|
||||||
(*env)->DeleteLocalRef(env, tmpAudioTrack);
|
|
||||||
|
|
||||||
(*env)->CallVoidMethod(
|
|
||||||
env, *audioTrack,
|
|
||||||
(*env)->GetMethodID(env, android_media_AudioTrack, "play", "()V"));
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char* writeToAudioTrack(uintptr_t java_vm, uintptr_t jni_env,
|
|
||||||
jobject audioTrack, int bytesPerSample, void* data, int length) {
|
|
||||||
JavaVM* vm = (JavaVM*)java_vm;
|
|
||||||
JNIEnv* env = (JNIEnv*)jni_env;
|
|
||||||
|
|
||||||
jbyteArray arrInBytes;
|
|
||||||
jshortArray arrInShorts;
|
|
||||||
switch (bytesPerSample) {
|
|
||||||
case 1:
|
|
||||||
arrInBytes = (*env)->NewByteArray(env, length);
|
|
||||||
(*env)->SetByteArrayRegion(env, arrInBytes, 0, length, data);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
arrInShorts = (*env)->NewShortArray(env, length);
|
|
||||||
(*env)->SetShortArrayRegion(env, arrInShorts, 0, length, data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
jint result;
|
|
||||||
static jmethodID write1 = NULL;
|
|
||||||
static jmethodID write2 = NULL;
|
|
||||||
if (!write1) {
|
|
||||||
write1 = (*env)->GetMethodID(env, android_media_AudioTrack, "write", "([BII)I");
|
|
||||||
}
|
|
||||||
if (!write2) {
|
|
||||||
write2 = (*env)->GetMethodID(env, android_media_AudioTrack, "write", "([SII)I");
|
|
||||||
}
|
|
||||||
switch (bytesPerSample) {
|
|
||||||
case 1:
|
|
||||||
result = (*env)->CallIntMethod(env, audioTrack, write1, arrInBytes, 0, length);
|
|
||||||
(*env)->DeleteLocalRef(env, arrInBytes);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
result = (*env)->CallIntMethod(env, audioTrack, write2, arrInShorts, 0, length);
|
|
||||||
(*env)->DeleteLocalRef(env, arrInShorts);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case -3: // ERROR_INVALID_OPERATION
|
|
||||||
return "invalid operation";
|
|
||||||
case -2: // ERROR_BAD_VALUE
|
|
||||||
return "bad value";
|
|
||||||
case -1: // ERROR
|
|
||||||
return "error";
|
|
||||||
}
|
|
||||||
if (result < 0) {
|
|
||||||
return "unknown error";
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/internal/jni"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
sampleRate int
|
|
||||||
channelNum int
|
|
||||||
bytesPerSample int
|
|
||||||
audioTrack C.jobject
|
|
||||||
buffer []byte
|
|
||||||
bufferSize int
|
|
||||||
chErr chan error
|
|
||||||
chBuffer chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
|
||||||
p := &Player{
|
|
||||||
sampleRate: sampleRate,
|
|
||||||
channelNum: channelNum,
|
|
||||||
bytesPerSample: bytesPerSample,
|
|
||||||
buffer: []byte{},
|
|
||||||
chErr: make(chan error),
|
|
||||||
chBuffer: make(chan []byte, 8),
|
|
||||||
}
|
|
||||||
if err := jni.RunOnJVM(func(vm, env, ctx uintptr) error {
|
|
||||||
audioTrack := C.jobject(nil)
|
|
||||||
bufferSize := C.int(0)
|
|
||||||
if msg := C.initAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
|
|
||||||
C.int(sampleRate), C.int(channelNum), C.int(bytesPerSample),
|
|
||||||
&audioTrack, &bufferSize); msg != nil {
|
|
||||||
return errors.New("driver: " + C.GoString(msg))
|
|
||||||
}
|
|
||||||
p.audioTrack = audioTrack
|
|
||||||
p.bufferSize = int(bufferSize)
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
go p.loop()
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) loop() {
|
|
||||||
for bufInBytes := range p.chBuffer {
|
|
||||||
var bufInShorts []int16
|
|
||||||
if p.bytesPerSample == 2 {
|
|
||||||
bufInShorts = make([]int16, len(bufInBytes)/2)
|
|
||||||
for i := 0; i < len(bufInShorts); i++ {
|
|
||||||
bufInShorts[i] = int16(bufInBytes[2*i]) | (int16(bufInBytes[2*i+1]) << 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := jni.RunOnJVM(func(vm, env, ctx uintptr) error {
|
|
||||||
msg := (*C.char)(nil)
|
|
||||||
switch p.bytesPerSample {
|
|
||||||
case 1:
|
|
||||||
msg = C.writeToAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
|
|
||||||
p.audioTrack, C.int(p.bytesPerSample),
|
|
||||||
unsafe.Pointer(&bufInBytes[0]), C.int(len(bufInBytes)))
|
|
||||||
case 2:
|
|
||||||
msg = C.writeToAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
|
|
||||||
p.audioTrack, C.int(p.bytesPerSample),
|
|
||||||
unsafe.Pointer(&bufInShorts[0]), C.int(len(bufInShorts)))
|
|
||||||
default:
|
|
||||||
panic("not reach")
|
|
||||||
}
|
|
||||||
if msg != nil {
|
|
||||||
return errors.New(C.GoString(msg))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
p.chErr <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Proceed(data []byte) error {
|
|
||||||
p.buffer = append(p.buffer, data...)
|
|
||||||
if len(p.buffer) < p.bufferSize {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
buf := p.buffer[:p.bufferSize]
|
|
||||||
select {
|
|
||||||
case p.chBuffer <- buf:
|
|
||||||
case err := <-p.chErr:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.buffer = p.buffer[p.bufferSize:]
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
// Copyright 2016 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.
|
|
||||||
|
|
||||||
// TODO: Can we unify this into driver_openal.go?
|
|
||||||
|
|
||||||
// +build ios
|
|
||||||
|
|
||||||
package driver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"golang.org/x/mobile/exp/audio/al"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxBufferNum = 8
|
|
||||||
)
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
alSource al.Source
|
|
||||||
alBuffers []al.Buffer
|
|
||||||
sampleRate int
|
|
||||||
isClosed bool
|
|
||||||
alFormat uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func alFormat(channelNum, bytesPerSample int) uint32 {
|
|
||||||
switch {
|
|
||||||
case channelNum == 1 && bytesPerSample == 1:
|
|
||||||
return al.FormatMono8
|
|
||||||
case channelNum == 1 && bytesPerSample == 2:
|
|
||||||
return al.FormatMono16
|
|
||||||
case channelNum == 2 && bytesPerSample == 1:
|
|
||||||
return al.FormatStereo8
|
|
||||||
case channelNum == 2 && bytesPerSample == 2:
|
|
||||||
return al.FormatStereo16
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("driver: invalid channel num (%d) or bytes per sample (%d)", channelNum, bytesPerSample))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
|
||||||
var p *Player
|
|
||||||
if err := al.OpenDevice(); err != nil {
|
|
||||||
return nil, fmt.Errorf("driver: OpenAL initialization failed: %v", err)
|
|
||||||
}
|
|
||||||
s := al.GenSources(1)
|
|
||||||
if e := al.Error(); e != 0 {
|
|
||||||
return nil, fmt.Errorf("driver: al.GenSources error: %d", e)
|
|
||||||
}
|
|
||||||
p = &Player{
|
|
||||||
alSource: s[0],
|
|
||||||
alBuffers: []al.Buffer{},
|
|
||||||
sampleRate: sampleRate,
|
|
||||||
alFormat: alFormat(channelNum, bytesPerSample),
|
|
||||||
}
|
|
||||||
runtime.SetFinalizer(p, (*Player).Close)
|
|
||||||
|
|
||||||
bs := al.GenBuffers(maxBufferNum)
|
|
||||||
const bufferSize = 1024
|
|
||||||
emptyBytes := make([]byte, bufferSize)
|
|
||||||
for _, b := range bs {
|
|
||||||
// Note that the third argument of only the first buffer is used.
|
|
||||||
b.BufferData(p.alFormat, emptyBytes, int32(p.sampleRate))
|
|
||||||
p.alSource.QueueBuffers(b)
|
|
||||||
}
|
|
||||||
al.PlaySources(p.alSource)
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Proceed(data []byte) error {
|
|
||||||
if err := al.Error(); err != 0 {
|
|
||||||
return fmt.Errorf("driver: before proceed: %d", err)
|
|
||||||
}
|
|
||||||
processedNum := p.alSource.BuffersProcessed()
|
|
||||||
if 0 < processedNum {
|
|
||||||
bufs := make([]al.Buffer, processedNum)
|
|
||||||
p.alSource.UnqueueBuffers(bufs...)
|
|
||||||
if err := al.Error(); err != 0 {
|
|
||||||
return fmt.Errorf("driver: Unqueue in process: %d", err)
|
|
||||||
}
|
|
||||||
p.alBuffers = append(p.alBuffers, bufs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.alBuffers) == 0 {
|
|
||||||
// This can happen (#207)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
buf := p.alBuffers[0]
|
|
||||||
p.alBuffers = p.alBuffers[1:]
|
|
||||||
buf.BufferData(p.alFormat, data, int32(p.sampleRate))
|
|
||||||
p.alSource.QueueBuffers(buf)
|
|
||||||
if err := al.Error(); err != 0 {
|
|
||||||
return fmt.Errorf("driver: Queue in process: %d", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.alSource.State() == al.Stopped || p.alSource.State() == al.Initial {
|
|
||||||
al.RewindSources(p.alSource)
|
|
||||||
al.PlaySources(p.alSource)
|
|
||||||
if err := al.Error(); err != 0 {
|
|
||||||
return fmt.Errorf("driver: PlaySource in process: %d", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Close() error {
|
|
||||||
if err := al.Error(); err != 0 {
|
|
||||||
return fmt.Errorf("driver: error before closing: %d", err)
|
|
||||||
}
|
|
||||||
if p.isClosed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var bs []al.Buffer
|
|
||||||
al.RewindSources(p.alSource)
|
|
||||||
al.StopSources(p.alSource)
|
|
||||||
if n := p.alSource.BuffersQueued(); 0 < n {
|
|
||||||
bs = make([]al.Buffer, n)
|
|
||||||
p.alSource.UnqueueBuffers(bs...)
|
|
||||||
p.alBuffers = append(p.alBuffers, bs...)
|
|
||||||
}
|
|
||||||
p.isClosed = true
|
|
||||||
if err := al.Error(); err != 0 {
|
|
||||||
return fmt.Errorf("driver: error after closing: %d", err)
|
|
||||||
}
|
|
||||||
runtime.SetFinalizer(p, nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,131 +0,0 @@
|
|||||||
// 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 driver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gopherjs/gopherjs/js"
|
|
||||||
)
|
|
||||||
|
|
||||||
// positionDelay is buffer in sample numbers for p.positionInSamples.
|
|
||||||
// Without this, adjusting p.positionInSamples with the context's currenTime
|
|
||||||
// much more often especially on Safari, which is the cause of noise (#307).
|
|
||||||
const positionDelay = 256
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
sampleRate int
|
|
||||||
channelNum int
|
|
||||||
bytesPerSample int
|
|
||||||
positionInSamples int64
|
|
||||||
bufferedData []byte
|
|
||||||
context *js.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIOS() bool {
|
|
||||||
ua := js.Global.Get("navigator").Get("userAgent").String()
|
|
||||||
if !strings.Contains(ua, "iPhone") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAndroidChrome() bool {
|
|
||||||
ua := js.Global.Get("navigator").Get("userAgent").String()
|
|
||||||
if !strings.Contains(ua, "Android") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !strings.Contains(ua, "Chrome") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
|
||||||
class := js.Global.Get("AudioContext")
|
|
||||||
if class == js.Undefined {
|
|
||||||
class = js.Global.Get("webkitAudioContext")
|
|
||||||
}
|
|
||||||
if class == js.Undefined {
|
|
||||||
return nil, errors.New("driver: audio couldn't be initialized")
|
|
||||||
}
|
|
||||||
p := &Player{
|
|
||||||
sampleRate: sampleRate,
|
|
||||||
channelNum: channelNum,
|
|
||||||
bytesPerSample: bytesPerSample,
|
|
||||||
bufferedData: []byte{},
|
|
||||||
context: class.New(),
|
|
||||||
}
|
|
||||||
// iOS and Android Chrome requires touch event to use AudioContext.
|
|
||||||
if isIOS() || isAndroidChrome() {
|
|
||||||
js.Global.Get("document").Call("addEventListener", "touchend", func() {
|
|
||||||
// Resuming is necessary as of Chrome 55+ in some cases like different
|
|
||||||
// domain page in an iframe.
|
|
||||||
p.context.Call("resume")
|
|
||||||
p.context.Call("createBufferSource").Call("start", 0)
|
|
||||||
p.positionInSamples = int64(p.context.Get("currentTime").Float()*float64(p.sampleRate)) + positionDelay
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p.positionInSamples = int64(p.context.Get("currentTime").Float()*float64(p.sampleRate)) + positionDelay
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toLR(data []byte) ([]int16, []int16) {
|
|
||||||
l := make([]int16, len(data)/4)
|
|
||||||
r := make([]int16, len(data)/4)
|
|
||||||
for i := 0; i < len(data)/4; i++ {
|
|
||||||
l[i] = int16(data[4*i]) | int16(data[4*i+1])<<8
|
|
||||||
r[i] = int16(data[4*i+2]) | int16(data[4*i+3])<<8
|
|
||||||
}
|
|
||||||
return l, r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Proceed(data []byte) error {
|
|
||||||
p.bufferedData = append(p.bufferedData, data...)
|
|
||||||
c := int64(p.context.Get("currentTime").Float() * float64(p.sampleRate))
|
|
||||||
if p.positionInSamples+positionDelay < c {
|
|
||||||
p.positionInSamples = c
|
|
||||||
}
|
|
||||||
// Heuristic data size which doesn't cause too much noise and too much delay (#299)
|
|
||||||
dataSize := int(float64(p.sampleRate)/13.5) / 4 * 4
|
|
||||||
for dataSize <= len(p.bufferedData) {
|
|
||||||
data := p.bufferedData[:dataSize]
|
|
||||||
size := len(data) / p.bytesPerSample / p.channelNum
|
|
||||||
// TODO: size must be const or you'll get noise (e.g. sample rate is 22050)
|
|
||||||
buf := p.context.Call("createBuffer", p.channelNum, size, p.sampleRate)
|
|
||||||
l := buf.Call("getChannelData", 0)
|
|
||||||
r := buf.Call("getChannelData", 1)
|
|
||||||
il, ir := toLR(data)
|
|
||||||
const max = 1 << 15
|
|
||||||
for i := 0; i < len(il); i++ {
|
|
||||||
l.SetIndex(i, float64(il[i])/max)
|
|
||||||
r.SetIndex(i, float64(ir[i])/max)
|
|
||||||
}
|
|
||||||
s := p.context.Call("createBufferSource")
|
|
||||||
s.Set("buffer", buf)
|
|
||||||
s.Call("connect", p.context.Get("destination"))
|
|
||||||
s.Call("start", float64(p.positionInSamples+positionDelay)/float64(p.sampleRate))
|
|
||||||
p.positionInSamples += int64(len(il))
|
|
||||||
p.bufferedData = p.bufferedData[dataSize:]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
// 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 darwin linux
|
|
||||||
// +build !js
|
|
||||||
// +build !android
|
|
||||||
// +build !ios
|
|
||||||
|
|
||||||
package driver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/go-openal/openal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// As x/mobile/exp/audio/al is broken on Mac OS X (https://github.com/golang/go/issues/15075),
|
|
||||||
// let's use github.com/hajimehoshi/go-openal instead.
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxBufferNum = 8
|
|
||||||
)
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
alDevice *openal.Device
|
|
||||||
alSource openal.Source
|
|
||||||
alBuffers []openal.Buffer
|
|
||||||
sampleRate int
|
|
||||||
isClosed bool
|
|
||||||
alFormat openal.Format
|
|
||||||
}
|
|
||||||
|
|
||||||
func alFormat(channelNum, bytesPerSample int) openal.Format {
|
|
||||||
switch {
|
|
||||||
case channelNum == 1 && bytesPerSample == 1:
|
|
||||||
return openal.FormatMono8
|
|
||||||
case channelNum == 1 && bytesPerSample == 2:
|
|
||||||
return openal.FormatMono16
|
|
||||||
case channelNum == 2 && bytesPerSample == 1:
|
|
||||||
return openal.FormatStereo8
|
|
||||||
case channelNum == 2 && bytesPerSample == 2:
|
|
||||||
return openal.FormatStereo16
|
|
||||||
}
|
|
||||||
panic(fmt.Sprintf("driver: invalid channel num (%d) or bytes per sample (%d)", channelNum, bytesPerSample))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
|
||||||
d := openal.OpenDevice("")
|
|
||||||
if d == nil {
|
|
||||||
return nil, fmt.Errorf("driver: OpenDevice must not return nil")
|
|
||||||
}
|
|
||||||
c := d.CreateContext()
|
|
||||||
if c == nil {
|
|
||||||
return nil, fmt.Errorf("driver: CreateContext must not return nil")
|
|
||||||
}
|
|
||||||
// Don't check openal.Err until making the current context is done.
|
|
||||||
// Linux might fail this check even though it succeeds (#204).
|
|
||||||
c.Activate()
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("driver: Activate: %v", err)
|
|
||||||
}
|
|
||||||
s := openal.NewSource()
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("driver: NewSource: %v", err)
|
|
||||||
}
|
|
||||||
p := &Player{
|
|
||||||
alDevice: d,
|
|
||||||
alSource: s,
|
|
||||||
alBuffers: []openal.Buffer{},
|
|
||||||
sampleRate: sampleRate,
|
|
||||||
alFormat: alFormat(channelNum, bytesPerSample),
|
|
||||||
}
|
|
||||||
runtime.SetFinalizer(p, (*Player).Close)
|
|
||||||
|
|
||||||
bs := openal.NewBuffers(maxBufferNum)
|
|
||||||
const bufferSize = 1024
|
|
||||||
emptyBytes := make([]byte, bufferSize)
|
|
||||||
for _, b := range bs {
|
|
||||||
// Note that the third argument of only the first buffer is used.
|
|
||||||
b.SetData(p.alFormat, emptyBytes, int32(p.sampleRate))
|
|
||||||
p.alBuffers = append(p.alBuffers, b)
|
|
||||||
}
|
|
||||||
p.alSource.Play()
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Proceed(data []byte) error {
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return fmt.Errorf("driver: starting Proceed: %v", err)
|
|
||||||
}
|
|
||||||
processedNum := p.alSource.BuffersProcessed()
|
|
||||||
if 0 < processedNum {
|
|
||||||
bufs := make([]openal.Buffer, processedNum)
|
|
||||||
p.alSource.UnqueueBuffers(bufs)
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return fmt.Errorf("driver: UnqueueBuffers: %v", err)
|
|
||||||
}
|
|
||||||
p.alBuffers = append(p.alBuffers, bufs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.alBuffers) == 0 {
|
|
||||||
// This can happen (#207)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
buf := p.alBuffers[0]
|
|
||||||
p.alBuffers = p.alBuffers[1:]
|
|
||||||
buf.SetData(p.alFormat, data, int32(p.sampleRate))
|
|
||||||
p.alSource.QueueBuffer(buf)
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return fmt.Errorf("driver: QueueBuffer: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.alSource.State() == openal.Stopped || p.alSource.State() == openal.Initial {
|
|
||||||
p.alSource.Rewind()
|
|
||||||
p.alSource.Play()
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return fmt.Errorf("driver: Rewind or Play: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Close() error {
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return fmt.Errorf("driver: starting Close: %v", err)
|
|
||||||
}
|
|
||||||
if p.isClosed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var bs []openal.Buffer
|
|
||||||
p.alSource.Rewind()
|
|
||||||
p.alSource.Play()
|
|
||||||
if n := p.alSource.BuffersQueued(); 0 < n {
|
|
||||||
bs = make([]openal.Buffer, n)
|
|
||||||
p.alSource.UnqueueBuffers(bs)
|
|
||||||
p.alBuffers = append(p.alBuffers, bs...)
|
|
||||||
}
|
|
||||||
p.alDevice.CloseDevice()
|
|
||||||
p.isClosed = true
|
|
||||||
if err := openal.Err(); err != nil {
|
|
||||||
return fmt.Errorf("driver: CloseDevice: %v", err)
|
|
||||||
}
|
|
||||||
runtime.SetFinalizer(p, nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
// 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 driver
|
|
||||||
|
|
||||||
// TODO: Use golang.org/x/sys/windows (NewLazyDLL) instead of cgo.
|
|
||||||
|
|
||||||
// #cgo LDFLAGS: -lwinmm
|
|
||||||
//
|
|
||||||
// #include <windows.h>
|
|
||||||
// #include <mmsystem.h>
|
|
||||||
//
|
|
||||||
// #define sizeOfWavehdr (sizeof(WAVEHDR))
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
type header struct {
|
|
||||||
buffer unsafe.Pointer
|
|
||||||
bufferSize int
|
|
||||||
waveHdr C.WAVEHDR
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHeader(waveOut C.HWAVEOUT, bufferSize int) (*header, error) {
|
|
||||||
// NOTE: This is never freed so far, and we don't have to because newHeader is called a certain number of times.
|
|
||||||
buf := C.malloc(C.size_t(bufferSize))
|
|
||||||
h := &header{
|
|
||||||
buffer: buf,
|
|
||||||
bufferSize: bufferSize,
|
|
||||||
waveHdr: C.WAVEHDR{
|
|
||||||
lpData: C.LPSTR(buf),
|
|
||||||
dwBufferLength: C.DWORD(bufferSize),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := C.waveOutPrepareHeader(waveOut, &h.waveHdr, C.sizeOfWavehdr); err != C.MMSYSERR_NOERROR {
|
|
||||||
return nil, fmt.Errorf("driver: waveOutPrepareHeader error: %d", err)
|
|
||||||
}
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *header) Write(waveOut C.HWAVEOUT, data []byte) error {
|
|
||||||
if len(data) != h.bufferSize {
|
|
||||||
return errors.New("driver: len(data) must equal to h.bufferSize")
|
|
||||||
}
|
|
||||||
C.memcpy(h.buffer, unsafe.Pointer(&data[0]), C.size_t(h.bufferSize))
|
|
||||||
if err := C.waveOutWrite(waveOut, &h.waveHdr, C.sizeOfWavehdr); err != C.MMSYSERR_NOERROR {
|
|
||||||
return fmt.Errorf("driver: waveOutWriter error: %d", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const numHeader = 8
|
|
||||||
|
|
||||||
type Player struct {
|
|
||||||
out C.HWAVEOUT
|
|
||||||
buffer []byte
|
|
||||||
headers []*header
|
|
||||||
}
|
|
||||||
|
|
||||||
const bufferSize = 4096
|
|
||||||
|
|
||||||
func NewPlayer(sampleRate, channelNum, bytesPerSample int) (*Player, error) {
|
|
||||||
numBlockAlign := channelNum * bytesPerSample
|
|
||||||
f := C.WAVEFORMATEX{
|
|
||||||
wFormatTag: C.WAVE_FORMAT_PCM,
|
|
||||||
nChannels: C.WORD(channelNum),
|
|
||||||
nSamplesPerSec: C.DWORD(sampleRate),
|
|
||||||
nAvgBytesPerSec: C.DWORD(sampleRate * numBlockAlign),
|
|
||||||
wBitsPerSample: C.WORD(bytesPerSample * 8),
|
|
||||||
nBlockAlign: C.WORD(numBlockAlign),
|
|
||||||
}
|
|
||||||
var w C.HWAVEOUT
|
|
||||||
if err := C.waveOutOpen(&w, C.WAVE_MAPPER, &f, 0, 0, C.CALLBACK_NULL); err != C.MMSYSERR_NOERROR {
|
|
||||||
return nil, fmt.Errorf("driver: waveOutOpen error: %d", err)
|
|
||||||
}
|
|
||||||
p := &Player{
|
|
||||||
out: w,
|
|
||||||
buffer: []byte{},
|
|
||||||
headers: make([]*header, numHeader),
|
|
||||||
}
|
|
||||||
for i := 0; i < numHeader; i++ {
|
|
||||||
var err error
|
|
||||||
p.headers[i], err = newHeader(w, bufferSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Proceed(data []byte) error {
|
|
||||||
p.buffer = append(p.buffer, data...)
|
|
||||||
if bufferSize > len(p.buffer) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
headerToWrite := (*header)(nil)
|
|
||||||
for _, h := range p.headers {
|
|
||||||
// TODO: Need to check WHDR_DONE?
|
|
||||||
if h.waveHdr.dwFlags&C.WHDR_INQUEUE == 0 {
|
|
||||||
headerToWrite = h
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if headerToWrite == nil {
|
|
||||||
// This can happen (#207)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := headerToWrite.Write(p.out, p.buffer[:bufferSize]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.buffer = p.buffer[bufferSize:]
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Player) Close() error {
|
|
||||||
// TODO: Implement this
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user