swiftaudiokitarc4randomaksequencer

Calling random AKPlayers in AKSequencer with repeated random results


This is a multiple audiofile playback project I am currently working on, where multiple AKPlayers are played in a random order, through AKSequencer.

First, I have an array of filenames:

Let filenames = [“1.mp3”, “2.mp3, “3.mp3”, … “10.mp3”]

This is loaded on AKPlayer individually, to later call it in a random order:

Let players: [AKPlayer] = {
    Do {
        Let filenames = [“1.mp3”, “2.mp3, “3.mp3”, … “10.mp3”]
        Return try filenames.map { AKPlayer(audioFile: try AKAudioFile(readRileName: $0)) }
    } catch {
        fatalError()
    }
}()

Then, I called AKPlayer through AKsequencer, by triggering it through ‘playRandom’ function:

Let sequencer = AKSequencer()
Let callbackInst = AKCallbackInstrument()

func playRandom() {
    let playerIndex = Int(arc4random_uniform(UInt32(players.count)))
    players[playerIndex].play()
}

func addTracks() {
    let track = sequencer.newTrack()!
    track.add(noteNumber: 48, velocity: 127, position: AKDuration(beats: 0), duration: AKDuration(beats: 16), channel: 0)
    track.setMIDIOutput(callbackInst.midiIn)
    callbackInst.callback = { status, note, vel in
        guard status == .noteOn else { return }
        self.playRandom()
        }
    }

Lastly, I set AKPlayer as AudioKit.output, and started the sequencer. So far this was successful! The sequencer plays AKPlayer seamlessly, in a random order.

But I wanted to try different kind of randomness: repeating randomly selected player 2 or 3 times. (like 2, 2, 3, 3, 3, 1, 1, 5, 5, 5, 9, 9, …) Right now, ‘playRandom’ simply chooses different AKPlayer on each repeat.

As one solution, thanks to StackOverFlow masters, I tried something like:

class RandomWithRepeats {
var range: ClosedRange<Int>
var repeatRange: ClosedRange<Int>
var repeatCount = 0
var value = 0

init(range: ClosedRange<Int>, repeatRange: ClosedRange<Int>) {
    self.range = range
    self.repeatRange = repeatRange
}

// generate a random number in a range
// Just use Int.random(in:) with Swift 4.2 and later
func random(in range: ClosedRange<Int>) -> Int {
    return Int(arc4random_uniform(UInt32(range.upperBound - range.lowerBound + 1))) + range.lowerBound
}

func nextValue() -> Int {
    // if repeatCount is 0, its time to generate a new value and
    // a new repeatCount
    if repeatCount == 0 {
        // For Swift 4.2, just use Int.random(in:) instead
        value = self.random(in: range)
        repeatCount = self.random(in: repeatRange)
    }

    repeatCount -= 1
    return value
}
}

And then I modified playRandom function like:

func playRandom() {
    Let rand = randomWithRepeats(range: 1…players.count, repeatRange: 2…3)
    Do { players[rand.nextValue()].play() }
}

Turns out, this is the exact same result because the playRandom function itself triggered (by AKSequencer) each repeat, so it doesn’t actually ‘repeat’ random AKPlayer 2 or 3 times. Can I solve this issue in a different way? Much appreciated. <3


Solution

  • The job of the RandomWithRepeats class is to keep track of when to deliver you a different AKPlayer instance, but you are creating a new instance of RandomWithRepeats each time you call playRandom(), so it can't keep track of anything.

    Create a single instance of RandomWithRepeats when you create the array of players and use this instance in your playRandom() method.

    let rand = RandomWithRepeats(range: 0 ... players.count, repeatRange: 2 ... 3)
    
    func playRandom() {
         try? players[rand.nextValue()].play() 
    }
    

    For a cleaner solution, you might encapsulate the logic of RandomWithRepeats together with the AKPlayer array into a single class, such as a RandomlyRepeatingPlayer class.