Rubyで実装したDAWとシンセサイザーの技術仕様

よくもまぁこんな面倒臭い大変なものを作ったなと自分で思う。
RubyのEnumerator::Yielderとlambdaを多用したリアルタイム処理とも親和性が高い作りをしてる。
だいぶ前に書いた文章が発掘されたので手直しして公開。
供養、ナムナム。

デモ

百聞は一見に如かず、まずはデモ音源を。
ドラム以外の音は全てRubyで生成したもの。
ドラムだけはLogic Pro Xで打ち込んでmp3出力して、Rubyに読み込ませてる。

パッケージ

すべてRubyGemsで公開してる。

audio_stream

Rubyで音楽制作できるようにした。
DAWとFXみたいなもの。
依存:vdsp coreaudio

GitHub - yoshida-eth0/ruby-audio_stream: AudioStream is a Digital Audio Workstation for CLI
audio_stream | RubyGems.org | your community gem host

インストール

coreaudio gemsのコンパイルで、Xcode 12系だと以下のエラーが出る。

compiling coreaudio.m
coreaudio.m:785:12: error: implicit declaration of function 'rb_thread_call_without_gvl' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
    return rb_thread_call_without_gvl(ca_buffer_wait, ptr,
           ^
1 error generated.

なので以下のようにしてまずはcoreaudio gemsをインストールしてからsynthesizer gemsをインストール。

gem install coreaudio -- --with-cflags="-Wno-error=implicit-function-declaration"
gem install synthesizer

audio_streamの機能と仕様

Audio Busの接続

オーディオファイルやらオーディオデバイスやらのAudioInputをObservableとして、Audio Busなど後続のObserverにBufferを通知する。
FXの接続、ステレオ/モノラル変換、Send ToでのBusの合流などもこのObserverパターンで実装してる。

ruby-audio_stream/audio_observable.rb at master · yoshida-eth0/ruby-audio_stream · GitHub

トラック間の同期

AudioInputはEnumerableを実装していて、Enumerator::Yielderがバッファを生成し続ける。
全てのトラックが同じタイミングで同じ回数Enumerator#nextを呼ぶためにSizedQueueを使って1回の呼び出しごとにThreadを止める。
そして全トラックのバッファが揃ったタイミングで、止めていたThreadを起動させて次のバッファを生成させる。

ruby-audio_stream/conductor.rb at master · yoshida-eth0/ruby-audio_stream · GitHub

FX

FXを自力実装することで今までなんとなく雰囲気で掛けてたFXの意味を正しく理解することが出来たのは良かった。
当初VST2相当の仕組みで作っていたけど、途中でヴォコーダーを作る時にVST2相当だとLRでキャリアとモジュレーションを分けるというあの懐かしい様式になってしまうのが嫌でサイドチェインに対応してVST3相当の汎用性を得たので満足感高い。
Rubyは数値解析系が充実してなくてリバーブの実装で一部SciPyの再開発をすることになったのは想定外で割と苦戦した。

BiquadFilterの美しさ

BiquadFilterには数学的美しさが詰まってると思う。

フィルター系のグラフ。
上から順に、LowPassFilter、HighPassFilter、LowShelfFilter、HighShelfFilter、BandPassFilter、PeakingFilter。
f:id:eth0jp:20211124202610j:plain:h320

上記フィルターを複数使ったイコライザー系のグラフ。
上から順に、2BandEqualizer、3BandEqualizer、GraphicEqualizer。
f:id:eth0jp:20211124202640j:plain:h320

オクターブ差の16帯域のBandPassFilterのグラフ。
周波数軸を対数表示にすることで等間隔のキレイなグラフになる。
440Hzを基準とした平均律の計算しやすさは数学的にも浮動小数点演算的にも美しい。
f:id:eth0jp:20211124202702j:plain:h320

synthesizerの機能と仕様

Oscillator、Amplifier、Filter、とシンセサイザーをいじったことのある人なら音を想像できるくらい直感的なインターフェイスになっていると思う。
VSTi開発の知識がある人であれば、自分でシンセサイザーのロジック部分だけを実装して使える。
シンセサイザーといえばC++が基本みたいなところあるけど、カジュアルにRubyで書けるというのは結構な利点だと思ってる。
f:id:eth0jp:20211124202727j:plain:h320

波形バッファの生成

バッファ分の波形を生成するlambdaをEnumerator::Yielderで実行してる。
NoteOnのタイミングでOscillator、Amplifier、Filterを初期化して、Enumerator#nextされる度に続きの波形を生成し続ける。

Oscillatorで生成する波形の角度はクロージャのような仕組みで保持してる。
なのでバッファが分割されても途切れることなく続きの波形を延々生成し続けることが出来る。
また、NoteOnから時間の推移とともに変化していくパラメータも同じ仕組みで状態保持しているのでADSRやLFOなどのエンベロープが掛けられるようになっている。

ruby-synthesizer/note_perform.rb at master · yoshida-eth0/ruby-synthesizer · GitHub
ruby-synthesizer/low.rb at master · yoshida-eth0/ruby-synthesizer · GitHub

NoteOn/NoteOffでやっていること

以下の設定や状態を計算して最終的な値を算出する。
シンセサイザー固有の設定や状態(PitchBend、Glide)
・Note固有の状態(Volume、Panなど)
・Oscillator固有の状態(Volume、Panなど)

NoteOnのタイミングでやることは以下。
・Oscillator、Amplifier、Filterを初期化。

NoteOnからNoteOffまでの間に延々やることは以下。
・Oscillator、Amplifierの各種パラメータのエンベロープを更新。
・VolumeにNoteOnのVelocityを反映。
・Tuneのエンベロープを更新してPitchBendを反映し、生成する波形の周波数を算出。
・上記の周波数とパラメータからOscillatorが波形バッファを生成。
・Filterのエンベロープを更新。
・Oscillatorが生成した波形バッファにFilterを掛ける。

NoteOff以降にやることは以下。
・ADSRの残響の計算。
・ADSRでRelease途中でVolumeが0を超えていればNoteOn中と同等の処理をする。
・ADSRでReleaseし終えてVolumeが0になったらNoteを削除。

この辺はもう普通にシンセサイザー
理論上は普通のGUIや物理ボタンを持つシンセサイザーと同等のことが出来る。

ruby-synthesizer/low.rb at master · yoshida-eth0/ruby-synthesizer · GitHub

エンベロープ

ADSR、LFO、Glide、Curve、を実装した。
これらを各種パラメータに掛けることができる。

エンベロープを掛けられるOscillatorのパラメータはVolume、Pan、Tune(semi / cent)、Sym、Sync、Unison(num / detune / stereo)など。
エンベロープを掛けられるAmplifierのパラメータはVolume、Pan、Tune(semi / cent)、Unison(num / detune / stereo)など。

ADSRのグラフ。
f:id:eth0jp:20211124224952j:plain:h320

ruby-synthesizer/modulation_value.rb at master · yoshida-eth0/ruby-synthesizer · GitHub
ruby-synthesizer/adsr.rb at master · yoshida-eth0/ruby-synthesizer · GitHub

Oscillator

波形生成ロジックは独立して外側に置けるので、sine、triangle、saw、squareなどの基本的な波形以外にも、フォルマントヴォコーダーをOscillatorとして実装するなんてことも出来るようにしてある。
実際にフォルマントヴォコーダーは実装して使えるようにした。
デモのCinemaではフォルマントヴォコーダーを使ってる。

ruby-synthesizer/base.rb at master · yoshida-eth0/ruby-synthesizer · GitHub
ruby-synthesizer/shape.rb at master · yoshida-eth0/ruby-synthesizer · GitHub

Amplifier

AmplifierにはADSRやLFOなどのエンベロープが指定できる。
ADSRの減衰やLFOの角度の状態は、Oscillatorの波形生成と同様にクロージャのような仕組みで保持してる。

Filter

FXに準ずるフィルターの実装。
パラレル/シリアルでのフィルター処理にも対応した。

ステップエディタ

ST/GT方式のステップエディタを実装した。
分解能とBPMを指定することで、Tickでの指定も出来るようにもした。(sync機能)
ダブステップとかでLFOBPMに同期させる時とかに便利。

ruby-synthesizer/st_gt.rb at master · yoshida-eth0/ruby-synthesizer · GitHub

あとがき

Enumerator::Yielder大好き芸人としてはかなり満足の出来。
coreaudioがコンパイル出来ないので今手元で動かせていないのが悔やまれる。
Ruby2.7系の時代に作ったものなので、(多分動くと思うけど)Ruby3.0系で動くか不明。
coreaudioがコンパイルできないことにはどうしようもないのでどうにかしたいところ。