JDK-6745051 : SourceDataLine underrun does not generate documented events or state.
  • Type: Bug
  • Component: client-libs
  • Sub-Component: javax.sound
  • Affected Version: 6u10
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: windows_2000
  • CPU: x86
  • Submitted: 2008-09-05
  • Updated: 2021-07-13
Related Reports
Relates :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.6.0_10-rc"
Java(TM) SE Runtime Environment (build 1.6.0_10-rc-b28)
Java HotSpot(TM) Client VM (build 11.0-b15, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows 2000 [Version 5.00.2195]

EXTRA RELEVANT SYSTEM CONFIGURATION :
(from dxdiag.exe)
DirectX 9.0c (4.09.0000.0904)

Device: SoundMAX Digital Audio
Driver: smwdm.sys 5.12.0001.3508 (English)

A DESCRIPTION OF THE PROBLEM :
This bug deals with the javax.sound.sampled package. The sub-category chosen was the only one that addressed sound, even though this package was not listed. The listed "java.media.audio.*" package does not exist. Hopefully, this is the right choice.

The API documentation for javax.sound.sampled.SourceDataLine states:
"If the delivery of audio output stops due to underflow, a STOP event is generated. A START event is generated when the audio output resumes."

The description of the DataLine.isActive() method states:
"Indicates whether the line is engaging in active I/O (such as playback or capture). When an inactive line becomes active, it sends a START event to its listeners. Similarly, when an active line becomes inactive, it sends a STOP event."

The description of the DataLine.isRunning() method states:
"Indicates whether the line is running. The default is false. An open line begins running when the first data is presented in response to an invocation of the start method, and continues until presentation ceases in response to a call to stop or because playback completes."

A SourceDataLine is created, opened, and is presented with data with the following format (as given by AudioFormat.toString()):
PCM_SIGNED 11025.0 Hz, 16 bit, mono, 2 bytes/frame, little-endian

When the last data is written to the line and playback of that data finishes, a buffer underflow condition occurs. According to the documentation quoted above, the Active state should be changed to false (!isActive()), the Running state should be changed to false (!isRunning()), and a STOP event should occur.

This never happens. No event is generated on a SourceDataLine buffer underflow, and the Active and Running states never change. If you add a LineListener to the line, it will never see a STOP event when the buffer underflow occurs. This means you can not use wait() after writing all of the sound data since the LineListener will never see a STOP event to call a notify().

The only way around this is to use the drain() method. This is bad because it will never return if playback is stopped for whatever reason before the line buffer is used up resulting in thread deadlock. You would have to add a LineListener anyway to call a flush() when a STOP event occurs to allow drain() to return.

The drain() method is also a poor choice because the DirectSound provider implementation polls the playback state repeatedly instead of responding to a single event. It would be far more efficient if we could simply respond to the STOP event, and query if the available() amount is less than the line buffer size to detect an underflow that we could then identify, within the context of our buffer writing code, as an unexpected underflow or playback completion.

  To confuse this even more, the isRunning() documentation states that the return will be false if the stop() method is called, or if "playback completes". There is no such thing as "playback completes", only an order to stop or an underflow. The line has no idea if the end of data in a buffer constitutes the end of playback. As stated, the Running state should also be set to false when an underflow occurs, but this seems to contradict the concept of "Running" that indicates data can be written to the line buffer, but not that data is actually being shipped out, for which you need a true Active state.

Either:
1. The DirectSound provider implementation must be corrected to accurately provide the documented sequence of events and state changes, and the documentation must be changed to correctly describe the isActive() and isRunning() methods, what purpose they were designed to serve, and how to correctly detect and deal with buffer underflow.

or

2. The documentation must be corrected to describe what the sequence of events and state changes should actually be, and provide descriptions of what purpose isActive() and isRunning() were designed to serve, and how to detect and deal with buffer underflows.

or

3. The Java Sound API must be updated to better handle buffer underflows, respond to events instead of polling, and the documentation must be updated to clearly and correctly describe this API and how the elements are meant to be used.

It would seem that choice #1 is the immediate solution, and that choice #3 is the correct long-term solution.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Create a SourceDataLine obtained from the Windows DirectSound provider. Add a LineListener that tests for the stop LineEvent Type. Write a buffer of data to the line, and wait(timeout) with a timeout longer than the playback time. The STOP event will never occur, and a call to isActive() and isRunning() after the wait() times out will both return true.

The STOP event will not occur until the stop() or close() line methods are called.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
As documented, the STOP event should occur when the line buffer is exhausted (underflow), and isActive() should return false. The isRunning() method should probably still return true until the line is closed indicating data can still be written to the line buffer. The drain() method should use an event and not polling.
ACTUAL -
No STOP event occurs on buffer underflow. The Active state remains true. It is unknown if a START event will occur after a buffer underflow STOP event as documented since the STOP event never occurs. The drain() method polls using a wait(timeout) loop instead of waiting on an event.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
package org.java.examples.sound;

import static java.lang.System.out;

import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;


public
class PlaySoundFile
{
    /** The frame size of the buffer used to shuttle data from the AudioInputStream to the SourceDataLine. */
    private static final int ShuttleBufferFrameSize = 0x8000;


    /**
     * Application entry.
     *
     * @param args  The array of command line arguments.
     */
    public static
    void
    main(String[] args)
    {
        if (args.length != 1) {
            out.println("PlaySoundFile");
            out.println("usage: java org.java.examples.sound.PlaySoundFile <soundfile>");
            out.println("or     java -jar <path>/PlaySoundFile.jar <soundfile>");
            return;
        }
        File soundFile = new File(args[0]);

        AudioInputStream audioInputStream;
        try {
            audioInputStream = AudioSystem.getAudioInputStream(soundFile);
        } catch (UnsupportedAudioFileException exception) {
            Logger.getLogger(PlaySoundFile.class.getName()).log(Level.SEVERE, null, exception);
            return;
        } catch (IOException exception) {
            Logger.getLogger(PlaySoundFile.class.getName()).log(Level.SEVERE, null, exception);
            return;
        }
        try {
            AudioFormat audioFormat = audioInputStream.getFormat();
            final SourceDataLine line;
            try {
                line = AudioSystem.getSourceDataLine(audioFormat);
                line.addLineListener(new LineListener() {
                    public void update(LineEvent event) {
                        LineEvent.Type type = event.getType();
                        out.println("*** Line event: " + type);
                        if (type == LineEvent.Type.STOP) {
                            synchronized (PlaySoundFile.class) {
                                PlaySoundFile.class.notify();
                            }
                        }
                    }
                });
                line.open(audioFormat);
            } catch (LineUnavailableException exception) {
                Logger.getLogger(PlaySoundFile.class.getName()).log(Level.SEVERE, null, exception);
                return;
            }
            try {
                line.start();
                out.println("Writing data...");
                try {
                    byte[] abData = new byte[ShuttleBufferFrameSize * audioFormat.getFrameSize()];
                    do {
                        int nBytesRead = audioInputStream.read(abData);
                        if (nBytesRead <= 0) break;
                        line.write(abData, 0, nBytesRead);
                    } while (line.isActive());
                } catch (IOException exception) {
                    Logger.getLogger(PlaySoundFile.class.getName()).log(Level.SEVERE, null, exception);
                    return;
                }

                out.println("Waiting...");
                synchronized (PlaySoundFile.class) {
                    try {
                        for (int count = 10; line.isActive() && count > 0; count -= 1) {
                            out.println(count);
                            PlaySoundFile.class.wait(1000);
                        }
                    } catch (InterruptedException exception) {
                        Logger.getLogger(PlaySoundFile.class.getName()).log(Level.SEVERE, null, exception);
                    }
                }
                if (line.isActive()) {
                    out.println("No STOP event occurred - stopping manually.");
                    line.stop();
                } else if (line.available() < line.getBufferSize()) {
                    out.println("Playback was interrupted.");
                } else {
                    out.println("Playback finished and a STOP event occurred.");
                }
            } finally {
                line.close();
            }
        } finally {
            try {
                audioInputStream.close();
            } catch (IOException exception) {
            }
        }
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Add a LineListener to call flush() when a STOP event occurs so that drain() will return if the line was stopped for a reason other than line buffer underflow.

Use drain() after the last data is written to the line. After it returns, test if available() == getBufferSize() indicating a normal line buffer underflow and not an aborted playback.

Take into account that the drain() implementation is a polling algorithm that may add unwanted load to waiting for playback to end, and that other circumstances may cause the playback to stop but not cause a STOP event. A watchdog wait(timeout) loop may be needed.