From 8f70d72ed1ea4538631a2c385305be6b05930e08 Mon Sep 17 00:00:00 2001 From: Recrown Date: Mon, 2 Jul 2018 00:52:55 -0500 Subject: [PATCH] mp3 audio processor complete; slow conversion to cleaner system --- .../audio/processor/AudioProcessor.java | 29 +- .../audio/processor/WAVAudioProcessor.java | 82 ++++++ .../desktop/audio/MP3AudioProcessor.java | 142 +++++++++ .../desktop/audio/Mp3Manager.java | 277 ------------------ .../rhythmbullet/desktop/audio/MusicList.java | 2 +- .../desktop/audio/WAVManager.java | 155 ---------- .../audio/visualizer/DesktopVisualizer.java | 1 - 7 files changed, 253 insertions(+), 435 deletions(-) create mode 100755 core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java create mode 100755 desktop/src/zero1hd/rhythmbullet/desktop/audio/MP3AudioProcessor.java delete mode 100755 desktop/src/zero1hd/rhythmbullet/desktop/audio/Mp3Manager.java delete mode 100755 desktop/src/zero1hd/rhythmbullet/desktop/audio/WAVManager.java diff --git a/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java b/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java index e79d35d..e291a02 100755 --- a/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java +++ b/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java @@ -1,4 +1,31 @@ package zero1hd.rhythmbullet.audio.processor; -public interface AudioProcessor { +import com.badlogic.gdx.utils.Disposable; + +public interface AudioProcessor extends Disposable { + /** + * Called once, contains the initiation to the stream, only called when play-back begins. + * Not thread safe as it should be the first thing to be called during read process. + */ + public void initiate(); + + /** + * @return number of channels + */ + public boolean isStereo(); + + /** + * @return sample rate + */ + public int getSampleRate(); + + /** + * Thread safe + * Reads samples (NOT FRAMES) with interwoven data for stereo. + * stored in 16 bit format (first 8 are the first byte of data while the second 8 are the second byte of data that composes a short value) + * @param pcm the array the samples should fill + * @param syncObj the object that this object should use to synchronize multiple threads. + * @return the amount of samples read. + */ + public int readSamples(short[] pcm, Object syncObj); } diff --git a/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java b/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java new file mode 100755 index 0000000..73a609e --- /dev/null +++ b/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java @@ -0,0 +1,82 @@ +package zero1hd.rhythmbullet.audio.processor; + +import java.io.IOException; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.UnsupportedAudioFileException; + +import com.badlogic.gdx.files.FileHandle; + +public class WAVAudioProcessor implements AudioProcessor { + private boolean stereo; + private int sampleRate; + private byte[] buffer; + private FileHandle fileHandle; + private AudioInputStream audioInputStream; + private boolean initiated; + + public WAVAudioProcessor(FileHandle fileHandle, int windowSize) throws IOException, UnsupportedAudioFileException { + this.fileHandle = fileHandle; + AudioFormat format = AudioSystem.getAudioFileFormat(fileHandle.file()).getFormat(); + stereo = format.getChannels() > 1 ? true : false; + sampleRate = (int) format.getSampleRate(); + } + + @Override + public void initiate() { + try { + audioInputStream = AudioSystem.getAudioInputStream(fileHandle.file()); + } catch (UnsupportedAudioFileException | IOException e) { + e.printStackTrace(); + } + buffer = new byte[audioInputStream.getFormat().getFrameSize()]; + initiated = true; + } + + public boolean isStereo() { + return stereo; + } + + public int getSampleRate() { + return sampleRate; + } + + @Override + public int readSamples(short[] pcm, Object syncObj) { + if (initiated) { + synchronized (syncObj) { + int framesRead = 0; + for (int sampleID = 0; sampleID < pcm.length; sampleID++) { + try { + if (audioInputStream.read(buffer) > 0) { + pcm[sampleID] = (short) ((buffer[1] << 8) + (buffer[0] & 0x00ff)); + if (stereo) { + short secondChan = (short) ((buffer[3] << 8) + (buffer[2] & 0x00ff)); + sampleID++; + pcm[sampleID] = secondChan; + } + framesRead++; + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + return framesRead; + } + } else { + throw new IllegalStateException("Stream has not been initialized."); + } + } + + @Override + public void dispose() { + try { + audioInputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/MP3AudioProcessor.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/MP3AudioProcessor.java new file mode 100755 index 0000000..881dd51 --- /dev/null +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/MP3AudioProcessor.java @@ -0,0 +1,142 @@ +package zero1hd.rhythmbullet.desktop.audio; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.files.FileHandle; +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.processor.AudioProcessor; + + +public class MP3AudioProcessor implements AudioProcessor { + private boolean stereo; + private int sampleRate; + private FileHandle fileHandle; + private byte[] currentByteSet; + private byte[] workset; + Bitstream bitstream; + MP3Decoder decoder; + OutputBuffer sampleBuffer; + private int indexHead = -1; + + + public MP3AudioProcessor(FileHandle fileHandle) { + this.fileHandle = fileHandle; + + bitstream = new Bitstream(fileHandle.read()); + + try { + Header header = bitstream.readFrame(); + if (header == null) throw new GdxRuntimeException("Empty MP3"); + stereo = header.mode() == Header.DUAL_CHANNEL; + sampleRate = header.getSampleRate(); + } catch (BitstreamException e) { + throw new GdxRuntimeException("error while preloading mp3", e); + } + + try { + bitstream.close(); + } catch (BitstreamException e) { + e.printStackTrace(); + } + } + + @Override + public void initiate() { + bitstream = new Bitstream(fileHandle.read()); + decoder = new MP3Decoder(); + sampleBuffer = new OutputBuffer(stereo ? 2 : 1, false); + decoder.setOutputBuffer(sampleBuffer); + workset = new byte[(stereo ? 2 : 1)*2]; + } + + @Override + public boolean isStereo() { + return stereo; + } + + + @Override + public int getSampleRate() { + return sampleRate; + } + + @Override + public int readSamples(short[] pcm, Object syncObj) { + int framesRead = 0; + for (int sid = 0; sid < pcm.length; sid++) { + for (int wsid = 0; wsid < workset.length; wsid++) { + workset[wsid] = nextByte(); + } + if (currentByteSet != null) { + pcm[sid] += (workset[1] << 8) + (workset[0] & 0x00ff); + if (stereo) { + short altChan = (short) ((workset[3] << 8) + (workset[2] & 0x00ff)); + sid++; + pcm[sid] = altChan > pcm[sid] ? altChan : pcm[sid]; + } + framesRead ++; + pcm[sid] /= Short.MAX_VALUE+1; + } + } + 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 ae) { + Gdx.app.debug("Mp3Manager", "Last buffer reached since array was out of bounds."); + } catch (DecoderException de) { + Gdx.app.error("MP3 Decoder Error", de.toString()); + } + bitstream.closeFrame(); + bytesRead = sampleBuffer.reset(); + currentByteSet = sampleBuffer.getBuffer(); + } else { + currentByteSet = null; + } + } catch (BitstreamException be) { + be.printStackTrace(); + } + return bytesRead; + } else { + return 0; + } + } + + @Override + public void dispose() { + Gdx.app.debug("MP3Manager", "Disposing..."); + try { + bitstream.close(); + bitstream = null; + } catch (BitstreamException e) { + e.printStackTrace(); + } + } +} diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/Mp3Manager.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/Mp3Manager.java deleted file mode 100755 index 485ecb6..0000000 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/Mp3Manager.java +++ /dev/null @@ -1,277 +0,0 @@ -package zero1hd.rhythmbullet.desktop.audio; - -import java.io.IOException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; - -import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; -import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; -import org.jaudiotagger.audio.mp3.MP3File; -import org.jaudiotagger.tag.TagException; - -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.MusicManager; - - -public class Mp3Manager implements MusicManager { - private int readWindowSize = 1024; - - private Music playbackMusic; - private int playbackIndex, readIndex; - - private int sampleRate; - private long sampleCount; - private double durationInSeconds; - private int channels; - Bitstream bitstream; - MP3Decoder decoder; - OutputBuffer sampleBuffer; - private byte[] currentByteSet; - private byte[] workset; - private int indexHead = -1; - - 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(); - try { - MP3File mp3File = new MP3File(audioFile.file()); - sampleRate = mp3File.getMP3AudioHeader().getSampleRateAsNumber(); - durationInSeconds = mp3File.getMP3AudioHeader().getPreciseTrackLength(); - channels = mp3File.getAudioHeader().getChannels().equals("Mono") ? 1:2; - sampleCount = MathUtils.round(Float.valueOf((float) (sampleRate*durationInSeconds))) * channels; - - } catch (IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { - e.printStackTrace(); - } - lock.unlock(); - }); - - bitstream = new Bitstream(audioFile.read()); - decoder = new MP3Decoder(); - - 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 int playbackIndexUpdate() { - playbackIndex = (int) (playbackMusic.getPosition() * sampleRate / readWindowSize); - return playbackIndex; - } - - @Override - public int getPlaybackIndexPosition() { - return playbackIndex; - } - - @Override - public int getReadWindowSize() { - return readWindowSize; - } - - @Override - public long getSampleCount() { - return sampleCount; - } - - @Override - public float getDuration() { - return Float.valueOf(String.valueOf(durationInSeconds)); - } - - @Override - public int readSampleFrames(float[] samples) { - readIndex++; - 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; - } - } - 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 ae) { - Gdx.app.debug("Mp3Manager", "Last buffer reached since array was out of bounds."); - } catch (DecoderException de) { - Gdx.app.error("MP3 Decoder Error", de.toString()); - } - bitstream.closeFrame(); - bytesRead = sampleBuffer.reset(); - currentByteSet = sampleBuffer.getBuffer(); - } else { - currentByteSet = null; - } - } catch (BitstreamException be) { - be.printStackTrace(); - } - return bytesRead; - } else { - return 0; - } - } - - @Override - public float getSampleRate() { - return sampleRate; - } - - @Override - public void pause() { - playbackMusic.pause(); - } - - @Override - public void play() { - playbackMusic.play(); - } - - @Override - public boolean isPlaying() { - return playbackMusic.isPlaying(); - } - - @Override - public float getPositionInSeconds() { - return playbackMusic.getPosition(); - } - - @Override - public void setPosition(float position) { - playbackMusic.setPosition(position); - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener) { - playbackMusic.setOnCompletionListener(listener); - } - - @Override - public void setVolume(float percent) { - playbackMusic.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; - } - } - } - - @Override - public String getBasicSongName() { - return basicSongName; - } - - @Override - public FileHandle getMusicFile() { - return file; - } - - @Override - public void dispose() { - Gdx.app.debug("MP3Manager", "Disposing..."); - playbackMusic.stop(); - playbackMusic.dispose(); - exec.shutdown(); - try { - bitstream.close(); - bitstream = null; - } catch (BitstreamException e) { - e.printStackTrace(); - } - } - - @Override - public int framesRead() { - return readIndex; - } - - @Override - public int getChannelCount() { - return channels; - } - - @Override - public Music getMusic() { - return playbackMusic; - } -} diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/MusicList.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/MusicList.java index f09a679..187e0cd 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/MusicList.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/MusicList.java @@ -98,7 +98,7 @@ public class MusicList extends Observable { if (file.extension().equalsIgnoreCase("wav")) { return new WAVManager(file); } else if (file.extension().equalsIgnoreCase("mp3")) { - return new Mp3Manager(file); + return new MP3AudioProcessor(file); } return null; } diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/WAVManager.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/WAVManager.java deleted file mode 100755 index 0242c46..0000000 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/WAVManager.java +++ /dev/null @@ -1,155 +0,0 @@ -package zero1hd.rhythmbullet.desktop.audio; - -import java.io.IOException; - -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 zero1hd.rhythmbullet.audio.MusicManager; -import zero1hd.rhythmbullet.audio.wavedecoder.WAVSampleReader; - -public class WAVManager implements MusicManager { - private int readWindowSize = 1024; - private int playbackIndex, readIndex; - private Music music; - private FileHandle fileHandle; - private AudioInputStream ais; - private AudioFormat af; - private WAVSampleReader d; - - public WAVManager(FileHandle file) { - this.fileHandle = file; - try { - ais = AudioSystem.getAudioInputStream(file.file()); - d = new WAVSampleReader(ais); - af = ais.getFormat(); - } catch (UnsupportedAudioFileException | IOException e) { - e.printStackTrace(); - } - music = Gdx.audio.newMusic(file); - } - - @Override - public void dispose() { - music.dispose(); - try { - ais.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public int playbackIndexUpdate() { - playbackIndex = (int) ((getSampleRate() * getDuration())/getReadWindowSize()); - return playbackIndex; - } - - @Override - public int getPlaybackIndexPosition() { - return playbackIndex; - } - - @Override - public int getReadWindowSize() { - return readWindowSize; - } - - @Override - public int readSampleFrames(float[] samples) { - try { - readIndex++; - return d.readSamplesAsFrames(samples); - } catch (IOException e) { - e.printStackTrace(); - } - return 0; - } - - @Override - public long getSampleCount() { - return ais.getFrameLength()*d.getChannels(); - } - - @Override - public float getDuration() { - return ais.getFrameLength()/af.getFrameRate(); - } - - @Override - public float getSampleRate() { - return af.getSampleRate(); - } - - @Override - public void pause() { - music.pause(); - } - - @Override - public void play() { - music.play(); - } - - @Override - public boolean isPlaying() { - return music.isPlaying(); - } - - @Override - public float getPositionInSeconds() { - return music.getPosition(); - } - - @Override - public void setPosition(float position) { - music.setPosition(position); - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener) { - music.setOnCompletionListener(listener); - } - - @Override - public void setVolume(float percent) { - music.setVolume(percent); - } - - @Override - public boolean isFinishedLoading() { - return true; - } - - @Override - public String getBasicSongName() { - return fileHandle.nameWithoutExtension(); - } - - @Override - public FileHandle getMusicFile() { - return fileHandle; - } - - @Override - public int framesRead() { - return readIndex; - } - - @Override - public int getChannelCount() { - return d.getChannels(); - } - - @Override - public Music getMusic() { - return music; - } -} diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java index 169850d..5264ec0 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java @@ -14,7 +14,6 @@ import com.badlogic.gdx.utils.reflect.ClassReflection; import com.badlogic.gdx.utils.reflect.Field; import com.badlogic.gdx.utils.reflect.ReflectionException; -import zero1hd.rhythmbullet.audio.MusicManager; import zero1hd.rhythmbullet.audio.visualizer.MusicManagerFFT; import zero1hd.rhythmbullet.audio.visualizer.Visualizer;