模擬信號傳輸過程當中就是利用傳感器把各類天然界各類連續的信號轉換爲幾乎如出一轍的電信號。
好比說話聲音,本來是聲帶的震動。通過麥克風的採集,將聲波信號轉換爲電信號,
電信號波形是和原來的聲波波形同樣的。只是換種物理量來表示和傳遞。(電信號模擬振動信號)。
複製代碼
下面的音頻波形,你們能夠聽一下,音頻放在這裏
前四聲同樣,咚咚咚咚,中四聲同樣,咚咚咚咚,但比較急促,後8聲很是極速,聲音大小基本一致html
[1] 音量 :(響度)聲波震動幅度---A--分貝
[2] 音調 : 聲音頻率(高音--頻率快--聲音尖 低音--頻率慢--聲音沉)----f--Hz
[3] 音色 :(音品)與材質有關 本質是諧波
複製代碼
聲壓級的單位,大約等於人耳一般可覺察響度差異的最小分度值
感受安靜:15分貝如下 正常說話:約60dB 燃放煙花爆竹的聲音:約150分貝
複製代碼
模擬信號(波形圖)-->
採樣(橫軸等距取點)-->
量化(縱軸量化)-->
編碼(量化值二進制化)-->
數字信號 (方波0-斷 1-通)
複製代碼
採樣大小:振幅的最大值。一個採樣的存儲空間,經常使用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
複製代碼
存儲容量: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
複製代碼
Hz:1s振動的次數
聽覺範圍 (20Hz 20KHz)
發聲範圍 (85Hz 1100Hz)
複製代碼
人並非在85Hz~1100Hz全部的聲音都是能聽到的,還要取決於響度
當頻率很低的時候須要更大的響度(振幅)才能被聽到
最簡單的響度-頻率關係圖以下(圖是我用ps修的,若是有誤,歡迎指正):
可見在3KHz~5KHz的閥值較小,也就是更容易聽到java
當某個時刻響起一個高分貝的聲音,它周圍會出現遮蔽區域
如在轟鳴的機械運轉中(紅色),工人普通語言交流(灰色)是困難的
在遮蔽區域內的聲音人耳是沒法識別的,這時能夠提升音量,突破閥值,達到有效聽覺區android
時域掩蔽
掩蔽聲音與被掩蔽聲音不一樣時出現時
若掩蔽聲音出現以前的一段時間內發生掩蔽效應,稱:超前掩蔽(pre-masking)
不然滯後掩蔽(post-masking)
產生時域掩蔽的主要緣由是人的大腦處理信息須要花費必定的時間
通常來講,超前掩蔽很短,只有大約5~20 ms,而滯後掩蔽能夠持續50~200 msgit
模擬信號的採集過程當中,無論人耳的能不能識別,它把能記錄的都記錄了
從而會產生一些人耳沒法識別的冗餘數據,這些數據顯然咱們是不想要的
在進行採樣以前,先結合心理聲學模型處理,可縮小採樣範圍,儘可能去除掉無用的信息github
科普就這麼多,有個印象就行,平時拿來吹吹牛仍是夠的,下面進入正題編程
PCM(Pulse Code Modulation)--脈衝編碼調製,今天只說PCM數組
主要過程是將話音、圖像等模擬信號每隔必定時間進行取樣,使其離散化,
同時將抽樣值按分層單位四捨五入取整量化,同時將抽樣值按一組二進制碼來表示抽樣脈衝的幅值
PCM編碼:最大程度的接近絕對保真,可是體積大
複製代碼
圖書館裏很差意思說話,僞裝咳嗽了兩聲:(用軟件AU打開的)緩存
動態權限申請這裏不說了,本身解決(錄音也要動態權限的)bash
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
複製代碼
界面很簡單,中間是幀動畫,按下時開啓,離開時中止並回到第一幀
按下時開啓錄音,手離開時中止錄音,最後在左邊顯示錄音時長,素材在源碼裏微信
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;
}
});
複製代碼
PCMRecordTask.java
錄音流程簡單示意圖/**
* 做者:張風捷特烈<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"));
}
}
}
}
}
}
複製代碼
/**
* 做者:張風捷特烈<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);
}
複製代碼
這裏文件的建立就不廢話了,採用時間做爲文件名(已封裝)
/**
* 開啓錄音
*/
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() + "秒");
}
複製代碼
若是錄音是模擬信號到數字信號的編碼,那麼播放則是數字信號到模擬信號的解碼
須要用到的類就是AudioTrack,注意怎麼編的碼就怎麼解,否則確定有問題嘛
/**
* 做者:張風捷特烈
* 時間: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();
}
}
}
}
複製代碼
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應該是四捨五入的)
複製代碼
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-3 | Android多媒體之認識聲音、錄音與播放(PCM) |
V0.1-github | 2018-1-4 | 碼率的計算稍做修改 |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人掘金 | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持