【问题标题】:Java - Audible Lag with Custom MIDI Sequencer using PPQ Ticks for TimingJava - 使用 PPQ Ticks 进行定时的自定义 MIDI 音序器的声音延迟
【发布时间】:2015-04-22 14:52:42
【问题描述】:

我一直在尝试实现我自己的Java 中的异步 MIDI 音序器,它通过处理 MidiEvent 的列表将 ShortMessage 发送到 VST。我需要性能达到最佳状态,以便在收听音频输出时不会出现延迟。

问题在于,随着滴答声的增量不准确(有时增量太快或太慢,这会打乱所有 MidiEvent 的时间),肯定会出现可听见的延迟。

下面是sequencer的代码:

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

【问题讨论】:

  • 以下是一篇关于 Java 实时、低延迟音频处理的好论文。有很多问题。抱歉,我没有实际的答案。我想了解有关 VST 编程的更多信息,并且会潜伏。不过,您可能会觉得这篇文章很有趣。 quod.lib.umich.edu/cgi/p/pod/…
  • 非常感谢这篇文章!我实际上是在为两个科学项目竞赛做这个项目,阅读绝对让我牢记 Java 潜在的音频延迟问题。此外,我已经设法通过让 VST 处理事件与事件序列器并行来解决这个问题 :)。我编辑了线程以包含新代码。
  • 感谢您提供代码!我已将它们添加为书签以供将来参考。我有一个我制作的 Java FM 合成器和一些我希望有一天能变成 VST 的效果器,可以在我的 DAW 中使用。

标签: java performance audio time midi


【解决方案1】:

我怀疑问题是:

event.setTick(event.getTick() + elapsedTicks);
add.put(event, event.getTick());

事件流可能已经有时间戳,所以不需要添加elapsedTicks。这只是意味着随着时间的推移,它们会越来越晚。

有几种非常明显的方法可以提高上述代码的性能。很难说它们是否是您的问题的原因:

1:不要忙-等待:上面的代码在有事可做之前无法阻塞(ConcurrentHashMap 不提供阻塞行为)。相反,即使无事可做,它也会不断循环消耗 CPU 周期。这种行为经常受到操作系统调度程序的惩罚。您的线程在未运行时无法安排事件,当前的设计鼓励这样做。

2: 为currentEvents 使用带有 MIDIEvent 键的 HashMap 是一个糟糕的选择并且效率低下。您需要迭代整个容器以查找需要传递到 VST 的事件。此外,由于没有顺序保证,您可能会在当前滴答中无序地交付事件。考虑使用SortedMap,其中关键是交货时间。现在对事件进行排序,最早的事件位于结构的开头。举办活动很便宜。

这条线还有一个潜在的问题——它不会导致不规则的计时,但我可能是说 oneTick 是错误的:

private long oneTick = (60000L / (BPM * PPQ)) * 1000000;

除以BPM * PPQ 会导致截断。先执行乘法。

【讨论】:

  • 非常感谢您提供的性能提示!实际上,我已经设法通过与音序器本身并行处理 VST 事件来解决可听延迟,但是通过您的提示,我对提高代码效率有了更好的了解:)。我还用一个简单的 ArrayList 替换了 Map,看到每个事件的刻度也可以在线程的循环中更新,而不是将其作为键。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-12-29
  • 1970-01-01
  • 2013-06-08
  • 1970-01-01
  • 2016-01-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多