4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

やりたいこと

リモートにある音楽ファイルをiOS端末で再生する。
iOSの音声再生APIは複数存在するが、信号処理をしたいのでAVAudioEngineを使う。
XambmPaQ.png

環境

手順

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

実装

step1. 音楽ファイルをダウンロードする

URLSessionを使って指定したURLからDataをダウンロードする。
downloaderを外から入れられるようにRemoteAudioDownloaderのprotocolを定義

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でパケットに分割する。
最初にDataをAudioFileStreamOpenを使って開く。
分割したパケットはAudioFileStreamServiceで処理する。
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として指定したpropertyListenerProcedure内で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として指定したpacketsProcedure内で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を使ってAVAudioCompressedBufferからAVAudioPCMBufferに変換する。

step3-1. DataからAVAudioCompressedBufferを生成する

AVAudioCompressedBufferを作ってDataをコピーする。

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を作成する。

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

step3-3. AVAudioConverterを使って変換する

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

4
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?