Android开发之PCM音频流采集| 音频流录制与PCM音频流播放的实现方法

在android中如果需要录制PCM流需要用到AudioRecord这个类,然后播放的话需要用AudioTrack

先看下效果图:

好了我们先看下如何录制PCM,看下核心代码

 try {
            //输出流
            OutputStream os = new FileOutputStream(recordFile);
            BufferedOutputStream bos = new BufferedOutputStream(os);
            DataOutputStream dos = new DataOutputStream(bos);
            /**
             * android.media.AudioRecord public static int getMinBufferSize(int             
               sampleRateInHz,int channelConfig,int audioFormat)
               返回成功创建 AudioRecord 对象所需的最小缓冲区大小,以字节为单位。 请注意,此大小        
               不能保证在负载下顺利录制,应根据 AudioRecord 实例轮询新数据的预期频率选择更高的 
               值。 有关有效配置值的更多信息,请参阅AudioRecord(int, int, int, int, int) 。

               参数:
               sampleRateInHz – 以赫兹表示的采样率。 AudioFormat.SAMPLE_RATE_UNSPECIFIED是不允许的。
               channelConfig – 描述音频通道的配置。 请参阅AudioFormat.CHANNEL_IN_MONO和 
               AudioFormat.CHANNEL_IN_STEREO
               audioFormat – 表示音频数据的格式。 请参阅AudioFormat.ENCODING_PCM_16BIT 。
               回报:
               ERROR_BAD_VALUE如果硬件不支持录制参数,或者传递了无效参数,或者如果实现无法查询 
               硬件以获取其输入属性或以字节表示的最小缓冲区大小,则为ERROR 
             */
            int bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_IN_STEREO, audioEncoding);
            AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, AudioFormat.CHANNEL_IN_STEREO, audioEncoding, bufferSize);

            short[] buffer = new short[bufferSize];
            audioRecord.startRecording();
            Log.e(TAG, "开始录音");
            isRecording = true;
            while (isRecording) {
                int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
                for (int i = 0; i < bufferReadResult; i++) {
                    dos.writeShort(buffer[i]);
                }
            }
            audioRecord.stop();
            dos.close();
        } catch (Throwable t) {
            Log.e(TAG, "录音失败");
            showToast("录音失败");
        }

再看下播放pcm,分两种

1.一次性读取所有pcm数据后在播放这种适合数据小的pcm流

2.一边读取pcm流一边播放,这种适合比较大的数据流

先看一次性读取的代码

  int musicLength = (int) (recordFile.length() / 2);
        short[] music = new short[musicLength];
        try {
            InputStream is = new FileInputStream(recordFile);
            BufferedInputStream bis = new BufferedInputStream(is);
            DataInputStream dis = new DataInputStream(bis);
            int i = 0;
            while (dis.available() > 0) {
                music[i] = dis.readShort();
                i++;
            }
            dis.close();
             /**
            android.media.AudioTrack public static int getMinBufferSize(int         
            sampleRateInHz, int channelConfig, int audioFormat)
            返回在MODE_STREAM模式下创建的 AudioTrack 对象所需的估计最小缓冲区大小。 大小是一个    
            估计值,因为它既不考虑路由也不考虑汇,因为两者都不知道。 请注意,此大小并不能保证在 
            负载下流畅播放,应根据缓冲区重新填充要播放的其他数据的预期频率选择更高的值。 例如, 
            如果您打算将 AudioTrack 的源采样率动态设置为高于初始源采样率的值,请务必根据计划的 
            最高采样率配置缓冲区大小。

            参数:
            sampleRateInHz – 以 Hz 表示的源采样率。 不允许 
            AudioFormat.SAMPLE_RATE_UNSPECIFIED 。
            channelConfig – 描述音频通道的配置。 请参阅AudioFormat.CHANNEL_OUT_MONO和 
            AudioFormat.CHANNEL_OUT_STEREO
            audioFormat – 表示音频数据的格式。 请参阅AudioFormat.ENCODING_PCM_16BIT和 
            AudioFormat.ENCODING_PCM_8BIT和AudioFormat.ENCODING_PCM_FLOAT 。
            返回:
            如果传递了无效参数,则为ERROR_BAD_VALUE如果无法查询输出属性,则为ERROR ,或者以字 
            节为单位表示的        最小缓冲区大小
            *
            */
            AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfiguration, audioEncoding, musicLength * 2, AudioTrack.MODE_STREAM);
            audioTrack.play();
            audioTrack.write(music, 0, musicLength);
            audioTrack.stop();
        } catch (Throwable t) {
            Log.e(TAG, "播放失败");
            showToast("播放失败");
        }

在看下一边读一边播放

 try {
//            recordFile = new File("/storage/emulated/0/Android/data/com.yhsh.recordpcm/cache/audio_cache/music.wav");
            //从音频文件中读取声音
            DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(recordFile)));
            //最小缓存区
            int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_OUT_STEREO, audioEncoding);
            //创建AudioTrack对象   依次传入 :流类型、采样率(与采集的要一致)、音频通道(采集是IN 播放时OUT)、量化位数、最小缓冲区、模式
            AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, AudioFormat.CHANNEL_OUT_STEREO, audioEncoding, bufferSizeInBytes, AudioTrack.MODE_STREAM);
            short[] data = new short[bufferSizeInBytes];
            //byte[] data = new byte[bufferSizeInBytes];
            //开始播放
            player.play();
            while (true) {
                int i = 0;
                while (dis.available() > 0 && i < data.length) {
                    //录音时write Byte 那么读取时就该为readByte要相互对应
                    data[i] = dis.readShort();
                    //data[i] = dis.readByte();
                    i++;
                }
                player.write(data, 0, data.length);
                //表示读取完了
                if (i != bufferSizeInBytes) {
                    player.stop();//停止播放
                    player.release();//释放资源
                    dis.close();
                    showToast("播放完成了!!!");
                    break;
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "播放异常: " + e.getMessage());
            showToast("播放异常!!!!");
            e.printStackTrace();
        }

好了如果看着乱,我贴下完整代码

先看MainActivity.java

package com.yhsh.recordpcm;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ScrollView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author DELL
 */
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = "MainActivity";
    private TextView tvAudioSuccess;
    private ScrollView mScrollView;
    private Button startAudio;
    private Button stopAudio;
    private Button playAudio;
    ThreadPoolExecutor mExecutorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(10));
    private boolean isChecked;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startAudio = findViewById(R.id.startAudio);
        startAudio.setOnClickListener(this);
        stopAudio = findViewById(R.id.stopAudio);
        stopAudio.setOnClickListener(this);
        CheckBox cbTogetherPlay = findViewById(R.id.cb_together_play);
        cbTogetherPlay.setOnCheckedChangeListener((buttonView, isChecked) -> MainActivity.this.isChecked = isChecked);
        playAudio = findViewById(R.id.playAudio);
        playAudio.setOnClickListener(this);
        Button deleteAudio = findViewById(R.id.deleteAudio);
        deleteAudio.setOnClickListener(this);
        tvAudioSuccess = findViewById(R.id.tv_audio_succeess);
        mScrollView = findViewById(R.id.mScrollView);
    }

    @SuppressLint("NonConstantResourceId")
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.startAudio:
                mExecutorService.execute(() -> {
                    PlayManagerUtils.getInstance().startRecord(new WeakReference<>(getApplicationContext()));
                    Log.e(TAG, "start");
                });
                printLog("开始录音");
                buttonEnabled(false, true, false);
                break;
            case R.id.stopAudio:
                PlayManagerUtils.getInstance().setRecord(false);
                buttonEnabled(true, false, true);
                printLog("停止录音");
                break;
            case R.id.playAudio:
                mExecutorService.execute(() -> PlayManagerUtils.getInstance().playPcm(isChecked));
                buttonEnabled(true, false, false);
                printLog("播放录音");
                break;
            case R.id.deleteAudio:
                deleteFile();
                break;
            default:
                break;
        }
    }

    /**
     * 打印log
     *
     * @param resultString 返回数据
     */
    private void printLog(final String resultString) {
        tvAudioSuccess.post(new Runnable() {
            @Override
            public void run() {
                tvAudioSuccess.append(resultString + "\n");
                mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
            }
        });
    }

    /**
     * 获取/失去焦点
     *
     * @param start 是否可点击
     * @param stop  是否可点击
     * @param play  是否可点击
     */
    private void buttonEnabled(boolean start, boolean stop, boolean play) {
        startAudio.setEnabled(start);
        stopAudio.setEnabled(stop);
        playAudio.setEnabled(play);
    }

    /**
     * 删除文件
     */
    private void deleteFile() {
        File recordFile = PlayManagerUtils.getInstance().getRecordFile();
        if (recordFile == null) {
            return;
        }
        recordFile.delete();
        printLog("文件删除成功");
    }
}

在看下布局文件activit_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="10dp">

    <Button
        android:id="@+id/startAudio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/button_bg"
        android:text="开始录音"
        android:textColor="@android:color/white" />

    <Button
        android:id="@+id/stopAudio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="10dp"
        android:background="@drawable/button_bg"
        android:enabled="false"
        android:text="停止录音"
        android:textColor="@android:color/white" />

    <CheckBox
        android:id="@+id/cb_together_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="录制两次一起播放" />

    <Button
        android:id="@+id/playAudio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/button_bg"
        android:enabled="false"
        android:text="播放音频"
        android:textColor="@android:color/white" />

    <Button
        android:id="@+id/deleteAudio"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:background="@drawable/button_bg"
        android:text="删除PCM"
        android:textColor="@android:color/white" />

    <ScrollView
        android:id="@+id/mScrollView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="5dp"
        android:layout_weight="1">

        <TextView
            android:id="@+id/tv_audio_succeess"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="初始化完成...."
            android:textColor="@color/purple_700" />

    </ScrollView>


</LinearLayout>

在看下录制与播放工具类

PlayManagerUtils.java
package com.yhsh.recordpcm;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.Looper;
import android.text.format.DateFormat;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author xiayiye5
 * @date 2021/12/17 18:10
 */
public class PlayManagerUtils {
    private static final String TAG = "PlayManagerUtils";
    private WeakReference<Context> weakReference;
    private File recordFile;
    private boolean isRecording;
    /**
     * 最多只能存2条记录
     */
    private final List<File> filePathList = new ArrayList<>(2);

    /**
     * 16K采集率
     */
    int sampleRateInHz = 16000;
    /**
     * 格式
     */
//    int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;
    int channelConfiguration = AudioFormat.CHANNEL_OUT_STEREO;
//    int channelConfiguration = AudioFormat.CHANNEL_IN_STEREO;
    /**
     * 16Bit
     */
    int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;

    private PlayManagerUtils() {
    }

    private static final PlayManagerUtils PLAY_MANAGER_UTILS = new PlayManagerUtils();

    public static PlayManagerUtils getInstance() {
        return PLAY_MANAGER_UTILS;
    }

    private final Handler handler = new Handler(Looper.getMainLooper());

    ThreadPoolExecutor mExecutorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(10));

    public void startRecord(WeakReference<Context> weakReference) {
        this.weakReference = weakReference;
        Log.e(TAG, "开始录音");
        //生成PCM文件
        String fileName = DateFormat.format("yyyyMMdd_HHmmss", Calendar.getInstance(Locale.getDefault())) + "_xiayiye5.pcm";
        File file = new File(weakReference.get().getExternalCacheDir(), "audio_cache");
        if (!file.exists()) {
            file.mkdir();
        }
        String audioSaveDir = file.getAbsolutePath();
        Log.e(TAG, audioSaveDir);
        recordFile = new File(audioSaveDir, fileName);
        Log.e(TAG, "生成文件" + recordFile);
        //如果存在,就先删除再创建
        if (recordFile.exists()) {
            recordFile.delete();
            Log.e(TAG, "删除文件");
        }
        try {
            recordFile.createNewFile();
            Log.e(TAG, "创建文件");
        } catch (IOException e) {
            Log.e(TAG, "未能创建");
            throw new IllegalStateException("未能创建" + recordFile.toString());
        }
        if (filePathList.size() == 2) {
            filePathList.clear();
        }
        filePathList.add(recordFile);
        try {
            //输出流
            OutputStream os = new FileOutputStream(recordFile);
            BufferedOutputStream bos = new BufferedOutputStream(os);
            DataOutputStream dos = new DataOutputStream(bos);
            int bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_IN_STEREO, audioEncoding);
            AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, AudioFormat.CHANNEL_IN_STEREO, audioEncoding, bufferSize);

            short[] buffer = new short[bufferSize];
            audioRecord.startRecording();
            Log.e(TAG, "开始录音");
            isRecording = true;
            while (isRecording) {
                int bufferReadResult = audioRecord.read(buffer, 0, bufferSize);
                for (int i = 0; i < bufferReadResult; i++) {
                    dos.writeShort(buffer[i]);
                }
            }
            audioRecord.stop();
            dos.close();
        } catch (Throwable t) {
            Log.e(TAG, "录音失败");
            showToast("录音失败");
        }
    }

    /**
     * 播放pcm流的方法,一次性读取所有Pcm流,读完后在开始播放
     */
    public void playAllRecord() {
        if (recordFile == null) {
            return;
        }
//        recordFile = new File("/storage/emulated/0/Android/data/com.yhsh.recordpcm/cache/audio_cache/music.wav");
        //读取文件
        int musicLength = (int) (recordFile.length() / 2);
        short[] music = new short[musicLength];
        try {
            InputStream is = new FileInputStream(recordFile);
            BufferedInputStream bis = new BufferedInputStream(is);
            DataInputStream dis = new DataInputStream(bis);
            int i = 0;
            while (dis.available() > 0) {
                music[i] = dis.readShort();
                i++;
            }
            dis.close();
            AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfiguration, audioEncoding, musicLength * 2, AudioTrack.MODE_STREAM);
            audioTrack.play();
            audioTrack.write(music, 0, musicLength);
            audioTrack.stop();
        } catch (Throwable t) {
            Log.e(TAG, "播放失败");
            showToast("播放失败");
        }
    }


    public void playPcm(boolean isChecked) {
        if (isChecked) {
            //两首一起播放
            for (File recordFiles : filePathList) {
                mExecutorService.execute(() -> playPcmData(recordFiles));
            }
        } else {
            //只播放最后一次录音
            playPcmData(recordFile);
        }
    }

    /**
     * 播放Pcm流,边读取边播
     */
    private void playPcmData(File recordFiles) {
        Log.e(TAG, "打印线程" + Thread.currentThread().getName());
        try {
//            recordFile = new File("/storage/emulated/0/Android/data/com.yhsh.recordpcm/cache/audio_cache/music.wav");
            DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(recordFiles)));
            //最小缓存区
            int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_OUT_STEREO, audioEncoding);
            AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, AudioFormat.CHANNEL_OUT_STEREO, audioEncoding, bufferSizeInBytes, AudioTrack.MODE_STREAM);
            short[] data = new short[bufferSizeInBytes];
            //开始播放
            player.play();
            while (true) {
                int i = 0;
                while (dis.available() > 0 && i < data.length) {
                    data[i] = dis.readShort();
                    i++;
                }
                player.write(data, 0, data.length);
                //表示读取完了
                if (i != bufferSizeInBytes) {
                    player.stop();//停止播放
                    player.release();//释放资源
                    dis.close();
                    showToast("播放完成了!!!");
                    break;
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "播放异常: " + e.getMessage());
            showToast("播放异常!!!!");
            e.printStackTrace();
        }
    }

    public File getRecordFile() {
        return recordFile;
    }

    public void setRecord(boolean isRecording) {
        this.isRecording = isRecording;
    }

    private void showToast(String msg) {
        handler.post(() -> Toast.makeText(weakReference.get(), msg, Toast.LENGTH_LONG).show());
    }
}


如果还是看着不明白,请下载源码查看

gitee源码下载:​​​​​​PCM音频流录制与播放源码下载

在此非常感谢两位博主:博主直达录音与播放

博主直达pcm播放

猜你喜欢

转载自blog.csdn.net/xiayiye5/article/details/122070070