轻仿QQ音乐之音频歌词播放、锁屏歌词

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

歌词播放界面
音乐播放界面
锁屏歌词界面

一. 项目概述

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

/** 图片 */
@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) id <WPFLyricViewDelegate> 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];

最后再附一下GitHub地址吧,欢迎Star

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

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,386评论 25 707
  • 简介 简单来说,音频可以分为2种 音效 又称“短音频”,通常在程序中的播放时长为1~2秒 在应用程序中起到点缀效果...
    JonesCxy阅读 903评论 1 2
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 人,所谓之性,其私也,其利也,望其社会,所贪所欲之人,皆仕途高升,展其不求名利之人,皆穷困潦倒。谓其此,所坚持之...
    f3149cceebc6阅读 128评论 0 0
  • 自从去年和自己很亲的姑姑去世以后,现年35岁的赵先生便患上了“恐癌症”。 姑姑是得了胃癌走的,从检查出来到离开前后...
    小新爱生活阅读 245评论 0 0