MSP Externals: time drift?

macourteau's icon

Hi,

I'm working on an MSP external for Max 6. I'm using the dsp64 method (as opposed to the dsp method).

In my case, the samplerate is 44100 and I'm receiving one channel of audio (from ezadc~). The buffer size/"sampleframes? is 128 samples (as configured in Max).

I'm doing some computations that depend on the time elapsed, so I assume that each frame moves me 128 samples further in times, and that I should receive 44100/128=~344.53 frames/second. However, I receive only ~294 frames/second (the CPU usage reported by the Max Mixer is less than 5%).

Is that a known issue? Could I be doing something wrong? I've read the documentation and couldn't find any potential explanation for that.

FWIW, I'm on a Mac (running OS X 10.7.5 with all updates) with more than enough CPU power and RAM. I'm using CoreAudio driver with the built-in microphone.

Thanks in advance for any help!

Timothy Place's icon

Hi,

How are you arriving at the conclusion that your perform64 method isn't called frequently enough? Do you have some source code that is instrumented to demonstrate the problem?

Cheers,
Tim

macourteau's icon

Hi Tim,

I've tried to strip down my source code to the bare essentials to demonstrate this, but it seems to be accurate in that case. I still have a few other things to try though.

A few questions:
- Could the number/type of inlets/outlets affect performance? I doubt so, but we never know.
- Could calling outlet_* (e.g. outlet_bang) from within the perform64 method cause the dropping of audio frames? If so, is there a pattern generally used to send data to non-signal outlets from within perform64?
- Is the perform64 method generally scheduled to run e.g. 128/44100=~2.9ms _after_ it has returned, or (in my example) every ~2.9ms? In the former case, that could cause drift if the code in perform64 is computationally expensive, as opposed to the latter case where there would be no drift.

Thanks!

andrea agostini's icon

Hi

Yes, it definitely could, as sending stuff out your outlets causes all the subsequent evaluation chain to be activated, and you don't know how much time this will take - even if you don't have computationally heavy stuff in it, it will lock mutexes, allocate memory and things like that, and you don't want to do that in your audio processing thread!!! - have a look at this http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing

So, as Nicolas said, always use a qelem or a clock to output messages from your perform method.

hth
aa

macourteau's icon

Awesome, thanks for the replies, I'm sure it'll help. I'll definitely give that a try when I have a few minutes. The Max documentation wasn't very clear about that (well, either that, or I didn't see any explanation relative to that anywhere I looked).

If I understand correctly then, I should qelem_new a qelem in my *_new method (and qelem_free it in my *_free method), and call qelem_set whenever I want to e.g. output_bang, and call output_bang from the qelem task method that I passed to qelem_new?

Also, which one is most lightweight and/or has the lowest "latency" between qelems/clocks? I understand that I can schedule calls for later with a clock, but if I want to output_* immediately, are qelem's better than calling e.g. clock_fdelay(x, 0)?

Thanks for the help :)

andrea agostini's icon

Nicolas, you're not wrong afaik - it doesn't happen often that you are... ;)

... but maybe I would be careful with directly put a clock with zero delay inside my perform routine unless you're totally sure that the call won't happen too often, just because it would not be canceled by other overlapping calls and you might clog the scheduler. But of course you can implement your own mechanism to prevent this from happening... sort of, you don't schedule a new clock unless the previous one has been serviced...

again, hth
aa

macourteau's icon

Ok, thanks to both of you for your help, it should be very useful. I'll give that a try when I get home!

I don't expect to be sending messages this way more than 2-3 times per second so it shouldn't clog the scheduler... :)

macourteau's icon

Yep, that seems to have fixed the problem.

Thanks again for all the help, it's really appreciated.

Luigi Castelli's icon

sorry, at the risk of sounding pedantic, it is not a good idea to directly put a clock with zero delay in the perform routine.
There's no way that you can be sure that the call won't happen too often because the user could potentially choose a block size of 1 or 2 samples and in that case you *will* clog the scheduler. The only way it could be done is - as Andrea was suggesting - if you conditionally call the clock function only when the previous event has already been serviced. In order to do that you have to sort of decouple the timing at which you call the clock_delay() function from the rate at which the audio thread runs.
If accuracy is not a concern you can always use the qelem mechanism, which would make things easier because you don't run the risk of clogging the scheduler.

- Luigi

macourteau's icon

Good point, although in my case, I'm not scheduling clocks for every block, but rather only when I detect certain events in the signal (which I rate-limit in my code to ~3 per second). I will keep this in mind though, and will make sure that the previously-scheduled clock has run before I schedule another one (although I have to admit, in my case, if that were to really happen, I'd probably have other things to worry about first :)).

I'm also using qelem's (after learning about them here) for running expensive but much less timing-critical operations. The clocks events being scheduled (in this case) are only calling some outlet_* functions in their method.

Thanks for the tip!

Also, somehow related, is there any way to tell, from within Max, if the audio thread has underruns? I'm not outputting any audio, so I wouldn't hear it if there were any underruns...

Peter Castine's icon

Another way of approaching this would be to add a non-MSP outlet to your object, take the value you are currently sending out via clock and cache it in a new member of your object, then send that value out the new outlet in response to a bang (or other message). Then you can connect a metro (or a metro-triggered message box) to your object and all the outlet_xxx() stuff happens in the Max event-handling thread.

This may seem a bit old school, or even clunky, but I've done it with several custom objects and it works well and is easy to do.