热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ios开发:一个音乐播放器的设计与实现案例

ios开发:一个音乐播放器的设计与实现案例-这个Demo,关于歌曲播放的主要功能都实现了的。下一曲、上一曲,暂停,根据歌曲的播放进度动态滚动歌词,将当前正在播放的歌词放大显示,拖动

这个Demo,关于歌曲播放的主要功能都实现了的。下一曲、上一曲,暂停,根据歌曲的播放进度动态滚动歌词,将当前正在播放的歌词放大显示,拖动进度条,歌曲跟着变化,并且使用Time Profiler进行了优化,还使用XCTest对几个主要的类进行了单元测试。

已经经过真机调试,在真机上可以后台播放音乐,并且锁屏时,显示一些主要的歌曲信息。

根据歌曲的播放来显示对应歌词的。用UITableView来显示歌词,可以手动滚动界面查看后面或者前面的歌词。

并且,当拖动进度条,歌词也会随之变化,下一曲、上一曲依然是可以使用的。

代码分析:

准备阶段,先是写了一个音频播放的单例,用这个单例来播放这个demo中的音乐文件,代码如下:

#import 
#import 
@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 
#import "ZYAudioManager.h"
#import 
 
@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 
 
@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 () 
@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 

 也没有什么好说的,整体思路就是,解析歌词,将歌词对应的播放时间、在当前播放时间的那句歌词一一对应,然后持有一个歌词播放的定时器,每次给ZYLrcView传入歌曲播放的当前时间,如果,歌曲的currentTime > 当前歌词的播放,并且小于下一句歌词的播放时间,那么就是播放当前的这一句歌词了。

我这里做了相应的优化,CADisplayLink生成的定时器,是每毫秒调用触发一次,1s等于1000ms,如果不做一定的优化,性能是非常差的,毕竟一首歌怎么也有四五分钟。在这里,我记录了上一句歌词的index,那么如果正常播放的话,它去查找歌词应该是从上一句播放的歌词在数组里面的索引开始查找,这样就优化了很多。


推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了如何在方法参数中指定一个对象的协议,以及如何调用符合该协议的方法。以一个具体的示例说明了如何在方法参数中指定一个UIView子类对象,并且该对象需要符合PixelUI协议,同时方法需要能够访问该对象的属性。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • Oracle seg,V$TEMPSEG_USAGE与Oracle排序的关系及使用方法
    本文介绍了Oracle seg,V$TEMPSEG_USAGE与Oracle排序之间的关系,V$TEMPSEG_USAGE是V_$SORT_USAGE的同义词,通过查询dba_objects和dba_synonyms视图可以了解到它们的详细信息。同时,还探讨了V$TEMPSEG_USAGE的使用方法。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 怀疑是每次都在新建文件,具体代码如下 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
author-avatar
大树沧海凝神的天空
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有