# Audiolänge einer MP3 Datei erhalten ohne diese vollständig zu laden



## RalleYTN (19. Mai 2017)

Hallöchen!
Hab mal wieder ein recht spezifisches Problem.
Ich bin gerade dabei mithilfe von JLayer und der MP3Spi Audiodateien abzuspielen.
Da es sich um Musik handelt dachte ich mir es wäre besser die Dateien zu Streamen anstatt alles über einen Clip in den RAM zu laden und einen OutOfMemoryError zu riskieren.
Der Player für die MP3 Datei soll natürlich auch fähig sein die Länge des Contents anzuzeigen.

Problem:
Ich würde für WAV Dateien die Länge wie folgt berechnen

```
length = (long)(1000000 * (audioInputStream.getFrameLength() / audioInputStream.getFormat().getFrameRate()));
```
Das funktioniert jedoch nicht, da audioInputStream.getFrameLength() bei MP3s -1 liefert.

Ich habe bereits versucht die Header mithilfe der Klassen die mit den Bibliotheken kommen auszulesen.

```
AudioFileFormat audioFileFormat = AudioSystem.getAudioFileFormat(this.resource);
              
if(audioFileFormat instanceof TAudioFileFormat) {
                  
   Map<?, ?> headers = ((TAudioFileFormat)audioFileFormat).properties();
   headers.forEach((key, value) -> System.out.println(key + ": " + value));
}
```

Ergebnis:

```
mp3.copyright: false
date: 2016
mp3.framesize.bytes: 1040
mp3.vbr: false
mp3.frequency.hz: 44100
mp3.framerate.fps: 38.28125
mp3.id3tag.track: 1
mp3.id3tag.v2: java.io.ByteArrayInputStream@b81eda8
mp3.channels: 2
mp3.vbr.scale: 0
mp3.version.encoding: MPEG1L3
mp3.bitrate.nominal.bps: 320000
mp3.version.layer: 3
mp3.id3tag.v2.version: 3
mp3.padding: false
mp3.header.pos: 1130
comment: ?ÿ?ww.dvdvideosoft.com?
mp3.version.mpeg: 1
mp3.mode: 1
mp3.crc: false
mp3.original: true
```

Jetzt weiss ich jedoch nicht weiter. :/


----------



## Tobse (19. Mai 2017)

Sieht für mich so aus, als ob du da mit einer Unschärfe von +-2-3 Skunden gut hinkommen kannst:
Du kennst mp3.framerate.fps, ich vermute mal, dass das Frames Per Second ist. Du weisst durch mp3.framesize.bytes auch, dass ein Frame 1040 Frames groß ist. Wenn die ganze Datei also 3MiB (= 3.145.728 bytes) groß ist, sind es 3145728 / 1040 = ~3024 Frames und damit 3024 / 38,28125 = 79 Sekunden.

Ich kenne das MP3-Format nicht so gut. Sicher weiss ich, dass die Framegröße aufgrund der High- und Lowcuts ggü. WAV oder FLAC deutlich geringer ist und damit Speicherplatz gespart wird. Wenn aber zusätzlich noch irgendeine Kompression (wie bspw. GZip) dabei ist, wird das mit dem ausrechnen der Dauer eher schwierig.

Ein anderer Punkt: je nachdem, wo deine App läuft, brauchst du dir um die OutOfMemoryException nicht so viele Sorgen machen. Wenn du keine Referenzen zu den Dateien rumliegen lässt sollte der GC gut hinterherkommen. Und auf einem System mit mehreren GB Arbeitsspeicher kannst du ne menge MP3s in den RAM laden, bevor du ein Problem bekommst.

Natürlich kannst du nicht jedes mal die Datei in dem RAM lesen, wenn du Infos dazu möchtest. Ich vermute die gängigen Audio-Player (WMP, Groove under Windows, Rythmbox und Bashee unter Linux bspw) haben eine SQLite Datenbank, in der sie solche schwer erarbeiteten Infos zwischenspeichern. Java + SQLite geht, mit H2DB kannst du aber auch arbeiten, wenn du auf native Libraries verzichten möchtest.


----------



## RalleYTN (19. Mai 2017)

Kommt dem Ergebnis nahe hat jedoch schon bei einem 6 Sekunden clip einen Verlust von 0,5 Sekunden.
Wenn ich nicht eine Meldung mit "mark not supported" bekommen würde, dann könnt ich ganz einfach einmal durch den AudioInputStream gehen und die bytes zählen (wären an dem Punkt nämlich nicht mehr komprimiert) und danach den AudioInputStream resetten und normal weiter arbeiten.

Die Lösung hat mich auf jeden Fall ein Stück weiter gebracht, jedoch ist sie nicht genau genug.


----------



## RalleYTN (19. Mai 2017)

HEUREKA!!!

```
int frameLength = 0;
byte[] buffer = new byte[this.audioInputStream.getFormat().getFrameSize()];

while(this.audioInputStream.read(buffer) != -1) {
                       
   frameLength++;
}

this.audioInputStream.close();
this.audioInputStream = AbstractAudio.getAudioInputStream(this.resource, this.fileFormat);
this.length = (long)(1000000 * (frameLength / this.audioInputStream.getFormat().getFrameRate()));
```

Ich resette den AudioInputStream jetzt einfach indem ich ihn schließe und neu initializiere.
Die Header brauche ich nicht. Der korrekte FrameSize ist im AudioFormat gespeichert.
Um an die Gesamtmenge der Frames zu kommen lese ich einfach alle frames einzeln aus und zähle Sie.
Danach kann ich ganz normal weiterrechnen.


----------



## Tobse (20. Mai 2017)

Das wird so nicht funktionieren. Read liest nicht zwangsläufig so viele bytes, wie der Buffer groß ist. Du musst den Rückgabewert von read aufsummieren und durch die FrameSize teilen, um ein akkurates Ergebnis zu bekommen.


----------



## dzim (22. Mai 2017)

Könntet ihr nicht den MediaPlayer von JavaFX einbinden (https://docs.oracle.com/javase/8/javafx/api/javafx/scene/media/MediaPlayer.html), der bietet eine Methode *#getTotalDuration()*: 





> The total amount of play time if allowed to play until finished.


.

Ah nee...


> Retrieves the total playback duration including all cycles (repetitions).



Aber dafür: Über *#getMedia()* kommt man an die abzuspielenden Daten.
https://docs.oracle.com/javase/8/javafx/api/javafx/scene/media/Media.html
Und dort: *#getDuration()* - sollte also gut verwendbar sein! Und da man *Media* auch ohne den *MediaPlayer* erstellen kann, brauchst du keine JavaFX-GUI-Komponenten unnütz zu erstellen.


----------



## Tobse (22. Mai 2017)

@dzim Und wer sagt, dass das damit die Datei nicht komplett in den RAM lädt? Und JavaFX nur wegen einer Klasse einzubinden ist es ggf. nicht wert


----------



## dzim (22. Mai 2017)

Zum einen: Das sagt niemand, Zum anderen: Soweit ich die JavaDoc richtig verstanden hab, ist das wie Streaming. Aber statt grundsätzlich meiner Idee ablehnend gegenüber zu stehen, hättest du ja einfach eine kleine Anwendung implementieren können, die den Test macht und den Speicherverbrauch der JVM tracken können.

Ausserdem: "Einbinden" klingt ja fast so, als wäre dafür ein separates Modul (MAven, Gradle) nötig, was im Moment schlicht falsch wäre...


----------



## Tobse (22. Mai 2017)

Den Speicherverbrauch bei einer 3MB Datei und 200MB Heapspace? Wird schwierig. Ich sehe das deshalb kritisch, weil JavaFX eine große Dependency ist (die Runtime Downloads sind ~7MB). Einfach lustig Dependencies in ein Projekt reinzukloppen, weil man nur eine Klasse braucht, macht z.B. das NodeJS/NPM Ökosystem komplett kaputt. Ich halte sowas daher für Bad-Practice.

Es wäre glaub eher angebracht, eine MP3-Library einzubinden. Die kann vmtl das gleiche, ist nicht auf am System installierte Codecs angewiesen und man kann für so Ein Projekt vmtl mehr davon benutzen. Ich wäre nicht verwundert, wenn der JavaFX Media-Player auf Linux ohne Codec nicht tut.


----------



## thecain (22. Mai 2017)

javafx ist doch im jre, wenn ich mich nicht irre. Ich sehe das Problem nicht.

Ob man die Länge auslesen kann, wird mMn davon abhängen ob sie im ID3 Header ist, sonst werden es wohl nur Annäherungen sein, ohne das ganze File zu laden.


----------



## Tobse (23. Mai 2017)

thecain hat gesagt.:


> javafx ist doch im jre, wenn ich mich nicht irre.



In dem Fall fände ich den MediaPlayer auch eine elegante Lösung.

Aber unabhängig davon finde ich es nach wie vor nicht schlimm, die ganze Datei zu laden. Wenn man viele Dateien der Art verarbeiten will macht eine Indizierung ohnehin Sinn.


----------



## dzim (23. Mai 2017)

@Tobse Ich versteh dich nicht mehr. Bist du nun dagegen, oder dafür?

Kurzum: Solange das Jigsaw-Modulsystem noch nicht da ist, ist die Media-Klasse (den MediaPlayer, also die JavaFX-GUI, die man hier überhaupt nicht braucht!) Teil des JVM und keine weitere Dependency in dem Sinne. Erst recht nicht im Sinne von NodeJS. Dagegen sind es jedoch Dependencies wie MP3Spi (auch wenn ich den Memory-Footprint-Impact für vernachlässigbar halte).

Ich denke jedoch, dass werder MP3Spi noch die Media-API von JavaFX das ganze File in den RAM-Schaufeln. Bei MP3Spi vermute ich es, bei der Media-API bin ich mir sicher, denn sie ist ja auch für Videos (FLV) gedacht, die ja durchaus auch einmal grösser als der RAM sein können, daher wird es maximal nur einen Bereich in den Speicher laden, oder aber - solange noch kein Player gestartet wurde - nur die Metadaten parsen.


----------



## RalleYTN (23. Mai 2017)

@dzim @Tobse 

JavaFX dafür zu verwenden wäre Unsinn, da ich doch schon alles dafür notwendige im javax.sound.sampled package finde. Die Rechnung funktioniert und es wird kein riesen Overhead erzeugt.


----------



## dzim (23. Mai 2017)

@RalleYTN Ok. Es war nur einfach mein spontaner Einfall. Ich wusste nicht einmal, dass es das package *javax.sound.** gibt...


----------



## mrBrown (23. Mai 2017)

RalleYTN hat gesagt.:


> es wird kein riesen Overhead erzeugt


Na na, ist immer noch Java 
SCNR


----------



## Tobse (23. Mai 2017)

RalleYTN hat gesagt.:


> @dzim @Tobse
> 
> JavaFX dafür zu verwenden wäre Unsinn, da ich doch schon alles dafür notwendige im javax.sound.sampled package finde. Die Rechnung funktioniert und es wird kein riesen Overhead erzeugt.


Naja, eine stabile API dafür zu haben ist auf jeden Fall Zukunftssicherer. Es geht um Separation of Concerns: Der Concern deiner Anwendung ist es, mit den Metadaten von Musikdateien einen Mehrwert für den Nutzer zu erzeugen. Mit Byte-geschubse und Dateiformaten beschäftigst du dich ja nur zwangsweise, nicht weil es Kernteil deines Business-Modells ist. Bei einer Library sieht es aber anders aus: dessen hauptsächlicher Concern _sind _Byte-Schubserei und Dateiformate in allen Farben und Formen.
Im Endeffekt lässt es sich der Code auf zwei Teile aufteilen: der Code, der die Metadaten besorgt und der, der mit den Daten was sinnvolles anstellt. Letzteren schreibst sowieso du, der ist aus der Diskussion. Aber wenn du für den Zweiten eine Library verwendest, hast du dafür den hochwertigeren Code in deiner fertigen Anwendung.


----------

