私は、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 kHz なので、プログラムを で実行し./synth 48000
ます。
alsa
サウンドサーバーとして使用できますが、相互作用jackd
などのあいまいな理由から、可能な場合は使用することを好みます.pulseaudio
alsa
プログラムを実行することができれば、ノコギリ波が一定間隔で再生されたり、再生されなかったりするのが聞こえ、再生の開始時と停止時にコンソール出力がオンになるはずです。noteOn
が に設定されている場合true
、シンセサイザは任意の周波数でノコギリ波の生成を開始し、noteOn
が false に設定されている場合は停止します。
うまくいけば、最初はオーディオの再生と停止にほぼ完全noteOn
true
にfalse
対応していることがわかると思いますが、私のマシンでは 1 分~1 分 30 秒あたりで非常に目立つようになるまで、少しずつオーディオ ソースが遅れ始めます。
次の理由から、私のプログラムとは何の関係もないと 99% 確信しています。
「オーディオ」は、プログラムを通じてこのパスをたどります。
キーが押されます。
クロックは と で 48 kHz でカチカチ音をたて、後でサンプル バッファとして使用される を
sample_processing_thread
呼び出しSynthesizer::get_sample
て出力を渡します。std::queue
RtAudio
ストリームがサンプルを必要とするときはいつでも、サンプル バッファからサンプルを取得して移動します。
ここで増加し続けるレイテンシーの原因となる可能性のある唯一のものは、クロックのカチカチ音ですが、ストリームがサンプルを消費するのと同じレートでカチカチ音をたてるので、それはあり得ません。クロックの動作が遅くなると、RtAudio
ストリーム アンダーランが発生し、顕著なオーディオの破損が発生する可能性がありますが、これは発生しません。
ただし、クロックのクリックは速くなる可能性がありますが、そうではないと思います。クロック自体を何度もテストしたため、ナノ秒のオーダーで少しジッターが見られますが、これは予想された。クロック自体に累積的な遅延はありません。
したがって、遅延が増大する唯一の原因はRtAudio
、サウンド サーバー自体の内部機能です。私は少しグーグルで検索しましたが、何も役に立ちませんでした。
私はこれを 1 週間か 2 週間解決しようとしてきましたが、私の側で問題が発生する可能性があるすべてをテストしました。
私が試したこと
- クロックに何らかの累積レイテンシがあるかどうかの確認:累積レイテンシは認識されていません
- キーを押してからオーディオの最初のサンプルが生成されるまでの遅延を計り、この遅延が時間とともに増加するかどうかを確認します。遅延は時間とともに増加しませんでした。
- ストリームがサンプルを要求してからサンプルがストリームに送信されるまでの遅延のタイミング ( の開始と終了
stream_callback
):遅延は時間の経過とともに増加しませんでした