mirror of
https://github.com/make-42/hayai.git
synced 2025-01-18 18:47:10 +01:00
391 lines
13 KiB
Go
391 lines
13 KiB
Go
// 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 oto
|
|
|
|
/*
|
|
|
|
#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 bitDepthInBytes, jobject* audioTrack, int bufferSize) {
|
|
JavaVM* vm = (JavaVM*)java_vm;
|
|
JNIEnv* env = (JNIEnv*)jni_env;
|
|
|
|
jclass android_os_Build_VERSION = (*env)->FindClass(env, "android/os/Build$VERSION");
|
|
const jint availableSDK =
|
|
(*env)->GetStaticIntField(
|
|
env, android_os_Build_VERSION,
|
|
(*env)->GetStaticFieldID(env, android_os_Build_VERSION, "SDK_INT", "I"));
|
|
(*env)->DeleteLocalRef(env, android_os_Build_VERSION);
|
|
|
|
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 (bitDepthInBytes) {
|
|
case 1:
|
|
encoding = android_media_AudioFormat_ENCODING_PCM_8BIT;
|
|
break;
|
|
case 2:
|
|
encoding = android_media_AudioFormat_ENCODING_PCM_16BIT;
|
|
break;
|
|
default:
|
|
return "invalid bitDepthInBytes";
|
|
}
|
|
|
|
// If the available Android SDK is at least 24 (7.0 Nougat), the FLAG_LOW_LATENCY is available.
|
|
// This requires a different constructor.
|
|
if (availableSDK >= 24) {
|
|
jclass android_media_AudioAttributes_Builder;
|
|
jclass android_media_AudioFormat_Builder;
|
|
jclass android_media_AudioAttributes;
|
|
|
|
local = (*env)->FindClass(env, "android/media/AudioAttributes$Builder");
|
|
android_media_AudioAttributes_Builder = (*env)->NewGlobalRef(env, local);
|
|
(*env)->DeleteLocalRef(env, local);
|
|
|
|
local = (*env)->FindClass(env, "android/media/AudioFormat$Builder");
|
|
android_media_AudioFormat_Builder = (*env)->NewGlobalRef(env, local);
|
|
(*env)->DeleteLocalRef(env, local);
|
|
|
|
local = (*env)->FindClass(env, "android/media/AudioAttributes");
|
|
android_media_AudioAttributes = (*env)->NewGlobalRef(env, local);
|
|
(*env)->DeleteLocalRef(env, local);
|
|
|
|
jint android_media_AudioAttributes_USAGE_UNKNOWN =
|
|
(*env)->GetStaticIntField(
|
|
env, android_media_AudioAttributes,
|
|
(*env)->GetStaticFieldID(env, android_media_AudioAttributes, "USAGE_UNKNOWN", "I"));
|
|
jint android_media_AudioAttributes_CONTENT_TYPE_UNKNOWN =
|
|
(*env)->GetStaticIntField(
|
|
env, android_media_AudioAttributes,
|
|
(*env)->GetStaticFieldID(env, android_media_AudioAttributes, "CONTENT_TYPE_UNKNOWN", "I"));
|
|
jint android_media_AudioAttributes_FLAG_LOW_LATENCY =
|
|
(*env)->GetStaticIntField(
|
|
env, android_media_AudioAttributes,
|
|
(*env)->GetStaticFieldID(env, android_media_AudioAttributes, "FLAG_LOW_LATENCY", "I"));
|
|
|
|
const jobject aattrBld =
|
|
(*env)->NewObject(
|
|
env, android_media_AudioAttributes_Builder,
|
|
(*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "<init>", "()V"));
|
|
|
|
(*env)->CallObjectMethod(
|
|
env, aattrBld,
|
|
(*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "setUsage", "(I)Landroid/media/AudioAttributes$Builder;"),
|
|
android_media_AudioAttributes_USAGE_UNKNOWN);
|
|
(*env)->CallObjectMethod(
|
|
env, aattrBld,
|
|
(*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "setContentType", "(I)Landroid/media/AudioAttributes$Builder;"),
|
|
android_media_AudioAttributes_CONTENT_TYPE_UNKNOWN);
|
|
(*env)->CallObjectMethod(
|
|
env, aattrBld,
|
|
(*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "setFlags", "(I)Landroid/media/AudioAttributes$Builder;"),
|
|
android_media_AudioAttributes_FLAG_LOW_LATENCY);
|
|
const jobject aattr =
|
|
(*env)->CallObjectMethod(
|
|
env, aattrBld,
|
|
(*env)->GetMethodID(env, android_media_AudioAttributes_Builder, "build", "()Landroid/media/AudioAttributes;"));
|
|
(*env)->DeleteLocalRef(env, aattrBld);
|
|
|
|
const jobject afmtBld =
|
|
(*env)->NewObject(
|
|
env, android_media_AudioFormat_Builder,
|
|
(*env)->GetMethodID(env, android_media_AudioFormat_Builder, "<init>", "()V"));
|
|
(*env)->CallObjectMethod(
|
|
env, afmtBld,
|
|
(*env)->GetMethodID(env, android_media_AudioFormat_Builder, "setSampleRate", "(I)Landroid/media/AudioFormat$Builder;"),
|
|
sampleRate);
|
|
(*env)->CallObjectMethod(
|
|
env, afmtBld,
|
|
(*env)->GetMethodID(env, android_media_AudioFormat_Builder, "setEncoding", "(I)Landroid/media/AudioFormat$Builder;"),
|
|
encoding);
|
|
(*env)->CallObjectMethod(
|
|
env, afmtBld,
|
|
(*env)->GetMethodID(env, android_media_AudioFormat_Builder, "setChannelMask", "(I)Landroid/media/AudioFormat$Builder;"),
|
|
channel);
|
|
const jobject afmt =
|
|
(*env)->CallObjectMethod(
|
|
env, afmtBld,
|
|
(*env)->GetMethodID(env, android_media_AudioFormat_Builder, "build", "()Landroid/media/AudioFormat;"));
|
|
(*env)->DeleteLocalRef(env, afmtBld);
|
|
|
|
const jobject tmpAudioTrack =
|
|
(*env)->NewObject(
|
|
env, android_media_AudioTrack,
|
|
(*env)->GetMethodID(env, android_media_AudioTrack, "<init>", "(Landroid/media/AudioAttributes;Landroid/media/AudioFormat;III)V"),
|
|
aattr, afmt, bufferSize, android_media_AudioTrack_MODE_STREAM, 0);
|
|
*audioTrack = (*env)->NewGlobalRef(env, tmpAudioTrack);
|
|
(*env)->DeleteLocalRef(env, tmpAudioTrack);
|
|
(*env)->DeleteLocalRef(env, aattr);
|
|
(*env)->DeleteLocalRef(env, afmt);
|
|
} else {
|
|
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);
|
|
*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 bitDepthInBytes, void* data, int length) {
|
|
JavaVM* vm = (JavaVM*)java_vm;
|
|
JNIEnv* env = (JNIEnv*)jni_env;
|
|
|
|
jbyteArray arrInBytes;
|
|
jshortArray arrInShorts;
|
|
switch (bitDepthInBytes) {
|
|
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 (bitDepthInBytes) {
|
|
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;
|
|
}
|
|
|
|
static char* releaseAudioTrack(uintptr_t java_vm, uintptr_t jni_env,
|
|
jobject audioTrack) {
|
|
JavaVM* vm = (JavaVM*)java_vm;
|
|
JNIEnv* env = (JNIEnv*)jni_env;
|
|
|
|
(*env)->CallVoidMethod(
|
|
env, audioTrack,
|
|
(*env)->GetMethodID(env, android_media_AudioTrack, "release", "()V"));
|
|
return NULL;
|
|
}
|
|
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"runtime"
|
|
"unsafe"
|
|
|
|
"golang.org/x/mobile/app"
|
|
)
|
|
|
|
type driver struct {
|
|
sampleRate int
|
|
channelNum int
|
|
bitDepthInBytes int
|
|
audioTrack C.jobject
|
|
chErr chan error
|
|
chBuffer chan []byte
|
|
tmp []byte
|
|
bufferSize int
|
|
}
|
|
|
|
func newDriver(sampleRate, channelNum, bitDepthInBytes, bufferSizeInBytes int) (tryWriteCloser, error) {
|
|
p := &driver{
|
|
sampleRate: sampleRate,
|
|
channelNum: channelNum,
|
|
bitDepthInBytes: bitDepthInBytes,
|
|
chErr: make(chan error),
|
|
chBuffer: make(chan []byte),
|
|
}
|
|
runtime.SetFinalizer(p, (*driver).Close)
|
|
|
|
if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
|
|
audioTrack := C.jobject(0)
|
|
bufferSize := C.int(bufferSizeInBytes)
|
|
if msg := C.initAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
|
|
C.int(sampleRate), C.int(channelNum), C.int(bitDepthInBytes),
|
|
&audioTrack, bufferSize); msg != nil {
|
|
return errors.New("oto: initAutioTrack failed: " + 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 *driver) loop() {
|
|
for bufInBytes := range p.chBuffer {
|
|
var bufInShorts []int16
|
|
if p.bitDepthInBytes == 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 := app.RunOnJVM(func(vm, env, ctx uintptr) error {
|
|
msg := (*C.char)(nil)
|
|
switch p.bitDepthInBytes {
|
|
case 1:
|
|
msg = C.writeToAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
|
|
p.audioTrack, C.int(p.bitDepthInBytes),
|
|
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.bitDepthInBytes),
|
|
unsafe.Pointer(&bufInShorts[0]), C.int(len(bufInShorts)))
|
|
default:
|
|
panic("not reach")
|
|
}
|
|
if msg != nil {
|
|
return errors.New("oto: loop failed: " + C.GoString(msg))
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
p.chErr <- err
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *driver) TryWrite(data []byte) (int, error) {
|
|
n := min(len(data), p.bufferSize-len(p.tmp))
|
|
p.tmp = append(p.tmp, data[:n]...)
|
|
|
|
if len(p.tmp) < p.bufferSize {
|
|
return n, nil
|
|
}
|
|
|
|
select {
|
|
case p.chBuffer <- p.tmp:
|
|
case err := <-p.chErr:
|
|
return 0, err
|
|
}
|
|
|
|
p.tmp = nil
|
|
return n, nil
|
|
}
|
|
|
|
func (p *driver) Close() error {
|
|
if p.audioTrack == 0 {
|
|
return nil
|
|
}
|
|
|
|
runtime.SetFinalizer(p, nil)
|
|
err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
|
|
if msg := C.releaseAudioTrack(C.uintptr_t(vm), C.uintptr_t(env),
|
|
p.audioTrack); msg != nil {
|
|
return errors.New("oto: release failed: " + C.GoString(msg))
|
|
}
|
|
return nil
|
|
})
|
|
|
|
p.audioTrack = 0
|
|
return err
|
|
}
|
|
|
|
func (d *driver) tryWriteCanReturnWithoutWaiting() bool {
|
|
return true
|
|
}
|