Created
May 8, 2021 11:21
-
-
Save helospark/9406f093cc39fe8c4ccf1ea61951e4ee to your computer and use it in GitHub Desktop.
Repro OpenJDK hanging bug on Linux (using ALSA) playback device. It doesn't always work with the same SIZE, delay, and stream size parameters on every machine. I guess it depends on sound hardware, buffers, etc.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.test; | |
import javax.sound.sampled.AudioFormat; | |
import javax.sound.sampled.AudioSystem; | |
import javax.sound.sampled.DataLine; | |
import javax.sound.sampled.LineUnavailableException; | |
import javax.sound.sampled.SourceDataLine; | |
/** | |
OpenJDK bug report: | |
---- Title | |
SourceDataLine.write hangs indefinetely on Linux | |
---- Description | |
With some bufferSize and delay combination (see source code) SourceDataLine.write hangs indefinitely. | |
Some technical notes from debugging this (based on source code attach below): | |
In method DirectAudioDevice.write, nWrite always returns with 0, therefore loop "while (!flushing)" never returns. (here: https://github.com/openjdk/jdk/blob/739769c8fc4b496f08a92225a12d07414537b6c0/src/java.desktop/share/classes/com/sun/media/sound/DirectAudioDevice.java#L714) | |
On nativeCode side this happens because snd_pcm_writei (ALSA function) return -11 error code. (happens here: https://github.com/openjdk/jdk/blob/739769c8fc4b496f08a92225a12d07414537b6c0/src/java.desktop/linux/native/libjsound/PLATFORM_API_LinuxOS_ALSA_PCM.c#L722) | |
This is from GDB of the same: | |
722 writtenFrames = snd_pcm_writei(info->handle, (const void*) data, (snd_pcm_uframes_t) frameSize); | |
(gdb) n | |
724 if (writtenFrames < 0) { | |
(gdb) p writtenFrames | |
$4 = -11 | |
After this, xrun_recovery (ALSA) function is called which returns 0 causing the whole DAUDIO_Write to return 0 therefore 0 is returned to nWrite to Java. | |
(I'm not entirely sure that "ret <= 0" is the proper condition for xrun_recovery error check, since xrun_recovery returns 0 on success and negative on error. Though this is not likely to be the root cause of this issue) | |
Some general observations: | |
- Without the "Thread.sleep(100);" between streaming the first bytes and creating the buffer it usually works. This indicates some race condition to me. On my machine the magic number is 43ms, with 42ms sleep it works, with 43ms it no longer works. | |
- If the buffer size is as big as the data I'm writing it also works (aka. SIZE instead of SIZE*2 when creating new byte[] array) | |
- It happens only on Linux | |
---- System | |
OS kernel: Linux black-comp 4.15.0-140-generic #144-Ubuntu SMP Fri Mar 19 14:12:35 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux | |
OS: Ubuntu Mate 18.04 (also reproducible on clean install of latest 20.04) | |
Java: openjdk version "15" 2020-09-15 (also reproducible on: 17-ea) | |
ALSA: alsa-base/bionic,bionic,now 1.0.25+dfsg-0ubuntu5 all [installed] | |
alsa-utils/bionic,now 1.1.3-1ubuntu1 amd64 [installed] | |
ALSA devices: | |
aplay --list-devices | |
**** List of PLAYBACK Hardware Devices **** | |
card 0: PCH [HDA Intel PCH], device 0: ALC269VC Analog [ALC269VC Analog] | |
Subdevices: 0/1 | |
Subdevice #0: subdevice #0 | |
card 0: PCH [HDA Intel PCH], device 3: HDMI 0 [HDMI 0] | |
Subdevices: 1/1 | |
Subdevice #0: subdevice #0 | |
---- Reproduction | |
Run the main() method from code attached. | |
---- Expected | |
Writing data continuously until program ends. | |
SourceDataLine.write returns after data is written to buffer. | |
Alternatively if writing data larger than the bufferSize is not supported by the implementation I would expect an Exception to be thrown to indicate the misconfiguration. | |
In console, the expected result would be something like: | |
Initializing sourceDataLine with bufferSize=3680 | |
Writing 7360 bytes at 1620469670147 | |
Writing finished at 1620469670153 | |
Writing 7360 bytes at 1620469670153 | |
Writing finished at 1620469670153 | |
Writing 7360 bytes at 1620469670153 | |
Writing finished at 1620469670154 | |
Writing 7360 bytes at 1620469670154 | |
... continues until program is killed | |
---- Actual | |
However SourceDataLine.write hangs indefinitely, full log on console: | |
Initializing sourceDataLine with bufferSize=3680 | |
Writing 7360 bytes at 1620469547701 | |
No further log is printed after this point. | |
---- Workaround: | |
- Possibly using larger buffer size. | |
- Immediately after sourceDataLine.start() write at least 1 empty sample to the SourceDataLine. If done, it will continue to work. | |
Like: | |
// ... init | |
sourceDataLine.start(); | |
byte[] data = new byte[4]; | |
sourceDataLine.write(data, 0, data.length); | |
*/ | |
public class SourceDataLineWriteHangs { | |
private static final int CHANNELS = 2; | |
private static final int BYTES_PER_SAMPLE = 2; | |
private static final int SAMPLE_RATE = 44100; | |
private static final int SIZE = 3680; | |
public static void main(String[] args) throws InterruptedException { | |
SourceDataLine sourceDataLine = null; | |
try { | |
DataLine.Info dataLineInfo; | |
System.out.println("Initializing sourceDataLine with bufferSize=" + SIZE); | |
AudioFormat format = new AudioFormat(SAMPLE_RATE, BYTES_PER_SAMPLE * 8, CHANNELS, true, true); | |
dataLineInfo = new DataLine.Info(SourceDataLine.class, format, SIZE); | |
sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo); | |
sourceDataLine.open(sourceDataLine.getFormat(), SIZE); | |
sourceDataLine.start(); | |
// byte[] data = new byte[4]; // workaround | |
// sourceDataLine.write(data, 0, data.length); | |
} catch (LineUnavailableException | IllegalArgumentException e) { | |
e.printStackTrace(); | |
} | |
Thread.sleep(100); // without this it usually works | |
while (true) { | |
byte[] data = new byte[SIZE * 2]; | |
System.out.println("Writing " + data.length + " bytes at " + System.currentTimeMillis()); | |
sourceDataLine.write(data, 0, data.length); | |
System.out.println("Writing finished at " + System.currentTimeMillis()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TLDR.: As a workaround reducing the SIZE significantly worked for me. The workarounds suggested in the comment in the code did not.
In my case this problem could be reproduced in 100% of the cases, even completely without the Thread.sleep(100) and also if i wrote an empty sample to the SourceDataLine immediately after
sourceDataLine.start()
. The described Error code (-11) is according to the documentation EAGAIN. The first message of this thread says EAGAIN "[...] means that the device is not ready to accept more data at this time, and the application should try again later".Since it happened for an indefinite time I guessed the device was never ready to handle a buffer of the given size (even though I would have expected it to be partially handled, as the API suggests it will). Reducing the byte[] data to a maximum size of 1024 solved the problem, even though I think the buffer size is a bit small I. Filling the buffer on a separate thread with maximum priority seems to be fast enough to prevent an underflow, but I would rather have this bug fixed.