Sine wave oscillators art work

IngoognI

A blog.

What is the sound of POV-Ray? This is something I wondered about for many moons. I know the various ways of creating and recording sound but it was not until I started using SuperCollider it clicked. SuperCollider is a real time audio synthesis environment. With several SuperCollider experiments I had an 'I can do that in POV-Ray' moment. So, time to explore.

The sound of POV-Ray Ingo 2021-03-31

Oh, POV-Ray, you may wonder. What is that? POV-Ray is a grand old Dame of ray tracing. A method of creating computer generated images (CGI). It is a tool dedicated to images only. But it has a, Turing complete, scene description language (SDL) that is powerfull enough to do other things with it than just creating lights, objects and cameras and positioning them.

POV-Ray has no means to work with sound cards. Also it is quite slow for this job, so no real time audio. Rendering and writing data to a WAVE audio file is the solution. This leads to the first two steps in my exploration, get POV-Ray to write WAVE files. To test the writing I need a predictable source of audio data. 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 sound source, SinOsc

A sine wave oscillator will be the first sound data source. SinOsc can be created with a single function:

#declare SinOsc = function(Freq, Phase, Amp, Tick, SRate){
  sin((Tick * TAU * (Freq/SRate)) + Phase) * Amp
};
Freq:
The requency in Herz (Hz) of the sine wave.
Phase:
The position the sine wave starts at on the y-axis. A phase of 0.5 * pi results in a cosine, 90° out of phase with a sine.
Amp:
The amplitude of the sine wave.
Tick:
The sample position we're at
SRate:
The sample rate (Hz) we sample the sine wave at. CD quality is 44100.

Note: tau is a constant in POV-Ray 3.8, not in 3.7 TAU = 2 * pi;

Run the oscillator

To run the oscillator we have to progress the Tick tick by tick. Let's do that for one second and print the output, but not at 44100 ticks per second.

With Amp = 1; the SinOsc's output is in the range of [-1,1]. Depending on the bit depth of an audio file we have to convert that range to the proper integer range. CD quality would be 16 bits per sample. The range then should be [(-2^16)/2, (2^16)/2] so multiply the SinOsc result by (pow(2, BitDepth) / 2.0) - 1. The result of this are still floats, not integers. When POV-Ray writes the data as uint16le to the actual file, it will automatically round the floats to the nearest integers.

// cmd: +w0 +h0 +q0 -d -a -f
#version 3.7;
global_settings{ assumed_gamma 1.0 }
#declare TAU = 2*pi;

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

#declare Dur = 1;
#declare BitDepth = 16;
#declare SampleRate = 100;
#declare Samples = Dur * SampleRate;
#declare C = (pow(2, BitDepth) / 2.0) - 1;

#declare Tick = 0;
  #while (Tick < Samples) 
    #declare Sample = SinOsc(101, 0, 0.5, Tick, SampleRate) * C;
    #debug concat(str(Sample,5,5),"\n")
    #declare Tick = Tick + 1;
  #end
#fclose WaveOutFile

The WAVE file

A WAVE file is made up of two parts, the header and the pulse code modulated (PCM) audio data.

The structure of a WAVE file

RIFF chunk
chunk id "RIFF"
format "WAVE"
Format sub chunk
sub chunk id "fmt "
The number and size of fields vary.
Fields describe the file format, their content depends on the audio data,compression and vendor specific structures.
Some field examples are number of channels, channel mask, bit rate, sample rate.
Data sub chunk
sub chunk id "data"
sub chunk size
data

The header

Despite a full file specification on hand I run in to trouble. My lack of knowledge.

The fine POV-Ray documentation tells me that POV-Ray can not write 32 unsigned bits yet several fields in the file header need it. From the ever help full bunch at the POV-Ray forum Tor Olav Kristensen had a quick and clean solution. Chopping the input integer up in 8 bit pieces and the write the piece to the file in the proper order.

The wave header code is a bit boring, so I'm not going through it. I kept the parameters close to the ones in the above linked spec. At the end this article is the full code as an include file. Consider it in Alpha state. There are no safe guards in place (yet). Using it with 16 bit depth and a 44100 Hz sample rate should work fine

Writing the audio data

First here is a little thing in POV-Ray to mention. When you open a file you have to give it a file handle identifier. This identifier is global and can not be passed to a macro as a parameter. So I chose WaveOutFile as file handle in all related files and macros. This works fine as long as there is just one file(handle) using a specific macro.

Now, to the audio data. As we specify the duration of the audio, the bit depth and sample rate we can almost write the header. There's one part missing. The amount of audio channels. For my exploration I kept it at a single, mono, channel. So after the header it is just writing the 16 bits after each other.

If you want to write stereo audio data you have to keep the data in proper order. Left channel first, then right.

After all audio data have been written, the last thing to do is check whether a padding byte has to be written. This has to be done of the amount of bytes is uneven. The WriteWaveHeader macro returns a value 1 if writing a padding byte is needed.

// cmd: +w0 +h0 +q0 -d -a -f
#version 3.7;
#include "waveheader.inc"
global_settings{ assumed_gamma 1.0 }
#declare TAU = 2*pi;

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

#declare Dur = 1;
#declare BitDepth = 16;
#declare SampleRate = 44100;
#declare Samples = Dur * SampleRate;
#declare C = (pow(2, BitDepth) / 2.0) - 1;

#declare Tick = 0;
#fopen WaveOutFile "SinOsc.wav" write
  #declare PadByte = WriteWaveHeader(Dur, 1, BitDepth, SampleRate);
  #while (Tick < Samples) 
    #declare Sample = SinOsc(501, 0, 0.5, Tick, SampleRate) * C;
    #write (WaveOutFile, sint16le Sample)
    #declare Tick = Tick + 1;
  #end
  #if (PadByte = 1)
    #write (WaveOutFile, uint8 0)
  #end
#fclose WaveOutFile

This results in a 1 second audio file with a 501 Hz tone. This little 'scene' is also the test for the waveheader.inc include file. If you render the include file directly the same aoutput is generated.

Next

This was just the beginning. Now the basics have been set up, there are a few areas I'd like to explore. What I'm currently thinking of are wave terrain synthesis, additive synthesis and frequency modulation. Oh, and off coarse with images the next time. Maybe even animations. It's ray tracing after all.

Download POVSound.zip

/*: waveheader.inc
Desc   : An include file to write a WAVE audio header.
Author : Ingo
Date   : 2021-03-24
Version: 0.1 Alpha
Remarks: 2021-03-24: For now, just use it for mono 16 bit @ 44.1kHz. This
         is not enforced and there are no safe guards.
*/
#version 3.7;
#ifndef(WAVEHEADER_INC_TEMP)
#declare WAVEHEADER_INC_TEMP = version;
#ifdef(View_POV_Include_Stack)
   #debug "including waveheader.inc\n"
#end

#include "math.inc"

#macro Write_UINT32LE(Integer)
  /*:Write an integer as a little endian uint32 to a file.
  
  requirement:
    An open file for writing, with the file handle WaveOutFile.

  parameter:
    Integer: (int) the integer to be 'converted' and written.
 
  attr: Tor Olav Kristensen  
  */
  #ifdef(WaveOutFile)
    #local R = Integer;
    #local Byte_0 = mod(R, 256);
    #local R = div(R, 256);
    #local Byte_1 = mod(R, 256);
    #local R = div(R, 256);
    #local Byte_2 = mod(R, 256);
    #local R = div(R, 256);
    #local Byte_3 = mod(R, 256);
    #write (WaveOutFile, uint8 Byte_0)
    #write (WaveOutFile, uint8 Byte_1)
    #write (WaveOutFile, uint8 Byte_2)
    #write (WaveOutFile, uint8 Byte_3)
  #else
    #error "\n No FILE_HANDLE_IDENTIFIER WaveOutFile defined\n"
  #end
#end


#macro WriteWaveHeader(Dur, nChannels, wBitsPerSample, nSamplesPerSec)
  /*: Writes a linear PCM WAVE file header. Returns a 0 or 1, depending
  on whether a padding byte has to added befor closing the file.
  
  See test scene at the end of file for an example of use. The reference for 
  implementation can be found at,
  http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
  
  requirement:
    An open file for writing, with the file handle WaveOutFile.
  
  parameter:
    Dur: (float, s) Duration of the audio.
    nChannels: (int) number of audio channels. Only 1 or 2 are supported.
    wBitsPerSample: (int) Bith depth of the audio samples. Currently 16 is safe.
    nSamplesPerSecond: (int, Hz) Sample rate. Currently 44100 is safe.
  
  return:
    return value of 0 or 1.
  */
  #ifdef(WaveOutFile)
    #local nSamples = Dur * nSamplesPerSec; 
    #local SampledData = (wBitsPerSample / 8) * nChannels * nSamples;
    #local PaddingByte =  odd(SampledData);
    // char:  riffChunkID {"R","I","F","F"};
    #write (WaveOutFile, uint8 82 73 70 70)
    // uint32:  riffChunkSize 
    #local riffChunkSize = 4 + 24 + (8 + (
        (wBitsPerSample / 8) * nChannels * nSamples
      ) + PaddingByte);
    Write_UINT32LE(riffChunkSize)
    // char:  waveID {"W","A","V","E"};  
    #write (WaveOutFile, uint8 87 65 86 69)
    // char:  fmtChunkID {"f","m","t"," "};
    #write (WaveOutFile, uint8 102 109 116 32)
    // uint32:  fmtChunkSize  16;   fixed for standard PCM wave
    Write_UINT32LE(16)
    // unint16le:  wFormatTag   1;    fixed for standard PCM wave
    #write (WaveOutFile, uint16le 1)
    // uint16le:  nChannels;
    #write (WaveOutFile, uint16le nChannels)
    // uint32:  nSamplesPerSec;
    Write_UINT32LE(nSamplesPerSec)
    // uint32:  nAvagBytesPerSec
    #local nAvagBytesPerSec = nSamplesPerSec * (wBitsPerSample / 8) * nChannels;
    Write_UINT32LE(nAvagBytesPerSec)
    // uint16le:  nBblockAlign;
    #local nBblockAlign = (wBitsPerSample / 8) * nChannels;
    #write (WaveOutFile, uint16le nBblockAlign)
    // uint16le:  wBitsPerSample;
    #write (WaveOutFile, uint16le wBitsPerSample)
    // char:  dataChunkID {"d","a","t","a"};
    #write (WaveOutFile, uint8 100 97 116 97)
    // uint32:  dataChunkSize;
    #local dataChunkSize = SampledData;
    Write_UINT32LE(dataChunkSize)
    // next, write audiodata
    // Padding byte if SampledData is odd
    // padding byte has to be written after all data is, 
    // just before closing the file. Hence the return value:
  #else
    #error "\n No FILE_HANDLE_IDENTIFIER WaveOutFile defined\n"
  #end
  PaddingByte
#end

/*--- Include file test scene ---*/
// cmd: +w0 +h0 +q0 -d -a -f
/*: Generates a 501 Hz tone and writes it to a WAVE file.*/

#if(input_file_name="waveheader.inc")
  global_settings{ assumed_gamma 1.0 }
  #declare TAU = 2*pi;
  #declare SinOsc = function(Freq, Phase, Amp, Tick, SRate){
    sin((Tick * TAU * (Freq/SRate)) + Phase) * Amp
  }; 
  #declare Dur = 1;
  #declare BitDepth = 16;
  #declare SampleRate = 44100;
  #declare Samples = Dur * SampleRate;
  #declare C = (pow(2, BitDepth) / 2.0) - 1;
  #declare Tick = 0;
  #fopen WaveOutFile "SinOsc.wav" write
    #declare PadByte = WriteWaveHeader(Dur, 1, BitDepth, SampleRate);
    #while (Tick < Samples) 
      #declare Sample = SinOsc(501, 0, 0.5, Tick, SampleRate) * C;
      #write (WaveOutFile, sint16le Sample)
      #declare Tick = Tick + 1;
    #end
    #if (PadByte = 1)
      #write (WaveOutFile, uint8 0)
    #end
  #fclose WaveOutFile
#end

#version WAVEHEADER_INC_TEMP;
#end