Android 音頻開發(fā) 目錄
- Android音頻開發(fā)(1):音頻相關(guān)知識
- Android音頻開發(fā)(2):使用AudioRecord錄制pcm格式音頻
- Android音頻開發(fā)(3):使用AudioRecord實(shí)現(xiàn)錄音的暫停和恢復(fù)
- Android音頻開發(fā)(4):PCM轉(zhuǎn)WAV格式音頻
- Android音頻開發(fā)(5):Mp3的錄制 - 編譯Lame源碼
- Android音頻開發(fā)(6):Mp3的錄制 - 使用Lame實(shí)時(shí)錄制MP3格式音頻
- Android音頻開發(fā)(7):音樂可視化-FFT頻譜圖
項(xiàng)目地址:https://github.com/zhaolewei/MusicVisualizer
視頻演示地址:https://www.bilibili.com/video/av30388154/
一、演示
image
二、實(shí)現(xiàn)
- 實(shí)現(xiàn)流程:
- 使用MediaPlayer播放傳入的音樂,并拿到mediaPlayerId
- 使用Visualizer類拿到拿到MediaPlayer播放中的音頻數(shù)據(jù)(wave/fft)
- 將數(shù)據(jù)用自定義控件展現(xiàn)出來
三、準(zhǔn)備工作
- 使用Visualizer需要錄音的動(dòng)態(tài)權(quán)限, 如果播放sd卡音頻需要STORAGE權(quán)限
private static final String[] PERMISSIONS = new String[]{
Manifest.permission.RECORD_AUDIO,
Manifest.permission.MODIFY_AUDIO_SETTINGS
};
ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS, 1);
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
四、開始播放
private MediaPlayer.OnPreparedListener preparedListener = new
/**
* 播放音頻
*
* @param raw 資源文件id
*/
private void doPlay(final int raw) {
try {
mediaPlayer = MediaPlayer.create(MyApp.getInstance(), raw);
if (mediaPlayer == null) {
Logger.e(TAG, "mediaPlayer is null");
return;
}
mediaPlayer.setOnErrorListener(errorListener);
mediaPlayer.setOnPreparedListener(preparedListener);
} catch (Exception e) {
Logger.e(e, TAG, e.getMessage());
}
}
/**
* 獲取MediaPlayerId
* 可視化類Visualizer需要此參數(shù)
* @return MediaPlayerId
*/
public int getMediaPlayerId() {
return mediaPlayer.getAudioSessionId();
}
五、使用可視化類Visualizer獲取當(dāng)前音頻數(shù)據(jù)
Visualizer 有兩個(gè)比較重要的參數(shù)
- 設(shè)置可視化數(shù)據(jù)的數(shù)據(jù)大小 范圍[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()[1]]
- 社會(huì)可視化數(shù)據(jù)的采集頻率 范圍[0~Visualizer.getMaxCaptureRate()]
- OnDataCaptureListener 有2個(gè)回調(diào),一個(gè)用于顯示FFT數(shù)據(jù),展示不同頻率的振幅,另一個(gè)用于顯示聲音的波形圖
private Visualizer.OnDataCaptureListener dataCaptureListener = new Visualizer.OnDataCaptureListener() {
@Override
public void onWaveFormDataCapture(Visualizer visualizer, final byte[] waveform, int samplingRate) {
audioView.post(new Runnable() {
@Override
public void run() {
audioView.setWaveData(waveform);
}
});
}
@Override
public void onFftDataCapture(Visualizer visualizer, final byte[] fft, int samplingRate) {
audioView2.post(new Runnable() {
@Override
public void run() {
audioView2.setWaveData(fft);
}
});
}
};
private void initVisualizer() {
try {
int mediaPlayerId = mediaPlayer.getMediaPlayerId();
if (visualizer != null) {
visualizer.release();
}
visualizer = new Visualizer(mediaPlayerId);
/**
*可視化數(shù)據(jù)的大小: getCaptureSizeRange()[0]為最小值,getCaptureSizeRange()[1]為最大值
*/
int captureSize = Visualizer.getCaptureSizeRange()[1];
int captureRate = Visualizer.getMaxCaptureRate() * 3 / 4;
visualizer.setCaptureSize(captureSize);
visualizer.setDataCaptureListener(dataCaptureListener, captureRate, true, true);
visualizer.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED);
visualizer.setEnabled(true);
} catch (Exception e) {
Logger.e(TAG, "請檢查錄音權(quán)限");
}
}
波形數(shù)據(jù)和傅里葉數(shù)據(jù)的關(guān)系請看面這張圖:
image
快速傅里葉轉(zhuǎn)換(FFT)詳細(xì)分析: https://zhuanlan.zhihu.com/p/19763358
六、編寫自定義控件,展示數(shù)據(jù)
- 處理數(shù)據(jù): visualizer 回調(diào)中的數(shù)據(jù)中是存在負(fù)數(shù)的,需要轉(zhuǎn)換一下,用于顯示
- 當(dāng)byte 為 -128時(shí)Math.abs(fft[i]) 計(jì)算出來的值會(huì)越界,需要手動(dòng)處理一下
byte 的范圍: -128~127
/**
* 預(yù)處理數(shù)據(jù)
*
* @return
*/
private static byte[] readyData(byte[] fft) {
byte[] newData = new byte[LUMP_COUNT];
byte abs;
for (int i = 0; i < LUMP_COUNT; i++) {
abs = (byte) Math.abs(fft[i]);
//描述:Math.abs -128時(shí)越界
newData[i] = abs < 0 ? 127 : abs;
}
return newData;
}
- 緊接著就是根據(jù)數(shù)據(jù)去繪制圖形
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
wavePath.reset();
for (int i = 0; i < LUMP_COUNT; i++) {
if (waveData == null) {
canvas.drawRect((LUMP_WIDTH + LUMP_SPACE) * i,
LUMP_MAX_HEIGHT - LUMP_MIN_HEIGHT,
(LUMP_WIDTH + LUMP_SPACE) * i + LUMP_WIDTH,
LUMP_MAX_HEIGHT,
lumpPaint);
continue;
}
switch (upShowStyle) {
case STYLE_HOLLOW_LUMP:
drawLump(canvas, i, false);
break;
case STYLE_WAVE:
drawWave(canvas, i, false);
break;
default:
break;
}
switch (downShowStyle) {
case STYLE_HOLLOW_LUMP:
drawLump(canvas, i, true);
break;
case STYLE_WAVE:
drawWave(canvas, i, true);
break;
default:
break;
}
}
}
/**
* 繪制矩形條
*/
private void drawLump(Canvas canvas, int i, boolean reversal) {
int minus = reversal ? -1 : 1;
if (waveData[i] < 0) {
Logger.w("waveData", "waveData[i] < 0 data: %s", waveData[i]);
}
float top = (LUMP_MAX_HEIGHT - (LUMP_MIN_HEIGHT + waveData[i] * SCALE) * minus);
canvas.drawRect(LUMP_SIZE * i,
top,
LUMP_SIZE * i + LUMP_WIDTH,
LUMP_MAX_HEIGHT,
lumpPaint);
}
/**
* 繪制曲線
* 這里使用貝塞爾曲線來繪制
*/
private void drawWave(Canvas canvas, int i, boolean reversal) {
if (pointList == null || pointList.size() < 2) {
return;
}
float ratio = SCALE * (reversal ? -1 : 1);
if (i < pointList.size() - 2) {
Point point = pointList.get(i);
Point nextPoint = pointList.get(i + 1);
int midX = (point.x + nextPoint.x) >> 1;
if (i == 0) {
wavePath.moveTo(point.x, LUMP_MAX_HEIGHT - point.y * ratio);
}
wavePath.cubicTo(midX, LUMP_MAX_HEIGHT - point.y * ratio,
midX, LUMP_MAX_HEIGHT - nextPoint.y * ratio,
nextPoint.x, LUMP_MAX_HEIGHT - nextPoint.y * ratio);
canvas.drawPath(wavePath, lumpPaint);
}
}