使用 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() 解决方案的影响。
现在,应该可以避免这一切了!假设您制作了一个名为 buzzer 的 Clip
并在程序的早期打开它,将其保存在一个实例变量中(如 Andrew 所建议的)。然后,假设您从无限期保持活动状态的线程(例如经典游戏循环)中调用 buzzer.start()。我认为 Clip start() 将启动它的守护线程并在该游戏循环线程继续存在时播放。这样,您就可以省去睡眠和线路收听。但如果没有实际编写示例并亲自尝试,我不能 100% 确定。
(按钮线程如何告诉游戏循环线程播放声音?也许按钮线程设置了一个标志,游戏循环线程检查该标志作为其正常 "update" 循环的一部分. 嗯。也许我真的需要在提供它作为建议之前尝试编码。)
或者,请随意检查代码,并可能使用 AudioCue。它在加载和播放方面类似于 Clip
,但在它下面使用 SourceDataLine
作为输出而不是 Clip
。如果您查看代码,您会看到 SourceDataLine
在其自己的线程中维护(作为 AudioCue
的一部分被打开)。这消除了很多并发症。
这种方法的一个很好的优点是它可以提供并发播放——例如,如果您想在第一个蜂鸣器仍在播放时启动第二个蜂鸣器实例。 AudioCue
还允许您做一些事情,例如以不同的速度播放蜂鸣器,这可能有助于利用您的 SFX 使声音听起来像多个提示。我尽力提供文档和示例。该许可证允许您剪切和粘贴最适合您特定需求的代码。
我实际操作的简化版本(当计时器到期时触发蜂鸣声),但这足以证明我的设计。
播放开始后,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() 解决方案的影响。
现在,应该可以避免这一切了!假设您制作了一个名为 buzzer 的 Clip
并在程序的早期打开它,将其保存在一个实例变量中(如 Andrew 所建议的)。然后,假设您从无限期保持活动状态的线程(例如经典游戏循环)中调用 buzzer.start()。我认为 Clip start() 将启动它的守护线程并在该游戏循环线程继续存在时播放。这样,您就可以省去睡眠和线路收听。但如果没有实际编写示例并亲自尝试,我不能 100% 确定。
(按钮线程如何告诉游戏循环线程播放声音?也许按钮线程设置了一个标志,游戏循环线程检查该标志作为其正常 "update" 循环的一部分. 嗯。也许我真的需要在提供它作为建议之前尝试编码。)
或者,请随意检查代码,并可能使用 AudioCue。它在加载和播放方面类似于 Clip
,但在它下面使用 SourceDataLine
作为输出而不是 Clip
。如果您查看代码,您会看到 SourceDataLine
在其自己的线程中维护(作为 AudioCue
的一部分被打开)。这消除了很多并发症。
这种方法的一个很好的优点是它可以提供并发播放——例如,如果您想在第一个蜂鸣器仍在播放时启动第二个蜂鸣器实例。 AudioCue
还允许您做一些事情,例如以不同的速度播放蜂鸣器,这可能有助于利用您的 SFX 使声音听起来像多个提示。我尽力提供文档和示例。该许可证允许您剪切和粘贴最适合您特定需求的代码。