Hey all,
I’m modifying the built in juce::Reverb class and I have some questions about it’s current design and performance. Here’s the docs↗ and the source↗ for reference. Please understand that I’m more of a “programmer” than an “audio programmer” or “C++ programmer” so I might be asking some rookie questions here.
- On line 140 of the source, you see this code in
processStereo
:
const float input = (left[i] + right[i]) * gain;
Why are the left and right channels added together before the reverb is applied? Doesn’t this preclude the possibility of a meaningful width parameter, which is supposed to introduce stereo bleed in the reverb at low width, while maintaining distinct left/right reverberations at high width? See the source at lines 76-77, when the left/right gains are calculated in setParameters
:
wetGain1.setTargetValue (0.5f * wet * (1.0f + newParams.width));
wetGain2.setTargetValue (0.5f * wet * (1.0f - newParams.width));
and 159-163, in processStereo
when the reverb is mixed back into the source buffer:
const float wet1 = wetGain1.getNextValue();
const float wet2 = wetGain2.getNextValue();
left[i] = outL * wet1 + outR * wet2 + left[i] * dry;
right[i] = outR * wet1 + outL * wet2 + right[i] * dry;
All this effort to create separate gains for the left and right channel reverb, isn’t it wasted if we mix the inputs together before calculating the reverb?
- The built in module uses eight comb filters which accumulate in series, and four all-pass filters which also act in series. I’m experimenting right now with using eight all-pass filters which are applied to the individual outputs of the comb filters. I’ve read online that both approaches are valid, but can anyone with more experience designing reverb clue me into the pros and cons of each approach and how to tune them for the best sound?
- What is the pattern in how the lengths of the filters were decided? Lines 92-93 of the source:
static const short combTunings[] = { 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 }; // (at 44100Hz)
static const short allPassTunings[] = { 556, 441, 341, 225 };
My client was surprised to learn about both the all-passes processing the accumulated comb outputs instead of each individually as well as lengths of their buffers. He expected the individual approach and for the all-pass filter lengths, when summed with the lengths of their associated comb filters, to equal a a fixed number. This would help align the the outputs in time.
I did a dirty little calculation with python, associating each all-pass filter with two comb filters, to see how they added up:
>>> cT = [556, 441, 341, 225]
>>> apT = [ 1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617 ]
>>> sum = [ 0, 0, 0, 0, 0, 0, 0, 0 ]
>>> for i in range (0, 8):
# Add the comb filter values to the sum
... sum[i] += cT[i]
...
>>> for i in range (0, 4):
# Add the all-pass filter values to sum at i and i+1 to surf the ramp
... sum[i*2] += apT[i]
... sum[(i*2) + 1] += apT[i]
...
>>> sum
[1672, 1744, 1718, 1797, 1763, 1832, 1782, 1842]
What does that translate to in the total time?
>>> max = max(sum)
>>> min = min(sum)
>>> timeShift = sampleRate * (max-min)
>>> timeShift
0.0035416666666666665
At a sampleRate of 48000, if its true that the filter buffer length determines the delay time of the samples, there could be up a 3.5ms mismatch between when samples are coming out of the different filters (note; I’m not if this is relevant in the case where the all-pass filters act on the accumulation, as in the provided source). Can anyone tell me what the pattern was for deciding the comb and all pass filter buffer lengths?