实时音视频的那些事儿(三)—— 音频编码

2023-11-16

前言

上一篇文章《实时音视频的那些事儿(二) —— 音频采集》中我们讲到了如何在iOS、Android、Windows平台实现音频采集,今天将介绍如何实现音频的编码。


一、iOS 中使用 AudioUnit 实现音频编码的过程

AudioUnit 是 iOS 中的音频处理框架,它提供了一组低级别的 API,用于音频输入/输出、音频处理和音频编码/解码。

在 iOS 中,常见的音频编码格式有 AACMP3AMR 等。使用 AudioUnit 进行音频编码的一般流程如下:

  1. 创建输入 AudioUnit

首先需要创建一个输入 AudioUnit,作为音频数据的源头。创建方法如下:

AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
AudioComponentInstanceNew(inputComponent, &_audioUnit);
  1. 配置 AudioUnit

在创建好输入 AudioUnit 后,需要对其进行配置,使其产生符合要求的音频输出。例如,配置采样率位深度声道数bufferSize 等参数。配置方法如下:

//采样率
Float64 sampleRate = 44100.00;
AudioUnitSetProperty(_audioUnit,
                      kAudioUnitProperty_SampleRate,
                      kAudioUnitScope_Output,
                      0,
                      &sampleRate,
                      sizeof(sampleRate));
UInt32 formatID = kAudioFormatLinearPCM;
UInt32 bytesPerChannel = 2;
UInt32 bitsPerChannel = bytesPerChannel * 8;
UInt32 channelsPerFrame = 1;//声道数
UInt32 framesPerPacket = 1;
UInt32 bytesPerFrame = bytesPerChannel * channelsPerFrame;

AudioStreamBasicDescription desc;
desc.mSampleRate = sampleRate;
desc.mFormatID = formatID;
desc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
desc.mBitsPerChannel = bitsPerChannel;
desc.mChannelsPerFrame = channelsPerFrame;
desc.mFramesPerPacket = framesPerPacket;
desc.mBytesPerPacket = bytesPerFrame * framesPerPacket;
desc.mBytesPerFrame = bytesPerFrame;
AudioUnitSetProperty(_audioUnit,
                      kAudioUnitProperty_StreamFormat,
                      kAudioUnitScope_Output,
                      1,
                      &desc,
                      sizeof(desc));

//bufferSize
UInt32 bufferSizeFrames = 4096;
AudioUnitSetProperty(_audioUnit,
                      kAudioUnitProperty_MaximumFramesPerSlice,
                      kAudioUnitScope_Global,
                      0,
                      &bufferSizeFrames,
                      sizeof(bufferSizeFrames));
  1. 创建编码器

配置好输入 AudioUnit 后,需要创建一个编码器实例,在输入音频数据时对其进行编码。不同的编码格式有不同的编码器实现。以 AAC 编码为例,编码器创建方法如下:

AudioStreamBasicDescription outputFormat;
memset(&outputFormat, 0, sizeof(outputFormat)); //清空结构体
outputFormat.mSampleRate = sampleRate; //采样率
outputFormat.mFormatID = kAudioFormatMPEG4AAC; //编码格式
outputFormat.mChannelsPerFrame = channelsPerFrame; //声道数
outputFormat.mFramesPerPacket = 1024; //每个 Packet 的帧数量,AAC 编码规定是 1024
outputFormat.mBytesPerFrame = 0;
outputFormat.mBytesPerPacket = 0;

AudioClassDescription *description = [self getAudioClassDescriptionWithType:kAudioFormatMPEG4AAC
                                                            fromManufacturer:kAppleSoftwareAudioCodecManufacturer];

OSStatus status = AudioConverterNewSpecific(&_outputFormat, &_outputFormat, 1, description, &_audioConverter);

getAudioClassDescriptionWithType:fromManufacturer: 方法用于获取编码器的描述信息,需要根据不同的编码格式进行修改。

  1. 处理音频数据

配置好输入 AudioUnit 和编码器后,就可以进行数据处理了。在输入音频数据时,需要通过回调的方法获取 buffer 中的音频数据,将其送入编码器进行编码,编码后的数据将存储在输出 buffer 中。如下:

//输入回调
static OSStatus inRenderCallback(void *inRefCon,
                                 AudioUnitRenderActionFlags *ioActionFlags,
                                 const AudioTimeStamp *inTimeStamp,
                                 UInt32 inBusNumber,
                                 UInt32 inNumberFrames,
                                 AudioBufferList *ioData) 
{
    AudioUnit audioUnit = *((AudioUnit *)inRefCon);
    OSStatus status = AudioUnitRender(audioUnit,
                                      ioActionFlags,
                                      inTimeStamp,
                                      inBusNumber,
                                      inNumberFrames,
                                      ioData);

    if (status == noErr) {
        AudioBuffer buffer = ioData->mBuffers[0];
        AudioBufferList outBufferList;
        outBufferList.mNumberBuffers = 1;
        outBufferList.mBuffers[0].mNumberChannels = buffer.mNumberChannels;
        outBufferList.mBuffers[0].mDataByteSize = buffer.mDataByteSize; //分配一块足够大小的输出 buffer
        outBufferList.mBuffers[0].mData = malloc(buffer.mDataByteSize);

        AudioStreamPacketDescription *packetDes = NULL;
        UInt32 packetsPerBuffer = 1;
        UInt32 outputDataPacketSize = buffer.mDataByteSize / packetsPerBuffer;
        packetDes = malloc(sizeof(AudioStreamPacketDescription) * packetsPerBuffer); //为输出 buffer 分配 packet 描述符空间
        memset(packetDes, 0, sizeof(AudioStreamPacketDescription) * packetsPerBuffer);

        OSStatus status = AudioConverterFillComplexBuffer(_audioConverter, inDestinationPacketCount, &ioData, &ioActionFlags, &outputDataPacketSize, outBufferList.mBuffers[0].mData, packetDes);

        if (status == noErr) {
            //编码成功,处理数据
            //TODO...
        }

        free(outBufferList.mBuffers[0].mData);
        free(packetDes);
    }
    return status;
}

其中,AudioConverterFillComplexBuffer 函数用于将输入音频数据进行编码,将编码后的数据存储在输出 buffer 中。

  1. 销毁 AudioUnit

在使用完毕后,需要销毁创建的 AudioUnit。销毁方法如下:

AudioUnitUninitialize(_audioUnit);
AudioComponentInstanceDispose(_audioUnit);

以上是使用 AudioUnit 实现 iOS 音频编码的基本流程,其中还有很多需要处理的细节,如错误处理、断点续传、编码后数据的打包等,需要按照具体需求进行编写。

二、Android平台下音频编码

Android平台下,音频编码一般使用的是基于OpenCore的Audio Encoder实现。它可以通过Android Native层的JNI接口来调用,实现录音数据的压缩编码。

该编码过程主要包括音频数据采集、音频数据压缩、音频数据封装三个部分。

  1. 音频数据采集

音频数据采集部分使用的是Android系统自带的AudioRecord类。通过设置录音来源、采样率、音频格式等参数来进行录音。

例如,在Java层代码中可以这样设置:

// 音频来源:麦克风
int audioSource = MediaRecorder.AudioSource.MIC;
// 采样率:44100Hz
int sampleRateInHz = 44100;
// 声道数:单声道
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
// 音频格式:PCM编码格式
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
// 音频数据缓冲区大小
int bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
// 创建AudioRecord对象
AudioRecord audioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes);
  1. 音频数据压缩

音频数据压缩部分使用的是Android系统自带的MediaCodec类。通过设置编码器类型、编码器参数等参数来进行音频数据压缩。

例如,在Java层代码中可以这样设置:

// 音频编码器名称
String mime = "audio/mp4a-latm";
// 音频编码器类型
MediaCodecInfo codecInfo = MediaUtils.selectCodec(mime);
// 音频编码器参数
MediaFormat format = MediaFormat.createAudioFormat(mime, sampleRateInHz, channelCount);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSizeInBytes);
MediaCodec mediaCodec = MediaCodec.createByCodecName(codecInfo.getName());
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  1. 音频数据封装

音频数据封装部分使用的是Android系统自带的MediaMuxer类。通过设置输出文件类型、输出文件路径等参数来进行音频数据封装。

例如,在Java层代码中可以这样设置:

// 输出文件路径
String outputPath = "/sdcard/audio_record.mp4";
// 输出文件类型
int outputFileType = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
// 创建MediaMuxer对象
try {
  mMuxer = new MediaMuxer(outputPath, outputFileType);
} catch (IOException e) {
  e.printStackTrace();
}

在音频数据压缩后,将音频数据写入到MediaMuxer的音频轨道中,最终生成封装了音频数据的MP4文件。

三、Windows平台音频采集编码

使用比较常用的第三方编码库LAME来进行MP3编码,大概可以分为一下几个步骤:

  1. 初始化音频设备:使用Windows API中的waveInOpen()函数打开音频采集设备,设置音频采样率、声道数、采样精度等参数。

WAVEFORMATEX audioFormat = {};
    audioFormat.wFormatTag = WAVE_FORMAT_PCM;
    audioFormat.nChannels = 1;
    audioFormat.nSamplesPerSec = 44100;
    audioFormat.wBitsPerSample = 16;
    audioFormat.nBlockAlign = audioFormat.nChannels * audioFormat.wBitsPerSample / 8;
    audioFormat.nAvgBytesPerSec = audioFormat.nSamplesPerSec * audioFormat.nBlockAlign;
    HWAVEIN hWaveIn = nullptr;
    MMRESULT result = waveInOpen(&hWaveIn, WAVE_MAPPER, &audioFormat, 0, 0, CALLBACK_NULL);
    if (result != MMSYSERR_NOERROR) {
        return 1;
    }
  1. 创建编码器:选择合适的音频编码算法,例如MP3、AAC等编码器,使用编码器提供的API创建编码器实例。

// 创建编码器
lame_t lameEncoder = lame_init();
lame_set_num_channels(lameEncoder, audioFormat.nChannels);
lame_set_in_samplerate(lameEncoder, audioFormat.nSamplesPerSec);
lame_set_mode(lameEncoder, MONO);
lame_set_quality(lameEncoder, 2);
lame_init_params(lameEncoder);
// 分配输入输出缓冲区
char* inputBuffer = new char[INPUT_BUFFER_SIZE];
char* outputBuffer = new char[OUTPUT_BUFFER_SIZE];
  1. 读取音频数据:使用waveInPrepareHeader()函数和waveInAddBuffer()函数设置每个音频缓冲区的大小和个数,调用waveInStart()函数开始读取音频数据。

// 读取音频数据
WAVEHDR waveHeader = {};
waveHeader.lpData = inputBuffer;
waveHeader.dwBufferLength = INPUT_BUFFER_SIZE;
waveInPrepareHeader(hWaveIn, &waveHeader, sizeof(WAVEHDR));
waveInAddBuffer(hWaveIn, &waveHeader, sizeof(WAVEHDR));
waveInStart(hWaveIn);
  1. 编码音频数据:每次读取完一段音频数据后,将音频数据传入编码器中进行编码,并将编码后的数据输出。

// 编码音频数据
    FILE* file = fopen("output.mp3", "wb");
    while (true) {
        result = waveInUnprepareHeader(hWaveIn, &waveHeader, sizeof(WAVEHDR));
        if (result != WAVERR_STILLPLAYING) {
            break;
        }
        WaveFormatHeaderToMp3Header(lameEncoder, (BYTE*)inputBuffer, (BYTE*)outputBuffer, INPUT_BUFFER_SIZE, OUTPUT_BUFFER_SIZE);
        fwrite(outputBuffer, sizeof(char), lame_get_framesize(lameEncoder), file);
        waveHeader.dwFlags = 0;
        waveHeader.dwBufferLength = INPUT_BUFFER_SIZE;
        waveHeader.dwBytesRecorded = 0;
        waveHeader.lpNext = nullptr;
        waveHeader.reserved = 0;
        waveHeader.dwUser = 0;
        waveHeader.dwLoops = 0;
        waveHeader.lpUser = nullptr;
        waveHeader.dwFlags |= WHDR_PREPARED;
        waveInPrepareHeader(hWaveIn, &waveHeader, sizeof(WAVEHDR));
        waveInAddBuffer(hWaveIn, &waveHeader, sizeof(WAVEHDR));
    }
    fclose(file);
  1. 停止音频采集:调用waveInStop()函数停止读取音频数据,使用waveInReset()函数清空缓冲区。

// 停止音频采集
waveInStop(hWaveIn);
waveInReset(hWaveIn);
result = waveInUnprepareHeader(hWaveIn, &waveHeader, sizeof(WAVEHDR));
    
  1. 释放资源:使用waveInClose()函数关闭音频设备,使用编码器提供的API释放编码器实例。

// 释放资源
delete[] inputBuffer;
delete[] outputBuffer;
lame_close(lameEncoder);
waveInClose(hWaveIn);

四、音频编码可以让我们实现一些有趣的功能
  1. 实时高清语音聊天

实时高清语音聊天是一种非常流行的应用程序,它能够让用户轻松地进行实时通话。这种应用程序通常使用音频编解码技术来实现高音质声音的传输和回放。

  1. 音乐播放

音乐播放器通常会使用音频编解码技术来压缩和解压缩音频文件。这样可以使音乐文件更小,并且可以在更短的时间内进行下载和传输。

  1. 语音识别

语音识别是一种非常有趣的应用程序,它能够让计算机理解和处理人类语言。这种应用程序通常会使用音频编解码技术来处理录音的语音文件。

  1. 唱歌比赛

唱歌比赛是一种非常流行的娱乐方式。这种应用程序通常会使用音频编解码技术来处理录音的歌唱文件。每个参赛者可以将自己的表演录制下来,并将其提交给比赛主办方进行评分。

总之,音频编码技术在现代生活中扮演着重要角色,对我们的日常生活产生了很大的影响。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实时音视频的那些事儿(三)—— 音频编码 的相关文章

  • 有没有办法替代Android中的标准Log?

    有没有办法以某种方式拦截对 android 中标准 Log 的调用并执行其他操作 在桌面 Java 中 人们通常会得到一些记录器 因此有多种方法可以安装不同的日志处理程序 实现 但是 Android似乎对Log有静态调用 我找不到任何有关替
  • Android Studio:Android 设备监视器未显示我的设备

    我的真实设备是索尼 Xperia c6502安卓版本4 3 我确定我将其连接到我的计算机然后打开开发者选项 USB调试 on 在 SDK 管理器中 Google USB 驱动程序已安装 I downloaded Xperia Z Drive
  • 将 Armadillo C++ 库导入 Xcode

    我是 Mac 用户 正在尝试安装和导入 C Armadillo 库 以下是我到目前为止所采取的步骤 1 我从其网站下载了犰狳库 2 我仔细阅读了下载文件中的 Readme txt 文件 解释了如何安装它 3 我使用CMake将犰狳下载文件制
  • iOS 如何触发视频退出全屏后继续播放?

    我正在构建一个在 iOS 中播放视频的网站 我有一个在 iOS 中工作的全屏按钮 但是退出全屏时视频会暂停 有谁知道一种方法可以强制视频在退出全屏时继续播放 或者如何设置一个侦听器来触发视频在退出全屏时自动播放 这是我的代码
  • 如何将CIFilter应用到UIView上?

    根据Apple docs 过滤属性CALayer不支持iOS 当我使用正在申请的应用程序之一时CIFilter to UIView即 Splice Funimate 和 Artisto 的视频编辑器 Videoshow FX 这意味着我们可
  • 无法从 com.android.aaptcompiler.ParsedResource@ef79973 提取资源

    无法从 com android aaptcompiler ParsedResource ef79973 提取资源 无法从 com android aaptcompiler ParsedResource 4c95ce87 提取资源 C Use
  • opencv人脸检测示例

    当我在设备上运行应用程序时 应用程序崩溃并显示以下按摩 java lang UnsatisfiedLinkError 无法加载 detector based tracker findLibrary 返回 null 我正在使用 OpenCV
  • Kotlin 和惯用的书写方式,基于可变值“如果不为空,则...”

    假设我们有这样的代码 class QuickExample fun function argument SomeOtherClass if argument mutableProperty null doSomething argument
  • 在 Swift 中以编程方式为 iOS 制作带有名字首字母的图像,例如 Gmail

    我需要在 UITableView 中显示与其姓名相对应的每个用户的个人资料图片 在下载图像之前 我需要显示一张带有他名字的第一个字母的图像 就像在 GMail 应用程序中一样 如何在 Swift for iOS 中以编程方式执行此操作 不需
  • cameraOverlayView 防止使用 allowedEditing 进行编辑

    在我的应用程序中 使用以下行在拍摄照片后对其进行编辑 移动和缩放 效果很好 imagePicker setAllowsEditing YES 但如果我还使用cameraOverlayView 则编辑模式将不再起作用 屏幕出现 但平移和捏合手
  • twitter4j => AndroidRuntime(446): java.lang.NoClassDefFoundError: twitter4j.http.AccessToken

    我正在尝试使用 twitter4j 我的应用程序来连接并发布到 Twitter 我正在关注本教程 http blog doityourselfandroid com 2011 02 13 guide to integrating twitt
  • iOS Swift 和 reloadRowsAtIndexPaths 编译错误

    我与 xCode Swift 陷入僵局并刷新 UITableView 的单行 这条线有效 self tableView reloadData 而这条线没有 self tableView reloadRowsAtIndexPaths curr
  • 如何在 iOS 上固定证书的公钥

    在提高我们正在开发的 iOS 应用程序的安全性时 我们发现需要对服务器的 SSL 证书 全部或部分 进行 PIN 操作以防止中间人攻击 尽管有多种方法可以做到这一点 但当您搜索此内容时 我只找到了固定整个证书的示例 这种做法会带来一个问题
  • 如何从webkit浏览器中检测Android版本和品牌?

    如何通过webkit浏览器检测Android版本和品牌 可靠吗 我相信你可以检查用户代理 但是 我认为它不安全 因为有很多方法可以用来欺骗用户代理 在谷歌上搜索这个问题给了我们很多答案 它甚至可以在默认浏览器上运行 您只需输入 about
  • UILabel UILongPressGestureRecognizer 不起作用?

    我怎样才能得到UILongPressGestureRecognizer在 uilabel 当我实现以下代码时 它不会调用该函数 那么请告诉我我做错了什么 UILongPressGestureRecognizer longPress UILo
  • 在尝试使用 GPS 之前如何检查 GPS 是否已启用

    我有以下代码 但效果不好 因为有时 GPS 需要很长时间 我该如何执行以下操作 检查GPS是否启用 如果启用了 GPS 请使用 GPS 否则请使用网络提供商 如果 GPS 时间超过 30 秒 请使用网络 我可以使用时间或 Thread sl
  • 下载进度条在 iOS 企业发行版中没有改变进度

    我正在通过企业分发开发和分发 iPad 应用程序 它们下载并执行良好 因此一切正常 Web 链接 ipa 文件 plist 文件 配置 问题 是 当用户单击链接进行下载时 iPad 中显示下载进度的进度条显示 正在等待 但却是空的并且永远不
  • 获取当前图片在图库中显示的位置

    在我的应用程序中 我有一个图片库 但我想检测当前显示图像的位置 例如 当我启动我的活动时 位置是 0 但是当我在图库中滚动时 我想获取当前显示图像的位置 我尝试过 OnFocusChanged OnItemClicked 但只有当我单击图库
  • Android BLE 扫描永远找不到设备

    几天以来 我尝试在我的应用程序中实现 BLE 连接 我知道我尝试连接的设备功能齐全 因此问题一定是我的代码 我用BluetoothLeScanner startScan 方法 但回调方法永远不会被调用 public void startSc
  • 我可以通过在 Android Activity 中声明适当的成员“静态”来提高效率吗

    如果一个 Activity 在实践中是单例 我认为我可以通过声明适当的成员 静态 来获得一些效率 且风险为零 是的 The Android 文档说 http developer android com guide topics fundam

随机推荐