Android AudioRecord和AudioTrack实现音频PCM数据的采集和播放,并读写音频wav文件

  • win7
  • Android Studio 3.0.1

本文目的:使用 AudioRecord 和 AudioTrack 完成音频PCM数据的采集和播放,并读写音频wav文件

本文链接 - Android音频PCM数据的采集和播放,读写音频wav文件

准备工作

Android提供了AudioRecord和MediaRecord。MediaRecord可选择录音的格式。
AudioRecord得到PCM编码格式的数据。AudioRecord能够设置模拟信号转化为数字信号的相关参数,包括采样率和量化深度,同时也包括通道数目等。

PCM

PCM是在由模拟信号向数字信号转化的一种常用的编码格式,称为脉冲编码调制,PCM将模拟信号按照一定的间距划分为多段,然后通过二进制去量化每一个间距的强度。
PCM表示的是音频文件中随着时间的流逝的一段音频的振幅。Android在WAV文件中支持PCM的音频数据。

WAV

WAV,MP3等比较常见的音频格式,不同的编码格式对应不通过的原始音频。为了方便传输,通常会压缩原始音频。
为了辨别出音频格式,每种格式有特定的头文件(header)。
WAV以RIFF为标准。RIFF是一种资源交换档案标准。RIFF将文件存储在每一个标记块中。
基本构成单位是trunk,每个trunk由标记位,数据大小,数据存储,三个部分构成。

PCM打包成WAV

PCM是原始音频数据,WAV是windows中常见的音频格式,只是在pcm数据中添加了一个文件头。

起始地址 占用空间 本地址数字的含义
00H 4byte RIFF,资源交换文件标志。
04H 4byte 从下一个地址开始到文件尾的总字节数。高位字节在后面,这里就是001437ECH,换成十进制是1325036byte,算上这之前的8byte就正好1325044byte了。
08H 4byte WAVE,代表wav文件格式。
0CH 4byte FMT ,波形格式标志
10H 4byte 00000010H,16PCM,我的理解是用16bit的数据表示一个量化结果。
14H 2byte 为1时表示线性PCM编码,大于1时表示有压缩的编码。这里是0001H。
16H 2byte 1为单声道,2为双声道,这里是0001H。
18H 4byte 采样频率,这里是00002B11H,也就是11025Hz。
1CH 4byte Byte率=采样频率*音频通道数*每次采样得到的样本位数/8,00005622H,也就是22050Byte/s=11025*1*16/2
20H 2byte 块对齐=通道数*每次采样得到的样本位数/8,0002H,也就是 2 == 1*16/8
22H 2byte 样本数据位数,0010H即16,一个量化样本占2byte。
24H 4byte data,一个标志而已。
28H 4byte Wav文件实际音频数据所占的大小,这里是001437C8H即1325000,再加上2CH就正好是1325044,整个文件的大小。
2CH 不定 量化数据

AudioRecord

AudioRecord可实习从音频输入设备记录声音的功能。得到PCM格式的音频。
读取音频的方法有read(byte[], int, int)read(short[], int, int)read(ByteBuffer, int)
可根据存储方式和需求选择使用这项方法。

需要权限<uses-permission android:name="android.permission.RECORD_AUDIO" />

AudioRecord 构造函数

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

  • audioSource 音源设备,常用麦克风MediaRecorder.AudioSource.MIC
  • samplerateInHz 采样频率,44100Hz是目前所有设备都支持的频率
  • channelConfig 音频通道,单声道还是立体声
  • audioFormat 该参数为量化深度,即为每次采样的位数
  • bufferSizeInBytes 可通过getMinBufferSize()方法确定,每次从硬件读取数据所需要的缓冲区的大小。
获取wav文件

若要获得wav文件,需要在PCM基础上增加一个header。可以将PCM文件转换成wav,这里提供一种PCM与wav几乎同时生成的思路。

PCM与wav同时创建,给wav文件一个默认的header。录制线程启动后,同时写PCM与wav。
录制完成时,重新生成header,利用RandomAccessFile修改wav文件的header。

AudioTrack

使用AudioTrack播放音频。初始化AudioTrack时,要根据录制时的参数进行设定。

代码示例

工具类WindEar实现音频PCM数据的采集和播放,与读写音频wav文件的功能。

  • AudioRecordThread 使用AudioRecord录制PCM文件,可选择同时生成wav文件
  • AudioTrackPlayThread 使用AudioTrack播放PCM或wav音频文件的线程
  • WindState 表示当前状态,例如是否在播放,录制等等

PCM文件的读写采用FileOutputStreamFileInputStream

generateWavFileHeader方法可以生成wav文件的header

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.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * 音频录制器
 * 使用 AudioRecord 和 AudioTrack API 完成音频 PCM 数据的采集和播放,并实现读写音频 wav 文件
 * 检查权限,检查麦克风的工作放在Activity中进行
 * Created by Rust on 2018/2/24.
 */
public class WindEar {
    private static final String TAG = "rustApp";
    private static final String TMP_FOLDER_NAME = "AnWindEar";
    private static final int RECORD_AUDIO_BUFFER_TIMES = 1;
    private static final int PLAY_AUDIO_BUFFER_TIMES = 1;
    private static final int AUDIO_FREQUENCY = 44100;

    private static final int RECORD_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
    private static final int PLAY_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_STEREO;
    private static final int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;

    private AudioRecordThread aRecordThread;           // 录制线程
    private volatile WindState state = WindState.IDLE; // 当前状态
    private File tmpPCMFile = null;
    private File tmpWavFile = null;
    private OnState onStateListener;
    private Handler mainHandler = new Handler(Looper.getMainLooper());

    /**
     * PCM缓存目录
     */
    private static String cachePCMFolder;

    /**
     * wav缓存目录
     */
    private static String wavFolderPath;

    private static WindEar instance = new WindEar();

    private WindEar() {

    }

    public static WindEar getInstance() {
        if (null == instance) {
            instance = new WindEar();
        }
        return instance;
    }

    public void setOnStateListener(OnState onStateListener) {
        this.onStateListener = onStateListener;
    }

    /**
     * 初始化目录
     */
    public static void init(Context context) {
        // 存储在App内或SD卡上
//        cachePCMFolder = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
        cachePCMFolder = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + TMP_FOLDER_NAME;

        File folder = new File(cachePCMFolder);
        if (!folder.exists()) {
            boolean f = folder.mkdirs();
            Log.d(TAG, String.format(Locale.CHINA, "PCM目录:%s -> %b", cachePCMFolder, f));
        } else {
            for (File f : folder.listFiles()) {
                boolean d = f.delete();
                Log.d(TAG, String.format(Locale.CHINA, "删除PCM文件:%s %b", f.getName(), d));
            }
            Log.d(TAG, String.format(Locale.CHINA, "PCM目录:%s", cachePCMFolder));
        }

        wavFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator
                + TMP_FOLDER_NAME;
//        wavFolderPath = context.getFilesDir().getAbsolutePath() + File.separator + TMP_FOLDER_NAME;
        File wavDir = new File(wavFolderPath);
        if (!wavDir.exists()) {
            boolean w = wavDir.mkdirs();
            Log.d(TAG, String.format(Locale.CHINA, "wav目录:%s -> %b", wavFolderPath, w));
        } else {
            Log.d(TAG, String.format(Locale.CHINA, "wav目录:%s", wavFolderPath));
        }
    }

    /**
     * 开始录制音频
     */
    public synchronized void startRecord(boolean createWav) {
        if (!state.equals(WindState.IDLE)) {
            Log.w(TAG, "无法开始录制,当前状态为 " + state);
            return;
        }
        try {
            tmpPCMFile = File.createTempFile("recording", ".pcm", new File(cachePCMFolder));
            if (createWav) {
                SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd_HHmmss", Locale.CHINA);
                tmpWavFile = new File(wavFolderPath + File.separator + "r" + sdf.format(new Date()) + ".wav");
            }
            Log.d(TAG, "tmp file " + tmpPCMFile.getName());
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (null != aRecordThread) {
            aRecordThread.interrupt();
            aRecordThread = null;
        }
        aRecordThread = new AudioRecordThread(createWav);
        aRecordThread.start();
    }

    public synchronized void stopRecord() {
        if (!state.equals(WindState.RECORDING)) {
            return;
        }
        state = WindState.STOP_RECORD;
        notifyState(state);
    }

    /**
     * 播放录制好的PCM文件
     */
    public synchronized void startPlayPCM() {
        if (!isIdle()) {
            return;
        }
        new AudioTrackPlayThread(tmpPCMFile).start();
    }

    /**
     * 播放录制好的wav文件
     */
    public synchronized void startPlayWav() {
        if (!isIdle()) {
            return;
        }
        new AudioTrackPlayThread(tmpWavFile).start();
    }

    public synchronized void stopPlay() {
        if (!state.equals(WindState.PLAYING)) {
            return;
        }
        state = WindState.STOP_PLAY;
    }

    public synchronized boolean isIdle() {
        return WindState.IDLE.equals(state);
    }

    /**
     * 音频录制线程
     * 使用FileOutputStream来写文件
     */
    private class AudioRecordThread extends Thread {
        AudioRecord aRecord;
        int bufferSize = 10240;
        boolean createWav = false;

        AudioRecordThread(boolean createWav) {
            this.createWav = createWav;
            bufferSize = AudioRecord.getMinBufferSize(AUDIO_FREQUENCY,
                    RECORD_CHANNEL_CONFIG, AUDIO_ENCODING) * RECORD_AUDIO_BUFFER_TIMES;
            Log.d(TAG, "record buffer size = " + bufferSize);
            aRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, AUDIO_FREQUENCY,
                    RECORD_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize);
        }

        @Override
        public void run() {
            state = WindState.RECORDING;
            notifyState(state);
            Log.d(TAG, "录制开始");
            try {
                // 这里选择FileOutputStream而不是DataOutputStream
                FileOutputStream pcmFos = new FileOutputStream(tmpPCMFile);

                FileOutputStream wavFos = new FileOutputStream(tmpWavFile);
                if (createWav) {
                    byte[] zeroHeader = new byte[44]; // 占位置
                    wavFos.write(zeroHeader);
                }
                aRecord.startRecording();
                byte[] byteBuffer = new byte[bufferSize];
                while (state.equals(WindState.RECORDING) && !isInterrupted()) {
                    int end = aRecord.read(byteBuffer, 0, byteBuffer.length);
                    pcmFos.write(byteBuffer, 0, end);
                    pcmFos.flush();
                    if (createWav) {
                        wavFos.write(byteBuffer, 0, end);
                        wavFos.flush();
                    }
                }
                aRecord.stop(); // 录制结束
                pcmFos.close();
                wavFos.close();
                if (createWav) {
                    // 修改header
                    RandomAccessFile wavRaf = new RandomAccessFile(tmpWavFile, "rw");
                    byte[] header = generateWavFileHeader(tmpPCMFile.length() - 44, AUDIO_FREQUENCY, aRecord.getChannelCount());
                    Log.d(TAG, "header: " + getHexString(header));
                    wavRaf.seek(0);
                    wavRaf.write(header);
                    wavRaf.close();
                    Log.d(TAG, "tmpWavFile.length: " + tmpWavFile.length());
                }
                Log.i(TAG, "audio tmp PCM file len: " + tmpPCMFile.length());
            } catch (Exception e) {
                Log.e(TAG, "AudioRecordThread:", e);
                notifyState(WindState.ERROR);
            }
            notifyState(state);
            state = WindState.IDLE;
            notifyState(state);
            Log.d(TAG, "录制结束");
        }

    }

    private static String getHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(Integer.toHexString(b)).append(",");
        }
        return sb.toString();
    }

    /**
     * AudioTrack播放音频线程
     * 使用FileInputStream读取文件
     */
    private class AudioTrackPlayThread extends Thread {
        AudioTrack track;
        int bufferSize = 10240;
        File audioFile = null;

        AudioTrackPlayThread(File aFile) {
            setPriority(Thread.MAX_PRIORITY);
            audioFile = aFile;
            int bufferSize = AudioTrack.getMinBufferSize(AUDIO_FREQUENCY,
                    PLAY_CHANNEL_CONFIG, AUDIO_ENCODING) * PLAY_AUDIO_BUFFER_TIMES;
            track = new AudioTrack(AudioManager.STREAM_MUSIC,
                    AUDIO_FREQUENCY,
                    PLAY_CHANNEL_CONFIG, AUDIO_ENCODING, bufferSize,
                    AudioTrack.MODE_STREAM);
        }

        @Override
        public void run() {
            super.run();
            state = WindState.PLAYING;
            notifyState(state);
            try {
                FileInputStream fis = new FileInputStream(audioFile);
                track.play();
                byte[] aByteBuffer = new byte[bufferSize];
                while (state.equals(WindState.PLAYING) &&
                        fis.read(aByteBuffer) >= 0) {
                    track.write(aByteBuffer, 0, aByteBuffer.length);
                }
                track.stop();
                track.release();
            } catch (Exception e) {
                Log.e(TAG, "AudioTrackPlayThread:", e);
                notifyState(WindState.ERROR);
            }
            state = WindState.STOP_PLAY;
            notifyState(state);
            state = WindState.IDLE;
            notifyState(state);
        }

    }

    private synchronized void notifyState(final WindState currentState) {
        if (null != onStateListener) {
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    onStateListener.onStateChanged(currentState);
                }
            });
        }
    }

    public interface OnState {
        void onStateChanged(WindState currentState);
    }

    /**
     * 表示当前状态
     */
    public enum WindState {
        ERROR,
        IDLE,
        RECORDING,
        STOP_RECORD,
        PLAYING,
        STOP_PLAY
    }

    /**
     * @param out            wav音频文件流
     * @param totalAudioLen  不包括header的音频数据总长度
     * @param longSampleRate 采样率,也就是录制时使用的频率
     * @param channels       audioRecord的频道数量
     * @throws IOException 写文件错误
     */
    private void writeWavFileHeader(FileOutputStream out, long totalAudioLen, long longSampleRate,
                                    int channels) throws IOException {
        byte[] header = generateWavFileHeader(totalAudioLen, longSampleRate, channels);
        out.write(header, 0, header.length);
    }

    /**
     * 任何一种文件在头部添加相应的头文件才能够确定的表示这种文件的格式,
     * wave是RIFF文件结构,每一部分为一个chunk,其中有RIFF WAVE chunk,
     * FMT Chunk,Fact chunk,Data chunk,其中Fact chunk是可以选择的
     *
     * @param pcmAudioByteCount 不包括header的音频数据总长度
     * @param longSampleRate    采样率,也就是录制时使用的频率
     * @param channels          audioRecord的频道数量
     */
    private byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
        long totalDataLen = pcmAudioByteCount + 36; // 不包含前8个字节的WAV文件总长度
        long byteRate = longSampleRate * 2 * channels;
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF
        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);

        header[8] = 'W';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//过渡字节
        //数据大小
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //编码方式 10H为PCM编码格式
        header[20] = 1; // format = 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) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数
        header[32] = (byte) (2 * channels);
        header[33] = 0;
        //每个样本的数据位数
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (pcmAudioByteCount & 0xff);
        header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);
        header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);
        header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);
        return header;
    }
}

参考资料

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

推荐阅读更多精彩内容

  • 几个小时前看电视,大概晚上九点多吧,偶然调到北京台在播一个为女人重塑形象的节目《哎呦,你真美!》,就多看了两眼,吸...
    三棵竖阅读 518评论 3 4
  • 每当争吵烦闷的源头关系到钱时,我只是怪自己太无能。 希望你快乐得无暇顾及流言蜚语。
    Joann喵阅读 111评论 0 0
  • 今天赵雷又发新歌——《无法长大》,深情而缠绵的唱法像是最浪漫的告白。 评论里看到,赵雷在一次音乐节的时候说希望他的...
    无法聚焦阅读 830评论 1 5