Java音频节拍器|时间和速度问题

时间:2014-06-08 19:59:10

标签: java multithreading audio

我开始研究Java中的音乐/节拍器应用程序,我遇到了时间和速度方面的一些问题。

出于测试目的,我试图以固定的间隔同时播放两个正弦波音,但是它们会同步播放几个节拍然后稍微不同步以获得几个节拍然后再次同步为了几个节拍。

通过研究好的节拍器编程,我发现Thread.sleep()对于计时来说太可怕了,所以我完全避免了这种情况,并检查了System.nanoTime()以确定声音应该播放的时间。

我正在为我的音频播放器使用AudioSystem的SourceDataLine,我正在为每个音调使用一个线程,不断轮询System.nanoTime()以确定声音何时播放。我创建一个新的SourceDataLine并在每次播放声音时删除前一个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();
}
}

1 个答案:

答案 0 :(得分:0)

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

  1. 当节拍器激活时,不是倒计时每个节拍之间的延迟,而是从开始算起延迟。举个例子,比方说你想要每秒钟节拍一次。由于延迟约20ms,在你的旧方法中你会得到20ms,1040,2060,3080等节拍...如果你从头开始倒数并将节拍放在1000,2000,3000等,那么他们将在20ms,1020,2020,3020等播放......由于dalay本身会有所不同,但仍然存在一些差异,但是节拍之间应该有大约1000ms并且它不会不同步(或者至少,问题不会随着时间的推移而恶化,可能无法听到。)

  2. 更好的选择,以及大多数此类程序使用的选项,是生成更大的音乐片段。缓冲例如提前20秒并播放。在那20秒内,时机应该是完美的。当这20秒几乎结束时,你必须产生一些新的声音。如果您可以了解如何执行此操作,则应将新波形附加到旧波形并使其连续播放。否则,只需生成一个新的20秒声音位并接受它们之间的延迟。

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