見出し画像

夏休みはVST! MATLABで楽々(?)VSTプラグイン開発


電子書籍発売しました!(2023/01/22)
動画・音声リンク付き紹介記事はこちら。

Amazon Kindle 電子書籍「MATLAB で簡単オーディオ プラグイン開発」

スクリプトはどなたでも無料でダウンロードできます。


入門編の記事を追加しました(2021/10/9)。

MATLABでVSTプラグイン開発 [初級編]~数分?で作れるVST~LMSで無相関プラグイン


R2023a で、AUv3 (macOS) の生成に対応したようです。
(現在 Mac 版を使用していないため未確認)

2023/03/16


前書き

VSTプラグインの開発方法はMATLABの公式ドキュメントに色々と詳しく載ってはいるのですが、例によって分散してかなり分量があり取っつきにくいかと思います。

特に日本語の資料は少ないようなので解説をしてみます。

本来VSTプラグイン開発はオブジェクト指向を理解していること前提のようですが、私自身、オブジェクト指向は20年以上前にC++を使ってみて、

「//でコメント書けるのちょー便利!」

「特に打合せもせず、複数人で書いたソースまとめると動いちゃうのすげー!!」

くらいの感じで止まっているので、オブジェクト指向とか知らなくても、ここに書いてあることをだいたい理解すれば、自由にVSTプラグインを作って活用できるようになることを目指します!


余裕のある方はあらかじめ、"audio plugin" で検索して公式ドキュメントを眺めてみてください。全て理解しなくても、「あそこになんか書かれてたような・・」と思い出せると理解が早まると思います。

ここでは信号処理自体には触れませんが、VSTプラグイン開発自体だけではなく、それをどう活用するかについても解説しているため、文字数はかなり多めです。

基本的に、Windows版での説明になります。

VSTプラグインとは

VST(Virtual Studio Technology)とは、スタインバーグ社(現ヤマハ傘下)がDAW用に開発したプラグイン規格です。
オーディオ用は VSTインストゥルメント (VSTi) と VSTエフェクトがありますが、MATLABで開発できるのはエフェクト(狭義のVST)の方です。

VSTって何に使えるの?

以下の3種類の使い方があります。

1.DAWに組み込む
-複数トラックへの適用が可能
-(DAWによっては)パラメータ操作のオートメーション(リアルタイム記録・編集等)が可能

画像1

この辺りは、DAWをお使いの方はよくご存じかと思いますので説明は省きます。(GUIの "Rotary knob" 等を表示させるには、REAPER等、カスタムGUI対応のDAWが必要です)

2.foobar2000、MusicBee等の音楽プレーヤーに組み込む
後述しますが、一部の音楽プレイヤーなどは拡張機能としてVSTプラグインの適用が可能です。

3.単体動作(exe)、VSTホストアプリの利用
外部入力に対して適用することができます。
後述するバーチャルI/O系を併用すれば、VST非対応のプレイヤーやWebブラウザー出力に(YouTube等にも)適用が可能です。

ただ私の環境ではexe版は、原因不明ですがI/Fによってプチプチノイズが発生したりします・・。
VSTホストを使った方が安定度が高い印象です。

----> 2021/1/19追記
今はノイズも出なくなりましたので追記します。

・PCの標準出力
 Realtek製である場合が多いと思いますが、サウンドコントロールパネルのプロパティ->詳細で、サンプリングレートを48kHz(Virtual In・Virtual Outも)にすることで解決しました。
 もしかするとRealtekの内部処理が48kHz固定(?)で、それ以外だとSRC(サンプリングレートコンバーター)が入って処理タイミングが合わなくなるのかもしれません。
 SRCは、原理的に44.1kHz <-> 48kHzの高精度変換処理量はかなり多く、SRCによっては音質劣化の原因ともなります。それぞれの環境に合わせて確認してみてください。

・USBオーディオI/F(Behringer UCA202/222)
 USBハブを使った場合に出やすいようです。PC直差しにしたらノイズは出なくなりました。

それと、PCでオーディオ関連の開発を行う場合は、サウンドコントロールパネルのプロパティに「立体音響」等のタブがあれば全て無効にしておきましょう。OFFにしないと、想定外のエフェクトが掛かってしまいます。
また、PCによってはそれと別の音響効果がデフォルトで入っている場合があるので、そちらもOFFにしておきましょう。例えばDELLであれば、全てのPCにWaves MaxxAudioというのが入っていますので、こちらの設定も確認しておきましょう。
<---- 2021/1/19追記

VSTプラグイン化のメリット

仕様にしたがってGUIパラメータの宣言さえすれば、そこそこ手間のかかるGUI自体を作る必要がありません
基本的にメインの信号処理部分だけを書けば、GUIでの操作が可能となります。
生成されたVSTはネイティブ動作なので処理が高速です

VST開発をMATLABで行うメリットは
-VST SDKを使わなくて良い
-信号処理部にMATLABの関数が使える
-MATLAB上でデバッグができる(通常のグラフィカルなデバッグも可能)
といったところでしょうか。

但し、VST動作では制限事項もあります。

-グラフ出力不可
-タイム関数やWebアクセス関数等使用不可(これらを使った認証等が行えません)
-ファイル読み込み不可
-GUIのカスタマイズ制限(背景画像等、一部画像貼り付けは可能)
-MIDIによるパラメータ操作不可
 audioTestBench 上では可能ですがVSTでは未対応です

売り物のVSTプラグインのような見た目もカッコイイのを作るには、通常通り VST SDKとC++を使う必要があります。

また、スタインバーグ社自体はVST2と互換性のないVST3に移行し、VST2の正式サポートをすでにやめてしまっていますが、MATLABがサポートしているのは今も広く使われているVST2のみです。(R2022aで、VST3も生成可能となりました)


32bit版や、単体exe(R2020a以降)、MacであればAU v2も生成可能です。
MATLAB Coder があれば、JUCEプロジェクトも生成できます。
プラットフォーム依存ですので、Linux版MATLABでは生成できません。

プラグイン形式としては継承元によって以下の4種類があるようですが、今のところ "BasicPlugin" だけで事足りていて他は使ったことがないので、今回は "BasicPlugin" のみの説明とさせて頂きます。

BasicPlugin < audioPlugin
BasicSourcePlugin < audioPluginSource
SystemObjectPlugin < audioPlugin & matlab.System
SystemObjectSourcePlugin < audioPluginSource & matlab.System

開発環境

CPU:なるべく速いヤツ
("Parallel Computing Toolbox" がなくても、ある程度自動的にマルチコア上で演算されます)
メモリ:なるべくいっぱい
モニター:なるべく高解像度で大きいヤツ

下のような私の環境でもほとんど問題はありませんが。
VAIO S11
 Core i5-8250U(1.60GHz)、RAM 8GB、SSD 256GB(NVMe対応)
モニター DELL 2560×1440 27型
外部HDD

MATLAB 本体以外に必要な Toolbox 

DSP System Toolbox
Signal Processing Toolbox
Audio Toolbox

(MATLAB Coder) ← JUCEプロジェクト生成には必要

これらがないと作れません。

最初の2つは "Audio Toolbox" の動作に必要なので、"Audio Toolbox" が動く状態ならVSTプラグインが作れると言うことになります。

MATLABでは、MATLABアプリは "MATLAB Compiler" 、コード生成及びネイティブアプリは "MATLAB Coder" がないと作れませんが、VSTは "Audio Toolbox" があれば作れます。ですので、"MATLAB Compiler/Coder" 製品自体がない MATLAB HOME ライセンスでも作ることができます

あともう一つ、Cコンパイラが必要です。PCに入っていない場合はインストールします。対応しているコンパイラについてはこちらをご参照ください。

私は Visual C++ 2017 を使っています。

Cコンパイラインストール後、MATLABコマンドラインで ​ 

> mex -setup

と打って、使用するコンパイラを指定します。

開発プロセス

VSTプラグイン開発のプロセスとしては以下のようになります。

(1)通常通りMATLABで信号処理部を開発
(2)プラグイン仕様にしたがってコード作成
(3)audioTestBench でテスト実行 & VST生成
 "run as MATLAB" でデバッグ
 "run as VST"(コード生成)でデバッグ
 VST生成

"run as MATLAB" と "run as VST" では一部動作が異なりますので完全にデバッグできるわけではありません。メインの信号処理部は、通常のMATLAB開発プロセスでしっかり動作を確認しておきましょう。
また、VST時固有のエラーがあると、 "run as VST" で長時間返ってこない場合もあるので、個人的にはコマンドラインでの

validateAudioPlugin / generateAudioPlugin

の使用をお勧めします。

validateAudioPlugin も規模が大きくなると(?)かなり長時間戻ってこないことがあるので、推奨はしませんがほぼデバッグができた後はこれはスキップして、直接 generateAudioPlugin することも可能です。


VSTを作ってみよう!

公式サンプルもありますので、ご自分で色々試してみたい方はどうぞ。抽象クラスを使ったより高度な例(Bandpass/Highpass/Lowpass IIR Filter)等も載っています。

ここでは、公式ドキュメント/サンプルだけでは理解しにくいところ、引っかかりやすいと思われるところに重点を置きながら、以下の簡単なVSTを例に解説をします。サンプルについては全て実際のコードも添付しますので、ぜひ実際に動かしてみてください。

1.チューナブルパラメータを利用した3バンドイコライザー(デバッグ用特性表示付き)
2.係数設計部付きフィルター(デバッグ用特性表示付き)
3.ステレオローテーション


1.チューナブルパラメータを利用した3バンドイコライザー(デバッグ用特性表示付き)

画像2

VSTtest_EQU1.m というファイル名で、通常のMATLAB関数を作成するのと同じ手順でコードを作成します。

以下、全コードについて簡単に説明をします。
(1)audioPlugin クラスを継承してファイル名と同じ VSTクラスを宣言します

classdef VSTtest_EQU1 < audioPlugin  % Inherit from audioPlugin

(2)GUIで変更したいパラメータを初期値と共に宣言します

  properties
       % GUI parameters
       
       % EUQ1 : 3Band-EQU ON/OFF
       EQU1 = true;
       % EQU1 parameters
       EQU1_F1 = 400;	EQU1_F2 = 1000;	EQU1_F3 = 5000; 
       EQU1_G1 = 0;	EQU1_G2 = 0;	EQU1_G3 = 0;
       EQU1_Q1 = 1.6;	EQU1_Q2 = 1.6;	EQU1_Q3 = 1.6;
       
   end

(3)audioPluginInterface を使ってVSTの概要を記述します

    properties (Constant)
       % set GUI properties and layout
       PluginInterface = audioPluginInterface( ...
           'PluginName','EUQ1',...
           'VendorName', '', ...
           'VendorVersion', '0.0.1', ...
           'UniqueId', 'hiro',...

audioPluginInterface のパラメータ詳細はここを参照してください。

'VendorName' を省略した場合は "MathWorks" になります。
'UniqueId' はとりあえず使う分には適当な4文字でOKです。
この行は省略可能です。

(4)続いて audioPluginParameter を使って各パラメータの属性とレイアウトを指定します

           audioPluginParameter('EQU1', 'Mapping', {'enum','OFF','ON'}, 'Layout', [1 1]), ...
           audioPluginParameter('EQU1_F1','DisplayName','F1', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [1 2]), ...
           audioPluginParameter('EQU1_G1','DisplayName','G1', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [3 2]), ...
           audioPluginParameter('EQU1_Q1','DisplayName','Q1', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [5 2]), ...
           audioPluginParameter('EQU1_F2','DisplayName','F2', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [1 3]), ...
           audioPluginParameter('EQU1_G2','DisplayName','G2', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [3 3]), ...
           audioPluginParameter('EQU1_Q2','DisplayName','Q2', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [5 3]), ...
           audioPluginParameter('EQU1_F3','DisplayName','F3', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [1 4]), ...
           audioPluginParameter('EQU1_G3','DisplayName','G3', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [3 4]), ...
           audioPluginParameter('EQU1_Q3','DisplayName','Q3', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [5 4]), ...
           audioPluginGridLayout('RowHeight', repmat([70 15],1,3), ...
           'ColumnWidth', repmat([80 80],1,2), ...
           'Padding', [20 20 20 20]) ...  % [left, bottom, right, top]
           );
   end

パラメータの属性に関しても公式ドキュメントをご参照ください。

レイアウトの指定方法はここに詳しく書いてありますが、最初は分かりにくいと思いますので解説します。

audioPluginGridLayout('RowHeight', repmat([70 15],1,3), ...
           'ColumnWidth', repmat([80 80],1,2), ...
           'Padding', [20 20 20 20]) ...  % [left, bottom, right, top]

と書くと、高さ:70 15 70 15 70 15 pixel、幅:80 80 80 80 pixel のグリッドが作成され、その間にパディング(デフォルト10)が入り、その外側に20のスペースが取られます。

画像5

上の図で、オレンジの大きい四角が 80x70pixel、小さい方が80x15pixelの枠になっています。

また、パラメータ名等を表示すると、それが一行分を占有します。

例えば 'EQU1_F1' は[1行目, 2列目]に配置していますが、 'DisplayName','F1' としているので、ロータリーノブ部分が1行目、'F1' が2行目に配置されます。したがって、 次の 'EQU1_G1' を置くべきは[3行目, 2列目]になります。
2行目に置くことはできません。

'DisplayNameLocation','above' とした場合は、1行目に置くことはできません。同様に、'DisplayNameLocation','left' とした場合は、1列目に置くことはできません。パラメータ名を消したい場合は 'DisplayNameLocation','none' とします。

タイトル部を除く内部サイズは、
横:80*4+10*3+20*2 = 390
縦:(70+15)*3+10*5+20*2 = 345
となります。背景画像を用いる場合はこのサイズにします。

(5)ユーザーが直接操作しないプライベートプロパティを宣言します
ここではフィルター処理を行うためのフィルターオブジェクトを宣言します。

   properties(Access = private)
       % EUQ1 : 3Band-EQU
       objEQU1;
   end

(6)コンストラクタで実際のフィルターオブジェクトを構築します

       % Constructor   
       function plugin = VSTtest_EQU1
           plugin@audioPlugin;
           
           plugin.objEQU1 = multibandParametricEQ('NumEQBands',3,'EQOrder',4, ...
               'HasLowShelfFilter',0,'HasHighShelfFilter',0,'HasLowpassFilter',0,'HasHighpassFilter',0, ...
               'Frequencies',[400, 1000, 5000],'PeakGains',ones(1,3)*0,'QualityFactors',ones(1,3)*1.6,'SampleRate',getSampleRate(plugin));
           
       end

(7)GUI操作をした場合の挙動をset/getに記述します
これ以降は、ユーザー操作によってアップデートされる内容になります。

%% EUQ1 : 3Band-EQU
       %-------------------------------------------
       % EUQ1 : 3Band-EQU
       function set.EQU1_F1(plugin,val)
           plugin.objEQU1.Frequencies(1) = val;  %#ok
       end
       function val = get.EQU1_F1(plugin)
           val = plugin.objEQU1.Frequencies(1);
       end
       function set.EQU1_G1(plugin,val)
           plugin.objEQU1.PeakGains(1) = val;  %#ok
       end
       function val = get.EQU1_G1(plugin)
           val = plugin.objEQU1.PeakGains(1);
       end
       function set.EQU1_Q1(plugin,val)
           plugin.objEQU1.QualityFactors(1) = val;  %#ok
       end
       function val = get.EQU1_Q1(plugin)
           val = plugin.objEQU1.QualityFactors(1);
       end
       
       function set.EQU1_F2(plugin,val)
           plugin.objEQU1.Frequencies(2) = val;  %#ok
       end
       function val = get.EQU1_F2(plugin)
           val = plugin.objEQU1.Frequencies(2);
       end
       function set.EQU1_G2(plugin,val)
           plugin.objEQU1.PeakGains(2) = val;  %#ok
       end
       function val = get.EQU1_G2(plugin)
           val = plugin.objEQU1.PeakGains(2);
       end
       function set.EQU1_Q2(plugin,val)
           plugin.objEQU1.QualityFactors(2) = val;  %#ok
       end
       function val = get.EQU1_Q2(plugin)
           val = plugin.objEQU1.QualityFactors(2);
       end
       
       function set.EQU1_F3(plugin,val)
           plugin.objEQU1.Frequencies(3) = val;  %#ok
       end
       function val = get.EQU1_F3(plugin)
           val = plugin.objEQU1.Frequencies(3);
       end
       function set.EQU1_G3(plugin,val)
           plugin.objEQU1.PeakGains(3) = val;  %#ok
       end
       function val = get.EQU1_G3(plugin)
           val = plugin.objEQU1.PeakGains(3);
       end
       function set.EQU1_Q3(plugin,val)
           plugin.objEQU1.QualityFactors(3) = val;  %#ok
       end
       function val = get.EQU1_Q3(plugin)
           val = plugin.objEQU1.QualityFactors(3);
       end

通常のMATLABのGUIでは
 set:GUI表示へのset
 get:GUIの値のget
なので、MATLABでGUIを作った経験がある方はかえって戸惑うかもしれませんが、
 set:ユーザーインタラクション → プロパティの値をset
 get:プロパティの値をget
です。

GUIパラメータ変更時にインタラクティブに何かしたい場合は、ここに書きます。メインルーチンで値を参照するだけであれば、これらは書く必要はありません。

基本的に、GUIパラメータの値をプログラム内から変えることはできません。ユーザーのGUI操作(またはDAW等からの操作)によってのみ変更できます。値自体を変えることは可能ですが、表示には反映されません。

"Run As MATLAB" では表示にも反映されますが、VSTにした場合には反映されませんので注意が必要です。他にも audiotesubench 上と実際のVSTでは見た目や動作が異なる場合もあるので、途中でVSTを生成して確認すると良いでしょう(単体exe版が、そのまま立ち上がるので便利)。


ここで、multibandParametricEQ について簡単に説明します。
最大10バンド(LPF/HPF/shelf使用時は最大12)のパラメトリックイコライザーを一度に生成できる便利な関数です。

詳しくはここを参照してください。
"Properties" で "Tunable: Yes" となっているパラメータは、オブジェクトを再定義することなく変更が可能です。

つまり、先ほどコンストラクタで構築したフィルターのチューナブルパラメータである各バンドのF/G/Qを、ここで代入してあげればフィルター特性が変わってくれます。

こうすることで、メインルーチン内での更新不要でユーザー操作があったときだけフィルター特性を変えられるので、実行速度を落とすことなく特性変更が可能となります。

そのまま書くと警告が出ますが問題ありません(MathWorks開発チームに確認済み)。気になる場合は行の最後に "%#ok" と書けば出なくなります。


(8)デバッグ用周波数特性ビジュアライズオブジェクトを定義します
multibandParametricEQ は visualize() で簡単に周波数特性表示ができますので、function visualize() 内にこう書いておけば、audiotesubench 上で表示できるようになります。

       function visualize(plugin,NFFT)
           if nargin < 2
               NFFT = 2048;
           end
           visualize(plugin.objEQU1, NFFT);
       end

(9)メイン処理部を書きます
入力データはオーディオI/Fの設定で決まるフレームサイズ×チャネル数で入ってきますが、MATLABの関数へはそのまま渡せばOKです。内部ディレイバッファの内容も保持されますのでケアする必要はありません。「3.ステレオローテーション」に、サンプル単位で処理する例を載せます。

       %------ main process -------
       function out = process(plugin,in)
           if plugin.EQU1
               out = step(plugin.objEQU1,in);
           else
               out = in;
           end
       end
       %---------------------------     

(10)リセット関数を書きます

       function reset(plugin)
           plugin.objEQU1.SampleRate = getSampleRate(plugin);
           reset(plugin.objEQU1);
       end

サンプリング周波数が変わったときに呼ばれます。


1-2.デバッグ
MATLAB のコマンドラインで audioTestBench を起動します。 

> audioTestBench(VSTtest_EQU1)

audioTestBenchの詳細はここを参照してください。

画像11

まずは "Input" を設定します。
ファイルから読みたい場合は "Audio File Reader"、オーディオI/F等にしたい場合は "Audio Device Reader" にしてから、横の歯車アイコンでさらに詳細を決定します。

同様に "Output" を設定します。

"Run"で音が出ます。

”View Source Code” でエディターにソースコードが表示され、ブレイクポイントの設定等もできます。

"Visualize Plugin" アイコンをクリックすると、周波数特性が表示されます。

同様に、"Time Scope" と "Spectrum Analyzer" も表示できます。
通常のMATLABと同じですので、凡例をクリックして各データ表示のON/OFF、右クリックでグラフプロパティ等も設定できます。
但し、マルチチャネルの場合は1ch目しか表示されないようです(2chステレオの場合はLchのみ)。


R2022b から、"Time Scope" と "Spectrum Analyzer" もステレオ表示されるようになりました。

(2022/11/24 加筆)

画像6

GENERATION -> Generate Script でスクリプトに出力し、それを修正してmファイルを実行すれば、複数ch表示も可能です。スペアナの場合、

'ChannelNames',{'Input channel 1','Output channel 1'});

'ChannelNames',{'Input channel L','Input channel R','Output channel L','Output channel R'});
   % Visualize input and output data in scope
   specScope([in(:,1),out(:,1)]);
   
   % Visualize input and output data in scope
   specScope([in(:,:),out(:,:)]);

にそれぞれ変更して実行すると、以下のように表示されます。
(右クリック -> スタイル でラインプロパティも変えています)

画像11

スクリプト実行させた場合の停止は、"Input" で指定した回数ソースの再生が終わるのを待つか、コマンドウィンドウで Ctrl+c です。

MIDIコントロール
audioTestBench 上ではMIDIコントロールチェンジも受けられるので、PCにrtpMIDI、スマホにMIDIコントローラーアプリ(YAMAHA Faders 等)を入れれば、LAN内でスマホからリアルタイムに複数のパラメータを変更したりもできます。(もちろん通常のMATLAB/Simulinkでもできます)

rtpMIDI の設定方法
藤本さんの記事に詳しく書いてあるのでそちらを参照してください。

YAMAHA Faders設定方法
右上の歯車アイコンをタッチして設定画面で "wireless LAN"を選択
"Wireless LAN MIDI Port" をタッチして、自分のPC名が出てくればそれを選択して終了です。

画像12

audioTestBench の設定
MIDIコネクタアイコンをクリック
同期したいパラメータをセレクト

画像13

下の "Unset" 部分をクリック
YAMAHA Faders の割り当てたいフェーダーを操作

画像14

すると "Unset" の部分に検出されたcontrolナンバーが入るはずです。
もう一つのフェーダーに別のパラメータを割り当てたい場合はこれを繰り返します。

これで "OK" で閉じれば、スマホから操作可能となります。

実際に iPad からフィルターの周波数を操作するデモ動画をご覧ください。


また、"Miditure" というアプリを使うと、X/Y/Z(Zは押し込み)の3軸で別々のパラメータを同時にコントロールすることもできます。

画像15
画像16

‎‎

Miditure でフィルターの周波数/ゲイン/Q を操作するデモ動画をご覧ください。


"Miditure" の設定歯車アイコンをタッチ -> XYZ axis assignment で、
X/Y/Z それぞれ、ONE/TWO/THREE の内 各一つだけをON にし、別々の CONTROL CHANGE NUMBER を割り当てます。

audioTestBench のMIDI設定に割り当てる際は面倒ですが、複数のコントロールチェンジ信号が出て行かないよう、X/ONE、Y/TWO、Z/THREE の内 一つだけをON/他の二つはOFF にして、3回に分けて行い、設定終了後、全てをONにします。

画像17


KORG nanoKONTROL2 などの有線コントローラーであれば rtpMIDI を用いなくても、同様の設定方法で使用できます。


1-3.プラグイン生成
"GENERATION" -> "Generate VST Plugin" でVST2プラグインが生成されます。

またはコマンドウィンドウで
generateAudioPlugin VSTtest_EQU1

32bit版が欲しい場合は
generateAudioPlugin -win32 VSTtest_EQU1

単体exeは
generateAudioPlugin -exe VSTtest_EQU1
(R2020a以降)

JUCEプロジェクト
generateAudioPlugin -juceproject VSTtest_EQU1
(要 MATLAB Coder)
とします。


2.係数設計部付きフィルター(デバッグ用特性表示付き

画像18

dsp.BiquadFilter オブジェクトを使ったフィルターの例です。

VSTtest_EQU2.m として作成します。
先ほどの VSTtest_EQU1 に加える形で作ってみます。
構成としては VSTtest_EQU1 と同じですが、フィルター係数計算/周波数特性表示ルーチンを自前で用意する必要がある分複雑になっています。

(1)audioPlugin クラスを継承してファイル名と同じ VSTクラスを宣言します

classdef VSTtest_EQU2 < audioPlugin  % Inherit from audioPlugin

(2)GUIで変更したいパラメータを初期値と共に宣言します
両方同時にON/OFFもしたいので、バイパスSWも加えましょう。

   properties
       % GUI parameters
       
       % EUQ1 : 3Band-EQU ON/OFF
       EQU1 = true;
       % EQU1 parameters
       EQU1_F1 = 400;	EQU1_F2 = 1000;	EQU1_F3 = 5000; 
       EQU1_G1 = 0;	EQU1_G2 = 0;	EQU1_G3 = 0;
       EQU1_Q1 = 1.6;	EQU1_Q2 = 1.6;	EQU1_Q3 = 1.6;
       
       % EQU2 ON/OFF
       EQU2 = true;    
       % EQU2 parameters
       EQU2_F = 400;  EQU2_G = 0; EQU2_Q = 1.6;
       
       % both of EQUs ON/OFF
       BYPASS= false;
   end

(3)audioPluginInterface を使ってVSTの概要を記述します
(4)audioPluginParameter を使って各パラメータの属性とレイアウトを指定します

    properties (Constant)
       % set GUI properties and layout
       PluginInterface = audioPluginInterface( ...
           'PluginName','EUQ2',...
           'VendorName', '', ...
           'VendorVersion', '0.0.1', ...
           'UniqueId', 'hiro',...
           audioPluginParameter('EQU1', 'Mapping', {'enum','OFF','ON'}, 'Layout', [1 1]), ...
           audioPluginParameter('EQU1_F1','DisplayName','F1', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [1 2]), ...
           audioPluginParameter('EQU1_G1','DisplayName','G1', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [3 2]), ...
           audioPluginParameter('EQU1_Q1','DisplayName','Q1', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [5 2]), ...
           audioPluginParameter('EQU1_F2','DisplayName','F2', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [1 3]), ...
           audioPluginParameter('EQU1_G2','DisplayName','G2', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [3 3]), ...
           audioPluginParameter('EQU1_Q2','DisplayName','Q2', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [5 3]), ...
           audioPluginParameter('EQU1_F3','DisplayName','F3', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [1 4]), ...
           audioPluginParameter('EQU1_G3','DisplayName','G3', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [3 4]), ...
           audioPluginParameter('EQU1_Q3','DisplayName','Q3', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [5 4]), ...
           audioPluginParameter('EQU2', 'Mapping', {'enum','OFF','ON'}, 'Layout', [7 1]), ...
           audioPluginParameter('EQU2_F','DisplayName','F', 'Mapping',{'log',20, 22000},'Style','rotaryknob', 'Layout', [7 2]), ...
           audioPluginParameter('EQU2_G','DisplayName','G', 'Mapping',{'lin',-12, 12},'Style','rotaryknob', 'Layout', [7 3]), ...
           audioPluginParameter('EQU2_Q','DisplayName','Q', 'Mapping',{'lin',0.2, 50},'Style','rotaryknob', 'Layout', [7 4]), ...
           audioPluginParameter('BYPASS', 'Mapping', {'enum','OFF','ON'}, 'Layout', [8 1]), ...
           audioPluginGridLayout('RowHeight', repmat([70 15],1,4), ...
           'ColumnWidth', repmat([80 80],1,2), ...
           'Padding', [20 20 20 20]) ...  % [left, bottom, right, top]
           );
   end

ここも同じですね

(5)ユーザーが直接操作しないプライベートプロパティを宣言します
ここではフィルター処理を行うためのフィルターオブジェクトと周波数特性表示用のオブジェクトを宣言します。

   properties(Access = private)
       % EUQ1 : 3Band-EQU
       objEQU1;
       
       % EUQ2
       Num, Den, objEQU2;
       
       % Visual object
       visualObj
       
       AreFiltersDesigned = false;
   end

(6)コンストラクターで、実際のフィルターオブジェクトを構築します

       % Constructor   
       function plugin = VSTtest_EQU2
           plugin@audioPlugin;
           
       	  plugin.objEQU1 = multibandParametricEQ('NumEQBands',3,'EQOrder',4, ...
               'HasLowShelfFilter',0,'HasHighShelfFilter',0,'HasLowpassFilter',0,'HasHighpassFilter',0, ...
               'Frequencies',[400, 1000, 5000],'PeakGains',ones(1,3)*0,'QualityFactors',ones(1,3)*1.6,'SampleRate',getSampleRate(plugin));

           plugin.objEQU2 = dsp.BiquadFilter('SOSMatrixSource','Input port', 'ScaleValuesInputPort',false);
           calculateCoefficients(plugin);
           
       end

dsp.BiquadFilter は Tunable パラメータがありませんので、'SOSMatrixSource', 'Input port' と設定して、フィルター係数を自分で計算して外部から更新できるようにします。

visualize() も適用できないので、
plugin.visualObj = dsp.DynamicFilterVisualizer ~
とし、それらに使用する変数 "Num", "Den" も、(5)で定義しておきます。

(7)GUI操作をした場合の挙動をsetに記述します
ここは長くなるのでEQU1の部分は省略します。
自分で宣言した変数に入れるだけなので、 get なしでこう書いてもOKです。

       % EQU2
       function set.EQU2_F(plugin,val)
           plugin.EQU2_F = val;
           needToDesignFilters(plugin);
       end
       function set.EQU2_G(plugin,val)
           plugin.EQU2_G = val;
           needToDesignFilters(plugin);
       end
       function set.EQU2_Q(plugin,val)
           plugin.EQU2_Q = val;
           needToDesignFilters(plugin);
       end

(8)デバッグ用周波数特性ビジュアライズオブジェクトを定義します
周波数特性表示用に dsp.DynamicFilterVisualizer オブジェクトを構築しておきます。

        function visualize(plugin,NFFT)
           if nargin < 2
               NFFT = 2048;
           end
           visualize(plugin.objEQU1, NFFT);

           if isempty(plugin.visualObj) || nargin > 1
               Fs = getSampleRate(plugin);                               
               
               plugin.visualObj = dsp.DynamicFilterVisualizer(...
                   NFFT,Fs,[20 20e3], ...
                   'XScale', 'Log', ...
                   'YLimits', [-12 12], ...
                   'Title', 'Equalizer'); 
           else
               if ~isVisible(plugin.visualObj)
                   show(plugin.visualObj);
               end
           end
           % Step the visual object with the filter
           step(plugin.visualObj, plugin);
       end

フィルター設定が変更されたときに飛ぶ needToDesignFilters
そのサブルーチンである calculateCoefficients
dsp.DynamicFilterVisualizer が hidden method として呼び出す freqz
そのサブルーチンの getFilterCoefficients
をそれぞれ用意します。

       function [B, A] = getFilterCoefficients(plugin)
           calculateCoefficients(plugin);
           B = plugin.Num.';
           A = plugin.Den;
           A = [ones(1,size(A,2));A].';
       end       
    methods (Access = private)
       function calculateCoefficients(plugin)
           % Function to compute filter coefficients
           Fs = getSampleRate(plugin);
           G = 10^(plugin.EQU2_G/20);  % convert dB to VGain
           if G < 1
               PEQ_q = plugin.EQU2_Q*G;
           else
               PEQ_q = plugin.EQU2_Q;
           end
           A = G/PEQ_q;
           W = tan (pi*plugin.EQU2_F/Fs);
           N = 1/(W^2 + W/PEQ_q + 1);
           B0 = N*(W^2 + W*A + 1);
           B1 = 2* N*(W^2 - 1);
           B2 = N*(W^2 - W*A + 1);
           A1 = B1;
           A2 = N*(W^2 - W/PEQ_q + 1);
           
           plugin.Num = [B0 B1 B2; 1 0 0]';
           plugin.Den = [A1 A2; 0 0]';
           
           plugin.AreFiltersDesigned = true;
       end
       
       function needToDesignFilters(plugin)
           plugin.AreFiltersDesigned = false;
           calculateCoefficients(plugin);
           % Update visual if visualize has been called
           if isempty(coder.target) && ~isempty(plugin.visualObj) 
               num = plugin.Num.';
               den = plugin.Den;
               den = [ones(1,size(den,2));den].';
               step(plugin.visualObj, num, den); 
               plugin.visualObj.SampleRate = plugin.getSampleRate;
           end
       end
   end
   methods (Hidden)
       function h = freqz(plugin, f, Fs)
           [B, A] = getFilterCoefficients(plugin);
           h = zeros(length(f),1);
           h(:,1) = freqz(B(1,:), A(1,:), f, Fs);
           for k = 2:size(B,1)
               h(:,1) = h(:,1).*freqz(B(k, :), A(k, :) , f, Fs).';
           end
       end
   end

(9)メイン処理部を書きます

       %------ main process -------
       function out = process(plugin,in)
           if plugin.BYPASS
               out = in;
           else
               if plugin.EQU1
                   out = step(plugin.objEQU1,in);
               else
                   out = in;
               end
               if plugin.EQU2
                   out = plugin.objEQU2(out, plugin.Num, plugin.Den);
               end
           end
       end
       %--------------------------- 

(10)リセット関数を書きます

        function reset(plugin)
           plugin.objEQU1.SampleRate = getSampleRate(plugin);
           reset(plugin.objEQU1);
           reset(plugin.objEQU2);
       end


3.ステレオローテーション

画像18

回転行列を使って、ステレオのままLRの定位を回転させます。

classdef VSTtest_Rot < audioPlugin  % Inherit from audioPlugin
properties
       % GUI parameters
       XT=1, YT=1, Speed=1, Gain=1;

       BYPASS = false;
       
       MaxAngle = '360';  MaxAngleN = 360;
       Pos_angle, pos_u, pos_d, ang
       Rate = 1/4000;
end
   
   properties (Constant)
       % set GUI properties and layout
       PluginInterface = audioPluginInterface( ...
           'PluginName','Stereo Rotation',...
           'VendorName', '', ...
           'VendorVersion', '0.0.1', ...
           'UniqueId', 'hiro',...
           audioPluginParameter('BYPASS', 'Mapping', {'enum','OFF','ON'}, 'Layout', [1 1]), ...
           audioPluginParameter('Gain', 'Mapping',{'lin', 0, 1},'Style','rotaryknob', 'Layout', [1 2]), ...
           audioPluginParameter('Speed', 'Mapping',{'lin', 0, 10},'Style','rotaryknob', 'Layout', [1 3]), ...
           audioPluginParameter('MaxAngle', 'DisplayNameLocation','above', 'Mapping',{'enum','15','30','45','90','180','270','360'},'Style','Dropdown', 'Layout', [4 1]), ...
           audioPluginParameter('XT', 'Mapping',{'int', 1, 10},'Style','rotaryknob', 'Layout', [3 2]), ...
           audioPluginParameter('YT', 'Mapping',{'int', 1, 10},'Style','rotaryknob', 'Layout', [3 3]), ...
           audioPluginGridLayout('RowHeight', [70 15 70 15], ...
           'ColumnWidth', [80 80 80], ...
           'Padding', [20 20 20 20]) ...  % [left, bottom, right, top]
           );
   end
   
   methods
       % Constructor   
       function plugin = VSTtest_Rot
           plugin@audioPlugin;
           
           plugin.Pos_angle = 0;
           plugin.pos_u = plugin.Rate;
           plugin.pos_d = plugin.pos_u;
           plugin.ang = plugin.Pos_angle;
       end

       function set.Speed(plugin,val)
           plugin.Speed = val;
           plugin.pos_d = val * plugin.Rate;  %#ok
           plugin.pos_u = val * plugin.Rate;  %#ok
       end
       
       function set.MaxAngle(plugin,val)
           plugin.MaxAngle = val;
           plugin.MaxAngleN = abs(str2double(val));  %#ok
       end
       function val = get.MaxAngle(plugin)
           val = plugin.MaxAngle;
       end
       
%% main        
       %------ main process -------
       function out = process(plugin,in)
           out = in * plugin.Gain;
           if ~plugin.BYPASS
               for n=1:length(in)
                   plugin.ang =  plugin.Pos_angle;
                   plugin.Pos_angle = plugin.Pos_angle + plugin.pos_d;
                   if plugin.Pos_angle >= plugin.MaxAngleN
                       plugin.Pos_angle = plugin.MaxAngleN;
                       plugin.pos_d = -plugin.pos_u;
                   elseif plugin.Pos_angle <= -plugin.MaxAngleN
                       plugin.Pos_angle = -plugin.MaxAngleN;
                       plugin.pos_d = plugin.pos_u;
                   end

                   cos_coef = cos(plugin.XT*plugin.ang/180*pi);
                   sin_coef = sin(plugin.YT*plugin.ang/180*pi);
                   mag = abs(cos_coef) + abs(sin_coef);
                   if mag <= 1; mag = 1; end
                   
                   Lin = in(n,1) * plugin.Gain;  Rin = in(n,2) * plugin.Gain;
                   out(n,1) = (Lin * cos_coef - Rin * sin_coef)/mag;
                   out(n,2) = (Lin * sin_coef + Rin * cos_coef)/mag;
               end
           end
       end
       %---------------------------       
     
   end
  
end

「MATLAB/Simulinkで動画Picture In Picture」でも用いたリサージュ関数を使っており、XT:YTの比率で回転パターンが変わります。

Speedが回転速度で、0 にするとそこで回転が止まります。

"Angle" はいわゆる立体音響でいう音源の位置ではなく、あくまでも「回転行列の回転角」です。

XT = YT = 1 の場合、以下のようになります。

Angle	L_out	R_out
 0		 L		 R
45		 S		 M
90		-R		 L
135		-M		 S
180		-L		-R
225		-S      -M
270		 R      -L
315		 M      -S

"M/S" は "Mid/Side" のことで
M = (L + R)/2; S = (L - R)/2;
です。


前の2例が分かれば特に説明の必要はないかと思います。コードも短いのでソースを見てください。

一点だけ
function set.MaxAngle(plugin,val) で

plugin.MaxAngleN = abs(str2double(val));

としていますが、コード生成時の str2double の戻り値は常に複素数となるため、absを取っています。


メイン関数は、実用上はサンプル単位で処理をする必要はないと思いますが、サンプル単位で処理する場合の例も兼ねてそうしてみました。ここで自前ルーチンでフィルター処理をする場合なども、ディレイバッファ等、変数の内容は保持されます。

サンプル単位の場合は特に、メインの処理が重くなるとMATLAB上ではリアルタイム動作しなくなりますので、アルゴリズム的に処理を軽くするか、その部分をmexファイル化して高速化する等の工夫が必要になります。もちろんそのままでも、VST化すれば速くなります。

たまに、プロパティ関連を変更した後に動作が重くなる場合があるようですが、その場合はMATLABを再立ち上げしてみましょう!


VSTの使用方法

ここからは、VSTプラグインを他のアプリと組み合わせて使用する方法を解説します。

foobar2000への組込み方法
32bit版のみ対応なので、32bit版VSTを生成します。
generateAudioPlugin -win32 ~

Foobar2000 で VST を使うためにはまず、VST Wrapper をインストールします。いくつかあるようですが、例えば Foobar2000 VST Wrapper があります。

-インストール
Foovarvst.zip を解凍後、"foo_dsp_vstwrap.dll" を以下のフォルダーに置いて下さい。
  C:\Program Files (x86)\foobar2000\components\
VST も同じフォルダーに置きます。

-設定
foobar2000 を起動し、File -> Preferences
 "Components" に "George Yohng's VST Wrapper" が追加されているのを確認
 "Playback" -> DSP Manager で、"Available DSPs" から "George Yohng's VST Wrapper" 右の "+"で "Active DSPs" に追加
(ここで "Resampler”も入れておくと、foobar2000出力のサンプリングレートを、ソースに寄らず固定にもできます)
 "Oputput" で出力デバイスやバッファサイズを設定できます
あとは、ツールバーアイコンで切替等を行います

画像18

ツールバーアイコンをクリック -> "Use VST Effect" で使用するプラグインを選択します
 "Show/Hide Plugin Editor" でGUIの表示ON/OFFができます
 アイコンが見当たらない場合は左の ^の中にあるはずですので探してみてください
VSTが一覧に表示されなかった場合は foobar2000 を立ち上げ直してみてください

単体exeの使用方法
単体で起動でき、外部入力は選べますが、このままでは音楽プレイヤーの出力等に処理を掛けることができません。

VB-CABLE(Donationware)等を使うと、Virtual In → Virtual Out へ信号を流せるので、プレイヤー出力を Virtual In、単体exeの入力を Virtual Out、出力を実際のスピーカー等とすると、

プレイヤー → Virtual In → Virtual Out → 単体exe → スピーカー

のように信号が流れて、プレイヤーで再生した音楽に処理を掛けてスピーカーから出すことができます。

Windowsタスクバーのスピーカーアイコンをクリックして、プライマリ出力を Virtual In にしておけば、Webブラウザー等の出力にも効果を掛けることができます。


-インストール
上記サイトから "VB-CABLE Driver" をDL/解凍後、右クリック→管理者インストール してからPCを再起動してください。
"VB-Audio Virtual Cable" がバーチャル再生/録音デバイスとしてそれぞれ登録されます。

-設定
(1)Windowsツールバーのスピーカーアイコンをクリックして "VB-Audio Virtual Cable" を選択します。
これでWindowsのデフォルト音声出力が Virtual Cable になります。
(使い終わったら戻しておきましょう)

VLC/foobar2000等、プレイヤー側で出力デバイスを選択できる場合はそちらで選択しても構いません。


(2)VSTの Options -> Audio/MIDI Settings で、"Input" を "Cable Output"に、"Output" を実際に音を出したいデバイスに設定します。
"Mute audio input" にチェックが付いている場合は外してください。

画像8


同じくVB-AUDIOの Voicemeeter/Banana/Potato 辺りを入れても
"VoiceMeeter VAIO”、"VoiceMeeter VAIO3" がサウンドドライバーとして登録されるので、同様に使うことができます。

VSTホスト
exe版でバッファサイズを大きくしても音切れが発生するようであれば、VSTホストの利用をお勧めします。

-設定方法
(1)DL/解凍後、vsthost.exe を起動(インストール不要)
(2)キャンバス上で右クリック File -> New Plungin でVSTを選択
(3)Devices -> Wave で Input:CABLE Output、Output:任意のデバイスに
(4)Buffer を、ノイズの出ない範囲で小さな値に設定
(5)Windowsのデフォルト音声出力を Virtual Cable に設定

これでPC上で何か音を出せば、
  Virtual Cable -> VSTHost -> VST -> Virtual Cable
経由で音が流れます。

画像18


追補

最近は、App Designer アプリで検証して仕様がだいたい決まってくると、それ以降はVSTも同時に作るようにしています。しかし、VSTの様なコード生成では、GUIパラメータ以外は定数としてプログラム内で設定する必要があります。まだ検討段階でGUIパラメータ以外も色々変えたいときは、VSTのソースもいちいち書き直すのは面倒ですし、書き間違いでバグが発生するかもしれません。

そこでその場合、アプリで設定ファイルの LOAD/SAVE ができるようにしておいて、その設定ファイルを、VSTのコンストラクタ内で変数に読み込んでコード生成するようにしています。
(表示への反映は不可なので、GUIパラメータ以外)

ただし、MathWorks 非推奨です!
開発チームとしては「コード上で直接指定することを "強く" 推奨する」とのことですので、自己責任で・・。


その例を示します。
先ほどのステレオローテーションで、元々内部定数である "Rate" に加え、MaxAngle もGUIから削除して、設定ファイルから読み込んだ初期値で固定することにします。

1.App Designer アプリ(またはMATLAB上で)
変数を設定ファイル(.mat)ファイルにSAVE

MaxAngleN = 360;  MaxAngle = num2str(MaxAngleN);  Rate = 1/4000;
save('VSTtest_Rot_para.mat', 'MaxAngleN', 'MaxAngle', 'Rate')

もしくは構造体でSAVEします。

s.MaxAngleN = 360;  s.MaxAngle = num2str(s.MaxAngleN);  s.Rate = 1/4000;
save('VSTtest_Rot_para.mat', '-struct', 's')

2.VST側
以下をコンストラクタ内に追加します。

paraFilename = 'VSTtest_Rot_para.mat';
s = coder.load(paraFilename);
plugin.MaxAngleN = s.MaxAngleN;  plugin.MaxAngle = s.MaxAngle;  plugin.Rate = s.Rate;

こうしておけば、パラメータファイルを変更あるいはVST内の paraFilename を変えてコンパイルすれば、ソースの他の部分を修正する必要がなくなります。

おまけ

ステレオローテーションのメイン処理をフレーム単位にして、回転角のリセットとマニュアル調整を付加したバージョンを、32bit版VST(ID登録済み)と一緒に載せておきます。
ご興味のある方は遊んでみてください!

画像19

"Reset" をチェックすると元の状態に戻り、チェックを外すと回転が始まります。
"Speed" を "0" にするとそこで回転が止まります。
"XT" : "YT" の比率で回転パターンが変わります。
"MaxAngle" を "Manual" にすると、 "Manual Angle" スライダーが有効になります。
"Manual Angle" は早く動かすと波形が不連続になりノイズが出ますので、ゆっくり動かすようにしてください。

Try it out!

VSTプラグインはDAWをやっている人以外にはあまり馴染みがないかもしれませんが、オーディオ信号処理の検証にもとても便利です。MATLAB使いであれば、ぜひ活用してみてはいかがでしょうか!?

逆に、VSTプラグインには興味あるけどCとかはちょっと・・、という方も、MATLABで始めてみませんか!?

それでは、充実した夏休みを!


その他公式参考リンク
Design an Audio Plugin
Convert MATLAB Code to an Audio Plugin
Export a MATLAB Plugin to a DAW
Design an Audio Plugin
Tips and Tricks for Plugin Authoring





この記事が気に入ったらサポートをしてみませんか?