iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
前回の続き。得られたZipファイル内のパスからファイルのデータを取り出す。
unzLocateFile(file, filename, 0);
unzOpenCurrentFile(file);
NSMutableData *data = [NSMutableData data];
void *buffer = (void *)malloc(BUFSIZ);
int len;
while ((len = unzReadCurrentFile(file, buffer, BUFSIZ)) != 0) {
[data appendBytes:buffer length:len];
}
free(buffer);
printf("----------\n");
for (NSUInteger i = 0U; i < [data length]; i++) {
printf("%c", ((char *)[data bytes])[i]);
}
printf("\n----------\n");
unzCloseCurrentFile(file);
日時: 2012/03/17(土) 13:00-17:00
会場:新宿三丁目 新宿伊藤ビル 4F
集合:現地
会費:500円
見学者は以下のフォームで募集しています。
http://www.cocoa-study.com/mail/
zipファイルの操作でなく、メモリ上のデータを圧縮/解凍するには、どんな方法がいいのか?
以前、zlibの解説文章を読んだ事があり、全く難解で理解できなかったトラウマがあって、zlibを直接利用する事は避けていたのだが、今、改めて調べてみると、それ程、怖がるものではないように感じている。
という訳で、今回は、直接、zlibを利用する話だ。
OS X / iOSでzlibを利用するのは、簡単だ。"libz.dylib"をプロジェクトに追加するだけでいい。
データを圧縮するコードは以下のとおり。
#define OUTBUFSIZ 1024
- (void)zip:(NSString *)path
{
NSMutableData *data = [[NSMutableData alloc] init];
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
deflateInit(&strm, Z_DEFAULT_COMPRESSION);
char str[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
strm.next_in = (Bytef *)str;
strm.avail_in = strlen(str);
char buffer[OUTBUFSIZ];
strm.next_out = (Bytef *)buffer;
strm.avail_out = OUTBUFSIZ;
int status;
for (;;) {
if (strm.avail_in == 0) {
status = deflate(&strm, Z_FINISH);
}
else {
status = deflate(&strm, Z_NO_FLUSH);
}
if (status == Z_STREAM_END) {
break;
}
if (status != Z_OK) {
DBGMSG(@"deflate: %s", (strm.msg) ? strm.msg : "error");
break;
}
if (strm.avail_out == 0) {
[data appendBytes:buffer length:OUTBUFSIZ];
strm.next_out = (Bytef *)buffer;
strm.avail_out = OUTBUFSIZ;
}
}
if (strm.avail_out != OUTBUFSIZ) {
[data appendBytes:buffer length:(OUTBUFSIZ - strm.avail_out)];
}
deflateEnd(&strm);
//[data writeToFile:path atomically:YES];
}
文字列strを圧縮するコードだ。圧縮したデータを保存する様にしてみたのだが、これだけだと、zip形式のファイルにならないようで、unzip出来ないので、コメントアウトしている。
XMLデータを操作するAPIに、DOM(Document Object Model)とSAX(Simple API for XML)があるが、これに対応するCocoaのクラスがNSXMLDocumentとNSXMLParserだ。ただし、iOSではNSXMLDocumentが利用できない。iOSでDOMを使用したい場合はlibxml2を利用する事になる。
XMLデータからエレメント名と値を取得する例は、以下のとおり。
ビュー・コントローラをNSXMLParserDelegateに対応させる。
@interface ViewController : UIViewController
@property (strong, nonatomic) NSString *elementName;
@end
指定されたURLのXMLデータを読み込み、エレメントとそれの値を見つけたらデバッグ出力する例だ。
@implementation ViewController
@synthesize elementName = _elementName;
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *requestString = [NSString stringWithString:@"http://www.kyuden.co.jp/power_usages/xml/electric_power_usage20120304.xml"];
NSURL *url = [NSURL URLWithString:requestString];
[self parseXMLFile:url];
}
- (void)parseXMLFile:(NSURL *)url
{
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
[parser setDelegate:self];
[parser parse];
}
- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict
{
if (elementName) {
NSLog(@"element: %@", elementName);
self.elementName = elementName;
}
}
- (void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
if (self.elementName) {
self.elementName = nil;
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
if ((self.elementName)
&& (![self.elementName isEqualToString:@""])
&& (string)) {
NSLog(@"char: %@", string);
}
}
@end
これ以上については著者も調査中なので、機会があれば次回で。
前回の続き。用語を整理する。
エレメントは以下の初期となる。
<タグ>内容</タグ>
エレメントとは、タグで囲まれたデータという事になる。
XMLデータを頭から処理していき、エレメントに到達すると- parser:didStartElement:namespaceURI:qualifiedName:attributes:が呼ばれる。そのエレメントの内容に到達すると- parser:foundCharacters:が呼ばれるイメージだ。
XMLデータは木構造となるが、SAXではイベント駆動でデリゲートのメソッドが呼ばれるので、アプリケーション側で木構造を意識して処理する事になる。
例えば、以下のようなXMLデータ。
<?xml version= "1.0" encoding="UTF8">
<article author="Yukio MURAKAMI">
<para>とても短い文章。</para>
</article>
iOSでは文字列を描画する方法が複数あるが、それぞれ、長所と短所があるので、適材適所で使い分ける事が大事なようだ。
UILabelを使用する方法。単に描画したいだけなので、ビューでなくてもと思うかもしれないが、高機能なので便利だ。
- (void)viewDidLoad
{
[super viewDidLoad];
self.label = [[UILabel alloc] initWithFrame:CGRectMake(10.0, 100.0, 200, 50)];
self.label.font = [UIFont systemFontOfSize:48.0];
self.label.text = @"UILabel";
self.label.adjustsFontSizeToFitWidth = YES;
[self.view addSubview:self.label];
}
UIViewのサブクラスを作成して、-drawRect:で、NSStringのUIKit Additionsで描画するのも手軽だ。
- (void)drawRect:(CGRect)rect
{
NSString *str = @"NSString";
[str drawAtPoint:CGPointMake(10.0, 20.0) withFont:[UIFont systemFontOfSize:48.0]];
}
CoreGraphicsを使う方法もあるが、日本語の扱いや、座標の扱いが難しいので、理由が無い限りはお勧めできない。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGFloat height = self.bounds.size.height;
CGContextTranslateCTM(context, 0.0, height);
CGContextScaleCTM(context, 1.0, - 1.0);
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
CGContextSetFillColorSpace(context, cs);
CGColorSpaceRelease(cs);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
CGContextSelectFont(context, "Helvetica", 48.0, kCGEncodingMacRoman);
CGContextShowTextAtPoint(context, 10.0, 10.0, "Quartz", strlen("Quartz"));
CGContextFlush(context);
CGContextRestoreGState(context);
}
これら三つを使って文字列を描画した例だ。
CoreAnimationは奥が深い。その為、申し訳ないが、今、開発中のアプリケーションで利用しそうな機能を中心に取り上げてゆく。今回はアニメーション・ブロック。
画面をタッチすると、タッチした位置に画像が移動するコードだ。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = touches.anyObject;
[UIView beginAnimations:@"center" context:nil];
self.atMarkImageView.center = [touch locationInView:self];
[UIView commitAnimations];
}
beginAnimations:context:とcommitAnimationsに囲まれた部分がブロックだ。ブロック内に記述されたUIViewの変更内容がアニメーションとなる。
今回は、iOS 5.1リリースに伴う、開発環境の更新に時間がかかっているため、いつもの練習が行えない。そこで、Developerサイトで見つけたページを紹介する。
iOSがバージョンアップすると、Xcodeもバージョンアップしないと、実機での動作確認が行えない。何時も、Xcodeのバージョンアップの方が少し遅れて始まるので、リリースを控えている場合は注意が必要だ。
そして、iOSがバージョナップすると、Xcodeのバージョンアップは必要か?ターゲットは、以前のバージョンのOSからなので、Xcodeのバージョンアップは?ということが話題になるように思えるが、その疑問に答えるサイトだ。
以前のADC時代から考えると、本当に最近のApple Developerサイトは、親切になったね!
画面に表示する画像を差し替える場合、カードをめくるような効果を使える例だ。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[UIView beginAnimations:@"flip view" context:nil];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft
forView:self.imageView
cache:YES];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:1.0];
if (self.isAtMark) {
self.isAtMark = NO;
self.imageView.image = self.arrowImage;
}
else {
self.isAtMark = YES;
self.imageView.image = self.atmarkImage;
}
[UIView commitAnimations];
}
前回の例との差は、transitionとタイミングの指定の追加だ。
画像に続いて文字列に対しても、何らかの画面上の効果を発生させたいと考えている。今、考えているのは、UILabeに対してだ。
前回までのUIViewに対してのアニメーションだと、対象のプロパティがアニメーションになるのだが、今回は効果に対応するプロパティを変更する必要が無い。変更したら、完了後に戻す処理が必要になる。
今、考えている事に合う手段はないか?と、探していて見つけたのだ、レイヤへの効果の適用だ。
まず、Quartz Coreフレームワークをプロジェクトに追加する。そして、QuartzCore/QuartzCore.hをインポートする。
サンプルではラベルの内容の変化が分かるように日付を設定している。
ラベルに現在日時を初期値として設定する。
- (void)awakeFromNib
{
NSDate *date = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterMediumStyle];
self.label.text = [formatter stringFromDate:date];
}
画面がタッチされたら、ラベルの内容を現在日時に変更して、アニメーションを実行する。
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CATransition *animation = [CATransition animation];
[animation setDelegate:self];
[animation setDuration:1.0f];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setType:kCATransitionPush];
[animation setSubtype:kCATransitionFromLeft];
NSDate *date = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateStyle:NSDateFormatterMediumStyle];
[formatter setTimeStyle:NSDateFormatterMediumStyle];
self.label.text = [formatter stringFromDate:date];
[[self.label layer] addAnimation:animation forKey:@"animation transition"];
}
文字が左から右に流れた後に、内容が変わる事が確認できるはずだ。
ビーコントローラでタッチ操作を検出するようにして、タッチされたら、OKボタンのみのアラートを表示する例だ。
ビューコントローラにプロトコルを使って、UIAlertViewDelegateへ対応させる。
@interface ViewController : UIViewController <UIAlertViewDelegate>
@end
タッチされたらOKボタンのみのシンプルなアラートを表示させる。
@implementation ViewController
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Demo Alert"
message:@"demo appl"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil];
[alert show];
}
@end
OKボタンが押下されたら呼び出される、デリゲートのメソッドを追加する。
@implementation ViewController
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSLog(@"- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex(%d)", (int)buttonIndex);
}
@end
実行。
前回のアラートは、ユーザに情報を伝える為のもの。今回のアクション・シートはユーザに選択させる為のものだ。
ビューコントローラにUIActionSheetDelegateプロトコルを設定する。
@interface ViewController : UIViewController <UIActionSheetDelegate>
@end
アクションシートを表示させる。
@implementation ViewController
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"My Action Sheet"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:@"destructive button"
otherButtonTitles:@"Button 1",
@"Button 2",
nil];
[actionSheet showInView:self.view];
}
@end
アラートと同様にアクション・シートもデリゲートのメソッドでボタン押下に対応する。
@implementation ViewController
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSLog(@"%s index(%d)", __func__, (int)buttonIndex);
}
- (void)actionSheetCancel:(UIActionSheet *)actionSheet
{
NSLog(@"%s", __func__);
}
@end
実行。
アクション・シートでは、追加するボタンの個数は可変だ。画面に収まりきれなく個数を指定した場合は、どうなるのだろうか?
@implementation ViewController
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"My Action Sheet"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:@"destructive button"
otherButtonTitles:@"Button 1",
@"Button 2",
@"Button 3",
@"Button 4",
@"Button 5",
@"Button 6",
nil];
[actionSheet showInView:self.view];
}
@end
なんと、ボタンの部分がピッカーになっている!
第50回関東Cocoa勉強会で@saeki24hさんが、自身が発見されたアクション・シートのバグを発表されていましたが、その時はボタンの個数が増えてピッカーになった際に、cancelボタンとdestructiveボタンの順番が変わって、その際、インデックスがおかしくなっていたが、iOS 5.1で修正されたのか、ボタンの順番が変わらず、その為か、インデックスは正しい値のようだ。
複数台のiPhoneやiPad、iPod touchを一括設定できるアプリケーションが、Mac App Storeで公開されている。
インストールして起動すると、こんなウィンドウが表示される。
既に、試された方がいるようだ。
実は、まだ、理解できていない箇所があるので、再挑戦だ!
Core Graphics (Quartz)のデフォルト座標系は、左下が原点で、X軸は右方向、Y軸は上方向が正だ。
一方、UIKitのデフォルト座標系は、左上が原点で、X軸は右方向、Y軸は下方向が正となる。
おそらく、UIKitでは、UIGraphicsBeginImageContextWithOptions()を使った操作に近い手順でコンテキストを用意していると思われる。この関数で得られるコンテキストは、左上が原点で、X軸は右報告、Y軸は下方向が正となる。
ここからが、ややこしい部分だ。
UIKitを使っていれば、慣れた左上が原点の座標系となり、何も悩む事はない、と考えると失敗する。
例えば、CoreGraphicsの関数で文字列を描画する場合。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
CGContextSetFillColorSpace(context, cs);
CGColorSpaceRelease(cs);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
CGContextSelectFont(context, "Helvetica", 48.0, kCGEncodingMacRoman);
CGContextShowTextAtPoint(context, 10.0, 10.0, "Quartz", strlen("Quartz"));
CGContextFlush(context);
CGContextRestoreGState(context);
}
文字が反転してしまう。
その為、上記の例では、文字列を描画する際に、座標系をCoreGraphicsのデフォルトの座標系に戻す必要がある。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGFloat height = self.bounds.size.height;
CGContextTranslateCTM(context, 0.0, height);
CGContextScaleCTM(context, 1.0, - 1.0);
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
CGContextSetFillColorSpace(context, cs);
CGColorSpaceRelease(cs);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
CGContextSelectFont(context, "Helvetica", 48.0, kCGEncodingMacRoman);
CGContextShowTextAtPoint(context, 10.0, 10.0, "Quartz", strlen("Quartz"));
CGContextFlush(context);
CGContextRestoreGState(context);
}
だたし、この場合は、描画する位置を計算し直さないといけなくなる。そうでないと、上記の例のように上段に文字列を描画したかったのに、下段となってしまった。
今回のCocoa勉強会(関東)は、自分は夏の節電対策で土日平日となった関係で参加できなかった、新宿三丁目にある新宿伊藤ビルで開催された。
発表の内容をざっと説明すると、UIPageViewControllerと座標と描画、Objective-Cの新しいリテラル、デバッグTIPS等だ。今回も業界の動向をうかがい知る事が出来る等、有意義な勉強会だった。
今回で、いままでのまとめだ。自信がないが、自分が理解したことを整理してみる。
OS X/iOSの描画システム、Core Graphics (Quartz) フレームワークのデフォルト座標系は、左下が原点で、X軸は右方向、Y軸は上方向が正だ。これをApple Developerサイトの文書『iOS描画および印刷ガイド』内では、LLO (lower-left-origin)と呼んでいる。
iOSのUIKitフレームワークとCore Animationフレームワークは、変換行列(CTM : Current Transformation Matrix)を使ってデフォルト座標系は、左上が原点で、X軸は右方向、Y軸は下方向が正だ。これをApple Developerサイトの文書『iOS描画および印刷ガイド』内では、ULO (upper-left-origin)と呼んでいる。
おそらく、UIKitでは、以下の感じでdrawRect:を呼んでいると考えられる。
UIGraphicsBeginImageContextWithOptions
CGContextRef context = UIGraphicsGetCurrentContext();
[インスタンス drawRect:rect];
UIGraphicsEndImageContext
そして、おそらく、UIGraphicsBeginImageContextWithOptionsでは、以下の感じで、座標系を原点左上 (ULO) に設定していると考えられる。
CGContextTranslateCTM(graphicsContext, 0.0, rect.size.height);
CGContextScaleCTM(graphicsContext, 1.0, -1.0);
UIViewのサブクラスのdrawRect:で描画すれば、他のシステムで慣れた原点左上で悩まなくて済む!と考えると運が悪いと痛い目にあってしまう。
実験してみよう。
上記の画像を、drawRect:内でCore Graphics関数を使って描画してみる。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIImage *image = [UIImage imageNamed:@"upper-left-origin.png"];
CGRect imageRect;
imageRect.origin = CGPointMake(10.0, 10.0);
imageRect.size = image.size;
CGContextDrawImage(context, imageRect, image.CGImage);
}
描画される位置は、左上からの座標だが、画像が反転している!
これをどう考えるかだが、自分はこう理解した。
座標系がどのように変換されるのかは、意識されない。上記の例では、例えば、以下の動作をする。
その為、X軸の下方向が正の座標系だと、上下が逆さまの図が描画される。
こういう訳で、Core Graphicsの関数で、文字列や画像、PDFのような座標の向きを意識する必要がある描画をおこなう場合は、座標系をCoreGraphicsのデフォルト座標系に戻してやる必要がある。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGFloat height = self.bounds.size.height;
CGContextTranslateCTM(context, 0.0, height);
CGContextScaleCTM(context, 1.0, - 1.0);
CGContextRestoreGState(context);
}
ただし、上記の様に、どの位置に描画するのかを指定する場合は、それを考慮しないと、例では画面の上部でなく下部に表示される。
その為、例では描画位置を座標系の違いを考慮して計算してあげる必要がある。
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGFloat height = self.bounds.size.height;
CGContextTranslateCTM(context, 0.0, height);
CGContextScaleCTM(context, 1.0, - 1.0);
UIImage *image = [UIImage imageNamed:@"upper-left-origin.png"];
CGRect imageRect;
imageRect.origin = CGPointMake(10.0, self.bounds.size.height - 10.0 - image.size.height);
imageRect.size = image.size;
CGContextDrawImage(context, imageRect, image.CGImage);
CGContextRestoreGState(context);
}
この話、先日のCocoa勉強会で発表したのだが、チームで開発する場合は、どういうポリシーとするのかを決めておかないと、座標系の変換が混ざってしまって、混乱したソースコードになるという指摘を受けた。
例えば、外部インターフェースで使用する座標系は原点左上にして、左下に戻す等は、モジュール内部に隠蔽するとか、面倒なので、全てを左下原点にするとかだ。
KT Tunstallで有名になったループペダル。
簡単に説明すると、ある一定の間隔(数秒)に録音した音をループして再生する機械で、機種によっては、複数の録音した音声をループ再生する事が出来る。
音を遅れて再生するという機能からも、その操作方法からも、エコーやディレイを行うエフェクターの仲間になると思われる。
実際に、どのように実装するかは製品毎の大きく異なると思われるが、基本的な動きはテープエコーに似ているのではないかと考えている。
輪になっている録音再生用のテープを回し続ける。録音と再生のヘッドの位置が異なっているので、二つのヘッドが離れている距離分、音が遅れて聴こえてくる
この輪になっているテープの長さや録音と再生ヘッドの距離を変えると、ループする音の長さや遅れの時間が変わるという事になる。
Core Audioを使用する事になる。Core Audioは機能別に複数のフレームワークとサービスで構成されていて、それぞれに特徴があり、どれを利用すべきが悩むところなので、各フレームワーク/サービス毎にサンプル・コードを作成してみて、Looping Recorder機能を実装するのにむいているのはどれなのかを複数回の日記で検討してみようと思っている。
ざっと思い浮かんだのは以下のとおり。
ただし、即応性があるとか、複数の音声を同期して記録再生できるというのは、リアルタイムに動作するシステムと考えてしまうかもしれないが、見方を変えて、そう感じる動作であればいいと考えると、色々と、アイディアが出てくるのではないかと考えている。
ループペダルの実装に関係する、Core Audioの各フレームワーク/サービスの特徴を表にしてみた。
再生 | 録音 | 備考 | |
---|---|---|---|
System Sound Services | ○ | × | 30秒以下の音声ファイルの再生。 |
AVFoundation | ○ | ○ | iPhone OS 2.2以降から。録音はiPhone OS 3.0以降から。 |
Audio Queue Services | ○ | ○ | 音声データのバッファに対して録音再生。 |
Audio Unit | ○ | ○ | 低レベルAPI。 |
以降では、各フレームワーク/サービスを使ったサンプル・コードを作成してみ、どれがループペダルの実装に向いているか、検証してみたいと思っている。
GitHubにサンプルコードが試せるデモ・アプリケーションを用意した。
デモ・アプリケーションはタブ・バーでビューが切り替えられるようになっていて、各サンプル・コードは、タブに対応したビュー・コントローラ毎に実装されている。
今回は、ここまで。
30秒以下の音声ファイルを再生するサービスで、主に警告音や操作音に使用される。
追加するフレームワークは『AudioToolbox.framework』。インポートするヘッダ・ファイルは『AudioToolbox/AudioToolbox.h』だ。
beep.aifファイルを再生するコードは、以下のとおり。
NSString *path = [[NSBundle mainBundle] pathForResource:@"beep" ofType:@"aif"];
NSURL *fileURL = [NSURL fileURLWithPath:path];
SystemSoundID systemSoundID;
AudioServicesCreateSystemSoundID((CFURLRef)fileURL, &systemSoundID);
AudioServicesPlaySystemSound(systemSoundID);
beep.aif ファイルのパスから NSURL 変数を生成して、 AudioServicesCreateSystemSoundID() でNSURL 変数に関連づけられた SystemSoundID 変数を取得する。この SystemSoundID 変数を AudioServicesPlaySystemSound() に渡すと再生される。
一度、再生したら停止するのではなくて、ループ再生させたい場合は、再生の停止時に呼ばれるコールバック関数を設定すれば行える。
- (void)viewDidLoad
{
:
AudioServicesAddSystemSoundCompletion(self.systemSoundID,
NULL,
NULL,
MyAudioServicesSystemSoundCompletionProc,
self);
:
}
static void MyAudioServicesSystemSoundCompletionProc(SystemSoundID ssID, void *clientData)
{
SystemSoundServicesViewController *systemSoundServicesViewController
= (SystemSoundServicesViewController *)clientData;
:
AudioServicesPlaySystemSound(systemSoundServicesViewController.systemSoundID);
:
}
System Sound Services はループ再生をすることは出来るが、録音機能はなく、正確に指定された周期で、指定したタイミングに再生するという観点からは、ループペダルの実装は難しいと思われる。
Objective-Cベースのフレームワークであるという事から察せられるとおり、最上位層のAPIということになると思う。iOS 4からAVAssetと呼ばれる、複数の動画や音声データをひとかたまりのグループとして扱える機能が追加され、 iMovieのような編集ソフトがより簡単に作る事が出来るようになった思われる。ループペダル・アプリケーションでの利用という観点からも、複数の音声データを同期して再生するのに利用できるのではと期待できる。
追加するフレームワークは、『AVFoundation.framework』。インポートするヘッダ・ファイルは『AVFoundation/AVFoundation.h』だ。
デモ・アプリケーションでは、基本的な動作の確認という観点から、AVAssetを使用しない例と鳴っている。
まず、コントローラとなるクラスがデリゲートになるよう、(形式)プロトコルを設定する。
@interface AVFoundationViewController : UIViewController
<AVAudioPlayerDelegate, AVAudioRecorderDelegate, AVAudioSessionDelegate>
:
@end
4秒間、録音するコードは、以下のとおり。
NSArray *filePaths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDir = [filePaths objectAtIndex:0];
NSString *path = [documentDir stringByAppendingPathComponent:@"demoaudio.caf"];
NSURL *recordingURL = [NSURL fileURLWithPath:path];
:
NSError *error = nil;
AVAudioRecorder *recorder = [[AVAudioRecorder alloc] initWithURL:recordingURL
settings:nil error:&error];
:
recorder.delegate = self;
[recorder recordForDuration:4.0];
:
録音したデータの保存先となるdemoaudio.cafのパスからNSURL変数を生成し、AVAudioRecorderクラスの生成時に渡して、- (BOOL)recordForDuration:を呼べば、録音が開始される。
再生も同様だ。
NSError *error = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc]
initWithContentsOfURL:recordingURL error:&error];
:
player.delegate = self;
[player play];
:
サンプル・コードでは、AVAudioPlayerクラスの初期化時にNSURL変数を渡しているが、NSData変数を渡す方法もある。ファイル経由だと再生の頭に遅延が発生する事が懸念されるので、楽器であるループペダルという観点から、気になるところだ。そして、- (BOOL)playメソッドを呼べば再生が開始する。
AVFoundationを使用した方法は、今後のiOSのフレームワークの方向性を考えると、ループペダルの実装方法の候補としては有力だ。
ただし、気になるのは録音先がファイルであること。そして、録音と再生の同期。
上手く説明が出来ないが、もっと、そもそものループペダルの仕組みに物理的に近い(ソフトなのに変な表現だが)方式が、いいのではないかと思っている。
Audio Queue Servicesは、OS XとiOSの両方で使用できるサービスだ。アプリケーション側で管理するバッファに対して録音と再生を行う方式で、バッファ管理の手間が発生するが、その分、アプリケーション側で自由にバッファを扱えるという利点がある。
追加するフレームワークは、『AudioToolbox.framework』。インポートするヘッダ・ファイルは『AudioToolbox/AudioToolbox.h』だ。
デモ・アプリケーションでは、録音も再生も同じバッファを使用している。バッファの長さは、音声データを4秒間録音できるサイズになっていて、再生時、再生位置がバッファの末尾に到達したら、頭に戻るようにしている。
そう、テープエコーのループするテープの仕組みを模している!
以下がバッファの初期化のコードだ。
- (void)prepareBuffer
{
UInt32 bytesPerPacket = 2;
UInt32 sec = 4;
self.startingPacketCount = 0;
self.maxPacketCount = (44100 * sec);
self.buffer = malloc(self.maxPacketCount * bytesPerPacket);
}
4秒分の長さのバッファで、self.startingPacketCountは録音/再生位置を指し、初期化時は先頭を表す0に設定される。self.maxPackerCountは、バッファの長さで、バッファの末尾を指す変数となる。
Audio Queue Servicesを使用したサンプルには、Audio File Servicesを使って音声データの読み書きを行っている場合があるが、本稿では、独自に用意したバッファで音声データを管理するので、バッファに対する独自の読み書きメソッドが必要となる。
以下が、読み出しメソッドのコードだ。
- (void)readPackets:(AudioQueueBufferRef)inBuffer
{
UInt32 bytesPerPacket = 2;
UInt32 numPackets = self.maxPacketCount - self.startingPacketCount;
if (self.numPacketsToRead < numPackets) {
numPackets = self.numPacketsToRead;
}
if (0 < numPackets) {
memcpy(inBuffer->mAudioData,
(self.buffer + (bytesPerPacket * self.startingPacketCount)),
(bytesPerPacket * numPackets));
inBuffer->mAudioDataByteSize = (bytesPerPacket * numPackets);
inBuffer->mPacketDescriptionCount = numPackets;
self.startingPacketCount += numPackets;
}
else {
inBuffer->mAudioDataByteSize = 0;
inBuffer->mPacketDescriptionCount = 0;
}
}
self.numPacketsToRead単位で読み出す。読み出した分、self.startingPacketCountを進める。再生位置がバッファの末尾に到達したら、読み出したサイズを0に設定して、読み出し側に末尾に到達した事を伝える。
次が、書き込みメソッドのコードだ。
- (void)writePackets:(AudioQueueBufferRef)inBuffer
{
UInt32 bytesPerPacket = 2;
UInt32 numPackets = (inBuffer->mAudioDataByteSize / bytesPerPacket);
if ((self.maxPacketCount - self.startingPacketCount) < numPackets) {
numPackets = (self.maxPacketCount - self.startingPacketCount);
}
if (0 < numPackets) {
memcpy((self.buffer + (bytesPerPacket * self.startingPacketCount)),
inBuffer->mAudioData,
(bytesPerPacket * numPackets));
self.startingPacketCount += numPackets;
}
}
引数で渡されたinBufferのデータをバッファにコピーする。
録音機能について説明する。録音の前準備を行うコードだ。
- (void)prepareAudioQueueForRecord
{
AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = 44100.0;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger
| kLinearPCMFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 1;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 2;
audioFormat.mBytesPerFrame = 2;
audioFormat.mReserved = 0;
AudioQueueNewInput(&audioFormat, MyAudioQueueInputCallback,
self, NULL, NULL, 0, &__audioQueueObject);
self.startingPacketCount = 0;
AudioQueueBufferRef buffers[3];
self.numPacketsToWrite = 1024;
UInt32 bufferByteSize = self.numPacketsToWrite * audioFormat.mBytesPerPacket;
int bufferIndex;
for (bufferIndex = 0; bufferIndex < 3; bufferIndex++) {
AudioQueueAllocateBuffer(self.audioQueueObject,
bufferByteSize, &buffers[bufferIndex]);
AudioQueueEnqueueBuffer(self.audioQueueObject,
buffers[bufferIndex], 0, NULL);
}
}
変数audioFormatに録音する音声データの形式を設定する。サンプルは、44.1kH/16ビット/モノラのリニアPCMの例となっている。
Audio Queue Servicesでは、コールバック関数を使って、バッファ・キューの管理を行うが、 AudioQueueNewInput()でコールバック関数を指定する。バッファ・キューはアプリケーション側で生成するが、サンプルはキューのバッファの個数は3個となっている。
簡単に説明すると、録音されたデータはキューで指しているバッファに書き込まれ、バッファが一杯になるとコールバック関数を呼ぶので、そこで、アプリケーションで管理している4秒分のバッファにコピーする、ということになる。
次が、そのコールバック関数のコードだ。
static void MyAudioQueueInputCallback(
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
UInt32 inNumberPacketDescriptions,
const AudioStreamPacketDescription *inPacketDescs)
{
AudioQueueServicesViewController *viewController
= (AudioQueueServicesViewController *)inUserData;
[viewController writePackets:inBuffer];
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
if (viewController.maxPacketCount <= viewController.startingPacketCount) {
[viewController stop:nil];
}
}
先ほど説明した、 witePackets:メソッドで録音したデータをバッファにコピーしている。サンプルでは、録音位置がバッファの末尾に到達したら、録音を停止している。
この後、AudioQueueStart() を呼ぶと、録音は開始される。
OSStatus err = AudioQueueStart(self.audioQueueObject, NULL);
次に再生の前準備を行うコードを説明する。
- (void)prepareAudioQueueForPlay
{
AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = 44100.0;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger
| kLinearPCMFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 1;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 2;
audioFormat.mBytesPerFrame = 2;
audioFormat.mReserved = 0;
AudioQueueNewOutput(&audioFormat, MyAudioQueueOutputCallback,
self, NULL, NULL, 0, &__audioQueueObject);
self.startingPacketCount = 0;
AudioQueueBufferRef buffers[3];
self.numPacketsToRead = 1024;
UInt32 bufferByteSize = self.numPacketsToRead * audioFormat.mBytesPerPacket;
int bufferIndex;
for (bufferIndex = 0; bufferIndex < 3; bufferIndex++) {
AudioQueueAllocateBuffer(self.audioQueueObject,
bufferByteSize, &buffers[bufferIndex]);
MyAudioQueueOutputCallback(self, self.audioQueueObject,
buffers[bufferIndex]);
}
}
録音したデータを再生するので、音声データの形式は録音と同じとなる。
AudioQueueNewOutput()関数で、再生するデータを設定するコールバック関数を登録する。
キューで管理するバッファの個数も録音と同じ3個だ。
次が、再生用のコールバック関数のコードだ。
static void MyAudioQueueOutputCallback(
void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer)
{
AudioQueueServicesViewController *viewController
= (AudioQueueServicesViewController *)inUserData;
[viewController readPackets:inBuffer];
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
if (viewController.maxPacketCount <= viewController.startingPacketCount) {
viewController.startingPacketCount = 0;
}
}
コードの流れは、録音用コールバック関数と似ているが、異なるのは再生位置がバッファの末尾に到達したら、再生位置を0、つまり先頭に戻している。この為、ループ再生されます。
この後、AudioQueueStart()を呼ぶと、再生は開始される。
OSStatus err = AudioQueueStart(self.audioQueueObject, NULL);
デモ・アプリケーションでは、録音するデータは1個で、再生も1個。また、再生と録音は別々に行っているが、内部バファを複数持たせ、再生用コールバックで合成すれば、複数の音声データをずれなく再生できると思われる。
PaintCodeは、App Storeで、2012年3月26日時点で、6,900円で販売しているアプリケーションだ。
スクリーンショットと、アプリケーションの説明文のみからの情報だが、このアプリケーションを使った描いた画像は、OS XとiOSの描画コードに変換され、自分のアプリケーションで利用できるというもののようだ。
以前から欲しかった機能なので、機械があれば使用してみたい。
なお、アプリケーションのサイトからは評価版をダウンロードできるので、購入前に機能を確認する事が出来るようだ。
Audio UnitはCore Audioを拡張するPlug-insの事で、Audio Unitを使用すれば、音の生成や変換等の機能が利用できる。
追加するフレームワークは、『AudioUnit.framework』。インポートするヘッダ・ファイルは『AudioUnit/AudioUnit.h』だ。
今回は、録音部分についてチャレンジだ!
ループペダルの実装には、録音と再生のためのAudio Unitを別々用意することになると思うが、扱いが楽になるという事と、将来の拡張を考え、Audio Unit Processing Graph Servicesを利用する事にする。
以下が初期化のコードだ。
- (AudioStreamBasicDescription)canonicalASBDSampleRate:(Float64)sampleRate channel:(UInt32)channel
{
AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = sampleRate;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagsCanonical;
audioFormat.mChannelsPerFrame = channel;
audioFormat.mBytesPerPacket = sizeof(AudioSampleType) * channel;
audioFormat.mBytesPerFrame = sizeof(AudioSampleType) * channel;
audioFormat.mFramesPerPacket = 1;
audioFormat.mBitsPerChannel = 8 * sizeof(AudioSampleType);
audioFormat.mReserved = 0;
return audioFormat;
}
- (void)prepareAUGraph
{
AUNode remoteIONode;
AudioUnit remoteIOUnit;
NewAUGraph(&__auGraph);
AUGraphOpen(self.auGraph);
AudioComponentDescription cd;
cd.componentType = kAudioUnitType_Output;
cd.componentSubType = kAudioUnitSubType_RemoteIO;
cd.componentManufacturer = kAudioUnitManufacturer_Apple;
cd.componentFlags = 0;
cd.componentFlagsMask = 0;
AUGraphAddNode(self.auGraph, &cd, &remoteIONode);
AUGraphNodeInfo(self.auGraph, remoteIONode, NULL, &remoteIOUnit);
UInt32 flag = 1;
AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flag, sizeof(flag));
AudioStreamBasicDescription audioFormat = [self canonicalASBDSampleRate:44100.0 channel:1];
AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &audioFormat, sizeof(AudioStreamBasicDescription));
AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioFormat, sizeof(AudioStreamBasicDescription));
AUGraphInitialize(self.auGraph);
}
RemoteIOユニットでマイクを開く。その際、入力がモノラルで、出力がステレオだと、片方から再生となってしまう為、出力をモノラルに合わせている。
recordボタンを押下されるとレンダリングを開始し、レンダリング通知関数を登録して、録音されたデータを受け取れるようにする。
- (IBAction)record:(id)sender
{
if (self.isRecording) return;
AUGraphStart(self.auGraph);
AUGraphAddRenderNotify(self.auGraph, MyAURenderCallack, NULL);
self.isRecording = YES;
}
レンダリング通知関数では、今な何もしていない。
static OSStatus MyAURenderCallack(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData)
{
DBGMSG(@"%s, inNumberFrames:%u", __func__, (unsigned int)inNumberFrames);
return noErr;
}
次回は、レンダリング通知関数に渡されたデータをリングバッファに書き込み、これをループ再生させる予定だ。
本日、新しい有料アプリケーションBitz NowPlayingをiTunes Storeからリリースしました。 iOS機器で再生している曲をtweetするアプリケーションです。 もし、望まれるのでしたら、ミシュランのように三ツ星印を追加する事で出来ます。 缶コーヒー一杯弱のお値段で貴方の音楽生活が充実するかも!?
そして、早速、機能改善したVer.1.0.1をiTCにアップロードしました。 次のバージョンもお楽しみに。
そもそも、録音のコールバック関数に渡されるデータは何なのだろうか?
型は以下のとおり。
static OSStatus MyAURenderCallack(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData);
typedef struct AudioBufferList {
UInt32 mNumberBuffers;
AudioBuffer mBuffers[1];
} AudioBufferList;
typedef struct AudioBuffer {
UInt32 mNumberChannels;
UInt32 mDataByteSize;
void* mData;
} AudioBuffer;
AudioBuffer構造体のmDataメンバー変数は、AudioUnitSampleType型だ。
typedef SInt32 AudioUnitSampleType;
OS XのAudio UnitではFloat32型という情報があったが、Xcodeの文書では、SInt32となっていた。サイズは同じで、それをどう使うのかアプリ次第なので、SInt32に統一されたのだろうか?ちなみに、iOSでは8.24固定小数点だそうだ。
先日の録音時に呼ばれるコールバック関数のデバッグ出力をもう少し詳細にしてみた。
static OSStatus MyAURenderCallack(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData)
{
DBGMSG(@"%s, inNumberFrames:%u", __func__, (unsigned int)inNumberFrames);
DBGMSG(@"ioData: mNumberBuffers(%u)", (unsigned int)ioData->mNumberBuffers);
DBGMSG(@"ioData->mBuffers: mNumberChannels(%u), mDataByteSize(%u)",
(unsigned int)ioData->mBuffers->mNumberChannels,
(unsigned int)ioData->mBuffers->mDataByteSize);
return noErr;
}
エミュレータでの出力結果は以下のとおり。
2012-03-30 00:56:22.237 DemoAudio[1566:12307] MyAURenderCallack, inNumberFrames:512
2012-03-30 00:56:22.239 DemoAudio[1566:12307] ioData: mNumberBuffers(1)
2012-03-30 00:56:22.240 DemoAudio[1566:12307] ioData->mBuffers: mNumberChannels(2), mDataByteSize(2048)
次回は、これをアプリケーションが独自に持っているリングバッファにどう書き込むかだ。
iOS 5から、UIPageViewControllerが追加された。
書籍を模した、ページめくりを行うアプリケーションのUIの実装をサポートするクラスだ。
これを使ったサンプル・コードの作成に挑戦したいと思っているが、今回は紹介のみ。これの使い方を学ぶには、XcodeでPage-Based Applicationの雛形が用意されているので、雛形として生成されるプロジェクトを調べる事が、今のところ近道のようだ。
進みが遅くて申し訳ないが、自分自身も確認しながらなので、一歩ずつ進めさせて欲しい。
サンプルでは、オーディオデータフォーマットを設定する関数として、『iPhone Core Audioプログラミング』の説明を参考にして、この本で説明されたAudio Unit正準形とオーディオ正準形の2つに対応したものを用意している。Audio Unit正準形は非インターリーブ、オーディオ正準形はインターリーブ。
録音のコールバック関数に渡されるデータについて確認して行こう。前回から少し出力内容を変更したが、以下がシミュレータで録音時に印字されるデバッグ出力だ。
2012-04-01 01:45:33.482 DemoAudio[7045:10703] -[AudioUnitViewController record:]
2012-04-01 01:45:34.925 DemoAudio[7045:17503] MyAURenderCallack, inNumberFrames:512
2012-04-01 01:45:34.926 DemoAudio[7045:17503] ioData: mNumberBuffers(1)
2012-04-01 01:45:34.928 DemoAudio[7045:17503] ioData->mBuffers[0]: mNumberChannels(2), mDataByteSize(2048)
inNumberFramesが記録されたサンプル数で、44.1kHzでは1秒間で44100個×チャンネル数となる。上記では512となっているので、1秒間に44100÷512≒約86回、コールバック関数が呼ばれるということになる。
オーディオデータフォーマットは、Audio Unit正準形に設定してる。なので、mNumberBuffersが1なのでモノラルということになるのか?
サンプルは、32ビットなので、4byte×512サンプル=2048。mDataByteSizeの値と一致している。
ただし、mNumberChannelsが2だ。文書によると非インターリーブの場合1なので、インターリーブなのか?
申し訳ない。続きは次回まで。