Статьи

Создайте MP3-плеер с AV Foundation

Конечный продукт
Что вы будете создавать

AV Foundation — это фреймворк для работы с аудио и визуальными медиа на iOS и OSX. Используя AV Foundation, вы можете воспроизводить, захватывать и кодировать мультимедиа. Это довольно обширный фреймворк, и в этом уроке мы сосредоточимся на аудио части. В частности, мы будем использовать класс AVAudioPlayer для воспроизведения файлов MP3.

Я предоставил стартовый проект, в котором все действия и выходы уже настроены, а соответствующие методы отключены. Классы, используемые в проекте, также уже заглушены, поэтому мы можем погрузиться прямо в код. Вы можете скачать стартовый проект с GitHub .

Прежде чем вы сможете использовать AV Foundation, вы должны связать проект со структурой. В Project Navigator убедитесь, что ваш проект выбран. На вкладке « Общие » перейдите в « Связанные фреймворки и библиотеки» и оттуда выберите AVFoundation.framework .

Связывание проекта с каркасом AV Foundation

В начальном проекте вы найдете файл с именем FileReader.swift . Откройте этот файл, чтобы просмотреть его содержимое.

1
2
3
4
5
import UIKit
 
class FileReader: NSObject {
    
}

Это простая заглушка класса, которую мы будем использовать для чтения файлов с диска. Он наследуется от NSObject . Мы реализуем метод readFiles , который будет методом типа . Методы типа позволяют вам вызывать метод самого класса, в отличие от экземпляра класса. Ниже приведена реализация метода readFiles .

1
2
3
class func readFiles() -> [String] {
    return NSBundle.mainBundle().pathsForResourcesOfType(«mp3», inDirectory: nil) as!
}

Основной пакет содержит код и ресурсы для вашего проекта, и именно здесь мы найдем MP3. Мы используем метод pathsForResourcesOfType(_:inDirectory:) , который возвращает массив, содержащий пути для указанного типа ресурса. В этом случае мы ищем тип "mp3" . Поскольку нас не интересует конкретный каталог, мы передаем nil .

Этот класс будет использоваться классом MP3Player , над которым мы будем работать дальше.

Далее откройте MP3Player.swift и просмотрите его содержимое.

1
2
3
4
5
6
import UIKit
import AVFoundation
 
class MP3Player: NSObject, AVAudioPlayerDelegate {
 
}

Обратите внимание, что мы принимаем протокол AVAudioPlayerDelegate . Этот протокол объявляет ряд полезных методов, одним из которых является audioPlayerDidFinishPlaying(_:successfully:) . audioPlayerDidFinishPlaying(_:successfully:) метод audioPlayerDidFinishPlaying(_:successfully:) , мы получим уведомление о завершении воспроизведения звука.

Добавьте следующее в MP3Player.swift .

1
2
3
4
5
class MP3Player: NSObject, AVAudioPlayerDelegate {
    var player:AVAudioPlayer?
    var currentTrackIndex = 0
    var tracks:[String] = [String]()
}

Свойство player будет экземпляром класса AVAudioPlayer , который мы будем использовать для воспроизведения, приостановки и остановки MP3. Переменная currentTrackIndex отслеживает, какой MP3 воспроизводится в данный момент. Наконец, переменная track будет массивом путей к списку MP3, включенных в комплект приложения.

1
2
3
4
5
override init(){
    tracks = FileReader.readFiles()
    super.init()
    queueTrack();
}

Во время инициализации мы readFiles метод readFiles чтобы readFiles пути к файлам MP3 и сохранить этот список в массиве tracks . Поскольку это назначенный инициализатор, мы должны вызвать метод init суперкласса. Наконец, мы вызываем queueTrack , о котором мы будем писать дальше.

Добавьте следующую реализацию для метода MP3Player класс MP3Player .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func queueTrack(){
    if (player != nil) {
        player = nil
    }
     
    var error:NSError?
    let url = NSURL.fileURLWithPath(tracks[currentTrackIndex] as String)
    player = AVAudioPlayer(contentsOfURL: url, error: &error)
         
    if let hasError = error {
        //SHOW ALERT OR SOMETHING
    } else {
        player?.delegate = self
        player?.prepareToPlay()
    }
}

Поскольку мы будем создавать новый экземпляр AVAudioPlayer каждый раз, когда вызывается этот метод, мы делаем небольшую уборку, устанавливая player в nil .

Мы объявляем необязательный NSError и постоянный url . Мы вызываем fileURLWithPath(_:) чтобы получить путь к текущему MP3 и сохранить значение в url . Мы передаем массив tracks в качестве параметра, используя currentTrackIndex в качестве индекса. Помните, что массив треков содержит пути к MP3, а не ссылку на сами файлы MP3.

Чтобы создать экземпляр player , мы передаем константу url и переменную error в инициализатор AVAudioPlayer . Если инициализация не удалась, переменная error заполняется описанием ошибки.

Если мы не сталкиваемся с ошибкой, мы устанавливаем делегат игрока на self и prepareToPlay метод prepareToPlay на плеере. Метод prepareToPlay предварительно загружает буферы и получает аудиооборудование, которое минимизирует любую задержку при вызове метода play .

Метод play сначала проверяет, play ли уже аудио, проверяя метко названное свойство playing . Если звук не воспроизводится, он вызывает метод play свойства player .

1
2
3
4
func play() {
    if player?.playing == false {
        player?.play()
}

Сначала метод stop проверяет, воспроизводится ли аудиоплеер. Если это так, он вызывает метод stop и устанавливает для свойства currentTime значение 0 . Когда вы вызываете метод stop , он просто останавливает звук. Он не сбрасывает звук обратно в начало, поэтому нам нужно сделать это вручную.

1
2
3
4
5
6
func stop(){
    if player?.playing == true {
        player?.stop()
        player?.currentTime = 0
    }
}

Так же, как и метод stop , мы сначала проверяем, воспроизводится ли аудиоплеер. Если это так, мы вызываем метод pause .

1
2
3
4
5
func pause(){
    if player?.playing == true{
        player?.pause()
    }
}

Метод nextSong(_:Bool) очередь следующую песню и, если проигрыватель играет, воспроизводит эту песню. Мы не хотим, чтобы следующая песня играла, если проигрыватель приостановлен. Однако этот метод также вызывается, когда заканчивается воспроизведение песни. В этом случае мы хотим воспроизвести следующую песню, для чего предназначен параметр songFinishedPlaying .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func nextSong(songFinishedPlaying:Bool){
    var playerWasPlaying = false
    if player?.playing == true {
        player?.stop()
        playerWasPlaying = true
    }
 
    currentTrackIndex++
     if currentTrackIndex >= tracks.count {
        currentTrackIndex = 0
    }
    queueTrack()
    if playerWasPlaying ||
        player?.play()
    }
}

Переменная playerWasPlaying используется, чтобы указать, играл ли игрок, когда этот метод был вызван. Если песня воспроизводилась, мы вызываем метод stop на player и устанавливаем для playerWasPlaying значение true .

Затем мы увеличиваем значение currentTrackIndex и проверяем, больше или равно tracks.count равно tracks.count . Свойство count массива дает нам общее количество элементов в массиве. Мы должны быть уверены, что не пытаемся получить доступ к элементу, который не существует в массиве track. Чтобы предотвратить это, мы устанавливаем currentTrackIndex обратно на первый элемент массива, если это так.

Наконец, мы вызываем queueTrack чтобы подготовить следующую песню и воспроизводить эту песню, если для playerWasPlaying или songFinishedPlaying true .

Метод previousSong работает очень похоже на nextSong . Разница лишь в том, что мы уменьшаем значение currentTrackIndex и проверяем, равно ли оно 0 . Если это так, мы устанавливаем его в индекс последнего элемента в массиве.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
func previousSong(){
    var playerWasPlaying = false
    if player?.playing == true {
        player?.stop()
        playerWasPlaying = true
    }
    currentTrackIndex—
    if currentTrackIndex < 0 {
        currentTrackIndex = tracks.count — 1
    }
         
    queueTrack()
    if playerWasPlaying {
        player?.play()
    }
}

Используя методы nextSong и previousSong , мы можем циклически перебирать все MP3 и начинать nextSong когда достигаем начала или конца списка.

Метод getCurrentTrackName возвращает имя MP3 без расширения.

1
2
3
4
func getCurrentTrackName() -> String {
    let trackName = tracks[currentTrackIndex].lastPathComponent.stringByDeletingPathExtension
    return trackName
}

Мы получаем ссылку на текущий MP3, используя tracks[currentTrackIndex] . Помните, однако, что это пути к MP3, а не сами файлы. Пути довольно длинные, потому что это полный путь к файлам MP3.

Например, на моей машине первый элемент массива track равен » / Users / jamestyner / Library / Developer / CoreSimulator / Devices / 80C8CD34-22AE-4F00-862E-FD41E2D8D6BA / data / Containers / Bundle / Application / 3BCF8543 -BA1B-4997-9777-7EC56B1C4348 / MP3Player.app / Lonesome Road Blues.mp3 «. Этот путь будет отличаться на реальном устройстве, конечно.

У нас есть большая строка, которая содержит путь к MP3, но мы просто хотим название самого MP3. Класс NSString определяет два свойства, которые могут нам помочь. Как видно из названия, свойство lastPathComponent возвращает последний компонент пути. Как вы уже догадались, свойство stringByDeletingPathExtension удаляет расширение.

Метод getCurrentTimeAsString использует свойство currentTime экземпляра player и возвращает его в виде удобочитаемой строки (например, 1:02) .

1
2
3
4
5
6
7
8
9
func getCurrentTimeAsString() -> String {
   var seconds = 0
   var minutes = 0
   if let time = player?.currentTime {
       seconds = Int(time) % 60
       minutes = (Int(time) / 60) % 60
   }
   return String(format: «%0.2d:%0.2d»,minutes,seconds)
  }

Свойство currentTime имеет тип NSTimeInterval , который является просто typealias для Double . Мы используем некоторую математику, чтобы получить seconds и minutes , убедившись, что мы конвертируем time в Int поскольку нам нужно работать с целыми числами Если вы не знакомы с оператором остатка (%), он находит остаток после деления одного числа на другое. Если бы переменная time была равна 65 , то seconds были бы равны 5, потому что мы используем 60 .

Метод getProgress используется экземпляром UIProgressView для указания количества воспроизведенного MP3. Этот прогресс представлен значением от 0,0 до 1,0 в виде числа с Float .

1
2
3
4
5
6
7
8
9
func getProgress()->Float{
    var theCurrentTime = 0.0
    var theCurrentDuration = 0.0
    if let currentTime = player?.currentTime, duration = player?.duration {
        theCurrentTime = currentTime
        theCurrentDuration = duration
    }
    return Float(theCurrentTime / theCurrentDuration)
}

Чтобы получить это значение, мы делим свойство currentTime player на свойство duration player , мы сохраняем эти значения в переменных theCurrentTime и theCurrentDuration . Как и currentTime , свойство duration имеет тип NSTimeInterval и представляет продолжительность песни в секундах.

Метод setVolume(_:Float) вызывает метод setVolume экземпляра player .

1
2
3
func setVolume(volume:Float){
    player?.volume = volume
}

Метод audioPlayerDidFinishPlaying(_:successfully:) — это метод протокола AVAudioPlayerDelegate . Этот метод принимает в качестве параметров экземпляр AVAudioPlayer и логическое значение. Логическое значение установлено на true если аудиоплеер завершил воспроизведение текущей песни.

1
2
3
4
5
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool){
    if flag == true {
        nextSong(true)
    }
}

Если песня успешно завершилась, мы вызываем метод nextSong , передавая значение true поскольку песня закончила играть самостоятельно.

Это завершает класс MP3Player . Мы ViewController к нему чуть позже, после реализации действий класса ViewController .

Откройте ViewController.swift и просмотрите его содержимое.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
mport UIKit
import AVFoundation
 
class ViewController: UIViewController {
    var mp3Player:MP3Player?
    var timer:NSTimer?
     
    @IBOutlet weak var trackName: UILabel!
    @IBOutlet weak var trackTime: UILabel!
    @IBOutlet weak var progressBar: UIProgressView!
     
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    @IBAction func playSong(sender: AnyObject) {
       
    }
    @IBAction func stopSong(sender: AnyObject) {
        
    }
     
    @IBAction func pauseSong(sender: AnyObject) {
         
    }
 
    @IBAction func playNextSong(sender: AnyObject) {
           }
     
     
    @IBAction func setVolume(sender: UISlider) {
        
    }
 
    @IBAction func playPreviousSong(sender: AnyObject) {
         
    }
     
     
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Переменная mp3Player является экземпляром класса MP3Player мы реализовали ранее. Переменная timer будет использоваться для обновления представлений trackTime и progressBar каждую секунду.

На следующих нескольких шагах мы реализуем действия класса ViewController . Но сначала мы должны создать экземпляр экземпляра MP3Player . Обновите реализацию метода viewDidLoad как показано ниже.

1
2
3
4
override func viewDidLoad() {
    super.viewDidLoad()
    mp3Player = MP3Player()
}

Введите следующее в метод playSong(_: AnyObject) .

1
2
3
@IBAction func playSong(sender: AnyObject) {
    mp3Player?.play()
}

В этом методе мы вызываем метод mp3Player объекта mp3Player . Сейчас мы можем приступить к тестированию приложения. Запустите приложение и нажмите кнопку воспроизведения. Песня должна начать играть.

Метод stopSong(_: AnyObject) вызывает метод mp3Player объекта mp3Player .

1
2
3
@IBAction func stopSong(sender: AnyObject) {
    mp3Player?.stop()
}

Запустите приложение еще раз и нажмите кнопку воспроизведения. Теперь вы сможете остановить песню, нажав кнопку остановки.

Как вы уже догадались, метод pauseSong(_: AnyObject) вызывает метод mp3Player объекта mp3Player .

1
2
3
@IBAction func pauseSong(sender: AnyObject) {
    mp3Player?.pause()
}
1
2
3
IBAction func playNextSong(sender: AnyObject) {
    mp3Player?.nextSong(false)
}

В playNextSong(_: AnyObject) мы nextSong метод mp3player объекта mp3player . Обратите внимание, что мы передаем false как параметр, потому что песня не закончила играть самостоятельно. Мы вручную запускаем следующую песню нажатием следующей кнопки.

1
2
3
@IBAction func playPreviousSong(sender: AnyObject) {
    mp3Player?.previousSong()
}

Как видите, реализация метода previousSong(_: AnyObject) очень похожа на реализацию nextSong(_: AnyObject) . Все кнопки MP3-плеера должны быть исправны. Если вы еще не тестировали приложение, сейчас самое время убедиться, что все работает как положено.

Метод setVolume(_: UISlider) вызывает метод mp3Player объекта mp3Player . Свойство тома имеет тип Float . Значение варьируется от 0,0 до 1,0 . Для объекта UISlider установлено значение 0.0 в качестве минимального значения и 1.0 в качестве максимального значения.

1
2
3
@IBAction func setVolume(sender: UISlider) {
    mp3Player?.setVolume(sender.value)
}

Запустите приложение еще раз и поиграйте с ползунком громкости, чтобы проверить, что все работает правильно.

Метод startTimer создает новый экземпляр NSTimer .

1
2
3
func startTimer(){
    timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector(«updateViewsWithTimer:»), userInfo: nil, repeats: true)
}

Параметр scheduledTimerWithTimeInterval(_:target:selector:userInfo:repeats:) инициализатор принимает в качестве параметров количество секунд между срабатыванием таймера, объект, для которого вызывается метод, указанный selector , метод, который вызывается при срабатывании таймера , необязательный словарь userInfo , и повторяется ли таймер, пока он не станет недействительным.

Мы используем метод с именем updateViewsWithTimer(_: NSTimer) в качестве селектора, поэтому мы создадим его следующим.

Метод updateViewsWithTimer(_: NSTimer) вызывает метод updateViews , который мы реализуем на следующем шаге.

1
2
3
func updateViewsWithTimer(theTimer: NSTimer){
    updateViews()
}

Метод updateViews обновляет представления trackTime и progressBar .

1
2
3
4
5
6
func updateViews(){
    trackTime.text = mp3Player?.getCurrentTimeAsString()
    if let progress = mp3Player?.getProgress() {
        progressBar.progress = progress
    }
}

Свойство text trackTime обновляется свойством currentTime , отформатированным в виде строки с getCurrentTimeAsString метода getCurrentTimeAsString . Мы объявляем постоянный progress используя метод mp3Player getProgress , и устанавливаем progressBar.progress используя эту константу.

Теперь нам нужно вызвать метод startTimer в соответствующих местах. Нам нужно запустить таймер в playSong(_: AnyObject) методе playNextSong(_ :AnyObject) методе playPreviousSong(_ :AnyObject) .

1
2
3
4
@IBAction func playSong(sender: AnyObject) {
    mp3Player?.play()
    startTimer()
}
1
2
3
4
@IBAction func playNextSong(sender: AnyObject) {
    mp3Player?.nextSong(false)
    startTimer()
}
1
2
3
4
@IBAction func playPreviousSong(sender: AnyObject) {
    mp3Player?.previousSong()
    startTimer()
}

Нам также нужно остановить timer при нажатии кнопок паузы и остановки. Вы можете остановить объект timer , вызвав метод NSTimer экземпляре NSTimer .

1
2
3
4
5
@IBAction func stopSong(sender: AnyObject) {
    mp3Player?.stop()
    updateViews()
    timer?.invalidate()
}
1
2
3
4
@IBAction func pauseSong(sender: AnyObject) {
    mp3Player?.pause()
    timer?.invalidate()
}

Метод setTrackName устанавливает text свойство trackName , вызывая getCurrentTrackName mp3Player объекта mp3Player .

1
2
3
func setTrackName(){
    trackName.text = mp3Player?.getCurrentTrackName()
}

Когда песня завершает воспроизведение, она должна автоматически показать название следующей песни и начать воспроизведение этой песни. Кроме того, когда пользователь нажимает кнопки воспроизведения, setTrackName или назад, должен вызываться метод setTrackName . Идеальным местом для этого является метод MP3Player класса MP3Player .

Нам нужен способ, чтобы класс ViewController классу ViewController о вызове метода setTrackName . Для этого мы будем использовать класс NSNotificationCenter . Центр уведомлений предоставляет возможность транслировать информацию по всей программе. Регистрируясь в качестве наблюдателя в центре уведомлений, объект может получать эти трансляции и выполнять операцию. Другим способом решения этой задачи было бы использование шаблона делегирования .

Добавьте следующий метод в класс ViewController .

1
2
3
4
5
6
func setupNotificationCenter(){
    NSNotificationCenter.defaultCenter().addObserver(self,
        selector:»setTrackName»,
        name:»SetTrackNameText»,
        object:nil)
}

Сначала мы получаем ссылку на центр уведомлений по умолчанию. Затем мы вызываем метод addObserver(_:selector:name:object:) в центре уведомлений. Этот метод принимает четыре параметра:

  • объект, регистрирующийся как наблюдатель, self в этом случае
  • сообщение, которое будет отправлено наблюдателю при публикации уведомления
  • название уведомления, для которого необходимо зарегистрировать наблюдателя
  • объект, чьи уведомления хочет получить наблюдатель

Передавая nil в качестве последнего аргумента, мы прослушиваем каждое уведомление с именем SetTrackNameText .

Теперь нам нужно вызвать этот метод в методе viewDidLoad контроллера представления.

1
2
3
4
5
override func viewDidLoad() {
    super.viewDidLoad()
    mp3Player = MP3Player()
    setupNotificationCenter()
}

Чтобы опубликовать уведомление, мы вызываем метод postNotificationName(_:object:) в центре уведомлений по умолчанию. Как я упоминал ранее, мы сделаем это в методе MP3Player класса MP3Player . Откройте MP3Player.swift и обновите метод queueTrack как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
func queueTrack(){
   if(player != nil){
       player = nil
   }
        
   var error:NSError?
   let url = NSURL.fileURLWithPath(tracks[currentTrackIndex] as String)
   player = AVAudioPlayer(contentsOfURL: url, error: &error)
        
   if let hasError = error {
       //SHOW ALERT OR SOMETHING
   } else {
       player?.delegate = self
       player?.prepareToPlay()
       NSNotificationCenter.defaultCenter().postNotificationName(«SetTrackNameText», object: nil)
   }
}

Если вы сейчас протестируете приложение и дадите песне проигрываться до конца, оно должно автоматически начать воспроизведение следующей песни. Но вам может быть интересно, почему название песни не появляется во время первой песни. Метод init класса MP3Player вызывает метод queueTrack , но, поскольку он не завершил инициализацию, он не оказывает влияния.

Все, что нам нужно сделать, это вручную вызвать метод setTrackName после того, как мы инициализируем объект mp3Player . Добавьте следующий код в метод viewDidLoad в ViewController.swift .

1
2
3
4
5
6
7
override func viewDidLoad() {
    super.viewDidLoad()
    mp3Player = MP3Player()
    setupNotificationCenter()
    setTrackName()
    updateViews()
}

Вы заметите, что я также вызвал метод updateViews . Таким образом, игрок показывает время 0:00 в начале. Если вы сейчас тестируете приложение, у вас должен быть полнофункциональный MP3-плеер.

Это было довольно длинное учебное пособие, но теперь у вас есть функциональный MP3-плеер для создания и расширения. Одно из предложений — позволить пользователю выбрать песню для воспроизведения, реализовав UITableView под проигрывателем. Спасибо за чтение, и я надеюсь, что вы узнали что-то полезное.