动手做一个iOS音乐播放器(2)AVAudioPlayer与AVAudioEngine

AVAudioPlayer播放音频的方法是最简单的,传入一个url或data初始化,然后play、pause、stop、currentTime等操作直接调用,还有播放完成后的代理回调,真是方便。
但也有局限性,它不能做一些例如混响、均衡器等处理。

如果我想要做一个有均衡器调节的播放器,那么我只能自制一个MyAudioPlayer了。
然后仿制了AVAudioPlayer的部分用法:

@protocol AVAudioPlayerDelegate;

@interface MyAudioPlayer : NSObject

//@property (nonatomic, strong) NSMutableData *pcmData;
@property (nonatomic, assign) NSTimeInterval currentTime;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, strong) NSURL *url;
@property (nonatomic, assign, getter=isPlaying) BOOL playing;
@property (nonatomic, weak) id<AVAudioPlayerDelegate> delegate;

- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (void)play;
- (void)pause;
- (void)stop;

@end

偶然找到了AVFoundation里面有AVAudioEngine这一套更能凸显个性的工具。

其中AVAudioPlayerNode是音源,AVAudioUnitEffect是效果器,最后mix和输出由engine完成


AVAudioEngine的其中一种连接方法

这套工具的用法非常贴近生活,而且有点眼熟。。。。就像下图👇中的样子。


就是这个感觉

那么我就用MyAudioPlayer来封装这套AVAudioEngine逻辑:

初始化:

- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * _Nullable __autoreleasing *)outError {
    self = [super init];
    if (self) {
//        self.duration = 10000000;
        self.url = url;
        [self myInit];
    }
    return self;
}


- (void)myInit {
    // create engine
    self.engine = [[AVAudioEngine alloc] init];

    // 音源
    self.playerNode = [[AVAudioPlayerNode alloc] init];

    // 一个10段均衡器
    self.audioEQ = [[AVAudioUnitEQ alloc] initWithNumberOfBands:10];
    AVAudioUnitEffect *effect = self.audioEQ;
    
    // 各种连线,注意顺序
    AVAudioMixerNode *mixer = self.engine.mainMixerNode;
    AVAudioFormat *format = [mixer outputFormatForBus:0];
    [self.engine attachNode:self.playerNode];
    [self.engine attachNode:effect];
    [self.engine connect:self.playerNode to:effect format:format];
    [self.engine connect:effect to:mixer format:format];
    
    // 打开电源开关
    NSError *error = nil;
    [self.engine startAndReturnError:&error];
    
    // 根据url创建一个audioFile
    self.audioFile = [[AVAudioFile alloc] initForReading:self.url error:nil];
    
    // 计算播放时长,这里似乎一个frame就是一个sample,所以直接用样品数除以采样率得到时间。
    AVAudioFrameCount frameCount = (AVAudioFrameCount)self.audioFile.length;
    double sampleRate = self.audioFile.processingFormat.sampleRate;
    if (sampleRate != 0) {
        self.duration = frameCount / sampleRate;
    } else {
        self.duration = 1;
    }
    
    // play file, or buffer
    __weak typeof(self) weself = self;
    // 我这里用setCurrentTime的方法来控制播放进度
    self.currentTime = 0.01;
    
//    // init a timer to catch current time;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01 repeats:YES block:^(NSTimer *timer) {
        [weself catchCurrentTime];
    }];
}

关于这个带block的timer在iOS10之前怎么办,如何在iOS9或更老系统版本中使用NSTimer+Block方法

播放、暂停、停止、完成后代理回调:

- (void)play {
    // 记得要电源开着的时候才能让playerNode play,否则会crash。(这不现实啊😂)
    if (!self.engine.running) {
        [self.engine prepare]; // 预防中断恢复后crash!!!
        [self.engine startAndReturnError:nil];
    }
    [self.playerNode play];
}

- (void)pause {
    [self.engine stop]; // 为什么这里要stop呢?如果不,到后面就会发现控制中心里的暂停键不会变化。
    [self.playerNode pause];
}

- (void)stop {
    // 一般来说,stop就代表着结束,那么就全部都结束吧。
    self.delegate = nil; // 手动停的必须设delegate nil,不然回调出去又播放下一首了,内存超大
    if (self.isPlaying) {
        [self.playerNode stop];
    }
    [self.engine stop];
}

- (void)didFinishPlay { 
    // 这里还用着原来的AVAudioPlayerDelegate
    if ([self.delegate respondsToSelector:@selector(audioPlayerDidFinishPlaying:successfully:)]) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.delegate audioPlayerDidFinishPlaying:(id)self successfully:self.isPlaying];
        });
    }
}

- (BOOL)isPlaying {
    return self.playerNode.isPlaying;
}

难点来了,设置和获取当前时间:

// 设置当前播放时间,上文中调用了一下self.currentTime = xxx,目的是为了顺便设置一下播放内容
- (void)setCurrentTime:(NSTimeInterval)currentTime {
    _currentTime = currentTime;
    
    BOOL isPlaying = self.isPlaying;
    id lastdelegate = self.delegate;
    self.delegate = nil;
    [self.playerNode stop];
    self.delegate = lastdelegate;
    __weak typeof(self) weself = self;
    AVAudioFramePosition startingFrame = currentTime * self.audioFile.processingFormat.sampleRate;
    // 要根据总时长和当前进度,找出起始的frame位置和剩余的frame数量
    AVAudioFrameCount frameCount = (AVAudioFrameCount)(self.audioFile.length - startingFrame);
    if (frameCount > 1000) { // 当剩余数量小于0时会crash,随便设个数
        lastStartFramePosition = startingFrame;
        [self.playerNode scheduleSegment:self.audioFile startingFrame:startingFrame frameCount:frameCount atTime:nil completionHandler:^{
            [weself didFinishPlay];
        }]; // 这里只有这个scheduleSegement的方法播放快进后的“片段”
    }
    if (isPlaying) {
        [self.playerNode play]; // 恢复播放
    }
}

// 获取当前播放时间
- (void)catchCurrentTime {
    if (self.playing) {
        AVAudioTime *playerTime = [self.playerNode playerTimeForNodeTime:self.playerNode.lastRenderTime];
        _currentTime = (lastStartFramePosition + playerTime.sampleTime) / playerTime.sampleRate;
        // 注意这里用了上文设置的lastStartFramePosition,原因是sampleTime是相对于它的,所以绝对的播放位置应该是lastStartFramePosition + sampleTime
    }
    if (_currentTime > self.duration) {
        [self.playerNode stop];
    }

最后记得dealloc时:

- (void)dealloc {
    NSLog(@"dealloc: %@", self);
    self.delegate = nil;
    [self.playerNode stop];
    [self.engine stop];
    [self.timer invalidate];
}

项目代码:https://github.com/ZJamm1993/simple_music_player.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,896评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,283评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,085评论 0 367
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,010评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,015评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,492评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,858评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,829评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,374评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,409评论 3 346
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,527评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,131评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,858评论 3 339
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,296评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,434评论 1 276
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,087评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,597评论 2 366

推荐阅读更多精彩内容