iOS开发之网络音乐播放器(SC音乐)

iOS开发之网络音乐播放器(SC音乐)

前言

一直都想做一款自己的网络音乐播放器,两个月前做了一个swift版的网络音乐播放器,但是那个播放器数据来源于我自己用VPS和nginx搭建的服务器,所有的文件都要自己准备,包括mp3、歌词、专辑图片等,非常麻烦,有兴趣的可以跟我要源码。现在这款音乐播放器数据是来源于百度音乐,前前后后花了一个多星期搞定,网上有一些音乐网站的API,有兴趣的同学可以去查一下。我这里贴一下我自己用到的百度音乐API:http://blog.csdn.net/zuiaisha1/article/details/61200422

正题

一、播放控制

SC音乐用的是AVPlayer,这个库是苹果自带的视频库,也可以播放音频,可以支持边播放边缓存,使用也比较简单。详细看苹果官网介绍:https://developer.apple.com/documentation/avfoundation/avplayer。这里介绍一下要用到的东西。我们知道,播放器要有播放、暂停、上一曲、下一曲的功能,还要知道播放总时间,当前时间,播放状态,能够从歌曲的任意时间点开始播放。在AVPlayer库中:

play ---- 播放

pause ---- 暂停

rate ---- 播放状态(0.0代表当前状态是暂停, 1.0代表当前状态是播放)

seekToTime ---- 从某个时间点开始播放(拖动进度条用到)

duration ---- 歌曲总时间

currentTime ---- 当前播放时间

上一曲和下一曲可以通过改变歌曲url来实现。

初始化一个AVPlayer需要一个playItem,所以先初始化一个playItem,再用这个playItem去实例化一个play,具体代码:

MusicPlayerManager.h

//

//  MusicPlayerManager.h

//  BaiduMusic

//

//  Created by 凌       陈 on 8/21/17.

//  Copyright © 2017 凌       陈. All rights reserved.

//

#import

#import

@interface MusicPlayerManager : NSObject

typedef enum : NSUInteger {

RepeatPlayMode,

RepeatOnlyOnePlayMode,

ShufflePlayMode,

} ShuffleAndRepeatState;

@property (nonatomic,strong) AVPlayer *play;

@property (nonatomic,strong) AVPlayerItem *playItem;

@property (nonatomic,assign) ShuffleAndRepeatState shuffleAndRepeatState;

@property (nonatomic,assign) NSInteger playingIndex;

+ (MusicPlayerManager *)sharedManager;

-(void) setPlayItem: (NSString *)songURL;

-(void) setPlay;

-(void) startPlay;

-(void) stopPlay;

-(void) play: (NSString *)songURL;

@end

MusicPlayerManager.m

//

//  MusicPlayerManager.m

//  BaiduMusic

//

//  Created by 凌       陈 on 8/21/17.

//  Copyright © 2017 凌       陈. All rights reserved.

//

#import "MusicPlayerManager.h"

@implementation MusicPlayerManager

static MusicPlayerManager *_sharedManager = nil;

+(MusicPlayerManager *)sharedManager {

@synchronized( [MusicPlayerManager class] ){

if(!_sharedManager)

_sharedManager = [[self alloc] init];

return _sharedManager;

}

return nil;

}

-(void) setPlayItem: (NSString *)songURL {

NSURL * url  = [NSURL URLWithString:songURL];

_playItem = [[AVPlayerItem alloc] initWithURL:url];

}

-(void) setPlay {

_play = [[AVPlayer alloc] initWithPlayerItem:_playItem];

}

-(void) startPlay {

[_play play];

}

-(void) stopPlay {

[_play pause];

}

-(void) play: (NSString *)songURL {

[self setPlayItem:songURL];

[self setPlay];

[self startPlay];

}

@end

将一首歌的url传进play方法就可以实现播放音乐了。上一曲下一曲只是改变一下歌曲的url就可以实现。

歌曲总时长:

_play.currentItem.duration

当前播放时间:

_play.currentTime

从某个时间点开始播放:

//播放器定位到对应的位置

CMTime targetTime = CMTimeMake((int64_t)(currentTime), 1);

[musicPlayer.play seekToTime:targetTime];

播放状态:

//播放或者暂停按键按下,要判断播放状态

if (_play.rate == 0) {

// 当前状态为暂停

// 下面要执行播放的代码

} else {

// 当前状态为播放

// 下面要执行暂停的代码

}

监管播放(更新播放进度条和当前时间):

_playerTimeObserver = [musicPlayer.play addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {

// 这里一秒进来一次,可以更新时间、播放进度条和歌词

// 需要注意的是_playerTimeObserver必须要每首歌播放结束后清掉,不清会有问题。

}

播放结束通知:

// 歌曲播放结束后会调用自定义的方法finishedPlaying

[[NSNotificationCenter defaultCenter]  addObserver:self selector:@selector(finishedPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:_play.currentItem];

二、获取百度音乐数据

百度音乐的全接口:http://tingapi.ting.baidu.com/v1/restserver/ting。所有的数据都是以这个为开头,后面加一些其他东西。

可以请求到的数据有很多,这里只说几个:

一、获取歌曲列表(新歌榜、热歌榜、经典老歌榜等)

例:method=baidu.ting.billboard.billList&type=1&size=10&offset=0

完整的请求地址:http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.billboard.billList&format=json&type=1&offset=0&size=100 (前100热门歌曲,要获取哪个榜只需要改变一下type的值就行了)

参数: type = 1-新歌榜,2-热歌榜,11-摇滚榜,12-爵士,16-流行,21-欧美金曲榜,22-经典老歌榜,23-情歌对唱榜,24-影视金曲榜,25-网络歌曲榜

size = 10 //返回条目数量

offset = 0 //获取偏移

获取到的数据如截图所示:

我们只需要“song_list”里面的数据,点开后发现“song_list“就是一个字典:

继续点开[0],里面是一首歌的信息,包括歌名、歌手名、专辑名等等,但是并没有歌曲url,别急这个要另外获取,需要用到这里的"song_id".

二、获取歌曲url

例:method=baidu.ting.song.lry&songid=877578

完整的请求地址:http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.song.play&songid=877578

参数:songid = 877578 //假设这个是歌曲id

获取到的数据如图所示:

我们只关注”bitrate“中的数据,点开发现里面有”file_link“,这个就是歌曲的url:

网络请求我用大名鼎鼎的AFNetworking,获取到的数据解析我用MJExtension,主要将数据转成NSArray,将这两个库拉入自己的工程,添加头文件#import "AFNetworking.h"   #import "MJExtension.h"即可。

// 获取歌曲信息请求

// 新歌榜

- (void)loadNewSongs

{

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

NSString *path = @"http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.billboard.billList&format=json&type=1&offset=0&size=100";//前100热门歌曲

[manager GET:path parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {

if ([responseObject isKindOfClass:[NSDictionary class]])

{

NSArray *array = [responseObject objectForKey:@"song_list"];

songInfo.OMSongs = [OMHotSongInfo mj_objectArrayWithKeyValuesArray:array];

//            [self reloadTableView:_radioAndMusicTableView];

[_mytableView reloadData];

}

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

NSLog(@"error--%@",error);

}];

}

其它的榜单改一下type的值就可以了。

获取歌曲url请求:

-(void)getSelectedSong: (NSString *)songID index: (long)index {

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

NSString *path = [@"http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.song.play&songid="  stringByAppendingString:songID];

[manager GET:path parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {

if ([responseObject isKindOfClass:[NSDictionary class]])

{

NSDictionary *array = [responseObject objectForKey:@"bitrate"];

self.file_link = [array objectForKey:@"file_link"];

self.file_size = [array objectForKey:@"file_size"];

self.file_duration = [array objectForKey:@"file_duration"];

self.playSongIndex = index;

}

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {

NSLog(@"error--%@",error);

}];

}

三、播放界面

1、圆形专辑图片(原先是矩形的,需要处理一下)

.h文件

@property (nonatomic, strong) UIView *playControllerView;

@property (nonatomic, strong) UIImageView *currentPlaySongImage;

.m文件

// 专辑图片

// 先将专辑图片放到正方形UIImageView, 再将UIImageView圆角设置为正方形边长的一半就得到圆形的UIImageView了

_currentPlaySongImage = [[SCImageView alloc] initWithFrame:CGRectMake(10, 10 , _playControllerView.frame.size.height - 20 , _playControllerView.frame.size.height - 20)];

_currentPlaySongImage.image = [UIImage imageNamed:@"album_default"];

_currentPlaySongImage.clipsToBounds = true;

_currentPlaySongImage.layer.cornerRadius = (_playControllerView.frame.size.height - 20) * 0.5;

[_playControllerView addSubview:_currentPlaySongImage];

2、专辑图片旋转

我封装了一个UIImageView的旋转动画类,代码如下:

SCImageView.h

//

//  SCImageView.h

//  BaiduMusic

//

//  Created by 凌       陈 on 8/22/17.

//  Copyright © 2017 凌       陈. All rights reserved.

//

#import

@interface SCImageView : UIImageView

-(void) startRotating;

-(void) stopRotating;

-(void) resumeRotate;

@end

SCImageView.c

//

//  SCImageView.m

//  BaiduMusic

//

//  Created by 凌       陈 on 8/22/17.

//  Copyright © 2017 凌       陈. All rights reserved.

//

#import "SCImageView.h"

@implementation SCImageView

// 开始旋转

-(void) startRotating {

    CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];

    rotateAnimation.fromValue = [NSNumber numberWithFloat:0.0];

    rotateAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2];  // 旋转一周

    rotateAnimation.duration = 20.0;                                // 旋转时间20秒

    rotateAnimation.repeatCount = MAXFLOAT;                          // 重复次数,这里用最大次数

    [self.layer addAnimation:rotateAnimation forKey:nil];

}

// 停止旋转

-(void) stopRotating {

    CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];

    self.layer.speed = 0.0;                                          // 停止旋转

    self.layer.timeOffset = pausedTime;                              // 保存时间,恢复旋转需要用到

}

// 恢复旋转

-(void) resumeRotate {

    If (self.layer.timeOffset == 0) {

        [self startRotating];

        return;

    }

    CFTimeInterval pausedTime = self.layer.timeOffset;

    self.layer.speed = 1.0;                                        // 开始旋转

    self.layer.timeOffset = 0.0;

    self.layer.beginTime = 0.0;

    CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] -    pausedTime;                                            // 恢复时间

    self.layer.beginTime = timeSincePause;                          // 从暂停的时间点开始旋转

}

@end

歌曲刚开始播放调用startRotating,开始旋转,点击暂停按键时调用stopRotating停止旋转,点击播放按键时调用resumeRotate恢复旋转(如果调用startRotating则又从头开始旋转)。

3、歌词解析和歌词滚动

首先我们先来看一下lrc文件的格式,如图:

不难发现,除去歌词头部信息后,正文前面[]里面是时间点,右边才是歌词,也就是一段歌词对应一个时间点,这个时间点是开始点,也就是说当歌曲播放到00:49.65这个时间点的时候,歌词应该滚动到“因为在 一千年以后”这段。知道原理就好办了。先解析歌词。

// 解析歌词

.h文件

@property (nonatomic,strong) NSMutableDictionary *mLRCDictinary;

@property (nonatomic,strong) NSMutableArray *mTimeArray;

@property (nonatomic, assign) BOOL mIsLRCPrepared;

.m文件

-(void) AnalysisLRC: (NSString *)lrcStr {

NSString* contentStr = lrcStr;

NSArray *lrcArray = [contentStr componentsSeparatedByString:@"\n"];

[mLRCDictinary removeAllObjects];

[mTimeArray removeAllObjects];

for (NSString *line in lrcArray) {

// 首先处理歌词中无用的东西

// [ti:][ar:][al:]这类的直接跳过

if ([line containsString:@"[0"] || [line containsString:@"[1"] || [line containsString:@"[2"] || [line containsString:@"[3"]) {

NSArray *lineArr = [line componentsSeparatedByString:@"]"];

NSString *str1 = [line substringWithRange:NSMakeRange(3, 1)];

NSString *str2 = [line substringWithRange:NSMakeRange(6, 1)];

if ([str1 isEqualToString:@":"] && [str2 isEqualToString:@"."]) {

NSString *lrcStr = lineArr[1];

NSString *timeStr = [lineArr[0] substringWithRange:NSMakeRange(1, 5)];

[songInfo.mLRCDictinary setObject:lrcStr forKey:timeStr];

[songInfo.mTimeArray addObject:timeStr];

}

} else {

continue;

}

}

_mIsLRCPrepared = true;

[self.tableView reloadData];

}

mLRCDictinary存放配对时间点和歌词段,mTimeArray存放时间点,通过时间点来找到相应的歌词段,不过这个时间点是NSString格式,需要转成int(我的精度要求不高,只到秒,后面的小数没要)

NSString转int

-(int) stringToInt: (NSString *)timeString {

NSArray *strTemp = [timeString componentsSeparatedByString:@":"];

int time = [strTemp.firstObject intValue] * 60 + [strTemp.lastObject intValue];

return time;

}

int转NSString(显示当前播放时间要用到)

-(NSString *)intToString: (int)needTransformInteger {

//实现00:00这种格式播放时间

int wholeTime = needTransformInteger;

int min  = wholeTime / 60;

int sec = wholeTime % 60;

NSString *str = [NSString stringWithFormat:@"%02d:%02d", min , sec];

return str;

}

歌词滚动:

// songInfo.lrcIndex记录歌词第几行,用currentTime 和 mTimeArray中第几行歌词的时间相比较,大于那个时间歌词tableView滚动到那一行。

if (songInfo.lrcIndex <= songInfo.mLRCDictinary.count - 1) {

if ((int)currentTime >= [songInfo stringToInt:songInfo.mTimeArray[songInfo.lrcIndex]]) {

_deliverView.midView.midLrcView.currentRow = songInfo.lrcIndex;

//

[_deliverView.midView.midLrcView.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:_deliverView.midView.midLrcView.currentRow inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];

[_deliverView.midView.midLrcView.tableView reloadData];

songInfo.lrcIndex = songInfo.lrcIndex + 1;

}

}

先写到这里,后续还会补充锁屏播放设置,后台播放设置,手势操作等。如果各位觉得还可以,别忘了加个星星哦!

CSDN地址:http://blog.csdn.net/u014636932/article/details/77622358

这里附上github地址:https://github.com/Mozartisnotmyname/SCMusic.git

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

推荐阅读更多精彩内容