麦芽を支える技術

麦芽(ばくが、英語:malt)とは、麦、特に大麦の種子を発芽させたもので、ビール、ウイスキー、水飴の原料となる。(Wikipediaより)

AVAudioEngineでの音声ファイル再生で、再生時間の取得・再生位置指定を行う

あまり明確な情報がなかなか見つからなくてだいぶ苦戦したけど、一応出来たのでメモ。 もうちょっとスマートなやり方あるのかもしれませんが。

やりたいこと

  1. AVAudioEngine、AVAudioPlayerNodeを使った音声データの再生
  2. 総再生時間、現在の再生時間を取得
  3. 任意の再生位置からの音声再生(シーク機能)

Webでいろいろ調べていると、上記1点目の基本的なAVAudioEngineの使用方法や、エフェクト・速度変更などの音声効果の使い方は以下のように結構載ってるんですが、通常の音声プレイヤーとして必要な2,3点目のやり方がなかなか見つからなかったんですよねー。

というわけで、AVAudioEngineの基本部分は端折って、2,3点目についてここに書いときます。 あ、ちなみにSwiftです。

総再生時間、現在の再生時間を取得

まず事前に、AVAudioFileより音声ファイルのサンプルレートをしておきます。

let audioFile: AVAudioFile = try AVAudioFile(forReading: url)
let sampleRate = audioFile.fileFormat.sampleRate

総再生時間を秒で取得

AVAudioFileクラスの length プロパティで音声ファイルの総フレーム数(AVAudioFramePosition)を取得できます。

AVAudioFile Class Reference

音声ファイルの総再生時間(秒)は、この総フレーム数をサンプルレートで割ると算出できます。

let duration = Double(audioFile.length) / sampleRate

現在の再生時間を秒で取得

AVAudioPlayerNode Class Reference

詳しい説明は公式リファレンスの↑この辺に記載がありますが、音声周りの時間軸(AVAudioTime)としてはAVAudioNodeクラスが持つ時間軸(ここでは便宜上Node Timelineと呼ぶ)と、AVAudioPlayerNodeクラスが持つ時間軸(Player Timeline)があります。

AVAudioPlayerNodeクラスのlastRenderTimeプロパティで、最後に再生した時間がNode Timeline形式で取得できます。

let nodeTime = audioPlayerNode.lastRenderTime

それをplayerTimeForNodeTime()メソッドでPlayer Timeline形式に変換することが出来ます。

let playerTime = audioPlayerNode.playerTimeForNodeTime(nodeTime!)

AVAudioTime Class Reference

AVAudioTimeクラスでは、sampleTimeプロパティにてその時間をフレーム数(AVAudioFramePosition)として取得することができるので、あとは先述の総再生時間の取得方法と同様、フレーム数をサンプルレートで割るとその秒が取得できます。

let currentTime = (Double(playerTime!.sampleTime) / sampleRate)

任意の再生位置からの音声再生(シーク機能)

上述までの内容を踏まえ、秒単位でのINPUTをAVAudioPlayNodeが扱える「フレーム」単位に変換する必要がありますので、必要な情報を以下の通り用意します。

// 前提:
// ・position にはDouble値で任意の再生位置(秒)が入ってくる前提
// ・self.duration は総再生時間(秒)
// ・sampleRateはサンプルレート

// シーク位置(AVAudioFramePosition)取得
let newsampletime = AVAudioFramePosition(sampleRate * position)

// 残り時間取得(sec)
let length = self.duration - position

// 残りフレーム数(AVAudioFrameCount)取得
let framestoplay = AVAudioFrameCount(sampleRate * length)

ここまでで用意した情報を用いて、AVAudioPlayerNodeクラスのscheduleSegment()メソッドで指定位置から再生させるようスケジューリングしなおすことで、指定位置からの再生を実現しています。

audioPlayerNode.stop()

if framestoplay > 100 {
    // 指定の位置から再生するようスケジューリング
    audioPlayerNode.scheduleSegment(audioFile,
        startingFrame: newsampletime,
        frameCount: framestoplay,
        atTime: nil,
        completionHandler: nil
    )
}

audioPlayerNode.play()

現在再生時間の補正

ここまでで音声は指定位置から開始するのですが、注意点としてここでAVAudioPlayerNodeを再生し直すことで、lastRenderTimeがまた0に初期化されてしまい、このシークした位置が音声開始時間(0:00:00)とみなされてしまいます。

これだと上述した「現在の再生時間を秒で取得」の内容がズレてしまうので、ここで以下のように補正してみます。

var offset = 0.0

...

// 再生位置指定あたりの処理

// シーク位置(AVAudioFramePosition)取得
let newsampletime = AVAudioFramePosition(sampleRate * position)
// 残り時間取得(sec)
let length = self.duration - position
// 残りフレーム数(AVAudioFrameCount)取得
let framestoplay = AVAudioFrameCount(sampleRate * length)

self.offset = position // ←シーク位置までの時間を一旦退避

...

// 現在の再生時間取得あたりの処理
let currentTime = (Double(playerTime!.sampleTime) / sampleRate) + self.offset // ←シーク位置以前の時間を追加

...

例えば、0:02:45の位置にシークした場合、補正しないとその位置が0:00:00とされてしまうので、シークした時にその時間をoffsetに保存しておいて、現在の再生時間を取得時には保存しておいたoffset分の時間を加算して辻褄を合わせています。

参考

Changing Playback Time (Seeking) using AVAudioEngine – Swift Explained

ios - AVAudioEngine seek the time of the song - Stack Overflow