Audio Features ermitteln mit Essentia.js

Seit 2021 stehen die Möglichkeiten von Essentia auch für Javascript zur Verfügung. Mit mehr als 200 Feature-Algorithmen ist es mit Abstand die größte Audio Feature Library, die in Javascript zu finden ist.

 

Anders als andere Javascript Libraries ist sie weniger für den Echtzeit-Betrieb ausgelegt, sondern eher für die Berechnung der Audio Features im Hintergrund. Für ihre Verwendung benötigt man mindestens 3 Libraries, die in den Header der Seite eingebunden werden müssen:

<script src="header/jquery.min.js"></script>
<!-- vereinfacht das Javascript Handling, hat sonst nichts mit Essentia.js zu tun -->
<script src="https://cdn.jsdelivr.net/npm/essentia.js@0.1.0/dist/essentia-wasm.web.js"></script>
<!-- Web Assembly Modul (WASM) = Backend, um die Standard C++ Algorithmen von Essentia verwenden zu können -->
<script src="header/essentia.js-core.js"></script>
<!-- die eigentliche Essentia Bibliothek-->

 

Auf der Seite selbst sollte im Body ein Audio-Player eingebunden sein, mit dem man sich das zu analysierende Klangbeispiel auch anhören kann:

<audio id="audioPlayer" controls style="width:100px">
<source id="audio-source" src="" type="audio/mp3" />
</audio>

 

Darüber hinaus benötigt man noch einen Button, mit dem man den Analysevorgang über die Funktion featureExtractor(); starten kann, z.B.

<button type="button" name="Signal Analysis" onclick="featureExtractor();">
Start Signal Analysis</button>

 

sowie einen Bereich für die Ausgabe des Ergebnisses, z.B.

<span id="datenausgabe"> </span>

 

und natürlich ein Klangbeispiel zum Analysieren, z.B. eine Fagott-Tonleiter .

Danach legt man mit <script defer> .. </script> einen Scriptbereich an, in dem die Analyse ausgeführt wird (das defer bedeutet, dass das Script mit der Ausführung wartet, bis alle Teile der Seite geladen und geparst sind).

In diesem Bereich bestimmt man zunächst die auf jeden Fall benötigten Variablen und legt einen Audio-Context fest:

<script defer>
//--------------------------------------------------------------------------
// grundlegende Variablen definieren

//--------------------------------------------------------------------------

let essentia; // Variable, über die alles abgewickelt wird
let audioURL = "klang/fagott_toene.mp3"; // Klangbeispiel
let audioData; //Variable für die Wellenform des Klangbeispiels

//--------------------------------------------------------------------------
//
Audio-Context für alle Browsertypen erzeugen

//--------------------------------------------------------------------------

const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();

</script>

 

Als nächsten Schritt bestimmt man die Funktion featureExtractor() {...}.
Innerhalb dieser werden alle gewünschten Audio Features extrahiert.
In dieser Funktion wird zunächst via essentia.getAudioChannelDataFromURL(...) die Wellenform aus der Audiodatei an die Variable audioData übergeben:

<script defer>
//--------------------------------------------------------------------------
// grundlegende Variablen definieren

//--------------------------------------------------------------------------

let essentia; // Variable, über die alles abgewickelt wird
let audioURL = "klang/fagott_toene.mp3"; // Klangbeispiel
let audioData; //Variable für die Wellenform des Klangbeispiels

//--------------------------------------------------------------------------
//
Audio-Context für alle Browsertypen erzeugen

//--------------------------------------------------------------------------

const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();

//--------------------------------------------------------------------------
// featureExtractor Funktion starten

//--------------------------------------------------------------------------

async function featureExtractor() {
audioData = await essentia.getAudioChannelDataFromURL(audioURL, audioCtx, 0); // übergebe die Daten von der Audio-Datei beschrieben in
// audioURL innerhalb des Audio-Context audioCtx an die Variable
// AudioData und verwende dabei den linken Kanal (0
)

}

</script>

 

Danach hat man zwei Möglichkeiten zur Feature Extraktion:

Man kann Audio Features mit Hilfe eines Vectors der Audiodaten essentia.arrayToVector(audioData) aus der Datei extrahieren oder über essentia.FrameGenerator(...) eine frame-weise Extraktion der Audiodaten ausformulieren:

 

 

Audio Features mit Hilfe von essentia.arrayToVector(audioData) extrahieren

Die meisten (eindimensionalen) Features lassen sich sehr komfortabel mit Hilfe von essentia.arrayToVector(audioData) extrahieren. Hier gibt es zum einen Einzelwerte für ein ganzes Stück, die in einzelnen Variablen extrahiert werden können, wie z.B. Dauer, Einschwingzeit, RMS oder Danceability.

Zum anderen gibt es Serien von aufeinanderfolgenden Werten, die in Arrays zurückgegeben werden, wie z.B. Tonhöhen oder spektrale Features.

Darüber hinaus gibt es auch Algorithmen-Sammlungen, mit deren Hilfe man auf einen Schlag gleich ganz viele Features extrahieren kann. Vor allem fünf Algorithmen-Sammlungen sind hier sehr spannend:

essentia.LowLevelSpectralEqloudExtractor(); // spektrale Eigenschaften extrahieren
essentia.LowLevelSpectralExtractor(); // spektrale Eigenschaften extrahieren
essentia.RhythmDescriptors(); // Rhythmische Eigenschaften extrahieren
essentia.TonalExtractor(); //
Tonart, Tongeschlecht etc. extrahieren
essentia.KeyExtractor(); // Tonart, Tongeschlecht etc. extrahieren

Um die eindimensionalen Features zu extrahieren, müssen die Audiodaten in der Funktion featureExtractor(){...} via essentia.arrayToVector(audioData); in einen Vektor umgewandelt werden, über den dann die Variablen erhoben werden können.
Die Funktion bis zur Umwandlung in einen Vektor sieht dann folgendermaßen aus:

<script defer>
//--------------------------------------------------------------------------
// grundlegende Variablen definieren

//--------------------------------------------------------------------------

let essentia; // Variable, über die alles abgewickelt wird
let audioURL = "klang/fagott_toene.mp3"; // Klangbeispiel
let audioData; //Variable für die Wellenform des Klangbeispiels

//--------------------------------------------------------------------------
//
Audio-Context für alle Browsertypen erzeugen

//--------------------------------------------------------------------------

const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();

//--------------------------------------------------------------------------
// featureExtractor Funktion starten

//--------------------------------------------------------------------------

async function featureExtractor() {
audioData = await essentia.getAudioChannelDataFromURL(audioURL, audioCtx, 0); // übergebe die Daten von der Audio-Datei beschrieben in
// audioURL innerhalb des Audio-Context audioCtx an die Variable
// AudioData und verwende dabei den linken Kanal (0)

//--------------------------------------------------------------------
// Umwandlung der Audio-Daten in einen Vektor
//--------------------------------------------------------------------
const audioVector = essentia.arrayToVector(audioData); // in der Variablen audioVector
// ist die Wellenform als Vektor hinterlegt. Hieraus können nun alle möglichen
// Informationen ermittelt werden.

// [...] hier würde dann die Auswertung der einzelnen Audio-Features stehen, z.B.
// let audio_duration = essentia.Duration(audioVector).duration;
// let onsetRate = essentia.OnsetRate(audioVector).onsetRate;
// let rms = essentia.RMS(audioVector).rms;
// etc.

}

</script>

 

Auf der Grundlage des audioVector lassen sich einzelne (grundlegende) Variablen sehr schnell ermitteln, z.B.

let audio_duration = essentia.Duration(audioVector).duration;
// Dauer des Beispiels wird an die Variable audio_duration übergeben
let onsetRate = essentia.OnsetRate(audioVector).onsetRate;
// Onset-Rate des Beispiels wird an die Variable onsetRate übergeben
let rms = essentia.RMS(audioVector).rms;
// RMS-Pegel des Beispiels wird an die Variable rms übergeben
let leq = essentia.Leq(audioVector).leq;
// Leq (equivalent Sound Level in dB) des Beispiels wird an die Variable leg übergeben
let loudness = essentia.Loudness(audioVector).loudness;
// Lautheit des Beispiels wird an die Variable loudness übergeben
let loudnessVickers = essentia.LoudnessVickers(audioVector).loudness;
// Lautheit nach Vickers des Beispiels wird an die Variable loudnessVickers übergeben
let gain = essentia.ReplayGain(audioVector, 44100).replayGain;
// Replay Gain [distance to suitable replay level (~-31dB)] wird an die Variable gain
// übergeben


Bei anderen Variablen müssen noch zusätzliche Informationen über die Samplerate, den Abstand der Frames (hopSize) oder die Breite der Frames (frameSize) hinzugefügt werden, wie z.B. bei BPM oder High Level Features wie Danceability o.ä., z.B.:

let bpm = essentia.PercivalBpmEstimator(audioVector,
1024, //frame size for the analysis of the input signal
2048, //frame size for the analysis of the Onset Strength Signal
128, //hop size for the analysis of the input signal
128, //hop size for the analysis of the Onset Strength Signal
210, //maximum BPM to detect
50, //minimum BPM to detect
44100 //the sampling rate of the audio signal [Hz]
).bpm; //BPM des Beispiels wird an die Variable bpm übergeben

let danceability = essentia.Danceability( audioVector,
8800, //maxTau, maximum segment length to consider [ms]
310, //minTau, minimum segment length to consider [ms]
44100, //SampleRate [Hz]
1.1 //tauMultiplier, multiplier to increment from min to max tau
).danceability; // Danceability des Beispiels wird an die Variable danceability
// übergeben

let intensity = essentia.Intensity(audioVector,
44100 //SampleRate [Hz]
).intensity; // Intensität des Beispiels wird an die Variable intensity übergeben

 

Arrays mit Audio Features werden auf eine ähnliche Art und Weise erhoben, nur muss der ermittelte Vektor wieder in ein Array zurückverwandelt werden, wie z.B. bei der Ermittlung des Tonhöhenverlaufs oder des Vibratos:

let pitchMelodia = essentia.PitchMelodia(audioVector,
// Tonhöhe ermitteln über den Melodia-Algorithmus
10, // binResolution, Genauigkeit der Tonhöheneinschätzung in Cents
3, // filterIterations, Anzahl der Filterungen, um Oktavverwechslungen auszuschließen
2048, // frameSize
false, // guessUnvoiced,
// ob bei nicht-erkennbarem Pitch dennoch einer erkannt werden soll

0.8, // harmonicWeight, Gewichtung zwischen zwei aufeinanderfolgenden Teiltönen
128, // hopSize
);
let pitchMelodia_array = essentia.vectorToArray(pitchMelodia.pitch);
// Tonhöhen-Vektor in ein Array umwandeln.

 

Über das Tonhöhen-Ergebnis pitchMelodia.pitch lässt sich z.B. das Vibrato ermitteln:

let vibratoOut = essentia.Vibrato(pitchMelodia.pitch,
500, // maxExtend, stärkster erwarteter Umfang des Vibratos in Cents
8, // maxFrequency, größte erwartete Vibrato-Frequenz in Hz
50, // minExtend, kleinster erwarteter Umfang des Vibratos in Cents
1, // minFrequency, kleinste erwartete Vibrato-Frequenz in Hz
344.53125 // Entdeckungs-Samplerate für das Vibrato: 44100 Hz / 128 hopSize = 344,53125
); //Einstellungen möglichst nicht verändern!!!
vibratoHz_array=essentia.vectorToArray(vibratoOut.vibratoFrequency);
// Vibrato-Frequenzen in ein Array umwandeln
vibratoDepth_array=essentia.vectorToArray(vibratoOut.vibratoExtend);
// Vibrato-Tiefe in ein Array umwandeln

 

oder die Tonhöhen-Kontur:

let pitchContour= essentia.PitchContourSegmentation(pitchMelodia.pitch, audioVector,
128, // hopSize
0.1, // minDuration, kürzeste Notendauer in s
60, // pitchDistanceThreshold, ab wann ein Tonhöhenunterschied erkannt wird (in Cents)
-2, // rmsThreshold, ab welcher Schwelle ein Tonwechsel erkannt wird
44100, //Samplerate
440 // Stimmtonhöhe
);
let pitchContourOnset_array = essentia.vectorToArray(pitchContour.onset);
// die gefundenen Onsets in ein Array umwandeln

let pitchContourDuration_array = essentia.vectorToArray(pitchContour.duration);
// die gefundenen Tondauern in ein Array umwandeln

let pitchContourMIDIpitch_array = essentia.vectorToArray(pitchContour.MIDIpitch);
// die gefundenen MIDI-Tonhöhen in ein Array umwandeln

 

In diesem Zusammenhang ist auch das Feature AfterMaxToBeforeMaxEnergyRatio() interessant, das anzeigt, ob eine Melodielinie eher absteigend oder eher aufsteigend verläuft:

let ascending_or_falling_pitch= essentia.AfterMaxToBeforeMaxEnergyRatio(pitchMelodia.pitch).afterMaxToBeforeMaxEnergyRatio;
// 0 bis 1 bedeutet, die Melodie ist eher aufsteigend.
// Ein Wert >1 bedeutet, die Melodie ist eher absteigend.

 

 

Spektrale Eigenschaften und Eigenschaften von Akkorden und Tonalität lassen sich am schnellsten über die "Sammelpackungen" LowLevelSpectralEqloudExtractor(); LowLevelSpectralExtractor(); und KeyExtractor(); ermitteln:

Grundlegende spektrale Features wie Spectral Centroid, Dissonance, Spectral Kurtosis etc. erhält man in zwei Schritten als Arrays über den LowLevelSpectralEqloudExtractor():
Erst wird über LowLevelSpectralEqloudExtractor()
die spektrale Information aus dem audioVector in ein Objekt geschrieben (d.h. eine Art Sammelvariable, die alle möglichen Variablen-Typen wie Arrays, Strings, Floats etc. beinhalten kann).
Dann werden aus diesem die Vektoren der einzelnen Features ermittelt, die dann via vectorToArray() jeweils in ein Array umgewandelt werden:

// 1. Ermitteln der spektralen Informationen pro Frame
LowLevelSpectralEq_object = essentia.LowLevelSpectralEqloudExtractor( audioVector,
2048, // frameSize
128, // hopSize
44100, // SampleRate [Hz]
);

// 2. Arrays zu den einzelnen Audio Features aus den spektralen Informationen ermitteln
let dissonance_array = essentia.vectorToArray(LowLevelSpectralEq_object.dissonance);
let spectral_centroid_array = essentia.vectorToArray(LowLevelSpectralEq_object.spectral_centroid);
let spectral_kurtosis_array = essentia.vectorToArray(LowLevelSpectralEq_object.spectral_kurtosis);
let spectral_skewness_array = essentia.vectorToArray(LowLevelSpectralEq_object.spectral_skewness);
let spectral_spread_array = essentia.vectorToArray(LowLevelSpectralEq_object.spectral_spread);

 

Dies geht noch viel ausgeprägter mit dem LowLevelSpectralExtractor():

// 1. Ermitteln der spektralen Informationen pro Frame
LowLevelSpectralEx_object = essentia.LowLevelSpectralExtractor( audioVector,
2048, // frameSize
128, // hopSize
44100, //Samplerate [[Hz]
);

// 2. Arrays zu den einzelnen Audio Features aus den spektralen Informationen ermitteln
let barkbands_kurtosis_array = essentia.vectorToArray(LowLevelSpectralEx_object.barkbands_kurtosis);
let barkbands_skewness_array = essentia.vectorToArray(LowLevelSpectralEx_object.barkbands_skewness);
let barkbands_spread_array = essentia.vectorToArray(LowLevelSpectralEx_object.barkbands_spread);
let hfc_array = essentia.vectorToArray(LowLevelSpectralEx_object.hfc);
let pitch_array = essentia.vectorToArray(LowLevelSpectralEx_object.pitch);
let pitch_instantaneous_confidence_array = essentia.vectorToArray(LowLevelSpectralEx_object.pitch_instantaneous_confidence);
let pitch_salience_array = essentia.vectorToArray(LowLevelSpectralEx_object.pitch_salience);
let silence_rate_20dB_array = essentia.vectorToArray(LowLevelSpectralEx_object.silence_rate_20dB);
let silence_rate_30dB_array = essentia.vectorToArray(LowLevelSpectralEx_object.silence_rate_30dB);
let silence_rate_60dB_array = essentia.vectorToArray(LowLevelSpectralEx_object.silence_rate_60dB);
let spectral_complexity_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_complexity);
let spectral_crest_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_crest);
let spectral_decrease_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_decrease);
let spectral_energy_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_energy);
let spectral_energyband_low_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_energyband_low);
let spectral_energyband_middle_low_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_energyband_middle_low);
let spectral_energyband_middle_high_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_energyband_middle_high);
let spectral_energyband_high_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_energyband_high);
let spectral_flatness_db_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_flatness_db);
let spectral_flux_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_flux);
let spectral_rms_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_rms);
let spectral_rolloff_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_rolloff);
let spectral_strongpeak_array = essentia.vectorToArray(LowLevelSpectralEx_object.spectral_strongpeak);
let zerocrossingrate_array = essentia.vectorToArray(LowLevelSpectralEx_object.zerocrossingrate);
let inharmonicity_array = essentia.vectorToArray(LowLevelSpectralEx_object.inharmonicity);
let oddtoevenharmonicenergyratio_array = essentia.vectorToArray(LowLevelSpectralEx_object.oddtoevenharmonicenergyratio);

 

Oder auch mit vielen Voreinstellungen (am besten so lassen) über den KeyExtractor(), der anstelle von Arrays einzelne Variablen ausgibt:

let key_object = essentia.KeyExtractor(audioVector, // Tonart ermitteln
true, // shifts a pcp to the nearest tempered bin
4096, // the framesize for computing tonal features
4096, // the hopsize for computing tonal features
12, // the size of the output HPCP (must be a positive nonzero multiple of 12)
3500, // max frequency to apply whitening to [Hz]
60, // the maximum number of spectral peaks
25, // min frequency to apply whitening to [Hz]
0.2, // pcp bins below this value are set to 0
'bgate', // the type of polyphic profile to use for correlation calculation
44100, // the sampling rate of the audio signal [Hz]
0.0001, // the threshold for the spectral peaks
440, // the tuning frequency of the input signal
'cosine', //type of weighting function for determining frequency contribution
'hann' //the window type, which can be 'hamming', 'hann', 'triangular', 'square' or
// 'blackmanharrisXX'

);
tonart = key_object.key;
tongeschlecht= key_object.scale;
tonart_confidence = key_object.strength;

 

Am Ende dieses Vorgangs liegen die Features als einzelne Variablen oder Arrays vor, so dass man die Einzelwerte in den Arrays über eine for-Schleife aus dem jeweiligen Array herausholen und in der Datenausgabe auf der Seite darstellen kann, z.B. für die Tonhöhen oder fürs Vibrato:

let pitchMelodia_daten = ""; // Variable für die Sammlung der Daten definieren
for (i=0;i<pitchMelodia_array.length; i++){ // For-Schleife über das Pitch-Array starten
pitchMelodia_daten = pitchMelodia_daten + pitchMelodia_array[i].toFixed(1)+ ", ";
} // für jeden Eintrag die Frequenz herausholen und an pitchMelodia_daten anhängen
pitchMelodia_ausgabe="pitchMelodia_array=["+pitchMelodia_daten+"];";
// die gesammelten Daten an die Variable pitchMelodia_ausgabe übergeben.

let vibratoHz_daten = ""; // Variable für die Sammlung der Daten definieren
for (i=0;i<vibratoHz_array.length; i++){ // For-Schleife über das VibratoHz-Array starten
vibratoHz_daten = vibratoHz_daten + vibratoHz_array[i]+ ", ";
} // für jeden Eintrag die Frequenz herausholen und an VibratoHz_daten anhängen
vibratoHz_ausgabe="VibratoHz_array=["+vibratoHz_daten+"];";
// die gesammelten Daten an die Variable VibratoHz_ausgabe übergeben.

let vibratoDepth_daten = ""; // Variable für die Sammlung der Daten definieren
for (i=0;i<vibratoDepth_array.length; i++){ // For-Schleife über das Vibrato-Array starten
vibratoDepth_daten = vibratoDepth_daten + vibratoDepth_array[i]+ ", ";
} // für jeden Eintrag die Modulationstiefe herausholen und an VibratoDepth_daten anhängen
vibratoDepth_ausgabe="vibratoDepth_array=["+vibratoDepth_daten+"];";
// die gesammelten Daten an die Variable VibratoDepth_ausgabe übergeben.

document.getElementById("datenausgabe").innerHTML= pitchMelodia_ausgabe+"<br><br><br>"+vibratoHz_ausgabe+"<br><br><br>"+vibratoDepth_ausgabe; // Den Inhalt der Variablen pitchMelodia_ausgabe, vibratoHz_ausgabe und vibratoDepth_ausgabe im Feld Datenausgabe auf der Webseite ausgeben.

 

Nun muss nur noch die featureExtractor() { ...}-Funktion mit essentia.algorithms.delete(); abgeschlossen werden und das WASM-Modul hinzugefügt werden, welches gestartet wird, nachdem die Seite vollständig geladen und geparst ist.

Das gesamte Script für die Erfassung der Tonhöhen und das Vibratos sähe dann z.B. folgendermaßen aus:

<!DOCTYPE html>
<html>
<head>

<script src="header/jquery.min.js"></script>
<!-- vereinfacht das Javascript Handling, hat sonst nichts mit Essentia.js zu tun -->
<script src="https://cdn.jsdelivr.net/npm/essentia.js@0.1.0/dist/essentia-wasm.web.js"></script>
<!-- Web Assembly Modul (WASM) = Backend, um die Standard C++ Algorithmen von Essentia verwenden zu können -->
<script src="header/essentia.js-core.js"></script>
<!-- die eigentliche Essentia Bibliothek-->

 

<script defer>
//--------------------------------------------------------------------------
// grundlegende Variablen definieren

//--------------------------------------------------------------------------

let essentia; // Variable, über die alles abgewickelt wird
let audioURL = "klang/fagott_toene.mp3"; // Klangbeispiel
let audioData; //Variable für die Wellenform des Klangbeispiels

//--------------------------------------------------------------------------
//
Audio-Context für alle Browsertypen erzeugen

//--------------------------------------------------------------------------

const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();

//--------------------------------------------------------------------------
// featureExtractor Funktion starten

//--------------------------------------------------------------------------

async function featureExtractor() {
audioData = await essentia.getAudioChannelDataFromURL(audioURL, audioCtx, 0); // übergebe die Daten von der Audio-Datei beschrieben in
// audioURL innerhalb des Audio-Context audioCtx an die Variable
// AudioData und verwende dabei den linken Kanal (0)

//--------------------------------------------------------------------
// Umwandlung der Audio-Daten in einen Vektor
//--------------------------------------------------------------------
const audioVector = essentia.arrayToVector(audioData); // in der Variablen audioVector
// ist die Wellenform als Vektor hinterlegt. Hieraus können nun alle möglichen
// Informationen ermittelt werden.

//--------------------------------------------------------------------
// Ermitteln der Audio Features
//--------------------------------------------------------------------

let pitchMelodia = essentia.PitchMelodia(audioVector,
// Tonhöhe ermitteln über den Melodia-Algorithmus
10, // binResolution, Genauigkeit der Tonhöheneinschätzung in Cents
3, // filterIterations, Anzahl der Filterungen, um Oktavverwechslungen auszuschließen
2048, // frameSize
false, // guessUnvoiced,
// ob bei nicht-erkennbarem Pitch dennoch einer erkannt werden soll

0.8, // harmonicWeight, Gewichtung zwischen zwei aufeinanderfolgenden Teiltönen
128, // hopSize
);
let pitchMelodia_array = essentia.vectorToArray(pitchMelodia.pitch);
// Tonhöhen-Vektor in ein Array umwandeln.

let vibratoOut = essentia.Vibrato(pitchMelodia.pitch,
500, // maxExtend, stärkster erwarteter Umfang des Vibratos in Cents
8, // maxFrequency, größte erwartete Vibrato-Frequenz in Hz
50, // minExtend, kleinster erwarteter Umfang des Vibratos in Cents
1, // minFrequency, kleinste erwartete Vibrato-Frequenz in Hz
344.53125 // Entdeckungs-Samplerate für das Vibrato: 44100 Hz / 128 hopSize = 344,53125
); //Einstellungen möglichst nicht verändern!!!
vibratoHz_array=essentia.vectorToArray(vibratoOut.vibratoFrequency);
// Vibrato-Frequenzen in ein Array umwandeln
vibratoDepth_array=essentia.vectorToArray(vibratoOut.vibratoExtend);
// Vibrato-Tiefe in ein Array umwandeln

//--------------------------------------------------------------------
// Ausgabe der Audio Features
//--------------------------------------------------------------------

let pitchMelodia_daten = ""; // Variable für die Sammlung der Daten definieren
for (i=0;i<pitchMelodia_array.length; i++){ // For-Schleife über das Pitch-Array starten
pitchMelodia_daten = pitchMelodia_daten + pitchMelodia_array[i].toFixed(1)+ ", ";
} // für jeden Eintrag die Frequenz herausholen und an pitchMelodia_daten anhängen
pitchMelodia_ausgabe="pitchMelodia_array=["+pitchMelodia_daten+"];";
// die gesammelten Daten an die Variable pitchMelodia_ausgabe übergeben.

let vibratoHz_daten = ""; // Variable für die Sammlung der Daten definieren
for (i=0;i<vibratoHz_array.length; i++){ // For-Schleife über das VibratoHz-Array starten
vibratoHz_daten = vibratoHz_daten + vibratoHz_array[i]+ ", ";
} // für jeden Eintrag die Frequenz herausholen und an VibratoHz_daten anhängen
vibratoHz_ausgabe="VibratoHz_array=["+vibratoHz_daten+"];";
// die gesammelten Daten an die Variable VibratoHz_ausgabe übergeben.

let vibratoDepth_daten = ""; // Variable für die Sammlung der Daten definieren
for (i=0;i<vibratoDepth_array.length; i++){ // For-Schleife über das Vibrato-Array starten
vibratoDepth_daten = vibratoDepth_daten + vibratoDepth_array[i]+ ", ";
} // für jeden Eintrag die Modulationstiefe herausholen und an VibratoDepth_daten anhängen
vibratoDepth_ausgabe="vibratoDepth_array=["+vibratoDepth_daten+"];";
// die gesammelten Daten an die Variable VibratoDepth_ausgabe übergeben.

document.getElementById("datenausgabe").innerHTML= pitchMelodia_ausgabe+"<br><br><br>"+vibratoHz_ausgabe+"<br><br><br>"+vibratoDepth_ausgabe; // Den Inhalt der Variablen pitchMelodia_ausgabe, vibratoHz_ausgabe und vibratoDepth_ausgabe im Feld Datenausgabe auf der Webseite ausgeben.

//--------------------------------------------------------------------------
// featureExtractor Funktion beenden

//--------------------------------------------------------------------------

essentia.algorithms.delete();
}

//--------------------------------------------------------------------------
//
WasmModul einbinden

//--------------------------------------------------------------------------

$(document).ready(function() { // Funktion, die startet, sobald Seite vollständig
// geladen und alles geparst und ausgeführt ist:

EssentiaWASM().then(async function(WasmModule) { // starte das WasmModule
let player = document.getElementById("audioPlayer"); // spreche den Audio-Player an
player.src = audioURL; // weise dem Audio-Player die Klangdabei zu
player.load(); // lade die Klangdatei in den Player
essentia = new Essentia(WasmModule); // weise das WasmModule der Variablen essentia zu,
// so dass darüber alle Funktionen mit den C++-Algorithmen ausgeführt werden können

});
});

</script>

</head>
<body>
<audio id="audioPlayer" controls style="width:100px">
<source id="audio-source" src="" type="audio/mp3" /></audio><br>
<button type="button" name="Signal Analysis" onclick="featureExtractor();">
Start Signal Analysis</button><br>
<span id="datenausgabe"> </span>

</body>
</html>

 

Ähnlich sähe dann auch die Ermittung und Datenausgabe für die anderen Features aus.

 

 

Audio Features mit Hilfe des essentia.FrameGenerator(...) extrahieren

Eine frame-weise Extrahierung lohnt sich bei Audio Features mit mehrdimensionalen Arrays, wie MFCCs, Barkbänder, Mel-Bänder, ERB-Bänder, Spectral Peaks oder auch die stärkste Amplitude aus dem Spektrum ermitteln (natürlich auch für FFTs).

Hier teilt man über essentia.FrameGenerator(...) das Audio zunächst in einzelne Zeitabschnitte auf, die dann via essentia.Spectrum(essentia.Windowing(frames.get(i)).frame).spectrum spektralanalysiert werden. Aus den Einzelspektren lassen sich dann pro Frame die gewünschten MFCCs, Barkbänder, Mel-Bänder, ERB-Bänder, Spectral Peaks ermitteln:

<script defer>
//--------------------------------------------------------------------------
// grundlegende Variablen definieren

//--------------------------------------------------------------------------

let essentia; // Variable, über die alles abgewickelt wird
let audioURL = "klang/fagott_toene.mp3"; // Klangbeispiel
let audioData; //Variable für die Wellenform des Klangbeispiels

//--------------------------------------------------------------------------
//
Audio-Context für alle Browsertypen erzeugen

//--------------------------------------------------------------------------

const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();

//--------------------------------------------------------------------------
// featureExtractor Funktion starten

//--------------------------------------------------------------------------

async function featureExtractor() {
audioData = await essentia.getAudioChannelDataFromURL(audioURL, audioCtx, 0); // übergebe die Daten von der Audio-Datei beschrieben in
// audioURL innerhalb des Audio-Context audioCtx an die Variable
// AudioData und verwende dabei den linken Kanal (0)

//--------------------------------------------------------------------
//Frameweise spetrale Extraction für zweidimensionale Daten
//--------------------------------------------------------------------
let mfccs_array =[]; // Array für MFCCs anlegen
let barkbands_array =[]; // Array für Barkbänder anlegen
let melbands_array =[]; // Array für Mel-Bänder anlegen
let ERBbands_array =[]; // Array für ERB-Bänder anlegen
let spectralPeaks_frequencies_array =[]; // Array für Spektral Peaks (Hz) anlegen
let spectralPeaks_magnitudes_array =[]; // Array für Spektral Peaks (Ampl.) anlegen
let strongestFrequency_array =[]; // Array für stärkste Frequenz anlegen

const frames = essentia.FrameGenerator(audioData, // FrameGenerator starten
1024, // frameSize = Größe der Frames
512 // hopSize = Abstand zwischen den Frames
);

for (var i=0; i<frames.size(); i++) { //Schleife über alle Frames laufen lassen
spectrum_pro_frames_array = essentia.Spectrum(essentia.Windowing(frames.get(i)).frame).spectrum; // Kernzeile, um
// über das jeweilige Frame mit einem bestimmten Fenster das Spektrum pro Frame zu
// errechnen

barkbands = essentia.vectorToArray(essentia.BarkBands(spectrum_pro_frames_array).bands);
// Pro Frame die 27 Barkbänder in ein Array schreiben. Die Bark Frequenzen sind: 0, 50,
// 100, 150, 200, 300, 400, 510, 630, 770, 920, 1080, 1270, 1480, 1720, 2000, 2320,
// 2700, 3150, 3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500, 20500, 27000 Hz

barkbands_array.push(barkbands); // die ermittelten Barkbänder dem barkband_array
// hinzufügen.
mfccs = essentia.vectorToArray(essentia.MFCC(spectrum_pro_frames_array).mfcc);
// Pro Frame die 13 MFCCs in ein Array schreiben
mfccs_array.push(mfccs); // die ermittelten MFCCs dem mfccs-array hinzufügen.
melbands = essentia.vectorToArray(essentia.MFCC(spectrum_pro_frames_array).bands);
// Pro Frame die 24 Mel-Bänder in ein Array schreiben
melbands_array.push(melbands); // die ermittelten 24 Mel-Bänder dem melbands_array
// hinzufügen.

ERBbands = essentia.vectorToArray(essentia.ERBBands(spectrum_pro_frames_array).bands);
// Pro Frame die 40 ERB-Bänder in ein Array schreiben
ERBbands_array.push(ERBbands); // die ermittelten 40 ERB-Bänder dem ERBbands_array
// hinzufügen.

spectralPeaks_frequencies = essentia.vectorToArray(essentia.SpectralPeaks(spectrum_pro_frames_array).frequencies);
// Pro Frame die Frequenzen von maximal 100 Spectral Peaks in ein Array schreiben
spectralPeaks_frequencies_array.push(spectralPeaks_frequencies); // die ermittelten
// maxinal 100 stärksten Frequenzen dem spectralPeaks_frequencies_array hinzufügen.
spectralPeaks_magnitudes = essentia.vectorToArray(essentia.SpectralPeaks(spectrum_pro_frames_array).magnitudes);
// Pro Frame die Amplituden von maximal 100 Spectral Peaks in ein Array schreiben
spectralPeaks_magnitudes_array.push(spectralPeaks_magnitudes); // die ermittelten
// maxinal 100 stärksten Amplituden dem spectralPeaks_magnitudes_array hinzufügen.
strongestFrequency = essentia.MaxMagFreq(spectrum_pro_frames_array).maxMagFreq;
// Pro Frame die stärkste Frequenz ermitteln
strongestFrequency_array.push(strongestFrequency);
// die ermittelte stärkste Frequenz dem strongestFrequency_array hinzufügen
}

}

</script>

 

Am Ende dieses Vorgangs liegen die Features als zweidimensionale Arrays vor, so dass man die Einzelwerte über zwei for-Schleifen aus den Arrays herausholen und in der Datenausgabe auf der Seite darstellen kann, z.B. für die MFCCs:

//--------------------------------------------------------------------------
//
Datenausgabe am Beispiel der MFCCs

//--------------------------------------------------------------------------

mfccs_daten=""; // Variable für die Textausgabe definieren
for (i=0; i<mfccs_array.length; i++){ // für alle Frames des mfccs_array eine for-
// Schleife starten

mfccs_spalte=""; // Variable für die Textausgabe pro Tabellenspalte definieren
for (j=0; j<mfccs_array[i].length; j++){ // für jedes MFCC innerhalb eines Frames [i]
// eine for-Schleife starten
mfccs_spalte=mfccs_spalte+mfccs_array[i][j].toFixed(8)+",";
//
jedes MFCC [j] innerhalb eines Frames [i] gerundet auf 8 Stellen ausgeben und mit einem Komma (",") getrennt an die bisherigen Ergebnisse innerhalb dieser Zeile anhängen
}
mfccs_daten=mfccs_daten+mfccs_spalte+"<br>"; // die vollständige Zeile mit allen Spalten mit einem Zeilenumbruch an die vorangegangenen Zeilen anhängen.
}
mfccs_ausgabe="<b>MFCCs: </b><br>"+mfccs_daten; // alle Daten an die Variable
// mfccs_ausgabe anhängen.

document.getElementById("datenausgabe").innerHTML= mfccs_ausgabe; // Den Inhalt der Variablen mfccs_ausgabe im Feld Datenausgabe auf der Webseite ausgeben.

 

Nun muss nur noch die featureExtractor() { ...}-Funktion mit essentia.algorithms.delete(); abgeschlossen werden und das WASM-Modul hinzugefügt werden, welches gestartet wird, nachdem die Seite vollständig geladen und geparst ist.

Das gesamte Script sähe für die frame-weise spetrale Extraction für die MFCCs dann folgendermaßen aus:

<!DOCTYPE html>
<html>
<head>

<script src="header/jquery.min.js"></script>
<!-- vereinfacht das Javascript Handling, hat sonst nichts mit Essentia.js zu tun -->
<script src="https://cdn.jsdelivr.net/npm/essentia.js@0.1.0/dist/essentia-wasm.web.js"></script>
<!-- Web Assembly Modul (WASM) = Backend, um die Standard C++ Algorithmen von Essentia verwenden zu können -->
<script src="header/essentia.js-core.js"></script>
<!-- die eigentliche Essentia Bibliothek-->

<script defer>
//--------------------------------------------------------------------------
// grundlegende Variablen definieren

//--------------------------------------------------------------------------

let essentia; // Variable, über die alles abgewickelt wird
let audioURL = "klang/fagott_toene.mp3"; // Klangbeispiel
let audioData; //Variable für die Wellenform des Klangbeispiels

//--------------------------------------------------------------------------
//
Audio-Context für alle Browsertypen erzeugen

//--------------------------------------------------------------------------

const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = new AudioContext();

//--------------------------------------------------------------------------
// featureExtractor Funktion starten

//--------------------------------------------------------------------------
async function featureExtractor() {
audioData = await essentia.getAudioChannelDataFromURL(audioURL, audioCtx, 0);
// übergebe die Daten von der Audio-Datei beschrieben in
// audioURL innerhalb des Audio-Context audioCtx an die Variable
// AudioData und verwende dabei den linken Kanal (0)

//--------------------------------------------------------------------
//Frameweise spetrale Extraction starten
//--------------------------------------------------------------------
let mfccs_array =[]; // Array für MFCCs anlegen

const frames = essentia.FrameGenerator(audioData, // FrameGenerator starten
1024, // frameSize = Größe der Frames
512 // hopSize = Abstand zwischen den Frames
);

for (var i=0; i<frames.size(); i++) { //Schleife über alle Frames laufen lassen
spectrum_pro_frames_array = essentia.Spectrum(essentia.Windowing(frames.get(i)).frame).spectrum;
// Kernzeile, um über das jeweilige Frame mit einem bestimmten Fenster das Spektrum pro
// Frame zu errechnen

mfccs = essentia.vectorToArray(essentia.MFCC(spectrum_pro_frames_array).mfcc);
// Pro Frame die 13 MFCCs in ein Array schreiben
mfccs_array.push(mfccs); // die ermittelten MFCCs dem mfccs-array hinzufügen.
}

//--------------------------------------------------------------------------
//
Datenausgabe am Beispiel der MFCCs

//--------------------------------------------------------------------------
mfccs_daten=""; // Variable für die Textausgabe definieren
for (i=0; i<mfccs_array.length; i++){ // für alle Frames des mfccs_array eine for-
// Schleife starten

mfccs_spalte=""; // Variable für die Textausgabe pro Tabellenspalte definieren
for (j=0; j<mfccs_array[i].length; j++){ // für jedes MFCC innerhalb eines Frames [i]
// eine for-Schleife starten
mfccs_spalte=mfccs_spalte+mfccs_array[i][j].toFixed(8)+",";
//
jedes MFCC [j] innerhalb eines Frames [i] gerundet auf 8 Stellen ausgeben und mit einem Komma (",") getrennt an die bisherigen Ergebnisse innerhalb dieser Zeile anhängen
}
mfccs_daten=mfccs_daten+mfccs_spalte+"<br>"; // die vollständige Zeile mit allen Spalten mit einem Zeilenumbruch an die vorangegangenen Zeilen anhängen.
}
mfccs_ausgabe="<b>MFCCs: </b><br>"+mfccs_daten; // alle Daten an die Variable
// mfccs_ausgabe anhängen.

document.getElementById("datenausgabe").innerHTML= mfccs_ausgabe; // Den Inhalt der Variablen mfccs_ausgabe im Feld Datenausgabe auf der Webseite ausgeben.

//--------------------------------------------------------------------------
// featureExtractor Funktion beenden

//--------------------------------------------------------------------------

essentia.algorithms.delete();
}

//--------------------------------------------------------------------------
//
WasmModul einbinden

//--------------------------------------------------------------------------

$(document).ready(function() { // Funktion, die startet, sobald Seite vollständig
// geladen und alles geparst und ausgeführt ist:

EssentiaWASM().then(async function(WasmModule) { // starte das WasmModule
let player = document.getElementById("audioPlayer"); // spreche den Audio-Player an
player.src = audioURL; // weise dem Audio-Player die Klangdabei zu
player.load(); // lade die Klangdatei in den Player
essentia = new Essentia(WasmModule); // weise das WasmModule der Variablen essentia zu,
// so dass darüber alle Funktionen mit den C++-Algorithmen ausgeführt werden können

});
});

</script>

</head>
<body>
<audio id="audioPlayer" controls style="width:100px">
<source id="audio-source" src="" type="audio/mp3" /></audio><br>
<button type="button" name="Signal Analysis" onclick="featureExtractor();">
Start Signal Analysis</button><br>
<span id="datenausgabe"> </span>

</body>
</html>

 

Ähnlich sähe dann auch die Ermittung und Datenausgabe für die anderen Arrays wie
barkbands_array,
melbands_array,
ERBbands_array,
spectralPeaks_frequencies_array,
spectralPeaks_magnitudes_array,
strongestFrequency_array
etc. aus.