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

一、對聲音的簡單認識

1、模擬信號[ 摘錄于此 ]

模擬信號傳輸過程中就是利用傳感器把各種自然界各種連續的信號轉換為幾乎一模一樣的電信號。
比如說話聲音,原本是聲帶的震動。經過麥克風的采集,將聲波信號轉換為電信號,
電信號波形是和原來的聲波波形一樣的。只是換種物理量來表示和傳遞。(電信號模擬振動信號)。

下面的音頻波形,大家可以聽一下, 音頻放在這里

前四聲一樣,咚咚咚咚,中四聲一樣,咚咚咚咚,但比較急促,后8聲非常極速,聲音大小基本一致

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

波形.png

2、聲音三要素: 正弦函數見

[1] 音量 :(響度)聲波震動幅度---A--分貝
[2] 音調 : 聲音頻率(高音--頻率快--聲音尖 低音--頻率慢--聲音沉)----f--Hz
[3] 音色 :(音品)與材質有關 本質是諧波

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

模擬信號.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音頻流碼率:采樣率采樣大小聲道數Kb/s

如:44.1K162=1411.2Kb/s=176.4KB/s 即每秒鐘176.4KB</code></pre>

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</code></pre>

二、心理聲學

1.人的聽覺范圍與發聲范圍

Hz:1s振動的次數
聽覺范圍 (20Hz 20KHz)
發聲范圍 (85Hz 1100Hz)

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

聽覺頻率與發生頻率對比圖.jpg

2.人耳的“掩蔽效應”: 參見--音視頻知識-掩蔽效應

人并不是在85Hz~1100Hz所有的聲音都是能聽到的,還要取決于響度

當頻率很低的時候需要更大的響度(振幅)才能被聽到

最簡單的響度-頻率關系圖如下(圖是我用ps修的,如果有誤,歡迎指正):

可見在3KHz~5KHz的閥值較小,也就是更容易聽到

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

響度-頻率曲線.jpg

當某個時刻響起一個高分貝的聲音,它周圍會出現遮蔽區域

如在轟鳴的機械運轉中(紅色),工人普通語言交流(灰色)是困難的

在遮蔽區域內的聲音人耳是無法識別的,這時可以提高音量,突破閥值,達到有效聽覺區

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

頻域遮蔽.jpg

時域掩蔽

掩蔽聲音與被掩蔽聲音不同時出現時

若掩蔽聲音出現之前的一段時間內發生掩蔽效應,稱:超前掩蔽(pre-masking)

否則滯后掩蔽(post-masking)

產生時域掩蔽的主要原因是人的大腦處理信息需要花費一定的時間

一般來說,超前掩蔽很短,只有大約5~20 ms,而滯后掩蔽可以持續50~200 ms

3.心理聲學的價值:

模擬信號的采集過程中,不管人耳的能不能識別,它把能記錄的都記錄了

從而會產生一些人耳無法識別的冗余數據,這些數據顯然我們是不想要的

在進行采樣之前,先結合心理聲學模型處理,可縮小采樣范圍,盡量去除掉無用的信息

科普就這么多,有個印象就行,平時拿來吹吹牛還是夠的,下面進入正題

三、PCM音頻的捕獲(AudioRecord)

PCM(Pulse Code Modulation)--脈沖編碼調制,今天只說PCM

主要過程是將話音、圖像等模擬信號每隔一定時間進行取樣,使其離散化,
同時將抽樣值按分層單位四舍五入取整量化,同時將抽樣值按一組二進制碼來表示抽樣脈沖的幅值

PCM編碼:最大程度的接近絕對保真,但是體積大</code></pre>

圖書館里不好意思說話,假裝咳嗽了兩聲:(用軟件AU打開的)

 

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

捕獲音頻.png

0.權限

動態權限申請這里不說了,自己解決(錄音也要動態權限的)

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

1.界面

界面很簡單,中間是幀動畫,按下時開啟,離開時停止并回到第一幀

按下時開啟錄音,手離開時停止錄音,最后在左邊顯示錄音時長,素材在源碼里

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

界面.png

2.幀動畫的xml版實現

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

資源圖片.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;
}

});</code></pre>

3. PCMRecordTask.java 錄音流程簡單示意圖

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

簡單示意.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"));
                }
            }
        }
    }
}

}</code></pre>

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); }</code></pre>

      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(); }</code></pre>
    /**
    • 停止錄制 */ private void stopRecode() { mPcmRecordTask.stopRecode(); mIdTvState.setText("錄制" + mPcmRecordTask.getWorkingTime() + "秒"); }</code></pre>

      四、PCM音頻的播放(AudioTrack)

      如果錄音是模擬信號到數字信號的編碼,那么播放則是數字信號到模擬信號的解碼

      需要用到的類就是AudioTrack,注意怎么編的碼就怎么解,不然肯定有問題嘛

      1.代碼實現

      /**
  • 作者:張風捷特烈
  • 時間:2018/7/13:15:52
  • 郵箱:1981462002@qq.com
  • 說明:CMP播放(解碼) */ public class CMPAudioPlayer { //默認配置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 CMPAudioPlayer mInstance;//單例 private int mMinBufferSize;//最小緩存大小

    public CMPAudioPlayer() {

     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 CMPAudioPlayer getInstance() { if (mInstance == null) {

       synchronized (CMPAudioPlayer.class) {
           if (mInstance == null) {
               mInstance = new CMPAudioPlayer();
           }
       }
      

      } 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();
       }
      

      } } }</code></pre>

      2.使用就一句話:

      CMPAudioPlayer.getInstance().startPlay("/sdcard/pcm錄音/20190103140621.pcm")

      最后提一下:希望大家分清編碼和格式(拓展名)

      這里我將文件名改為 20190103140621.toly 也正常播放,文件中的內容(流)不變

      AudioTrack解析的是流,跟拓展名無關,拓展名是為了讓軟件識別文件

      20190103140621.toly 的文件用AU(音頻編輯器)就打不開,改成 .cmp 就能打開

      現在明白CMP編碼和 .cmp 后綴名的區別了嗎...

      最后來點有意思的:
      咳嗽兩聲用了1.991秒

碼率:一個PCM音頻流碼率:采樣率采樣大小聲道數Kb/s 44.1K161=705.6Kb/s=88.2KB/s 即每秒鐘88.2KB 1.991s*88.2KB/s=175.6062 KB ----貌似計算值比實際值大一些,約大5KB</code></pre>

后記:捷文規范

1.本文成長記錄及勘誤表

項目源碼 日期 備注
V0.1-github 2018-1-3 Android多媒體之認識聲音、錄音與播放(PCM)

2.更多關于我

筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站

 本文由用戶 碼頭工人 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!