visualizer works in a basic manner, disposing needed, gui needs

tweaking, visualizer bar rate could be adjusted;
This commit is contained in:
Harrison Deng 2018-08-11 02:21:05 -05:00
parent 4a7f4962e2
commit f2a60ea490
11 changed files with 104 additions and 70 deletions

View File

@ -203,4 +203,5 @@ public class RhythmBullet extends Game {
} }
super.dispose(); super.dispose();
} }
} }

View File

@ -1,15 +0,0 @@
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

@ -3,21 +3,22 @@ package zero1hd.rhythmbullet.audio.visualizer;
import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Disposable;
import zero1hd.rhythmbullet.audio.MusicController; import zero1hd.rhythmbullet.audio.MusicController;
public class DoubleHorizontalVisualizer { public class DoubleHorizontalVisualizer implements Disposable {
private int width, height, barCount, barWidth, spaceBetweenBars; private int width, height, barCount, barWidth, spaceBetweenBars;
private int x, y; private int x, y;
private float barRate = 0.75f;
private ShapeRenderer shapeRenderer; private ShapeRenderer shapeRenderer;
private PCMSystem pcm; private PCMSystem pcm;
private int smoothRange; private float[] amplitudes;
private int multiplier = 300;
private int[] amplitudes;
private int[] barHeights; private int[] barHeights;
private int binsPerBar; private int binsPerBar;
private float normalFactor = 20;
private float barChangeRate = 4f;
private int smoothRange = 1;
/** /**
* *
* @param barCount amount of bars this visualizer should have. * @param barCount amount of bars this visualizer should have.
@ -27,14 +28,14 @@ public class DoubleHorizontalVisualizer {
public DoubleHorizontalVisualizer(int barCount, int width, int height, float spacePercentage, MusicController musicController, PCMSystem PCMSystem) { public DoubleHorizontalVisualizer(int barCount, int width, int height, float spacePercentage, MusicController musicController, PCMSystem PCMSystem) {
this.barCount = barCount; this.barCount = barCount;
this.barWidth = width/barCount; this.barWidth = width/barCount;
this.spaceBetweenBars = (int) (barWidth * spacePercentage); this.spaceBetweenBars = MathUtils.round(barWidth * spacePercentage);
this.barWidth -= spaceBetweenBars; this.barWidth -= spaceBetweenBars;
pcm = PCMSystem; pcm = PCMSystem;
if (barWidth < 1) throw new IllegalArgumentException("The arguments you passed caused the bar width to be 0."); if (barWidth < 1) throw new IllegalArgumentException("The arguments you passed caused the bar width to be 0.");
binsPerBar = (pcm.getFrequencyBins().length/barCount); binsPerBar = (pcm.getFrequencyBins().length/barCount);
this.width = width; this.width = width;
this.height = height; this.height = height;
amplitudes = new int[barCount]; amplitudes = new float[barCount];
barHeights = new int[barCount]; barHeights = new int[barCount];
shapeRenderer = new ShapeRenderer(); shapeRenderer = new ShapeRenderer();
} }
@ -45,26 +46,29 @@ public class DoubleHorizontalVisualizer {
for (int freq = bar*binsPerBar; freq < (bar*binsPerBar) + binsPerBar; freq++) { for (int freq = bar*binsPerBar; freq < (bar*binsPerBar) + binsPerBar; freq++) {
normalizedAmplitude += Math.abs(pcm.getFrequencyBins()[freq]); normalizedAmplitude += Math.abs(pcm.getFrequencyBins()[freq]);
} }
amplitudes[bar] = (int) (normalizedAmplitude*multiplier); amplitudes[bar] += normalizedAmplitude * normalFactor;
amplitudes[bar] /= binsPerBar; amplitudes[bar] /= binsPerBar;
barHeights[bar] += Math.max(0, (amplitudes[bar] - barHeights[bar]) * barRate * delta);
if (barHeights[bar] > amplitudes[bar]) barHeights[bar] = amplitudes[bar];
} }
for (int bar = 0; bar < barHeights.length; bar++) { for (int bar = 0; bar < barHeights.length; bar++) {
int smoothCount = 1; int smoothCount = 1;
for (int range = 0; range < smoothRange; range++) { for (int range = 0; range < smoothRange; range++) {
if (bar+range < amplitudes.length) { if (bar+range < amplitudes.length) {
barHeights[bar] += amplitudes[bar+range]; amplitudes[bar] += amplitudes[bar+range];
smoothCount++; smoothCount++;
} }
if (bar-range > 0) { if (bar-range > 0) {
barHeights[bar] += amplitudes[bar-range]; amplitudes[bar] += amplitudes[bar-range];
smoothCount++; smoothCount++;
} }
} }
barHeights[bar] /= smoothCount; amplitudes[bar] /= smoothCount;
int pixelsMoved = MathUtils.round(amplitudes[bar] - barHeights[bar]);
pixelsMoved = MathUtils.floor(pixelsMoved*delta*barChangeRate);
barHeights[bar] += pixelsMoved;
if (barHeights[bar] < 0) barHeights[bar] = 0;
if (barHeights[bar] > height) barHeights[bar] = height;
} }
} }
@ -113,4 +117,9 @@ public class DoubleHorizontalVisualizer {
public int getY() { public int getY() {
return y; return y;
} }
@Override
public void dispose() {
pcm.dispose();
}
} }

View File

@ -1,6 +1,8 @@
package zero1hd.rhythmbullet.audio.visualizer; package zero1hd.rhythmbullet.audio.visualizer;
public interface PCMSystem { import com.badlogic.gdx.utils.Disposable;
public interface PCMSystem extends Disposable {
float[] getFrequencyBins(); float[] getFrequencyBins();

View File

@ -2,8 +2,10 @@ package zero1hd.rhythmbullet.desktop.audio;
import static org.lwjgl.openal.AL10.alGetSourcef; import static org.lwjgl.openal.AL10.alGetSourcef;
import java.math.RoundingMode;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ShortBuffer; import java.nio.ShortBuffer;
import java.text.DecimalFormat;
import java.util.Observable; import java.util.Observable;
import java.util.Observer; import java.util.Observer;
@ -11,27 +13,29 @@ 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.math.MathUtils;
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 edu.emory.mathcs.jtransforms.fft.FloatFFT_1D;
import zero1hd.rhythmbullet.audio.MusicController; import zero1hd.rhythmbullet.audio.MusicController;
import zero1hd.rhythmbullet.audio.visualizer.BasicFFT;
import zero1hd.rhythmbullet.audio.visualizer.PCMSystem; import zero1hd.rhythmbullet.audio.visualizer.PCMSystem;
public class PCMObtainer implements Observer, Disposable, PCMSystem { public class PCMObtainer implements Observer, PCMSystem {
private int windowSize = 1024; private int windowSize = 1024;
private int sampleRate;
private long millisPerWindow;
private DecimalFormat df;
private float[] PCM = new float[windowSize]; private float[] PCM = new float[windowSize];
private float[] frequencyBins = new float[windowSize / 2]; private float[] frequencyBins = new float[windowSize / 2];
private BasicFFT fft = new BasicFFT(windowSize); private FloatFFT_1D fft = new FloatFFT_1D(windowSize);
private ShortBuffer playingBuffer; private ShortBuffer playingBuffer;
private ShortBuffer compareBuffer; private ShortBuffer compareBuffer;
private ShortBuffer buffer; private ShortBuffer buffer;
private int sourceID; private int sourceID;
private int channelCount; private int channelCount;
private int sampleRate;
private MusicController mc; private MusicController mc;
private BufferStreamReadThread streamReadThread; private BufferStreamReadThread streamReadThread;
private int windowsRead; private int windowsRead;
@ -51,6 +55,9 @@ public class PCMObtainer implements Observer, Disposable, PCMSystem {
} }
streamReadThread = new BufferStreamReadThread(); streamReadThread = new BufferStreamReadThread();
df = new DecimalFormat("#.###");
df.setRoundingMode(RoundingMode.HALF_EVEN);
} }
private synchronized void calcPCMData() { private synchronized void calcPCMData() {
@ -116,7 +123,8 @@ public class PCMObtainer implements Observer, Disposable, PCMSystem {
channelCount = mc.getCurrentMusicHeader().getChannelCount(); channelCount = mc.getCurrentMusicHeader().getChannelCount();
sampleRate = mc.getCurrentMusicHeader().getSampleRate(); sampleRate = mc.getCurrentMusicHeader().getSampleRate();
String millisPerWindowF = df.format(windowSize/(float) sampleRate);
millisPerWindow = (long) (Float.valueOf(millisPerWindowF)*1000);
playingBuffer = ShortBuffer.allocate(buffer.capacity() * 2); playingBuffer = ShortBuffer.allocate(buffer.capacity() * 2);
buffer.rewind(); buffer.rewind();
playingBuffer.put(buffer); playingBuffer.put(buffer);
@ -134,7 +142,6 @@ public class PCMObtainer implements Observer, Disposable, PCMSystem {
public float[] getFrequencyBins() { public float[] getFrequencyBins() {
if (updated) { if (updated) {
synchronized (this) { synchronized (this) {
fft.fft(PCM);
System.arraycopy(PCM, 1, frequencyBins, 0, frequencyBins.length); System.arraycopy(PCM, 1, frequencyBins, 0, frequencyBins.length);
} }
} }
@ -150,34 +157,50 @@ public class PCMObtainer implements Observer, Disposable, PCMSystem {
private String name = "PCM-Audio-Processing"; private String name = "PCM-Audio-Processing";
private Thread thread; private Thread thread;
private volatile boolean run = true; private volatile boolean run = true;
private boolean paused;
private long timeOfLastRead; private long timeOfLastRead;
private int waitTime; private long waitTime;
private int syncC, normC;
private float syncPercentage;
@Override @Override
public void run() { public void run() {
while (run) { while (run) {
if (mc.isPlaying()) { if (mc.isPlaying()) {
if (paused) { //record time of read
timeOfLastRead = TimeUtils.millis(); timeOfLastRead = TimeUtils.millis();
paused = false;
}
waitTime = sampleRate / windowSize / Gdx.graphics.getFramesPerSecond();
if (TimeUtils.timeSinceMillis(timeOfLastRead) >= waitTime) {
calcPCMData();
updated = true;
windowsRead++;
timeOfLastRead = TimeUtils.millis();
currentPlaybackWindow = (int) ((mc.getCurrentPosition() * sampleRate) / windowSize); //calculate current pcm data and notify that there is new data
if (windowsRead != currentPlaybackWindow) { calcPCMData();
synchronizeBufferWithPlayback(); fft.realForward(PCM);
updated = true;
windowsRead++;
//contemplate synchronization
try {
currentPlaybackWindow = MathUtils.round((mc.getCurrentPosition() * sampleRate) / windowSize);
} catch (UnsatisfiedLinkError ule) {
if (run) {
ule.printStackTrace();
} }
} }
if (windowsRead != currentPlaybackWindow) {
synchronizeBufferWithPlayback();
syncC++;
} else {
normC++;
}
syncPercentage = (syncC*100)/(float)(normC+syncC);
System.out.printf("%.2f", syncPercentage);
System.out.print("%\n");
//wait for a bit before reading again depending on the speed at which the system does playback.
waitTime = Math.max(0, millisPerWindow - TimeUtils.timeSinceMillis(timeOfLastRead));
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else { } else {
synchronized (this) { synchronized (this) {
try { try {
paused = true;
wait(); wait();
} catch (InterruptedException e) { } catch (InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
@ -199,6 +222,7 @@ public class PCMObtainer implements Observer, Disposable, PCMSystem {
} }
public void stop() { public void stop() {
Gdx.app.debug("PCMObtainer", "stopping " + thread.getName());
run = false; run = false;
} }
} }

View File

@ -91,7 +91,7 @@ public class GraphicsOptions extends Table {
pack(); pack();
} }
public void save() { public void saveOptions() {
Gdx.app.debug("Preferences", "Saved shading values values."); Gdx.app.debug("Preferences", "Saved shading values values.");
prefs.putInteger("glow shader", (int) glowShaderLevel.getValue()); prefs.putInteger("glow shader", (int) glowShaderLevel.getValue());
} }

View File

@ -34,7 +34,10 @@ public class Page extends Group implements Disposable {
} }
@Override @Override
protected void setStage(Stage stage) { public void setStage(Stage stage) {
if (stage == null && !hasParent()) {
dispose();
}
if (!(stage.getViewport() instanceof ScreenViewport)) { if (!(stage.getViewport() instanceof ScreenViewport)) {
throw new IllegalArgumentException("Pages are explicitly for GUIs, and thus should have a 1:1 ratio between pixel and texture size for maximum clarity. This means that the stage should be using a ScreenViewport."); throw new IllegalArgumentException("Pages are explicitly for GUIs, and thus should have a 1:1 ratio between pixel and texture size for maximum clarity. This means that the stage should be using a ScreenViewport.");
} }
@ -50,7 +53,16 @@ public class Page extends Group implements Disposable {
setPosition(baseXPos*getWidth(), baseYPos*getHeight()); setPosition(baseXPos*getWidth(), baseYPos*getHeight());
} }
@Override
public void setParent(Group parent) {
if (parent == null && getStage() == null) {
dispose();
}
super.setParent(parent);
}
@Override @Override
public void dispose() { public void dispose() {
Gdx.app.debug(getClass().getSimpleName(), "Disposing...");
} }
} }

View File

@ -41,8 +41,8 @@ public class GraphicsPage extends Page {
super.act(delta); super.act(delta);
} }
public void save() { public void saveOptions() {
graphicsTable.save(); graphicsTable.saveOptions();
} }
public int getBloomLevel() { public int getBloomLevel() {

View File

@ -111,6 +111,7 @@ public class MainPage extends Page implements Observer {
@Override @Override
public void dispose() { public void dispose() {
dhv.dispose();
super.dispose(); super.dispose();
} }

View File

@ -99,11 +99,6 @@ public class MainScreen extends ScreenAdapter implements ResizeReadyScreen {
@Override @Override
public void preAssetLoad() { public void preAssetLoad() {
stage.clear(); stage.clear();
mainPage.dispose();
optionsPage.dispose();
creditsPage.dispose();
keybindPage.dispose();
musicSelectionPage.dispose();
bloomShader.dispose(); bloomShader.dispose();
bloomShader = null; bloomShader = null;
background = null; background = null;
@ -187,7 +182,8 @@ public class MainScreen extends ScreenAdapter implements ResizeReadyScreen {
public void saveAll() { public void saveAll() {
if (optionsPage != null) { if (optionsPage != null) {
optionsPage.saveOptions(rhythmBullet.getPreferences()); optionsPage.saveOptions();
graphicsPage.saveOptions();
rhythmBullet.getPreferences().flush(); rhythmBullet.getPreferences().flush();
} }
} }
@ -203,6 +199,7 @@ public class MainScreen extends ScreenAdapter implements ResizeReadyScreen {
@Override @Override
public void dispose() { public void dispose() {
Gdx.app.debug("Mainscreen", "disposing...");
stage.dispose(); stage.dispose();
musicMetadataController.dispose(); musicMetadataController.dispose();
screenBatch.dispose(); screenBatch.dispose();

View File

@ -22,9 +22,11 @@ public class OptionsPage extends Page {
private ProgressBar fxVolSlider; private ProgressBar fxVolSlider;
private TextField directoryField; private TextField directoryField;
private float musicSearchTimer; private float musicSearchTimer;
private Preferences prefs;
public OptionsPage(MusicController musicController, Skin skin, Preferences preferences, ChangeListener backButtonListener, ChangeListener graphicsButtonListener, ChangeListener controlsButtonListener) { public OptionsPage(MusicController musicController, Skin skin, Preferences preferences, ChangeListener backButtonListener, ChangeListener graphicsButtonListener, ChangeListener controlsButtonListener) {
super(-1, 0, "General", skin); super(-1, 0, "General", skin);
this.prefs = preferences;
//Back button //Back button
TextButton backButton = new TextButton("Back", skin); TextButton backButton = new TextButton("Back", skin);
@ -140,13 +142,14 @@ public class OptionsPage extends Page {
optionsTable.add(usageLabel).colspan(2); optionsTable.add(usageLabel).colspan(2);
} }
public void saveOptions(Preferences prefs) { public void saveOptions() {
prefs.putString("music dir", directoryField.getText()); prefs.putString("music dir", directoryField.getText());
Gdx.app.debug("Preferences", "Saved all basic options page values."); Gdx.app.debug("Preferences", "Saved all basic options page values.");
} }
@Override @Override
public void dispose() { public void dispose() {
saveOptions();
super.dispose(); super.dispose();
} }
} }