AudioTrack详解

写在前面:AudioTrack是管理和播放单一音频资源的类。AudioTrack仅仅能播放已经解码的PCM流,用于PCM音频流的回放。

AudioTrack架构图.png
AudioTrack播放PCM音频
  1. 配置基本参数
    • StreamType音频流类型:系统声音的音频流、音乐播放的音频流、用于通话的音频流、用于通知的音频流等。该参数的含义就是告诉系统,我现在想使用的是哪种类型的声音,这样系统就可以对应管理他们了。
      Android 为什么要定义这么多的流类型?这与 Android 的音频管理策略有关,例如:
      • 音频流的音量管理,调节一个类型的音频流音量,不会影响到其他类型的音频流
      • 根据流类型选择合适的输出设备;比如插着有线耳机期间,音乐声(STREAM_MUSIC)只会输出到有线耳机,而铃声(STREAM_RING)会同时输出到有线耳机和外放。
        一个典型的场景:比如你在听music的时候接到电话,这个时候music播放肯定会停止,此时你只能听到电话,如果你调节音量的话,这个调节肯定只对电话起作用。当电话打完了,再回到music,你肯定不用再调节音量了。
        应用开发者应该根据应用场景选择相应的流类型,以便系统为这道流选择合适的输出设备。
    • Mode模式
      • AudioTrack.MODE_STREAM: STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到AudioTrack中
      • AudioTrack.MODE_STATIC:STATIC就是数据一次性交付给接收方。
    • 采样率mSampleRateInHz:
    • 音频量化位数mAudioFormat(只支持8bit和16bit两种。)
    • 通道数目mChannelConfig:最多只支持双音道:
在audioParamCheck()中
static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) {
      int channelCount = 0;
      switch(channelConfig) {
      case AudioFormat.CHANNEL_OUT_MONO:
      case AudioFormat.CHANNEL_CONFIGURATION_MONO:
          channelCount = 1;
          break;
      case AudioFormat.CHANNEL_OUT_STEREO:
      case AudioFormat.CHANNEL_CONFIGURATION_STEREO:
          channelCount = 2;
          break;
      default:
          if (!isMultichannelConfigSupported(channelConfig)) {
              loge("getMinBufferSize(): Invalid channel configuration.");
              return ERROR_BAD_VALUE;
          } else {
              channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig);
          }
      }
  }
  • buffer获取最小缓冲区大小

字面意思是返回最小数据缓冲区的大小,它是声音能正常播放的最低保障,从函数参数来看,返回值取决于采样率、采样深度、声道数这三个属性。MODE_STREAM 模式下,应用程序重点参考其返回值然后确定分配多大的数据缓冲区。如果数据缓冲区分配得过小,那么播放声音会频繁遭遇 underrun,underrun 是指生产者(AudioTrack)提供数据的速度跟不上消费者(AudioFlinger::PlaybackThread)消耗数据的速度,反映到现实的后果就是声音断续卡顿,严重影响听觉体验。

static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) {
    
    ....

    int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat);
    if (size <= 0) {
        loge("getMinBufferSize(): error querying hardware");
        return ERROR;
    }
    else {
        return size;
    }}

可以看到,mMinBufferSize取决于采样率、声道数和采样深度三个属性,其中源码缓冲区的大小的实现在native层中:

//frameworks/base/core/jni/android_media_AudioTrack.cpp

static jint android_media_AudioTrack_get_min_buff_size(JNIEnv*env,  jobject thiz,

jint sampleRateInHertz,jint nbChannels, jint audioFormat) {

int frameCount = 0;

if(AudioTrack::getMinFrameCount(&frameCount, AUDIO_STREAM_DEFAULT,sampleRateInHertz) != NO_ERROR) {

    return -1;

 }

 return  frameCount * nbChannels * (audioFormat ==javaAudioTrackFields.PCM16 ? 2 : 1);

}

使用示例

int sampleRateInHz=32000;
    int nb_channels=2;
        //固定格式的音频码流
        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        //声道布局
        int channelConfig;
        if(nb_channels == 1){
            channelConfig = android.media.AudioFormat.CHANNEL_OUT_MONO;
        }else if(nb_channels == 2){
            channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO;
        }else{
            channelConfig = android.media.AudioFormat.CHANNEL_OUT_STEREO;
        }

        int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

        AudioTrack audioTrack = new AudioTrack(
                AudioManager.STREAM_MUSIC,// 指定流的类型
                sampleRateInHz,// 设置音频数据的採样率 32k,假设是44.1k就是44100
                channelConfig,// 设置输出声道为双声道立体声,而CHANNEL_OUT_MONO类型是单声道
                audioFormat,// 设置音频数据块是8位还是16位。这里设置为16位。
                bufferSizeInBytes,//缓冲区大小
                AudioTrack.MODE_STREAM // 设置模式类型,在这里设置为流类型,第二种MODE_STATIC貌似没有什么效果
        );
audio.play(); // 启动音频设备。以下就能够真正開始音频数据的播放了
// 打开mp3文件,读取数据,解码等操作省略 ...
byte[] buffer = new buffer[4096];
int count;
while(true)
{
    // 最关键的是将解码后的数据,从缓冲区写入到AudioTrack对象中
    audio.write(buffer, 0, 4096);
    if(文件结束) break;
}
//关闭并释放资源
audio.stop();
audio.release();

AudioTrack源码分析

创建AudioTrack对象

取得mMinBufferSize后,可以创建一个AudioTrack对象了:


public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)throws IllegalArgumentException {
    this(streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);
    }

这里是调用了该public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId)构造方法:


public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
        int mode, int sessionId)
                throws IllegalArgumentException {
    super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK);
    
    .....

    // native initialization
    int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes,
            sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
            mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/);
    if (initResult != SUCCESS) {
        loge("Error code "+initResult+" when initializing AudioTrack.");
        return; // with mState == STATE_UNINITIALIZED
    }

    mSampleRate = sampleRate[0];
    mSessionId = session[0];

    if (mDataLoadMode == MODE_STATIC) {
        mState = STATE_NO_STATIC_DATA;
    } else {
        mState = STATE_INITIALIZED;
    }

    baseRegisterPlayer();}

AudioTrack对象的创建过程涉及到native_setup方法

//*frameworks/base/core/jni/android_media_AudioTrack.cpp

static int  android_media_AudioTrack_native_setup(JNIEnv*env, jobject thiz, jobject weak_this,
        jint streamType, jintsampleRateInHertz, jint javaChannelMask,
        jint audioFormat, jintbuffSizeInBytes, jint memoryMode, jintArray jSession)

{   
    .....
    sp<AudioTrack>lpTrack = new AudioTrack();
    .....
AudioTrackJniStorage* lpJniStorage =new AudioTrackJniStorage();
//frameworks/av/media/libaudioclient/AudioTrack.cpp

status_t AudioTrack::createTrack_l()
{
status_t status;
bool callbackAdded = false;

const sp<IAudioFlinger>& audioFlinger = AudioSystem::get_audio_flinger();
…

sp<IAudioTrack> track = audioFlinger->createTrack(input, output, &status);

}
//frameworks/av/media/libaudioclient/include/media/IAudioFlinger.h
virtual sp<IAudioTrack> createTrack(const CreateTrackInput& input,
CreateTrackOutput& output,
status_t *status) = 0;

每一个音频流对应着一个AudioTrack类的一个实例,每个AudioTrack会在创建时注册到 AudioFlinger中,由AudioFlinger把所有的AudioTrack进行混合(Mixer),然后输送到AudioHardware中进行播放,目前Android同时最多可以创建32个音频流,也就是说,Mixer最多会同时处理32个AudioTrack的数据流:


示意图.png

AudioPolicyService 与 AudioFlinger 是 Android 音频系统的两大基本服务。前者是音频系统策略的制定者,负责音频设备切换的策略抉择、音量调节策略等;后者是音频系统策略的执行者,负责音频流设备的管理及音频流数据的处理传输,所以 AudioFlinger 也被认为是 Android 音频系统的引擎。

  • AudioFlinger服务启动
//frameworks/av/media/audioserver/main_audioserver.cpp
android::hardware::configureRpcThreadpool(4, false /*callerWillJoin*/);
sp<ProcessState> proc(ProcessState::self());
sp<IServiceManager> sm = defaultServiceManager();
ALOGI("ServiceManager: %p", sm.get());
AudioFlinger::instantiate();
AudioPolicyService::instantiate();
//可见 audioserver 把音频相关的服务都加载了,包括 AudioFlinger、AudioPolicyService、RadioService、SoundTriggerHwService。
// AAudioService should only be used in OC-MR1 and later.
// And only enable the AAudioService if the system MMAP policy explicitly allows it.
// This prevents a client from misusing AAudioService when it is not supported.
aaudio_policy_t mmapPolicy = property_get_int32(AAUDIO_PROP_MMAP_POLICY,
AAUDIO_POLICY_NEVER);
if (mmapPolicy == AAUDIO_POLICY_AUTO || mmapPolicy == AAUDIO_POLICY_ALWAYS) {
AAudioService::instantiate();
}

SoundTriggerHwService::instantiate();
ProcessState::self()->startThreadPool();
IPCThreadState::self()->joinThreadPool();

AudioFlinger 服务启动后,其他进程可以通过 ServiceManager 来获取其代理对象 IAudioFlinger,通过 IAudioFlinger 可以向 AudioFlinger 发出各种服务请求,从而完成自己的音频业务。
音频流控制最常用的三个接口:

  • AudioFlinger::PlaybackThread::Track::start:开始播放:把该 Track 置 ACTIVE 状态,然后添加到 mActiveTracks 向量中,最后调用AudioFlinger::PlaybackThread::broadcast_l() 告知 PlaybackThread 情况有变
  • AudioFlinger::PlaybackThread::Track::stop:停止播放:把该 Track 置 STOPPED 状态,最后调用 AudioFlinger::PlaybackThread::broadcast_l() 告知 PlaybackThread 情况有变
  • AudioFlinger::PlaybackThread::Track::pause:暂停播放:把该 Track 置 PAUSING 状态,最后调用 AudioFlinger::PlaybackThread::broadcast_l() 告知 PlaybackThread 情况有变

AudioFlinger 响应的服务请求主要有:

  • 获取硬件设备的配置信息
  • 音量调节
  • 静音操作
  • 音频模式切换
  • 音频参数设置
  • 输入输出流设备管理
  • 音频流管理
  • AudioTrack(写数据):AudioTrack 在 FIFO 中找到一块可用空间,把用户传入的音频数据写入到这块可用空间上,然后更新写位置(对于 AudioFinger 来说,意味 FIFO 上有更多的可读数据了);如果用户传入的数据量比可用空间要大,那么要把用户传入的数据拆分多次写入到 FIFO 中(AudioTrack 和 AudioFlinger 是不同的进程,AudioFlinger 同时也在不停地读取数据,所以 FIFO 可用空间是在不停变化的)
  • AudioFlinger(读数据):AudioFlinger 在 FIFO 中找到一块可读数据块,把可读数据拷贝到目的缓冲区上,然后更新读位置(对于 AudioTrack 来说,意味着 FIFO 上有更多的可用空间了);如果FIFO 上可读数据量比预期的要小,那么要进行多次的读取,才能积累到预期的数据量(AudioTrack 和 AudioFlinger 是不同的进程,AudioTrack 同时也在不停地写入数据,所以 FIFO 可读的数据量是在不停变化的)
    write方法的使用:
// 创建一个 AudioTrack 实例
        AudioTrack track = new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, TEST_FORMAT, 
                minBuffSize, TEST_MODE);
        byte data[] = new byte[minBuffSize/2];
        //--------    test        --------------
        // 调用 write() 写入回放数据
        track.write(data, 0, data.length);
        track.write(data, 0, data.length);
        // 调用 play() 开始播放
        track.play();

开始播放

play()->startImpl()->native_start()->android_media_AudioTrack_start.lpTrack->start()(JNI)->
mAudioTrack->start()(native)->通过IAudioTrack调用
virtual status_t start() = 0;

停止播放
停止播放音频数据,如果是STREAM模式,会等播放完最后写入buffer的数据才会停止。如果立即停止,要调用pause()方法,然后调用flush方法,会舍弃还没有播放的数据(flush()只在模式为STREAM下可用)。

stop()->native_stop()->
android_media_AudioTrack_stop中lpTrack->stop()(JNI)->

mAudioTrack->stop()->virtual void stop() = 0(IAudioTrack.h)

释放本地AudioTrack资源

release()->native_release()->android_media_AudioTrack_release


static void android_media_AudioTrack_release(JNIEnv *env, jobject thiz) {
sp<AudioTrack> lpTrack = setAudioTrack(env, thiz, 0);
if (lpTrack == NULL) {
return;
}
//ALOGV("deleting lpTrack: %x\n", (int)lpTrack);

// delete the JNI data
AudioTrackJniStorage* pJniStorage = (AudioTrackJniStorage *)env->GetLongField(
thiz, javaAudioTrackFields.jniData);
// reset the native resources in the Java object so any attempt to access
// them after a call to release fails.
env->SetLongField(thiz, javaAudioTrackFields.jniData, 0);

if (pJniStorage) {
Mutex::Autolock l(sLock);
audiotrack_callback_cookie *lpCookie = &pJniStorage->mCallbackData;
//ALOGV("deleting pJniStorage: %x\n", (int)pJniStorage);
while (lpCookie->busy) {
if (lpCookie->cond.waitRelative(sLock,
milliseconds(CALLBACK_COND_WAIT_TIMEOUT_MS)) !=
NO_ERROR) {
break;
}
}
sAudioTrackCallBackCookies.remove(lpCookie);
// delete global refs created in native_setup
env->DeleteGlobalRef(lpCookie->audioTrack_class);
env->DeleteGlobalRef(lpCookie->audioTrack_ref);
delete pJniStorage;
}
}

返回当前的播放状态

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356