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

github地址:https://github.com/wzpziyi1/MusicPlauer

这个medo,关于歌曲播放的主要功能都实现了的。下一曲、上一曲,暂停,根据歌曲的播放进度动态滚动歌词,将当前正在播放的歌词放大显示,拖动进度条,歌曲跟着变化,并且使用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

时间: 2024-12-15 01:41:22

ios开发:一个音乐播放器的设计与实现的相关文章

Android MVC实现一个音乐播放器

MVCPlayer 我尝试在android上使用MVC模式来开发一个音乐播放器.GitHub地址:https://github.com/skyhacker2/MVCPlayer 什么是MVC 来自维基百科 控制器 Controller - 负责转发请求,对请求进行处理. 视图 View - 界面设计人员进行图形界面设计. 模型 Model - 程序员编写程序应有的功能(实现算法等等).数据库专家进行数据管理和数据库设计(可以实现具体的功能). 那么在android上,Activity就是Cont

Flex4/Flash+BlazeDS+JAVA+MySql 开发在线音乐播放器实例

要求 必备知识 本文要求基本了解 Adobe Flex编程知识和JAVA基础知识. 开发环境 MyEclipse10/Flash Builder4.6/Flash Player11及以上 演示地址 演示地址 传统网络程序的开发是基于页面的.服务器端数据传递的模式,把网络程序的表现层建立于HTML页面之上,而HTML是适合于文本的,传统的基于页面的系统已经渐渐不能满足网络浏览者的更高的.全方位的体验要求了.而富互联网应用(Rich Internet Applications,缩写为RIA)的出现就

每天看一片代码系列(三):codepen上一个音乐播放器的实现

今天我们看的是一个使用纯HTML+CSS+JS实现音乐播放器的例子,效果还是很赞的: codePen地址 HTML部分 首先我们要思考一下,一个播放器主要包含哪些元素.首先要有播放的进度信息,还有播放/暂停或者上一首下一首等必要的按钮,同时还要显示一些当前播放的音乐名称等信息.播放多首歌曲时,要显示播放列表...因此,从语义上可以构造出基本的HTML结构: // 背景区块,用于显示当前播放音乐的图片 <div class='background' id='background'></di

Android开发之音乐播放器的实现

Android音乐播放器 使用到Android的Actiivity和Service组件 播放音乐使用Service组件 操作按钮等使用Activity与用户交互 同时为了把服务所在进程变成服务进程,防止Activity销毁时依旧执行Service,需要使用Service启动的混合调用,即先使用startService(),然后使用bindService()方法. 原理:使用bindService()方法,启动service.在ServiceConnection的onServiceConnecte

给博客增加一个音乐播放器特效

我是直接使用网音乐音乐的 我感觉这样比较快一点 把自己喜欢的音乐放进去 生成相应的代码就可以了 具体步骤: 首先:找到网易云音乐官网登录 然后:找到自己喜欢的音乐 旁边有一个生成外链播放器,直接点击进入就会得到: 把下面生成的代码直接复制粘贴到博客里面的公告栏就可以了 至于位置,需要自己来衡量了 上面也说明了很多博客网站不支持嵌入iframe 这个博客园就是不会支持iframe的嵌入 我们需要把iframe更换为:embed  就可以了 有的小伙伴,可能不会只是添加一首歌(举个栗子,我就是这样的

VUE项目实现音乐播放器(四)------- 设计播放进度条 + 播放控制按钮

2020.3.31 9:18 好的,早上好各位,今天我们来进行一个很炫酷的页面开发——播放器控制页面( src\components\Play.vue ),如下图: 我们可以看到,该页面有很多元素组成,歌曲的封面.左上角的页面隐藏按钮,中间的播放进度条.歌词栏.下方的播放控制按钮.右下角的显示播放列表按钮,这些元素完美的结合在一起,整个页面有没有一种很高大上的感觉!好了,让我们来亲自动手实现它吧! 1. 歌曲封面&隐藏按钮 首先通过 getters 获取到歌曲的封面: computed: { i

javascript开发迷你音乐播放器

知识点:html/css布局思维,音频标签api运用,css3自定义动画,Js音乐播放控制,歌词同步等. html代码: <textarea id="txt" style="display:none"> [00:00.64]小幸运 - 谭嘉仪 [00:02.15]词:徐世珍&吴辉福 [00:03.70]曲:JerryC [00:04.14]编曲:JerryC [00:13.77]我听见雨滴落在青青草地 [00:19.89]我听见远方下课钟声响起

iOS开发拓展篇—音频处理(音乐播放器4)

iOS开发拓展篇—音频处理(音乐播放器4) 说明:该文主要介绍音乐播放器实现过程中的一些细节控制. 实现的效果: 一.完整的代码 YYPlayingViewController.m文件 1 // 2 // YYPlayingViewController.m 3 // 20-音频处理(音乐播放器1) 4 // 5 // Created by apple on 14-8-13. 6 // Copyright (c) 2014年 yangyong. All rights reserved. 7 //

4个小时实现一个HTML5音乐播放器

技术点:ES6+Webpack+HTML5 Audio+Sass 这里,我们将一步步的学到如何从零去实现一个H5音乐播放器. 首先来看一下最终的实现效果:Demo链接 接下来就步入正题: 要做一个音乐播放器就要非常了解在Web中音频播放的方式,通常都采用HTML5的audio标签关于audio标签,它有大量的属性.方法和事件,在这里我就做一个大致的介绍. 属性:src:必需,音频来源:controls:常见,设置后显示浏览器默认的audio控制面板,不设置默认隐藏audio标签:autoplay