OK I finished implementing the changes, and there is a lot of improvement. There is still a little seam, but I think it just boils down to getting that Audacity edit perfect, because there is no more skip in the rhythm, which imho is more noticable that a little sound dropout. This is what I have now:
package com.noah.breakit.assets;
import java.io.IOException;
import java.net.URL;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
import com.noah.breakit.util.Util;
public class Song {
// music credit to sketchylogic
public static final Song titlesong = new Song("songs/titlesong.wav");
public static final Song playfieldsong = new Song("songs/playfieldsongintro.wav", "songs/playfieldsongbody.wav");
public static final Song briefingsong = new Song("songs/briefingsong.wav");
public static final Song gameoversong = new Song("songs/gameoversong.wav");
private static volatile boolean playing;
private boolean killThread;
private URL url;
private URL url2;
private AudioInputStream ais;
private AudioFormat baseFormat;
private AudioFormat decodeFormat;
private DataLine.Info info;
private SourceDataLine sdl;
private FloatControl gainControl;
private String name;
private Song(String filename) {
name = filename;
try {
url = this.getClass().getClassLoader().getResource(filename);
} catch (Exception e) {
e.printStackTrace();
}
}
private Song(String filename1, String filename2) {
this(filename1);
try {
url2 = this.getClass().getClassLoader().getResource(filename2);
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void loopSong() {
SoundThreadPool.execute(new Runnable() {
public void run() {
while (playing) {
} // wait for any other song threads to finish executing...
playing = true;
loop();
playing = false;
}
});
}
public synchronized void playSong() {
SoundThreadPool.execute(new Runnable() {
public void run() {
while (playing) {
} // wait for any other song threads to finish executing...
playing = true;
play();
playing = false;
}
});
}
public synchronized void playIntroLoopBody() {
playing = true;
SoundThreadPool.execute(new Runnable() {
public void run() {
while (playing) {
} // wait for any other song threads to finish executing...
playing = true;
play_intro_loop_body();
playing = false;
}
});
}
private void setup() {
try {
ais = AudioSystem.getAudioInputStream(url);
baseFormat = ais.getFormat();
decodeFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, baseFormat.getSampleRate(), 16,
baseFormat.getChannels(), baseFormat.getChannels() * 2, baseFormat.getSampleRate(), false);
info = new DataLine.Info(SourceDataLine.class, decodeFormat);
sdl = (SourceDataLine) AudioSystem.getLine(info);
sdl.open();
gainControl = (FloatControl) sdl.getControl(FloatControl.Type.MASTER_GAIN);
sdl.start();
} catch (Exception e) {
e.printStackTrace();
}
}
private void teardown(){
sdl.drain();
sdl.stop();
sdl.close();
try {
ais.close();
} catch (IOException e) {
e.printStackTrace();
}
if (killThread) killThread = false;
}
private void play() {
try {
setup();
int nBytesRead = 0;
byte[] data = new byte[sdl.getBufferSize()];
int offset;
while (true) {
nBytesRead = ais.read(data, 0, data.length);
if (nBytesRead < 0) break;
offset = 0;
while (offset < nBytesRead) {
offset += sdl.write(data, 0, nBytesRead);
}
if (killThread) break;
}
teardown();
} catch (Exception e) {
e.printStackTrace();
}
}
private void loop() {
try {
setup();
int nBytesRead = 0;
byte[] data = new byte[sdl.getBufferSize()];
int offset;
// Thread 1
AudioInputStream ais_swap = null;
while (true) {
boolean swap = false;
synchronized (sdl) {
if (ais_swap != null) {
ais = ais_swap;
ais_swap = null;
swap = true;
}
}
nBytesRead = ais.read(data, 0, data.length);
if (nBytesRead < 0) {
ais_swap = AudioSystem.getAudioInputStream(url);
swap = true;
}
if (swap) sdl.drain();
offset = 0;
while (offset < nBytesRead) {
offset += sdl.write(data, 0, nBytesRead);
}
if (killThread) break;
}
teardown();
} catch (Exception e) {
e.printStackTrace();
}
}
private void play_intro_loop_body() {
try {
setup();
int nBytesRead = 0;
byte[] data = new byte[sdl.getBufferSize()];
int offset;
// Thread 1
AudioInputStream ais_swap = null;
while (true) {
boolean swap = false;
synchronized (sdl) {
if (ais_swap != null) {
ais = ais_swap;
ais_swap = null;
swap = true;
}
}
nBytesRead = ais.read(data, 0, data.length);
if (nBytesRead < 0) {
ais_swap = AudioSystem.getAudioInputStream(url2);
swap = true;
}
if (swap) sdl.drain();
offset = 0;
while (offset < nBytesRead) {
offset += sdl.write(data, 0, nBytesRead);
}
if (killThread) break;
}
teardown();
} catch (Exception e) {
e.printStackTrace();
}
}
public void adjustGain(float gain) {
if (gainControl == null) return;
float value = Util.clamp((gainControl.getValue() + gain), gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(value);
}
public void setGain(float gain) {
if (gainControl == null) return;
gainControl.setValue(Util.clamp(gain, gainControl.getMinimum(), gainControl.getMaximum()));
}
public boolean atMin() {
if (gainControl == null) return false;
return gainControl.getValue() == gainControl.getMinimum();
}
public boolean atMax() {
if (gainControl == null) return false;
return gainControl.getValue() == gainControl.getMaximum();
}
public boolean isPlaying() {
return playing;
}
public boolean fadeToBlack() {
adjustGain(-0.4f);
if (atMin()) {
killThread = true;
System.out.println(name + " killThread set to true...");
}
return atMin();
}
}
I’m pleased with the results. I think the only thing I can do now is go into Audacity and get those edits juuust right…
Major thanks to @philfrei and @Icecore for their willingness to share knowledge and expertise!