# JUCEでエフェクターを作って遊ぼう:02 こちらの記事は デジクリ Advent Calender 2022 11日目の記事です。デジクリとは何ぞや、という方は[こちらのページ](https://digicre.net)をご覧ください。 ## はじめに こんにちは。5日ぶりのbayashiです。 [前回の記事](https://hackmd.io/@bayashi/BkbvY21Dj)ではほとんどJUCEの説明で終わってしまいましたが、今回は実際に作ったプラグインについて書きたいと思います。GUIの実装、イベント管理といった音の処理以外の部分は前回の記事と同じ要領で実装しているので割愛し、どんな音がするエフェクトを作ったのかについて中心に書いていきます。ノリとしては「試作品の紹介」に近いので、そういった感覚で眺めていただけると助かりますm(__)m ## 本題の前に : 訂正のお知らせ 前回のソースコードについて、なんとなく入力された音のデータが`float`型だと思って処理に関わる変数を`float`型で定義していましたが、実際にはソフトによって64ビット浮動小数点(つまり `double`)をサポートしている場合があることを思い出しました。JUCEフレームワークとしては`float`も`double`も両方サポートしているとのことなので、とりあえず変数の型を`double`に統一することとしました。 前回分のコードについては更新済みです。 ## リサージュメーターを眺めて 普段楽曲制作をする上で一応確認しているメーター類がいくつかあるのですが、そのうちの一つにリサージュメーターというものがあります。 何を見るためのメーターかというと、ステレオのLチャンネルとRチャンネルがどの程度一致しているのかを確認するためのもので、このような見た目をしています。(画像はOzone Imagerのもの) ![](https://i.imgur.com/P2W5lTQ.png) この青いもやもやは何かというと、ある時刻の音の振幅について、LチャンネルとRチャンネルを下記のような2次元平面上にプロットしたものです。 ![](https://i.imgur.com/K34YmdH.jpg) (実際には端点が常に±1となるわけではなく、最大振幅に合わせて適当にスケーリングされます) つまり、LチャンネルとRチャンネルが完全に一致していれば縦一直線に青い点が並び、正負が常に真逆なら横一直線に並び、そのどちらでもなく無関係ならおおよそ円形に青い点が分布します。 これで何が分かるのかというと、おおよそのステレオ感が分かります。細かな議論をすればキリがないのですが、リサージュメーターの横方向の分布の広がりは音の広がり感を確認する一つの目安になります。 ここで、音の広がり感を増やしたいとき、普通はこの分布全体を横方向に広げる処理が行われます。(Mid/Side処理と呼ばれるものです) …しかし私は素朴な疑問を持ちました。 **このひし形の領域を4分割して、それぞれ個別に音量を変えることができたら、どんな音になるのだろう** と。 例えばこんな風に、4つの領域ごとに音量を変えられたら何かしら面白い音が得られるのではなかろうか、と思ってみました。 ![](https://i.imgur.com/XErPBSG.png) ## やってみた まずは音をリサージュメーター上の4つの領域ごとに分離します。これは簡単で、LチャンネルとRチャンネルの振幅によって次のように場合分けします。 ![](https://i.imgur.com/cqWMA8S.png) 場合分けして、それぞれの音量をスライダーで変化させられるようにします。 スライダーの実装は前回とほぼ同じことをしているので割愛し、`PluginProcessor.cpp`の`processBlock()`に記述する処理はこんな感じになります。 ``` // midFoward : 上 // sideLeft : 左 // midBack : 下 // sideRight : 右 if (totalNumInputChannels == 2) { auto* channelData_L = buffer.getWritePointer (0); auto* channelData_R = buffer.getWritePointer (1); for (int sample = 0; sample < buffer.getNumSamples(); sample++) { if (channelData_L[sample] >= 0 && channelData_R[sample] >= 0) { channelData_L[sample] = channelData_L[sample] * midFoward; channelData_R[sample] = channelData_R[sample] * midFoward; } if (channelData_L[sample] > 0 && channelData_R[sample] < 0) { channelData_L[sample] = channelData_L[sample] * sideLeft; channelData_R[sample] = channelData_R[sample] * sideLeft; } if (channelData_L[sample] <= 0 && channelData_R[sample] <= 0) { channelData_L[sample] = channelData_L[sample] * midBack; channelData_R[sample] = channelData_R[sample] * midBack; } if (channelData_L[sample] < 0 && channelData_R[sample] > 0) { channelData_L[sample] = channelData_L[sample] * sideRight; channelData_R[sample] = channelData_R[sample] * sideRight; } } } ``` ## できたもの & 考察 では、出来上がったプラグインが動作する様子をご覧ください。今回は正弦波を鳴らして撮影しているため初めは聞き取りにくいかもしれませんが **できるだけ音量を絞って再生することをおすすめします。** <iframe width="560" height="315" src="https://www.youtube.com/embed/So65rqcMBPs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> 想定通り、4つの領域それぞれを個別に音量変化させることができました。しかし、**いざ鳴らしてみると音が激しく歪んでいることが確認できます。** それも、前回の記事で扱ったクリップディストーションのように心地よい歪みではなく、チリチリとしたやや不快なノイズに聞こえます。 何がいけなかったのでしょうか。というか、私がこのアイデアを思いついた時点から怪しさを感じていた方もいるかもしれません。 初めに私はリサージュメーターについて「ある時刻のLチャンネルとRチャンネルの振幅がプロットされるもの」と説明しました。 しかし、それはデジタルデータで音を扱う場合の話で、例えばアナログ電気信号を入力とする計器としてのリサージュメーターは「L信号とR信号の**振幅変化の軌跡が表示されるもの**」です。 そう考えてみると、今回のプラグインでやっていることは、**本来連続的だった振幅変化の軌跡を途中でぶった切る処理**になります。これによって波形に不自然で急激な変化をする箇所が発生し、このような歪みが発生したと考えられます。 ## ならば線形な処理は? 再びリサージュメーターを眺めてみます。 ![](https://i.imgur.com/K34YmdH.jpg) 昔、線形代数の授業で「回転行列」というものをやったな、とふと思い出しました。 (2次元の)回転行列というのは次の通りです。 $$ \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta\end{pmatrix} $$ これを座標 $x, y$ に対して次のように計算することで、そこから原点の反時計回りに $\theta$ だけ回転した後の座標 $x', y'$ を求めることができます。 $$\begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta\end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix}$$ これを書き換えると次のようになります。 $$x' = x \cos\theta - y \sin\theta$$ $$y' = x \sin\theta + y \cos\theta$$ 試しにこれをリサージュメーター上でやってみることにします。 ## 実装 ### とりあえず回してみる 実装の流れは前回記事とほぼ変わりませんが、折角なのでスライダーの見た目を円形にして、値の範囲も-180から+180にしてみます。今回使うスライダーは`rotateSlider`という変数名で宣言しているので、その見た目を決める`setSliderStyle()`という関数を次のように指定しました。 ``` rotateSlider.setSliderStyle(juce::Slider::Rotary); ``` するとスライダーの見た目はこのようになります。 ![](https://i.imgur.com/lvd8OZX.png) このスライダーは時計回りが正方向なので、$\theta$ も時計回りに回ってほしいです。そこで $x$ 座標を $L$ チャンネルに、$y$ 座標を $R$ チャンネルに置き換えて次のように変換しました。[^2] $$L' = L \cos\theta - R \sin\theta$$ $$R' = L \sin\theta + R \cos\theta$$ `PluginProcessor.cpp`の`processBlock()`には上記の式をそのまま実装します。`sin()`と`cos()`の入力は弧度法で、スライダーの入力は度数法であることに注意します。 ``` // angle : rotateSliderから入力された角度 if (totalNumInputChannels == 2) { auto* channelData_L = buffer.getWritePointer (0); auto* channelData_R = buffer.getWritePointer (1); for (int sample = 0; sample < buffer.getNumSamples(); sample++) { double rotated_L = channelData_L[sample] * cos(angle / 180.0 * 3.14159) - channelData_R[sample] * sin(angle / 180.0 * 3.14159); double rotated_R = channelData_L[sample] * sin(angle / 180.0 * 3.14159) + channelData_R[sample] * cos(angle / 180.0 * 3.14159); channelData_L[sample] = rotated_L; channelData_R[sample] = rotated_R; } } ``` では動かしてみましょう。 <iframe width="560" height="315" src="https://www.youtube.com/embed/C6ntSi5M9vw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> 確かにリサージュメーター上でぐるぐると回りました。 一応 $-45^\circ < \theta < 45^\circ$ くらいの範囲では疑似的なステレオパンニングとしても機能するのですが、全体的には **なんか不思議な空間変化** が起こりました。 ### LRチャンネルもそれぞれ回転させてみる ここまではリサージュメーター全体をぐるぐる回すことを考えてきましたが、今度はLRの軸ごと回すことを考えてみましょう。ちょっとややこしいですが、Lチャンネルだけを回転させるとき、回転後のLRチャンネルをそれぞれ $L_L', R_L'$ として、次のように書けます。 $$L_L' = L \cos\theta$$ $$R_L' = L \sin\theta$$ 同様にRチャンネルだけ回転させるとき、$L_R', R_R'$ は次のように書けます。 $$L_R' = - R \sin\theta$$ $$R_R' = R \cos\theta$$ 最後に、それぞれの回転後のLRチャンネルを加算します。[^4] $$L' = L_L' + L_R'$$$$R' = R_L' + R_R'$$ ついでに、LRの軸を回転させる処理をした後、リサージュメーター全体も回せるようにしてみましょう。 また、視覚的な分かりやすさのため、軸を回転させるスライダーの初期値は左を $-45^\circ$ 、右を $+45^\circ$ としたかったので、その分を処理時に補正しています。 ``` // angleLeft : 左チャンネルの角度、初期値は -45.0 // angleRight : 右チャンネルの角度、初期値は 45.0 // angleCenter : 全体の回転角 (上のコードの angle に相当) if (totalNumInputChannels == 2) { auto* channelData_L = buffer.getWritePointer (0); auto* channelData_R = buffer.getWritePointer (1); for (int sample = 0; sample < buffer.getNumSamples(); sample++) { double rotated_L_L = channelData_L[sample] * cos((angleLeft + 45.0) / 180.0 * 3.14159); double rotated_R_L = channelData_L[sample] * sin((angleLeft + 45.0) / 180.0 * 3.14159); double rotated_L_R = - channelData_R[sample] * sin((angleRight - 45.0) / 180.0 * 3.14159); double rotated_R_R = channelData_R[sample] * cos((angleRight - 45.0) / 180.0 * 3.14159); channelData_L[sample] = rotated_L_L + rotated_L_R; channelData_R[sample] = rotated_R_L + rotated_R_R; double rotated_L = channelData_L[sample] * cos(angleCenter / 180.0 * 3.14159) - channelData_R[sample] * sin(angleCenter / 180.0 * 3.14159); double rotated_R = channelData_L[sample] * sin(angleCenter / 180.0 * 3.14159) + channelData_R[sample] * cos(angleCenter / 180.0 * 3.14159); channelData_L[sample] = rotated_L; channelData_R[sample] = rotated_R; } } ``` 動かしたものがこちらです。 <iframe width="560" height="315" src="https://www.youtube.com/embed/pPOyOHCf6_Q" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> 軸を回転させることが出来ることで、ステレオ幅や音源の聴こえる方向について少し柔軟に設定することが出来るようになりました。例えば全体を回転させるだけではステレオ感が広がりすぎるor狭まりすぎるときに、これを使えば多少の調整を加えることが出来るかも…?と思います。 ## まとめ 今回はリサージュメーターを眺めながら着想を得たステレオ感に関わるプラグインを試作してみました。後半で実装した、リサージュメーター上の軌跡を回転させるプラグインについては、ステレオの空気感や方向感覚を柔軟に操作できる可能性を感じるものでした。 次回は、またリサージュメーター上で音を加工して遊んだプラグインの紹介と、実用的なプラグインとして動かすにあたっての技術的な話題について触れる予定です。(実は今のままだとパラメータが保存できなかったり、オートメーションが書けなかったりします。)また、`PluginProcessor.h`の「内部パラメータなのに`public`で宣言してしまっている」問題の解決も同時に触れていきます。 ## 次回予告 ![](https://i.imgur.com/3luWqX3.png) ????????????????? ([次回](https://hackmd.io/@bayashi/HyGN9tZdo)は1週間後の予定です) [^2]:回転にあたって、リサージュメーターの対角線を結ぶような成分については最大振幅が $\sqrt{2}$ を取る場合がある、つまり最大で 3dB ほど出力が大きくなる場合があることに注意が必要です。今後の課題として、回転したときに実際のLRチャンネルに出力される最大振幅が 1(0dB) となるように調整するオプションを用意する必要があるかと感じています。 [^4]:この処理によって、最終的にLまたはRチャンネルに出力される最大振幅が 2 を取る場合がある、つまり最大で 6db ほど大きくなる場合があることに注意が必要です。これについても最大振幅が 1(0dB) となるように調整するオプションを将来的に用意したいと考えています。