Finished basic visualizer and mass refactoring on the audio analyzer to
be compatible with the structure
This commit is contained in:
parent
0dce05050a
commit
7dff918802
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
core/src/zero1hd/rhythmbullet/audio/SupportedFormats.java
Executable file
5
core/src/zero1hd/rhythmbullet/audio/SupportedFormats.java
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
package zero1hd.rhythmbullet.audio;
|
||||||
|
|
||||||
|
public enum SupportedFormats {
|
||||||
|
WAV, MP3;
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
15
core/src/zero1hd/rhythmbullet/audio/visualizer/BasicFFT.java
Executable file
15
core/src/zero1hd/rhythmbullet/audio/visualizer/BasicFFT.java
Executable 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,9 +65,28 @@ 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++;
|
||||||
pcm[sid] /= Short.MAX_VALUE+1;
|
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
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
Gdx.app.debug("MP3Manager", "Disposing...");
|
Gdx.app.debug("MP3Manager", "Disposing...");
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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());
|
||||||
|
Loading…
x
Reference in New Issue
Block a user