Android音视频开发入门(三)

Android音视频开发入门(三)

      • 任务目标
      • AudioRecord采集PCM数据
        • 1.实现一个AudioRecord实例
        • 2.初始化一个Buffer
        • 3.调用startRecording()
        • 4.写数据
        • 5.停止采集并关闭数据流
      • AudioTrack播放PCM数据
        • 1.创建AudioTrack实例
        • 2.创建一个Buffer
        • 3.调用play()
        • 4.读写数据
        • 5.停止播放,关闭流
      • PCM转WAV文件
        • WAV
      • 参考资料

任务目标

用AudioRecord采集PCM数据到SDCard,并用AudioTrack播放采集的PCM数据,最后实现读写wav文件。

AudioRecord采集PCM数据

AudioRecord可以记录从硬件设备输入的音频,生成PCM格式的音频数据。有三个读取数据的方法可以选择read(byte[], int, int), read(short[], int, int) 或 read(ByteBuffer, int),用户可以根据最方便的存储格式来选择使用那个方法。

1.实现一个AudioRecord实例

看一下AudioRecord的构造方法
public AudioRecord (int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
API介绍上有这么一段
Class constructor. Though some invalid parameters will result in an IllegalArgumentException exception, other errors do not. Thus you should call getState() immediately after construction to confirm that the object is usable.

类构造函数。一些无效的参数会导致IllegalArgumentException异常,但是不会返回其他错误。因此你应该在AudioRecord构造函数之后立马调用getState()方法来确定AudioRecord对象是否可用。
我看好多人都没有调用这个函数,不过也没有太大影响。下面分析一下构造函数的参数

- audioSource: 音源,常用麦克风MediaRecorder.AudioSource.MIC,也可以是通话的话音( MediaRecorder.AudioSource.VOICE_CALL,MediaRecorder.AudioSource.VOICE_DOWNLINK即对方声音,MediaRecorder.AudioSource.VOICE_UPLINK即本方声音 )
- sampleRateInHz: 采样率,单位是Hz(赫兹)。如8000,16000,11025,22050,44100等,这儿选择44100是目前所有的设备都支持的采样率。
-channelConfig: 声道数,分为单声道(AudioFormat.CHANNEL_IN_MONO)和立体声(AudioFormat.CHANNEL_STEREO)。因为有的设备不支持立体声,这儿我们选择单声道。
-audioFormat: 音频格式,AudioFormat.ENCODING_PCM_16BIT、AudioFormat.ENCODING_PCM_8BIT和AudioFormat.ENCODING_PCM_FLOAT可选,同样为了设备支持直接选择ENCODING_PCM_16BIT.
-bufferSizeInBytes: 最小缓冲区大小,可通过getMinBufferSize()方法确定。设置的值比getMinBufferSize()还小则会导致初始化失败。注意: 这个大小并不保证在负荷下的流畅录制,应根据预期的频率来选择更高的值,AudioRecord实例在推送新数据时使用此值。

2.初始化一个Buffer

该buffer大小要大于等于AudioRecord读取数据的buffer大小

3.调用startRecording()

startRecording()之后就可以不断调用 read函数取得声音数据。这个函数是阻塞试的,下层没有足够的数据会停在它里面。一般来说,在一个独立线程里处理录音的数据采集比较好。

4.写数据

创建一个数据流,一边从AudioRecord读取声音数据到初始化的buffer,一边将buffer中的数据导入数据流

5.停止采集并关闭数据流

以上就是采集PCM数据的步骤,不要忘记添加权限,写数据需要读写权限,用麦克风录音需要麦克风权限




我之前就是忘记请求麦克风权限导致getState()获取的状态总是0(未初始化成功状态)。下面是部分代码

	/**
     * 采集PCM数据线程
     */
    private class RecordThread extends AsyncTask {

        @Override
        protected Void doInBackground(Void... voids) {
            //返回成功创建AudioRecord对象所需要的最小缓冲区大小
            //注意:这个大小并不保证在负荷下的流畅录制,应根据预期的频率来选择更高的值,AudioRecord实例在推送新数据时使用此值。
            int mMinBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
            //创建AudioRecord
            mRecord = new AudioRecord(MediaRecorder.AudioSource.MIC/*用麦克风采集*/, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, mMinBufferSize);
            //API提示创建AudioRecord后立马调用getState获取其可用状态
            int state = mRecord.getState();
            Log.w("record state", "state =" + state);
            //这儿为了方便看到采集的pcm数据直接写入到sd卡中
            File file = new File(Environment.getExternalStorageDirectory().getPath(), "record.pcm");
            if (file.exists()) {
                boolean isDelete = file.delete();
                Log.w("file delete", "isDelete = " + isDelete);
            }
            try {
                BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file));
                outputStream = new DataOutputStream(bufferedOutputStream);
                byte[] buffer = new byte[mMinBufferSize];
                mRecord.startRecording();
                isRecording = true;
                while (isRecording) {
                    int read = mRecord.read(buffer, 0, buffer.length);
                    //如果读取音频数据没有出现错误,则将数据写入到文件
                    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                        outputStream.write(read);
                        outputStream.flush();//这一句代码一定要添加,之前参考网上的demo都不可以播放
                    }
                }
                isRecording = false;
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mRecordStatus.setText(getString(R.string.stop_recording));
                    }
                });
                mRecord.stop();
                mRecord.release();
                mRecord = null;
                outputStream.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }

AudioTrack播放PCM数据

AudioTrack类为Java应用程序管理和播放单一的音频资源。它允许将PCM音频缓冲器流式传输到音频接收器以进行播放。这是通过使用write(byte [],int,int),write(short [],int,int)和write(float [],int,int)之一将数据“推送”到AudioTrack对象来实现的。
AudioTrack可以在两种模式下运行:static or streaming
在streaming模式下,应用程序通过其中的一个write()方法将连续的流数据写入到AudioTrack,当数据从Java层传输到Native层并排队等待播放时,他们会阻塞并返回。在播放音频数据块时,流模式非常有用。如:

  • 由于音频播放时间太长不能写入到内存中
  • 由于音频数据的特性而不能写入到内存中(高采样率,每个样本的位数…)
  • 在先前排队的音频正在播放时接收或生成

在处理能够装入内存的短音时,应选择静态模式,并且需要尽可能以最小的延迟播放。因此,对于经常播放的UI和游戏声音而言,静态模式将是优选的,并且可能具有最小的开销。

1.创建AudioTrack实例

由于public AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)在API26以后不推荐使用了,所以这儿只介绍下面的构造函数
public AudioTrack (AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes, int mode, int sessionId)
-attributes: 一个非空的AudioAttributes实例,看一下官方给的示例

AudioTrack myTrack = new AudioTrack(
         new AudioAttributes.Builder()
             .setUsage(AudioAttributes.USAGE_MEDIA)
             .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
             .build(),
         myFormat, myBuffSize, AudioTrack.MODE_STREAM, mySession);

-format: 一个非空的AudioFormat实例,描述了通过AudioTrack播放的数据格式。可以通过AudioFormat配置音频格式参数,如编码,通道掩码和采样率等。
-bufferSizeInBytes: 内部缓冲区的总大小(以字节为单位),其中读取音频数据以进行播放。这应该是帧大小的非零倍数(以字节为单位)。
如果音轨的创建模式为MODE_STATIC,那么这就是实例可以播放的最大长度样本或音频剪辑。
如果音轨的创建模式为MODE_STREAM,则是AudioTrack满足应用程序延迟要求所需的缓冲区大小。如果bufferSizeInBytes小于输出接收器的最小缓冲区大小,则将其增加到最小缓冲区大小。方法getBufferSizeInFrames()返回创建的缓冲区的帧的实际大小,这决定了写入流式AudioTrack的最小频率以避免欠载。
-mode: streaming or static buffer.MODE_STATIC or MODE_STREAM
-sessionId: AudioTrack必须依附的音频会话ID。如果会话在构造时未知,则为AudioManager.AUDIO_SESSION_ID_GENERATE

2.创建一个Buffer

具体已在bufferSizeInBytes说明

3.调用play()

调用此函数后就可以读写数据了,同AudioRecord一样write()方法也是阻塞的

4.读写数据

同AudioRecord一边把数据读到缓冲区,一边把缓冲区的数据导入数据流

5.停止播放,关闭流

以上就是播放PCM数据的基本步骤,下面是有两份关键代码推荐第二份

	/**
     * 播放PCM数据
     */
    private class PlayThread extends AsyncTask {

        @Override
        protected Void doInBackground(Void... voids) {
            File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "record.wav");
            if (!file.exists()) {
                Log.w("file state", "file not found");
                return null;
            }
            int channelMask = AudioFormat.CHANNEL_OUT_MONO;
            //创建AudioTrack
            AudioTrack mTrack = new AudioTrack(
                    new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_MEDIA)
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build(),
                    new AudioFormat.Builder()
                            .setSampleRate(SAMPLE_RATE)
                            .setEncoding(AUDIO_FORMAT)
                            .setChannelMask(channelMask)
                            .build(), (int) file.length(), AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
            int state = mTrack.getState();
            Log.w("state", "state=" + state);
            try {
                DataInputStream inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
                short[] buffer = new short[(int) (file.length() / 2)];
                mTrack.play();
                isPlaying = true;
                int read = 0;
                while (isPlaying && inputStream.available() > 0) {
                    buffer[read] = inputStream.readShort();
                    read++;
                }
                mTrack.write(buffer, 0, buffer.length);
                Log.w("play state", "complete");
                isPlaying = false;
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mPlayStatus.setText(getString(R.string.stop_play));
                    }
                });
                mTrack.stop();
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }

这份代码是参考别人的代码写的,之前用的byte[]都是噪音,至今不知什么原因,希望有同样问题的解决后告知一下,还有一个问题是数组分配空间的问题,之前用AudioTrack.getMinBufferSize()方法获取的值总是报数组越界。之后参考Android音视频开发初探之AudioRecord与AudioTrack完成音频采集与播放的代码修改为下面的实现,非常感谢,注意的问题都在注释中写了。

	/**
     * 播放音频线程
     */
    private class AudioTrackPlayThread extends Thread {
        AudioTrack mAudioTrack;
        int minBufferSize = 10240;
        File autoFile; //要播放的文件

        //        @RequiresApi(api = Build.VERSION_CODES.M)
        AudioTrackPlayThread(File file) {
            setPriority(MAX_PRIORITY);
            autoFile = file;
            //播放缓冲的最小大小
            minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AUDIO_FORMAT);
            // 创建用于播放的 AudioTrack
            mAudioTrack = new AudioTrack(
                    new AudioAttributes.Builder()
                            //我发现这个参数设置会影响播放,如果不行就多试几个,我在这儿花了好多时间
                            .setUsage(AudioAttributes.USAGE_ALARM)
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build(),
                    new AudioFormat.Builder()
                            .setEncoding(AUDIO_FORMAT)
                            .setSampleRate(SAMPLE_RATE)
                            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                            .build(), minBufferSize, AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);

        }

        @Override
        public void run() {

            Log.d("audio", "播放开始");
            try {
                FileInputStream fis = new FileInputStream(autoFile);
                mAudioTrack.play();
                isPlaying = true;
                byte[] bytes = new byte[minBufferSize];

                while (isPlaying) {
                    int read = fis.read(bytes);
                    //若读取有错则跳过
                    if (AudioTrack.ERROR_INVALID_OPERATION == read
                            || AudioTrack.ERROR_BAD_VALUE == read) {
                        continue;
                    }

                    if (read != 0 && read != -1) {
                        mAudioTrack.write(bytes, 0, minBufferSize);
                    }
                    if (read == -1) {
                        isPlaying = false;
                    }
                }
                mAudioTrack.stop();
                mAudioTrack.release();//释放资源
                fis.close();//关流

            } catch (Exception e) {
                e.printStackTrace();
            }

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mPlayStatus.setText(getString(R.string.stop_play));
                }
            });
            Log.d("audio", "播放停止");
        }
    }

个人认为AudioRecord比较简单,AudioTrack坑稍多,虽然API都差不多,学习的时候还需要注意。

PCM转WAV文件

PCM数据转WAV关键是弄明白wav文件的头信息,只要了解头信息后做起来就比较容易了。

WAV

WAV为微软公司(Microsoft)开发的一种声音文件格式,它符合RIFF(Resource Interchange File Format)文件规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持,该格式也支持MSADPCM,CCITT A LAW等多种压缩运算法,支持多种音频数字,取样频率和声道,标准格式化的WAV文件和CD格式一样,也是44.1K的取样频率,16位量化数字,因此在声音文件质量和CD相差无几!
通常使用三个参数来表示声音,量化位数,取样频率和采样点振幅。量化位数分为8位,16位,24位三种,声道有单声道和立体声之分,单声道振幅数据为n1矩阵点,立体声为n2矩阵点,取样频率一般有11025Hz(11kHz) ,22050Hz(22kHz)和44100Hz(44kHz) 三种,不过尽管音质出色,但在压缩后的文件体积过大!相对其他音频格式而言是一个缺点,其文件大小的计算方式为:WAV格式文件所占容量(B) = (取样频率 X量化位数X 声道) X 时间 / 8 (字节= 8bit) 每一分钟WAV格式的音频文件的大小为10MB,其大小不随音量大小及清晰度的变化而变化。
WAV是最接近无损的音乐格式,所以文件大小相对也比较大。
以上是百度百科对wav的介绍,下面看一下wav的头文件信息
Android音视频开发入门(三)_第1张图片

偏移 命名 说明
00-03 ChunkId 大写字符串"RIFF",标明该文件为有效的 RIFF 格式文档
04-07 ChunkSize 从下一个字段首地址开始到文件末尾的总字节数。该字段的数值加 8 为当前文件的实际长度
08-11 fccType 所有 WAV 格式的文件此处为字符串"WAVE",标明该文件是 WAV 格式文件
12-15 SubChunkId1 "fmt"标志,最后一位空格
16-19 SubChunkSize1 其数值不确定,取决于编码格式。可以是 16、 18 、20、40 等,一般取16
20-21 AudioFormat 内容为一个短整数,表示格式种类(值为1时,表示数据为线性PCM编码)
22-23 NumChannels 声道数,单声道为1,双声道为2
24-27 SampleRate 每个声道单位时间采样次数。常用的采样频率有 11025, 22050 和 44100 kHz
28-31 ByteRate 码率:声道数×采样频率×每样本的数据位数/8。播放软件利用此值可以估计缓冲区的大小
32-33 BlockAlign 采样帧大小。该数值为:声道数×位数/8。播放软件需要一次处理多个该值大小的字节数据,用该数值调整缓冲区
34-35 BitsPerSample 存储每个采样值所用的二进制数位数。常见的位数有 4、8、12、16、24、32
36-39 Subchunk2ID 数据标记符"data"
40-43 Subchunk2Size 语音数据的长度
44 data 音频数据

了解了wav的头文件信息后,我们只需要知道PCM数据的采样率、通道数及音频格式后就可以通过给PCM数据加一个wav的文件头把pcm数据转为wav格式的数据了,我觉得这个格式比较固定没必要非得记住,需要的时候查资料就好。下面给出整理后的工具类。

public class PcmToWavUtil {

    /**
     * 缓存的音频大小
     */
    private int mBufferSize;
    /**
     * 采样率
     */
    private int mSampleRate;
    /**
     * 声道数
     */
    private int mChannel;

    /**
     * @param sampleRate  采样率
     * @param channel     通道数
     * @param audioFormat 音频格式
     */
    public PcmToWavUtil(int sampleRate, int channel, int audioFormat) {
        this.mSampleRate = sampleRate;
        this.mChannel = channel;
        this.mBufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat);
    }

    public void pcmToWav(String inputFilePath, String outFilePath) {
        FileInputStream inputStream;
        FileOutputStream outputStream;
        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = mSampleRate;
        int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
        long byteRates = 16 * mSampleRate * channels / 8;
        byte[] buffer = new byte[mBufferSize];
        try {
            inputStream = new FileInputStream(inputFilePath);
            outputStream = new FileOutputStream(outFilePath);
            totalAudioLen = inputStream.getChannel().size();
            totalDataLen = totalAudioLen + 36;

            writeWavFileHeader(outputStream, totalAudioLen, totalDataLen, longSampleRate, channels, byteRates);
            while (inputStream.read(buffer) != -1) {
                outputStream.write(buffer);
            }
            Log.w("pcm to wav", "完成");
            inputStream.close();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 写wav头信息
     *
     * @param outputStream   输出流
     * @param totalAudioLen  全部音频长度
     * @param totalDataLen   全部数据长度
     * @param longSampleRate 采样率
     * @param channels       通道数
     * @param byteRates      音频传输速率
     */
    private void writeWavFileHeader(FileOutputStream outputStream, long totalAudioLen, long totalDataLen,
                                    long longSampleRate, int channels, long byteRates) {
        byte[] header = new byte[44];
        //RIFF
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) (totalDataLen >> 8 & 0xff);
        header[6] = (byte) (totalDataLen >> 16 & 0xff);
        header[7] = (byte) (totalDataLen >> 24 & 0xff);
        //WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//过度字节
        //4 bytes : size of fmt chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //编码方式 10H为PCM编码方式
        header[20] = 1;
        header[21] = 0;
        //通道数
        header[22] = (byte) channels;
        header[23] = 0;
        //采样率,每个通道的播放速度
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) (longSampleRate >> 8 & 0xff);
        header[26] = (byte) (longSampleRate >> 16 & 0xff);
        header[27] = (byte) (longSampleRate >> 24 & 0xff);
        //音频数据传输速率,采样率×通道数×采样深度/8
        header[28] = (byte) (byteRates & 0xff);
        header[29] = (byte) (byteRates >> 8 & 0xff);
        header[30] = (byte) (byteRates >> 16 & 0xff);
        header[31] = (byte) (byteRates >> 24 & 0xff);
        //确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数×采样位数
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        //每个样本的数据位数
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) (totalAudioLen >> 8 & 0xff);
        header[42] = (byte) (totalAudioLen >> 16 & 0xff);
        header[43] = (byte) (totalAudioLen >> 24 & 0xff);
        try {
            outputStream.write(header, 0, header.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

至此,PCM转wav也完成了,可以用之前的AudioTrack播放测试。最后感谢所有资料的分享者。
代码已上传到Github

参考资料

Android音视频开发初探之AudioRecord与AudioTrack完成音频采集与播放
Android音视频开发-入门(二)
音频PCM数据的采集和播放
AudioRecord官方文档
AudioTrack官方文档

你可能感兴趣的:(Android音视频入门)