プログラマでありたい

おっさんになっても、プログラマでありつづけたい

ボールのラジコン「Sphero」をiPhoneの音声認識で操作しよう!!


 「Sphero Robotic Ball」というラジコンのようなボールをご存知でしょうか?iPhoneやアンドロイドをコントローラとして、前後左右に自由に動かせて、更に様々な色に点滅させてと中々面白いおもちゃです。小さな子どもや犬猫と遊ぶとかなり楽しい代物です。完全に球形なので、よほど大きな衝撃を与えない限り壊れないという優れ物です。遊んでいる風景は公式ページにアップされているので、是非御覧ください。


 そんなSpheroですが、更にエンジニア心をくすぐる仕掛けがあります。Spheroとコントローラ(iPhone,Android)との通信はBluetoothで行なっているのですが、何とAPIは全て公開されています。そしてiPhone/AndroidのSDKも提供されているので、独自のプログラムで自由に動かすことが出来るのです。楽しいでしょう。
 ということで、私も作ってみました。Siriを使って、音声で操作するアプリです。作っているっている時の脳内イメージは、鉄人28号みたいに「進め」や「止まれ」で命令して操作するロボットです。しかし、よく考えたら鉄人28号はリモコンで動いていましたね。

Sphero音声コントローラーの作り方



 実装の殆どは、kishikawakatsumi/VoiceNavigationに少し手を加えただけです。Siriの音声認識を使う為にUITextInputのstartDictation, stopDictation, cancelDictation メソッドを利用しています。ちなみに非公開のAPIを使っているので、アプリを作ってもAppleに申請することは出来ません。


viewDidLoadでNSNotificationCenterをひたすら行なっています。iOS6では、keyboardWillShowの挙動が変わっているっぽいので下の方で無理やり呼び出しています。(iOS5だと、ここで呼び出す必要はありません。)

- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
    [super viewDidLoad];

    dictationView.layer.cornerRadius = 8.0f;
    
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(keyboardWillShow:) 
                                                 name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(applicationWillEnterForeground:) 
                                                 name:UIApplicationWillEnterForegroundNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(applicationDidEnterBackground:) 
                                                 name:UIApplicationDidEnterBackgroundNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(dictationRecordingDidEnd:) 
                                                 name:VNDictationRecordingDidEndNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(dictationRecognitionSucceeded:) 
                                                 name:VNDictationRecognitionSucceededNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(dictationRecognitionFailed:) 
                                                 name:VNDictationRecognitionFailedNotification object:nil];
    /*Register for application lifecycle notifications so we known when to connect and disconnect from the robot*/
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
    [self keyboardWillShow:nil];
    /*Only start the blinking loop when the view loads*/
    robotOnline = NO;
    
    calibrateHandler = [[RUICalibrateGestureHandler alloc] initWithView:self.view];
}


音声認識の繰り返し。ループで一定期間間隔でスタートとストップで繰り返すようにしてあります。

- (void)startDictation {
    NSLog(@"startDictation");
    [dictationController performSelector:@selector(startDictation)];
    
    displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTimer:)];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    
    [self resetProgress];
    micImageView.hidden = NO;
    resultLabel.text = nil;
}

- (void)stopDictation {
    NSLog(@"stopDictation");
    [dictationController performSelector:@selector(stopDictation)];
    
    [displayLink invalidate];
    displayLink = nil;
    
    [self showWaitingServerProcessIndicator];
    micImageView.hidden = YES;
}

- (void)cancelDictation {
    NSLog(@"cancelDictation");
    [dictationController performSelector:@selector(cancelDictation)];
    
    [displayLink invalidate];
    displayLink = nil;
    
    [self resetProgress];
    micImageView.hidden = NO;
    resultLabel.text = nil;
}


 音声検出した後です。dictationRecognitionSucceededで解析の実体であるprocessDictationTextを呼び出しています。

- (void)dictationRecognitionSucceeded:(NSNotification *)notification {
    NSLog(@"dictationRecognitionSucceeded");
    NSDictionary *userInfo = notification.userInfo;
    NSArray *dictationResult = [userInfo objectForKey:VNDictationResultKey];
    
    NSString *text = [self wholeTestWithDictationResult:dictationResult];
    [self processDictationText:text];
    
    [self hideWaitingServerProcessIndicator];
    
    [self performSelector:@selector(startDictation) withObject:nil afterDelay:VNDictationRepeatInterval];
}

- (void)dictationRecognitionFailed:(NSNotification *)notification {
    NSLog(@"dictationRecognitionFailed");
    resultLabel.text = @"-";
    
    [self hideWaitingServerProcessIndicator];
    
    [self performSelector:@selector(startDictation) withObject:nil afterDelay:VNDictationRepeatInterval];
}


 音声認識はif文での分岐処理で行なっています。ここはゴリゴリ書いているだけなので、もう少しスマートに書くのが良いと思います。

- (void)processDictationText:(NSString *)text {
    NSLog(@"processDictationText");
    resultLabel.text = text;
    NSLog(@"text");
    if ([self hasString:text Search:@"とまれ"] || [self hasString:text Search:@"止"]) {
        NSLog(@"止まれ");
        [RKRollCommand sendStop];
        //[webView goBack];
    } else if ([self hasString:text Search:@"戻れ"]) {
        NSLog(@"戻る");
        [RKRollCommand sendCommandWithHeading:180.0 velocity:0.5];
        //[webView goBack];
    } else if ([self hasString:text Search:@"進め"]) {
        NSLog(@"進む");
        [RKRollCommand sendCommandWithHeading:0.0 velocity:0.5];
        //[webView goForward];
    } else if ([self hasString:text Search:@"お前の血は何色だ"]) {
        NSLog(@"お前の血は何色だ");
        [self changeColorRed:NULL];
    } else if ([self hasString:text Search:@"青"]) {
        NSLog(@"青");
        [self changeColorBlue:NULL];
    } else if ([self hasString:text Search:@"赤"] || [self hasString:text Search:@"あか"]) {
        NSLog(@"赤");
        [self changeColorRed:NULL];
    } else if ([self hasString:text Search:@"緑"]) {
        NSLog(@"緑");
        [self changeColorGreen:NULL];
    }
    //[RKRollCommand sendCommandWithHeading:0.0 velocity:0.5];
}

 なお、このアプリはSiriが使える機種限定です。2013年3月現在だと、iPhone 4S以降、新しいiPad以降、iPad miniとなっています。ソースは、GitHubで公開してあります。

デモ



 動きが解るように、ボタンも用意してあります。そして、肝心の音声認識ですが、赤をどうしても馬鹿と認識してしまいます。これは私の滑舌の問題かと思いますw何回も赤といっても、Siriに"ばか"と言い返されますw
 傾向としては、Siriは単語系の認識率は悪いですね。会話等の文脈判断がある程度あるのだと思います。

まとめ



 今回は、音声でラジコンを動かしていますが、ラジコン以外でも操作出来るものは色々あると思います。本命は、Glamo iRemocon(アイリモコン)などと組み合わせて、家の家電を音声操作する未来の家なのではないでしょうか?そのうち試してみようと思います。iPhone/iPad miniの消費電力だと常時動かしていても、たかが知れてますからね。是非、お試しあれ!!
(実は1年ほど前に作ってブログを書きかけていたのを、下書きから救出して書いていますw)


See Also:
dkfj/SpheroVoiceNavigation


参照:
Sphero | Robotic Ball Gaming System for iOS and Android
kishikawakatsumi/VoiceNavigation
iOS 5.1 の音声入力を使ってアプリケーションを操作してみる - 24/7 twenty-four seven