Finished basic visualizer and mass refactoring on the audio analyzer to

be compatible with the structure
This commit is contained in:
Harrison Deng 2018-07-23 00:15:31 -05:00
parent 0dce05050a
commit 7dff918802
22 changed files with 623 additions and 747 deletions

View File

@ -8,25 +8,31 @@ import org.jaudiotagger.audio.AudioHeader;
import org.jaudiotagger.audio.exceptions.CannotReadException; import org.jaudiotagger.audio.exceptions.CannotReadException;
import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
import org.jaudiotagger.audio.mp3.MP3AudioHeader;
import org.jaudiotagger.audio.mp3.MP3File;
import org.jaudiotagger.tag.TagException; import org.jaudiotagger.tag.TagException;
import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.files.FileHandle;
public class MinimalAudioHeader { public class MinimalAudioHeader {
private int sampleRate, channelCount; private int sampleRate, channelCount;
private SupportedFormats format;
private FileHandle musicFile;
public MinimalAudioHeader(FileHandle musicFile) { public MinimalAudioHeader(FileHandle musicFile) {
this.musicFile = musicFile;
format = SupportedFormats.valueOf(musicFile.extension().toUpperCase());
try { try {
AudioFile file = AudioFileIO.read(musicFile.file()); AudioFile file = AudioFileIO.read(musicFile.file());
AudioHeader header = file.getAudioHeader(); AudioHeader header = file.getAudioHeader();
sampleRate = header.getSampleRateAsNumber(); sampleRate = header.getSampleRateAsNumber();
channelCount = (header.getChannels().equals("Mono") ? 1 : 2); channelCount = (header.getChannels().equals("Mono") ? 1 : 2);
} catch (CannotReadException | IOException | TagException | ReadOnlyFileException } catch (CannotReadException | IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e) {
| InvalidAudioFrameException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
public int getSampleRate() { public int getSampleRate() {
return sampleRate; return sampleRate;
} }
@ -34,4 +40,21 @@ public class MinimalAudioHeader {
public int getChannelCount() { public int getChannelCount() {
return channelCount; 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;
}
}
} }

View File

@ -177,7 +177,7 @@ public class MusicController extends Observable implements OnCompletionListener,
if (musicHeader != null) { if (musicHeader != null) {
return musicHeader; return musicHeader;
} else { } else {
return musicList.newMinimalAudioHeader(getCurrentMusicFileHandle()); return musicHeader = musicList.newMinimalAudioHeader(getCurrentMusicFileHandle());
} }
} }

View File

@ -41,13 +41,16 @@ public class MusicList extends Observable {
/** /**
* @param file * @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) { public AudioProcessor newAudioProcessor(FileHandle file) {
if (file.extension().equalsIgnoreCase("wav")) { switch (SupportedFormats.valueOf(file.extension().toUpperCase())) {
return new WAVAudioProcessor(file); case MP3:
} else if (file.extension().equalsIgnoreCase("mp3")) {
return audioProcFactory.newMP3AudioProcessor(file); return audioProcFactory.newMP3AudioProcessor(file);
case WAV:
return new WAVAudioProcessor(file);
default:
break;
} }
return null; return null;
} }
@ -124,8 +127,11 @@ public class MusicList extends Observable {
if (files[i].isDirectory()) { if (files[i].isDirectory()) {
musicFiles.addAll(recursiveMusicSearch(files[i])); musicFiles.addAll(recursiveMusicSearch(files[i]));
} else { } else {
if (files[i].extension().equalsIgnoreCase("wav") || files[i].extension().equalsIgnoreCase("mp3")) { try {
SupportedFormats.valueOf(files[i].extension().toUpperCase());
musicFiles.add(files[i]); musicFiles.add(files[i]);
} catch (IllegalArgumentException e) {
Gdx.app.log("MusicList", "Unsupported file format: " + files[i].name());
} }
} }
} }

View File

@ -77,10 +77,16 @@ public class MusicMetadataController implements Disposable, Observer {
for (int i = 0; i < musicList.getTotal(); i++) { for (int i = 0; i < musicList.getTotal(); i++) {
FileHandle musicFile = musicList.getMusicArray().get(i); FileHandle musicFile = musicList.getMusicArray().get(i);
synchronized (this) { synchronized (this) {
if (musicFile.extension().equalsIgnoreCase("wav")) { switch (SupportedFormats.valueOf(musicFile.extension().toUpperCase())) {
metadataArray.add(new WAVMetadata(musicFile)); case MP3:
} else if (musicFile.extension().equalsIgnoreCase("mp3")) {
metadataArray.add(new MP3Metadata(musicFile)); metadataArray.add(new MP3Metadata(musicFile));
break;
case WAV:
metadataArray.add(new WAVMetadata(musicFile));
break;
default:
break;
} }
} }
} }

View File

@ -0,0 +1,5 @@
package zero1hd.rhythmbullet.audio;
public enum SupportedFormats {
WAV, MP3;
}

View File

@ -1,53 +1,296 @@
package zero1hd.rhythmbullet.audio.analyzer; package zero1hd.rhythmbullet.audio.analyzer;
import java.util.concurrent.ExecutorService; import com.badlogic.gdx.Gdx;
import java.util.concurrent.Executors; import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Disposable; 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 { public class AudioAnalyzer implements Runnable, Disposable {
private ExecutorService exec; private Thread thread;
private String threadName = "Audio-Analyzer";
private SpectralFluxAnalysisRunnable sfar; private volatile boolean work = true;
private ThresholdCalcRunnable tcr; private int windowSize = 1024;
private PruneFluxRunnable pfr;
private PeakDetectionRunnable pdr;
public AudioAnalyzer(MusicManager musicManager) { private FloatArray bassSpectralFlux = new FloatArray();
exec = Executors.newSingleThreadExecutor(); private FloatArray mSpectralFlux = new FloatArray();
sfar = new SpectralFluxAnalysisRunnable(musicManager); private FloatArray umSpectralFlux = new FloatArray();
tcr = new ThresholdCalcRunnable(sfar);
pfr = new PruneFluxRunnable(tcr);
pdr = new PeakDetectionRunnable(pfr, sfar.getPUID());
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() { public void start() {
exec.submit(sfar); if (thread == null || !thread.isAlive()) {
exec.submit(tcr); work = true;
exec.submit(pfr); thread = new Thread(this, threadName);
exec.submit(pdr); thread.start();
}
} }
private void stop() { @Override
sfar.work = false; public void run() {
tcr.work = false; spectralFluxAnalysis();
pfr.work = false; thresholdCalculation();
pdr.work = false; 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 @Override
public void dispose() { public void dispose() {
stop(); if (thread != null) {
exec.shutdown(); work = false;
}
} }
public boolean isDone() { public void stop() {
if ((sfar.isDone() && tcr.isDone() && pfr.isDone() && pdr.isDone())) { work = false;
return true;
}
return false;
} }
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,14 +1,9 @@
package zero1hd.rhythmbullet.audio.processor; package zero1hd.rhythmbullet.audio.processor;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.Disposable;
public interface AudioProcessor extends 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 * @return number of channels
*/ */
@ -20,12 +15,30 @@ public interface AudioProcessor extends Disposable {
public int getSampleRate(); public int getSampleRate();
/** /**
* <b>Thread safe</b> * Reads samples with interwoven data for stereo.
* 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) * 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 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. * @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();
} }

View File

@ -16,31 +16,23 @@ public class WAVAudioProcessor implements AudioProcessor {
private byte[] buffer; private byte[] buffer;
private FileHandle fileHandle; private FileHandle fileHandle;
private AudioInputStream audioInputStream; private AudioInputStream audioInputStream;
private boolean initiated; private long sampleFrames;
public WAVAudioProcessor(FileHandle fileHandle) { public WAVAudioProcessor(FileHandle fileHandle) {
this.fileHandle = fileHandle; this.fileHandle = fileHandle;
AudioFormat format; AudioFormat format;
try { try {
format = AudioSystem.getAudioFileFormat(fileHandle.file()).getFormat(); audioInputStream = AudioSystem.getAudioInputStream(fileHandle.file());
format = audioInputStream.getFormat();
stereo = format.getChannels() > 1 ? true : false; stereo = format.getChannels() > 1 ? true : false;
sampleRate = (int) format.getSampleRate(); sampleRate = (int) format.getSampleRate();
sampleFrames = audioInputStream.getFrameLength();
} catch (UnsupportedAudioFileException | IOException e) { } catch (UnsupportedAudioFileException | IOException e) {
Gdx.app.debug("WAVAudioProcessor", "Couldn't instantiate WAVAUdioProcessor due to error."); Gdx.app.debug("WAVAudioProcessor", "Couldn't instantiate WAVAUdioProcessor due to error.");
e.printStackTrace(); e.printStackTrace();
} }
}
@Override
public void initiate() {
try {
audioInputStream = AudioSystem.getAudioInputStream(fileHandle.file());
} catch (UnsupportedAudioFileException | IOException e) {
e.printStackTrace();
}
buffer = new byte[audioInputStream.getFormat().getFrameSize()]; buffer = new byte[audioInputStream.getFormat().getFrameSize()];
initiated = true;
} }
public boolean isStereo() { public boolean isStereo() {
@ -52,9 +44,7 @@ public class WAVAudioProcessor implements AudioProcessor {
} }
@Override @Override
public int readSamples(short[] pcm, Object syncObj) { public int readSamples(short[] pcm) {
if (initiated) {
synchronized (syncObj) {
int framesRead = 0; int framesRead = 0;
for (int sampleID = 0; sampleID < pcm.length; sampleID++) { for (int sampleID = 0; sampleID < pcm.length; sampleID++) {
try { try {
@ -74,9 +64,37 @@ public class WAVAudioProcessor implements AudioProcessor {
} }
return framesRead; return framesRead;
} }
} else {
throw new IllegalStateException("Stream has not been initialized."); @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 @Override

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -3,11 +3,12 @@ package zero1hd.rhythmbullet.desktop.audio.processor;
import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.files.FileHandle;
import zero1hd.rhythmbullet.audio.AudioProcessorFactory; import zero1hd.rhythmbullet.audio.AudioProcessorFactory;
import zero1hd.rhythmbullet.audio.MinimalAudioHeader;
import zero1hd.rhythmbullet.audio.processor.AudioProcessor; import zero1hd.rhythmbullet.audio.processor.AudioProcessor;
public class DesktopAudioProcessorFactory implements AudioProcessorFactory { public class DesktopAudioProcessorFactory implements AudioProcessorFactory {
@Override @Override
public AudioProcessor newMP3AudioProcessor(FileHandle fileHandle) { public AudioProcessor newMP3AudioProcessor(FileHandle fileHandle) {
return new MP3AudioProcessor(fileHandle); return new MP3AudioProcessor(fileHandle, new MinimalAudioHeader(fileHandle));
} }
} }

View File

@ -2,7 +2,6 @@ package zero1hd.rhythmbullet.desktop.audio.processor;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.GdxRuntimeException;
import javazoom.jl.decoder.Bitstream; import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException; import javazoom.jl.decoder.BitstreamException;
@ -10,12 +9,14 @@ import javazoom.jl.decoder.DecoderException;
import javazoom.jl.decoder.Header; import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.MP3Decoder; import javazoom.jl.decoder.MP3Decoder;
import javazoom.jl.decoder.OutputBuffer; import javazoom.jl.decoder.OutputBuffer;
import zero1hd.rhythmbullet.audio.MinimalAudioHeader;
import zero1hd.rhythmbullet.audio.processor.AudioProcessor; import zero1hd.rhythmbullet.audio.processor.AudioProcessor;
public class MP3AudioProcessor implements AudioProcessor { public class MP3AudioProcessor implements AudioProcessor {
private boolean stereo; private boolean stereo;
private int sampleRate; private int sampleRate;
private long sampleFrames;
private FileHandle fileHandle; private FileHandle fileHandle;
private byte[] currentByteSet; private byte[] currentByteSet;
private byte[] workset; private byte[] workset;
@ -25,30 +26,16 @@ public class MP3AudioProcessor implements AudioProcessor {
private int indexHead = -1; private int indexHead = -1;
public MP3AudioProcessor(FileHandle fileHandle) { public MP3AudioProcessor(FileHandle fileHandle, MinimalAudioHeader minimalAudioHeader) {
this.fileHandle = fileHandle; this.fileHandle = fileHandle;
bitstream = new Bitstream(fileHandle.read()); 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 { stereo = minimalAudioHeader.getChannelCount() == 1 ? false : true;
bitstream.close(); sampleRate = minimalAudioHeader.getSampleRate();
} catch (BitstreamException e) { sampleFrames = minimalAudioHeader.estimateSampleFrames();
e.printStackTrace();
}
}
@Override
public void initiate() {
bitstream = new Bitstream(fileHandle.read());
decoder = new MP3Decoder(); decoder = new MP3Decoder();
sampleBuffer = new OutputBuffer(stereo ? 2 : 1, false); sampleBuffer = new OutputBuffer(stereo ? 2 : 1, false);
decoder.setOutputBuffer(sampleBuffer); decoder.setOutputBuffer(sampleBuffer);
@ -67,8 +54,8 @@ public class MP3AudioProcessor implements AudioProcessor {
} }
@Override @Override
public int readSamples(short[] pcm, Object syncObj) { public int readSamples(short[] pcm) {
int framesRead = 0; int samplesRead = 0;
for (int sid = 0; sid < pcm.length; sid++) { for (int sid = 0; sid < pcm.length; sid++) {
for (int wsid = 0; wsid < workset.length; wsid++) { for (int wsid = 0; wsid < workset.length; wsid++) {
workset[wsid] = nextByte(); workset[wsid] = nextByte();
@ -78,6 +65,25 @@ public class MP3AudioProcessor implements AudioProcessor {
if (stereo) { if (stereo) {
short altChan = (short) ((workset[3] << 8) + (workset[2] & 0x00ff)); short altChan = (short) ((workset[3] << 8) + (workset[2] & 0x00ff));
sid++; 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]; pcm[sid] = altChan > pcm[sid] ? altChan : pcm[sid];
} }
framesRead++; framesRead++;
@ -129,6 +135,16 @@ public class MP3AudioProcessor implements AudioProcessor {
} }
} }
@Override
public FileHandle getMusicFileHandle() {
return fileHandle;
}
@Override
public long getSampleFrames() {
return sampleFrames;
}
@Override @Override
public void dispose() { public void dispose() {
Gdx.app.debug("MP3Manager", "Disposing..."); Gdx.app.debug("MP3Manager", "Disposing...");

View File

@ -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;
}
}

View File

@ -11,17 +11,20 @@ import org.lwjgl.openal.AL11;
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl.audio.OpenALMusic; 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.TimeUtils;
import com.badlogic.gdx.utils.reflect.ClassReflection; import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.Field; import com.badlogic.gdx.utils.reflect.Field;
import com.badlogic.gdx.utils.reflect.ReflectionException; import com.badlogic.gdx.utils.reflect.ReflectionException;
import zero1hd.rhythmbullet.audio.MinimalAudioHeader;
import zero1hd.rhythmbullet.audio.MusicController; 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 int windowSize = 1024;
private float[] PCM = new float[windowSize]; private float[] PCM = new float[windowSize];
private float[] frequencyBins = new float[windowSize/2];
private BasicFFT fft = new BasicFFT(windowSize);
private ShortBuffer playingBuffer; private ShortBuffer playingBuffer;
private ShortBuffer compareBuffer; private ShortBuffer compareBuffer;
private ShortBuffer buffer; private ShortBuffer buffer;
@ -32,8 +35,11 @@ public class DesktopVisualizer implements Observer {
private BufferStreamReadThread streamReadThread; private BufferStreamReadThread streamReadThread;
private int windowsRead; private int windowsRead;
private int currentPlaybackWindow; private int currentPlaybackWindow;
private volatile boolean updated;
public DesktopVisualizer() { public PCMMachine(MusicController musicController) {
this.mc = musicController;
mc.addObserver(this);
try { try {
Field bufferField = ClassReflection.getDeclaredField(OpenALMusic.class, "tempBuffer"); Field bufferField = ClassReflection.getDeclaredField(OpenALMusic.class, "tempBuffer");
bufferField.setAccessible(true); bufferField.setAccessible(true);
@ -109,7 +115,7 @@ public class DesktopVisualizer implements Observer {
windowsRead = (int) ((mc.getCurrentPosition()*sampleRate)/windowSize); windowsRead = (int) ((mc.getCurrentPosition()*sampleRate)/windowSize);
} }
public void setMusic(MinimalAudioHeader header) { private void setMusic() {
try { try {
Field sourceIDField = ClassReflection.getDeclaredField(OpenALMusic.class, "sourceID"); Field sourceIDField = ClassReflection.getDeclaredField(OpenALMusic.class, "sourceID");
sourceIDField.setAccessible(true); sourceIDField.setAccessible(true);
@ -117,8 +123,9 @@ public class DesktopVisualizer implements Observer {
} catch (ReflectionException e) { } catch (ReflectionException e) {
e.printStackTrace(); e.printStackTrace();
} }
channelCount = header.getChannelCount();
sampleRate = header.getSampleRate(); channelCount = mc.getCurrentMusicHeader().getChannelCount();
sampleRate = mc.getCurrentMusicHeader().getSampleRate();
playingBuffer = ShortBuffer.allocate(buffer.capacity()*2); playingBuffer = ShortBuffer.allocate(buffer.capacity()*2);
buffer.rewind(); buffer.rewind();
@ -133,15 +140,26 @@ public class DesktopVisualizer implements Observer {
buffer.rewind(); buffer.rewind();
} }
public synchronized float[] getAudioPCM() { public float[] getFrequencyBins() {
return PCM; 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 String name = "PCM-Audio-Processing";
private Thread thread; private Thread thread;
private boolean run, paused; private volatile boolean run;
private volatile long timeOfLastRead; private boolean paused;
private long timeOfLastRead;
private int waitTime; private int waitTime;
@Override @Override
@ -155,10 +173,10 @@ public class DesktopVisualizer implements Observer {
waitTime = sampleRate/windowSize/Gdx.graphics.getFramesPerSecond(); waitTime = sampleRate/windowSize/Gdx.graphics.getFramesPerSecond();
if (TimeUtils.timeSinceMillis(timeOfLastRead) >= waitTime) { if (TimeUtils.timeSinceMillis(timeOfLastRead) >= waitTime) {
calcPCMData(); calcPCMData();
updated = true;
windowsRead++; windowsRead++;
timeOfLastRead = TimeUtils.millis(); timeOfLastRead = TimeUtils.millis();
currentPlaybackWindow = (int) ((mc.getCurrentPosition()*sampleRate)/windowSize); currentPlaybackWindow = (int) ((mc.getCurrentPosition()*sampleRate)/windowSize);
if (windowsRead != currentPlaybackWindow) { if (windowsRead != currentPlaybackWindow) {
synchronizeBufferWithPlayback(); synchronizeBufferWithPlayback();
@ -177,22 +195,28 @@ public class DesktopVisualizer implements Observer {
} }
} }
public synchronized void start() { public void start() {
if (thread == null && !thread.isAlive()) { if (thread == null && !thread.isAlive()) {
thread = new Thread(this, name); thread = new Thread(this, name);
thread.start(); thread.start();
} else { } else {
synchronized (this) {
notify(); notify();
} }
} }
} }
public void stop() {
run = false;
}
}
@Override @Override
public void update(Observable o, Object arg) { public void update(Observable o, Object arg) {
if (o == mc) { if (o == mc) {
switch ((MusicController.States) arg) { switch ((MusicController.States) arg) {
case Loaded: case Loaded:
setMusic(mc.getCurrentMusicHeader()); setMusic();
break; break;
case Playing: case Playing:
streamReadThread.start(); streamReadThread.start();
@ -202,4 +226,9 @@ public class DesktopVisualizer implements Observer {
} }
} }
} }
@Override
public void dispose() {
streamReadThread.stop();
}
} }

View File

@ -8,7 +8,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Widget;
import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.Disposable;
import zero1hd.rhythmbullet.audio.visualizer.HorizontalVisualizer; 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 { public class HorizontalVisualizerWidget extends Widget implements Disposable {
private HorizontalVisualizer vis; private HorizontalVisualizer vis;
@ -18,7 +18,7 @@ public class HorizontalVisualizerWidget extends Widget implements Disposable {
private float visRefreshRate; private float visRefreshRate;
private float timer; private float timer;
public HorizontalVisualizerWidget() { public HorizontalVisualizerWidget() {
vis = new HorizontalVisualizer(new DesktopVisualizer()); vis = new HorizontalVisualizer(new PCMMachine());
} }
@Override @Override

View File

@ -11,7 +11,7 @@ import com.badlogic.gdx.utils.viewport.ExtendViewport;
import zero1hd.rhythmbullet.RhythmBullet; import zero1hd.rhythmbullet.RhythmBullet;
import zero1hd.rhythmbullet.audio.visualizer.CircularVisualizer; 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; import zero1hd.rhythmbullet.game.GameController;
public class GameScreen extends ScreenAdapter { public class GameScreen extends ScreenAdapter {
@ -25,7 +25,7 @@ public class GameScreen extends ScreenAdapter {
this.assets = assets; this.assets = assets;
batch = new SpriteBatch(); batch = new SpriteBatch();
viewport = new ExtendViewport(RhythmBullet.WORLD_WIDTH, RhythmBullet.WORLD_HEIGHT); 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.setCenter(Gdx.graphics.getWidth()/2, Gdx.graphics.getHeight()/2);
circleVisualizer.setCamera(viewport.getCamera()); circleVisualizer.setCamera(viewport.getCamera());
circleVisualizer.setColor(Color.CYAN.toFloatBits()); circleVisualizer.setColor(Color.CYAN.toFloatBits());