Dienstag, 18. Januar 2011

How music went on the internet...

To use the previously described algorithm I wrote a small software which creates the fingerprint and sends a request to the respective web service. I will describe the web service in a future blog.
 
There is a user and a password needed to test the software. The test user is 'test' and the password is 'test1234'.
 
The initialization will search for updates, load the configuration, and afterwards initialize the program surface.
 


The user will be asked to log in after the update and the loading of the configuration is done.






You will then see an empty program window with only three menu buttons.


 
The first button opens a dialog which allows the user to load a folder - only mp3 files are supported at this time.



After selecting a folder you will see all files in this folder (and subfolders) in the window.


 
You can unfold and or fold up the track list using the +/- button.
You can open a dialog using the button called 'title information'. This button becomes active when an input is necessary.




In this dialog the user has the opportunity between entering title information and load it by using 'upload' or register the selected title by choosing a hit of the suggestion list. The advantage of selecting a title is that the data do not have to be uploaded again - the disadvantage is that the quality of the suggested song might be worse than the song of the user.

The second button of the program window opens a dialog which allows you to change the settings.




'Upload' gives you the opportunity to change upload settings. The checkbox 'Delete folder from list after upload' automatically deletes all folders, from which all songs have been loaded into the system, from the list. This function is not implemented by now.
The option 'Number of synchronous uploads' indicates the number of simultaneous uploads. It also indicates how many songs are analyzed simultaneously.
I strongly advise against from a value higher than 4 because the analysis will increase the workload of the core of the CPU to 100% and will steadily increase the storage usage because the audio file is fully loaded to the storage as pcm/wav.
This is a good point to tell you that the storage usage of the program is very high so I have to redesign the software design. Tips and comments are highly desired!
 
The source code is available under http://sources.my-musix.com/ .
The starting point is 'MyMusixJava2', the surface and its logic is available under 'MyMusixUploader'.
You will need sources & libraries in the following folders to build the software:
EuphonySymphony
MyMusixCommon
MyMusixI18N
MyMusixJava2
MyMusixLibShared
MyMusixUploader
MyMusixWSClient
 
All files under ...MyMusixJava2/Distributed are executable versions which can be built from the available sources.

  

Dienstag, 4. Januar 2011

Wie die Musik ins Internet auswanderte...

Um das im vorher beschriebene Verfahren zu nutzen, habe ich eine kleine Software geschrieben, die den Fingerabdruck erzeugt und eine Anfrage an einen entsprechenden Webservice stellt. Den Webservice werde ich in einem späterem Blog beschreiben.

Um die wird ein Benutzer und ein Passwort benötigt. Um die Software testen zu können, gibt es den Testbenutzer  'test' mit dem Passwort 'test1234'.

Beim Programmstart wird nach Updates gesucht, die Konfiguration geladen und anschließend die Programmoberfläche initialisiert.

Nach erfolgtem Update und laden der Konfiguration, wird der Benutzer aufgefordert sich einzuloggen.


Anschließend erscheint ein leeres Programmfenster mit lediglich drei Menübuttons.
Der erste Button öffnet eine Dialog über den der Benutzer einen Ordner laden kann - derzeit werden ausschließlich mp3 Dateien unterstützt.
Nach Auswahl eines Ordners werden alle in diesem Ordner liegenden (sowie Unterordnern) Dateien in dem Programmfenster angezeigt.

Über den +/- Button kann die Titelliste aus- bzw. eingeklappt werden.
Hinter den Titeln kann über den Button mit der Aufschrift "Titelinformationen" ein Dialog geöffnet werden. Dieser Button wird erst aktiv, wenn eine Benutzereingabe erforderlich wird.
In diesem Dialog kann der Benutzer entweder Titelinformationen eingeben und anschließend über "Hochladen" den Titel in das System einspielen, oder durch Auswahl eines Treffers in der Vorschlagsliste den ausgewählten Titel in der Benutzermusikbibliothek registrieren.
Der Vorteil an der Auswahl eines Titels liegt darin, dass die Daten nicht mehr hoch geladen werden müssen - der Nachteil ist, dass eventuell die Qualität des vorgeschlagenen Lieds schlechter ist, als die des Lieds des Benutzers.

Der zweite Button des Programmfensters öffnet einen Dialog in dem Einstellungen vorgenommen werden können.
Unter 'Upload' werden Uploadeinstellungen vorgenommen. Die Checkbox 'Delete folder from list after upload' sorgt dafür, dass Ordner dessen Lieder alle in das System eingespielt wurden, automatisch von der Liste gelöscht werden. Diese Funktion ist derzeit noch nicht implementiert.
Die Option 'Number of synchronous uploads' gibt die Anzahl der zeitgleichen Uploads an. Sie stellt aber auch ein, wie viele Lieder zeitgleich analysiert werden.
Von einem höheren Wert als 4 rate ich dringend ab, da die Analyse einen Kern einer CPU auf 100% Auslastung ansteigen lässt sowie der Speicherverbrauch stetig wächst da bei jeder Analyse eine Audiodatei komplett als pcm/wav in den Speicher geladen wird.
Hier kann ich kurz anmerken, dass der Speicherverbrauch des Programms recht groß ist und ich daher das Softwaredesign überarbeiten muss. Tipps und Anmerkungen sind daher erwünscht!



Die Quellen des Codes kann man unter http://sources.my-musix.com/ einsehen.
Der Einstiegspunkt ist 'MyMusixJava2', die Oberfläche sowie dessen Logik liegt aber unter 'MyMusixUploader'.
Um die Software builden zu können werden Sources & Bibliotheken in folgenden Ordner benötigt:
EuphonySymphony
MyMusixCommon
MyMusixI18N
MyMusixJava2
MyMusixLibShared
MyMusixUploader
MyMusixWSClient

Alle Dateien unter ...MyMusixJava2/Distributed stellen eine lauffähige Version dar, die aus den vorhanden Quellen erstellt werden kann.

Dienstag, 16. November 2010

Es war einmal...

So beginnt typischerweise ein Märchen - und mein Blog - nur das dieser kein Märchen ist und noch weniger das Projekt, um das es sich hier dreht.

Alles begann ende Oktober letzten Jahres mit einem Artikel über Chromium Os und einem Nebensatz der so etwa wie folgt war: "... die Anwendungen sind dabei nicht auf dem Computer sondern im Internet...". Dabei ist mir in den Sinn gekommen, dass es doch toll wäre, wenn ich von überall meine Musik höre könnte ohne, dass ich diese wirklich bei mir haben muss und sogar auf sie zugreifen könnte, wenn ich nicht an meinem eigenen PC sitzen würde. So entstand eine Internetseite die als einzige Aufgabe hatte einen Musik-Player darzustellen die kurz darauf auf den mehr oder weniger guten Namen "My-Musix" getauft wurde.

Da ich das Projekt nicht nur für mich zugänglich machen wollte musste ich mir überlegen wie ich Duplikate vermeiden konnte. Ein Verfahren das auf MD5-Werten, auf Länge oder ID3-Informationen arbeitet schied von vornherein aus - wieso sage ich jetzt aber nicht ;-)

Mein erster Schritt ging dahin, dass ich Wavelab öffnete und mich die Kurve anschaute. Wenn ich mir dann zwei Kurven gleicher Lieder unterschiedlicher Qualität (ich verwendete mp3-codierte Lieder) ansah, war zu sehen, dass diese logischerweise eine sehr hohe Ähnlichkeit aufwiesen.
Mein Schnellschuss war "etwas weniger" performant/praktikabel. Ich erzeugte zwei Bilder gleicher Dimensionen und streckte den durch die Kurve erzeugten Bereich auf diese Größe - anschließend berechnete ich wie weit diese Deckungsgleich zueinander waren. Das war nicht nur Langsam - auch wenn ich die Bilder nicht wirklich zeichnete - sondern war für mich unvorstellbar, wie ich das auf eventuell tausende Lieder anwenden soll oder gar, wie ich das in einer Datenbank darstellen könnte.

Also entschied ich mich dafür, weiter Wavelab anzustarren. Dabei ist mir dann das vorher eher ignorierte Level-&Spectrum-Meter aufgefallen. Die dann folgende Idee auf das Level einzugehen verwarf ich innerhalb weniger Minuten, da leider manche Lieder in unterschiedlicher Lautstärke aufgenommen sind.
Da die Frequenzen in einem Lied aber immer gleich sind, konzentrierte ich mich auf die Frequenzen und wie man diese aus dem PCM-Stream lesen kann. Fouriertransformation ist hier das Stichwort.

Mein erster Ansatz mit Frequenzen beschränkte sich darauf, die Frequenz mit der maximalen Intensität zu bestimmen. Dies war in einer Datenbank relativ leicht darstellbar hatte aber eine hohe Ungenauigkeit zur Folge. Daher entschloss ich mich jeden betrachteten Moment in Bereiche einzuteilen. Es nach Höhne, Mitten und Bässen zu unterscheiden macht Sinn. Da sich nach meinem Verständnis der charakteristische Bereich eines Lieds in den Mitten befindet, beschloss ich den Frequenzbereich von etwa 100-3500Hz zu wählen - mit diesem Spektrum decke ich den gesamten Bereich der Mitten, sowie einen Teil der Bässe und Höhen ab. Dieses Spektrum teile ich in vier Bereiche (ursprünglich fünf) ein:
100Hz - 400Hz - 900Hz - 2000Hz - 3500Hz

Daraus folgt, dass ich für mein untersuchtes Lied eine Menge von Untersuchten Teilabschnitten habe und zu diesen jeweils vier Frequenzen.
Diese vier Frequenzen bilden für den gewählten Zeitabschnitt das für mich relevante Merkmal. Da der dritte Frequenzbereich sowohl drei als auch vier Stellen betragen kann, füge ich im Falle von drei Stellen eine führende 0 an

Hier ein kleines Beispiel:
100-400Hz: "187"
400-900Hz: "691"
900-2000Hz: "0982"
2000-3500Hz: "2893"
Zusammengehängt: "18769109822893"

Da es ab jetzt um die Implementierung geht, hier ein kleiner aber wichtiger Hinweis:
Der Methode die die Fouriertransformation durchführt, werden n-Elemente übergeben und gibt n/2-Elemente zurück.
Wenn ich einen Zeitraum von 1/10Sekunde betrachten möchte und die Samplerate des PCM-Streams 44.1 kHz beträgt hätte ich 4410 Elemente zu übergeben. In meinem Fall führe ich eine FFT durch, die 2^n Elemente erwartet - naheliegend ist hier für n=4096 zu wählen.
Grob gerundet komme ich auf die Bereiche 10-40-85-185-330.

Übersetzt man das eben beschriebene in Java erhält man diese Klasse:


import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.UnsupportedAudioFileException;

import com.mymusix.euphony.symphony.math.FastFourierTransform;

public class Anna{
    public final int FFT_AMOUNT = 4096; // Anzahl an Elemente für die FFT (2^n)
    private int MAX_LENGTH = 330; // Maximaler Wert der Frequenzbereiches (Entspricht bei FFT_AMOUNT 4096 einer Frequenz von etwa 3500Hz)
 private File wavFile;
 private long[] result;

 /**
  * 
  * @param f muss eine PCM/WAV-Datei sein
  * @author kirchrath
  */
 public Anna(File f)
 {
     this.wavFile = f;
     this.action();
 }

 public long[] getResult()
 {
     return this.result;
 }

 public String toString()
 {
     if (this.result == null)
     {
         return super.toString();
     }
     else
     {
         StringBuilder sb = new StringBuilder();
         for(long l : this.result)
         {
             sb.append(l).append("|");
         }
         return sb.toString();
     }
 }
 
 private void action()
 {
        try {
            WAVE wavData = this.getWAVEFromFile(wavFile);

            byte[] bs = wavData.getBytestream();
            byte[] ba = new byte[bs.length];
            System.arraycopy(bs, 0, ba, 0, bs.length);

            float[] signal = this.preprocessing(ba);

            int max = signal.length/FFT_AMOUNT;
            long[] hashcodes = new long[max];

            int start   = 0;
            int i = 0;
            int c = 0;
            // -FFT_AMOUNT da signal noch mindestens so viele Elemente ab aktueller Position beinhalten muss
            while(i<signal.length-FFT_AMOUNT) 
            {
                /**
                 * Datensatz für FFT erzeugen
                 */
                float[] internal = new float[FFT_AMOUNT];
                System.arraycopy(signal, i, internal, start, FFT_AMOUNT);

                /**
                 * FFT Initialisieren und durchführen
                 * Ich arbeite hier auf float Werte, da komplexe Zahlen in Java
                 * einen Wrapper benötigen und dies die Geschwindigkeit / den
                 * Speicher zu stark beinflusst
                 */
                FastFourierTransform fft = new FastFourierTransform();
                FFTR result = new FFTR(fft.fftMag(internal));

                // Das Ergebnis dem internen Ergebnisarray hinzugügen und i & c entsprechend erhöhen
                hashcodes[c] = result.hash();
                i = i + FFT_AMOUNT;
                c++;
                if(c==max) break;
            }
            // Dem Klassenergebnis das internet Ergebnisarray zuweisen.
            this.result = hashcodes;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (UnsupportedAudioFileException e) {
            e.printStackTrace();
        }
 }

    private float[] preprocessing(byte[] bs)
    {
        /**
         * 1. Wandel alle bytes entsprechend in ein float um
         * 2. wenn das signal in stereo ist, in mono umwandeln 
         */
        float[] signal = new float[bs.length / (4)];
        int i = 0;
        int c = 0;
        while(i<signal.length)
        {
            float[] internal = new float[2];

            for(int k=0; k<internal.length; k++)
            {
                int a = c++;
                int b = c++;
                byte[] buffer = new byte[]{0,0,bs[b],bs[a]};

                internal[k] = this.convertByteArrayToInt(buffer);
                internal[k] = (32768 - internal[k])*-1;
                if(internal[k]<0)
                {
                    internal[k] = (32768 + internal[k])*-1;
                }else
                {
                    internal[k] = (32768 - internal[k]);
                }
                internal[k] = internal[k] -1;
            }
            if( (internal[0]<0 && internal[1]>0) || (internal[0]>0 && internal[1]<0))
            {
                signal[i] = (internal[0] + internal[1]);
            }
            else 
            {
                signal[i] = (internal[0] + internal[1]) / 2;
            }
            i++;
        }
        return signal;
    }

    
    /**
     * 
     * @param f PCM/WAV Datei
     * @return  privates Wrapper-Object
     * @throws UnsupportedAudioFileException
     * @throws IOException
     */
    private WAVE getWAVEFromFile(File f) throws UnsupportedAudioFileException, IOException
    {
        AudioInputStream aIS = AudioSystem.getAudioInputStream(f);
        AudioFormat format = aIS.getFormat();
        int x = (int) (aIS.getFrameLength() * format.getFrameSize());
        byte[] abs = new byte[x];
        aIS.read(abs);
        return new WAVE(format, abs);
    }

    /**
     * Methode zum convertieren eines byte-Arrays in einen int
     * http://www.tutorials.de/forum/java/228129-konvertierung-von-integer-byte-array.html#post1188505
     * @param buffer    ein byte-Array der länge 4
     * @return
     */
    private int convertByteArrayToInt(byte[] buffer) {
        if (buffer.length != 4) {
            throw new IllegalArgumentException("buffer length must be 4 bytes!");
        }
        int
        value  = (0xFF & buffer[0]) << 24 ;
        value |= (0xFF & buffer[1]) << 16;
        value |= (0xFF & buffer[2]) << 8;
        value |= (0xFF & buffer[3]);
        return value;
    }

    
    /**
     *  Wrapperklasse für PCM/WAV-Input
     * @author kirchrath
     *
     */
    private class WAVE
    {
        private AudioFormat af;
        private byte[] bytestream;
        public WAVE(AudioFormat format, byte[] abs) {
            af = format;
            bytestream = abs;
        }
        
        @SuppressWarnings("unused")
        public AudioFormat getAudioformat() {
            return af;
        }
        
        public byte[] getBytestream() {
            return bytestream;
        }
    }
    
    /**
     * FFT-Response Wrapperklasse
     * Erzeugt den Wert (hash) zum Untersuchten Abschnitt
     * @author kirchrath
     *
     */
    private class FFTR{
        // Auf Welche Zahl soll gerundet werden?
        private final int precision = 2;
        float[] fftr;
        FFTR(float[] fftr)
        {
            this.fftr = fftr;
        }

        /**
         * 
         * @return Aktueller Wert aus maxima der bereiche 10-40-85-185-330
         */
        public long hash()
        {
            float[] buffers = new float[4];
            long[] fqs = new long[4];
            for(int i=0; i<fftr.length && i < MAX_LENGTH; i++)
            {
                int index;
                if (i >= 10)
                {
                    if (i<40)
                    {
                        index = 0;
                    }else if(i<85)
                    {
                        index = 1;
                    }else if(i<185)
                    {
                        index = 2;
                    }else
                    {
                        index = 3;
                    }

                    if (buffers[index] < fftr[i])
                    {
                        buffers[index] = fftr[i];
                        fqs[index] = i;
                    }
                }
            }

            long p1 = (fqs[0] - (fqs[0] % precision));
            long p2 = (fqs[1] - (fqs[1] % precision));
            long p3 = (fqs[2] - (fqs[2] % precision));
            long p4 = (fqs[3] - (fqs[3] % precision));

            // Konkatenation der einzelen Teilmaxima 
            return  p4 * 10000000 + p3* 10000 + p2 * 100 + p1; 
        }
    }
    
    public static void main(String[] args)
    {
     File f = new File("./99c8fa7dd27164116a12616ef33f37d7.wav");

        Anna a = new Anna(f);
        System.out.println(a);
    }
}


Lasse ich diese Klasse für zwei Lieder die Codes ausgeben bekomme ich diese (gekürzte) Liste:
2360945212        0000000000 
1841144610        0000000000 
2561144610        1860924614 
2561144610        2400964612 
2421144610        1841144610 
2941144610        2561144610 
3121144610        2561144610 
2081144410        2421144610 
2081844622        2241144610 
2080924622        3121144610  
2081844622        2081144622 
3001084622        2081844622 
3001084622        2081844622 
2441084622        3001084622 
2441084622        3001084622  
2441084622        2441084622
2441084622        2441084622 

2081084610        2441084622 
2081084610        2441084622 
2081084610        2441084610  
2081084610        2081844610 
2441544610        2081084610 
2441544610        2081084610 
2181505410        2441084610 
3141825418        2441544610 
2001825436        2441544610 
2001825436        2181825410 
2200925418        3141825418

Diese beiden Listen müssen jetzt verglichen werden. Vergleiche ich jetzt einfach immer das linke mit dem rechten Element in einer Zeile, habe ich vier Zeilen in denen der linke und rechte Wert gleich sind. Prozentual sind das etwa 14%. Würde eines der Lieder mit ein paar Sekunden Versatz anfangen (Stille am Anfang), würde die Übereinstimmung um vieles geringer ausfallen. Erzeuge ich an einer Liste einen Versatz, kann sich die Übereinstimmung auch verbessern:

xxxxxxxxxx    0000000000 
xxxxxxxxxx    0000000000 
xxxxxxxxxx    1860924614 
2360945212    2400964612  
1841144610    1841144610
2561144610    2561144610
2561144610    2561144610
2421144610    2421144610 

2941144610    2241144610  
3121144610    3121144610 
2081144410    2081144622  
2081844622    2081844622 
2080924622    2081844622 
2081844622    3001084622  
3001084622    3001084622
3001084622    2441084622
2441084622    2441084622
2441084622    2441084622
2441084622    2441084622
2441084622    2441084610 

2081084610    2081844610  
2081084610    2081084610
2081084610    2081084610 

2081084610    2441084610  
2441544610    2441544610
2441544610    2441544610 

2181505410    2181825410 
3141825418    3141825418 
2001825436    2001825436    
2200925418   

Beachtet man nur die Zeilen in denen eine Zahl größer 0 steht, habe ich 25 Zeilen in denen 17 gleich sind.
Auf diese Weise verglich ich die Codes der Lieder, bis ich letzten Sommer auf einen Blog stieß, der genau das gleiche Thema behandelte und eine simple Lösung für dieses Problem zeigte.
Jeder Wert einer Zeile eines Lieds ist mit einem Zeitpunkt verknüpft (Index). Diesen Wert sucht man in den Werten eines anderen Lieds und subtrahiert die Indexwerte. Die Differenzen sind Zählerindizes die entsprechend erhöht werden.
Der Betrag des Zählerindex mit dem größten Wert ist die Trefferquote - dividiert durch die Anzahl der Werte unsere Werteliste bekommt man die prozentuale Übereinstimmung der verglichenen Lieder.
Für gewöhnlich greift man aber nicht auf die Codes eines einzelnen Liedes zu, sondern auf eine Menge von Liedern.
Hier müssen die Codes zusätzlich noch mit einem Lied verknüpft werden und die Zählerindizes abhängig von dem Lied erzeugt werden.

Da die webservicesseitige Implementierung einen Fehler aufweist, gibt es vorerst die ursprüngliche PHP-Implementierung.
$this->fingerprint ist eine Zeichenkette von Codes die mit einem senkrechten Strich miteinander verbunden sind. $this->lenght ist die Länge des Liedes. Dadurch schränke ich die Ergebnisse bereits etwas ein.


$size = (int) round((int) $this->length, -1);
  $time = array($size - 20, $size, $size+20);

  $hashcodes = explode('|', $this->fingerprint);
  
  $hashcode_count = count($hashcodes);

  $sql = 'SELECT songid, time, hashcode FROM mm_music.hashcodes WHERE tracklength IN :length AND hashcode IN :hashcodes';
  $query = DB::query(Database::SELECT, $sql, FALSE)->param(':length', $time)->param(':hashcodes', $hashcodes);

  $query = $query->execute()->as_array();
  
  $hashtable = array();
  foreach($query as $row)
  {
   $code = $row['hashcode'];
   $songid = $row['songid'];
   $time = $row['time'];
   if ( ! isset( $hashtable[$code] ))
   {
    $hashtable[$code] = array();
   }
   $hashtable[$code][] = (object) array
   (
    'time' => $time,
    'song' => $songid
   );
  }
  
  $songs = array();

  for($i = 0; $i<$hashcode_count; $i++)
  {
   $hash = $hashcodes[$i];
   $time = $i;
   
   if (isset($hashtable[$hash]))
   {
    $elms = $hashtable[$hash];
    foreach ($elms as $elm)
    {
 
     $timedif = $time - $elm->time;
     if ( ! isset($songs[$elm->song]))
     {
      $songs[$elm->song] = array();
      $songs[$elm->song]['timestamps'] = array();
      $songs[$elm->song]['max'] = 1;
     }
     if ( ! isset($songs[$elm->song]['timestamps'][$timedif]))
     {
      $songs[$elm->song]['timestamps'][$timedif] = 1;
     }
     else
     {
      $songs[$elm->song]['timestamps'][$timedif] = $songs[$elm->song]['timestamps'][$timedif] + 1;
      if ($songs[$elm->song]['max'] < $songs[$elm->song]['timestamps'][$timedif])
      {
       $songs[$elm->song]['max'] = $songs[$elm->song]['timestamps'][$timedif];
      }
     }
    }
   }
  }
  
  $results = array();
  foreach ($songs as $key => $value)
  {
   $precision = $value['max'] / $hashcode_count;

   if ($precision > 0.1)
   {
    $results[$key] = (object) array();
    $results[$key]->precision = $precision;
    $this->num++;
   }
  }

  uasort($results, function($a, $b){
   if ($a->precision == $b->precision) {
          return 0;
      }
      return ($a->precision > $b->precision) ? -1 : 1;
  });
  
  // Verkleinere das Array auf die ersten 10 Einträge - mehr sollen dem Benutzer nicht angezeigt werden
  if ($this->num > 10)
  {
   $r_tmp = array();
   $c = 0;
   foreach($results as $key => $value)
   {
    if ($c >=10)
    {
     break;
    }
    $r_tmp[$key] = $value;
    $c++;
   }
   $results = $r_tmp;
  }


In Kürze werde ich die hier vorgestellten Codes auf Sourceforge veröffentlichen.