【初学音频】Android的Audio系统之AudioTrack

目录

前言  

1. AudioTrack

2. 用例介绍

2.1 过程

2.2 数据加载模式

2.3 音频流的类型

2.4 Buffer分配和Frame的概念

3. AudioTrack (Java空间) 分析

3.1 AudioTrack的构造

3.2 AudioTrackJniStorage分析

3.2.1 共享内存介绍

3.2.2 MemoryHeapBase和MemoryBase类介绍

3.2.3 play和write的分析

3.2.4 release的分析

3.2.5 AudioTrack (Java空间) 的分析总结

4. AudioTrack (Native空间) 分析

4.1 new AudioTrack和set分析

4.1.1 IAudioTrack和AT、AF的关系

 4.1.2 共享内存及其Control Block

 4.1.3 数据的Push or Pull

4.2 write输入数据

4.3 obtainBuffer和releaseBuffer

4.4 delete AudioTrack

5. AudioTrack的总结


前言  

之前了解到了Audio系统的关键组件有三个:

  • AudioRecorder与AudioTrack
  • AudioFlinger
  • AudioPolicyService

又知道Audio的系统框架可以分为:

  1. Java API Framwork 应用框架
  2. JNI (Java Native Interface) :连接Java层和Native层的桥梁
  3. Native Framework 原生框架:包含AudioRecord 和AudioTrack
  4. Binder IPC:提供进程间通信
  5. MediaServer:包含AudioFlinger 和AudioPolicyService
  6. HAL (Hardware Abstraction Layer) 硬件抽象
  7. Linux Kernel 内核驱动

接下来,以API类的AudioTrack开始,从Java层到Native层一步步了解工作原理。

AudioTrack.java

android_media_AudioTrack.cpp

AudioTrack.cpp

AudioPolicyService

1. AudioTrack

AudioTrack属于Audio系统对外提供的API类,所以它在Java层和Native层均有对应的类,先从Java层开始。

[MainActivity.java]

import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MainActivity extends Activity {
    private static final String TAG = "VoiceRecord";
    private static final int RECORDER_SAMPLERATE = 8000;
    private static final int RECORDER_CHANNELS_IN = AudioFormat.CHANNEL_IN_MONO;
    private static final int RECORDER_CHANNELS_OUT = AudioFormat.CHANNEL_OUT_MONO;
    private static final int RECORDER_AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
    private static final int AUDIO_SOURCE = MediaRecorder.AudioSource.MIC;
    // Initialize minimum buffer size in bytes.
    private int bufferSize = AudioRecord.getMinBufferSize(RECORDER_SAMPLERATE, RECORDER_CHANNELS_IN, RECORDER_AUDIO_ENCODING);
    private AudioRecord recorder = null;
    private Thread recordingThread = null;
    private boolean isRecording = false;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((Button) findViewById(R.id.start_button)).setOnClickListener(btnClick);
        ((Button) findViewById(R.id.stop_button)).setOnClickListener(btnClick);
        enableButtons(false);
    }
    private void enableButton(int id, boolean isEnable) {
        ((Button) findViewById(id)).setEnabled(isEnable);
    }
    private void enableButtons(boolean isRecording) {
        enableButton(R.id.start_button, !isRecording);
        enableButton(R.id.stop_button, isRecording);
    }
    private void startRecording() {
        if( bufferSize == AudioRecord.ERROR_BAD_VALUE)
            Log.e(TAG, "Bad Value for \"bufferSize\", recording parameters are not supported by the hardware");
        if( bufferSize == AudioRecord.ERROR )
            Log.e( TAG, "Bad Value for \"bufferSize\", implementation was unable to query the hardware for its output properties");
        Log.e( TAG, "\"bufferSize\"="+bufferSize);
        // Initialize Audio Recorder.
        recorder = new AudioRecord(AUDIO_SOURCE, RECORDER_SAMPLERATE, RECORDER_CHANNELS_IN, RECORDER_AUDIO_ENCODING, bufferSize);
        // Starts recording from the AudioRecord instance.
        recorder.startRecording();
        isRecording = true;
        recordingThread = new Thread(new Runnable() {
            public void run() {
                writeAudioDataToFile();
            }
        }, "AudioRecorder Thread");
        recordingThread.start();
    }
    private void writeAudioDataToFile() {
        //Write the output audio in byte
        String filePath = "/sdcard/8k16bitMono.pcm";
        byte saudioBuffer[] = new byte[bufferSize];
        FileOutputStream os = null;
        try {
            os = new FileOutputStream(filePath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        while (isRecording) {
            // gets the voice output from microphone to byte format
            recorder.read(saudioBuffer, 0, bufferSize);
            try {
                //  writes the data to file from buffer stores the voice buffer
                os.write(saudioBuffer, 0, bufferSize);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void stopRecording() throws IOException {
        //  stops the recording activity
        if (null != recorder) {
            isRecording = false;
            recorder.stop();
            recorder.release();
            recorder = null;
            recordingThread = null;
            PlayShortAudioFileViaAudioTrack("/sdcard/8k16bitMono.pcm");
        }
    }
    private void PlayShortAudioFileViaAudioTrack(String filePath) throws IOException{
        // We keep temporarily filePath globally as we have only two sample sounds now..
        if (filePath==null)
            return;
        //Reading the file..
        File file = new File(filePath); // for ex. path= "/sdcard/samplesound.pcm" or "/sdcard/samplesound.wav"
        byte[] byteData = new byte[(int) file.length()];
        Log.d(TAG, (int) file.length()+"");
        FileInputStream in = null;
        try {
            in = new FileInputStream( file );
            in.read( byteData );
            in.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // Set and push to audio track..
        int intSize = android.media.AudioTrack.getMinBufferSize(RECORDER_SAMPLERATE, RECORDER_CHANNELS_OUT, RECORDER_AUDIO_ENCODING);
        Log.d(TAG, intSize+"");
        AudioTrack at = new AudioTrack(AudioManager.STREAM_MUSIC, RECORDER_SAMPLERATE, RECORDER_CHANNELS_OUT, RECORDER_AUDIO_ENCODING, intSize, AudioTrack.MODE_STREAM);
        if (at!=null) {
            at.play();
            // Write the byte array to the track
            at.write(byteData, 0, byteData.length);
            at.stop();
            at.release();
        }
        else
            Log.d(TAG, "audio track is not initialised ");
    }
    private View.OnClickListener btnClick = new View.OnClickListener() {
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.start_button: {
                    enableButtons(true);
                    startRecording();
                    break;
                }
                case R.id.stop_button: {
                    enableButtons(false);
                    try {
                        stopRecording();
                    } catch (IOException e) {
                        //  TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    break;
                }
            }
        }
    };
    // onClick of backbutton finishes the activity.
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            finish();
        }
        return super.onKeyDown(keyCode, event);
    }
}

<!--activity_main.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/start_button"
        android:text="start"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/stop_button"
        android:text="stop"/>
</LinearLayout>

在AndroidManifest.xml中添加权限

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

2. 用例介绍

AudioTrack类用于管理和播放单个音频资源。它允许将PCM音频缓冲区流传输到音频接收器以进行播放。AudioRecord录制PCM数据是通过“read”形式读入缓冲区,那么AudioTrack则是通过"write"形式从缓冲区输出。

2.1 过程

[-->AudioTrackAPI使用例子(Java层)]

//1. 根据音频数据的特性来确定所要分配的缓冲区的最小size
int bufsize =
            AudioTrack.getMinBufferSize(8000,     //采样率:每秒8000个点                     
            AudioFormat.CHANNEL_CONFIGURATION_STEREO,      //声道数:双声道           
            AudioFormat.ENCODING_PCM_16BIT        //采样精度:一个采样点16比特,相当于2个字节
    
);

//2. 创建AudioTrack
AudioTrack trackplayer = new AudioTrack(
            AudioManager.STREAM_MUSIC,        //音频流类型
            8000,AudioFormat.CHANNEL_CONFIGURATION_STEREO,
            AudioFormat.ENCODING_PCM_16BIT, bufsize,
            AudioTrack.MODE_STREAM);         //数据加载模式

//3. 开始播放
trackplayer.play() ;


......

//4. 调用write写数据
trackplayer.write(bytes_pkg, 0,bytes_pkg.length) ;     //往track中写数据


......

//5. 停止播放和释放资源
trackplayer.stop();            //停止播放
trackplayer.release();            //释放底层资源
  1. 初始化一个缓冲区,该缓冲区大小大于等于AudioTrack对象用于读取声音数据的缓冲区大小,用于缓存读取的音频数据。缓冲区大小可以通过getMinBufferSize方法得到。
  2. 构造一个AudioTrack对象。如果缓冲区容量过小,将导致对象构造的失败。
  3. 调用play()准备播放声音
  4. 创建一个数据流,不断地从数据流中读取声音数据到缓冲区,然后使用write()将缓冲区中的数据输出。
  5. 停止播放,释放资源

上面的用例引入了两个新的概念,一个是数据加载模式,另一个是音频流类型。

2.2 数据加载模式

AudioTrack有两种数据加载模式,对应不同的使用场景:

  • MODE_STREAM
  • MODE_STATIC

MODE_STREAM:在这种模式下,应用程序持续地将音频数据流write到AudioTrack中,而且write动作将堵塞直到数据流从Java层传输到native层,同一时候增加到播放队列中。这样的模式适用于播放大音频数据或者持续增加的音频数据,使用场景如大文件播放,实时音频传输。但是这种工作方式每次都需要把数据从用户提供的Buffer中拷贝到AudioTrack内部的Buffer中,在一定程度上会引起延时

MODE_STATIC:在这种模式下,在play之前只需要先把全部数据一次性write到AudioTrack的内部缓冲区中,后续就不必再传递数据了。这种模式适用于播放内存占用小、延时要求较高的音频数据,例如经常播放的短提示音、铃声等。但是它也有一个缺点,就是一次write的数据不能太多,否则系统无法分配足够的内存来存储全部数据。

2.3 音频流的类型

在AudioTrack构造函数中,会接触到AudioManager.STREAM_MUSIC这个参数。它的含义与Android系统对音频流的管理和分类有关。

Android将系统的声音分为好几种流类型,下面是几个常见的:

  • STREAM_ALARM:警告声
  • STREAM_MUSIC:音乐声,例如music等
  • STREAM_RING:铃声
  • STREAM_SYSTEM:系统声音,例如低电提示音,锁屏音等
  • STREAM_VOCIE_CALL:通话声

注意:上面这些类型的划分和音频数据本身并没有关系。例如MUSIC和RING类型都可以是某首MP3歌曲。另外,声音流类型的选择没有固定的标准,例如,铃声预览中的铃声可以设置为MUSIC类型。

2.4 Buffer分配和Frame的概念

在用例中碰到的第一个重要函数就是getMinBufferSize。这个函数对于确定应用层分配多大的数据Buffer具有重要指导意义。来看这个函数的实现:

[-->AudioTrack.java]

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);
         }
    }

// 目前只支持PCM8和PCM16精度的音频数据
// See {@link AudioFormat#ENCODING_PCM_16BIT} and {@link AudioFormat#ENCODING_PCM_8BIT}, and {@link AudioFormat#ENCODING_PCM_FLOAT}.
    if (!AudioFormat.isPublicEncoding(audioFormat)) {
        loge("getMinBufferSize(): Invalid audio format.");
        return ERROR_BAD_VALUE;
    }

// 采样频率的最大最小值可以修改,但是必须在规定的范围内
// Note: AudioFormat.SAMPLE_RATE_UNSPECIFIED is not allowed
    if (((sampleRateInHz < AudioFormat.SAMPLE_RATE_HZ_MIN) || (sampleRateInHz > AudioFormat.SAMPLE_RATE_HZ_MAX)) && (sampleRateInHz != AudioFormat.SAMPLE_RATE_HZ_MAX_FOR_DIRECT)) {
        loge("getMinBufferSize(): " + sampleRateInHz + " Hz is not a supported sample rate.");
        return ERROR_BAD_VALUE;
    }

// 调用Native函数,先想想为什么,如果是简单计算,那么Java层做不到吗?原来,还需要确认硬件是否支持这些参数,当然得进入Native层查询了
    int size = native_get_min_buff_size(sampleRateInHz, channelCount, audioFormat);
    if (size <= 0) {
        loge("getMinBufferSize(): error querying hardware");
        return ERROR;
    }
    else {
        return size;
    }
}

Native的函数将查询Audio系统中音频输出硬件HAL对象的一些信息,并确认它们是否支持这些采样率和采样精度。

来看Native的native_get_min_buff_size函数。它在android_media_track.cpp中。

[-->android_media_track.cpp]

// 注意我们传入的参数是:
// sampleRateInHertz = 8000,nbChannels = 2
// audioFormat = AudioFormat.ENCODING_PCM_16BIT

static jint android_media_AudioTrack_get_min_buff_size(JNIEnv*env,  jobject thiz,
                 jintsampleRateInHertz, jint nbChannels, jint audioFormat)

{
    intafSamplingRate;
    intafFrameCount;
    uint32_t afLatency;

// 下面这些调用涉及了AudioSystem,这个和AudioPolicy有关系。这里仅把它们看成是信息查询即可

    // 查询采样率,一般返回的是所支持的最高采样率,例如44100
    if(AudioSystem::getOutputSamplingRate(&afSamplingRate) != NO_ERROR) {
       return -1;
}
    // 查询硬件内部缓冲的大小,以Frame为单位。什么是Frame?
    if(AudioSystem::getOutputFrameCount(&afFrameCount) != NO_ERROR) {
       return -1;
    }
    // 查询硬件的延时时间
    if(AudioSystem::getOutputLatency(&afLatency) != NO_ERROR) {
       return -1;
}

......

代码中出现了音频系统中的一个重要概念:Frame(帧)。

说明:

Frame是一个单位,经多方查寻,最终在ALSA的wiki中找到了对它的解释。

Frame直观上用来描述数据量的多少,例如,一帧等于多少字节。1单位的Frame等于1个采样点的字节数×声道数(比如PCM16,双声道的1个Frame等于2×2=4字节)。

我们知道,1个采样点只针对一个声道,而实际上可能会有一或多个声道。由于不能用一个独立的单位来表示全部声道一次采样的数据量,也就引出了Frame的概念。

Frame的大小,就是一个采样点的字节数×声道数。

另外,在目前的声卡驱动程序中,其内部缓冲区也是采用Frame作为单位来分配和管理的。

......

    // minBufCount表示缓冲区的最少个数,它以Frame作为单位
    uint32_t minBufCount = afLatency / ((1000 *afFrameCount)/afSamplingRate);
    if(minBufCount < 2) minBufCount = 2;//至少要两个缓冲

    //计算最小帧个数
    uint32_tminFrameCount = (afFrameCount*sampleRateInHertz*minBufCount)/afSamplingRate;

    //下面根据最小的FrameCount计算最小的缓冲大小   
    intminBuffSize = minFrameCount //计算方法完全符合我们前面关于Frame的介绍
            * (audioFormat == javaAudioTrackFields.PCM16 ? 2 : 1)
            * nbChannels;

    return minBuffSize;
}

getMinBufSize会综合考虑硬件的情况(诸如是否支持采样率,硬件本身的延迟情况等)后,得出一个最小缓冲区的大小。一般我们分配的缓冲大小会是它的整数倍。

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

3. AudioTrack (Java空间) 分析

3.1 AudioTrack的构造

回顾一下用例中调用AudioTrack构造函数的代码:

AudioTrack trackplayer = new AudioTrack(
                    AudioManager.STREAM_MUSIC,    
                    8000,AudioFormat.CHANNEL_CONFIGURATION_ STEREO,
                    AudioFormat.ENCODING_PCM_16BIT,bufsize,
                    AudioTrack.MODE_STREAM);

AudioTrack构造函数的实现在AudioTrack.java中。

[-->AudioTrack.java]

public AudioTrack(int streamType, intsampleRateInHz, int channelConfig,
                 intaudioFormat,int bufferSizeInBytes, int mode)
                 throws IllegalArgumentException {   

       mState= STATE_UNINITIALIZED;
       //检查参数是否合法
       audioParamCheck(streamType, sampleRateInHz, channelConfig,                          audioFormat,mode);
       //bufferSizeInBytes是通过getMinBufferSize得到的,所以下面的检查肯定能通过
       audioBuffSizeCheck(bufferSizeInBytes);

       /*
           调用native层的native_setup,构造一个WeakReference传进去。
           不了解Java WeakReference读者可以上网查一下,很简单
       */

       int initResult = native_setup(new WeakReference<AudioTrack>(this),
       mStreamType,//这个值是AudioManager.STREAM_MUSIC   
       mSampleRate, //这个值是8000       
       mChannels,   //这个值是2
       mAudioFormat,//这个值是AudioFormat.ENCODING_PCM_16BIT      
       mNativeBufferSizeInBytes,//这个值等于bufferSizeInBytes              
       mDataLoadMode);//DataLoadMode是MODE_STREAM       
       ....
}

weak reference弱引用:

当一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收。如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象。

WeakReference的语法:

WeakReference<T> weakReference = new WeakReference<T>(referent);

 native_setup对应的JNI层函数是android_media_AudioTrack_native_setup。

[-->android_media_AudioTrack.cpp]

static int
android_media_AudioTrack_native_setup(JNIEnv*env, jobject thiz,   
                         jobjectweak_this,jint streamType,        
                         jintsampleRateInHertz, jint channels,   
                         jintaudioFormat, jint buffSizeInBytes,  
                         jintmemoryMode)                 
{
    intafSampleRate;
    intafFrameCount;
    //进行一些信息查询
    AudioSystem::getOutputFrameCount(&afFrameCount, streamType);
    AudioSystem::getOutputSamplingRate(&afSampleRate, streamType);
    AudioSystem::isOutputChannel(channels); 
    //popCount用于统计一个整数中有多少位为1,有很多经典的算法
    int nbChannels = AudioSystem::popCount(channels);
    //Java层的值和JNI层的值转换
    if(streamType == javaAudioTrackFields.STREAM_MUSIC)
         atStreamType = AudioSystem::MUSIC;
    intbytesPerSample = audioFormat == javaAudioTrackFields.PCM16 ? 2 : 1; 
    intformat = audioFormat == javaAudioTrackFields.PCM16 ?
                  AudioSystem::PCM_16_BIT : AudioSystem::PCM_8_BIT;

    //计算以帧为单位的缓冲大小
    intframeCount = buffSizeInBytes / (nbChannels * bytesPerSample);

    //1. AudioTrackJniStorage对象,它保存了一些信息,后面将详细分析
    AudioTrackJniStorage* lpJniStorage = new AudioTrackJniStorage();
    ......

    //2. 创建Native层的AudioTrack对象
    AudioTrack* lpTrack = new AudioTrack();
       if(memoryMode == javaAudioTrackFields.MODE_STREAM) {

           //3. STREAM模式
           lpTrack->set(
               atStreamType,//指定流类型
               sampleRateInHertz,
               format,// 采样点的精度,一般为PCM16或者PCM8
               channels,
               frameCount, 
               0,// flags
               audioCallback, //该回调函数定义在android_media_AudioTrack.cpp中   
               &(lpJniStorage->mCallbackData),
               0,
               0,// 共享内存,STREAM模式下为空。实际使用的共享内存由AF创建
               true);//内部线程可以调用JNI函数,还记得“zygote偷梁换柱”那一节吗?
       } else if (memoryMode == javaAudioTrackFields.MODE_STATIC) {
       //如果是static模式,需要先创建共享内存
       lpJniStorage->allocSharedMem(buffSizeInBytes);
       lpTrack->set(
               atStreamType,// stream type
               sampleRateInHertz,
               format,// word length, PCM
               channels,
               frameCount,
               0,// flags
               audioCallback,
               &(lpJniStorage->mCallbackData),
               0,
               lpJniStorage->mMemBase, //STATIC模式下,需要传递该共享内存
               true);
    }

    ......
    /*
      把JNI层中new出来的AudioTrack对象指针保存到Java对象的一个变量中,
      这样就把JNI层的AudioTrack对象和Java层的AudioTrack对象关联起来了,
      这是Android的常用技法。                        
   */   
     env->SetIntField(thiz,javaAudioTrackFields.nativeTrackInJavaObj,
                     (int)lpTrack);

     // lpJniStorage对象指针也保存到Java对象中
     env->SetIntField(thiz, javaAudioTrackFields.jniData,(int)lpJniStorage);
}

以上列出了三个点,这一节仅分析AudioTrackJniStorage这个类,其余的作为Native AudioTrack部分放在后面进行分析。

3.2 AudioTrackJniStorage分析

AudioTrackJniStorage是一个辅助类,其中有一些有关共享内存方面的较重要的知识。

3.2.1 共享内存介绍

共享内存,作为进程间数据传递的一种手段,在AudioTrack和AudioFlinger中被大量使用。先简单了解一下有关共享内存的知识:

  • 每个进程的内存空间是4GB,这个4GB是由指针长度决定的,如果指针长度为32位,那么地址的最大编号就是0xFFFFFFFF,为4GB。
  • 上面说的内存空间是进程的虚拟地址空间。换言之,在应用程序中使用的指针其实是指向虚拟空间地址的。那么,如何通过这个虚地址找到存储在真实物理内存中的数据呢?

上面的问题,引出了内存映射的概念。内存映射让虚拟空间中的内存地址和真实物理内存地址之间建立了一种对应关系。也就是说,进程中操作的0x12345678这块内存的地址,在经过OS内存管理机制的转换后,它实际对应的物理地址可能会是0x87654321。当然,这一切对进程来说都是透明的,这些活都由操作系统悄悄地完成了。

共享内存和内存映射有着重要关系,下图为“共享内存示意图”。

真实内存中0x87654321标志的这块内存页(OS的内存管理机制将物理内存分成了一个个的内存页,一块内存页的大小一般是4KB)可以同时映射到进程A和进程B中,这样就做到了内存在两个进程间共享。

如何创建和共享内存呢?不同系统会有不同的方法。Linux平台的一般做法是:

  • 进程A创建并打开一个文件,得到一个文件描述符fd。
  • 通过mmap调用将fd映射成内存映射文件。在mmap调用中指定特定参数表示要创建进程间共享内存。
  • 进程B打开同一个文件,也得到一个文件描述符,这样A和B就打开了同一个文件。
  • 进程B也要用mmap调用指定参数表示想使用共享内存,并传递打开的fd。这样A和B就通过打开同一个文件并构造内存映射,实现了进程间内存共享。

注意:这个文件也可以是设备文件。一般来说,mmap函数的具体工作由参数中的那个文件描述符所对应的驱动或内核模块来完成。

除上述一般方法外,Linux还有System V的共享内存创建方法,这里就不再介绍了。总之,AT和AF之间的数据传递,就是通过共享内存方式来完成的。这种方式对于跨进程的大数据量传输来说,是非常高效的。

3.2.2 MemoryHeapBase和MemoryBase类介绍

AudioTrackJniStorage用到了Android对共享内存机制的封装类。所以我们有必要先看看AudioTrackJniStorage的内容。

[-->android_media_AudioTrack.cpp::AudioTrackJniStorage相关]

//下面这个结构就是保存一些变量,没有什么特别的作用
struct audiotrack_callback_cookie {
   jclass      audioTrack_class;
   jobject     audioTrack_ref;
 };
class AudioTrackJniStorage {
   public:
       sp<MemoryHeapBase>    mMemHeap;//这两个Memory很重要
       sp<MemoryBase>         mMemBase;

       audiotrack_callback_cookie mCallbackData;
       int                       mStreamType;
 
      boolallocSharedMem(int sizeInBytes) {

     /* 
      注意关于MemoryHeapBase和MemoryBase的用法。   
      先new一个MemoryHeapBase,再以它为参数new一个MemoryBase        
    */  

   //1. MemoryHeapBase
    mMemHeap = new MemoryHeapBase(sizeInBytes, 0,"AudioTrack Heap Base");
   //2. MemoryBase
    mMemBase= new MemoryBase(mMemHeap, 0, sizeInBytes);

    return true;
   }
};

代码中标识1和2的地方,很好地展示了这两个Memory类的用法。下图展示了MemoryHeapBase和MemoryBase的家谱。

MemoryHeapBase是一个基于Binder通信的类,根据前面的Binder知识,BnMemoryHeapBase由客户端使用,而MemoryHeapBase完成BnMemoryHeapBase的业务工作。

从MemoryHeapBase开始分析,它的使用方法是:

mMemHeap = new MemoryHeapBase(sizeInBytes, 0,"AudioTrack Heap Base");

 它的代码在MemoryHeapBase.cpp中。

[-->MemoryHeapBase.cpp]

/*
     MemoryHeapBase有两个构造函数,我们用的是第一个。
     size表示共享内存大小,flags为0,name为"AudioTrackHeap Base"
*/

MemoryHeapBase::MemoryHeapBase(size_t size,uint32_t flags,char const * name)
     :mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags),
     mDevice(0), mNeedUnmap(false)

{
    constsize_t pagesize = getpagesize();//获取系统中的内存页大小,一般为4KB
    size =((size + pagesize-1) & ~(pagesize-1));

    /*   
     创建共享内存,ashmem_create_region函数由libcutils提供。
     在真实设备上将打开/dev/ashmem设备得到一个文件描述符,在模拟器上则创建一个tmp文件    
    */

    int fd= ashmem_create_region(name == NULL ? "MemoryHeapBase" : name, size);
    //下面这个函数将通过mmap方式得到内存地址,这是Linux的标准做法,有兴趣的读者可以看看
    mapfd(fd,size);
}

MemoryHeapBase构造完后,得到了以下结果:

  • mBase变量指向共享内存的起始位置。
  • mSize是所要求分配的内存大小。
  • mFd是ashmem_create_region返回的文件描述符。

另外,MemoryHeapBase提供了以下几个函数,可以获取共享内存的大小和位置。由于这些函数都很简单,仅把它们的作用描述一下即可。

MemoryHeapBase::getBaseID() //返回mFd,如果为负数,表明刚才创建共享内存失败了
MemoryHeapBase::getBase()  //共享内存起始地址
MemoryHeapBase::getSize() //返回mSize,表示内存大小

MemoryHeapBase确实比较简单,它通过ashmem_create_region得到一个文件描述符。

说明:Android系统通过ashmem创建共享内存的原理,和Linux系统中通过打开文件创建共享内存的原理类似,但ashmem设备驱动在这方面做了较大的改进,例如增加了引用计数、延时分配物理内存的机制(即真正使用的时候才去分配内存)等。这些内容,感兴趣的读者还可以自行对其研究。

接下来学习MemoryBase。

MemoryBase也是一个基于Binder通信的类,它比起MemoryHeapBase就更显简单了,看起来更像是一个辅助类。它的声明在MemoryBase.h中。一起来看:

[-->MemoryBase.h::MemoryBase声明]

class MemoryBase : public BnMemory
{
public:
   MemoryBase(const sp<IMemoryHeap>& heap,ssize_t offset, size_tsize);
   virtual sp<IMemoryHeap> getMemory(ssize_t* offset, size_t* size)const;


protected:
    size_tgetSize() const { return mSize; }//返回大小
    ssize_tgetOffset() const { return mOffset;}//返回偏移量
    //返回MemoryHeapBase对象     
    constsp<IMemoryHeap>& getHeap() const { return mHeap;}
};
//MemoryBase的构造函数
MemoryBase::MemoryBase(constsp<IMemoryHeap>& heap,ssize_t offset, size_t size)
    :mSize(size), mOffset(offset), mHeap(heap)
{
}

MemoryHeapBase和MemoryBase都够简单吧?总结起来不过是:

  • 分配了一块共享内存,这样两个进程可以共享这块内存。
  • 基于Binder通信,这样使用这两个类的进程就可以交互了。

提醒:这两个类没有提供同步对象来保护这块共享内存,所以后续在使用这块内存时,必然需要一个跨进程的同步对象来保护它。这一点,是我在AT中第一次见到它们时想到的,不知道你是否注意过这个问题。

3.2.3 play和write的分析

//3. 开始播放
trackplayer.play() ;

//4. 调用write写数据
trackplayer.write(bytes_pkg, 0,bytes_pkg.length) ;//往track中写数据

1. play的分析

先看看play函数对应的JNI层函数,它是android_media_AudioTrack_start。

[-->android_media_AudioTrack.cpp]

static void
android_media_AudioTrack_start(JNIEnv *env,jobject thiz)
{
/*
   从Java的AudioTrack对象中获取对应Native层的AudioTrack对象指针。
   从int类型直接转换成指针,不过要是以后ARM平台支持64位指针了,代码就得大修改了。
*/
   AudioTrack *lpTrack = (AudioTrack *)env->GetIntField(
        thiz,javaAudioTrackFields.nativeTrackInJavaObj);
   lpTrack->start(); //很简单的调用
}

2. write的分析

Java层的write函数有两个:

  • 写PCM16数据:一个采样点的数据量是两个字节。
  • 写PCM8数据:一个采样点的数据量是一个字节。

我们的用例中采用的是PCM16数据。它对应的JNI层函数是android_media_AudioTrack_native_write_short:

[-->android_media_AudioTrack.cpp]

static jint android_media_AudioTrack_native_write_short(
                 JNIEnv*env, jobject thiz,
                 jshortArrayjavaAudioData, jint offsetInShorts,
                 jintsizeInShorts, jint javaAudioFormat) {

        return(android_media_AudioTrack_native_write(
                 env, thiz,(jbyteArray)javaAudioData, offsetInShorts*2,
                 sizeInShorts * 2, javaAudioFormat) / 2);
}

无论PCM16还是PCM8数据,最终都会调用writeToTrack函数。

[-->android_media_AudioTrack.cpp]

jint writeToTrack(AudioTrack* pTrack, jintaudioFormat,
                 jbyte*data,jint offsetInBytes, jint sizeInBytes) {
    
     ssize_t written = 0;
/*
     如果是STATIC模式,sharedBuffer()返回不为空
     如果是STREAM模式,sharedBuffer()返回空
*/
        if (pTrack->sharedBuffer() == 0) {
        //我们的用例是STREAM模式,调用write函数写数据
                written = pTrack->write(data + offsetInBytes, sizeInBytes);
        } else {
                if (audioFormat == javaAudioTrackFields.PCM16) {
                     if ((size_t)sizeInBytes > pTrack->sharedBuffer()->size()) {
                           sizeInBytes = pTrack->sharedBuffer()->size();
               }
        
//在STATIC模式下,直接把数据memcpy到共享内存,记住在这种模式下要先调用write,后调用play
               memcpy(pTrack->sharedBuffer()->pointer(),
                     data + offsetInBytes, sizeInBytes);
                 written = sizeInBytes;

                } else if (audioFormat == javaAudioTrackFields.PCM8) {
                //如果是PCM8数据,则先转换成PCM16数据再拷贝
                ......
                    }
                }
     return written;
}

3.2.4 release的分析

当数据都write完后,需要调用stop停止播放,或者直接调用release来释放相关资源。由于release和stop有一定的相关性,这里只分析release调用。

[-->android_media_AudioTrack.cpp]

static voidandroid_media_AudioTrack_native_release(JNIEnv *env,  jobject thiz) {    
    //调用android_media_AudioTrack_native_finalize真正释放资源
    android_media_AudioTrack_native_finalize(env, thiz);
    //之前保存在Java对象中的指针变量此时都要设置为零
    env->SetIntField(thiz, javaAudioTrackFields.nativeTrackInJavaObj, 0);
    env->SetIntField(thiz, javaAudioTrackFields.jniData, 0);
}
[-->android_media_AudioTrack.cpp]

static voidandroid_media_AudioTrack_native_finalize(JNIEnv *env, jobject thiz) {
   AudioTrack *lpTrack = (AudioTrack *)env->GetIntField(
                           thiz, javaAudioTrackFields.nativeTrackInJavaObj);
    if(lpTrack) {
       lpTrack->stop();//调用stop
       delete lpTrack; //调用AudioTrack的析构函数
     }
.....
}

扫尾工作也很简单,没什么需要特别注意的。

至此,在Java空间的分析工作就完成了。但在进入Native空间的分析之前,要总结一下Java空间使用Native的AudioTrack的流程,只有这样,在进行Native空间分析时才能有章可循。

3.2.5 AudioTrack (Java空间) 的分析总结

AudioTrack在JNI层使用了Native的AudioTrack对象,总结一下调用Native对象的流程:

  1. new一个AudioTrack,使用无参的构造函数;
  2. 调用set函数,把Java层的参数传进去,另外还设置了一个audiocallback回调函数;
  3. 调用了AudioTrack的start函数;
  4. 调用AudioTrack的write函数;
  5. 工作完毕后,调用stop;
  6. 最后就是Native对象的delete;

说明:为什么要总结流程呢?

  1. 控制了流程,就把握了系统工作的命脉,这一点至关重要。
  2. 有些功能的实现纵跨Java/Native层,横跨两个进程,这中间有很多封装、很多的特殊处理,但是其基本流程是不变的。通过精简流程,我们才能把注意力集中在关键点上。

4. AudioTrack (Native空间) 分析

4.1 new AudioTrack和set分析

Native的AudioTrack代码在AudioTrack.cpp中。这一节,分析它的构造函数和set调用。

[-->AudioTrack.cpp]

AudioTrack::AudioTrack()//我们使用无参构造函数
    :mStatus(NO_INIT)
{
  //把状态初始化成NO_INIT。Android的很多类都采用了这种状态控制
}

再看看set调用,这个函数有很多内容。

[-->AudioTrack.cpp]

/* 
    还记得我们传入的参数吗?
    streamType=STREAM_MUSIC,sampleRate=8000,format=PCM_16
    channels=2,frameCount由计算得来,可以假设一个值,例如1024,不影响分析。
    flags=0,cbf=audiocallback, user为cbf的参数,notificationFrames=0
    因为是流模式,所以sharedBuffer=0。threadCanCallJava 为true
*/

status_t AudioTrack::set(int streamType,uint32_t sampleRate,int format,
    int channels,int frameCount,uint32_t flags,callback_t cbf,void* user,
    int notificationFrames,const sp<IMemory>& sharedBuffer,
    boolthreadCanCallJava)
{
    //前面有一些判断,都是和AudioSystem有关的,以后再分析
    ......
  /*
    audio_io_handle_t是一个int类型,通过typedef定义,这个值的来历非常复杂,
    涉及AudioFlinger和AudioPolicyService, 后边的分析试将其解释清楚。
    这个值主要被AudioFlinger使用,用来表示内部的工作线程索引号。AudioFlinger会根据
    情况创建几个工作线程,下面的AudioSystem::getOutput会根据流类型等其他参数最终选
    取一个合适的工作线程,并返回它在AF中的索引号。
    而AudioTrack一般使用混音线程(Mixer Thread)
 */
    audio_io_handle_t output = AudioSystem::getOutput(
                         (AudioSystem::stream_type)streamType,
                                  sampleRate,format, channels,
                         (AudioSystem::output_flags)flags);
   //调用creatTrack
   status_t status = createTrack(streamType, sampleRate, format,channelCount,
                                  frameCount,flags, sharedBuffer, output);  


//cbf是JNI层传入的回调函数audioCallback,如果用户设置了回调函数,则启动一个线程
  if (cbf!= 0) {
       mAudioTrackThread = new AudioTrackThread(*this, threadCanCallJava);
   }
   return NO_ERROR;
}

再看createTrack函数:

[-->AudioTrack.cpp]

status_t AudioTrack::createTrack(intstreamType,uint32_t sampleRate,
        int format,int channelCount,int frameCount, uint32_t flags,
        const sp<IMemory>& sharedBuffer, audio_io_handle_t output)
{
    status_tstatus;
/*
    得到AudioFlinger的Binder代理端BpAudioFlinger。
    关于这部分内容,我们已经很熟悉了,以后的讲解会跨过Binder,直接分析Bn端的实现
*/
    constsp<IAudioFlinger>& audioFlinger = AudioSystem::get_audio_flinger();
/*
    向AudioFinger发送createTrack请求。注意其中的几个参数,
    在STREAM模式下sharedBuffer为空
    output为AudioSystem::getOutput得到一个值,代表AF中的线程索引号
    该函数返回IAudioTrack(实际类型是BpAudioTrack)对象,后续AF和AT的交互就是
    围绕IAudioTrack进行的
*/
    sp<IAudioTrack> track = audioFlinger->createTrack(getpid(),
        streamType,sampleRate,format,channelCount,frameCount,
            ((uint16_t)flags) << 16,sharedBuffer,output,&status);        
/*
    在STREAM模式下,没有在AT端创建共享内存,但前面提到了AT和AF的数据交互是
    通过共享内存完成的,这块共享内存最终由AF的createTrack创建。我们以后分析
    AF时再做介绍。下面这个调用会取出AF创建的共享内存
*/
    sp<IMemory> cblk = track->getCblk();
    mAudioTrack.clear();//sp的clear
    mAudioTrack= track;
    mCblkMemory.clear();
    mCblkMemory= cblk;//cblk是control block的简写
/*
    IMemory的pointer在此处将返回共享内存的首地址,类型为void*,
    static_cast直接把这个void*类型转成audio_track_cblk_t,表明这块内存的首部中存在
    audio_track_cblk_t这个对象
*/
    mCblk= static_cast<audio_track_cblk_t*>(cblk->pointer());
    mCblk->out = 1;//out为1表示输出,out为0表示输入
    mFrameCount = mCblk->frameCount;
    if(sharedBuffer == 0) {
       //buffers指向数据空间,它的起始位置是共享内存的首部加上audio_track_cblk_t的大小
       mCblk->buffers= (char*)mCblk + sizeof(audio_track_cblk_t);
  } else {
       //STATIC模式下的处理
        mCblk->buffers =sharedBuffer->pointer();
        mCblk->stepUser(mFrameCount);//更新数据位置,后面要分析stepUser的作用
    }
    return NO_ERROR;
}

4.1.1 IAudioTrack和AT、AF的关系

 由上图可以看到,IAudioTrack是联系AT和AF的关键纽带

 4.1.2 共享内存及其Control Block

通过前面的代码分析,我们发现IAudioTrack中有一块共享内存,其头部是一个audio_track_cblk_t(简称CB)对象,在该对象之后才是数据缓冲。这个CB对象有什么作用呢?

还记得前面提到的那个深层次思考的问题吗?即MemoryHeapBase和MemoryBase都没有提供同步对象,那么,AT和AF作为典型的数据生产者和消费者,如何正确协调二者生产和消费的步调呢?

Android为顺应民意,便创造出了这个CB对象,其主要目的就是协调和管理AT和AF二者数据生产和消费的步伐。先来看CB都管理些什么内容。它的声明在AudioTrackShared.h中,而定义却在AudioTrack.cpp中。
 

[-->AudioTrackShared.h::audio_track_cblk_t声明]

struct audio_track_cblk_t
{
       Mutex       lock;
       Condition   cv;//这是两个同步变量,初始化的时候会设置为支持跨进程共享
/*
   一块数据缓冲同时被生产者和消费者使用,最重要的就是维护它的读写位置了。
   下面定义的这些变量就和读写的位置有关,虽然它们的名字并不是那么直观。
   另外,这里提一个扩展问题,读者可以思考一下:
   volatile支持跨进程吗?要回答这个问题需要理解volatile、CPU Cache机制和共享内存的本质
*/
   volatile    uint32_t   user;   //当前写位置(即生产者已经写到什么位置了)
   volatile    uint32_t   server;  //当前读位置
/*
   userBase和serverBase要和user及server结合起来用。
   CB巧妙地通过上面几个变量把一块线性缓冲当做环形缓冲来使用,以后将单独分析这个问题
*/
   uint32_t    userBase;  //
   uint32_t    serverBase;
   void*       buffers; //指向数据缓冲的首地址
   uint32_t    frameCount;//数据缓冲的总大小,以Frame为单位  

   uint32_t    loopStart; //设置打点播放(即设置播放的起点和终点)
   uint32_t    loopEnd;
   int         loopCount;//循环播放的次数 

   volatile    union {
                    uint16_t    volume[2];
                    uint32_t    volumeLR;
                }; //和音量有关系,可以不管它
   uint32_t    sampleRate;//采样率
   uint32_t    frameSize;//一单位Frame的数据大小
   uint8_t     channels;//声道数
   uint8_t     flowControlFlag;//控制标志,见下文分析
   uint8_t     out; // AudioTrack为1,AudioRecord为0
   uint8_t     forceReady;
   uint16_t    bufferTimeoutMs;
   uint16_t    waitTimeMs;
   //下面这几个函数很重要,后续会详细介绍它们
   uint32_t    stepUser(uint32_tframeCount);//更新写位置
   bool        stepServer(uint32_tframeCount);//更新读位置
   void*       buffer(uint32_toffset) const;//返回可写空间起始位置
   uint32_t    framesAvailable();//还剩多少空间可写
   uint32_t    framesAvailable_l();
   uint32_t    framesReady();//是否有可读数据
}

关于CB对象,这里要专门讲解一下其中flowControlFlag的意思:

  • 对于音频输出来说,flowControlFlag对应着underrun状态,underrun状态是指生产者提供数据的速度跟不上消费者使用数据的速度。这里的消费者指的是音频输出设备。由于音频输出设备采用环形缓冲方式管理,当生产者没有及时提供新数据时,输出设备就会循环使用缓冲中的数据,这样就会听到一段重复的声音。这种现象一般被称作“machinegun”。对于这种情况,一般的处理方法是暂停输出,等数据准备好后再恢复输出。
  • 对于音频输入来说,flowControlFlag对于着overrun状态,它的意思和underrun一样,只是这里的生产者变成了音频输入设备,而消费者变成了Audio系统的AudioRecord。

说明:目前这个参数并不直接和音频输入输出设备的状态有关系。它在AT和AF中的作用必须结合具体情况,才能分析。

下图表示CB对象和它所驻留的共享内存间的关系:

注意:CB实际是按照环形缓冲来处理数据读写的,所以user和server的真实作用还需要结合userBase和serverBase。上图只是一个示意图。 

另外,关于CB,还有一个神秘的问题。先看下面这行代码:

mCblk =static_cast<audio_track_cblk_t*>(cblk->pointer());

这看起来很简单,但仔细琢磨会发现其中有一个很难解释的问题:

cblk->pointer返回的是共享内存的首地址,怎么把audio_track_cblk_t对象塞到这块内存中呢?

这个问题将通过对AudioFlinger的分析,得到答案。

AudioTrack 与 AudioFlinger 是通过进程间共享内存的方式实现的 。具体是通过 audio_track_cblk_t 这块共享内存。audio_track_cblk_t 这个结构是FIFO 实现的关键,该结构是在 createTrack 的时候,由 AudioFlinger 申请相应的内存,然后通过 IMemory 接口返回 AudioTrack 的,这样 AudioTrack 和AudioFlinger 管理着同一个 audio_track_cblk_t,通过它实现了环形 FIFO。AudioTrack 向 FIFO 中写入音频数据,AudioFlinger 从 FIFO 中读取音 频数据,经 Mixer 后送给 AudioHardware 进行播放。

 4.1.3 数据的Push or Pull

在JNI层的代码中可以发现,在构造AudioTrack时,传入了一个回调函数audioCallback。由于它的存在,导致了Native的AudioTrack还将创建另一个线程AudioTrackThread。它有什么用呢?

这个线程与外界数据的输入方式有关系,AudioTrack支持两种数据输入方式:

  • Push方式:用户主动调用write写数据,这相当于数据被push到AudioTrack。MediaPlayerService一般使用这种这方式提供数据。
  • Pull方式:AudioTrackThread将利用这个回调函数,以EVENT_MORE_DATA为参数主动从用户那pull数据。ToneGenerator使用这种方式为AudioTrack提供数据。

这两种方式都可以使用,不过回调函数除了EVENT_MORE_DATA外,还能表达其他许多意图,这是通过回调函数的第一个参数来表明的。一起来看:

[-->AudioTrack.h::event_type]

enum event_type {
        EVENT_MORE_DATA = 0, //表示AudioTrack需要更多数据
        EVENT_UNDERRUN = 1,//这是Audio的一个术语,表示Audio硬件处于低负荷状态
        //AT可以设置打点播放,即设置播放的起点和终点,LOOP_END表示已经到达播放终点
        EVENT_LOOP_END= 2,
/*
        数据使用警戒通知。该值可通过setMarkerPosition ()设置。
        当数据使用超过这个值时,AT会且仅通知一次,有点像WaterMarker。
        这里所说的数据使用,是针对消费者AF消费的数据量而言的
*/
        EVENT_MARKER = 3,
/*
        数据使用进度通知。进度通知值由setPositionUpdatePeriod()设置,
        例如每使用500帧通知一次
*/
        EVENT_NEW_POS = 4,
        EVENT_BUFFER_END = 5   //数据全部被消耗
}; 

请看AudioTrackThread的线程函数threadLoop:

[-->AudioTrack.cpp]

bool AudioTrack::AudioTrackThread::threadLoop()
{
  //mReceiver就是创建该线程的AudioTrack
  return mReceiver.processAudioBuffer(this);
}
[-->AudioTrack.cpp]

bool AudioTrack::processAudioBuffer(constsp<AudioTrackThread>& thread)
{
    BufferaudioBuffer;
    uint32_t frames;
    size_twrittenSize;

    //处理underun的情况

    if(mActive && (mCblk->framesReady() == 0)) {
        if(mCblk->flowControlFlag == 0) {
           mCbf(EVENT_UNDERRUN, mUserData, 0);//under run 通知
           if (mCblk->server == mCblk->frameCount) {
           /*
             server是读位置,frameCount是buffer中的数据总和
             当读位置等于数据总和时,表示数据都已经使用完了
           */
               mCbf(EVENT_BUFFER_END, mUserData, 0);
           }
           mCblk->flowControlFlag = 1;
           if (mSharedBuffer != 0) return false;
        }
    }
 
    // 循环播放通知
    while(mLoopCount > mCblk->loopCount) {
        int loopCount = -1;
        mLoopCount--;
        if(mLoopCount >= 0) loopCount = mLoopCount;
        //一次循环播放完毕,loopCount表示还剩多少次
        mCbf(EVENT_LOOP_END, mUserData, (void *)&loopCount);
    }

     if(!mMarkerReached && (mMarkerPosition > 0)) {
        if(mCblk->server >= mMarkerPosition) {
           //如果数据使用超过警戒值,则通知用户
           mCbf(EVENT_MARKER, mUserData, (void *)&mMarkerPosition);
           //只通知一次,因为该值被设为true
           mMarkerReached = true;
        }
    }


    if(mUpdatePeriod > 0) {
       while (mCblk->server >= mNewPosition) {
       /*
          进度通知,但它不是以时间为基准,而是以帧数为基准的。
          例如设置每500帧通知一次,假设消费者一次就读了1500帧,那么这个循环会连续通知3次
       */
           mCbf(EVENT_NEW_POS, mUserData, (void *)&mNewPosition);
           mNewPosition += mUpdatePeriod;
        }
    }
 

    if(mSharedBuffer != 0) {
       frames = 0;
    } else {
       frames = mRemainingFrames;
    }

    do {
       audioBuffer.frameCount = frames;
       //得到一块可写的缓冲
       status_t err = obtainBuffer(&audioBuffer, 1);
       ......      

       //从用户那pull数据
       mCbf(EVENT_MORE_DATA, mUserData, &audioBuffer);
       writtenSize = audioBuffer.size;

       ......
       if(writtenSize > reqSize) writtenSize = reqSize;
       //PCM8数据转PCM16
       ...... 

       audioBuffer.size = writtenSize;
       audioBuffer.frameCount = writtenSize/mCblk->frameSize; 

       frames -= audioBuffer.frameCount;

       releaseBuffer(&audioBuffer);//写完毕,释放这块缓冲
    }
    while(frames);
    ......
    return true;
}

关于obtainBuffer和releaseBuffer,后面再分析。这里有一个问题值得思考:

用例会调用write函数写数据,AudioTrackThread的回调函数也让我们提供数据。难道我们同时在使用Push和Pull模式?

这太奇怪了!来查看这个回调函数的实现,了解一下究竟是怎么回事。该回调函数是通过set调用传入的,对应的函数是audioCallback。

[-->android_media_AudioTrack.cpp]

static void audioCallback(int event, void* user,void *info) {
    if(event == AudioTrack::EVENT_MORE_DATA) {
         //很好,没有提供数据,也就是说,虽然AudioTrackThread通知了EVENT_MORE_DATA,
         //但是我们并没有提供数据给它
         AudioTrack::Buffer* pBuff = (AudioTrack::Buffer*)info;
         pBuff->size = 0;  
     }
    ......

悬着的心终于放下来了,还是老老实实地看Push模式下的数据输入吧。

4.2 write输入数据

write函数涉及Audio系统中最重要的关于数据如何传输的问题,在分析它的时候,不妨先思考一下它会怎么做。回顾一下我们已了解的信息:

  • 有一块共享内存。
  • 有一个控制结构,里边有一些支持跨进程的同步变量。

有了这些东西,write的工作方式就非常简单了:

  • 通过共享内存传递数据。
  • 通过控制结构协调生产者和消费者的步调。

好了,现在开始分析write,看看它的实现是不是如所想的那样。

[-->AudioTrack.cpp]

ssize_t AudioTrack::write(const void* buffer,size_t userSize)
{
  if(mSharedBuffer != 0) return INVALID_OPERATION;
    if(ssize_t(userSize) < 0) {
       returnBAD_VALUE;
    }
    ssize_t written = 0;
    constint8_t *src = (const int8_t *)buffer;
    BufferaudioBuffer; // Buffer是一个辅助性的结构

 do {
        //以帧为单位
        audioBuffer.frameCount = userSize/frameSize();
        //obtainBuffer从共享内存中得到一块空闲的数据块
        status_terr = obtainBuffer(&audioBuffer, -1);
        ......

        size_t toWrite;

        if(mFormat == AudioSystem::PCM_8_BIT &&
                          !(mFlags &AudioSystem::OUTPUT_FLAG_DIRECT)) {
             //PCM8数据转PCM16
        }else {
           //空闲数据缓冲的大小是audioBuffer.size。
           //地址在audioBuffer.i8中,数据传递通过memcpy完成
           toWrite = audioBuffer.size;
           memcpy(audioBuffer.i8, src, toWrite);
           src += toWrite;
        }
        userSize -= toWrite;
        written += toWrite;
        //releaseBuffer更新写位置,同时会触发消费者
        releaseBuffer(&audioBuffer);
    }while (userSize);

    return written;
}

通过write函数,会发现数据的传递其实是很简单的memcpy,但消费者和生产者的协调,则是通过obtainBuffer与releaseBuffer来完成的。现在来看这两个函数。

4.3 obtainBuffer和releaseBuffer

这两个函数展示了做为生产者的AT和CB对象的交互方法。先简单看看,然后把它们之间交互的流程记录下来,以后在CB对象的单独分析部分,我们再来做详细介绍。

[-->AudioTrack.cpp]

status_t AudioTrack::obtainBuffer(Buffer*audioBuffer, int32_t waitCount)
{
    intactive;
    status_t result;
    audio_track_cblk_t* cblk = mCblk;
    ......
  //1.调用framesAvailable,得到当前可写的空间大小
    uint32_t framesAvail = cblk->framesAvailable();

    if(framesAvail == 0) {
        ......
      //如果没有可写空间,则要等待一段时间
        result= cblk->cv.waitRelative(cblk->lock,milliseconds(waitTimeMs));
        ......
   }

   cblk->waitTimeMs = 0;

   if(framesReq > framesAvail) {
       framesReq = framesAvail;
   }

 //user为可写空间起始地址
   uint32_t u = cblk->user;
   uint32_tbufferEnd = cblk->userBase + cblk->frameCount;

   if (u+ framesReq > bufferEnd) {
       framesReq = bufferEnd - u;
   }
 
   ......
 //2.调用buffer,得到可写空间的首地址
   audioBuffer->raw = (int8_t *)cblk->buffer(u);
   active= mActive;
   return active ? status_t(NO_ERROR) : status_t(STOPPED);
}

obtainBuffer的功能,就是从CB管理的数据缓冲中得到一块可写空间,而releaseBuffer,则是在使用完这块空间后更新写指针的位置。

[-->AudioTrack.cpp]

void AudioTrack::releaseBuffer(Buffer*audioBuffer)
{
   audio_track_cblk_t* cblk = mCblk;
 //3.调用stepUser更新写位置
   cblk->stepUser(audioBuffer->frameCount);
}

obtainBuffer和releaseBuffer与CB交互,一共会有三个函数调用,如下所示:

  • framesAvailable判断是否有可写空间。
  • buffer得到写空间起始地址。
  • stepUser更新写位置。

请记住这些流程,以后在分析CB时会发现它们有重要作用。

4.4 delete AudioTrack

到这里,AudioTrack的使命就进入倒计时阶段了。来看在它生命的最后还会做一些什么工作。

[-->AudioTrack.cpp]

AudioTrack::~AudioTrack()
{
    if(mStatus == NO_ERROR) {
        stop();//调用stop
        if(mAudioTrackThread != 0) {
           //通知AudioTrackThread退出
           mAudioTrackThread->requestExitAndWait();
           mAudioTrackThread.clear();
        }
        mAudioTrack.clear();
      //将残留在IPCThreadState 发送缓冲区的信息发送出去
        IPCThreadState::self()->flushCommands();
    }
}

如果不调用stop,析构函数也会先调用stop,这个做法很周到。

[-->AudioTrack.cpp]

void AudioTrack::stop()
{
    sp<AudioTrackThread> t = mAudioTrackThread;
    if (t!= 0) {
       t->mLock.lock();
    }

    if(android_atomic_and(~1, &mActive) == 1) {
       mCblk->cv.signal();

     //mAudioTrack是IAudioTrack类型,其stop的最终处理在AudioFlinger端
       mAudioTrack->stop();
     //清空循环播放设置
       setLoop(0, 0, 0);
       mMarkerReached = false;
       
       if (mSharedBuffer != 0) {
           flush();
       }
       if(t != 0) {
           t->requestExit();//请求退出AudioTrackThread
       }else {
           setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_NORMAL);
        }
    }

    if (t!= 0) {
        t->mLock.unlock();
    }
}

stop的工作比较简单,就是调用IAudioTrack的stop,并且还要求退出回调线程。要重点关注IAudioTrack的stop函数,这个将做为AT和AF交互流程中的一个步骤来分析。

5. AudioTrack的总结

AudioTrack就这样完了吗?它似乎也不是很复杂。其实,在进行AT分析时,对于一些难度比较大的地方暂时没做介绍。不过,在将AudioFlinger分析完之后,肯定不会怕它们的。

OK,在完成对AudioTrack的分析之前,应把它和AudioFlinger交互的流程总结下,如下图所示。这些流程是以后攻克AudioFlinger的重要武器。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值