【问题标题】:Java Audio Metronome | Timing and Speed ProblemsJava 音频节拍器 |时间和速度问题
【发布时间】:2014-06-09 08:54:15
【问题描述】:

我开始使用 Java 开发音乐/节拍器应用程序,但在时间和速度方面遇到了一些问题。

出于测试目的,我尝试定期同时播放两个正弦波音调,但它们会同步播放几拍,然后稍微不同步几拍,然后再次同步播放几拍。

通过研究优秀的节拍器编程,我发现 Thread.sleep() 不适合计时,所以我完全避免了这种情况,并检查 System.nanoTime() 以确定何时播放声音。

我正在为我的音频播放器使用 AudioSystem 的 SourceDataLine,并且我为每个音调使用一个线程,该线程不断轮询 System.nanoTime() 以确定何时播放声音。每次播放声音时,我都会创建一个新的 SourceDataLine 并删除前一个,因为如果我将线路打开并继续在同一线路上播放声音,音量会波动。我在轮询 nanoTime() 之前创建了播放器,这样播​​放器就已经创建好了,它所要做的就是在时间到的时候播放声音。

从理论上讲,这似乎是让每个声音按时播放的好方法,但它不能正常工作。我不确定时间问题是否来自运行不同的线程,或者它是否与删除和重新创建 SourceDataLine 有关,或者它是否在播放声音或究竟是什么......

目前这只是 Java 中的一个简单测试,但我的目标是在移动设备(Android、iOS、Windows Phone 等)上创建我的应用程序......但是我目前的方法甚至没有保持完美的时间在 PC 上,所以我担心某些资源有限的移动设备会有更多的时间问题。我还将为其添加更多声音以创建更复杂的节奏,因此它需要能够同时处理多个声音而不会出现声音滞后。

我遇到的另一个问题是最大速度是由音调的长度控制的,因为音调不会相互重叠。我尝试添加额外的线程,以便播放的每个音调都有自己的线程......但这真的搞砸了时间,所以我把它拿出来了。我想有一种方法可以重叠以前的声音以允许更高的节奏。

如果能帮助您解决这些时间和速度问题,我们将不胜感激! 谢谢。

SoundTest.java:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

import java.io.*;
import javax.sound.sampled.*;

public class SoundTest implements ActionListener {
static SoundTest soundTest;


// ENABLE/DISABLE SOUNDS
boolean playSound1  = true;
boolean playSound2  = true;


JFrame mainFrame;
JPanel mainContent;
JPanel center;
JButton buttonPlay;

int sampleRate = 44100;
long startTime; 
SourceDataLine line = null; 
int tickLength;
boolean playing = false;

SoundElement sound01;
SoundElement sound02;

public static void main (String[] args) {       
    soundTest = new SoundTest();

    SwingUtilities.invokeLater(new Runnable() { public void run() {
        soundTest.gui_CreateAndShow();
    }});
}

public void gui_CreateAndShow() {
    gui_FrameAndContentPanel();
    gui_AddContent();
}

public void gui_FrameAndContentPanel() {
    mainContent = new JPanel();
    mainContent.setLayout(new BorderLayout());
    mainContent.setPreferredSize(new Dimension(500,500));
    mainContent.setOpaque(true);

    mainFrame = new JFrame("Sound Test");               
    mainFrame.setContentPane(mainContent);              
    mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    mainFrame.pack();
    mainFrame.setVisible(true);
}

public void gui_AddContent() {
    JPanel center = new JPanel();
    center.setOpaque(true);

    buttonPlay = new JButton("PLAY / STOP");
    buttonPlay.setActionCommand("play");
    buttonPlay.addActionListener(this);
    buttonPlay.setPreferredSize(new Dimension(200, 50));

    center.add(buttonPlay);
    mainContent.add(center, BorderLayout.CENTER);
}

public void actionPerformed(ActionEvent e) {
    if (!playing) {
        playing = true;

        if (playSound1)
            sound01 = new SoundElement(this, 800, 1);
        if (playSound2)
            sound02 = new SoundElement(this, 1200, 1);

        startTime = System.nanoTime();

        if (playSound1)
            new Thread(sound01).start();
        if (playSound2)
            new Thread(sound02).start();
    }
    else {
        playing = false;
    }
}
}

SoundElement.java

import java.io.*;
import javax.sound.sampled.*;

public class SoundElement implements Runnable {
SoundTest soundTest;


// TEMPO CHANGE
// 750000000=80bpm | 300000000=200bpm | 200000000=300bpm
long nsDelay = 750000000;


int clickLength = 4100; 
byte[] audioFile;
double clickFrequency;
double subdivision;
SourceDataLine line = null;
long audioFilePlay;

public SoundElement(SoundTest soundTestIn, double clickFrequencyIn, double subdivisionIn){
    soundTest = soundTestIn;
    clickFrequency = clickFrequencyIn;
    subdivision = subdivisionIn;
    generateAudioFile();
}

public void generateAudioFile(){
    audioFile = new byte[clickLength * 2];
    double temp;
    short maxSample;

    int p=0;
    for (int i = 0; i < audioFile.length;){
        temp = Math.sin(2 * Math.PI * p++ / (soundTest.sampleRate/clickFrequency));
        maxSample = (short) (temp * Short.MAX_VALUE);
        audioFile[i++] = (byte) (maxSample & 0x00ff);           
        audioFile[i++] = (byte) ((maxSample & 0xff00) >>> 8);
    }
}

public void run() {
    createPlayer();
    audioFilePlay = soundTest.startTime + nsDelay;

    while (soundTest.playing){
        if (System.nanoTime() >= audioFilePlay){
            play();
            destroyPlayer();
            createPlayer();
            audioFilePlay += nsDelay;
        }
    }
    try { destroyPlayer(); } catch (Exception e) { }
}

public void createPlayer(){
    AudioFormat af = new AudioFormat(soundTest.sampleRate, 16, 1, true, false);
    try {
        line = AudioSystem.getSourceDataLine(af);
        line.open(af);
        line.start();
    }
    catch (Exception ex) { ex.printStackTrace(); }
}

public void play(){
    line.write(audioFile, 0, audioFile.length);
}

public void destroyPlayer(){
    line.drain();
    line.close();
}
}

【问题讨论】:

  • 已经 6 年了,您是否解决了问题,因为我遇到了同样的问题,但在 Android 中。如果解决了,那你是怎么做到的?
  • 是的,我解决了。我曾在 Java-Gaming 方面寻求帮助,并且有人能够帮助我使其正常工作。这是线程,在帖子 #19 上,我提供了我的源代码:jvm-gaming.org/t/java-audio-metronome-timing-and-speed-problems/…

标签: java multithreading audio


【解决方案1】:

这种事情很难做对。您必须意识到,为了播放声音,必须将其加载到音频驱动程序(可能还有声卡)中。这需要时间,你必须考虑到这一点。基本上有两种选择:

  1. 与其倒计时每个节拍之间的延迟,不如倒计时开始时的延迟,当节拍器激活时。例如,假设您想要每秒节拍一次。由于约 20 毫秒的延迟,在您的旧方法中,您会在 20 毫秒、1040、2060、3080 等处获得节拍...如果您从开始倒计时并将节拍放在 1000、2000、3000 等处,那么他们将以 20 毫秒、1020、2020、3020 等播放...由于 dalay 本身会有所不同,因此仍然会有一些差异,但节拍之间应该有大约 1000 毫秒,并且不会不同步(或至少,问题不会随着时间的推移变得更糟,而且很可能听不见)。

  2. 更好的选择,也是大多数此类程序使用的选择,是生成更大的音乐片段。例如提前 20 秒缓冲并播放。在这 20 秒内,时机应该是完美的。当这 20 秒快结束时,您必须产生一些新的声音。如果您能找到如何做到这一点,您应该将新波形附加到旧波形上并让它连续播放。否则,只需生成一个新的 20 秒声音位并接受它们之间的延迟。

现在至于您的声音无法重叠的问题...我不是专家,我真的不知道答案,但我知道:如果您需要,必须混合声音重叠。您可以通过组合波形字节在软件中自己做到这一点(我认为这是一些对数空间中的加法),或者您需要将不同的重叠声音发送到不同的“通道”,在这种情况下,音频驱动程序或声卡会它给你。我不知道这在 Java 中是如何工作的,或者我忘记了,但我通过反复试验和使用 .mod 文件了解到这一点。

【讨论】:

  • 感谢幽灵守护者的回复。我不得不单独发布一个答案,因为它太长了,无法直接回复您的回复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-25
  • 1970-01-01
相关资源
最近更新 更多