Different implementations for polyphony

jj__

New member
Hi everyone,
I'm trying to build a polysynth that can be used in a live-looping manner (synthesizing one "instrument" loop at a time over the playback of loops previously created).
I’ve been considering different approaches to polyphony and would like advice on CPU usage and DSP scalability on a Teensy 4.1 with audio board and an additional 8 MB PSRAM chip.

1) Voice Pool (parameter-based instruments)​

I define a fixed number of voices. Each voice is a reusable DSP instance (oscillator + filters + envelope + FX state). Instruments are just parameter containers (structs), not actual DSP chains.
When I play a note, I allocate a free voice from the pool and configure it using the instrument’s parameters. When the note ends, the voice is released back into the pool.
This seems to be a the standard polyphonic synth architecture.

-> Question 1 : How many voices can I realistically expect, given I don't use CPU hungry effects and 1 oscillator ? What about 3 oscillators per voice (with detune) ? I've seen up to 8 voices polysynths (overclocked) and I'm afraid that wouldn't be enough for my use case.

2) Per-instrument DSP chains (fully duplicated engines) but with dynamic patching​

Each instrument owns its own complete DSP graph: oscillators, filters, envelopes, and effects are instantiated per instrument and they are all running in parallel.

-> Question 2 : How many instruments can I expect (broadly speaking) ?
-> Question 3 : Does a module that is not patched to anything still consume CPU ? If not, can dynamically disconnecting the oscillators (like in the library example) to the rest of the DSP chain of the instrument decrease CPU usage when the note is off ?

3) Single DSP chain + audio looping (render-and-playback)​

I only run one active DSP chain at a time (1 instrument). When I play something, I render it into an audio buffer (loop), then stop or freeze the synthesis and replay it using a loop player (e.g. SD / PSRAM-backed buffers), stacking multiple recorded layers.
Each instrument is effectively printed into audio and then mixed in playback rather than continuously synthesized.
This minimizes CPU load but shifts the problem to memory bandwidth and storage management.

-> Question 4 : How many instruments can I expect (playing loops around 1 min each) ?

And last but not least :​

Question 5 : Is it possible to instantiate modules at runtime like in this example from the library where it's done with AudioConnections ?
I'd love to have a list of modules on my TFT display and be able to add them and patch them on the fly.

I'm a beginner regarding a lot of concepts at play here so please tell me if I've understood anything the wrong way (and I hope I'm making sense, english is not my first langage).
Thank you so much in advance!
 
Last edited:
Questions 1 and 2 - the answer is bound to be “it depends“. But 12-poly is possible with pretty complex topologies on an overclocked Teensy.

Question 3 - a completely disconnected object is marked inactive and consumes negligible CPU time (just enough to discover it’s inactive and move on to the next one. All inputs and outputs must be disconnected…

Question 4 - one standard 8MB PSRAM can hold 95s of mono audio. 16MB devices are available, at a cost, and you can use up to 2 PSRAM chips for a bit over 6 minutes of mono audio. If you did some destructive re-rendering you could get a 1 minute stereo loop, but it’s probably not a very useful approach.

Question 5 - the standard Audio library does not support destruction of modules at runtime. You can instantiate them OK, but obviously after a while you’ll run out of memory. I’ve put some effort in to making this possible with edits to the Audio library itself and a couple of files in the Teensy cores. See this thread. It needs bringing up to date with the latest Teensyduino, though. A few people have used it, and as far as I’m aware there are no outstanding issues … or maybe they’ve stopped using it!
 
It very largely depends on the code and modules that you use. For example state variable filter is way cheaper than Ladder filter. Properly anti-aliased oscillators are more expensive than wavetables.
The only way to find out is actually put some code and measure time required for each module that you intend to use.

ARM Cycle Counter register (ARM_DWT_CYCCNT)
Provides the absolute highest resolution for your measurement needs. It tracks CPU clock cycles, reading directly from the processor.
  • Speed: Requires only about 3 CPU cycles to read, offering precision of just 1.67 ns on a 600 MHz Teensy 4.0.
  • Rollover: Because it counts every CPU cycle, the 32-bit counter rolls over very quickly (e.g., every ~ 7.15 seconds on a 600 MHz Teensy 4.x)
C++:
uint32_t start_time = ARM_DWT_CYCCNT;
CallWhateverModuleYouNeed()
uint32_t elapsed = ARM_DWT_CYCCNT - start_time;  // to correct timing differences even when it overflows.
 
Thank you both for your very useful answers!
I'm now moving towards a solution where repetitive and percussive elements (such as bass guitar, drums, etc) are synthesized on the DSP once (notes by notes) and then stored in PSRAM (and afterwards the DSP will be completely disconnected). This will yield short samples that don't take too much PSRAM, but can be played repetitively without using much CPU.
For the synth lead/piano etc, they'll be synthesized by the DSP chain as they are more subject to variation throughout the piece.

I've got some follow up questions :
6) My understanding from the SamplePlayer from the example files is that the AudioPlayMemory reads an array which is defined as :
C++:
PROGMEM
const unsigned int
Will reading from the PSRAM like so work ?
C++:
EXTMEM
const unsigned int

7) Can I record the currently synthesized instrument into a queue, while playing back samples from the PSRAM (using a second queue ?). This would mean several simultaneous accesses to the PSRAM, I don't know if it's possible.

8) If I use the SD card for long pre-recorded samples (voice), while using the PSRAM (as explained above) as well as the DSP chain, would there be a possible bottleneck (DMA ?) ?

I'll try to implement speed tests as suggested to see the real-life performance. Thanks!
 
Question 6 - yes, the ARM processor essentially makes no distinction between memory areas, unlike older Arduino generations where you had to treat PROGMEM and RAM very differently. PROGMEM (Flash) and EXTMEM (PSRAM) run at very similar speeds, i.e. slower than the 1MB of internal RAM, but plenty fast enough for audio purposes.

Question 7 - yes, you can. Bear in mind that there is essentially no such thing as simultaneous accesses to any memory, as there's only one CPU and it can only do one thing at a time. The synthesis and playback will actually occur a few microseconds apart (depending on what's going on). You very likely won't notice, because by default a whole new set of 128 samples is played back / synthesized / queued for output to the audio adaptor once every 2.9ms or so. What you hear was actually calculated several milliseconds ago, but unless you try really hard or have golden ears this latency is imperceptible.

Question 8 - the built-in SD playback is usable only in the very simplest of cases, e.g. when the Teensy is doing very ittle else, no other interaction with the SD card, and even then only a couple of files at once. There is no built-in SD recording - you have to make your own using record queue objects.

I've made an vastly improved set of objects to stream audio to / from SD cards - see this thread. I believe at least one person has used it successfully to build a looper type of application, though we discovered a couple of updates were needed before it worked properly. It's still a little finicky to use, but as far as I can tell that's unavoidable given the way some of the libraries it depends on are coded. One of the examples records 16 mono synthesized tracks to three WAV files of 4, 6 and 8 tracks each (yes, a couple of tracks are silent!), then plays back the result.
 
6) My understanding from the SamplePlayer from the example files is that the AudioPlayMemory reads an array which is defined as :
C++:
PROGMEM
const unsigned int
Will reading from the PSRAM like so work ?
C++:
EXTMEM
const unsigned int
This will give the appearance of working but won't behave correctly - you can't declare const arrays in EXTMEM, only uninitialized data.
 
I imagine the const was just copypasta, but @jmarsh makes a good point which is often overlooked - EXTMEM and DMAMEM can’t be initialised at compile time. All you can do is define the space, then fill in some data at run time, e.g. from a file on SD card or live from a record queue.

And of course you do have to leave the const qualifier off, or the compiler won’t let you write to your RAM!
 
I've been wondering whether gcc 15.2 offers any new capability to initialize variables in DMAMEM and EXTMEM?

Fixing this has long been on my high priority issue list. I spent a lot of time with gcc 11.3 that ultimately turned out be fruitless.
 
It's easy enough to do as long as you use different specifiers for initialized data vs uninitialized data, similar to how FLASHMEM and FASTRUN work. This separates data into separate sections so the uninitialized variables can be tagged with NOLOAD. Otherwise there is no way to do it with a single specifier like DMAMEM / EXTMEM because it lumps everything together, you end up with a big chunk of zeroes in the final output due to the uninitialized data not being pruned.
 
Back
Top