ПРОГРАММИРОВАНИЕ ПОД IPHONE, IPAD OBJECTIVE-C часть 3

Полноценный проигрыватель аудио

В этом примере я продолжу тему воспроизведения аудиофайлов. В нем я покажу как правильно воспроизводить длинные *.mp3 файлы. Фактически, этот пример является продолжением примера Простой проигрыватель звуков. Я предлагаю не создавать новый проект, а воспользоваться кодом с вышеупомянутого примера.

 

Первое, что нам следует сделать — это добавить фреймворк (AVFoundation), который поможет нам с воспроизведением. Если кто-то забыл как это делается — можно почитать здесь.

 

Теперь импортируем только что добавленную библиотеку и сделаем наш плеер синглтоном.

 

SoundPlayer.h

<code data-result="[object Object]">#import &lt;AudioToolbox/AudioServices.h&gt;
#import &lt;AVFoundation/AVFoundation.h&gt;

@interface SoundPlayer : NSObject {

}

+(SoundPlayer *)sharedPlayer;

+ (void)playSound:(NSString*)soundName;

@end</code>

 

SoundPlayer.m

<code data-result="[object Object]">#import "SoundPlayer.h"

@implementation SoundPlayer

static SoundPlayer * sharedPlayer = NULL;
+ (SoundPlayer *) sharedPlayer {
    if ( !sharedPlayer || sharedPlayer == NULL ) {
        sharedPlayer = [SoundPlayer new];
    }

    return sharedPlayer;
}

+ (void)playSound:(NSString*)soundName {
    SystemSoundID volleyFile;
    NSString *volleyPath = [[NSBundle mainBundle] pathForResource:soundName ofType:nil];
    CFURLRef volleyURL = (CFURLRef ) [NSURL fileURLWithPath:volleyPath];
    AudioServicesCreateSystemSoundID (volleyURL, &amp;volleyFile);
    AudioServicesPlaySystemSound(volleyFile);
}

@end</code>

 

Для того, чтобы класс стал синглтоном достаточно добавить в него всего один метод. Этот метод будет возвращаться единственный экземпляр класса, а если такого нет — то создат этот экземпляр.

 

Теперь добавим в класс переменную, в которой будут храниться все аудиофайлы подготовленные для воспроизведения и методы управления звуком. Таким образом, интерфейс класса SoundPlayer должен выглядеть так:

 

<code data-result="[object Object]">#import &lt;AudioToolbox/AudioServices.h&gt;
#import &lt;AVFoundation/AVFoundation.h&gt;

@interface SoundPlayer : NSObject {
    NSMutableDictionary *audioFiles;
}

@property (nonatomic,retain) NSMutableDictionary *audioFiles;

+(SoundPlayer *)sharedPlayer;

+ (void)playSound:(NSString*)soundName;

- (void)cacheWithFiles:(NSArray *)sounds;
- (void)playFile:(NSString*)soundFileName volume:(CGFloat)volume loops:(NSInteger)numberOfLoops;
- (void)resumePlaing:(NSString*)soundFileName;
- (void)resumePlaing:(NSString*)soundFileName withVolume:(CGFloat)volume;
- (void)pausePlaing:(NSString*)soundFileName;
- (void)stopPlaing:(NSString*)soundFileName;
- (BOOL)isPlaying:(NSString*)soundFileName;

@end</code>

 

Изменим соответственно реализацию класса SoundPlayer. В первую очередь синтезируем методы доступа и организуем очистку памяти для переменной audioFiles:

 

<code data-result="[object Object]">#import "SoundPlayer.h"

@implementation SoundPlayer

@synthesize audioFiles;

- (void)dealloc {
    self.audioFiles = nil;
    [super dealloc];
}

static SoundPlayer * sharedPlayer = NULL;
+ (SoundPlayer *) sharedPlayer {
    if ( !sharedPlayer || sharedPlayer == NULL ) {
        sharedPlayer = [SoundPlayer new];
    }

    return sharedPlayer;
}

+ (void)playSound:(NSString*)soundName {
    SystemSoundID volleyFile;
    NSString *volleyPath = [[NSBundle mainBundle] pathForResource:soundName ofType:nil];
    CFURLRef volleyURL = (CFURLRef ) [NSURL fileURLWithPath:volleyPath];
    AudioServicesCreateSystemSoundID (volleyURL, &amp;volleyFile);
    AudioServicesPlaySystemSound(volleyFile);
}

@end</code>

 

Я специально не добавил в код реализацию методов описаных в интерфейсе. Будем добавлять их по очереди и разберать, что они делают. Первый в списке метод cacheWithFiles:

 

<code data-result="[object Object]">- (void)cacheWithFiles:(NSArray *)sounds {
    NSBundle *mainBundle = [NSBundle mainBundle];
    NSError *error;

    self.audioFiles = [NSMutableDictionary dictionary];

    for (NSString *fileName in sounds) {
        NSURL *soundURL = [NSURL fileURLWithPath:[mainBundle pathForResource:fileName ofType:nil]];
        AVAudioPlayer *myAudioPlayer = [[[AVAudioPlayer alloc] initWithContentsOfURL:soundURL 
                                                                              error:&amp;error]  autorelease];
        if (myAudioPlayer) {
            [myAudioPlayer prepareToPlay];
            [audioFiles setObject:myAudioPlayer forKey:fileName];
        } else {
            NSLog(@"Error in file(%@): %@
", fileName, [error localizedDescription]);
        }
    }    
}</code>

 

Разберем код по строкам. В первых двух строчках мы создаем переменные. В переменной mainBundle хранится путь к песочнице проекта (оттуда мы будем читать файлы), а в переменной error будет храниться описание возможной ошибки. Затем мы инициализируем словарь, в котором будут храниться аудиофайлы. После подготовки всех переменных мы запускаем цикл, в котором перебераем все имена файлов. То есть, в наш класс мы будем передавать список имен файлов, которые собераемся воспроизводить. В первой строке тела цикла мы на основании имени файла создаем ссылку на него (дело в том, что AVAudioPlayer в качестве параметра принимает именно ссылку на файл). Затем создаем сам плеер (объект класса AVAudioPlayer). После чего следует условный оператор. Если плеер создался успешно — подготавливаем его к проигрыванию данного файла и добавляем в словарь. В противном случае — выводим в консоль ошибку, по которой файл не был создан.

 

Как вы уже заметили, в словаре у нас не файлы, а плееры, которые в дальнейшем мы будем запускать на проигрывание. А имена ключей к этим плеерам совпадают с именами файлов в проекте. Правомерным будет вопрос «зачем такая сложность? почему бы просто не создавать плеер и не проигрывать музыку при каком-то событии, к примеру, нажатие на кнопку?». Дело в том, что перед воспроизведением плеер как бы кешируем файл, это происходит при вызове метода prepareToPlay. И процесс кеширования блокирует интерфейс, а длительность блокировки зависит от размера файла, который вы хотите воспроизвести. То есть, если файл будет порядка 2-3 мегабайт то программа может «зависнуть» на 0,5-1 секурнды и будет создаваться впечатление, что приложение «тупит». Воизбежание такой ситуации я предлагаю подготовить все файлы к воспроизведению в начале загрузки приложения. Или если это игра, вы можете выводить окошко загрузки  очередного уровня и загружать только те файлы, которые будут воспроизводиться на данном уровне.

 

Это было небольшое риторическое отступление. Теперь перейдем к следующему методу:

 

<code data-result="[object Object]">- (void)playFile:(NSString*)soundFileName volume:(CGFloat)volume loops:(NSInteger)numberOfLoops {
    AVAudioPlayer *sound = [audioFiles objectForKey:soundFileName];
    sound.volume = volume;
    sound.numberOfLoops = numberOfLoops;
    sound.currentTime = 0.0f;
    [sound play];
}</code>

 

В этом методе мы получаем со словаря по заданному ключу плеер. Устанавливаем ему громкость, количество повторов проигрывания и время с которого будет начато проигрывание. Затем с помощью команды play запускаем процесс. Здесь есть несколько важных моментов. Первый — это как правильно задать сколько раз файл будет проигран. Если значение numberOfLoops будет равно нулю — файл проиграется один раз, если значение будет равно еденице — файл проиграется два раза и так далее. А любое отрицательное значение этой переменной заставит плеер проигрывать файл все время (пока вы его не остановите другим методом). Второй важный момент — это установка времении, с которого будет начинаться воспроизведение. Дело в том, что если мы пошлем плееру коммандуstop — его больше невозможно будет заставить играть. То есть, если представить ситуацию с двумя контроллерами представления для каждого из которых будет использован свой звуковой файл. При переходе от одного контроллера ко второму нам нужно остановить первый файл и включить второй. Если мы будем использовать метод stop для остановки проигрывания — файл первого контроллера мы уже не сможем заставить проигрываться при возврате к первому контроллеру. Для выхода с этой ситуации я предлагаю плеер ставить на паузу, а чтобы при начале проигрывания файла он начинался сначала — устанивливать его свойсту currentTime время начала файла. И на последок, следует помнить, что уровень громкости — это вещественное число. Его минимальное значение 0.0, а максимально 1.0.

 

Следующие методы относятся к управлению процессом проигрывания.

 

<code data-result="[object Object]">- (void)resumePlaing:(NSString*)soundFileName {
    AVAudioPlayer *sound = [audioFiles objectForKey:soundFileName];
    [sound play];    
}

- (void)resumePlaing:(NSString*)soundFileName withVolume:(CGFloat)volume {
    AVAudioPlayer *sound = [audioFiles objectForKey:soundFileName];
    sound.volume = volume;
    [sound play];    
}

- (void)pausePlaing:(NSString*)soundFileName {
    AVAudioPlayer *sound = [audioFiles objectForKey:soundFileName];
    [sound pause];
}

- (void)stopPlaing:(NSString*)soundFileName {
    AVAudioPlayer *sound = [audioFiles objectForKey:soundFileName];
    [sound stop];    
}</code>

 

В каждом из них мы получаем текущий плеер и передаем ему соответствующую команду. Следует обратить внимание на метод — (void)resumePlaing:(NSString*)soundFileName withVolume:(CGFloat)volume, в нем мы не только продолжаем проигрывание, но и устанавливаем новый уровень громкости. Это может пригодится в случае, если вы используете элемент интерфейса слайдер и с помощью него задаете уровень громкости в программе.

 

Последний метод нашего плеера isPlaying.

 

<code data-result="[object Object]">- (BOOL)isPlaying:(NSString*)soundFileName {
    AVAudioPlayer *sound = [audioFiles objectForKey:soundFileName];
    return sound.playing;    
}</code>

 

Он возвращает логическое значение проигрывается сейчас данные файл или нет.

 

Наш плеер готов. Можно смело им пользоваться. Для демонстрации его возможностей изменим класс MySoundPlayerViewController. Добавим в него таблицу как это делали в примере Добавляем таблицу на View. Только теперь у нас будут массив не студентов, а файлов. После внесения всех изменений класс MySoundPlayerViewController должен выглядеть так:

 

MySoundPlayerViewController.h

<code data-result="[object Object]">#import &lt;UIKit/UIKit.h&gt;
#import "SoundPlayer.h"

@interface MySoundPlayerViewController : UIViewController 
&lt;UITableViewDelegate, UITableViewDataSource&gt; 
{
    UITableView *namesList;
    NSArray *files;
}

@property (nonatomic, retain) IBOutlet UITableView *namesList;
@property (nonatomic, retain) NSArray *files;

@end</code>

 

MySoundPlayerViewController.m

<code data-result="[object Object]">#import "MySoundPlayerViewController.h"

@implementation MySoundPlayerViewController

@synthesize namesList;
@synthesize files;

- (void)dealloc {
    self.namesList = nil;
    [super dealloc];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    self.namesList = nil;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.files = [NSArray arrayWithObjects:
                  @"Arrival.mp3",
                  @"Cool Breeze.mp3", 
                  @"Desert Rose.mp3", 
                  @"Fields Of Gold.mp3", nil];

    [[SoundPlayer sharedPlayer] cacheWithFiles:files];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [files count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                                       reuseIdentifier:CellIdentifier] autorelease];
    }

    cell.textLabel.text = [files objectAtIndex:indexPath.row];

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    for (NSString *file in files) {
        if ([[SoundPlayer sharedPlayer] isPlaying:file]) {
            [[SoundPlayer sharedPlayer] pausePlaing:file];
        }
    }

    NSString *fileName = [files objectAtIndex:indexPath.row];
    [[SoundPlayer sharedPlayer] playFile:fileName volume:0.5f loops:0];
}

@end</code>

 

Практически ничего нового я здесь не написал. Все самое интересно начинается в методе didSelectRowAtIndexPath (в момент нажатия на ячейку). Здесь мы ищим и ставим на паузу файлы, которые в данный момент проигрываются. А затем запускаем выбранный с таблици файл.

 

Осталось лишь добавить перечисленные в массиве файлы в проект. Конечно же вы можете добавить свои файлы и соответственно, в массиве указать их имена. Только после этого пример будет корректно работать. Из-за размера файлов я не включаю их в проект, как уже сказано выше — вам следует добавить свои.

 

Исходный код этого проекта можно скачать здесь. А сам класс SoundPlayer забирать отсюда.

Comments are closed.