Thursday, December 18, 2014

How would you add reverb/echo to audio using Swift

Since an echo is simply a reflected copy of our sound, it is easy to simulate.
We can play a delayed copy of our sound with a smaller amplitude.
For reverb, we will simply repeat the process for the echoes.

In swift, an AVAudioPlayer instance has the playAtTime method, which allows us to schedule sound for future play. Exactly what we need for making echoes and reverbs!

The first thing we need to do is load an mp3 to check things are working. Then, we can add effect functions.

Since I am testing the code in an iPhone app, I will get the sound files and initialize the players in the viewDidLoad method, I will also hook up a button to call the playEcho method.

Here is the code:
class PlaySoundsViewController: UIViewController {
    var audioPlayer:AVAudioPlayer!
    var audioPlayer2:AVAudioPlayer!
    override func viewDidLoad() {
        super.viewDidLoad()
        let bundle = NSBundle.mainBundle()
        let mp3Path = bundle.pathForResource("soundFileName",
                                              ofType: "mp3")
        var mp3URL = NSURL(fileURLWithPath: mp3Path!)

        audioPlayer = AVAudioPlayer(contentsOfURL: mp3URL,      
                                    fileTypeHint: "mp3",
                                    error: nil)
       
        audioPlayer2 = AVAudioPlayer(contentsOfURL: mp3URL,
                                     fileTypeHint: "mp3",
                                     error: nil)
    }
   
    /*
     hook this up to a button that will trigger 
     the echo in the UI 
    */
    @IBAction func playEcho(sender: UIButton) {
        audioPlayer.stop()
        audioPlayer.currentTime = 0;
        audioPlayer.play()
        
        
        let delay:NSTimeInterval = 0.1//100ms
        var playtime:NSTimeInterval 
        playtime = audioPlayer2.deviceCurrentTime + delay
        audioPlayer2.stop()
        audioPlayer2.currentTime = 0
        audioPlayer2.volume = 0.8;
        audioPlayer2.playAtTime(playtime)
    }
}


On to reverbs...

The procedure is almost the same, but now we have more players, different delays, and different volume levels. To deal with this, we can use an array of players. Each player will play its copy of the sound 20ms after the previous one starts.
[player0, player1, player2, ..., playerN]
    |        |        |             |
    0       20ms     40ms         20ms*N


We can use an exponential decay in the volume for each delayed signal. The decaying amplitude will make the later echoes less pronounced. This also reflects the exponential decay multiple echoes would have.
    var reverbPlayers:[AVAudioPlayer] = []
    let N:Int = 10

    //initialize reverbPlayers in viewDidLoad

    @IBAction func playReverb(sender: UIButton) {
        /*
         20ms produces detectable delays
        */
        let delay:NSTimeInterval = 0.02
        for i in 0...N {
            var curDelay:NSTimeInterval = delay*NSTimeInterval(i)
            var player:AVAudioPlayer = reverbPlayers[i]
            //M_E is e=2.718...
            //dividing N by 2 made it sound ok for the case N=10
            var exponent:Double = -Double(i)/Double(N/2)
            var volume = Float(pow(Double(M_E), exponent))
            player.volume = volume
            player.playAtTime(player.deviceCurrentTime + curDelay)
        }
    }
Full code:
import UIKit
import AVFoundation

class PlaySoundsViewController: UIViewController {
    var audioPlayer:AVAudioPlayer!
    var audioPlayer2:AVAudioPlayer!
    var reverbPlayers:[AVAudioPlayer] = []
    let N:Int = 10

    override func viewDidLoad() {
        super.viewDidLoad()
        let bundle = NSBundle.mainBundle()
        let mp3Path = bundle.pathForResource("fileName",
                                             ofType: "mp3")
        
        var mp3URL = NSURL(fileURLWithPath: mp3Path!)
        
        audioPlayer = AVAudioPlayer(contentsOfURL: mp3URL,
                                    fileTypeHint: "mp3",
                                    error: nil)
                
        audioPlayer2 = AVAudioPlayer(contentsOfURL: mp3URL,
                                     fileTypeHint: "mp3",
                                     error: nil) 
        for i in 0...N {
            var temp = AVAudioPlayer(contentsOfURL: mp3URL,
                                     fileTypeHint: "mp3",
                                     error: nil) 
            reverbPlayers.append(temp)
        }
    }
    
    @IBAction func playEcho(sender: UIButton) {
        audioPlayer.stop()
        audioPlayer.currentTime = 0;
        audioPlayer.play()
        
        let delay:NSTimeInterval = 0.1//100ms
        var playtime:NSTimeInterval
        playtime = audioPlayer2.deviceCurrentTime + delay
        audioPlayer2.stop()
        audioPlayer2.currentTime = 0
        audioPlayer2.volume = 0.8;
        audioPlayer2.playAtTime(playtime)
    }
    
    @IBAction func playReverb(sender: UIButton) {
        /*
         20ms produces detectable delays
        */
        let delay:NSTimeInterval = 0.02
        for i in 0...N {
            var curDelay:NSTimeInterval = delay*NSTimeInterval(i)
            var player:AVAudioPlayer = reverbPlayers[i]
            //M_E is e=2.718...
            //dividing N by 2 made it sound ok for the case N=10
            var exponent:Double = -Double(i)/Double(N/2)
            var volume = Float(pow(Double(M_E), exponent))
            player.volume = volume
            player.playAtTime(player.deviceCurrentTime + curDelay)
        }
    }
}

3 comments:

  1. This is great. Thank you so much for writing this blog. My name is Kunal and I teach the iOS course you took at Udacity.

    I love how you implemented echo. Here is another link that may help achieve reverb - https://developer.apple.com/library/prerelease/ios/documentation/AVFoundation/Reference/AVAudioUnitReverb_Class/index.html

    ReplyDelete
  2. Any way you could show how to do this in objective c?

    ReplyDelete