採樣率
一秒鐘內對聲音信號的採樣次數稱爲採樣率,單位 Hz。採樣率越高所表示的聲波越平滑,對聲音的還原度就越好,須要的存儲空間也會更大。在數字音頻領域常見的採樣率有:java
採樣精度
對聲音信號的每一次採樣在計算機中都表示爲一個數字,數字的取值範圍越大所表示的聲音振幅的變化範圍就越大,在 Android 中支持的採樣精度有三種,定義在 AudioFormat
中c++
// 一次採樣須要 2 個字節表示 public static final int ENCODING_PCM_16BIT = 2; // 一次採樣須要 3 個字節表示 public static final int ENCODING_PCM_8BIT = 3; // 一次採樣須要 4 個字節表示 public static final int ENCODING_PCM_FLOAT = 4;
這裏咱們採用線性疊加求平均值的方式對兩道音頻流進行混音,混音前須要保證兩道音頻流具備相同的採樣率、採樣位深和聲道數。緩存
採樣位深爲 8 bit 時只須要將兩端 pcm 數據對應位置的數字相加求平均值便可ide
// 將 audioSrc 混入 audioDst public static void mixAsByte(byte[] audioSrc, byte[] audioDst) { for (int i = 0; i <audioData2.length; ++i) { audioDst[i] = (byte) ((audioDst[i] + audioSrc[i]) / 2); } }
採樣位深爲 16 bit 時,一次採樣須要用兩個 byte 表示,因此須要先把連續的兩個 byte 轉換成 short 再相加求平均值。同時還須要考慮字節序問題,字節序分爲大端字節序(高位字節在前,低位字節在後)和小端字節序(低位字節在前,高位字節在後),在 Android 中經過 MediaCodec 解碼或從 AudioRecord 中錄製的音頻數據所採用的字節序均可以經過 ByteOrder.nativeOrder() 方法得到,通常都採用的小端字節序。ui
// 將 audioSrc 混入 audioDst public static void mixAsShort(byte[] audioSrc, byte[] audioDst) { ShortBuffer sAudioSrc = ByteBuffer.wrap(audioSrc).order(ByteOrder.nativeOrder()).asShortBuffer(); ShortBuffer sAudioDst = ByteBuffer.wrap(audioData2).order(ByteOrder.nativeOrder()).asShortBuffer(); for (int i = 0; i < sAudioData1.capacity(); ++i) { sAudioDst.put(i, (short) ((sAudioSrc.get(i) + sAudioDst.get(i)) / 2)); } }
實現混音時須要保證兩道音頻流的格式(採樣率、位深和聲道數)徹底相同,所以在混音前須要對原始音頻數據進行重採樣.這裏咱們使用 ffmpeg 來實現,只須要編譯 libswresample、libavformat 和 libavformat 這三個庫就能夠
```c++
// 建立重採樣上下文
SwrContext *swrcontext = swr_alloc();
// 經過 channel count 獲取 channel layout
int64_t in_channel_layout = av_get_default_channel_layout(in_channelcount);
int64_t out_channel_layout = av_get_default_channel_layout(out_channelcount);
// 設置源通道數和目標通道數
av_opt_set_channel_layout(swrcontext, "in_channel_layout", in_channel_layout, 0);
av_opt_set_channel_layout(swrcontext, "out_channel_layout", out_channel_layout, 0);
// 設置源採樣率和目標採樣率
av_opt_set_int(swrcontext, "in_sample_rate", in_sample_rate, 0);
av_opt_set_int(swrcontext, "out_sample_rate", out_sample_rate, 0);
// 設置源採樣位深和目標採樣位深
av_opt_set_sample_fmt(swrcontext, "in_sample_fmt", in_sample_fmt, 0);
av_opt_set_sample_fmt(swrcontext, "out_sample_fmt", out_samplefmt, 0);
// 初始化重採樣上下文
error = swr_init(swrcontext);code
上面的代碼中建立了一個重採樣須要的上下文環境 SwrContext,並配置了重採樣前和重採樣後的必備參數,接下來看下如何使用 SwrContext 實現重採樣 ```c++ int Resample(JNIEnv* env, jobject caller, jint byte_count) { // 輸入採樣樣本數目 = 輸入字節數 / (輸入聲道數 x 輸入音頻單聲道單次採樣字節數) int in_samples = byte_count / (in_channel_count_ * in_sample_bytes_); // 計算重採樣後指望輸出的採樣數目 int out_samples = av_rescale_rnd(swr_get_delay(swr_context_, in_sample_rate_) + in_samples, out_sample_rate_, in_sample_rate_, AV_ROUND_UP); // 進行重採樣,返回值 out_samples 是本次重採樣實際輸出的採樣數目 out_samples = swr_convert( swr_context_, // 重採樣的結果輸出到 out_buffer_address_ reinterpret_cast<uint8_t**>(&out_buffer_address_), // 指望輸出的採樣數目 out_samples, // 輸入音頻數據存儲在 in_buffer_address_ (const uint8_t**)(&in_buffer_address_), // 輸入的採樣數目 in_samples); // 最後返回重採樣後獲得的數據字節數 = 輸出採樣數目 x 輸出聲道數 * 輸出音頻單聲道單次採樣字節數 return out_samples * out_channel_count_ * out_sample_bytes_; }
上面代碼實現中出現的 in_bufferaddress 和 out_bufferaddress 是 Android 層調用 ByteBuffer.allocateDirect() 方法提早建立的輸入/輸出緩存,由於 Android 調用 cxx 涉及到 JNI 調用,且重採樣方法的調用也會比較頻繁,建立緩存能夠避免頻繁的分配和銷燬內存產生的消耗。另外還須要注意,實際輸出的採樣數目並不必定與輸入的採樣數目相同,在建立輸出緩存時須要將緩存大小設置的比預期稍高一些或動態調整緩存大小。orm
最後在重採樣結束後還須要釋放上下文佔用的資源
```c++
swr_free(&swrcontext);視頻
## 總結 本文首先採用 ffmpeg 將源音頻數據重採樣爲目標音頻格式,再將兩道音頻格式相同的音頻流混成一道音頻流,採用的混音方式是將兩道音頻流對應位置的採樣數據相加求平均值,這種混音方式不會引入額外的噪音,可是在音頻流數量比較多時會致使整體音量降低的問題。對於混音的方式,感興趣的讀者能夠到網上搜索其餘資料深刻了解。