Closed10

AVAudioEngine Tutorial for iOS: Getting Started

アイカワアイカワ

iOS Audio Frameworks は色々な要素で構成されていて、例えば以下のようなものがある

  • CoreAudio, AudioToobox
    • 低レベルな C frameworks
  • AVFoundation
    • Objective-C/Swift framework
    • AVAudioSession, AVAudioEngine, AVAudioBuffer, AVAudioPlayer, AVAudioPlayerNode, AVAudioFormat, AVAudioRecorder, AVAudioFile など
  • AVFoundation の一部である AVAudioEngine
    • 接続された audio nodes のグループを定義する class
    • 今回のプロジェクトでは AVAudioPlayerNodeAVAudioUnitTimePitch を追加する
アイカワアイカワ

file が音声 file であれば、その url から AVAudioFile を作り出すことができる。

let file = try AVAudioFile(forReading: fileURL)

AVAudioFileprocessingFormat: AVAudioFormat という property を持っており、そこから音声ファイルのいくつかの metadata を取り出すことができる。

  let format = file.processingFormat
  
  audioLengthSamples = file.length
  audioSampleRate = format.sampleRate
  audioLengthSeconds = Double(audioLengthSamples) / audioSampleRate
アイカワアイカワ

AVAudioNode は connect する前に必ず attach する必要がある。

engine.attach(player)
engine.attach(timeEffect)

engine.connect(
  player,
  to: timeEffect,
  format: format)
engine.connect(
  timeEffect,
  to: engine.mainMixerNode,
  format: format)

AudioEngine は mainMixerNode というものを提供しており、これはデフォルトでは output node engine に connect している。(通常は iOS Device speaker)

prepare() を呼び出せば、必要なリソースを事前に割り当てることができる。

engine.prepare()

また、Device に audio を再生する準備ができたことを伝えるために

try engine.start()

を呼ぶ必要がある。

アイカワアイカワ
player.scheduleFile(file, at: nil) {
  self.needsFileScheduled = true
}

scheduleFile(_:at:) で audio file の再生のスケジュールを行うことができる。

atAVAudioTime を指定することができて、未来の時間に再生したい場合はここに未来の時間を指定する。
今回のように nil を指定した場合は、即座に再生が始まる。

audio file の再生が完了したら、completion block が呼ばれるため、そこに処理を記述すれば audio file 再生完了後の処理を記述できる。

audio の scheduling のための function は他にもいくつか用意されている

  • scheduleBuffer(_:completionHandler:)
    • audio data を preload した buffer を提供する
  • scheduleSegment(_:startingFrame:frameCount:at:completionHandler:)
    • schedule(_:at:) と似ている。異なるのは、「どの audio frame から再生するのか」「どのくらいの frame を再生するのか」を指定すること
アイカワアイカワ

player.pause() で音声を停止できるし、player.play() で音声を再生できる。

アイカワアイカワ

次に audio file がどれだけ再生されているかを可視化する方法を見ていく。

可視化のために CADisplayLink というものを使う。
CADisplayLink は display の refresh rate に同期して動作する timer object である。

displayLink = CADisplayLink(target: self, selector: #selector(updateDisplay))
displayLink?.add(to: .current, forMode: .default)
displayLink?.isPaused = true

上記では displayLink を default run lopp に add した後で、CADisplayLink をまだ開始しないようにするために isPaused を true にしている。

合わせて、再生ボタンを押した時に呼ばれる playOrPausedisplayLink.isPaused を切り替える。

  func playOrPause() {
    isPlaying.toggle()
    
    if player.isPlaying {
      displayLink?.isPaused = true
      disconnectVolumeTap()

      player.pause()
    } else {
      displayLink?.isPaused = false
      connectVolumeTap()

      if needsFileScheduled {
        scheduleAudioFile()
      }
      player.play()
    }
  }

音楽の再生・停止に合わせて displayLink.isPaused の値も変更している。

さらに先ほど呼んでいた updateDisplay というロジックは以下のように実装される。

    // ここでは `currentPosition` が不正な値にならないように調整している
    currentPosition = currentFrame + seekFrame
    currentPosition = max(currentPosition, 0)
    currentPosition = min(currentPosition, audioLengthSamples)
    
    // `currentPosition` が `audioLengthSamples` 以上、つまり
    // 音声の再生が完了しているということなので、それに応じた処理を行っている
    if currentPosition >= audioLengthSamples {
      player.stop()
      
      seekFrame = 0
      currentPosition = 0
      
      isPlaying = false
      displayLink?.isPaused = true
      
      disconnectVolumeTap()
    }
    // 実際に表示される progress の値を計算する
    playerProgress = Double(currentPosition) / Double(audioLengthSamples)
    
    let time = Double(currentPosition) / audioSampleRate
    playerTime = PlayerTime(
      elapsedTime: time,
      remainingTime: audioLengthSeconds - time
    )

これで音声の再生・停止に progress が追従するようになる。

アイカワアイカワ

VU Meters というのは、音声の音量に応じてバウンスするグラフィックを描写することによって live audio 感を示すものである。

AVAudioEngine では以下のようにすれば AVAudioFormat を取得することができる。

let format = engine.mainMixerNode.outputFormat(forBus: 0)

forBus で必要としているのは AVAudioNodeBus というものらしく、これは audio node 上の bus の index を示すらしい。
bus というのは調べたところによると、「さまざまな音声信号が乗り入れるライン」を指しているらしい (https://dtm.conceptmol.com/バス(bus)/)

installTap(onBus:bufferSize:format:block:) を利用すれば、mainMixerNode の output bus 上の audio data にアクセスすることができるようになる。

engine.mainMixerNode.installTap(
  onBus: 0,
  bufferSize: 1024,
  format: format
) { buffer, _ in
  // ...
}

ここに指定する bufferSize については、Apple のドキュメントにもどんな制限があるかは記載されておらず、特に大きすぎたり小さすぎたりするものを指定した場合、そのリクエストされた bufferSize は保証されないものとなる。
blockAVAudioPCMBufferAVAudioTime を利用できる。
ここでは実際の buffer size を定めるために buffer.frameLength をチェックすることができる。

buffer.floatChannelData でそれぞれの sample data への pointer の配列を得ることができる。

  guard let channelData = buffer.floatChannelData else {
    return
  }
  
  let channelDataValue = channelData.pointee

計算のしやすさのために UnsafeMutablePointer<Float> array を Float array に変換するためには、例えば以下のようなことができる。

  let channelDataValueArray = stride(
    from: 0,
    to: Int(buffer.frameLength),
    by: buffer.stride)
    .map { channelDataValue[$0] }

engine.mainMixerNode.removeTap(onBus: 0) で特定の bus の audio tap を削除することができる。

アイカワアイカワ

秒数のような時間を audio frame position に変換するためには audioSampleRate をかける必要がある。

let offset = AVAudioFramePosition(time * audioSampleRate)

player.scheduleSegment(_:startingFrame:frameCount:at:) は audio file の seekFrame の位置から再生するようにスケジュールしてくれる。
ファイルの最後まで再生したい場合は audioLengthSamples - seekFrame にする。
atnil を指定すれば、将来のある時点ではなく、すぐに再生することを意味できる。

  let frameCount = AVAudioFrameCount(audioLengthSamples - seekFrame)
  // 3
  player.scheduleSegment(
    audioFile,
    startingFrame: seekFrame,
    frameCount: frameCount,
    at: nil
  ) {
    self.needsFileScheduled = true
  }
アイカワアイカワ

audio の rate (1x speeds) を変更したい場合は、AVAudioUnitTimePitchrate を変更すれば良い。

let selectedRate = allPlaybackRates[playbackRateIndex]
timeEffect.rate = Float(selectedRate.value)

pitch を変更したい場合は、同じく AVAudioUnitTimePitchpitch を変更すれば良い。

let selectedPitch = allPlaybackPitches[playbackPitchIndex]
timeEffect.pitch = 1200 * Float(selectedPitch.value)

AVAudioUnitTimePitch.pitchによると、その値は cents で測られるらしい。
octave は 1200 cents と等しい。

このスクラップは2023/02/03にクローズされました