From 7dff9188020399b644215e2244400e6cd25ded74 Mon Sep 17 00:00:00 2001 From: Recrown Date: Mon, 23 Jul 2018 00:15:31 -0500 Subject: [PATCH] Finished basic visualizer and mass refactoring on the audio analyzer to be compatible with the structure --- .../audio/MinimalAudioHeader.java | 29 +- .../rhythmbullet/audio/MusicController.java | 2 +- .../zero1hd/rhythmbullet/audio/MusicList.java | 16 +- .../audio/MusicMetadataController.java | 12 +- .../rhythmbullet/audio/SupportedFormats.java | 5 + .../audio/analyzer/AudioAnalyzer.java | 309 ++++++++++++++++-- .../audio/analyzer/PeakDetectionRunnable.java | 125 ------- .../audio/analyzer/PruneFluxRunnable.java | 96 ------ .../SpectralFluxAnalysisRunnable.java | 141 -------- .../audio/analyzer/ThresholdCalcRunnable.java | 102 ------ .../audio/processor/AudioProcessor.java | 33 +- .../audio/processor/WAVAudioProcessor.java | 88 +++-- .../audio/visualizer/BasicFFT.java | 15 + .../audio/visualizer/MirrorVisualizer.java | 75 ----- .../audio/visualizer/MusicManagerFFT.java | 51 --- .../audio/visualizer/Visualizer.java | 21 -- .../DesktopAudioProcessorFactory.java | 3 +- .../audio/processor/MP3AudioProcessor.java | 64 ++-- .../DoubleHorizontalVisualizer.java | 112 +++++++ ...DesktopVisualizer.java => PCMMachine.java} | 63 +++- .../HorizontalVisualizerWidget.java | 4 +- .../desktop/screens/GameScreen.java | 4 +- 22 files changed, 623 insertions(+), 747 deletions(-) create mode 100755 core/src/zero1hd/rhythmbullet/audio/SupportedFormats.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/analyzer/PeakDetectionRunnable.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/analyzer/PruneFluxRunnable.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/analyzer/SpectralFluxAnalysisRunnable.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/analyzer/ThresholdCalcRunnable.java create mode 100755 core/src/zero1hd/rhythmbullet/audio/visualizer/BasicFFT.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/visualizer/MirrorVisualizer.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/visualizer/MusicManagerFFT.java delete mode 100755 core/src/zero1hd/rhythmbullet/audio/visualizer/Visualizer.java create mode 100755 desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DoubleHorizontalVisualizer.java rename desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/{DesktopVisualizer.java => PCMMachine.java} (78%) diff --git a/core/src/zero1hd/rhythmbullet/audio/MinimalAudioHeader.java b/core/src/zero1hd/rhythmbullet/audio/MinimalAudioHeader.java index 48fda47..33efdca 100755 --- a/core/src/zero1hd/rhythmbullet/audio/MinimalAudioHeader.java +++ b/core/src/zero1hd/rhythmbullet/audio/MinimalAudioHeader.java @@ -8,25 +8,31 @@ import org.jaudiotagger.audio.AudioHeader; import org.jaudiotagger.audio.exceptions.CannotReadException; import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; +import org.jaudiotagger.audio.mp3.MP3AudioHeader; +import org.jaudiotagger.audio.mp3.MP3File; import org.jaudiotagger.tag.TagException; import com.badlogic.gdx.files.FileHandle; public class MinimalAudioHeader { private int sampleRate, channelCount; - + private SupportedFormats format; + private FileHandle musicFile; public MinimalAudioHeader(FileHandle musicFile) { + this.musicFile = musicFile; + format = SupportedFormats.valueOf(musicFile.extension().toUpperCase()); try { AudioFile file = AudioFileIO.read(musicFile.file()); AudioHeader header = file.getAudioHeader(); sampleRate = header.getSampleRateAsNumber(); channelCount = (header.getChannels().equals("Mono") ? 1 : 2); - } catch (CannotReadException | IOException | TagException | ReadOnlyFileException - | InvalidAudioFrameException e) { + } catch (CannotReadException | IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { e.printStackTrace(); } } + + public int getSampleRate() { return sampleRate; } @@ -34,4 +40,21 @@ public class MinimalAudioHeader { public int getChannelCount() { return channelCount; } + + + public long estimateSampleFrames() { + switch (format) { + case MP3: + try { + MP3File file = (MP3File) AudioFileIO.read(musicFile.file()); + MP3AudioHeader header = file.getMP3AudioHeader(); + return header.getNumberOfFrames(); + } catch (CannotReadException | IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) { + e.printStackTrace(); + } + return -1; + default: + return -1; + } + } } diff --git a/core/src/zero1hd/rhythmbullet/audio/MusicController.java b/core/src/zero1hd/rhythmbullet/audio/MusicController.java index 0dc885c..3582675 100755 --- a/core/src/zero1hd/rhythmbullet/audio/MusicController.java +++ b/core/src/zero1hd/rhythmbullet/audio/MusicController.java @@ -177,7 +177,7 @@ public class MusicController extends Observable implements OnCompletionListener, if (musicHeader != null) { return musicHeader; } else { - return musicList.newMinimalAudioHeader(getCurrentMusicFileHandle()); + return musicHeader = musicList.newMinimalAudioHeader(getCurrentMusicFileHandle()); } } diff --git a/core/src/zero1hd/rhythmbullet/audio/MusicList.java b/core/src/zero1hd/rhythmbullet/audio/MusicList.java index 60c6f09..7d8b8f7 100755 --- a/core/src/zero1hd/rhythmbullet/audio/MusicList.java +++ b/core/src/zero1hd/rhythmbullet/audio/MusicList.java @@ -41,13 +41,16 @@ public class MusicList extends Observable { /** * @param file - * @return a {@link #zero1hd.rhythmbullet.audio.processor.AudioProcessor()} of the given music file. Will return null if theres a format error. + * @return a {@link AudioProcessor} of the given music file. Will return null if theres a format error. */ public AudioProcessor newAudioProcessor(FileHandle file) { - if (file.extension().equalsIgnoreCase("wav")) { - return new WAVAudioProcessor(file); - } else if (file.extension().equalsIgnoreCase("mp3")) { + switch (SupportedFormats.valueOf(file.extension().toUpperCase())) { + case MP3: return audioProcFactory.newMP3AudioProcessor(file); + case WAV: + return new WAVAudioProcessor(file); + default: + break; } return null; } @@ -124,8 +127,11 @@ public class MusicList extends Observable { if (files[i].isDirectory()) { musicFiles.addAll(recursiveMusicSearch(files[i])); } else { - if (files[i].extension().equalsIgnoreCase("wav") || files[i].extension().equalsIgnoreCase("mp3")) { + try { + SupportedFormats.valueOf(files[i].extension().toUpperCase()); musicFiles.add(files[i]); + } catch (IllegalArgumentException e) { + Gdx.app.log("MusicList", "Unsupported file format: " + files[i].name()); } } } diff --git a/core/src/zero1hd/rhythmbullet/audio/MusicMetadataController.java b/core/src/zero1hd/rhythmbullet/audio/MusicMetadataController.java index 42b64d0..c89b654 100755 --- a/core/src/zero1hd/rhythmbullet/audio/MusicMetadataController.java +++ b/core/src/zero1hd/rhythmbullet/audio/MusicMetadataController.java @@ -77,10 +77,16 @@ public class MusicMetadataController implements Disposable, Observer { for (int i = 0; i < musicList.getTotal(); i++) { FileHandle musicFile = musicList.getMusicArray().get(i); synchronized (this) { - if (musicFile.extension().equalsIgnoreCase("wav")) { - metadataArray.add(new WAVMetadata(musicFile)); - } else if (musicFile.extension().equalsIgnoreCase("mp3")) { + switch (SupportedFormats.valueOf(musicFile.extension().toUpperCase())) { + case MP3: metadataArray.add(new MP3Metadata(musicFile)); + break; + case WAV: + metadataArray.add(new WAVMetadata(musicFile)); + break; + default: + break; + } } } diff --git a/core/src/zero1hd/rhythmbullet/audio/SupportedFormats.java b/core/src/zero1hd/rhythmbullet/audio/SupportedFormats.java new file mode 100755 index 0000000..a08125d --- /dev/null +++ b/core/src/zero1hd/rhythmbullet/audio/SupportedFormats.java @@ -0,0 +1,5 @@ +package zero1hd.rhythmbullet.audio; + +public enum SupportedFormats { + WAV, MP3; +} diff --git a/core/src/zero1hd/rhythmbullet/audio/analyzer/AudioAnalyzer.java b/core/src/zero1hd/rhythmbullet/audio/analyzer/AudioAnalyzer.java index 1a67bcc..c7a99d8 100755 --- a/core/src/zero1hd/rhythmbullet/audio/analyzer/AudioAnalyzer.java +++ b/core/src/zero1hd/rhythmbullet/audio/analyzer/AudioAnalyzer.java @@ -1,53 +1,296 @@ package zero1hd.rhythmbullet.audio.analyzer; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.FloatArray; -import zero1hd.rhythmbullet.audio.MusicManager; +import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D; +import zero1hd.rhythmbullet.audio.processor.AudioProcessor; -public class AudioAnalyzer implements Disposable { - private ExecutorService exec; +public class AudioAnalyzer implements Runnable, Disposable { + private Thread thread; + private String threadName = "Audio-Analyzer"; - private SpectralFluxAnalysisRunnable sfar; - private ThresholdCalcRunnable tcr; - private PruneFluxRunnable pfr; - private PeakDetectionRunnable pdr; + private volatile boolean work = true; + private int windowSize = 1024; - public AudioAnalyzer(MusicManager musicManager) { - exec = Executors.newSingleThreadExecutor(); - sfar = new SpectralFluxAnalysisRunnable(musicManager); - tcr = new ThresholdCalcRunnable(sfar); - pfr = new PruneFluxRunnable(tcr); - pdr = new PeakDetectionRunnable(pfr, sfar.getPUID()); - + private FloatArray bassSpectralFlux = new FloatArray(); + private FloatArray mSpectralFlux = new FloatArray(); + private FloatArray umSpectralFlux = new FloatArray(); + + private FloatArray bassThreshold = new FloatArray(); + private FloatArray mThreshold = new FloatArray(); + private FloatArray umThreshold = new FloatArray(); + + private FloatArray bassPrunned = new FloatArray(); + private FloatArray mPrunned = new FloatArray(); + private FloatArray umPrunned = new FloatArray(); + + private FloatArray bassPeaks = new FloatArray(); + private FloatArray mPeaks = new FloatArray(); + private FloatArray umPeaks = new FloatArray(); + + private float bassMaxValue, mMaxValue, umMaxValue, secondsPerWindow, mAvg, bassAvg, umAvg; + + AudioProcessor processor; + + private int PUID; + private int progress; + + public AudioAnalyzer(AudioProcessor audioProcessor) { + this.processor = audioProcessor; } public void start() { - exec.submit(sfar); - exec.submit(tcr); - exec.submit(pfr); - exec.submit(pdr); + if (thread == null || !thread.isAlive()) { + work = true; + thread = new Thread(this, threadName); + thread.start(); + } } - private void stop() { - sfar.work = false; - tcr.work = false; - pfr.work = false; - pdr.work = false; + @Override + public void run() { + spectralFluxAnalysis(); + thresholdCalculation(); + pruneFluxValues(); + peakDetection(); + } + + private void spectralFluxAnalysis() { + progress = 0; + int tasksDone = 0; + long totalTasks = MathUtils.round((float)processor.getSampleFrames()/windowSize); + + float[] audioPCM = new float[windowSize]; + float[] spectrum = new float[(windowSize/2)+1]; + float[] lastSpectrum = new float[(windowSize/2)+1]; + + int bassBinBegin = 1; + int bassBinEnd = 11; + + int mBinBegin = 50; + int mBinEnd = 250; + + int umBinBegin = 350; + int umBinEnd = 513; + + Gdx.app.debug("Read freq", String.valueOf(processor.getSampleRate())); + Gdx.app.debug("Using following bin ranges", "\nBass freq begin: " + bassBinBegin + "\nBass freq end: " + bassBinEnd + "\nMain freq begin: " + umBinBegin + "\nMain freq end: " + umBinEnd); + + Gdx.app.debug("Total tasks", String.valueOf(totalTasks)); + + FloatFFT_1D fft = new FloatFFT_1D(windowSize); + int seedDigit = 0; + + while (processor.readFrames(audioPCM) > 0 && work) { + + fft.realForward(audioPCM); + + //Building a PUID (Pseudo unique ID) + if (tasksDone == (seedDigit*totalTasks/9)) { + float avg = 0; + for (int frame = 0; frame < spectrum.length; frame++) { + avg += spectrum[frame]; + } + avg /= spectrum.length; + if (avg < 0) { + avg *= -1f; + } + PUID +=(int) Math.pow(10, 9-seedDigit) * ((int)(avg*1000f)-(int)(avg*100f)*10); + seedDigit ++; + } + + System.arraycopy(spectrum, 0, lastSpectrum, 0, spectrum.length); + System.arraycopy(audioPCM, 0, spectrum, 0, spectrum.length); + + float fluxVal; + //bass detection + fluxVal = 0; + for (int i = bassBinBegin; i < bassBinEnd; i++) { + fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0 + ? 0 : (spectrum[i] - lastSpectrum[i]); + } + bassSpectralFlux.add(fluxVal); + + //m detection + fluxVal = 0; + for (int i = mBinBegin; i < mBinEnd; i++) { + fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0 + ? 0 : (spectrum[i] - lastSpectrum[i]); + } + mSpectralFlux.add(fluxVal); + + //um detection + fluxVal = 0; + for (int i = umBinBegin; i < umBinEnd; i++) { + fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0 + ? 0 : (spectrum[i] - lastSpectrum[i]); + } + umSpectralFlux.add(fluxVal); + tasksDone++; + progress = (int) (100f*tasksDone/totalTasks); + } + + if (work) { + Gdx.app.debug("Audio Analyzer", "Done getting spectral flux."); + Gdx.app.debug("Audio Analyzer", "window count: " + bassSpectralFlux.size); + Gdx.app.debug("Audio Analyzer", "USING SEED: " + PUID); + progress = 100; + } + } + + private void thresholdCalculation() { + Gdx.app.debug("Audio Analyzer", "beginning threshold calc."); + + float bassThresholdMultiplier = 1.5f; + float mThresholdMultiplier = 1.4f; + float umThresholdMultiplier = 1.4f; + + int bassThresholdCalcRange = (int) (0.27/(windowSize/processor.getSampleRate())); + int mThresholdCalcRange = (int) (0.4/(windowSize/processor.getSampleRate())); + int umThresholdCalcRange = (int) (0.4/(windowSize/processor.getSampleRate())); + //threshold calculation + for (int i = 0; i < umSpectralFlux.size && work; i++) { + int start = Math.max(0, i - bassThresholdCalcRange/2); + int end = Math.min(umSpectralFlux.size - 1, i + bassThresholdCalcRange/2); + float average = 0; + for (int j = start; j <= end; j++) { + average += bassSpectralFlux.get(j); + } + average /= (end - start); + bassThreshold.add(average * bassThresholdMultiplier); + + start = Math.max(0, i - mThresholdCalcRange/2); + end = Math.min(umSpectralFlux.size - 1, i + mThresholdCalcRange/2); + average = 0; + for (int j = start; j <= end; j++) { + average+= mSpectralFlux.get(j); + } + average /= (end - start); + mThreshold.add(average*mThresholdMultiplier); + + start = Math.max(0, i - umThresholdCalcRange/2); + end = Math.min(umSpectralFlux.size - 1, i + umThresholdCalcRange/2); + average = 0; + for (int j = start; j <= end; j++) { + average+= umSpectralFlux.get(j); + } + average /= (end - start); + umThreshold.add(average*umThresholdMultiplier); + } + Gdx.app.debug("Audio Analyzer", "Threshold calculated."); + } + + private void pruneFluxValues() { + //pruning data + float prunnedCurrentVal; + + for (int i = 0; i < umSpectralFlux.size && work; i++) { + prunnedCurrentVal = bassSpectralFlux.get(i) - bassThreshold.get(i); + if (prunnedCurrentVal >= 0) { + bassPrunned.add(prunnedCurrentVal); + } else { + bassPrunned.add(0); + } + + prunnedCurrentVal = mSpectralFlux.get(i) - mThreshold.get(i); + if (prunnedCurrentVal >= 0 ) { + mPrunned.add(prunnedCurrentVal); + } else { + mPrunned.add(0); + } + + prunnedCurrentVal = umSpectralFlux.get(i) - umThreshold.get(i); + if (prunnedCurrentVal >= 0 ) { + umPrunned.add(prunnedCurrentVal); + } else { + umPrunned.add(0); + } + } + Gdx.app.debug("Audio Analyzer", "Data prunned."); + } + + private void peakDetection() { + int lastBeatID = 0; + float bassBeats = 0; + float mBeats = 0; + float umBeats = 0; + float avgSPB = -1f; + + for (int i = 0; i < umPrunned.size-1 && work; i++) { + bassPeaks.add((bassPrunned.get(i) > bassPrunned.get(i+1) ? bassPrunned.get(i) : 0f)); + if (bassPeaks.get(i) > bassMaxValue) { + bassMaxValue = bassPeaks.get(i); + } + + mPeaks.add((mPrunned.get(i) > mPrunned.get(i+1) ? mPrunned.get(i) : 0f)); + if (mPeaks.get(i) > mMaxValue) { + mMaxValue = mPeaks.get(i); + } + + umPeaks.add((umPrunned.get(i) > umPrunned.get(i+1) ? umPrunned.get(i) : 0f)); + if (umPeaks.get(i) > umMaxValue) { + umMaxValue = umPeaks.get(i); + } + + + if (avgSPB != -1) { + if (bassPeaks.get(i) == 0) { + avgSPB ++; + } else { + lastBeatID = i; + } + } else if (bassPeaks.get(i) != 0) { + avgSPB = 0; + } + + if (bassPeaks.get(i) != 0) { + bassAvg += bassPeaks.get(i); + bassBeats++; + } + + if (mPeaks.get(i) != 0) { + mAvg += mPeaks.get(i); + mBeats++; + } + + if (umPeaks.get(i) != 0) { + umAvg += umPeaks.get(i); + umBeats++; + } + } + + secondsPerWindow = windowSize/processor.getSampleRate(); + + //then we minus one from the beats so it actually works out + avgSPB -= bassPrunned.size-lastBeatID; + avgSPB *= secondsPerWindow; + avgSPB /= bassBeats; + Gdx.app.debug("Audio Analyzer", "Avg SPB: " + avgSPB); + + bassAvg /= bassBeats; + mAvg /= mBeats; + umAvg /= umBeats; + Gdx.app.debug("Audio Analyzer", "Avg bass: " + bassAvg); + Gdx.app.debug("Audio Analyzer", "Avg M: " + mAvg); + Gdx.app.debug("Audio Analyzer", "Avg UM: " + umAvg); + + } + + public int getProgress() { + return progress; } @Override public void dispose() { - stop(); - exec.shutdown(); + if (thread != null) { + work = false; + } } - public boolean isDone() { - if ((sfar.isDone() && tcr.isDone() && pfr.isDone() && pdr.isDone())) { - return true; - } - return false; + public void stop() { + work = false; } } diff --git a/core/src/zero1hd/rhythmbullet/audio/analyzer/PeakDetectionRunnable.java b/core/src/zero1hd/rhythmbullet/audio/analyzer/PeakDetectionRunnable.java deleted file mode 100755 index ffee2d3..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/analyzer/PeakDetectionRunnable.java +++ /dev/null @@ -1,125 +0,0 @@ -package zero1hd.rhythmbullet.audio.analyzer; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.utils.FloatArray; - -import zero1hd.rhythmbullet.audio.AudioDataPackage; -import zero1hd.rhythmbullet.audio.MusicManager; - -public class PeakDetectionRunnable implements Runnable { - boolean work = true, done; - private FloatArray bassPrunned; - private FloatArray mPrunned; - private FloatArray umPrunned; - private FloatArray bassPeaks = new FloatArray(); - private FloatArray mPeaks = new FloatArray(); - private FloatArray umPeaks = new FloatArray(); - private float bassMaxValue; - private float mMaxValue; - private float umMaxValue; - private float secondsPerWindow; - private float mAvg; - private float bassAvg; - private float umAvg; - private MusicManager musicManager; - private AudioDataPackage pack; - private int PUID; - - public PeakDetectionRunnable(PruneFluxRunnable pfr, int PUID) { - bassPrunned = pfr.getBassPrunned(); - mPrunned = pfr.getmPrunned(); - umPrunned = pfr.getUmPrunned(); - musicManager = pfr.getMusicManager(); - this.PUID = PUID; - } - - @Override - public void run() { - int lastBeatID = 0; - float bassBeats = 0; - float mBeats = 0; - float umBeats = 0; - float avgSPB = -1f; - - for (int i = 0; i < umPrunned.size-1 && work; i++) { - bassPeaks.add((bassPrunned.get(i) > bassPrunned.get(i+1) ? bassPrunned.get(i) : 0f)); - if (bassPeaks.get(i) > bassMaxValue) { - bassMaxValue = bassPeaks.get(i); - } - - mPeaks.add((mPrunned.get(i) > mPrunned.get(i+1) ? mPrunned.get(i) : 0f)); - if (mPeaks.get(i) > mMaxValue) { - mMaxValue = mPeaks.get(i); - } - - umPeaks.add((umPrunned.get(i) > umPrunned.get(i+1) ? umPrunned.get(i) : 0f)); - if (umPeaks.get(i) > umMaxValue) { - umMaxValue = umPeaks.get(i); - } - - - if (avgSPB != -1) { - if (bassPeaks.get(i) == 0) { - avgSPB ++; - } else { - lastBeatID = i; - } - } else if (bassPeaks.get(i) != 0) { - avgSPB = 0; - } - - if (bassPeaks.get(i) != 0) { - bassAvg += bassPeaks.get(i); - bassBeats++; - } - - if (mPeaks.get(i) != 0) { - mAvg += mPeaks.get(i); - mBeats++; - } - - if (umPeaks.get(i) != 0) { - umAvg += umPeaks.get(i); - umBeats++; - } - } - - secondsPerWindow = musicManager.getReadWindowSize()/musicManager.getSampleRate(); - - //then we minus one from the beats so it actually works out - avgSPB -= bassPrunned.size-lastBeatID; - avgSPB *= secondsPerWindow; - avgSPB /= bassBeats; - Gdx.app.debug("Audio Analyzer", "Avg SPB: " + avgSPB); - - bassAvg /= bassBeats; - mAvg /= mBeats; - umAvg /= umBeats; - Gdx.app.debug("Audio Analyzer", "Avg bass: " + bassAvg); - Gdx.app.debug("Audio Analyzer", "Avg M: " + mAvg); - Gdx.app.debug("Audio Analyzer", "Avg UM: " + umAvg); - - pack = new AudioDataPackage(); - pack.setBassData(bassPeaks, bassMaxValue, bassAvg); - pack.setmData(mPeaks, mMaxValue, mAvg); - pack.setUmData(umPeaks, umMaxValue, umAvg); - - pack.setPUID(PUID); - pack.setAvgSPB(avgSPB); - pack.setSecPerWin(secondsPerWindow); - pack.setMusicInfo(musicManager); - if (work) { - Gdx.app.debug("Audio Analyzer", "Peak detection complete. Data ready for map gen."); - } - - done = true; - } - - public AudioDataPackage getPack() { - return pack; - } - - public boolean isDone() { - return done; - } -} diff --git a/core/src/zero1hd/rhythmbullet/audio/analyzer/PruneFluxRunnable.java b/core/src/zero1hd/rhythmbullet/audio/analyzer/PruneFluxRunnable.java deleted file mode 100755 index b06ade9..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/analyzer/PruneFluxRunnable.java +++ /dev/null @@ -1,96 +0,0 @@ -package zero1hd.rhythmbullet.audio.analyzer; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.utils.FloatArray; - -import zero1hd.rhythmbullet.audio.MusicManager; - -public class PruneFluxRunnable implements Runnable { - boolean work = true; - private boolean done; - private FloatArray bassSpectralFlux; - private FloatArray mSpectralFlux; - private FloatArray umSpectralFlux; - private FloatArray bassThreshold, mThreshold, umThreshold; - private FloatArray bassPrunned = new FloatArray(); - private FloatArray mPrunned = new FloatArray(); - private FloatArray umPrunned = new FloatArray(); - private MusicManager musicManager; - public PruneFluxRunnable(ThresholdCalcRunnable tcr) { - bassSpectralFlux = tcr.getBassSpectralFlux(); - mSpectralFlux = tcr.getmSpectralFlux(); - umSpectralFlux = tcr.getUmSpectralFlux(); - - bassThreshold = tcr.getBassThreshold(); - umThreshold = tcr.getUmThreshold(); - mThreshold = tcr.getmThreshold(); - - this.musicManager = tcr.getMusicManager(); - } - - @Override - public void run() { - //pruning data - float prunnedCurrentVal; - - for (int i = 0; i < umSpectralFlux.size && work; i++) { - prunnedCurrentVal = bassSpectralFlux.get(i) - bassThreshold.get(i); - if (prunnedCurrentVal >= 0) { - bassPrunned.add(prunnedCurrentVal); - } else { - bassPrunned.add(0); - } - - prunnedCurrentVal = mSpectralFlux.get(i) - mThreshold.get(i); - if (prunnedCurrentVal >= 0 ) { - mPrunned.add(prunnedCurrentVal); - } else { - mPrunned.add(0); - } - - prunnedCurrentVal = umSpectralFlux.get(i) - umThreshold.get(i); - if (prunnedCurrentVal >= 0 ) { - umPrunned.add(prunnedCurrentVal); - } else { - umPrunned.add(0); - } - } - done = true; - Gdx.app.debug("Audio Analyzer", "Data prunned."); - } - - public FloatArray getBassSpectralFlux() { - return bassSpectralFlux; - } - public FloatArray getmSpectralFlux() { - return mSpectralFlux; - } - public FloatArray getBassThreshold() { - return bassThreshold; - } - public FloatArray getmThreshold() { - return mThreshold; - } - public FloatArray getUmSpectralFlux() { - return umSpectralFlux; - } - public FloatArray getUmThreshold() { - return umThreshold; - } - public FloatArray getBassPrunned() { - return bassPrunned; - } - public FloatArray getmPrunned() { - return mPrunned; - } - public FloatArray getUmPrunned() { - return umPrunned; - } - public boolean isDone() { - return done; - } - - public MusicManager getMusicManager() { - return musicManager; - } -} diff --git a/core/src/zero1hd/rhythmbullet/audio/analyzer/SpectralFluxAnalysisRunnable.java b/core/src/zero1hd/rhythmbullet/audio/analyzer/SpectralFluxAnalysisRunnable.java deleted file mode 100755 index 3ef896a..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/analyzer/SpectralFluxAnalysisRunnable.java +++ /dev/null @@ -1,141 +0,0 @@ -package zero1hd.rhythmbullet.audio.analyzer; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.math.MathUtils; -import com.badlogic.gdx.utils.FloatArray; - -import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D; -import zero1hd.rhythmbullet.audio.MusicManager; - -public class SpectralFluxAnalysisRunnable implements Runnable { - boolean work = true; - private boolean done; - - private FloatArray bassSpectralFlux = new FloatArray(); - private FloatArray mSpectralFlux = new FloatArray(); - private FloatArray umSpectralFlux = new FloatArray(); - - - MusicManager musicManager; - - private int PUID; - private int progress; - - public SpectralFluxAnalysisRunnable(MusicManager mm) { - this.musicManager = mm; - } - - @Override - public void run() { - progress = 0; - int tasksDone = 0; - long totalTasks = MathUtils.round((float)musicManager.getSampleCount()/musicManager.getChannelCount()/musicManager.getReadWindowSize()); - - float[] audioPCM = new float[musicManager.getReadWindowSize()]; - float[] spectrum = new float[(musicManager.getReadWindowSize()/2)+1]; - float[] lastSpectrum = new float[(musicManager.getReadWindowSize()/2)+1]; - - int bassBinBegin = 1; - int bassBinEnd = 11; - - int mBinBegin = 50; - int mBinEnd = 250; - - int umBinBegin = 350; - int umBinEnd = 513; - - Gdx.app.debug("Analyzing Song", musicManager.getBasicSongName()); - Gdx.app.debug("Read freq", String.valueOf(musicManager.getSampleRate())); - Gdx.app.debug("Using following bin ranges", "\nBass freq begin: " + bassBinBegin + "\nBass freq end: " + bassBinEnd + "\nMain freq begin: " + umBinBegin + "\nMain freq end: " + umBinEnd); - - Gdx.app.debug("Total tasks", String.valueOf(totalTasks)); - - FloatFFT_1D fft = new FloatFFT_1D(musicManager.getReadWindowSize()); - int seedDigit = 0; - - while (musicManager.readSampleFrames(audioPCM) > 0 && work) { - - fft.realForward(audioPCM); - - //Building a PUID (Pseudo unique ID) - if (tasksDone == (seedDigit*totalTasks/9)) { - float avg = 0; - for (int frame = 0; frame < spectrum.length; frame++) { - avg += spectrum[frame]; - } - avg /= spectrum.length; - if (avg < 0) { - avg *= -1f; - } - PUID +=(int) Math.pow(10, 9-seedDigit) * ((int)(avg*1000f)-(int)(avg*100f)*10); - seedDigit ++; - } - - System.arraycopy(spectrum, 0, lastSpectrum, 0, spectrum.length); - System.arraycopy(audioPCM, 0, spectrum, 0, spectrum.length); - - float fluxVal; - //bass detection - fluxVal = 0; - for (int i = bassBinBegin; i < bassBinEnd; i++) { - fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0 - ? 0 : (spectrum[i] - lastSpectrum[i]); - } - bassSpectralFlux.add(fluxVal); - - //m detection - fluxVal = 0; - for (int i = mBinBegin; i < mBinEnd; i++) { - fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0 - ? 0 : (spectrum[i] - lastSpectrum[i]); - } - mSpectralFlux.add(fluxVal); - - //um detection - fluxVal = 0; - for (int i = umBinBegin; i < umBinEnd; i++) { - fluxVal += ((spectrum[i] - lastSpectrum[i])) < 0 - ? 0 : (spectrum[i] - lastSpectrum[i]); - } - umSpectralFlux.add(fluxVal); - tasksDone++; - progress = (int) (100f*tasksDone/totalTasks); - } - - if (work) { - Gdx.app.debug("Audio Analyzer", "Done getting spectral flux."); - Gdx.app.debug("Audio Analyzer", "window count: " + bassSpectralFlux.size); - Gdx.app.debug("Audio Analyzer", "USING SEED: " + PUID); - progress = 100; - } - done = true; - } - - public synchronized int getProgress() { - return progress; - } - public FloatArray getBassSpectralFlux() { - return bassSpectralFlux; - } - public FloatArray getmSpectralFlux() { - return mSpectralFlux; - } - public FloatArray getUmSpectralFlux() { - return umSpectralFlux; - } - public int getPUID() { - return PUID; - } - public boolean isDone() { - return done; - } - public boolean isWorking() { - return work; - } - public void setWork(boolean work) { - this.work = work; - } - public MusicManager getMusicManager() { - return musicManager; - } -} diff --git a/core/src/zero1hd/rhythmbullet/audio/analyzer/ThresholdCalcRunnable.java b/core/src/zero1hd/rhythmbullet/audio/analyzer/ThresholdCalcRunnable.java deleted file mode 100755 index 21e2f8e..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/analyzer/ThresholdCalcRunnable.java +++ /dev/null @@ -1,102 +0,0 @@ -package zero1hd.rhythmbullet.audio.analyzer; - -import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.utils.FloatArray; - -import zero1hd.rhythmbullet.audio.MusicManager; - -public class ThresholdCalcRunnable implements Runnable { - boolean work = true; - private boolean done; - - private MusicManager musicManager; - - private FloatArray bassSpectralFlux; - private FloatArray mSpectralFlux; - private FloatArray umSpectralFlux; - private FloatArray bassThreshold = new FloatArray(); - private FloatArray mThreshold = new FloatArray(); - private FloatArray umThreshold = new FloatArray(); - - public ThresholdCalcRunnable(SpectralFluxAnalysisRunnable sfar) { - this.bassSpectralFlux = sfar.getBassSpectralFlux(); - this.mSpectralFlux = sfar.getmSpectralFlux(); - this.umSpectralFlux = sfar.getUmSpectralFlux(); - this.musicManager = sfar.getMusicManager(); - } - - @Override - public void run() { - Gdx.app.debug("Audio Analyzer", "beginning threshold calc."); - - float bassThresholdMultiplier = 1.5f; - float mThresholdMultiplier = 1.4f; - float umThresholdMultiplier = 1.4f; - - int bassThresholdCalcRange = thresholdRangeCalc(0.27f); - int mThresholdCalcRange = thresholdRangeCalc(0.4f); - int umThresholdCalcRange = thresholdRangeCalc(0.4f); - //threshold calculation - for (int i = 0; i < umSpectralFlux.size && work; i++) { - int start = Math.max(0, i - bassThresholdCalcRange/2); - int end = Math.min(umSpectralFlux.size - 1, i + bassThresholdCalcRange/2); - float average = 0; - for (int j = start; j <= end; j++) { - average += bassSpectralFlux.get(j); - } - average /= (end - start); - bassThreshold.add(average * bassThresholdMultiplier); - - start = Math.max(0, i - mThresholdCalcRange/2); - end = Math.min(umSpectralFlux.size - 1, i + mThresholdCalcRange/2); - average = 0; - for (int j = start; j <= end; j++) { - average+= mSpectralFlux.get(j); - } - average /= (end - start); - mThreshold.add(average*mThresholdMultiplier); - - start = Math.max(0, i - umThresholdCalcRange/2); - end = Math.min(umSpectralFlux.size - 1, i + umThresholdCalcRange/2); - average = 0; - for (int j = start; j <= end; j++) { - average+= umSpectralFlux.get(j); - } - average /= (end - start); - umThreshold.add(average*umThresholdMultiplier); - } - Gdx.app.debug("Audio Analyzer", "Threshold calculated."); - done = true; - } - - private int thresholdRangeCalc(float durationOfRange) { - return (int) (durationOfRange/(musicManager.getReadWindowSize()/musicManager.getSampleRate())); - } - - public boolean isDone() { - return done; - } - - public FloatArray getBassSpectralFlux() { - return bassSpectralFlux; - } - public FloatArray getmSpectralFlux() { - return mSpectralFlux; - } - public FloatArray getUmSpectralFlux() { - return umSpectralFlux; - } - public FloatArray getBassThreshold() { - return bassThreshold; - } - public FloatArray getmThreshold() { - return mThreshold; - } - public FloatArray getUmThreshold() { - return umThreshold; - } - - public MusicManager getMusicManager() { - return musicManager; - } -} diff --git a/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java b/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java index e291a02..18c9aea 100755 --- a/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java +++ b/core/src/zero1hd/rhythmbullet/audio/processor/AudioProcessor.java @@ -1,14 +1,9 @@ package zero1hd.rhythmbullet.audio.processor; +import com.badlogic.gdx.files.FileHandle; 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 */ @@ -20,12 +15,30 @@ public interface AudioProcessor extends Disposable { public int getSampleRate(); /** - * Thread safe - * Reads samples (NOT FRAMES) with interwoven data for stereo. + * Reads samples 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); + public int readSamples(short[] pcm); + + /** + * Reads 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 + * @return the amount of samples read. + */ + public int readFrames(float[] pcm); + + /** + * + * @return The music file's {@link FileHandle} + */ + public FileHandle getMusicFileHandle(); + + /** + * + * @return the number of sample frames in the song. + */ + public long getSampleFrames(); } diff --git a/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java b/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java index 7eb72a8..8f175ef 100755 --- a/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java +++ b/core/src/zero1hd/rhythmbullet/audio/processor/WAVAudioProcessor.java @@ -16,31 +16,23 @@ public class WAVAudioProcessor implements AudioProcessor { private byte[] buffer; private FileHandle fileHandle; private AudioInputStream audioInputStream; - private boolean initiated; + private long sampleFrames; public WAVAudioProcessor(FileHandle fileHandle) { this.fileHandle = fileHandle; AudioFormat format; try { - format = AudioSystem.getAudioFileFormat(fileHandle.file()).getFormat(); + audioInputStream = AudioSystem.getAudioInputStream(fileHandle.file()); + format = audioInputStream.getFormat(); stereo = format.getChannels() > 1 ? true : false; sampleRate = (int) format.getSampleRate(); + sampleFrames = audioInputStream.getFrameLength(); } catch (UnsupportedAudioFileException | IOException e) { Gdx.app.debug("WAVAudioProcessor", "Couldn't instantiate WAVAUdioProcessor due to error."); e.printStackTrace(); } - - } - - @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() { @@ -52,33 +44,59 @@ public class WAVAudioProcessor implements AudioProcessor { } @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(); + public int readSamples(short[] pcm) { + 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++; } - return framesRead; + } catch (IOException e) { + e.printStackTrace(); } - } else { - throw new IllegalStateException("Stream has not been initialized."); + } + return framesRead; } - + + @Override + public int readFrames(float[] pcm) { + 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)); + pcm[sampleID] = secondChan > pcm[sampleID] ? secondChan : pcm[sampleID]; + } + framesRead++; + pcm[sampleID] /= Short.MAX_VALUE+1; + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + return framesRead; + } + + @Override + public FileHandle getMusicFileHandle() { + return fileHandle; + } + + @Override + public long getSampleFrames() { + return sampleFrames; + } + @Override public void dispose() { try { diff --git a/core/src/zero1hd/rhythmbullet/audio/visualizer/BasicFFT.java b/core/src/zero1hd/rhythmbullet/audio/visualizer/BasicFFT.java new file mode 100755 index 0000000..c899731 --- /dev/null +++ b/core/src/zero1hd/rhythmbullet/audio/visualizer/BasicFFT.java @@ -0,0 +1,15 @@ +package zero1hd.rhythmbullet.audio.visualizer; + +import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D; + +public class BasicFFT { + private FloatFFT_1D fft; + + public BasicFFT(int window) { + fft = new FloatFFT_1D(window); + } + + public void fft(float[] PCM) { + fft.realForward(PCM); + } +} diff --git a/core/src/zero1hd/rhythmbullet/audio/visualizer/MirrorVisualizer.java b/core/src/zero1hd/rhythmbullet/audio/visualizer/MirrorVisualizer.java deleted file mode 100755 index 0414585..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/visualizer/MirrorVisualizer.java +++ /dev/null @@ -1,75 +0,0 @@ -package zero1hd.rhythmbullet.audio.visualizer; - -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.graphics.g2d.Batch; -import com.badlogic.gdx.graphics.g2d.Sprite; -import com.badlogic.gdx.math.MathUtils; -import com.badlogic.gdx.math.Vector2; - -public class MirrorVisualizer { - private int xPos, yPos; - private float rotation; - private Sprite[] bars; - private boolean flip; - - private Vector2 rectCoordRot; - public MirrorVisualizer() { - rectCoordRot = new Vector2(); - } - - public void setup(Sprite[] bars, float xPos, float yPos, float rotation) { - this.bars = new Sprite[bars.length]; - this.xPos = (int) xPos; - this.yPos = (int) yPos; - this.rotation = rotation; - rectCoordRot.set(MathUtils.cosDeg(rotation), MathUtils.sinDeg(rotation)); - - for (int i = 0; i < bars.length; i++) { - this.bars[i] = new Sprite(bars[i]); - } - } - - public void render(int renderIndex, Batch batch, float parentAlpha, Sprite[] bars) { - this.bars[renderIndex].setSize(bars[renderIndex].getWidth(), bars[renderIndex].getHeight()); - this.bars[renderIndex].draw(batch); - } - - public void position(int positionIndex, int barWidth, int spaceBetweenBars) { - if (flip) { - bars[positionIndex].setRotation(rotation+180); - } else { - bars[positionIndex].setRotation(rotation); - } - int barSpace = positionIndex*(barWidth+spaceBetweenBars); - bars[positionIndex].setPosition(xPos + barSpace*rectCoordRot.x, yPos + barSpace*rectCoordRot.y); - } - - public void setColor(Color color) { - for (int i = 0; i < bars.length; i++) { - bars[i].setColor(color); - } - } - - public void setColor(float r, float g, float b, float a) { - for (int i = 0; i < bars.length; i++) { - bars[i].setColor(r, g, b, a); - } - } - - public float getRotation() { - return rotation; - } - - public void setRotation(float rotation) { - this.rotation = rotation; - } - - public void setyPos(int yPos) { - this.yPos = yPos; - } - - public void setxPos(int xPos) { - this.xPos = xPos; - } - -} diff --git a/core/src/zero1hd/rhythmbullet/audio/visualizer/MusicManagerFFT.java b/core/src/zero1hd/rhythmbullet/audio/visualizer/MusicManagerFFT.java deleted file mode 100755 index 0786ba1..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/visualizer/MusicManagerFFT.java +++ /dev/null @@ -1,51 +0,0 @@ -package zero1hd.rhythmbullet.audio.visualizer; - -import java.util.concurrent.locks.ReentrantLock; - -import com.badlogic.gdx.utils.Disposable; - -import edu.emory.mathcs.jtransforms.fft.FloatFFT_1D; -import zero1hd.rhythmbullet.audio.MusicManager; - -public class MusicManagerFFT implements Disposable { - protected MusicManager mm; - private FloatFFT_1D fft; - private boolean calc; - private ReentrantLock lock; - protected float[] audioPCM; - - public MusicManagerFFT() { - lock = new ReentrantLock(); - } - - public void calculate() { - if (mm != null && calc && mm.isPlaying()) { - lock.lock(); - fft.realForward(audioPCM); - lock.unlock(); - } - } - - public void setMM(MusicManager mm) { - lock.lock(); - calc = false; - if (audioPCM == null || audioPCM.length != mm.getReadWindowSize()) { - audioPCM = new float[mm.getReadWindowSize()]; - fft = new FloatFFT_1D(mm.getReadWindowSize()); - } - this.mm = mm; - calc = true; - lock.unlock(); - } - - @Override - public void dispose() { - } - - public MusicManager getMm() { - return mm; - } - public float[] getAudioPCM() { - return audioPCM; - } -} diff --git a/core/src/zero1hd/rhythmbullet/audio/visualizer/Visualizer.java b/core/src/zero1hd/rhythmbullet/audio/visualizer/Visualizer.java deleted file mode 100755 index bbfbd1b..0000000 --- a/core/src/zero1hd/rhythmbullet/audio/visualizer/Visualizer.java +++ /dev/null @@ -1,21 +0,0 @@ -package zero1hd.rhythmbullet.audio.visualizer; - -import com.badlogic.gdx.graphics.g2d.Batch; -import com.badlogic.gdx.utils.Disposable; - -import zero1hd.rhythmbullet.audio.MusicManager; - -public interface Visualizer extends Disposable { - - void calcPCMData(); - - void setMM(MusicManager mm); - - MusicManager getMM(); - - void render(Batch batch, float delta); - - float[] getAudioPCMData(); - - void fft(); -} \ No newline at end of file diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/DesktopAudioProcessorFactory.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/DesktopAudioProcessorFactory.java index efbb106..38dfbe4 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/DesktopAudioProcessorFactory.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/DesktopAudioProcessorFactory.java @@ -3,11 +3,12 @@ package zero1hd.rhythmbullet.desktop.audio.processor; import com.badlogic.gdx.files.FileHandle; import zero1hd.rhythmbullet.audio.AudioProcessorFactory; +import zero1hd.rhythmbullet.audio.MinimalAudioHeader; import zero1hd.rhythmbullet.audio.processor.AudioProcessor; public class DesktopAudioProcessorFactory implements AudioProcessorFactory { @Override public AudioProcessor newMP3AudioProcessor(FileHandle fileHandle) { - return new MP3AudioProcessor(fileHandle); + return new MP3AudioProcessor(fileHandle, new MinimalAudioHeader(fileHandle)); } } diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/MP3AudioProcessor.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/MP3AudioProcessor.java index 2581d28..862fc3e 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/MP3AudioProcessor.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/processor/MP3AudioProcessor.java @@ -2,7 +2,6 @@ package zero1hd.rhythmbullet.desktop.audio.processor; 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; @@ -10,12 +9,14 @@ import javazoom.jl.decoder.DecoderException; import javazoom.jl.decoder.Header; import javazoom.jl.decoder.MP3Decoder; import javazoom.jl.decoder.OutputBuffer; +import zero1hd.rhythmbullet.audio.MinimalAudioHeader; import zero1hd.rhythmbullet.audio.processor.AudioProcessor; public class MP3AudioProcessor implements AudioProcessor { private boolean stereo; private int sampleRate; + private long sampleFrames; private FileHandle fileHandle; private byte[] currentByteSet; private byte[] workset; @@ -25,30 +26,16 @@ public class MP3AudioProcessor implements AudioProcessor { private int indexHead = -1; - public MP3AudioProcessor(FileHandle fileHandle) { + public MP3AudioProcessor(FileHandle fileHandle, MinimalAudioHeader minimalAudioHeader) { 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()); + + stereo = minimalAudioHeader.getChannelCount() == 1 ? false : true; + sampleRate = minimalAudioHeader.getSampleRate(); + sampleFrames = minimalAudioHeader.estimateSampleFrames(); + decoder = new MP3Decoder(); sampleBuffer = new OutputBuffer(stereo ? 2 : 1, false); decoder.setOutputBuffer(sampleBuffer); @@ -67,8 +54,8 @@ public class MP3AudioProcessor implements AudioProcessor { } @Override - public int readSamples(short[] pcm, Object syncObj) { - int framesRead = 0; + public int readSamples(short[] pcm) { + int samplesRead = 0; for (int sid = 0; sid < pcm.length; sid++) { for (int wsid = 0; wsid < workset.length; wsid++) { workset[wsid] = nextByte(); @@ -78,9 +65,28 @@ public class MP3AudioProcessor implements AudioProcessor { if (stereo) { short altChan = (short) ((workset[3] << 8) + (workset[2] & 0x00ff)); sid++; + pcm[sid] = altChan; + } + samplesRead ++; + } + } + return samplesRead; + } + + @Override + public int readFrames(float[] pcm) { + 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)); pcm[sid] = altChan > pcm[sid] ? altChan : pcm[sid]; } - framesRead ++; + framesRead++; pcm[sid] /= Short.MAX_VALUE+1; } } @@ -129,6 +135,16 @@ public class MP3AudioProcessor implements AudioProcessor { } } + @Override + public FileHandle getMusicFileHandle() { + return fileHandle; + } + + @Override + public long getSampleFrames() { + return sampleFrames; + } + @Override public void dispose() { Gdx.app.debug("MP3Manager", "Disposing..."); diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DoubleHorizontalVisualizer.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DoubleHorizontalVisualizer.java new file mode 100755 index 0000000..5ef2e3f --- /dev/null +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DoubleHorizontalVisualizer.java @@ -0,0 +1,112 @@ +package zero1hd.rhythmbullet.desktop.audio.visualizer; + +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; + +import zero1hd.rhythmbullet.audio.MusicController; + +public class DoubleHorizontalVisualizer { + private int width, height, barCount, barWidth, spaceBetweenBars; + private int x, y; + private float barRate = 0.75f; + private ShapeRenderer shapeRenderer; + private PCMMachine pcm; + private int smoothRange; + private int multiplier = 300; + private int[] amplitudes; + private int[] barHeights; + private int binsPerBar; + + /** + * + * @param barCount amount of bars this visualizer should have. + * @param width the width of the visualizer. + * @param spacePercentage the percentage of a bar that should be space. + */ + public DoubleHorizontalVisualizer(int barCount, int width, float spacePercentage, int height, MusicController musicController) { + this.barCount = barCount; + this.barWidth = width/barCount; + this.spaceBetweenBars = (int) (barWidth * spacePercentage); + this.barWidth -= spaceBetweenBars; + if (barWidth < 1) throw new IllegalArgumentException("The arguments you passed caused the bar width to be 0."); + binsPerBar = (pcm.getWindowSize()/barCount); + this.width = width; + this.height = height; + pcm = new PCMMachine(musicController); + amplitudes = new int[barCount]; + barHeights = new int[barCount]; + shapeRenderer = new ShapeRenderer(); + shapeRenderer.set(ShapeType.Filled); + } + + public void act(float delta) { + for (int bar = 0; bar < amplitudes.length; bar++) { + float normalizedAmplitude = 0; + for (int freq = bar*binsPerBar; freq < bar*binsPerBar + binsPerBar; freq++) { + normalizedAmplitude += Math.abs(pcm.getFrequencyBins()[freq]); + } + amplitudes[bar] = (int) (normalizedAmplitude*multiplier); + amplitudes[bar] /= binsPerBar; + + float cappedDelta = Math.max(0, delta); + cappedDelta = Math.min(1f, delta); + + barHeights[bar] += Math.max(0, (amplitudes[bar] - barHeights[bar]) * barRate * cappedDelta); + + } + for (int bar = 1; bar <= barHeights.length; bar++) { + int smoothCount = 1; + for (int range = 0; range < smoothRange; range++) { + if (bar+range < amplitudes.length) { + barHeights[bar] += amplitudes[bar+range]; + smoothCount++; + } + if (bar-range > 0) { + barHeights[bar] += amplitudes[bar-range]; + smoothCount++; + } + } + barHeights[bar] /= smoothCount; + } + } + + public void draw(Batch batch, float parentAlpha) { + shapeRenderer.begin(); + int beginX = x + spaceBetweenBars/2, beginY = y; + for (int bar = 0; bar < barCount; bar++) { + shapeRenderer.rect(beginX + spaceBetweenBars*bar, beginY+height, beginX+barWidth, beginY+barHeights[bar]+height); + shapeRenderer.rect(beginX + spaceBetweenBars*bar, beginY, beginX+barWidth, beginY+barHeights[barHeights.length - 1 - bar]); + } + shapeRenderer.end(); + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public void setX(int x) { + this.x = x; + } + + public void setY(int y) { + this.y = y; + } + + public void setPosition(int x, int y) { + this.x = x; + this.y = y; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } +} diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java b/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/PCMMachine.java similarity index 78% rename from desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java rename to desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/PCMMachine.java index d231fa9..debefe4 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/DesktopVisualizer.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/audio/visualizer/PCMMachine.java @@ -11,17 +11,20 @@ import org.lwjgl.openal.AL11; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.backends.lwjgl.audio.OpenALMusic; +import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.TimeUtils; 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.MinimalAudioHeader; import zero1hd.rhythmbullet.audio.MusicController; +import zero1hd.rhythmbullet.audio.visualizer.BasicFFT; -public class DesktopVisualizer implements Observer { +public class PCMMachine implements Observer, Disposable { private int windowSize = 1024; private float[] PCM = new float[windowSize]; + private float[] frequencyBins = new float[windowSize/2]; + private BasicFFT fft = new BasicFFT(windowSize); private ShortBuffer playingBuffer; private ShortBuffer compareBuffer; private ShortBuffer buffer; @@ -32,8 +35,11 @@ public class DesktopVisualizer implements Observer { private BufferStreamReadThread streamReadThread; private int windowsRead; private int currentPlaybackWindow; - - public DesktopVisualizer() { + private volatile boolean updated; + + public PCMMachine(MusicController musicController) { + this.mc = musicController; + mc.addObserver(this); try { Field bufferField = ClassReflection.getDeclaredField(OpenALMusic.class, "tempBuffer"); bufferField.setAccessible(true); @@ -109,7 +115,7 @@ public class DesktopVisualizer implements Observer { windowsRead = (int) ((mc.getCurrentPosition()*sampleRate)/windowSize); } - public void setMusic(MinimalAudioHeader header) { + private void setMusic() { try { Field sourceIDField = ClassReflection.getDeclaredField(OpenALMusic.class, "sourceID"); sourceIDField.setAccessible(true); @@ -117,8 +123,9 @@ public class DesktopVisualizer implements Observer { } catch (ReflectionException e) { e.printStackTrace(); } - channelCount = header.getChannelCount(); - sampleRate = header.getSampleRate(); + + channelCount = mc.getCurrentMusicHeader().getChannelCount(); + sampleRate = mc.getCurrentMusicHeader().getSampleRate(); playingBuffer = ShortBuffer.allocate(buffer.capacity()*2); buffer.rewind(); @@ -133,17 +140,28 @@ public class DesktopVisualizer implements Observer { buffer.rewind(); } - public synchronized float[] getAudioPCM() { - return PCM; + public float[] getFrequencyBins() { + if (updated) { + synchronized (PCM) { + fft.fft(PCM); + System.arraycopy(PCM, 0, frequencyBins, 0, frequencyBins.length); + } + } + return frequencyBins; } - public class BufferStreamReadThread implements Runnable { + public int getWindowSize() { + return windowSize; + } + + private class BufferStreamReadThread implements Runnable { private String name = "PCM-Audio-Processing"; private Thread thread; - private boolean run, paused; - private volatile long timeOfLastRead; + private volatile boolean run; + private boolean paused; + private long timeOfLastRead; private int waitTime; - + @Override public void run() { while (run) { @@ -155,10 +173,10 @@ public class DesktopVisualizer implements Observer { waitTime = sampleRate/windowSize/Gdx.graphics.getFramesPerSecond(); if (TimeUtils.timeSinceMillis(timeOfLastRead) >= waitTime) { calcPCMData(); + updated = true; windowsRead++; timeOfLastRead = TimeUtils.millis(); - currentPlaybackWindow = (int) ((mc.getCurrentPosition()*sampleRate)/windowSize); if (windowsRead != currentPlaybackWindow) { synchronizeBufferWithPlayback(); @@ -177,14 +195,20 @@ public class DesktopVisualizer implements Observer { } } - public synchronized void start() { + public void start() { if (thread == null && !thread.isAlive()) { thread = new Thread(this, name); thread.start(); } else { - notify(); + synchronized (this) { + notify(); + } } } + + public void stop() { + run = false; + } } @Override @@ -192,7 +216,7 @@ public class DesktopVisualizer implements Observer { if (o == mc) { switch ((MusicController.States) arg) { case Loaded: - setMusic(mc.getCurrentMusicHeader()); + setMusic(); break; case Playing: streamReadThread.start(); @@ -202,4 +226,9 @@ public class DesktopVisualizer implements Observer { } } } + + @Override + public void dispose() { + streamReadThread.stop(); + } } diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/graphics/ui/components/HorizontalVisualizerWidget.java b/desktop/src/zero1hd/rhythmbullet/desktop/graphics/ui/components/HorizontalVisualizerWidget.java index 1dcfc31..983e160 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/graphics/ui/components/HorizontalVisualizerWidget.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/graphics/ui/components/HorizontalVisualizerWidget.java @@ -8,7 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Widget; import com.badlogic.gdx.utils.Disposable; import zero1hd.rhythmbullet.audio.visualizer.HorizontalVisualizer; -import zero1hd.rhythmbullet.desktop.audio.visualizer.DesktopVisualizer; +import zero1hd.rhythmbullet.desktop.audio.visualizer.PCMMachine; public class HorizontalVisualizerWidget extends Widget implements Disposable { private HorizontalVisualizer vis; @@ -18,7 +18,7 @@ public class HorizontalVisualizerWidget extends Widget implements Disposable { private float visRefreshRate; private float timer; public HorizontalVisualizerWidget() { - vis = new HorizontalVisualizer(new DesktopVisualizer()); + vis = new HorizontalVisualizer(new PCMMachine()); } @Override diff --git a/desktop/src/zero1hd/rhythmbullet/desktop/screens/GameScreen.java b/desktop/src/zero1hd/rhythmbullet/desktop/screens/GameScreen.java index 7d2c785..ece95db 100755 --- a/desktop/src/zero1hd/rhythmbullet/desktop/screens/GameScreen.java +++ b/desktop/src/zero1hd/rhythmbullet/desktop/screens/GameScreen.java @@ -11,7 +11,7 @@ import com.badlogic.gdx.utils.viewport.ExtendViewport; import zero1hd.rhythmbullet.RhythmBullet; import zero1hd.rhythmbullet.audio.visualizer.CircularVisualizer; -import zero1hd.rhythmbullet.desktop.audio.visualizer.DesktopVisualizer; +import zero1hd.rhythmbullet.desktop.audio.visualizer.PCMMachine; import zero1hd.rhythmbullet.game.GameController; public class GameScreen extends ScreenAdapter { @@ -25,7 +25,7 @@ public class GameScreen extends ScreenAdapter { this.assets = assets; batch = new SpriteBatch(); viewport = new ExtendViewport(RhythmBullet.WORLD_WIDTH, RhythmBullet.WORLD_HEIGHT); - circleVisualizer = new CircularVisualizer(new DesktopVisualizer()); + circleVisualizer = new CircularVisualizer(new PCMMachine()); circleVisualizer.setCenter(Gdx.graphics.getWidth()/2, Gdx.graphics.getHeight()/2); circleVisualizer.setCamera(viewport.getCamera()); circleVisualizer.setColor(Color.CYAN.toFloatBits());