为啥我的 Java 应用程序没有在每个循环中播放声音?

Posted

技术标签:

【中文标题】为啥我的 Java 应用程序没有在每个循环中播放声音?【英文标题】:Why doesn't my Java application play the sound at each loop?为什么我的 Java 应用程序没有在每个循环中播放声音? 【发布时间】:2020-04-11 16:13:04 【问题描述】:

我正在尝试制作一个 Java 应用程序来模拟某人在他们的键盘上打字。击键声音以可变间隔循环播放(Java 在其他中随机选择击键声音并播放)(以模拟真人打字)。

它在开始时运行良好,但在大约 第 95 次 迭代之后,它停止播放声音(同时仍在循环)不到 4 秒,然后再次播放声音。在 第 160 次 迭代之后,它几乎每秒播放一次声音(而不是每 3 到 6 秒)。 一段时间后,它会停止播放声音很长一段时间,然后永远停止。

这是AudioPlayer.java 类的来源:

package entity;

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.Audiosystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;

public class AudioPlayer implements Runnable 
    private String audioFilePath;

    public void setAudioFilePath(String audioFilePath) 
         this.audioFilePath = audioFilePath;
    

    @Override
    public void run() 
        File audioFile = new File(audioFilePath);

        try 
            AudioInputStream audioStream = AudioSystem.getAudioInputStream(audioFile);
            AudioFormat format = audioStream.getFormat();
            DataLine.Info info = new DataLine.Info(Clip.class, format);
            Clip audioClip = (Clip) AudioSystem.getLine(info);
            audioClip.open(audioStream);
            audioClip.start();
            boolean playCompleted = false;
            while (!playCompleted) 
                try 
                    Thread.sleep(500);
                    playCompleted = true;
                
                catch (InterruptedException ex) 
                    ex.printStackTrace();
                
            
            audioClip.close();
         catch (UnsupportedAudioFileException ex) 
            System.out.println("The specified audio file is not supported.");
            ex.printStackTrace();
         catch (LineUnavailableException ex) 
            System.out.println("Audio line for playing back is unavailable.");
            ex.printStackTrace();
         catch (IOException ex) 
            System.out.println("Error playing the audio file.");
            ex.printStackTrace();
        
    

这是用于测试击键模拟器的Main.java 类:

package sandbox;

import java.util.Random;

import entity.AudioPlayer;

public class Main 
    public static void main(String[] args) 
        Random rnd = new Random();
        AudioPlayer audio;

        for(int i = 0; i < 10000; i++) 
            int delay = rnd.nextInt(200)+75;
            try 
                Thread.sleep(delay);
            
            catch (InterruptedException ie) 
            int index = rnd.nextInt(3)+1;
            audio = new AudioPlayer();
            audio.setAudioFilePath("resources/keystroke-0"+index+".wav");
            Thread thread = new Thread(audio);
            thread.start();
            System.out.println("iteration "+i);
        
    

我在资源目录中使用了多个不同声音击键的短(小于 200 毫秒)波形文件(总共 3 个)。

编辑

我阅读了您的答案和 cmets。我在想,也许我误解了他们,因为建议的解决方案不起作用,或者我应该清楚自己到底想要什么。另外,我需要注意的是我不经常使用线程(并且不知道互斥锁是什么)。

所以我将首先解释我到底想要程序做什么。它应该能够模拟击键,所以我使用了一个线程,因为它允许两个击键声音重叠,就像真人打字时一样。基本上我使用的声音剪辑是击键声音,击键声音由两种声音组成: 按键的声音。 释放键的声音。

如果程序在某个时候允许两次击键重叠,则听起来好像有人按下一个键然后另一个键然后释放第一个键。真正的打字听起来就是这样!

现在我在使用建议的解决方案时遇到的问题是: 直接调用AudioPlayer的run()方法时,

public static void main(String[] args)

    // Definitions here

    while (running) 
        Date previous = new Date();
        Date delay = new Date(previous.getTime()+rnd.nextInt(300)+75);

        // Setting the audio here

        audio.run();
        Date now = new Date();

        if (now.before(delay)) 
            try  
                Thread.sleep(delay.getTime()-now.getTime());
             catch (InterruptedException e) 
            
        

        System.out.println("iteration: "+(++i));
    

声音按顺序播放(一个接一个),播放速率取决于 AudioPlayer 的睡眠持续时间(或者如果 main() 方法中的延迟高于 AudioPlayer 的睡眠持续时间,则取决于延迟),这是不好的,因为它听起来不像普通的打字员(更像是一个刚开始打字并且在打字时仍然在寻找每个键的人)。

调用AudioPlayer的Thread的join()方法时,

public static void main(String[] args)

    //Variable definitions here

    while (running) 
        int delay = rnd.nextInt(200)+75;
        try
        
            Thread.sleep(delay);
        
        catch (InterruptedException ie)
        

        

        //Setting the AudioPlayer and creating its Thread here

        thread.start();
        try
        
            thread.join();
        
        catch(InterruptedException ie)
        

        
        System.out.println("iteration "+(++i));
    

声音也按顺序播放,播放速度取决于 AudioPlayer 的睡眠持续时间(或者,如果 main() 方法中的延迟高于 AudioPlayer 的睡眠持续时间,则取决于延迟) , 和以前一样不好。

所以,回答评论者的问题之一。是的!还有其他未表达的问题首先需要线程。

我找到了一个“解决”我的问题的解决方法(但我不认为这是一个合适的解决方案,因为我在某种程度上是在作弊):我所做的是将 AudioPlayer 的睡眠持续时间增加到在程序停止(24 小时)之前不太可能达到,而且据我所知,即使一个多小时后它也不会使用太多资源。

您可以查看我想要什么,运行建议的解决方案时得到什么,以及使用this youtube videos 上的解决方法得到什么(不幸的是,*** 没有视频上传功能。所以我不得不把它放在 youtube 上) .

编辑 音效可here下载。

【问题讨论】:

您好,由于多线程取决于您计算机上一次可以运行的并发进程的数量,是否是您的计算机耗尽了 CPU 内核来运行您启动的线程? @user123 我对此一无所知,因为就像我说的,在第 95 次迭代之前一切正常(而且我没有 95 个物理或逻辑内核)。另外,如果我没记错的话,线程在播放完声音后会被垃圾收集器删除。 @PaikuHan 听起来是个有趣的小项目;但是,多线程并不能保证一致的“等待...播放...等待...播放...”类型的行为。如果您想要多线程的这种行为,您将不得不使用互斥锁或其他线程协调工具强制执行排序,除非这里没有表达其他需要首先使用线程的问题,否则这太费力了。 这不是“内核耗尽”的问题。这是一个告诉操作系统你有一堆独立任务的问题,并希望操作系统有足够的空闲时间来始终一致地安排它们。最终,操作系统不会一致地安排它们,并且会认为这不是问题,因为它们是作为独立任务呈现的。在没有线程启动器的情况下呈现循环,它会产生更好的结果。 @EdwinBuck 我现在在我的代码中使用游戏循环样式设置,但仍然需要线程。检查上面的编辑。如果您有时间,不妨看看这些视频,它们会让您更清楚地了解我的问题所在。 【参考方案1】:

这个单线程解决方案怎么样?它是您自己的一个更简洁的版本,但可以重新使用缓冲区中已经打开的剪辑?对我来说,打字听起来很自然,即使没有两种声音同时播放。您可以通过更改Application类中对应的静态常量来调整打字速度。

package de.scrum_master.***.q61159885;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine.Info;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static javax.sound.sampled.AudioSystem.getAudioInputStream;
import static javax.sound.sampled.AudioSystem.getLine;

public class AudioPlayer implements Closeable 
  private final Map<String, Clip> bufferedClips = new HashMap<>();

  public void play(String audioFilePath) throws IOException, UnsupportedAudioFileException, LineUnavailableException 
    Clip clip = bufferedClips.get(audioFilePath);
    if (clip == null) 
      AudioFormat audioFormat = getAudioInputStream(new File(audioFilePath)).getFormat();
      Info lineInfo = new Info(Clip.class, audioFormat);
      clip = (Clip) getLine(lineInfo);
      bufferedClips.put(audioFilePath, clip);
      clip.open(getAudioInputStream(new File(audioFilePath)));
    
    clip.setMicrosecondPosition(0);
    clip.start();
  

  @Override
  public void close() 
    bufferedClips.values().forEach(Clip::close);
  

package de.scrum_master.***.q61159885;

import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import java.io.IOException;
import java.util.Random;

public class Application 
  private static final Random RANDOM = new Random();

  private static final int ITERATIONS = 10000;
  private static final int MINIMUM_WAIT = 75;
  private static final int MAX_RANDOM_WAIT = 200;

  public static void main(String[] args) throws UnsupportedAudioFileException, IOException, LineUnavailableException 
    try (AudioPlayer audioPlayer = new AudioPlayer()) 
      for (int i = 0; i < ITERATIONS; i++) 
        sleep(MINIMUM_WAIT + RANDOM.nextInt(MAX_RANDOM_WAIT));
        audioPlayer.play(randomAudioFile());
      
    
  

  private static void sleep(int delay) 
    try 
      Thread.sleep(delay);
     catch (InterruptedException ignored) 
  

  private static String randomAudioFile() 
    return "resources/keystroke-0" + (RANDOM.nextInt(3) + 1) + ".wav";
  

您可能已经注意到AudioPlayerCloseable,即您可以在调用应用程序中使用“使用资源尝试”。这样可以确保在程序结束时自动关闭所有剪辑。

重播同一片段的关键当然是在开始之前clip.setMicrosecondPosition(0)


更新:如果要模拟多人,只需像这样修改主类即可。顺便说一句,我对音频编程以及是否有办法更好地处理混音器和重叠声音一无所知。这只是一个概念证明,以便给你一个想法。每个人有一个线程,但每个人都以串行方式键入,而不是同时键入两个键。但是多人可以重叠,因为每个人都有一个 AudioPlayer 有自己的一组缓冲剪辑。

package de.scrum_master.***.q61159885;

import java.util.Random;

public class Application 
  private static final Random RANDOM = new Random();

  private static final int PERSONS = 2;
  private static final int ITERATIONS = 10000;
  private static final int MINIMUM_WAIT = 150;
  private static final int MAX_RANDOM_WAIT = 200;

  public static void main(String[] args) 
    for (int p = 0; p < PERSONS; p++)
      new Thread(() -> 
        try (AudioPlayer audioPlayer = new AudioPlayer()) 
          for (int i = 0; i < ITERATIONS; i++) 
            sleep(MINIMUM_WAIT + RANDOM.nextInt(MAX_RANDOM_WAIT));
            audioPlayer.play(randomAudioFile());
          
         catch (Exception ignored) 
      ).start();
  

  private static void sleep(int delay) 
    try 
      Thread.sleep(delay);
     catch (InterruptedException ignored) 
  

  private static String randomAudioFile() 
    return "resources/keystroke-0" + (RANDOM.nextInt(3) + 1) + ".wav";
  

【讨论】:

@kiregaex。它可以工作,但由于某些原因,当 ITERATION 高于 1000000 时循环会停止。但由于它的工作时间比我需要的时间长,所以我选择了你的解决方案。【参考方案2】:

除了 EdwinBuck 所说的之外,我相信您在 AudioPlayer 课程中(并且每次)都在做很多工作。尝试为您的所有音频文件预先创建一个 AudioPlayer 实例(我相信它是 4 个?),并添加一个单独的 play() 方法,以便在循环中您可以执行类似 audioplayers[index].play( )。

另请注意,在您的 AudioPlayer 类中,您等待声音完成 500 毫秒,这比您等待播放下一个声音的时间要长。这将 - 一段时间后 - 导致您用尽可用线程...也许您可以在 AudioClip 完成时使用回调,而不是等待。

【讨论】:

如果音频播放不需要重叠,这将是有意义的。不幸的是,情况并非如此。 在线程上使用 Linelistener 后问题消失了。【参考方案3】:

对于线程,您谈论的是独立的执行流程。您的程序被设计成选择延迟和播放声音不是独立的。

类似

    for (int i = 0; i < 10000; i++) 
        int delay = rnd.nextInt(200)+75;
        try 
            Thread.sleep(delay);
         catch (InterruptedException ie) 

        
        int index = rnd.nextInt(3)+1;
        audio = new AudioPlayer();
        audio.setAudioFilePath("resources/keystroke-0"+index+".wav");
        audio.run();
        System.out.println("iteration "+i);
    

表示您“等待”然后“运行 wav”然后“重复”。

现在,您依靠内核上执行线程的一致调度来获得所需的结果;除了线程不是用来表达一致的执行调度的方式。线程旨在成为表达独立执行调度的方式。

赔率介于您当前的等待和当前的“播放波形”之间,还有其他一些事情介于两者之间,如果有足够的时间,甚至可能会乱序播放 wav 文件。我会明确排序。

如果您需要对循环内的时间进行良好控制,请查看游戏循环类型设置。它类似于您的 for 循环,但看起来像

while (running) 
  SomeTimeType previous = new TimeType();
  SomeTimeOffset delay = new TimeOffset(rnd.nextInt(200)+75);

  ...
  audio.run();
  SomeTimeType now = new TimeType();
  if (now.minus(offset).compareTo(previous) > 0) 
    try  
      Thread.sleep(now.minus(offset).toMillis())
     catch (InterruptedException e) 
    
  

这里的主要区别是您的随机延迟从 wav 文件的播放时间开始到下一个 wav 文件的播放时间开始,如果延迟比 wav 文件的播放时间短,则文件之间没有延迟时间。

另外,我会研究一下 AudioPlayer 是否可以在波形文件播放之间重复使用,因为这可能会给你带来更好的结果。

现在,如果您确实需要在单独的线程中播放,则需要将循环线程加入 AudioPlayer 线程,以确保 AudioPlayer 在循环线程前进之前完成。即使您在 for 循环中等待更长的时间,请记住,任何进程都可能随时脱离 CPU 内核,因此您的等待并不能保证 for 循环每次迭代花费的时间比 AudioPlayer 每次 wav 花费的时间更多文件,如果 CPU 必须处理其他事情(如网络数据包)。

    for (int i = 0; i < 10000; i++) 
        int delay = rnd.nextInt(200)+75;
        try 
            Thread.sleep(delay);
         catch (InterruptedException ie) 

        
        int index = rnd.nextInt(3)+1;
        audio = new AudioPlayer();
        audio.setAudioFilePath("resources/keystroke-0"+index+".wav");
        Thread thread = new Thread(audio);
        thread.start();
        thread.join();
        System.out.println("iteration "+i);
    

thread.join() 将强制 for 循环进入睡眠状态(可能将其移出 CPU),直到音频线程完成。

【讨论】:

埃德温,这是一个复杂的答案。但是我是这里唯一一个认为 OP 想要做多线程的事情是不正确的吗?具有同步执行的简单循环更容易且资源效率更高。如果您基本上按顺序执行它们,为什么要创建 10,000 个线程,总是在启动线程 n+1 之前等待线程 n 完成?此外,只有 3 个不同的音频文件,我什至会尽可能多地重复使用它们并再次播放它们。 只有当我想模拟几个人同时打字时才需要 IMO 多线程,每个人都有自己的打字节奏。然后,我将每人使用一个线程,并且可以轻松地对程序进行参数化以设置模拟的人数。但除非打字声音重叠,否则每个人都是一个同步线程,对于 n=1,我不需要启动任何线程。但 OP 并没有详细描述他到底想模拟什么。 @kriegaex 你不是唯一的,这就是为什么我说声音和延迟应该有一个执行线程(不管它们是如何排序的)。现在,如果您想要有多种声音,就像他们正在打字一样,那么您可能希望每个模拟人有一个线程(假设音频播放器可以正确混合以进行输出)。 @kriegaex 你们都错了。当有人快速打字时,可以先按一个键,然后再按另一个键,然后松开第一个键。我应该提到,使用的每个声音效果都是按下一个键然后释放的声音(全部在不到 150 毫秒内),所以几乎同时播放另一个声音效果会让听起来像一个键被按下然后另一个然后第一个被释放然后第二个。检查我对问题的编辑。 好吧,@PaikuHan,我不会说我们中的任何一个都错了,但你没有很好地描述用例 - 这导致了很多不必要的工作和讨论周期。就像生活中经常发生的那样,需求工程也遵循“垃圾进,垃圾出”的规则。【参考方案4】:

库AudioCue 正是为这类事情而设计的。您可以尝试运行“青蛙池塘”演示,模拟许多青蛙的叫声,所有这些都是从单个青蛙叫声记录中生成的。

您可以单击打字机并从中运行所有内容,创建一个提示,例如允许 10 个同时重叠。然后使用 RNG 选择要单击的 10 个“光标”中的哪一个。这 10 位打字员可以各自有自己的音量和声相位置,并且音调可能略有不同,因此听起来打字机是不同的型号,或者按键被不同的重量击打(就像旧的手动打字机一样)。

可以针对不同的打字速度(使用不同的睡眠时间)调整 RNG 算法。

对于我自己,我编写了一个事件系统,其中播放命令使用ConcurrentSkipListSet 在事件系统上排队,其中存储的对象包括一个计时值(给定零点后的毫秒数),用于排序为以及控制播放何时执行。如果您不打算经常做这种事情,那可能有点过头了。

【讨论】:

以上是关于为啥我的 Java 应用程序没有在每个循环中播放声音?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 ALSA 示例使用循环播放/捕获?

如何制作和播放程序生成的啁啾声

ALSA:循环声音问题

为啥我的 HTML5 音频不会循环播放?

为啥我的音频功能一直循环而不是一直运行?

在 iOS 上循环播放一段音频