Я заканчиваю программный синтезатор с MIDI-управлением. Вход MIDI и синтез работают нормально, но у меня возникла проблема с воспроизведением самого звука.
Я использую jackd
в качестве своего аудиосервера из-за возможности настроить его для приложений с низкой задержкой, например, в моем случае, для MIDI-инструментов в реальном времени, с alsa
в качестве jackd
бэкенда.
В своей программе я использую RtAudio
, довольно известную библиотеку C++ для подключения к различным звуковым серверам и предлагающую базовые потоковые операции на них. Как следует из названия, он оптимизирован для звука в реальном времени.
Я также использую библиотеку Vc
, которая обеспечивает векторизацию различных математических функций, чтобы ускорить процесс аддитивного синтеза. Я в основном складываю множество синусоид разных частот и амплитуд, чтобы получить на выходе сложную форму волны, например, пилообразную или прямоугольную.
Теперь проблема не в том, что задержка высока с самого начала, поскольку это, вероятно, можно решить или обвинить во многих вещах, таких как ввод MIDI или что-то еще. Проблема в том, что задержка между моим программным синтезатором и конечным аудиовыходом начинается очень мало, а через пару минут становится невыносимо высокой.
Поскольку я планирую использовать это для игры «вживую», то есть у себя дома, меня не беспокоит игра с постоянно растущей задержкой между нажатиями клавиш и звуковой обратной связью, которую я слышу.
Я пытался полностью сократить базу кода, воспроизводящую проблему, и больше не могу ее сокращать.
#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(¶ms, 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 кГц, я запускаю программу с ./synth 48000
.
alsa
можно использовать в качестве звукового сервера, хотя я предпочитаю использовать jackd
, когда это возможно, по неясным причинам, включая взаимодействия pulseaudio
и alsa
.
Если вы вообще запустите программу, вы должны услышать, надеюсь, не слишком раздражающую пилообразную волну, играющую и не играющую через равные промежутки времени, с выводом консоли, когда игра должна начинаться и останавливаться. Когда noteOn
установлено на true
, синтезатор начинает производить пилообразную волну на любой частоте и останавливается, когда noteOn
установлено на false.
Надеюсь, вы увидите, что сначала noteOn
true
и false
почти идеально соответствуют воспроизведению и остановке звука, но мало-помалу источник звука начинает отставать, пока это не становится очень заметным примерно от 1 минуты до 1 минуты 30 секунд. моя машина.
Я на 99% уверен, что это не имеет никакого отношения к моей программе по следующим причинам.
«Аудио» проходит этот путь через программу.
Клавиша нажата.
Часы тикают на частоте 48 кГц в
sample_processing_thread
, вызываютSynthesizer::get_sample
и передают вывод вstd::queue
, который позже используется в качестве буфера сэмплов.Всякий раз, когда потоку
RtAudio
нужны сэмплы, он берет их из буфера сэмплов и перемещается дальше.
Единственное, что может быть источником постоянно увеличивающейся задержки, — это тиканье часов, но они тикают с той же скоростью, что и поток, потребляющий сэмплы, так что это не может быть так. Если бы часы тикали медленнее, RtAudio
жаловался бы на опустошение потока и были бы заметные искажения звука, чего не происходит.
Тем не менее, часы могли бы щелкать быстрее, но я не думаю, что это так, поскольку я тестировал часы сам по себе во многих случаях, и хотя они показывают небольшое дрожание, порядка наносекунд, это быть ожидаемым. Для самих часов нет кумулятивной задержки.
Таким образом, единственным возможным источником увеличения задержки будут внутренние функции RtAudio
или самого звукового сервера. Я немного погуглил и не нашел ничего полезного.
Я пытался решить эту проблему уже неделю или две, и я проверил все, что могло пойти не так на моей стороне, и это работает, как и ожидалось, поэтому я действительно не знаю, что может происходить.
Что я пробовал
- Проверка часов на наличие какой-либо кумулятивной задержки: Суммарной задержки не обнаружено
- Определение времени задержки между нажатиями клавиш и воспроизводимым первым образцом звука, чтобы увидеть, увеличивается ли эта задержка со временем: Задержка не увеличивается со временем
- Определение времени задержки между потоком, запрашивающим образцы, и отправкой образцов в поток (начало и конец
stream_callback
): задержка не увеличивалась со временем