Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What is going on with this article?

More than 1 year has passed since last update.

@fuziki

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

やりたいこと

リモートにある音楽ファイルを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

6
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
6
Help us understand the problem. What is going on with this article?