Search code examples
iosswiftavfoundationavaudioplayercore-motion

How to play a short audio file with CoreMotion and AVAudioPlayer in iOS?


I'm trying to play short sounds (1 to 4 seconds) when I move the iPhone on the X axis (I'm using CoreMotion and AVAudioPlayer). I want to play one sound for each movement direction change.

I wrote the code below, but when I move the iPhone, the sound is played many times without the movement direction change. The print calls below show many Down and Up such as Down Down Down Up Down Down Up Up.... If I comment both play callbacks, the print shows the alternation that I expect: Down Up Down Up Down Up....

Why the AVAudioPlayer.play is called more than one time when the movement direction changes?

override func viewDidLoad() {
    super.viewDidLoad()

    // Audio
    audioURL = NSBundle.mainBundle().URLForResource("shortSound", withExtension: "wav")!
    do {
        try audioPlayer = AVAudioPlayer(contentsOfURL: audioURL)
        audioPlayer.prepareToPlay()
    } catch {
        print("audioPlayer failure")
    }

    // Sensor
    lastDirection = 0
    threshold = 2.1

    motionManager = CMMotionManager()
    if motionManager.accelerometerAvailable {
        let queue = NSOperationQueue()
        motionManager.startAccelerometerUpdatesToQueue(queue, withHandler: {
            data, error in

            guard let data = data else{
                return
            }

            // Get the acceleration
            let xAccel = data.acceleration.x
            let xPositive = xAccel > 0

            // Run if the acceleration is higher than theshold
            if abs(xAccel) > self.threshold {

                // Run only if the direction is changed
                if self.lastDirection != 1 && xPositive {
                    print("Up")
                    self.play() 
                    self.lastDirection = 1
                } else if self.lastDirection != -1 && !xPositive {
                    print("Down")
                    self.play()
                    self.lastDirection = -1
                }
            }
        })
    }
}

func play() {
    audioPlayer.currentTime = 0
    audioPlayer.play()
}

Solution

  • You probably have a threading problem. You are running the updates on a background queue (queue, an arbitrary NSOperationQueue, which by the way you are also failing to retain), but then you are talking to self.lastDirection and calling self.play() on that same background queue without regard to the thread-safety of those activities.

    I would suggest at the very least rewriting this section:

                if self.lastDirection != 1 && xPositive {
                    print("Up")
                    self.play() 
                    self.lastDirection = 1
                } else if self.lastDirection != -1 && !xPositive {
                    print("Down")
                    self.play()
                    self.lastDirection = -1
                }
    

    ...more like this:

               dispatch_async(dispatch_get_main_queue()) {
                   if self.lastDirection != 1 && xPositive {
                        self.lastDirection = 1
                        print("Up")
                        self.play() 
                    } else if self.lastDirection != -1 && !xPositive {
                        self.lastDirection = -1
                        print("Down")
                        self.play()
                    }
                }
    

    Note that I've made two changes: I've stepped out to the main thread for the entire check-print-play-toggle dance, and I've reversed the order of events so that it goes check-toggle-print-play.

    Also I would suggest two other changes: retain the operation queue (i.e. make it a property instead of a local), and reduce the frequency of motion manager updates (by setting a lower accelerometerUpdateInterval).