【问题标题】:Playing audio files from a JAR-application does not work on Linux从 JAR 应用程序播放音频文件在 Linux 上不起作用
【发布时间】:2022-01-12 16:05:39
【问题描述】:

我的 Java 应用程序需要一些帮助。它的目的是阅读某个网站,所以我需要连续播放很多音频文件。 JAR 是使用 Java 8 编译的。我使用 Windows 11 和 Java 16.0.1 测试了我的应用程序,一切正常。然后我使用了最新的 Ubuntu Linux 和 Java 11.0.13 以及 Java 8:它播放一些音频,但不是每个文件。

我编写了一个测试类,结果是——无论我以何种顺序播放音频——只播放前(完全正确!)62 个文件。每个下一个文件(即使是最初成功播放的文件)都会产生我的代码在此位置抛出的异常:

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

我确保每个音频文件都是.WAV

  • 8k 采样率,
  • 平均每秒 16k 字节,
  • 16 位,并且
  • pcm_s16le 编解码器。

我的应用程序构建为 JAR 文件,包括资源目录中的音频文件。

这是我的代码:

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;
    }
}

我已经尝试了什么?

  1. 我在另一台也运行 Ubuntu Linux 的计算机上进行了尝试。
  2. 每次播放新音频时,我都会创建一个 PlayAudio() 的新实例。
  3. 我在每个音频之后都使用audioClip0.stop();
  4. 我将每个音频后的睡眠毫秒数增加到音频长度加上 1 秒。
  5. 我重建了项目......近 1k 次。

如何重现错误?

我只需要在 Linux Ubuntu 下播放超过 62 个运行我的 JAR 文件的音频文件。

你能帮我什么忙?

我不知道如何处理这个问题。在 Linux 上播放 .WAV-files 有什么问题? 有没有一种通用的方法来解决这个问题? (我不允许使用除 OracleJDK 和 OpenJDK 之外的任何库。)

【问题讨论】:

  • 您抛出的异常似乎是由//initialize new Mixer 周围的代码故障引起的。或许你应该为第 63 个音频文件调试这部分代码。
  • 感谢您的提示,@Bodo。我将尝试在我的虚拟机上安装 IntelliJ 并在 Linux 中调试我的程序。
  • 将所有捕获更改为第一个e.printStackTrace()。原样的代码会丢弃有价值的信息。我通常会在上面看到的版本上使用AudioSystem.getAudioInputStream(URL)
  • 您正在打开资源(输入流,可能是与音频系统相关的其他资源),但没有关闭它们,可能您遇到了一些打开的句柄限制。请使用try-with-resources 确保在完成所有资源后关闭它们。
  • 只是为了详细说明@AndrewThompson 的提示:通过使用InputStream 而不是URL,您丢弃了文件扩展名AudioSystem 的有价值提示。没有它,系统必须解析您的流并猜测文件类型才能对其进行解码。使用应该在 URL 中的 .wav 文件扩展名,这要容易得多。

标签: java linux ubuntu jar javasound


【解决方案1】:

应用@Phil Freihofner 的答案后,这对我有用:

/**
 * 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;
    }
}

感谢大家为我的解决方案做出贡献。

【讨论】:

  • 另外一个用于回复我们的解决方案,并清楚地解释它。一个(迟到的)加一个问题,写得很好,解释得很好。
  • 谢谢你,@AndrewThompson!所以我对我的第一篇文章很满意。 :D
【解决方案2】:

#1 建议由 Mark Rotteveel 提出。 AudioInputStream 类需要关闭。这通常会让人们感到惊讶,因为 Java 以管理垃圾收集而闻名。但是对于AudioInputStream,有一些资源需要释放。 API 没有充分指出这一点,恕我直言,但可以从 AudioInputStream.close() 方法的描述中推断出需要处理:

关闭此音频输入流并释放所有系统资源 与流相关联。

来自 Andrew Thompson 和 Hendrik 的第 2 条建议可能比直接解决方案更有帮助,但它仍然是一个非常好的主意,而且在我看来,所有额外的、不需要的基础设施的低效率似乎是合理的(ClassLoaderInputStreamBufferedInputStream)可能导致了这个问题。但我对底层代码的理解真的不够好,无法知道它的相关性。

但是,我认为您可以做得更好。不要使用Clip。你目前使用Clip 违背了它的设计理念。 Clips 适用于要保存在内存中并多次播放的短时声音,而不是在每次播放之前重复重新加载的文件。适合这种使用(加载和播放)的类是SourceDataLine

可以在javasound wiki 中找到使用SourceDataLine 进行回放的示例。此示例还说明了使用URL 来获取必要的AudioInputStream。我将在此处逐字引用。

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();
    }
}

您必须进行一些编辑,因为该示例设置为通过main 方法运行。此外,您将使用您的音频格式,并且音频文件的名称及其文件夹位置必须与您在 getResource() 方法中使用的参数中指定的相对或绝对位置相匹配。此外,buffer 数组的更大尺寸可能是首选。 (我经常用8192)。

但最重要的是,请注意,在此示例中,我们关闭了 SourceDataLineAudioInputStream。使用 try-with-resources 的替代建议是一个很好的建议,并且还会释放资源。

如果在更改上述内容以适应您的程序时遇到困难,我相信如果您向我们展示您的尝试,我们可以帮助实现它。

【讨论】:

  • “来自 Andrew Thompson 和 Hendrik 的 #2 建议可能更有帮助。但是,我认为你可以做得更好。不要使用 Clip。。 。” 这是个好主意。特别是因为(AFAIR)如果 JavaSound 可以识别 Clip 的源(FileURL),它将缓存它。 Clip 是一个非常方便的类,但很快由于性能是一个问题,最好完全避免它。
  • 我不知道缓存。谢谢你。
  • 嘿@AndrewThompson,感谢您的详细回答!我能够将您的大部分示例代码应用到我的应用程序中,并且(在更改了一些细节之后)我的应用程序运行时不再产生错误。只有当我尝试执行我的jar 文件时,我才得到一个java.io.IOExceptionmark/reset [is] not supported 在这一行:try (AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(classLoader.getResourceAsStream(audioFilePath));) 谁能猜出为什么会发生这种情况? (使用 Windows 时同样的问题。)谢谢你,对不起我的延误。 :)
  • 出现标记/重置错误的问题是因为您使用的是返回 InputStream 的 .getResourceAsStream() 而不是返回 URL 的 .getResource(),并将其用作 getAudioInputStream() 的参数.这就是为什么我建议使用提供的示例代码,它使用后一种形式。原因可以从 .getAudioInputStream(URL) 和 .getAudioInputStream(InputStream) 的 API 对比中看出。如果您对您的代码有效并且不想弄乱它感到高兴,我理解。但是 .getResource() 和 URL 比你正在做的更干净和简单。
  • @PhilFreihofner,我用适合我的代码回答了我的问题。另一个可选参数speed 现在应该会影响新音频播放开始后的等待时间。
猜你喜欢
  • 2015-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多