Simulated 1 Volt Per Octave
-
- KVRAF
- Topic Starter
- 3080 posts since 17 Apr, 2005 from S.E. TN
I wonder if there is a more "computationally efficient" way to implement the following:
Lets name a computer version of 1 Volt Per Octave Control Voltage, DCV (Digital Control Voltage). This is such a common idea it may be reinventing the wheel for the nth time, dunno.
Furthermore:
DCV = log2(Frequency)
Frequency = 2 ^ DCV
Therefore DCV of 0 = 1 Hz. DCV of -2 = 0.25 Hz. DCV of 2 = 4 Hz. DCV of 14.28771 = 20 kHz.
Frequency of MIDI Note 0 would be 440 * 2 ^ (-69 / 12) = 8.1758 Hz. DCV of MIDI Note 0 would be log2(8.1758) = 3.0314
Frequency of MIDI Note 127 would be 440 * 2 ^ ((127 - 69) / 12) = 12543.854 Hz. DCV of MIDI Note 127 would be log2(12543.854) = 13.615
___ What's the Point? ____
You can simple-add many control signals. You can mix and match both mono-polar and bi-polar control signals. The simple sum will always provide perfectly symmetrical modulation in log frequency space. Same advantage as 1 Volt Per Octave in analog synths.
For instance if we instantiate 2 DCV-input LFOs-- The LFO has default output amplitude of +/- 1.0.
[DCV = 0]-->LFO1---->LFO2---->
LFO1, with a DCV input of 0, runs at 1 Hz. Driving the DCV input of LFO2 with LFO1 output, LFO2 will have a nominal center frequency of 1 Hz, modulating up as high as 2 Hz and modulating down to 0.5 Hz. A +/- 1 octave modulation balanced around the center frequency, in log frequency space.
OK, now lets add to LFO1 a constant DCV bias of 1.0. The LFO2 modulation will now go from 0 to +2, and LFO 2 will have a center frequency of 2 Hz, modulating up to 4 Hz and down to 1 Hz. We still have the same +/- 1 octave modulation, balanced in log frequency space.
We can change LFO2 center frequency without affecting the modulation amount by tuning the DCV Bias. We can change the LFO2 amount of modulation without affecting the center frequency by attenuating or boosting LFO1 output before it is added to the DCV bias. If we just want to modulate +/- 1 semitone, then we could add [DCV Bias + LFO1Out / 12].
Yadda yadda. Typical 1 V Per Octave analog synth concepts. One practical reason I'd want to go to the extra bother, is to cross-modulate LFO1 and LFO2 to get a pair of chaotic-frequency LFO's. The output of LFO1 causes LFO2 to stagger in rate and the staggering rate of LFO2 causes LFO1 to stagger in rate. Back when I built those in analog circuits and they can be fun.
The first victim target of the Chaotic LFO pair would be a stereo Phaser. LFO1 drives Left channel center frequency, LFO2 drives Right channel center frequency.
One knob for Phaser Center Frequency and a second knob for Phaser Modulation amount in semitones.
By using the same DCV scheme to tune the Phaser Allpass filters, it would be very precise-- For instance if the Allpass Filter DCV Bias is set to tune for 1000 Hz center frequency and the LFO outputs are unattenuated at +/- 1.0, then there would be smooth sweep between 500 Hz and 2000 Hz, with the sweep having the same "average velocity" in both the positive and negative directions (in log frequency space). To my ear some phasers and flangers don't have even sweep speeds, and using such a DCV modulation scheme ought to be as even/smooth a pitch sweep that the ear could expect. The Sweep Distance would remain +/- 1 octave regardless where the Center Frequency DCV Bias knob is set for the allpass filters. Or alternately, if you change the attenuation of the LFO outputs, the Center Frequency stays exactly the same as the distance of sweep is increased or decreased.
____ Can it be done more efficient? ____
The biggest issue is that this method, modulating multiple LFO's and Filters per-sample with the DCV, is that it could involve a lot of overhead calling 2^x .
Is there some less-expensive way to calc DCV_To_Frequency() than calling 2^DCV ?
Interpolated lookup tables would be easy and maybe faster than 2^x. Dunno. Maybe there is some less-cpu-expensive way to directly calculate it?
Lets name a computer version of 1 Volt Per Octave Control Voltage, DCV (Digital Control Voltage). This is such a common idea it may be reinventing the wheel for the nth time, dunno.
Furthermore:
DCV = log2(Frequency)
Frequency = 2 ^ DCV
Therefore DCV of 0 = 1 Hz. DCV of -2 = 0.25 Hz. DCV of 2 = 4 Hz. DCV of 14.28771 = 20 kHz.
Frequency of MIDI Note 0 would be 440 * 2 ^ (-69 / 12) = 8.1758 Hz. DCV of MIDI Note 0 would be log2(8.1758) = 3.0314
Frequency of MIDI Note 127 would be 440 * 2 ^ ((127 - 69) / 12) = 12543.854 Hz. DCV of MIDI Note 127 would be log2(12543.854) = 13.615
___ What's the Point? ____
You can simple-add many control signals. You can mix and match both mono-polar and bi-polar control signals. The simple sum will always provide perfectly symmetrical modulation in log frequency space. Same advantage as 1 Volt Per Octave in analog synths.
For instance if we instantiate 2 DCV-input LFOs-- The LFO has default output amplitude of +/- 1.0.
[DCV = 0]-->LFO1---->LFO2---->
LFO1, with a DCV input of 0, runs at 1 Hz. Driving the DCV input of LFO2 with LFO1 output, LFO2 will have a nominal center frequency of 1 Hz, modulating up as high as 2 Hz and modulating down to 0.5 Hz. A +/- 1 octave modulation balanced around the center frequency, in log frequency space.
OK, now lets add to LFO1 a constant DCV bias of 1.0. The LFO2 modulation will now go from 0 to +2, and LFO 2 will have a center frequency of 2 Hz, modulating up to 4 Hz and down to 1 Hz. We still have the same +/- 1 octave modulation, balanced in log frequency space.
We can change LFO2 center frequency without affecting the modulation amount by tuning the DCV Bias. We can change the LFO2 amount of modulation without affecting the center frequency by attenuating or boosting LFO1 output before it is added to the DCV bias. If we just want to modulate +/- 1 semitone, then we could add [DCV Bias + LFO1Out / 12].
Yadda yadda. Typical 1 V Per Octave analog synth concepts. One practical reason I'd want to go to the extra bother, is to cross-modulate LFO1 and LFO2 to get a pair of chaotic-frequency LFO's. The output of LFO1 causes LFO2 to stagger in rate and the staggering rate of LFO2 causes LFO1 to stagger in rate. Back when I built those in analog circuits and they can be fun.
The first victim target of the Chaotic LFO pair would be a stereo Phaser. LFO1 drives Left channel center frequency, LFO2 drives Right channel center frequency.
One knob for Phaser Center Frequency and a second knob for Phaser Modulation amount in semitones.
By using the same DCV scheme to tune the Phaser Allpass filters, it would be very precise-- For instance if the Allpass Filter DCV Bias is set to tune for 1000 Hz center frequency and the LFO outputs are unattenuated at +/- 1.0, then there would be smooth sweep between 500 Hz and 2000 Hz, with the sweep having the same "average velocity" in both the positive and negative directions (in log frequency space). To my ear some phasers and flangers don't have even sweep speeds, and using such a DCV modulation scheme ought to be as even/smooth a pitch sweep that the ear could expect. The Sweep Distance would remain +/- 1 octave regardless where the Center Frequency DCV Bias knob is set for the allpass filters. Or alternately, if you change the attenuation of the LFO outputs, the Center Frequency stays exactly the same as the distance of sweep is increased or decreased.
____ Can it be done more efficient? ____
The biggest issue is that this method, modulating multiple LFO's and Filters per-sample with the DCV, is that it could involve a lot of overhead calling 2^x .
Is there some less-expensive way to calc DCV_To_Frequency() than calling 2^DCV ?
Interpolated lookup tables would be easy and maybe faster than 2^x. Dunno. Maybe there is some less-cpu-expensive way to directly calculate it?
- KVRAF
- 15269 posts since 8 Mar, 2005 from Utrecht, Holland
Hmmm... "One" represents an octave. Twelve notes in that, each a hundred cents, so 1200 cents per octave. You need a precision of cents or better, say 1/2000 = 0.0005.
An interpolation table could work, or polynominal approximation. Or try to implement Feynman's algorithm
Nice project to test out some different strategies and compare their performance.
An interpolation table could work, or polynominal approximation. Or try to implement Feynman's algorithm
Nice project to test out some different strategies and compare their performance.
We are the KVR collective. Resistance is futile. You will be assimilated.
My MusicCalc is served over https!!
My MusicCalc is served over https!!
- KVRian
- 1253 posts since 31 Dec, 2008
The main problem with lowering CPU in such a case is precision. pitch is extremely sensitive to precision. Human ears can detect very slight variations with pitch easily.JCJR wrote: ↑Tue Aug 06, 2019 5:24 pm
____ Can it be done more efficient? ____
The biggest issue is that this method, modulating multiple LFO's and Filters per-sample with the DCV, is that it could involve a lot of overhead calling 2^x .
Is there some less-expensive way to calc DCV_To_Frequency() than calling 2^DCV ?
Interpolated lookup tables would be easy and maybe faster than 2^x. Dunno. Maybe there is some less-cpu-expensive way to directly calculate it?
I guess the question is how much you can get a way with approximation vs. how much CPU saving is there. I haven't delved allot into it
The only thing I'm doing so far (may be obvious) is to store the last frequency 2^X value and never call 2^X again unless X changes. This works well with midi notes as they are sparse, but doesn't do any thing for sample by sample FM.
www.solostuff.net
Advice is heavy. So don’t send it like a mountain.
Advice is heavy. So don’t send it like a mountain.
- KVRist
- 296 posts since 1 Apr, 2009 from Hannover, Germany
Depending on how often you do it, the pow() will be more or less expensive. You don't have to do it for every sample.
It's a good idea to try and stay in the DCV realm as long and widely as possible. The expensive part is when you have to finally get to some filter coefficients or whatever, and then you can probably optimize larger parts of those computations with an interpolated table lookup or two (after doing a bit of math).
It's a good idea to try and stay in the DCV realm as long and widely as possible. The expensive part is when you have to finally get to some filter coefficients or whatever, and then you can probably optimize larger parts of those computations with an interpolated table lookup or two (after doing a bit of math).
-
- KVRian
- 1000 posts since 1 Dec, 2004
You can approximate 2^x as:
Results can be ~0.08 cents flat to ~0.04 cents sharp.
Code: Select all
#include <math.h>
float fast_exp(float x) {
// warning : doesn't work on negative numbers or numbers over 30.9999.. !
int whole = (int)x;
float z = x - whole;
float polynomial_approximation = ((1.f/24)*z*z*z*z + (1.f/6)*z*z*z + (3.f/4)*z*z + (13.f/6)*z)*(8.f/25) + 1.f;
float out = (1 << whole) * polynomial_approximation;
return out;
}
- KVRAF
- 15269 posts since 8 Mar, 2005 from Utrecht, Holland
exp, not log!
We are the KVR collective. Resistance is futile. You will be assimilated.
My MusicCalc is served over https!!
My MusicCalc is served over https!!
-
- KVRAF
- Topic Starter
- 3080 posts since 17 Apr, 2005 from S.E. TN
Thanks for the good ideas. I had read some opinions that older "tricks with treating IEEE floats as ints" and various approximations, and even table lookups, are not necessarily faster than just calling exp() and log() on modern cpus but I don't have recent personal experience.
Think I have an RBJ fast EXP2 somewhere that I ported into Reaper js awhile back, that is not quite as hairy as MadBrain's version but would have to look it up again to verify. As said, dunno if it would really be quicker anyway. I never constructed a js speed test to compare it against an exp-based exp2. Supposedly of the built-in functions available in js (pow() etc), ordinary log() and exp() are the fastest variants. I'm too old and don't have enough patience to fool with C++ and VST's at the moment. Too low a tolerance for frustration in my dotage. Would be more like a job than a hobby.
Js "ordinary" log2 and exp2 I've been using. It would run faster without the safety numeric if:then checks, which could be removed if the calling code is always guaranteed error-free.
Re lookup table and accuracy, it would depend on how critical pitch accuracy might be. Maybe something "real smooth" for an LFO or tracking filter could have too much slop for a real-critical audio oscillator?
Kicking around the lookup table idea-- A table of double floats, Hz Frequencies or maybe some other equivalent more convenient for some task-- A DCV range of -4 up to 14 would span a frequency range from 0.0625 Hz (LFO Period of 16 seconds) up to a highest frequency of 16384 Hz. Maybe some folks would want a slightly bigger "legal DCV span" in the lookup table, dunno.
A DCV range of -4..14 would span 18 octaves. If we want a semitone lookup table that would be something like 18 * 12 + 1 elements, a 217 element array.
So every exact semitone based on 1 Hz would be "perfect". But in an application where we want every concert-tuned chromatic musical pitch "perfect" then the table could be offset to line up perfect with A-440 or other reference, rather than line up perfect semitones based on 1 Hz.
I spot-checked linear interpolation within a semitone frequency interval and the error doesn't seem ridiculous--
InterpolationPercent____Cents
05%_________________05.140
10%_________________10.264
20%_________________20.467
30%_________________30.611
40%_________________40.696
50%_________________50.722
60%_________________60.690
70%_________________70.602
80%_________________80.457
90%_________________90.256
95%_________________95.135
Seems less than a cent error linear interpolating between exact semitone frequencies. I was surprised that the biggest error seems to be in the middle. Have never been none too swooft on math but I suspected that it would show underestimation errors on one end and overestimation errors on the other end. And suspected that some errors might turn out bigger than 1 cent.
Think I have an RBJ fast EXP2 somewhere that I ported into Reaper js awhile back, that is not quite as hairy as MadBrain's version but would have to look it up again to verify. As said, dunno if it would really be quicker anyway. I never constructed a js speed test to compare it against an exp-based exp2. Supposedly of the built-in functions available in js (pow() etc), ordinary log() and exp() are the fastest variants. I'm too old and don't have enough patience to fool with C++ and VST's at the moment. Too low a tolerance for frustration in my dotage. Would be more like a job than a hobby.
Js "ordinary" log2 and exp2 I've been using. It would run faster without the safety numeric if:then checks, which could be removed if the calling code is always guaranteed error-free.
Code: Select all
LN2_VAL = log(2.0);
INV_LN2_VAL = 1.0 / LN2_VAL;
//arbitrary limits are imposed, which may or may not be sensible
MIN_LOG2_INPUT = 10 ^ -15; //about -300 dB
MIN_LOG2_OUTPUT = log(MIN_LOG2_INPUT) * INV_LN2_VAL;
MAX_LOG2_INPUT = 10 ^ 15; //about +300 dB
MAX_LOG2_OUTPUT = log(MAX_LOG2_INPUT) * INV_LN2_VAL;
function Log_2(a_x)
local (l_result)
(
((a_x += MIN_LOG2_INPUT) >= MAX_LOG2_INPUT) ?
l_result = MAX_LOG2_OUTPUT
:
l_result = log(a_x) * INV_LN2_VAL;
l_result;
);
function Exp_2(a_x)
local (l_result)
(
(a_x <= MIN_LOG2_OUTPUT) ?
l_result = MIN_LOG2_INPUT
:
(a_x >= MAX_LOG2_OUTPUT) ?
l_result = MAX_LOG2_INPUT
:
l_result = exp(a_x * LN2_VAL);
l_result;
);
Kicking around the lookup table idea-- A table of double floats, Hz Frequencies or maybe some other equivalent more convenient for some task-- A DCV range of -4 up to 14 would span a frequency range from 0.0625 Hz (LFO Period of 16 seconds) up to a highest frequency of 16384 Hz. Maybe some folks would want a slightly bigger "legal DCV span" in the lookup table, dunno.
A DCV range of -4..14 would span 18 octaves. If we want a semitone lookup table that would be something like 18 * 12 + 1 elements, a 217 element array.
So every exact semitone based on 1 Hz would be "perfect". But in an application where we want every concert-tuned chromatic musical pitch "perfect" then the table could be offset to line up perfect with A-440 or other reference, rather than line up perfect semitones based on 1 Hz.
I spot-checked linear interpolation within a semitone frequency interval and the error doesn't seem ridiculous--
InterpolationPercent____Cents
05%_________________05.140
10%_________________10.264
20%_________________20.467
30%_________________30.611
40%_________________40.696
50%_________________50.722
60%_________________60.690
70%_________________70.602
80%_________________80.457
90%_________________90.256
95%_________________95.135
Seems less than a cent error linear interpolating between exact semitone frequencies. I was surprised that the biggest error seems to be in the middle. Have never been none too swooft on math but I suspected that it would show underestimation errors on one end and overestimation errors on the other end. And suspected that some errors might turn out bigger than 1 cent.
- KVRAF
- 7890 posts since 12 Feb, 2006 from Helsinki, Finland
You should probably use this one from 2DaT; it's scaled for base-e, but you can skip the multiply by l2e:
viewtopic.php?p=7170633#p7170633
Also in general, bitshift+multiply limits your range unnecessarily without any real benefit compared to just adding the integer part into the floating point exponent directly (which basically works as long as you don't under/overflow the "normal" floating-point range).
-
- KVRian
- 631 posts since 21 Jun, 2013
That's the point of having fast 2^x function. If you want accurate modulation, then you want to compute it for every sample, for every oscillator, for every voice; that can get expensive, so it's a good candidate for optimization.
But it can get more interesting: some analog exponentiators are sloppy and don't exponentiate that good on the upper range and get flat, resembling an Omega function.
https://en.m.wikipedia.org/wiki/Wright_Omega_function
I think it is possible to get reasonably fast approximation of this non elementary function, but that would be non elementary
I'm not an EE expert though, maybe it does not make any sense.
But it can get more interesting: some analog exponentiators are sloppy and don't exponentiate that good on the upper range and get flat, resembling an Omega function.
https://en.m.wikipedia.org/wiki/Wright_Omega_function
I think it is possible to get reasonably fast approximation of this non elementary function, but that would be non elementary
I'm not an EE expert though, maybe it does not make any sense.
- KVRist
- 323 posts since 19 Jul, 2008
If MadBrain's code was fixed to scale x by log(2) before taking the expansion, it'd be a -1.30 cent error at the upper extreme. I think your 0.08 and 0.04 cent error calculation is incorrect. You could add a fifth term to the series to get a -0.15 cent error.
Better yet, you could expand to fourth order around 1/2 instead of 0 to get a maximum error of 0.69 cents. The problem is that there's a discontinuity at x=1,2,3,... and you would definitely hear a jump with pitch sweeps.
You could instead expand to *third* order (let's expand 2^x rather than e^x now) around a well-chosen center 0.4654... to get an identical -1.013 cent error on both bounds so there is no discontinuity.
Fifth order would meet my own error requirements. The following code has -0.0040 cent error and is 7x faster than `std::pow(2, x)`.
Better yet, you could expand to fourth order around 1/2 instead of 0 to get a maximum error of 0.69 cents. The problem is that there's a discontinuity at x=1,2,3,... and you would definitely hear a jump with pitch sweeps.
You could instead expand to *third* order (let's expand 2^x rather than e^x now) around a well-chosen center 0.4654... to get an identical -1.013 cent error on both bounds so there is no discontinuity.
Fifth order would meet my own error requirements. The following code has -0.0040 cent error and is 7x faster than `std::pow(2, x)`.
Code: Select all
float fast2pow(float x) {
// assert(x >= 0);
int xi = x;
x -= xi;
float y = 1 << xi;
// Fifth order expansion of 2^x around 0.4752 in Horner form
y *= 0.9999976457798443f + x*(0.6931766804601935f + x*(0.2400729486415728f + x*(0.05592817518644387f + x*(0.008966320633544f + x*0.001853512473884202f))));
return y;
}
VCV Rack, the Eurorack simulator