使用 Swing 播放音频 - 我的代码线程安全吗?

Playing Audio with Swing - is my code thread safe?

我实际操作的简化版本(当计时器到期时触发蜂鸣声),但这足以证明我的设计。

播放开始后,Swing 无需再次接触音频剪辑。我已经能够确认此代码确实会播放声音并且不会阻塞事件分派线程,但我想确保没有我在不知不觉中违反的其他线程安全问题。谢谢!

import java.io.IOException;
import java.net.URL;

import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class TriggeringSoundTest extends JFrame
{
    private static final long serialVersionUID = -4573437469199690850L;

    private boolean soundPlaying = false;

    public TriggeringSoundTest()
    {
        JButton button = new JButton("Play Sound");
        button.addActionListener(e -> new Thread(() -> playBuzzer()).start());
        getContentPane().add(button);
    }

    private void playBuzzer()
    {
        URL url = getClass().getResource("resources/buzzer.wav");
        try (AudioInputStream stream = AudioSystem.getAudioInputStream(url); 
                Clip clip = AudioSystem.getClip())
        {
            clip.open(stream);
            clip.addLineListener(e -> {
                if (e.getType() == LineEvent.Type.STOP)
                    soundPlaying = false;
            });
            soundPlaying = true;
            clip.start();
            while (soundPlaying)
            {
                try
                {
                    Thread.sleep(1000);
                }
                catch (InterruptedException exp)
                {
                    // TODO Auto-generated catch block
                    exp.printStackTrace();
                }
            }
        }
        catch (UnsupportedAudioFileException exp)
        {
            // TODO Auto-generated catch block
            exp.printStackTrace();
        }
        catch (IOException exp)
        {
            // TODO Auto-generated catch block
            exp.printStackTrace();
        }
        catch (LineUnavailableException exp)
        {
            // TODO Auto-generated catch block
            exp.printStackTrace();
        }
    }

    private static void createAndShowGUI()
    {
        JFrame frame = new TriggeringSoundTest();
        frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(() -> createAndShowGUI());
    }
}

我已经摆脱了与 Clips 一起工作的所有麻烦,因为它们有很多烦人的方面。

但是是的,如评论中所述,您应该在程序开始时对每个 Clip 初始化(打开)一次且仅一次。然后,每次需要时重新启动 Clip。 API 将向您显示将 Clip 重置为其起始帧并重新播放的确切命令。

管理线程很烦人。如果我理解正确,Clip 会为实际的音频输出启动一个守护线程。守护线程在父线程终止时死亡。因此,您的解决方案是放入 Thread.sleep() 命令和 LineListener 以使线程在 SFX 期间保持活动状态,因此它的守护进程不会当父线程终止时得到 chomped。

不过我担心,如果您使用这种方法,调用线程(按下按钮启动的代码)将无法在您的 "sleep" 期间执行任何其他操作。如果按钮代码只执行播放,那么你应该没问题。但是,如果您稍后决定通过同一按钮按下来触发其他任何内容(如蜂鸣器动画)怎么办?可能有必要添加另一层复杂性,例如将剪辑包装在另一个线程中,该线程唯一负责剪辑播放。然后你的按钮按下可以启动这个包装器并做其他事情而不会受到 Thread.sleep() 解决方案的影响。

现在,应该可以避免这一切了!假设您制作了一个名为 buzzerClip 并在程序的早期打开它,将其保存在一个实例变量中(如 Andrew 所建议的)。然后,假设您从无限期保持活动状态的线程(例如经典游戏循环)中调用 buzzer.start()。我认为 Clip start() 将启动它的守护线程并在该游戏循环线程继续存在时播放。这样,您就可以省去睡眠和线路收听。但如果没有实际编写示例并亲自尝试,我不能 100% 确定。

(按钮线程如何告诉游戏循环线程播放声音?也许按钮线程设置了一个标志,游戏循环线程检查该标志作为其正常 "update" 循环的一部分. 嗯。也许我真的需要在提供它作为建议之前尝试编码。)

或者,请随意检查代码,并可能使用 AudioCue。它在加载和播放方面类似于 Clip,但在它下面使用 SourceDataLine 作为输出而不是 Clip。如果您查看代码,您会看到 SourceDataLine 在其自己的线程中维护(作为 AudioCue 的一部分被打开)。这消除了很多并发症。

这种方法的一个很好的优点是它可以提供并发播放——例如,如果您想在第一个蜂鸣器仍在播放时启动第二个蜂鸣器实例。 AudioCue 还允许您做一些事情,例如以不同的速度播放蜂鸣器,这可能有助于利用您的 SFX 使声音听起来像多个提示。我尽力提供文档和示例。该许可证允许您剪切和粘贴最适合您特定需求的代码。