Generating a sine wave and altering amplitude within an IntervalTimer

Status
Not open for further replies.

mfromano

Member
Hi all,

This is my first time using the Audio library, and I have a couple of questions with respect to my code.
As a brief overview, I do this:

1. I generate a sine wave, and initialize an IntervalTimer object that calls a function (called "capture") every 50 ms
2. During each function call, I occasionally modulate the sine wave amplitude for exactly 700 ms. (line 143 and line 149)
3. During this function call, I also adjust a few other experimental parameters, though these are to my knowledge, irrelevant.

My questions, I hope, are pretty simple!

0. The length of each audio block synthesized using a "sine" should be 128 samples / 44.1 kHz = 2.9 ms, correct?

1. Would I ever expect the sine wave to terminate during "capture", because the IntervalTimer function runs at a higher priority? Like, can the sine wave simply run out of data? My supposition would be that, by setting the AudioMemory to 128, it would store 128 blocks of audio data at a time, and if each block lasts ~2.9 ms, there would basically never be a case in which the audio signal would get interrupted (the maximum length of each "capture" interrupt should hover around 1.1 ms). So, my understanding is that, between calls to "capture", the audio would add more audio blocks, which should be sufficient to .

2. I noticed looking at the audio signal that each period of high amplitude lasts either approximately ~699 ms, or ~702 ms, but nothing in between. Is this because the "amplitude" change only effects the sine wave output in increments corresponding to individual "blocks"? Also, does the amplitude change should occur relatively immediately, correct? As in, the amplitude should effect the next block, which would occur within 2.3 ms.

Let me know if I need to provide more clarification! And thank you for any help!

Kind regards,
Mike
 

Attachments

  • main_audio.cpp
    4.7 KB · Views: 70
I suppose I could rephrase the question a bit more simply:

1. If I have a sine wave playing with a higher priority interrupt generated periodically (every 50 ms) via an IntervalTimer that lasts about 1-2 ms, would I ever expect the sine wave to be interrupted? Or can I avoid this by making the AudioMemory large?
2. If I change the amplitude of the sine-wave, how soon will it take effect? Does the size of the AudioMemory I use change this? Thanks again!
 
Basically, the audio library processes things blockwise. Thus, changes occur earliest next block and last always an integer number of blocks. You might reduce the block size from 128 words down to 16 words, which makes processing more cpu intensive (more interrupts) but shortens that latency. Thus, you trade one for the other. You might for better control over the timing wrap your specific code into a new audio lib object and thus benefit from block synchronized updates.
 
Thanks! That makes perfect sense. The jitter (2.9 ms) is fine for me. I guess my bigger concern is point (1). With a, let's say, 2 ms IntervalTimer function called every 50 ms, is there a chance that the audio output gets interrupted as well? My thinking is that in the 48 ms of downtime between IntervalTimer calls, the Audio Library can refill its buffer and that therefore there should never be a point during which the sine output runs out of "words". Is my thinking correct? Does this make sense?

Thanks again,
Mike
 
There is a function called audioCPUusage() which allows to estimate how much cpu time the audio processing of the library is eating up. Your interrupt takes 2ms, an audio buffer is consumed in 2.9ms. Worst case is your interrupt colliding with one single buffer, thus taking ~70% of the time. Thus, if the audioCpuUsage remains below 30%, no stuttering will occur in theory.
 
The answer depends on the details of interrupt priorities, CPU usage in the audio library, and how long your interrupt runs.

The safest approach would be to set your IntervalTimer to a low priority like 255. Then it can't rob CPU from the audio interrupts. Or just use elapsedMillis from within loop(), which avoids interrupt conflicts completely.

If your interrupt runs at higher priority than the audio update, which is the default, then you'll face 2 possible issues. The simpler issue to understand is whether you have enough CPU time available. To know this, you'll need to measure or estimate how long your interrupt takes to run, and use the audio library's CPU reporting functions to figure out how long the audio update is running. If you find the audio lib is using 40% of the CPU time, then just multiply 2.9ms by 0.4. When the audio lib update is running for 1.16 ms, that leaves 1.74 ms of time for other stuff. If your interrupt blocks the audio library for 1.74 ms or longer, you'll risk disrupting the audio.

The more complex issue is whether the audio features you're controlling from an IntervalTimer interrupt are in fact safe to change from an interrupt which might run while the parameter is in use. The audio library wasn't really designed to be controlled this way. But it usually works, because most of the settings are write-only from the application side and read-only from the audio update side. Writing to those sorts of parameters merely changes the copy in memory, and the audio library reads the new value on the next update. But some parameters, like the relative phase angle of the waveform synth, are read, modified and written back during each update. If you happen to change one of those during the brief time it's in use, your change will likely be lost when the audio update writes the updated value.

The best approach to update something every 50 ms would be to avoid IntervalTimer and use an elapsedMillis variable that you check in loop() to schedule your code to run every 50 ms. However, this really only works well if nothing else you do from loop() takes a long time (relative to 50 ms).
 
Opps, I misspoke (mis-remembered) slightly on the phase offset. Long ago this was done with fiddling with the phase accumulator. But looking at the code just now, since at least 1.42 the user adjustable phase offset is done with write-only from the application API and read-only from the audio update. But it's not quite that simple. It's actually read twice during the update. So depending on when your interrupt alters it, you could see different effects.

Likewise for the amplitude, the code wasn't really designed for interrupts changing the parameters. The memory-based parameter is used all over the place. I believe the compiler is very likely optimizing this to just 1 read, but there's no way to know for sure which dissecting the generated asm. Maybe someday this should be altered so the actual parameter is volatile and read just once at the beginning of the update, so we can precisely know the behavior without having to consider how the compiler optimizes the code.

But the general point is the audio library wasn't designed for that sort of atomic once-per-update access to parameters. Altering parameters from a higher priority interrupt while it's running will usually work, but it certainly wasn't designed for that sort of thing to be safe.
 
Thanks very much! So the maxCPUUsage that I'm seeing is 1%, so I think that there should be no stuttering. Thanks for the help with that.

As to the amplitude aspect, it seems like it works for now, though your point is well taken that it was not meant for that sort of behavior!

The timing issue is tricky. I'm triggering a camera to pulse at exactly 20 Hz, so I do need that to be precise as well as the audio. I think I will keep it as is for now, as after a lot of testing (well over 200 trials) it seems like the amplitude tinkering doesn't have any bizarre impact on the data. I tried using ellapsedMicros and ellapsedMillis to work around it, but that does introduce some delays. I will keep that in mind, however. Thanks very much!

Regards,
Mike
 
I guess a general follow-up, which may be unanswerable would be: what might be a worst case scenario to change the amplitude at a random point during an interrupt? Would it be that the changed amplitude would not be sustained for the desired amount of time? Or is the answer more complicated?
 
Yes. Or more complicated. A through analysis of the compiler generated assembly would be more work that redesigning the code. At this moment, I'm focused on developing new hardware, so I'm not going to pour dev time into either.
 
So I've changed my code to utilize a timer as you've suggested. However, I've noticed about a ~6-8 ms delay before I see an increase in the amplitude of a sine wave. I've attached the code, but basically, here is a plot of a digital pulse from a different pin and the amplitude envelope of the filtered sine wave, and my code as modified. Essentially, I use ellapsedMicros to initiate a change in the amplitude envelope, and I am measuring the change in voltage out of the audio pin on my Teensy 3.2. Does anyone know of a reason for the delay? I notice it's a little over 2 blocks long if each is approximately 2.9 ms.
Screenshot from 2018-10-26 14-08-27.png

View attachment main_audio.cpp
 
And here is an image of tone latency (difference between the rising time of the blue line, which is the tone's amplitude envelope, and the orange line, which is the digital pulse):

Screenshot from 2018-10-26 14-30-20.png
 
Status
Not open for further replies.
Back
Top