string ensemble chorus - anyone try it?

Status
Not open for further replies.

quarterturn

Well-known member
I want a good string ensemble chorus sound without resorting to expensive implementations of actual BBD circuits. It should not be hard, I think. Here is what is needed at a high level:

0.6 Hz and 6.0 Hz sinewaves (with a phase of zero) are combined producing an original signal with phase zero, plus two more having phase 120 and 240, respectively. These are each used to modulate the time parameter of a delay. Something between 5ms and 30ms, I am not sure, but that's not critical as it should be a physical control input anyway.

All I should need are three pairs of 0.6 Hz and 6.0 sinewaves, with phase 0, 120, and 240. I am guessing sine objects are phase-locked, otherwise specifying the phase would not make much sense.

Are the sine objects phase-locked? If they are off by a sample or two, that will probably make no audible difference.

Thanks!
 
Hi @quarterturn,

in fact I am trying this. See my post about modulated delay just before your post. But I currently have some trouble with clicking noise.

At night I had an idea how to fix this. When it is fixed, I think it should not be a big problem to add multiple taps.

Regards, Holger
 
Ah yes - looks like delay does not play nice with continually updating the time parameter. That's too bad. I have an Electrosmash Pedalshield lying around, I should get another Due for it and look back through the example code for delays. It should not be hard to create the sinewave modulators via a lookup wavetable. If I can get it working there I'll get a Teensy 3.6 and get it going there - should be even easier since there is hardware floating point.
 
Ah yes - looks like delay does not play nice with continually updating the time parameter

Can you post a program which demonstrates the problem?

As far as I know, this problem isn't anywhere on my list of known issues. It won't get on that list unless there's a program which clearly demonstrates the problem.
 
Ah, I see... it's an issue with code posted on a forum message, not something we've published in the audio lib, right?
 
Ah, I see... it's an issue with code posted on a forum message, not something we've published in the audio lib, right?

Hopefully COd3man will update.

In the meantime, I will try it the "hard way" which is to create a wavetable in setup() something like:
Code:
2+sin(((2.0*pi)/samples)*index)) + sin(((20.0*pi)/samples)*index))

and fiddle around with interrupts per sample until the output looks like 6.0 Hz and 0.6 Hz added together. The Electrosmash sinewave generator example used 600 samples for their wavetable. That would be 60 samples for the 6.0 Hz component and the full 600 for the 0.6 Hz. The wavetable size probably needs to be increased.

Creating the phasors should be as simple as setting the pointer to the right location such that they are separated by thirds in the wavetable.
 
Ah yes - looks like delay does not play nice with continually updating the time parameter. That's too bad. I have an Electrosmash Pedalshield lying around, I should get another Due for it and look back through the example code for delays. It should not be hard to create the sinewave modulators via a lookup wavetable. If I can get it working there I'll get a Teensy 3.6 and get it going there - should be even easier since there is hardware floating point.

As you found, the delay object does not expect to be changed very often...it isn't designed that way. At best, it might be possible to change the delay time for every block of audio data, which is every 128 samples. At 44.1 kHz, this would be an update every ~3ms. Even if you were able to change the delay time that fast, I think that it'll sound bad as this updating will sound nothing like the smooth changes that would happen if it were updated every sample.

To update every sample (via a software LFO or via a wavetable...however you want), you'd need to re-write the guts of the delay effect object. And if you're going to do that, you should probably start with the chorus object instead as that might be easier.


Chip
 
I just looked at the chorus effect code that's in the Teensy audio library...

h file: https://github.com/PaulStoffregen/Audio/blob/master/effect_chorus.h
cpp file: https://github.com/PaulStoffregen/Audio/blob/master/effect_chorus.cpp

Unfortunately, the chorus code in the Teensy audio library (I'm looking at the "update" method on the cpp file) is pretty terse and lacks comments to help newbies (like myself) to clearly see the algorithm.

In trying to understand the code, I'm assuming that the underlying algorithm is something like:
* A delay line is used to store the incoming audio
* One or more LFOs are used to continuously vary lookup points into the delay lines
* The delayed version(s) of the audio are mixed with the original (dry) audio

In looking at the Teensy code, I do see some variable names that allude to these elements, but I can't seem to understand what's going on. Or, perhaps this code is implementing an algorithm that is very different from my assumption?

Can anyone help explain how the Teensy chorus works? I have questions like:
* Where is the memory allocated to implement the delay line?
* Surely, there is an LFO that is sweeping the delay in order to make the chorus effect...how do I deduce its basic properties like LFO frequency?

Depending upon the answers, it might be that this code won't be very helpful as a guide for the OP in making their own chorus/ensemble effect.

Chip
 
Last edited:
As you found, the delay object does not expect to be changed very often...it isn't designed that way. At best, it might be possible to change the delay time for every block of audio data, which is every 128 samples. At 44.1 kHz, this would be an update every ~3ms. Even if you were able to change the delay time that fast, I think that it'll sound bad as this updating will sound nothing like the smooth changes that would happen if it were updated every sample.

To update every sample (via a software LFO or via a wavetable...however you want), you'd need to re-write the guts of the delay effect object. And if you're going to do that, you should probably start with the chorus object instead as that might be easier.


Chip

If I get it working I'll certainly post a follow-up. I will probably start from scratch and not use the audio library though.
 
It looks like it is varying a pointer offset to the audio block from 0 to delaytime and back down. It is like a triangle wave. It takes the delay offset to the pointer and sums the sample data from that to the index. I am not great with C++ but it does not appear to allocate new memory, just uses pointers to the existing audio block.

I suppose keeping a phase relationship would be offsetting the delay pointer by thirds of the audio sample block. This gives me a good deal to think about.
 
Hi Guys,

I am a little bit confused: you are discussing about an algorithm which is exactly the one I posted some hours before this thread was opened. I also pointed to my thread (Modulated-Delay-Chorus-Flanger) at the start of _this_ thread, but noone seems to took a look at the code I posted. My code is "nearly" working... there is a problem with some noise at changing signs of the modulation (perhaps an access outside the array boundaries?). So we can now try to solve the problem on our own or build a functional version together.

Regards, Holger

P.S.: Code is now located at https://codeberg.org/dcoredump/Test_ModulatedDelay
 
Last edited:
Sorry, I can't get involved in helping with this. Later this year, well after Teensy 4.0 is released, I probably will be able to do this sort of help. But right now, I just can't. I can barely even keep up with the most essential support requests.
 
Sorry, I can't get involved in helping with this. Later this year, well after Teensy 4.0 is released, I probably will be able to do this sort of help. But right now, I just can't. I can barely even keep up with the most essential support requests.

Absolute no problem @PaulStoffregen! I'm really curious about the new Teensy. Maybe by then we'll have a modulated delay / chorus that works.
 
Yes, it does look like Holger did what he said...extended the chorus to use an external signal source as the modulation. So to get ensemble working, you'd need several of these in parallel.

Bummer that it still has audio artifacts. It's not obvious in looking at the code what the problem is.

Chip
 
As promised, here's my attempt: https://github.com/quarterturn/due_ensenble_chorus

It is not working well - there's a TON of noise and aliasing. This may well be down to the Due plus PedalShield hardware. Sorry I could not write it for the Teensy (don't have one at the moment) but it won't be hard to adapt the code to it. You just have to figure out the ADC init and interrupt timing to get 44100 interrupts/sec for audio.

I used a circular buffer concept. The index for the current sample and the delayed versions wrap around if it reaches the end of the buffer. The modulation wavetable is scaled to provide something like a 5 - 10 ms delay range, and it is sized so if it is updated every 10 interrupts it will "play back" at the right speed.

All in theory. I am sure there is an obvious mistake or two. I've been working on it for a while and I'm in need of a break to let it mentally digest so I can maybe see what's wrong.
 
As promised, here's my attempt: https://github.com/quarterturn/due_ensenble_chorus

It is not working well - there's a TON of noise and aliasing. This may well be down to the Due plus PedalShield hardware. Sorry I could not write it for the Teensy (don't have one at the moment) but it won't be hard to adapt the code to it. You just have to figure out the ADC init and interrupt timing to get 44100 interrupts/sec for audio.

I used a circular buffer concept. The index for the current sample and the delayed versions wrap around if it reaches the end of the buffer. The modulation wavetable is scaled to provide something like a 5 - 10 ms delay range, and it is sized so if it is updated every 10 interrupts it will "play back" at the right speed.

All in theory. I am sure there is an obvious mistake or two. I've been working on it for a while and I'm in need of a break to let it mentally digest so I can maybe see what's wrong.

Congrats on getting something to work! Sorry it's not satisfying sounding. Some thoughts after looking at your code:

Because you're using the Due's ADC, your audio signal has a big DC component to it, which might slowly change. So, separate from your quest to make a chorus ensemble effect, I would recommend that you go from uint32 to int32 and then do a highpass filter to bring it all down to be zero mean. That'll make you bit-shift operation in place of a divide-by-2 to be more reliably artifact free on your audio.

Second, if you're getting a Teensy 3.5 or 3.6, I recommend that you go floating-point. That'll let you worry about the algorithm and not the numerics.

Third, if you're getting a Teensy, are you getting an audio shield, too? The ADC/DAC on any of these microcontrollers are not really up to the task of audio (IMO). They'll always sound weird and/or noisy. Switching to an audio codec will help. The Teensy Audio board is pretty noisy, but it is low cost and easy to use and better (again, IMO) than the built-in ADC/DAC.

Fourth, and this is the only one that has to do with the actual algorithm, your current algorithm slowly increments the index into the memory buffer to get the delayed signal value. That's totally great. The problem is that the LFOs are slow enough that you really want to increment only a fraction of an index. Instead, though, your algorithm (I believe) stays at one index for a while and then jumps to the next index, stays there for a while, and then jumps to the next, and so on. The overall speed is probably right, but the quantization surely contributes to aliasing/unsatisfying sound that you report.

To fix this problem, you could introduce interpolation (which has its own artifacts, but it's better than quantization), or you can run at a higher sampling rate (but that's a pain), or you could use a completely different algorithm where phase is more easily controlled (such as in the frequency domain or with all pass filters or something).

Congrats on getting something to work, though! That's pretty hard! (Sadly, getting something to sound great is also really really hard)

Chip
 
Congrats on getting something to work! Sorry it's not satisfying sounding. Some thoughts after looking at your code:

Because you're using the Due's ADC, your audio signal has a big DC component to it, which might slowly change. So, separate from your quest to make a chorus ensemble effect, I would recommend that you go from uint32 to int32 and then do a highpass filter to bring it all down to be zero mean. That'll make you bit-shift operation in place of a divide-by-2 to be more reliably artifact free on your audio.

Second, if you're getting a Teensy 3.5 or 3.6, I recommend that you go floating-point. That'll let you worry about the algorithm and not the numerics.

Third, if you're getting a Teensy, are you getting an audio shield, too? The ADC/DAC on any of these microcontrollers are not really up to the task of audio (IMO). They'll always sound weird and/or noisy. Switching to an audio codec will help. The Teensy Audio board is pretty noisy, but it is low cost and easy to use and better (again, IMO) than the built-in ADC/DAC.

Fourth, and this is the only one that has to do with the actual algorithm, your current algorithm slowly increments the index into the memory buffer to get the delayed signal value. That's totally great. The problem is that the LFOs are slow enough that you really want to increment only a fraction of an index. Instead, though, your algorithm (I believe) stays at one index for a while and then jumps to the next index, stays there for a while, and then jumps to the next, and so on. The overall speed is probably right, but the quantization surely contributes to aliasing/unsatisfying sound that you report.

To fix this problem, you could introduce interpolation (which has its own artifacts, but it's better than quantization), or you can run at a higher sampling rate (but that's a pain), or you could use a completely different algorithm where phase is more easily controlled (such as in the frequency domain or with all pass filters or something).

Congrats on getting something to work, though! That's pretty hard! (Sadly, getting something to sound great is also really really hard)

Chip

No doubt the audio shield would sound better, as would using floating point to update the delay time each sample, vs having to stretch it out to avoid a very large wavetable. The PedalShield does take an AC waveform and adds an offset for the ADC, and does the opposite the other way around. It's not a bad design. I know at least the DACs are capable of very fast readout using DMA - Nuts and Volts had a 2018 project called Arduino Graphics Interface which used it as a vector generator to run a scope in XY mode and it looks great. They have a version with the Teensy 3.6 as well, but there they use the CPU speed to brute-force it and don't use DMA.

There's definitely a problem in my code. I've tried a few other simpler algorithms (like just delay swept with an up-down counter) and they sound better - though not great.

I've got the boards for an Oakley Sound Systems ensemble chorus on-order, just in case! It's an old-school BBD-based chorus and it fully nails the 1970's string machine sound. It's a ton of rather hard-to-source and expensive through-hole - I've told myself no more through-hole after this project!
 
Last edited:
Hey, if you do that Oakley Sound project, and if you post any pics or a writeup on the web, be sure to share the link here. Definitely interested.

Back to the audio issues that you're having, I was not trying to suggest that the problem was with your wavetable...no, no...the problem is inherent with digital delay systems being used for this kind of thing.

With a naive digital delay system (like we're doing here with Teensy or your Due), we are only doing delays that are an integer number of audio samples. For example, you can delay 342 samples or you can delay by 343 samples. In a naive system, you cannot delay by 342.2 samples.

Chorus, though, is trying to slowly and continuously sweep the delay of one signal versus another. It doesn't want to jump between delay times, it wants to smoothly sweep between them. Your sweep might want 342.3. samples of delay, then 342.4, then 342.5, etc. But, you won't get that. Instead, you get 342 samples of delay, then you get 342 again, then it jumps to 343 samples, then you get 343 again, etc. *That* is the quantization that (I think) is the problem.

When sampling at 44.1kHz, one sample of delay is 0.023 msec. That seems to be a nice and small number, but remember that your chorus is trying to smoothly (and slowly) sweep through only ~20 msec of delay. Its sweep rates are usually somewhere around 1 Hz (so, about 20 msec/sec). Because the digital delay amount is quantized to the nearest sample, you're going to get an undesirable little jump in delay time a whole bunch of times: (20 msec/sec) / 0.023 msec = 870 times per sec. Yeah, you're going to have this quantization artifact happening around 870 Hz (along with lots of other weird frequencies related to this one). You're gonna hear that. Ick.

It's this quantization of the delay that is a problem. Interpolation will definitely help. Algorithms that are designed for smooth shifting of phase (ie, delay) are really the right answer. Those kinds of algorithms take more smarts, though. I've not studied them and I don't know how to implement them.

Chip
 
As you found, the delay object does not expect to be changed very often...it isn't designed that way. At best, it might be possible to change the delay time for every block of audio data, which is every 128 samples. At 44.1 kHz, this would be an update every ~3ms. Even if you were able to change the delay time that fast, I think that it'll sound bad as this updating will sound nothing like the smooth changes that would happen if it were updated every sample.

To update every sample (via a software LFO or via a wavetable...however you want), you'd need to re-write the guts of the delay effect object. And if you're going to do that, you should probably start with the chorus object instead as that might be easier.

Chip

Earlier this year I was working on a chorus for my BALibrary. In order to get the chorus sounding right with no glitches, I had to do exactly what Chip says here. You cannot use a static delay value for each audio block. The delay value must be uniquely calculated for every single sample. And this delay value is unlikely to fall right on the buffered samples so you must linearly interpolate between the samples. It gets worse because delay can be increasing or decreasing, the actual two audio samples from the delay buffer you're interpolating from can overflow into the next audio buffer block, or underflow into the previous one! So, you need more than one audio block.

If you don't smoothly modulate the delay on a sample-by-sample basis, you will get glitching as the delay value jumps at each audio block boundary.

In order to support a proper chorus effect, I added some helper tools to my BALibrary, an LFO class, and updated my AudioDelay class to support fractional sample delays via linear interpolation.

I have a working version of my chorus on a branch here:
https://github.com/Blackaddr/BALibrary/tree/feature/AudioEffectAnalogChorus

It works just fine but I haven't had time to fine tune the the IIR filters or the parameter ranges yet to make it sound really musical which is why it's not merged or announced in a thread yet. It takes a lot more work to get an effect model sounding "good" after you get it sounding "functional", ie. glitch free.

It starts to get interesting around here:
AudioEffectAnalogChorus.cpp

A working demo where the chorus parameters are controlled by the TGA Pro wtih Expansion Board can be found here.
 
Have a look at the latest code: https://github.com/quarterturn/due_ensenble_chorus/tree/master/chorus_test. If you ignore the Due-specific stuff you should easily get it working on a Teensy. It's now quite close to something like my SRE330 in triple BBD mode.

There is still glitching which seems to be LFO dependent. I can't see any place I am under or overflowing, nor do I think I'm not properly wrapping around in the ring buffer. Grrr so close!

I thought I'd need oversampling and interpolation but it seems to work fine (other than the glitching) to let the LFO wavetable speed up or slow down the reads from the circular buffers (I have one for each voice). I hope someone can give my code a shot and figure out the issue with the LFO glitching. I hope it's something simple and not a fundamental issue.
 
Here's a link to a quick video demo: https://youtu.be/trdEuEkJ5kU

I used my Casio MT-600, since it's battery operated and easy to get close to my code desk. I chose the driest preset it has, which is "violin". There's a tiny bit of chorus in it, but you can definitely hear the springy animation of the ensemble chorus. Also you can hear how the unwanted noise tracks the LFO.
 
Try declaring this lot as volatile:
Code:
volatile int16_t in_ADC0, in_ADC1, out_DAC0, out_DAC1;  //variables for 2 ADCs values (ADC0, ADC1)
volatile int16_t POT0, POT1, POT2; //variables for 3 pots (ADC8, ADC9, ADC10)

and these:
Code:
// track the input index
volatile int16_t inIndex1 = 0;
// track the output index
volatile int16_t outIndex1 = 512; // stick it in the middle so there's room to shift it around
volatile int16_t outIndex2 = 512;
volatile int16_t outIndex3 = 512;
// track lfo index
volatile int16_t lfoIndex1 = 0;
volatile int16_t lfoIndex2 = 245;
volatile int16_t lfoIndex3 = 490;
// used to slow down reading the LFO wavetable
volatile uint16_t interruptCount = 0;
// the offset value read from the wavetable
volatile int16_t offset1 = 0;
volatile int16_t offset2 = 0;
volatile int16_t offset3 = 0;
// the resulting input index plus the offset
volatile int16_t offsetIndex1 = 0;
volatile int16_t offsetIndex2 = 0;
volatile int16_t offsetIndex3 = 0;

I'm not sure that they all need to be volatile but for sure in_ADC0 does. It is used in loop() and in TC4_Handler().

The interrupt routine does a fair bit of work. I would test how long it takes to execute to see if it is able to handle the interrupt in a timely fashion.
Add this code at the end of setup() and then move NVIC_EnableIRQ after this so that the IRQ is not enabled while this test is done (just in case):
Code:
#define TIME_INTERRUPT
#ifdef TIME_INTERRUPT
  uint32_t now = micros();
  for(int i = 0; i < 10000; i++) {
    TC4_Handler();
  }
  now = micros() - now;
  Serial.print("time = ");
// Round the result up
  Serial.print((now+5000)/10000);
  Serial.println(" us");
  while(1);
#endif

If this result is close to, or exceeds, 22 you'll have to find some way to speed up TC4_Handler().

Pete
 
The timer code returns "time = 5 us", so at least I'm not overrunning the interrupt. Every 200 interrupts it advances the LFO indexes, but it's only a few lines of code which doesn't do much. I've forced that with an 'if (1)' and it's still 5 uS. I also changed all the variables used in the interrupt code to volatile. No change in the artifact noise though.

I commented out the code which writes to the buffers and it makes the same noise. So it's able to do it even with a zeroed array.
 
Status
Not open for further replies.
Back
Top