前言
上一篇文章《实时音视频的那些事儿(二) —— 音频采集》中我们讲到了如何在iOS、Android、Windows平台实现音频采集,今天将介绍如何实现音频的编码。
一、iOS 中使用 AudioUnit 实现音频编码的过程
AudioUnit 是 iOS 中的音频处理框架,它提供了一组低级别的 API,用于音频输入/输出、音频处理和音频编码/解码。
在 iOS 中,常见的音频编码格式有 AAC、MP3、AMR 等。使用 AudioUnit 进行音频编码的一般流程如下:
创建输入 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);
配置 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));
创建编码器
配置好输入 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: 方法用于获取编码器的描述信息,需要根据不同的编码格式进行修改。
处理音频数据
配置好输入 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 中。
销毁 AudioUnit
在使用完毕后,需要销毁创建的 AudioUnit。销毁方法如下:
AudioUnitUninitialize(_audioUnit);
AudioComponentInstanceDispose(_audioUnit);
以上是使用 AudioUnit 实现 iOS 音频编码的基本流程,其中还有很多需要处理的细节,如错误处理、断点续传、编码后数据的打包等,需要按照具体需求进行编写。
二、Android平台下音频编码
Android平台下,音频编码一般使用的是基于OpenCore的Audio Encoder实现。它可以通过Android Native层的JNI接口来调用,实现录音数据的压缩编码。
该编码过程主要包括音频数据采集、音频数据压缩、音频数据封装三个部分。
音频数据采集
音频数据采集部分使用的是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);
音频数据压缩
音频数据压缩部分使用的是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);
音频数据封装
音频数据封装部分使用的是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编码,大概可以分为一下几个步骤:
初始化音频设备:使用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;
}
创建编码器:选择合适的音频编码算法,例如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];
读取音频数据:使用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);
编码音频数据:每次读取完一段音频数据后,将音频数据传入编码器中进行编码,并将编码后的数据输出。
// 编码音频数据
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);
停止音频采集:调用waveInStop()函数停止读取音频数据,使用waveInReset()函数清空缓冲区。
// 停止音频采集
waveInStop(hWaveIn);
waveInReset(hWaveIn);
result = waveInUnprepareHeader(hWaveIn, &waveHeader, sizeof(WAVEHDR));
释放资源:使用waveInClose()函数关闭音频设备,使用编码器提供的API释放编码器实例。
// 释放资源
delete[] inputBuffer;
delete[] outputBuffer;
lame_close(lameEncoder);
waveInClose(hWaveIn);
四、音频编码可以让我们实现一些有趣的功能
实时高清语音聊天
实时高清语音聊天是一种非常流行的应用程序,它能够让用户轻松地进行实时通话。这种应用程序通常使用音频编解码技术来实现高音质声音的传输和回放。
音乐播放
音乐播放器通常会使用音频编解码技术来压缩和解压缩音频文件。这样可以使音乐文件更小,并且可以在更短的时间内进行下载和传输。
语音识别
语音识别是一种非常有趣的应用程序,它能够让计算机理解和处理人类语言。这种应用程序通常会使用音频编解码技术来处理录音的语音文件。
唱歌比赛
唱歌比赛是一种非常流行的娱乐方式。这种应用程序通常会使用音频编解码技术来处理录音的歌唱文件。每个参赛者可以将自己的表演录制下来,并将其提交给比赛主办方进行评分。
总之,音频编码技术在现代生活中扮演着重要角色,对我们的日常生活产生了很大的影响。