I have an issue with playing sound in my game. When the Thread that handles the sound playback exits it's run method it doesn't terminate/end/stop. I know it's this method that causes the problem, since when I comment the whole thing away no more Threads get created. (Checked with JVisualVM). The problem is that Threads do not get terminated after exiting the run method. I've placed a print command to ensure that it actually reaches the end of the run() method, and it always does.
However, when I check the process with JVisualVM, the thread count grows by 1 for each sound played. I also noted that the number of daemon threads is increased by 1 for each sound played. I am not sure what daemon threads are and how they work, but I've tried to kill the Thread in a number of ways. Including Thread.currentThread .stop() .destroy() .suspend() .interrupt() and returning from the run() method by return;
While writing this message I realised I need to close the clip object. This resulted in no extra threads being created and sustained. However, now the sound sometimes disappears and I have no idea why. Right now, I can choose between having sound in parallel and see my cpu get overloaded by an endless number of threads or have the sounds end abruptly whenever a new sound is played.
If anyone knows of a different approach of playing multiple sounds in parallel or knows what's wrong with my code, I would greatly appreciate any help.
Here is the method:
public static synchronized void playSound(final String folder, final String name) {
new Thread(new Runnable() { // the wrapper thread is unnecessary, unless it blocks on the Clip finishing, see comments
#Override
public void run() {
Clip clip = null;
AudioInputStream inputStream = null;
try{
do{
if(clip == null || inputStream == null)
clip = AudioSystem.getClip();
inputStream = AudioSystem.getAudioInputStream(SoundP.class.getResource(folder + "/" + name));
if(clip != null && !clip.isActive())
inputStream = AudioSystem.getAudioInputStream(SoundP.class.getResource(folder + "/" + name));
clip.open(inputStream);
clip.start();
}while(clip.isActive());
inputStream.close();
} catch (LineUnavailableException e) {
e.printStackTrace();
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
A few things about Java Threads:
A Thread always dies when exiting it's Run() method. In your case, other threads are created inside the methods you called, but your thread ends (you can check it by naming it and see when it dies).
Never kill a thread using .stop(), .destroy() or .suspend(). These methods are deprecated and should not be used. Instead, you should basically get to the end of the Run() method. That's what Thread.interrupt() is for, but you'll have to support interrupting your thread by checking the Thread.isInterrupted() flag and then throwing InterruptedException and handling it (for more details see How to Stop a Thread).
"A daemon thread is a thread, that does not prevent the JVM from exiting when the program finishes but the thread is still running".
A few things about your code:
You have missing curly braces as mentioned by many users
I didn't quite understand what you're trying to achieve, but the do-while loop seems redundant. There are other good ways to wait for the sound to finish playing (if that's your goal), and a loop is not one of them. A while loop running many times without Sleeping, eats up your CPU for no good reason.
You should Close() (and Stop()) the Clip as you mentioned, in order to free system resources.
A working example with debug notes:
Try running this code, and see if it meets your requirements. I've added some thread methods calls and some System.out.prints for you to see when every bit of code happens. Try playing with tryToInterruptSound and mainTimeOut to see how it effects the output.
import java.io.File;
import java.io.IOException;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
public class PlaySound {
private static boolean tryToInterruptSound = false;
private static long mainTimeOut = 3000;
private static long startTime = System.currentTimeMillis();
public static synchronized Thread playSound(final File file) {
Thread soundThread = new Thread() {
#Override
public void run() {
try{
Clip clip = null;
AudioInputStream inputStream = null;
clip = AudioSystem.getClip();
inputStream = AudioSystem.getAudioInputStream(file);
AudioFormat format = inputStream.getFormat();
long audioFileLength = file.length();
int frameSize = format.getFrameSize();
float frameRate = format.getFrameRate();
long durationInMiliSeconds =
(long) (((float)audioFileLength / (frameSize * frameRate)) * 1000);
clip.open(inputStream);
clip.start();
System.out.println("" + (System.currentTimeMillis() - startTime) + ": sound started playing!");
Thread.sleep(durationInMiliSeconds);
while (true) {
if (!clip.isActive()) {
System.out.println("" + (System.currentTimeMillis() - startTime) + ": sound got to it's end!");
break;
}
long fPos = (long)(clip.getMicrosecondPosition() / 1000);
long left = durationInMiliSeconds - fPos;
System.out.println("" + (System.currentTimeMillis() - startTime) + ": time left: " + left);
if (left > 0) Thread.sleep(left);
}
clip.stop();
System.out.println("" + (System.currentTimeMillis() - startTime) + ": sound stoped");
clip.close();
inputStream.close();
} catch (LineUnavailableException e) {
e.printStackTrace();
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
System.out.println("" + (System.currentTimeMillis() - startTime) + ": sound interrupted while playing.");
}
}
};
soundThread.setDaemon(true);
soundThread.start();
return soundThread;
}
public static void main(String[] args) {
Thread soundThread = playSound(new File("C:\\Booboo.wav"));
System.out.println("" + (System.currentTimeMillis() - startTime) + ": playSound returned, keep running the code");
try {
Thread.sleep(mainTimeOut );
} catch (InterruptedException e) {
e.printStackTrace();
}
if (tryToInterruptSound) {
try {
soundThread.interrupt();
Thread.sleep(1);
// Sleep in order to let the interruption handling end before
// exiting the program (else the interruption could be handled
// after the main thread ends!).
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("" + (System.currentTimeMillis() - startTime) + ": End of main thread; exiting program " +
(soundThread.isAlive() ? "killing the sound deamon thread" : ""));
}
}
playSound runs on a daemon thread, so that when the main (and only non-daemon) Thread ends, it stops.
I have calculated the sound file length according to this guy, so that I know in advanced how long to keep playing the Clip. This way I can let the Thread Sleep() and don't use the CPU. I use an additional isActive() as a test to see if it really ended, and if not - calculate the remaining time and Sleep() again (the sound will probably still be playing after the first Sleep due to two facts: 1. the length calculation doesn't take microseconds into consideration, and 2. "you cannot assume that invoking sleep will suspend the thread for precisely the time period specified").
Your code is actually this
public static synchronized void playSound(final String folder, final String name) {
new Thread(new Runnable() { // the wrapper thread is unnecessary, unless it blocks on the Clip finishing, see comments
#Override
public void run() {
Clip clip = null;
AudioInputStream inputStream = null;
try{
do{
if(clip == null || inputStream == null){
clip = AudioSystem.getClip();
}
inputStream = AudioSystem.getAudioInputStream(SoundP.class.getResource(folder + "/" + name));
if(clip != null && !clip.isActive()){
inputStream = AudioSystem.getAudioInputStream(SoundP.class.getResource(folder + "/" + name));
}
clip.open(inputStream);
clip.start();
}while(clip.isActive());
inputStream.close();
} catch (LineUnavailableException e) {
e.printStackTrace();
} catch (UnsupportedAudioFileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
The if statements are only working on the first command following them. The second 'if' is pointless as it is, as the statement has already run. It looks to me like every time you loop through the do the clip is '.start'ed again spawning another thread regardless of whether the clip is active or not.
Related
Good afternoon all,
I'm working with Java's semaphore and concurrency for a school project and had a few questions regarding how it works!
If there are no permits available, I need the thread to exit the "queue" - not just sleep until one is ready. Is this possible? As you can see in my try, catch, finally - there is no handle for this event:
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " aquired for 3 seconds " + semaphore.toString());
Thread.sleep(3000);
}
catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " released " + semaphore.toString());
}
Daniel brought up the tryAquire function - this looks great but the tutorials I have read state that semaphores require a try, catch, finally block to prevent a deadlock. My current code (implementing tryAquire) will release in the finally block even if that thread was never acquired. Do you have any suggestions?
public void seatCustomer(int numBurritos) {
try {
if(semaphore.tryAcquire()) {
System.out.println(Thread.currentThread().getName() + " aquired for 3 seconds " + semaphore.toString());
Thread.sleep(3000);
} else {
System.out.println(Thread.currentThread().getName() + " left due to full shop");
}
}
catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " released " + semaphore.toString());
}
}
I suggest you read the JavaDocs for Semaphor. In particular, look at the tryAcquire method.
Acquires a permit from this semaphore, only if one is available at the time of invocation.
Acquires a permit, if one is available and returns immediately, with the value true, reducing the number of available permits by one.
If no permit is available then this method will return immediately with the value false.
What this means is you can try to acquire a permit if any are available. If none are available, this method returns false immediately instead of blocking.
You'll have to make your "finally" block a little smarter.
boolean hasPermit = false;
try {
hasPermit = semaphore.tryAcquire();
if (hasPermit) {
// do stuff.
}
} finally {
if (hasPermit) {
semaphore.release();
}
}
I'm reading a server log file after an event is performed on the UI. I have a while loop which waits for certain conditions to match and then returns that line of the log. Sometimes, however, there's a case where an event occurs before the code looks at the log and cannot get the new line. This causes the while loop to just hang and this hangs until another event occurs with the provided conditions. This is problematic for obvious reasons. Is there a way to break out of the while loop after a few seconds no matter what the case maybe? Following is my code
public String method(String, a, String b, String c) {
channel = session.openChannel("exec");
((ChannelExec) channel).setCommand(a + "\n" + b);
channel.connect();
fromServer = new BufferedReader (new InputStreamReader(channel.getInputStream()));
String time = methodToFireAnEventInUI();
Thread.sleep(2000);
String x = null;
while (true){
x = fromServer.readLine();
if(!x.equals(null) && x.contains(c) && x.contains(time)){
break;
}
}
msg = x.toString();
}catch (Exception e){
e.printStackTrace();
}
closeConnection();
return msg;
}
If you look at the above code, it hangs right at "x = fromServer.readline();" and just doesn't go anywhere, and that is where I want the logic for it to wait for an x amount of time and just abort the loop after that.
My attempt of "thread.sleep" ahead of the while loop doesn't work either.
You can put this logic in a separate thread and use a while like this:
class TestThread extends Thread {
#Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
method();
}
}
public void method() {
try {
// this method hangs. You can replace it with your method
while (true) {
sleep(100);
}
} catch (Exception e) {
System.out.println("Thread is interrupted");
}
}
}
After that you can interrupt this thread if it takes longer than some time frame like this:
public static void main(String[] args) throws Exception {
TestThread t1 = new TestThread();
long startTime = System.currentTimeMillis();
t1.start();
long currentTime = System.currentTimeMillis();
while (currentTime - startTime < 5000) { // you can decide the desired interval
sleep(1000); // sleep some time
currentTime = System.currentTimeMillis();
System.out.println(currentTime); //print this to ensure that the program is still running
}
t1.interrupt(); //interrupt the thread
}
How about simply:
long timeOut = System.currentTimeMillis() + 5000; // change the value to what ever(millis)
while(System.currentTimeMillis() < timeOut){
// do whatever
}
As your while loop blocks at "x = fromServer.readline();" you can just share the reader instance to another thread and make that thread close the reader after timeout. This will cause your readLine to throw exception which you can handle and proceed.
Find answer here:
How do I measure time elapsed in Java?
Try the approach below:
long startTime = System.nanoTime(); //fetch starting time
while(true ||(System.nanoTime()-startTime)<200000)
{
// do something
}
I have some old code I am working with, and I'm not too experienced with Threads (mostly work on the front end). Anyway, this Thread.sleep is causing the thread to hang and I'm unsure what to do about it. I thought about using a counter and throwing a Thread.currentThread.interupt, but unsure of where to put it or which thread it will interupt. Here is an example of the dump. As you can see the thread count is getting pretty high at 1708.
Any advice?
"Thread-1708" prio=6 tid=0x2ceec400 nid=0x2018 waiting on condition
[0x36cdf000] java.lang.Thread.State: TIMED_WAITING (sleeping) at
java.lang.Thread.sleep(Native Method) Locked ownable synchronizers:
- None "Thread-1707" prio=6 tid=0x2d16b800 nid=0x215c waiting on condition [0x36c8f000] java.lang.Thread.State: TIMED_WAITING
(sleeping) at java.lang.Thread.sleep(Native Method) Locked ownable
synchronizers:
- None
#Override
public void run()
{
Connection con = null;
int i = 0;
while (is_running)
{
try
{
con = ConnectionManager.getConnection();
while (!stack.isEmpty())
{
COUNT++;
String line = (String) stack.pop();
getPartMfr(line);
try
{
if (this.mfr != null && !this.mfr.equals(EMPTY_STR))
{
lookupPart(con, line);
}
}
catch (SQLException e)
{
e.printStackTrace();
}
if (COUNT % 1000 == 0)
{
Log log = LogFactory.getLog(this.getClass());
log.info("Processing Count: " + COUNT);
}
}
}
catch (NamingException e)
{
e.printStackTrace();
}
catch (SQLException e)
{
e.printStackTrace();
}
finally
{
try
{
ConnectionManager.close(con);
}
catch (SQLException e)
{
e.printStackTrace();
}
}
try {
Thread.sleep(80);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.finished = true;
}
Here is where it calls the run method, as you can see it does set it to false, but I guess it is missing threads?
HarrisWorker w[] = new HarrisWorker[WORKER_POOL_SIZE];
try
{
for (int i = 0; i < w.length; i++)
{
w[i] = new HarrisWorker(pw);
w[i].start();
}
pw.println(headers());
File inputDir = new File(HARRIS_BASE);
String files[] = inputDir.list();
for (String file : files)
{
try
{
File f = new File(HARRIS_BASE + File.separator + file);
if (f.isDirectory())
continue;
final String workFile = workDir + File.separator + file;
f.renameTo(new File(workFile));
FileReader fr = new FileReader(workFile);
BufferedReader br = new BufferedReader(fr);
String line = br.readLine();
boolean firstLine = true;
while (line != null)
{
if (firstLine)
{
firstLine = false;
line = br.readLine();
continue;
}
if (line.startsWith(","))
{
line = br.readLine();
continue;
}
// if(line.indexOf("103327-1") == -1)
// {
// line = br.readLine();
// continue;
// }
HarrisWorker.stack.push(line);
line = br.readLine();
}
br.close();
fr.close();
for (int i = 0; i < w.length; i++)
{
w[i].is_running = false;
while (!w[i].finished)
{
Thread.sleep(80);
}
}
move2Processed(file, workFile);
long etime = System.currentTimeMillis();
System.out.println("UNIQUE PARTS TOTAL FOUND: " + HarrisWorker.getFoundCount() + " of " + HarrisWorker.getUniqueCount() + ", "
+ (HarrisWorker.getFoundCount() / HarrisWorker.getUniqueCount()));
System.out.println("Time: " + (etime - time));
}
catch (Exception e)
{
e.printStackTrace();
File f = new File(workDir + File.separator + file);
if (f.exists())
{
f.renameTo(new File(HARRIS_BASE + File.separator + ERROR + File.separator + file));
}
}
}
}
As a direct answer to the question in your title - nowhere. There is nowhere in this code that needs a Thread.interrupt().
The fact that the thread name is Thread-1708 does not necessarily mean there are 1708 threads. One can choose arbitrary names for threads. I usually include the name of the executor or service in the thread name. Maybe 1600 are now long stopped and there are only around a hundred alive. Maybe this particular class starts naming at 1700 to distinguish from other uses.
1708 threads may not be a problem. If you have a multi-threaded server that is serving 2000 connections in parallel, then it certainly expectable that there are 2000 threads doing that, along with a bunch of other threads.
You have to understand why the sleep is there and what purpose it serves. It's not there to just hog memory for nothing.
Translating the code to "plaintext" (btw it can be greatly simplified by using try-with-resources to acquire and close the connection):
Acquire a connection
Use the connection to send (I guess) whatever is in the stack
When failed or finished - wait 80ms (THIS is your sleep)
If run flag is still set - repeat from step 1
Finish the thread.
Now reading through this, it's obvious that it's not the sleep that's the problem. It's that the run flag is not set to false. And your thread just continues looping, even if it can't get the connection at all - it will simply spend most of its time waiting for the retry. In fact - even if you completely strip the sleep out (instead of interrupting it mid-way), all you will achieve is that the Threads will start using up more resources. Given that you have both a logger and you print to stdout via printStackTrace, I would say that you have 2 problems:
Something is spawning threads and not stopping them afterwards (not setting their run flag to false when done)
You are likely getting exceptions when getting the Connection, but you never see them in the log.
It might be that the Thread is supposed to set it's own run flag (say when the stack is drained), but you would have to decide that yourself - that depends on a lot of specifics.
Not an answer but some things you should know if you are writing code for a live, production systemn:
:-( Variable and method both have the same name, run. A better name for the variable might be keep_running Or, change the sense of it so that you can write while (! time_to_shut_down) { ... }
:-( Thread.sleep(80) What is this for? It looks like a big red flag to me. You can never fix a concurrency bug by adding a sleep() call to your code. All you can do is make the bug less likely to happen in testing. That means, when the bug finally does bite, it will bite you in the production system.
:-( Your run() method is way too complicated (the keyword try appears four times). Break it up, please.
:-( Ignoring five different exceptions catch (MumbleFoobarException e) { e.printStackTrace(); } Most of those exceptions (but maybe not the InterruptedException) mean that something is wrong. Your program should do something more than just write a message to the standard output.
:-( Writing error messages to standard output. You should be calling log.error(...) so that your application can be configured to send the messages to someplace where somebody might actually see them.
I have a thread that copies a file and give the progress bar the progress, I have a method the stops the filestream copy and kills the thread, I am trying to implement a jOptionPanel yes_no to ask the user if they are sure they want to delete. But i need to be able to pause the fileInputStream copy until the thread either stops or is continued. The thread waits but the file copy keeps going. how can i fix this? Any help would be great
First is the thread I want to pause
thr = new Thread("cool") {
#Override
public void run() {
while (running == true) {
try {
newWorkFileName();
long length = fileIn.length();
long counter = 1;
int r;
byte[] b = new byte[1024];
fin = new FileInputStream(fileIn);
fout = new FileOutputStream(fileOut);
tvConvertInterface.updateJLabel2("Creating File");
while ((r = fin.read(b)) != -1) {
counter += r;
fout.write(b, 0, r);
prog = (int) Math.round(100 * counter / length);
tvConvertInterface.updateJProgressbar1(prog);
}
fin.close();
fout.close();
tvConvertInterface.disableStopBtn();
tvConvertInterface.updateJLabel2("Work File Created");
running = false;
} catch (NullPointerException | IOException ex) {
tvConvertInterface.disableStopBtn();
tvConvertInterface.updateJLabel2("File is not valid");
Logger.getLogger(TvConvertGutts.class.getName()).log(Level.SEVERE, null, ex);
tvConvertInterface.updateJProgressbar1(0);
running = false;
}
}
}
};
Next is the Method that i want to implement the jOptionPanel
public boolean terminate() throws IOException, InterruptedException {
synchronized (thr) {
thr.wait();//MAKES THREAD WAIT
//I NEED TO PAUSE THE FILESTREAM AND BE ABLE TO RESUME
}
int n = JOptionPane.showConfirmDialog(
null, "SURE YOU WANT TO DELETE?",
"Kill It Dude",
JOptionPane.YES_NO_OPTION);
if (n == JOptionPane.YES_OPTION) {
fin.close(); //CLOSE INPUT STREAM
fout.flush();//FLUSH OUTPUT STREAM
fout.close(); //CLOSE OUTPUT STREAM
thr.interrupt();//KIILL THE THREAD
System.out.println(thr.getState());//PRINT STATE OF THREAD
TvConvertInterface bst = new TvConvertInterface(null, true);
bst.disableStopBtn();//SET PROGRESSBAR TO 0
} else if (n == JOptionPane.NO_OPTION) {
synchronized (thr) {
thr.notify();//DONT DELETE AND CONTINUE
}
} else {
System.out.println("spewin");
}
return false;
}
This terminate method is called by a button click. From a class called TvConvertInterface
private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {
try {
check.terminate();
} catch (IOException ex) {
Logger.getLogger(TvConvertInterface.class.getName()).log(Level.SEVERE, null, ex);
}
You are using the JAVA concurrency API incorrectly.
The object.wait() will pause the current thread and the object in this case would serve as a monitor object.
What you need to do is the following:
define a boolean shouldPauseThread variable
the while loop should check for the value in this variable; if it is true you should wait on a monitor object
if the user approves the cancelling, then the thread should end its execution, so there needs to be a check for another variable boolean stopThread
if the user wants to continue with processing, you will just reset the shouldPauseThread variable and notify on the previously used monitor object
you need to handle all the transitions from the mentioned states
I have searched and searched, and have not found the cause of my problem.
Setting: I am trying to play a list of clips in sequence indefinitely till the GUI of the program tells to stop. Also, the user can adjust the volume while the clips are being played.
Let's say I have clips A, B and C. The sequence of play will be ABCABCABCABC.....
Problem: The problem is that the first time clips A, B and C start - there are POPs - after that for the GUI tells to stop, there are no POPs.
Code: In the run() method of the thread, get all filenames of the clips, create all clips and save them in a LinkedHashSet. Next, play these clips in loop, creating the volume control object for each.
My Observation: The offending lines are commented (with // TODO: UNCOMMENT ME!!!!!) With the commented lines, there are no POPs, but the volume control is disabled. When these lines are uncommented, the POPs return.
Question: Where am I going wrong? I don't have any hair on the head to pull out! :)
private LinkedHashSet<Clip> clips = new LinkedHashSet<Clip>();
private FloatControl volControl = null;
.
.
.
.
#Override
public void run() {
List<String> fileNames = smd.getFiles();
if ( !(numFiles.isEmpty()) ) {
try {
// Create all clips and save in LinkedHashSet
//
for (String s: fileNames) {
clips.add(CreateClip(s));
}
// Play each clip till GUI says to stop
//
while (smd.getStopPlayStatus()) {
for (Clip c: clips) {
// Calculate and set the clip volume to what GUI says
//
float dB = (float) (Math.log(smd.getSwarMVolume()/100f) / Math.log(10.0) * 80.0);
System.out.println(" vol = " + volumeLevel/100f + " || dB = " + dB);
Control[ ] ctls = c.getControls();
for (Control ctl : ctls) {
if (ctl.toString().toLowerCase().contains("master gain")) {
volControl = (FloatControl) ctl;
break;
}
}
// volControl.setValue(dB); // TODO: UNCOMMENT ME!!!!!!!!
// Play the clip
//
loop(c, 0);
}
}
} catch (ConcurrentModificationException ex) {
}
}
}
void loop(Clip clip, int times){
if (times == -1) {
clip.loop(Clip.LOOP_CONTINUOUSLY);
} else {
clip.loop(times);
}
while (clip.isRunning()) {
float dB = (float) (Math.log(smd.getVolume()/100f) / Math.log(10.0) * 80.0);
// volControl.setValue(dB); // TODO: UNCOMMENT ME!!!!!!!!
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
clip.drain();
}
private Clip CreateClip(String fileName) {
Clip c = null;
try {
File file = new File(fileName);
AudioInputStream sound = AudioSystem.getAudioInputStream(file);
AudioFormat format = sound.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, format);
c = (Clip) AudioSystem.getLine(info);
c.addLineListener(this);
c.open(sound);
} catch (MalformedURLException ex) {
} catch (UnsupportedAudioFileException ex) {
} catch (IOException ex) {
} catch (LineUnavailableException ex) {
}
return c;
}
The volume controls that are provided are very crude. When you set a new volume, the entire change happens all at once. This can create a discontinuity in the sound that results in a pop.
To eliminate this, one can try making the volume change more gradually. Sometimes this works. You will have to tinker with it to find the best trade-off of the amount of time needed for a transition and smoothness.
There are several issues that pertain. (1) The "distance" one can travel in a single volume change without a pop can be different at the low end versus the high end, depending on the scaling in use. (2) The number of changes you can make over a period of time is limited by the buffer size. You can only make one change per buffer. Thus decreasing the buffer size will allow more granularity in the volume changes, but will increase the risk of drop-outs.
Frankly, I just threw up my hands and gave up using the provided volume lines. Instead, I program the changes to transition on a per-frame basis. This can be done by taking each buffer and converting the bytes to PCM, doing the volume multiplication on the individual frames and converting back to bytes. The processing cost turns out to be pretty minor.