3

I'm trying to make a simple game with a hit sound that has a different pitch whenever you hit something. I thought it'd be simple, but it ended up with a whole lot of stuff (most of which I completely copied from someone else):

func hitSound(value: Float) {  
 
    let audioPlayerNode = AVAudioPlayerNode()  
 
     audioPlayerNode.stop()  
     engine.stop() // This is an AVAudioEngine defined previously  
     engine.reset()  
 
     engine.attach(audioPlayerNode)  
 
     let changeAudioUnitTime = AVAudioUnitTimePitch()  
     changeAudioUnitTime.pitch = value  
 
     engine.attach(changeAudioUnitTime)  
     engine.connect(audioPlayerNode, to: changeAudioUnitTime, format: nil)  
     engine.connect(changeAudioUnitTime, to: engine.outputNode, format: nil)  
     audioPlayerNode.scheduleFile(file, at: nil, completionHandler: nil) // File is an AVAudioFile defined previously  
     try? engine.start()  
 
     audioPlayerNode.play()  
 }  

Since this code seems to stop playing any sounds currently being played in order to play the new sound, is there a way I can alter this behaviour so it doesn't stop playing anything? I tried removing the engine.stop and engine.reset bits, but this just crashes the app. Also, this code is incredibly slow when called frequently. Is there something I could do to speed it up? This hit sound is needed very frequently.

MysteryPancake
  • 1,365
  • 1
  • 18
  • 47
  • Sorry to anyone reading this if I sound like a dick who knows nothing about code and is just demanding others to do stuff, I'm very new to this and I haven't even touched any audio functions yet so I'm just poking around trying to figure out what's what. Swift seems quite similar to Lua so far, and I've done some other working stuff, I'm just stuck with this audio problem. – MysteryPancake Oct 06 '16 at 10:56

1 Answers1

6

You're resetting the engine every time you play a sound! And you're creating extra player nodes - it's actually much simpler than that if you only want one instance of the pitch shifted sound playing at once:

// instance variables
let engine = AVAudioEngine()
let audioPlayerNode = AVAudioPlayerNode()
let changeAudioUnitTime = AVAudioUnitTimePitch()

call setupAudioEngine() once:

func setupAudioEngine() {
    engine.attach(self.audioPlayerNode)

    engine.attach(changeAudioUnitTime)
    engine.connect(audioPlayerNode, to: changeAudioUnitTime, format: nil)
    engine.connect(changeAudioUnitTime, to: engine.outputNode, format: nil)
    try? engine.start()
    audioPlayerNode.play()
}

and call hitSound() as many times as you like:

func hitSound(value: Float) {
    changeAudioUnitTime.pitch = value

    audioPlayerNode.scheduleFile(file, at: nil, completionHandler: nil) // File is an AVAudioFile defined previously
}

p.s. pitch can be shifted two octaves up or down, for a range of 4 octaves, and lies in the numerical range of [-2400, 2400], having the unit "cents".

p.p.s AVAudioUnitTimePitch is very cool technology. We definitely didn't have anything like it when I was a kid.

UPDATE

If you want multi channel, you can easily set up multiple player and pitch nodes, however you must choose the number of channels before you start the engine. Here's how you'd do two (it's easy to extend to n instances, and you'll probably want to choose your own method of choosing which channel to interrupt when all are playing):

// instance variables
let engine = AVAudioEngine()
var nextPlayerIndex = 0
let audioPlayers = [AVAudioPlayerNode(), AVAudioPlayerNode()]
let pitchUnits = [AVAudioUnitTimePitch(), AVAudioUnitTimePitch()]

func setupAudioEngine() {
    var i = 0
    for playerNode in audioPlayers {
        let pitchUnit = pitchUnits[i]

        engine.attach(playerNode)
        engine.attach(pitchUnit)
        engine.connect(playerNode, to: pitchUnit, format: nil)
        engine.connect(pitchUnit, to:engine.mainMixerNode, format: nil)

        i += 1
    }

    try? engine.start()

    for playerNode in audioPlayers {
        playerNode.play()
    }
}

func hitSound(value: Float) {
    let playerNode = audioPlayers[nextPlayerIndex]
    let pitchUnit = pitchUnits[nextPlayerIndex]

    pitchUnit.pitch = value

    // interrupt playing sound if you have to
    if playerNode.isPlaying {
        playerNode.stop()
        playerNode.play()
    }

    playerNode.scheduleFile(file, at: nil, completionHandler: nil) // File is an AVAudioFile defined previously

    nextPlayerIndex = (nextPlayerIndex + 1) % audioPlayers.count
}
Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
  • Thanks for replying, that makes a whole lot more sense! I'll try that out, I was really confused as to why it was so laggy as many games are able to do far more complicated stuff. – MysteryPancake Oct 08 '16 at 01:36
  • Okay, I tried your version out and it certainly works far faster, but I'm looking for a way to play more than one instance of the sound at once at a different pitch, if possible. Sorry to annoy you again, but currently the sounds play out of time since they all play after eachother – MysteryPancake Oct 08 '16 at 03:20
  • Thanks very much for explaining. One last question though- currently I'm trying this to stop the currently playing channel in order to play the sound again: `let playerNode = audioPlayers[nextPlayerIndex] if playerNode.isPlaying { playerNode.stop() playerNode.play()}` It works, but it sometimes has a bit of an odd cutoff sound, and it seems a bit slow to me. Am I doing something wrong again? – MysteryPancake Oct 08 '16 at 05:53
  • Yep, I'm doing something wrong. The isPlaying thing always returns true no matter if an audio file is actually playing or not, so it resets every single time. Surprisingly it's quite slow to reset. I'll try using the completion handler of the scheduleFile part to do something or other – MysteryPancake Oct 08 '16 at 06:10
  • Hopefully this will be the last thing I'll ever bother you with - what way would you do to interrupt a channel, just for example? I'm surprised stopping and starting a node is so slow, so is there an alternative I overlooked? I checked all the Apple documentation for the node and the only other thing I saw was resetting it, but this seems to do nothing that you can't do by stopping the node. – MysteryPancake Oct 08 '16 at 14:04
  • stopping and starting the player node can do this, see the updated answer – Rhythmic Fistman Oct 08 '16 at 22:28
  • I tried stopping and starting the node, but as I said before it gives a cutoff sound sometimes and is also incredibly laggy when called frequently. I thought of a simpler alternative - just not playing the sound at all if the node is already playing - but I can't even do this one as the isPlaying function always seems to return true. – MysteryPancake Oct 09 '16 at 02:07
  • Weird. Mine works well. Can you link to the sound you're playing? – Rhythmic Fistman Oct 09 '16 at 02:09
  • Sure. https://www.dropbox.com/s/gmw66pljgvji6t7/Blop.wav?raw=1 Also, I'm testing this on an iPad 2 if that makes any difference. The problem is that since I have multitouch enabled on my app and the user can literally hit about 10 blocks in less than a second, things start to get a little slow. Honestly, before when I was using AudioServicesPlaySystemSound I thought it worked better. I'll probably end up using that if this is too laggy – MysteryPancake Oct 09 '16 at 02:26
  • I think I'll make a new thread since my original issue is now solved – MysteryPancake Oct 14 '16 at 07:08
  • How to save new audio(effect added)? – Bhavin Ramani May 31 '19 at 12:49