Distorting Oscillators art work

IngoognI

A blog.

Swing POV-Ray Swing Ingo 2021-04-04

An oscillators creates a periodic signal swinging between a maximum and minimum value. This periodic signal can have many shapes. I created an include file with the basic wave forms. I'll also show two ways to design oscillators with complex wave shapes. But first,

A word of warning

MIND YOUR EARS!

Experiments with sound synthesis can create extremely loud and nasty sounds.

Don't use headphones! Turn your amps down! Build attenuators into your scripts!

Check the resulting waveform in an audio editor before listening. Audacity or Ocenaudio for example. Both support multiple platforms.

Parameters

Most oscillators here have the same set of parameters:

Freq:
(float, Hz) frequency of wave form.
Phase:
(float, radians) offset of the starting point of the wave form.
Amp:
(float) amplitude of the wave form.
Tick:
(float) one step for progressing the wave form.
SRate:
(int, Hz) sample rate. For CD quality 44100 Hz.

Note: TAU is used in the include file. TAU = 2 * pi;

SineOsc

The sine wave oscillator showed up a few times before. Besides generating sine waves, it is also a useful building block to create other oscillators. The one in the next paragraph, the square wave oscillator is one of them.

Sine wave oscillator, wave form
/*:Sine wave oscillator*/
#declare SinOsc = function(Freq, Phase, Amp, Tick, SRate){
  Amp * sin((Tick * TAU * (Freq/SRate)) + Phase)
};

SquareOsc

There are quite a lot of ways of generating a square wave oscillator. This one is based on the sine wave and follows the sign of the sign wave function. It skips all intermediate values and flips from maximum +1 to minimum -1 and back. (the sgn function comes from the math.inc standard include file)

The square wave is a special case of the pulse oscillator, with a duty cycle 0f 50%, as I'll show later on

Square wave oscillator, wave form
/*:Square wave oscillator*/
#declare SquareOsc = function(Freq, Phase, Amp, Tick, SRate){
  Amp * sgn(SinOsc(Freq, Phase, 1, Tick, SRate))  
}

Ramp, a building block

The ramp function is a major building block in creating oscillators. It goes from 0 to 1 in one period and then drops back to 0.

Ramp wave form

The ramp function, or phasor, is never used directly as all its energy is delivered on one side of the x-axis. This means a high direct current component in the signal and that's not something the voice coils of loudspeakers like.

/*:Ramp Oscilator, or Phasor, goes from 0 to 1 over 1 Frequency period*/
#declare Ramp = function(Freq, Phase, Tick, SRate){
  mod(Tick + ((Phase/Freq)/(TAU)) * SRate, SRate/Freq) * Freq/SRate
}

SawOsc

The simplest oscillator based on the ramp wave is the saw tooth. It is just a ramp function translated down to get an even spread on the positive and negative side.

Saw wave oscillator, wave form

The sawtooth has a wide and rich harmonic spectrum.

/*:Sawtooth oscillator*/
#declare SawOsc = function(Freq, Phase, Amp, Tick, SRate){
  Amp * (Ramp(Freq, Phase, Tick, SRate) * 2 - 1)
}

PulseOsc

The pulse oscillator is like a square wave oscillator, but with a variable duty cycle. The duty cycle runs from 0 to 100%. At the outer limits there is no oscillation. A flat signal at min or max is the result. Avoid these extreme values.

Pulse wave oscillator, wave form at 33% duty cycle

The code shows why the Ramp function is so useful. Look at the SelectDuty sub-function. R is the ramp function result, D the duty cycle. You can read this code as, "while the ramp function is smaller than the duty cycle value the result is +1. Past that point the result is -1."


/*:Pulse wave oscillator*/
#local SelectDuty = function(R,D){
  select(
    R - D,
    1,
    -1    
  )
}
#declare PulseOsc = function(Freq, DutyCycle, Phase, Amp, Tick, SRate){
  Amp * (SelectDuty(Ramp(Freq, Phase, Tick, SRate), DutyCycle/100))
}

TriOsc

In creating the triangle waves the same method as in PulseOsc is used. It is taken a bit further by chopping the ramp up in more pieces. Is the result of the ramp smaller than 0.25, is it between 0.25 and 0.75 or is it bigger than 0.75?

Triangle wave oscillator, wave form
/*:Triangle wave Oscilator*/
#declare TriSelect = function(R) {
  select(
    R - 0.25,
    R,
    select(
      R - 0.75,
      0.5 - R,
      R - 1
    )
  )
};
#declare TriOsc = function(Freq, Phase, Amp, Tick, SRate) {
  4*Amp*TriSelect(Ramp(Freq, Phase, Tick, SRate))
};

SawSinOsc

And again the ramp wave is used. Now a section of it is used to implant a half sine wave when the result is smaller than 0.5. Above that a saw tooth is used.

This method of using segments of the ramp function for other functions is a powerful possibility to build complex wave forms. Yet I find coding these using the select function in POV-Ray tedious. In the next section I show an alternative, although both methods will not give you the same results.

SawSin wave oscillator, wave form
/*:SawSin oscillator, half a period sine wave, halve a period saw tooth*/
#declare SawSinSelector = function(R, S){
  select(
    R - 0.5,
    S,
    R * 2 - 1
  )  
}
#declare SawSinOsc = function(Freq, Phase, Amp, Tick, SRate){
  Amp * (
    SawSinSelector(
      Ramp(Freq, Phase, Tick, SRate), 
      SinOsc(Freq, Phase, 1, Tick, SRate)
    ) * 2 - 1)  
};

Spline oscillators

With splines you can "draw" your own wave forms. Guess what, we use the ramp function results as spline parameters to query for the position vector. The result is a vector, but the oscillator can only deal with a float. So I take the .x value of the resulting output.

It is not possible to pass a spline as a parameter to a function. An in this case it is a good thing. It means you have to create one specific oscillator for one spline and things won't get mixed up. An example in the code below.

Spline wave oscillator, wave form
#declare SPL = function{
  spline {
    linear_spline
    0.00, <0.25,0>
    0.25, <-1,0>
    0.50, <1,0>
    0.98, <-1,0>
    1.00, <0.25,0>
  }
};
#declare SplineOsc = function(Freq, Phase, Amp, Tick, SRate){
  Amp * SPL(Ramp(Freq, Phase, Tick, SRate)).x  
}

Points of attention

When designing a spline oscillator, keep an eye on the maximum and minimum output values. Keep them within the [-1,1] range with a built in attenuator. Especially for the curvy splines, when all control points are within the limits, the curve can still escape them.

The spline is driven by the ramp function, that is set at a certain frequency. When your spline crosses the x-axis more tan once the pitch you'll hear is above the set frequency. This does not mean you should only cross once. Go wild.

Steep angles make richer sounds with more harmonics. Discontinuities in the curve also do this. Sharp discontinuities result in harsh sounds. For smooth sounds take care that the endpoints of the curves connect smoothly

Multiple splines

A thought that occurred to me while writing this and looking at the SplineOsc. A spline can have five dimensions, so we can define five different wave forms within it. Instead of selecting the .x as in the example, there is the choice of .x .y .z .filter .transmit. It can be automated. It can be modulated. It can be randomised.

Now imagine that the five dimension for one t value also form a spline. Or, can be interpolated as a spline. This means we can morph from one wave form to another with just one container object and two "clock" values, t0, t1.

Thinking of that, two variables, u, v. A parametric function to pick values from a 5D spline. Feels like we're in Wave Terrain Synthesis again. Just ideas. Something to figure out in time.

Visualisation and files

For your eyes

A quick and simple oscilloscope scene file for visualising the wave forms. The sample rate sets the resolution. Higher frequencies need higher sample rates. But the scene always only show five cycles. Keep the frequency low ~20Hz.

#declare nWaves = 5;
#declare Freq = 20;
#declare Dur = nWaves/Freq;
#declare SampleRate = 10000;
#declare Samples = Dur * SampleRate;
#declare Tick = 0;
#while (Tick < Samples) 
  
  #declare Sample = SawOsc(Freq, 0, 1, Tick, SampleRate);
  
  sphere{
    <(Tick/Samples)*nWaves, Sample, 0>, 0.02
    pigment{rgb 1}
  }
  #declare Tick = Tick + 1;
#end

 
cylinder{
  <0,1,0.1>, <nWaves+0.5,1,0.1>, 0.01
  pigment{rgb<1,0,0>}
}
cylinder{
  <0,0,0.1>, <nWaves+0.5,0,0.1>, 0.01
  pigment{rgb<1,0,0>}
}
cylinder{
  <0,-1,0.1>, <nWaves+0.5,-1,0.1>, 0.01
  pigment{rgb<1,0,0>}
}
cylinder{
  <0,-3,0.1>,<0,3,0.1>, 0.01
  pigment{rgb<1,0,0>}
}

light_source{<0,0,-2500> color rgb 1}

camera {
  angle 3.5 
  location  <nWaves/2, 0,-100>
  right     x*image_width/image_height
  look_at   <nWaves/2, 0, 0.0>
}

Files

The povosc.inc include file has a demo scene at the end of it. You can select to render sound or image. Run the povosc.inc file as it it were a .pov file. The waveheader.inc file is required to render audio and is included in the zip file

Download POVSound.zip