随着时间的推移,基本软件合成器的延迟会增加

时间:2018-02-18 10:07:46

标签: c++ synthesizer jack sound-synthesis rtaudio

我正在完成MIDI控制软件合成器的整理过程。 MIDI输入和合成工作正常,但我似乎有一个问题是播放音频本身。

我使用jackd作为我的音频服务器,因为可以为低延迟应用程序配置它,例如在我的情况下,实时MIDI乐器,alsa作为jackd后端。

在我的程序中,我使用RtAudio这是一个相当着名的C ++库,可以连接到各种声音服务器,并为它们提供基本的流操作。顾名思义,它针对实时音频进行了优化。

我还使用Vc库,它是一个为各种数学函数提供矢量化的库,以加速添加剂合成过程。我基本上将不同频率和幅度的大量正弦波相加,以便在输出上产生复杂的波形,例如锯齿波或方波。

现在,问题不在于延迟很高,因为这可能会解决或归咎于很多事情,例如MIDI输入或不支持。问题是我的软合成器和最终音频输出之间的延迟开始非常低,几分钟后,它会变得难以忍受。

由于我打算使用它来播放" live",即在我的家里,我真的不愿意在我的按键和音频反馈之间不断增长的延迟时间听到。

我已经尝试减少将问题重复出现的代码库,我无法再进一步减少它。

#include <queue>
#include <array>
#include <iostream>
#include <thread>
#include <iomanip>
#include <Vc/Vc>
#include <RtAudio.h>
#include <chrono>
#include <ratio>
#include <algorithm>
#include <numeric>


float midi_to_note_freq(int note) {
    //Calculate difference in semitones to A4 (note number 69) and use equal temperament to find pitch.
    return 440 * std::pow(2, ((double)note - 69) / 12);
}


const unsigned short nh = 64; //number of harmonics the synthesizer will sum up to produce final wave

struct Synthesizer {
    using clock_t = std::chrono::high_resolution_clock;


    static std::chrono::time_point<clock_t> start_time;
    static std::array<unsigned char, 128> key_velocities;

    static std::chrono::time_point<clock_t> test_time;
    static std::array<float, nh> harmonics;

    static void init();
    static float get_sample();
};


std::array<float, nh> Synthesizer::harmonics = {0};
std::chrono::time_point<std::chrono::high_resolution_clock> Synthesizer::start_time, Synthesizer::test_time;
std::array<unsigned char, 128> Synthesizer::key_velocities = {0};


void Synthesizer::init() { 
    start_time = clock_t::now();
}

float Synthesizer::get_sample() {

    float t = std::chrono::duration_cast<std::chrono::duration<float, std::ratio<1,1>>> (clock_t::now() - start_time).count();

    Vc::float_v result = Vc::float_v::Zero();

    for (int i = 0; i<key_velocities.size(); i++) {
        if (key_velocities.at(i) == 0) continue;
        auto v = key_velocities[i];
        float f = midi_to_note_freq(i);
        int j = 0;
        for (;j + Vc::float_v::size() <= nh; j+=Vc::float_v::size()) {
            Vc::float_v twopift = Vc::float_v::generate([f,t,j](int n){return 2*3.14159268*(j+n+1)*f*t;});
            Vc::float_v harms = Vc::float_v::generate([harmonics, j](int n){return harmonics.at(n+j);});
            result += v*harms*Vc::sin(twopift); 
        }
    }
    return result.sum()/512;
}                                                                                                


std::queue<float> sample_buffer;

int streamCallback (void* output_buf, void* input_buf, unsigned int frame_count, double time_info, unsigned int stream_status, void* userData) {
    if(stream_status) std::cout << "Stream underflow" << std::endl;
    float* out = (float*) output_buf;
    for (int i = 0; i<frame_count; i++) {
        while(sample_buffer.empty()) {std::this_thread::sleep_for(std::chrono::nanoseconds(1000));}
        *out++ = sample_buffer.front(); 
        sample_buffer.pop();
    }
    return 0;
}


void get_samples(double ticks_per_second) {
    double tick_diff_ns = 1e9/ticks_per_second;
    double tolerance= 1/1000;

    auto clock_start = std::chrono::high_resolution_clock::now();
    auto next_tick = clock_start + std::chrono::duration<double, std::nano> (tick_diff_ns);
    while(true) {
        while(std::chrono::duration_cast<std::chrono::duration<double, std::nano>>(std::chrono::high_resolution_clock::now() - next_tick).count() < tolerance) {std::this_thread::sleep_for(std::chrono::nanoseconds(100));}
        sample_buffer.push(Synthesizer::get_sample());
        next_tick += std::chrono::duration<double, std::nano> (tick_diff_ns);
    }
}


int Vc_CDECL main(int argc, char** argv) {
    Synthesizer::init();

    /* Fill the harmonic amplitude array with amplitudes corresponding to a sawtooth wave, just for testing */
    std::generate(Synthesizer::harmonics.begin(), Synthesizer::harmonics.end(), [n=0]() mutable {
            n++;
            if (n%2 == 0) return -1/3.14159268/n;
            return 1/3.14159268/n;
        });

    RtAudio dac;

    RtAudio::StreamParameters params;
    params.deviceId = dac.getDefaultOutputDevice();
    params.nChannels = 1;
    params.firstChannel = 0;
    unsigned int buffer_length = 32;

    std::thread sample_processing_thread(get_samples, std::atoi(argv[1]));
    std::this_thread::sleep_for(std::chrono::milliseconds(10));

    dac.openStream(&params, nullptr, RTAUDIO_FLOAT32, std::atoi(argv[1]) /*sample rate*/, &buffer_length /*frames per buffer*/, streamCallback, nullptr /*data ptr*/);

    dac.startStream();

    bool noteOn = false;
    while(true) {
        noteOn = !noteOn;
        std::cout << "noteOn = " << std::boolalpha << noteOn << std::endl;
        Synthesizer::key_velocities.at(65) = noteOn*127;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    sample_processing_thread.join();
    dac.stopStream();
}

使用g++ -march=native -pthread -o synth -Ofast main.cpp /usr/local/lib/libVc.a -lrtaudio

进行编译

程序期望采样率作为第一个参数。在我的设置中,我使用jackd -P 99 -d alsa -p 256 -n 3 &作为我的声音服务器(需要当前用户的实时优先权限)。由于jackd的默认采样率为48 kHz,因此我使用./synth 48000运行程序。

alsa可以用作声音服务器,但我可能会因为包含jackdpulseaudio互动等不明原因而使用alsa

如果你完全运行该程序,你应该听到一个希望不太烦人的锯齿波播放而不是定期播放,当播放应该开始和停止时控制台输出打开。当noteOn设置为true时,合成器会以任何频率开始产生锯齿波,并在noteOn设置为false时停止。

您希望首先看到noteOn truefalse与音频播放和停止几乎完全一致,但一点一点地,音频源开始滞后在我的机器上开始变得非常明显,大约1分钟到1分30秒。

我99%确定它与我的程序无关,原因如下。

&#34;音频&#34;通过该计划走这条路。

  • 按下键。

  • 时钟在sample_processing_thread中以48 kHz的速率进行,并调用Synthesizer::get_sample并将输出传递给std::queue,后者将用作样本缓冲区。

  • 每当RtAudio流需要样本时,它就会从样本缓冲区中获取样本并继续前进。

这里唯一可能成为不断增加的延迟的因素是时钟滴答,但它的流速与流消耗样本的速率相同,因此无法实现。如果时钟滴答较慢,RtAudio会抱怨流不足,并且会出现明显的音频损坏,这种情况并非如此。

然而,时钟可以更快点击,但我不认为是这样,因为我已经在很多场合自己测试了时钟,虽然它确实显示了一点点抖动,大约纳秒,这是可以预料的。时钟本身没有累积延迟。

因此,增长延迟的唯一可能来源是RtAudio或声音服务器本身的内部功能。我已经谷歌了一下,没有找到任何用处。

我一直试图解决这个问题一两个星期,我已经测试了我身边可能出错的一切,并且它按预期工作,所以我真的不知道是什么可能会发生。

我尝试了什么

  • 检查时钟是否有某种累积延迟:没有注意到累积延迟
  • 定时按键和产生的第一个音频样本之间的延迟,以查看此延迟是否随时间增长:延迟未随时间增长
  • 定时请求样本的流与发送到流的样本之间的延迟(stream_callback的开始和结束):延迟没有随时间增长

1 个答案:

答案 0 :(得分:2)

我认为你的get_samples线程生成的音频比streamCallback消耗它们更快或更慢。使用时钟进行流量控制是不可靠的。

修复的简单方法,删除该线程和sample_buffer队列并直接在streamCallback函数中生成样本。

如果您确实想为您的应用程序使用多线程,则需要在生产者和消费者之间进行适当的同步。更复杂。但简而言之,步骤如下。

  1. 用一个相当小的固定长度循环缓冲区替换你的队列。从技术上讲,std :: queue也可以工作,因为基于指针的速度很慢,你需要手动限制max.size。

  2. 在生产者线程中实现无限循环,检查缓冲区中是否有空格,如果有空间产生更多音频,如果没有,则等待消费者从缓冲区消耗数据。

  3. 在消费者streamCallback回调中,将数据从循环缓冲区复制到output_buf。如果没有足够的数据可用,请唤醒生产者线程并等待它生成数据。

  4. 不幸的是,有效的实现非常棘手。您需要同步来保护共享数据,但您不需要太多同步,否则生产者和使用者将被序列化,并且只使用单个硬件线程。一种方法是使用单个std :: mutex来保护缓冲区,同时移动指针/ size / ofset(但在读取/写入数据时解锁),以及两个std :: condition_variable,一个供生产者在没有空闲空间时休眠。缓冲区,当缓冲区中没有数据时,另一个用于消费者休眠。