android开发实现语音数据实时采集/播放

Posted LanTingShuXu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android开发实现语音数据实时采集/播放相关的知识,希望对你有一定的参考价值。

最近做的项目是和语音实时采集并发送,对方实时接收并播放相关,下面记录下实现的核心代码。
很多android开发者应该知道android有个MediaRecorder对象和MediaPlayer对象,用于录制和播放音频。这个弊端在于他们不能实时采集并发送出去,所以,我们只能使用AudioRecord和AudioTrack来实现。
记得申明权限:

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

一、AudioRecord实现核心代码介绍如下:
1、先申明相关录制配置参数

private AudioRecord audioRecord;// 录音对象
private int frequence = 8000;// 采样率 8000
private int channelInConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;// 定义采样通道
private int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;// 定义音频编码(16位)
private byte[] buffer = null;// 录制的缓冲数组

2、在开始录制前,我们需要初始化AudioRecord类。

// 根据定义好的几个配置,来获取合适的缓冲大小
// int bufferSize = 800;
int bufferSize = AudioRecord.getMinBufferSize(frequence,
        channelInConfig, audioEncoding);
// 实例化AudioRecord
audioRecord = new AudioRecord(MediaRecorder.Audiosource.MIC,
        frequence, channelInConfig, audioEncoding, bufferSize);
// 定义缓冲数组
buffer = new byte[bufferSize];

3、准备开始录制,使用循环不断读取数据。

audioRecord.startRecording();// 开始录制
isRecording = true;// 设置录制标记为true

// 开始录制
while (isRecording) {
// 录制的内容放置到了buffer中,result代表存储长度
int result = audioRecord.read(buffer, 0, buffer.length);
/*.....result为buffer中录制数据的长度(貌似基本上都是640)。
剩下就是处理buffer了,是发送出去还是直接播放,这个随便你。*/
}

//录制循环结束后,记得关闭录制!!
if (audioRecord != null) {
    audioRecord.stop();
}

二、AudioTrack代码实现介绍如下:
1、声明播放相关配置。

private AudioTrack track = null;// 录音文件播放对象
private int frequence = 8000;// 采样率 8000
private int channelInConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;// 定义采样通道
private int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;// 定义音频编码(16位)
private int bufferSize = -1;// 播放缓冲大小

2、初始化AudioTrack对象(初始化一次,该对象可重复使用)

// 获取缓冲 大小
bufferSize = AudioTrack.getMinBufferSize(frequence, channelInConfig,
        audioEncoding);
// 实例AudioTrack
track = new AudioTrack(AudioManager.STREAM_MUSIC, frequence,
        channelInConfig, audioEncoding, bufferSize,
        AudioTrack.MODE_STREAM);

3、使用AudioTrack播放语音数据。

//将语音数据写入即可。
track.write(dataArray, buffer, len);

问题一:
由于目前的项目是实时采集,实时发送,所以需要考虑到包的大小,经测试,我们使用160个byte作为一个包传递可以做到比较良好的播放效果(也就是将一份buffer拆分成四个发送)。处理代码如下:

// 将数据通过监听接口回调出去
if (audioRecordingCallback != null) {
    int offset = result % MAX_DATA_LENGTH > 0 ? 1 : 0;
    //将一个buffer拆分成几份小数据包 MAX_DATA_LENGTH 为包的最大byte数
    for (int i = 0; i < result / MAX_DATA_LENGTH + offset; i++) {
        int length = MAX_DATA_LENGTH;
        if ((i + 1) * MAX_DATA_LENGTH > result) {
            length = result - i * MAX_DATA_LENGTH;
        }
    //写到回调接口
    audioRecordingCallback.onRecording(buffer, i
            * MAX_DATA_LENGTH, length);
    }
}

问题二:
有时候传输的过来播放声音会一卡一卡的,为了解决这样的问题,暂时使用了语音双缓冲机制来解决,问题优化很明显。代码和示意图如下:
双缓冲示意图

【有朋友说要源码,那我就贴下】


【声音采集的源码】

/**
 * 实时音频录制处理类<br/>
 * 记得申明系统权限:MODIFY_AUDIO_SETTINGS、RECORD_AUDIO<br/>
 * 使用实例代码:<br/>
 * 
 * <pre>
 * audioRecoderHandler = new AudioRecoderHandler(this);
 * audioRecoderHandler.startRecord(new AudioRecordingCallback() {
 *  &#064;Override
 *  public void onStopRecord(String savedPath) {
 * 
 *  }
 * 
 *  &#064;Override
 *  public void onRecording(byte[] data, int startIndex, int length) {
 *      // TODO 录制监听。处理data即可。立即播放or发送出去,随你。
 *  }
 * });
 * </pre>
 * 
 * @author 李长军
 * 
 */
@SuppressWarnings("deprecation")
public class AudioRecoderHandler {

    /**
     * 录音数据单次回调数组最大为多少
     */
    private static int MAX_DATA_LENGTH = 160;

    private AudioRecord audioRecord;// 录音对象
    private boolean isRecording = false;// 标记是否正在录音中
    private int frequence = 8000;// 采样率 8000
    private int channelInConfig = AudioFormat.CHANNEL_CONFIGURATION_MONO;// 定义采样通道(过时,但是使用其他的又不行
    private int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;// 定义音频编码(16位)
    private byte[] buffer = null;// 录制的缓冲数组
    private File lastCacheFile = null;// 记录上次录制的文件名

    public CacheCleanManager cacheManager;

    public AudioRecoderHandler(Context context) {
        if (context == null) {
            throw new RuntimeException("Context could not be null!");
        }
    }

    /**
     * 开始录制音频
     * 
     * @param callBackListener
     *            录制过程中的回调函数
     */
    public void startRecord(AudioRecordingCallback audioRecordingCallback) {
        RecordTask task = new RecordTask(audioRecordingCallback);
        task.execute();// 开始执行
    }

    /**
     * 停止录制
     */
    public void stoppRecord() {
        isRecording = false;
    }

    /**
     * 删除上次录制的文件(一般是用户取消发送导致删除上次录制的内容)
     * 
     * @return true表示删除成功,false表示删除失败,一般是没有上次录制的文件,或者文件已经被删除了
     */
    public boolean deleteLastRecordFile() {
        boolean success = false;
        if (lastCacheFile != null && lastCacheFile.exists()) {
            success = lastCacheFile.delete();
        }
        return success;
    }

    /**
     * 录制音频的任务类
     * 
     * @author 李长军
     * 
     */
    private class RecordTask extends AsyncTask<String, Integer, String> {

        private AudioRecordingCallback audioRecordingCallback = null;

        public RecordTask(AudioRecordingCallback audioRecordingCallback) {
            this.audioRecordingCallback = audioRecordingCallback;
        }

        @Override
        protected void onPreExecute() {
            // 根据定义好的几个配置,来获取合适的缓冲大小
            // int bufferSize = 800;
            int bufferSize = AudioRecord.getMinBufferSize(frequence,
                    channelInConfig, audioEncoding);
            // 实例化AudioRecord
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    frequence, channelInConfig, audioEncoding, bufferSize);
            // 定义缓冲数组
            buffer = new byte[bufferSize];
            MAX_DATA_LENGTH = bufferSize / 2;
            audioRecord.startRecording();// 开始录制
            isRecording = true;// 设置录制标记为true
        }

        @Override
        protected void onPostExecute(String result) {
            audioRecord = null;
            if (result == null) {
                lastCacheFile = null;
            } else {
                lastCacheFile = new File(result);
            }
            if (audioRecordingCallback != null) {
                audioRecordingCallback.onStopRecord(result);
            }
        }

        @Override
        protected String doInBackground(String... params) {
            String tempFileName = null;
            // 开始录制
            while (isRecording) {
                // 录制的内容放置到了buffer中,result代表存储长度
                int result = audioRecord.read(buffer, 0, buffer.length);
                // 将数据回调出去
                if (audioRecordingCallback != null) {
                    int offset = result % MAX_DATA_LENGTH > 0 ? 1 : 0;
                    for (int i = 0; i < result / MAX_DATA_LENGTH + offset; i++) {
                        int length = MAX_DATA_LENGTH;
                        if ((i + 1) * MAX_DATA_LENGTH > result) {
                            length = result - i * MAX_DATA_LENGTH;
                        }
                        audioRecordingCallback.onRecording(buffer, i
                                * MAX_DATA_LENGTH, length);
                    }
                }
            }
            if (audioRecord != null) {
                audioRecord.stop();
            }
            return tempFileName;
        }
    }

    /**
     * 监听录制过程,用于实时获取录音数据
     * 
     * @author 李长军
     * 
     */
    public static interface AudioRecordingCallback {
        /**
         * 录音数据获取回调
         * 
         * @param data
         *            数据数组对象
         * @param startIndex
         *            数据其开始
         * @param length
         *            数据的结尾
         */
        public void onRecording(byte[] data, int startIndex, int length);

        /**
         * 录音结束后的回调
         * 
         * @param savedPath
         *            录音文件存储的路径
         */
        public void onStopRecord(String savedPath);
    }

    /**
     * 释放资源
     */
    public void release() {
        if (audioRecord != null) {
            audioRecord.release();
            audioRecord = null;
        }

    }

}

【声音播放的源码】


/**
 * 实时音频播放处理类<br/>
 * 使用示例代码如下:<br/>
 * 
 * <pre>
 * audioPlayerHandler = new AudioPlayerHandler();
 * audioPlayerHandler.prepare();// 播放前需要prepare。可以重复prepare
 * // 直接将需要播放的数据传入即可
 * audioPlayerHandler.onPlaying(data, 0, data.length);
 * </pre>
 * 
 * @author 李长军
 * 
 */
@SuppressWarnings("deprecation")
public class AudioPlayerHandler implements Runnable {

    private AudioTrack track = null;// 录音文件播放对象
    private boolean isPlaying = false;// 标记是否正在录音中
    private int frequence = 8000;// 采样率 8000
    private int channelInConfig = AudioFormat.CHANNEL_OUT_MONO;// 定义采样为双声道(过时,但是使用其他的又不行
    private int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;// 定义音频编码(16位)
    private int bufferSize = -1;// 播放缓冲大小
    private LinkedBlockingDeque<Object> dataQueue = new LinkedBlockingDeque<>();
    // 互斥信号量
    private Semaphore semaphore = new Semaphore(1);
    // 是否释放资源的标志位
    private boolean release = false;

    public AudioPlayerHandler() {
        // 获取缓冲 大小
        bufferSize = AudioTrack.getMinBufferSize(frequence, channelInConfig,
                audioEncoding);

        // 实例AudioTrack
        track = new AudioTrack(AudioManager.STREAM_MUSIC, frequence,
                channelInConfig, audioEncoding, bufferSize,
                AudioTrack.MODE_STREAM);
        track.setStereoVolume(AudioTrack.getMaxVolume(),
                AudioTrack.getMaxVolume());
        try {
            // 默认需要抢占一个信号量。防止播放进程执行
            semaphore.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 开启播放线程
        new Thread(this).start();
    }

    /**
     * 播放,当有新数据传入时,
     * 
     * @param data
     *            语音byte数组
     * @param startIndex
     *            开始的偏移量
     * @param length
     *            数据长度
     */
    public synchronized void onPlaying(byte[] data, int startIndex, int length) {
        if (AudioTrack.ERROR_BAD_VALUE == bufferSize) {// 初始化错误
            return;
        }
        try {
            dataQueue.putLast(data);
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 准备播放
     */
    public void prepare() {
        if (track != null && !isPlaying) {
            track.play();
            isPlaying = true;
        }

    }

    /**
     * 停止播放
     */
    public void stop() {
        if (track != null) {
            track.stop();
            isPlaying = false;
        }
    }

    /**
     * 释放资源
     */
    public void release() {
        release = true;
        semaphore.release();
        if (track != null) {
            track.release();
            track = null;
        }
    }

    @Override
    public void run() {
        while (true) {
            if (release) {
                return;
            }
            if (dataQueue.size() > 0) {
                byte[] data = (byte[]) dataQueue.pollFirst();
                track.write(data, 0, data.length);
            } else {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

以上是关于android开发实现语音数据实时采集/播放的主要内容,如果未能解决你的问题,请参考以下文章

Android上的实时视频监控怎么实现的?

基于kaldi的iOS实时语音识别(本地)+03+音频采集传输

语音数据采集-实时语音数据可视化

WebRTC Native M96 回调音频裸数据IAudioFrameObserver--采集和播放语音混音后的数据(onMixedAudioFrame)

WebRTC Native M96 回调音频裸数据IAudioFrameObserver--采集和播放语音混音后的数据(onMixedAudioFrame)

Android多媒体功能开发(11)——使用AudioRecord类录制音频