AVAudioSession細かいことまとめ(late 2014)

以前も書きました「AudioSessionの細かいことまとめ」のlate 2014版を書きます。

個人的なあらすじ

以前アプリを作る際に、AudioSession(C言語)とAVAudioSession(Obj-C)のどちらを使おうかと検討したことがありました。そのときはAVAudioSession(Obj-C)の方に機能が足りない部分が多少あって、これは主流にならないのではと思い、AudioSession(C言語)の方で実装しました。しかし予想は外れ、AudioSession(C言語)の方がiOS7より非推奨となったので、今回改めてAVAudioSession(Obj-C)の方でまとめてみます。せっかくなのでSwiftの方も併記して書いてみようと思います。基本的にAVAudioSessionのクラスリファレンスを読んでまとめるだけなので詳しく知りたいことがある場合はそちらを見て下さい。
ちなみに作っているアプリはSilentBonusTrackというカセットテープアプリで、iPodのような音楽再生機能が中心となっています。私の興味の対象はその辺の音楽再生系が中心になります。

重要な項目

まずは音を扱う場合には目を通しておく必要があるAudioSession関係です。

importするもの

//Swift
import AVFoundation
//Objective-C
#import <AVFoundation/AVFoundation.h>

インスタンスの取得

このインスタンスに対して音の扱いに関するいろいろなメソッドを実行出来ます。
//Swift
let session: AVAudioSession = AVAudioSession.sharedInstance()
//Objective-C
AVAudioSession  *audioSession = [AVAudioSession sharedInstance];
Swiftの方はもしvarにする必要があればvarにしてください。

カテゴリーの指定

アプリの音の扱い方法を数パターンの中から選んで指定します。
//Swift
var error: NSError?
session.setCategory(AVAudioSessionCategoryPlayback,error:&error)
//Objective-C
NSError *error = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayback error:&error];
このメソッドではエラー情報をセットする変数を指定します。これはnilでもかまわないそうです。SwiftではNSErrorのオプショナル型を定義してそのアドレス(という言い方でいいのか?)を渡します。SwiftのこのsetCategoryメソッドのエラーの引数の型は定義によるとNSErrorPointerというものなので、直接NSErrorPointerインスタンスを作成して引数として渡すやり方もいけそうな気がしましたがエラーが出ます。ポインタの性質を持つだけに通常の型と性質が違うものなんでしょう。
この先、メソッドでエラーを渡す場合はnilとします。

使用出来るカテゴリー
AVAudioSessionCategoryAmbient
AVAudioSessionCategorySoloAmbient
AVAudioSessionCategoryPlayback
AVAudioSessionCategoryRecord
AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryAudioProcessing
AVAudioSessionCategoryMultiRoute

アクティブ化、非アクティブ化

//Swift
session.setActive(true, error:nil)
//Objective-C
[audioSession setActive:YES error:nil];

割り込みなど

割り込み処理や出力経路の変更の通知はNotificationという機能を使います。Notificationの説明は省きます。今までは、いろいろなやり方がありましたが、Notificationの方に統一(?)されるようです。重要なものを抜粋すると
AVAudioSessionInterruptionNotification : 割り込みを通知する
AVAudioSessionRouteChangeNotification : 音声信号の経路変化を通知する
AVAudioSessionMediaServicesWereResetNotification : メディアサーバというものがリセットされた時に通知する
それぞれ見ていきます。

AVAudioSessionInterruptionNotification

userInfoに入っている内容をAVAudioSessionInterruptionTypeKeyで取り出します。これにより割り込みの開始か終了かの情報を取得します。

//Swift Notification登録
NSNotificationCenter.defaultCenter().addObserver(self, selector: "audioSessionInterrupted:", name: "AVAudioSessionInterruptionNotification", object: nil)
//Swift 呼び出されるメソッド
 func audioSessionInterrupted(notification: NSNotification) {
  //println("Interrupt")
  
  if let userInfos = notification.userInfo {
   if let type: AnyObject = userInfos["AVAudioSessionInterruptionTypeKey"] {
    if type is NSNumber {
      if type.unsignedLongValue == AVAudioSessionInterruptionType.Began.rawValue{
       //println("Began")
      }
      if type.unsignedLongValue == AVAudioSessionInterruptionType.Ended.rawValue{
       //println("Ended")
      }
    }
   }
  }
 }
ちょっと長くなりましたがひとつづつ処理を書きました。NSNumberに入っている値はObj-CだとNSUIntegerなのでSwiftも同じように「32と64の環境によるビット数自動切り替わり符号なし整数」だと仮定しました。ところがSwiftのunsignedIntegerValueで取り出そうとするとこれはUIntではなくIntを返します(なぜ?)。unsignedIntegerValueを返すものはSwiftのNSNumberにはunsignedLongValueしかありませんのでそれを使いました。

//Objective-C Notification登録
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionInterrupted:) name:AVAudioSessionInterruptionNotification object:nil];
//Objective-C 呼び出されるメソッド
- (void)audioSessionInterrupted:(NSNotification *)notification {
 NSNumber *interruptType = [notification.userInfo objectForKey:@"AVAudioSessionInterruptionTypeKey"];
 if ([interruptType unsignedIntegerValue] == AVAudioSessionInterruptionTypeBegan){
  //割り込み開始時の処理

 }else if([interruptType unsignedIntegerValue] == AVAudioSessionInterruptionTypeEnded){
  //割り込み終了時の処理

 }
}
通知センターへの登録解除も適切なタイミングで行います。

AVAudioSessionRouteChangeNotification

扱う可能性のある情報は
・変化の理由
・変化前の状態
・変化後の状態(現在の状態)
の3つです。
userInfoに入っている内容からAVAudioSessionRouteChangeReasonKeyで経路変化の理由を取り出し、AVAudioSessionRouteChangePreviousRouteKeyで変化前の状態を取り出します。
現在の状態を取得したいかと思いますが、これはuserInfoではなくAudioSessionインスタンスのcurrentRouteプロパティで取り出せます。

変化前の状態と変化後の状態はどちらもAVAudioSessionRouteDescriptionというクラスで与えられます。これは
AVAudioSessionRouteDescriptionとは
  NSArray *inputs (AVAudioSessionPortDescriptionの配列)
    AVAudioSessionPortDescriptionとは
      NSString *portName
      NSString *portType
      NSArray *channels (AVAudioSessionChannelDescriptionの配列)
        AVAudioSessionChannelDescriptionとは
          NSString *channelName
          NSUInteger channelNumber
          NSString *owningPortUID
          AudioChannelLabel channelLabel (AudioChannelLabelはUInt32)
       NSString *UID
  NSArray *outputs (AVAudioSessionPortDescriptionの配列)
    inputsと構成は同じ
という複雑な構成になっています。inputとoutputそれぞれにportがいくつかあって、それぞれのportがいくつかのチャンネルを持ちます。

具体的な処理はそれぞれのアプリで違うと思うので骨組みだけ書きます。
//Swift Notification登録
NSNotificationCenter.defaultCenter().addObserver(self, selector: "audioSessionRouteChange:", name: "AVAudioSessionRouteChangeNotification", object: nil)
//Swift 呼び出されるメソッド
 func audioSessionRouteChange(notification: NSNotification) {
  if let userInfos = notification.userInfo {
   if let type: AnyObject = userInfos["AVAudioSessionRouteChangeReasonKey"] {
    if type is NSNumber {
     if type.unsignedLongValue == AVAudioSessionRouteChangeReason.NewDeviceAvailable.rawValue{
      println("NewDeviceAvailable")
     }
     if type.unsignedLongValue == AVAudioSessionRouteChangeReason.Override.rawValue{
      println("Override")
     }
    }
   }
  }
  for port in session.currentRoute.outputs as [AVAudioSessionPortDescription] {
   //println(port.portName)
   //println(port.portType)
   //println(port.UID)
   if port.portType == AVAudioSessionPortBuiltInSpeaker {
    //内臓スピーカが選ばれている時の処理
    //println("スピーカ")
   }else if port.portType == AVAudioSessionPortHeadphones {
    //ヘッドホンが選ばれている時の処理
    //println("ヘッドホン")
   }
   for channel in port.channels as [AVAudioSessionChannelDescription] {
    //左右チャンネルなどの情報が欲しいとき、以下を検討
    //println(channel.channelName)
    //println(channel.channelNumber)
    //println(channel.owningPortUID)
    //println(channel.channelName)
   }
  }
 }
//Objective-C Notification登録
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioSessionRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil];
//Objective-C 呼び出されるメソッド
- (void)audioSessionRouteChange:(NSNotification *)notification {
 NSNumber *reason = [notification.userInfo objectForKey:@"AVAudioSessionRouteChangeReasonKey"];
 if ([reason unsignedIntegerValue] == AVAudioSessionRouteChangeReasonNewDeviceAvailable){
  //NSLog(@"NewDevice");
 }else if([reason unsignedIntegerValue] == AVAudioSessionRouteChangeReasonOverride){
  //NSLog(@"Override");
 }
 
 for(AVAudioSessionPortDescription *port in audioSession.currentRoute.outputs){
  //現在の出力装置をひとつひとつ取り出す
  //NSLog(@"%@",port.portName);
  //NSLog(@"%@",port.portType);
  //NSLog(@"%@",port.UID);
  if([port.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]){
   //内臓スピーカが選ばれている時の処理
   //NSLog(@"内臓スピーカ");
  }else if([port.portType isEqualToString:AVAudioSessionPortHeadphones]){
   //ヘッドホンが選ばれている時の処理
   //NSLog(@"ヘッドホン");
  }
  for(AVAudioSessionChannelDescription *channel in port.channels){
   //左右チャンネルなどの情報が欲しいとき、以下を検討
   //NSLog(@"%@",channel.channelName);
   //NSLog(@"%lu",(unsigned long)channel.channelNumber);
   //NSLog(@"%@",channel.owningPortUID);
   //NSLog(@"%u",channel.channelLabel);//UInt32の最大数が返ってくる
  }
 }
}

AVAudioSessionMediaServicesWereResetNotification

メディアサーバは共有サーバプロセスの形で、音声その他のマルチメディア機能を提供するそうです。システムやそれぞれのアプリのオーディオ状態を管理するまとめ役みたいなもんでしょうか。それがリセットさせたときにこの通知を受けます。これを受けた際に必要な処理は、
  • 孤児状態になっている音声オブジェクトを破棄し、新たに音声オブジェクトを生成する
  • 内部的に追跡している音声の状態をリセットする(AVAudioSessionのプロパティもすべて対象)
  • 妥当であれば、setActive:error:メソッドでAVAudioSessionを作動し直す
また、アプリケーションは、AVAudioSessionの通知を配送するよう登録し直さなくても構いません。また、AVAudioSessionプロパティのキー値監視をリセットする必要もありません、とのこと。
説明によるといれることが必須のような雰囲気があります。入れておいたほうがよいでしょう。ただ、漠然とした初期化処理をするのみとなってしまうと思います。

以上必須の項目でした。自分で音声を扱う場合には設定が必要です。例外はSystem Sound Services、またはUIKitのplayInputClickメソッド(文字入力時の効果音を扱う)だけで音声を処理している場合とのことです。

今までの方法で非推奨になったものが結構あります。
  • C言語によるAudioSession。これは初期化時に割り込み処理関数を指定するやり方などがありました。代わりにAVAudioSessionを使用して下さい。
  • AVAudioPlayerのdelegateプロパティを使って割り込み処理をするやり方。これは割り込みが行われた時に呼びだされる関数の名前が決まっていて、その関数を実装しているインスタンス(クラス)をdelegataに指定してやるという方法がありました。代わりにNotificationを使用して下さい。ちなみにdelegateプロパティ自体はまだ残っていて、再生が完了した時にメソッドを呼び出すのに使ったりします。
  • AVAudioSessionのdelegateプロパティとそのdelegateに任せていたメソッドbeginInterruptionなどがありました。代わりにNotificationを使用して下さい。
  • AVAudioSessionにあった情報取得設定系メソッドの中に別のメソッドに名前が変わったものがあります。例えばcurrentHardwareInputNumberOfChannelsはinputNumberOfChannelsに変わっています。単に名前を修正したかっただけでしょうか。

Swiftの仕様をまだ全部読んでいないので何か間違いがあるかもしれません。



必要に応じて検討する項目

ここからは必要に応じて検討する項目です。

カテゴリーとカテゴリーオプションの指定

カテゴリーと同時にカテゴリーオプションという項目を使うと追加の情報を与えることが出来ます。
session.setCategory(AVAudioSessionCategoryPlayback,withOptions:AVAudioSessionCategoryOptions.MixWithOthers,error:nil)
[audioSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
withOptionsには一度に複数のものを設定出来ます。与える値はビットORを実行して与えるやり方です。全部リセットしたいというときは0を与えればいいのですが、Swiftのほうは型の決まりがあるのでAVAudioSessionCategoryOptions.allZerosというあらかじめ定義されているものを使います。Obj-Cのほうはこれにあたる値が定義されていないので直接0を与えるしかないようです。
あるオプションだけを制御したいが現在の値が何かわからないときはビット演算することになると思います。

使用できるカテゴリーオプション(Swift 途中にドットがあります)
AVAudioSessionCategoryOptions.AllowBluetooth
AVAudioSessionCategoryOptions.DefaultToSpeaker
AVAudioSessionCategoryOptions.DuckOthers
AVAudioSessionCategoryOptions.MixWithOthers
AVAudioSessionCategoryOptions.allZeros

使用できるカテゴリーオプション(Objective C)
AVAudioSessionCategoryOptionMixWithOthers
AVAudioSessionCategoryOptionDuckOthers
AVAudioSessionCategoryOptionAllowBluetooth
AVAudioSessionCategoryOptionDefaultToSpeaker

モード

カテゴリーやオプションによってオーディオの挙動を指定しますが、さらにその他にモードというものがあります。どれも挙動を指定するという役割は変わらないのですがiOSが多機能になるにしたがって、つぎはぎになっているのではないかと思います。
session.setMode(AVAudioSessionModeDefault, error: nil)
[audioSession setMode:AVAudioSessionModeDefault error:nil];

使用できるモード
AVAudioSessionModeDefault
AVAudioSessionModeVoiceChat
AVAudioSessionModeGameChat
AVAudioSessionModeVideoRecording
AVAudioSessionModeMeasurement
AVAudioSessionModeMoviePlayback
AVAudioSessionModeVideoChat

バックグラウンドにしてからも音を鳴らし続けたい

ソースコードではなく、ターゲットの設定で行います。
ターゲット -> Capabilities -> Background ModesをONにして、その中のAudio and AirPlayにチェックをいれます。info.plistの方も連動して書かれるようです。

情報Set系メソッドにおけるPreferredの意味

設定した値が必ず即座に反映されるのではなく、その値を設定するのにふさわしい(or差し支えない)状況と内部で判断されると反映されます。

MixWithOthersの謎

以前も書きましたが、mixWithOthersに関する疑問があります。
アプリAを、カテゴリー:PlaybackでmixWithOthers:ON
アプリBを、カテゴリー:SoloAmbient
のとき、アプリAで音(iPodの音楽をsystemMusicPlayerで)を出してから閉じて(音は鳴り続ける)Bを立ちあげてBの音を鳴らしてもAの音が消えません。BはSoloAmbientなので他のアプリの音を消すはずですが…。これはバグなのか私の勘違いなのか。



AudioSessinプログラミングガイドについて

2014//11/13に日本語版が新しくなりましたが、古い内容を新しく書き直す必要がある部分が幾つか残っています。

オーディオセッションを初期化する

(誤)音声割り込みを処理する際には、AVAudioSessionクラスの割り込み通知、またはAVAudioPlayerクラスと AVAudioRecorderクラスのデリゲートプロトコルを使って、自動初期化の仕組みを活用するとよいでしょう。
(正)音声割り込みを処理する際はNotificationを使って下さい。
英文でも書いてあるので英文を直さないといけない

オーディオセッションを作動/停止する

(誤)望ましいハードウェア値の設定に先だっておこないます
(正)望ましいハードウェア値の設定を先だっておこないます
英文は正しいので日本語訳のミス

音声処理技術に応じた音声割り込みの処理方法のAV Foundationフレームワーク

(誤)AVAudioPlayerクラスとAVAudioRecorderクラスは、割り込みの開始と 終了に関するデリゲートメソッドを備えています。ユーザインターフェイスを更新し、割り込み終了後、必要であれば一時停止していた再生を再開 するためにこれらのメソッドを実装します。
(正)割り込みの開始と 終了に関するデリゲートメソッドは非推奨になりました。適切なNSNotification通知を配送するよう登録してください
英文でも書いてあるので英文を直さないといけない

AVAudioPlayerクラスによる音声割り込みの処理

全体的に間違っています。ちなみにdelegate自体は残っています。再生終了時に呼び出すデリゲートメソッドなどは今もあります。

コメント