Android多媒体之认识声音、录音与播放(PCM)

5,154 阅读9分钟

一、对声音的简单认识

1、模拟信号[摘录于此]
模拟信号传输过程中就是利用传感器把各种自然界各种连续的信号转换为几乎一模一样的电信号。
比如说话声音,原本是声带的震动。经过麦克风的采集,将声波信号转换为电信号,
电信号波形是和原来的声波波形一样的。只是换种物理量来表示和传递。(电信号模拟振动信号)。

下面的音频波形,大家可以听一下,音频放在这里
前四声一样,咚咚咚咚,中四声一样,咚咚咚咚,但比较急促,后8声非常极速,声音大小基本一致

波形.png


2、声音三要素:正弦函数见
[1] 音量 :(响度)声波震动幅度---A--分贝
[2] 音调 : 声音频率(高音--频率快--声音尖 低音--频率慢--声音沉)----f--Hz
[3] 音色 :(音品)与材质有关 本质是谐波

模拟信号.png


3、音量(响度)的单位:分贝(dB):
声压级的单位,大约等于人耳通常可觉察响度差别的最小分度值
感觉安静:15分贝以下 正常说话:约60dB  燃放烟花爆竹的声音:约150分贝

二、声音的量化(简)

1.模拟信号(波形)转化为数字信号
模拟信号(波形图)-->
采样(横轴等距取点)-->
量化(纵轴量化)-->
编码(量化值二进制化)-->
数字信号 (方波0-断 1-通)

2.采样中的一些参数
采样大小:振幅的最大值。一个采样的存储空间,常用16bit (0-65535)振幅
采样率  :采样频率 8K、16K、32k、(AAC)44.1K、48K(1s在模拟信号上采集48K次) 
20Hz 频率即1s振动20次,使用48K采样,一个周期中采样48,000/20=2400次
20KHz 频率即1s振动20K次,使用48K采样,一个周期中采样48K/20K=2.4次
声道数:单声道、双声道、多声道
码率:一个PCM音频流码率:采样率*采样大小*声道数b/s

如:44100*16*2=1411200b/s=1378.125Kb/s= 172.265625KB/s 即每秒钟172.265625KB

3.字节(Byte)与位(bit)
存储容量:1KB 1MB 1GB 1TB,它们之间进率是1024,也是说,1MB=1024KB,1GB=1024MB等
宽带大小:2M,4M 即:2Mb/s(2Mbps),4Mb/s(4Mbps)。
下载速度:128KB/s,256KB/s

它们之间转换:1MB=1024KB  1Mb/s=1024Kb/s(千位/秒)   1字节=8位
1M的宽带下载速度:1024Kb/s=1024千位/秒= (1024/8千字节)/秒=128千字节/秒=128KB/s

二、心理声学

1.人的听觉范围与发声范围
Hz:1s振动的次数
听觉范围 (20Hz 20KHz)
发声范围 (85Hz 1100Hz)

听觉频率与发生频率对比图.jpg


2.人耳的“掩蔽效应”:参见--音视频知识-掩蔽效应

人并不是在85Hz1100Hz所有的声音都是能听到的,还要取决于响度
当频率很低的时候需要更大的响度(振幅)才能被听到
最简单的响度-频率关系图如下(图是我用ps修的,如果有误,欢迎指正):
可见在3KHz
5KHz的阀值较小,也就是更容易听到

响度-频率曲线.jpg


当某个时刻响起一个高分贝的声音,它周围会出现遮蔽区域
如在轰鸣的机械运转中(红色),工人普通语言交流(灰色)是困难的
在遮蔽区域内的声音人耳是无法识别的,这时可以提高音量,突破阀值,达到有效听觉区

频域遮蔽.jpg


时域掩蔽
掩蔽声音与被掩蔽声音不同时出现时
若掩蔽声音出现之前的一段时间内发生掩蔽效应,称:超前掩蔽(pre-masking)
否则滞后掩蔽(post-masking)
产生时域掩蔽的主要原因是人的大脑处理信息需要花费一定的时间
一般来说,超前掩蔽很短,只有大约5~20 ms,而滞后掩蔽可以持续50~200 ms


3.心理声学的价值:

模拟信号的采集过程中,不管人耳的能不能识别,它把能记录的都记录了
从而会产生一些人耳无法识别的冗余数据,这些数据显然我们是不想要的
在进行采样之前,先结合心理声学模型处理,可缩小采样范围,尽量去除掉无用的信息

科普就这么多,有个印象就行,平时拿来吹吹牛还是够的,下面进入正题


三、PCM音频的捕获(AudioRecord)

PCM(Pulse Code Modulation)--脉冲编码调制,今天只说PCM

主要过程是将话音、图像等模拟信号每隔一定时间进行取样,使其离散化,
同时将抽样值按分层单位四舍五入取整量化,同时将抽样值按一组二进制码来表示抽样脉冲的幅值

PCM编码:最大程度的接近绝对保真,但是体积大 

图书馆里不好意思说话,假装咳嗽了两声:(用软件AU打开的)

捕获音频.png

0.权限

动态权限申请这里不说了,自己解决(录音也要动态权限的)

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

1.界面

界面很简单,中间是帧动画,按下时开启,离开时停止并回到第一帧
按下时开启录音,手离开时停止录音,最后在左边显示录音时长,素材在源码里

界面.png


2.帧动画的xml版实现

资源图片.png

play.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
                android:oneshot="false">
    <item android:drawable="@mipmap/a_0" android:duration="200"/>
    <item android:drawable="@mipmap/a_1" android:duration="200"/>
    <item android:drawable="@mipmap/a_2" android:duration="200"/>
    <item android:drawable="@mipmap/a_3" android:duration="200"/>
    <item android:drawable="@mipmap/a_4" android:duration="200"/>
    <item android:drawable="@mipmap/a_5" android:duration="200"/>
    <item android:drawable="@mipmap/a_6" android:duration="200"/>
    <item android:drawable="@mipmap/a_7" android:duration="200"/>
    <item android:drawable="@mipmap/a_8" android:duration="200"/>
    <item android:drawable="@mipmap/a_9" android:duration="200"/>
</animation-list>

动画效果的实现
mIdIvRecode.setBackgroundResource(R.drawable.play);
animation = (AnimationDrawable) mIdIvRecode.getBackground();
mIdIvRecode.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {

        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                animation.start();
                //TODO录音
                break;
            case MotionEvent.ACTION_UP:
                animation.stop();
                animation.selectDrawable(0);
                //TODO停止录音
                break;
        }
        return true;
    }
});

3.PCMRecordTask.java录音流程简单示意图

简单示意.png

/**
 * 作者:张风捷特烈<br/>
 * 时间:2019/1/3 0003:10:58<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:PCM编码音频录制辅助
 */
public class PCMRecordTask {
    //默认配置AudioRecord
    private static final int DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC;////麦克风采集
    private static final int DEFAULT_SAMPLE_RATE = 44100;//采样频率
    private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;//单声道
    private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;//输出格式:16位pcm

    private AudioRecord mAudioRecord;//录音机
    private int mMinBufferSize = 2048;//最小缓存数组大小

    private Thread mRecordThread;//录音线程
    private boolean mIsStarted = false;//是否已开启
    private volatile boolean mIsRecording = false;//是否正在录制

    private OnRecording mOnRecording;//录制时的监听
    private long mStartTime;//开始录制时间
    private int mWorkingTime;


    /**
     * 开始录制
     *
     * @return
     */
    public boolean recode() {
        return recode(DEFAULT_SOURCE, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG,
                DEFAULT_AUDIO_FORMAT);
    }

    /**
     * 开始录制
     *
     * @return
     */
    public boolean recode(int source, int sampleRate, int channel, int format) {
        if (mIsStarted) {//如果已经开始,返回false
            return false;
        }
        mMinBufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
        mAudioRecord = new AudioRecord(source, sampleRate, channel, format, mMinBufferSize);
        mAudioRecord.startRecording();

        mIsRecording = true;//正在录制
        mRecordThread = new Thread(new RecodeRunnable());
        mRecordThread.start();
        mIsStarted = true;//已开启
        mStartTime = System.currentTimeMillis();//开始时间
        return true;
    }

    /**
     * 停止录制
     */
    public void stopRecode() {
        if (!mIsStarted) {
            return;
        }

        mIsRecording = false;//不在录音
        try {
            mRecordThread.interrupt();
            mRecordThread.join(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
            mAudioRecord.stop();//状态为录制中,停止
        }

        mAudioRecord.release();//释放资源
        mIsStarted = false;//未开启

        //录制花费时间
        mWorkingTime = (int) ((System.currentTimeMillis() - mStartTime) / 1000);
    }

    public int getWorkingTime() {
        return mWorkingTime;
    }


    public void setOnRecording(OnRecording onRecording) {
        mOnRecording = onRecording;
    }

    public boolean isStarted() {
        return mIsStarted;
    }
    
    private class RecodeRunnable implements Runnable {
        @Override
        public void run() {
            while (mIsRecording) {//如果正在录制
                byte[] buf = new byte[mMinBufferSize];//缓存字节数组
                int read = mAudioRecord.read(buf, 0, mMinBufferSize);
                if (mOnRecording != null) {
                    if (read > 0) {//有数据,则回调onRecording
                        mOnRecording.onRecording(buf, read);
                    } else {
                        mOnRecording.onError(new RuntimeException("Error When Read"));
                    }
                }
            }
        }
    }
}

4.录制监听
/**
 * 作者:张风捷特烈<br/>
 * 时间:2019/1/3 0003:13:28<br/>
 * 邮箱:1981462002@qq.com<br/>
 * 说明:录制监听
 */
public interface OnRecording {
    /**
     * 录制中监听
     * @param data 数据
     * @param len 长度
     */
    void onRecording(byte[] data, int len);

    /**
     * 错误监听
     * @param e
     */
    void onError(Exception e);
}

5.使用:开始和停止

这里文件的创建就不废话了,采用时间作为文件名(已封装)

/**
 * 开启录音
 */
private void startRecord() {
    try {
        //创建录音文件---这里创建文件不是重点,我直接用了
        mFile = FileHelper.get().createFile("pcm录音/" + StrUtil.getCurrentTime_yyyyMMddHHmmss() + ".pcm");
        mFos = new FileOutputStream(mFile);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    mPcmRecordTask.recode();
}

 /**
  * 停止录制
  */
 private void stopRecode() {
     mPcmRecordTask.stopRecode();
     mIdTvState.setText("录制" + mPcmRecordTask.getWorkingTime() + "秒");
 }

四、PCM音频的播放(AudioTrack)

如果录音是模拟信号到数字信号的编码,那么播放则是数字信号到模拟信号的解码
需要用到的类就是AudioTrack,注意怎么编的码就怎么解,不然肯定有问题嘛

1.代码实现
/**
 * 作者:张风捷特烈
 * 时间:2018/7/13:15:52
 * 邮箱:1981462002@qq.com
 * 说明:PCM播放(解码)
 */
public class PCMAudioPlayer {
    //默认配置AudioTrack-----此处是解码,要环和编码的配置对应
    private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_MUSIC;//音乐
    private static final int DEFAULT_SAMPLE_RATE = 44100;//采样频率
    private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO;//注意是out
    private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM;
    private final ExecutorService mExecutorService;

    private AudioTrack audioTrack;//音轨
    private DataInputStream dis;//流
    private boolean isStart = false;
    private static PCMAudioPlayer mInstance;//单例
    private int mMinBufferSize;//最小缓存大小

    public PCMAudioPlayer() {
        mMinBufferSize = AudioTrack.getMinBufferSize(
                DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG, AudioFormat.ENCODING_PCM_16BIT);
        //实例化AudioTrack
        audioTrack = new AudioTrack(
                DEFAULT_STREAM_TYPE, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG,
                DEFAULT_AUDIO_FORMAT, mMinBufferSize * 2, DEFAULT_PLAY_MODE);
        mExecutorService = Executors.newSingleThreadExecutor();//线程池
    }

    /**
     * 获取单例对象
     *
     * @return
     */
    public static PCMAudioPlayer getInstance() {
        if (mInstance == null) {
            synchronized (PCMAudioPlayer.class) {
                if (mInstance == null) {
                    mInstance = new PCMAudioPlayer();
                }
            }
        }
        return mInstance;
    }

    /**
     * 播放文件
     *
     * @param path
     * @throws Exception
     */
    private void setPath(String path) throws Exception {
        File file = new File(path);
        dis = new DataInputStream(new FileInputStream(file));
    }

    /**
     * 启动播放
     *
     * @param path 文件了路径
     */
    public void startPlay(String path) {
        try {
            isStart = true;
            setPath(path);//设置路径--生成流dis
            mExecutorService.execute(new PlayRunnable());//启动播放线程
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 停止播放
     */
    public void stopPlay() {
        try {
            if (audioTrack != null) {
                if (audioTrack.getState() == AudioRecord.STATE_INITIALIZED) {
                    audioTrack.stop();
                }
            }
            if (dis != null) {
                isStart = false;
                dis.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放资源
     */
    public void release() {
        if (audioTrack != null) {
            audioTrack.release();
        }
        mExecutorService.shutdownNow();//停止线程池
    }

    //播放线程
    private class PlayRunnable implements Runnable {
        @Override
        public void run() {
            try {
                //标准较重要音频播放优先级
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
                byte[] tempBuffer = new byte[mMinBufferSize];
                int readCount = 0;
                while (dis.available() > 0) {
                    readCount = dis.read(tempBuffer);//读流
                    if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                        continue;
                    }
                    if (readCount != 0 && readCount != -1) {//
                        audioTrack.play();
                        audioTrack.write(tempBuffer, 0, readCount);
                    }
                }
                stopPlay();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.使用就一句话:
PCMAudioPlayer.getInstance().startPlay("/sdcard/pcm录音/20190103140621.pcm")

最后提一下:希望大家分清编码和格式(拓展名)
这里我将文件名改为20190103140621.toly也正常播放,文件中的内容(流)不变
AudioTrack解析的是流,跟拓展名无关,拓展名是为了让软件识别文件
20190103140621.toly的文件用AU(音频编辑器)就打不开,改成.PCM就能打开
现在明白PCM编码和.PCM后缀名的区别了吗...


最后来点有意思的:
咳嗽两声用了1.991秒

码率:一个PCM音频流码率:采样率*采样大小*声道数Kb/s
44100*16*1=705600b/s=8820B/s 即每秒钟8820B(字节)
1.991s*88.2KB/s=17560.62 B ----字节数几乎一直(1.991s应该是四舍五入的)

歌曲信息.png


后记:捷文规范

1.本文成长记录及勘误表
项目源码日期备注
V0.1-github2018-1-3Android多媒体之认识声音、录音与播放(PCM)
V0.1-github2018-1-4码率的计算稍作修改
2.更多关于我
笔名QQ微信爱好
张风捷特烈1981462002zdl1994328语言
我的github我的简书我的掘金个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


我的博客即将同步至 OSCHINA 社区,这是我的 OSCHINA ID:张风捷特烈,邀请大家一同入驻:www.oschina.net/sharing-pla…

icon_wx_200.png