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>
|