[iOS]Looping Recorderの基礎(Audio Queue Services)
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個。また、再生と録音は別々に行っているが、内部バファを複数持たせ、再生用コールバックで合成すれば、複数の音声データをずれなく再生できると思われる。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub
関連情報
Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
iPhone Core Audioプログラミング(永野 哲久 著)
とれも参考にさせていただきました。
[iOS]Looping Recorderの基礎(AVFoundation)
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のフレームワークの方向性を考えると、ループペダルの実装方法の候補としては有力だ。
ただし、気になるのは録音先がファイルであること。そして、録音と再生の同期。
上手く説明が出来ないが、もっと、そもそものループペダルの仕組みに物理的に近い(ソフトなのに変な表現だが)方式が、いいのではないかと思っている。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub
関連情報
Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
[iOS]Looping Recorderの基礎(System Sound Services)
30秒以下の音声ファイルを再生するサービスで、主に警告音や操作音に使用される。
追加するフレームワークは『AudioToolbox.framework』。インポートするヘッダ・ファイルは『AudioToolbox/AudioToolbox.h』だ。
beep.aifファイルを再生するコードは、以下のとおり。
NSString *path = [[NSBundle mainBundle] pathForResource:@"beep" ofType:@"aif"];
NSURL *fileURL = [NSURL fileURLWithPath:path];
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 はループ再生をすることは出来るが、録音機能はなく、正確に指定された周期で、指定したタイミングに再生するという観点からは、ループペダルの実装は難しいと思われる。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub
関連情報
Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
[iOS][Core Audio]Looping Recorderの基礎(その壱)
KT Tunstallで有名になったループペダル。
簡単に説明すると、ある一定の間隔(数秒)に録音した音をループして再生する機械で、機種によっては、複数の録音した音声をループ再生する事が出来る。
音を遅れて再生するという機能からも、その操作方法からも、エコーやディレイを行うエフェクターの仲間になると思われる。
その仕組みは?
実際に、どのように実装するかは製品毎の大きく異なると思われるが、基本的な動きはテープエコーに似ているのではないかと考えている。
輪になっている録音再生用のテープを回し続ける。録音と再生のヘッドの位置が異なっているので、二つのヘッドが離れている距離分、音が遅れて聴こえてくる
この輪になっているテープの長さや録音と再生ヘッドの距離を変えると、ループする音の長さや遅れの時間が変わるという事になる。
iOSでのオーディオ処理
Core Audioを使用する事になる。Core Audioは機能別に複数のフレームワークとサービスで構成されていて、それぞれに特徴があり、どれを利用すべきが悩むところなので、各フレームワーク/サービス毎にサンプル・コードを作成してみて、Looping Recorder機能を実装するのにむいているのはどれなのかを複数回の日記で検討してみようと思っている。
ループペダルに求められる機能
ざっと思い浮かんだのは以下のとおり。
- 即応性がある。
- 複数を音声を同期して録音再生できる。
- 同期して録音再生する音声を随時変更できる。
- 記録再生する音の長さが調節できる。
ただし、即応性があるとか、複数の音声を同期して記録再生できるというのは、リアルタイムに動作するシステムと考えてしまうかもしれないが、見方を変えて、そう感じる動作であればいいと考えると、色々と、アイディアが出てくるのではないかと考えている。
Core Audioについて
ループペダルの実装に関係する、Core Audioの各フレームワーク/サービスの特徴を表にしてみた。
再生 録音 備考
System Sound Services ○ × 30秒以下の音声ファイルの再生。
AVFoundation ○ ○ iPhone OS 2.2以降から。録音はiPhone OS 3.0以降から。
Audio Queue Services ○ ○ 音声データのバッファに対して録音再生。
Audio Unit ○ ○ 低レベルAPI。
以降では、各フレームワーク/サービスを使ったサンプル・コードを作成してみ、どれがループペダルの実装に向いているか、検証してみたいと思っている。
サンプルコードについて
GitHubにサンプルコードが試せるデモ・アプリケーションを用意した。
デモ・アプリケーションはタブ・バーでビューが切り替えられるようになっていて、各サンプル・コードは、タブに対応したビュー・コントローラ毎に実装されている。
今回は、ここまで。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/DemoAudio - GitHub
関連情報
Cocoa Life KOF2011特別編 - Facebook
Cocoa勉強会 関西の会誌。
[iOS]座標と描画(その6)
今回で、いままでのまとめだ。自信がないが、自分が理解したことを整理してみる。
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();
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);
}
描画される位置は、左上からの座標だが、画像が反転している!
これをどう考えるかだが、自分はこう理解した。
座標系がどのように変換されるのかは、意識されない。上記の例では、例えば、以下の動作をする。
- ペンを、(10, 10)に移動する。
- (10, 10)から(20, 60)にペンを走らせる。
- (20, 60)から(110, 10)にペンを走らせる。
その為、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勉強会で発表したのだが、チームで開発する場合は、どういうポリシーとするのかを決めておかないと、座標系の変換が混ざってしまって、混乱したソースコードになるという指摘を受けた。
例えば、外部インターフェースで使用する座標系は原点左上にして、左下に戻す等は、モジュール内部に隠蔽するとか、面倒なので、全てを左下原点にするとかだ。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Coordinate - GitHub
関連情報
Programming with Quartz: 2D and PDF Graphics in Mac OS X
Drawing and Printing Guide for iOS
Apple Developerの情報です。
[Mac][iOS]第51回Cocoa勉強会(関東)
今回のCocoa勉強会(関東)は、自分は夏の節電対策で土日平日となった関係で参加できなかった、新宿三丁目にある新宿伊藤ビルで開催された。
発表の内容をざっと説明すると、UIPageViewControllerと座標と描画、Objective-Cの新しいリテラル、デバッグTIPS等だ。今回も業界の動向をうかがい知る事が出来る等、有意義な勉強会だった。
関連情報
[cocoa][勉強会]第51回 Cocoa勉強会に行ってきた(3/17)
[cocoa]Cocoa勉強会#51 発表 - clang trunkの新しいリテラル
[iOS]座標と描画(その6)
[iOS]座標と描画(その5)
実は、まだ、理解できていない箇所があるので、再挑戦だ!
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);
}
だたし、この場合は、描画する位置を計算し直さないといけなくなる。そうでないと、上記の例のように上段に文字列を描画したかったのに、下段となってしまった。
ソースコード
GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Coordinate - GitHub
関連情報
Programming with Quartz: 2D and PDF Graphics in Mac OS X
Drawing and Printing Guide for iOS
Apple Developerの情報です。
[Mac][iOS]Apple Configurator
複数台のiPhoneやiPad、iPod touchを一括設定できるアプリケーションが、Mac App Storeで公開されている。
インストールして起動すると、こんなウィンドウが表示される。
既に、試された方がいるようだ。
Apple Configuratorを使ってみよ(設定編) - 日々是笑心