Objective-Cでコマンドの中身を作るまで

前回までに、コマンドの雛形は出来上がった。あとは価値ある中身を作るだけ。

そういえば、AppleScriptで音を鳴らすのはbeepしか知らない。beepではシステム環境設定の警告音に設定した音しか鳴らせない。その他の警告音を鳴らすには、システム環境設定の警告音を変更するしかない...。もっと自由に警告音を活用したいと常々思っていた。警告音を自由に鳴らせるコマンドを作れば、それを簡単にAppleScriptから利用できる。

そうだ、soundコマンドを作ってみよう!

  • 幸い自分のMacBookには、まだsoundというコマンドは存在しない。コマンド名としてsoundが使えるのだ。
$ sound
-bash: sound: command not found


  • afplayコマンド、ありました!(soundコマンド作るまでもなく)
$ afplay /System/Library/Sounds/Submarine.aiff 

作業環境

  • MacBook OSX 10.6.2
  • Developer Toolsをインストール済

Cocoaを利用する

  • コマンドを作ると言っても、0からすべてを作り上げる能力はないので、目的に叶ったライブラリを活用させて頂くことになる。
  • OSX環境なら、迷わずCocoa環境の活用を考えたくなる。
NSSound

そして調べてみると...

Cocoaには NSSound が用意されていて、これを使うとたった2行で音が出せる。

NSSound *sound = [NSSound soundNamed:@"Submarine"];
 [sound play];
Cocoaの日々

求めていたものがここにあった!

Xcodeで試す
  • 早速Xcodeで、指定した音を鳴らすアプリケーションを作って試してみた。
  • 新規プロジェクト... >> Mac OSXのApplication >> Cocoa Application を選択して、[選択...]ボタンを押す。
  • プロジェクト名は安易に「sound」とした。
  • 開いたプロジェクトウィンドウの「グループとファイル」のClassesを選択して、soundAppDelegate.mを以下のように編集した。
    • // Insert code here to initialize your application 以下に2行追加しただけ。
//
//  soundAppDelegate.m
//  sound
//
//  Created by zari on 10/02/04.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//

#import "soundAppDelegate.h"

@implementation soundAppDelegate

@synthesize window;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
	// Insert code here to initialize your application 
    NSSound *sound = [NSSound soundNamed:@"Submarine"];
    [sound play];
}

@end
  • 追記したら、すかさず、ツールバーの[ビルドと実行]ボタンを押す。
  • 空白のウィンドウが開き、「フォ〜ン」と警告音Submarineが鳴った!(たったこれだけでも成功すると嬉しい)
Cocoaを利用する雛形
  • 実験用のGUIアプリケーションとしては上記の作り方で問題ないのだが、今欲しいのは、main()でコマンドが処理する過程でNSSoundを利用したい。
  • 前回作ったhelloコマンドに単純に2行追加したくなるが...
#include <stdio.h>

int main (int argc, const char * argv[]) {
    NSSound *sound = [NSSound soundNamed:@"Submarine"];
    [sound play];
    // insert code here...
    printf("Hello, World!\n");
    return 0;
}
  • それだけではCocoaの機能は使えないらしい。ビルドするとエラー出まくり...。


Command Line ToolプロジェクトでCocoa環境を利用するには、以下の手順が必要だった。

  • ファイル名を変更
    • main.c → main.mに変更した。(main.cを選択して、右クリック、名称変更)
    • 拡張子.cは、C言語と解釈される。
    • 拡張子.mは、Objective-cと解釈される。
  • Cocoa.frameworkの追加
    • Cocoa.frameworkを利用するためには、プロジェクトに追加する必要がある。
    • プロジェクト >> プロジェクトに追加...(command-option-A)で、/System/Library/Frameworks/Cocoa.framework/を追加した。
    • 追加する時の設定は、デフォルト状態のまま[追加]ボタンを押した。
  • そして、main.mに以下の追記をした。
    • Cocoa.frameworkのインポート //__(1)__
    • NSAutoreleasePoolによるメモリ管理 //__(2)__
#include <stdio.h>
#import <Cocoa/Cocoa.h> //__(1)__

int main (int argc, char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; //__(2)__
    
    NSSound *sound = [NSSound soundNamed:@"Submarine"];
    [sound play];
    
    // insert code here...
    printf("Hello, World!\n");
    
    [pool release]; //__(2)__
    return 0;
}
  • [ビルドと実行]ボタンを押してみる。
...(中略)...
run
[Switching to process 10091]
実行中...
Hello, World!
  • 問題なく完了しましたと表示された。
  • しかし、警告音が鳴らない...。なぜだろう?
  • Hello, World!は表示されているので、処理は正常に進んでいると思うのだが...。
  • 暫し悩んで気付いたのが、NSSoundオブジェクトが生成されて即、リリース(解放)されてしまっていること。
  • GUIアプリケーションとして作った時は、アプリケーションはメニューの終了を実行するまで起動していた。
  • その間、NSSoundオブジェクトは保持されているのかもしれない。
  • sleep(1);を追記して、終了まで1秒間の猶予を与えてみた。
#include <stdio.h>
#import <Cocoa/Cocoa.h>

int main (int argc, char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    NSSound *sound = [NSSound soundNamed:@"Submarine"];
    [sound play];
    sleep(1); // 秒単位で処理を停止する
    
    // insert code here...
    printf("Hello, World!\n");
    
    [pool release];
    return 0;
}
  • [ビルドと実行]ボタンを押してみる。

...フォ〜ン

  • 警告音が鳴った!成功だ。
ターミナルからコンパイルする

ターミナルからコマンドラインコンパイルする方法も試してみた。

  • ポイントは-frameworkオプションで、Cocoaを指定することだった。


上記ファイルがデスクトップにsound.mというファイル名で存在する場合...

  • -frameworkオプションで、Cocoaを指定した。
    • Xcodeでプロジェクトにframeworkを追加する操作に対応するのかもしれない。
  • -oオプションで、出力される実行ファイル名を指定した。
    • -oオプションを指定しないと、a.outで出力される。
$ cd ~/Desktop
$ gcc sound.m -o sound -framework Cocoa
$ ./sound
Hello, World!
  • ./soundを実行してみると、フォ〜ンと警告音が鳴った!いい感じ。

コマンドの雛形に組み込む

  • まだ、soundコマンドのオプション仕様は考えていないが、引数に警告音の名前を取ることだけ想定して作ってみる。
  • 雛形として残すために、無意味な-a --abcオプションのみ解析するようにしてみた。
#include <stdio.h>      /* printf() */
#include <stdlib.h>     /* exit() */
#include <unistd.h>     /* ?????? */
#include <getopt.h>     /* getopt_long() */
#import <Cocoa/Cocoa.h>

int main(int argc, char * argv[]){
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    int opt, option_index;
    struct option long_options[] = {
        // {"name1", no_argument,         NULL,   'n'   }
        // {"name2", required_argument,   &flag,  "name2"}
        // {"name3", optional_argument,   &flag,  "name3"}
        {"abc", no_argument, NULL, 'a'},
        {0, 0, 0, 0} // 配列の最後はすべて0で埋める
    };
    
    //opterr = 0;/* getopt側のエラーメッセージを非表示にする */
    
    while((opt = getopt_long_only(argc, argv, "a", long_options, &option_index)) != -1){
        switch(opt){
            case 'a':
                printf("Option -%c = %s\n", opt, optarg);
                break;
                
            // 解析できないオプションが見つかった場合は「?」を返す
            // オプション引数が不足している場合も「?」を返す
            case '?':
                //printf("Unknown or required argument option -%c\n", optopt);
                printf("Usage: COMMAND [-m | -e] [-s suffix] name ...\n");
                return 1;   // exit(EXIT_FAILURE);と同等 http://okwave.jp/qa/q794746.html
        }
    }
    
    NSString *name = [NSString stringWithCString: argv[optind] encoding: NSUTF8StringEncoding];
    //printf("%s, playing...\n", argv[optind]);
    NSLog(@"%@, playing %d", name, i);
    NSSound *sound = [NSSound soundNamed: name];
    [sound play];
    sleep(1); // 秒単位で処理を停止する
    
    [pool release];
    return 0;   // exit(EXIT_SUCCESS);と同等 http://okwave.jp/qa/q794746.html
}
$ gcc sound.m -o sound -framework cocoa
$ ./sound -abc Blow
Option -a = (null)
2010-02-10 05:59:02.852 sound[12221:903] Blow, playing...

これで、引数に警告音の名前を指定できるようになった!

もう少し洗練させる

  • -l --loopオプションを設定して、繰り返し回数を指定できるようにしてみた。
  • 音楽ファイルのパスも指定できるようにしてみた。
  • その際、再生中かどうかisPlayingで確認するようにした。
    • そうしないと、すべての演奏時間が1秒になってしまう...。
    • でも、使い方によってはイントロクイズみたいで面白いかも。
  • -v --verboseオプションも設定して、指定した時だけNSLog()を出力するようにしてみた。
#include <stdio.h>      /* printf() */
#include <stdlib.h>     /* exit() */
#include <unistd.h>     /* ?????? */
#include <getopt.h>     /* getopt_long() */
#import <Cocoa/Cocoa.h>

int main(int argc, char * argv[]){
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    
    int i, loop = 1;
    BOOL isVerbose;
    int opt, option_index;
    struct option long_options[] = {
        // {"name1", no_argument,         NULL,   'n'   }
        // {"name2", required_argument,   &flag,  "name1"}
        // {"name3", optional_argument,   &flag,  "name2"}
        {"loop", required_argument, NULL, 'l'},
        {"verbose", no_argument, NULL, 'v'},
        {0, 0, 0, 0} // 配列の最後はすべて0で埋める
    };
    
    //opterr = 0; // getopt側のエラーメッセージを非表示にする
    
    while((opt = getopt_long_only(argc, argv, "l:v", long_options, &option_index)) != -1){
        switch(opt){
            case 'l':
                //printf("Option -%c = %s\n", opt, optarg);
                loop = atoi(optarg);
                break;
            case 'v':
                //printf("Option -%c = %s\n", opt, optarg);
                isVerbose = TRUE;
                break;
                
            // 解析できないオプションが見つかった場合は「?」を返す
            // オプション引数が不足している場合も「?」を返す
            case '?':
                //printf("Unknown or required argument option -%c\n", optopt);
                printf("Usage: COMMAND [[-l|--loop] num] [-v|--verbose] name_or_path\n");
                return 1;   // exit(EXIT_FAILURE);と同等 http://okwave.jp/qa/q794746.html
        }
    }
    
    NSString *name_or_path = [NSString stringWithCString: argv[optind] encoding: NSUTF8StringEncoding];
    NSSound *sound = [NSSound soundNamed: name_or_path];
    if (sound == nil) {
        sound = [[[NSSound alloc] initWithContentsOfFile: name_or_path byReference: YES] autorelease];
    }
    
    for (i = 0; i < loop; i++) {
        BOOL result;
        
        [sound setCurrentTime: 0];
        result = [sound play];
        
        if (isVerbose && result) {
            //printf("%s, playing...\n", argv[optind]);
            NSLog(@"%@ (%d), playing...", name_or_path, i);
        }
        
        while ([sound isPlaying]) {
            //sleep(1); // 秒単位で処理を停止する
            usleep(100); // ミリ秒単位で処理を停止する
        }
        
        if (isVerbose) {
            //printf("%s, stopped\n", argv[optind]);
            NSLog(@"%@ (%d), stopped", name_or_path, i);
        }
    }
    
    [pool release];
    return 0;   // exit(EXIT_SUCCESS);と同等 http://okwave.jp/qa/q794746.html
}
$ gcc sound.m -o sound -framework cocoa

$ ./sound -loop 2 Purr

$ ./sound -loop 2 -v Purr
2010-02-10 5:22:55.699 sound[14380:903] Purr (0), playing...
2010-02-10 5:22:56.702 sound[14380:903] Purr (0), stopped
2010-02-10 5:22:56.703 sound[14380:903] Purr (1), playing...
2010-02-10 5:22:57.704 sound[14380:903] Purr (1), stopped

$ ./sound -v /System/Library/Sounds/Sosumi.aiff 
2010-02-10 5:24:26.908 sound[14395:903] /System/Library/Sounds/Sosumi.aiff (0), playing...
2010-02-10 5:24:27.911 sound[14395:903] /System/Library/Sounds/Sosumi.aiff (0), stopped
  • いい感じで動いているみたい。大分コマンドらしくなった!
  • パスを指定すれば、iTunesライブラリの音楽ファイルだって再生できるのだ。


ひとまず、soundコマンドの出来上がり!

NSSoundの仕様