Quantcast
Channel: 音楽方丈記
Viewing all articles
Browse latest Browse all 339

iPhoneを左右に振ってJavaScriptで方位を取得してテルミンを演奏するサウンドWebアプリを作ってみました

$
0
0
あけましておめでとうございます。
本年も音楽方丈記をよろしくお願いします。
年々更新頻度が落ちてますが細々とがんばります。



iPhone を左右に振ってテルミンっぽいサウンドを鳴らすシンプルなサウンド Web アプリ Theremin-1 を作ってみました。

iOS 6以降の iPhone/iPod touch/iPad の Mobile Safari で動きます。
Mobile Safari 以外や Android では未確認。もしかしたら動くかもしれません。



 前に Web Audio API と加速度センサーを利用した簡単な Web アプリを作ったので、次はジオロケーションの方位を使って何か手軽にできないかと考えて思いついたのがこれです。

 iPhone を左右に振るとテルミンっぽい演奏ができるシンプルなサウンド Web アプリで、Web Audio API でサイン波(正弦波)を生成してシンプルなディレイをかけつつ、方位を取得する DeviceOrientationEvent を利用して音程を連続的に変化させるというものです。

Theremin-1 の使い方


   
←左の QR コードをタップするか、iPhone のカメラで読み取って Theremin-1 にアクセスしてください。


  • iPhone を手前に出して「Play」ボタンを押してください。
  • iPhone を左右に弧を描くように動かすと音程が連続的に変化します。
  • 調整できるパラメーターは以下の4種類です。
    • Sensitivity: 振り角に対する音程変化の度合いを調整
    • Delay Time: ディレイタイムの調整 (0~1000ms)
    • Feedback: フィードバックレベルの調整
    • Wet Level: ウェットレベルの調整(ドライ:ウェットの比率)

  振るときに iPhone がすっぽ抜けないように十分注意してください。
  (万が一 iPhone を落として壊してもこちらは責任は負いません)

プログラムの内容について

プログラムの処理内容に興味がない人はここから先はスルーしてください。

 UI 部分はお手軽な jQuery + jQuery UI の組み合わせで、タッチデバイス上のスライダーハンドルのドラッグ操作に対応するため jQuery プラグインの jquery-ui-touch-punch を利用しました。

 テーマは MEDIALOOT で配布されているカスタマイズテーマの jQuery UI Theme: Retro のフリー版を元に、さらに少しだけカスタマイズを施しました。


 あと、特に必要というわけではないのですが、画面左上のアイコンをタッチすると画面が右にスライドして QR コードと短縮 URL を表示するようにしてあります。
 この部分の動作は jQuery プラグイン mmenu を利用しました。

↓JavaScript のサウンド関連部分だけ抜粋
var context = null;
var osc = null;
var baseFreq = 880; // A5
var baseDir = 0;
var ratio = 5;
var delay = null;
var wetGain = null;
var dryGain = null;
var fbGain = null;
var delayTime = 0.3;
var fbLevel = 0.3;
var wetLevel = 0.3;
var valueMax = 50;

// Audio コンテキストの生成
try{
  var AudioContext = window.AudioContext || window.webkitAudioContext;
  context = new AudioContext();
}catch(e){
  context = null;
}

// サウンド出力開始
function playSound(){
  baseDir = 0;
  $(".slider").slider("disable");
  $("#stop").button("enable").focus();
  $("#play").button("disable");
  
  // 正弦波のオシレーター生成
  osc = context.createOscillator();
  osc.type = 0;
  osc.frequency.value = baseFreq;

  // ディレイとゲインのノード生成
  delay = context.createDelay();
  wetGain = context.createGain();
  dryGain = context.createGain();
  fbGain = context.createGain();
  
  // 各ノード間のルーティング
  osc.connect(delay);
  osc.connect(dryGain);
  delay.connect(wetGain);
  delay.connect(fbGain);
  fbGain.connect(delay);
  wetGain.connect(context.destination);
  dryGain.connect(context.destination);
 
  // ディレイパラメーターの指定
  delay.delayTime.value = delayTime;
  fbGain.gain.value = fbLevel;
  wetGain.gain.value = wetLevel;
  dryGain.gain.value = 1 - wetLevel;

  // 再生開始
  osc.noteOn(0);
}

// サウンド出力停止
function stopSound(){
  $(".slider").slider("enable");
  $("#play").button("enable").focus();
  $("#stop").button("disable");

  // 再生停止
  osc.noteOff(0);
}

// DeviceOrientationEvent のハンドラー
function deviceOrientationHandler(e){
  // compassHeading: 角度 0~360, 0=真北
  var dir = e.originalEvent.compassHeading || e.originalEvent.webkitCompassHeading || 0;

  // 相対位置に変換してから、振り角に変化倍率をかける
  if(dir > 180) dir -= 360;
  dir *= ratio;
  
  if(baseDir == 0){
    // 基準方位(Playボタンを押したときの方位)
    baseDir = dir;
  }else{
    // 出力周波数の指定:現在方位と基準方位の差を基準周波数(880Hz)に加算
    osc.frequency.value = baseFreq + (dir - baseDir);
  }
}

$(function(){
  // DeviceOrientationEvent のハンドラー指定
  $(window).on("deviceorientation", deviceOrientationHandler)
  
  // ページ遷移時(pagehide)のハンドラー指定
  $(window).on("pagehide", function(e){ stopSound(); });
  
  // Play ボタン押下イベント
  $("#play").button().click(playSound).focus();

  // Stop ボタン押下イベント
  $("#stop").button().click(stopSound).button("disable");
  
  // スライダー1: 振り角に対する音程変化の比率
  $("#ratio").slider({
    orientation: 'vertical', min: 1, max: 10, range:'min',
    value: ratio,
    slide: function(event, ui){ ratio = ui.value; }
  }).draggable();

  // スライダー2: ディレイタイム
  $("#delaytime").slider({
    orientation: 'vertical', min: 0, max: valueMax, range:'min',
    value: delayTime * valueMax,
    slide: function(event, ui){ delayTime = ui.value / valueMax; }
  }).draggable();

  // スライダー3: フィードバック
  $("#feedback").slider({
    orientation: 'vertical', min: 0, max: valueMax, range:'min',
    value: fbLevel * valueMax,
    slide: function(event, ui){ fbLevel = ui.value / valueMax; }
  }).draggable();

  // スライダー1: Dry/Wet の比率
  $("#wetlevel").slider({
    orientation: 'vertical', min: 0, max: valueMax, range:'min',
    value: wetLevel * valueMax,
    slide: function(event, ui){ wetLevel = ui.value / valueMax; }
  }).draggable();

});

デバイス方向のイベント DeviceOrientationEvent

iOS はバージョン 5 からデバイスの傾きや方位の値を読み出すことができる DeviceOrientationEvent が追加されていて、Safari の JavaScript からイベントハンドラを指定してリアルタイムに現在値を取得することができます。

 » Safari Developer Library - DeviceOrientationEvent Class Reference

window.addEventListener("deviceorientation", function(e){
  var z = e.alpha;        // z軸回転量
  var x = e.beta;          // x軸回転量
  var y = e.gamma;     // y軸回転量
  var heading  = e.webkitCompassHeading;   // 方位 0=真北~360
  var accuracy = e.webkitCompassAccuracy; // 方位の誤差
});

// jQuery の .on(), .bind() で使う場合はプロパティの前に originalEvent を付加
$(window).on("deviceorientation", function(e){
  var heading = e.originalEvent.webkitCompassHeading;
});

 Theremin-1 では方位 (webkitCompassHeading) だけを利用しています。返される値は 0~360で真北が0になります。

 最初は DeviceOrientationEvent を知るより先に Geographic Location API の navigator.geolocation.watchPosition() が返すパラメーターに現在の方位を返す coords.heading (0~360)があるのを見つけていて、使えるだろうと思っていざ組み込んで試してみたら、ある程度移動しながらでないと現在方位が変化しないことが分かったので、今回の用途には使えないことが分かりました。

 iOS 標準のマップやコンパスアプリ、他社製の地図アプリでも位置を移動しなくても方位を常時取得して表示しているものがあるので、おそらく別にコンパス API みたいなものがあるんだろうと調べて見つけたのが、DeviceOrientationEvent で、試してみると compassHeading で簡単に現在方位を取得することができました。

Mobile Safari の Web Audio API

iOS6 以降の Mobile Safari から実装された Web Audio API を使用しています。

 Safari Developer Library - Playing Sounds with the Web Audio API

↓ サウンド出力部分だけ抜粋
function playSound(){
  
  // 正弦波のオシレーター生成
  osc = context.createOscillator();
  osc.type = 0;
  osc.frequency.value = baseFreq;  // 周波数初期値

  // ディレイとゲインのノード生成
  delay = context.createDelay();
  wetGain = context.createGain();
  dryGain = context.createGain();
  fbGain = context.createGain();
  
  // 各ノード間のルーティング
  osc.connect(delay);
  osc.connect(dryGain);
  delay.connect(wetGain);
  delay.connect(fbGain);
  fbGain.connect(delay);
  wetGain.connect(context.destination);
  dryGain.connect(context.destination);
 
  // ディレイパラメーターの指定
  delay.delayTime.value = delayTime;
  fbGain.gain.value = fbLevel;
  wetGain.gain.value = wetLevel;
  dryGain.gain.value = 1 - wetLevel;

  // 再生開始
  osc.noteOn(0);
}

» ノードの接続チャート


 ノードの接続チャートを見ると分かるように構造はいたってシンプルです。

 正弦波のオシレーターを生成して、1つはそのままドライ出力の Gain ノードから Web Audio API の終端入力 (context.destination) へ、もう1つは Delay ノードに渡します。

 Delay ノード単体では、入力したシグナルをディレイタイムで設定した時間(ms単位)だけ遅らせて出力するだけの機能しかないため、フィードバック回路は自分で作ってやる必要があります。

 フィードバック回路は Delay ノードからフィードバック用の Gain ノードへ入力して、指定ゲイン値に下げた後で Delay ノードへ出力を戻してやります。
 ここをぐるぐる回ることでフィードバックの出力が段々小さくなっていって、ディレイエコー独特の減衰効果が生成されます。

 ディレイから出力されるシグナルは、ウェット出力の Gain ノードで指定したウェットレベルのゲイン値に調整されてから終端 (context.destination) へ渡されます。
 今回はパラメーターを簡略化するために、ドライレベルの出力ゲインは、最大値(1) からウェットゲインを引いた残りの値を設定しています。

 オシレーターノードは noteOn() を呼んで再生を開始すると noteOff() を呼ぶまで鳴りっぱなしになり、その間にオシレーター周波数のパラメーター値 (frequency.value) を変更してやると、すぐに出力に反映される仕様になっています。

 今回は方位を取得する DeviceOrientationEvent のハンドラー内で現在方位からオシレーター周波数 (frequency.value) を決定しているので、方位が変わるたびに音程が連続的に変化していきます。

その他 Tips

 スライダーのようなドラッグ対応のコントロールを操作していると、まれに意図せずテキストの範囲選択状態になって鬱陶しいので、CSS に以下の宣言を追加して Mobile Safari の範囲選択を無効にしました。
* {
  -webkit-user-select: none;
}

 あと、縦にスライダーを配置すると、1画面に収まっているページの場合に上下スクロールの押し戻しがスライダーのドラッグ操作を邪魔するので touchmove イベントを無効にしてみました。
// HTML の場合
<body ontouchmove="event.preventDefault()">

// jQuery の場合
$(window).on("touchmove", function(e){ e.preventDefault(); });


面白いサウンド Web アプリを思いついたらまた公開したいと思います。

試すときはくれぐれも iPhone を落とさないように注意してください。

[関連サイト]
 jQuery
 jQuery UI
 jquery-ui-touch-punch
 MEDIALOOT - jQuery UI Theme: Retro
 mmenu
 Web Audio API
 Safari Developer Library


Moog Theremini 【並行輸入】

Moog Theremini 【並行輸入】

  • 出版社/メーカー: Moog
  • メディア:
テルミン―エーテル音楽と20世紀ロシアを生きた男

テルミン―エーテル音楽と20世紀ロシアを生きた男

  • 作者: 竹内 正実
  • 出版社/メーカー: 岳陽舎
  • 発売日: 2000/08
  • メディア: 単行本



Viewing all articles
Browse latest Browse all 339

Trending Articles