最近音视频开发学习到了第四章,这一章讲的是移动平台的音视频渲染,对于AudioTrack以及OpenSLES相当于又复习了一遍。这一章结合之前的ffmpeg解码来进行mp3文件的播放。主要的难点有linux多线程的同步,生产者消费者模型等等。如果对ffmpeg解码不熟悉的可以看https://blog.csdn.net/a568478312/article/details/80268498,这篇文章的重点在于api的使用以及多线程的控制。
AudioTrack的API其实用起来比较方便,一般需要结合解码器来使用。首先我们需要知道pcm数据的采样率,比特率,声道数等信息,用来之后初始化AudioTrack。和之前的解码一样,这里同样使用ffmpeg。
private boolean initMetaData(String path) {
int[] metaArray = new int[]{0, 0, 0};
decoder.getMusicMetaByPath(path, metaArray);
this.sampleRateInHz = metaArray[0];
this.bitRate = metaArray[1];
this.channel=metaArray[2];
if (sampleRateInHz <= 0 || bitRate <= 0) {
return false;
}
totalCapacity = (new File(path)).length();
mp3CapacityPerSec = bitRate / BITS_PER_BYTE;
if (mp3CapacityPerSec == 0) {
return false;
}
duration = (int) (totalCapacity / mp3CapacityPerSec);
Log.d("tedu", " bitrate duration: " + duration);
int byteCountPerSec = sampleRateInHz * CHANNEL_PER_FRAME * BITS_PER_CHANNEL / BITS_PER_BYTE;
//比特率不变 采样率是被压缩的
Log.d("tedu", "initMetaData: bitRate " + bitRate + "mp3CapacityPerSec " + mp3CapacityPerSec +
"byteCountPerSec " + byteCountPerSec);
DECODE_BUFFER_SIZE = (int) ((byteCountPerSec / 2) * 0.2);
initPlayState();
seekBaseMillsTime = 0;
audioTrackBaseHeadPosition = 0;
return true;
}
接着初始化解码的核心功能,并且计算出来之后每一个解码buffer的大小,接着初始化队列以及开启线程。PacketPool维护一个链表实现的队列,用来保存解码的裸数据。
void AccompanyDecoderController::init(const char* accompanyPath,
float packetBufferTimePercent) {
//初始化两个全局变量
volume = 1.0f;
accompanyMax = 1.0f;
//计算计算出伴奏和原唱的bufferSize
int accompanyByteCountPerSec = accompanySampleRate * CHANNEL_PER_FRAME
* BITS_PER_CHANNEL / BITS_PER_BYTE;
accompanyPacketBufferSize = (int) ((accompanyByteCountPerSec / 2)
* packetBufferTimePercent);
//初始化两个decoder
initAccompanyDecoder(accompanyPath);
//初始化队列以及开启线程
packetPool = PacketPool::GetInstance();
packetPool->initDecoderAccompanyPacketQueue();
initDecoderThread();
}
下面是开启解码线程。
void AccompanyDecoderController::initDecoderThread() {
isRunning = true;
pthread_mutex_init(&mLock, NULL);
pthread_cond_init(&mCondition, NULL);
pthread_create(&songDecoderThread, NULL, startDecoderThread, this);
}
解码线程中不断去文件中解码数据,并且将解码数据放入队列中,如果队列的数量超过阈值,释放锁并进行等待。
void* AccompanyDecoderController::startDecoderThread(void* ptr) {
LOGI("enter AccompanyDecoderController::startDecoderThread");
AccompanyDecoderController* decoderController =
(AccompanyDecoderController *) ptr;
int getLockCode = pthread_mutex_lock(&decoderController->mLock);
while (decoderController->isRunning) {
decoderController->decodeSongPacket();
if (decoderController->packetPool->geDecoderAccompanyPacketQueueSize() > QUEUE_SIZE_MAX_THRESHOLD) {
pthread_cond_wait(&decoderController->mCondition,
&decoderController->mLock);
}
}
pthread_mutex_unlock(&decoderController->mLock);
}
接着我们初始化AudioTrack,注意这里的最好是使用它提供的方法来进行计算。然后使用我们获取的采样率,声道数之类的将AudioTrack初始化。这里需要注意AudioTrack的两种播放模式,一种是流的模式一种静态模式。
private void initAudioTrack() {
int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
audioTrack = new AudioTrack(this.audioStreamType, sampleRateInHz, channelConfig, audioFormat, bufferSize,
AudioTrack.MODE_STREAM);
}
接下来我开启java线程来不断从native的队列里读取数据。
class PlayerThread implements Runnable {
private short[] samples;
@Override
public void run() {
int sample_count = 0;
boolean isPlayTemp = isPlaying = false;
try {
//?0.2S的数据
samples = new short[DECODE_BUFFER_SIZE];
short[] fakeSamples = new short[DECODE_BUFFER_SIZE];
int[] extraSlientSampleSize = new int[1];
// int sampleDurationMs = (int)((float)DECODE_BUFFER_SIZE /
// sampleRateInHz / CHANNEL_PER_FRAME * 1000);
while (!isStop) {
// long start = System.currentTimeMillis();
extraSlientSampleSize[0] = 0;
sample_count = decoder.readSamples(samples, extraSlientSampleSize);
// Log.i(TAG, "end Native Mp3 playing......
// DECODE_BUFFER_SIZE="+DECODE_BUFFER_SIZE+"
// sample_count="+sample_count);
if (sample_count == -2) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Log.i(TAG, "WARN : no play data");
continue;
}
if (sample_count < 0) {
break;
}
while (true) {
synchronized (NativeMp3Player.class) {
isPlayTemp = isPlaying;
}
if (isPlayTemp)
break;
else
Thread.yield();
}
if (null != audioTrack && audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.setStereoVolume(1, 1);
if (extraSlientSampleSize[0] > 0) {
int totalSize = extraSlientSampleSize[0] + sample_count;
short[] playSamples = new short[totalSize];
System.arraycopy(samples, 0, playSamples, extraSlientSampleSize[0], sample_count);
audioTrack.write(playSamples, 0, totalSize);
} else {
audioTrack.write(samples, 0, sample_count);
}
}
}
decoder.destory();
} catch (Error e) {
e.printStackTrace();
}
samples = null;
// Log.i(TAG, "end Native Mp3 player thread...");
}
}
读取队列里的解码数据,并且在数据数量小于阈值时,通知解码线程进行解码。
int AccompanyDecoderController::readSamples(short* samples, int size,
int* slientSizeArr) {
int result = -1;
AudioPacket* accompanyPacket = NULL;
packetPool->getDecoderAccompanyPacket(&accompanyPacket, true);
if (NULL != accompanyPacket) {
int samplePacketSize = accompanyPacket->size;
if (samplePacketSize != -1 && samplePacketSize <= size) {
//copy the raw data to samples
memcpy(samples, accompanyPacket->buffer, samplePacketSize * 2);
adjustSamplesVolume(samples, samplePacketSize,
volume / accompanyMax);
delete accompanyPacket;
result = samplePacketSize;
}
} else {
result = -2;
}
if (packetPool->geDecoderAccompanyPacketQueueSize() < QUEUE_SIZE_MIN_THRESHOLD) {
int getLockCode = pthread_mutex_lock(&mLock);
if (result != -1) {
pthread_cond_signal(&mCondition);
}
pthread_mutex_unlock (&mLock);
}
return result;
}
最后再简单梳理一下流程:
源码整个看下来只要熟悉使用linux下的多线程的pthread_mutex_t mLock,pthread_cond_t mCondition来进行线程间的同步,没有太大难点。
OpenSL ES是可以让你使用C或者C++来实现高性能,低延迟音频API。ES代表Embedded Systems,在嵌入式系统中单独设计。使用OpenSL ES可以省去native层到java层的数据拷贝的开销,更加高效,然而API设计的并不是特别简单,总感觉略坑。不过使用下来大部分还是模板代码,稍微熟悉一下还是没什么大问题的。要使用OpenSLES我们需要先在CMakeLists.txt中引入库。
add_library( libmedia
STATIC
opensl_es_context.cpp
sound_service.cpp
)
target_link_libraries(
libmedia
#加入OpenSLES
OpenSLES
)
接着引入头文件
#include
#include
然后我们就可以初始化OpenSL ES的环境了。首先了解一下,这套API是使用C语言来实现面向对象的设计,通用的使用流程是。
了解了之后我们来正式看代码。
SLresult createEngine() {
// OpenSL ES for Android is designed to be thread-safe,
// so this option request will be ignored, but it will
// make the source code portable to other platforms.
SLEngineOption engineOption[] = {{(SLuint32) SL_ENGINEOPTION_THREADSAFE, (SLuint32) SL_BOOLEAN_TRUE}};
// Create the OpenSL ES engine object
return slCreateEngine(&engineObject, ARRAY_LEN(engineOption), engineOption, 0,
0, 0
);
}
接着我们Realize初始化的引擎对象
void OpenSLESContext::init() {
LOGI("createEngine");
SLresult result = createEngine();
LOGI("createEngine result is s%", ResultToString(result));
if (SL_RESULT_SUCCESS == result) {
LOGI("Realize the engine object");
// Realize the engine object
result = RealizeObject(engineObject);
if (SL_RESULT_SUCCESS == result) {
LOGI("Get the engine interface");
// Get the engine interface
result = GetEngineInterface();
}
}
}
简单说明下,创建的OpenSL的对象都至少实现了一个接口,但是其他的接口是可选的或者不支持的。这时候需要自己去指定接口(mids),接口数量,以及是否绝对需要(mreq)。如果指定了绝对需要的,然而不支持,就会报错,返回SL_RESULT_FEATURE_UNSUPPORTED。
SLresult CreateOutputMix() {
const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean mreq[1] = {SL_BOOLEAN_TRUE};
return (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
}
LOGI("Realize output mix object");
result = RealizeObject(outputMixObject);
if (SL_RESULT_SUCCESS != result) {
return result;
}
SLresult CreateBufferQueueAudioPlayer() {
SLDataLocator_AndroidBufferQueue dataSourceLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
1
};
//PCM data source format
// SLDataFormat_PCM dataSourceFormat = { SL_DATAFORMAT_PCM, // format type
// 2, // channel count
// accompanySampleRate, // samples per second in millihertz
// 16, // bits per sample
// 16, // container size
// SL_SPEAKER_FRONT_CENTER, // channel mask
// SL_BYTEORDER_LITTLEENDIAN // endianness
// };
int samplesPerSec = opensl_get_sample_rate(accompanySampleRate);
//使用pcm数据格式,还可以使用URI等
SLDataFormat_PCM dataSourceFormat={
SL_DATAFORMAT_PCM,
static_cast(channels),
static_cast(samplesPerSec),
SL_PCMSAMPLEFORMAT_FIXED_16, // bits per sample
SL_PCMSAMPLEFORMAT_FIXED_16, // container size
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, // channel mask
SL_BYTEORDER_LITTLEENDIAN
};
// Data source is a simple buffer queue with PCM format
SLDataSource dataSource={&dataSourceLocator,&dataSourceFormat};
// Output mix locator for data sink
SLDataLocator_OutputMix dataSinkLocator={SL_DATALOCATOR_OUTPUTMIX,
outputMixObject
};
// Data sink is an output mix
SLDataSink dataSink = { &dataSinkLocator, // locator
0 // format
};
// Interfaces that are requested
SLInterfaceID interfaceIds[]={SL_IID_BUFFERQUEUE};
// Required interfaces. If the required interfaces
// are not available the request will fail
SLboolean requiredInterfaces[] = { SL_BOOLEAN_TRUE // for SL_IID_BUFFERQUEUE
};
return (*engineEngine)->CreateAudioPlayer(engineEngine,&audioPlayerObject,&dataSource,&dataSink,
ARRAY_LEN(interfaceIds), interfaceIds, requiredInterfaces
);
}
接着同样实现
LOGI("Realize audio player object");
// Realize audio player object
result = RealizeObject(audioPlayerObject);
if (SL_RESULT_SUCCESS != result) {
return result;
}
SLresult GetAudioPlayerBufferQueueInterface() {
// Get the buffer queue interface
return (*audioPlayerObject)->GetInterface(audioPlayerObject, SL_IID_BUFFERQUEUE, &audioPlayerBufferQueue);
};
SLresult SoundService::RegisterPlayerCallback() {
return (*audioPlayerBufferQueue)->RegisterCallback(audioPlayerBufferQueue, PlayerCallback,
this);
}
在回调函数中入队数据,这里解码和之前的AudioTrack是一样的,我们只需要在回调函数拿队列的数据就行了。
static void PlayerCallback(SLAndroidSimpleBufferQueueItf bq,void *context){
SoundService* service= static_cast(context);
service->producePacket();
}
void SoundService::producePacket() {
LOGI("SoundService::producePacket() audio player call back method... ");
// Read data
byte *audioBuffer = buffer + (currentFrame % bufferNums) * packetBufferSize;
int result = -1;
if (NULL != decoderController) {
result = decoderController->readSamples(target, packetBufferSize / 2, NULL);
LOGI("enter SoundService::producePacket() PLAYING_STATE_PLAYING packetBufferSize=%d, result=%d",
packetBufferSize, result);
}
if (0 < result) {
//转成char数组后入队数据
convertByteArrayFromShortArray(target, result, audioBuffer);
(*audioPlayerBufferQueue)->Enqueue(audioPlayerBufferQueue, audioBuffer, result * 2);
} else {
LOGI("before DestroyContext");
// DestroyContext();
LOGI("after DestroyContext");
JNIEnv *env;
//播放完毕后会叫java函数
//获取对应线程的env
if (g_jvm->AttachCurrentThread(&env, NULL) != JNI_OK) {
LOGE("%s: AttachCurrentThread() failed", __FUNCTION__);
}
jclass jcls = env->GetObjectClass(obj);
jmethodID onCompletionCallBack = env->GetMethodID(jcls, "onCompletion", "()V");
LOGI("before env->CallVoidMethod");
env->CallVoidMethod(obj, onCompletionCallBack);
LOGI("after env->CallVoidMethod");
if (g_jvm->DetachCurrentThread() != JNI_OK) {
LOGE("%s: DetachCurrentThread() failed", __FUNCTION__);
}
}
currentFrame = (currentFrame + 1) % bufferNums;
}
Lresult SoundService::play() {
//一开始需要主动调用回调函数,或者直接首先填充数据
LOGI("enter SoundService::play()...");
// Set the audio player state playing
LOGI("Set the audio player state playing");
if (playingState == PLAYING_STATE_PLAYING)
return 0;
SLresult result = SetAudioPlayerStatePlaying();
if (SL_RESULT_SUCCESS != result) {
return result;
}
LOGI("Set the slient audio player state playing");
// result = SetSlientAudioPlayerStatePlaying();
// if (SL_RESULT_SUCCESS != result) {
// return result;
// }
LOGI(" Enqueue the first buffer to start");
// for (int i = 0; i < bufferNums; i++) {
// int result = -1;
// if (NULL != decoderController) {
// result = decoderController->readSamples(target, packetBufferSize / 2, NULL);
// }
// LOGI("read_count %d", result);
// // If data is read
// if (0 < result) {
// convertByteArrayFromShortArray(target, result, buffer + i * packetBufferSize);
// (*audioPlayerBufferQueue)->Enqueue(audioPlayerBufferQueue,
// buffer + i * packetBufferSize, result * 2);
// }
// }
playingState = PLAYING_STATE_PLAYING;
PlayerCallback(nullptr, this);
LOGI("out SoundService::play()...");
return SL_RESULT_SUCCESS;
}
Android Studio 3.1.2写c++的方法没有返回值编译器不提示报错,然而运行的时候却特么会崩溃,真的是醉了,native使用ndk-stack快速调试,可以看这篇文章:https://blog.csdn.net/a568478312/article/details/78182422
最后附上说明OpenSL ES文档https://www.khronos.org/registry/OpenSL-ES/specs/OpenSL_ES_Specification_1.0.1.pdf
源码:https://download.csdn.net/download/a568478312/10421205