P5 und ML5: Tonhöhenerkennung in komplexen Klängen (mp3-file)


Über ML5 kann P5 auf Machine-Learning-Modelle zugeifen, die im Bereich der Mustererkennung Dinge ermöglichen, die vor einigen Jahren für einfache Scriptsprachen wie Javascript oder P5 noch undenkbar waren. So ist es auch möglich eine der komplexesten Aufgaben in der musikalischen Akustik die Tonhöhenerkennung mit Hilfe von trainierten Modellen zu bewältigen:

Um auf diese Möglichkeit zugreifen zu können, muss im Header der HTML-Seite neben der P5-Library und der p5.sound-Library auch die ML5-Library eingebunden werden, so dass es im Header der Seite folgendermaßen heisst:

<script src="header/ml5.min_0.12.2.js"></script>

Im Script werden nach der Deklarierung der für die Verbindung mit der Tonhöhenerkennung via facemesh und die Bilddarstellung notwendigen Variablen ...

var audioContext; // Variable für den Inhalt des Audio-Kontexts
var sound; // Variable für das Klangbeispiel
var pitch; // Array für das Tonhöhenerkennungs-Modell
var tonhoehe = 0; // Variable für die erkannte Frequenz
var modell_geladen = 0; // Zustand des Tonhöhenerkennungsmodells
var pitch_array=[]; // Array für alle erkannten Tonhöhen

wird die setup-Funktion gestartet, in der sowohl der Canvas zur Darstellung der Pitch-Visualisierung erstellt wird als auch der Mikrofoneingang und ein Tongenerator zur Kontrolle angelegt und gestartet werden:

function setup() {
var container = createCanvas(800,400); // Canvas erstellen
container.parent('p5container'); // an DIV-Container anhängen
loadSound('klang/fagott_toene.mp3', soundLoaded); // Klang laden und die Funktion soundLoaded aufrufen.
container.mouseClicked(togglePlay); // mit Klick auf Container togglePlay-Funktion
//auslösen.

sinuston = new p5.SinOsc(); // Sinusgenerator anlegen
sinuston.start(); // Sinusgenerator starten
sampleRate(60); // Bildwiederholfrequenz einstellen
}

 

Dann die TogglePlay-Funktion hinzufügen, dass die Tonerkennung erst dann gestartet wird, wenn der Klang geladen ist.

function togglePlay() {
if (sound && sound.isLoaded()) { //sobald das Klangbeispiel geladen ist und der
// Variablen "sound" zugewiesen wurde:

sound.play(); //Spiele das Klangbeispiel ab.
}
}

Sobald das Klangbeispiel geladen ist, wird es der Variablen "sound" zugeordnet. Im AudioContext wird ein MediaStream generiert und das Klangbeispiel wird in der Variablen "sound" diesem MediaStream zugeordnet. Der MediaStream wird dann an das Tonhöhenerkennungs-Modell geschickt:

function soundLoaded(loadedSound) {
sound = loadedSound; // das geladene Klangbeispiel wird an die Variable "sound" übergeben
sound.play(); // diese wird in den Abspiel-Modus versetzt.
const destination = audioContext.createMediaStreamDestination();
// Ein MediaStream wird generiert
sound.connect(destination); // Das Klangbeispiel wird einem Knoten hinzugefügt ...
const mediaStream = destination.stream; //...der dem MediaStream entspricht.
startPitch(mediaStream); // Das Tonhöhenerkennungsmodell wird mit diesem MediaStream
// gestartet.

}

Sobald der MediaStream gestartet ist, wird mit der Funktion startPitch() die Tonhöhenerkennung geladen:

function startPitch(stream) {
pitch = ml5.pitchDetection('./model_pitch/', audioContext , stream, modelLoaded); // Lade das Modell für die Tonhöhenerkennung, das sich auf dem Server im
// gleichen Ordner unter model_pitch befindet, beziehe es auf die aktuelle Audio-Umgebung
// (audioContext) und darin auf den oben angelegten MediaStream (stream). Sobald es geladen ist,
// starte die Funktion modelLoaded.

}

Sobald das Tonhöhenerkennungsmodell geladen ist, kann mit der Funktion getPitch(), die aktuelle Tonhöhe ermittelt werden:

function modelLoaded() {
modell_geladen = 1; // sobald das
Tonhöhenerkennungsmodell geladen ist ...
getPitch(); // ermittle die aktuelle Tonhöhe
}

Starte mit der Funktion getPitch() die Tonhöhenerkennung:

function getPitch() {
pitch.getPitch(function(err, frequency) { // starte mit dem Modell pitch die Tonhöhenerkennung und gebe sowohl Fehlermeldungen (err) als auch Frequenzen aus (frequency)
if (frequency) { // wenn eine Frequenz ausgegeben wird...
tonhoehe = round(frequency*10)/10; // weise sie mit einer Stelle hinter dem Komma der
// Variablen tonhoehe zu.

} else { // in allen anderen Fällen ...
tonhoehe = 0; // setze den Wert der Frequenz in tonhoehe auf 0 (Hz)
}
})
}

In der draw-Funktion werden dann die erfassten Frequenzen ausgegeben und können dann beliebig weiterverarbeitet werden, z.B.:

function draw(){
background(255,255,255); // weißer Hintergrund einstellen
stroke(150,150,150); strokeWeight(1); // graue Strichfarbe mit Liniendicke 1
// einstellen

line(0,0,0,400);line(0,400,800,400); // X- und Y-Achse für Koordinatensystem anlegen.
for(i=0; i<400; i=i+20){ // in 20er Schritten waagerechte Linien hochzählen
line(0,i,10,i); line(70,i,800,i); // und erstellen
text(2000-(i*5) + " Hz", 14,i+4); // Achsen beschriften
}

if (modell_geladen ==1){ // sobald das Modell geladen ist
getPitch(); // rufe mit jedem Durchlauf die Tonhöhenerkennung auf
text("aktuelle Frequenz: " + tonhoehe + " Hz",600,10); // gebe die in der
// Variablen tonhoehe erfasste Frequenz aus

} else {
text("Pitch-Detection-Modell lädt noch ...",600,10); // gebe aus, dass das
// Tonhöhenerkennungsmodell noch geladen wird

}
if (pitch_array.length<800){ // erzeuge ein fließendes Array mit 800 Einträgen: Solange // weniger als 800 Einträge vorhanden sind:
pitch_array.push(tonhoehe); // füge die erkannte Frequenz am Ende des Arrays hinzu
} else { // sonst (sobald 800 Einträge vorhanden sind):
pitch_array.push(tonhoehe); // füge die erkannte Frequenz am Ende des Arrays hinzu
pitch_array.shift(); // und entferne die erste Frequenz am Anfang des Arrays
}
stroke(150,50,50); strokeWeight(2); // wähle dunkelrot und eine Liniendicke von 2
// Pixeln

beginShape(POINTS); // beginne eine Kurve zu zeichnen ...
for(i=0; i<pitch_array.length; i++){ // über alle Punkte des Arrays
pitch_angepasst = map(pitch_array[i],0,2000,400,0); // passe die erhobenen
// Frequenzen an die Maße des Canvas an

vertex(i,pitch_angepasst); // setze für jede erhobene Frequenz einen Punkt
}
endShape(); // beende die Kurve
sinuston.freq(tonhoehe); // gebe zur Kontrolle einen Sinuston in gleicher Höhe aus.
}

 

Insgesamt sieht das Script dann folgendermaßen aus:

<script src="header/ml5.min_0.12.2.js"></script>
<script src="header/p5.js"></script>

<script>
var audioContext;
var pitch;
var sound;
var tonhoehe = 0;
var modell_geladen = 0;
var pitch_array=[];

function setup() {
var container = createCanvas(800,400);
container.parent('p5container');
audioContext = getAudioContext();
loadSound('klang/fagott_toene.mp3', soundLoaded);
container.mouseClicked(togglePlay);
sinuston = new p5.SinOsc();
sinuston.start();
sampleRate(60);
}

function togglePlay() {
if (sound && sound.isLoaded()) {
sound.play();
}
}

function soundLoaded(loadedSound) {
sound = loadedSound;
sound.play();
const destination = audioContext.createMediaStreamDestination();
sound.connect(destination);
const mediaStream = destination.stream;
startPitch(mediaStream);
}

function startPitch(stream) {
pitch = ml5.pitchDetection('./model_pitch/', audioContext , stream, modelLoaded);
}

function modelLoaded() {
modell_geladen = 1;
getPitch();
}

function getPitch() {
pitch.getPitch(function(err, frequency) {
if (frequency) {
tonhoehe = round(frequency*10)/10;
} else {
tonhoehe = 0;
}
})
}

function draw(){
background(255,255,255);
stroke(150,150,150); strokeWeight(1);
line(0,0,0,400);line(0,400,800,400);
for(i=0; i<400; i=i+20){
line(0,i,10,i); line(70,i,800,i);
text(2000-(i*5) + " Hz", 14,i+4);
}
if (modell_geladen ==1){
getPitch();
text("aktuelle Frequenz: " + tonhoehe + " Hz",600,10);
} else {
text("Pitch-Detection-Modell lädt noch ...",600,10);
}
if (pitch_array.length<800){
pitch_array.push(tonhoehe);
} else {
pitch_array.push(tonhoehe);
pitch_array.shift();
}
stroke(150,50,50); strokeWeight(2);
beginShape(POINTS);
for(i=0; i<pitch_array.length; i++){
pitch_angepasst = map(pitch_array[i],0,2000,400,0);
vertex(i,pitch_angepasst);
}
endShape();
sinuston.freq(tonhoehe);
}

</script>

<DIV id="p5container" style="width:800;border: 1px solid #333;box-shadow: 8px 8px 5px #444;padding: 8px 12px;background-color:ffffff"></DIV>