博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
轻仿QQ音乐之音频歌词播放、锁屏歌词
阅读量:7034 次
发布时间:2019-06-28

本文共 13058 字,大约阅读时间需要 43 分钟。

2016.09.04

最近闲下来自己写了个小demo,轻仿QQ音乐播放界面,本文主要讲一下音频的基本播放、歌词的滚动对应、锁屏歌词的实现(会持续更新音频相关的知识点),老规矩,先上效果图

一. 项目概述

前面内容实在是太基础。。只想看知识点的同学可以直接跳到第三部分的干货

  • 项目播放的mp3文件及lrc文件均来自QQ音乐
  • 本文主要主要讲解锁屏歌词的实现,音频、歌词的播放网上资源略多,因此不做重点讲解,项目也是采取最简单的MVC+storyboard方式
  • 项目GitHub地址:
  • 音乐模型-->WPFMusic
/** 图片 */@property (nonatomic,copy) NSString *image;/** 歌词 */@property (nonatomic,copy) NSString *lrc;/** 歌曲 */@property (nonatomic,copy) NSString *mp3;/** 歌曲名 */@property (nonatomic,copy) NSString *name;/** 歌手 */@property (nonatomic,copy) NSString *singer;/** 专辑 */@property (nonatomic,copy) NSString *album;/** 类型 */@property (nonatomic,assign) WPFMusicType type;复制代码

对应plist存储文件

  • 歌词模型-->WPFLyric
/** 歌词开始时间 */@property (nonatomic,assign) NSTimeInterval time;/** 歌词内容 */@property (nonatomic,copy) NSString *content;复制代码
  • 歌词展示界面-->WPFLyricView
@property (nonatomic,weak) id 
delegate;/** 歌词模型数组 */@property (nonatomic,strong) NSArray *lyrics;/** 每行歌词行高 */@property (nonatomic,assign) NSInteger rowHeight;/** 当前正在播放的歌词索引 */@property (nonatomic,assign) NSInteger currentLyricIndex;/** 歌曲播放进度 */@property (nonatomic,assign) CGFloat lyricProgress;/** 竖直滚动的view,即歌词View */@property (nonatomic,weak) UIScrollView *vScrollerView;#warning 以下为私有属性/* 水平滚动的大view,包含音乐播放界面及歌词界面 */@property (nonatomic,weak) UIScrollView *hScrollerView;/** 定位播放的View */@property (nonatomic,weak) WPFSliderView *sliderView;复制代码
  • 当前正在播放的歌词label-->WPFColorLabel
/** 歌词播放进度 */@property (nonatomic,assign) CGFloat progress;/** 歌词颜色 */@property (nonatomic,strong) UIColor *currentColor;复制代码
  • 播放管理对象-->WPFPlayManager
/** 单例分享 */+ (instancetype)sharedPlayManager;/** *  播放音乐的方法 * *  @param fileName 音乐文件的名称 *  @param complete 播放完毕后block回调 */- (void)playMusicWithFileName:(NSString *)fileName didComplete:(void(^)())complete;/** 音乐暂停 */- (void)pause;复制代码
  • 歌词解析器-->WPFLyricParser (主要就是根据 .lrc 文件解析歌词的方法)
+ (NSArray *)parserLyricWithFileName:(NSString *)fileName {        // 根据文件名称获取文件地址    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];        // 根据文件地址获取转化后的总体的字符串    NSString *lyricStr = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL];        // 将歌词总体字符串按行拆分开,每句都作为一个数组元素存放到数组中    NSArray *lineStrs = [lyricStr componentsSeparatedByString:@"\n"];        // 设置歌词时间正则表达式格式    NSString *pattern = @"\\[[0-9]{2}:[0-9]{2}.[0-9]{2}\\]";    NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];        // 创建可变数组存放歌词模型    NSMutableArray *lyrics = [NSMutableArray array];        // 遍历歌词字符串数组    for (NSString *lineStr in lineStrs) {                NSArray *results = [reg matchesInString:lineStr options:0 range:NSMakeRange(0, lineStr.length)];                // 歌词内容        NSTextCheckingResult *lastResult = [results lastObject];        NSString *content = [lineStr substringFromIndex:lastResult.range.location + lastResult.range.length];                // 每一个结果的range        for (NSTextCheckingResult *result in results) {                        NSString *time = [lineStr substringWithRange:result.range];            #warning 对于类似 NSDateFormatter 的重大开小对象,最好使用单例管理            NSDateFormatter *formatter = [NSDateFormatter sharedDateFormatter];            formatter.dateFormat = @"[mm:ss.SS]";            NSDate *timeDate = [formatter dateFromString:time];            NSDate *initDate = [formatter dateFromString:@"[00:00.00]"];                        // 创建模型            WPFLyric *lyric = [[WPFLyric alloc] init];            lyric.content = content;            // 歌词的开始时间            lyric.time = [timeDate timeIntervalSinceDate:initDate];                        // 将歌词对象添加到模型数组汇总            [lyrics addObject:lyric];        }    }        // 按照时间正序排序    NSSortDescriptor *sortDes = [NSSortDescriptor sortDescriptorWithKey:@"time" ascending:YES];    [lyrics sortUsingDescriptors:@[sortDes]];       return lyrics;}复制代码

二. 主要知识点讲解

  • 音频播放AppDelegate中操作
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    // 注册后台播放    AVAudioSession *session = [AVAudioSession sharedInstance];    [session setCategory:AVAudioSessionCategoryPlayback error:NULL];        // 开启远程事件  -->自动切歌    [application beginReceivingRemoteControlEvents];        return YES;}复制代码
  • 音频播放加载文件播放方式
NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:NULL];复制代码
  • 在 ViewController 中点击事件
#warning 播放/暂停按钮点击事件- (IBAction)play {    WPFPlayManager *playManager = [WPFPlayManager sharedPlayManager];    if (self.playBtn.selected == NO) {        [self startUpdateProgress];        WPFMusic *music = self.musics[self.currentMusicIndex];        [playManager playMusicWithFileName:music.mp3 didComplete:^{            [self next];        }];        self.playBtn.selected = YES;    }else{        self.playBtn.selected = NO;        [playManager pause];        [self stopUpdateProgress];    }}#warning 下一曲按钮点击事件- (IBAction)next {    // 循环播放    if (self.currentMusicIndex == self.musics.count -1) {        self.currentMusicIndex = 0;    }else{        self.currentMusicIndex ++;    }    [self changeMusic];}#warning changeMusic 方法// 重置音乐对象,各种基础赋值- (void)changeMusic {    // 防止切歌时歌词数组越界        self.currentLyricIndex = 0;    // 切歌时销毁当前的定时器    [self stopUpdateProgress];        WPFPlayManager *pm = [WPFPlayManager sharedPlayManager];        WPFMusic *music = self.musics[self.currentMusicIndex];    // 歌词    // 解析歌词    self.lyrics = [WPFLyricParser parserLyricWithFileName:music.lrc];        // 给竖直歌词赋值    self.lyricView.lyrics = self.lyrics;    // 专辑    self.albumLabel.text = music.album;    // 歌手    self.singerLabel.text = [NSString stringWithFormat:@"—  %@  —", music.singer];    // 图片    UIImage *image = [UIImage imageNamed:music.image];    self.vCenterImageView.image = image;    self.bgImageView.image = image;    self.hCennterImageView.image = image;    self.playBtn.selected = NO;    self.navigationItem.title = music.name;    [self play];    self.durationLabel.text = [WPFTimeTool stringWithTime:pm.duration];}复制代码

三. 锁屏歌词详细讲解

  • 更新锁屏界面的方法最好在一句歌词唱完之后的方法中调用(还是结合代码添加注释吧,干讲... 臣妾做不到啊)
- (void)updateLockScreen {#warning 锁屏界面的一切信息都要通过这个原生的类来创建:MPNowPlayingInfoCenter    // 获取音乐播放信息中心    MPNowPlayingInfoCenter *nowPlayingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];    // 创建可变字典存放信息    NSMutableDictionary *info = [NSMutableDictionary dictionary];    // 获取当前正在播放的音乐对象    WPFMusic *music = self.musics[self.currentMusicIndex];        WPFPlayManager *playManager = [WPFPlayManager sharedPlayManager];    // 专辑名称    info[MPMediaItemPropertyAlbumTitle] = music.album;    // 歌手    info[MPMediaItemPropertyArtist] = music.singer;    // 专辑图片    info[MPMediaItemPropertyArtwork] = [[MPMediaItemArtwork alloc] initWithImage:[self lyricImage]];    // 当前播放进度    info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(playManager.currentTime);    // 音乐总时间    info[MPMediaItemPropertyPlaybackDuration] = @(playManager.duration);    // 音乐名称    info[MPMediaItemPropertyTitle] = music.name;        nowPlayingInfoCenter.nowPlayingInfo = info;}复制代码
  • 更新锁屏歌词的原理就是获取专辑图片后,将前后三句歌词渲染到图片上,使用富媒体将当前正在播放的歌词和前后的歌词区分开大小和颜色
- (UIImage *)lyricImage {    WPFMusic *music = self.musics[self.currentMusicIndex];    WPFLyric *lyric = self.lyrics[self.currentLyricIndex];    WPFLyric *lastLyric = [[WPFLyric alloc] init];    WPFLyric *nextLyric = [[WPFLyric alloc] init];        if (self.currentLyricIndex > 0) {        lastLyric = self.lyrics[self.currentLyricIndex - 1];        if (!lastLyric.content.length && self.currentLyricIndex > 1) {            lastLyric = self.lyrics[self.currentLyricIndex - 2];        }    }        if (self.lyrics.count > self.currentLyricIndex + 1) {        nextLyric = self.lyrics[self.currentLyricIndex + 1];                // 筛选空的时间间隔歌词        if (!nextLyric.content.length && self.lyrics.count > self.currentLyricIndex + 2) {            nextLyric = self.lyrics[self.currentLyricIndex + 2];        }    }        UIImage *bgImage = [UIImage imageNamed:music.image];        // 创建ImageView    UIImageView *imgView = [[UIImageView alloc] initWithImage:bgImage];    imgView.bounds = CGRectMake(0, 0, 640, 640);    imgView.contentMode = UIViewContentModeScaleAspectFill;        // 添加遮罩    UIView *cover = [[UIView alloc] initWithFrame:imgView.bounds];    cover.backgroundColor = [UIColor colorWithWhite:0 alpha:0.3];    [imgView addSubview:cover];        // 添加歌词    UILabel *lyricLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 480, 620, 150)];    lyricLabel.textAlignment = NSTextAlignmentCenter;    lyricLabel.numberOfLines = 3;    NSString *lyricString = [NSString stringWithFormat:@"%@ \n%@ \n %@", lastLyric.content, lyric.content, nextLyric.content];    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:lyricString attributes:@{                            NSFontAttributeName : [UIFont systemFontOfSize:29],                            NSForegroundColorAttributeName : [UIColor lightGrayColor]                                                }];        [attributedString addAttributes:@{                NSFontAttributeName : [UIFont systemFontOfSize:34],                NSForegroundColorAttributeName : [UIColor whiteColor]                                    } range:[lyricString rangeOfString:lyric.content]];    lyricLabel.attributedText = attributedString;    [imgView addSubview:lyricLabel];        // 开始画图    UIGraphicsBeginImageContext(imgView.frame.size);    CGContextRef context = UIGraphicsGetCurrentContext();    [imgView.layer renderInContext:context];        // 获取图片    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();        // 结束上下文    UIGraphicsEndImageContext();        return img;}复制代码
  • 当然不是所有的时候都要去更新锁屏多媒体信息的,可以采用下面的方法进行监听优化:只在锁屏而且屏幕亮着的时候才会去设置,啥都不说了,都在代码里了
#warning 声明的全局变量及通知名称static uint64_t isScreenBright;static uint64_t isLocked;#define kSetLockScreenLrcNoti @"kSetLockScreenLrcNoti"#warning 在 viewDidLoad 方法中监听    // 监听锁屏状态    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, updateEnabled, CFSTR("com.apple.iokit.hid.displayStatus"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);   CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, lockState, CFSTR("com.apple.springboard.lockstate"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);    });    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLockScreen) name:kSetLockScreenLrcNoti object:nil];复制代码
  • 上面两个监听对应的方法:
// 监听在锁定状态下,屏幕是黑暗状态还是明亮状态static void updateEnabled(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo) {        //    uint64_t state;        int token;        notify_register_check("com.apple.iokit.hid.displayStatus", &token);        notify_get_state(token, &isScreenBright);        notify_cancel(token);        [ViewController checkoutIfSetLrc];        //    NSLog(@"锁屏状态:%llu",isScreenBright);}// 监听屏幕是否被锁定static void lockState(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo) {        uint64_t state;        int token;        notify_register_check("com.apple.springboard.lockstate", &token);        notify_get_state(token, &state);        notify_cancel(token);    isLocked = state;    [ViewController checkoutIfSetLrc];    //    NSLog(@"lockState状态:%llu",state);}#warning 这个方法不太好,有好想法的可在评论区讨论+ (void)checkoutIfSetLrc {    // 如果当前屏幕被锁定 && 屏幕处于 active 状态,就发送通知调用对象方法    if (isLocked && isScreenBright) {        [[NSNotificationCenter defaultCenter] postNotificationName:kSetLockScreenLrcNoti object:nil];    }}复制代码

四. 后续干货补充(不定时更新)

  • 当前音频被其他app音频、照相机、闹钟、电话等打断,打断结束后立刻恢复播放
#warning AppDelegate中// 指明应用启动原因的dictionary。如果用户直接启动应用的话,dictionary为nil。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {      // 监听音频被打断的通知      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(interruptionNotificationCallback:) name:AVAudioSessionInterruptionNotification object:nil];}// 音频被打断后响应的方法- (void)interruptionNotificationCallback:(NSNotification *)noti {    UInt32 optionKey = [noti.userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntValue];    AudioSessionInterruptionType interruptionType = [noti.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntValue];        NSLog(@"optionKey-->%d", optionKey);    NSLog(@"interruptionType-->%d", interruptionType);       if (optionKey == 1 && interruptionType == 0) {        // 由于音频被打断的状态改变后才会发送通知,因此需要在项目中记录用户最后一次手动操作的状态(暂停/播放),在此处获取,如果记录的用户最后一次操作是播放,那么就继续播放        if ([[NSUserDefaults standardUserDefaults] boolForKey:@"kUserControlPlayState"]) {            #warning 在这里调用项目中继续播放音频的方法哦        }    }}复制代码
  • 禁止锁屏

默认情况下,当设备一段时间没有触控动作时,iOS会锁住屏幕。但有一些情况是不需要锁屏的,比如视频播放器,或者播放歌词界面的音乐播放器

[UIApplication sharedApplication].idleTimerDisabled = YES;or[[UIApplication sharedApplication] setIdleTimerDisabled:YES];复制代码

千万别打赏!!点个赞就好?

转载地址:http://eeyal.baihongyu.com/

你可能感兴趣的文章
[leetcode 240]Search a 2D Matrix II
查看>>
域名指的是这一级目录
查看>>
[Angular] Creating an Observable Store with Rx
查看>>
[转]Porting to Oracle with Entity Framework NLog
查看>>
chmod更改文件的权限
查看>>
oracle 10g/11g RAC 启停归档模式
查看>>
poj3461 Oulipo
查看>>
OAuth2.0学习(1-12)开源的OAuth2.0项目和比较
查看>>
Gitlab,这也就O了???
查看>>
2014 百度之星 1003 题解 Xor Sum
查看>>
Linux中在主机上实现对备机上文件夹及文件的操作的C代码实现
查看>>
iOS 块的简单理解
查看>>
idea中如何配置git以及在idea中初始化git
查看>>
关于JQuery Class选择器的一点
查看>>
POJ3264 Balanced Lineup
查看>>
redis-cli 连接远程服务器
查看>>
emlog通过pjax实现无刷新加载网页--完美解决cnzz统计和javascript失效问题
查看>>
sublime 之 vitage/emmet
查看>>
代码管理(四)SVN和Git对比
查看>>
python - hadoop,mapreduce demo
查看>>