今天比較簡單,先理一下錄製和播放的四位大將
再說一下SoundPool的使用和pcm轉wav
講一下C++文件如何在Android中使用,也就是傳說中的JNI
最後講一下變速播放和變調播放html
第一天:
AudioRecord(錄音)
、AudioTrack(音頻播放)
次日:MediaPlayer(媒體播放器--音頻部分)
第三天:MediaRecorder(媒體播放器--錄音部分)
git
優勢:
對音頻的實時處理,適合流媒體和語音電話
缺點:
輸出的是PCM的語音數據,須要本身處理字節數據
若是保存成音頻文件不能被播放器播放
PCM採集的數據須要AudioTrack播放,AudioTrack也能夠將PCM的數據轉換成其餘格式
複製代碼
int audioSource
int channelConfig
錄音的聲道信息是加IN的github
audioFormat
優勢:
MediaRecorder錄製的音頻文件是通過壓縮後的
已集成了錄音,編碼,壓縮等,支持一些的音頻格式文件(.arm,.mp3,.3gp,.aac,.mp4,.webm)
操做簡單,不須本身處理字節流,傳入文件便可
缺點:
沒法實現實時處理音頻,輸出的音頻格式少。
複製代碼
int audio_source
和AudioRecord的基本一致web
int output_format
int video_encoder
AudioTrack只能播放已經解碼的PCM流(wav音頻格式文件)
複製代碼
int streamType
int mode
MODE_STREAM:適合大文件
經過write一次次把音頻數據寫到AudioTrack中。
用戶提供的Buffer數據-->AudioTrack內部的Buffer,這在必定程度上會使引入延時。
MODE_STATIC:適合小文件
全部數據經過一次write調用傳遞到AudioTrack中的內部緩衝區。
這種模式適用於像鈴聲這種內存佔用量較小,延時要求較高的文件。
複製代碼
int channelConfig
錄音的聲道信息是加OUT的算法
int audioFormat
這個和AudioRecord同樣編程
MediaPlayer能夠播放多種格式的聲音文件(mp3,w4a,aac)
MediaPlayer在framework層也實例化了AudioTrack,
其實質是MediaPlayer在framework層進行解碼後,生成PCM流,而後代理委託給AudioTrack,
最後AudioTrack傳遞給AudioFlinger進行混音,而後才傳遞給硬件播放
複製代碼
話說
殺雞焉用牛刀
,對於常常播放比較短小的音效,用SoundPool更好
SoundPool源碼就616行,小巧不少,看到pool確定是池啦數組
作一個兩個音效每次點擊依次播放一個的效果緩存
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));
}
複製代碼
注意:資源加載完成會稍遲一些,若是加載和播放在上下行執行會無效
你能夠初始時加載,稍後有動做再播放,也能夠進行加完成載監聽bash
public void onViewClicked() {
//資源Id,左音量,右音量,優先級,循環次數,速率
int id = mSoundMap.get(isOne ? "effect1" : "effect2");
mSp.play(id, 1.0f, 1.0f, 1, 2, 1.0f);
isOne = !isOne;
}
複製代碼
三個參數:soundPool,第幾個,狀態(0==success)微信
mSp.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
});
複製代碼
二者區別:pcm是沒法被播放器播放的,wav能夠被播放器播放
但它們的實質幾乎同樣,wav至關於披了件衣服(文件頭),讓播放器認識它
pcm轉爲wav並不複雜,就加個頭就好了,網上有不少,這裏參見
符合 RIFF(Resource Interchange FileFormat)規範。
全部的WAV都有一個文件頭,這個文件頭音頻流的編碼參數。
數據塊的記錄方式是little-endian字節順序,標誌符並非字符串而是單獨的符號
複製代碼
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);
}
}
複製代碼
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);
複製代碼
[1] 音量 :(響度)聲波震動幅度---A--分貝
[2] 音調 : 聲音頻率(高音--頻率快--聲音尖 低音--頻率慢--聲音沉)----f--Hz
[3] 音色 :(音品)與材質有關 本質是諧波
複製代碼
變速的實現:
播放時採樣頻率進行倍速,使得週期發生變化。
如兩倍速時,採樣頻率*2,波的週期減半,原本2s的波,1s就能放完
因爲聲音頻率變化,聲音的效果也隨之變化
如2倍速時:頻率快,高音,聲音尖,0.5倍速時:頻率慢,低音,聲音沉
2倍速是就像一些短視頻的倍速變聲配音,0.5倍速時就像怪獸的吼聲...
複製代碼
第一天已經實現了播放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();
}
}
複製代碼
佈局挺簡單的,不廢話了
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));
});
複製代碼
本段參考
慕課網免費教程
:詳見
兩個臨時的float數組是爲了和C++的函數對應,用來處理數據流的
/**
* 做者:張風捷特烈<br/>
* 時間:2019/1/7 0007:9:50<br/>
* 郵箱:1981462002@qq.com<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);
}
複製代碼
#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);
}
複製代碼
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);
複製代碼
佈局基本同樣,在拖拽時設置變聲的分率,點擊也就播放而已
有個問題,也就是吱吱的聲音,通過測試,發現是bufferSize的鍋
若是讀取時的緩衝大小和AudioEffect緩衝大小同樣,會吱吱地響
通過一點點的調參,發現mMinBufferSize/3.388598效果還行,有一點點吱吱
最後打印一下mMinBufferSize = 7072 ,7072*/3.388598=2086.99
而後靈機一動,不就是2048嗎?------而後完美解決...費了我一個多小時...心塞
ok,就這樣,我能夠很認真的說...到這裏剛摸到Android多媒體的門(也就是入門都沒有)
項目源碼 | 日期 | 備註 |
---|---|---|
V0.1-github | 2018-1-7 | Android多媒體之SoundPool+pcm流的音頻操做 |
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
個人github | 個人簡書 | 個人掘金 | 我的網站 |
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大編程愛好者共同交流
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持