3.2 Klangsynthese

Synthesizer Moog 55. Quelle: Wikipedia Commons.

Neben Funktionen für das Abspielen von Klangdateien stellt die Sound-Library von Processing auch Funktionen für die Synthese von Klängen bereit. Weiterführende Informationen finden sich im Netz unter https://processing.org/tutorials/sound/ sowie in Roederer und Raffaseder (S. 223-247)

1 Oszillatoren

Die Grundlegende Art, Klänge zu erzeugen, sind Oszillatoren. Ein Oszillator erzeugt eine Schwingung, z.B. eine Sinus-Schwingung. Daneben gibt es etwa Sägezahn- Rechteck-, Dreieckschwingungen, die jeweils unterschiedliche Klangeigenschaften aufweisen.

Zunächst wird die sound-Library importiert und ein Oszillator-Objekt erzeugt:


import processing.sound.*;
SinOsc sino = new SinOsc(this);

Die Amplitude (Lautstärke) eines Oszillators wird durch amp() festgelegt. Die maximale Lautstärke wird durch einen Wert von 1 eingestellt, 0 bedeutet Stille.


sino.amp(0.5);

Die Frequenz, mit welche der Oszillator schwingt, wird mit freq() definiert und in Hertz angegeben:


sino.freq(220);

Ein Oszillator lässt sich mit play() starten (solange dies nicht geschieht ist er nicht zu hören):


sino.play();

Analog zum Sinus-Oszillator gibt es die Objekte SawOsc (Sägezahnschwingung) SqrOsc (Rechteckschwingung) TriOsc (Dreickschwingung) und Puls (Pulsgenerator) weitere für die anderen Schwingungstypen.

Im folgenden Beispiel SoundDrawwird das bereits bekannte Zeichen-Beispiel um KLanglichkeit erweitert. So entsteht eine audiovisuelle Verbindung, in diesem Fall eine Kongruenz: je weiter sich eine zeichnerische Geste in der y-Position nach oben bewegt, desto höher die Frequenz (und damit die Tonhöhe) des Klangs. So entstehen kurze audiovisuelle Gesten.

Beispiel 3-3: SoundDraw


import processing.sound.*; 

SinOsc sino;

void setup(){
  size(800,600);
  background(255);
  sino = new SinOsc(this);
  fill(255,150);
}

void draw(){

}

void mousePressed(){
  sino.freq(height-mouseY);
  sino.play();
}

void mouseDragged(){
   stroke(0,150);
   line(mouseX, mouseY, pmouseX, pmouseY); 
   sino.freq(height-mouseY+100);
}

void mouseReleased(){
  sino.stop();
  noStroke();
  rect(0,0,width, height);
}


Befehl Funktion
play() Generator starten
set() Mehrere Parameter gleichzeitig einstellen
freq() Schwingungsfrequenz einstellen
amp() Lautstärke des Generators einstellen (0 bis 1.0)
add() um andere Audiosignale zu modulieren
pan() Ausgabe im Stereopanorama bewegen (-1.0 bis 1.0)
stop() Generator stoppen


2. Rauschen

Weißes Rauschen


WhiteNoise noise;
noise = new WhiteNoise(this);
noise.play();

Rosa Rauschen

Die Amplitude von Rosa Rauschen nimmmt im Vergleich zum gleichverteilten Weißen Rauschen um 3dB pro Oktave ab.


PinkNoise noise;
noise = new PinkNoise(this);
noise.play();

Brownsches Rauschen

Die Amplitude von Brownschem Rauschen nimmmt im Vergleich zum gleichverteilten Weißen Rauschen um 6dB pro Oktave ab.


BrownNoise noise;
noise = new BrownNoise(this);
noise.play();

Alle drei Rauschgeneratoren verfügen über die folgenden Funktionen:

Befehl Funktion
play() Generator starten
set() Mehrere Parameter gleichzeitig einstellen
amp() Lautstärke des Generators einstellen (0 bis 1.0)
add() um andere Audiosignale zu modulieren
pan() Ausgabe im Stereopanorama bewegen (-1.0 bis 1.0)
stop() Generator stoppen

In unserem Beispiel ›SoundDraw‹ lässt sich nun die Verbindung von Bewegung etwas komplexer gestalten, indem die Lautstärke eines Rausgenerators mit der Bewegung der Maus auf der y-Achse gekoppelt wird. Je näher die Maus am oberen Bildschirmrand ist, desto mehr Rauschanteil wird dem Klang beigemengt. Gleichzeitig verändert sich die visuelle Repsäsentation: Die eindeutige Linie reisst analog zum Rauschanteil aus und stellt eine audiovisuelle Analogie her.

Beispiel 3-4: SoundDraw + Dimension Noise


import processing.sound.*; 

SinOsc sino;
BrownNoise noise;

void setup(){
  size(800,600);
  frameRate(120);
  background(255);
  sino = new SinOsc(this);
  noise = new BrownNoise(this);
  fill(255,150);
}

void draw(){

}

void mousePressed(){
  sino.freq(height-mouseY);
  sino.play();
  noise.play();
  noise.amp(0);
}

void mouseDragged(){
   stroke(0,150);
   fill(0);
   sino.freq(height-mouseY+200);
   float noiseL = map(height-mouseY, height, 0, 0.1, 0);
   noise.amp(noiseL);
   strokeWeight(noiseL*40);
   line(mouseX+(randomGaussian()*noiseL*400), mouseY+(randomGaussian()*noiseL*400), pmouseX, pmouseY);
}

void mouseReleased(){
  sino.stop();
  noise.stop();
  noStroke();
  fill(255);
  rect(0,0,width, height);
}

3. Filter

LowPass

Zunächst muss wieder ein entsprechendes Objekt deklariert werden:


LowPass lowPass;

Dann wird dieses initialisiert:


lowPass = new LowPass(this);

Dann wird der Filter gestartet:


lowPass.process(noise);         // "noise" stellt das Signal dar, auf das der Filter

Um die cut off - Frequenz einzustellen wird die Funktion freq() verwendet:


lowPass.freq(880);             // cut off - Frequenz bei 880Hz

Neben dem LowPass-Filter bietet Processing einen BandPass und einen HighPass-Filter.

In unserem kleinen Beispielprogramm SoundDraw lässt sich nun eine weitere Klangdimension einführen, indem die x-Position der Maus mit der Steuerung eines Bandpass-Filters gekoppelt wird. Die durch den Filter gesteuerten höheren und tieferen Klanganteile werden visuell durch die Farbigkeit der visuellen Elemente aufgenommen.

Beispiel 3-5: SoundDraw + Dimension Noise + Dimension Bandpass


import processing.sound.*; 

SinOsc sino;
WhiteNoise noise;
BandPass bandPass;

int cHue;
color c;

void setup(){
  size(800,600);
  frameRate(120);
  background(0);
  sino = new SinOsc(this);
  noise = new WhiteNoise(this);
  bandPass = new BandPass(this);
  fill(255,150);
  colorMode(HSB); 
}

void draw(){

}

void mousePressed(){
  sino.freq(height-mouseY);
  sino.play();
  noise.play();
  sino.amp(0.2);
  noise.amp(0);
  bandPass.process(noise, 100, 50);
  cHue = round(random(255));
  c = color(cHue, 200, 100);
}

void mouseDragged(){ 
   fill(c, 20);
   noStroke();
   rect(0,0,width, height);
   sino.freq(height-mouseY+200);
   float noiseL = map(height-mouseY, height, 0, 0.1, 0);
   noise.amp(noiseL);
   bandPass.set(mouseX*10, mouseX/2); 
   strokeWeight(noiseL*80);
   strokeWeight(4);
   //stroke(0,mouseX/2);
   float bright = map(mouseX, 0, width, 100, 255);
   stroke(random(cHue-mouseX/10, cHue+mouseX/10), 255, bright, 255);
   line(mouseX+(randomGaussian()*noiseL*400), mouseY+(randomGaussian()*noiseL*400), pmouseX, pmouseY);
}

void mouseReleased(){
  bandPass.stop();
  sino.stop();
  noise.stop();

  noStroke();
  fill(0);
  rect(0,0,width, height);
}

Wie lässt sich das Beispiel SoundDraw mit Object-Tracking aus dem Kapitel Computer Vision kombinieren?

4. Hüllkurven

Hüllkurve mit den vier Phasen Attack, Decay, Sustain, Release. Quelle: Wikipedia Commons


Env hk;
env  = new Env(this); 
env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime);

Pointilistisches Klangbeispiel, in dem kein Klangkontinuum, sondern Klangwolken entstehen. Diese Wolken sind durch Mausinteraktion und Zufallsfunktionen gesteuert.

Beispiel 3-6


coming

Steinberg Retrologue virtueller analog VST-Synthesizer

5. Einfacher Synthesizer

Mit dem erreichten Wissen lässt sich nun ein einfacher Synthesizer mit 3 Klangerzeugern und Hüllkurvengenerator programmieren.

Wie ließe sich die Lautstärke der einzelnen Oszillatoren zueinander einstellen? Wie ließen sich die drei Filtertypen im Display-Feld darstellen?

Beispiel 3-7


import controlP5.*;
import processing.sound.*;

ControlP5 cp5;

int count; 
int Duration = 1000;

boolean sinoPlay = false;
boolean sawoPlay = false; 
boolean squaroPlay = false;
boolean delayDo = false;
boolean envelopeDo = false;
float FreqSin, FreqSaw, FreqRect;
float attack, sustain, susLevel, release;

SinOsc sino;
SawOsc sawo;
SqrOsc squaro;
WhiteNoise noise;
BandPass bandPass;
Env envelope;
Reverb reverb;

void setup() {
  size(500,200);
  background(0);

  sino = new SinOsc(this);
  sawo = new SawOsc(this);
  squaro = new SqrOsc(this);
  noise = new WhiteNoise(this);
  bandPass = new BandPass(this);
  envelope  = new Env(this);
  reverb = new Reverb(this);
  cp5 = new ControlP5(this);

  cp5.addBang("bang")
     .setPosition(10, 10)
     .setSize(40, 20)
     .setTriggerEvent(Bang.RELEASE)
     .setLabel("Play")
     ;

  cp5.addSlider("Duration")
     .setPosition(100,10)
     .setSize(100, 20)
     .setRange(0, 2000) // values can range from big to small as well
     .setValue(1000)
     .setNumberOfTickMarks(5)
     .setSliderMode(Slider.FLEXIBLE)
     ;

   // Oszillatoren   
   cp5.addToggle("Sinus")
     .setPosition(310,10)
     .setSize(40,20)
     .setValue(true)
     ;
   cp5.addToggle("Saegezahn")
     .setPosition(360,10)
     .setSize(40,20)
     .setValue(false)
     ;
   cp5.addToggle("Rechteck")
     .setPosition(410,10)
     .setSize(40,20)
     .setValue(false)
     ;
   cp5.addSlider("FreqSin")
     .setPosition(310,50)
     .setSize(40, 20)
     .setRange(16, 2000) 
     .setValue(440)
     //.setSliderMode(Slider.FLEXIBLE)
     ;
   cp5.getController("FreqSin").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
   cp5.addSlider("FreqSaw")
     .setPosition(360,50)
     .setSize(40, 20)
     .setRange(16, 2000) 
     .setValue(440)
     .setSliderMode(Slider.FLEXIBLE)
     ;
   cp5.getController("FreqSaw").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);
   cp5.addSlider("FreqRect")
     .setPosition(410,50)
     .setSize(40, 20)
     .setRange(16, 2000) 
     .setValue(440)
     .setSliderMode(Slider.FLEXIBLE)
     ;
    cp5.getController("FreqRect").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);

   //Envelope
   cp5.addToggle("envelopeOn")
     .setPosition(10,160)
     .setSize(40,20)
     .setValue(false)
     .setLabel("Env on");
     ;

   cp5.addSlider("attack")
     .setPosition(60,160)
     .setSize(40, 20)
     .setRange(0.01, 0.5) 
     .setValue(0.1)
     .setSliderMode(Slider.FLEXIBLE)
     ;
     cp5.getController("attack").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);

   cp5.addSlider("sustain")
     .setPosition(110,160)
     .setSize(40, 20)
     .setRange(0.01, 0.5) // values can range from big to small as well
     .setValue(0.2)
     .setSliderMode(Slider.FLEXIBLE)
     ;
     cp5.getController("sustain").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);

   cp5.addSlider("susLevel")
     .setPosition(160,160)
     .setSize(40, 20)
     .setRange(0, 1) // values can range from big to small as well
     .setValue(0.5)
     .setSliderMode(Slider.FLEXIBLE)
     ;
     cp5.getController("susLevel").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);

   cp5.addSlider("release")
     .setPosition(210,160)
     .setSize(40, 20)
     .setRange(0.01, 0.5) // values can range from big to small as well
     .setValue(0.1)
     .setSliderMode(Slider.FLEXIBLE)
     ;
     cp5.getController("release").getCaptionLabel().align(ControlP5.LEFT, ControlP5.BOTTOM_OUTSIDE).setPaddingX(0);

   //Delay
   /*cp5.addToggle("delayOn")
     .setPosition(10,410)
     .setSize(40,20)
     .setValue(false)
     ;*/
}      

void draw() {
    fill(20,20,50);
    noStroke();
  // Zeichnen des Hüllkurven-Diagramms
    rect(10,50,280,100);
    stroke(255);
    float sp = 150-susLevel*100;
    line(10,150, attack*200+10, sp);
    line(attack*200+10,sp, attack*200+sustain*200+10,sp);
    line(attack*200+sustain*200+10, sp, attack*200+sustain*200+release*200+10,149);

  // Ton beenden
    if (millis() > count+Duration && envelopeDo == false){
      if (sinoPlay){ sino.stop(); }
      if (sawoPlay){ sawo.stop(); }
      if (squaroPlay){ squaro.stop(); }
    }
}

void bang() {
  if (sinoPlay){
       sino.stop();
       sino.play(FreqSin, 0.5);

       if (envelopeDo){
         envelope.play(sino, attack, sustain, susLevel, release);
       }
       if (delayDo){
         reverb.process(sino);
         reverb.set(5, 0.8, 1); 
       }
  }
  if(sawoPlay){
       sawo.stop();
       sawo.play(FreqSaw, 0.5);
       if (envelopeDo){
         envelope.play(sawo, attack, sustain, susLevel, release);
       }

  }
  if(squaroPlay){
       squaro.stop();
       squaro.play(FreqRect, 0.5);
       if (envelopeDo){
         envelope.play(squaro, attack, sustain, susLevel, release);
       }

  }

  count = millis();
}

void envelopeOn(boolean flag){
  if(flag){
     envelopeDo = true;
  }else{
     envelopeDo = false;
  }
}

void delayOn(boolean flag){
  if(flag){
     delayDo = true;
  }else{
     delayDo = false;
  }
}

void Sinus(boolean flag){
  if(flag){
     sinoPlay = true;
  }else{
     sinoPlay = false;
  }
}

void Saegezahn(boolean flag){
  if(flag){
     sawoPlay = true;
  }else{
     sawoPlay = false;
  }
}

void Rechteck(boolean flag){
  if(flag){
     squaroPlay = true;
  }else{
     squaroPlay = false;
  }
}

6. Simple Reactable

Möchte man das grundlegende Prinzip des Reaktables nachvollziehen, so lässt sich das Beispiel zur Erkennung von Markern aus dem Kapitel `Computer Vision nun um Klanggenaratoren erweitern.

Beispiel 3-7-2


import processing.video.*;
import processing.sound.*;                   // Processing Sound importieren
import boofcv.processing.*;                  // BoofCV importieren
import java.util.*;                          // Die Java Library util importieren

Capture cam;                                 // Cam-Objekt deklarieren
SimpleFiducial detector;                     // Objekt für den Detector erkennen 
SinOsc sino = new SinOsc(this);              // Sinus Oszillator deklarieren
SawOsc sawo = new SawOsc(this);              // Sinus Oszillator deklarieren
boolean sinoPlay = false;
boolean sawoPlay = false;
boolean sinoIsPlaying = false;
boolean sawoIsPlaying = false;
PVector coord;

void setup() {
  size(1280, 720);
  coord = new PVector();
  //cam = new Capture(this, 640, 480);
  cam = new Capture(this,  1280, 720, "Live! Cam Sync HD VF0770");
  cam.start();
  surface.setSize(cam.width, cam.height);
  detector = Boof.fiducialSquareBinaryRobust(0.1);
  detector.guessCrappyIntrinsic(cam.width,cam.height);
}

void draw() {
  if (cam.available() == true) {
    cam.read();
    List found = detector.detect(cam);
    println(found.size());
    if (found.size() == 0){
      sino.stop();
      sinoIsPlaying = false;
      sawo.stop();
      sawoIsPlaying = false;
    }
    image(cam, 0, 0);
    //background(20);
    for( FiducialFound f : found ) {
      detector.render(this,f);
      float xRaw = (float)f.getFiducialToCamera().getTranslation().getX();
      float yRaw = (float)f.getFiducialToCamera().getTranslation().getY();
      float zRaw = (float)f.getFiducialToCamera().getTranslation().getZ();
      float xs = map(xRaw, -0.37*zRaw, 0.37*zRaw, 0, width);
      float ys = map(yRaw, -0.27*zRaw, 0.27*zRaw, 0, height);
      float zs = zRaw;
      sinoPlay = false;
      if (f.getId() == 284){
        fill(255,0,0);
        if (sinoIsPlaying == false){          
          sinoIsPlaying = true;
          sino.freq(700);
          sino.play();
        }
      }
      if (f.getId() == 643){
        fill(255);
        if (sawoIsPlaying == false){          
          sawoIsPlaying = true;
          sawo.freq(700);
          sawo.play();
        }
      }

      noStroke();
      ellipse(xs, ys, 40,40);
      stroke(255);
      strokeWeight(3);
      line(xs, ys, width/2, height/2);
      //sino.amp(map(ys, 0, height, 0,1));

    }  
  }
}