瞑想や学習向けの音声を再生する小さなアプリを個人開発で出したとき、最初に届いた不具合報告が「電話が来た後、音が戻ってこない」というものでした。再生中に着信があると音が止まるのは当然なのですが、通話が終わってもアプリは止まったまま、画面の再生ボタンは「再生中」の見た目のまま固まっていたのです。
原因は、AVAudioSession の中断を一切ハンドリングしていなかったことでした。再生は AVAudioPlayer に任せきりで、OS から「いま中断したよ」「もう戻っていいよ」という通知が来ていることに、私のコードがまったく耳を傾けていなかったのです。音声アプリの品質は、機能が一通り動くかどうかよりも、この種の割り込みにどれだけ丁寧に応えるかで決まると、その時に痛感しました。
ここから、電話・Siri による中断、イヤホンの抜き差しによるルート変更を正しく扱い、再生が破綻しない実装を、Rork Max が生成するネイティブ Swift にそのまま組み込める形で見ていきます。
まずオーディオセッションのカテゴリを決める
何より先に、アプリがどういう音を出すアプリなのかを OS に宣言します。これを怠ると、サイレントスイッチで音が消えたり、他アプリの音楽を不必要に止めてしまったりします。
import AVFoundation
func configureAudioSession() {
let session = AVAudioSession.sharedInstance()
do {
// 再生主体のアプリ。サイレントスイッチに影響されず再生する
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
} catch {
print("Audio session setup failed: \(error)")
}
}.playback は「このアプリの音はコンテンツそのものである」という宣言で、ロック中やバックグラウンドでも鳴らせます。逆に、他アプリの音楽の上に短い効果音を重ねたいだけなら .ambient を選ぶ、というように、用途に応じてカテゴリを変えます。最初の宣言を正しく選ぶことが、後の挙動すべての土台になります。
中断通知を購読する
中断は AVAudioSession.interruptionNotification で届きます。.began(中断開始)と .ended(中断終了)の二つの局面があり、終了時には「自分から再開してよいか」のヒントが付いてきます。
final class PlaybackController {
private var wasPlayingBeforeInterruption = false
func observeInterruptions() {
NotificationCenter.default.addObserver(
self, selector: #selector(handleInterruption),
name: AVAudioSession.interruptionNotification, object: nil)
}
@objc private func handleInterruption(_ note: Notification) {
guard let info = note.userInfo,
let raw = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: raw) else { return }
switch type {
case .began:
// 電話やSiriが割り込んだ。状態を覚えておく
wasPlayingBeforeInterruption = player.isPlaying
player.pause()
case .ended:
guard let optsRaw = info[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optsRaw)
// shouldResume が立っていて、かつ中断前に再生中だったときだけ再開
if options.contains(.shouldResume), wasPlayingBeforeInterruption {
try? AVAudioSession.sharedInstance().setActive(true)
player.play()
}
@unknown default:
break
}
}
}ここで肝になるのが shouldResume の扱いです。OS は「再開してよい中断」と「再開すべきでない中断」を区別しています。たとえば他の音楽アプリがユーザー操作で前面に出てきたときは、勝手に再生を奪い返すべきではありません。shouldResume が立っていないのに再生を再開すると、ユーザーの意図に反してしまいます。中断前に再生中だったかどうかも併せて確認し、両方を満たすときだけ静かに戻すのが行儀の良い実装です。