Montag, 24. Oktober 2016

Audio-Visualisierung in JavaFX - AudioSpectrumListener

Hallo zusammen!

In diesem Beitrag möchte ich zeigen wie man in JavaFX ohne zusätzliche Frameworks oder Bibliotheken Audio visualisieren kann, um zum Beispiel einen Visualizer für einen mp3-Player zu erstellen. [gehe zur englischen Version]

ACHTUNG!
Ich gehe davon aus, dass bereits bekannt ist wie man in JavaFX Audio-Dateien über den MediaPlayer abspielt. Ansonsten einfach mal danach bei Google suchen ;). Außerdem funktioniert dies nicht auf einem Raspberry, da die Media-API von JavaFX dort generell nicht funktioniert!

Okay los gehts!



Woher bekomme ich die Daten?

Um Audio zu visualisieren, benötigen wir zunächst einmal Daten um diese anzeigen zu können. Dazu besitzt JavaFX den sogenannten AudioSpectrumListener. Dieser kann einem MediaPlayer Objekt zugewiesen werden durch die setAudioSpectrumListener()-Methode.

 mediaPlayer.setAudioSpectrumListener(new SpektrumListener());  
   
 private class SpektrumListener implements AudioSpectrumListener {  
     @Override  
     public void spectrumDataUpdate(double timestamp, double duration,   
 float[] magnitudes, float[] phases) {}
 }  

In diesem Fall ist mediaPlayer ein MediaPlayer-Objekt und um die Übersicht zu verbessern, verwende ich eine interne Klasse statt einer anonymen von einem AudioSpectrumListener.

Wie man sieht enthält der AudioSpectrumListener eine spectrumDataUpdate()-Methode. Diese übergibt uns alle 100ms (Standard-Wert) den Zeitstempel, den Zeitraum über welchen die Daten gesammelt wurden, die Amplitude (zumindest glaube ich, dass dies das deutsche Äquivalent ist) und die Phase. Zu beachten ist, das dies für jedes Frequenzband gemacht wird (Standard-Wert: 128), diese werden gleichmäßig von JavaFX aufgeteilt und daher bekommen wir ein Array von Amplituden und Phasen.

Doch nun ist die Frage, was können wir mit diesen Werten anfangen? Nun ja, ich bin leider kein Musiker, daher verwende ich persönlich immer nur die Amplituden um Audio zu visualisieren. Leider kann man in den meisten Fällen die erhaltenen Werte nicht ohne weiteres nutzen, da diese Werte kleiner gleich 0 (Dezibel) sind. Für alle die sich nun denken: "Okay, dann nehme ich diese Werte halt einfach *(-1) und gut ist." So einfach geht dies nicht. Frequenzbänder welche momentan keine Töne abspielen, haben keinen Wert von 0 sondern von -60! Das heißt wenn man diese einfach nur in positive Werte umwandelt durch *(-1) erhalten wir einen Wert von 60, obwohl überhaupt kein Ton abgespielt wird! Ein Musiker könnte nun ganz genau erklären warum dies so ist, aber da ich in diesem Bereich keine Ahnung habe, belassen wir es erstmal dabei. Doch die wichtigere Frage ist: Woher kommt die -60?

Die -60 ist der Standard-Wert für den Schwellenwert in JavaFX, also die Grenze ab wann JavaFX die Werte begrenzt. Natürlich können wir diesen mit der setAudioSpectrumThreshold()-Methode jederzeit ändern. Hierbei ist dann zu bedenken, dass keine Töne diesen geänderten Wert haben, also wenn der neue Schwellenwert -100 ist, so haben keine Töne ebenfalls einen Wert von -100! Dies können wir uns nun zu Nutze machen um korrekte positive Werte zu bekommen. Dazu müssen wir von den erhaltenen Werten einfach den Schwellenwert abziehen, so wird aus -60 eine 0 bei keinen Tönen. Der folgende Quellcode zeigt dies nochmal anhand des ersten Frequenzbandes:

 mediaPlayer.setAudioSpectrumListener(new SpektrumListener());  
   
 private class SpektrumListener implements AudioSpectrumListener {  
     @Override  
     public void spectrumDataUpdate(double timestamp, double duration,   
 float[] magnitudes, float[] phases) {  
   
     correctedMagnitude[0] = magnitudes[0] - mediaPlayer.getSpektrumThreshold();  
 }  

Da wir jedoch nicht nur den Wert vom ersten Frequenzband haben wollen sondern von allen, nutzen wir noch eine for-Schleife:

 mediaPlayer.setAudioSpectrumListener(new SpektrumListener());  
   
 private class SpektrumListener implements AudioSpectrumListener {  
     @Override  
     public void spectrumDataUpdate(double timestamp, double duration,   
 float[] magnitudes, float[] phases) {  
   
     for (int i = 0; i < magnitudes.length; i++) {
         correctedMagnitude[i] = magnitudes[i] - mediaPlayer.getSpektrumThreshold(); 
     } 
 }  

Nun können wir die Werte in correctedMagnitude[] nutzen um Audio zu visualisieren, da dies nun positive Werte sind, welche einfach visualisiert werden können. Später werde ich ein Beispiel zeigen!

Wofür der Schwellenwert?

Doch vorher noch eine kleine Sache: Denn wer diese Werte sich nun anschaut wird feststellen, dass die höheren Frequenzbänder immer kleinere Werte bekommen. Diese sind irgendwann so tief, dass sie im Verhältnis zu den anderen Werten zu niedrig sind um effektiv angezeigt werden zu können. Um dies ein bisschen besser zeigen zu können hier die Werte aus dem Song Money for Nothing von Dire Straits (1:20 bis 1:30) für 32 Frequenzbänder mit einem Standard-Schwellenwert von -60:

Positive Werte der Frequenzbänder bei einem Schwellenwert von -60.

Wie man sieht kann das letzte Band überhaupt nicht angezeigt werden, da es in einem Bereich unter 1 ist, auch wenn dort eigentlich Töne vorhanden sind. Wenn wir nun aber den Schwellenwert auf -120 herabsetzen sieht unsere Grafik schon etwas anders aus:

Positive Werte der Frequenzbänder bei einem Schwellenwert von -120.

Wie man jetzt sieht, ist nun auch das letzte Frequenzband eindeutig sichtbar. Daher würde ich empfehlen ein wenig mit dem Schwellenwert herum zu spielen um zu schauen wie stark man auch die letzten Frequenzbänder anzeigen will. Meine generelle Empfehlung ist ein Schwellenwert von -100, falls man alle genau anzeigen möchte, oder -80, wenn es nicht unbedingt alle sein müssen.

Beispiel: Visualizer

So nun zu einem kleinen Beispiel wie man diese Werte nutzen kann um Audio in JavaFX zu visualisieren. Hierbei möchte ich noch einmal darauf hinweisen, dass ich kein großer Fan von zusätzlichen Frameworks oder Bibliotheken bin, wenn man das gleiche auch mit den JavaFX eigenen Bibliotheken machen kann. Daher nutzen wir ein AreaChart um einen Visualizer zu erstellen!

Ich möchte hier nun nicht den FXML Code zeigen für ein AreaChart, daher gehe ich einfach nur davon aus das dieser die fx:id="spektrum" hat und im Controller bekannt ist. Daten in einem AreaChart werden in Serien dargestellt, in unserem Fall haben wir nur eine Serie von Daten, welche sich regelmäßig ändern. Daher erstellen wir in der initialize()-Methode des Controllers eine neue Serie:

 public void initialize(URL location, ResourceBundle resources) {  
   
     XYChart.Series<String, Number> series1 = new XYChart.Series<>();  

}  

Diese Serie benötigt aber nun auch Daten, welche durch ein Array vom Typ XYChart.Data dargestellt werden. Da wir auch die Werte zu Beginn alle auf 0 stehen haben wollen benötigen wir noch eine for-Schleife, welche dies erledigt und anschließend der Serie zuweist. Am Ende fügen wir die Serie dann dem AreaChart "Spektrum" hinzu. Die Konstante BANDS repräsentiert die Anzahl der Frequenzbänder also im Grunde mediaPlayer.getAudioSpectrumNumBands(). Integer.toString(i + 1) dient lediglich dazu, dass das Frequenzband 1 auch als 1 erscheint und nicht als 0, was aber keinen Einfluss auf die Visualisierung hat.

 public void initialize(URL location, ResourceBundle resources) {  
   
     XYChart.Series<String, Number> series1 = new XYChart.Series<>();  
     XYChart.Data[] series1Data = new XYChart.Data[BANDS];  
     for (int i = 0; i < series1Data.length; i++) {  
       series1Data[i] = new XYChart.Data<>(Integer.toString(i + 1), 0);  
       series1.getData().add(series1Data[i]);  
     }  
     spektrum.getData().add(series1);  
  
}  

Nun müssen wir die Daten in der Serie im AudioSpectrumListener ändern und JavaFX zeigt diese automatisch im AreaChart an. Hier der entsprechende Code:

 private class SpektrumListener implements AudioSpectrumListener {  
   float[] buffer = createFilledBuffer(BANDS, mediaplayer.getAudioSpectrumThreshold());  
   
   @Override  
   public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {    
     for (int i = 0; i < magnitudes.length; i++) {
        series1Data[i].setYValue(magnitudes[i] - mediaplayer.getAudioSpectrumThreshold());
     }  
   }  
 }  

Tja, das war es im Grunde auch schon. Jetzt haben wir einen Visualizer, welcher Audio visualisiert. Jedoch sieht dieser ein wenig abgehackt aus, dies liegt daran das er direkt die neuen Werte anzeigt ohne fließenden Übergang. Wenn man Animationen beim AreaChart aktiviert, bringt dies auch keine Verbesserung, da die Werteänderungen nun zu schnell für die Animation sind. Daher fügen wir manuell einen "Buffer" ein, welcher extra ein wenig hinter her ist, damit es flüssiger aussieht. Der folgende Code zeigt den Buffer:

 private class SpektrumListener implements AudioSpectrumListener {  
   float[] buffer = createFilledBuffer(BANDS, mediaplayer.getAudioSpectrumThreshold());  
   
   @Override  
   public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases) {  
   
     for (int i = 0; i < magnitudes.length; i++) {  
       if (magnitudes[i] >= buffer[i]) {  
         buffer[i] = magnitudes[i]; 
         series1Data[i].setYValue(magnitudes[i] - mediaplayer.getAudioSpectrumThreshold());  
       } else { 
         series1Data[i].setYValue(buffer[i] - mediaplayer.getAudioSpectrumThreshold());  
         buffer[i] -= 0.25;  
       }  
     }  
   }  
 }  

private float[] createFilledBuffer(int size, float fillValue) {  
   float[] floats = new float[size];  
   Arrays.fill(floats, fillValue);  
   return floats;  
}

Im Grunde ist dieser Code ziemlich selbsterklärend, falls der aktuelle Wert über dem Buffer ist, wird der aktuelle Wert genommen, ansonsten der Wert vom Buffer und anschließend um einen kleinen Wert, hier 0.25, verringert.

Um das ganze in Aktion zu sehen hier ein kleines Video von meinem FXPlayer, aus welchem dieser Code im Grunde stammt:





Des weiteren hier noch mal ein Video von meiner anderen JavaFX Anwendung AudiPic, welche ebenfalls den AudioSpectrumListener nutzt um Bilder der Songs zu erstellen:


Ich hoffe ich konnte euch damit ein wenig weiterhelfen und wünsche euch viel Spaß beim visualisieren von Audio in JavaFX!

Bis zum nächsten mal!

Keine Kommentare:

Kommentar veröffentlichen