やりたいこと
リモートにある音楽ファイルをiOS端末で再生する。
iOSの音声再生APIは複数存在するが、信号処理をしたいのでAVAudioEngineを使う。
環境
- Xcode: 11.1
- iOS: 13.1.2
- リポジトリ: https://github.com/fuziki/RemoteAudioPlayerNode
手順
- 音楽ファイルをダウンロードする
- パケットに分割する
- パケットをPCMに変換する
- PCMをAVAudioEngineを使って再生する
実装
step1. 音楽ファイルをダウンロードする
URLSessionを使って指定したURLからDataをダウンロードする。
downloaderを外から入れられるようにRemoteAudioDownloaderのprotocolを定義。
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でパケットに分割する。
最初にDataをAudioFileStreamOpenを使って開く。
分割したパケットはAudioFileStreamServiceで処理する。
propertyの処理にはAudioFileStreamService.propertyListenerProcedureが呼ばれる。
packetのdataの処理にはAudioFileStreamService.packetsProcedureが呼ばれる。
inClientDataのポインタとして自信のポインタを渡す。
AudioFileStreamService.propertyListenerProcedure, AudioFileStreamService.packetsProcedure内でinClientDataをAudioFileStreamServiceにキャストすることで、自信を呼び出すことができる。
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として指定したpropertyListenerProcedure内でAVAudioFormatを取得する。
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として指定したpacketsProcedure内でpacketのDataとAudioStreamPacketDescriptionを取得する。
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を使ってAVAudioCompressedBufferからAVAudioPCMBufferに変換する。
step3-1. DataからAVAudioCompressedBufferを生成する
AVAudioCompressedBufferを作ってDataをコピーする。
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を生成する
let pcmBuff = AVAudioPCMBuffer(pcmFormat: dstFormat, frameCapacity: frames)!
pcmBuff.frameLength = pcmBuff.frameCapacity
step3-3. AVAudioConverterを使って変換する
AVAudioConverterを使って変換する。
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を使って再生する
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