diff --git a/build.gradle b/build.gradle index d0e1c1e..8253c09 100755 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,7 @@ project(":core") { compile "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" compile group: 'com.badlogicgames.jlayer', name: 'jlayer', version: '1.0.1-gdx' + compile group: 'com.googlecode.soundlibs', name: 'mp3spi', version: '1.9.5-1' compile "org.apache.commons:commons-math3:3.2" compile "com.github.rwl:jtransforms:2.4.0" diff --git a/core/src/zero1hd/rhythmbullet/audio/Mp3Manager.java b/core/src/zero1hd/rhythmbullet/audio/Mp3Manager.java index ad41cb8..a2a9f06 100755 --- a/core/src/zero1hd/rhythmbullet/audio/Mp3Manager.java +++ b/core/src/zero1hd/rhythmbullet/audio/Mp3Manager.java @@ -1,121 +1,69 @@ package zero1hd.rhythmbullet.audio; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.nio.channels.Channels; -import java.security.InvalidParameterException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Map; -import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; -import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; -import org.jaudiotagger.audio.mp3.MP3File; -import org.jaudiotagger.tag.TagException; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + +import org.tritonus.share.sampled.TAudioFormat; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Music.OnCompletionListener; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.math.MathUtils; -import com.badlogic.gdx.utils.GdxRuntimeException; -import javazoom.jl.decoder.Bitstream; -import javazoom.jl.decoder.BitstreamException; -import javazoom.jl.decoder.DecoderException; -import javazoom.jl.decoder.Header; -import javazoom.jl.decoder.MP3Decoder; -import javazoom.jl.decoder.OutputBuffer; +import zero1hd.rhythmbullet.audio.wavedecoder.AudioSampleReader; + public class Mp3Manager implements MusicManager { private int readWindowSize = 1024; - - private int readWindowIndex; - private Music playbackMusic; + private Music music; private int playbackIndex; + private FileHandle fileHandle; + private AudioInputStream ais; + private AudioFormat af; + private AudioSampleReader d; + private Map properties; - private long bitrate; - private int sampleRate; - private long sampleCount; - private double durationInSeconds; - private byte channels; - Bitstream bitstream; - MP3Decoder decoder; - OutputBuffer sampleBuffer; - InputStream inputStream; - private byte[] currentByteSet; - private byte[] workset; - private int indexHead = -1; - private RandomAccessFile raf; - private FileHandle file; - ReentrantLock lock = new ReentrantLock(); - - private ExecutorService exec; - - private String basicSongName; - public Mp3Manager(FileHandle audioFile) { - this.file = audioFile; - lock = new ReentrantLock(); - this.basicSongName = audioFile.name(); - exec = Executors.newSingleThreadExecutor(); - exec.submit(() -> { - lock.lock(); - Logger.getLogger("org.jaudiotagger").setLevel(Level.OFF); - try { - MP3File mp3File = new MP3File(audioFile.file()); - sampleCount = MathUtils.round(Float.valueOf((float) (mp3File.getAudioHeader().getSampleRateAsNumber()*mp3File.getMP3AudioHeader().getPreciseTrackLength()))); - sampleRate = mp3File.getMP3AudioHeader().getSampleRateAsNumber(); - durationInSeconds = mp3File.getMP3AudioHeader().getPreciseTrackLength(); - bitrate = mp3File.getAudioHeader().getBitRateAsNumber(); - } catch (IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { - e.printStackTrace(); - } - lock.unlock(); - }); - + public Mp3Manager(FileHandle file) { + this.fileHandle = file; try { - raf = new RandomAccessFile(audioFile.file(), "r"); - bitstream = new Bitstream(inputStream = Channels.newInputStream(raf.getChannel())); - decoder = new MP3Decoder(); - } catch (FileNotFoundException e2) { - e2.printStackTrace(); - throw new InvalidParameterException("Error with creating RandomAccessFile."); - } - try { - Header header = bitstream.readFrame(); - if (header == null) throw new GdxRuntimeException("Empty MP3"); - channels = (byte) (header.mode() == Header.SINGLE_CHANNEL ? 1 : 2); - sampleBuffer = new OutputBuffer(channels, false); - decoder.setOutputBuffer(sampleBuffer); - workset = new byte[channels*2]; - } catch (BitstreamException e) { - throw new GdxRuntimeException("error while preloading mp3", e); - } - - playbackMusic = Gdx.audio.newMusic(audioFile); - - } - @Override - public void setReadIndexToPlaybackIndex() { - try { - long pointer = (long) (getPositionInSeconds()*(bitrate*sampleRate/8f)); - raf.seek(pointer); - System.out.println(pointer); - readWindowIndex = playbackIndex; - currentByteSet = null; - } catch (IOException e) { + AudioInputStream in; + in = AudioSystem.getAudioInputStream(file.file()); + AudioFormat baseFormat = in.getFormat(); + properties = ((TAudioFormat)baseFormat).properties(); + af = new AudioFormat( + AudioFormat.Encoding.PCM_SIGNED, + baseFormat.getSampleRate(), + 16, baseFormat.getChannels(), + baseFormat.getChannels()*2, + baseFormat.getSampleRate(), false); + ais = AudioSystem.getAudioInputStream(baseFormat, in); + in.close(); + d = new AudioSampleReader(ais); + music = Gdx.audio.newMusic(file); + } catch (UnsupportedAudioFileException | IOException e) { e.printStackTrace(); } } + @Override + public void dispose() { + music.dispose(); + try { + ais.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + @Override public void playbackIndexUpdate() { - playbackIndex = (int) (playbackMusic.getPosition() * sampleRate / readWindowSize); + playbackIndex = (int) ((getDuration()*getSampleRate())/getReadWindowSize()); } @Override @@ -128,172 +76,122 @@ public class Mp3Manager implements MusicManager { return readWindowSize; } + @Override + public int readSamples(float[] samples) { + try { + return d.readSamples(samples); + } catch (IOException e) { + e.printStackTrace(); + }; + return 0; + } + @Override public long getSampleCount() { - return sampleCount; + return ais.getFrameLength(); } @Override public float getDuration() { - return Float.valueOf(String.valueOf(durationInSeconds)); + return ((long) properties.get("duration"))/1000000f; } - @Override - public int readSamples(float[] samples) { - int framesRead = 0; - for (int sid = 0; sid < samples.length; sid++) { - for (int wsid = 0; wsid < workset.length; wsid++) { - workset[wsid] = nextByte(); - } - if (currentByteSet != null) { - samples[sid] += (workset[1] << 8) + (workset[0] & 0x00ff); - if (channels > 1) { - short altChan = (short) ((workset[3] << 8) + (workset[2] & 0x00ff)); - - samples[sid] = altChan > samples[sid] ? altChan : samples[sid]; - } - framesRead ++; - samples[sid] /= Short.MAX_VALUE+1; - } - } - readWindowIndex++; - - return framesRead; - } - - public byte nextByte() { - indexHead++; - if (currentByteSet == null || indexHead >= currentByteSet.length) { - loadNextBuffer(); - if (currentByteSet == null) { - return 0; - } - indexHead = 0; - } - - return currentByteSet[indexHead]; - } - - public int loadNextBuffer() { - if (bitstream != null) { - int bytesRead = 0; - try { - Header header = bitstream.readFrame(); - if (header != null) { - - try { - decoder.decodeFrame(header, bitstream); - } catch (ArrayIndexOutOfBoundsException | DecoderException e) { - System.out.println(e); - } - - bitstream.closeFrame(); - bytesRead = sampleBuffer.reset(); - - currentByteSet = sampleBuffer.getBuffer(); - } else { - currentByteSet = null; - } - } catch (BitstreamException e1) { - e1.printStackTrace(); - } - return bytesRead; - } else { - return 0; - } - } - @Override public float getSampleRate() { - return sampleRate; - } - - @Override - public long getBitrate() { - return bitrate; - } - - @Override - public int getreadWindowIndex() { - return readWindowIndex; + return af.getSampleRate(); } @Override public void pause() { - Gdx.app.debug("MP3Manager", "Pausing..."); - playbackMusic.pause(); + music.pause(); } @Override public void play() { - Gdx.app.debug("Mp3Manager", "MP3 Playing..."); - playbackMusic.play(); + music.play(); } @Override public boolean isPlaying() { - return playbackMusic.isPlaying(); + return music.isPlaying(); } @Override public float getPositionInSeconds() { - return playbackMusic.getPosition(); + + return music.getPosition(); } @Override public void setPosition(float position) { - playbackMusic.setPosition(position); + music.setPosition(position); } @Override public void setOnCompletionListener(OnCompletionListener listener) { - playbackMusic.setOnCompletionListener(listener); + music.setOnCompletionListener(listener); } @Override public void setVolume(float percent) { - playbackMusic.setVolume(percent); + music.setVolume(percent); } @Override public boolean isFinishedLoading() { - if (lock.isHeldByCurrentThread()) { - return true; - } else { - try { - if (lock.tryLock(0, TimeUnit.SECONDS)) { - return true; - } else { - return false; - } - } catch (InterruptedException e) { - return false; - } - } + return true; } @Override public String getBasicSongName() { - return basicSongName; + return fileHandle.nameWithoutExtension(); } @Override public FileHandle getMusicFile() { - return file; + return fileHandle; + } + + @Override + public void seek(long position) { + long totalSkipped = 0; + long skipped = 0; + closeStream(); + reInitStream(); + while (totalSkipped < position) { + try { + skipped = ais.skip(position-totalSkipped); + } catch (IOException e) { + e.printStackTrace(); + } + + if (skipped == 0) break; + totalSkipped += skipped; + } } - @Override - public void dispose() { - Gdx.app.debug("MP3Manager", "Disposing..."); - playbackMusic.stop(); - playbackMusic.dispose(); - exec.shutdown(); + private void closeStream() { + if (ais != null) { + try { + ais.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private void reInitStream() { try { - bitstream.close(); - bitstream = null; - inputStream.close(); - } catch (BitstreamException | IOException e) { + AudioInputStream in = AudioSystem.getAudioInputStream(fileHandle.file()); + ais = AudioSystem.getAudioInputStream(af, in); + in.close(); + } catch (UnsupportedAudioFileException | IOException e) { e.printStackTrace(); } } + + @Override + public void synchronize() { + seek(MathUtils.round(((Integer) properties.get("audio.length.bytes")).intValue() * (getPositionInSeconds()/getDuration()))); + } } diff --git a/core/src/zero1hd/rhythmbullet/audio/MusicManager.java b/core/src/zero1hd/rhythmbullet/audio/MusicManager.java index 7ae2a4f..396b357 100755 --- a/core/src/zero1hd/rhythmbullet/audio/MusicManager.java +++ b/core/src/zero1hd/rhythmbullet/audio/MusicManager.java @@ -50,16 +50,6 @@ public interface MusicManager extends Disposable { */ public float getSampleRate(); - /** - * @return the bitrate of the audio. - */ - public long getBitrate(); - /** - * The current numerical value of the last window read - * @return - */ - public int getreadWindowIndex(); - public void pause(); public void play(); @@ -93,7 +83,13 @@ public interface MusicManager extends Disposable { public FileHandle getMusicFile(); /** - * Synchronizes reading to current playback. + * sets the reading position to the given position; + * @param position the byte to skip to. */ - public void setReadIndexToPlaybackIndex(); + public void seek(long position); + + /** + * Synchronizes the playback and read threads + */ + public void synchronize(); } diff --git a/core/src/zero1hd/rhythmbullet/audio/WAVManager.java b/core/src/zero1hd/rhythmbullet/audio/WAVManager.java index 2186d82..1613178 100755 --- a/core/src/zero1hd/rhythmbullet/audio/WAVManager.java +++ b/core/src/zero1hd/rhythmbullet/audio/WAVManager.java @@ -1,167 +1,177 @@ package zero1hd.rhythmbullet.audio; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.Channels; -import java.security.InvalidParameterException; import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.audio.Music.OnCompletionListener; import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.math.MathUtils; -import zero1hd.rhythmbullet.audio.wavedecoder.WavDecoder; +import zero1hd.rhythmbullet.audio.wavedecoder.AudioSampleReader; public class WAVManager implements MusicManager { private int readWindowSize = 1024; - private AudioFormat format; private int playbackIndex; - private int readWindowIndex; - private Music playbackMusic; - private WavDecoder decoder; - private FileHandle file; - private String basicSongName; - private RandomAccessFile raf; + private Music music; + private FileHandle fileHandle; + private AudioInputStream ais; + private AudioFormat af; + private AudioSampleReader d; - private int sampleSizeInBits; public WAVManager(FileHandle file) { - this.file = file; - basicSongName = file.name(); + this.fileHandle = file; try { - raf = new RandomAccessFile(file.file(), "r"); - decoder = new WavDecoder(new BufferedInputStream(Channels.newInputStream(raf.getChannel()))); - } catch (InvalidParameterException | IOException e) { - // TODO Auto-generated catch block + ais = AudioSystem.getAudioInputStream(file.file()); + d = new AudioSampleReader(ais); + af = ais.getFormat(); + } catch (UnsupportedAudioFileException | IOException e) { e.printStackTrace(); } - playbackMusic = Gdx.audio.newMusic(file); - format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, (float) decoder.getSampleRate(), decoder.getSampleSizeInBits(), decoder.getChannels(), decoder.getChannels()*2, (float) decoder.getSampleRate(), false); - sampleSizeInBits = decoder.getSampleSizeInBits(); + music = Gdx.audio.newMusic(file); } @Override - public void playbackIndexUpdate() { - playbackIndex = (int) (playbackMusic.getPosition() * decoder.getSampleRate() / readWindowSize); + public void dispose() { + music.dispose(); + try { + ais.close(); + } catch (IOException e) { + e.printStackTrace(); + } } - + + @Override + public void playbackIndexUpdate() { + playbackIndex = (int) ((getSampleRate() * getDuration())/getReadWindowSize()); + } + @Override public int getPlaybackIndexPosition() { return playbackIndex; } - + @Override public int getReadWindowSize() { return readWindowSize; } - - @Override - public void setReadIndexToPlaybackIndex() { - try { - long pointer = (long) (getPositionInSeconds()*(getBitrate()/8f)); - raf.seek(pointer); - readWindowIndex = playbackIndex; - } catch (IOException e) { - e.printStackTrace(); - } - } - + @Override public int readSamples(float[] samples) { - int samplesRead = 0; try { - samplesRead = decoder.readSamples(samples); + return d.readSamples(samples); } catch (IOException e) { e.printStackTrace(); } - - if (samplesRead != 0) { - readWindowIndex++; - } - return samplesRead; + return 0; } @Override public long getSampleCount() { - return decoder.getFrameLength(); - } - - @Override - public void dispose() { - playbackMusic.stop(); - playbackMusic.dispose(); - decoder.cleanAndClose(); + return ais.getFrameLength(); } @Override public float getDuration() { - return decoder.getDurationInSeconds(); + return ais.getFrameLength()/af.getFrameRate(); } @Override public float getSampleRate() { - return format.getSampleRate(); - } - - @Override - public int getreadWindowIndex() { - return readWindowIndex; + return af.getSampleRate(); } @Override public void pause() { - playbackMusic.pause(); + music.pause(); } @Override public void play() { - playbackMusic.play(); + music.play(); } @Override public boolean isPlaying() { - return playbackMusic.isPlaying(); + return music.isPlaying(); } @Override public float getPositionInSeconds() { - return playbackMusic.getPosition(); + return music.getPosition(); } @Override public void setPosition(float position) { - playbackMusic.setPosition(position); + music.setPosition(position); } @Override public void setOnCompletionListener(OnCompletionListener listener) { - playbackMusic.setOnCompletionListener(listener); + music.setOnCompletionListener(listener); } @Override public void setVolume(float percent) { - playbackMusic.setVolume(percent); + music.setVolume(percent); } @Override public boolean isFinishedLoading() { return true; } - + @Override public String getBasicSongName() { - return basicSongName; + return fileHandle.nameWithoutExtension(); } @Override public FileHandle getMusicFile() { - return file; + return fileHandle; } @Override - public long getBitrate() { - return (long) (getSampleRate()*sampleSizeInBits); + public void seek(long position) { + long totalSkipped = 0; + long skipped = 0; + + restartStream(); + while (totalSkipped < position) { + try { + skipped = ais.skip(position-totalSkipped); + } catch (IOException e) { + e.printStackTrace(); + } + + if (skipped == 0) break; + totalSkipped += skipped; + } + } + + private void restartStream() { + try { + ais.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + try { + AudioInputStream in = AudioSystem.getAudioInputStream(fileHandle.file()); + ais = AudioSystem.getAudioInputStream(af, in); + in.close(); + } catch (UnsupportedAudioFileException | IOException e) { + e.printStackTrace(); + } + } + + @Override + public void synchronize() { + seek(MathUtils.round(getPositionInSeconds()*getSampleRate())); } } diff --git a/core/src/zero1hd/rhythmbullet/audio/wavedecoder/WavDecoder.java b/core/src/zero1hd/rhythmbullet/audio/wavedecoder/AudioSampleReader.java similarity index 69% rename from core/src/zero1hd/rhythmbullet/audio/wavedecoder/WavDecoder.java rename to core/src/zero1hd/rhythmbullet/audio/wavedecoder/AudioSampleReader.java index f2e3fdf..1c35256 100755 --- a/core/src/zero1hd/rhythmbullet/audio/wavedecoder/WavDecoder.java +++ b/core/src/zero1hd/rhythmbullet/audio/wavedecoder/AudioSampleReader.java @@ -1,30 +1,23 @@ package zero1hd.rhythmbullet.audio.wavedecoder; -import java.io.BufferedInputStream; import java.io.IOException; import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.UnsupportedAudioFileException; -public class WavDecoder { +public class AudioSampleReader { private int channels; private double sampleRate; private byte[] buffer; private int sampleSize; private AudioInputStream audioInputStream; - public WavDecoder(BufferedInputStream inputStream) throws IOException { - try { - audioInputStream = AudioSystem.getAudioInputStream(inputStream); - buffer = new byte[audioInputStream.getFormat().getFrameSize()]; - - channels = audioInputStream.getFormat().getChannels(); - sampleRate = audioInputStream.getFormat().getSampleRate(); - sampleSize = audioInputStream.getFormat().getSampleSizeInBits(); - } catch (UnsupportedAudioFileException e) { - e.printStackTrace(); - } + public AudioSampleReader(AudioInputStream ais) throws IOException { + audioInputStream = ais; + buffer = new byte[audioInputStream.getFormat().getFrameSize()]; + + channels = audioInputStream.getFormat().getChannels(); + sampleRate = audioInputStream.getFormat().getSampleRate(); + sampleSize = audioInputStream.getFormat().getSampleSizeInBits(); } public int getChannels() { diff --git a/core/src/zero1hd/rhythmbullet/graphics/ui/components/Visualizer.java b/core/src/zero1hd/rhythmbullet/graphics/ui/components/Visualizer.java index 6625dac..305fc33 100755 --- a/core/src/zero1hd/rhythmbullet/graphics/ui/components/Visualizer.java +++ b/core/src/zero1hd/rhythmbullet/graphics/ui/components/Visualizer.java @@ -29,8 +29,7 @@ public class Visualizer extends Widget implements Disposable { if (mm != null && mm.isFinishedLoading() && !mmSet) { Gdx.app.debug("Visualizer", "\nsample count: " + mm.getSampleCount() + "\nDuration in seconds: " + mm.getDuration() + - "\nSample rate: " + mm.getSampleRate() - + "\nBit rate: " + mm.getBitrate()); + "\nSample rate: " + mm.getSampleRate()); vis.setMM(mm); mmSet = true; }