Android多媒體之認識聲音、錄音與播放(PCM)

張風捷特烈發表於2019-01-03

一、對聲音的簡單認識

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.人耳的“掩蔽效應”:參見--音視訊知識-掩蔽效應

人並不是在85Hz~1100Hz所有的聲音都是能聽到的,還要取決於響度
當頻率很低的時候需要更大的響度(振幅)才能被聽到
最簡單的響度-頻率關係圖如下(圖是我用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-github 2018-1-3 Android多媒體之認識聲音、錄音與播放(PCM)
V0.1-github 2018-1-4 位元速率的計算稍作修改
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章