Базовый программный синтезатор растет со временем

Я нахожусь в процессе завершения программного синтезатора, управляемого 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(&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 кГц, я запускаю программу с ./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): Задержка не росла со временем

2

Решение

Я думаю, что ваш поток get_samples генерирует аудио быстрее или медленнее, чем streamCallback потребляет их. Использование часов для управления потоком ненадежно.

Простой способ исправить, удалить этот поток и очередь sample_buffer и генерировать образцы непосредственно в функции streamCallback.

Если вы хотите использовать многопоточность для своего приложения, это требует правильной синхронизации между производителем и потребителем. Гораздо сложнее. Но вкратце, шаги ниже.

  1. Замените свою очередь достаточно маленьким кольцевым буфером фиксированной длины. Технически, std :: queue тоже будет работать, только медленнее, потому что основан на указателе, и вам нужно вручную ограничить max.size.

  2. В потоке производителя реализован бесконечный цикл, который проверяет, есть ли в буфере пустое пространство, если есть место, генерирующее больше звука, если нет, дождитесь, пока потребитель получит данные из буфера.

  3. В обратном вызове приемника streamCallback скопируйте данные из кольцевого буфера в output_buf. Если данных недостаточно, разбудите поток производителя и подождите, пока он произведет данные.

К сожалению, эффективная реализация этого довольно сложна. Вам необходима синхронизация для защиты общих данных, но вам не нужно слишком много синхронизации, иначе производитель и потребитель будут сериализованы и будут использовать только один аппаратный поток. Одним из подходов является одиночный std :: mutex для защиты буфера при перемещении указателей / размера / ofset (но разблокировки при чтении / записи данных), и два std :: condition_variable, один для производителя, который спит, когда в свободном месте нет свободного места. буфер, еще один для потребителя, чтобы спать, когда нет данных в буфере.

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]