我一直在尝试用Java 实现我自己的异步MIDI音序器,它通过处理MidiEvent的列表将ShortMessage发送到VST,这就是为什么我需要将性能设置为最佳,以便在列出音频输出时不会出现声音延迟。
虽然主要的问题是肯定存在可听见的延迟,因为嘀嗒声增加不准确(增加太快或太慢,有时会扰乱所有MidiEvent的时间)。
以下是音序器的代码:
package com.dranithix.spectrum.vst;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.ShortMessage;
import com.synthbot.audioplugin.vst.vst2.JVstHost2;
/**
*
* @author Kenta Iwasaki
*
*/
public class VstSequencer implements Runnable {
public static long BPM = 128L, PPQ = 4L;
private long oneTick = (60000L / (BPM * PPQ)) * 1000000;
private Map<MidiEvent, Long> currentEvents = new ConcurrentHashMap<MidiEvent, Long>();
private long startTime = System.nanoTime(), elapsedTicks = 0;
private JVstHost2 vst;
public VstSequencer(JVstHost2 vst) {
this.vst = vst;
}
@Override
public void run() {
while (true) {
if (System.nanoTime() - startTime >= oneTick) {
elapsedTicks++;
startTime = System.nanoTime();
}
Iterator<MidiEvent> it = currentEvents.keySet().iterator();
while (it.hasNext()) {
MidiEvent currentEvent = it.next();
long eventTime = currentEvent.getTick() - elapsedTicks;
if (eventTime <= 0) {
vst.queueMidiMessage((ShortMessage) currentEvent
.getMessage());
it.remove();
}
}
}
}
public void queueEvents(List<MidiEvent> events) {
Map<MidiEvent, Long> add = new HashMap<MidiEvent, Long>();
for (MidiEvent event : events) {
event.setTick(event.getTick() + elapsedTicks);
add.put(event, event.getTick());
}
currentEvents.putAll(add);
}
public void queueEvent(MidiEvent event) {
event.setTick(event.getTick() + elapsedTicks);
currentEvents.put(event, event.getTick());
}
}
有没有办法改善这个系统的性能,或者有没有办法确保这种系统没有听觉延迟(类似于固定的时间步长)?
提前致谢。
编辑:为了找出声音延迟的原因,我可以确认VST本身或从发送MIDI信息到VST的框架没有延迟。它与目前在音序器中使用的基于音调的定时系统有关。
已解决:我通过将VST事件处理代码包含在同一个线程中(它们最初位于不同的线程上)来解决VST事件与事件序列器本身并行处理的问题。对于那里的任何人都读取这个并且一直在寻找将MIDI事件排序到JVstHost2或任何类似的Java VST主机库,随意使用部分固定代码用于您自己的项目,因为我很难找到合适的VST排序在线,因为VST是一种很少用Java接触的商业格式。
解决代码:
package com.dranithix.spectrum.vst;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.ShortMessage;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import com.synthbot.audioplugin.vst.vst2.JVstHost2;
/**
*
* @author Kenta Iwasaki
*
*/
public class VstSequencer implements Runnable {
private static final float ShortMaxValueAsFloat = (float) Short.MAX_VALUE;
public static float BPM = 120f, PPQ = 2f;
private static float oneTick = 60000f / (BPM * PPQ);
private List<MidiEvent> currentEvents = new ArrayList<MidiEvent>();
private long startTime = System.currentTimeMillis(), elapsedTicks = 0;
private JVstHost2 vst;
private final float[][] fInputs;
private final float[][] fOutputs;
private final byte[] bOutput;
private int blockSize;
private int numOutputs;
private int numAudioOutputs;
private AudioFormat audioFormat;
private SourceDataLine sourceDataLine;
public VstSequencer(JVstHost2 vst) {
this.vst = vst;
numOutputs = vst.numOutputs();
numAudioOutputs = Math.min(2, numOutputs);
blockSize = vst.getBlockSize();
fInputs = new float[vst.numInputs()][blockSize];
fOutputs = new float[numOutputs][blockSize];
bOutput = new byte[numAudioOutputs * blockSize * 2];
audioFormat = new AudioFormat((int) vst.getSampleRate(), 16,
numAudioOutputs, true, false);
DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class,
audioFormat);
sourceDataLine = null;
try {
sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
sourceDataLine.open(audioFormat, bOutput.length);
sourceDataLine.start();
} catch (LineUnavailableException lue) {
lue.printStackTrace(System.err);
System.exit(1);
}
}
@Override
protected void finalize() throws Throwable {
try {
sourceDataLine.drain();
sourceDataLine.close();
} finally {
super.finalize();
}
}
private byte[] floatsToBytes(float[][] fData, byte[] bData) {
int index = 0;
for (int i = 0; i < blockSize; i++) {
for (int j = 0; j < numAudioOutputs; j++) {
short sval = (short) (fData[j][i] * ShortMaxValueAsFloat);
bData[index++] = (byte) (sval & 0x00FF);
bData[index++] = (byte) ((sval & 0xFF00) >> 8);
}
}
return bData;
}
@Override
public void run() {
while (true) {
if (Thread.interrupted()) {
break;
}
if (System.currentTimeMillis() - startTime >= oneTick) {
elapsedTicks++;
startTime = System.currentTimeMillis();
}
vst.processReplacing(fInputs, fOutputs, blockSize);
sourceDataLine.write(floatsToBytes(fOutputs, bOutput), 0,
bOutput.length);
Iterator<MidiEvent> it = currentEvents.iterator();
while (it.hasNext()) {
MidiEvent currentEvent = it.next();
long eventTime = currentEvent.getTick() - elapsedTicks;
if (eventTime <= 0) {
vst.queueMidiMessage((ShortMessage) currentEvent
.getMessage());
it.remove();
}
}
}
}
public void queueEvents(List<MidiEvent> events) {
for (MidiEvent event : events) {
event.setTick(event.getTick() + elapsedTicks);
}
currentEvents.addAll(events);
}
public void queueEvent(MidiEvent event) {
event.setTick(event.getTick() + elapsedTicks);
currentEvents.add(event);
}
}
答案 0 :(得分:2)
我怀疑问题是:
event.setTick(event.getTick() + elapsedTicks);
add.put(event, event.getTick());
事件流可能已经有时间戳,因此不需要向它们添加elapsedTicks
。这只是意味着随着时间的推移它们会越来越晚。
有几种非常明显的方法可以改善上面代码的性能。他们是否是你问题的原因很难说:
1:不要忙等待:上面的代码无法阻止,直到有事情要做(ConcurrentHashMap
不提供阻止行为)。相反,它会循环不断地燃烧CPU周期,即使没有任何事情可做。这种行为经常受到操作系统调度程序的惩罚。你的线程无法在没有运行的情况下安排事件,而且它的当前设计鼓励这一点。
2:使用带有MIDIEvent的HashMap for currentEvents
是一个糟糕的选择,而且效率低下。您需要迭代整个容器以查找需要传递到VST的事件。此外,由于没有订购保证,您可能无法按顺序提供当前滴答中的事件。考虑使用SortedMap
,其中密钥是交付时间。现在订购事件,最快结构的开头。提供活动很便宜。
另一个潜在的问题在于这条线 - 它不会导致不规则的时间,但我可能意味着oneTick错了:
private long oneTick = (60000L / (BPM * PPQ)) * 1000000;
BPM * PPQ
除法导致截断。首先执行乘法。