ios开发:一个音乐播放器的设计与实现
github地址:https://github.com/wzpziyi1/MusicPlauer
这个Demo,关于歌曲播放的主要功能都实现了的。下一曲、上一曲,暂停,根据歌曲的播放进度动态滚动歌词,将当前正在播放的歌词放大显示,拖动进度条,歌曲跟着变化,并且使用Time Profiler进行了优化,还使用XCTest对几个主要的类进行了单元测试。
已经经过真机调试,在真机上可以后台播放音乐,并且锁屏时,显示一些主要的歌曲信息。
首页:
歌曲内部播放:
当拖动小的进度条的时候,歌曲也会随之变化。
显示歌词界面:
这是根据歌曲的播放来显示对应歌词的。用UITableView来显示歌词,可以手动滚动界面查看后面或者前面的歌词。
并且,当拖动进度条,歌词也会随之变化,下一曲、上一曲依然是可以使用的。
代码分析:
准备阶段,先是写了一个音频播放的单例,用这个单例来播放这个demo中的音乐文件,代码如下:
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@interface ZYAudioManager : NSObject
+ (instancetype)defaultManager; //播放音乐
- (AVAudioPlayer *)playingMusic:(NSString *)filename;
- (void)pauseMusic:(NSString *)filename;
- (void)stopMusic:(NSString *)filename; //播放音效
- (void)playSound:(NSString *)filename;
- (void)disposeSound:(NSString *)filename;
@end #import "ZYAudioManager.h" @interface ZYAudioManager ()
@property (nonatomic, strong) NSMutableDictionary *musicPlayers;
@property (nonatomic, strong) NSMutableDictionary *soundIDs;
@end static ZYAudioManager *_instance = nil; @implementation ZYAudioManager + (void)initialize
{
// 音频会话
AVAudioSession *session = [AVAudioSession sharedInstance]; // 设置会话类型(播放类型、播放模式,会自动停止其他音乐的播放)
[session setCategory:AVAudioSessionCategoryPlayback error:nil]; // 激活会话
[session setActive:YES error:nil];
} + (instancetype)defaultManager
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
} - (instancetype)init
{
__block ZYAudioManager *temp = self; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if ((temp = [super init]) != nil) {
_musicPlayers = [NSMutableDictionary dictionary];
_soundIDs = [NSMutableDictionary dictionary];
}
});
self = temp;
return self;
} + (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
} //播放音乐
- (AVAudioPlayer *)playingMusic:(NSString *)filename
{
if (filename == nil || filename.length == 0) return nil; AVAudioPlayer *player = self.musicPlayers[filename]; //先查询对象是否缓存了 if (!player) {
NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:nil]; if (!url) return nil; player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:nil]; if (![player prepareToPlay]) return nil; self.musicPlayers[filename] = player; //对象是最新创建的,那么对它进行一次缓存
} if (![player isPlaying]) { //如果没有正在播放,那么开始播放,如果正在播放,那么不需要改变什么
[player play];
}
return player;
} - (void)pauseMusic:(NSString *)filename
{
if (filename == nil || filename.length == 0) return; AVAudioPlayer *player = self.musicPlayers[filename]; if ([player isPlaying]) {
[player pause];
}
}
- (void)stopMusic:(NSString *)filename
{
if (filename == nil || filename.length == 0) return; AVAudioPlayer *player = self.musicPlayers[filename]; [player stop]; [self.musicPlayers removeObjectForKey:filename];
} //播放音效
- (void)playSound:(NSString *)filename
{
if (!filename) return; //取出对应的音效ID
SystemSoundID soundID = (int)[self.soundIDs[filename] unsignedLongValue]; if (!soundID) {
NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
if (!url) return; AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID); self.soundIDs[filename] = @(soundID);
} // 播放
AudioServicesPlaySystemSound(soundID);
} //摧毁音效
- (void)disposeSound:(NSString *)filename
{
if (!filename) return; SystemSoundID soundID = (int)[self.soundIDs[filename] unsignedLongValue]; if (soundID) {
AudioServicesDisposeSystemSoundID(soundID); [self.soundIDs removeObjectForKey:filename]; //音效被摧毁,那么对应的对象应该从缓存中移除
}
}
@end
就是一个单例的设计,并没有多大难度。我是用了一个字典来装播放过的歌曲了,这样如果是暂停了,然后再开始播放,就直接在缓存中加载即可。但是如果不注意,在 stopMusic:(NSString *)fileName 这个方法里面,不从字典中移除掉已经停止播放的歌曲,那么你下再播放这首歌的时候,就会在原先播放的进度上继续播放。在编码过程中,我就遇到了这个Bug,然后发现,在切换歌曲(上一曲、下一曲)的时候,我调用的是stopMusic方法,但由于我没有从字典中将它移除,而导致它总是从上一次的进度开始播放,而不是从头开始播放。
如果在真机上想要后台播放歌曲,除了在appDelegate以及plist里面做相应操作之外,还得将播放模式设置为:AVAudioSessionCategoryPlayback。特别需要注意这里,我在模拟器上调试的时候,没有设置这种模式也是可以进行后台播放的,但是在真机上却不行了。后来在StackOverFlow上找到了对应的答案,需要设置播放模式。
这个单例类,在整个demo中是至关重要的,要保证它是没有错误的,所以我写了这个类的XCTest进行单元测试,代码如下:
#import <XCTest/XCTest.h>
#import "ZYAudioManager.h"
#import <AVFoundation/AVFoundation.h> @interface ZYAudioManagerTests : XCTestCase
@property (nonatomic, strong) AVAudioPlayer *player;
@end
static NSString *_fileName = @"10405520.mp3";
@implementation ZYAudioManagerTests - (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
} - (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
} - (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
} /**
* 测试是否为单例,要在并发条件下测试
*/
- (void)testAudioManagerSingle
{
NSMutableArray *managers = [NSMutableArray array]; dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
[managers addObject:tempManager];
}); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
[managers addObject:tempManager];
}); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
[managers addObject:tempManager];
}); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
[managers addObject:tempManager];
}); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ZYAudioManager *tempManager = [[ZYAudioManager alloc] init];
[managers addObject:tempManager];
}); ZYAudioManager *managerOne = [ZYAudioManager defaultManager]; dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [managers enumerateObjectsUsingBlock:^(ZYAudioManager *obj, NSUInteger idx, BOOL * _Nonnull stop) {
XCTAssertEqual(managerOne, obj, @"ZYAudioManager is not single");
}]; });
} /**
* 测试是否可以正常播放音乐
*/
- (void)testPlayingMusic
{
self.player = [[ZYAudioManager defaultManager] playingMusic:_fileName];
XCTAssertTrue(self.player.playing, @"ZYAudioManager is not PlayingMusic");
} /**
* 测试是否可以正常停止音乐
*/
- (void)testStopMusic
{
if (self.player == nil) {
self.player = [[ZYAudioManager defaultManager] playingMusic:_fileName];
} if (self.player.playing == NO) [self.player play]; [[ZYAudioManager defaultManager] stopMusic:_fileName];
XCTAssertFalse(self.player.playing, @"ZYAudioManager is not StopMusic");
} /**
* 测试是否可以正常暂停音乐
*/
- (void)testPauseMusic
{
if (self.player == nil) {
self.player = [[ZYAudioManager defaultManager] playingMusic:_fileName];
}
if (self.player.playing == NO) [self.player play];
[[ZYAudioManager defaultManager] pauseMusic:_fileName];
XCTAssertFalse(self.player.playing, @"ZYAudioManager is not pauseMusic");
} @end
需要注意的是,单例要在并发的条件下测试,我采用的是dispatch_group,主要是考虑到,必须要等待所有并发结束才能比较结果,否则可能会出错。比如说,并发条件下,x线程已经执行完毕了,它所对应的a对象已有值;而y线程还没开始初始化,它所对应的b对象还是为nil,为了避免这种条件的产生,我采用dispatch_group来等待所有并发结束,再去做相应的判断。
首页控制器的代码:
#import "ZYMusicViewController.h"
#import "ZYPlayingViewController.h"
#import "ZYMusicTool.h"
#import "ZYMusic.h"
#import "ZYMusicCell.h" @interface ZYMusicViewController ()
@property (nonatomic, strong) ZYPlayingViewController *playingVc; @property (nonatomic, assign) int currentIndex;
@end @implementation ZYMusicViewController - (ZYPlayingViewController *)playingVc
{
if (_playingVc == nil) {
_playingVc = [[ZYPlayingViewController alloc] initWithNibName:@"ZYPlayingViewController" bundle:nil];
}
return _playingVc;
} - (void)viewDidLoad {
[super viewDidLoad]; [self setupNavigation];
} - (void)setupNavigation
{
self.navigationItem.title = @"音乐播放器";
} #pragma mark ----TableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1;
} - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [ZYMusicTool musics].count;
} - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ZYMusicCell *cell = [ZYMusicCell musicCellWithTableView:tableView];
cell.music = [ZYMusicTool musics][indexPath.row];
return cell;
} #pragma mark ----TableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 70;
} - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES]; [ZYMusicTool setPlayingMusic:[ZYMusicTool musics][indexPath.row]]; ZYMusic *preMusic = [ZYMusicTool musics][self.currentIndex];
preMusic.playing = NO;
ZYMusic *music = [ZYMusicTool musics][indexPath.row];
music.playing = YES;
NSArray *indexPaths = @[
[NSIndexPath indexPathForItem:self.currentIndex inSection:0],
indexPath
];
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; self.currentIndex = (int)indexPath.row; [self.playingVc show];
} @end
重点需要说说的是这个界面的实现:
这里做了比较多的细节控制,具体在代码里面有相应的描述。主要是想说说,在实现播放进度拖拽中遇到的问题。
控制进度条的移动,我采用的是NSTimer,添加了一个定时器,并且在不需要它的地方都做了相应的移除操作。
这里开发的时候,遇到了一个问题是,我拖动滑块的时候,发现歌曲播放的进度是不正确的。代码中可以看到:
//得到挪动距离
CGPoint point = [sender translationInView:sender.view];
//将translation清空,免得重复叠加
[sender setTranslation:CGPointZero inView:sender.view];
在使用translation的时候,一定要记住,每次处理过后,一定要将translation清空,以免它不断叠加。
我使用的是ZYLrcView来展示歌词界面的,需要注意的是,它继承自UIImageView,所以要将userInteractionEnabled属性设置为Yes。
代码:
#import <UIKit/UIKit.h> @interface ZYLrcView : UIImageView
@property (nonatomic, assign) NSTimeInterval currentTime;
@property (nonatomic, copy) NSString *fileName;
@end #import "ZYLrcView.h"
#import "ZYLrcLine.h"
#import "ZYLrcCell.h"
#import "UIView+AutoLayout.h" @interface ZYLrcView () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, weak) UITableView *tableView;
@property (nonatomic, strong) NSMutableArray *lrcLines;
/**
* 记录当前显示歌词在数组里面的index
*/
@property (nonatomic, assign) int currentIndex;
@end @implementation ZYLrcView #pragma mark ----setter\geter方法 - (NSMutableArray *)lrcLines
{
if (_lrcLines == nil) {
_lrcLines = [ZYLrcLine lrcLinesWithFileName:self.fileName];
}
return _lrcLines;
} - (void)setFileName:(NSString *)fileName
{
if ([_fileName isEqualToString:fileName]) {
return;
}
_fileName = [fileName copy];
[_lrcLines removeAllObjects];
_lrcLines = nil;
[self.tableView reloadData];
} - (void)setCurrentTime:(NSTimeInterval)currentTime
{
if (_currentTime > currentTime) {
self.currentIndex = 0;
}
_currentTime = currentTime; int minute = currentTime / 60;
int second = (int)currentTime % 60;
int msecond = (currentTime - (int)currentTime) * 100;
NSString *currentTimeStr = [NSString stringWithFormat:@"%02d:%02d.%02d", minute, second, msecond]; for (int i = self.currentIndex; i < self.lrcLines.count; i++) {
ZYLrcLine *currentLine = self.lrcLines[i];
NSString *currentLineTime = currentLine.time;
NSString *nextLineTime = nil; if (i + 1 < self.lrcLines.count) {
ZYLrcLine *nextLine = self.lrcLines[i + 1];
nextLineTime = nextLine.time;
} if (([currentTimeStr compare:currentLineTime] != NSOrderedAscending) && ([currentTimeStr compare:nextLineTime] == NSOrderedAscending) && (self.currentIndex != i)) { NSArray *reloadLines = @[
[NSIndexPath indexPathForItem:self.currentIndex inSection:0],
[NSIndexPath indexPathForItem:i inSection:0]
];
self.currentIndex = i;
[self.tableView reloadRowsAtIndexPaths:reloadLines withRowAnimation:UITableViewRowAnimationNone]; [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForItem:self.currentIndex inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
} }
}
#pragma mark ----初始化方法 - (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self commitInit];
}
return self;
} - (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder]) {
[self commitInit];
}
return self;
} - (void)commitInit
{
self.userInteractionEnabled = YES;
self.image = [UIImage imageNamed:@"28131977_1383101943208"];
self.contentMode = UIViewContentModeScaleToFill;
self.clipsToBounds = YES;
UITableView *tableView = [[UITableView alloc] init];
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
tableView.backgroundColor = [UIColor clearColor];
self.tableView = tableView;
[self addSubview:tableView];
[self.tableView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(0, 0, 0, 0)];
} #pragma mark ----UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
} - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.lrcLines.count;
} - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ZYLrcCell *cell = [ZYLrcCell lrcCellWithTableView:tableView];
cell.lrcLine = self.lrcLines[indexPath.row]; if (indexPath.row == self.currentIndex) { cell.textLabel.font = [UIFont boldSystemFontOfSize:16];
}
else{
cell.textLabel.font = [UIFont systemFontOfSize:13];
}
return cell;
} - (void)layoutSubviews
{
[super layoutSubviews]; // NSLog(@"++++++++++%@",NSStringFromCGRect(self.tableView.frame));
self.tableView.contentInset = UIEdgeInsetsMake(self.frame.size.height / 2, 0, self.frame.size.height / 2, 0);
}
@end
也没有什么好说的,整体思路就是,解析歌词,将歌词对应的播放时间、在当前播放时间的那句歌词一一对应,然后持有一个歌词播放的定时器,每次给ZYLrcView传入歌曲播放的当前时间,如果,歌曲的currentTime > 当前歌词的播放,并且小于下一句歌词的播放时间,那么就是播放当前的这一句歌词了。
我这里做了相应的优化,CADisplayLink生成的定时器,是每毫秒调用触发一次,1s等于1000ms,如果不做一定的优化,性能是非常差的,毕竟一首歌怎么也有四五分钟。在这里,我记录了上一句歌词的index,那么如果正常播放的话,它去查找歌词应该是从上一句播放的歌词在数组里面的索引开始查找,这样就优化了很多。
这是锁屏下的界面展示:
这是使用Instruments的Time Profiler时的情景:
还有其他许多细节,就不一一例举了......
github地址:https://github.com/wzpziyi1/MusicPlauer
ios开发:一个音乐播放器的设计与实现的更多相关文章
- Flex4/Flash开发在线音乐播放器 , 含演示地址
要求 必备知识 本文要求基本了解 Adobe Flex编程知识和JAVA基础知识. 开发环境 MyEclipse10/Flash Builder4.6/Flash Player11及以上 演示地址 演 ...
- Android音乐播放器的设计与实现
目录 应用开发技术及开发平台介绍 应用需求分析 应用功能设计及其描述 应用UI展示 一.应用开发技术及平台介绍 ①开发技术: 本系统是采用面向对象的软件开发方法,基于Android studio开发平 ...
- python 开发在线音乐播放器-简易版
在线音乐播放器,使用python的Tkinter库做了一个界面,感觉这个库使用起来还是挺方便的,音乐的数据来自网易云音乐的一个接口,通过urllib.urlopen模块打开网址,使用Json模块进行数 ...
- 痞子衡嵌入式:基于恩智浦i.MXRT1010的MP3音乐播放器(RT-Mp3Player)设计
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是基于i.MXRT1011的MP3播放器参考设计. i.MXRT1011是恩智浦i.MXRT四位数系列的入门型号,虽然是入门级,可也是50 ...
- Android开发之音乐播放器的实现
Android音乐播放器 使用到Android的Actiivity和Service组件 播放音频的代码应该运行在服务中,定义一个播放服务MusicService,服务里定义play.stop.paus ...
- 每天看一片代码系列(三):codepen上一个音乐播放器的实现
今天我们看的是一个使用纯HTML+CSS+JS实现音乐播放器的例子,效果还是很赞的: codePen地址 HTML部分 首先我们要思考一下,一个播放器主要包含哪些元素.首先要有播放的进度信息,还有播放 ...
- Android开发之音乐播放器
做了一天的音乐播放器小项目,已经上传到github,将链接发到这里供大家参阅提议 https://github.com/wangpeng0531/MusicPlayer.git
- 【Blazor】在ASP.NET Core中使用Blazor组件 - 创建一个音乐播放器
前言 Blazor正式版的发布已经有一段时间了,.NET社区的各路高手也创建了一个又一个的Blazor组件库,其中就包括了我和其他小伙伴一起参与的AntDesign组件库,于上周终于发布了第一个版本0 ...
- Android应用开发--MP3音乐播放器代码实现(一)
需求1:将内存卡中的MP3音乐读取出来并显示到列表当中 1. 从数据库中查询所有音乐数据,保存到List集合当中,List当中存放的是Mp3Info对象 2. 迭代List集合,把每一个Mp3 ...
随机推荐
- Flutter常用组件(Widget)解析-Text
单一格式的文本. 文本组件是以字符串形式显示的单一格式,这个文本字符串可以是多行显示也可以是单独一行显示,主要取决于你的布局限制. 这样式内容是可选择的,如果你省略了,则会使用文本的默认样式来显示.如 ...
- Mongodb - 切片搭建
0.概述 mongodb分片搭建,版本号4.0.2,以下除了创建opt文件夹以外,所有操作均在mongodb用户下面执行 准备三台机器:192.168.56.81192.168.56.82192.16 ...
- UltraEdit 不生成.bak文件
UE不自动生成.bak文件每次保存之后都能看到后面加个.bak后缀的文件出现有时真的很烦,而且还容易搞混,下面的方法可以解除这种烦恼.版本不同可以会有些差别. 中文版按照如下顺序设置:高级--> ...
- 安卓工作室 android studio 的 汉化 美化 定制 Android studio's Chinesization beautification customization
安卓工作室 android studio 的 汉化 美化 定制 Android studio's Chinesization beautification customization 汉化包 百度云盘 ...
- loj#2128. 「HAOI2015」数字串拆分 矩阵乘法
目录 题目链接 题解 代码 题目链接 loj#2128. 「HAOI2015」数字串拆分 题解 \(f(s)\)对于\(f(i) = \sum_{j = i - m}^{i - 1}f(j)\) 这个 ...
- socket 远程命令
# -*- coding: utf-8 -*- # 斌彬电脑 from socket import * import subprocess server = socket(AF_INET, SOCK_ ...
- [NOIp2010提高组]关押罪犯
OJ题号:洛谷1525 思路:贪心. 先将所有的人按怨气值从大到小排一下,然后依次尝试将双方分入两个不同的监狱,如果失败(即已分入相同的监狱),则输出这个怨气值. #include<cstdio ...
- Chrome for Mac键盘快捷键!来自Google Chrome官网!
⌘-N 打开新窗口. ⌘-T 打开新标签页. ⌘-Shift-N 在隐身模式下打开新窗口. 按 ⌘-O,然后选择文件. 在 Google Chrome 浏览器中打开计算机中的文件. 按住 ⌘ 键,然后 ...
- 安装 jenkins
1. 将jenkins.war包放在 tomcat 的 webapps 目录下即可 2 重启 tomcat 3. 通过浏览器访问 IP:8080/jenkins
- Codeforces Round #407 div2 题解【ABCDE】
Anastasia and pebbles 题意:你有两种框,每个框可以最多装k重量的物品,但是你每个框不能装不一样的物品.现在地面上有n个物品,问你最少多少次,可以把这n个物品全部装回去. 题解:其 ...