Android音频开发
這篇博客 轉(zhuǎn)載自 https://www.jianshu.com/p/c0222de2faed
這里涉及到ndk的一些知識(shí),對(duì)于.mk文件不太熟悉的同學(xué)要自己去 官網(wǎng) 或者搜索一些博客了解基本知識(shí)。
Android音頻開(kāi)發(fā)
- 1. 音頻基礎(chǔ)知識(shí)
- 音頻基礎(chǔ)知識(shí)
- 常用音頻格式
- 音頻開(kāi)發(fā)的主要應(yīng)用
- 音頻開(kāi)發(fā)的具體內(nèi)容
- 2. 使用AudioRecord錄制pcm格式音頻
- AudioRecord類(lèi)的介紹
- 實(shí)現(xiàn)
- 其他
- 3. 使用AudioRecord實(shí)現(xiàn)錄音的暫停和恢復(fù)
- 解決辦法
- 實(shí)現(xiàn)
- 其他
- 4. PCM轉(zhuǎn)WAV格式音頻
- wav 和 pcm
- WAV頭文件
- java 生成頭文件
- PCM轉(zhuǎn)Wav
- 參考鏈接:
- 5. Mp3的錄制 - 編譯Lame源碼
- 編譯 so包
- 編譯
- 6. Mp3的錄制 - 使用Lame實(shí)時(shí)錄制MP3格式音頻
- 前言
- 代碼實(shí)現(xiàn)
- 使用
- 7. 音樂(lè)可視化-FFT頻譜圖
- 實(shí)現(xiàn)
- 準(zhǔn)備工作
- 開(kāi)始播放
- 使用可視化類(lèi)Visualizer獲取當(dāng)前音頻數(shù)據(jù)
- 編寫(xiě)自定義控件,展示數(shù)據(jù)
1. 音頻基礎(chǔ)知識(shí)
音頻基礎(chǔ)知識(shí)
采樣和采樣頻率:
現(xiàn)在是數(shù)字時(shí)代,在音頻處理時(shí)要先把音頻的模擬信號(hào)變成數(shù)字信號(hào),這叫A/D轉(zhuǎn)換。要把音頻的模擬信號(hào)變成數(shù)字信號(hào),就需要采樣。一秒鐘內(nèi)采樣的次數(shù)稱(chēng)為采樣頻率
采樣位數(shù)/位寬:
數(shù)字信號(hào)是用0和1來(lái)表示的。采樣位數(shù)就是采樣值用多少位0和1來(lái)表示,也叫采樣精度,用的位數(shù)越多就越接近真實(shí)聲音。如用8位表示,采樣值取值范圍就是-128 ~ 127,如用16位表示,采樣值取值范圍就是-32768 ~ 32767。
聲道(channel):
通常語(yǔ)音只用一個(gè)聲道。而對(duì)于音樂(lè)來(lái)說(shuō),既可以是單聲道(mono),也可以是雙聲道(即左聲道右聲道,叫立體聲stereo),還可以是多聲道,叫環(huán)繞立體聲。
編解碼 :
通常把音頻采樣過(guò)程也叫做脈沖編碼調(diào)制編碼,即PCM(Pulse Code Modulation)編碼,采樣值也叫PCM值。 如果把采樣值直接保存或者發(fā)送,會(huì)占用很大的存儲(chǔ)空間。以16kHz采樣率16位采樣位數(shù)單聲道為例,一秒鐘就有16/8*16000 = 32000字節(jié)。為了節(jié)省保存空間或者發(fā)送流量,會(huì)對(duì)PCM值壓縮。
目前主要有三大技術(shù)標(biāo)準(zhǔn)組織制定壓縮標(biāo)準(zhǔn):
一些大公司或者組織也制定壓縮標(biāo)準(zhǔn),比如iLBC,OPUS。
壓縮:
對(duì)于自然界中的音頻信號(hào),如果轉(zhuǎn)換成數(shù)字信號(hào),進(jìn)行音頻編碼,那么只能無(wú)限接近,不可能百分百還原。所以說(shuō)實(shí)際上任何信號(hào)轉(zhuǎn)換成數(shù)字信號(hào)都會(huì)“有損”。但是在計(jì)算機(jī)應(yīng)用中,能夠達(dá)到最高保真水平的就是PCM編碼。因此,PCM約定俗成了無(wú)損編碼。我們而習(xí)慣性的把MP3列入有損音頻編碼范疇,是相對(duì)PCM編碼的。強(qiáng)調(diào)編碼的相對(duì)性的有損和無(wú)損
碼率:
碼率 = 采樣頻率 * 采樣位數(shù) * 聲道個(gè)數(shù); 例:采樣頻率44.1KHz,量化位數(shù)16bit,立體聲(雙聲道),未壓縮時(shí)的碼率 = 44.1KHz * 16 * 2 = 1411.2Kbps = 176.4KBps,即每秒要錄制的資源大小,理論上碼率和質(zhì)量成正比。
常用音頻格式
WAV 格式:音質(zhì)高 無(wú)損格式 體積較大
AAC(Advanced Audio Coding) 格式:相對(duì)于 mp3,AAC 格式的音質(zhì)更佳,文件更小,有損壓縮,一般蘋(píng)果或者Android SDK4.1.2(API 16)及以上版本支持播放,性?xún)r(jià)比高
AMR 格式:壓縮比比較大,但相對(duì)其他的壓縮格式質(zhì)量比較差,多用于人聲,通話(huà)錄音
mp3 格式:特點(diǎn) 使用廣泛, 有損壓縮,犧牲了12KHz到16KHz高音頻的音質(zhì)
音頻開(kāi)發(fā)的主要應(yīng)用
- 音頻播放器
- 錄音機(jī)
- 語(yǔ)音電話(huà)
- 音視頻監(jiān)控應(yīng)用
- 音視頻直播應(yīng)用
- 音頻編輯/處理軟件(ktv音效、變聲, 鈴聲轉(zhuǎn)換)
- 藍(lán)牙耳機(jī)/音箱
音頻開(kāi)發(fā)的具體內(nèi)容
- 音頻采集/播放
- 音頻算法處理(去噪、靜音檢測(cè)、回聲消除、音效處理、功放/增強(qiáng)、混音/分離,等等)
- 音頻的編解碼和格式轉(zhuǎn)換
- 音頻傳輸協(xié)議的開(kāi)發(fā)(SIP,A2DP、AVRCP,等等)
2. 使用AudioRecord錄制pcm格式音頻
AudioRecord類(lèi)的介紹
1. AudioRecord構(gòu)造函數(shù):
/*** @param audioSource :錄音源* 這里選擇使用麥克風(fēng):MediaRecorder.AudioSource.MIC* @param sampleRateInHz: 采樣率* @param channelConfig:聲道數(shù) * @param audioFormat: 采樣位數(shù).* See {@link AudioFormat#ENCODING_PCM_8BIT}, {@link AudioFormat#ENCODING_PCM_16BIT},* and {@link AudioFormat#ENCODING_PCM_FLOAT}.* @param bufferSizeInBytes: 音頻錄制的緩沖區(qū)大小* See {@link #getMinBufferSize(int, int, int)} */ public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes)2. getMinBufferSize()
/** * 獲取AudioRecord所需的最小緩沖區(qū)大小 * @param sampleRateInHz: 采樣率 * @param channelConfig:聲道數(shù) * @param audioFormat: 采樣位數(shù). */ public static int getMinBufferSize (int sampleRateInHz, int channelConfig, int audioFormat)3. getRecordingState()
/** * 獲取AudioRecord當(dāng)前的錄音狀態(tài) * @see AudioRecord#RECORDSTATE_STOPPED * @see AudioRecord#RECORDSTATE_RECORDING */ public int getRecordingState()4. startRecording()
/*** 開(kāi)始錄制*/public int startRecording()5. stop()
/*** 停止錄制*/public int stop()6. read()
/*** 從錄音設(shè)備中讀取音頻數(shù)據(jù)* @param audioData 音頻數(shù)據(jù)寫(xiě)入的byte[]緩沖區(qū)* @param offsetInBytes 偏移量* @param sizeInBytes 讀取大小* @return 返回負(fù)數(shù)則表示讀取失敗* see {@link #ERROR_INVALID_OPERATION} -3 : 初始化錯(cuò)誤{@link #ERROR_BAD_VALUE} -3: 參數(shù)錯(cuò)誤{@link #ERROR_DEAD_OBJECT} -6: {@link #ERROR} */ public int read(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)實(shí)現(xiàn)
實(shí)現(xiàn)過(guò)程就是調(diào)用上面的API的方法,構(gòu)造AudioRecord實(shí)例后再調(diào)用startRecording(),開(kāi)始錄音,并通過(guò)read()方法不斷獲取錄音數(shù)據(jù)記錄下來(lái),生成PCM文件。涉及耗時(shí)操作,所以最好在子線(xiàn)程中進(jìn)行。
public class RecordHelper {//0.此狀態(tài)用于控制線(xiàn)程中的循環(huán)操作,應(yīng)用volatile修飾,保持?jǐn)?shù)據(jù)的一致性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, "狀態(tài)異常當(dāng)前狀態(tài): %s", state.name());return;}recordFile = new File(filePath);String tempFilePath = getTempFilePath();Logger.i(TAG, "tmpPCM File: %s", tempFilePath);tmpFile = new File(tempFilePath);//1.開(kāi)啟錄音線(xiàn)程并準(zhǔn)備錄音audioRecordThread = new AudioRecordThread();audioRecordThread.start();}public void stop() {if (state == RecordState.IDLE) {Logger.e(TAG, "狀態(tài)異常當(dāng)前狀態(tài): %s", state.name());return;}state = RecordState.STOP;}private class AudioRecordThread extends Thread {private AudioRecord audioRecord;private int bufferSize;AudioRecordThread() {//2.根據(jù)錄音參數(shù)構(gòu)造AudioRecord實(shí)體對(duì)象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, "開(kāi)始錄制");FileOutputStream fos = null;try {fos = new FileOutputStream(tmpFile);audioRecord.startRecording();byte[] byteBuffer = new byte[bufferSize];while (state == RecordState.RECORDING) {//3.不斷讀取錄音數(shù)據(jù)并保存至文件中int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);fos.write(byteBuffer, 0, end);fos.flush();}//4.當(dāng)執(zhí)行stop()方法后state != RecordState.RECORDING,終止循環(huán),停止錄音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, "錄音結(jié)束");}} }其他
- 這里實(shí)現(xiàn)了PCM音頻的錄制,AudioRecord
API中只有開(kāi)始和停止的方法,在實(shí)際開(kāi)發(fā)中可能還需要暫停/恢復(fù)的操作,以及PCM轉(zhuǎn)WAV的功能,下一篇再繼續(xù)完善。 - 需要錄音及文件處理的動(dòng)態(tài)權(quán)限
3. 使用AudioRecord實(shí)現(xiàn)錄音的暫停和恢復(fù)
上一部分主要寫(xiě)了AudioRecord實(shí)現(xiàn)音頻錄制的開(kāi)始和停止,AudioRecord并沒(méi)有暫停和恢復(fù)播放功能的API,所以需要手動(dòng)實(shí)現(xiàn)。
解決辦法
思路很簡(jiǎn)單,現(xiàn)在可以實(shí)現(xiàn)音頻的文件錄制和停止,并生成pcm文件,那么暫停時(shí)將這次文件先保存下來(lái),恢復(fù)播放后開(kāi)始新一輪的錄制,那么最后會(huì)生成多個(gè)pcm音頻,再將這些pcm文件進(jìn)行合并,這樣就實(shí)現(xiàn)了暫停/恢復(fù)的功能了。
實(shí)現(xiàn)
- 實(shí)現(xiàn)的重點(diǎn)在于如何控制錄音的狀態(tài)
其他
在此后如若需要添加錄音狀態(tài)回調(diào),記得使用Handler做好線(xiàn)程切換。
4. PCM轉(zhuǎn)WAV格式音頻
前面幾部分已經(jīng)介紹了PCM音頻文件的錄制,這一部分主要介紹下pcm轉(zhuǎn)wav。
wav 和 pcm
一般通過(guò)麥克風(fēng)采集的錄音數(shù)據(jù)都是PCM格式的,即不包含頭部信息,播放器無(wú)法知道音頻采樣率、位寬等參數(shù),導(dǎo)致無(wú)法播放,顯然是非常不方便的。pcm轉(zhuǎn)換成wav,我們只需要在pcm的文件起始位置加上至少44個(gè)字節(jié)的WAV頭信息即可。
RIFF
- WAVE文件是以RIFF(Resource Interchange File Format, “資源交互文件格式”)格式來(lái)組織內(nèi)部結(jié)構(gòu)的。
RIFF文件結(jié)構(gòu)可以看作是樹(shù)狀結(jié)構(gòu),其基本構(gòu)成是稱(chēng)為"塊"(Chunk)的單元. - WAVE文件是由若干個(gè)Chunk組成的。按照在文件中的出現(xiàn)位置包括:RIFF WAVE Chunk, Format Chunk, Fact Chunk(可選), Data Chunk。
WAV頭文件
所有的WAV都有一個(gè)文件頭,這個(gè)文件頭記錄著音頻流的編碼參數(shù)。數(shù)據(jù)塊的記錄方式是little-endian字節(jié)順序。
| 00-03 | ChunkId | “RIFF” |
| 04-07 | ChunkSize | 下個(gè)地址開(kāi)始到文件尾的總字節(jié)數(shù)(此Chunk的數(shù)據(jù)大小) |
| 08-11 | fccType | “WAVE” |
| 12-15 | SubChunkId1 | "fmt ",最后一位空格。 |
| 16-19 | SubChunkSize1 | 一般為16,表示fmt Chunk的數(shù)據(jù)塊大小為16字節(jié),即20-35 |
| 20-21 | FormatTag | 1:表示是PCM 編碼 |
| 22-23 | Channels | 聲道數(shù),單聲道為1,雙聲道為2 |
| 24-27 | SamplesPerSec | 采樣率 |
| 28-31 | BytesPerSec | 碼率 :采樣率 * 采樣位數(shù) * 聲道個(gè)數(shù),bytePerSecond = sampleRate * (bitsPerSample / 8) * channels |
| 32-33 | BlockAlign | 每次采樣的大小:位寬*聲道數(shù)/8 |
| 34-35 | BitsPerSample | 位寬 |
| 36-39 | SubChunkId2 | “data” |
| 40-43 | SubChunkSize2 | 音頻數(shù)據(jù)的長(zhǎng)度 |
| 44-… | data | 音頻數(shù)據(jù) |
java 生成頭文件
WavHeader.class
public static class WavHeader {/*** RIFF數(shù)據(jù)塊*/final String riffChunkId = "RIFF";int riffChunkSize;final String riffType = "WAVE";/*** FORMAT 數(shù)據(jù)塊*/final String formatChunkId = "fmt ";final int formatChunkSize = 16;final short audioFormat = 1;short channels;int sampleRate;int byteRate;short blockAlign;short sampleBits;/*** FORMAT 數(shù)據(jù)塊*/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轉(zhuǎn)Wav
WavUtils.java
public class WavUtils {private static final String TAG = WavUtils.class.getSimpleName();/*** 生成wav格式的Header* wave是RIFF文件結(jié)構(gòu),每一部分為一個(gè)chunk,其中有RIFF WAVE chunk,* FMT Chunk,Fact chunk(可選),Data chunk** @param totalAudioLen 不包括header的音頻數(shù)據(jù)總長(zhǎng)度* @param sampleRate 采樣率,也就是錄制時(shí)使用的頻率* @param channels audioRecord的頻道數(shù)量* @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寫(xiě)入到pcm文件中 不修改文件名** @param file 寫(xiě)入的pcm文件* @param header wav頭數(shù)據(jù)*/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);//這里實(shí)現(xiàn)上一篇未完成的工作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
官網(wǎng)(科學(xué)上網(wǎng)): http://lame.sourceforge.net/download.php
lame-3.100:https://pan.baidu.com/s/1U77GAq1nn3bVXFMEhRyo8g
2.使用ndk-build編譯源碼
2.1 在任意位置創(chuàng)建如下的目錄結(jié)構(gòu):
文件夾名稱(chēng)隨意,與.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 編寫(xiě)Mp3Encoder.c和Mp3Encoder.h對(duì)接java代碼
2.4.1 Mp3Encoder.c
注意修改包名 #include "lame-3.100_libmp3lame/lame.h" #include "Mp3Encoder.h"static lame_global_flags *glf = NULL; //TODO這里包名要與java中對(duì)接文件的路徑一致(這里是路徑是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 編寫(xiě)Android.mk 和Application.mk
路徑與創(chuàng)建的目錄應(yīng)當(dāng)一致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編譯
到達(dá)這一步,所有的文件都已經(jīng)準(zhǔn)備好了
在命令行中切換到j(luò)ni目錄中,執(zhí)行ndk-build開(kāi)始編譯
6. Mp3的錄制 - 使用Lame實(shí)時(shí)錄制MP3格式音頻
前言
上一篇介紹了如何去編譯so文件,這一篇主要介紹下如何實(shí)時(shí)將pcm數(shù)據(jù)轉(zhuǎn)換為MP3數(shù)據(jù)。
實(shí)現(xiàn)過(guò)程:
AudioRecorder在開(kāi)啟錄音后,通過(guò)read方法不斷獲取pcm的采樣數(shù)據(jù),每次獲取到數(shù)據(jù)后交給lame去處理,處理完成后存入文件中。
這一篇相對(duì)之前代碼,增加了兩個(gè)類(lèi):Mp3Encoder.java 和 Mp3EncoderThread.java
- Mp3Encoder: 通過(guò)Jni調(diào)用so文件的c代碼,將pcm轉(zhuǎn)換成mp3格式數(shù)據(jù)
- Mp3EncodeThread: 將pcm轉(zhuǎn)換成mp3時(shí)需要開(kāi)啟子線(xiàn)程進(jìn)行統(tǒng)一管理,以及全部轉(zhuǎn)碼完成的回調(diào)
代碼實(shí)現(xiàn)
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數(shù)據(jù)后將數(shù)據(jù)打包成ChangeBuffer 類(lèi)型,通過(guò)addChangeBuffer()存放到線(xiàn)程隊(duì)列當(dāng)中,線(xiàn)程開(kāi)啟后會(huì)不斷輪詢(xún)隊(duì)列內(nèi)容,當(dāng)有內(nèi)容后開(kāi)始轉(zhuǎn)碼,無(wú)內(nèi)容時(shí)進(jìn)入阻塞,直到數(shù)據(jù)全部處理完成后,關(guān)閉輪詢(xún)。
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;/*** 是否繼續(xù)輪詢(xún)數(shù)據(jù)隊(duì)列*/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, "處理數(shù)據(jù):%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, "轉(zhuǎn)換結(jié)束 :%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 {/*** 格式轉(zhuǎn)換完畢*/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. 音樂(lè)可視化-FFT頻譜圖
項(xiàng)目地址:https://github.com/zhaolewei/MusicVisualizer
視頻演示地址:https://www.bilibili.com/video/av30388154/
實(shí)現(xiàn)
實(shí)現(xiàn)流程:
- 使用MediaPlayer播放傳入的音樂(lè),并拿到mediaPlayerId
- 使用Visualizer類(lèi)拿到拿到MediaPlayer播放中的音頻數(shù)據(jù)(wave/fft)
- 將數(shù)據(jù)用自定義控件展現(xiàn)出來(lái)
準(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" />開(kāi)始播放
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* 可視化類(lèi)Visualizer需要此參數(shù)* @return MediaPlayerId*/public int getMediaPlayerId() {return mediaPlayer.getAudioSessionId();}使用可視化類(lèi)Visualizer獲取當(dāng)前音頻數(shù)據(jù)
Visualizer 有兩個(gè)比較重要的參數(shù)
- 設(shè)置可視化數(shù)據(jù)的數(shù)據(jù)大小 范圍[Visualizer.getCaptureSizeRange()[0]~Visualizer.getCaptureSizeRange()[1]]
- 設(shè)置可視化數(shù)據(jù)的采集頻率 范圍[0~Visualizer.getMaxCaptureRate()]
OnDataCaptureListener 有2個(gè)回調(diào),一個(gè)用于顯示FFT數(shù)據(jù),展示不同頻率的振幅,另一個(gè)用于顯示聲音的波形圖。
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);/***可視化數(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, "請(qǐng)檢查錄音權(quán)限");} }波形數(shù)據(jù)和傅里葉數(shù)據(jù)的關(guān)系如圖:
快速傅里葉轉(zhuǎn)換(FFT)詳細(xì)分析: https://zhuanlan.zhihu.com/p/19763358
編寫(xiě)自定義控件,展示數(shù)據(jù)
1.處理數(shù)據(jù): visualizer 回調(diào)中的數(shù)據(jù)中是存在負(fù)數(shù)的,需要轉(zhuǎn)換一下,用于顯示
當(dāng)byte 為 -128時(shí)Math.abs(fft[i]) 計(jì)算出來(lái)的值會(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;}2. 緊接著就是根據(jù)數(shù)據(jù)去繪制圖形
@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);}/*** 繪制曲線(xiàn)* 這里使用貝塞爾曲線(xiàn)來(lái)繪制*/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);}}總結(jié)
以上是生活随笔為你收集整理的Android音频开发的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 电信卡流量套餐超40G后,该如何解除网速
- 下一篇: (转)无需安装SDK,3部完成Andro