为什么我的 Java 应用程序不在每个循环中播放声音?

Why doesn't my Java application play the sound at each loop?

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

开始时它工作正常,但在大约 第 95 次 迭代后,它停止播放声音(同时仍在循环)不到 4 秒然后再次播放声音。在 160th 迭代之后,它几乎每秒播放一次声音(而不是每三分之一到六分之一秒)。
一段时间后,它会停止播放很长时间的声音,然后永远停止播放。

这是 AudioPlayer.java class 的来源:

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 class 来测试击键模拟器:

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 个)。

编辑

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

因此,我将首先解释我究竟希望该程序做什么。它应该能够模拟击键,所以我使用了 Thread,因为它允许两个击键声音重叠,就像真人打字时一样。基本上我使用的声音片段是击键声音,击键声音由两种声音组成: 按键的声音。 释放按键的声音。

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

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

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),这是不好的,因为它听起来不像普通的打字员(更像是刚开始打字并且在打字时仍在寻找每个键的人)。

调用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 的睡眠持续时间) , 同样,出于与以前相同的原因,它也不好。

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

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

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

编辑 音效可以下载here.

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

类似

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

会表示你 "wait" 然后 "run a wav" 然后 "repeat".

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

几率介于您当前的等待时间和当前的 "play the wave" 中间还有一些其他事情,如果有足够的时间,甚至可能会乱序播放 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 循环每次迭代比如果 CPU 必须处理其他内容(如网络数据包),AudioPlayer 需要每个 wav 文件。

    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)直到音频线程完成。

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

另请注意,在您的 AudioPlayer class 中,您需要等待 500 毫秒才能让声音结束,这比您等待播放下一个声音的时间要长。一段时间后,这将导致您 运行 没有可用线程...也许当 AudioClip 完成时您可以使用回调,而不是等待。

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

中相应的静态常量来调整打字速度
package de.scrum_master.Whosebug.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.Whosebug.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,即您可以在调用应用程序中使用 "try with resouces"。这样可以确保在程序结束时自动关闭所有剪辑。

重播同一个片段的关键当然是clip.setMicrosecondPosition(0)再开始。


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

package de.scrum_master.Whosebug.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";
  }
}

AudioCue 正是为这类事情而创建的。您可以尝试 运行使用 "frog pond" 演示,模拟许多青蛙都在呱呱叫,所有这些都是从单个青蛙呱呱录音中生成的。

您可以点击打字机,然后 运行 一切,创建一个提示,比如允许 10 个同时重叠。然后使用 RNG 来选择要点击的 10 "cursors" 中的哪一个。 10 名打字员每个人都可以有自己的音量和平移位置,并且音高可以略有不同,这样听起来就像打字机是不同的型号或者按键的重量不同(就像旧的手动打字机一样)。

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

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