1

I need some help with my Java application. Its purpose is to read a certain website, so I need to play many audio files in a row. The JAR is compiled using Java 8. I tested my application with Windows 11 and Java 16.0.1, everything works fine. Then I used the latest Ubuntu Linux and Java 11.0.13 as well as Java 8: It plays some audio, but not every file.

I wrote a test class and the result was, that - no matter in which order I play the audio - only the first (exactly!) 62 files are played. Every next file (even the ones, that were successfully played at first) produces the exception my code throws at this position:

if (mixerSelected != null) {
    audioClip0 = AudioSystem.getClip(mixerSelected);
} else {
    throw new IllegalArgumentException("File is not compatible: '" + audioFilePath + "'.");
}

I ensured that every audio file is .WAV with

  • 8k sample rate,
  • 16k Bytes per second in average,
  • 16 Bits, and
  • pcm_s16le codec.

My application is built as JAR-file including my audio files in the resources directory.

This is my code:

public class PlayAudio {

    /**
     * plays an audio file
     *
     * @param audioFilePath String: path to the audio file
     * @param speed double: speed applied to the audios
     */
    public boolean singleFile(String audioFilePath, double speed) {

        //audioFilePath = "audio" + File.separator + audioFilePath;
        audioFilePath = "audio" + "/" + audioFilePath;

        AudioInputStream audioStream0;

        //create new file using path to the audio
        try {
            //load files from resources folder as stream
            ClassLoader classLoader = getClass().getClassLoader();
            InputStream inputStream = classLoader.getResourceAsStream(audioFilePath);
            InputStream bufferedInputStream = new BufferedInputStream(inputStream);

            if (bufferedInputStream == null) {
                throw new IllegalArgumentException("File not found: '" + audioFilePath + "'.");
            } else {
                //create new AudioStream
                audioStream0 = AudioSystem.getAudioInputStream(bufferedInputStream);
            }
        } catch (IllegalArgumentException e) {
            //handle
            return false;
        } catch (IOException e) {
            //handle
            return false;
        } catch (UnsupportedAudioFileException e) {
            //handle
            return false;
        }

        try {
            //create new AudioFormat
            AudioFormat audioFormat0 = audioStream0.getFormat();

            //create new Info
            DataLine.Info info0 = new DataLine.Info(Clip.class, audioFormat0);

            //initialize new Mixer
            Mixer.Info mixerSelected = null;

            for (Mixer.Info mixerInfo : AudioSystem.getMixerInfo()) {
                Mixer mixer = AudioSystem.getMixer(mixerInfo);

                if (mixer.isLineSupported(info0)) {
                    mixerSelected = mixerInfo;
                    break;
                }
            }

            //create new Clip
            Clip audioClip0;

            if (mixerSelected != null) {
                audioClip0 = AudioSystem.getClip(mixerSelected);
            } else {
                //THIS EXCEPTION GETS THROWN!!!
                throw new IllegalArgumentException("File is not compatible: '" + audioFilePath + "'.");
            }

            //open created Clips via created AudioStream
            audioClip0.open(audioStream0);

            //start the play of audio file
            audioClip0.start();

            //wait until play completed
            double waitTime = (double)((((double)audioClip0.getMicrosecondLength()/1000.0)/speed + 50.0) * 0.8);
            Thread.sleep((long)waitTime);

            return true;

        //handle exceptions
        } catch (LineUnavailableException e) {
            //handle
            return false;
        } catch (IOException e) {
            //handle
            return false;
        } catch (InterruptedException e) {
            //handle
            return false;
        } catch (IllegalArgumentException e) {
            //THIS EXCEPTION GETS THROWN!!!
            //handle invalid audio clips
            System.out.println(e);
            e.printStackTrace();
            return false;
        }
    }



    /**
     * plays multiple audio files in the order they are stored in an ArrayList
     *
     * @param fileNames ArrayList<String>: list with filenames of audio files to play
     * @param speaker String: speaker to use for playing the audios (can be 'm' or 'w')
     * @param speed double: speed applied to the audios
     * @return boolean: true if playing audios completed successfully, otherwise false
     */
    public static boolean multiFiles(ArrayList<String> fileNames, String speaker, double speed) {

        PlayAudio player = new PlayAudio();

        //play every audio file in the array of file names
        for (int i = 0; (i < fileNames.toArray().length); i ++) {
            //generate file names
            String fullFileName = speaker + "_" + fileNames.toArray()[i];

            //play audio
            player.singleFile(fullFileName, speed);
        }

        return true;
    }
}

What did I already try?

  1. I tried it on another computer that runs Ubuntu Linux as well.
  2. I created a new instance of PlayAudio() everytime a new audio is played.
  3. I used audioClip0.stop(); after every audio.
  4. I increased the milliseconds of sleep after every audio to length of the audio plus 1 second.
  5. I rebuilt the projects ... nearly 1k times.

How can I reproduce the error?

I simply need to play more than 62 audio files running my JAR-file under Linux Ubuntu.

How can you help me?

I don't know how to handle this issue. What is the problem playing .WAV-files with Linux? Is there a common way to fix this? (I am not allowed to use any library except OracleJDK and OpenJDK.)

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Jan Rother
  • 23
  • 5
  • The exception you throw seems to be caused by some failure of the code around `//initialize new Mixer`. Maybe you should debug this part of the code for the 63th audio file. – Bodo Jan 12 '22 at 16:23
  • Thanks for your hint, @Bodo. I will try to install IntelliJ on my virtual machine and debug my program in Linux. – Jan Rother Jan 12 '22 at 16:31
  • 1
    Change all the catches to first `e.printStackTrace()`. The code as-is throws away valuable information. I'd generally use [`AudioSystem.getAudioInputStream(URL)`](https://docs.oracle.com/javase/7/docs/api/javax/sound/sampled/AudioSystem.html#getAudioInputStream(java.net.URL)) over the versions seen above. – Andrew Thompson Jan 13 '22 at 03:23
  • 1
    You are opening resources (input streams, possibly others like related to the audio system), but not closing them, possibly you're running into some open handle limit. Please use [try-with-resources](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html) to ensure you close every resource when you're done with them. – Mark Rotteveel Jan 13 '22 at 07:42
  • 1
    Just to elaborate on @AndrewThompson 's hint: By using an `InputStream` rather than a `URL` you are throwing away a valuable hint for the `AudioSystem`, the file extension. Without it, the system has to parse your stream and guess the file type in order to decode it. With the `.wav` file extension, which should be in the URL, it's much easier. – Hendrik Jan 13 '22 at 08:08

2 Answers2

1

The #1 suggestion is by Mark Rotteveel. The AudioInputStream class needs closing. This is often a surprise for people, because Java is well known for managing garbage collection. But for AudioInputStream there are resources that need to be released. The API doesn't do an adequate job of pointing this out, imho, but the need for handling can be inferred from the description for the AudioInputStream.close() method:

Closes this audio input stream and releases any system resources associated with the stream.

The #2 suggestion is from both Andrew Thompson and Hendrik may be more a helpful hint than a direct solution, but it is still a very good idea, and it seems plausible to me that the inefficiency of all the additional, unneeded infrastructure (ClassLoader, InputStream, BufferedInputStream) might be contributing to the issue. But I really don't have a good enough understanding of the underlying code to know how pertinent that is.

However, I think you can do even better. Don't use Clip. You current use of Clip goes against the concept of its design. Clips are meant for short duration sounds that are to be held in memory and played multiple times, not files that are repeatedly reloaded before each playback. The proper class for this sort of use (load and play) is the SourceDataLine.

An example of playback using a SourceDataLine can be found in the javasound wiki. This example also illustrates the use of URL for obtaining the necessary AudioInputStream. I will quote it here verbatim.

public class ExampleSourceDataLine {

    public static void main(String[] args) throws Exception {
        
        // Name string uses relative addressing, assumes the resource is  
        // located in "audio" child folder of folder holding this class.
        URL url = ExampleSourceDataLine.class.getResource("audio/371535__robinhood76__06934-distant-ship-horn.wav");
        // The wav file named above was obtained from https://freesound.org/people/Robinhood76/sounds/371535/ 
        // and matches the audioFormat.
        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(url);
        
        AudioFormat audioFormat = new AudioFormat(
                Encoding.PCM_SIGNED, 44100, 16, 2, 4, 44100, false);
        Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
        SourceDataLine sourceDataLine = (SourceDataLine)AudioSystem.getLine(info);
        sourceDataLine.open(audioFormat);
        
        int bytesRead = 0;
        byte[] buffer = new byte[1024];
        sourceDataLine.start();
        while((bytesRead = audioInputStream.read(buffer)) != -1)
        {
            // It is possible at this point manipulate the data in buffer[].
            // The write operation blocks while the system plays the sound.
            sourceDataLine.write(buffer, 0, bytesRead);                                 
        }   
        sourceDataLine.drain();
        // release resources
        sourceDataLine.close();
        audioInputStream.close();
    }
}

You will have to do some editing, as the example was set up to run via a main method. Also, you'll be using your audio format, and that the names of the audio files with their folder locations will have to match the relative or absolute location specified in the argument you use in getResource() method. Also, a larger size for the buffer array might be preferred. (I often use 8192).

But most importantly, notice that in this example, we close both the SourceDataLine and the AudioInputStream. The alternate suggestion to use try-with-resources is a good one and will also release the resources.

If there are difficulties altering the above to fit into your program, I'm sure if you show us what you try, we can help with making it work.

Phil Freihofner
  • 7,645
  • 1
  • 20
  • 41
  • *"The #2 suggestion is from both Andrew Thompson and Hendrik may be more a helpful hint .. However, I think you can do even better. Don't use `Clip`. .."* That's a great idea. Especially since (AFAIR) if JavaSound can identify the source (`File` or `URL`) of a `Clip`, it will **cache it.** A `Clip` is a very handy class but as soon as the performance is an issue, it's probably best to avoid it altogether. – Andrew Thompson Jan 14 '22 at 09:29
  • I didn't know about the caching. Thanks for that. – Phil Freihofner Jan 14 '22 at 09:35
  • Hey @AndrewThompson, thank you for your detailed answer! I was able to apply most of your example code to mine and (after changing some details) my application ran without producing the error anymore. Only when I try to execute my `jar` file, I get an `java.io.IOException` saying that `mark/reset [is] not supported` at this line: `try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(classLoader.getResourceAsStream(audioFilePath));)` Can anyone guess why this happens? (Same problem when using Windows.) Thank you and sorry for my delay. :) – Jan Rother Jan 14 '22 at 20:00
  • I just found the solution: I had to parse the `Stream` using a `BufferedInputStream` like [here](https://stackoverflow.com/a/5529906/17162004). Thank you all for your help! :D – Jan Rother Jan 14 '22 at 20:20
  • 1
    The problem with the mark/reset error arises because you are using .getResourceAsStream() which returns an InputStream instead of .getResource() which returns a URL, and using that as the argument to getAudioInputStream(). This is why I am recommending using the example code provided, which uses the latter form. The reason can be seen in comparing the API of .getAudioInputStream(URL) to .getAudioInputStream(InputStream). If you are happy that your code works & don't want to mess with it, I understand. But the .getResource() and URL is much cleaner and simpler than what you are doing. – Phil Freihofner Jan 14 '22 at 21:21
  • @JanRother, on the link you listed as a solution, please see my answer from Feb. 17, 2012 for an alternative answer that matches my previous comment. – Phil Freihofner Jan 14 '22 at 21:25
  • @Phil Freihofner, everytime I used `.getResource()` it did not work when executing my `.JAR`-file. Because of that I started using `.getResourceAsStream()`. Or is there a difference when calling the method with `URL`? – Jan Rother Jan 15 '22 at 22:31
  • And another question concerning your solution, @Phil Freihofner: How can I adjust the speed of the playback? When using `Clip` I needed a `Thread.sleep(x)` anyway, where I was able to adjust the sleep time of the thread. However, the `SourceDataLine` seems to wait automatically. But is there a possibility to adjust speed nevertheless? – Jan Rother Jan 15 '22 at 22:34
  • @JanRother I'd need to see the code you tried and the error message to give you an intelligent answer to your question about `getResource()` vs `getResourceAsStream()` when running from a Jar. Both forms are commonly used in projects running from jars. I'm afraid I'm not clear on what you are asking about the speed of playback. Do you want there to be a pause before the playback starts? Actually an opened `SourceDataline` should start playing more quickly than an unloaded `Clip` (which is what you have in your example). Can you show code via editing your question? – Phil Freihofner Jan 16 '22 at 03:16
  • 1
    @PhilFreihofner, I answered my question with the code that works for me. Another optional parameter `speed` should now influence the wait time after the start of a new audio playback. – Jan Rother Jan 18 '22 at 12:05
1

After applying the answer from @Phil Freihofner this worked for me:

/**
 * plays an audio file
 *
 * @param audioFilePath String: path to the audio file
 * @param speed double: speed applied to the audios
 */
public boolean singleFile(String audioFilePath) {

    //get class
    ClassLoader classLoader = getClass().getClassLoader();

    //use try-with-resources
    //load files from resources folder as stream
    try (
            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(
            new BufferedInputStream(
                    Objects.requireNonNull(classLoader.getResourceAsStream(audioFilePath))))
    ) {

        if (audioInputStream == null) {
            throw new IllegalArgumentException("File not found: '" + audioFilePath + "'.");
        }

        //create new AudioFormat
        AudioFormat audioFormat = audioInputStream.getFormat();

        //create new Info
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);

        //create new SourceDataLine and open it
        SourceDataLine sourceDataLine = (SourceDataLine)AudioSystem.getLine(info);
        sourceDataLine.open(audioFormat);

        //start the play of the audio file
        int bytesRead;
        byte[] buffer = new byte[8192];
        sourceDataLine.start();

        while ((bytesRead = audioInputStream.read(buffer)) != -1) {
            sourceDataLine.write(buffer, 0, bytesRead);
        }

        sourceDataLine.drain();
        sourceDataLine.close();
        audioInputStream.close();

        //return true, because play finished
        return true;
    } catch (Exception e) {
        //ignore exceptions
        return false;
    }
}

Thak you all for contributing to my solution.

Jan Rother
  • 23
  • 5