kxmovie 源码分析(3)-KxAudioManager
学习该类,我们应该首先学习该类播放音频的知识.大概浏览下该类,该类播放音频使用的技术框架AudioTooBox框架,因此,我们先要用该框架进行音频播放,才能看懂作者的封装.
基础知识
audioSession 结构图
音频是ios ,tvos和watchos的托管服务。系统通过AudioSession管理应用程序,应用程序之间和设备基本的音频行为。
表达音频行为的主要机制就是AudioSession的category。通过设置category,我们可以指示应用程序是输入路径还是输出路径(录音还是播放),是否希望音乐继续与音频一起播放,等等。
配置音频会话
音频会话的category是标识app的一组音频行为的key。通过设置category,我们可以向系统指出我们的音频意图。例如,当翻转Ringer/Silent开关时候我们的音频是否应该继续,我们可以通过设置音频会话category 覆盖和修改器开关来定义音频行为。
Category | 通过铃声/静音开关或者锁屏是否需要静音 | 是否中断不可以混合的音频 | 支持录音和播放 |
---|---|---|---|
AVAudioSessionCategoryAmbient | Yes | No | Output only |
AVAudioSessionCategorySoloAmbient (Default) | Yes | Yes | Output only |
AVAudioSessionCategoryPlayback | No | Yes by default; no by using override switch | Output only |
AVAudioSessionCategoryRecord | No (recording continues with screen locked) | Yes | Input only |
AVAudioSessionCategoryPlayAndRecord | No | Yes by default; no by using override switch | Input and output |
AVAudioSessionCategoryMultiRoute | No | Yes | Input and output |
音频播放步骤
1.
AudioSessionInitialize
初始化音频
2.通过kAudioSessionProperty_AudioRoute
category 检测下音频组件是否正常.(kAudioSessionProperty_AudioRoute
在ios 7 以后用kAudioSessionProperty_AudioRouteDescription
category)
- 声明音频category
kAudioSessionProperty_AudioCategory
中的播放组件kAudioSessionCategory_MediaPlayback
- 设置监听中断事件
kAudioSessionProperty_AudioRouteChange
- 设置监听硬件声音改变事件
kAudioSessionProperty_CurrentHardwareOutputVolume
6 .设置音频buffer 大小kAudioSessionProperty_PreferredHardwareIOBufferDuration
该属性可以提高实时响应速度。这将导致RemoteIO更频繁地请求更短的回调缓冲区(在实时线程上)。不要在这些实时音频回调中花费大量时间,否则将终止应用程序的音频线程和所有音频。 与使用NSTimer相比,只计算较短的RemoteIO回调缓冲区的精度要高10倍,并且延迟更低。对音频回调缓冲区内的样本进行计数以定位声音混合的开始会给您提供亚毫秒相对时间。
- 激活音频回话
- 获取采样速率
kAudioSessionProperty_CurrentHardwareSampleRate
,声音大小kAudioSessionProperty_CurrentHardwareOutputVolume
- 声明音频组件
_audioUnit
10.获取音频组件的输出流- 设置渲染回调函数
在渲染回调函数里面,我们就需要向buffer中输入数据
- 这个时候音频组件就会不停的调用这个回调函数,我们需要向这个回调函数中写入音频数据就可以播放了.
- (BOOL) renderFrames: (UInt32) numFrames
ioData: (AudioBufferList *) ioData
{
for (int iBuffer=0; iBuffer < ioData->mNumberBuffers; ++iBuffer) {
memset(ioData->mBuffers[iBuffer].mData, 0, ioData->mBuffers[iBuffer].mDataByteSize);
}
if (_playing && _outputBlock ) {
// Collect data to render from the callbacks
_outputBlock(_outData, numFrames, _numOutputChannels);
// Put the rendered data into the output buffer
if (_numBytesPerSample == 4) // then we've already got floats
{
float zero = 0.0;
for (int iBuffer=0; iBuffer < ioData->mNumberBuffers; ++iBuffer) {
int thisNumChannels = ioData->mBuffers[iBuffer].mNumberChannels;
for (int iChannel = 0; iChannel < thisNumChannels; ++iChannel) {
vDSP_vsadd(_outData+iChannel, _numOutputChannels, &zero, (float *)ioData->mBuffers[iBuffer].mData, thisNumChannels, numFrames);
}
}
}
else if (_numBytesPerSample == 2) // then we need to convert SInt16 -> Float (and also scale)
{
// dumpAudioSamples(@"Audio frames decoded by FFmpeg:\n",
// _outData, @"% 12.4f ", numFrames, _numOutputChannels);
float scale = (float)INT16_MAX;
vDSP_vsmul(_outData, 1, &scale, _outData, 1, numFrames*_numOutputChannels);
#ifdef DUMP_AUDIO_DATA
LoggerAudio(2, @"Buffer %u - Output Channels %u - Samples %u",
(uint)ioData->mNumberBuffers, (uint)ioData->mBuffers[0].mNumberChannels, (uint)numFrames);
#endif
for (int iBuffer=0; iBuffer < ioData->mNumberBuffers; ++iBuffer) {
int thisNumChannels = ioData->mBuffers[iBuffer].mNumberChannels;
for (int iChannel = 0; iChannel < thisNumChannels; ++iChannel) {
vDSP_vfix16(_outData+iChannel, _numOutputChannels, (SInt16 *)ioData->mBuffers[iBuffer].mData+iChannel, thisNumChannels, numFrames);
}
#ifdef DUMP_AUDIO_DATA
dumpAudioSamples(@"Audio frames decoded by FFmpeg and reformatted:\n",
((SInt16 *)ioData->mBuffers[iBuffer].mData),
@"% 8d ", numFrames, thisNumChannels);
#endif
}
}
}
return noErr;
}
这就是音频组件的buffer中写入数据的具体过程
- 首先清空音频回调buffer 中的数据
- 从外界获取数据.保存在_outData 中
- 根据编码大小来向缓存buffer中写入数据
写入buffer 中的时候出现了一个函数 vDSP_vsadd 该函数的作用
vDSP_vsadd( const float *__A,vDSP_Stride __IA,const float *__B, float *__C, vDSP_Stride __IC, vDSP_Length __N)
上面的公式等于for (n = 0; n < N; ++n) C[n] = A[n] + B[0];
vDSP_vsmul 函数的使用
for (n = 0; n < N; ++n) C[n] = A[n] * B[0];
这里我们看看外界如何写数据到buffer中的的
写入数据在KxMovieViewController 的- (void) enableAudio: (BOOL) on
函数中
真正的写入数据是下列的方法
- (void) audioCallbackFillData: (float *) outData
numFrames: (UInt32) numFrames
numChannels: (UInt32) numChannels
{
//fillSignalF(outData,numFrames,numChannels);
//return;
if (_buffered) {
memset(outData, 0, numFrames * numChannels * sizeof(float));
return;
}
@autoreleasepool {
while (numFrames > 0) {
if (!_currentAudioFrame) {
@synchronized(_audioFrames) {
NSUInteger count = _audioFrames.count;
if (count > 0) {
KxAudioFrame *frame = _audioFrames[0];
#ifdef DUMP_AUDIO_DATA
LoggerAudio(2, @"Audio frame position: %f", frame.position);
#endif
if (_decoder.validVideo) {
const CGFloat delta = _moviePosition - frame.position;
if (delta < -0.1) {
memset(outData, 0, numFrames * numChannels * sizeof(float));
#ifdef DEBUG
LoggerStream(0, @"desync audio (outrun) wait %.4f %.4f", _moviePosition, frame.position);
_debugAudioStatus = 1;
_debugAudioStatusTS = [NSDate date];
#endif
break; // silence and exit
}
[_audioFrames removeObjectAtIndex:0];
if (delta > 0.1 && count > 1) {
#ifdef DEBUG
LoggerStream(0, @"desync audio (lags) skip %.4f %.4f", _moviePosition, frame.position);
_debugAudioStatus = 2;
_debugAudioStatusTS = [NSDate date];
#endif
continue;
}
} else {
[_audioFrames removeObjectAtIndex:0];
_moviePosition = frame.position;
_bufferedDuration -= frame.duration;
}
_currentAudioFramePos = 0;
_currentAudioFrame = frame.samples;
}
}
}
if (_currentAudioFrame) {
const void *bytes = (Byte *)_currentAudioFrame.bytes + _currentAudioFramePos;
const NSUInteger bytesLeft = (_currentAudioFrame.length - _currentAudioFramePos);
const NSUInteger frameSizeOf = numChannels * sizeof(float);
const NSUInteger bytesToCopy = MIN(numFrames * frameSizeOf, bytesLeft);
const NSUInteger framesToCopy = bytesToCopy / frameSizeOf;
memcpy(outData, bytes, bytesToCopy);
numFrames -= framesToCopy;
outData += framesToCopy * numChannels;
if (bytesToCopy < bytesLeft)
_currentAudioFramePos += bytesToCopy;
else
_currentAudioFrame = nil;
} else {
memset(outData, 0, numFrames * numChannels * sizeof(float));
//LoggerStream(1, @"silence audio");
#ifdef DEBUG
_debugAudioStatus = 3;
_debugAudioStatusTS = [NSDate date];
#endif
break;
}
}
}
}
入参 outData 需要写入音频的buffer
numFrames 一共需要写入的帧数
numChannels 通到的数量.以上参数理解可以参考这里.定位到->理解音频单元的渲染回调函数 标题
这里需要了解点音频知识,各种概念之间的计算
struct AudioStreamBasicDescription
{
Float64 mSampleRate;
AudioFormatID mFormatID;
AudioFormatFlags mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
mSampleRate 音频采样率 (相当于一秒生成mSampleRate channel)
mBytesPerPacket 一个packet的大小
mFramesPerPacket 一个packet 有多少个frames
mBytesPerFrame 一帧有多少字节
mChannelsPerFrame 一帧有多少通道
mBitsPerChannel 每个通道多少字节
假设采样率 为44100 双通道 , 那么一秒的样本大小就是
样本大小 = 44100 * 2 * mBitsPerChannel;
假设采样率是44100 单通道 , 那么一秒的样本大小就是
样本大小 = 44100 * 1* mBitsPerChannel;
我们需要知道的知识 ,一个packet 在音频没有压缩的情况下,默认一个packet 包含一个frame , 而每个frame是包含所有通道数量的 .
结构关系图如下
结构关系图因此我们知道根据frame 的数量和 channel 的数量,是没办法计算出packet的大小的,只要知道了channel的大小,就可以计算出packet大小了
函数解释
1.调用memset 清理掉
outData
中的数据. 这里我们知道我们应该是channel大小是sizeof(float)=4
- 从音频数组中获取出出第一帧音频KxAudioFrame
- 获取samples 赋值给_currentAudioFrame
const NSUInteger frameSizeOf = numChannels * sizeof(float);
声明每一帧的大小
5.const NSUInteger bytesToCopy = MIN(numFrames * frameSizeOf, bytesLeft);
找buffer 和 需要copy进buffer的字节较小的值(copy内容的多少不能大于buffer )
6.计算需要copy的帧数
- 将数据copy进入buffer中
- 计算帧数和buffer 数据偏移
基本流程如图
image.png
这里我们添加在outdata中的数据是 每个channel四个字节的数据.这里我们就要看解码成音频的帧数据的格式了.
这里我们在回来回顾下- (kxMovieError) openAudioStream: (NSInteger) audioStream
函数
- (kxMovieError) openAudioStream: (NSInteger) audioStream
{
AVCodecContext *codecCtx = _formatCtx->streams[audioStream]->codec;
SwrContext *swrContext = NULL;
AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
if(!codec)
return kxMovieErrorCodecNotFound;
if (avcodec_open2(codecCtx, codec, NULL) < 0)
return kxMovieErrorOpenCodec;
if (!audioCodecIsSupported(codecCtx)) {
id<KxAudioManager> audioManager = [KxAudioManager audioManager];
swrContext = swr_alloc_set_opts(NULL,
av_get_default_channel_layout(audioManager.numOutputChannels),
AV_SAMPLE_FMT_S16,
audioManager.samplingRate,
av_get_default_channel_layout(codecCtx->channels),
codecCtx->sample_fmt,
codecCtx->sample_rate,
0,
NULL);
if (!swrContext ||
swr_init(swrContext)) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(codecCtx);
return kxMovieErroReSampler;
}
}
_audioFrame = av_frame_alloc();
if (!_audioFrame) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(codecCtx);
return kxMovieErrorAllocateFrame;
}
_audioStream = audioStream;
_audioCodecCtx = codecCtx;
_swrContext = swrContext;
AVStream *st = _formatCtx->streams[_audioStream];
avStreamFPSTimeBase(st, 0.025, 0, &_audioTimeBase);
LoggerAudio(1, @"audio codec smr: %.d fmt: %d chn: %d tb: %f %@",
_audioCodecCtx->sample_rate,
_audioCodecCtx->sample_fmt,
_audioCodecCtx->channels,
_audioTimeBase,
_swrContext ? @"resample" : @"");
return kxMovieErrorNone;
}
swrContext = swr_alloc_set_opts(NULL,
av_get_default_channel_layout(audioManager.numOutputChannels),
AV_SAMPLE_FMT_S16,
audioManager.samplingRate,
av_get_default_channel_layout(codecCtx->channels),
codecCtx->sample_fmt,
codecCtx->sample_rate,
0,
NULL);
这里我们设置输出数据的通道数-> 这个是音频回话中开启就知道了
AV_SAMPLE_FMT_S16 是我们每个channel的大小. 取样点是2^16
输出的取样率 audioManager.samplingRate
剩下的是解码器的取样率 通道数和 channel的大小
因此该函数就是将解码器的取样率转换成设备的取样率
到此,源码主要的流程已经完毕.剩下的就不做分析了
这里可以将音频部分换成AVFoundation 来播放.