/** * */ package com.soundlooper.audio.player; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.Timer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jouvieje.fmodex.Channel; import org.jouvieje.fmodex.DSP; import org.jouvieje.fmodex.Sound; import org.jouvieje.fmodex.defines.FMOD_MODE; import org.jouvieje.fmodex.defines.FMOD_TIMEUNIT; import org.jouvieje.fmodex.enumerations.FMOD_CHANNELINDEX; import org.jouvieje.fmodex.enumerations.FMOD_DSP_PITCHSHIFT; import org.jouvieje.fmodex.enumerations.FMOD_DSP_TYPE; import org.jouvieje.fmodex.enumerations.FMOD_RESULT; import org.jouvieje.fmodex.structures.FMOD_CREATESOUNDEXINFO; import org.jouvieje.fmodex.utils.BufferUtils; import org.jouvieje.fmodex.utils.SizeOfPrimitive; import com.soundlooper.exception.PlayerException; import com.soundlooper.model.SoundLooperPlayer; /** * ---------------------------------------------------------------------------- * ---- AudioEngine is an audio engine based on FMOD Copyright (C) 2014 * Alexandre NEDJARI * * This program 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. * * This program 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 * this program. If not, see <http://www.gnu.org/licenses/>. * * represent a sound * * @author Alexandre NEDJARI * @since 9 ao�t 2011 * ------------------------------------------------------------ * -------------------- */ public class SoundFile { /** * Timer to update the slider media time */ private Timer timerSlide; /** * The original file */ private File file; /** * The sound */ Sound sound = new Sound(); /** * The channel */ Channel channel = new Channel(); /** * The initial frequency of the sound */ float initialFrequency; /** * The logger dor this class */ private Logger logger = LogManager.getLogger(this.getClass()); /** * DSP for pitch effect (for timstretch) */ private DSP dspPitch = new DSP(); /** * duration of the current song */ int duration; /** * Save the current timeStretch */ private int currentTimeStretch; /** * buffer utilis� pour les appels */ ByteBuffer buffer = BufferUtils.newByteBuffer(SizeOfPrimitive.SIZEOF_INT); /** * buffer utilis� pour les appels */ ByteBuffer buffer2 = BufferUtils.newByteBuffer(SizeOfPrimitive.SIZEOF_INT); /** * Construct a sound from file * * @param file * the file to read * @throws PlayerException * If a {@link PlayerException} is threw * @throws IOException * If an {@link IOException} is threw */ public SoundFile(File file, Player player) throws PlayerException, IOException { super(); logger.info("Create sound file on file '" + file.getAbsolutePath() + "'"); this.file = file; player.setCurrentSound(this); // A SUPPRIMER FMOD_CREATESOUNDEXINFO exinfo = this.getExifInfo(); // A RESTAURER // ByteBuffer soundBuffer = // SoundFile.loadMediaIntoMemory(this.file.getAbsolutePath()); // // this.logger.info("JNI : Avant allocation exinfo"); // FMOD_CREATESOUNDEXINFO exinfo = FMOD_CREATESOUNDEXINFO.allocate(); // exinfo.setLength(soundBuffer.capacity()); // ///// this.logger.info("JNI : Apr�s allocation exinfo"); this.logger.info("JNI : Avant cr�ation du stream"); Player.errorCheck(Player.getSystem().createStream(this.file.getAbsolutePath(), FMOD_MODE.FMOD_SOFTWARE | FMOD_MODE.FMOD_ACCURATETIME | FMOD_MODE.FMOD_LOOP_NORMAL, exinfo, this.sound)); this.logger.info("JNI : Apr�s cr�ation du stream"); // soundBuffer.clear(); // soundBuffer = null; exinfo.release(); this.logger.info("JNI : Avant lecture du son (pour initialiser le channel"); Player.errorCheck(Player.getSystem().playSound(FMOD_CHANNELINDEX.FMOD_CHANNEL_FREE, this.sound, false, this.channel)); this.logger.info("JNI : Apr�s lecture du son (pour initialiser le channel"); this.logger.info("JNI : Avant pause du son (pour initialiser le channel"); Player.errorCheck(this.channel.setPaused(true)); this.stopTimer(); this.logger.info("JNI : Apr�s pause du son (pour initialiser le channel"); // FloatBuffer frequencyBuffer = BufferUtils.newFloatBuffer(1); this.logger.info("JNI : Avant r�cup�ration de la fr�quence"); Player.errorCheck(this.channel.getFrequency(this.buffer.asFloatBuffer())); this.logger.info("JNI : Apr�s r�cup�ration de la fr�quence"); this.initialFrequency = this.buffer.getFloat(0); this.buffer.clear(); this.initializeDuration(); this.initializeLoopPoint(); this.setTimeStrechPercent(100); } /** * @return * @throws FileNotFoundException * @throws IOException */ private FMOD_CREATESOUNDEXINFO getExifInfo() throws FileNotFoundException, IOException { InputStream is = PlayerActionLoad.class.getResourceAsStream(this.file.getAbsolutePath()); if (is == null) { if (new File(this.file.getAbsolutePath()).exists()) { is = new FileInputStream(new File(this.file.getAbsolutePath())); } else if (new File("." + this.file.getAbsolutePath()).exists()) { is = new FileInputStream(new File("." + this.file.getAbsolutePath())); } else { throw new FileNotFoundException("Le fichier '" + this.file.getAbsolutePath() + "' n'existe pas"); } } this.logger.info("JNI : Avant allocation exinfo"); FMOD_CREATESOUNDEXINFO exinfo = FMOD_CREATESOUNDEXINFO.allocate(); exinfo.setLength(is.available()); return exinfo; } /** * Retourne la repr�sentation temporelle de la chanson * * @throws PlayerException * Si une fonction retourne une erreur */ public BufferedImage generateImage() throws PlayerException, IOException { // Comme cette g�n�ration est faite dans un thread � part, n'utilise que // des variables locales pour �viter les conflits File fileLocal = this.file; ByteBuffer bufferLocal = BufferUtils.newByteBuffer(SizeOfPrimitive.SIZEOF_INT); FMOD_CREATESOUNDEXINFO exinfo = this.getExifInfo(); Sound soundImage = new Sound(); Player.errorCheck(Player.getSystem().createStream(fileLocal.getAbsolutePath(), FMOD_MODE.FMOD_SOFTWARE | FMOD_MODE.FMOD_ACCURATETIME | FMOD_MODE.FMOD_LOOP_NORMAL, exinfo, soundImage)); exinfo.release(); // http: //www.asawicki.info/news_1385_music_analysis_-_spectrogram.html Player.errorCheck(soundImage.getLength(bufferLocal.asIntBuffer(), FMOD_TIMEUNIT.FMOD_TIMEUNIT_PCM)); int tailleTotaleInt = bufferLocal.getInt(0); bufferLocal.clear(); int min = Integer.MAX_VALUE; int max = 0; int largeurImage = 2048; int hauteurImage = 512; int facteurImageY = Integer.MAX_VALUE / hauteurImage * 2; int sampleParPixel = tailleTotaleInt / largeurImage; // La taille du beffer est un multiple du nombre de samble par pixel * // 4, pour �viter tout probl�me de d�calage int tailleBuffer = sampleParPixel * 4; ByteBuffer dataBuffer = BufferUtils.newByteBuffer(SizeOfPrimitive.SIZEOF_BYTE * tailleBuffer); IntBuffer nbReadBuffer = BufferUtils.newIntBuffer(SizeOfPrimitive.SIZEOF_INT); int minImage = new Double(min * facteurImageY).intValue(); int maxImage = new Double(max * facteurImageY).intValue(); soundImage.seekData(0); BufferedImage off_Image = new BufferedImage(largeurImage, hauteurImage, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = off_Image.createGraphics(); // g2.setColor(new Color(36, 168, 206)); g2.setColor(new Color(127, 127, 127)); g2.fillRect(0, 0, largeurImage, hauteurImage); g2.setColor(Color.WHITE); int pixelX = 0; FMOD_RESULT resultat; int lu; int valeur; IntBuffer intDataBuffer; do { resultat = soundImage.readData(dataBuffer, tailleBuffer, nbReadBuffer); lu = nbReadBuffer.get() / 4; intDataBuffer = dataBuffer.asIntBuffer(); min = Integer.MAX_VALUE; max = 0; for (int i = 0; i < lu; i++) { valeur = intDataBuffer.get(i); if (valeur > max) { max = valeur; } if (valeur < min) { min = valeur; } if ((i + 1) % sampleParPixel == 0) { minImage = min / facteurImageY; maxImage = max / facteurImageY; // on vient de finir de calculer un pixel, on le sauvegarde // et on r�initialise les variables if (minImage > 0) { g2.drawRect(pixelX, hauteurImage / 2 - minImage, 1, minImage); } else { g2.drawRect(pixelX, hauteurImage / 2, 1, -minImage); } if (maxImage > 0) { g2.drawRect(pixelX, hauteurImage / 2 - maxImage, 1, maxImage); } else { g2.drawRect(pixelX, hauteurImage / 2, 1, -maxImage); } // On met � jour les variables min = Integer.MAX_VALUE; max = 0; pixelX++; } } dataBuffer.clear(); nbReadBuffer.clear(); } while (resultat != FMOD_RESULT.FMOD_ERR_FILE_EOF); dataBuffer.clear(); return off_Image; } /** * get the file * * @return the file */ public File getFile() { return this.file; } /** * Update the song volume * * @param volume * the new volume value (between 0 and 1) * @throws PlayerException * If the volume cannot be set */ public void setVolume(float volume) throws PlayerException { this.logger.info("JNI : Avant application volume"); Player.errorCheck(this.channel.setVolume(volume)); this.logger.info("JNI : Apr�s application volume"); } /** * Deallocate the sound and free streams * * @throws PlayerException * If deallocation fail * */ public void deallocate() throws PlayerException { this.logger.info("JNI : Avant d�sallocation chanson"); Player.errorCheck(this.channel.stop()); this.stopTimer(); this.logger.info("JNI : Apr�s d�sallocation chanson"); } /** * Stop the play * * @throws PlayerException * if a {@link PlayerException} is threw */ public void stop() throws PlayerException { this.logger.info("JNI : Avant stop chanson (pause)"); Player.errorCheck(this.channel.setPaused(true)); this.stopTimer(); this.logger.info("JNI : Apr�s stop chanson (pause)"); } /** * set the media time * * @param millisecondMediaTime * the millisecond time * @throws PlayerException * If a {@link PlayerException} is threw */ public void setMediaTime(int millisecondMediaTime) throws PlayerException { this.logger.info("JNI : Avant changement media time (" + millisecondMediaTime + "/" + this.duration + ")"); Player.errorCheck(this.channel.setPosition(millisecondMediaTime, FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS)); this.logger.info("JNI : Apr�s changement media time (" + millisecondMediaTime + ")"); } /** * get the media duration in ms * * @return the media duration */ public int getDuration() { return this.duration; } /** * Initialize the duration * * @throws PlayerException * if a {@link PlayerException} is threw */ private void initializeDuration() throws PlayerException { this.logger.info("JNI : Avant init dur�e"); Player.errorCheck(this.sound.getLength(this.buffer.asIntBuffer(), FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS)); this.logger.info("JNI : Apr�s init dur�e"); this.duration = this.buffer.getInt(0); this.buffer.clear(); } /** * Initialize the loop points * * @throws PlayerException * if a {@link PlayerException} is threw */ private void initializeLoopPoint() throws PlayerException { this.logger.info("JNI : Avant init loop points"); Player.errorCheck(this.sound.getLoopPoints(this.buffer.asIntBuffer(), FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS, this.buffer2.asIntBuffer(), FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS)); this.logger.info("JNI : Apr�s init loop points"); int loopPointBegin = this.buffer.getInt(0); int loopPointEnd = this.buffer2.getInt(0); SoundLooperPlayer.getInstance().setLoopPointBegin(loopPointBegin); SoundLooperPlayer.getInstance().setLoopPointEnd(loopPointEnd); // setLoopPoints(loopPointBegin, loopPointEnd); this.buffer.clear(); this.buffer2.clear(); } /** * get the media time in ms * * @return the media time * @throws PlayerException * If a {@link PlayerException} is threw */ protected int getMediaTime() throws PlayerException { Player.errorCheck(this.channel.getPosition(this.buffer.asIntBuffer(), FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS)); int mediaTime = this.buffer.getInt(0); this.buffer.clear(); // result.clear(); return mediaTime; } /** * Start playing song */ public void start() { this.logger.info("JNI : Avant play"); this.channel.setPaused(false); this.logger.info("JNI : Apr�s play"); } /** * Set a timestretch on the song * * @param percent * the percent between 50% and 200% * @throws PlayerException * if a {@link PlayerException} is threw */ public void setTimeStrechPercent(int percent) throws PlayerException { if (percent > 200 || percent < 50) { throw new PlayerException("for timeStretch, percent factor must be between 50% and 200%, here it's " + percent + "%"); } if (percent == 100) { this.setTimeStrech(1); } float factor = 0; if (percent > 100) { factor = 1 - ((percent - 100) / 200.0f); } else { factor = 1 + (100 - percent) / 50.0f; } this.setTimeStrech(factor); this.currentTimeStretch = percent; } /** * remove the current timestretch * * @throws PlayerException * If a {@link PlayerException} is threw */ public void removeTimeStrech() throws PlayerException { this.logger.info("JNI : Avant test dsp"); if (!this.dspPitch.isNull()) { this.logger.info("JNI : Avant remove timestretch (dsp)"); Player.errorCheck(this.dspPitch.release()); this.logger.info("JNI : Apr�s remove timestretch (dsp)"); this.logger.info("JNI : Avant remove timestretch (frequency)"); Player.errorCheck(this.channel.setFrequency(this.initialFrequency)); this.logger.info("JNI : Apr�s remove timestretch (frequency)"); } } /** * Apply a timestretch * * @param facteur * the factor (between 0.5 an 2 * @throws PlayerException * if a playerException is threw */ private void setTimeStrech(float facteur) throws PlayerException { try { this.logger.info("JNI : Avant release du DSP"); if (!this.dspPitch.isNull()) { Player.errorCheck(this.dspPitch.release()); } this.logger.info("JNI : Apr�s release du DSP"); // set new Timestretch this.logger.info("JNI : Avant creation du DSP"); Player.errorCheck(Player.getSystem().createDSPByType(FMOD_DSP_TYPE.FMOD_DSP_TYPE_PITCHSHIFT, this.dspPitch)); this.logger.info("JNI : Avant setParameter du DSP"); Player.errorCheck(this.dspPitch.setParameter(FMOD_DSP_PITCHSHIFT.FMOD_DSP_PITCHSHIFT_PITCH.asInt(), facteur)); this.logger.info("JNI : Avant add du DSP"); Player.errorCheck(Player.getSystem().addDSP(this.dspPitch, null)); this.logger.info("JNI : Apr�s add du DSP"); this.logger.info("JNI : Avant changement de fr�quence : " + (this.initialFrequency / facteur)); Player.errorCheck(this.channel.setFrequency(this.initialFrequency / facteur)); this.logger.info("JNI : Apr�s changement de fr�quence"); } catch (PlayerException e) { // if an exception occurred, try to restore original timestrecht this.logger.info("JNI : Avant restauration DSP (frequency)"); this.channel.setFrequency(this.initialFrequency); this.logger.info("JNI : Avant restauration DSP (DSP)"); this.dspPitch.release(); this.logger.info("JNI : Apr�s restauration DSP"); throw e; } } /** * set the loop points * * @param beginPoint * the begin point * @param endPoint * the end point * @throws PlayerException * if a PlayerException is threw */ public void setLoopPoints(int beginPoint, int endPoint) throws PlayerException { int mediaTime = getMediaTime(); this.logger.info("JNI : Avant set loop points " + beginPoint + " to " + endPoint + "(position:" + mediaTime + ", duration:" + this.getDuration() + ")"); if (endPoint == this.duration) { endPoint--; } logger.error("CHANGEMENT DE LOOP POINT : " + beginPoint + " -> " + endPoint); Player.errorCheck(this.channel.setLoopPoints(beginPoint, FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS, endPoint, FMOD_TIMEUNIT.FMOD_TIMEUNIT_MS)); if (mediaTime > endPoint || mediaTime < beginPoint) { setMediaTime(beginPoint); } this.logger.info("JNI : Apr�s set loop points "); } /** * @return the currentTimeStretch */ public int getCurrentTimeStretch() { return this.currentTimeStretch; } /** * Stop the timer for the slider */ public void stopTimer() { if (this.timerSlide != null) { this.timerSlide.cancel(); this.timerSlide = null; } } }