2018-03-10 03:28:09 +00:00
|
|
|
/*
|
|
|
|
* Android AudioTrack audio output driver.
|
|
|
|
* Copyright (C) 2018 Aman Gupta <aman@tmm1.net>
|
|
|
|
* Copyright (C) 2012-2015 VLC authors and VideoLAN, VideoLabs
|
|
|
|
* Authors: Thomas Guillem <thomas@gllm.fr>
|
|
|
|
* Ming Hu <tewilove@gmail.com>
|
|
|
|
*
|
|
|
|
* This file is part of mpv.
|
|
|
|
*
|
|
|
|
* mpv is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
|
|
* License as published by the Free Software Foundation; either
|
|
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* mpv is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Lesser General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
|
|
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "ao.h"
|
|
|
|
#include "internal.h"
|
|
|
|
#include "common/msg.h"
|
|
|
|
#include "audio/format.h"
|
|
|
|
#include "options/m_option.h"
|
|
|
|
#include "osdep/threads.h"
|
|
|
|
#include "osdep/timer.h"
|
|
|
|
#include "misc/jni.h"
|
|
|
|
|
|
|
|
struct priv {
|
|
|
|
jobject audiotrack;
|
|
|
|
jint samplerate;
|
|
|
|
jint channel_config;
|
|
|
|
jint format;
|
|
|
|
jint size;
|
|
|
|
|
|
|
|
jobject timestamp;
|
|
|
|
int64_t timestamp_fetched;
|
|
|
|
bool timestamp_set;
|
|
|
|
int timestamp_stable;
|
|
|
|
|
|
|
|
uint32_t written_frames; /* requires uint32_t rollover semantics */
|
|
|
|
uint32_t playhead_pos;
|
|
|
|
uint32_t playhead_offset;
|
|
|
|
bool reset_pending;
|
|
|
|
|
|
|
|
void *chunk;
|
|
|
|
int chunksize;
|
|
|
|
jbyteArray bytearray;
|
|
|
|
jshortArray shortarray;
|
|
|
|
jfloatArray floatarray;
|
|
|
|
jobject bbuf;
|
|
|
|
|
2023-02-20 03:32:50 +00:00
|
|
|
bool cfg_pcm_float;
|
2018-03-10 03:28:09 +00:00
|
|
|
int cfg_session_id;
|
|
|
|
|
|
|
|
bool needs_timestamp_offset;
|
|
|
|
int64_t timestamp_offset;
|
|
|
|
|
|
|
|
bool thread_terminate;
|
|
|
|
bool thread_created;
|
|
|
|
pthread_t thread;
|
|
|
|
pthread_mutex_t lock;
|
|
|
|
pthread_cond_t wakeup;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct JNIByteBuffer {
|
|
|
|
jclass clazz;
|
|
|
|
jmethodID clear;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} ByteBuffer = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIByteBuffer, member)
|
|
|
|
{"java/nio/ByteBuffer", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
|
|
|
|
{"java/nio/ByteBuffer", "clear", "()Ljava/nio/Buffer;", MP_JNI_METHOD, OFFSET(clear), 1},
|
|
|
|
{0},
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
|
|
|
struct JNIAudioTrack {
|
|
|
|
jclass clazz;
|
|
|
|
jmethodID ctor;
|
2021-10-19 21:25:18 +00:00
|
|
|
jmethodID ctorV21;
|
2018-03-10 03:28:09 +00:00
|
|
|
jmethodID release;
|
|
|
|
jmethodID getState;
|
|
|
|
jmethodID getPlayState;
|
|
|
|
jmethodID play;
|
|
|
|
jmethodID stop;
|
|
|
|
jmethodID flush;
|
|
|
|
jmethodID pause;
|
|
|
|
jmethodID write;
|
|
|
|
jmethodID writeFloat;
|
|
|
|
jmethodID writeShortV23;
|
|
|
|
jmethodID writeBufferV21;
|
2021-10-19 21:26:26 +00:00
|
|
|
jmethodID getBufferSizeInFramesV23;
|
2018-03-10 03:28:09 +00:00
|
|
|
jmethodID getPlaybackHeadPosition;
|
|
|
|
jmethodID getTimestamp;
|
|
|
|
jmethodID getLatency;
|
|
|
|
jmethodID getMinBufferSize;
|
|
|
|
jmethodID getNativeOutputSampleRate;
|
|
|
|
jint STATE_INITIALIZED;
|
|
|
|
jint PLAYSTATE_STOPPED;
|
|
|
|
jint PLAYSTATE_PAUSED;
|
|
|
|
jint PLAYSTATE_PLAYING;
|
|
|
|
jint MODE_STREAM;
|
|
|
|
jint ERROR;
|
|
|
|
jint ERROR_BAD_VALUE;
|
|
|
|
jint ERROR_INVALID_OPERATION;
|
|
|
|
jint WRITE_BLOCKING;
|
|
|
|
jint WRITE_NON_BLOCKING;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioTrack = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioTrack, member)
|
|
|
|
{"android/media/AudioTrack", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
|
|
|
|
{"android/media/AudioTrack", "<init>", "(IIIIIII)V", MP_JNI_METHOD, OFFSET(ctor), 1},
|
2021-10-19 21:25:18 +00:00
|
|
|
{"android/media/AudioTrack", "<init>", "(Landroid/media/AudioAttributes;Landroid/media/AudioFormat;III)V", MP_JNI_METHOD, OFFSET(ctorV21), 0},
|
2018-03-10 03:28:09 +00:00
|
|
|
{"android/media/AudioTrack", "release", "()V", MP_JNI_METHOD, OFFSET(release), 1},
|
|
|
|
{"android/media/AudioTrack", "getState", "()I", MP_JNI_METHOD, OFFSET(getState), 1},
|
|
|
|
{"android/media/AudioTrack", "getPlayState", "()I", MP_JNI_METHOD, OFFSET(getPlayState), 1},
|
|
|
|
{"android/media/AudioTrack", "play", "()V", MP_JNI_METHOD, OFFSET(play), 1},
|
|
|
|
{"android/media/AudioTrack", "stop", "()V", MP_JNI_METHOD, OFFSET(stop), 1},
|
|
|
|
{"android/media/AudioTrack", "flush", "()V", MP_JNI_METHOD, OFFSET(flush), 1},
|
|
|
|
{"android/media/AudioTrack", "pause", "()V", MP_JNI_METHOD, OFFSET(pause), 1},
|
|
|
|
{"android/media/AudioTrack", "write", "([BII)I", MP_JNI_METHOD, OFFSET(write), 1},
|
|
|
|
{"android/media/AudioTrack", "write", "([FIII)I", MP_JNI_METHOD, OFFSET(writeFloat), 1},
|
|
|
|
{"android/media/AudioTrack", "write", "([SIII)I", MP_JNI_METHOD, OFFSET(writeShortV23), 0},
|
|
|
|
{"android/media/AudioTrack", "write", "(Ljava/nio/ByteBuffer;II)I", MP_JNI_METHOD, OFFSET(writeBufferV21), 1},
|
2021-10-19 21:26:26 +00:00
|
|
|
{"android/media/AudioTrack", "getBufferSizeInFrames", "()I", MP_JNI_METHOD, OFFSET(getBufferSizeInFramesV23), 0},
|
2018-03-10 03:28:09 +00:00
|
|
|
{"android/media/AudioTrack", "getTimestamp", "(Landroid/media/AudioTimestamp;)Z", MP_JNI_METHOD, OFFSET(getTimestamp), 1},
|
|
|
|
{"android/media/AudioTrack", "getPlaybackHeadPosition", "()I", MP_JNI_METHOD, OFFSET(getPlaybackHeadPosition), 1},
|
|
|
|
{"android/media/AudioTrack", "getLatency", "()I", MP_JNI_METHOD, OFFSET(getLatency), 1},
|
|
|
|
{"android/media/AudioTrack", "getMinBufferSize", "(III)I", MP_JNI_STATIC_METHOD, OFFSET(getMinBufferSize), 1},
|
|
|
|
{"android/media/AudioTrack", "getNativeOutputSampleRate", "(I)I", MP_JNI_STATIC_METHOD, OFFSET(getNativeOutputSampleRate), 1},
|
|
|
|
{"android/media/AudioTrack", "WRITE_BLOCKING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(WRITE_BLOCKING), 0},
|
|
|
|
{"android/media/AudioTrack", "WRITE_NON_BLOCKING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(WRITE_NON_BLOCKING), 0},
|
|
|
|
{"android/media/AudioTrack", "STATE_INITIALIZED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(STATE_INITIALIZED), 1},
|
|
|
|
{"android/media/AudioTrack", "PLAYSTATE_STOPPED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_STOPPED), 1},
|
|
|
|
{"android/media/AudioTrack", "PLAYSTATE_PAUSED", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_PAUSED), 1},
|
|
|
|
{"android/media/AudioTrack", "PLAYSTATE_PLAYING", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(PLAYSTATE_PLAYING), 1},
|
|
|
|
{"android/media/AudioTrack", "MODE_STREAM", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(MODE_STREAM), 1},
|
|
|
|
{"android/media/AudioTrack", "ERROR", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR), 1},
|
|
|
|
{"android/media/AudioTrack", "ERROR_BAD_VALUE", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_BAD_VALUE), 1},
|
|
|
|
{"android/media/AudioTrack", "ERROR_INVALID_OPERATION", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_INVALID_OPERATION), 1},
|
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
2021-10-19 21:25:18 +00:00
|
|
|
struct JNIAudioAttributes {
|
|
|
|
jclass clazz;
|
|
|
|
jint CONTENT_TYPE_MOVIE;
|
2023-08-06 15:09:13 +00:00
|
|
|
jint CONTENT_TYPE_MUSIC;
|
2021-10-19 21:25:18 +00:00
|
|
|
jint USAGE_MEDIA;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioAttributes = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioAttributes, member)
|
|
|
|
{"android/media/AudioAttributes", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0},
|
|
|
|
{"android/media/AudioAttributes", "CONTENT_TYPE_MOVIE", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CONTENT_TYPE_MOVIE), 0},
|
2023-08-06 15:09:13 +00:00
|
|
|
{"android/media/AudioAttributes", "CONTENT_TYPE_MUSIC", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CONTENT_TYPE_MUSIC), 0},
|
2021-10-19 21:25:18 +00:00
|
|
|
{"android/media/AudioAttributes", "USAGE_MEDIA", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(USAGE_MEDIA), 0},
|
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
|
|
|
struct JNIAudioAttributesBuilder {
|
|
|
|
jclass clazz;
|
|
|
|
jmethodID ctor;
|
|
|
|
jmethodID setUsage;
|
|
|
|
jmethodID setContentType;
|
|
|
|
jmethodID build;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioAttributesBuilder = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioAttributesBuilder, member)
|
|
|
|
{"android/media/AudioAttributes$Builder", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0},
|
|
|
|
{"android/media/AudioAttributes$Builder", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 0},
|
|
|
|
{"android/media/AudioAttributes$Builder", "setUsage", "(I)Landroid/media/AudioAttributes$Builder;", MP_JNI_METHOD, OFFSET(setUsage), 0},
|
|
|
|
{"android/media/AudioAttributes$Builder", "setContentType", "(I)Landroid/media/AudioAttributes$Builder;", MP_JNI_METHOD, OFFSET(setContentType), 0},
|
|
|
|
{"android/media/AudioAttributes$Builder", "build", "()Landroid/media/AudioAttributes;", MP_JNI_METHOD, OFFSET(build), 0},
|
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
2018-03-10 03:28:09 +00:00
|
|
|
struct JNIAudioFormat {
|
|
|
|
jclass clazz;
|
|
|
|
jint ENCODING_PCM_8BIT;
|
|
|
|
jint ENCODING_PCM_16BIT;
|
|
|
|
jint ENCODING_PCM_FLOAT;
|
|
|
|
jint ENCODING_IEC61937;
|
|
|
|
jint CHANNEL_OUT_MONO;
|
|
|
|
jint CHANNEL_OUT_STEREO;
|
|
|
|
jint CHANNEL_OUT_FRONT_CENTER;
|
2023-08-06 16:00:19 +00:00
|
|
|
jint CHANNEL_OUT_QUAD;
|
2018-03-10 03:28:09 +00:00
|
|
|
jint CHANNEL_OUT_5POINT1;
|
2023-08-06 16:00:19 +00:00
|
|
|
jint CHANNEL_OUT_BACK_CENTER;
|
|
|
|
jint CHANNEL_OUT_7POINT1_SURROUND;
|
2018-03-10 03:28:09 +00:00
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioFormat = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioFormat, member)
|
|
|
|
{"android/media/AudioFormat", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
|
|
|
|
{"android/media/AudioFormat", "ENCODING_PCM_8BIT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_8BIT), 1},
|
|
|
|
{"android/media/AudioFormat", "ENCODING_PCM_16BIT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_16BIT), 1},
|
|
|
|
{"android/media/AudioFormat", "ENCODING_PCM_FLOAT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_PCM_FLOAT), 1},
|
|
|
|
{"android/media/AudioFormat", "ENCODING_IEC61937", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ENCODING_IEC61937), 0},
|
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_MONO", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_MONO), 1},
|
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_STEREO", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_STEREO), 1},
|
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_FRONT_CENTER", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_FRONT_CENTER), 1},
|
2023-08-06 16:00:19 +00:00
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_QUAD", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_QUAD), 1},
|
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_5POINT1", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_5POINT1), 1},
|
2018-03-10 03:28:09 +00:00
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_BACK_CENTER", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_BACK_CENTER), 1},
|
2023-08-06 16:00:19 +00:00
|
|
|
{"android/media/AudioFormat", "CHANNEL_OUT_7POINT1_SURROUND", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(CHANNEL_OUT_7POINT1_SURROUND), 0},
|
2018-03-10 03:28:09 +00:00
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
2021-10-19 21:25:18 +00:00
|
|
|
struct JNIAudioFormatBuilder {
|
|
|
|
jclass clazz;
|
|
|
|
jmethodID ctor;
|
|
|
|
jmethodID setEncoding;
|
|
|
|
jmethodID setSampleRate;
|
|
|
|
jmethodID setChannelMask;
|
|
|
|
jmethodID build;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioFormatBuilder = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioFormatBuilder, member)
|
|
|
|
{"android/media/AudioFormat$Builder", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 0},
|
|
|
|
{"android/media/AudioFormat$Builder", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 0},
|
|
|
|
{"android/media/AudioFormat$Builder", "setEncoding", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setEncoding), 0},
|
|
|
|
{"android/media/AudioFormat$Builder", "setSampleRate", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setSampleRate), 0},
|
|
|
|
{"android/media/AudioFormat$Builder", "setChannelMask", "(I)Landroid/media/AudioFormat$Builder;", MP_JNI_METHOD, OFFSET(setChannelMask), 0},
|
|
|
|
{"android/media/AudioFormat$Builder", "build", "()Landroid/media/AudioFormat;", MP_JNI_METHOD, OFFSET(build), 0},
|
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
2018-03-10 03:28:09 +00:00
|
|
|
struct JNIAudioManager {
|
|
|
|
jclass clazz;
|
|
|
|
jint ERROR_DEAD_OBJECT;
|
|
|
|
jint STREAM_MUSIC;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioManager = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioManager, member)
|
|
|
|
{"android/media/AudioManager", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
|
|
|
|
{"android/media/AudioManager", "STREAM_MUSIC", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(STREAM_MUSIC), 1},
|
|
|
|
{"android/media/AudioManager", "ERROR_DEAD_OBJECT", "I", MP_JNI_STATIC_FIELD_AS_INT, OFFSET(ERROR_DEAD_OBJECT), 0},
|
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
|
|
|
struct JNIAudioTimestamp {
|
|
|
|
jclass clazz;
|
|
|
|
jmethodID ctor;
|
|
|
|
jfieldID framePosition;
|
|
|
|
jfieldID nanoTime;
|
|
|
|
struct MPJniField mapping[];
|
|
|
|
} AudioTimestamp = {.mapping = {
|
|
|
|
#define OFFSET(member) offsetof(struct JNIAudioTimestamp, member)
|
|
|
|
{"android/media/AudioTimestamp", NULL, NULL, MP_JNI_CLASS, OFFSET(clazz), 1},
|
|
|
|
{"android/media/AudioTimestamp", "<init>", "()V", MP_JNI_METHOD, OFFSET(ctor), 1},
|
|
|
|
{"android/media/AudioTimestamp", "framePosition", "J", MP_JNI_FIELD, OFFSET(framePosition), 1},
|
|
|
|
{"android/media/AudioTimestamp", "nanoTime", "J", MP_JNI_FIELD, OFFSET(nanoTime), 1},
|
|
|
|
{0}
|
|
|
|
#undef OFFSET
|
|
|
|
}};
|
|
|
|
|
2021-10-19 21:25:18 +00:00
|
|
|
#define MP_JNI_DELETELOCAL(o) (*env)->DeleteLocalRef(env, o)
|
|
|
|
|
2018-03-10 03:28:09 +00:00
|
|
|
static int AudioTrack_New(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
2021-10-19 21:25:18 +00:00
|
|
|
jobject audiotrack = NULL;
|
|
|
|
|
|
|
|
if (AudioTrack.ctorV21) {
|
|
|
|
MP_VERBOSE(ao, "Using API21 initializer\n");
|
|
|
|
jobject tmp = NULL;
|
2018-03-10 03:28:09 +00:00
|
|
|
|
2021-10-19 21:25:18 +00:00
|
|
|
jobject format_builder = MP_JNI_NEW(AudioFormatBuilder.clazz, AudioFormatBuilder.ctor);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setEncoding, p->format);
|
|
|
|
MP_JNI_DELETELOCAL(tmp);
|
|
|
|
tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setSampleRate, p->samplerate);
|
|
|
|
MP_JNI_DELETELOCAL(tmp);
|
|
|
|
tmp = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.setChannelMask, p->channel_config);
|
|
|
|
MP_JNI_DELETELOCAL(tmp);
|
|
|
|
jobject format = MP_JNI_CALL_OBJECT(format_builder, AudioFormatBuilder.build);
|
|
|
|
MP_JNI_DELETELOCAL(format_builder);
|
|
|
|
|
|
|
|
jobject attr_builder = MP_JNI_NEW(AudioAttributesBuilder.clazz, AudioAttributesBuilder.ctor);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
tmp = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.setUsage, AudioAttributes.USAGE_MEDIA);
|
|
|
|
MP_JNI_DELETELOCAL(tmp);
|
2023-08-06 15:09:13 +00:00
|
|
|
jint content_type = (ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC) ?
|
|
|
|
AudioAttributes.CONTENT_TYPE_MUSIC : AudioAttributes.CONTENT_TYPE_MOVIE;
|
|
|
|
tmp = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.setContentType, content_type);
|
2021-10-19 21:25:18 +00:00
|
|
|
MP_JNI_DELETELOCAL(tmp);
|
|
|
|
jobject attr = MP_JNI_CALL_OBJECT(attr_builder, AudioAttributesBuilder.build);
|
|
|
|
MP_JNI_DELETELOCAL(attr_builder);
|
|
|
|
|
|
|
|
audiotrack = MP_JNI_NEW(
|
|
|
|
AudioTrack.clazz,
|
|
|
|
AudioTrack.ctorV21,
|
|
|
|
attr,
|
|
|
|
format,
|
|
|
|
p->size,
|
|
|
|
AudioTrack.MODE_STREAM,
|
|
|
|
p->cfg_session_id
|
|
|
|
);
|
|
|
|
|
|
|
|
MP_JNI_DELETELOCAL(format);
|
|
|
|
MP_JNI_DELETELOCAL(attr);
|
|
|
|
} else {
|
|
|
|
MP_VERBOSE(ao, "Using legacy initializer\n");
|
|
|
|
audiotrack = MP_JNI_NEW(
|
|
|
|
AudioTrack.clazz,
|
|
|
|
AudioTrack.ctor,
|
|
|
|
AudioManager.STREAM_MUSIC,
|
|
|
|
p->samplerate,
|
|
|
|
p->channel_config,
|
|
|
|
p->format,
|
|
|
|
p->size,
|
|
|
|
AudioTrack.MODE_STREAM,
|
|
|
|
p->cfg_session_id
|
|
|
|
);
|
|
|
|
}
|
2023-08-06 13:57:32 +00:00
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0 || !audiotrack) {
|
2018-03-10 03:28:09 +00:00
|
|
|
MP_FATAL(ao, "AudioTrack Init failed\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (MP_JNI_CALL_INT(audiotrack, AudioTrack.getState) != AudioTrack.STATE_INITIALIZED) {
|
|
|
|
MP_JNI_CALL_VOID(audiotrack, AudioTrack.release);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
(*env)->DeleteLocalRef(env, audiotrack);
|
|
|
|
MP_ERR(ao, "AudioTrack.getState failed\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2021-10-19 21:26:26 +00:00
|
|
|
if (AudioTrack.getBufferSizeInFramesV23) {
|
|
|
|
int bufferSize = MP_JNI_CALL_INT(audiotrack, AudioTrack.getBufferSizeInFramesV23);
|
|
|
|
if (bufferSize > 0) {
|
|
|
|
MP_VERBOSE(ao, "AudioTrack.getBufferSizeInFrames = %d\n", bufferSize);
|
|
|
|
ao->device_buffer = bufferSize;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-10 03:28:09 +00:00
|
|
|
p->audiotrack = (*env)->NewGlobalRef(env, audiotrack);
|
|
|
|
(*env)->DeleteLocalRef(env, audiotrack);
|
|
|
|
if (!p->audiotrack)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int AudioTrack_Recreate(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.release);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
(*env)->DeleteGlobalRef(env, p->audiotrack);
|
|
|
|
p->audiotrack = NULL;
|
|
|
|
return AudioTrack_New(ao);
|
|
|
|
}
|
|
|
|
|
|
|
|
static uint32_t AudioTrack_getPlaybackHeadPosition(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
if (!p->audiotrack)
|
|
|
|
return 0;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
uint32_t pos = 0;
|
2023-09-10 00:09:28 +00:00
|
|
|
int64_t now = mp_raw_time_ns();
|
2018-03-10 03:28:09 +00:00
|
|
|
int state = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlayState);
|
|
|
|
|
|
|
|
int stable_count = 20;
|
2023-09-15 21:08:27 +00:00
|
|
|
int64_t wait = p->timestamp_stable < stable_count ? 50000000 : 3000000000;
|
2018-03-10 03:28:09 +00:00
|
|
|
|
|
|
|
if (state == AudioTrack.PLAYSTATE_PLAYING && p->format != AudioFormat.ENCODING_IEC61937 &&
|
|
|
|
(p->timestamp_fetched == 0 || now - p->timestamp_fetched >= wait)) {
|
|
|
|
if (!p->timestamp_fetched)
|
|
|
|
p->timestamp_stable = 0;
|
|
|
|
|
2023-09-15 21:08:27 +00:00
|
|
|
int64_t time1 = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime);
|
2018-03-10 03:28:09 +00:00
|
|
|
if (MP_JNI_CALL_BOOL(p->audiotrack, AudioTrack.getTimestamp, p->timestamp)) {
|
|
|
|
p->timestamp_set = true;
|
|
|
|
p->timestamp_fetched = now;
|
|
|
|
if (p->timestamp_stable < stable_count) {
|
|
|
|
uint32_t fpos = 0xFFFFFFFFL & MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.framePosition);
|
2023-09-15 21:08:27 +00:00
|
|
|
int64_t time2 = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime);
|
|
|
|
//MP_VERBOSE(ao, "getTimestamp: fpos= %u / time= %"PRId64" / now= %"PRId64" / stable= %d\n", fpos, time2, now, p->timestamp_stable);
|
|
|
|
if (time1 != time2 && time2 != 0 && fpos != 0) {
|
2018-03-10 03:28:09 +00:00
|
|
|
p->timestamp_stable++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* AudioTrack's framePosition and playbackHeadPosition return a signed integer,
|
|
|
|
* but documentation states it should be interpreted as a 32-bit unsigned integer.
|
|
|
|
*/
|
|
|
|
if (p->timestamp_set) {
|
|
|
|
pos = 0xFFFFFFFFL & MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.framePosition);
|
|
|
|
uint32_t fpos = pos;
|
2023-09-15 21:08:27 +00:00
|
|
|
int64_t time = MP_JNI_GET_LONG(p->timestamp, AudioTimestamp.nanoTime);
|
|
|
|
if (time == 0)
|
2018-03-10 03:28:09 +00:00
|
|
|
fpos = pos = 0;
|
|
|
|
if (p->needs_timestamp_offset) {
|
2023-09-15 21:08:27 +00:00
|
|
|
if (time != 0 && !p->timestamp_offset)
|
|
|
|
p->timestamp_offset = now - time;
|
|
|
|
time += p->timestamp_offset;
|
2018-03-10 03:28:09 +00:00
|
|
|
}
|
2023-09-15 21:08:27 +00:00
|
|
|
if (fpos != 0 && time != 0 && state == AudioTrack.PLAYSTATE_PLAYING) {
|
|
|
|
double diff = (double)(now - time) / 1e9;
|
2018-03-10 03:28:09 +00:00
|
|
|
pos += diff * ao->samplerate;
|
|
|
|
}
|
2023-09-15 21:08:27 +00:00
|
|
|
//MP_VERBOSE(ao, "position = %u via getTimestamp (state = %d / fpos= %u / time= %"PRId64")\n", pos, state, fpos, time);
|
2018-03-10 03:28:09 +00:00
|
|
|
} else {
|
|
|
|
pos = 0xFFFFFFFFL & MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlaybackHeadPosition);
|
|
|
|
//MP_VERBOSE(ao, "playbackHeadPosition = %u (reset_pending=%d)\n", pos, p->reset_pending);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (p->format == AudioFormat.ENCODING_IEC61937) {
|
|
|
|
if (p->reset_pending) {
|
|
|
|
// after a flush(), playbackHeadPosition will not reset to 0 right away.
|
|
|
|
// sometimes, it will never reset at all.
|
|
|
|
// save the initial offset after the reset, to subtract it going forward.
|
|
|
|
if (p->playhead_offset == 0)
|
|
|
|
p->playhead_offset = pos;
|
|
|
|
p->reset_pending = false;
|
|
|
|
MP_VERBOSE(ao, "IEC/playbackHead offset = %d\n", pos);
|
|
|
|
}
|
|
|
|
|
|
|
|
// usually shortly after a flush(), playbackHeadPosition will reset to 0.
|
|
|
|
// clear out the position and offset to avoid regular "rollover" below
|
|
|
|
if (pos == 0 && p->playhead_offset != 0) {
|
|
|
|
MP_VERBOSE(ao, "IEC/playbackHeadPosition %d -> %d (flush)\n", p->playhead_pos, pos);
|
|
|
|
p->playhead_offset = 0;
|
|
|
|
p->playhead_pos = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// sometimes on a new AudioTrack instance, playbackHeadPosition will reset
|
|
|
|
// to 0 shortly after playback starts for no reason.
|
|
|
|
if (pos == 0 && p->playhead_pos != 0) {
|
|
|
|
MP_VERBOSE(ao, "IEC/playbackHeadPosition %d -> %d (reset)\n", p->playhead_pos, pos);
|
|
|
|
p->playhead_offset = 0;
|
|
|
|
p->playhead_pos = 0;
|
|
|
|
p->written_frames = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
p->playhead_pos = pos;
|
|
|
|
return p->playhead_pos - p->playhead_offset;
|
|
|
|
}
|
|
|
|
|
|
|
|
static double AudioTrack_getLatency(struct ao *ao)
|
|
|
|
{
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
if (!p->audiotrack)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
uint32_t playhead = AudioTrack_getPlaybackHeadPosition(ao);
|
|
|
|
uint32_t diff = p->written_frames - playhead;
|
|
|
|
double delay = diff / (double)(ao->samplerate);
|
|
|
|
if (!p->timestamp_set &&
|
|
|
|
p->format != AudioFormat.ENCODING_IEC61937)
|
|
|
|
delay += (double)MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getLatency)/1000.0;
|
2021-10-19 21:26:44 +00:00
|
|
|
if (delay > 2.0) {
|
2018-03-10 03:28:09 +00:00
|
|
|
//MP_WARN(ao, "getLatency: written=%u playhead=%u diff=%u delay=%f\n", p->written_frames, playhead, diff, delay);
|
|
|
|
p->timestamp_fetched = 0;
|
|
|
|
return 0;
|
|
|
|
}
|
2021-10-19 21:26:44 +00:00
|
|
|
return MPCLAMP(delay, 0.0, 2.0);
|
2018-03-10 03:28:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static int AudioTrack_write(struct ao *ao, int len)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
if (!p->audiotrack)
|
|
|
|
return -1;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
void *buf = p->chunk;
|
|
|
|
|
|
|
|
jint ret;
|
|
|
|
if (p->format == AudioFormat.ENCODING_IEC61937) {
|
|
|
|
(*env)->SetShortArrayRegion(env, p->shortarray, 0, len / 2, buf);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeShortV23, p->shortarray, 0, len / 2, AudioTrack.WRITE_BLOCKING);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
if (ret > 0) ret *= 2;
|
|
|
|
|
|
|
|
} else if (AudioTrack.writeBufferV21) {
|
2023-08-06 14:22:39 +00:00
|
|
|
// reset positions for reading
|
2018-03-10 03:28:09 +00:00
|
|
|
jobject bbuf = MP_JNI_CALL_OBJECT(p->bbuf, ByteBuffer.clear);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
(*env)->DeleteLocalRef(env, bbuf);
|
|
|
|
ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeBufferV21, p->bbuf, len, AudioTrack.WRITE_BLOCKING);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
|
2023-08-06 14:22:39 +00:00
|
|
|
} else if (p->format == AudioFormat.ENCODING_PCM_FLOAT) {
|
|
|
|
(*env)->SetFloatArrayRegion(env, p->floatarray, 0, len / sizeof(float), buf);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.writeFloat, p->floatarray, 0, len / sizeof(float), AudioTrack.WRITE_BLOCKING);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
if (ret > 0) ret *= sizeof(float);
|
|
|
|
|
2018-03-10 03:28:09 +00:00
|
|
|
} else {
|
|
|
|
(*env)->SetByteArrayRegion(env, p->bytearray, 0, len, buf);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
ret = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.write, p->bytearray, 0, len);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0) return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void uninit_jni(struct ao *ao)
|
|
|
|
{
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
mp_jni_reset_jfields(env, &AudioTrack, AudioTrack.mapping, 1, ao->log);
|
|
|
|
mp_jni_reset_jfields(env, &AudioTimestamp, AudioTimestamp.mapping, 1, ao->log);
|
|
|
|
mp_jni_reset_jfields(env, &AudioManager, AudioManager.mapping, 1, ao->log);
|
|
|
|
mp_jni_reset_jfields(env, &AudioFormat, AudioFormat.mapping, 1, ao->log);
|
2021-10-19 21:25:18 +00:00
|
|
|
mp_jni_reset_jfields(env, &AudioFormatBuilder, AudioFormatBuilder.mapping, 1, ao->log);
|
|
|
|
mp_jni_reset_jfields(env, &AudioAttributes, AudioAttributes.mapping, 1, ao->log);
|
|
|
|
mp_jni_reset_jfields(env, &AudioAttributesBuilder, AudioAttributesBuilder.mapping, 1, ao->log);
|
2018-03-10 03:28:09 +00:00
|
|
|
mp_jni_reset_jfields(env, &ByteBuffer, ByteBuffer.mapping, 1, ao->log);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int init_jni(struct ao *ao)
|
|
|
|
{
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
if (mp_jni_init_jfields(env, &AudioTrack, AudioTrack.mapping, 1, ao->log) < 0 ||
|
|
|
|
mp_jni_init_jfields(env, &ByteBuffer, ByteBuffer.mapping, 1, ao->log) < 0 ||
|
|
|
|
mp_jni_init_jfields(env, &AudioTimestamp, AudioTimestamp.mapping, 1, ao->log) < 0 ||
|
|
|
|
mp_jni_init_jfields(env, &AudioManager, AudioManager.mapping, 1, ao->log) < 0 ||
|
2021-10-19 21:25:18 +00:00
|
|
|
mp_jni_init_jfields(env, &AudioAttributes, AudioAttributes.mapping, 1, ao->log) < 0 ||
|
|
|
|
mp_jni_init_jfields(env, &AudioAttributesBuilder, AudioAttributesBuilder.mapping, 1, ao->log) < 0 ||
|
|
|
|
mp_jni_init_jfields(env, &AudioFormatBuilder, AudioFormatBuilder.mapping, 1, ao->log) < 0 ||
|
2018-03-10 03:28:09 +00:00
|
|
|
mp_jni_init_jfields(env, &AudioFormat, AudioFormat.mapping, 1, ao->log) < 0) {
|
|
|
|
uninit_jni(ao);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void *playthread(void *arg)
|
|
|
|
{
|
|
|
|
struct ao *ao = arg;
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
mpthread_set_name("audiotrack");
|
|
|
|
pthread_mutex_lock(&p->lock);
|
|
|
|
while (!p->thread_terminate) {
|
|
|
|
int state = AudioTrack.PLAYSTATE_PAUSED;
|
|
|
|
if (p->audiotrack) {
|
|
|
|
state = MP_JNI_CALL_INT(p->audiotrack, AudioTrack.getPlayState);
|
|
|
|
}
|
|
|
|
if (state == AudioTrack.PLAYSTATE_PLAYING) {
|
|
|
|
int read_samples = p->chunksize / ao->sstride;
|
|
|
|
int64_t ts = mp_time_us();
|
|
|
|
ts += (read_samples / (double)(ao->samplerate)) * 1e6;
|
|
|
|
ts += AudioTrack_getLatency(ao) * 1e6;
|
|
|
|
int samples = ao_read_data(ao, &p->chunk, read_samples, ts);
|
2023-08-06 14:57:26 +00:00
|
|
|
int ret = AudioTrack_write(ao, samples * ao->sstride);
|
2018-03-10 03:28:09 +00:00
|
|
|
if (ret >= 0) {
|
|
|
|
p->written_frames += ret / ao->sstride;
|
|
|
|
} else if (ret == AudioManager.ERROR_DEAD_OBJECT) {
|
|
|
|
MP_WARN(ao, "AudioTrack.write failed with ERROR_DEAD_OBJECT. Recreating AudioTrack...\n");
|
|
|
|
if (AudioTrack_Recreate(ao) < 0) {
|
|
|
|
MP_ERR(ao, "AudioTrack_Recreate failed\n");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
MP_ERR(ao, "AudioTrack.write failed with %d\n", ret);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
struct timespec wait = mp_rel_time_to_timespec(0.300);
|
|
|
|
pthread_cond_timedwait(&p->wakeup, &p->lock, &wait);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pthread_mutex_unlock(&p->lock);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void uninit(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
if (p->audiotrack) {
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.stop);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.flush);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
}
|
|
|
|
|
|
|
|
pthread_mutex_lock(&p->lock);
|
|
|
|
p->thread_terminate = true;
|
|
|
|
pthread_cond_signal(&p->wakeup);
|
|
|
|
pthread_mutex_unlock(&p->lock);
|
|
|
|
|
|
|
|
if (p->thread_created)
|
|
|
|
pthread_join(p->thread, NULL);
|
|
|
|
|
|
|
|
if (p->audiotrack) {
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.release);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
(*env)->DeleteGlobalRef(env, p->audiotrack);
|
|
|
|
p->audiotrack = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p->bytearray) {
|
|
|
|
(*env)->DeleteGlobalRef(env, p->bytearray);
|
|
|
|
p->bytearray = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p->shortarray) {
|
|
|
|
(*env)->DeleteGlobalRef(env, p->shortarray);
|
|
|
|
p->shortarray = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p->floatarray) {
|
|
|
|
(*env)->DeleteGlobalRef(env, p->floatarray);
|
|
|
|
p->floatarray = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p->bbuf) {
|
|
|
|
(*env)->DeleteGlobalRef(env, p->bbuf);
|
|
|
|
p->bbuf = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p->timestamp) {
|
|
|
|
(*env)->DeleteGlobalRef(env, p->timestamp);
|
|
|
|
p->timestamp = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
pthread_cond_destroy(&p->wakeup);
|
|
|
|
pthread_mutex_destroy(&p->lock);
|
|
|
|
|
|
|
|
uninit_jni(ao);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int init(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
if (!env)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
pthread_mutex_init(&p->lock, NULL);
|
|
|
|
pthread_cond_init(&p->wakeup, NULL);
|
|
|
|
|
|
|
|
if (init_jni(ao) < 0)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
if (af_fmt_is_spdif(ao->format)) {
|
|
|
|
p->format = AudioFormat.ENCODING_IEC61937;
|
|
|
|
} else if (ao->format == AF_FORMAT_U8) {
|
|
|
|
p->format = AudioFormat.ENCODING_PCM_8BIT;
|
2023-08-06 14:22:39 +00:00
|
|
|
} else if (p->cfg_pcm_float && af_fmt_is_float(ao->format)) {
|
2018-03-10 03:28:09 +00:00
|
|
|
ao->format = AF_FORMAT_FLOAT;
|
|
|
|
p->format = AudioFormat.ENCODING_PCM_FLOAT;
|
|
|
|
} else {
|
|
|
|
ao->format = AF_FORMAT_S16;
|
|
|
|
p->format = AudioFormat.ENCODING_PCM_16BIT;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (AudioTrack.getNativeOutputSampleRate) {
|
|
|
|
jint samplerate = MP_JNI_CALL_STATIC_INT(
|
|
|
|
AudioTrack.clazz,
|
|
|
|
AudioTrack.getNativeOutputSampleRate,
|
|
|
|
AudioManager.STREAM_MUSIC
|
|
|
|
);
|
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) == 0) {
|
|
|
|
MP_VERBOSE(ao, "AudioTrack.nativeOutputSampleRate = %d\n", samplerate);
|
2023-08-06 14:09:13 +00:00
|
|
|
ao->samplerate = MPMIN(samplerate, ao->samplerate);
|
2018-03-10 03:28:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
p->samplerate = ao->samplerate;
|
|
|
|
|
2023-08-06 16:00:19 +00:00
|
|
|
/* https://developer.android.com/reference/android/media/AudioFormat#channelPositionMask */
|
|
|
|
static const struct mp_chmap layouts[] = {
|
|
|
|
{0}, // empty
|
|
|
|
MP_CHMAP_INIT_MONO, // mono
|
|
|
|
MP_CHMAP_INIT_STEREO, // stereo
|
|
|
|
MP_CHMAP3(FL, FR, FC), // 3.0
|
|
|
|
MP_CHMAP4(FL, FR, BL, BR), // quad
|
|
|
|
MP_CHMAP5(FL, FR, FC, BL, BR), // 5.0
|
|
|
|
MP_CHMAP6(FL, FR, FC, LFE, BL, BR), // 5.1
|
|
|
|
MP_CHMAP7(FL, FR, FC, LFE, BL, BR, BC), // 6.1
|
|
|
|
MP_CHMAP8(FL, FR, FC, LFE, BL, BR, SL, SR), // 7.1
|
|
|
|
};
|
|
|
|
const jint layout_map[] = {
|
|
|
|
0,
|
|
|
|
AudioFormat.CHANNEL_OUT_MONO,
|
|
|
|
AudioFormat.CHANNEL_OUT_STEREO,
|
|
|
|
AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER,
|
|
|
|
AudioFormat.CHANNEL_OUT_QUAD,
|
|
|
|
AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER,
|
|
|
|
AudioFormat.CHANNEL_OUT_5POINT1,
|
|
|
|
AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER,
|
|
|
|
AudioFormat.CHANNEL_OUT_7POINT1_SURROUND,
|
|
|
|
};
|
|
|
|
static_assert(MP_ARRAY_SIZE(layout_map) == MP_ARRAY_SIZE(layouts), "");
|
2018-03-10 03:28:09 +00:00
|
|
|
if (p->format == AudioFormat.ENCODING_IEC61937) {
|
|
|
|
p->channel_config = AudioFormat.CHANNEL_OUT_STEREO;
|
|
|
|
} else {
|
2023-08-06 16:00:19 +00:00
|
|
|
struct mp_chmap_sel sel = {0};
|
|
|
|
for (int i = 0; i < MP_ARRAY_SIZE(layouts); i++) {
|
|
|
|
if (layout_map[i])
|
|
|
|
mp_chmap_sel_add_map(&sel, &layouts[i]);
|
|
|
|
}
|
|
|
|
if (!ao_chmap_sel_adjust(ao, &sel, &ao->channels))
|
|
|
|
goto error;
|
|
|
|
p->channel_config = layout_map[ao->channels.num];
|
|
|
|
assert(p->channel_config);
|
2018-03-10 03:28:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
jint buffer_size = MP_JNI_CALL_STATIC_INT(
|
|
|
|
AudioTrack.clazz,
|
|
|
|
AudioTrack.getMinBufferSize,
|
|
|
|
p->samplerate,
|
|
|
|
p->channel_config,
|
|
|
|
p->format
|
|
|
|
);
|
2023-08-06 13:57:32 +00:00
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0 || buffer_size <= 0) {
|
2018-03-10 03:28:09 +00:00
|
|
|
MP_FATAL(ao, "AudioTrack.getMinBufferSize returned an invalid size: %d", buffer_size);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2023-08-06 14:18:10 +00:00
|
|
|
// Choose double of the minimum buffer size suggested by the driver, but not
|
|
|
|
// less than 75ms or more than 150ms.
|
|
|
|
const int bps = af_fmt_to_bytes(ao->format);
|
|
|
|
int min = 0.075 * p->samplerate * bps * ao->channels.num;
|
2021-10-19 21:39:48 +00:00
|
|
|
int max = min * 2;
|
2023-08-06 14:18:10 +00:00
|
|
|
min = MP_ALIGN_UP(min, bps);
|
|
|
|
max = MP_ALIGN_UP(max, bps);
|
2018-03-10 03:28:09 +00:00
|
|
|
p->size = MPCLAMP(buffer_size * 2, min, max);
|
|
|
|
MP_VERBOSE(ao, "Setting bufferSize = %d (driver=%d, min=%d, max=%d)\n", p->size, buffer_size, min, max);
|
2023-08-06 14:18:10 +00:00
|
|
|
assert(p->size % bps == 0);
|
|
|
|
ao->device_buffer = p->size / bps;
|
2018-03-10 03:28:09 +00:00
|
|
|
|
2021-10-19 21:27:37 +00:00
|
|
|
p->chunksize = p->size;
|
2023-01-10 14:18:12 +00:00
|
|
|
p->chunk = talloc_size(ao, p->size);
|
2021-10-19 21:27:37 +00:00
|
|
|
|
2018-03-10 03:28:09 +00:00
|
|
|
jobject timestamp = MP_JNI_NEW(AudioTimestamp.clazz, AudioTimestamp.ctor);
|
2023-08-06 13:57:32 +00:00
|
|
|
if (MP_JNI_EXCEPTION_LOG(ao) < 0 || !timestamp) {
|
2018-03-10 03:28:09 +00:00
|
|
|
MP_FATAL(ao, "AudioTimestamp could not be created\n");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
p->timestamp = (*env)->NewGlobalRef(env, timestamp);
|
|
|
|
(*env)->DeleteLocalRef(env, timestamp);
|
|
|
|
|
2023-08-06 14:22:39 +00:00
|
|
|
// decide and create buffer of right type
|
2018-03-10 03:28:09 +00:00
|
|
|
if (p->format == AudioFormat.ENCODING_IEC61937) {
|
|
|
|
jshortArray shortarray = (*env)->NewShortArray(env, p->chunksize / 2);
|
|
|
|
p->shortarray = (*env)->NewGlobalRef(env, shortarray);
|
|
|
|
(*env)->DeleteLocalRef(env, shortarray);
|
|
|
|
} else if (AudioTrack.writeBufferV21) {
|
2023-08-06 14:22:39 +00:00
|
|
|
MP_VERBOSE(ao, "Using NIO ByteBuffer\n");
|
2018-03-10 03:28:09 +00:00
|
|
|
jobject bbuf = (*env)->NewDirectByteBuffer(env, p->chunk, p->chunksize);
|
|
|
|
p->bbuf = (*env)->NewGlobalRef(env, bbuf);
|
|
|
|
(*env)->DeleteLocalRef(env, bbuf);
|
2023-08-06 14:22:39 +00:00
|
|
|
} else if (p->format == AudioFormat.ENCODING_PCM_FLOAT) {
|
|
|
|
jfloatArray floatarray = (*env)->NewFloatArray(env, p->chunksize / sizeof(float));
|
|
|
|
p->floatarray = (*env)->NewGlobalRef(env, floatarray);
|
|
|
|
(*env)->DeleteLocalRef(env, floatarray);
|
2018-03-10 03:28:09 +00:00
|
|
|
} else {
|
|
|
|
jbyteArray bytearray = (*env)->NewByteArray(env, p->chunksize);
|
|
|
|
p->bytearray = (*env)->NewGlobalRef(env, bytearray);
|
|
|
|
(*env)->DeleteLocalRef(env, bytearray);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* create AudioTrack object */
|
|
|
|
if (AudioTrack_New(ao) != 0) {
|
|
|
|
MP_FATAL(ao, "Failed to create AudioTrack\n");
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pthread_create(&p->thread, NULL, playthread, ao)) {
|
|
|
|
MP_ERR(ao, "pthread creation failed\n");
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
p->thread_created = true;
|
|
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
error:
|
|
|
|
uninit(ao);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void stop(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
if (!p->audiotrack) {
|
|
|
|
MP_ERR(ao, "AudioTrack does not exist to stop!\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.pause);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.flush);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
|
|
|
|
p->playhead_offset = 0;
|
|
|
|
p->reset_pending = true;
|
|
|
|
p->written_frames = 0;
|
|
|
|
p->timestamp_fetched = 0;
|
|
|
|
p->timestamp_set = false;
|
|
|
|
p->timestamp_offset = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void start(struct ao *ao)
|
|
|
|
{
|
|
|
|
struct priv *p = ao->priv;
|
|
|
|
if (!p->audiotrack) {
|
|
|
|
MP_ERR(ao, "AudioTrack does not exist to start!\n");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
JNIEnv *env = MP_JNI_GET_ENV(ao);
|
|
|
|
MP_JNI_CALL_VOID(p->audiotrack, AudioTrack.play);
|
|
|
|
MP_JNI_EXCEPTION_LOG(ao);
|
|
|
|
|
|
|
|
pthread_cond_signal(&p->wakeup);
|
|
|
|
}
|
|
|
|
|
|
|
|
#define OPT_BASE_STRUCT struct priv
|
|
|
|
|
|
|
|
const struct ao_driver audio_out_audiotrack = {
|
|
|
|
.description = "Android AudioTrack audio output",
|
|
|
|
.name = "audiotrack",
|
|
|
|
.init = init,
|
|
|
|
.uninit = uninit,
|
|
|
|
.reset = stop,
|
audio: redo internal AO API
This affects "pull" AOs only: ao_alsa, ao_pulse, ao_openal, ao_pcm,
ao_lavc. There are changes to the other AOs too, but that's only about
renaming ao_driver.resume to ao_driver.start.
ao_openal is broken because I didn't manage to fix it, so it exits with
an error message. If you want it, why don't _you_ put effort into it? I
see no reason to waste my own precious lifetime over this (I realize the
irony).
ao_alsa loses the poll() mechanism, but it was mostly broken and didn't
really do what it was supposed to. There doesn't seem to be anything in
the ALSA API to watch the playback status without polling (unless you
want to use raw UNIX signals).
No idea if ao_pulse is correct, or whether it's subtly broken now. There
is no documentation, so I can't tell what is correct, without reverse
engineering the whole project. I recommend using ALSA.
This was supposed to be just a simple fix, but somehow it expanded scope
like a train wreck. Very high chance of regressions, but probably only
for the AOs listed above. The rest you can figure out from reading the
diff.
2020-05-31 13:00:35 +00:00
|
|
|
.start = start,
|
2018-03-10 03:28:09 +00:00
|
|
|
.priv_size = sizeof(struct priv),
|
2023-08-06 16:47:12 +00:00
|
|
|
.priv_defaults = &(const OPT_BASE_STRUCT) {
|
|
|
|
.cfg_pcm_float = 1,
|
|
|
|
},
|
2018-03-10 03:28:09 +00:00
|
|
|
.options = (const struct m_option[]) {
|
2023-02-20 03:32:50 +00:00
|
|
|
{"pcm-float", OPT_BOOL(cfg_pcm_float)},
|
options: change option macros and all option declarations
Change all OPT_* macros such that they don't define the entire m_option
initializer, and instead expand only to a part of it, which sets certain
fields. This requires changing almost every option declaration, because
they all use these macros. A declaration now always starts with
{"name", ...
followed by designated initializers only (possibly wrapped in macros).
The OPT_* macros now initialize the .offset and .type fields only,
sometimes also .priv and others.
I think this change makes the option macros less tricky. The old code
had to stuff everything into macro arguments (and attempted to allow
setting arbitrary fields by letting the user pass designated
initializers in the vararg parts). Some of this was made messy due to
C99 and C11 not allowing 0-sized varargs with ',' removal. It's also
possible that this change is pointless, other than cosmetic preferences.
Not too happy about some things. For example, the OPT_CHOICE()
indentation I applied looks a bit ugly.
Much of this change was done with regex search&replace, but some places
required manual editing. In particular, code in "obscure" areas (which I
didn't include in compilation) might be broken now.
In wayland_common.c the author of some option declarations confused the
flags parameter with the default value (though the default value was
also properly set below). I fixed this with this change.
2020-03-14 20:28:01 +00:00
|
|
|
{"session-id", OPT_INT(cfg_session_id)},
|
2018-03-10 03:28:09 +00:00
|
|
|
{0}
|
|
|
|
},
|
|
|
|
.options_prefix = "audiotrack",
|
|
|
|
};
|