Search code examples
objective-cmacosspeech-recognitionosx-elcapitanappkit

NSSpeechRecognizer and .delegate=self; Problems


I've run into an issue with this little Objective-C project I'm doing and it's proving to be a bit of a roadblock. I'm playing around with Apple's NSSpeechRecognizer software on El Capitan, and I'm trying to get this guy running properly so that when the riddle I give it is posed to the user, the user can respond with a word to "do something cool". As it stands right now, the delegate method:

-(void) speechRecognizer:(NSSpeechRecognizer *)sender didRecognizeCommand:(NSString *)command { ... }`

is never even called, even though it appears the recognition icon is correctly detecting the answer to the riddle.


Solution

  • The problem is that your main function has a loop that is continually checking whether the speech has been recognizing. You are not giving NSSpeechRecognizer a chance to actually deliver any messages to you.

    Your app needs to let the main "run loop" run, so it can deliver messages. Normally, in an OS X app, your main would just call NSApplicationMain, which does this for you.

    Your code is effectively this:

    @interface RecognizerDelegate : NSObject <NSSpeechRecognizerDelegate>
    
    @property (nonatomic) NSSpeechRecognizer *recognizer;
    @property (nonatomic) BOOL didRecognize;
    
    @end
    
    @implementation RecognizerDelegate
    
    - (id)init
    {
        if ((self = [super init])) {
            self.didRecognize = NO;
    
            self.recognizer = [[NSSpeechRecognizer alloc] init];
            self.recognizer.listensInForegroundOnly = NO;
            self.recognizer.blocksOtherRecognizers = YES;
            self.recognizer.delegate = self;
            self.recognizer.commands = @[ @"hello" ];
            [self.recognizer startListening];
        }
    
        return self;
    }
    
    - (void)speechRecognizer:(NSSpeechRecognizer *)sender didRecognizeCommand:(NSString *)command
    {
        self.didRecognize = YES;
    }
    
    @end
    
    int main(int argc, const char * argv[])
    {
        @autoreleasepool
        {
            RecognizerDelegate *recognizerDelegate = [[RecognizerDelegate alloc] init];
    
            while (recognizerDelegate.didRecognize == NO) {
                // do nothing
            }
    
            NSLog(@"Recognized!");
        }
    
        return 0;
    }
    

    That while loop is doing nothing useful, just running your CPU in a loop and wasting time and energy. You are not letting any other code in NSSpeechSynthesizer, or any of the system frameworks like Foundation or AppKit, get the chance to do anything. So, nothing happens.

    To fix this in the short term: you can let the main run loop run for a little while in each pass through the loop. This code would let the system run for a second, then would return to your code, so you could check again:

    while (recognizerDelegate.didRecognize == NO) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
    }
    

    The longer-term fix would be to move your code out of main and to structure it like a real OS X app. Instead of using a loop to poll a condition like recognizerDelegate.didRecognize, you would just trigger the "next thing" directly from delegate methods like -speechRecognizer:didRecognizeCommand:, or you would use things like NSTimer to run code periodically.

    For more details, see the Apple doc Cocoa Application Competencies for OS X, specifically the "Main Event Loop" section.