package memory.lecteur;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javazoom.spi.PropertiesContainer;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
import org.tritonus.share.sampled.file.TAudioFileFormat;
/**
* LecteurMP3 est un lecteur de fichier MP3 simplissime basé sur l'API JavaSound
* et sur le BasicPlayer de JavaZoom
*/
public class LecteurMP3 implements Runnable
{
/************** LES CONSTANTES **************************************************************************/
//elles indiquent dans quel état est le lecteur et permettent de gérer le thread
private static final int UNKNOWN = -1,
PLAYING = 0,
PAUSED = 1,
STOPPED = 2,
SEEKING = 4,
OPENED = 3;
/************** LES ATTRIBUTS ***************************************************************************/
//le tampon de lecture
private static int EXTERNAL_BUFFER_SIZE = 4000 * 4;
//un lecteur de fichier MP3
private static MpegAudioFileReader mafr = new MpegAudioFileReader();
// // ? peut-être les octets d'entête des fichiers MP3 ?
// private static int SKIP_INACCURACY_SIZE = 1200;
//la source audio sous forme d'un fichier mp3
private File source;
//le thread de lecture
private Thread thread = null;
//gestion du flux de lecture (le flux encodé en MP3, IO)
private AudioInputStream encodedAudioInputStream;
//la longueur du fichier en octet
private int nbBytes = -1;
//gestion du flux audio (le flux décodé, AudioSystem)
private AudioInputStream audioInputStream;
//les infos sur le format du fichier
private AudioFileFormat audioFileFormat;
//la sortie son (AudioSystem)
private SourceDataLine sortieSon;
//le controle de volume
protected FloatControl controleVolume;
//l'état du lecteur (au départ inconnu)
private int etat = UNKNOWN;
// les écouteurs qui seront informés des changements d'état
private Collection listeners = null;
//le nombre d'octets à passer avant de commencer à jouer
private int nbByteSkip = -1;
/************** CONSTRUCTEUR ******************************************************************/
public LecteurMP3()
{
//lorsque l'on crée le lecteur, la source n'existe pas
this.source = null;
//il n'y a pas non plus d'écouteurs d'évènements (liste vide)
this.listeners = new ArrayList();
//initialiser le lecteur
reset();
}
/************** INITIALISATIONS ***************************************************************/
/**
* initialiser le lecteur
*/
protected void reset()
{
//l'état du lecteur est inconnu (en fait ni lecture en cours, pause, arrêté ...)
this.etat = UNKNOWN;
//si un fichier audio est en lecture au moment de l'initialisation, fermer le flux
if (this.audioInputStream != null)
{
synchronized (this.audioInputStream)
{
closeStream();
}
}
//le flux entrant est null
this.audioInputStream = null;
//le format du fichier est null
this.audioFileFormat = null;
//l'encodage du flux est null
this.encodedAudioInputStream = null;
//la longueur de l'encodage est -1 (sans objet)
this.nbBytes = -1;
//Si la ligne de sortie son n'est pas nulle, on l'arrête, la ferme et la passe à null
if (this.sortieSon != null)
{
this.sortieSon.stop();
this.sortieSon.close();
this.sortieSon = null;
}
//le volume est null
this.controleVolume = null;
}
/**
* Initialiser le flux et le format audio à partir du fichier source
* @throws LecteurException les exceptions du lecteur
*/
protected void initAudioInputStream() throws LecteurException
{
try
{
//on réinitialise le lecteur
reset();
//informer les listeners de l'état du lecteur
//cette information sera lancée dans un thread à part pour ne pas ralentir le lecteur
//On s'intéresse essentiellemnt à la position dans le fichier de lecture
notifyEvent(LecteurEvent.OPENING);
//initialiser le flux audio (c'est un fichier local)
initAudioInputStream(this.source);
//création de la ligne de sortie du son
createLine();
//Pour les mp3 il faut passer par l'API Tritonus SPI. Il faut cependant prévoir le cas
//où le fichier lu ne serait pas compatible avec l'interface Tritonus
Map proprietesMP3 = null;
if(this.audioFileFormat instanceof TAudioFileFormat)
{
//on passe le format compatible avec l'API Tritonus
proprietesMP3 = ((TAudioFileFormat) this.audioFileFormat).properties();
}
else throw new LecteurException(this.source + " n'est pas un fichier mp3");
//le nombre d'octets du fichier : utile pour déterminer approximativement le nombre d'octets
//à sauter avant de commencer à jouer un morceau. Ce nombre d'octet ne comporte pas que les
//octets audio mais aussi les tags. Ces derniers représentent une infime partie du fichier
//audio lui-même et donc l'erreur engendrée est minime.
this.nbBytes = this.audioFileFormat.getByteLength ();
//la durée du morceau en microsecondes
Long duree = (Long) proprietesMP3.get ("duration");
//prévenir chaque écouteur de la durée du chant
Iterator it = this.listeners.iterator();
while (it.hasNext())
{
LecteurListener listener = (LecteurListener) it.next();
listener.opened (duree);
}
//le flux est "ouvert"
this.etat = OPENED;
//il faut en informer les listeners
notifyEvent(LecteurEvent.OPENED);
}
//les erreurs susceptibles de survenir
catch (LineUnavailableException e)
{
throw new LecteurException(e);
}
catch (UnsupportedAudioFileException e)
{
throw new LecteurException(e);
}
catch (IOException e)
{
throw new LecteurException(e);
}
}
// /**
// * Initialiser la ressource audio à partir du fichier MP3
// * @param file le fichier à lire
// * @throws UnsupportedAudioFileException erreur audio
// * @throws IOException erreur fichier
// */
// protected void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException
// {
// this.audioInputStream = AudioSystem.getAudioInputStream(file);
// this.audioFileFormat = AudioSystem.getAudioFileFormat(file);
// }
/**
* Initialiser la ressource audio à partir d'une URL représentant un fichier MP3
* @param file le fichier à lire
* @throws UnsupportedAudioFileException erreur audio
* @throws IOException erreur fichier
*/
protected void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException
{
// System.out.println("initAudioInputStream " + url);
this.audioInputStream = AudioSystem.getAudioInputStream(url);
this.audioFileFormat = AudioSystem.getAudioFileFormat(url);
}
/************* ACTIONS SUR LE FLUX ***********************************************************/
/**
* fermer le flux
*/
private void closeStream()
{
try
{
if (this.audioInputStream != null) this.audioInputStream.close();
}
catch (IOException e)
{
System.out.println("erreur sur la fermeture du flux");
e.printStackTrace ();
}
}
/************* ACTIONS SUR LE LECTEUR ***********************************************************/
/**
* Arrêter la lecture
* Etat du lecteur = STOPPED.
* le thread doit libérer les ressources audio
*/
private void stopPlayback()
{
if ((this.etat == PLAYING) || (this.etat == PAUSED))
{
if (this.sortieSon != null)
{
this.sortieSon.flush();
this.sortieSon.stop();
}
this.etat = STOPPED;
notifyEvent(LecteurEvent.STOPPED);
synchronized (this.audioInputStream)
{
closeStream();
}
}
}
/**
* Faire une pause dans la lecture
*
* Etat du lecteur = PAUSED.
*/
private void pausePlayback()
{
if (this.sortieSon != null)
{
if (this.etat == PLAYING)
{
this.sortieSon.flush();
this.sortieSon.stop();
this.etat = PAUSED;
notifyEvent(LecteurEvent.PAUSED);
}
}
}
/**
* Relancer la lecture après une pause
* Etat du lecteur = PLAYING.
*/
private void resumePlayback()
{
if (this.sortieSon != null)
{
if (this.etat == PAUSED)
{
this.sortieSon.start();
this.etat = PLAYING;
notifyEvent(LecteurEvent.RESUMED);
}
}
}
/**
* Démarrer la lecture
*/
private void startPlayback() throws LecteurException
{
if (this.etat == STOPPED) initAudioInputStream();
if (this.etat == OPENED)
{
//ATTENTION : le thread précédent est encore en lecture
if ((this.thread != null) && (this.thread.isAlive()))
{
int cnt = 0;
while (this.etat != OPENED)
{
try
{
if (this.thread != null)
{
cnt++;
Thread.sleep(1000);
if (cnt > 2)
{
//s'il ne veut pas s'arrêter de lui même on l'interrompt
this.thread.interrupt();
}
}
}
catch (InterruptedException e)
{
throw new LecteurException(LecteurException.WAITERROR, e);
}
}
}
// Ouvrir la sortie son
try
{
initLine();
}
catch (LineUnavailableException e)
{
throw new LecteurException(LecteurException.CANNOTINITLINE, e);
}
// créer un nouveau thread
this.thread = new Thread(this, "LecteurMP3");
this.thread.start();
if (this.sortieSon != null)
{
this.sortieSon.start();
this.etat = PLAYING;
notifyEvent(LecteurEvent.PLAYING);
}
}
}
/************* ACTIONS SUR LA SORTIE SON *****************************************************/
/**
* Initialiser la sortie son
* C'est là que les choses sérieuses commencent
* @throws LineUnavailableException les erreurs sur la sortie son
*/
private void initLine() throws LineUnavailableException
{
//créer la sortie si elle n'existe pas
if (this.sortieSon == null) createLine();
//l'ouvrir si elle n'est pas ouverte
if (!this.sortieSon.isOpen())
{
openLine();
}
else
{
//comparer le format audio qu'attend la sortie son et celui du flux
//s'ils sont différents, fermer puis rouvrir la ligne
AudioFormat lineAudioFormat = this.sortieSon.getFormat();
AudioFormat audioInputStreamFormat = (this.audioInputStream == null ? null : this.audioInputStream.getFormat());
if (!lineAudioFormat.equals(audioInputStreamFormat))
{
this.sortieSon.close();
openLine();
}
}
}
/**
* Créer la sortie son
* @throws LineUnavailableException les erreurs sur la sortie son
*/
private void createLine() throws LineUnavailableException
{
if (this.sortieSon == null)
{
//récupérer le format de la source
AudioFormat sourceFormat = this.audioInputStream.getFormat();
//pour les fichiers MP3 choisis nSampleSizeInBits = 16;
AudioFormat targetFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
sourceFormat.getSampleRate(), 16,
sourceFormat.getChannels(),
2 * sourceFormat.getChannels(),
sourceFormat.getSampleRate(), false);
// garder une référence sur le flux audio pour la progression de la lecture
// encodedAudioInputStream est le format encodé de départ (IO)
// audioInputStream sera le format décodé (système audio)
this.encodedAudioInputStream = this.audioInputStream;
//Créer le flux décodé
this.audioInputStream = AudioSystem.getAudioInputStream(targetFormat, this.audioInputStream);
//récupérer le format décodé
AudioFormat audioFormat = this.audioInputStream.getFormat();
//Informer la sortie son
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
this.sortieSon = (SourceDataLine) AudioSystem.getLine(info);
}
}
/**
* Ouvrir la sortie son
* @throws LineUnavailableException les erreurs sur la sortie son
*/
private void openLine() throws LineUnavailableException
{
if (this.sortieSon != null)
{
// if (this.sortieSon.isOpen ()) this.sortieSon.close ();
//créer une sortie son
AudioFormat audioFormat = this.audioInputStream.getFormat();
int buffersize = this.sortieSon.getBufferSize();
this.sortieSon.open(audioFormat, buffersize);
//la sortie son supporte-t-elle les modifications de volume ?
if (this.sortieSon.isControlSupported(FloatControl.Type.MASTER_GAIN))
{
this.controleVolume = (FloatControl) this.sortieSon.getControl(FloatControl.Type.MASTER_GAIN);
}
}
}
/************* LA BOUCLE PRINCIPALE **********************************************************/
/**
* la boucle principale
*
* Etat == STOPPED || SEEKING => Fin du thread et libérer les ressources audio
* Etat == PLAYING => Le flux audio est envoyé vers la sortie
* Etat == PAUSED => Attendre un nouvel état
*
*/
public void run()
{
int nBytesRead = 1;
byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
// Verrouiller le flux pendant la lecture
// if (this.audioInputStream!=null)
// {
//bloquer le flux pendant la lecture
synchronized (this.audioInputStream)
{
//la boucle play/pause
while ((nBytesRead != -1) && (this.etat != STOPPED) && (this.etat != UNKNOWN) /*&& (this.etat != SEEKING)*/)
{
if (this.etat == PLAYING)
{
// Play.
try
{
if (this.nbByteSkip > 0)
{
this.audioInputStream.skip (this.nbByteSkip);
this.nbByteSkip = 0;
}
nBytesRead = this.audioInputStream.read(abData, 0, abData.length);
if (nBytesRead >= 0)
{
byte[] pcm = new byte[nBytesRead];
System.arraycopy(abData, 0, pcm, 0, nBytesRead);
this.sortieSon.write(abData, 0, nBytesRead);
// int nBytesWritten = this.sortieSon.write(abData, 0, nBytesRead);
// // Compute position in bytes in encoded stream.
// int nEncodedBytes = getEncodedStreamPosition();
// Notify listeners
Iterator it = this.listeners.iterator();
while (it.hasNext())
{
LecteurListener listener = (LecteurListener) it.next();
if (this.audioInputStream instanceof PropertiesContainer)
{
Map proprietes = ((PropertiesContainer) this.audioInputStream).properties ();
long y = (Long) proprietes.get("mp3.position.microseconds");
listener.progress (y);
}
else throw new LecteurException("impossible d'obtenir la progression de la lecture");
// bpl.progress (nEncodedBytes, this.sortieSon.getMicrosecondPosition(), pcm, empty_map);
}
}
}
catch (IOException e)
{
this.etat = STOPPED;
notifyEvent(LecteurEvent.STOPPED);
e.printStackTrace ();
}
catch (LecteurException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
else
{
// Pause
try
{
Thread.sleep(1000);
}
catch (InterruptedException e) {}
}
}
// Libérer les ressources car la lecture est finie
if (this.sortieSon != null)
{
this.sortieSon.drain();
this.sortieSon.stop();
this.sortieSon.close();
this.sortieSon = null;
}
// Notification of "End Of Media"
if (nBytesRead == -1)
{
notifyEvent(LecteurEvent.EOM);
}
closeStream();
}
this.etat = STOPPED;
//informer de l'arrêt
notifyEvent(LecteurEvent.STOPPED);
}
protected String etatEnChaine ()
{
switch (this.etat)
{
case UNKNOWN : return "inconnu";
case PLAYING : return "en lecture";
case PAUSED : return "en pause";
case STOPPED : return "à l'arrêt";
case SEEKING : return "saut";
case OPENED : return "ouvert";
}
return "cas non prévu" ;
}
/**
* Informer les écouteurs d'un évènement lié au lecteur. Cette information est lancée dans
* un thread spécifique pour ne pas ralentir la lecture
* @param code le code de l'évènement
*/
protected void notifyEvent(int code)
{
LecteurEventLauncher trigger = new LecteurEventLauncher(code,new ArrayList(this.listeners));
trigger.start();
}
/**
* @return la position dans le flux de lecture
*/
protected int getEncodedStreamPosition()
{
int nEncodedBytes = -1;
try
{
if (this.encodedAudioInputStream != null)
{
//encodedAudioInputStream.available() = ce qui reste à lire
// nEncodedBytes = this.encodedLength - this.encodedAudioInputStream.available();
nEncodedBytes = this.nbBytes - this.encodedAudioInputStream.available();
}
}
catch (IOException e) {}
return nEncodedBytes;
}
/**
* @return vrai si le controle de volume est supporté
*/
public boolean hasGainControl()
{
if (this.controleVolume == null)
{
// Try to get Gain control again (to support J2SE 1.5)
if ( (this.sortieSon != null) && (this.sortieSon.isControlSupported(FloatControl.Type.MASTER_GAIN)))
this.controleVolume = (FloatControl) this.sortieSon.getControl(FloatControl.Type.MASTER_GAIN);
}
return this.controleVolume != null;
}
/**
* @return la valeur du volume
*/
public float getGainValue()
{
if (hasGainControl()) return this.controleVolume.getValue();
else return 0.0F;
}
/**
* @return la valeur maximale du volume
*/
public float getMaximumGain()
{
if (hasGainControl())
{
return this.controleVolume.getMaximum();
}
else
{
return 0.0F;
}
}
/**
* @return la valeur minimale du volume
*/
public float getMinimumGain()
{
if (hasGainControl())
{
return this.controleVolume.getMinimum();
}
else
{
return 0.0F;
}
}
// public int getNbByteSkip ()
// {
// return this.nbByteSkip ;
// }
// public void setNbByteSkip (int nbByteSkip)
// {
// this.nbByteSkip = nbByteSkip ;
// }
/**
* lancer la lecture
* @throws LecteurException erreur de lecture
*/
public void play() throws LecteurException
{
startPlayback();
}
/**
* Arrêter la lecture
* @throws LecteurException
*/
public void stop()
{
stopPlayback();
}
/**
* Faire une pause
* @throws LecteurException
*/
public void pause()
{
pausePlayback();
}
/**
* Reprendre la lecture après une pause
* @throws LecteurException
*/
public void resume()
{
resumePlayback();
}
/**
* modifie le volume
* La sortie son est nécessairement ouverte avant l'appel
* Echelle linéaire 0.0 <--> 1.0
* Threshold Coef. : 1/2 pour éviter la saturation.
*/
public void setGain(double fGain) throws LecteurException
{
if (hasGainControl())
{
double minGainDB = getMinimumGain();
double ampGainDB = ((10.0f / 20.0f) * getMaximumGain()) - minGainDB;
double cste = Math.log(10.0) / 20;
double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * fGain);
this.controleVolume.setValue((float) valueDB);
notifyEvent(LecteurEvent.GAIN);
}
else throw new LecteurException(LecteurException.GAINCONTROLNOTSUPPORTED);
}
/**
* Initialiser la ressource audio à partir du fichier MP3
* @param file le fichier à lire
* @throws UnsupportedAudioFileException erreur audio
* @throws IOException erreur fichier
*/
protected void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException
{
// this.audioInputStream = AudioSystem.getAudioInputStream(file);
// this.audioFileFormat = AudioSystem.getAudioFileFormat(file);
this.audioInputStream = mafr.getAudioInputStream(file);
this.audioFileFormat = mafr.getAudioFileFormat(file);
}
/**
* On se positionne à n'importe quel endroit du morceau exprimé sous la forme
* d'un pourcentage. Il est nécessaire ensuite d'utiliser play pour écouter
* ce qui reste du morceau.
* @param x un nombre entre 0 et 1 exprimant le pourcentage à sauter
*/
public void skip (double x)
{
if ((x < 0) || (x>1)) throw new IllegalArgumentException("paramètre illégal : x (" + x + ") devrait être entre 0 et 1");
//le nombre d'octets à sauter se calcule à partir du nombre d'octets du fichier. Ce nombre
//d'octets ne comporte pas que les octets audio mais aussi les tags. Ces derniers représentent
//une infime partie du fichier audio lui-même et donc l'erreur engendrée est minime.
this.nbByteSkip = (int) (x * this.nbBytes);
}
/**************************************************************************************************/
/**
* Ajouter un écouteur à informer
* @param bpl le listener
*/
public void addLecteurListener(LecteurListener bpl)
{
this.listeners.add(bpl);
}
/************** TEST *****************************************************************************/
/**
* Initialiser la source
* @param file Le fichier à lire (mp3)
* @throws LecteurException les exceptions du lecteur
*/
public void open(File file) throws LecteurException
{
if (file != null)
{
// System.out.println(file.getAbsolutePath());
//définir la source
this.source = file;
//une fois la source connue, initialiser le flux
initAudioInputStream();
}
}
/**
* un test
* @param args
*/
public static void main(String[] args)
{
final LecteurMP3 lecteur = new LecteurMP3();
File file = new File("chants\\merle noir.mp3");
// URL url = Fichier.chargerChant("merle noir.mp3");
// System.out.println(file.getAbsolutePath());
try
{
lecteur.open (file);
lecteur.skip (0.90);
lecteur.play ();
}
catch (LecteurException e)
{
// System.out.println("erreur en ouvrant : " + url);
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}