Lynx-EyEDの電音鍵盤 新館

広帯域制御屋の駄文とか

PhoneGapからiOSデバイスのハードウェアボリュームを制御する

前回の続きになります。
有線でiOSデバイスに信号を送るのに手っ取り早い方法は、音声信号にデコードした情報をマイク入力で送るか、リモコンで情報を送る方法だと思います。
あとは送信された音声データや音量レベルをデコードするアプリを書けば良いのです。

HTML5にはmedia要素にvolumechangeイベント、またWeb Audio APIにもAudioGainNode.gain.valueが存在し、音量の調整・読み込みが出来るのですが、試したところ音源の音量を調整・検出するのみでハードウェアボリュームとは連動しませんでした(って、当たり前か…)

参考:Getting Started with Web Audio API - HTML5 Rocks

なのでPhoneGapでもHTML+JSのみでは無理でObjective-Cでネイティブプラグインコードを書いてやる必要があります。*1

f:id:Lynx-EyED:20140221225951p:plain

プラグインの導入

iOSネイティブプラグインの作り方:PhoneGap API Documentation

読んでてクッソめんどくさいなとおもって諦めかけてたところこんなものを発見

devgeeks/VolumeSlider · GitHub
コードを見るとMPVolumeViewクラスがラッピングされている事が分かります。
このプラグインを自分のプロジェクトに導入します。ターミナル.appで自分のプロジェクトディレクトリに移動し、

phonegap local plugin add https://github.com/devgeeks/VolumeSlider.git

これで、プラグインを追加した際のxmlの設定など、煩雑な事を全部やってくれます。
あとは適宜必要なコードを追加するだけです。

ボリュームコントロールするコードの追記

VolumeSliderプラグインはスライダを初期化、UIに表示、非表示する機能のみなので、ハードウェアボリュームの値を読む、制御するObj-Cコードを追記します。

  • 音量レベルのリード例
float value = [[MPMusicPlayerController applicationMusicPlayer] volume]; 

valueは0(最小)~1(最大音量)の範囲になります。

  • 音量レベルの指定例
[MPMusicPlayerController applicationMusicPlayer].volume = 0.5f;

以上をふまえてプロジェクトの/Plugins の中にVolumeSlider.mにコードを追記しました。
なおディレクトリ階層は前回のプロジェクトの続きに作っているので以下の様に見えています。
f:id:Lynx-EyED:20140221231543p:plain

//VolumeSlider.mに追記

- (void)getVolumeSlider:(CDVInvokedUrlCommand *)command
{
    
    NSArray* arguments = [command arguments];
    self.callbackId = command.callbackId;
   
    NSString *resultType = [arguments objectAtIndex:0];
    
    float value = [[MPMusicPlayerController applicationMusicPlayer] volume];  // return value for example
    CDVPluginResult *result;
    
    
    if ( [resultType isEqualToString:@"level"] ) {
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:value];
        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
    }
    else {
        [MPMusicPlayerController applicationMusicPlayer].volume = 0.5f;
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Changed!"];
        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
    }
}
//VolumeSlider.hに追記

- (void)getVolumeSlider:(CDVInvokedUrlCommand *)command;

次に上記ネイティブ関数をコールするexec()関数をJSで記述します。exec()関数は以下の文法になります

exec( resultHandler, errorHandler, native_class, native_function, [resultType]);

PhoneGapは上記の関数によってネイティブコードをコールします。
exec()関数は、resultHandler、errorHandler、呼び出すネイティブクラス(native_class)、ネイティブ関数への参照(native_function)、ネイティブコードに渡されるパラメーターの配列([resultType])をネイティブ関数へ引き渡します。

先ほど書いたObj-Cのコード(VolumeSlider.m / .h)はexec関数からコールされる関数です。今回のコードではJSからネイティブへ"level"という文字列とともにコールされた場合はresultHandlerにボリュームレベルを戻り値としてコールバックしています。

result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:value]; // valueには音量レベルが代入されている
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];

また、それ以外の文字列ならば、ボリュームレベルを0.5にして"Changed!"という文字列を戻り値としてerrorHandlerをコールバックしています。

     [MPMusicPlayerController applicationMusicPlayer].volume = 0.5f;
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Changed!"];
        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
  • JSの修正

/www配下のVolumeSlider.jsを探します。
f:id:Lynx-EyED:20140221233022p:plain

VolumeSlider.jsに以下を追記します。

module.exports = {

         //中略
        ,  // <-前の関数のあとにカンマを付けるのを忘れずに
               
	getVolumeSlider : function (success,fail,resultType) {
        	return exec(success, fail,
                    "VolumeSlider","getVolumeSlider",[resultType]);
    	}
};

次に、index.htmlのscriptにハンドラを記述します

            </script>
                var volumeSlider = null;
                var volLevel = null;
// 中略
                volumeSlider = window.plugins.volumeSlider;
// 中略
            function getVolume() {
                volumeSlider.getVolumeSlider(VSLevelHandler, VSControlHandler, "level" );
                return volLevel;
            }
            
            function resetVolume() {
                volumeSlider.getVolumeSlider(VSLevelHandler, VSControlHandler, "reset" );
            }
            
            function VSLevelHandler (result) {
                volLevel = result;
            }
            
            function VSControlHandler (error) {
                document.getElementById("res").innerHTML = "result=" + error;
            }
            
            </script>

index.html全文

そして前回の記事で作ったプロジェクトに追記したindex.html全文です

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no;" />
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
            <style>
                .btn {
                    font-family: Arial, Helvetica, sans-serif;
                    font-size: 28px;
                    color: #fff;
                    padding: 12px 18px;
                    
                    background: #8893da;
                    background: -webkit-gradient(
                    linear, left top, left bottom,
                    from(#8893da),
                    to(#344c77)
                    );
                    
                    border-radius: 10px;
                    
                    -webkit-box-shadow:
                    12px 12px 12px rgba(000, 000, 000, 0.3),
                    inset 0px 0px 0px rgba(255, 255, 255, 0);
                    
                    text-shadow:
                    10px 10px 10px rgba(000, 000, 000, 1),
                    
                }
            </style>
            
            <script type="text/javascript" charset="utf-8" src="phonegap.js"></script>
            <script type="text/javascript" charset="utf-8">
                
                var rec = null;
                var src = null;
                var volumeSlider = null;
                var volLevel = null;
                var timerID = null;
                document.addEventListener("deviceready", init(), false);
                
                function init(){
                    src = "test.wav";
                }
            
            function startRec(){
                rec = new Media(src,
                                // success callback
                                function() {
                                console.log("Audio Success");
                                },
                                // error callback
                                function(err) {
                                console.log("Audio Error: "+ err.code);
                                });
                                
                                // Record audio
                                rec.startRecord();
                                document.getElementById("stat").innerHTML = "recording...";
            }
            function stopRec(){
                rec.stopRecord();
                document.getElementById("stat").innerHTML = "stop recording";
            }
            function playRec(){
                volumeSlider = window.plugins.volumeSlider;
                volumeSlider.createVolumeSlider(10,350,300,30);
                volumeSlider.showVolumeSlider();


                rec.play();
                document.getElementById("stat").innerHTML = "play...";
                
                resetVolume(); // 音量は0.5fにリセット
                timerID = setInterval(intervalGetVolume,300);
                
            }
            
            function intervalGetVolume(){
                getVolume();
                document.getElementById("res").innerHTML = "vol=" + volLevel;
            }
            function playStop(){
                rec.stop();
                clearInterval(timerID);
                document.getElementById("stat").innerHTML = "stop playing";
            }
            
            function getVolume() {
                volumeSlider.getVolumeSlider(VSLevelHandler, VSControlHandler, "level" );
                return volLevel;
            }
            
            function resetVolume() {
                volumeSlider.getVolumeSlider(VSLevelHandler, VSControlHandler, "reset" );
            }
            
            function VSLevelHandler (result) {
                volLevel = result;
            }
            
            function VSControlHandler (error) {
                document.getElementById("res").innerHTML = "result=" + error;
            }
            
            </script>
            </head>
    <body>
        <h1>Using Media APIs</h1>
        <div>
            <button class="btn" onclick="startRec()">REC</button>
            <button class="btn" onclick="stopRec()">STOP REC</button>
            <button class="btn" onclick="playRec()">PLY</button>
            <button class="btn" onclick="playStop()">STOP PLAY</button>
        </div>
        <div id="stat"></div>
        <div id="res"></div>
    </body>
</html>
  • 使い方

立ち上がったら、とりあえずRECを押して数秒間録音します(10秒くらい)。そのあとSTOP RECで止めます。
f:id:Lynx-EyED:20140221234758p:plain

PLYを押して再生します。音源の再生が終わるまで、リモコンや側面ボリュームボタンを押すとそれに応じてボリュームスライダが変化し、音量レベルを数値で表示します。
f:id:Lynx-EyED:20140221235632p:plain

おしまい。

*1:ちなみにPhoneGapにはvolumedown/volumeupイベントがありますが、これはblackberryのみ対応です http://docs.phonegap.com/en/3.3.0/cordova_events_events.md.html#volumeupbutton