zoukankan      html  css  js  c++  java
  • 轻仿QQ音乐之音频歌词播放、锁屏歌词-b

    先上效果图

    歌词播放界面

    音乐播放界面

    锁屏歌词界面

    一. 项目概述

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

    • 项目播放的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) 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:@"
    "];    // 设置歌词时间正则表达式格式     
        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:@"%@ 
    %@ 
     %@", 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]);
        });
    }
  • 相关阅读:
    BZOJ1119: [POI2009]SLO
    BZOJ1486: [HNOI2009]最小圈
    BZOJ1098: [POI2007]办公楼biu
    BZOJ2242: [SDOI2011]计算器
    PAT A1023
    SpringCloud之整合Feign
    SpringCloud之整合Feign
    小程序在wxml页面格式化类似的2019-02-16T10:54:47.831000时间
    小程序在wxml页面格式化类似的2019-02-16T10:54:47.831000时间
    Javascript基础之-var,let和const深入解析(二)
  • 原文地址:https://www.cnblogs.com/isItOk/p/5866900.html
Copyright © 2011-2022 走看看