一个音乐播放器的踩坑实践

前言

这是这个系列的第二篇文章,和第一篇文章相同的是Demo中的资源文件和一些关键代码是搜索和学习得来的。一是因为没有相关的资源文件,譬如音乐文件、歌词文件、歌曲封面等;二是着实有点力有未逮的感觉(ps:在Demo具体功能中体现出来的就是歌词随着歌曲播放进度的不断滚动以及颜色的渐变,主要的原因是不知道lrc的文件内容以及一些API的用法,后面将会谈到。)。为了“发现更大的世界”(ps:其实就想捡点便宜),就百度了一番,在这里找到了“好东西”!笔者学习了其中的关键代码,并结合自己的想法改造了代码和实现了一些新的功能,像歌词颜色渐变之类的。如果你愿意继续看下去的话,那咱们接着慢慢讲!

三军未动,粮草先行

Demo的功能界面展示在原作者博客中可以看到,这里就不赘述了。既然想要做一个音乐播放器,如果只有音乐的声音,那就太单调了,再次感谢原作者的分享精神!有了上面的一系列“粮草”(包括一些UI的设计),就不至于在“半路”上被“饿死”了!

呃!等等,先来看看以lrc作为后缀的文件里面是怎样的“乾坤”:

这里写图片描述

简单来说,我们可以从中得到时间信息和歌词信息,只不过它们不是唾手可得,而需要通过一定的数据拆分和加工。每两行的时间差可以帮助我们控制歌词颜色渐变的时间,但有一个细节应当注意:需要保证下一行的歌词信息存在。得益于歌词文件的最后一行为空白行,时间的确定只需让当前一行减去下一行的开始时间即可。

显然,这样做能实现的只是时间均匀的过渡,要实现向酷狗音乐那样有快有慢的节奏从技术上来看只需要调整时间分段即可,但是从lrc的文件中我们得不到关于每段歌词的时间分量。如果你用酷狗下载过音乐歌词的话,你会发现它是krc为后缀的文件。可以想到的一点是这里面一定有关于每一行歌词各部分时间分量的控制信息。在早期的歌词显示中,歌词都是采用这样均匀的渐变。估计在智能手机普及之后,又有公司(比如酷狗音乐)花了精力在歌词中融入了更多的信息来让用户获得更好的使用体验,也算是提高市场竞争力的一种手段吧,像虾米音乐的歌词还是沿用均匀渐变这一设计。貌似krc只能用酷狗音乐软件,还要求是在播放状态读取,这是对自我知识产权的保护。没办法了,我们就将就lrc用用吧!

遵循Demo数据从简原则,笔者就用plist文件来展示歌曲的详细信息,下面是一首歌曲的信息:

这里写图片描述

同时将它与Music类关联起来:

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *filename;
@property (nonatomic, copy) NSString *lrcname;
@property (nonatomic, copy) NSString *singer;
@property (nonatomic, copy) NSString *singerIcon;

这样就设计好了音乐展示页面的数据模型了,并构造了一个MusicTool类,来避免控制器之间直接横向依赖,也就是笔者在这篇文章里面所转载的文章中所说的“依赖下沉”。MusicTool用来获取所有音乐,并通过文件名获取当前要播放的音乐。下一曲和上一曲就简单的通过增加和减少当前音乐索引值,在所有的音乐中用该值索引即可。

Demo中将音乐的播放功能通过一个AudioManager类的单例来管理,包含播放、暂停、停止三个业务。

知道了lrc歌词文件的内容,我们就能合理地设计数据模型来从文件中解析出实现业务功能所需要的数据了。Demo中以LyricLines类的实例来集合一句歌词中需要被外界的访问的各类信息,就像这样:

@interface LyricLines : NSObject

@property (copy, nonatomic) NSString *word;

@property (assign, nonatomic) CGFloat gradientTime;
@property (assign, nonatomic) CGFloat time;
@property (assign, nonatomic) CGRect stringRect;

 - (NSMutableArray<LyricLines *> *)lyricLinesWithFileName:(NSString *)fileName;
  • word是具体的歌词

  • gradientTime是当前歌词渐变时长

  • time是当前歌词的时间信息

  • stringRect是歌词的长度尺寸(ps:这个用于设置渐变时前景UILabelframe,这是根据歌词文字加上cell的宽高计算出的,目的是更加精确地控制渐变过程,即让前景UILabel大小与歌词文字完全重合)

下面的这个静态方法可以根据歌词文件名返回对应歌曲的歌词信息数组,它主要包含了这样一些逻辑:

  1. 通过NSBundle的动态方法URLForResource:得到歌词文件的NSURL,再用NSString的动态方法stringWithContentsOfURL:encoding:error:获取歌词的字符串流。

  2. 以“\n”分割该字符串流,分割后将生成一个数组,数组的每个下标元素均代表一行歌词。此时,时间信息与歌词信息是一个整体,即字符串。

  3. 使用枚举器遍历数组,即enumerateObjectsUsingBlock:,分割当前行歌词时间分量、渐变时长、歌词信息和歌词长度,然后存入一个LyricLines实例对应的属性中,并加入到一个临时可变数组,最后返回该数组即可。

在计算时间时,用到了一个自定义的时间转换工具TimeConvertTool——用于将时间字符串与时间浮点数相互转换,下面是它对外的接口:

@interface TimeConvertTool : NSObject

+ (NSString *)timeNormalStringWithTime:(NSTimeInterval)time;

+ (NSTimeInterval)timeWithTimeDetailString:(NSString *)timeDetailString;

@end

Just do a sample demo

这里面设计了两个控制器MusicViewControllerPlayingViewController。前者是歌曲信息的展示控制器,后者是播放界面控制器。(ps:起初起名字没怎么思考,后面当发现命名有所不当时,想改时又发现成本太大,就放置了这一想法。命名也是学问啊!)

展示控制器的界面是这个样子的:

这里写图片描述

黄色的圆圈表示这首歌正在播放,至于画圈的功能交给了ImageTool类来完成,它里面只有一个静态方法:

+ (UIImage *)circleImageWithName:(NSString *)imageName BorderWidth:(CGFloat)borderWidth BorderColor:(UIColor *)borderColor;

为了在播放控制器中选择了上一曲或者下一曲能够在歌曲展示的控制器中有所体现,笔者这里设计了一个简单的代理,并把它放在Music类中(ps:实际上这种做法不怎么好,因为它从本质上来说只是一个接口协议而已。当一个对象需要遵循这个协议时,只需要导入协议文件就可以了,其他的对它来说都多余了,所以最好将它放入一个单独的头文件。):

@protocol MusicSwithDelegate <NSObject>

@required

- (void)switchMusicWithCurrentIndex:(NSInteger)currentIndex DestinedIndex:(NSInteger)destinedIndex;

@end

在给MusicCell设置数据模型music时,同时将它的delegate设置为展示控制器就可以了。

进入播放界面后,看到的界面是下面的这个样子:

这里写图片描述

上面的两个按钮分别作为退出播放视图和显示歌词的功能键。进度条上的白色按钮显示当前的歌曲已经播放的时间,按住它可以拖动并改变时间进度,点击灰色的进度条也能实现此功能。

播放控制器关联了一个歌词视图控制器,而不是一个歌词视图,因为歌词界面也设计了比较多的逻辑。(ps:这里能知道一点这样做的原因)如果播放控制器视图中没有包含了歌词视图,则在点击歌词按钮时将LyricsViewController作为子控制器加到播放控制器中,然后将它的视图也加到这上面来;如果包含了,那么就做相反的工作。

在歌词控制器中歌词视图的单元格类LyricCell想向外暴露的接口shouldGradient用于控制单元格是否执行渐变效果,当indexPath.row等于_currentIndex时即开始渐变。

_currentIndex是这样计算的:

- (void)setCurrentTime:(NSTimeInterval)currentTime {
    //when play next or previous song, set _currentIndex is equal to 0
    if (_currentTime > currentTime) {
        _currentIndex = 0;
    }

    _currentTime = currentTime;

    for (NSInteger i = _currentIndex; i < self.lyricLines.count; i ++) {
        LyricLines *currentLine = self.lyricLines[i];

        CGFloat currentLineTime = currentLine.time;
        CGFloat nextLineTime = 0;

        if (i + 1 < self.lyricLines.count) {
            LyricLines *nextLine = self.lyricLines[i + 1];
            nextLineTime = nextLine.time;

            if (currentTime >= currentLineTime && currentTime <= nextLineTime && _currentIndex != i) {
                NSArray *reloadLines = @[[NSIndexPath indexPathForRow:_currentIndex inSection:0], [NSIndexPath indexPathForRow:i inSection:0]];

                _currentIndex = i;

                [self.lyricsView reloadRowsAtIndexPaths:reloadLines withRowAnimation:UITableViewRowAnimationFade];
                [self.lyricsView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];

                break;
            }
        }
    }
}

CALayer有一个属性mask,它的类型是CALayer,就像它的名字一样,它是一个图层的掩模层,它的frameanchorPointposition之间满足这样的关系(更多信息可参考这里):

frame.origin.x = position.x - anchorPoint.x * bounds.size.width
frame.origin.y = position.y - anchorPoint.y * bounds.size.height

有一点让人奇怪的是它的frame反而是被其mask的图层的可见区域,而不是被遮掩的区域。

这样就不难理解为什么要像下面这样设置了(position默认值为宽高的一半,anchorPoint默认值为(0.5, 0.5)):

_gradientMaskLayer.anchorPoint = CGPointMake(0, 0.5);

self.gradientLayer.position = CGPointMake(0, self.frontLabel.bounds.size.height / 2);

self.gradientLayer.bounds = CGRectMake(0, 0, 0, self.frontLabel.bounds.size.height);

我们就可用动画的形式来控制这个遮罩层的宽度来变相实现歌词的颜色渐变了,而想要不同的颜色改变前景文字颜色就可以了。(ps: 如果设置的是渐变层的frame,就不需要设置positionposition本质上是anchorPoint的实际坐标)

玩具车用了之后,遥控器上还能接着用

其实我们就只是想实现一个远程控制而已,没有玩具车。:)

PlayingViewController中有这样一段代码:

+ (void)initialize {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    [session setActive:YES error:nil];
}

你没看错!这只是用来设置音乐后台播放的会话策略而已,遥控器还没“买”。

UIResponder有这样一个方法:

- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);

这个方法用于远程事件的处理,关于音乐播放的远程事件主要有以下几种子类型:

UIEventSubtypeRemoteControlPlay              = 100,
UIEventSubtypeRemoteControlPause             = 101,
UIEventSubtypeRemoteControlNextTrack         = 104,
UIEventSubtypeRemoteControlPreviousTrack     = 105
......

当接收到远程事件时,根据事件的子类型选择执行相关方法即可。

控制器遵循并实现了AVAudioPlayerDelegate协议,其中一点应用就是当前歌曲播放完成之后自动播放下一首,另外就是中断处理程序,这里就只完成了简单暂停播放:

#pragma mark - AVFundation Delegate

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    [self nextSong];
}

- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player {
    [self playOrPauseSong];
}

- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player {
    [self playOrPauseSong];
}

你可能注意到,当使用音乐软件时,即使锁屏,也会有相关的信息在锁屏界面展示。这需要MPNowPlayingInfoCenter的支持,如果需要监听远程控制事件,还需要这样做:

[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];

Demo中在开始播放音乐方法中定义了一个私有方法:updateLockedScreenMusic,这个方法里完成了上面的任务。

总结

这篇文章到此笔者能说的大致都写出来了,中间断断续续了一段时间。因为期末临近,加上各种实验课,真是忙的够呛!完整的Demo这里可以找到,有需要的朋友可以看看。

参考资料:

紫忆:ios开发——一个音乐播放器的设计与实现

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值