AVAudioEngineでリモートの音楽ファイルを再生する

# やりたいこと
リモートにある音楽ファイルをiOS端末で再生する。
iOSの音声再生APIは複数存在するが、信号処理をしたいので[AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine)を使う。
<img width="256" height="256" alt="XambmPaQ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/fb521f5f-eaa5-509e-4b55-4ee1ef48a4e7.png">

# 環境
* Xcode: 11.1
* iOS: 13.1.2
* リポジトリ: https://github.com/fuziki/RemoteAudioPlayerNode

# 手順
1. 音楽ファイルをダウンロードする
2. パケットに分割する
3. パケットをPCMに変換する
4. PCMをAVAudioEngineを使って再生する

# 実装
## step1. 音楽ファイルをダウンロードする
[URLSession](https://developer.apple.com/documentation/foundation/urlsession)を使って指定したURLからDataをダウンロードする。
downloaderを外から入れられるように[RemoteAudioDownloaderのprotocolを定義](https://github.com/fuziki/RemoteAudioPlayerNode/blob/master/Sources/RemoteAudioDownloader.swift)。

``` RemoteAudioDownloader.swift
public protocol RemoteAudioDownloader {
    func request(url: URL, completionHandler: @escaping (_ data: Data) -> Void)
}
 
internal class DefaultRemoteAudioDownloader: RemoteAudioDownloader {
    var task: URLSessionDataTask?
    var completionHandler: ((_ data: Data) -> Void)?
    func request(url: URL, completionHandler: @escaping (Data) -> Void) {
        self.completionHandler = completionHandler
        let request = URLRequest(url: url)
        task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data: Data?, response: URLResponse?, error: Error?) in
            guard let self = self, let data = data else { return }
            self.completionHandler?(data)
        })
        task?.resume()
    }
}
```
## step2. パケットに分割する
### step2-1.  AudioFileStreamOpenを使ってDataを開く
ダウンロードしたDataを[Audio File Stream Services](https://developer.apple.com/documentation/audiotoolbox/audio_file_stream_services)でパケットに分割する。
最初にDataを[AudioFileStreamOpen](https://developer.apple.com/documentation/audiotoolbox/1391498-audiofilestreamopen)を使って開く。
分割したパケットは[AudioFileStreamService](https://github.com/fuziki/RemoteAudioPlayerNode/blob/master/Sources/AudioFileStreamService.swift)で処理する。
propertyの処理にはAudioFileStreamService.propertyListenerProcedureが呼ばれる。
packetのdataの処理にはAudioFileStreamService.packetsProcedureが呼ばれる。
inClientDataのポインタとして自信のポインタを渡す。
AudioFileStreamService.propertyListenerProcedure, AudioFileStreamService.packetsProcedure内でinClientDataをAudioFileStreamServiceにキャストすることで、自信を呼び出すことができる。

```AudioFileStreamService.swift
public class AudioFileStreamService {
    private var streamID: AudioFileStreamID?
    public init() {
        let inClientData = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        _ = AudioFileStreamOpen(inClientData,
                                { AudioFileStreamService.propertyListenerProcedure($0, $1, $2, $3) },
                                { AudioFileStreamService.packetsProcedure($0, $1, $2, $3, $4) },
                                kAudioFileMP3Type,
                                &streamID)
    }
}
```

### step2-2. AudioFileStream_PropertyListenerProcに届いたpropertyを処理する
[AudioFileStream_PropertyListenerProc](https://developer.apple.com/documentation/audiotoolbox/audiofilestream_propertylistenerproc)として指定した[propertyListenerProcedure](https://github.com/fuziki/RemoteAudioPlayerNode/blob/master/Sources/AudioFileStreamService.swift#L72)内でAVAudioFormatを取得する。

```AudioFileStreamService.swift
extension AudioFileStreamService {
    static func propertyListenerProcedure(_ inClientData: UnsafeMutableRawPointer, _ inAudioFileStream: AudioFileStreamID, _ inPropertyID: AudioFileStreamPropertyID, _ ioFlags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
        let audioFileStreamService = Unmanaged<AudioFileStreamService>.fromOpaque(inClientData).takeUnretainedValue()
        switch inPropertyID {
        case kAudioFileStreamProperty_DataFormat:
            var description = AudioStreamBasicDescription()
            var propSize: UInt32 = 0
            _ = AudioFileStreamGetPropertyInfo(inAudioFileStream, inPropertyID, &propSize, nil)
            _ = AudioFileStreamGetProperty(inAudioFileStream, inPropertyID, &propSize, &description)
            print("format: ", AVAudioFormat(streamDescription: &description))
        default:
            print("unknown propertyID \(inPropertyID)")
        }
    }
}
```

### step2-3. AudioFileStream_PacketsProcに届いたpacketsを処理する
[AudioFileStream_PacketsProc](https://developer.apple.com/documentation/audiotoolbox/audiofilestream_packetsproc)として指定した[packetsProcedure](https://github.com/fuziki/RemoteAudioPlayerNode/blob/master/Sources/AudioFileStreamService.swift#L106)内でpacketのDataとAudioStreamPacketDescriptionを取得する。

```AudioFileStreamService.swift
extension AudioFileStreamService {
    static func packetsProcedure(_ inClientData: UnsafeMutableRawPointer, _ inNumberBytes: UInt32, _ inNumberPackets: UInt32, _ inInputData: UnsafeRawPointer, _ inPacketDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
        let audioFileStreamService = Unmanaged<AudioFileStreamService>.fromOpaque(inClientData).takeUnretainedValue()
        var packets: [(data: Data, description: AudioStreamPacketDescription)] = []
        let packetDescriptionList = Array(UnsafeBufferPointer(start: inPacketDescriptions, count: Int(inNumberPackets)))
        for i in 0 ..< Int(inNumberPackets) {
            let packetDescription = packetDescriptionList[i]
            let startOffset = Int(packetDescription.mStartOffset)
            let byteSize = Int(packetDescription.mDataByteSize)
            let packetData = Data(bytes: inInputData.advanced(by: startOffset), count: byteSize)
            packets.append((data: packetData, description: packetDescription))
        }
        print("packets: ", packets)
    }
}
```

## step3. パケットをPCMに変換する
[AVAudioConverter](https://developer.apple.com/documentation/avfoundation/avaudioconverter)を使って[AVAudioCompressedBuffer](https://developer.apple.com/documentation/avfoundation/avaudiocompressedbuffer)から[AVAudioPCMBuffer](https://developer.apple.com/documentation/avfoundation/avaudiopcmbuffer)に変換する。

### step3-1. DataからAVAudioCompressedBufferを生成する
[AVAudioCompressedBufferを作ってDataをコピーする。](https://github.com/fuziki/RemoteAudioPlayerNode/blob/master/Sources/CompressedBufferConverter.swift#L28)

```CompressedBufferConverter.swift
let compBuff = AVAudioCompressedBuffer(format: srcFormat, packetCapacity: 1, maximumPacketSize: Int(data.count))
_ = data.withUnsafeBytes({ (ptr: UnsafeRawBufferPointer) in
    memcpy(compBuff.data, ptr.baseAddress!, data.count)
})
compBuff.packetDescriptions?.pointee = AudioStreamPacketDescription(mStartOffset: 0, mVariableFramesInPacket: 0, mDataByteSize: UInt32(data.count))
compBuff.packetCount = 1
compBuff.byteLength = UInt32(data.count)
```
### step3-2. AVAudioPCMBufferを生成する

[出力用のAVAudioPCMBufferを作成する。](https://github.com/fuziki/RemoteAudioPlayerNode/blob/master/Sources/CompressedBufferConverter.swift#L49)

```CompressedBufferConverter.swift
let pcmBuff = AVAudioPCMBuffer(pcmFormat: dstFormat, frameCapacity: frames)!
pcmBuff.frameLength = pcmBuff.frameCapacity
```

### step3-3. AVAudioConverterを使って変換する
[AVAudioConverter](https://developer.apple.com/documentation/avfoundation/avaudioconverter)を使って変換する。

```CompressedBufferConverter.swift
converter.convert(to: pcmBuff, error: &error, withInputFrom: { [weak self] (count: AVAudioPacketCount, input: UnsafeMutablePointer<AVAudioConverterInputStatus>) -> AVAudioBuffer? in
    input.pointee = .haveData
    let buff = self.audioCompressedBuffer[self.index]
    self?.index += 1
    return buff
})
```
## step4. PCMをAVAudioEngineを使って再生する

```ViewController.swift
        engine.attach(player)
        engine.connect(player, to: engine.mainMixerNode, format: internalFormat)
        engine.prepare()
        try! engine.start()
        player.play()
        if let ret = converter?.read(frames: frameCountPerRead) {
            self.scheduleBuffer(ret)
        }
```

# さいごに
Audio File Stream Servicesは古いAPIなので使いにくいと感じました。
mp3は最初の数百フレームが無音として出力されるので、正しく出力できているのか、鳴らしてみるまで疑心暗鬼でしたw