Android多媒體之SoundPool+pcm流的音訊操作
零、前言
今天比較簡單,先理一下錄製和播放的四位大將
再說一下SoundPool的使用和pcm轉wav
講一下C++檔案如何在Android中使用,也就是傳說中的JNI
最後講一下變速播放和變調播放
一、AudioRecord和MediaRecorder,AudioTrack和MediaPlayer
0.到現在接觸了四個類:
第一天:
AudioRecord(錄音)
、AudioTrack(音訊播放)
第二天:MediaPlayer(媒體播放器--音訊部分)
第三天:MediaRecorder(媒體播放器--錄音部分)
1.AudioRecord(基於位元組流錄音)
優點:
對音訊的實時處理,適合流媒體和語音電話
缺點:
輸出的是PCM的語音資料,需要自己處理位元組資料
如果儲存成音訊檔案不能被播放器播放
PCM採集的資料需要AudioTrack播放,AudioTrack也可以將PCM的資料轉換成其他格式
複製程式碼
1.1:音訊來源:int audioSource
1.2:聲道資訊:int channelConfig
錄音的聲道資訊是加IN的
1.3:資料輸出格式:audioFormat
2.MediaRecorder(基於檔案錄音)
優點:
MediaRecorder錄製的音訊檔案是經過壓縮後的
已集成了錄音,編碼,壓縮等,支援一些的音訊格式檔案(.arm,.mp3,.3gp,.aac,.mp4,.webm)
操作簡單,不須自己處理位元組流,傳入檔案即可
缺點:
無法實現實時處理音訊,輸出的音訊格式少。
複製程式碼
2.1:音訊來源:int audio_source
和AudioRecord的基本一致
2.2:輸出格式:int output_format
2.3:音訊編碼方式:int video_encoder
3.AudioTrack
AudioTrack只能播放已經解碼的PCM流(wav音訊格式檔案)
複製程式碼
3.1:流型別:int streamType
3.2:模式:int mode
MODE_STREAM:適合大檔案
通過write一次次把音訊資料寫到AudioTrack中。
使用者提供的Buffer資料-->AudioTrack內部的Buffer,這在一定程度上會使引入延時。
MODE_STATIC:適合小檔案
所有資料通過一次write呼叫傳遞到AudioTrack中的內部緩衝區。
這種模式適用於像鈴聲這種記憶體佔用量較小,延時要求較高的檔案。
複製程式碼
3.3:播放聲道:int channelConfig
錄音的聲道資訊是加OUT的
3.4:資料輸出格式:int audioFormat
這個和AudioRecord一樣
4.MediaPlayer
MediaPlayer可以播放多種格式的聲音檔案(mp3,w4a,aac)
MediaPlayer在framework層也例項化了AudioTrack,
其實質是MediaPlayer在framework層進行解碼後,生成PCM流,然後代理委託給AudioTrack,
最後AudioTrack傳遞給AudioFlinger進行混音,然後才傳遞給硬體播放
複製程式碼
二、SoundPool的使用
話說
殺雞焉用牛刀
,對於經常播放比較短小的音效,用SoundPool更好
SoundPool原始碼就616行,小巧很多,看到pool肯定是池啦
1.初始化
做一個兩個音效每次點選依次播放一個的效果
private SoundPool mSp;
private HashMap<String, Integer> mSoundMap = new HashMap<>();
private boolean isOne;
private void initSound() {
SoundPool.Builder spb = new SoundPool.Builder();
//設定可以同時播放的同步流的最大數量
spb.setMaxStreams(10);
//建立SoundPool物件
mSp = spb.build();
mSoundMap.put("effect1", mSp.load(this, R.raw.fall, 1));
mSoundMap.put("effect2", mSp.load(this, R.raw.luozi, 1));
}
複製程式碼
2.播放
注意:資源載入完成會稍遲一些,如果載入和播放在上下行執行會無效
你可以初始時載入,稍後有動作再播放,也可以進行加完成載監聽
public void onViewClicked() {
//資源Id,左音量,右音量,優先順序,迴圈次數,速率
int id = mSoundMap.get(isOne ? "effect1" : "effect2");
mSp.play(id, 1.0f, 1.0f, 1, 2, 1.0f);
isOne = !isOne;
}
複製程式碼
3.載入完成監聽
三個引數:soundPool,第幾個,狀態(0==success)
mSp.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
});
複製程式碼
三、pcm與wav
兩者區別:pcm是無法被播放器播放的,wav可以被播放器播放
但它們的實質幾乎一樣,wav相當於披了件衣服(檔案頭),讓播放器認識它
pcm轉為wav並不複雜,就加個頭就行了,網上有很多,這裡參見
符合 RIFF(Resource Interchange FileFormat)規範。
所有的WAV都有一個檔案頭,這個檔案頭音訊流的編碼引數。
資料塊的記錄方式是little-endian位元組順序,標誌符並不是字串而是單獨的符號
複製程式碼
1.程式碼實現:PcmToWavUtil
public class PcmToWavUtil {
/**
* 快取的音訊大小
*/
private int mBufferSize;
/**
* 取樣率
*/
private int mSampleRate;
/**
* 聲道數
*/
private int mChannel;
/**
* @param sampleRate sample rate、取樣率
* @param channel channel、聲道
* @param encoding Audio data format、音訊格式
*/
public PcmToWavUtil(int sampleRate, int channel, int encoding) {
this.mSampleRate = sampleRate;
this.mChannel = channel;
this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);
}
/**
* pcm檔案轉wav檔案
*
* @param inFilename 原始檔路徑
* @param outFilename 目標檔案路徑
*/
public void pcmToWav(String inFilename, String outFilename) {
FileInputStream in;
FileOutputStream out;
long totalAudioLen;
long totalDataLen;
long longSampleRate = mSampleRate;
int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
long byteRate = 16 * mSampleRate * channels / 8;
byte[] data = new byte[mBufferSize];
try {
in = new FileInputStream(inFilename);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out, totalAudioLen, totalDataLen,
longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 加入wav檔案頭
*/
private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
long totalDataLen, long longSampleRate, int channels, long byteRate)
throws IOException {
byte[] header = new byte[44];
// RIFF/WAVE header
header[0] = 'R';
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);
//WAVE
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
// 'fmt ' chunk
header[12] = 'f';
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
// 4 bytes: size of 'fmt ' chunk
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
// format = 1
header[20] = 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);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// block align
header[32] = (byte) (2 * 16 / 8);
header[33] = 0;
// bits per sample
header[34] = 16;
header[35] = 0;
//data
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
}
複製程式碼
2.使用:
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
String inPath = "/sdcard/pcm錄音/keke.pcm";
String outPath = "/sdcard/pcm錄音/keke.wav";
PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(DEFAULT_SAMPLE_RATE,DEFAULT_CHANNEL_CONFIG,DEFAULT_AUDIO_FORMAT);
pcmToWavUtil.pcmToWav(inPath,outPath);
複製程式碼
四、變速播放
0.回顧一下第一天對聲音的介紹:聲音三要素
[1] 音量 :(響度)聲波震動幅度---A--分貝
[2] 音調 : 聲音訊率(高音--頻率快--聲音尖 低音--頻率慢--聲音沉)----f--Hz
[3] 音色 :(音品)與材質有關 本質是諧波
複製程式碼
變速的實現:
播放時取樣頻率進行倍速,使得週期發生變化。
如兩倍速時,取樣頻率*2,波的週期減半,本來2s的波,1s就能放完
由於聲音訊率變化,聲音的效果也隨之變化
如2倍速時:頻率快,高音,聲音尖,0.5倍速時:頻率慢,低音,聲音沉
2倍速是就像一些短視訊的倍速變聲配音,0.5倍速時就像怪獸的吼聲...
複製程式碼
1.程式碼實現
第一天已經實現了播放pcm流的程式碼,基於此修改一下
AudioTrack在讀pcm時可以設定取樣頻率,抽成變數傳進去就行了
/**
* 啟動播放
*
* @param path 檔案了路徑
*/
public void startPlay(String path, int rate) {
try {
isStart = true;
setPath(path);//設定路徑--生成流dis
mMinBufferSize = AudioTrack.getMinBufferSize(
rate, DEFAULT_CHANNEL_CONFIG, AudioFormat.ENCODING_PCM_16BIT);
//例項化AudioTrack
audioTrack = new AudioTrack(
DEFAULT_STREAM_TYPE, rate, DEFAULT_CHANNEL_CONFIG,
DEFAULT_AUDIO_FORMAT, mMinBufferSize * 2, DEFAULT_PLAY_MODE);
mExecutorService.execute(new PlayRunnable());//啟動播放執行緒
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
2.Activity中使用
佈局挺簡單的,不廢話了
private float rate = 1;
//SeekBar的滑動監聽
mIdSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
rate = progress / 100.f;
setInfo();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
//點選播放
mIvStartPlay.setOnClickListener(e -> {
PCMAudioPlayerWithRate.getInstance().startPlay("/sdcard/pcm錄音/20190107075814.pcm", (int) (44100 * rate));
});
複製程式碼
五、JNI的一些簡單認識
1.新建一個支援C++的Android專案,看一下有哪裡不同
2.app的gradle裡:
3.CMakeLists.txt何許人也
4.依葫蘆畫瓢
5.建立native函式
五、音調的變化
本段參考
慕課網免費教程
:詳見
1.Java類
兩個臨時的float陣列是為了和C++的函式對應,用來處理資料流的
/**
* 作者:張風捷特烈<br/>
* 時間:2019/1/7 0007:9:50<br/>
* 郵箱:[email protected]<br/>
* 說明:處理音調的變化
*/
public class AudioEffect {
private int mBufferSize;
private byte[] mOutBuffer;
private float[] mTempInBuffer;
private float[] mTempOutBuffer;
static {
//載入so庫
System.loadLibrary("audio-effect");
}
public AudioEffect(int bufferSize) {
mBufferSize = bufferSize;
mOutBuffer = new byte[mBufferSize];
mTempInBuffer = new float[mBufferSize/2];
mTempOutBuffer = new float[mBufferSize/2];
}
/**
* 資料處理
* @param rate 變換引數
* @param in 資料
* @param simpleRate 取樣頻率
* @return 處理後的資料流
*/
public synchronized byte[] process(float rate,byte[] in,int simpleRate) {
native_process(rate,in,mOutBuffer,mBufferSize,simpleRate,mTempInBuffer,mTempOutBuffer);
return mOutBuffer;
}
private static native void native_process(float rate, byte[] in, byte[] out, int size, int simpleRate,float[] tempIn, float[] tempOut);
}
複製程式碼
2.資料的處理:smbPitchShift.cpp
#include <jni.h>
extern "C"
JNIEXPORT void JNICALL
Java_top_toly_sound_audio_effect_AudioEffect_native_1process(JNIEnv *env, jclass type, jfloat rate,
jbyteArray in_, jbyteArray out_,
jint size, jint simpleRate,
jfloatArray tempIn_,
jfloatArray tempOut_) {
//array轉化為指標
jbyte *in = env->GetByteArrayElements(in_, NULL);
jbyte *out = env->GetByteArrayElements(out_, NULL);
jfloat *tempIn = env->GetFloatArrayElements(tempIn_, NULL);
jfloat *tempOut = env->GetFloatArrayElements(tempOut_, NULL);
// 輸入:byte[]轉為float[]
for (int i = 0; i < size; i += 2) {
int lo = in[i] & 0x000000FF;//取低位
int hi = in[i + 1] & 0x000000FF;//取高位
int frame = (hi << 8) + lo;//高位左移8位+低位
tempIn[i >> 1] = (signed short) frame;//
}
smbPitchShift(rate, 1024, 1024, 4, simpleRate, tempIn, tempOut);
//float[]輸出轉為byte
for (int i = 0; i < size; i += 2) {
int frame = (int) tempOut[i >> 1];
out[i] = (jbyte) (frame & 0x000000FF);//取第一個位元組
out[i + 1] = (jbyte) (frame >> 8);//右移8位,取第二個位元組
}
//釋放指標
env->ReleaseByteArrayElements(in_, in, 0);
env->ReleaseByteArrayElements(out_, out, 0);
env->ReleaseFloatArrayElements(tempIn_, tempIn, 0);
env->ReleaseFloatArrayElements(tempOut_, tempOut, 0);
}
複製程式碼
3.播放對流操作:PCMAudioPlayerWithRat中
//private float rate = 1;//音調分率
public void setRate(float rate) {
this.rate = rate;
}
//開始是初始化startPlay中-----
if (mAudioEffect == null) {
L.d(mMinBufferSize + L.l());//7072
mAudioEffect = new AudioEffect(2048);
}
//PlayRunnable中,讀流時對流進行處理
//對讀到的流進行處理
tempBuffer = rate == 1 ? tempBuffer :
mAudioEffect.process(rate, tempBuffer, DEFAULT_SAMPLE_RATE);
複製程式碼
4.Activity中播放
佈局基本一樣,在拖拽時設定變聲的分率,點選也就播放而已
5.小插曲
有個問題,也就是吱吱的聲音,經過測試,發現是bufferSize的鍋
如果讀取時的緩衝大小和AudioEffect緩衝大小一樣,會吱吱地響
經過一點點的調參,發現mMinBufferSize/3.388598效果還行,有一點點吱吱
最後列印一下mMinBufferSize = 7072 ,7072*/3.388598=2086.99
然後靈機一動,不就是2048嗎?------然後完美解決...費了我一個多小時...心塞
ok,就這樣,我可以很認真的說...到這裡剛摸到Android多媒體的門(也就是入門都沒有)
後記:捷文規範
1.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-7 | Android多媒體之SoundPool+pcm流的音訊操作 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援