上一期剛剛掀完桌子沒多久《Android MP3錄製,波形顯示,音訊許可權相容與播放》,就有小夥伴問我:“一個音訊的網路地址,如何根據這個獲取它的波形圖?”··· WTF(ノಠ益ಠ)ノ彡┻━┻,那一瞬間那是熱淚盈眶啊,為什麼我就沒想到呢···反正肯定不是為了再水一篇文章就對了<( ̄︶ ̄)>。
我是DEMO,快點我點我
Android的音訊播放與錄製
MediaPlayer、MediaRecord、AudioRecord,這三個都是大家耳目能詳的Android多媒體類(= =沒聽過的也要假裝聽過),包含了音視訊播放,音視訊錄製等…
但是還有一個被遺棄的熊孩子AudioTrack,這個因為太不好用了而被人過門而不入(反正肯定不是因為懶),這Android上多媒體四大家族就齊了,MediaPlayer、MediaRecord是封裝好了的錄製與播放,AudioRecord、AudioTrack是需要對資料和自定義有一定需要的時候用到的。(什麼,還有SoundPool?我不聽我不聽…)
MP3的波形資料提取
當那位小夥提出這個需求的時候,我就想起了AudioTrack這個類,和AudioRecord功能的使用方法十分相似,使用的時候初始化好之後對資料的buffer執行write就可以發出呻吟了,因為資料是read出來的,所以你可以對音訊資料做任何你愛做的事情。
但是問題來了,首先AudioTrack只能播放PCM的原始音訊檔案,那要MP3怎麼辦?這時候萬能的Google告訴了我一個方向,“移植Libmad到android平臺”,類似上篇文章中利用mp3lame實現邊錄邊轉碼的功能(有興趣的朋友可以看一下,很不錯)。
但WTF(ノಠ益ಠ)ノ彡┻━┻,這麼重的模式怎麼適合我們敏(lan)捷(ren)開發呢,除錯JNI各種躺坑呢。這時候作為一個做責任的社會主義青少年,我發現了這個MP3RadioStreamPlayer,看簡介:An MP3 online Stream player that uses MediaExtractor, MediaFormat, MediaCodec and AudioTrack meant as an alternative to using MediaPlayer.…嗯~臨表涕零,不知所言。
MediaCodec解碼
4.1以上Android系統(這和支援所有系統有什麼區別),支援mp3,wma等,可以用於編解碼,感謝上帝,以前的自己真的孤陋顧問了。
其中MediaExtractor,我們需要支援網路資料,這個類可以負責中間的過程,即將從DataSource得到的原始資料解析成解碼器需要的es資料,並通過MediaSource的介面輸出。
下面直接看程式碼吧,都有註釋(真的不是懶得講╮(╯_╰)╭):
流程就是定義好buffer,初始化MediaExtractor來獲取資料,MediaCodec對資料進行解碼,初始化AudioTrack播放資料。
- 因為上一期的波形播放資料是short形狀的,所以我們為了相容就把資料轉為short,這裡要注意合成short可能有大小位的問題,然後計算音量用於提取特徵值。
ByteBuffer[] codecInputBuffers;
ByteBuffer[] codecOutputBuffers;
// 這裡配置一個路徑檔案
extractor = new MediaExtractor();
try {
extractor.setDataSource(this.mUrlString);
} catch (Exception e) {
mDelegateHandler.onRadioPlayerError(MP3RadioStreamPlayer.this);
return;
}
//獲取多媒體檔案資訊
MediaFormat format = extractor.getTrackFormat(0);
//媒體型別
String mime = format.getString(MediaFormat.KEY_MIME);
// 檢查是否為音訊檔案
if (!mime.startsWith("audio/")) {
Log.e("MP3RadioStreamPlayer", "不是音訊檔案!");
return;
}
// 聲道個數:單聲道或雙聲道
int channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
// if duration is 0, we are probably playing a live stream
//時長
duration = format.getLong(MediaFormat.KEY_DURATION);
// System.out.println("歌曲總時間秒:"+duration/1000000);
//時長
int bitrate = format.getInteger(MediaFormat.KEY_BIT_RATE);
// the actual decoder
try {
// 例項化一個指定型別的解碼器,提供資料輸出
codec = MediaCodec.createDecoderByType(mime);
} catch (IOException e) {
e.printStackTrace();
}
codec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
codec.start();
// 用來存放目標檔案的資料
codecInputBuffers = codec.getInputBuffers();
// 解碼後的資料
codecOutputBuffers = codec.getOutputBuffers();
// get the sample rate to configure AudioTrack
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
// 設定聲道型別:AudioFormat.CHANNEL_OUT_MONO單聲道,AudioFormat.CHANNEL_OUT_STEREO雙聲道
int channelConfiguration = channels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
//Log.i(TAG, "channelConfiguration=" + channelConfiguration);
Log.i(LOG_TAG, "mime " + mime);
Log.i(LOG_TAG, "sampleRate " + sampleRate);
// create our AudioTrack instance
audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
channelConfiguration,
AudioFormat.ENCODING_PCM_16BIT,
AudioTrack.getMinBufferSize(
sampleRate,
channelConfiguration,
AudioFormat.ENCODING_PCM_16BIT
),
AudioTrack.MODE_STREAM
);
//開始play,等待write發出聲音
audioTrack.play();
extractor.selectTrack(0);//選擇讀取音軌
// start decoding
final long kTimeOutUs = 10000;//超時
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
// 解碼
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int noOutputCounter = 0;
int noOutputCounterLimit = 50;
while (!sawOutputEOS && noOutputCounter < noOutputCounterLimit && !doStop) {
//Log.i(LOG_TAG, "loop ");
noOutputCounter++;
if (!sawInputEOS) {
inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
bufIndexCheck++;
// Log.d(LOG_TAG, " bufIndexCheck " + bufIndexCheck);
if (inputBufIndex >= 0) {
ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
int sampleSize =
extractor.readSampleData(dstBuf, 0 /* offset */);
long presentationTimeUs = 0;
if (sampleSize < 0) {
Log.d(LOG_TAG, "saw input EOS.");
sawInputEOS = true;
sampleSize = 0;
} else {
presentationTimeUs = extractor.getSampleTime();
}
// can throw illegal state exception (???)
codec.queueInputBuffer(
inputBufIndex,
0 /* offset */,
sampleSize,
presentationTimeUs,
sawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
if (!sawInputEOS) {
extractor.advance();
}
} else {
Log.e(LOG_TAG, "inputBufIndex " + inputBufIndex);
}
}
// decode to PCM and push it to the AudioTrack player
// 解碼資料為PCM
int res = codec.dequeueOutputBuffer(info, kTimeOutUs);
if (res >= 0) {
//Log.d(LOG_TAG, "got frame, size " + info.size + "/" + info.presentationTimeUs);
if (info.size > 0) {
noOutputCounter = 0;
}
int outputBufIndex = res;
ByteBuffer buf = codecOutputBuffers[outputBufIndex];
final byte[] chunk = new byte[info.size];
buf.get(chunk);
buf.clear();
if (chunk.length > 0) {
//播放
audioTrack.write(chunk, 0, chunk.length);
//根據資料的大小為把byte合成short檔案
//然後計算音訊資料的音量用於判斷特徵
short[] music = (!isBigEnd()) ? byteArray2ShortArrayLittle(chunk, chunk.length / 2) :
byteArray2ShortArrayBig(chunk, chunk.length / 2);
sendData(music, music.length);
calculateRealVolume(music, music.length);
if (this.mState != State.Playing) {
mDelegateHandler.onRadioPlayerPlaybackStarted(MP3RadioStreamPlayer.this);
}
this.mState = State.Playing;
hadPlay = true;
}
//釋放
codec.releaseOutputBuffer(outputBufIndex, false /* render */);
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(LOG_TAG, "saw output EOS.");
sawOutputEOS = true;
}
} else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
Log.d(LOG_TAG, "output buffers have changed.");
} else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat oformat = codec.getOutputFormat();
Log.d(LOG_TAG, "output format has changed to " + oformat);
} else {
Log.d(LOG_TAG, "dequeueOutputBuffer returned " + res);
}
}
Log.d(LOG_TAG, "stopping...");
relaxResources(true);
this.mState = State.Stopped;
doStop = true;
// attempt reconnect
if (sawOutputEOS) {
try {
if (isLoop || !hadPlay) {
MP3RadioStreamPlayer.this.play();
}
return;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}複製程式碼
顯示波形和提取特徵
既然都有資料了,那還愁什麼波形,和上一期一樣直接傳┑( ̄Д  ̄)┍入AudioWaveView的List就好啦。
提取特徵
這裡曾經有過一個坑,躺屍好久,那時候的我還是個通訊工程的孩紙,滿腦子什麼FFT快速傅立葉變化,求包絡,自相關,卷積什麼的,然後就從網上扒了一套演算法很開心的計算頻率和頻譜,最後實現的效果很是堪憂,特別是錄音條件下的實時效果很差,誰讓我數學不是別人家的孩子呢┑( ̄Д  ̄)┍。
反正這次實現的沒那麼高深,很low的做法:
- 先計算當前資料的音量大小(用上期MP3處理的方法)
- 設定一個閾值
- 判斷閾值,與上一個資料比對
- 符合就改變顏色
if (mBaseRecorder == null)
return;
//獲取音量大小
int volume = mBaseRecorder.getRealVolume();
//Log.e("volume ", "volume " + volume);
//縮減過濾掉小資料
int scale = (volume / 100);
//是否大於給定閾值
if (scale < 5) {
mPreFFtCurrentFrequency = scale;
return;
}
//這個資料和上個資料之間的比例
int fftScale = 0;
if (mPreFFtCurrentFrequency != 0) {
fftScale = scale / mPreFFtCurrentFrequency;
}
//如果連續幾個或者大了好多就可以改變顏色
if (mColorChangeFlag == 4 || fftScale > 10) {
mColorChangeFlag = 0;
}
if (mColorChangeFlag == 0) {
if (mColorPoint == 1) {
mColorPoint = 2;
} else if (mColorPoint == 2) {
mColorPoint = 3;
} else if (mColorPoint == 3) {
mColorPoint = 1;
}
int color;
if (mColorPoint == 1) {
color = mColor1;
} else if (mColorPoint == 2) {
color = mColor3;
} else {
color = mColor2;
}
mPaint.setColor(color);
}
mColorChangeFlag++;
//儲存資料
if (scale != 0)
mPreFFtCurrentFrequency = scale;
...
/**
* 此計算方法來自samsung開發範例
*
* @param buffer buffer
* @param readSize readSize
*/
protected void calculateRealVolume(short[] buffer, int readSize) {
double sum = 0;
for (int i = 0; i < readSize; i++) {
// 這裡沒有做運算的優化,為了更加清晰的展示程式碼
sum += buffer[i] * buffer[i];
}
if (readSize > 0) {
double amplitude = sum / readSize;
mVolume = (int) Math.sqrt(amplitude);
}
}複製程式碼
怎麼樣,很簡單是吧,有沒感覺又被我水了一篇<( ̄︶ ̄)>,不知道你有沒有收穫呢,歡迎留言喲。
最後收兩句:
有時候會聽到有人說做業務程式碼只是在搬磚,對自己的技術沒有什麼提升,這種理論我個人並不是十分認同的,因為相對於自己開源和學習新的技術,業務程式碼可以讓你更加嚴謹的對待你的程式碼,會遇到更多你無法迴避的問題,各種各類的坑才是你提升的關鍵,當前,前提是你能把各種坑都儲存好,不要每次都跳進去。所以,對你的工作好一些吧…..((/- -)/
個人Github : github.com/CarGuo