LeapMotionでiTunesをジェスチャー操作してみる
サンプルにちょこっと手を加えただけの”やっつけ”なんだけど、今日は Leap Motion の勉強会?にお邪魔するので、それまでにちょっとはコードを見ておかないといけないよな〜と思ったのでメモ。
ちょっと腕が上過ぎてジェスチャーがわかり難いけど、こんな感じ。
LeapMotion SDKの Sampleにちょこっと手を加えただけ!
とにかく、Leap Motion SDK の Sample が良くできているので、そこのジェスチャーに合わせて AppleScript で iTunes を操作している。
機能はこれだけ。
- 「指 1本でキーを叩く動作する」と「曲をプレイ」
- 「指 2本以上でキーを叩く動作する」と「曲をポーズ」
- 「時計回りに指を回す」と「次の曲へ移動」
- 「半時計回りに指を回す」と「前の曲へ移動」
それぞれ、左側の括弧が Leap Motion で取れるジェスチャーで、右側の括弧が iTunes で行われる機能。
Leap Motion の Gesture からコマンドを決める
Leap Motion SDK の Sample.m には
- (void)onFrame:(NSNotification *)notification;
なんてメソッドがあって、こいつがジェスチャーの判断から、手や指の動きの状態まで事細かに取得してくれている。
で、今回はこの中のジェスチャーの LEAP_GESTURE_TYPE_CIRCLE と LEAP_GESTURE_TYPE_KEY_TAP で、iTunes を動かす動作にしてみた。
コードはこんな感じ。
- (void)onFrame:(NSNotification *)notification { LeapController *aController = (LeapController *)[notification object]; // Get the most recent frame and report some basic information LeapFrame *frame = [aController frame:0]; int fingerCount; if ([[frame hands] count] != 0) { // Get the first hand LeapHand *hand = [[frame hands] objectAtIndex:0]; // Check if the hand has any fingers NSArray *fingers = [hand fingers]; fingerCount = [fingers count]; if ([fingers count] != 0) { // Calculate the hand's average finger tip position LeapVector *avgPos = [[LeapVector alloc] init]; for (int i = 0; i < [fingers count]; i++) { LeapFinger *finger = [fingers objectAtIndex:i]; avgPos = [avgPos plus:[finger tipPosition]]; } avgPos = [avgPos divide:[fingers count]]; } // Get the hand's normal vector and direction const LeapVector *normal = [hand palmNormal]; const LeapVector *direction = [hand direction]; } NSArray *gestures = [frame gestures:nil]; for (int g = 0; g < [gestures count]; g++) { LeapGesture *gesture = [gestures objectAtIndex:g]; switch (gesture.type) { case LEAP_GESTURE_TYPE_CIRCLE: { LeapCircleGesture *circleGesture = (LeapCircleGesture *)gesture; NSString *clockwiseness; BOOL isClockwise = YES; if ([[[circleGesture pointable] direction] angleTo:[circleGesture normal]] <= LEAP_PI/4) { clockwiseness = @"clockwise"; } else { clockwiseness = @"counterclockwise"; isClockwise = NO; } // Calculate the angle swept since the last frame float sweptAngle = 0; if(circleGesture.state != LEAP_GESTURE_STATE_START) { LeapCircleGesture *previousUpdate = (LeapCircleGesture *)[[aController frame:1] gesture:gesture.id]; sweptAngle = (circleGesture.progress - previousUpdate.progress) * 2 * LEAP_PI; } if (circleGesture.state == LEAP_GESTURE_STATE_START || circleGesture.state == LEAP_GESTURE_STATE_STOP) { NSLog(@"Circle id: %d, %@, progress: %f, radius %f, angle: %f degrees %@", circleGesture.id, [Sample stringForState:gesture.state], circleGesture.progress, circleGesture.radius, sweptAngle * LEAP_RAD_TO_DEG, clockwiseness); } if (circleGesture.state == LEAP_GESTURE_STATE_STOP) { if (isClockwise) { [self fwd]; } else { [self rwd]; } } break; } case LEAP_GESTURE_TYPE_SWIPE: { LeapSwipeGesture *swipeGesture = (LeapSwipeGesture *)gesture; break; } case LEAP_GESTURE_TYPE_KEY_TAP: { LeapKeyTapGesture *keyTapGesture = (LeapKeyTapGesture *)gesture; if (keyTapGesture.state == LEAP_GESTURE_STATE_STOP) { if (fingerCount>1) { [self playerPause]; } else { [self playerPlay]; } } break; } case LEAP_GESTURE_TYPE_SCREEN_TAP: { LeapScreenTapGesture *screenTapGesture = (LeapScreenTapGesture *)gesture; break; } default: NSLog(@"Unknown gesture type"); break; } } }
NSLog を削除したけど長いな…。
int fingerCount; は、1本指かそれ以上の指かを判断するのに用意した。
手が 2つ以上検知されている場合は、最初の手の指の数になっちゃうけど、ま、今回はとりあえずなので他の手は無視。
指を回すジェスチャー(LEAP_GESTURE_TYPE_CIRCLE)はちゃんと「時計回り」か「半時計回り」かを確認してくれている。
なので、それを BOOL isClockwise; として確保して、「次の曲に移動」か「前の曲に戻る」かを判断している。
キーを叩くジェスチャー(LEAP_GESTURE_TYPE_KEY_TAP)そのものには「何本指か?」という情報はないので、先にゲットしておいた fingerCount で1本かそれ以上かを判断して「曲をプレイする」か「曲をポーズする」を決めている。
やろうと思えば、3本指なら「曲の始めに戻る」とかも簡単。
ざっと Leam Motion の動作からコマンドにする部分はこんな感じ。
基本的にジェスチャーの state がちゃんとジェスチャーの終了を教えてくれる(LEAP_GESTURE_STATE_END)ので、終了したらどんなジェスチャーだったのかを確認してコマンドを飛ばすだけで良い。
楽チン!
AppleScript で iTunes を動かす
と、言っても「play」「pause」「rwd」「fwd」だけなので簡単。
まずは、それぞれのコマンドをターミナルで動作確認してみた。
iMac-coreI3-27:~ paraches$ osascript -e 'tell app "iTunes" to play' iMac-coreI3-27:~ paraches$ osascript -e 'tell app "iTunes" to stop' iMac-coreI3-27:~ paraches$ osascript -e 'tell app "iTunes" to previous track' iMac-coreI3-27:~ paraches$ osascript -e 'tell app "iTunes" to next track'
無事動作することを確認したら、今度は Cocoa からコマンドを飛ばす。
こんな感じのメソッドを用意。
- (void)doAppleScript:(NSString *)theScript { NSDictionary *errorDict; NSAppleEventDescriptor *returnDescripter = NULL; NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithSource:theScript]; returnDescripter = [scriptObject executeAndReturnError:&errorDict]; }
returnDescripter なんてわざわざ用意してるけど、エラーとかなんとか全く処理するつもり無し!
で、実際の AppleScript のコマンド文字列は、それぞれの動作のメソッド側で用意。
- (void)playerPlay { [self doAppleScript:@"tell app \"iTunes\" to play"]; }
というわけで、あれこれ調べながら 2時間程でやっつけたものなので色々と難はあるけど、ま、勉強会の前に触っておこうという目的は達成できたので OK!
最後に
Leap Motion SDK の Sample が良くできていて、簡単にジェスチャーを取ることができるのだけど、おかしな認識をしないように精度を上げようとするとかなり大変だと思う。
本当はボリュームはスワイプでと思ってたのだけど、スワイプのジェスチャーを上手に判断できなかったので今回はパスした。
とにかく、onFrame で取れる情報はそのまんまの”生”な感じのデータなので、そこから何がどのように動いたのか?を知る作業が精度の肝で辛いところ。
今回の件は、Macに向かって作業をしながらちょっとしたジェスチャーであれこれできると便利かな?と思ってやってみたけど、やっぱりコントロールパネルに入れてジェスチャーにコマンドを割り付けるような機能があると嬉しいな〜。