Search code examples
iosaudiokit

AudioKit AKPlayer can not loopback to beginning with setPosition


In v4, If you use AKPlayer with looping and use setPosition API, the player loops between the position set with setPosition and the players total duration. What I needed was to start from the position set by setPosition API and loop to the beginning when looping occurs. I am not sure if this is intentional or not but I need to be able to seek to a certain point in the Player and loop to the beginning and for now it seems that this is not a feature in AudioKit. Is there a way or workaround to achieve this? I need to accomplish this as a requirement. To reproduce here is an full playground example:

import AudioKitPlaygrounds
import AudioKit

let file = try AKAudioFile(readFileName: "3.aac")

//: Set up a player to the loop the file's playback
var player = AKPlayer(audioFile: file)
player.isLooping = true
player.buffering = .always

AKManager.output = player
try AKManager.start()


player.setPosition(player.duration - 2)
player.play(at: AVAudioTime.now())
// player loops between player.duration - 2, player.duration  forever. I want it to loop between 0, player.duration

import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

Expected behavior I expected it to loop back to the beginning.

Details:

  • AudioKit Version 4.11.2

Solution

  • If you intend to develop some kind of looper application, I think the approach is to use a sequencer to coordinate the playback of your audio files. At each position where a sound should start playing, you add an event to the sequence.

    The sequencer can be connected to a callback instrument, which is a node that passes the events to an user-defined function. In your callback function, you would start playback of the audio file indicated by that event.

    This is an outline of what should be done:

    1. Create a track to contain the playback events, using AKSequencer's addTrack method. Connect this track to an AKCallbackInstrument. Please see this answer on how to connect an AKCallbackInstrument to an AKSequencer track. Set the sequencer's and the track's length to the total duration of the song (notice these properties are represented in beats instead of seconds). Set the sequencer's loopEnabled as desired.
    2. Add the playback events to the track, at the time positions where you want each sound to play. As you will be interpreting the events yourself with a callback function, it doesn't really matter what type of event you use. You could simply use a Note On.
    3. In the callback function, start playing the corresponding sound when that event is received.

    You would need a couple of classes to represent some basic information about where each sample occurs in the song:

    class Beat {
        var sample: Sample!
        var onsetTime: Double
    
        var endTime {
            get {
                return onsetTime + sample.duration
            }
        }
    }
    
    class Sample {
        var url: URL!
        var duration: Double
    }
    
    class ViewController: UIViewController {
        var samples: [Sample] = []
        var beats: [Beat] = []
    
        var player: AKPlayer!
        var sequencer: AKSequencer!
    }
    

    On the note’s pitch you would store the index of the sample you want to play. This is what your callback function would look like:

    func playCallback(status:UInt8, note:MIDINoteNumber, vel:MIDIVelocity) -> () {
        guard let status = AKMIDIStatus(byte: status),
            let type = status.type,
            type == .noteOn else { return }
        DispatchQueue.main.async {
            player.load(samples[note].url)
            player.play()
        }
    }
    

    You can use AKSequencer’s seek method to start playing your song at some arbitrary position.

    There is a special case where the sequencer starts to play in the middle of a beat. In that case, you must manually start the playback of that sample.

    func play(at time: Double = 0) {
        sequencer.seek(time)
        sequencer.play()
        for beat in beats {
            if beat.onsetTime < time && time < beat.endTime {
                player.load(beat.sample.url)
                player.play(from: time - beat.onsetTime)
            }
        }
    }