# Java Sound - Skipping in .mp3 Files und Bug (Bitte überprüfenl)



## s4ke (20. Apr 2011)

Hallo allerseits,

Ich bin jetzt schon seit einiger Zeit dabei, einen mp3 Player in Java zu realisieren. Abspielen tut er schon lange brav, und pausieren geht auch schon gut. Aber mein derzeitiges Hauptproblem, dass ich bis jetzt noch nicht gelöst bekommen habe ist, wie man in einer .mp3 Datei richtig skippt. Leider kann ich euch nicht sagen, wo das Problem in untigem SourceCode liegt (Methode seek). Ihr müsst mir den Code nicht verbessern, ein Hinweis genügt schon .

btw: Es handelt sich um dieses Projekt:

https://github.com/s4ke/pure_mp3/wiki

Noch etwas:

Ich habe jetzt schon seit längerem das Gefühl, dass in der aktuellen OpenJDK in der Klasse SourceDataLine etwas schief läuft. Und zwar gibt die Methode Namens getMicroSecondsPosition() Millisekunden zurück (zumindest bei mir).

Hier der dazugehörige Bug bei launchpad:

https://bugs.launchpad.net/ubuntu/+source/openjdk-6/+bug/740485

Könntet ihr das mal prüfen?


```
/**
 *  @author Martin Braun
 *  MusicPlayer inspired by Matthias Pfisterer's examples on JavaSound
 *  (jsresources.org). Because of the fact, that this Software is meant 
 *  to be Open-Source and I don't want to get anybody angry about me 
 *  using parts of his intelligence without mentioning it, I hereby 
 *  mention him as inspiration, because his code helped me to write this class.
 * 
 *  This file is part of pure.mp3.
 *
 *  pure.mp3 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  
 *  pure.mp3 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with pure.mp3.  If not, see <http://www.gnu.org/licenses/>.
 */

package pure_mp3;

//import java.net.URL;
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.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.FloatControl;

public class StreamMusicPlayer extends Thread implements MusicPlayer
{
	
	private static final int	EXTERNAL_BUFFER_SIZE = 128000;
	private AudioInputStream audioInputStream;
	private SourceDataLine line;
	private AudioFormat audioFormat;
	private DataLine.Info info;
	private Player player;
	private Song song;
	private boolean playing;
	private long skippedDurationInMicroSeconds;
	private int skippedFrames;
//	private long position;
//	private boolean skippingAllowed;
//	private boolean running = false;
	private boolean pause = false;
	private boolean stop = false;
//	private static boolean lineAvailable = true;
	
	public StreamMusicPlayer(Song xSong, Player xPlayer)
	{
		super();
//		position = 0;
		player = xPlayer;
		song = xSong;
		try
		{
			insert(song);
		}
		catch(Exception e)
		{
			System.out.println("Error while inserting the file");
//			player.next();
		}
		
	}

	public void insert(Song song)
	{		
		try
		{
			audioInputStream = AudioSystem.getAudioInputStream(song.getSource());
		}
		catch (Exception e)
		{
			System.out.println("Error while parsing URL to File/Stream");
//			stop = true;
//			player.stop();
//			player.next();
		}		
		if (audioInputStream != null)
		{
			AudioFormat	format = audioInputStream.getFormat();
			if ( format.getEncoding() != AudioFormat.Encoding.PCM_SIGNED ) 
			{
		         AudioFormat newFormat = new AudioFormat(
		            AudioFormat.Encoding.PCM_SIGNED, 
		            format.getSampleRate(),
		            16,
		            format.getChannels(),
		            format.getChannels() * 2,
		            format.getSampleRate(),
		            false );
		         AudioInputStream newStream = AudioSystem.getAudioInputStream( newFormat, audioInputStream );
		         format = newFormat;
		         audioInputStream = newStream;
		   }
		}
		audioFormat = audioInputStream.getFormat();
		info = new DataLine.Info(SourceDataLine.class,audioFormat);
	}

	public void run()
	{
//		running = true;
		try
		{
			line = (SourceDataLine) AudioSystem.getLine(info);
			line.open(audioFormat);
			line.start();
		}
		catch(LineUnavailableException e)
		{
			System.out.println(e.getMessage());
			System.out.println("Line unavailable");
		}
		catch(Exception e)
		{
			System.out.println("Error while starting playback");
		}
		int nBytesRead = 0;
		int bufferSize = EXTERNAL_BUFFER_SIZE;
		if(audioFormat != null)
		{
			bufferSize = (int) audioFormat.getSampleRate() * audioFormat.getFrameSize();
			System.out.println(audioFormat);
		}
		byte[]	abData = new byte[bufferSize];
		System.out.println("Buffer Size: " + bufferSize);
		
		if(line != null)
		{
			setVolume(Global.VOLUME);
		}
		while (nBytesRead != -1 && !stop && line != null)
		{
			synchronized(this) 
			{
                while (pause && !stop)
                {
                	try
                	{
                		System.out.println("paused");
                		wait();
                	}
                	catch(Exception e)
                	{
                	}                	
                }
                notify();
            }
			if(!stop)
			{
				try
				{	
					nBytesRead = audioInputStream.read(abData, 0, abData.length);
//					position = position + nBytesRead;
				}
				catch (IOException e)
				{
					nBytesRead = -1;
				}
				if(nBytesRead != -1);
				{
					try
					{
						line.write(abData, 0, nBytesRead);
					}
					catch(IllegalArgumentException e)
					{
						//Has to be caught, because if the stream ends, there may occur an error, 
						//that the amount of bytes read is not valid (occurred under OpenJDK).
					}
				}
			}
		}
		if(line != null)
		{
			line.drain();
			line.close();
		}
//		running = false;
		if(!stop)
		{
			player.playNext();
		}
	}
	
	@Override
	public synchronized void pause()
	{
		pause = !pause;
		if(!pause)
		System.out.println("Weiter Geht's!");
		notify();
	}
	
	@Override
	public synchronized void stop_()
	{
		stop = true;
		notify();
	}
	
	@Override
	public synchronized boolean isStopped()
	{
		return stop;
	}
	
	public synchronized boolean isPlaying()
	{
		return playing;
	}
	
	@Override
	public void setVolume(float xVolume)
	{
		Global.setVolume(xVolume);
		if(line.isControlSupported(FloatControl.Type.MASTER_GAIN))
		{
			xVolume = xVolume/100; 
            FloatControl volume = (FloatControl) line.getControl( FloatControl.Type.MASTER_GAIN);
            double minGainDB = volume.getMinimum();
            double ampGainDB = ((10.0f/20.0f)*volume.getMaximum())-volume.getMinimum(); 
            double valueDB = minGainDB + (1/Global.LINEARSCALAR)*Math.log(1+(Math.exp(Global.LINEARSCALAR*ampGainDB)-1)*xVolume);
            volume.setValue((float) valueDB); 			
		}
		else if( line.isControlSupported(FloatControl.Type.VOLUME)) 
		{
            FloatControl volume = (FloatControl) line.getControl( FloatControl.Type.VOLUME);
            float onePercent = volume.getMaximum()/100;
            volume.setValue(xVolume*onePercent);
        }
	}
	
	@Override
	public long getMicrosecondPosition()
    {
        return line.getMicrosecondPosition() + (skippedDurationInMicroSeconds/1000);
    }
	
	public int getFramePosition()
	{
		if(line != null)
		{
			return line.getFramePosition()+skippedFrames;
		}
		else
		{
			return 0;
		}
	}
	
	public AudioFormat getAudioFormat() 
	{
		return audioFormat;
	}
	
//	public long getPosition()
//	{
//		return position;
//	}
	
	public AudioInputStream getAudioInputStream()
	{
		return audioInputStream;
	}
	
	public Song getCurrentSong()
	{
		return song;
	}
	
	public void seek(int percentage)
	{	
			// just pause the playback
			pause();
			try 
			{
				File file = null;
				try
				{
					file = new File(song.getSource().toURI());
				}
				catch(Exception e)
				{
					file = new File(song.getSource().getPath());
				}
				if(file != null)
				{
					long durationInSeconds = getDurationInSeconds();
					double skippedPercentage = (double)percentage/100;
					long skippedDurationInSeconds = (long) (durationInSeconds * skippedPercentage);
					long bytesToSkip = (long)((skippedDurationInSeconds)*(double)(audioFormat.getSampleRate()*audioFormat.getFrameSize()));
					System.out.println("Length in Bytes: " + (durationInSeconds)*((long)audioFormat.getSampleRate())*audioFormat.getFrameSize());
					System.out.println("Bytes to Skip: " + bytesToSkip);
					long bytesSkipped = audioInputStream.skip(bytesToSkip);
					System.out.println("Bytes Skipped: " + bytesSkipped);
					skippedFrames = skippedFrames + (int) bytesSkipped/audioFormat.getFrameSize();
					System.out.println("Frame Length: " + getFrameLength());
					System.out.println("Frames Skipped: " + skippedFrames);
				}
			} 
			catch (IOException e) 
			{
				e.printStackTrace();
			}	
			catch(Exception e)
			{
				e.printStackTrace();
			}
			// start playing again
			pause();
	}
	
	public int getFrameLength()
	{
//		if(stop)
//		{
//			return -1;
//		}
		try
		{
			int durationInSeconds = getDurationInSeconds();
			if(durationInSeconds > 0)
			{
				return (int)(durationInSeconds)*((int)audioFormat.getSampleRate());
			}
			return -1;
		}
		catch(Exception e)
		{
			System.out.println("Some other bug!");
			return -1;
		}
	}
	
	public int getDurationInSeconds()
	{
		return song.getDurationInSeconds();
	}
	
}
```

Grüße

s4ke


----------



## s4ke (21. Apr 2011)

Keiner eine Lösung?


----------



## Ralph-Uwe (23. Apr 2011)

Hallo,

ich habe mir Deinen Code noch nicht weiter angesehen,
aber ich habe gesehen, dass Du mit einem Audiostream arbeitest.

Um an eine bestimmte Stelle zu springen, muss Du wahrscheinlich den Stream
von vorne neu lesen, die nicht benötigten Bytes verwerfen und erst ab der gewünschten Stelle
die Daten wieder an die Soundkarte schicken.

Das wäre zumindest ein Ansatz.


----------



## s4ke (23. Apr 2011)

Hmm. Das habe ich mir auch schon ueberlegt. Ich haette vll doch was dazu schreiben sollen. Bei mir hapert es hauptsaechlich an der Berechnung der Position an die man springen will. Auf jsresources habe ich schon sowas gesehen, aber da war die Erklaerung irgendwie lueckenhaft.

Koenntest du vll auch mal den Bug ansehen, den ich gemeldet habe?


----------



## Ralph-Uwe (23. Apr 2011)

Kann ich machen, kann aber etwas  dauern.

Ich bräuchte aber noch das Interface "MusicPlayer" um den Code zum Laufen zu bringen.

Gruß


----------



## s4ke (23. Apr 2011)

-> github

https://github.com/s4ke/pure_mp3

Aber im Grunde brauchst du das nicht, ich habe das nur eingeführt, weil ich möglicherweise einen anderen Player brauche. Vielleicht auch nicht, aber dann fliegts halt wieder raus.


----------



## s4ke (29. Apr 2011)

So, ich glaube ich weiß woran es liegt, und es scheint so, als wäre das irgendwie nicht so einfach zu beheben. Deswegen habe ich das mal auf später verschoben. Das derzeitige Problem ist, wie ich die Audio Länge einer ".wav" Datei herausfinde. Per AudioFileFormat geht das ja nicht (genauso wie die FrameLength). Oder muss ich mir das selbst aus der FrameLength die ich nur vom AudioInputStream selbst bekomme, herausrechnen?


----------



## s4ke (29. Apr 2011)

Wegen dem Sourcecode. Im Anhang ist er mal gezippt.

EDIT: Mir ist gerade etwas eingefallen. Wie wäre es, wenn ich statt dem skip Mechanismus in AudioInputStream einfach in meine Runmethode einbaue, dass er nur abspielen darf, wenn er sich über dem Minimum befindet, ab dem er abspielen darf. Alles was darunter ist, wird zwar eingelesen aber einfach nicht abgespielt und das sollte dann auch einigermaßen performant sein. Oder nicht?


----------



## Ralph-Uwe (29. Apr 2011)

Hi,

um die länge eines Soundfile zu ermitteln benutze ich zwei verschiedene Varianten. Abhänig, ob 
es sich um eine .wav oder .mp3 Datei handelt.


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

public class Test {
	public static void main(String[] args) throws UnsupportedAudioFileException, IOException {
		File song = new File("d:/xxxx.mp3"); // oder xxxx.mp3
		long timeInMillisec = 0;
		
		AudioInputStream in = AudioSystem.getAudioInputStream(AudioFormat.Encoding.PCM_SIGNED, AudioSystem.getAudioInputStream(song));
		AudioFormat audioFormat = in.getFormat();
			    
		if (in.getFrameLength() == -1){
			timeInMillisec = song.length()/audioFormat.getSampleSizeInBits(); // berechnung für mp3
		}
		else{
			timeInMillisec = ((in.getFrameLength()/(long)audioFormat.getFrameRate())*1000); // berechnung für wav
		}
		System.out.println(timeInMillisec);
	}
}
```

Damit in diesem Beispiel eine mp3 Datei funktioniert wird das mp3plugin.jar benötigt.

Ich hoffe ich konnte helfen.


----------



## Ralph-Uwe (29. Apr 2011)

> Ich habe jetzt schon seit längerem das Gefühl, dass in der aktuellen OpenJDK in der Klasse SourceDataLine etwas schief läuft. Und zwar gibt die Methode Namens getMicroSecondsPosition() Millisekunden zurück (zumindest bei mir).



Bei mir wird der Wert auch in Millisekunden zurückgegeben.
Reicht aber völlig aus 

Gruß


----------



## s4ke (29. Apr 2011)

Naja mein Problem an der Sache ist aber, dass die sich die Sun Implementierung anders verhält.

Wegen der Berechnung, so hatte ich das ja auch vor, bzw. mache ich das bereits.

EDIT: Mich intressiert deine Meinung zu meinem Skippingeinfall.


----------



## s4ke (30. Apr 2011)

So, das mit dem Skippen indem man einfach bis zu einem bestimmten Punkt einliest klappt wunderbar. Es gibt zwar kleine Lags, aber es ist genau und funktioniert mit ".wavs" und ".mp3s" gleichermaßen!

EDIT: Scheint halt nicht anders als mit BruteForce zu gehen...

EDIT2: Irgendwie doch ein bischen lahm. Weiß jemand einen schöneren Weg?

EDIT3: Das lustige ist ja, dass .wavs kein Problem sind, egal wie groß sie sind...


----------



## Ralph-Uwe (2. Mai 2011)

Hi

habe ich das richtig verstanden, Du möchtest der Stream permanent von Anfang bis Ende
in einem Thread laufen lassen und die Daten nur dann an die Soundkarte weiterreichen, wenn
die aktuelle Position des Streams größer ist als die "Skipposition" ?
Wobei die "Skipposition" bei jeden Durchlauf um die Länge der an die Soundkarte weitergereichen Bufferlänge ergänzt werden müsste.

Wenn das so ist, kann ich mir vorstellen, dass Du den Sound nicht mehr sauber abgespielt bekommst.
Besonders bei großen Dateien.

Gruß


----------



## twseitex (21. Mai 2011)

Hallo,

mein Java-Player für mp3, wave und midi nutzt neben Java SE die JL-Bibliothek
für mp3 von Javazoom MP3 library for the Java Platform.
Damit muss unter Java nur die Ansteuerung von wave und midi manuell komplett
neu programmiert werden.
Ich benutze grundsätzlich nur BufferedInutStream, da mein Javaplayer ein
Applet für Webseiten ist und lokal und im Internet laufen soll.
Das Vorspielen von mp3 ist Sache der o.g. Bibliothek, die inzwischen leicht
an das aktuelle Java angepasst werden muss. Ich habe allerdings auf
sowas verzichtet und dafür die Animation plus Player-GUI fast frei konfigurierbar
gemacht.
Ich habe mich bewusst NUR für Java SE entschieden, da nur das regelmäßig
gepflegt wird von jetzt Oracle.
Bei mp3 per JL-Bibliothek kennt der Player getPosition(), aber kein setPosition(),
dafür aber play(int frames). Per Class AdvancedPlayer ist
play(int start, int end) möglich, also das Positionieren in der mp3.
Der Hersteller der Bibliothek hat eine komplette DOC dazu im Angebot.

Mein Player, der diese Bibliothek benutzt, ist auf meiner Webseite twseiten.com 
audio, flash and java ansehbar und downloadbar (Webseite ist in Englisch,
aber Player wird auch in Deutsch beschrieben).

Ciao !


----------



## s4ke (21. Mai 2011)

Ich benutze jl ja auch möchte aber den Player ganz schreiben, da ich dann später libs hinzufügen kann. Danke trotzdem .


----------

