見出し画像

MATLAB でマルチチャネル対応 VST 開発 ~ Ducking VST ~ ナレーションに合わせて BGM 音量を自動調節

System Object Plugin を直接作る方法の記事を追加しました。(2023/1/2)

System Object Plugin ~ Simulink にもそのまま読み込めるプラグインを作る

まえがき

Audio Toolbox では、2ch を超えるマルチチャネル入出力に対応しています。上限は特に書いてありません。

マルチチャネル対応の VST を作りたい場合、audioPluginInterface() で 'InputChannels'、'OutputChannels' をそれぞれ指定するだけです。

ただ、デバッグ環境である audioTestbench では マルチストリームとかには対応していないようですし、DAW でどうやって使うのかも分からなかったので、今までスルーしていました。

今回、デバッグ方法や DAW での対応方法が分かったのでまとめておきます。


Ducking VST

簡単な例として、ナレーションが入ると自動的に BGM の音量を下げる Ducking VST を作ってみます。完成後の実行例がこちらです。


この章の最後に全スクリプトを置いておきますが、マルチチャネル対応部は最初のチャネル数設定部と、入出力それぞれのチャネル(列)指定だけです。(出力は 2ch でも良いのですが、デバッグ用に 4ch 出しています)

audioPluginInterface( ~
     'InputChannels',4, 'OutputChannels',4, 

チャネル数設定部

input = in(:,1:2) * inGain;

sideIn = in(:,3:4);


outTmp = outTmp * outGain;
out(:,3:4) = outTmp(1:size(in,1),1:2);
out(:,1:2) = outTmp(1:size(in,1),1:2);
out(:,1:2) = out(:,1:2) * outGain + sideIn;

入出力チャネル指定部


Ducking は、コンプレッサーをサイドチェインで使います。

通常のコンプレッサーではゲイン計算対象処理対象が同じですが、ゲイン計算を別の信号により行うのがサイドチェイン機能です。

今回の場合、ナレーションをサイドチェイン入力としてゲイン計算を行い、その結果を BGM に反映させナレーションと足し合わせて出力します。ナレーション自体は何も処理をしません。

サイドチェインは、パーカッシブなトラック(ドラムの一部など)を入力として他の楽器の音量を下げ、リズムを際立たせる場合などにもよく使われます。


サイドチェイン付きコンプレッサー

Audio Toolbox にはサイドチェイン機能付きのコンプレッサーオブジェクトが最初から用意されているので、サイドチェインを有効にするだけで使えます。

plugin.dRC = compressor(plugin.LimitdB, SampleRate=Fs, EnableSidechain = true);

outTmp = plugin.dRC(input,sideIn);

サイドチェイン付きコンプレッサー

Audio Toolbox のダイナミックレンジコントロールオブジェクト 4 種に関しては Dynamic Range Control に詳しい説明が載っていますのでご参照ください。

コンプレッサーは、有名なあの論文のインプリのようです。


全スクリプト

classdef Ducking < audioPlugin
    % Copyright 2023
    % AudiiSion Sound Lab. LLC.
    % All rights reserved.  
    properties
        inputGaindB = 0;
        outputGaindB = 0;
        LimitdB = -80;
        kneeWidth = 5;
        attackTime = 200;
        releaseTime = 2000;
        compRatio = 2;

        dRL;  % limiter obj
        dRC;  % compressor obj

        ModeC = {'Limiter','Compressor'};

        Mode = 'Compressor';
        ModeN = 0;
        ModeN_old = 0;
    end

    properties (Constant)
        PluginInterface = audioPluginInterface( ...
            'PluginName','Ducking',...
            'VendorName', 'AudiiSion Sound Lab.', ...
            'VendorVersion', '0.0.1', ...
            'UniqueId', 'hiro',...
            audioPluginParameter('inputGaindB', ...
            'DisplayName','Input Gain', 'Label','dB','Mapping',{'pow', 1/3, -80, 6}, ...
            'Layout',[2,1;2,2], 'Style','rotary'), ...
            audioPluginParameter('outputGaindB', ...
            'DisplayName','Output Gain', 'Label','dB','Mapping',{'pow', 1/3, -80, 6}, ...
            'Layout',[2,5;2,6], 'Style','rotary'), ...
            audioPluginParameter('Mode','Mapping',{'enum', 'OFF','Limiter','Compressor'}, ...
            'Layout',[1,4;1,6], 'DisplayNameLocation','none'), ...
            audioPluginParameter('kneeWidth', ...
            'DisplayName','KneeWidth', 'Label','dB', 'Mapping',{'pow', 1/3, 0, 12}, ...
            'Layout',[4,3;4,4], 'Style','rotary'), ...
            audioPluginParameter('LimitdB', ...
            'DisplayName','Threshold', 'Label','dB','Mapping',{'pow', 1/3, -80, 6}, ...
            'Layout',[4,5;4,6], 'Style','rotary'), ...
            audioPluginParameter('compRatio', ...
            'DisplayName','Comp Ratio', 'Mapping',{'lin', 0, 10}, ...
            'Layout',[6,1;6,2], 'Style','rotary'), ...
            audioPluginParameter('attackTime', ...
            'DisplayName','Attack Time', 'Label','ms', 'Mapping',{'lin', 0, 1000}, ...
            'Layout',[6,3;6,4], 'Style','rotary'), ...
            audioPluginParameter('releaseTime', ...
            'DisplayName','Release Time', 'Label','ms', 'Mapping',{'lin', 0, 5000}, ...
            'Layout',[6,5;6,6], 'Style','rotary'), ...
            audioPluginGridLayout('RowHeight', [40 70 15 70 15 70 15], ...
            'ColumnWidth', [repmat([40 40],1,3)], 'Padding', [10 10 10 20]), ...
            'InputChannels',4, 'OutputChannels',4, ...
            'BackgroundColor',[1 1 1],'BackgroundImage','DLossy_bk3.jpg' ...
            );
    end

    methods
        function plugin = Ducking  % constructor
            plugin@audioPlugin;
            Fs = getSampleRate(plugin);

            % limiter
            plugin.dRL = limiter(plugin.LimitdB, SampleRate=Fs, EnableSidechain = true);
            % compressor
            plugin.dRC = compressor(plugin.LimitdB, SampleRate=Fs, EnableSidechain = true);
        end

        function out = process(plugin,in)
            out = zeros(size(in,1),4);

            inGain  = 10^(plugin.inputGaindB/20);
            outGain = 10^(plugin.outputGaindB/20);

            % set ModeN in out of frame loop
            % prevent Mode change within frame
            switch plugin.Mode
                case plugin.ModeC{1}
                    plugin.ModeN = 1;
                case plugin.ModeC{2}
                    plugin.ModeN = 2;
                otherwise  % OFF
                    plugin.ModeN = 0;
            end

            input = in(:,1:2) * inGain;

            plugin.dRL.KneeWidth = plugin.kneeWidth;
            plugin.dRL.AttackTime = plugin.attackTime/1000;
            plugin.dRL.ReleaseTime = plugin.releaseTime/1000;
            plugin.dRL.Threshold = plugin.LimitdB;

            plugin.dRC.KneeWidth = plugin.kneeWidth;
            plugin.dRC.AttackTime = plugin.attackTime/1000;
            plugin.dRC.ReleaseTime = plugin.releaseTime/1000;
            plugin.dRC.Threshold = plugin.LimitdB;
            plugin.dRC.Ratio = plugin.compRatio;

            sideIn = in(:,3:4);
            switch plugin.ModeN
                case 1
                    outTmp = plugin.dRL(input,sideIn);
                case 2
                    outTmp = plugin.dRC(input,sideIn);
                otherwise
                    outTmp = in * inGain;
            end

            outTmp = outTmp * outGain;
            out(:,3:4) = outTmp(1:size(in,1),1:2);
            out(:,1:2) = outTmp(1:size(in,1),1:2);
            out(:,1:2) = out(:,1:2) * outGain + sideIn;
        end

        function reset(plugin)
           plugin.dRL.SampleRate = getSampleRate(plugin);
           plugin.dRC.SampleRate = getSampleRate(plugin);
           reset(plugin.dRL);
           reset(plugin.dRC);
        end

    end
end

Ducking.m

コード生成対応用サイズ確定のため余分に見える部分があるかもしれませんが、基本はコンプレッサーを当てて出しているだけなので特に説明はいらないかと思います。


一応、リミッターとコンプレッサーを切り替えられるようにしています。

ただし Simulink では boolean、リテラル文字ベクトルまたは非数値構造体の値をもつパラメーターを調整可能にすることができないので、実行中は切替不可です。実行中に切り替えたい場合は、"数値"にする必要があります。


各 GUI パラメータを毎フレーム代入していますが、set function を置いて変更時のみアップデートするようにした方がほんの少し処理量は減ります。

今回はちょっと試したかっただけなので、process() 内で毎回入れています。

その辺りは
MATLABでVSTプラグイン開発 [初級編]
夏休みはVST! MATLABで楽々(?)VSTプラグイン開発
をご参照ください。


デバッグ方法

Audio Toolbox では、例えば2つのステレオオーディオファイルを合わせて 4ch 入力とする等、フレキシブルなチャネルマッピングには対応していません。

一旦 VST にしてから MATLAB あるいは Simulink でホスティングすれば実行はできますが、デバッグには適しません。

その場合、Simulink の MATLAB System ブロックを使うと、ソースファイルのまま「コード生成/インタープリター型実行」を切り替えて多チャンネル入出力シミュレーションが行え便利です。


MATLAB System ブロック

MATLAB System ブロックは、ストリーミング処理に適した特殊な MATLAB オブジェクトである System object を Simulink で使用するためのブロックです。

なにやら難しそうに聞こえるかもしれませんが使い方は簡単で、VST ソースファイルに自分で手を加える必要も一切ありません

1.まず(ソースコードを2ch入出力にして) audioTestBench でデバッグし、VST スクリプト自体が動くことを確認します

2.VST 用ソースファイルから、Simulink 用 Audio Plugin System に変換します
(ソースファイルを Ducking.m、出力ファイル名を DuckingSL.m とする)

>> generateSimulinkAudioPlugin(Ducking,"DuckingSL")

これでラッパーが生成されます。

最初から System object 兼用でソースコードを書く方法もありますが、少なくとも私にとってはこちらの方がやりやすいのでこの方法をとります。ただし、generateSimulinkAudioPlugin() は R2022b 以降です。それより前のバージョンをお使いの場合は、System Object Plugin として書く必要があります。

今回は試していませんが、Audio Plugins in MATLAB を参考に書き換えれば動くのではないかと思われます。確か、ほぼ function 名等を書き換える程度だったはず・・。(普通に VST も生成できます)

3.Simulink モデル上に MATLAB System ブロックを置きます。

ダブルクリックして System object 名: の右をクリックすると先ほど変換した System object 名が一覧に出てくるはずです。

MATLAB System ブロックの設定


ここで OK を押すと System object 名のブロックができあがり、もう一度ダブルクリックするとパラメータ調整画面が開きます。

MATLAB System ブロックパラメータ


あとは外から調整したいパラメータを "Specify ~ from input port" にして、constant で入れるなり Gain Slider を通すなり、好きなようにします。

ここではとりあえず、Dashboard の Knob を置いて調整できるようにしてみました。リアルタイムに調整する必要がなければ上のままでも構いません。

Simulink モデル例


これで Simulink 上でリアルタイムにパラメータを動かしながらシミュレーションを行うことができ、グラフなどを使ったデバッグも行えます。

以下にシミュレーションの様子を示します。
(パラメータは動かしていませんが)

Simulink シミュレーションの様子

また、右クリック -> マスク -> マスクの編集 で、以下のような設定にすると、ダブルクリックでタブとダイヤル GUI 表示をすることなどもできます。
( SimulinkでカスタムGUI 参照)

マスクの編集

最近のバージョンでタブを作るには、「タブコンテナ」を置いてからその下に作る必要があります。


ダブルクリックでタブグループ付き GUI が表示される
Tunable タブ
Preset タブ


DAW への組込方法

シミュレーションで正常動作が確認できたら、VST プラグインを generate して DAW に組み込みます。

ここでは Reaper の例を示します。

  1. トラック1に BGM、トラック2にナレーションを挿入

  2. トラック1の FX に VST を挿入

  3. トラック1のボリュームつまみを右クリック、または ”Route" をクリック

  4. ”Track channels:" を 4 に

  5. トラック2のボリュームつまみを右クリック、または ”Route" をクリック

  6. "Add new send” で、トラック1の 3/4 にルーティング(今回は Rch のみ使用)

トラック1ルート設定
トラック2ルート設定
設定後の Reaper メイン画面


”Route" にカーソルを持って行くと情報が表示されます。古いバージョンはちょこちょこバグがあるようなので最新にしておきましょう。これは v6.73/win64 です。


”Route" が表示されていれば、トラック2の ”Route" をトラック1にドラッグしても設定ができるようです。

Mixer が表示されていると ”Route” が Mixer に移動してしまうので、Mixer で操作するか、Mixer を消すか、Alt+r してみてください。

Mixer 表示


あとがき

今までなんとなく「(デバッグ環境も含めると)簡単にはできないんじゃないか?」と思っていた、マルチチャネル入出力のリアルタイムオーディオ信号処理が簡単にできることが分かりました。

とりあえず使い道は思いつきませんが。w

何か面白いのができたら教えてください。 (。・_・。)ノ


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