// 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 #include 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, "", "()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, "", "()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, "", "(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, "", "(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 }