NAudioPlayer播放器介绍
通过前面几个章节的学习后,我们掌握了实现一款播放器的基本知识。现在,我们就用学到的知识着手开发简单的音频播放器。
NAudioPlayer这款简单的音频播放器有以下几个模块组成:
- NAudioFileStream: 负责音频文件解析
- NAudioQueue: 负责音频播放
- NAudioSession: 负责会话
- NAudioPlayer: 负责管理上面的各个模块
NAudioFileStream
这个类主要负责音频文件解析,使用到我们学到的AudioFileStreamOpen。
初始化方法,需要传入音频文件路径、音频总长度
- (instancetype)initWithFilePath:(NSString *)path fileSize:(NSInteger )fileSize
{
self = [super init];
if (self) {
_path = path;
NSLog(@"文件总长度, NAudioFileStream, _fileSize: %ld", (long)_fileSize);
_fileSize = fileSize;
[self createAudioFileStream];
}return self;
}
打开文件
- (void)createAudioFileStream
{
/*
AudioFileStreamOpen的参数说明如下:
1. inClientData:用户指定的数据,用于传递给回调函数,这里我们指定(__bridge NAudioFileStream*)self
2. inPropertyListenerProc:当解析到一个音频信息时,将回调该方法
3. inPacketsProc:当解析到一个音频帧时,将回调该方法
4. inFileTypeHint:指明音频数据的格式,如果你不知道音频数据的格式,可以传0
5. outAudioFileStream:AudioFileStreamID实例,需保存供后续使用
*/
OSStatus status = AudioFileStreamOpen((__bridge void *)self, NAudioFileStreamPropertyListener, NAudioFileStreamPacketCallBack, 0, &_audioFileStreamID);
if (status != noErr) {
_audioFileStreamID = NULL;
NSLog(@"_audioFileStreamID is null");
}
NSError *error;
[self _errorForOSStatus:status error:&error];
}
解析数据
- (void)parseData:(NSData *)data
{
/// 解析数据
/// NSLog(@"每次读取data.length: %u", (unsigned int)data.length);
if (!_audioFileStreamID) {
NSLog(@"audioFileStreamID is null");
return;
}
OSStatus status = AudioFileStreamParseBytes(_audioFileStreamID, (UInt32)data.length, data.bytes, 0);
if (status != noErr) {
NSLog(@"AudioFileStreamParseBytes 失败");
}
}
拖动滑块seek
/// 拖动进度条,需要到几分几秒,而我们实际上操作的是文件,即寻址到第几个字节开始播放音频数据
- (unsigned long long)seekToTime:(double)newTime
{
if (_bitRate == 0.0 || _fileSize <= 0){
NSLog(@"_bitRate, _fileLength is 0");
return 0.0;
}
/// 近似seekByteOffset = 数据偏移 + seekToTime对应的近似字节数
_seekByteOffset = _dataOffset + (newTime / _duration) * (_fileSize - _dataOffset);
if (_packetDuration > 0) {
/*
1. 首先需要计算每个packet对应的时长_packetDuration
2. 再然后计算_packetDuration位置seekToPacket
*/
SInt64 seekToPacket = floor(newTime / _packetDuration);
UInt32 ioFlags = 0;
SInt64 outDataByteOffset;
OSStatus status = AudioFileStreamSeek(_audioFileStreamID, seekToPacket, &outDataByteOffset, &ioFlags);
NSLog(@"seek status : %d, _seekByteOffset: %llu", status, _seekByteOffset);
if ((status == noErr) && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)){
_seekByteOffset = outDataByteOffset + _dataOffset;
}
}
NSLog(@"_seekByteOffset: %llu, _fileSize: %ld", _seekByteOffset, (long)_fileSize);
/// 继续播放的操作, audioQueue处理
return _seekByteOffset;
}
关闭文件
- (void)close
{
if (!_audioFileStreamID) {
NSLog(@"audioFileStreamID is null");
return;
}
OSStatus status = AudioFileStreamClose(_audioFileStreamID);
if (status != noErr) {
NSLog(@"AudioFileStreamClose 失败");
return;
}
_audioFileStreamID = NULL;
}
音频文件读取速率
- (UInt32)calculateBitRate
{
if (_packetDuration && _processedPacketsCount > BitRateEstimationMinPackets && _processedPacketsCount <= BitRateEstimationMaxPackets){
double averagePacketByteSize = _processedPacketsSizeTotal / _processedPacketsCount;
return _bitRate = 8.0 * averagePacketByteSize / _packetDuration;
}
return 0.0;
}
音频文件总时长
- (void)calculateDuration
{
if (_fileSize > 0 && _bitRate > 0){
_duration = ((_fileSize - _dataOffset) * 8.0) / _bitRate;
}
}
首先需要计算每个packet对应的时长
- (void)calculatePacketDuration
{
if (_audioStreamBasicDescription.mSampleRate > 0) {
_packetDuration = _audioStreamBasicDescription.mFramesPerPacket / _audioStreamBasicDescription.mSampleRate;
}
NSLog(@"当前已读取了多少个packet, %.2f", _packetDuration);
}
播放速率
- (UInt32)bitRate
{
return [self calculateBitRate];
}
总时长
- (NSTimeInterval)duration
{
return ((_fileSize - _dataOffset) * 8.0) / _bitRate;
}
NAudioQueue
这个类主要负责音频文件播放,使用到我们学到的AudioQueueRef。
初始化
需要传入AudioStreamBasicDescription、AudioFileStreamID
- (instancetype)initWithAudioDesc:(AudioStreamBasicDescription)audioDesc
audioFileStreamID:(AudioFileStreamID)audioFileStreamID
{
self = [super init];
if (self) {
_started = NO;
currBufferIndex = 0;
currBufferFillOffset = 0;
currBufferPacketCount = 0;
_audioStreamBasicDescription = audioDesc;
_audioFileStreamID = audioFileStreamID;
}return self;
}
创建AudioQueueRef
/*
参数及返回说明如下:
1. inFormat:该参数指明了即将播放的音频的数据格式
2. inCallbackProc:该回调用于当AudioQueue已使用完一个缓冲区时通知用户,用户可以继续填充音频数据
3. inUserData:由用户传入的数据指针,用于传递给回调函数
4. inCallbackRunLoop:指明回调事件发生在哪个RunLoop之中,如果传递NULL,表示在AudioQueue所在的线程上执行该回调事件,一般情况下,传递NULL即可。
5. inCallbackRunLoopMode:指明回调事件发生的RunLoop的模式,传递NULL相当于kCFRunLoopCommonModes,通常情况下传递NULL即可
6. outAQ:该AudioQueue的引用实例,
*/
- (void)createAudioQueue
{
sampleRate = self.audioStreamBasicDescription.mSampleRate;
packetDuration = self.audioStreamBasicDescription.mFramesPerPacket / sampleRate;
NSLog(@"createAudioQueue, sampleRate:%.2f, packetDuration:%.2f", sampleRate, packetDuration);
OSStatus status;
status = AudioQueueNewOutput(&_audioStreamBasicDescription, NAudioQueueOutputCallback, (__bridge void * _Nullable)(self), NULL, NULL, 0, &_audioQueue);
if (status != noErr) {
NSLog(@"AudioQueueNewOutput 失败");
return;
}
NSLog(@"AudioQueueNewOutput 成功");
// start the queue if it has not been started already
// listen to the "isRunning" property
status = AudioQueueAddPropertyListener(_audioQueue, kAudioQueueProperty_IsRunning, ASAudioQueueIsRunningCallback, (__bridge void * _Nullable)(self));
if (status) {
NSLog(@"AudioQueueNewOutput error");
return;
}
#define kAQDefaultBufSize 2048 // Number of bytes in each audio queue buffer
// get the packet size if it is available
UInt32 sizeOfUInt32 = sizeof(UInt32);
status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_PacketSizeUpperBound, &sizeOfUInt32, &packetBufferSize);
if (status || packetBufferSize == 0)
{
status = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_MaximumPacketSize, &sizeOfUInt32, &packetBufferSize);
if (status || packetBufferSize == 0)
{
// No packet size available, just use the default
packetBufferSize = kAQDefaultBufSize;
}
}
NSLog(@"packetBufferSize: %d", packetBufferSize);
[self createBuffer];
NSLog(@"AudioQueueAllocateBuffer 成功!!!");
// get the cookie size
UInt32 cookieSize;
Boolean writable;
OSStatus ignorableError;
ignorableError = AudioFileStreamGetPropertyInfo(_audioFileStreamID, kAudioFileStreamProperty_MagicCookieData, &cookieSize, &writable);
if (ignorableError)
{
return;
}
// get the cookie data
void* cookieData = calloc(1, cookieSize);
ignorableError = AudioFileStreamGetProperty(_audioFileStreamID, kAudioFileStreamProperty_MagicCookieData, &cookieSize, cookieData);
if (ignorableError)
{
return;
}
// set the cookie on the queue.
ignorableError = AudioQueueSetProperty(_audioQueue, kAudioQueueProperty_MagicCookie, cookieData, cookieSize);
free(cookieData);
if (ignorableError)
{
return;
}
// 设置音量
AudioQueueSetParameter(_audioQueue, kAudioQueueParam_Volume, 1.0);
}
创建播放buffer
/*
该方法的作用是为存放音频数据的缓冲区开辟空间
参数及返回说明如下:
1. inAQ:AudioQueue的引用实例
2. inBufferByteSize:需要开辟的缓冲区的大小
3. outBuffer:开辟的缓冲区的引用实例
*/
- (void)createBuffer
{
OSStatus status;
for (int i = 0; i < kNumberOfBuffers; i++) {
status = AudioQueueAllocateBuffer(_audioQueue, kAQBufSize, &audioQueueBuffer[i]);
inUsed[i] = NO; /// 默认都是未使用
if (status != noErr) {
NSLog(@"AudioQueueAllocateBuffer 失败!!!");
continue;
}
}
NSLog(@"AudioQueueAllocateBuffer 成功!!!");
}
开始播放
- (void)start
{
if (!_audioQueue) {
NSLog(@"audioQueue is null!!!");
return;
}
OSStatus status;
/// 队列处理开始,此后系统开始自动调用回调(Callback)函数
status = AudioQueueStart(_audioQueue, nil);
if (status != noErr) {
NSLog(@"AudioQueueStart 失败!!!");
}
NSLog(@"AudioQueueStart 成功!!!");
/// 标记start始成功
_started = YES;
}
暂停
- (void)pause
{
_started = NO;
if (!_audioQueue) {
NSLog(@"audioQueue is null!!!");
return;
}
OSStatus status= AudioQueuePause(_audioQueue);
if (status!= noErr){
// [self.audioProperty error:LLYAudioError_AQ_PauseFail];
return;
}
NSLog(@"pause, status: %d", status);
}
停止
- (void)stop
{
_started = NO;
if (!_audioQueue) {
NSLog(@"audioQueue is null!!!");
return;
}
OSStatus status= AudioQueueStop(_audioQueue, YES);
if (status!= noErr){
// [self.audioProperty error:LLYAudioError_AQ_StopFail];
return;
}
NSLog(@"stop, status: %d, _started: %d", status, _started);
}
重置
- (void)reset
{
_started = NO;
if (!_audioQueue) {
NSLog(@"audioQueue is null!!!");
return;
}
OSStatus status = AudioQueueReset(_audioQueue);
if (status!= noErr){
// [self.audioProperty error:LLYAudioError_AQ_StopFail];
return;
}
NSLog(@"reset, status: %d", status);
}
销毁
- (void)dispose
{
if (!_audioQueue) {
NSLog(@"audioQueue is null!!!");
return;
}
OSStatus status = AudioQueueDispose(_audioQueue, YES);
if (status!= noErr){
// [self.audioProperty error:LLYAudioError_AQ_StopFail];
return;
}
NSLog(@"dispose, status: %d", status);
}
回收buffer
- (void)freeBuffer
{
for (NSInteger i = 0; i < kNumberOfBuffers; i++) {
OSStatus status = AudioQueueFreeBuffer(_audioQueue, audioQueueBuffer[i]);
if (status!= noErr){
// [self.audioProperty error:LLYAudioError_AQ_StopFail];
return;
}
NSLog(@"freeBuffer, status: %d", status);
}
}
配置数据
- (void)playData:(NSData *)data
inputData:(nonnull const void *)inputData
inNumberPackets:(UInt32)inNumberPackets
packetDescriptions:(AudioStreamPacketDescription *)packetDescriptions
isEof:(BOOL)isEof
{
[_lock lock];
if (inputData == NULL) {
NSLog(@"inputData is null");
[_lock unlock];
return;
}
if (packetDescriptions == NULL) {
NSLog(@"packetDescriptions is null");
[_lock unlock];
return;
}
for (int i = 0; i < inNumberPackets; ++i) {
/// 获取 AudioStreamPacketDescription对象
AudioStreamPacketDescription packetDesc = packetDescriptions[i];
SInt64 packetOffset = packetDesc.mStartOffset;
UInt32 packetSize = packetDesc.mDataByteSize;
if ((packetSize + bytesFilled) >= packetBufferSize) {
/*
该方法用于将已经填充数据的AudioQueueBuffer入队到AudioQueue
*/
NSLog(@"当前buffer_%u已经满了,送给audioqueue去播吧",(unsigned int)fillBufferIndex);
[self enqueueBuffer];
}
NSLog(@"给当前buffer_%u填装数据中",(unsigned int)fillBufferIndex);
/// 给当前buffer填充数据
@synchronized(self)
{
// If there was some kind of issue with enqueueBuffer and we didn't
// make space for the new audio data then back out
//
if (bytesFilled + packetSize > packetBufferSize){
return;
}
// copy data to the audio queue buffer
AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex];
memcpy((char*)fillBuf->mAudioData + bytesFilled, (const char*)inputData + packetOffset, packetSize);
fillBuf->mAudioDataByteSize = bytesFilled + packetSize;
// fill out packet description
packetDescs[packetsFilled] = packetDescriptions[i];
packetDescs[packetsFilled].mStartOffset = bytesFilled;
// keep track of bytes filled and packets filled
bytesFilled += packetSize;
packetsFilled += 1;
}
}
[_lock unlock];
}
将buffer交给AudioQueue播放
- (void)enqueueBuffer
{
@synchronized(self){
inuse[fillBufferIndex] = YES;
OSStatus status;
AudioQueueBufferRef fillBuf = audioQueueBuffer[fillBufferIndex];
if (packetsFilled > 0){
status = AudioQueueEnqueueBuffer(_audioQueue, fillBuf, packetsFilled, packetDescs);
}else{
status = AudioQueueEnqueueBuffer(_audioQueue, fillBuf, 0, NULL);
}
if (status != noErr) {
NSLog(@"enqueueBuffer error, status: %d", status);
return;
}
if (!_started) {
NSLog(@"播放开始, status: %d, fillBufferIndex: %u", status, fillBufferIndex);
[self start];
}
// go to next buffer
if (++fillBufferIndex >= kNumberOfBuffers) fillBufferIndex = 0;
bytesFilled = 0; // reset bytes filled
packetsFilled = 0; // reset packets filled
}
// wait until next buffer is not in use
pthread_mutex_lock(&queueBuffersMutex);
while (inuse[fillBufferIndex]){
pthread_cond_wait(&queueBufferReadyCondition, &queueBuffersMutex);
}
pthread_mutex_unlock(&queueBuffersMutex);
}
NAudioPlayer
负责调度模块,提供api供外部使用
初始化
- (instancetype)initWithFilePath:(NSString *)filePath
{
self = [super init];
if (self) {
_status = NAudioPlayerStatusStopped;
_pauseRequired = NO;
_seekWasRequested = NO;
_filePath = filePath;
_audioFileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
_fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] fileSize];
NSLog(@"文件总长度, NAudioPlayer, _fileSize: %llu", _fileSize);
}return self;
}
解析文件
- (void)createAudioFileStream
{
_audioFileStream = [[NAudioFileStream alloc] initWithFilePath:_filePath fileSize:_fileSize];
_audioFileStream.delegate = self;
}
开始播放
- (void)play
{
if (self.status == NAudioPlayerStatusStopped) {
/// 更新播放状态: 等待播放
[self setStatusInternal:(NAudioPlayerStatusWaiting)];
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
[_thread start];
}else if (self.status == NAudioPlayerStatusPaused){
/// 更新播放状态: 等待播放
[self setStatusInternal:(NAudioPlayerStatusWaiting)];
[_audioQueue start];
}
}
暂停
- (void)pause
{
if (!_audioQueue) {
NSLog(@"_audioQueue is null");
return;
}
if (self.status == NAudioPlayerStatusWaiting || self.status == NAudioPlayerStatusPlaying) {
NSLog(@"暂停播放");
/// 更新播放状态: NAudioPlayerStatusPaused
[self setStatusInternal:NAudioPlayerStatusPaused];
[self.audioQueue pause];
// _pauseRequired = YES;
}
}
停止
- (void)stop
{
if (!_audioQueue) {
NSLog(@"_audioQueue is null");
return;
}
if (self.status == NAudioPlayerStatusWaiting || self.status == NAudioPlayerStatusPlaying) {
NSLog(@"停止播放");
/// 更新播放状态: NAudioPlayerStatusStopped
[self setStatusInternal:NAudioPlayerStatusStopped];
[self.audioQueue stop];
}
}
seek
- (void)seekToTime:(double)newTime
{
@synchronized (self) {
_seekWasRequested = YES;
_seekTime = newTime;
}
}
do。。。while循环解析数据并启动播放
- (void)threadMain
{
/// 创建文件解析对象
if (!_audioFileStream) {
[self createAudioFileStream];
}
do {
// NSLog(@"do。。。");
/// seek
if (_seekWasRequested) {
NSLog(@"拖动滑块seek了");
[self setStatusInternal:(NAudioPlayerStatusStopped)];
_timingOffset = _seekTime - _audioQueue.playedTime;
unsigned long long _seekOffset = [_audioFileStream seekToTime:_seekTime];
[_audioFileHandle seekToFileOffset:_seekOffset];
[_audioQueue reset];
_seekWasRequested = NO;
[self setStatusInternal:(NAudioPlayerStatusWaiting)];
}
/// pause
if (self.status == NAudioPlayerStatusPaused) {
NSLog(@"for 循环 里 暂停播放");
[self setStatusInternal:NAudioPlayerStatusStopped];
[self.audioQueue pause];
}
/// play
if (self.status == NAudioPlayerStatusWaiting || self.status == NAudioPlayerStatusPlaying) {
[self _play];
}
} while (self.status != NAudioPlayerStatusStopped);
[_audioFileStream close];
[self.audioFileHandle closeFile];
}
- (void)_play
{
/// 更新播放状态: 等待播放
/// [self setStatusInternal:(NAudioPlayerStatusWaiting)];
/// 外部读的数据比内部多
NSData *data = [self.audioFileHandle readDataOfLength:kAudioFileBufferSize];
if ((data == nil) || ([data length] == 0)) {
[self stop];
return;
}
[_audioFileStream parseData:data]; /// 解析数据
}
设置播放状态
- (void)setStatusInternal:(NAudioPlayerStatus)status
{
if (_status == status)
{
return;
}
[self willChangeValueForKey:@"status"];
_status = status;
[self didChangeValueForKey:@"status"];
}
音频总时长
- (NSTimeInterval)duration
{
if (!_audioFileStream) {
return 0;
}
return [_audioFileStream duration];
}
播放进度
- (NSTimeInterval)progress
{
if (_seekWasRequested)
{
return _seekTime;
}
return _timingOffset + _audioQueue.playedTime;
}