這篇博客 轉載自 https://www.jianshu.com/p/c0222de2faed
這裏涉及到ndk的一些知識,對於.mk文件不太熟悉的同學要自己去 官網 或者搜索一些博客瞭解基本知識。
Android音頻開發
1. 音頻基礎知識
音頻基礎知識
採樣和採樣頻率:
現在是數字時代,在音頻處理時要先把音頻的模擬信號變成數字信號,這叫A/D轉換。要把音頻的模擬信號變成數字信號,就需要採樣。一秒鐘內採樣的次數稱爲採樣頻率
採樣頻率越高,越接近原始信號,但是也加大了運算處理的複雜度。16000Hz和44.1kHZ(1)
採樣位數/位寬:
數字信號是用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值壓縮。
目前主要有三大技術標準組織制定壓縮標準:
- ITU,主要制定有線語音的壓縮標準(g系列),有g711/g722/g726/g729等。
- 3GPP,主要制定無線語音的壓縮標準(amr系列等),有amr-nb/amr-wb。後來ITU吸納了amr-wb,形成了g722.2。
- MPEG,主要制定音樂的壓縮標準,有11172-3,13818-3/7,14496-3等。
一些大公司或者組織也制定壓縮標準,比如iLBC,OPUS。
編碼過程:模擬信號->抽樣->量化->編碼->數字信號
壓縮:
對於自然界中的音頻信號,如果轉換成數字信號,進行音頻編碼,那麼只能無限接近,不可能百分百還原。所以說實際上任何信號轉換成數字信號都會“有損”。但是在計算機應用中,能夠達到最高保真水平的就是PCM編碼。因此,PCM約定俗成了無損編碼。我們而習慣性的把MP3列入有損音頻編碼範疇,是相對PCM編碼的。強調編碼的相對性的有損和無損
碼率:
碼率 = 採樣頻率 * 採樣位數 * 聲道個數; 例:採樣頻率44.1KHz,量化位數16bit,立體聲(雙聲道),未壓縮時的碼率 = 44.1KHz * 16 * 2 = 1411.2Kbps = 176.4KBps,即每秒要錄製的資源大小,理論上碼率和質量成正比。
800 bps – 能夠分辨的語音所需最低碼率(需使用專用的FS-1015語音編解碼器)
8 kbps —電話質量(使用語音編碼)
8-500 kbps --Ogg Vorbis和MPEG1 Player1/2/3中使用的有損音頻模式
500 kbps–1.4 Mbps —44.1KHz的無損音頻,解碼器爲FLAC Audio,WavPack或Monkey's Audio
1411.2 - 2822.4 Kbps —脈衝編碼調製(PCM)聲音格式CD光碟的數字音頻
5644.8 kbps —SACD使用的Direct Stream Digital格式
常用音頻格式
WAV 格式:音質高 無損格式 體積較大
AAC(Advanced Audio Coding) 格式:相對於 mp3,AAC 格式的音質更佳,文件更小,有損壓縮,一般蘋果或者Android SDK4.1.2(API 16)及以上版本支持播放,性價比高
AMR 格式:壓縮比比較大,但相對其他的壓縮格式質量比較差,多用於人聲,通話錄音
AMR分類:
AMR(AMR-NB): 語音帶寬範圍:300-3400Hz,8KHz抽樣
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);
}
@Override
public 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文件進行合併,這樣就實現了暫停/恢復的功能了。
實現
- 實現的重點在於如何控制錄音的狀態
public class RecordHelper {
private volatile RecordState state = RecordState.IDLE;
private AudioRecordThread audioRecordThread;
private File recordFile = null;
private File tmpFile = null;
private List<File> files = new ArrayList<>();
public void start(String filePath, RecordConfig config) {
this.currentConfig = 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);
audioRecordThread = new AudioRecordThread();
audioRecordThread.start();
}
public void stop() {
if (state == RecordState.IDLE) {
Logger.e(TAG, "狀態異常當前狀態: %s", state.name());
return;
}
//若在暫停中直接停止,則直接合並文件即可
if (state == RecordState.PAUSE) {
makeFile();
state = RecordState.IDLE;
} else {
state = RecordState.STOP;
}
}
public void pause() {
if (state != RecordState.RECORDING) {
Logger.e(TAG, "狀態異常當前狀態: %s", state.name());
return;
}
state = RecordState.PAUSE;
}
public void resume() {
if (state != RecordState.PAUSE) {
Logger.e(TAG, "狀態異常當前狀態: %s", state.name());
return;
}
String tempFilePath = getTempFilePath();
Logger.i(TAG, "tmpPCM File: %s", tempFilePath);
tmpFile = new File(tempFilePath);
audioRecordThread = new AudioRecordThread();
audioRecordThread.start();
}
private class AudioRecordThread extends Thread {
private AudioRecord audioRecord;
private int bufferSize;
AudioRecordThread() {
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);
}
@Override
public void run() {
super.run();
state = RecordState.RECORDING;
notifyState();
Logger.d(TAG, "開始錄製");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(tmpFile);
audioRecord.startRecording();
byte[] byteBuffer = new byte[bufferSize];
while (state == RecordState.RECORDING) {
int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
fos.write(byteBuffer, 0, end);
fos.flush();
}
audioRecord.stop();
//1. 將本次錄音的文件暫存下來,用於合併
files.add(tmpFile);
//2. 再此判斷終止循環的狀態是暫停還是停止,並做相應處理
if (state == RecordState.STOP) {
makeFile();
} else {
Logger.i(TAG, "暫停!");
}
} 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());
}
}
if (state != RecordState.PAUSE) {
state = RecordState.IDLE;
notifyState();
Logger.d(TAG, "錄音結束");
}
}
}
private void makeFile() {
//合併文件
boolean mergeSuccess = mergePcmFiles(recordFile, files);
//TODO:轉換wav
Logger.i(TAG, "錄音完成! path: %s ; 大小:%s", recordFile.getAbsoluteFile(), recordFile.length());
}
/**
* 合併Pcm文件
*
* @param recordFile 輸出文件
* @param files 多個文件源
* @return 是否成功
*/
private boolean mergePcmFiles(File recordFile, List<File> files) {
if (recordFile == null || files == null || files.size() <= 0) {
return false;
}
FileOutputStream fos = null;
BufferedOutputStream outputStream = null;
byte[] buffer = new byte[1024];
try {
fos = new FileOutputStream(recordFile);
outputStream = new BufferedOutputStream(fos);
for (int i = 0; i < files.size(); i++) {
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(files.get(i)));
int readCount;
while ((readCount = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, readCount);
}
inputStream.close();
}
} catch (Exception e) {
Logger.e(e, TAG, e.getMessage());
return false;
} finally {
try {
if (fos != null) {
fos.close();
}
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//3. 合併後記得刪除緩存文件並清除list
for (int i = 0; i < files.size(); i++) {
files.get(i).delete();
}
files.clear();
return true;
}
}
其他
在此後如若需要添加錄音狀態回調,記得使用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。
Fact Chunk 在壓縮後或在非PCM編碼時存在
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;
}
}
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
中
3.100版本 有42個文件
2.3 修改部分文件
- 刪除fft.c文件的47行的
#include "vector/lame_intrin.h"
- 刪除set_get.h文件的24行的
#include <lame.h>
- 將util.h文件的570行的
extern ieee754_float32_t fast_log2(ieee754_float32_t x);
替換爲extern float fast_log2(float x);
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
#endif
2.5 編寫Android.mk 和Application.mk
路徑與創建的目錄應當一致
2.5.1 Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LAME_LIBMP3_DIR := lame-3.100_libmp3lame
LOCAL_MODULE := mp3lame
LOCAL_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.c
include $(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開始編譯
如果不能識別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);
}
@Override
public 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);
}
}
@Override
public 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() {
@Override
public 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() {
@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);
/**
*可視化數據的大小: 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. 緊接着就是根據數據去繪製圖形
@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);
}
}