音频概念介绍采样率一秒钟内对声音信号的采样次数称为采样率,单位 Hz。采样率越高所表示的声波越平滑,对声音的还原度就越好,需要的存储空间也会更大。在数字音频领域常见的采样率有:
8000Hz 电话所用采样率
22050Hz 无线电广播所用采样率
32000Hz miniDV 数码视频 camcorder、DAT(LPmode) 所用采样率
44100Hz 音频 CD,也常用于 MPEG-1 音频(VCD,SVCD,MP3)所用采样率
48000Hz miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率
96000或192000Hz DVD-Audio、一些 LPCMDVD 音轨、BD-ROM(蓝光盘)音轨、和 HD-DVD(高清晰度 DVD )音轨所用所用采样率
采样精度对声音信号的每一次采样在计算机中都表示为一个数字,数字的取值范围越大所表示的声音振幅的变化范围就越大,在 Android 中支持的采样精度有三种,定义在 AudioFormat 中
// 一次采样需要 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 时的混音实现采样位深为 8 bit 时只需要将两端 pcm 数据对应位置的数字相加求平均值即可
// 将 audioSrc 混入 audioDstpublic 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 时的混音实现采样位深为 16 bit 时,一次采样需要用两个 byte 表示,所以需要先把连续的两个 byte 转换成 short 再相加求平均值。同时还需要考虑字节序问题,字节序分为大端字节序(高位字节在前,低位字节在后)和小端字节序(低位字节在前,高位字节在后),在 Android 中通过 MediaCodec 解码或从 AudioRecord 中录制的音频数据所采用的字节序都可以通过 ByteOrder.nativeOrder() 方法获得,一般都采用的小端字节序。
// 将 audioSrc 混入 audioDstpublic 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 实现重采样实现混音时需要保证两道音频流的格式(采样率、位深和声道数)完全相同,因此在混音前需要对原始音频数据进行重采样.这里我们使用 ffmpeg 来实现,只需要编译 libswresample、libavformat 和 libavformat 这三个库就可以
// 创建重采样上下文SwrContext *swr_context_ = swr_alloc();// 通过 channel count 获取 channel layoutint64_t in_channel_layout = av_get_default_channel_layout(in_channel_count_);int64_t out_channel_layout = av_get_default_channel_layout(out_channel_count_);// 设置源通道数和目标通道数av_opt_set_channel_layout(swr_context_, "in_channel_layout", in_channel_layout, 0);av_opt_set_channel_layout(swr_context_, "out_channel_layout", out_channel_layout, 0);// 设置源采样率和目标采样率av_opt_set_int(swr_context_, "in_sample_rate", in_sample_rate, 0);av_opt_set_int(swr_context_, "out_sample_rate", out_sample_rate, 0);// 设置源采样位深和目标采样位深av_opt_set_sample_fmt(swr_context_, "in_sample_fmt", in_sample_fmt, 0);av_opt_set_sample_fmt(swr_context_, "out_sample_fmt", out_sample_fmt, 0);// 初始化重采样上下文error_ = swr_init(swr_context_);上面的代码中创建了一个重采样需要的上下文环境 SwrContext,并配置了重采样前和重采样后的必备参数,接下来看下如何使用 SwrContext 实现重采样
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_buffer_address_ 和 out_buffer_address_ 是 Android 层调用 ByteBuffer.allocateDirect() 方法提前创建的输入/输出缓存,因为 Android 调用 cxx 涉及到 JNI 调用,且重采样方法的调用也会比较频繁,创建缓存可以避免频繁的分配和销毁内存产生的消耗。另外还需要注意,实际输出的采样数目并不一定与输入的采样数目相同,在创建输出缓存时需要将缓存大小设置的比预期稍高一些或动态调整缓存大小。
最后在重采样结束后还需要释放上下文占用的资源
swr_free(&swr_context_);总结本文首先采用 ffmpeg 将源音频数据重采样为目标音频格式,再将两道音频格式相同的音频流混成一道音频流,采用的混音方式是将两道音频流对应位置的采样数据相加求平均值,这种混音方式不会引入额外的噪音,但是在音频流数量比较多时会导致总体音量下降的问题。对于混音的方式,感兴趣的读者可以到网上搜索其他资料深入了解。