4
$\begingroup$

I'm quite new to digital signal processing and currently learning the ropes. I'm fascinated by the concept of emulating analog circuits with DSP and I'm eager to understand how one might emulate an analog oscillator digitally. What are the basic principles and steps involved in this process? Are there specific algorithms or methods that are beginner-friendly to start with?

$\endgroup$
4
  • $\begingroup$ Do you mean an arbitrary waveform? Like sinusoidal, triangular, square, sawtooth? Or just sinusoidal? There are two common ways of doing this. One is algorithmically (i.e. do the math that defines the waveform) and the other is table lookup (where one cycle of the waveform is stored in a table). $\endgroup$ Dec 8, 2023 at 16:40
  • $\begingroup$ Also, you say you're a "beginner" but what is your level about writing C or C++ code? And your basic math chops? Knowing that might save us time. $\endgroup$ Dec 8, 2023 at 16:43
  • $\begingroup$ I'm quite a beginner when it comes to DSP, but I'm not a beginner in C++ $\endgroup$
    – thc
    Dec 8, 2023 at 23:28
  • $\begingroup$ Does it even make sense if you don't know how an analog oscillator works? $\endgroup$
    – pipe
    Dec 9, 2023 at 0:53

3 Answers 3

2
$\begingroup$

Here's the other common method for an oscillator of any waveshape. It comes from this more general file implementing wavetable synthesis with up to 3 axes of control. A simple oscillator has 0 axis of control (other than the frequency).

It doesn't show how you fill up the circular lookup table with your waveform. I'll let you figure that out. It just shows how to make a waveform generator and to manage the phase-accumulator and how to do linear interpolation between points. In this implementation, the size of the wavetable must be a power of 2.

//
//  This typedef in wavetable_oscillator.h
//
typedef struct
    {
    float* output_ptr;
    int samples_per_block;

    uint32_t phase;
    int32_t phaseIncrement;
    int32_t frequencyIncrement;

    unsigned int num_fractionalBits;
    uint32_t mask_fractionalBits;       // 2^num_fractionalBits - 1
    unsigned int mask_waveIndex;
    float scaler_fractionalBits;        // 2^(-num_fractionalBits)


    float* wave000;
    } wavetable_oscillator_data;
//
// #include "wavetable_oscillator.h"
//


void wavetable_0dimensional_oscillator(wavetable_oscillator_data* this_oscillator)
    {
    float* out = this_oscillator->output_ptr;
    int num_samples_remaining = this_oscillator->samples_per_block;

    uint32_t phase = this_oscillator->phase;
    int32_t phaseIncrement = this_oscillator->phaseIncrement;
    int32_t frequencyIncrement = this_oscillator->frequencyIncrement;

    float scaler_fractionalBits = this_oscillator->scaler_fractionalBits;
    unsigned int num_fractionalBits = this_oscillator->num_fractionalBits;
    uint32_t mask_fractionalBits = this_oscillator->mask_fractionalBits;
    unsigned int mask_waveIndex = this_oscillator->mask_waveIndex;
        
    float* wave000 = this_oscillator->wave000;

    while (num_samples_remaining-- > 0)
        {
        unsigned int waveIndex0 = (unsigned int)(phase>>num_fractionalBits) & mask_waveIndex;
        unsigned int waveIndex1 = (waveIndex0 + 1) & mask_waveIndex;
        float linearGain1 = scaler_fractionalBits * (float)(phase & mask_fractionalBits);
        float linearGain0 = 1.0f - linearGain1;
        
        float _wave000 = wave000[waveIndex0]*linearGain0 + wave000[waveIndex1]*linearGain1;
        
        phase += phaseIncrement;
        phaseIncrement += frequencyIncrement;
        
        *out++ = _wave000;
        }

    this_oscillator->phase = phase;
    this_oscillator->phaseIncrement = phaseIncrement;
}
$\endgroup$
3
  • $\begingroup$ i'm checking this approach and is really cool , It aims to replicate the waveform produced by an analog oscillator using a series of precalculated samples of that waveform, stored in a table or array,sounds really cool this method $\endgroup$
    – thc
    Dec 9, 2023 at 22:57
  • $\begingroup$ This table-lookup oscillator is really old potatoes. Nowadays they call it a NCO (numerically controlled oscillator) or DDS (direct digital synthesis). Built into it is the phase-accumulator and you want wrap-around arithmetic for that. So if you're implementing this in some other language than C (or C++), make sure that the phase value wraps around and does not saturate when it is incremented. BTW, since it does linear interpolation, you want a lotta points for the lookup table. Whatever your waveform, maybe 8 times the index of the highest harmonic. $\endgroup$ Dec 10, 2023 at 1:33
  • $\begingroup$ If you're doing wavetable synthesis for music note synthesis, you should get that original file I linked to. You can dynamically change the waveform with up to 3 different axes of control. (One can be time since MIDI note onset, another can be the MIDI pitch of the note, and a third could be MIDI key velocity or some expression control, like the mod wheel.) Maybe email me and I can give you some more specific info. Doing this costs lots of memory (depends on how big the wavetable is and how many you got), but sometimes memory is cheap. $\endgroup$ Dec 10, 2023 at 1:46
3
$\begingroup$

That's a fairly broad question.

I'm eager to understand how one might emulate an analog oscillator digitally.

If you really want to model an analog circuit this is typically done by using differential equation solvers where you can put in the detailed properties of each part in the circuit. A popular example for this type of modelling software is https://www.pspice.com/

However, the best way to implement a sine wave oscillator is NOT to model an analog circuit but to develop a digital oscillator directly. As it turns out this can be done very efficiently using the recursive difference equation

$$y[n] = 2\cos(\omega) \cdot y[n-1] - y[n-2], \qquad y[0] = 1, y[1] = \cos(\omega) $$

The math behind this is actually rather complicated, but the implementation is almost trivial (see the Matlab code below).

Are there specific algorithms or methods that are beginner-friendly to start with?

I strongly recommend taking a formal class on digital signal processing. The subject is fairly math heavy and the fundamentals are important and not particularly intuitive.

%% oscilator parameters
fs = 48000; % sample rate in Hz
fosc = 1000; % oscilator frequency in Hz
nx = 1024; % number of samples

% initialization
omega = 2*pi*fosc./fs;  % normalized frequency
a1 = -2*cos(omega);
y = zeros(nx,1);
y(1) = 1;
y(2) = cos(omega);

% run the actual oscilator sample by sample
for i = 3:nx
  y(i) = -a1*y(i-1) - y(i-2);
end

% and plot it
plot((0:nx-1)/fs,y);
$\endgroup$
4
  • $\begingroup$ This is great information, I'm currently reading "Understanding Digital Signal Processing," which has been really insightful. I understand the efficiency of developing a digital oscillator directly as you've demonstrated...but for it sounds like analog i have to add something like saturation to it? or or simply the difference is minimal $\endgroup$
    – thc
    Dec 8, 2023 at 21:21
  • $\begingroup$ Nowadays programmers seem to use BLEP based implementations. See KVR's DSP and Plugin Dev forum as for an example kvraudio.com/forum/viewforum.php?f=33 . Dunno if these sounds more analog than any other. $\endgroup$
    – Juha P
    Dec 9, 2023 at 16:37
  • $\begingroup$ + here's Martin Finke's Blog with some code - martin-finke.de and a demonstration at shadertoy - shadertoy.com/view/wljXWy# $\endgroup$
    – Juha P
    Dec 9, 2023 at 20:46
  • $\begingroup$ wow nice , i'll take a look $\endgroup$
    – thc
    Dec 10, 2023 at 0:34
2
$\begingroup$

Because you say you are emulating an oscillator rather than simulating an oscillator, I assume that your definition of it "acting right" is that it gives some desired result, rather than being the most accurate prediction of how an actual circuit will work.

In that case, the two most popular methods that I know of are to:

  1. Make a simple linear sampled-time system with a complex conjugate pair of poles on the unit circle in $z$. Give this system a starting state, and let it run. You'll find that, in practice, the states will grow or shrink, to a speed that's roughly inversely proportional to your bits of precision.
    1. You can overcome this by only letting the oscillator run for a finite amount of time. Clearly this is not desirable if you want a steady tone forever.
    2. You can, with more complexity, overcome this by monitoring the state amplitudes and servoing them, making the poles slightly damping if it is too large, and slightly unstable if it is too small.
  2. Maintain a phase accumulator, of sufficient precision to satisfy your performance desires. Make it so that it rolls over every $2 \pi$ radians. Take the phase output, apply it to a function (i.e., $\sin \theta$) or a look-up table (a "wavetable"), and take that output as your oscillator output.
    • In a digital signal processing context, the easiest way to implement the phase accumulation is to:
      • Choose a data path width that is convenient for your processor -- i.e. 16, 32, 64, etc.
      • Then for an $N$-bit word, declare that the phase is scaled such that $2^N$ corresponds to $2 \pi$ radians.
      • Then at each step, just add your phase increment, and allow overflow to happen. This won't be as trivial these days as it was in days of yore, but in C/C++ you do it by using unsigned integer arithmetic (which specifically allows overflow to "just happen"), in Rust you do it by using the "Wrapping" type, and I'm pretty sure that in Python you also do it by using unsigned integer arithmetic.
    • You can also maintain your phase accumulator in floating point, and just subtract $2 \pi$ from it whenever its magnitude equals or exceeds $2 \pi$ -- but if your product is the least bit sensitive to cost and/or speed, this won't be a good approach.
$\endgroup$
4
  • $\begingroup$ I'm trying to wrap my head around these concepts, I note it, unlike the wavetable method this is more computer intensive to me $\endgroup$
    – thc
    Dec 9, 2023 at 23:16
  • $\begingroup$ My second suggestion can have the wave function implemented as a wavetable, and often is. It's just written in more detail than the other "wavetable" answer. $\endgroup$
    – TimWescott
    Dec 10, 2023 at 17:01
  • 1
    $\begingroup$ Tim, the code I posted does a simple phase accumulator with a uint32_t for the phase accumulator value. That's enough bits with a frequency precision of $2^{-32}$ cycle per sample. $\endgroup$ Dec 10, 2023 at 17:22
  • $\begingroup$ RBJ: I saw your answer and liked it (and Hilmar's for that matter). I thought it would be good to put up answers just in English, stripped of code or math, in case that floats someone's boat more easily than otherwise. $\endgroup$
    – TimWescott
    Dec 10, 2023 at 17:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.