My app runs on Mac only. It records and plays back MIDI data and also displays pages of scanned PDFs, where I’m turning pages by sliding pages using graphics. MIDI recording is very accurate because MIDI events arrive with timestamps. Playback is a different story. I’m using a timer (20ms) to check the time and send MIDI events with timestamps, looking ahead to send all events scheduled before the next timer action. This works well until the CPU gets bogged down with activity, such as scrolling through pages quickly. The timer actions get delayed and there are delays in playback.
I thought I could get better timing with Threads or Workers. I tried with Threads (even priority 10), but there are still delays (only one core). I tried to use a Worker, but found that my Lite version of Xojo won’t build Workers and since they use threads in the debugger, I can’t know for sure.
I only code for myself as a hobby so I would have to upgrade my Xojo Lite license to even find out if a Worker would solve the problem. Would a Worker run on another core and give me solid timing, regardless of the CPU drain from my scrolling? My timer action takes less than 0.2 ms.
Ideally, interrupts should be used to lock in the timing. Does such a thing exist in macOS? Mach scheduling? I know solid timing is possible because all of the professional DAWs playback solidly, even in the background. I just don’t know how they do it.
I’m assuming that you can send the events as far in advance as you want, and the playback device won’t play the event until its timestamp occurs.
If that’s true then maybe you can increase the lookahead period beyond the next timer action, like 3 times the timer period? That might create enough of a buffer of already sent but not yet executed events to smooth out timing irregularities.
Good idea. You’re right. I tried increasing the lookahead to as much as 10 seconds and it works great, although I have to take other steps to flush the unplayed notes if I pause and calculate where the pause would actually be in real time so resuming works correctly.
I thought this would solve my problem, but my app also allows one to choose the internal MidiPlaybackMBS synthesizer or any installed virtual instrument plugins as my destination. Unfortunately, these devices fire when the event is sent, without an option to schedule them (as far as I know).
I’m hoping to find a solid playback method for those options, too. Thanks for the suggestion.
A second thread is the answer, but it will need to be either a Worker or one of the new preemptive threads. You aren’t ever going to be able to guarantee that the main thread will have enough time to do what it needs to do with that much precision, because it is responsible for UI interactions - as you’ve seen, it didn’t take much to cause hiccups.
A cooperative thread won’t help because if the main thread is occupied with the UI, all secondary threads are paused.
1 Like
Hmm…I don’t think I tried a preemptive thread. I’ll take a look at that. Thanks.
I’m using a metronome click to test my timer integrity. I tried setting it to a preemptive thread, but it still is affected by my page scrolling.
Here is the thread’s Run code:
self.Type = thread.Types.Preemptive
var ClickThreadTimer As new Timer
AddHandler ClickThreadTimer.Action, AddressOf TimerAction
myBPM = BPM
'Send a click now. The timer action event contains these 2 lines
myClick.SendMidiEvent (&h99, 33 , MetronomeVolume, 0)
myClick.SendMidiEvent (&h99, 33 , 0, 0)
ClickThreadTimer.Period = 60000 / BPM
ClickThreadTimer.Mode = 2
ClickThreadTimer.Enabled = True
ClickThreadTimer.Reset
While True
app.DoEvents
if myBPM <> BPM then
myBPM = BPM
ClickThreadTimer.Period = 60000 / BPM
ClickThreadTimer.Mode = 2
ClickThreadTimer.Enabled = True
ClickThreadTimer.Reset
end
Wend
RemoveHandler ClickThreadTimer.Action, AddressOf TimerAction
Is there a problem in this code?
Timers always run on the main thread, so that’s no solution. I think you want the thread to run continuously with a SleepCurrent command to simulate a timer period.
And you should never, ever use App.DoEvents in a desktop app. Remove it from your vocabulary.
1 Like
OK, here is roughly what this would look like.
You’d have a class of Thread with type=Preemptive. You can do this in the Constructor:
me.Type=Thread.Type.Preemptive
The Run event of the Thread would have something like this in it:
do
'do something every second
DoSomething
'exit under certain condition
if someCondition then
stop
exit
end
'take a 1 second nap
sleep(1000)
loop
This code loops and does something, then checks to see if it should exit; if not, it sleeps for 1 second and then the loop repeats. This code runs reliably in the background even if the UI is completely occupied; for example, by dragging a window around constantly, or looking through the menus.
This should be enough for you to get a metronome working and hopefully you can adapt it for the rest of your app as well. I’ve attached a quickie little project that lets you spawn multiple preemptive threads that beep every second - press the “Spawn Beeper” button as many times as you like, and hold he Option key to stop them.
Preemptive Beeper.xojo_binary_project.zip (5.3 KB)
3 Likes
First, lol…I’ll heed your advice
Second, thanks for this. I can’t believe I’ve gotten away with using a timer for years. I tried the sleep routine with the metronome and it works great. I’ll revise my MIDI timing which will take a bit more work and see how it works.