IOS Development: A music player design and implementation case _ios

Source: Internet
Author: User
Tags current time sessions uikit

This demo, about the main function of song playback has been achieved. Next, on a song, pause, according to the song Play Progress dynamic scrolling lyrics, the currently playing lyrics enlarged display, drag progress bar, songs follow the changes, and use time Profiler for optimization, but also using xctest for several major classes of the unit test.

Already after the real machine debugging, in the real machine can play music backstage, and the lock screen, display some of the main song information.

Displays the corresponding lyrics according to the song's play. Using UITableView to display lyrics, you can manually scroll through the screen to see the lyrics behind or in front of you.

Also, when you drag the progress bar, the lyrics will change, and the next and last song is still available.

Code Analysis:

In the preparation phase, first write an audio playback of a single example, use this single example to play the demo of the music file, the code is as follows:

#import <Foundation/Foundation.h> #import <AVFoundation/AVFoundation.h> @interface Zyaudiomanager:
 
NSObject + (instancetype) Defaultmanager;
Play Music-(Avaudioplayer *) Playingmusic: (NSString *) filename;
-(void) Pausemusic: (NSString *) filename;
 
-(void) Stopmusic: (NSString *) filename;
Play sound Effects-(void) PlaySound: (NSString *) filename;
-(void) Disposesound: (NSString *) filename; @end #import "ZYAudioManager.h" @interface Zyaudiomanager () @property (nonatomic, strong) Nsmutabledictionary *mu
Sicplayers;
@property (nonatomic, strong) Nsmutabledictionary *soundids;
 
@end static Zyaudiomanager *_instance = nil; @implementation Zyaudiomanager + (void) Initialize {//audio session avaudiosession *session = [Avaudiosession sharedinstance
   
  ];
   
  Set the session type (playback type, playback mode, will automatically stop other music playback) [Sessions Setcategory:avaudiosessioncategoryplayback Error:nil];
Activate session [Sessions 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 dictio
      Nary];
    _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; //Play Music-(Avaudioplayer *) Playingmusic: (NSString *) filename {if (filename = = Nil | | filename.length = 0) return ni
   
  L   Avaudioplayer *player = Self.musicplayers[filename];
     
    First query whether the object caches the 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; Object is newly created, cache it once} if (![
  Player IsPlaying]) {//If there is no playing, start playing and if it is playing, then there is no need to change [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];
   
  //Play sound effect-(void) PlaySound: (NSString *) filename {if (!filename) return;
   
  Remove the corresponding sound effect 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);
}//Play Audioservicesplaysystemsound (Soundid);
   
   
  }//Destroy sound effect-(void) Disposesound: (NSString *) filename {if (!filename) return;
   
  Systemsoundid soundid = (int) [self.soundids[filename] unsignedlongvalue];
     
    if (Soundid) {audioservicesdisposesystemsoundid (soundid);  [Self.soundids Removeobjectforkey:filename];
 Sound is destroyed, the corresponding object should be removed from the cache} @end

is a single example of the design, and not much difficulty. I used a dictionary to play the songs, so if it is paused, and then start playing, it will be loaded directly in the cache. But if you don't notice, in Stopmusic: (NSString *) FileName This method, do not remove from the dictionary has stopped playing songs, then you play the song again, will be played on the progress of the original playback. In the coding process, I encountered this bug, then I found out that when I switched the song (the previous one, the next), I called the Stopmusic method, but because I didn't remove it from the dictionary, it always started playing from the previous progress rather than from the beginning.

If you want to play the song backstage in the real machine, in addition to Appdelegate and plist inside do the corresponding operation, but also set the playback mode to: Avaudiosessioncategoryplayback. In particular, I need to pay attention here, when I debug on the simulator, there is no setting this mode can also play backstage, but in the real machine is not. Later on the stackoverflow found the corresponding answer, you need to set the playback mode.

This single example class, which is critical throughout the demo, is guaranteed to be correct, so I wrote the xctest of this class for unit testing, and the code is as follows:

#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 the setup code here.
This is called before the invocation of the all test method in the class. }-(void) teardown {//Put teardown code here.
  This are called after the invocation of the all test method in the class.
[Super teardown];
  }-(void) Testexample {//This are an example of the a functional test case.
Use Xctassert and related functions to verify your tests the produce correct.
   
  /** * Test is a single example, to test under concurrent conditions/-(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 Enumera teobjectsusingblock:^ (Zyaudiomanager *obj, Nsuinteger idx, BOOL * _nonnull stop) {xctassertequal (managerone, obj, @
    "Zyaudiomanager is not single");
     
  }];
}); /** * Test Whether music can be played properly/-(void) Testplayingmusic {self.player = [[Zyaudiomanager Defaultmanager] Playingmusic:_file
  Name];
Xctasserttrue (self.player.playing, @ "Zyaudiomanager is not Playingmusic"); /** * Test whether music can stop properly/-(void) Teststopmusic {if (Self.player = = nil) {self.player = [[Zyaudiomanager Default
  Manager] Playingmusic:_filename];
   
  if (self.player.playing = NO) [Self.player play];
  [[Zyaudiomanager Defaultmanager] stopmusic:_filename];
Xctassertfalse (self.player.playing, @ "Zyaudiomanager is not Stopmusic"); /** * Test If music can be paused properly/-(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

It's important to note that a single example is tested under concurrent conditions, and I'm using dispatch_group, mainly considering that you have to wait for all concurrent ends to compare results, or you might get an error. For example, under concurrent conditions, the x thread has been executed. It corresponds to a object already has a value, and the Y thread has not started initialization, its corresponding B object or for nil, in order to avoid this condition, I use Dispatch_group to wait for all the concurrent end, then to make the corresponding judgment.

Home Controller's Code:

 #import "ZYMusicViewController.h" #import "ZYPlayingViewController.h" #import "ZYMusicTool.h" #import "ZYMusic.h" Import "ZYMusicCell.h" @interface Zymusicviewcontroller () @property (nonatomic, strong) Zyplayingviewcontroller *playi
 
NGVC;
@property (nonatomic, assign) int currentindex; @end @implementation Zymusicviewcontroller-(Zyplayingviewcontroller *) PLAYINGVC {if (_PLAYINGVC = = nil) {_PL
  AYINGVC = [[Zyplayingviewcontroller alloc] initwithnibname:@ "Zyplayingviewcontroller" bundle:nil];
return _PLAYINGVC;
   
  }-(void) viewdidload {[Super viewdidload];
[Self setupnavigation];
 
}-(void) setupnavigation {self.navigationItem.title = @ "Music player";}
 
#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], INDEXPA
  TH];
   
  [Self.tableview reloadrowsatindexpaths:indexpaths Withrowanimation:uitableviewrowanimationnone]; Self.currEntindex = (int) Indexpath.row;
[SELF.PLAYINGVC show]; 
 } @end

The key point to talk about is the implementation of this interface:

Here do a more detailed control, specifically in the code has a corresponding description. The main point is to say, in the implementation of playback progress in the problem encountered.

Control the movement of the progress bar, I used the Nstimer, added a timer, and did not need it where all the corresponding removal operations.

In the development here, I encountered a problem is that when I drag the slider, I found that the progress of the song playback is not correct. You can see in the code:

Get moving distance
  cgpoint point = [Sender TranslationInView:sender.view];
  Will translation empty, lest repeat overlay
  [sender Settranslation:cgpointzero InView:sender.view];

When using translation, be sure to remember that after each processing, be sure to empty the translation to avoid overlapping.

I'm using Zylrcview to show the lyrics interface, and note that it inherits from Uiimageview, so set the Userinteractionenabled property to Yes.

Code:

#import <UIKit/UIKit.h> @interface Zylrcview:uiimageview @property (nonatomic, assign) Nstimeinterval Currentti
Me
@property (nonatomic, copy) NSString *filename; @end #import "ZYLrcView.h" #import "ZYLrcLine.h" #import "ZYLrcCell.h" #import "uiview+autolayout.h" @interface ZY
Lrcview () <uitableviewdatasource, uitableviewdelegate> @property (nonatomic, weak) UITableView *tableView;
@property (nonatomic, strong) Nsmutablearray *lrclines;
/** * Records the current display lyrics in the array inside the index */@property (nonatomic, assign) int currentindex; @end @implementation Zylrcview #pragma mark----Setter\geter Method-(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.%0
   
  2d ", 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:NEXTL
                   Inetime] = = 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----Initialization Method-(Instancetype) initWithFrame: (CGRect) Frame {if (self = [Super Initwithframe:fra
  Me]) {[Self commitinit];
return self; }-(Instancetype) Initwithcoder: (Nscoder *) Adecoder {if (self = [Super Initwithcoder:adecoder]) {[Self Commitini
  T];
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.cou
nt }-(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

There is nothing to say, the whole idea is to parse the lyrics, the lyrics corresponding to the playback time, the current play time of the sentence one by one corresponding, and then hold a song to play the timer, each time to give Zylrcview the current time to play the song, if, the song CurrentTime > The current lyrics play, and is less than the next line of the song play time, then is to play the current sentence lyrics.

I have done the corresponding optimization, Cadisplaylink generated by the timer, is invoked every millisecond trigger once, 1s equals 1000ms, if not to do a certain optimization, performance is very poor, after all, a song how also has four or five minutes. Here, I recorded the last line of the lyrics index, so if the normal playback, it goes to find the lyrics should be from the last sentence played in the index of the words in the array to start looking, so that a lot of optimization.

This is the display of the interface under the lock screen:

The above is the entire content of this article, I hope to help you learn, but also hope that we support the cloud habitat community.

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.