VST 主机播放时序问题 (VST.NET + NAudio)

VST host playback timing issues (VST.NET + NAudio)

我刚刚开始尝试为我已经开发了一段时间的小型音乐程序设计 VST 插件托管。我现在已经达到了我能够将旋律存储在我的程序中并将 midi 数据发送到托管插件(使用 VST.NET)并将音频输出到 WaveOut(NAudio)的地步。问题是音频输出播放得太快而且不及时。

这是我用于播放的代码,其中部分代码基于 VST.NET 示例项目中的示例主机:

public class PhraseEditorWaveProvider : VstWaveProvider
{

    public PhraseEditor PhraseEditor { get; private set; }


    public Rational PlaybackBeat { get; private set; }


    public PhraseEditorWaveProvider(PhraseEditor phraseEditor, string pluginPath, WaveFormat waveFormat = null)
        : base(pluginPath, waveFormat)
    {
        PhraseEditor = phraseEditor;
    }



    public override int Read(byte[] buffer, int offset, int count)
    {
        decimal framesPerBeat = (60 / (decimal)PhraseEditor.Phrase.Tempo) * WaveFormat.SampleRate;

        Rational startBeat = PlaybackBeat;
        Rational endBeat = startBeat + Rational.FromDecimal(count / framesPerBeat);

        //Get list of note starts and note ends that occur within the beat range
        List<VstEvent> vstEvents = new List<VstEvent>();
        foreach(Note note in PhraseEditor.Phrase.Notes)
        {
            if(note.StartBeat >= startBeat && note.StartBeat < endBeat)
                vstEvents.Add(NoteOnEvent(1, (byte)note.Pitch.Value, 100, (int)(note.Duration * framesPerBeat), (int)((note.StartBeat - startBeat) * framesPerBeat)));

            if(note.EndBeat >= startBeat && note.EndBeat < endBeat)
                vstEvents.Add(NoteOffEvent(1, (byte)note.Pitch.Value, (int)((note.EndBeat - startBeat) * framesPerBeat)));
        }

        foreach(Chord chord in PhraseEditor.Phrase.Chords)
        {
            if(chord.StartBeat >= startBeat && chord.StartBeat < endBeat)
            {
                //Play each note within a chord in the 4th octave, with velocity 70
                foreach (Pitch pitch in chord.Pitches)
                    vstEvents.Add(NoteOnEvent(1, (byte)((pitch.Value % 12) + 48), 70, (int)(chord.Duration * framesPerBeat), (int)((chord.StartBeat - startBeat) * framesPerBeat)));
            }
            if(chord.EndBeat >= startBeat && chord.EndBeat < endBeat)
            {
                foreach(Pitch pitch in chord.Pitches)
                    vstEvents.Add(NoteOffEvent(1, (byte)((pitch.Value % 12) + 48), (int)((chord.EndBeat - startBeat) * framesPerBeat)));
            }
        }
        PlaybackBeat = endBeat;

        return base.Read(vstEvents.OrderBy(x => x.DeltaFrames).ToArray(), buffer, offset, count);
    }

}

public abstract class VstWaveProvider : IWaveProvider
{

    private WaveFormat _waveFormat;
    public WaveFormat WaveFormat
    {
        get
        {
            return _waveFormat;
        }
        set
        {
            _waveFormat = value;
            BytesPerWaveSample = _waveFormat.BitsPerSample / 8;
        }
    }

    public VstPluginContext VstContext { get; private set; }

    public int BytesPerWaveSample { get; private set; }



    public VstWaveProvider(VstPluginContext vstContext, WaveFormat waveFormat = null)
    {
        WaveFormat = (waveFormat == null) ? new WaveFormat(44100, 2) : waveFormat;
        VstContext = vstContext;
    }

    public VstWaveProvider(string pluginPath, WaveFormat waveFormat = null)
    {
        WaveFormat = (waveFormat == null) ? new WaveFormat(44100, 2) : waveFormat;
        VstContext = OpenPlugin(pluginPath);
    }


    public abstract int Read(byte[] buffer, int offset, int count);


    protected int Read(VstEvent[] vstEvents, byte[] outputBuffer, int offset, int count)
    {
        VstAudioBufferManager inputBuffers = new VstAudioBufferManager(
            VstContext.PluginInfo.AudioInputCount,
            count / (Math.Max(1, VstContext.PluginInfo.AudioInputCount) * BytesPerWaveSample)
        );

        return Read(inputBuffers, vstEvents, outputBuffer, offset, count);
    }


    protected int Read(VstAudioBufferManager inputBuffers, VstEvent[] vstEvents, byte[] outputBuffer, int offset, int count)
    {
        VstAudioBufferManager outputBuffers = new VstAudioBufferManager(
            VstContext.PluginInfo.AudioOutputCount,
            count / (VstContext.PluginInfo.AudioOutputCount * BytesPerWaveSample)
        );

        VstContext.PluginCommandStub.StartProcess();
        if(vstEvents.Length > 0)
            VstContext.PluginCommandStub.ProcessEvents(vstEvents);
        VstContext.PluginCommandStub.ProcessReplacing(inputBuffers.ToArray(), outputBuffers.ToArray());
        VstContext.PluginCommandStub.StopProcess();

        //Convert from multi-track to interleaved data
        int bufferIndex = offset;
        for (int i = 0; i < outputBuffers.BufferSize; i++)
        {
            foreach (VstAudioBuffer vstBuffer in outputBuffers)
            {
                Int16 waveValue = (Int16)((vstBuffer[i] + 1) * 128);
                byte[] bytes = BitConverter.GetBytes(waveValue);
                outputBuffer[bufferIndex] = bytes[0];
                outputBuffer[bufferIndex + 1] = bytes[1];
                bufferIndex += 2;
            }
        }
        return count;
    }


    private VstPluginContext OpenPlugin(string pluginPath)
    {
        HostCommandStub hostCmdStub = new HostCommandStub();
        hostCmdStub.PluginCalled += new EventHandler<PluginCalledEventArgs>(HostCmdStub_PluginCalled);

        VstPluginContext ctx = VstPluginContext.Create(pluginPath, hostCmdStub);

        ctx.Set("PluginPath", pluginPath);
        ctx.Set("HostCmdStub", hostCmdStub);

        ctx.PluginCommandStub.Open();
        ctx.PluginCommandStub.MainsChanged(true);

        return ctx;
    }


    private void HostCmdStub_PluginCalled(object sender, PluginCalledEventArgs e)
    {
        Debug.WriteLine(e.Message);
    }


    protected VstMidiEvent NoteOnEvent(byte channel, byte pitch, byte velocity, int noteLength, int deltaFrames = 0)
    {
        return new VstMidiEvent(deltaFrames, noteLength, 0, new byte[] { (byte)(144 + channel), pitch, velocity, 0 }, 0, 0);
    }


    protected VstMidiEvent NoteOffEvent(byte channel, byte pitch, int deltaFrames = 0)
    {
        return new VstMidiEvent(deltaFrames, 0, 0, new byte[] { (byte)(144 + channel), pitch, 0, 0 }, 0, 0);
    }

}

它会被以下调用:

WaveOut waveOut = new WaveOut(WaveCallbackInfo.FunctionCallback());
waveOut.Init(new PhraseEditorWaveProvider(this, @"C:\Users\james\Downloads\Cobalt\Cobalt\Cobalt 64bit\Cobalt.dll"));
waveOut.Play();

其中 Cobalt 是我用于测试的当前插件。

就上下文而言,Rational 是我自己的数据类型,因为我程序的其他部分正在对旋律进行大量操作,而且我发现双精度和小数没有给我所需的精度。

此外,VST 插件上下文和 WaveOut 都设置为具有 44.1kHz 的采样率,因此在将插件输出数据传递到 WaveOut 缓冲区时不需要任何 up/down-sampling。

我完全不明白为什么音频播放速度比预期的要快。它似乎比预期快大约 4 倍。如果有人可以提供任何指示可能导致此问题的原因,我将不胜感激。

由于播放超时,我怀疑这是因为我没有正确理解 deltaFrame 属性 在 VstMidiEvent 中的工作方式。我试过同时使用 deltaFrame 和 noteOffset,尽管两者似乎都不太走运,但我目前正在假设它们从当前数据块开始测量音频帧数, 到该块内的事件时间。不幸的是,我一直在努力寻找关于此的有用文档,所以我可能完全错了。

期待任何回复

亲切的问候

詹姆斯

好的,我想我找到了导致问题的原因,它在这段代码中:

public override int Read(byte[] buffer, int offset, int count)
{
    decimal framesPerBeat = (60 / (decimal)PhraseEditor.Phrase.Tempo) * WaveFormat.SampleRate;

    Rational startBeat = PlaybackBeat;
    Rational endBeat = startBeat + Rational.FromDecimal(count / framesPerBeat);
    ...
}

我刚刚改成了这个:

public override int Read(byte[] buffer, int offset, int count)
{
    decimal framesPerBeat = (60 / (decimal)PhraseEditor.Phrase.Tempo) * WaveFormat.SampleRate;
    int samplesRequired = count / (WaveFormat.Channels * (WaveFormat.BitsPerSample / 8));

    Rational startBeat = PlaybackBeat;
    Rational endBeat = startBeat + Rational.FromDecimal(samplesRequired / framesPerBeat);
    ...
}

我犯了一个愚蠢的错误,除了获取即将到来的 MIDI 事件的方法外,我一直在从比特率转换为采样率。我的音频现在以更接近我预期的速度播放,并且在时间上似乎更可靠,尽管我还没有机会对此进行全面测试。