iOS音频播放(八)一款简单播放器NAudioPlayer的实现

·  阅读 818

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;
}
复制代码

项目地址

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改