見出し画像

HRTF を聴き比べ ~ MATLAB の VST 生成機能を利用してスタンドアローンアプリを作る ~ Home ライセンスでもOK


R2023bで SOFA ファイルを直接取り扱えるようになり、外部 API を使用する必要がなくなりました。
その件を記事中に追記しています。

2023/09/16 更新


試聴だけしたい方は、以下を飛ばしてアプリ説明・使用方法へどうぞ。Windows 版のみとなっております。


まえがき

VST(Virtual Studio Technology) プラグインとは、スタインバーグ社(現ヤマハ傘下)がDAW用に開発したプラグイン規格です。

詳しくはこちらをご参照ください。


VST プラグインは、一般的に DAW 等に組み込んで使用します。

一方、JUCE や MATLAB では、単体動作可能なスタンドアローンの VST も生成できます。

DAW なしで単体起動でき、マイク入力や外部オーディオ I/F の入力に対して処理が行えます。

PC上の任意の音(プレイヤーやWebブラウザ等)に対して処理が行いたい場合は、Virtual I/O を使います。

その辺りはこちらをご覧ください。


つまり、なんらかの外部入力に対してリアルタイム処理を行うのが VST(エフェクト)プラグインです。

JUCE では VST プラグイン形式ではなく通常のアプリ開発も行えますが、MATLAB にその機能はありません。

しかし、音源(オーディオソース)を内蔵しそれを内部で入力データとして使えば、外部からの入力不要な普通(?)のアプリとして動作させることもできます。(ということに気付きました)

入力データを入れ替えるだけなので併用も可能です。

今回は HRTF データベースを読み込み、それを切り替え試聴できるアプリを作ってみましょう。

おまけ?で、AudiiSion EP との切替も付けてます。

HRTF 試聴アプリ


「HRTF って?」という方はこちらをご覧ください。


AudiiSion EP とは、イヤホン独特の狭い音場(頭内定位)を解消し、自然なリスニングを可能とする独自技術で、コナミさんのアメージングボンバーマンのゲーム内効果音にも採用されています。

主な特徴は以下の通りです。

  • マルチチャネルソース不要(通常の 2 ch ステレオソースのみ使用)

  • 効果の個人差・音質変化が小さい

  • 音像定位がクリアで聴き分けやすくなる

  • APTF 係数精度は 8 ビット 固定小数点・整数演算が可能(32 ビット推奨)

  • 演算量が少ない 固定小数点演算で 16 MIPS~ 程度(48 kHz サンプリング時 オーバーヘッドを除く)

  • 圧縮処理を行っても効果が弱まりにくい

  • 44.1 k / 48 k / 96 kHz サンプリング対応

  • コーダーレイテンシー 約 0.02 ms(= PCM1サンプル分 フレーム・ブロック処理不要)


パラメータは日々更新されていますので、最新版のデモ、詳細ついてはこちらをご参照ください。



HRTF は個人差が大きいので、他人の HRTF と精度良く合致する確率はかなり低いと思われますが、今回使わせていただいている RIEC のデータベースは 105 人分と他の DB より多く、うまくすれば自分と合った HRTF データが見つかるかもしれません(?)。


VST 開発のメリット

MATLAB でアプリと言ったら App Designer でしょう。

App Designer によるアプリ開発に関してはこちらをご覧ください。


しかし、MATLAB ライセンス不要で単独実行可能な MATLAB アプリは MATLAB Compiler、コード生成及びネイティブアプリは MATLAB Coder がないと作れません。これらの Toolbox はかなり高価で、Home ライセンスでは提供されていません。

また、MATLAB Compiler で生成したアプリの実行には、そこそこ巨大な(最近は4GB超え?)ランタイムも必要です。

それに対し、VST にすると以下のようなメリットがあります。

ネイティブアプリになるので高速
・MATLAB ランタイムが不要
Audio Toolbox があればよいので Home ライセンスでも開発可能

逆にデメリットとしては、例えば以下のようなことがあります。

Audio Toolbox  が必要
・GUI の制約が多く、グラフやテキストボックス等も表示できない
・内蔵できるデータ量に制限がある
 公式な記載は見当たりませんが、wav データをそのまま入れると30MB程度?
 それを超えると validate は通りますが、generate の最後で C コンパイラのビルドエラーが発生します
 なので、CD 品質で 3 分も入りません
 テストトーン等内部で生成可能なデータであれば問題ありません


簡単な GUI さえあればよく、軽いアプリで高速動作を行わせたい場合などに有効な手段かと思われます。

別途 C コンパイラも必要ですが、Visual Studio でも良いですし、MinGW-w64 C/C++ Compiler であれば MATLAB のメニューからアドオンとしてインストール可能です。C の開発環境を自分で操作する必要は一切ありません。


では、実際に作っていきましょう。


データ準備

まずは、ソースとなる音楽ファイルと HRTF データベースを、VST スクリプトで読み込んで内蔵できるよう mat ファイルに変換しておきます。HRTF DB は必要なデータのみを取り出します。

コード生成では、通常の audioread 関数等は使えません。低水準ファイル操作は可能ですので、テキスト/バイナリファイルの読み書き等は VST(スタンドアローン版のみ) でもできます。

wav ファイル -> mat ファイル変換

これは wav ファイルを読み込んでそのまま mat ファイルとして保存すればOKです。

スクリプト例

clear

[fName, path, ~] = uigetfile('MultiSelect','on','*.wav','Select wav file');

if ~iscell(fName)
    fName = mat2cell(fName,1);
end

fileN = length(fName);
for k = 1:fileN
    [src, ~] = audioread([path '\' fName{k}]);

    [~,fname,~] = fileparts(fName{k})
    save([fname '.mat'], 'src')
end


HRTF DB

今回は、RIEC のを使わせていただいています。

東北大学・秋田県立大学・東北学院大学の連携でまとめられたもので105人(×左右両耳)分の、360度データが含まれています。2014年発表ですが、同じ測定条件でこれだけのデータが揃っているのは現在でも他にはなかなかないと思います。

共通フォーマットである SOFA 形式で書かれているので取り扱いにも便利です。

使用に当たっては以下を参照し、そこからダウンロードしてください。

研究用に無償で公開されており、商用は許諾が必要です。

今回、無償での使用許可を頂いております。

以下もご参照ください。


K. Watanabe, Y. Iwaya, Y. Suzuki, S. Takane, and S. Sato, "Dataset of head-related transfer functions measured with a circular loudspeaker array," Acoust. Sci. & Tech. 35(3), 159 – 165(2014)


渡邉 貫治, 立体音響研究のための公開頭部伝達関数(HRTF)データベースとその動向, 日本音響学会誌, 78-8, pp.449-456, 2022


あらかじめ SOFA API を準備する必要があるため、先ほど上げた記事の「MATLABでHRTFをいじろう」の章を参照してインストールしておいてください。

もうしばらく待つと、取り扱いがもっと楽になるかもしれません(?)。

R2023bで、SOFA ファイルを直接読み書きできる sofaread
 / sofawrite 関数が追加され、外部 API を使用する必要がなくなりました。

2023/09/16 追記


あとは、例えば以下のようにして読み込んで mat ファイルに変換できます。

今回は、azimuth = 90/270 度、elevation = 0 度のデータのみを使用しています。

SOFAstart;

azimuth = 90;
elevation = 0;
dbN = 105;
hrirLL = zeros(dbN,512);  % L -> L ear
hrirLR = zeros(dbN,512);  % L -> R ear
hrirRL = zeros(dbN,512);  % R -> L ear
hrirRR = zeros(dbN,512);  % R -> R ear
for k = 1:dbN
    dbNumStr = sprintf('%03d',k);
    dbName = 'C:\HRTF\RIEC\RIEC_hrtf_all\RIEC_hrir_subject_';  % HRTF DB のディレクトリを指定
    sofaFileName = [dbName dbNumStr '.sofa'];
    hrtf = SOFAload(sofaFileName);

    idx = find(hrtf.SourcePosition(:,1)==azimuth & hrtf.SourcePosition(:,2)==elevation);
    hrirLL(k,:) = squeeze(hrtf.Data.IR(idx, 1, :));
    hrirLR(k,:) = squeeze(hrtf.Data.IR(idx, 2, :));
    idx = find(hrtf.SourcePosition(:,1)==mod(360-azimuth,360) & hrtf.SourcePosition(:,2)==elevation);
    hrirRL(k,:) = squeeze(hrtf.Data.IR(idx, 1, :));
    hrirRR(k,:) = squeeze(hrtf.Data.IR(idx, 2, :));
end

save('RIEC_HRIRs.mat',"hrirLL","hrirLR","hrirRL","hrirRR")

R2023b 以降では、SOFAstart; は不要です。その場合は、
 SOFAloadsofaread
 Data.SamplingRate
→ SamplingRate
 Data.IR → Numerator
に置き換えてください。

2023/09/16 追記

sofaread() 使用例を以下の記事に追加しました。

MATLABでHRTF~頭部伝達関数とは?~聴覚の仕組みは解明されていない~
https://note.com/leftbank/n/n83db3a4d6108

2023/09/18 追記


SOFAフォーマットでは、azimuth は水平面での正面を 0 として反時計回りの角度、elevation は仰角です。

Azimuth
Elevation


Azimuth=90の、左耳用/右耳用それぞれのインパルス応答(HRIR)、周波数特性を確認してみましょう。

通常 plot は5人分だけ、waterfall は105人分全てを表示しています。

taps = size(hrtf.Data.IR,3);
Fs = hrtf.Data.SamplingRate;
dbNum = 105;

lx = logspace(1,5,512)*pi/Fs/2;
hrtfs_L = zeros(dbNum, 512);
hrtfs_R = zeros(dbNum, 512);
fx = lx/(2*pi)*Fs/1000;
dispSubj = 1:25:dbNum;
legendStr = string(dispSubj);

for k=1:dbNum
   h = freqz(hrirLL(k,:),1,lx);  % L -> L ear
   hrtfs_L(k,:) = 20*log10(abs(h));
   h = freqz(hrirLR(k,:),1,lx);  % L -> R ear
   hrtfs_R(k,:) = 20*log10(abs(h));
end

close all

figure  % HRIRs for L ear
plot(hrirLL(dispSubj,:)')
grid on
xlim([60 180])
xlabel('Samples')
yl = ylim;
ylabel('Amplitude')
title('HRIRs Azimuth=90, Elevation=0, L -> L ear ')
lgd = legend(legendStr, Location='best');
title(lgd,'Subject #')

figure  % HRIRs for R ear
plot(hrirLR(dispSubj,:)')
grid on
xlim([60 180])
xlabel('Samples')
ylim([yl(1) yl(2)])
ylabel('Amplitude')
title('HRIRs Azimuth=90, Elevation=0, L -> R ear ')
lgd = legend(legendStr, Location='best');
title(lgd,'Subject #')


figure  % HRTFs for L ear
waterfall(fx, 1:dbNum, hrtfs_L)
xlim([20/1000 Fs/2/1000])
xlabel('Frequency (kHz)')
ylim([1 dbNum])
ylabel('Subject #')
zlabel('Gain (dB)')
title('HRTFs Azimuth=90, Elevation=0, L -> L ear ')

figure  % HRTFs for L ear
plot(fx, hrtfs_L(dispSubj,:)')
grid on
xlim([20/1000 Fs/2/1000])
xlabel('Frequency (kHz)')
ylim([-60 20])
ylabel('Gain (dB)')
title('HRTFs Azimuth=90, Elevation=0, L -> L ear ')
lgd = legend(legendStr, Location='best');
title(lgd,'Subject #')


figure  % HRTFs for R ear
waterfall(fx, 1:dbNum, hrtfs_R)
xlim([20/1000 Fs/2/1000])
xlabel('Frequency (kHz)')
ylim([1 dbNum])
ylabel('Subject #')
zlabel('Gain (dB)')
title('HRTFs Azimuth=90, Elevation=0, L -> R ear ')

figure  % HRTFs for R ear
plot(fx, hrtfs_R(dispSubj,:)')
grid on
xlim([20/1000 Fs/2/1000])
xlabel('Frequency (kHz)')
ylim([-60 20])
ylabel('Gain (dB)')
title('HRTFs Azimuth=90, Elevation=0, L -> R ear ')
lgd = legend(legendStr, Location='best');
title(lgd,'Subject #')


HRIR L -> L  ear
HRIR L -> R ear
HRTF L -> L  ear
HRTF L -> L  ear
HRTF L -> R  ear
HRTF L -> R  ear


個人差がかなり大きいことが分かるかと思います。

また右耳へは、音源が左にあるので遅れて届き、頭によって遮られているのでその分減衰し、特に高域は大きく減衰しています。

MATLAB をお持ちであれば、waterfall 表示はマウスでグリグリ動かせますので眺めてみてください。

アプリ説明・使用方法

アプリ(Windows)

(MATLAB で生成したファイルを圧縮しただけで、note でもアップロード時にウィルスチェックはがあるので問題ないと思いますが、Chrome でブロックされた場合はメニューからダウンロードを表示(または Ctrl+J)して、「危険なファイルを保存」してください。Edge ではそのままダウンロード可でした。)

非営利の個人使用に限り、制限を設けません。
転載・再配布等、一切の二次使用は禁止とさせていただきます。
使用に際し生じた損害に対する責任は一切負いません。

インストール

解凍後にできる exe ファイルをお好きな場所で実行してください。

アンインストール

C:\Users\(ユーザー名)\AppData\Roaming\HRTF_ASEP_Listening\
に前回の設定が保存されます。サイズも 1 kB 程度で何か影響があるわけではありませんが、気になる方は HRTF_ASEP_Listening ディレクトリごと消してください。

初期設定

最初に Option -> Audio/MIDI Settings を開き、Mute を外し、Output として出力デバイス(イヤホンまたはイヤホンが繋がっている I/F )を選択してください。

Mute は外部入力に対してなので外さなくても動作に関係はないのですが、外さないと警告が出たままになります。

Sample rate は DB が 48 kHz 用のみなので固定にしています。48 kHz のオーディオデバイスを出力として設定してください。

Audio buffer size は、音切れのしない範囲で小さい値に設定してみてください。

音切れが解消しない場合は、Audio device type を一旦他のデバイスに切り替えてから戻すと解決する場合もあります。

サウンドコントロールパネルのプロパティに「立体音響」等のタブがあれば全てオフにしてください。

Windows タスクバーのスピーカーアイコンを右クリック -> サウンドの設定 で、実際に音を出すデバイスをクリックし、オーディオの強化空間オーディオオフにします。

システム -> サウンド(Windows11)
サウンドプロパティ

また、PC によってはそれと別の音響効果がデフォルトで入っている場合があるので、そちらもオフにしてください。

例えば DELL であれば、全ての PCに Waves MaxxAudio というのが入っていますので、こちらの設定も確認してください。検索ウインドウで Waves MaxxAudio で検索すると設定が出てくると思います。

個人的には、普段使いでもこれらの設定はオフの方が好ましいと感じます。

多少広がって聞こえたりはしますが、少なくともデフォルト設定で元より「良い音」になっている例を知りません。あくまでも個人的な感想ですが・・。

また、Windows アップデートで勝手にまたオンになっていたりすることもあるので注意が必要です。


アプリ使用法

ON:処理のON/OFFを切り替えます ✔なしだとBYPASSです

HRTF DB:HRTF データ 001~105 を切り替えます それぞれ異なる被験者のデータになります

HRTF / ASEP:処理を、HRTF か AudiiSion EP か切り替えます

Audio Source:オーディオソースを切り替えます

HRTF Gain:HRTF 処理時のゲインを調整します AudiiSion EP 時は固定です

PLAY/STOP:再生/停止を切り替えます 再生中はループ再生され、停止させると曲の頭から再生されます

Input:externalにすると、通常の VST として動作します 設定で入力デバイスを選択してください ASEP 選択時は無効です

処理としては単純で、L/R 2つの音源から左右の耳への HRTF 特性を掛け(HRIR 係数を畳み込み)、耳ごとに足し合わせて再生します。

一般的な仮想サラウンド処理


オーディオソースは、効果が分かりやすいように左右のパンを大きく取ってますが、普通の 2ch ステレオです。

HRTF の処理は、各音源位置毎に違う HRTF 特性を掛ける必要があります。

今回は音が一番広がって効果が分かりやすいよう、Lch に対し90度、Rch に対し270度のデータを掛けています。つまり両耳真横方向に L/R それぞれの音源があるとしています。

今回の設定


自分の HRTF と合っていれば左右に大きく広がるはずです。(DB としては頭の中心から距離 1.5 m )

ただ、OFF の時と比べ、音像があまり上下前後に移動したり音質が変わっていれば、それは自分の HRTF と正確には合っていないということになります。

イヤホンの特性や装着具合によっても聞こえ方はかなり変わります。

カナル型の場合、音がなるべく耳奥に真っ直ぐ届くようなイメージで調整すると良いと思います。

音が耳壁にすぐぶつかるような装着をすると音が広がりにくくなります。
耳穴は水平ではありませんし、大きさも形も左右で違います。

音を聞きながら、色々試してみてください。

一番広がりを感じやすい装着方法が、普段のリスニングにおいてもイヤホンの性能が一番発揮される傾向があると思います。

AudiiSion EP はアルゴリズム非公開のため処理済みを入れていますが、元々 2ch ステレオ音源に対しそのまま掛ければ良いように作ってあるので処理条件としては同じです。

個人の HRTF と合わない限り、恐らく AudiiSion EP の方が音質変化が小さく、音像も上ずらず、左右の広がりも大きく感じるとは思います。

また、今回のように音源がマルチトラックではなく通常の 2ch ステレオの場合、HRTF では音像がセンターと左右端に分かれて聞こえやすいのに対し、AudiiSion EP ではその中間定位も滑らかに再現されるのが特徴です。

さらに処理量も AudiiSion EP はかなり少なく、コーダーレイテンシーも PCM 1 サンプル分 = 約 0.02 ms です。


VST プラグイン・スクリプト

全スクリプトを以下に示します。

HRTF DB や 曲データは添付しませんので、各自ご用意ください。
非商用であれば、自由に改変してお使いください。

もし使用される場合は、この記事へのリンクを貼っていただけると喜びます。

>> generate -exe HRTF_ASEP_Listening

とするとスタンドアローンプラグインが生成されます。

オプションを省略、または -vst とすると VST2 プラグイン、
-vst3 で VST3 プラグイン、
macOS では -au で AUv2、-auv3 で AUv3 プラグインが生成されます。


classdef HRTF_ASEP_Listening < audioPlugin
   properties
       HRTFGain = -5.5;  % dB
       HRTF_ASEP = 'HRTF';
       SourceSel = 'Thunderstorm';
       HRTFDB = '001';
       sw = true;
       play = false;
       copyRights = 'http://www.riec.tohoku.ac.jp/';
       Fs = '48 kHz';
       input = 'internal';
   end

   properties (Access = private)
       songs = {'Thunderstorm', 'Botchan-PillowBook', 'Car','Let It Flow','You Make Me Feel Good'};
       src1, src2, src3, src4, src5
       src1e, src2e, src3e, src4e, src5e  % ASEP processed
       srcBuf
       mPointer = 1;
       HR_fir_LL, HR_fir_RL, HR_fir_LR, HR_fir_RR  % for HRTF processing
       RIEC_hrirLL, RIEC_hrirRL, RIEC_hrirLR, RIEC_hrirRR  % all HRIRs

   end

   properties (Constant)
       PluginInterface = audioPluginInterface( ...
           'PluginName','HRTF-ASEP Listening',...
           'VendorName', 'VendorName:AudiiSion Sound Lab.', ...
           'VendorVersion', '0.0.1', ...
           'UniqueId', 'hiro',...
           audioPluginParameter('sw','DisplayName','ON', 'Mapping',{'enum','OFF','ON'},'Layout', [2 1]), ...
           audioPluginParameter('input', 'DisplayName','Input', 'DisplayNameLocation','none', ...
           'Mapping',{'enum','internal', 'enternal'}, 'Layout', [5 1], 'Style','Dropdown'), ...
           audioPluginParameter('HRTF_ASEP', 'DisplayName','HRTF / ASEP', 'DisplayNameLocation','none', ...
           'Mapping',{'enum','HRTF', 'AudiiSion EP'}, 'Layout', [1 3], 'Style','Dropdown'), ...
           audioPluginParameter('SourceSel', 'DisplayName','Audio Source', ...
           'Mapping',{'enum','Thunderstorm', 'Botchan-PillowBook', 'Car','Let It Flow','You Make Me Feel Good'}, ...
           'DisplayNameLocation','none', 'Layout', [2,3], 'Style','Dropdown'), ...
           audioPluginParameter('HRTFDB', 'DisplayName','HRTF DB', 'Mapping',{'enum', ...
           '001','002','003','004','005','006','007','008','009','010', ...
           '011','012','013','014','015','016','017','018','019','020', ...
           '021','022','023','024','025','026','027','028','029','030', ...
           '031','032','033','034','035','036','037','038','039','040', ...
           '041','042','043','044','045','046','047','048','049','050', ...
           '051','052','053','054','055','056','057','058','059','060', ...
           '061','062','063','064','065','066','067','068','069','070', ...
           '071','072','073','074','075','076','077','078','079','080', ...
           '081','082','083','084','085','086','087','088','089','090', ...
           '091','092','093','094','095','096','097','098','099','100', ...
           '101','102','103','104','105'}, ...
           'Layout',[2,2], 'Style','Dropdown', 'DisplayNameLocation','above'), ...
           audioPluginParameter('HRTFGain', ...
           'DisplayName','HRTF Gain', 'Label','dB','Mapping',{'lin', -12, 12}, ...
           'Filmstrip','knobs_60x60_241.png', 'FilmstripFrameSize',[60,60], ...
           'Layout',[4,2], 'Style','rotary'), ...
           audioPluginParameter('play','DisplayName','PLAY/STOP', 'DisplayNameLocation','none', ...
           'Mapping',{'enum','STOP','PLAY'},'Layout', [4 3], 'Style','vrocker', ...
           'Filmstrip','stop_play_60x60.png', 'FilmstripFrameSize',[60,60]), ...
           audioPluginParameter('copyRights', 'DisplayName','HRTF DB from', 'DisplayNameLocation','above', ...
           'Mapping',{'enum','http://www.riec.tohoku.ac.jp/', 'pub/hrtf/index.html'}, ...
           'Layout', [7 2], 'Style','Dropdown'), ...
           audioPluginParameter('Fs', 'DisplayName','Fs (Fixed)', 'DisplayNameLocation','above', ...
           'Mapping',{'enum','48 kHz','---'}, 'Layout', [7 3], 'Style','Dropdown'), ...
           audioPluginGridLayout('RowHeight', [25 25 10 100 20 20 20], 'ColumnWidth', [100 120 120], ...
           'RowSpacing',15), ...
           'BackgroundImage', 'logo_105_50.png','BackgroundColor',[1 1 1]);
   end

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

            plugin.HR_fir_LL = dsp.FIRFilter([1, zeros(1,511)]);  % L -> L ear
            plugin.HR_fir_LR = dsp.FIRFilter(zeros(1,512));  % L -> R ear
            plugin.HR_fir_RL = dsp.FIRFilter(zeros(1,512));  % R -> L ear
            plugin.HR_fir_RR = dsp.FIRFilter([1, zeros(1,511)]);  % R -> R ear

            RIEC = coder.load('RIEC_HRIRs_2.mat');
            plugin.RIEC_hrirLL = RIEC.hrirLL;   plugin.RIEC_hrirRL = RIEC.hrirRL;
            plugin.RIEC_hrirLR = RIEC.hrirLR;   plugin.RIEC_hrirRR = RIEC.hrirRR;


            HRTFDBnum = str2double(plugin.HRTFDB);
            plugin.HR_fir_LL.Numerator = plugin.RIEC_hrirLL(HRTFDBnum,:); 
            plugin.HR_fir_LR.Numerator = plugin.RIEC_hrirLR(HRTFDBnum,:);
            plugin.HR_fir_RL.Numerator = plugin.RIEC_hrirRL(HRTFDBnum,:);
            plugin.HR_fir_RR.Numerator = plugin.RIEC_hrirRR(HRTFDBnum,:);

            y = coder.load('thunderstorm_R_Org_48k.mat');  % src
            plugin.src1 = y.src;
            y = coder.load('Bocchan-Makuranosoushi_s10_Org_48k.mat');  % src
            plugin.src2 = y.src;
            y = coder.load('Car_Org_48k.mat');  % src
            plugin.src3 = y.src;
            y = coder.load('LetItFlow_17s_org_48k.mat');  % src
            plugin.src4 = y.src;
            y = coder.load('YouMakeMeFeelGood_31s_Org_48k.mat');  % src
            plugin.src5 = y.src;
            
            y = coder.load('thunderstorm_R_48k_230629A08_16b.mat');  % src
            plugin.src1e = y.src;
            y = coder.load('Bocchan-Makuranosoushi_10s_48k_230629A08_16b.mat');  % src
            plugin.src2e = y.src;
            y = coder.load('Car_48k_230629A08_16b.mat');  % src
            plugin.src3e = y.src;
            y = coder.load('LetItFlow_17s_48k_230629A08_16b.mat');  % src
            plugin.src4e = y.src;
            y = coder.load('YouMakeMeFeelGood_31s_48k_230629A08_16b.mat');  % src
            plugin.src5e = y.src;

            plugin.srcBuf = dsp.AsyncBuffer;
            % initialize
            write(plugin.srcBuf,[0 0;0 0]);
            read(plugin.srcBuf,2);
            
            reset(plugin)
       end  % constructor


       function set.play(plugin,val)
           plugin.play = val;
           plugin.mPointer = 1;  %#ok
           reset(plugin.srcBuf)  %#ok
       end

       function set.SourceSel(plugin,val)
           plugin.SourceSel = val;
           plugin.mPointer = 1;  %#ok
           reset(plugin.srcBuf)  %#ok
       end

       function set.HRTFDB(plugin,val)
           plugin.HRTFDB = val;
           HRTFDBnum = str2double(plugin.HRTFDB);
           plugin.HR_fir_LL.Numerator = plugin.RIEC_hrirLL(HRTFDBnum,:);  %#ok
           plugin.HR_fir_LR.Numerator = plugin.RIEC_hrirLR(HRTFDBnum,:);  %#ok
           plugin.HR_fir_RL.Numerator = plugin.RIEC_hrirRL(HRTFDBnum,:);  %#ok
           plugin.HR_fir_RR.Numerator = plugin.RIEC_hrirRR(HRTFDBnum,:);  %#ok
           reset(plugin.HR_fir_LL),  reset(plugin.HR_fir_LR)  %#ok
           reset(plugin.HR_fir_RL),  reset(plugin.HR_fir_RR)  %#ok
           reset(plugin.srcBuf)  %#ok
       end


       function out = process(plugin,in)
           out = in;  % initialize for code gen

           samples = length(in);

           switch plugin.SourceSel
               case plugin.songs{1}
                   mLength = min(length(plugin.src1),length(plugin.src1e));
               case plugin.songs{2}
                   mLength = min(length(plugin.src2),length(plugin.src2e));
               case plugin.songs{3}
                   mLength = min(length(plugin.src3),length(plugin.src3e));
               case plugin.songs{4}
                   mLength = min(length(plugin.src4),length(plugin.src4e));
               case plugin.songs{5}
                   mLength = min(length(plugin.src5),length(plugin.src5e));
               otherwise
                   mLength = min(length(plugin.src1),length(plugin.src1e));
           end
          
           mEnd = plugin.mPointer + samples;
           if mEnd > mLength  % loop play
               plugin.mPointer = 1;  % rewind
               mEnd = plugin.mPointer + samples;
           end

           if (strcmp(plugin.HRTF_ASEP,'AudiiSion EP') && plugin.sw)  % play processed
               switch plugin.SourceSel
                   case plugin.songs{1}
                       write(plugin.srcBuf, plugin.src1e(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{2}
                       write(plugin.srcBuf, plugin.src2e(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{3}
                       write(plugin.srcBuf, plugin.src3e(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{4}
                       write(plugin.srcBuf, plugin.src4e(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{5}
                       write(plugin.srcBuf, plugin.src5e(plugin.mPointer:mEnd,1:2));
                   otherwise
                       write(plugin.srcBuf, plugin.src1e(plugin.mPointer:mEnd,1:2));
               end
           else  % play non-processed
               switch plugin.SourceSel
                   case plugin.songs{1}
                       write(plugin.srcBuf, plugin.src1(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{2}
                       write(plugin.srcBuf, plugin.src2(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{3}
                       write(plugin.srcBuf, plugin.src3(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{4}
                       write(plugin.srcBuf, plugin.src4(plugin.mPointer:mEnd,1:2));
                   case plugin.songs{5}
                       write(plugin.srcBuf, plugin.src5(plugin.mPointer:mEnd,1:2));
                   otherwise
                       write(plugin.srcBuf, plugin.src1(plugin.mPointer:mEnd,1:2));
               end
           end

           plugin.mPointer = mEnd + 1; 


           if (strcmp(plugin.HRTF_ASEP,'AudiiSion EP') || (strcmp(plugin.HRTF_ASEP,'HRTF') && strcmp(plugin.input,'internal')) )
               in = read(plugin.srcBuf,samples);
           end

           if (strcmp(plugin.HRTF_ASEP, 'HRTF') && plugin.sw)
               LL = plugin.HR_fir_LL(in(:,1));  LR = plugin.HR_fir_LR(in(:,1));
               RL = plugin.HR_fir_RL(in(:,2));  RR = plugin.HR_fir_RR(in(:,2));
               out = [LL + RL, LR + RR] * plugin.play * db2mag(plugin.HRTFGain);
           else
               out = in * plugin.play;
           end
       end  % process

       function reset(plugin)
           reset(plugin.HR_fir_LL),  reset(plugin.HR_fir_LR)
           reset(plugin.HR_fir_RL),  reset(plugin.HR_fir_RR)
           reset(plugin.srcBuf)
           plugin.mPointer = 1;
       end

   end  % method
end  % classdef


かなり無駄な記述があるように見えるかもしれませんが、MATLAB 動作時と違いコード生成時は基本的に動的変数がサポートされず、サイズを固定する必要があるのでこのようになっています。

audioTestbench でのデバッグ時も、MATLAB 動作であれば通常の自由度の高い記述でも動作しますが、コード生成でエラーとなります。

GUI に関しても、内部から動かすことはできません。あくまでも GUI 上で操作する必要があります。

audioTestbench での MATLAB 動作では内部で GUI プロパティ変数を変えると GUI の表示も変わりますが、コード生成後は変化しません。内部で変えてしまわないよう気をつけてください。

(リセット後の)初期状態だけは、最初の宣言で決めることができます。

最後の状態は設定ファイルに保存されるので、再起動時はその状態となります。

初期状態に戻したい場合は Options から Reset してください。

単純なフレーム単位の処理ではありますが、コード生成に関する制限を回避するため、一旦非同期 FIFO を通して処理しています。

もう少し簡単に書けるやり方があるかもしれませんが、FIFO を入れておいた方が後々別の処理を入れたくなったときにも変更が少なく済むと思います。

FIFO の使い方は

辺りをご参照ください。


HRTF DB の enum 設定は105個あるので書くのが大変ですが、そういうのはスクリプトを組んで、fprintf で生成して貼り付けています。

PLAY/STOP ボタンと HRTF Gain knob は、MATLAB 標準のではなく、'FilmStrip' で指定しています。

変化分のフレーム画像を縦か横に並べた画像を用意すればそれを使って表示することができます。

stop_play_60x60.png

Photoshop で自分としてはがんばって作ってみました。

Knob は、スムーズに動かすには1度刻みくらいの画像が必要で、今回は241枚使っています。

knobs_60x60_241.png


PLAY/STOP 同様元画像は Photoshop の Generative Fill で生成したものに手を加えて使っています。

作業中の Photoshop Layer

Photoshop で241枚作ってつなぎ合わせるのは大変なので、ベース画像とマーカー画像を別に出力し、Simulink でマーカーだけ回転させて合成、つなぎ合わせて透過PNG として書き込み、をやっています。

Simulink モデル

Simulink モデル(R2023a)

実行には、Image Processing ToolboxComputer Vision Toolbox が必要です。


ただ Simulink では透過 PNG の透明度を扱えないようなので、合成時には手動でレベル調整、書き込みは MATLAB Function でやっています。MATLABでは、透過PNG の透明度の読み書きができます。

function fcn(knobs, en)
    coder.extrinsic('imread','imwrite')
    if en
        [~,~,alpha] = imread("knob_60x60_bg.png");
        alphas = repmat(alpha,1,241);
        imwrite(knobs, "knobs_60x60_241.png",'Alpha',alphas)
        disp('Completed')
    end
end

Write Transparent PNG ブロック(MATLAB Function)


最下段の二つの ComboBox は、MATLAB の VST にはテキストボックスがないので、表示用に使っています。

一行に32文字以内しか表示できません。


あとがき

今まで、MATLAB の VST でこういうことができることに気付いていませんでした。

これで、ちょっとした試聴アプリをサクッと作ることができるので便利そうです。(u_u)

もうちょっと自由度を高めてくれると、さらに便利なんですけどね。(¬_¬)


タイトル画像モデル:綾夏
The title image was created using Adobe Generative Fill based on this picture.

Original Image


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