AudioSynthKarplusStrong object with modulation/tuning input

That’s certainly a beast! The only thing I can think of to reduce CPU load further is that noise is noise, so could you not get rid of 3/4 of your pink and white noise generators?

I did a quick search but am still not sure of your current architecture, however, I did wonder if splitting the load between multiple Teensys would work? One way would be to have 3 of them, two doing 6 voices each with I2S slave outputs to the third one, which could have an adapted audio library with smaller blocks to reduce latency between inputs and output. Control and MIDI would be an interesting problem, of course…

This probably needs a separate thread :)
 
I'm wondering if, since the averaging of the two last sample in the loop filter can be considered as a two-tap FIR filter, the right code to steal wouldn't be the one of the FIR filter. It seems to use a very specific arm_fir_init_q15 function, which is described here. It might only work if you accept a minimum ring buffer size, ie limit the maximum frequency.

I'll try to look into the optimisation first, since this method is of no use to me if Its really too resource consuming!
It’s not so much the init as the update code, but that was indeed my sticking point. However, any filter is likely to be more resource-hungry, so not really of interest to you.

Good luck with the optimisation, I’m fairly sure you’ll find plenty of options!
 
That’s certainly a beast! The only thing I can think of to reduce CPU load further is that noise is noise, so could you not get rid of 3/4 of your pink and white noise generators?

Because of the WIP nature of my TeensyMIDIPolySynth (TMPS) implementation, so far, I've only posted verbal descriptions of the design. My intent is to eventually publish everything (code, design, parts BOM, PCB, case, etc.) to allow anyone else to build their own copy. My OCD nature won't let me to take that step until I'm 200% certain that I can't get it to fail (I think I'm finally done adding capabilities, so now it just a matter of reliability/usability) !!

As for the noise, one each of the pink & white noise sources are available for mixing into the audio stream. The remaining pink & white noise sources are used as VFO modulators, so it is only slightly desirable that they be separate sources (so modulation is not necessarily in sync across the three voices), as each can be individually & selectively filtered. I'll play with reducing that number & see what difference it might make.

I know that this will likely be completely unreadable, but here's what the audio design tool setup currently looks like (the disconnect at the right-hand side is where @Pio's non-standard reverb object is inserted manually into the code):

1745074208267.png


Mark J Culross
KD5RXT
 
Yup, completely unreadable :LOL:

It's an ideal candidate for the updated Design Tool that @manicksan did a few years back. Makes it much easier to maintain a hierarchical / modular topology than the vanilla version. It also allows you to create and place custom objects, though I've found that facility a bit tricky to use in the past - may be better now.
 
@h4yn0nnym0u5e: Yes, I am aware of the amazing work that @manicksan did to provide easy support for arrays of audio objects. In fact, he used an earlier version of my TMPS as one of his initial <test cases>. Unfortunately, I was so heavily invested in the hardware & the large amount of source code that I had at that time, that I just couldn't bring myself to make such a drastic change. Now that I'm wrapping up the current TMPS version, maybe I can invest the time in testing out the capability that @manicksan developed & made available. It'll be interesting to see how using his audio tool & capabilities would affect RAM usage & processor loading, especially in comparison to what I have currently.

Mark J Culross
KD5RXT
 
Hmmm … I’d expect no change to CPU load, as there’s no obvious need to add any real audio objects; maybe a tiny bit more RAM1 because of the extra classes and their constructors. But the latter could be offset by making your overall code simpler, depending on how clever you’ve been so far about writing that - I’ve seen some terrible examples with screeds of switch/case statement, every single one of which needs rewriting when the poly count changes; I’m sure you haven’t done that…

The ability to put class methods in each page of the design, along with variables, is not to be ignored, either. The Voice class can define its own noteOn(), and your MIDI allocator needs to know nothing of its content, though of course the Voice can also have a method to tell the allocator its status so you don’t need a separate idle/sounding/releasing array.

To be fair, you’ve already dealt with this, so porting to a class-based design is more likely to be an effort in making things more elegant than for improving efficiency of operation or maintenance.
 
I was thinking particularly about the mixers. There are multiple places where there are several 4-source mixers in parallel at the same level in order to accommodate the number of input objects making up a mix (e.g. the different LFO waveform & DC level & modulation depth & noise generators for a single note = 9), where as an N-source mixer would reduce the number of logical mixer objects, and potentially the number of update() methods to be traversed. The resulting calculations would most likely be roughly the same, but there might be some slight savings in the number of function calls inside the audio interrupt processing for each audio block.

Yes, I have a single constant called MAX_POLY (& another called LIMIT_POLY that allows me to artificially constrain the number of POLY notes, allowing me to easily test the boundary case where a noteON() is called, but all of the notes are already allocated. I allow two selectable modes: "steal the oldest note" (default) or "ignore the new note"). MAX_POLY Is used everywhere that the number of notes is needed (defining array sizes, processing loops through all notes for initialization, etc.), & LIMIT_POLY is used for all run-time determinations (dropping/stealing note objects, killing all notes upon command, etc.).

In my particular design, I already have an array of "note objects" that keeps track of all the aspects of a particular "note" (MIDI channel, base pitch, octave modifier, time of initiation, time of termination, etc.) corresponding to one poly path, so I don't have to manage anything except noteOn() & noteOff() . . . everything else is managed by the audio objects themselves & their modifiers (envelope, etc.). This allows me to easily handle activities such as re-striking a note that is already playing - no need to allocate a new note object, just re-use the one that matches all of the particulars of this noteOn() request, etc.

Since I do this for fun (unlike my professional software job that I retired from two years ago), I am often lazy in my hobby software, but I do try to avoid being sloppy !! This potential activity of porting to a class-based design would be mostly for educational purposes . . . no telling what I might learn along the way.

Mark J Culross
KD5RXT
 
Mixers are astonishingly cheap. I did an improved version which optimised channels with zero gain (oddly enough only 1.0 gain was optimised before), and found a 0.0033% improvement per zero-gain channel. So every mixer4 costs a bit over 0.0132% to run. Your 164 are probably only costing you 2.5%, and wider ones are unlikely to improve on that. They do of course make your design a lot cleaner (and guess what … they are included as part of Design Tool++ :) ).

Sure, there are lots of ways of keeping track of voice allocation and properties, you’ll obviously know all about that. Just saying that the class-based approach makes it easier to partition responsibilities. Does a voice know when it started, whether it’s active, or still in the release phase after a noteOff()? Clearly yes, so it should be responsible for providing that information. Does it know it can be stolen if you run out of polyphony? No - that’s the allocator’s responsibility, though it’s likely to have to poll the voices to find the victim, be it oldest, quietist, already playing that note etc.

Sorry, this kinda sounds like I’m criticising your code, sight unseen! No such intention, honest…
 
No offense taken at all. I enjoy getting other perspectives . . . it's an opportunity to see things from a different point of view. No one person can know or think of every way to do something...I'm especially a creature of habit in my software implementations, as I usually am in life in general. I have definitely enjoyed the various discussions along the way. Looking at different ways to approach a common problem is very enlightening & enjoyable to me.

Mark J Culross
KD5RXT
 
Here !
Also I made a PR, but for some reason my VScode thought it was nice to add a space for each line.
I'll fix it and make a better one, but you can already see the changes, they are quite minimal. I hope this could be of use to you!
 
Just pulled it into a separate branch to do a cross-check; I'm thinking the filtered version is going to use even more CPU, so we need ways of turning it off if it's not wanted.

I'm not sure how you tested it, because what you submitted for PR doesn't compile: the declaration for noteOn() doesn't match the definition. You've also removed the setFeedbackLevel() method (but for some reason not the associated _feedbackLevel variable), which breaks my test code. @kd5rxt-mark , do you use that, or might you do so in the future? If not then it might make sense to remove it if the changes @BenIP has done are the better route (sound-wise and CPU-use-wise).

EDIT: just to clarify, could you please submit another PR with code that compiles correctly!
 
Last edited:
@kd5rxt-mark , do you use that, or might you do so in the future?
@h4yn0nnym0u5e: I don't currently make use of setFeedbackLevel(), & don't anticipate making use of it in my TeensyMIDIPolySynth (TMPS) implementation.

EMBARRASSING ADMISSION: When I was doing my processor loading tests, I didn't realize that I had my LIMIT_POLY constant set to 10 (after stating so strongly that less than 12 was personally unacceptable !!). I'm guessing that this was left over from my testing attempts, trying to convince myself that 10-poly might be OK . . . WHOOPS !! Once I put everything back to 12, I also had to boost the overclocking to the MAX 1.008 GHz (& as a result, also had to add a heatsink to the audio processor & a cooling fan to my TMPS case) just to keep everything running. Anything that can be done to potentially reduce the processor load would be immensely beneficial for my application.

Mark J Culross
KD5RXT
 
Last edited:
OK, so we could reasonably lose my setFeedbackLevel() if it makes sense to do that.

I didn’t really look at code optimisation, e.g. use of DSP instructions etc., when I was working on this, so there may well be opportunities to save cycles.
 
Forgot to mention that, instead of replacing the standard synth_karplusstrong.* files with these updates, I have just copied the new source files to my local TMPS folder & made the two sets of classes unique by adding "Modulated" to the names of the new classes. Along with this, I have defined the "DISABLE_MODULATED_STRINGS" constant in my audio processor source file (& selectively define/undefine it) to allow me to easily compare operations using the original strings audio object vs. using the updated modulated/tunable strings audio object. With this, I can very likely test anything that's needed.
 
I've played around a bit and reduced the CPU load somewhat - let me know if it looks OK to you. There's a slight loss of precision in the calculations, but it'll only affect tuning ever so slightly, I think.
 
@h4yn0nnym0u5e: I finally found some time to play/test today's update to the modulated/tunable strings audio object.

Unfortunately, it seems that today's latest version has some kind of a flaw (it causes my T4.0 audio processor to appear to hang). I don't have the ability to understand exactly what is going on, but I am at least able to characterize where I believe that the problem may lie.

Concentrating on this particular code snippett (from your updated synth_karplusstrong.h):

Code:
          // expect moderately sane index, so avoid
          // division by using multiple addition / subtractions
LINE 1:   while (index < 0) index += sampleCount;
LINE 2:   while (index >= sampleCount) index -= sampleCount;

To assist in troubleshooting, I am placing the following line of code in different places & observing the results: digitalWrite(DEBUG_PIN, HIGH);

- when placed before LINE 1, the DEBUG_PIN always goes HIGH
- when placed between LINE 1 & LINE 2, the DEBUG_PIN always goes HIGH
- when placed after LINE 2, the DEBUG_PIN does not go HIGH

This seems to indicate that the processor gets stuck in the 2nd while loop (which makes absolutely no sense to me). If I simply cause this new code to be commented out & the old version to be activated (BTW, your use of comment pairs the way that you have done to make this extremely easy is very clever !!), then correct processing is restored.

I don't have the required ability to determine exactly what is going on here. Hopefully, you'll see immediately what's actually happening & be able to both explain it & remedy it !!

Thanks again for your efforts in this regard . . .

Mark J Culross
KD5RXT
 
That’s very odd. As you say, it only makes sense if a note is sounding but sampleCount is zero, which should Never Happen. I’ll try to pop some diagnostic code in there and push it up for you to try, unless you can figure out a set of circumstances that lets me trigger it here. Probably need to know note pitch, if it’s a repeated noteOn(), and if audio blocks are plentiful.

I was quite pleased to figure out the comment pairs trick - unless I saw it somewhere and copied it, really can’t remember :) . It’s very useful, though sometimes the compiler warns about nested comments so you can’t leave it in production code.
 
Last edited:
I've pushed a debug-only commit - don't use for anything else! Not 100% sure it'll work, but you'll get the idea.
  • needs AudioSynthKarplusStrongFail() implemented in your sketch
  • has most of the AudioSynthKarplusStrong class variables exposed
You need to add some code to your sketch:
C++:
// if this goes non-null, print diagnostics and set back to null
AudioSynthKarplusStrong* failedObj = nullptr;
void AudioSynthKarplusStrongFail(AudioSynthKarplusStrong* obj)
{
  failedObj = obj;
}

// poll this code in loop()
void pollFail(void)
{
    if (nullptr != failedObj)
    {
        // ... print stuff
        failedObj = nullptr;
    }
}

You can probably figure out interesting stuff to print, if not we may need to figure out how I can provoke the failure here. Won't be able to work much further on this today, probably.
 
@h4yn0nnym0u5e: OK, I've spent some time hacking up the audio portion of my TeensyMIDIPolySynth (TMPS) source files (which normally would require a cooperative touchpanel display managed by a T4.1 to setup & activate all of the controls & configuration) & have come up with a way to (hopefully) allow you both to observe & (if/when you have time) to troubleshoot the errant behavior that I'm experiencing.

TEST HARDWARE:
T4.0 w/ Audio Adapter connected. Unlike the fully functional setup, this hackup/setup can run at the default 600MHz speed (&, by default, can be built using the "Faster" optimization level as well), thus avoiding the need for any external cooling of the T4.0.

TEST SOFTWARE:
The self-contained contents of the attached ZIP file includes all source files needed to create a standalone configuration of the audio portion of my TMPS which calls the updated strings audio object's noteOn() function every 200ms (& blinks the onboard LED to show when the call is being made).

TEST CONDITIONS:
The build includes an updated synth_karplusstrong_mod.h source file, which, for comparison, can be selectively modified to include either the original version of the limitToBuffer() function (using the MOD operator), or to include the newer version (using while() loops to effect division using multiple addition / subtractions).

OBSERVED/ANTICIPATED RESULTS:
- when the synth_karplusstrong_mod.h source file is built using the original version of the limitToBuffer() function (using the MOD operator), the onboard LED blinks ON/OFF as expected & the repeating periodic strings sound are heard
- when the synth_karplusstrong_mod.h source file is built using the newer version (using while() loops to effect division using multiple addition / subtractions), the onboard LED turns on, but never turns off & no repeating periodic string sounds are heard


Hopefully, you'll find this beneficial, & it will allow you to readily observe the apparent hang that I'm experiencing.

Thanks again in advance,

Mark J Culross
KD5RXT


[EDIT] P.S. If you open a Serial Monitor while this test capability is executing, you will observe repeating reports every 200ms. The EAd:Txxx.xx; report indicates the current temperature of the audio processor (in degrees C). The EAd:Lxxx.xx/yyy.yy; report indicates the current audio processor load (in percentage), followed by the maximum audio processor load (in percentage), each separated by a slash (/). As some background info, the "EAd" in these reports stands for "external audio debug", referring to the fact that the audio capability and the display capability each run on separate T4.x processors, intercommunicating over a shared serial port running at 500kbps. MJC
 

Attachments

  • 20250427-0030 - TeensyMIDI-RA8875-PolySynth-Standalone.zip
    81.3 KB · Views: 13
Last edited:
Well that's very odd. I can reproduce the issue with your code; if I put in my changed code, to time out the second while() loop and log the offending object, the issue no longer appears at all!

Further investigation needed...
 
OK, so the problem was (I think) due to calling noteOff() when the note hadn't actually been started. I haven't scanned the sketch thoroughly, but fixing the problem allows it to run OK.

New commit pushed to the repo - see what you think.
 
I'll take a look later today, but note that I initially hacked the test sketch to only call noteOn(), with no calls to noteOff(). Thinking that I was causing the problem by never calling noteOff(), I added those after the fact. The problem is observed the same either way, so not sure that pre-calling noteOff() is a specific cause . . . & doing so should be harmless & handled gracefully, regardless, shouldn't it ?? I couldn't see anything that looked like it would object if one tried to stop a note that wasn't playing, then again, this is all still way above my ability to fully understand !!

Mark J Culross
KD5RXT
 
Last edited:
As a side note (reminded by seeing Paul's <response> to your other post with the requested PR for remedying on optimization hang), curiously, I noticed during yesterday's testing that the hacked up test sketch does not create the desired sound if any of the LTO optimizations are used to build . . .

Mark J Culross
KD5RXT
 
Back
Top