Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strictly scheduled loop timing in Swift

What is the best way to schedule a repeated task with very strict timing (accurate and reliable enough for musical sequencing)? From the Apple docs, it is clear that NSTimer is not reliable in this sense (i.e., "A timer is not a real-time mechanism"). An approach that I borrowed from AudioKit's AKPlaygroundLoop seems consistent within about 4ms (if not quite accurate), and might be feasible:

class JHLoop: NSObject{
    var trigger: Int {
        return Int(60 * duration)  // 60fps * t in seconds
    }
    var counter: Int = 0
    var duration: Double  = 1.0 // in seconds, but actual loop is ~1.017s
    var displayLink: CADisplayLink?
    weak var delegate: JHLoopDelegate?

    init(dur: Double) {
        duration = dur
    }

    func stopLoop() {
        displayLink?.invalidate()
    }

    func startLoop() {
        counter = 0
        displayLink = CADisplayLink(target: self, selector: "update")
        displayLink?.frameInterval = 1
        displayLink?.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
    }

    func update() {
        if counter < trigger {
            counter++
        } else {
            counter = 0

            // execute loop here
            NSLog("loop executed")
            delegate!.loopBody()
        }
    }
}
protocol JHLoopDelegate: class {
    func loopBody()
}    

↑ Replaced code with the actual class I will try to use for the time being.

For reference, I am hoping to make a polyrhythmic drum sequencer, so consistency is most important. I will also need to be able to smoothly modify the loop, and ideally the looping period, in real time.

Is there a better way to do this?

like image 888
c_booth Avatar asked Oct 19 '22 14:10

c_booth


1 Answers

You can try to use mach_wait_until() api. It’s pretty good for high precision timer. I changed apple example from here a little. It works fine in mine command line tool project. In below code snippet I changed main() method from my project to startLoop(). Also you can see this. Hope it helps.

static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
static mach_timebase_info_data_t timebase_info;

static uint64_t nanos_to_abs(uint64_t nanos) {
    return nanos * timebase_info.denom / timebase_info.numer;
}

func startLoop() {
        while(true) { // 
            int64_t nanosec = waitSomeTime(1000); // each second
            NSLog(@"%lld", nanosec);
        update() // call needed update here 
        }
}

uint64_t waitSomeTime(int64_t eachMillisec) {
    uint64_t        start;
    uint64_t        end;
    uint64_t        elapsed;
    uint64_t        elapsedNano;

    if ( timebase_info.denom == 0 ) {
        (void) mach_timebase_info(&timebase_info);
    }

    // Start the clock.
    start = mach_absolute_time();

    mach_wait_until(start + nanos_to_abs(eachMillisec * NANOS_PER_MILLISEC));

    // Stop the clock.
    end = mach_absolute_time();

    // Calculate the duration.
    elapsed = end - start;
    elapsedNano = elapsed * timebase_info.numer / timebase_info.denom;

    return elapsedNano;
}
like image 153
Denis Vert Avatar answered Oct 21 '22 04:10

Denis Vert