欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-08-17 16:04:21
ios开发之网络音乐播放器(sc音乐)(一) 前言 一直都想做一款自己的网络音乐播放器,两个月前做了一个swift版的网络音乐播放器,但是那个播放器数据来源于我自己用vps和nginx搭建的服务器,...

ios开发之网络音乐播放器(sc音乐)(一)

前言

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

 

一、播放控制

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];

 

 

二、获取百度音乐数据

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

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

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

例:method=baidu.ting.billboard.billlist&type=1&size=10&offset=0
完整的请求地址:https://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 //获取偏移

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

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

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

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

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

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

 

二、获取歌曲url

例:method=baidu.ting.song.lry&songid=877578
完整的请求地址:https://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.song.play&songid=877578
参数:songid = 877578 //假设这个是歌曲id

获取到的数据如图所示:

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

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

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

 

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

// 获取歌曲信息请求

// 新歌榜

- (void)loadnewsongs
{
    afhttprequestoperationmanager *manager = [afhttprequestoperationmanager manager];
    nsstring *path = @"https://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 = [@"https://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文件的格式,如图:

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

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

不难发现,除去歌词头部信息后,正文前面[]里面是时间点,右边才是歌词,也就是一段歌词对应一个时间点,这个时间点是开始点,也就是说当歌曲播放到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;
                        
    } 
}

先写到这里,后续还会补充锁屏播放设置,后台播放设置,手势操作等。