Java - Audible Lag с пользовательским MIDI-секвенсором, использующим тики PPQ для синхронизации
Я пытался реализовать свой собственный асинхронный MIDI-секвенсор в Java, который отправляет ShortMessage в VST, обрабатывая списки MidiEvent, поэтому мне нужно, чтобы производительность была оптимальной, чтобы не было слышимой задержки при выводе на аудиовыход.
Хотя главная проблема заключается в том, что определенно есть слышимое отставание, поскольку тики увеличиваются неточно (увеличиваются или слишком быстро или слишком медленно, что портит синхронизацию всех событий 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 Host, не стесняйтесь использовать части фиксированного кода для ваших собственных проектов, так как мне было очень трудно найти правильную последовательность 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);
}
}
1 ответ
Я подозреваю, что проблема заключается в:
event.setTick(event.getTick() + elapsedTicks);
add.put(event, event.getTick());
Поток событий, по-видимому, уже имеет метки времени, поэтому нет необходимости добавлять elapsedTicks
им. Это просто означает, что они становятся позже и позже, по мере того, как время прогрессирует
Есть несколько очень очевидных способов улучшить производительность кода выше. Трудно сказать, являются ли они причиной ваших проблем:
1: Не заняты - подождите: приведенный выше код не имеет возможности блокировать, пока не будет что-то сделать (ConcurrentHashMap
не обеспечивает блокировку поведения). Вместо этого он непрерывно записывает циклы ЦП, даже когда ничего не нужно делать. Такое поведение часто наказывается планировщиками операционной системы. Ваш поток не может планировать события, когда он не запущен, и его текущий дизайн поощряет это.
2: Использование HashMap с ключом MIDIEvent для currentEvents
плохой выбор и неэффективный. Вам нужно перебрать весь контейнер, чтобы найти события, которые нужно доставить в VST. Кроме того, поскольку нет гарантии заказа, вы потенциально доставляете события, выпадающие в текущем тике из строя. Рассмотрите возможность использования SortedMap
где ключ это время доставки. События теперь упорядочены, причем самые быстрые в начале структуры. Доставка событий стоит дешево.
Еще одна потенциальная проблема связана с этой строкой - она не будет вызывать нерегулярное время, но я могу сказать, что oneTick не так:
private long oneTick = (60000L / (BPM * PPQ)) * 1000000;
Деление на BPM * PPQ
вызывает усечение. Сначала выполните умножение.