AVAudioPlayerでiPodと同時に音を鳴らし、かつサイレントモードに対応させる方法

AVAudioPlayerでアラーム音を鳴らそうとしていたのですが、

  1. iPodでの音楽再生に重ねてアラームを鳴らす
  2. スリープ状態でも鳴る
  3. サイレントモードがオンの場合はヘッドフォン利用時のみアラームを鳴らす

という感じで鳴らすのはなかなか一筋縄ではいかないみたいです。


まず、要求1と2を同時に満たすには、オーディオセッションの設定で

UInt32 ssnCate = kAudioSessionCategory_MediaPlayback;  
AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(ssnCate), &ssnCate);  
UInt32 mixWithOthers = 1;  
AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryMixWithOthers, sizeof(mixWithOthers), &mixWithOthers);  

という様にセッションカテゴリをMediaPlaybackにし、かつカテゴリをオーバーライドしてiPodと同時に鳴るように設定します。


しかし、この設定ではiPhoneのサイレントモードのON/OFFに関わらず音がなってしまうため、要求3を満たすことができません。

そこで、別の手段でサイレントモードのON/OFFを判別した上で、セッションカテゴリの設定を場合分けするようにします。
サイレントモードの状態を取得するコードは以下になります。

UInt32 routeSize = sizeof (CFStringRef);
CFStringRef route;
AudioSessionGetProperty (kAudioSessionProperty_AudioRoute,&routeSize,&route);

if (CFStringGetLength(route)>0) {
	//サイレントモードがオフの場合の処理
}else {
	//サイレントモードがオンの場合の処理
}

ちなみにこのコードでサイレントスイッチがオンと判定されるのは、iPhoneのサイレントスイッチがオンになっていて、かつiPodで音楽を再生していない状態に限ります。iPodで音楽を再生中だと、サイレントスイッチがオンでも非サイレント状態だと判定されます。


この判別コードを利用して作った最終的なコードが以下になります。

//オーディオセッションの初期化
AudioSessionInitialize (NULL,NULL,NULL,NULL);
	
//AVPlayerの初期化
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"サウンドファイル名" ofType:@"wav"];
NSURL    *soundFileURL  = [[NSURL alloc] initFileURLWithPath:soundFilePath];
avPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:nil];
avPlayer.delegate = self;
[avPlayer setNumberOfLoops:1];

//オーディオ経路を取得する
UInt32 routeSize = sizeof (CFStringRef);
CFStringRef route;
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute,&routeSize,&route);

if (CFStringGetLength(route)>0) { //サイレントモードがオフ
	//AudioSessionカテゴリーをMediaPlaybackにセット(サイレントモードに従わない、iPodと共存不可)  
	UInt32 ssnCate = kAudioSessionCategory_MediaPlayback;  
	AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, 
							sizeof(ssnCate), 
							&ssnCate);  
	
	UInt32 mixWithOthers = 1;  
	AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryMixWithOthers, 
							sizeof(mixWithOthers), 
							&mixWithOthers);  
}else {	//サイレントモードがオン
	//AudioSessionカテゴリーをAmbientSoundにセット(サイレントモードでは音は鳴らない)  		
	UInt32 ssnCate = kAudioSessionCategory_AmbientSound;  
	AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, 
							sizeof(ssnCate), 
							&ssnCate);  
}

[avPlayer play];

これで1〜3の要求を全て満たすアラーム音の再生が可能になります。
サイレントモードオン時にAmbientSoundをセットしているのは、このコード中には含まれていませんが、アラーム音再生中に同時にバイブレーションを鳴らすようにしているため、無音であっても[avPlayer play]を実行する必要があるからです。


ちなみに、iPod再生中にアラーム音を鳴らす場合に、iPodの再生音を一時的に小さくすることもできます。
その場合は

- (void) toggleCrossfadeOn:(UInt32)onOff
{
	if(onOff==0){
		//ボリュームを戻す際、オーディオカテゴリがMediaPlaybackのままだと再生が停止してしまうので、元に戻す
		UInt32 ssnCate = kAudioSessionCategory_AmbientSound;  
		AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(ssnCate), &ssnCate);  
	}
	//crossfade the ipod music
	AudioSessionSetProperty(kAudioSessionProperty_OtherMixableAudioShouldDuck,sizeof(onOff),&onOff);
	AudioSessionSetActive(onOff);
}

というメソッドを用意して、[avPlayer play]の前で

[self toggleCrossfadeOn:1];

アラーム停止時(audioPlayerDidFinishPlayingの中など)に

[self toggleCrossfadeOn:0];

として呼び出せばOKです。

AppStoreのレビューを取得するperlスクリプト

とある理由でAppStore上のユーザレビューを
大量に取得する必要があり、調べてたら、


Scraping AppStore Reviews


で詳しい方法とPerlスクリプトが公開されてました。
でも、最近になってiTunes Storeの構造が変わったため、
そのままではうまく行かない。


現状に対応したやり方がなかなか見当たらなかったけど、
ちょっといじったら取得できるようになったのでメモっておきます。
(基本的には上記のページを参考に)


問題の原因は、今までレビューは圧縮された状態で提供されていたけど、
最近になってそれが無くなったこと。ただそれだけです。


だから、参考ページのスクリプトで、 gunzipしてる部分を消すだけです。


ついでに、参考ページのスクリプトだとレビュー本文の1行目しか取得できないので、HTML::TagParserを使って本文全体を取得できるように修正してみました。
(本文だけを綺麗に取ってくることはできたのですが、
タイトルや投稿者名だけを取り出すのはちょっと面倒だったので今回はパスしました)


書いたスクリプトを載せておきます。
CPANでHTML::TagParserモジュールをインストールしておく必要があります)


実はperl触ったのこれが初めてなんで色々おかしいかもしれないですが…

ソースコード(getappreview.pl)

#! /usr/bin/perl
# 引数に与えられたIDのアプリについてのレビュー(本文のみ)を
# AppStoreから取得し、標準出力とファイルの両方に出力します。

use HTML::TagParser;

#コマンドライン引数からアプリIDを取得
$appid=$ARGV[0];
if(!$appid && $appid eq "") {
   print "error: App ID was not specified in ARGV[0]\n"; exit(0);
}

$currentSoftware = $appid;
getAllReviews();


sub getAllReviews()
{
   #AppStoreでの国の設定(日本)
   $store =  143462;

   open(OUT,">review$currentSoftware.txt");

   #ページ番号を変えながら、最大100件目までを取得する
   #(1ページ10件。ループの値を変えて最大値を変更します)
   for($i=0;$i<10;$i++){
      fetchReviews($i); 
   }

   close(OUT);
}


sub fetchReviews()
{
   #ユーザエージェントを偽装してAppStoreから情報を取得
   my $pageid = $_[0];	
   my $doit = qq{curl -s -A "iTunes/9.1.0.79" -H "X-Apple-Store-Front: $store-1" 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=$currentSoftware&pageNumber=$pageid&sortOrdering=4&type=Purple+Software' | xmllint --format -};

   my $riz = `$doit`;

   #取得したXMLをHTML::TagParserによって解析し、本文のみを抽出する
   my $source = HTML::TagParser->new($riz);
   my @textViews = $source->getElementsByTagName("TextView");

   foreach my $textView (@textViews) {
      if($textView->getAttribute("topInset") eq "2" and $textView->getAttribute("styleSet") eq "normal11") {
	 print $textView->innerText;
	 print OUT $textView->innerText;
         
         print "\n-----\n";
	 print OUT "\n-----\n";
      }

   }
}

実行方法

% perl getappreview.pl アプリID

実行すると review(アプリID).txtという名前のファイルに取得したレビューが出力されます。


アプリIDは、iTunesStoreでアプリを右クリックして、
「URLをコピー」して得られるURL中に記述されているものです。
例えば駅.LockyだとURLは
http://itunes.apple.com/jp/app/id335126084?mt=8
で、idは335126084になります。

Rails tips

いまいち本とかに詳しく載ってなくて苦労したあたりを覚え書き。
RailsからGoogleMapを扱うのは何かと大変・・

Railsコードからjavascriptの関数を呼び出す

AJAXリクエストを送信するメソッド内で、

render :update do |page|
    page << "javacpriptのコード"
end

または、レンダリング用のrjsテンプレート内で同様の

page << "javascriptコード"

とやればOK

submit_to_remoteでidを渡す

<%= submit_to_remote 'delete','削除', :url => { :action => 'delete_spot', :id => i } %>

こんなかんじ

Ruby on Rails

ひさびさに更新。
これまでひたすらiPhoneアプリ作ってきて、色々とノウハウが溜まってはいるんだけどここを更新する余裕無くて全く書いてませんでしたね。また書けるときにちょこちょこ更新していこうかと。

さて、今更の卒研始動に伴い、Webアプリ開発のためにRailsを使う事にしたので、これからRailsの勉強をしていこうかと。時間が全くないので、とにかく速攻でやらねば・・

MacEclipse環境で開発できる様にAptana Studioをインストール。
こっから頑張ります

iPhoneでSQLiteを使う(準備)

以前から作っている電車位置マップに関して、時刻表(ダイアグラム)データはテキストファイルとして保存し、起動時にパースして使っていたのだが、かなり大量のデータになるのでこれでは扱いが大変。
今後iPhoneで色々とデータベースを扱う機会は増えそうなので、ここで使い方について調べてまとめておこうかと思う。


まず初期準備

  • Frameworksにlibsqlite3.0.dylibを追加しておく。
  • データベースを扱うクラスのヘッダにて、をインポート(#import )し、メンバ変数にsqlite3型のポインタ変数を定義(sqlite3 *database)する。

次に、データベースへの書き込みを可能にするために、リソースディレクトリにあるデータベースファイルをユーザーが書き換え可能なディレクトリ(NSDocumentDirectory)に必要に応じてコピーする。

// まず、同じファイルが既にユーザーフォルダに存在するかをチェックする
BOOL isExist;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@"database.sql"];
isExist = [fileManager fileExistsAtPath:writableDBPath];
if (isExist) return;    //存在するならreturn

// 存在しなければ、リソースフォルダからユーザーフォルダにデータベースをコピーする
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"database.sql"];
isExist = [fileManager copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];
if (!isExist) {
    //失敗したら警告を表示
    NSAssert1(0, @"Failed to create writable database file with message '%@'.", [error localizedDescription]);
}


この後、データベースに接続し、クエリを送信、結果の受け取りを行うが、これについては次に書きます。

iPhoneで麻雀アプリ

AppStoreは相変わらず盛況だが、
未だに麻雀アプリがでない・・・なぜだ・・・
上海とかはいくつもあって、有料アプリでも売れているというのに。
日本の麻雀は日本だけのものだから、
日本の本格麻雀アプリを出しても売れないだろうってことなんだろうか。
なんにしても、麻雀アプリが出たら有料でいくらになろうと絶対に買うだろう。


・・・まてよ、なんなら自分で作ればいいんじゃないのか?
んでAppStoreで売りにだせば、ボロ儲けじゃないか!!


ってな話を友人としてて、
iPhoneで麻雀アプリ作ってやろうじゃないかって話で盛り上がった。
UIは頑張れば作れるだろうが、問題はAI。
友人はかなりの雀士だが、さすがに麻雀の思考アルゴリズムを一から作るのはキツい。


ってことで、大学の本屋にこんなもってこいな本があったのを思い出した。

コンピュータ麻雀のアルゴリズム (I・O BOOKS)

コンピュータ麻雀のアルゴリズム (I・O BOOKS)

そしてこれを即購入。麻雀AIのアルゴリズムを丁寧に教えてくれている。
なるほどこういう仕組みになっているのか。この本、かなり面白い。

まあ内容はいいとして、これで麻雀アプリを作る準備は完了した。


あとは、タッチ操作を生かしたオリジナル要素について考えてみる。


まず、自分で好きに理牌(リーパイ)できる仕組みは欲しい。
牌を捨てたり鳴く時にはちゃんと牌を自分で移動させたりと、
ちょっと面倒になるかもしれないけど、タッチ操作によってリアルさを
追求させたインターフェースにしてみたい。



なんて感じで夢は膨らみ、俄然やる気がでてきた。
あとはとにかく実装あるのみ!!


・・・まあ、そんなことやってる時間あんましないんだけどorz