Android音频开发
這篇博客 轉載自 https://www.jianshu.com/p/c0222de2faed
這里涉及到ndk的一些知識,對于.mk文件不太熟悉的同學要自己去 官網 或者搜索一些博客了解基本知識。
Android音頻開發
- 1. 音頻基礎知識
- 音頻基礎知識
- 常用音頻格式
- 音頻開發的主要應用
- 音頻開發的具體內容
- 2. 使用AudioRecord錄制pcm格式音頻
- AudioRecord類的介紹
- 實現
- 其他
- 3. 使用AudioRecord實現錄音的暫停和恢復
- 解決辦法
- 實現
- 其他
- 4. PCM轉WAV格式音頻
- wav 和 pcm
- WAV頭文件
- java 生成頭文件
- PCM轉Wav
- 參考鏈接:
- 5. Mp3的錄制 - 編譯Lame源碼
- 編譯 so包
- 編譯
- 6. Mp3的錄制 - 使用Lame實時錄制MP3格式音頻
- 前言
- 代碼實現
- 使用
- 7. 音樂可視化-FFT頻譜圖
- 實現
- 準備工作
- 開始播放
- 使用可視化類Visualizer獲取當前音頻數據
- 編寫自定義控件,展示數據
1. 音頻基礎知識
音頻基礎知識
采樣和采樣頻率:
現在是數字時代,在音頻處理時要先把音頻的模擬信號變成數字信號,這叫A/D轉換。要把音頻的模擬信號變成數字信號,就需要采樣。一秒鐘內采樣的次數稱為采樣頻率
采樣位數/位寬:
數字信號是用0和1來表示的。采樣位數就是采樣值用多少位0和1來表示,也叫采樣精度,用的位數越多就越接近真實聲音。如用8位表示,采樣值取值范圍就是-128 ~ 127,如用16位表示,采樣值取值范圍就是-32768 ~ 32767。
聲道(channel):
通常語音只用一個聲道。而對于音樂來說,既可以是單聲道(mono),也可以是雙聲道(即左聲道右聲道,叫立體聲stereo),還可以是多聲道,叫環繞立體聲。
編解碼 :
通常把音頻采樣過程也叫做脈沖編碼調制編碼,即PCM(Pulse Code Modulation)編碼,采樣值也叫PCM值。 如果把采樣值直接保存或者發送,會占用很大的存儲空間。以16kHz采樣率16位采樣位數單聲道為例,一秒鐘就有16/8*16000 = 32000字節。為了節省保存空間或者發送流量,會對PCM值壓縮。
目前主要有三大技術標準組織制定壓縮標準:
一些大公司或者組織也制定壓縮標準,比如iLBC,OPUS。
壓縮:
對于自然界中的音頻信號,如果轉換成數字信號,進行音頻編碼,那么只能無限接近,不可能百分百還原。所以說實際上任何信號轉換成數字信號都會“有損”。但是在計算機應用中,能夠達到最高保真水平的就是PCM編碼。因此,PCM約定俗成了無損編碼。我們而習慣性的把MP3列入有損音頻編碼范疇,是相對PCM編碼的。強調編碼的相對性的有損和無損
碼率:
碼率 = 采樣頻率 * 采樣位數 * 聲道個數; 例:采樣頻率44.1KHz,量化位數16bit,立體聲(雙聲道),未壓縮時的碼率 = 44.1KHz * 16 * 2 = 1411.2Kbps = 176.4KBps,即每秒要錄制的資源大小,理論上碼率和質量成正比。
常用音頻格式
WAV 格式:音質高 無損格式 體積較大
AAC(Advanced Audio Coding) 格式:相對于 mp3,AAC 格式的音質更佳,文件更小,有損壓縮,一般蘋果或者Android SDK4.1.2(API 16)及以上版本支持播放,性價比高
AMR 格式:壓縮比比較大,但相對其他的壓縮格式質量比較差,多用于人聲,通話錄音
mp3 格式:特點 使用廣泛, 有損壓縮,犧牲了12KHz到16KHz高音頻的音質
音頻開發的主要應用
- 音頻播放器
- 錄音機
- 語音電話
- 音視頻監控應用
- 音視頻直播應用
- 音頻編輯/處理軟件(ktv音效、變聲, 鈴聲轉換)
- 藍牙耳機/音箱
音頻開發的具體內容
- 音頻采集/播放
- 音頻算法處理(去噪、靜音檢測、回聲消除、音效處理、功放/增強、混音/分離,等等)
- 音頻的編解碼和格式轉換
- 音頻傳輸協議的開發(SIP,A2DP、AVRCP,等等)
2. 使用AudioRecord錄制pcm格式音頻
AudioRecord類的介紹
1. AudioRecord構造函數:
/*** @param audioSource :錄音源* 這里選擇使用麥克風:MediaRecorder.AudioSource.MIC* @param sampleRateInHz: 采樣率* @param channelConfig:聲道數 * @param audioFormat: 采樣位數.* See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT},* and {@link AudioFormat#ENCODING_PCM_FLOAT}.* @param bufferSizeInBytes: 音頻錄制的緩沖區大小* See {@link #getMinBufferSize(int, int, int)} */ public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)2. getMinBufferSize()
/** * 獲取AudioRecord所需的最小緩沖區大小 * @param sampleRateInHz: 采樣率 * @param channelConfig:聲道數 * @param audioFormat: 采樣位數. */ public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)3. getRecordingState()
/** * 獲取AudioRecord當前的錄音狀態 * @see AudioRecord#RECORDSTATE_STOPPED * @see AudioRecord#RECORDSTATE_RECORDING */ public int getRecordingState()4. startRecording()
/*** 開始錄制*/public int startRecording()5. stop()
/*** 停止錄制*/public int stop()6. read()
/*** 從錄音設備中讀取音頻數據* @param audioData 音頻數據寫入的byte[]緩沖區* @param offsetInBytes 偏移量* @param sizeInBytes 讀取大小* @return 返回負數則表示讀取失敗* see {@link #ERROR_INVALID_OPERATION} -3 : 初始化錯誤{@link #ERROR_BAD_VALUE} -3: 參數錯誤{@link #ERROR_DEAD_OBJECT} -6: {@link #ERROR} */ public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)實現
實現過程就是調用上面的API的方法,構造AudioRecord實例后再調用startRecording(),開始錄音,并通過read()方法不斷獲取錄音數據記錄下來,生成PCM文件。涉及耗時操作,所以最好在子線程中進行。
public class RecordHelper {//0.此狀態用于控制線程中的循環操作,應用volatile修飾,保持數據的一致性private volatile RecordState state = RecordState.IDLE;private AudioRecordThread audioRecordThread;private File tmpFile = null;public void start(String filePath, RecordConfig config) {if (state != RecordState.IDLE) {Logger.e(TAG, "狀態異常當前狀態: %s", state.name());return;}recordFile = new File(filePath);String tempFilePath = getTempFilePath();Logger.i(TAG, "tmpPCM File: %s", tempFilePath);tmpFile = new File(tempFilePath);//1.開啟錄音線程并準備錄音audioRecordThread = new AudioRecordThread();audioRecordThread.start();}public void stop() {if (state == RecordState.IDLE) {Logger.e(TAG, "狀態異常當前狀態: %s", state.name());return;}state = RecordState.STOP;}private class AudioRecordThread extends Thread {private AudioRecord audioRecord;private int bufferSize;AudioRecordThread() {//2.根據錄音參數構造AudioRecord實體對象bufferSize = AudioRecord.getMinBufferSize(currentConfig.getFrequency(),currentConfig.getChannel(), currentConfig.getEncoding()) * RECORD_AUDIO_BUFFER_TIMES;audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getFrequency(),currentConfig.getChannel(), currentConfig.getEncoding(), bufferSize);}@Overridepublic void run() {super.run();state = RecordState.RECORDING;Logger.d(TAG, "開始錄制");FileOutputStream fos = null;try {fos = new FileOutputStream(tmpFile);audioRecord.startRecording();byte[] byteBuffer = new byte[bufferSize];while (state == RecordState.RECORDING) {//3.不斷讀取錄音數據并保存至文件中int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);fos.write(byteBuffer, 0, end);fos.flush();}//4.當執行stop()方法后state != RecordState.RECORDING,終止循環,停止錄音audioRecord.stop();} catch (Exception e) {Logger.e(e, TAG, e.getMessage());} finally {try {if (fos != null) {fos.close();}} catch (IOException e) {Logger.e(e, TAG, e.getMessage());}}state = RecordState.IDLE;Logger.d(TAG, "錄音結束");}} }其他
- 這里實現了PCM音頻的錄制,AudioRecord
API中只有開始和停止的方法,在實際開發中可能還需要暫停/恢復的操作,以及PCM轉WAV的功能,下一篇再繼續完善。 - 需要錄音及文件處理的動態權限
3. 使用AudioRecord實現錄音的暫停和恢復
上一部分主要寫了AudioRecord實現音頻錄制的開始和停止,AudioRecord并沒有暫停和恢復播放功能的API,所以需要手動實現。
解決辦法
思路很簡單,現在可以實現音頻的文件錄制和停止,并生成pcm文件,那么暫停時將這次文件先保存下來,恢復播放后開始新一輪的錄制,那么最后會生成多個pcm音頻,再將這些pcm文件進行合并,這樣就實現了暫停/恢復的功能了。
實現
- 實現的重點在于如何控制錄音的狀態
其他
在此后如若需要添加錄音狀態回調,記得使用Handler做好線程切換。
4. PCM轉WAV格式音頻
前面幾部分已經介紹了PCM音頻文件的錄制,這一部分主要介紹下pcm轉wav。
wav 和 pcm
一般通過麥克風采集的錄音數據都是PCM格式的,即不包含頭部信息,播放器無法知道音頻采樣率、位寬等參數,導致無法播放,顯然是非常不方便的。pcm轉換成wav,我們只需要在pcm的文件起始位置加上至少44個字節的WAV頭信息即可。
RIFF
- WAVE文件是以RIFF(Resource Interchange File Format, “資源交互文件格式”)格式來組織內部結構的。
RIFF文件結構可以看作是樹狀結構,其基本構成是稱為"塊"(Chunk)的單元. - WAVE文件是由若干個Chunk組成的。按照在文件中的出現位置包括:RIFF WAVE Chunk, Format Chunk, Fact Chunk(可選), Data Chunk。
WAV頭文件
所有的WAV都有一個文件頭,這個文件頭記錄著音頻流的編碼參數。數據塊的記錄方式是little-endian字節順序。
| 00-03 | ChunkId | “RIFF” |
| 04-07 | ChunkSize | 下個地址開始到文件尾的總字節數(此Chunk的數據大小) |
| 08-11 | fccType | “WAVE” |
| 12-15 | SubChunkId1 | "fmt ",最后一位空格。 |
| 16-19 | SubChunkSize1 | 一般為16,表示fmt Chunk的數據塊大小為16字節,即20-35 |
| 20-21 | FormatTag | 1:表示是PCM 編碼 |
| 22-23 | Channels | 聲道數,單聲道為1,雙聲道為2 |
| 24-27 | SamplesPerSec | 采樣率 |
| 28-31 | BytesPerSec | 碼率 :采樣率 * 采樣位數 * 聲道個數,bytePerSecond = sampleRate * (bitsPerSample / 8) * channels |
| 32-33 | BlockAlign | 每次采樣的大小:位寬*聲道數/8 |
| 34-35 | BitsPerSample | 位寬 |
| 36-39 | SubChunkId2 | “data” |
| 40-43 | SubChunkSize2 | 音頻數據的長度 |
| 44-… | data | 音頻數據 |
java 生成頭文件
WavHeader.class
public static class WavHeader {/*** RIFF數據塊*/final String riffChunkId = "RIFF";int riffChunkSize;final String riffType = "WAVE";/*** FORMAT 數據塊*/final String formatChunkId = "fmt ";final int formatChunkSize = 16;final short audioFormat = 1;short channels;int sampleRate;int byteRate;short blockAlign;short sampleBits;/*** FORMAT 數據塊*/final String dataChunkId = "data";int dataChunkSize;WavHeader(int totalAudioLen, int sampleRate, short channels, short sampleBits) {this.riffChunkSize = totalAudioLen;this.channels = channels;this.sampleRate = sampleRate;this.byteRate = sampleRate * sampleBits / 8 * channels;this.blockAlign = (short) (channels * sampleBits / 8);this.sampleBits = sampleBits;this.dataChunkSize = totalAudioLen - 44;}public byte[] getHeader() {byte[] result;result = ByteUtils.merger(ByteUtils.toBytes(riffChunkId), ByteUtils.toBytes(riffChunkSize));result = ByteUtils.merger(result, ByteUtils.toBytes(riffType));result = ByteUtils.merger(result, ByteUtils.toBytes(formatChunkId));result = ByteUtils.merger(result, ByteUtils.toBytes(formatChunkSize));result = ByteUtils.merger(result, ByteUtils.toBytes(audioFormat));result = ByteUtils.merger(result, ByteUtils.toBytes(channels));result = ByteUtils.merger(result, ByteUtils.toBytes(sampleRate));result = ByteUtils.merger(result, ByteUtils.toBytes(byteRate));result = ByteUtils.merger(result, ByteUtils.toBytes(blockAlign));result = ByteUtils.merger(result, ByteUtils.toBytes(sampleBits));result = ByteUtils.merger(result, ByteUtils.toBytes(dataChunkId));result = ByteUtils.merger(result, ByteUtils.toBytes(dataChunkSize));return result;} }ByteUtils: https://github.com/zhaolewei/ZlwAudioRecorder/blob/master/recorderlib/src/main/java/com/zlw/main/recorderlib/utils/ByteUtils.java
PCM轉Wav
WavUtils.java
public class WavUtils {private static final String TAG = WavUtils.class.getSimpleName();/*** 生成wav格式的Header* wave是RIFF文件結構,每一部分為一個chunk,其中有RIFF WAVE chunk,* FMT Chunk,Fact chunk(可選),Data chunk** @param totalAudioLen 不包括header的音頻數據總長度* @param sampleRate 采樣率,也就是錄制時使用的頻率* @param channels audioRecord的頻道數量* @param sampleBits 位寬*/public static byte[] generateWavFileHeader(int totalAudioLen, int sampleRate, int channels, int sampleBits) {WavHeader wavHeader = new WavHeader(totalAudioLen, sampleRate, (short) channels, (short) sampleBits);return wavHeader.getHeader();}}/*** 將header寫入到pcm文件中 不修改文件名** @param file 寫入的pcm文件* @param header wav頭數據*/public static void writeHeader(File file, byte[] header) {if (!FileUtils.isFile(file)) {return;}RandomAccessFile wavRaf = null;try {wavRaf = new RandomAccessFile(file, "rw");wavRaf.seek(0);wavRaf.write(header);wavRaf.close();} catch (Exception e) {Logger.e(e, TAG, e.getMessage());} finally {try {if (wavRaf != null) {wavRaf.close();}} catch (IOException e) {Logger.e(e, TAG, e.getMessage());}}RecordHelper.java
private void makeFile() {mergePcmFiles(recordFile, files);//這里實現上一篇未完成的工作byte[] header = WavUtils.generateWavFileHeader((int) resultFile.length(), currentConfig.getSampleRate(), currentConfig.getChannelCount(), currentConfig.getEncoding());WavUtils.writeHeader(resultFile, header);Logger.i(TAG, "錄音完成! path: %s ; 大小:%s", recordFile.getAbsoluteFile(), recordFile.length());}參考鏈接:
http://soundfile.sapp.org/doc/WaveFormat/
5. Mp3的錄制 - 編譯Lame源碼
編譯 so包
1.下載lame
官網(科學上網): http://lame.sourceforge.net/download.php
lame-3.100:https://pan.baidu.com/s/1U77GAq1nn3bVXFMEhRyo8g
2.使用ndk-build編譯源碼
2.1 在任意位置創建如下的目錄結構:
文件夾名稱隨意,與.mk 文件中路徑一致即可
2.2 解壓下載好的lame源碼
解壓后將其/lame-3.100/libmp3lame/目錄中.c和.h文件和/lame-3.100//include/中的 lame.h拷貝到/jni/lame-3.100_libmp3lame中
2.3 修改部分文件
2.4 編寫Mp3Encoder.c和Mp3Encoder.h對接java代碼
2.4.1 Mp3Encoder.c
注意修改包名 #include "lame-3.100_libmp3lame/lame.h" #include "Mp3Encoder.h"static lame_global_flags *glf = NULL; //TODO這里包名要與java中對接文件的路徑一致(這里是路徑是com.zlw.main.recorderlib.recorder.mp3,java文件: Mp3Encoder.java),下同 JNIEXPORT void JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_init(JNIEnv *env, jclass cls, jint inSamplerate, jint outChannel,jint outSamplerate, jint outBitrate, jint quality) {if (glf != NULL) {lame_close(glf);glf = NULL;}glf = lame_init();lame_set_in_samplerate(glf, inSamplerate);lame_set_num_channels(glf, outChannel);lame_set_out_samplerate(glf, outSamplerate);lame_set_brate(glf, outBitrate);lame_set_quality(glf, quality);lame_init_params(glf); }JNIEXPORT jint JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_encode(JNIEnv *env, jclass cls, jshortArray buffer_l, jshortArray buffer_r,jint samples, jbyteArray mp3buf) {jshort* j_buffer_l = (*env)->GetShortArrayElements(env, buffer_l, NULL);jshort* j_buffer_r = (*env)->GetShortArrayElements(env, buffer_r, NULL);const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf);jbyte* j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL);int result = lame_encode_buffer(glf, j_buffer_l, j_buffer_r,samples, j_mp3buf, mp3buf_size);(*env)->ReleaseShortArrayElements(env, buffer_l, j_buffer_l, 0);(*env)->ReleaseShortArrayElements(env, buffer_r, j_buffer_r, 0);(*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0);return result; }JNIEXPORT jint JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_flush(JNIEnv *env, jclass cls, jbyteArray mp3buf) {const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf);jbyte* j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL);int result = lame_encode_flush(glf, j_mp3buf, mp3buf_size);(*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0);return result; }JNIEXPORT void JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_close(JNIEnv *env, jclass cls) {lame_close(glf);glf = NULL; }2.4.2 Mp3Encoder.h
注意修改包名 /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h>#ifndef _Included_Mp3Encoder #define _Included_Mp3Encoder #ifdef __cplusplus extern "C" { #endif /** Class: com.zlw.main.recorderlib.recorder.mp3.Mp3Encoder* Method: init*/ JNIEXPORT void JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_init(JNIEnv *, jclass, jint, jint, jint, jint, jint);JNIEXPORT jint JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_encode(JNIEnv *, jclass, jshortArray, jshortArray, jint, jbyteArray);JNIEXPORT jint JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_flush(JNIEnv *, jclass, jbyteArray);JNIEXPORT void JNICALL Java_com_zlw_main_recorderlib_recorder_mp3_Mp3Encoder_close(JNIEnv *, jclass);#ifdef __cplusplus } #endif #endif2.5 編寫Android.mk 和Application.mk
路徑與創建的目錄應當一致2.5.1 Android.mk
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LAME_LIBMP3_DIR := lame-3.100_libmp3lameLOCAL_MODULE := mp3lameLOCAL_SRC_FILES :=\ $(LAME_LIBMP3_DIR)/bitstream.c \ $(LAME_LIBMP3_DIR)/fft.c \ $(LAME_LIBMP3_DIR)/id3tag.c \ $(LAME_LIBMP3_DIR)/mpglib_interface.c \ $(LAME_LIBMP3_DIR)/presets.c \ $(LAME_LIBMP3_DIR)/quantize.c \ $(LAME_LIBMP3_DIR)/reservoir.c \ $(LAME_LIBMP3_DIR)/tables.c \ $(LAME_LIBMP3_DIR)/util.c \ $(LAME_LIBMP3_DIR)/VbrTag.c \ $(LAME_LIBMP3_DIR)/encoder.c \ $(LAME_LIBMP3_DIR)/gain_analysis.c \ $(LAME_LIBMP3_DIR)/lame.c \ $(LAME_LIBMP3_DIR)/newmdct.c \ $(LAME_LIBMP3_DIR)/psymodel.c \ $(LAME_LIBMP3_DIR)/quantize_pvt.c \ $(LAME_LIBMP3_DIR)/set_get.c \ $(LAME_LIBMP3_DIR)/takehiro.c \ $(LAME_LIBMP3_DIR)/vbrquantize.c \ $(LAME_LIBMP3_DIR)/version.c \ MP3Encoder.cinclude $(BUILD_SHARED_LIBRARY)2.5.2 Application.mk
若只需要編譯armeabi的so包可將其他刪除 APP_ABI := armeabi armeabi-v7a arm64-v8a x86 x86_64 mips mips64 APP_MODULES := mp3lame APP_CFLAGS += -DSTDC_HEADERS APP_PLATFORM := android-21編譯
到達這一步,所有的文件都已經準備好了
在命令行中切換到jni目錄中,執行ndk-build開始編譯
6. Mp3的錄制 - 使用Lame實時錄制MP3格式音頻
前言
上一篇介紹了如何去編譯so文件,這一篇主要介紹下如何實時將pcm數據轉換為MP3數據。
實現過程:
AudioRecorder在開啟錄音后,通過read方法不斷獲取pcm的采樣數據,每次獲取到數據后交給lame去處理,處理完成后存入文件中。
這一篇相對之前代碼,增加了兩個類:Mp3Encoder.java 和 Mp3EncoderThread.java
- Mp3Encoder: 通過Jni調用so文件的c代碼,將pcm轉換成mp3格式數據
- Mp3EncodeThread: 將pcm轉換成mp3時需要開啟子線程進行統一管理,以及全部轉碼完成的回調
代碼實現
Mp3Encoder.java
public class Mp3Encoder {static {System.loadLibrary("mp3lame");}public native static void close();public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);public native static int flush(byte[] mp3buf);public native static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);public static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate) {init(inSampleRate, outChannel, outSampleRate, outBitrate, 7);} }Mp3EncodeThread.java
每次有新的pcm數據后將數據打包成ChangeBuffer 類型,通過addChangeBuffer()存放到線程隊列當中,線程開啟后會不斷輪詢隊列內容,當有內容后開始轉碼,無內容時進入阻塞,直到數據全部處理完成后,關閉輪詢。
public class Mp3EncodeThread extends Thread {private static final String TAG = Mp3EncodeThread.class.getSimpleName();/*** mp3文件的碼率 32kbit/s = 4kb/s*/private static final int OUT_BITRATE = 32;private List<ChangeBuffer> cacheBufferList = Collections.synchronizedList(new LinkedList<ChangeBuffer>());private File file;private FileOutputStream os;private byte[] mp3Buffer;private EncordFinishListener encordFinishListener;/*** 是否已停止錄音*/private volatile boolean isOver = false;/*** 是否繼續輪詢數據隊列*/private volatile boolean start = true;public Mp3EncodeThread(File file, int bufferSize) {this.file = file;mp3Buffer = new byte[(int) (7200 + (bufferSize * 2 * 1.25))];RecordConfig currentConfig = RecordService.getCurrentConfig();int sampleRate = currentConfig.getSampleRate();Mp3Encoder.init(sampleRate, currentConfig.getChannelCount(), sampleRate, OUT_BITRATE);}@Overridepublic void run() {try {this.os = new FileOutputStream(file);} catch (FileNotFoundException e) {Logger.e(e, TAG, e.getMessage());return;}while (start) {ChangeBuffer next = next();Logger.v(TAG, "處理數據:%s", next == null ? "null" : next.getReadSize());lameData(next);}}public void addChangeBuffer(ChangeBuffer changeBuffer) {if (changeBuffer != null) {cacheBufferList.add(changeBuffer);synchronized (this) {notify();}}}public void stopSafe(EncordFinishListener encordFinishListener) {this.encordFinishListener = encordFinishListener;isOver = true;synchronized (this) {notify();}}private ChangeBuffer next() {for (; ; ) {if (cacheBufferList == null || cacheBufferList.size() == 0) {try {if (isOver) {finish();}synchronized (this) {wait();}} catch (Exception e) {Logger.e(e, TAG, e.getMessage());}} else {return cacheBufferList.remove(0);}}}private void lameData(ChangeBuffer changeBuffer) {if (changeBuffer == null) {return;}short[] buffer = changeBuffer.getData();int readSize = changeBuffer.getReadSize();if (readSize > 0) {int encodedSize = Mp3Encoder.encode(buffer, buffer, readSize, mp3Buffer);if (encodedSize < 0) {Logger.e(TAG, "Lame encoded size: " + encodedSize);}try {os.write(mp3Buffer, 0, encodedSize);} catch (IOException e) {Logger.e(e, TAG, "Unable to write to file");}}}private void finish() {start = false;final int flushResult = Mp3Encoder.flush(mp3Buffer);if (flushResult > 0) {try {os.write(mp3Buffer, 0, flushResult);os.close();} catch (final IOException e) {Logger.e(TAG, e.getMessage());}}Logger.d(TAG, "轉換結束 :%s", file.length());if (encordFinishListener != null) {encordFinishListener.onFinish();}}public static class ChangeBuffer {private short[] rawData;private int readSize;public ChangeBuffer(short[] rawData, int readSize) {this.rawData = rawData.clone();this.readSize = readSize;}short[] getData() {return rawData;}int getReadSize() {return readSize;}}public interface EncordFinishListener {/*** 格式轉換完畢*/void onFinish();} }使用
private class AudioRecordThread extends Thread {private AudioRecord audioRecord;private int bufferSize;AudioRecordThread() {bufferSize = AudioRecord.getMinBufferSize(currentConfig.getSampleRate(),currentConfig.getChannelConfig(), currentConfig.getEncodingConfig()) * RECORD_AUDIO_BUFFER_TIMES;audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getSampleRate(),currentConfig.getChannelConfig(), currentConfig.getEncodingConfig(), bufferSize);if (currentConfig.getFormat() == RecordConfig.RecordFormat.MP3 && mp3EncodeThread == null) {initMp3EncoderThread(bufferSize);}}@Overridepublic void run() {super.run();startMp3Recorder();}private void initMp3EncoderThread(int bufferSize) {try {mp3EncodeThread = new Mp3EncodeThread(resultFile, bufferSize);mp3EncodeThread.start();} catch (Exception e) {Logger.e(e, TAG, e.getMessage());}}private void startMp3Recorder() {state = RecordState.RECORDING;notifyState();try {audioRecord.startRecording();short[] byteBuffer = new short[bufferSize];while (state == RecordState.RECORDING) {int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);if (mp3EncodeThread != null) {mp3EncodeThread.addChangeBuffer(new Mp3EncodeThread.ChangeBuffer(byteBuffer, end));}notifyData(ByteUtils.toBytes(byteBuffer));}audioRecord.stop();} catch (Exception e) {Logger.e(e, TAG, e.getMessage());notifyError("錄音失敗");}if (state != RecordState.PAUSE) {state = RecordState.IDLE;notifyState();if (mp3EncodeThread != null) {mp3EncodeThread.stopSafe(new Mp3EncodeThread.EncordFinishListener() {@Overridepublic void onFinish() {notifyFinish();}});} else {notifyFinish();}} else {Logger.d(TAG, "暫停");}}} }7. 音樂可視化-FFT頻譜圖
項目地址:https://github.com/zhaolewei/MusicVisualizer
視頻演示地址:https://www.bilibili.com/video/av30388154/
實現
實現流程:
- 使用MediaPlayer播放傳入的音樂,并拿到mediaPlayerId
- 使用Visualizer類拿到拿到MediaPlayer播放中的音頻數據(wave/fft)
- 將數據用自定義控件展現出來
準備工作
使用Visualizer需要錄音的動態權限, 如果播放sd卡音頻需要STORAGE權限。
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需要此參數* @return MediaPlayerId*/public int getMediaPlayerId() {return mediaPlayer.getAudioSessionId();}使用可視化類Visualizer獲取當前音頻數據
Visualizer 有兩個比較重要的參數
- 設置可視化數據的數據大小 范圍[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()[1]]
- 設置可視化數據的采集頻率 范圍[0~Visualizer.getMaxCaptureRate()]
OnDataCaptureListener 有2個回調,一個用于顯示FFT數據,展示不同頻率的振幅,另一個用于顯示聲音的波形圖。
private Visualizer.OnDataCaptureListener dataCaptureListener = new Visualizer.OnDataCaptureListener() {@Overridepublic void onWaveFormDataCapture(Visualizer visualizer, final byte[] waveform, int samplingRate) {audioView.post(new Runnable() {@Overridepublic void run() {audioView.setWaveData(waveform);}});}@Overridepublic void onFftDataCapture(Visualizer visualizer, final byte[] fft, int samplingRate) {audioView2.post(new Runnable() {@Overridepublic void run() {audioView2.setWaveData(fft);}});}};private void initVisualizer() {try {int mediaPlayerId = mediaPlayer.getMediaPlayerId();if (visualizer != null) {visualizer.release();}visualizer = new Visualizer(mediaPlayerId);/***可視化數據的大小: 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, "請檢查錄音權限");} }波形數據和傅里葉數據的關系如圖:
快速傅里葉轉換(FFT)詳細分析: https://zhuanlan.zhihu.com/p/19763358
編寫自定義控件,展示數據
1.處理數據: visualizer 回調中的數據中是存在負數的,需要轉換一下,用于顯示
當byte 為 -128時Math.abs(fft[i]) 計算出來的值會越界,需要手動處理一下 byte 的范圍: -128~127 /*** 預處理數據** @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時越界newData[i] = abs < 0 ? 127 : abs;}return newData;}2. 緊接著就是根據數據去繪制圖形
@Overrideprotected 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);}}總結
以上是生活随笔為你收集整理的Android音频开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电信卡流量套餐超40G后,该如何解除网速
- 下一篇: jmeter断言beanshell判断日