はじめに
みなさん、テルミンという楽器を知っていますか?正弦波を生成する電子機器を用いて音を鳴らす楽器で、発明者のロシア人物理学者から命名されました。
これは空中に手を置いて、テルミン自身と演奏者の手の距離よって音程の違う音を鳴らす楽器です。今回はコレをLeapMotionで作る過程を通してLeapMotionの扱い方とUnityでの音の動的生成について簡単に学んでみたいと思います。完成品はこんな感じ。
仮想テルミン(withLeapMotion) pic.twitter.com/fljpDZmm1r
— 避雷 (@lucknknock) 2019年2月1日
制作過程
※LeapMotion自体の動作確認は出来ている前提で進めます。新バージョンのLeapMotionSDKの知見全然なくて渋い…
LeapMotionSDKの準備
こちらからDOWNLOAD UNITY CORE ASSETSをダウンロードしてUnityにインポートします。
動作を確認してみる
Assets/LeapMotion/Core/Examples/CapsuleHands(Desktop)を開いて実行してみましょう。ちゃんと手がトラッキングされればインポートは成功です。今回はCapsuleHands(Desktop)のシーンを書き換えてテルミンを作ってみます。
任意の波長の正弦波を生成する
Unityには出力される音にフィルタを掛ける機能(AudioFilter)がデフォルトでついています。このAudioFilterに入力される音をガン無視して自分で正弦波を生成して出力側に回せば任意の波長の正弦波を出力することができます。
(参考:Unityでサイン波とか使って音を鳴らしたい!)
using UnityEngine;
using System; // Needed for Math
public class Sinus : MonoBehaviour
{
public double frequency = 440;
public double gain = 0.05;
private double increment;
private double phase;
private double sampling_frequency = 48000;
void OnAudioFilterRead(float[] data, int channels)
{
increment = frequency * 2 * Math.PI / sampling_frequency;
for (var i = 0; i < data.Length; i = i + channels)
{
phase = phase + increment;
data[i] = (float)(gain*Math.Sin(phase));
if (channels == 2) data[i + 1] = data[i];
if (phase > 2 * Math.PI) phase = 0;
}
}
}
このスクリプトをAudioListerコンポネントが付いているのと同じオブジェクト(通常はMainCamera)にアタッチします。試しに実行してみるときちんと正弦波が出力されていることがわかると思います。ピーーーーーって音が聞こえれば成功です。
手とLeapMotionの距離を測定する
今回はpalmのlocalPositionを使ってLeapMotionと手の距離を取得します。左右の手のpalmのlocalPositionの座標系はLeapMotionの座標を原点に設定されているのでこれのy座標がLeapMotionと手の距離になります。
距離に対応して正弦波の波長を変える
先ほどの正弦波生成のスクリプトのfrequencyの部分に右手とLeapMotionの距離に応じた倍率を掛ければ正弦波の波長を変えることができます。因みに440Hzは基準周波数といわれる波長です。frequencyMultiplyの値が倍率なのですが、SG.frequencyMultiply = palmPositionR.localPosition.y * 10f - 0.5f
ぐらいにするといいでしょう。(-0.5fしたのはあまり手をLeapMotionの近くに持っていくとトラッキングが外れてしまうからです)
using UnityEngine;
using System; // Needed for Math
public class SoundGenerator : MonoBehaviour
{
public double frequency = 440;
private double increment;
private double phase;
private double sampling_frequency = 48000;
public float frequencyMultiply = 1;
public float gainMultiply = 1;
void OnAudioFilterRead(float[] data, int channels)
{
increment = frequency * frequencyMultiply * 2 * Math.PI / sampling_frequency;
for (var i = 0; i < data.Length; i = i + channels)
{
phase = phase + increment;
data[i] = (float)(gain * Math.Sin(phase));
if (channels == 2) data[i + 1] = data[i];
if (phase > 2 * Math.PI) phase = 0;
}
}
}
距離に対応して正弦波の音量を変える
音響出力の際のGainの部分に左手とLeapMotionの距離に応じた倍率をかけてやれば正弦波の音量を変えることができます。(GainよりVolumeを弄った方が良さげ?)SG.gainMultiply = (palmPositionL.localPosition.y * 100f - 10f) * 0.05f;
ぐらいにしておけばいいでしょう。
using UnityEngine;
using System; // Needed for Math
public class SoundGenerator : MonoBehaviour
{
public double frequency = 440;
public double gain = 0.05;
private double increment;
private double phase;
private double sampling_frequency = 48000;
public float frequencyMultiply = 1;
public float gainMultiply = 1;
void OnAudioFilterRead(float[] data, int channels)
{
increment = frequency * frequencyMultiply * 2 * Math.PI / sampling_frequency;
for (var i = 0; i < data.Length; i = i + channels)
{
phase = phase + increment;
data[i] = (float)(gain * gainMultiply * Math.Sin(phase));
if (channels == 2) data[i + 1] = data[i];
if (phase > 2 * Math.PI) phase = 0;
}
}
}
トラッキングが外れたら音を出さないようにする
トラッキングが外れた状態だと不安定でいきなり音が鳴ったりしてアレなのでトラッキングする手がないときには音を鳴らさないようにします。
RightRoundHand_LとRightRoundHand_Rのアクティブ状態で認知するといいでしょう。
例えば
右手のみトラッキング出来ているときは一定の音量で鳴らす
左手のみトラッキング出来ているときは鳴らさない
両手ともトラッキング出来ていないときは音を鳴らさない
といった具合にするといいでしょう。
手の形状を認識する
最後に手を握るととっさに音を止められるようにしてみます。
手の握り具合はControllerから取得できます。ControllerからFrameを取得し、さらにそこから手のリストを生成してそれらの握り具合からgrabAmountを取得しています(握っているときが0、開いているときが1)。これをgainMultiplyに掛けてやれば手を握ったときに音がスッと止まる仕組みが作れます。
using Leap;
-----------------初期化
private Controller controller = new Controller();
-----------------
void OnApplicationQuit(){
if (controller != null)
{
controller.StopConnection();
controller.Dispose();
controller = null;
}
}
-----------------ここから下はUpdate内で呼び出す
Frame frame = controller.Frame();
float grabAmount = 0;
if (frame.Hands.Count > 0)
{
List<Hand> _hands = frame.Hands;
Hand[] hands = _hands.ToArray();
grabAmount = 1 - Mathf.Max(Array.ConvertAll(hands,new Converter<Hand,float>(i => i.GrabStrength)));
Debug.Log(grabAmount);
}
最後に
スイマセン、なんちゃってテルミンです。実用には及ばない気がします。テルミンの音の出方をもっと正確に把握している人がいたら連絡ください。音周りの勉強したい…