Sine wave oscillators art work

IngoognI

A blog.

This is what actually set me on the path of experimenting with POV-Ray for sound synthesis. I was rendering images from functions and feeding them in to SuperCollider. There the images are sampled as if it where wave tables. If you can render the images, why not the sound? So, lets go.

The sound of Height Fields Ingo 2021-04-01

I didn't know at the time, but the technology actually has a name, Wave Terrain Synthesis.

A surface is scanned and the height variation is the wave form. More practically, grey scales are transformed to wave forms. And POV-Ray can produce high quality grey scale images. With the use of functions, we don't even need the actual images. 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.

The terrain

A long time ago I came across an interesting function. It lives in a folder named "Shuhei Kawachi", the creator of the function I assume. It has a nice variation and is still symmetrical.

#declare Terrain1 = function (x, y, A, B){
  ((cos(x) * cos(y) + cos((sqrt(A) * x - y) / B)*
  cos((x + sqrt(A) * y) / B) + cos((sqrt(A) * x + y) / B)*
  cos((x - sqrt(A) * y) / B)) / 3)
}

The function has to be tamed a bit, it's output is divided by three to keep the result in the range [-1,1]. For visualisation it has to be tamed even more. POV-Ray has the tendency to clip values above 1 in textures, as can be seen in the image at the top.

Scan the function

After some experiments with simple linear scanning I chose to scan with two circles. The first one controll the centre of the second one. Each one spins with its own frequency. The second circle samples the function at a rate of 44.1kHz. Hint: you can use splines in functions.

#declare SinOsc = function(Freq, Phase, Amp, Tick, SRate){
  sin((Tick * tau * (Freq/SRate)) + Phase) * Amp
}; 

#declare FCx = function(CX, Radius, Freq, Tick, SRate){
  CX + (Radius * SinOsc(Freq, pi/2, 1, Tick, SRate))
};
#declare FCy = function(CY, Radius, Freq, Tick, SRate){
  CY + (Radius * SinOsc(Freq,    0, 1, Tick, SRate))
};

The sine wave oscillator, we've seen before, drives the two other parametric functions. They provide the x and y coordinates on a circle. There actually is not much more to it. Every Tick drives the circles a step forward, a sample is taken and written to file.

The scene file below renders a 20 second sound clip and a height field that shows one position of both circles. With some effort it can all be animated :)

Image of scanning a height field for sound

Download POVSound.zip, it contains the waveheader.inc file and the Wave Terrain scene file

// author:  Ingo
// date:    2021-03-30
//------------------------------------------------------------------------
/*:Wave Terrain synthesis
*/

#version 3.7;
global_settings{ assumed_gamma 1.0 }
#default{ finish{ ambient 0.1 diffuse 0.9 }} 

#include "functions.inc"
#include "math.inc"  
#include "waveheader.inc"

#declare SinOsc = function(Freq, Phase, Amp, Tick, SRate){
  sin((Tick * tau * (Freq/SRate)) + Phase) * Amp
}; 

#declare FCx = function(CX, Radius, Freq, Tick, SRate){
  CX + (Radius * SinOsc(Freq, pi/2, 1, Tick, SRate))
};
#declare FCy = function(CY, Radius, Freq, Tick, SRate){
  CY + (Radius * SinOsc(Freq,    0, 1, Tick, SRate))
};

#declare Terrain1 = function (x, y, A, B){
    ((cos(x) * cos(y) + cos((sqrt(A) * x - y) / B)*
    cos((x + sqrt(A) * y) / B) + cos((sqrt(A) * x + y) / B)*
    cos((x - sqrt(A) * y) / B)) / 3)
}

//------SOUND------------SOUND------------SOUND------------SOUND------///

#declare RadiusC1 = 3;
#declare CircleFreqC1 = 10;
#declare CentreC1 = <0,0>;
#declare RadiusC2 = 4;
#declare CircleFreqC2 = 70;
#declare Aa = pi;
#declare Bb = 1;

#declare Dur = 20; 
#declare Amp = 0.5; 
#declare Path = "wt.wav"; 
#declare BitDepth = 16; 
#declare SampleRate = 44100;

#declare Samples = Dur * SampleRate;
#declare C = (pow(2, BitDepth) / 2.0) - 1;
   
#declare Tick = 0.0;
#fopen WaveOutFile Path write
  #declare PadByte = WriteWaveHeader(Dur, 1, BitDepth, SampleRate);
  #while(Tick < Samples)
    #declare CentreC2 = <
      FCx(CentreC1.x, RadiusC1, CircleFreqC1, Tick, SampleRate), 
      FCy(CentreC1.y, RadiusC1, CircleFreqC1, Tick, SampleRate)
    >;
    #declare SamplePos = <
      FCx(CentreC2.x, RadiusC2, CircleFreqC2, Tick,  SampleRate), 
      FCy(CentreC2.y, RadiusC2, CircleFreqC2, Tick,  SampleRate)
    >;
    #declare Sample = Terrain1((SamplePos.x), (SamplePos.y), Aa, Bb) * C * Amp;
    //#declare Sample = (f_noise3d(SamplePos.x, SamplePos.y, 0)-0.5) * C * Amp;
    #write (WaveOutFile, sint16le Sample)
    
    
    // modify, modulate parameters here .
    #declare CircleFreqC1 = adj_range2(Tick,0,Samples, 10, 30); 
    #declare Bb = adj_range2(Tick,0,Samples, 1, 3); 
    

    #declare Tick = Tick + 1;
  #end
  #if (PadByte = 1)
    #write (WaveOutFile, uint8 0)
  #end
#fclose WaveOutFile      

//------VISION------------VISION------------VISION------------VISION-----//

camera {
  angle 35
  location  <0.0 , 80.0 ,-40.0>
  right x*image_width/image_height
  look_at   <0.0 , 0.0 , 0.0>
}

light_source{<5500,2500,-2000> color rgb 0.5}

sky_sphere{ 
  pigment{ 
    gradient <0,1,0>
    color_map{ 
      [0   color rgb<1,1,1>]
      [0.6 color rgb<0.08,0.08,0.36>]
      [1.0 color rgb<1,1,1>         ]
    }
  scale 2
  }
} 

height_field{ 
  function 500, 500 {(Terrain1(x*20,y*20,pi,1.5)/2)+0.5}
  smooth 
  double_illuminate
  translate<-0.5,-0.0,-0.5>
  texture{ 
    pigment {
      function{(Terrain1(x*20,y*20, pi, 1.5)/2)+0.5} 
      rotate<90,0,0>
    }
    //finish {ambient 1 diffuse 0}
  }
  scale <50,10,50>
  rotate<0 0,0>
  translate<0,0,0>
}  

#declare C1 = difference{
  cylinder{
    CentreC1+<0, 9.99,0>, CentreC1+<0, 10,0>, RadiusC1
  }
  cylinder{
    CentreC1-<0,9.98,0>, CentreC1+<0, 10.1,0>, RadiusC1-0.1
  }
  scale (1/20) * 50 
}
//object{C1 texture{pigment{rgb 1}}}

#declare C2 = difference{
  cylinder{
    CentreC2+<0, 9.99,0>, CentreC2+<0, 10,0>, RadiusC2
  }
  cylinder{
    CentreC2-<0,9.98,0>, CentreC2+<0, 10.1,0>, RadiusC2-0.1
  }
  scale (1/20) * 50 
}
//object{C2 texture{pigment{rgb 1}}}

light_source{
  CentreC1+<0,250,0>, rgb<0,1,2>
  parallel
  point_at CentreC1+<0,-250,0>
  projected_through{C1}
}

light_source{
  CentreC2+<0,250,0>, rgb<2,0,0>
  parallel
  point_at CentreC2+<0,-250,0>
  projected_through{C2}
}