Sound in ProcessingEinführung in den Umgang mit Audio-Daten in Processing

Fokus der Lesson ist der praktische Einstieg in die Arbeit mit Sound in Processing unter Verwendung der Minim-Bibliothek. Dabei steht das Abspielen und die einfache Analyse von Pegel und Frequenzen aus Audio-Dateien und Mikrofon-Signalen im Mittelpunkt.

Die minim Bibliothek

  • Minim(this) Eine Instanz der Minim Klasse dient als Schnittstelle zur Soundkarte. Nach dem Importieren kann diese mit new erzeugt werden. Dabei muss jeder Minim-Instanz eine Referenz auf den Processing-Sketch in Form eines Parameters übergeben werden.
    Minim m = new Minim (this);
  • loadFile() läd ein Audiodokument und gibt dieses als Datentyp AudioPlayer zurück.
    AudioPlayer player = m.loadFile ("myTrack.wav");
  • loadSample() läd ein Audiodokument und gibt dieses als Datentyp AudioSample zurück. Ein AudioSample ist eine primitivere Form eines AudioPlayers, mit Fokus auf loopende Abspielweise.
    AudioSample sample = m.loadSample ("mySample.wav");
  • loadSnippet() läd ein Audiodokument und gibt dieses als Datentyp AudioSnippet zurück. Bei diesem Datentyp handelt es sich um die anspruchloseste Form Audiodaten innerhalb von Processing zu speichern. Vor allem spiegelt sich dies im geringem Funktionsumfang wieder.
    AudioSnippet snippet = m.loadSnippet ("mySnippet.wav");

Laden von Audiodateien

AudioPlayer

Drei Objekte bietet minim für das Laden/Speichern von Audiodateien. Der AudioPlayer bietet im Vergleich mit dem AudioSample und dem AudioSnippet den größten Funktionsumfang. Als Quelle können alle drei mit den Formaten mp3, ogg, wav und aif arbeiten. Wie beim Laden von Bildern und Schriften muss sich die zu ladende Datei im /data Ordner des Sketches befinden. Ebenfalls sollte der Aufruf der minim-Funktionen zum Auslesen der Audiodatei innerhalb des setup()-Block statt finden.
  • cue(int millis) verschiebt die Abspielposition zu einem bestimmten Punkt, gemessen in Millisekunden vom Anfang der Audiodatei. Genaue Informationen befinden sich im JavaDoc.
    player.cue (0); // An den Anfang
    player.cue (player.length () / 2); // In die Mitte
  • getMetaData () erlaubt den Zugriff auf ID3 Tag-Informationen, welche im Audiodokument abgelegt sind (z.B. in MP3s oder Dateien vom OGG-Format).
    println (player.getMetaData ().title());
    println (player.getMataData ().author ());
  • isLooping() gibt true zurück wenn die Audiodatei in einer Schleife abgespielt wird.
  • isPlaying() gibt true zurück wenn die Audiodatei momentan abgespielt wird.
  • length() liefert die Länge der Audiodatei in Millisekunden zurück.
  • loop() veranlasst das Abspielen der Audiodatei in einer Schleife. Wenn kein Parameter übergeben wird, läuft diese Schleife endlos. Optional kann die Anzahl der Wiederholungen durch einen Wert vom Typ int angegeben werden.
    player.loop (); // Endlosschleife
    player.loop (7); // Spielt die Datei 7mal in Folge ab
  • loopCount() liefert die Anzahl der verbleibenden Schleifendurchläufe.
  • pause() pausiert den Abspielvorgang.
  • play() startet den Abspielvorgang an der aktuellen Position.
  • play(int millis) startet den Abspielvorgang an einer gegebenen Position.
  • position() gibt die aktuelle Abspielposition in Millisekunden zurück.
Das Grundgerüst zum Abspielen von Audiodateien untergliedert sich in zwei Bereiche. Zum Ersten muss minim in den aktuellen Sketch einbezogen werden. Dies geschieht über die Zeile import ddf.minim.*;, welche automatisch durch das Auswählen von "Sketch" → "Import Library…" → "minim" erzeugt wird. Im globalen Bereich, zwischen import und setup(), erfolgt die Definition der Variablen für die minim- und die Player-Instanz (global da sie im setup() sowie im draw() benötigt werden). Zu Beginn des Processing Sketches wird die Datei mit Hilfe von minim aus dem '/data' Ordner geladen und das Abspielen durch play() eingeleitet.
// Einbeziehen des minim-Audio-Toolkits
import ddf.minim.*;
// Instanz der minim Bibliothek
Minim minim;
// Instanz die das geladene Audiodokument repräsentiert
AudioPlayer player;

void setup () {
  // Audiotoolkit erstellen
  minim = new Minim (this);
  // Audiodatei aus dem data-Ordner laden
  player = minim.loadFile ("track.aif");
  // Audiodatei abspielen
  player.play ();
}

void draw () {
  // Mögliches Auswerden des aktuellen Spektrums 
  // bzw. von anderen Audioeigenschaften.
}

Beispiele

Alle folgenden Processing Sketche sollen einen kurzen und flächendeckenden Einstieg in das Arbeiten mit der minim Library aufzeigen. Vom simplen Laden bis zur Auswertung von Frequenzbereichen ist jeder erdenklicher Umgang mit Audioinformationen möglich - jedoch, wie im letzten Beispiel, mit vermehrter Schreibarbeit verbunden. minim ist eine der best-dokumentiertesten Werkzeuge für Processing. Neben einer umfangreichen Beschreibung werden außerdem Beispiele online angeboten.
Abspielen eines AudioPlayers in Processing

Zum Einstieg zeigt dieses Beispiel das rudimentären Laden und Abspielen einer Sounddatei. In diesem Fall greifen wir über die minim Instanz auf die Datei track.aif zu (sie muss sich im 'data'-Ordner befinden). Nach dem Laden im setup() startet der Aufruf von play() das Abspielen. Innerhalb des draw()s wird die aktuelle Position im Stück ermittelt und durch einen Kreis im Sketchfenster dargestellt. Die Breite der Zeichenfläche entspricht dabei der Tracklänge.

import ddf.minim.*;

Minim minim;
AudioPlayer player;

void setup () {
  // Sketcheinstellungen
  size (320, 170);
  smooth ();
  stroke (100);
  
  // Audiotoolkit erzeugen
  minim = new Minim (this);
  // Audiodatei in das Object 'player' laden
  player = minim.loadFile ("track.aif");
  // Audiodatei abspielen
  player.play ();
}

void draw () {
  background (0);
  // Relative Abspielposition im Stück ermitteln.
  // Dieser Wert bewegt sich zwischen 0 und 1.
  float playPos = player.position ();
  float playLen = player.length ();
  float xpos = (playPos / playLen) * width;
  // Zeichnen der Ellipse als 
  ellipse (xpos, height / 2, 20, 20);
}
Die beiden folgenden Funktionen ermöglichen das Steuern des Abspielens der Audiodatei durch Maus- bzw. Tastaturinteraktion. Je nach Position der Maus auf der x-Achse wird bei Klick der 'Spielkopf' verschoben. Ähnlich wie bei der Positionsermittlung berechnet Processing die neue Abspielposition durch relatives mappen der Mausposition zur Breite des Fensters, und überträgt diesen Wert auf die Gesamtlänge der Aufnahme. Da bei der Division von zwei Ganzzahlen (int) das Ergebnis ebenfalls keine Kommastellen enthällt, muss einer der beiden Werte zum float umgewandelt werden. Anderenfalls erhält man für die relative Mausposition (zwischen 0 und 1) immer 0.
void mousePressed () {
  // Relative Position der Maus auf der x-Achse, bezogen 
  // auf die Breite des Fensters (ebenfalls zw. 0 und 1).
  // Multipliziert mit der Länge der Audiodatei.
  float pos = ((float) mouseX / width) * player.length ();
  // Setzt neue Abspielposition.
  player.cue ((int) pos);
}

void keyPressed () {
  // Pausieren und Abspielen auf Tastendruck
  if (player.isPlaying ()) {
    player.pause ();
  }else{
    player.play ();
  }
}
Lautstärke ermittlen in Processing

Neben der Position im Audiodokument und dessen Steuerung, erlaubt die minim Bibliothek das Auslesen von Eigenschaften. Eine bezeichnendes Kriterium ist die wiedergegebene Lautsträke zu einem bestimmten Zeitpunkt. Während des Abspielens durch den play() Aufruf, kann im draw() das sogenannte 'level' für jedes Einzelbild erfragt werden. Die mix.level()-Funktion beschreibt die Auslastung dieses Merkmals in einem Wertebereich von 0 und 1.
Das Objekt mix repräsentiert das Sterosignal, welches sich aus dem linken (left) und dem rechten (right) Kanal zusammen setzt.

import ddf.minim.*;

Minim minim;
AudioPlayer player;

void setup () {
  // Sketch einrichten
  size (320, 240);
  noStroke ();
  
  // Audiodatei laden und abspielen
  minim = new Minim (this);
  player = minim.loadFile ("track.aif");
  player.play ();
}

void draw () {
  // Schwarzes semitransparentes Recht 
  // über die gesamte Bühne zeichnen
  fill (0,30);
  rectMode (CORNER);
  rect (0, 0, width, height);
  
  // Größe des Quadrats in Abhänigkeit von
  // der Lautstärke berechnen
  float dimension = player.mix.level () * 250;
  
  // Rechteck zeichnen
  fill (255);
  rectMode (CENTER);
  rect (width/2, height/2, dimension, dimension);
}
Mikrofoneingang nutzen in Processing

Angewendet auf ein Mikrofon-Input visualisiert der folgende Sketch die chronologische Entwicklung der Umgebungsgeräusche. Je lauter, desto größer und separierter wird ein Kreis auf der Zeichenfläche abgebildet. Dabei verschiebt sich die Position auf der x-Achse von links nach rechts, bis es zu einem Umbruch in die nächste Zeile kommt. Mittels der globalen Variablen x und y wird die aktuelle Position gespeichert und durch die if-Bedingung am Ende des draw()-Blocks kontrolliert.

import ddf.minim.*;

float x;
float y;
Minim minim;
AudioInput input;

void setup () {
  // Sketch einstellen
  size (320, 240);
  smooth();
  stroke (255, 25);
  noFill ();
  
  // Startposition festlegen
  x = 0;
  y = 20;
  
  // Audiotoolkit anlegen
  minim = new Minim (this);
  input = minim.getLineIn (Minim.STEREO, 512);
  background (0);
}

void draw () {
  // Kreisgröße Abhängig von Lautstärke
  float dim = input.mix.level () * width;
  // Kreis x-Position verschieben
  x += input.mix.level () * 20;
  // Kreis zeichnen
  ellipse (x, y, dim, dim);
  
  if (x > width) {
    x = 0;
    y += 20;
  }
}
Simples Spektrum (Wellenform) in Processing

Eine der bekanntesten Darstellungen von Ton und Klang ist das klassische Spekturm. In minim als AudioBuffer bezeichnet, beinhaltet es ein float Array mit einem Wert pro Kanal. Die Anzal der Kanäle wird bei dem Erzeugen des AudioInputs festgelegt. In diesem Fall unterteilen wir das Spektrum zu Beginn im setup-Blocks in 96 Bereiche.
Mit jedem Einzelbild wird eine Kopie des Buffers (toArray()) erzeugt um beim Durchlaufen des Arrays alle Einträge im Spektrum abzubilden. In der dafür benutzen for-Schleife verbindet die Oberseite eines Rechtecks jeweils zwei Teile des Spektrums. Neben der y-Position gibt der Mittelwert beider Kanäle die HSB-Füllfarbe an.

import ddf.minim.*;

Minim minim;
AudioInput input;

float yStart = 100;
float yScale = 140;

void setup () {
  // Sketch einstellen
  size (320, 240);
  smooth();
  noStroke ();
  colorMode (HSB);
  
  // Audiotoolkit anlegen
  minim = new Minim (this);
  input = minim.getLineIn (Minim.STEREO, 96);
}

void draw () {
  background (0);
  
  // Auslesen und speichern des Spektrums
  float[] buffer = input.mix.toArray ();
  // Breite der Rechtecke berechnen
  float step = ceil ((float) width / buffer.length);
  
  // Für jeden einzelnen Eintrag im Buffer wird ein 
  // Rechteck gezeichnet. Es entsteht durch das verbinden
  // zweier Buffer-Einträge. Die Schleife beginnt bei 1, 
  // jeder Eintrag wird mit seinem Vorgänger verbunden.
  for (int i=1; i < buffer.length; i++) {
    
    // Positionen für alle 4 Punkt bestimmen
    float x1 = (i-1) * step;
    float x2 = i * step;
    float y1 = yStart + buffer[i-1] * yScale;
    float y2 = yStart + buffer[i] * yScale;
    
    // Füllfarbe definieren
    float h = (buffer[i-1] + buffer[i]) / 2;
    fill (h * 255, 255, 255);
    
    // Rechteck zeichnen
    beginShape (QUADS);
    vertex (x1, y1);
    vertex (x2, y2);
    vertex (x2, height);
    vertex (x1, height);
    endShape ();
  }
}
Frequenz Gruppen ermitteln in Processing

Die minim Bibliothek bietet neben dem Laden/Abspielen von Dokumenten, dem Nutzen von Audiohardware ein Paket von Werkzeugen um Klange zu erzeugen bzw. bestehendes Material auszuwerten. Eine bekannte Mehthode ist die sog. FFT (Fast Fourier Transform), bei der das Frequenzspektrum in eine feste Anzahl von Frequnzbändern unterteilt wird. Durch das Auslesen dieser Bänder erhält man Aufschluss über die Verteilung von Höhen, Mitten und Tiefen in der Audiodatei.

Da es bei hohen Auflösungen in den Bereichen von 1024, 512 oder 256 Bändern (wie in diesem Fall) nicht zu einer Abfrage jedes Frequenzbandes kommen muss, werden diese in den meisten Fällen zu Gruppen zusammengefasst. Im folgenden Beispiel liefert die Funktion getGroup() diese verkürzte Fassung mit einer Auflösung von 16 Bereichen, welche als Parameter angegeben wird. Die Anzahl der Bereich muss dabei immer ein Vielfaches von 2 sein (2, 4, 8, 16, 32, 64, 128, …). Innerhalb der Funktion wird ein Array angelegt und der Mittelwert einer jeden Frequenzgruppe berechnet, final in einem der Arrayfelder abgelegt. Nach dem Aufruf im draw(), kommt es zu einer simplen Abbildung der Audiodaten um die Funktionsweise zu verdeutlichen.

import ddf.minim.*;
import ddf.minim.analysis.*;
 
Minim minim;
AudioInput input;
FFT fft;

float maxSpec = 0;

void setup () {
  size(320, 240);
  noStroke ();
  
  // Audiotool-Box und Mikrofoneingang erstellen 
  // und einrichten. Die '256' bestimmen dabei die 
  // spätere Auflösung des Spektrums.
  minim = new Minim (this);
  input = minim.getLineIn (Minim.STEREO, 256);
  // FFT-Instanz für die Spektrumsanalyse
  fft = new FFT (input.bufferSize (), 
                 input.sampleRate ());
}
 
void draw () {
  background (0);
  
  float g = 0;    // Grünwert der Füllfarbe
  float h = 0;    // Höhe von Rechteck und Linie
  float specStep; // Breite einer horiz. Linie
  float specScale = (float) width / (fft.specSize () - 1);
  
  // Erzeugen der 'Frequenz-Gruppen' (16 Bereich)
  // mögliche Schritte: 2-4-8-16-32-64-128
  float[] group = getGroup (16);
  
  // Zeichnen des detailierten Frequenzspektrums
  noStroke ();
  for (int i = 0; i < fft.specSize (); i++) {
    g = map (fft.getBand (i), 0, maxSpec, 50, 255);
    h = map (fft.getBand (i), 0, maxSpec, 2, height);
    fill (0, g, 0);
    rect (i * specScale, height - h, specScale, h);
  }
  
  // Zeichnen der Gruppen (Linien)
  stroke (255, 255, 0, 200);
  specStep = width / group.length;
  for (int i=0; i < group.length; i++) {
    h = height - map (group[i], 0, maxSpec, 0, height);
    line (i * specStep, h, (i+1) * specStep, h);
  }
}

/** 
 * Funktion fasst das vorliegende FFT-Spektrum
 * in eine durch den Parameter 'theGroupNum' 
 * festgelegte Anzahl von gleichgroßen Bereichen
 * zusammen – und gibt deren Mittelwert zurück.
 */
float[] getGroup (int theGroupNum) {
  fft.forward (input.mix);
  
  // Leeres Array für die Gruppen erstellen
  float[] group  = new float[theGroupNum];
  // Das FFT-Spekturm hat eine Stelle mehr 
  // als beim Input definiert. (256->257).
  // Diese wird ignoriert.
  int specLimit  = fft.specSize () - 1;
  // Anzahl der Frequenzbänder pro Gruppe
  int groupSize = specLimit / theGroupNum;
  
  // Alle Gruppen mit einem Startwert füllen
  for (int i=0; i < group.length; i++) {
    group[i] = 0;
  }
  
  // Für jedes FFT-Frequenz-Band
  for (int i=0; i < specLimit; i++) {
    // Maximum?
    if (fft.getBand (i) > maxSpec) {
      maxSpec = fft.getBand (i);
    }
    // Jedes Band einer Gruppe zuweisen
    int index = (int) Math.floor (i / groupSize);
    group[index] += fft.getBand (i);
  }
  
  // Der Wert jeder Gruppe durch die Anzahl
  // der enthaltenen Bänder Teilen - >Mittelwert
  for (int i=0; i < group.length; i++) {
    group[i] /= groupSize;
  }
  // Gruppe zurück geben.
  return group;
}