Python Basics

Basics

Python is used as the primary programming language for the DSP section of ringbuffer.org. Although there are various online resources for learning Python, this chapter introduces the most important aspects.

For further reading: as

Syntax

No Termination

Unlike many other languages, python does not require semicolons to terminate statements:

a=1
b=2

Indents

In Python, code indents are a part of the syntax. This is a very important and unique feature of python. This will become more important when using loops and conditions, as indatation can have an effect on the outcome of the code.

Code blocks always need to have the same indatation level. When using wrong indents:

a = 1
b = 2
 c = 3

the interpreter will give the following error message:

IndentationError: unexpected indent

Importing Modules

In Python, modules can be imported to extend the functionality. This is the equivalent to including libraries in other programming languages. Modules can be imported at any point in a script, before they are needed, but it is commom to import all modules at the beginning. After import, functions from a module can be called with the dot syntax:

import numpy
# call the rand function from numpy's random module
numpy.random.rand()

It is common to import modules with shorter aliases for shorter code. Standard modules have typical aliases, that are used throughout the community:

import numpy as np
# call the rand function from numpy's random module
np.random.rand()

If only a specific part of a module is needed, it can be imported exclusively with an alias to make it lightweight and fast:

# import only the window part of scipy
from scipy.signal import windows

# create a Gaussian with twelve values and sigma = 1
w = windows.gaussian(12,1)

Data Types

Strings

s = "I am stringing!"
s[3]

Lists

The list is Python's default, builtin data type to store collections of data. Lists are ordered, mutable and can combine different data types. Elements of lists can be accessed with brackets.

x = [1, 0, 4, 3]
y = [1, "two", 4]

print(x[2])
print(z[1])

Arrays

import numpy as np
a = np.array([1, 0, 3, 4])

Control Structures

Loops

Conditions

Writing Functions

Functions are defined using the def keyword. Arguments are declared in the parenthesis after the function's name. Return values are specified by the return keyword. Multiple values can be returned if comma separated. Note the indent to mark the function's body.

Since python code is not compiled but interpreted, functions need to be defined before they can be called:

import numpy as np

def pythagoras(a,b):
        c = np.sqrt(pow(a,2) + pow(b,2))
        return c

pythagoras(4, 5)

Namespaces

All variables defined at the main level of the program (not in a function or class) are stored in the Global Namespace as global variables. All gobal variable can be listed as follows:

dir()

Every function creates a new namespace while being executed - see the LEGB rule.

SuperCollider: Light Dependent Resistor

This example shows how a single sensor can be streamed via serial data from the Arduino to SuperCollider.


Breadboard Circuit

The breadboard circuit is the same as in the first Arduino sensor example:

/images/basics/ldr_input_fritzing.png

Arduino Code

For the SC example, serial data is sent in a simple way. The additional scaling is optional, but makes it easier to process the data in SuperCollider.

void setup() {

   Serial.begin(9600);
}

void loop() {

 int sensorValue = analogRead(A0);

 // scale to 0..1
 float voltage   = sensorValue/1024.0 ;

 Serial.println(voltage);

}

SC Code

On Linux, the Arduino's serial interface can be found in the terminal:

$ ls -l /dev/ttyACM*

On the SC receiver end, a serial port object is initialized with the matching serial interface:

(
p = SerialPort(
  "/dev/ttyACM0",
  baudrate: 9600,
  crtscts: true);
)

A control rate bus is used to visualize the received data and make it accessible to other nodes:

~sensorBUS = Bus.control(s,1);
~sensorBUS.scope;

The actual receiving and decoding of the data happens inside a routine with an infinite loop. It appends incoming characters, until a return character (13) is received. In this case, the assembled string is converted to a Float and written to the sensor bus:

(
r= Routine({
    var byte, str, res;
    inf.do{|i|
        if(p.read==10, {
            str = "";
            while({byte = p.read; byte !=13 }, {
                str= str++byte.asAscii;
            });
            res= str.asFloat;

            // ("read value:"+res).postln;

                    ~sensorBUS.set(res);
        });
    };
}).play;
)

External Resources

The SuperCollider Tutorial by Eli Fieldsteel shows a similar solution for getting Arduino sensors into SuperCollider via USB.


Exercise

Asteroids - NeoWs

NeoWs

At https://api.nasa.gov/, the NASA offers various APIs. This example uses data from the 'Asteroids - NeoWs' RESTful web service, which contains data of near earth Asteroids.


JSON Data Structure

The JSON data is arraned as an array, featuring the data on 20 celestial bodies, accessible via index:

links {…}
page  {…}
near_earth_objects
  0   {…}
  1   {…}
  2   {…}
  3   {…}
  4   {…}
  5   {…}
  6   {…}
  7   {…}
  8   {…}
  9   {…}
  10  {…}
  11  {…}
  12  {…}
  13  {…}
  14  {…}
  15  {…}
  16  {…}
  17  {…}
  18  {…}
  19  {…}

Harmonic Sonification

Mapping

All entries of the individual Asteroids can be used as synthesis parameters in a sonification system with Web Audio. This example uses two parameters of the Asteroids within an additive synthesis paradigm:

orbital_period       = sine wave frequency
absolute_magnitude_h = sine wave  amplitude

More info on the orbital parameters:

https://en.wikipedia.org/wiki/Orbital_period

https://en.wikipedia.org/wiki/Absolute_magnitude


The Result


Spatial Additive in SuperCollider

The following example creates a spatially distributed sound through additive synthesis. A defined number (40) of partials is routed to individual virtual sound sources which are rendered to a 3rd order Ambisonics signal.

A Partial SynthDef

A SynthDef for a single partial with amplitude and frequency as arguments. In addition, the output bus can be set. The sample rate is considered to avoid aliasing for high partial frequencies.

(
SynthDef(\spatial_additive,

      {
              |outbus = 16, freq=100, amp=1|

              // anti aliasing safety
              var gain = amp*(freq<(SampleRate.ir*0.5));

              var sine = gain*SinOsc.ar(freq);

              Out.ar(outbus, sine);

      }
).send;
)

The Partial Synths

Create an array with 40 partial Synths, using integer multiple frequencies of 100 Hz. Their amplitude decreases towards higher partials. An audio bus with 40 channels receives all partial signals separately. All synths are added to a dedicated group to ease control over the node order.

~partial_GROUP = Group(s);

~npart         = 40;

~partial_BUS   = Bus.audio(s,~npart);

(
~partials = Array.fill(40,
{ arg i;
  Synth(\spatial_additive, [\outbus,~partial_BUS.index+i, \freq, 100*(i+1),\amp, 1/(1+i*~npart*0.1)],~partial_GROUP)
});
)

s.scope(16,~partial_BUS.index);

The Encoder SynthDef

A simple encoder SynthDef with dynamic input bus and the control parameters azimuth and elevation.

~ambi_BUS      = Bus.audio(s,16);

(
SynthDef(\encoder,
      {
              |inbus=0, azim=0, elev=0|

              Out.ar(~ambi_BUS,HOAEncoder.ar(3,In.ar(inbus),azim,elev));
      }
).send;
)

The Encoder Synths

An array of 16 3rd order decoders is created in a dedicated encoder group. This group is added after the partial group to ensure the correct order of the synths. Each encoder synth receives a single partial from the partial bus. All 16 encoded signals are sent to a 16-channel audio bus.

~encoder_GROUP = Group(~partial_GROUP,addAction:'addAfter');

(
~encoders = Array.fill(~npart,
      {arg i;
              Synth(\encoder,[\inbus,~partial_BUS.index+i,\azim, i*0.1],~encoder_GROUP)
});
)

~ambi_BUS.scope

The Decoder Synth

A decoder is added after the encoder group and fed with the encoded Ambisonics signal. The binaural output is routed to outputs 0,1 - left and right.

// load binaural IRs for the decoder
HOABinaural.loadbinauralIRs(s);

(
~decoder = {
Out.ar(0, HOABinaural.ar(3, In.ar(~ambi_BUS.index, 16)));
}.play;
)


~decoder.moveAfter(~encoder_GROUP);

Sliders in PD

This very basic sine.pd example creates a sine wave oscillator. Its frequency can be controlled with a horizontal slider. Ann additional toggle object allows to switch the sine on and of, by means of multiplication. When used without arguments, the dac~ object has two inlets, which relate to the left and right channel of the audio interface. The additional message box with the bang can be used to activate the DSP. This is necessary for getting any sound and can also be done in the menu.

/images/basics/pd-sine.png

References

1997

  • Miller S. Puckette. Pure Data. In Proceedings of the International Computer Music Conference (ICMC). Thessaloniki, \\ Greece, 1997.
    [details] [BibTeX▼]

1988

  • Miller S. Puckette. The patcher. In Proceedings of the International Computer Music Conference (ICMC). Computer Music Association, 1988.
    [details] [BibTeX▼]

Osaka 1970 - German Pavilion

The German Pavilion

The German Pavilion at the 1970 Expo in Osaka was an elaborate spatial audio project. Various artists, including Bernd Alois Zimmermann, Boris Blacher and Karlheinz Stockhausen, contributed works for the spherical loudspeaker setup:

/images/spatial/osaka.jpg

The German Pavilion at the 1970 Expo in Osaka.

A device for live spatialization in the Osaka Pavilion was developed at TU Berlin.

/images/spatial/osaka_sensor.jpg

TU-designed 'Kugelsensor' for live spatialization in the auditorium.

Stockhausen

The following sketch shows Stockhausen's approach for working with the system, using an 8-track tape machine:

/images/spatial/hinab-hinauf.png

Sketch for Stockhausen's work 'Hinab-Hinauf'.

TU Composition

/images/spatial/Musik-fuer-Osaka.gif

Simple Waveguides in C++

Waveguides can be used to model acoustical oscillators, such as strings, pipes or drumheads. The theory of digital waveguides is covered in the sound synthesis introduction.

The WaveGuide Class

The WaveGuide class implements all necessary components for a waveguide string emulation. Inside the class, the actual waveguide-buffers are realized as pointers to double arrays.

From ``WaveGuide.h``:

/// length of the delay lines
int     l_delayLine;

/// leftward delay line
double  *dl_l;

/// rightward delay line
double  *dl_r;

The pointer arrays need to be initialized in the constructor of the WaveGuide class.

From ``WaveGuide.cpp``:

dl_l = new double[l_delayLine];
dl_r = new double[l_delayLine];

for (int i=0; i<l_delayLine-1; i++)
{
    dl_l[i] = 0.0;
    dl_r[i] = 0.0;
}

Plucking the String

The method void WaveGuide::excite(double pos, double shift) is called for plucking the string, respectively exciting it. It receives a plucking position and a shift parameter. In two loops, the method fills the two waveguides with the excitation function. In this example, a simple triangular function is chosen.

From ``WaveGuide.cpp``:

// set positive slope until plucking index
for (int i=0; i<idx; i++)
{
    dl_l[i] = 0.5* ((double) i / (double)(idx));
    dl_r[i] = 0.5* ((double) i / (double)(idx));
}
// set negative slope from plucking index to end
for (int i=idx; i<l_delayLine; i++)
{
    dl_l[i] = 0.5*(1.0 - ((double) i / (double) (l_delayLine-idx)));
    dl_r[i] = 0.5*(1.0 - ((double) i / (double) (l_delayLine-idx)));
}

Oscillating

Karplus-Strong in Faust

White Tone Oscillator

As explained in the Sound Synthesis Introduction, the Karplus-Strong algorithm is based on a sequence of white noise. The following example uses a feedback structure to create a looped version of a white noise array:

text


Main components of the above example are the excitation and the resonator. The resonator is a feedback loop with an adjustable delay:

text


The excitation passes a random sequence to the resonator, once the gate is activated. It will oscillate until the gate is released.

Load the example in the Faust online IDE for a quick start:

// white_tone.dsp
//
// Henrik von Coler
// 2021-07-04

import("all.lib");

// Control parameters:
freq = hslider("freq Hz", 50, 20, 1000, 1) : si.smoo; // Hz
gate = button("gate");

// processing elements for excitation:
diffgtz(x) = (x-x') > 0;
decay(n,x) = x - (x>0)/n;
release(n) = + ~ decay(n);
trigger(n) = diffgtz : release(n) : > (0.0);

P = SR/freq;

// Resonator:
resonator = (+ : delay(4096, P) * gate) ~ _;

 // processing function:
process = noise : *(gate : trigger(P)): resonator <: _,_;

Karplus-Strong in Faust

The Karplus-Strong algorithm for plucked string sounds is explained in detail in the Sound Synthesis Introduction. That implementation is based on a ring buffer with a moving average filter. For the Faust implementation, this example has been adjusted, slightly (Smith, 2007).

Exercise


References

2018

  • Romain Michon, Julius Smith, Chris Chafe, Ge Wang, and Matthew Wright. The faust physical modeling library: a modular playground for the digital luthier. In International Faust Conference. 2018.
    [details] [BibTeX▼]

2007

Fourier Transform Properties