audio: Make 'oto' package and use that (#351)

This commit is contained in:
Hajime Hoshi 2017-05-04 21:09:02 +09:00
parent 951e5bccef
commit 766072cdbb
6 changed files with 3 additions and 837 deletions

View File

@ -35,7 +35,7 @@ import (
"time"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio/internal/driver"
"github.com/hajimehoshi/oto"
)
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.
type Context struct {
players *players
driver *driver.Player
driver *oto.Player
sampleRate int
frames int64
writtenBytes int64
@ -239,7 +239,7 @@ func (c *Context) Update() error {
// e.g. a variable for JVM on Android might not be set.
if c.driver == nil {
// 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
if err != nil {
return err

View File

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

View File

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

View File

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

View File

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

View File

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