How I Built an Audio Player App  –  Part 3

Sep 4, 2017

Category

Author

Other posts in this series

Post 1 – How I Built an Audio Player App — Part 1

Post 2 – How I Built an Audio Player App — Part 2

Intro

I recently worked on a small music player app for Churches where they can play, control, and modulate different components of a song. Components being Drums, Vocals, Bass, Guitar etc.. Moreover, they can change Pitch as well as BPM (beats per minute – rate). This is way too many steps ahead of a simple audio player app where you only play one song, with no control on its components, no BPM change, no pitch change.

Adding modulation for components

Now we gonna add modulation for components like drums, guitar, vocals etc. For that, we have to have those components separately available as MP3 files. Note: you can’t split a song into the components it is made of. It is like a cake, you can’t get its ingredients from a baked cake. So, you need to have all MP3 files for every component and you have to play all of them together to create your music.

I will be adding few component nodes here in our AudioEngine:

fileprivate var acousticNode = AVAudioPlayerNode()
fileprivate var bassNode = AVAudioPlayerNode()
fileprivate var drumsNode = AVAudioPlayerNode()

class AudioManager: NSObject {
...
  override init() {
    let format = engine.inputNode!.inputFormat(forBus: 0)

    engine.attach(acousticNode)
    engine.attach(bassNode)
    engine.attach(drumsNode)

    engine.connect(acousticNode, to: acousticPitch, format: format)
    engine.connect(bassNode, to: bassPitch, format: format)
    engine.connect(drumsNode, to: drumsPitch, format: format)
  }
...
}

engine.attach(bassNode)

func play() {
  acousticFile = try! AVAudioFile(forReading: acousticURL)
  bassFile = try! AVAudioFile(forReading: bassURL)
  drumsFile = try! AVAudioFile(forReading: drumsURL)

  acousticNode.scheduleFile(acousticFile, 
                            at: nil, 
                            completionHandler: nil)
  bassNode.scheduleFile(bassFile, 
                        at: nil, 
                        completionHandler: nil)
  drumsNode.scheduleFile(drumsFile, 
                         at: nil, 
                         completionHandler: nil)

  acousticNode.play()
  bassNode.play()
  drumsNode.play()
...
}

Since we have multiple MP3s playing at the same time, for pause and resume actions, we have to perform it for all the nodes.

func pause() {
  acousticNode.pause()
  bassNode.pause()
  drumsNode.pause()
}

func resume() {
  acousticNode.resume()
  bassNode.resume()
  drumsNode.resume()
}

Controlling each component

I created an enum to simplify things a little in AudioManager. Since the components of an audio are fixed, it makes sense to have that type check in the code.

enum Component {
  case acoustic
  case bass
  case drums
  ...
}

This cleans up our code for volume control:

func set(volume value: Float, for component: Component) {
  switch component:
  case .acoustic:
    acousticNode.volume = value
  case .bass:
    bassNode.volume = value
  case .drums:
    drums.volume = value
  ...
}

Wrapping up: We now have a working Audio Manager to play our song components, modulate them individually, play/pause, scrub. This ends this Audio Player series of posts.

LEAVE A COMMENT