manipulating SourceDataLine

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!

You’re welcome

Because ais and ais_swap is changed by both threads during synchronize

This how it works “Raw explain”:
Every object have boolean value – [icode]is_synchronized[/icode]

When Thread enter synchronized block
Thread check


if(is_synchronized){
wait
}
is_synchronized = true;
on leave block
is_synchronized = false;

null object don’t have any Object data
and for changeable object
you have [icode]obj1, obj2[/icode]
[icode]Thread1 synchronized(obj1)[/icode]
then swap links [icode]obj1 = obj2 [/icode]
[icode]Thread2 try use synchronized(obj1)[/icode] and shi use it, because
Technically obj1 is different object hi have [icode]is_synchronized = false[/icode]
Even when Thread1 still in synchronized block above

I also reread about [icode]drain[/icode] you can use it only on Thread stop
Drain = wait, until all written data - played to end
It prevents clipping sound on Force thread Stop, but don’t have any result on filling new data to buffer