Simulated 1 Volt Per Octave

DSP, Plugin and Host development discussion.
Post Reply New Topic
RELATED
PRODUCTS

Post

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?

Post

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.
We are the KVR collective. Resistance is futile. You will be assimilated. Image
My MusicCalc is served over https!!

Post

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?
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.

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.

Post

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).

Post

You can approximate 2^x as:

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;
}
Results can be ~0.08 cents flat to ~0.04 cents sharp.

Post

BertKoor wrote: Wed Aug 07, 2019 7:35 am Or try to implement Feynman's algorithm ;-)
:dog: exp, not log!
We are the KVR collective. Resistance is futile. You will be assimilated. Image
My MusicCalc is served over https!!

Post

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.

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;
  );
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.

Post

MadBrain wrote: Wed Aug 07, 2019 8:15 pm You can approximate 2^x as:
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).

Post

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 :D
I'm not an EE expert though, maybe it does not make any sense.

Post

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)`.

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

Post Reply

Return to “DSP and Plugin Development”