先上效果图
歌词播放界面
音乐播放界面
锁屏歌词界面
一. 项目概述
前面内容实在是太基础。。只想看知识点的同学可以直接跳到第三部分的干货
-
项目播放的mp3文件及lrc文件均来自QQ音乐
-
本文主要主要讲解锁屏歌词的实现,音频、歌词的播放网上资源略多,因此不做重点讲解,项目也是采取最简单的MVC+storyboard方式
-
项目GitHub地址: https://github.com/PengfeiWang666/WPFMusicPlayer
-
音乐模型-->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存储文件
音乐模型所对应的 plist 存储文件
-
歌词模型-->WPFLyric
/** 歌词开始时间 */@property (nonatomic,assign) NSTimeInterval time;/** 歌词内容 */@property (nonatomic,copy) NSString *content;
-
歌词展示界面-->WPFLyricView
@property (nonatomic,weak) iddelegate;/** 歌词模型数组 */@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]; } }
最后再附一下GitHub地址:https://github.com/PengfeiWang666/WPFMusicPlayer
雾化:
- (UIImage *)blur:(CGFloat)radius{ CIContext *context = [CIContext contextWithOptions:nil]; CIImage *inputImage = [[CIImage alloc] initWithImage:self]; // create blur filter CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; [filter setValue:inputImage forKey:kCIInputImageKey]; [filter setValue:[NSNumber numberWithFloat:radius] forKey:@"inputRadius"]; // blur image CIImage *result = [filter valueForKey:kCIOutputImageKey]; float insert = 60; CGRect extent = CGRectInset(filter.outputImage.extent, insert, insert); CGImageRef cgImage = [context createCGImage:result fromRect:extent]; UIImage *image = [UIImage imageWithCGImage:cgImage]; CGImageRelease(cgImage); return image;}- (UIImage *)blur{ return [self blur:9.0f];}-(void)asyncApplyBlur:(UIImageAsyncBlurBlock)block{ __weak typeof(self) wself = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ UIImage *blueImage = [wself blur ]; dispatch_async(dispatch_get_main_queue(), ^{ block(blueImage); });// block([wself blur]); });}