[macOS][Swift4.1] Audio Queue Servicesを使って録音/再生、波形表示する。

CoreAudioのAudio Queue Servicesを使って、録音/再生する方法を記載します。
また録音した音声を波形データとして表示します。

![app.png](https://qiita-image-store.s3.amazonaws.com/0/156085/a170beb0-a2ca-43f3-5f42-529de1ba5f03.png)

サンプルコードは以下にアップ。
Macアプリで書いてますがUI以外は基本iOSでも使えると思います。
https://github.com/atsushijike/AudioService

* Xcode 9.4.1
* Swift 4.1

CoreAudioの概要（日本語）
https://developer.apple.com/jp/documentation/MusicAudio/Conceptual/CoreAudioOverview/Introduction/Introduction.html

Audio Queue Services Programming Guide（英語）
https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioQueueProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005343

CoreAudio自体その歴史の古さもあってかMacアプリ開発時代のObj-Cの情報が多く、
今回調査しながら一旦Obj-Cで書いて動くことを確認してから、現代のプロジェクトにも組み込みやすいようswift 4.1でリライトというアプローチで進めた。

# 定義
オーディオキューオブジェクトとバッファを用いて録音/再生管理を行うクラス `AudioService` を定義する。
各変数の概要は次の通り。

```Swift:AudioService.swift
class AudioService {
    // バッファ
    var buffer: UnsafeMutableRawPointer
    // オーディオキューオブジェクト
    var audioQueueObject: AudioQueueRef?
    // 再生時のパケット数
    let numPacketsToRead: UInt32 = 1024
    // 録音時のパケット数
    let numPacketsToWrite: UInt32 = 1024
    // 再生/録音時の読み出し/書き込み位置
    var startingPacketCount: UInt32
    // 最大パケット数。（サンプリングレート x 秒数）
    var maxPacketCount: UInt32
    // パケットのバイト数
    let bytesPerPacket: UInt32 = 2
    // 録音時間（＝再生時間）
    let seconds: UInt32 = 10
    // オーディオストリームのフォーマット
    var audioFormat: AudioStreamBasicDescription {
        return AudioStreamBasicDescription(mSampleRate: 48000.0,  // サンプリング周波数
                                           mFormatID: kAudioFormatLinearPCM,  // フォーマットID（リニアPCM, MP3, AAC etc）
                                           mFormatFlags: AudioFormatFlags(kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked),  // フォーマットフラグ（エンディアン, 整数or浮動小数点数）
                                           mBytesPerPacket: 2,  // １パケットのバイト数（データ読み書き単位）
                                           mFramesPerPacket: 1,  // １パケットのフレーム数
                                           mBytesPerFrame: 2,  // １フレームのバイト数
                                           mChannelsPerFrame: 1,  // １フレームのチャンネル数
                                           mBitsPerChannel: 16,  // １チャンネルのビット数
                                           mReserved: 0)
    }
    // 書き出し/読み出し用のデータ
    var data: NSData?

    ...

}
```

# :microphone2: 録音
大体の流れは、 オーディオキューオブジェクトとバッファを準備して定義したコールバックが呼ばれたらバッファにデータをコピーして書き込む。いっぱいになったら停止する。
## 準備
`AudioQueueNewInput` でオーディオフォーマットとコールバック関数を指定して オーディオキューオブジェクト `AudioQueueRef` を得る。
オーディオキューで管理するバッファを3個にして準備しておく。
録音されたデータは ここで用意している `buffers` に書き込まれ、いっぱいになると指定したコールバックが呼ばれ、 変数の `buffer` にmemcpyされる仕組みとなっている。

`AudioQueueAllocateBuffer` でキューに確保するバッファを指示し、
`AudioQueueEnqueueBuffer` でキューの最後にバッファを追加している。

```Swift:AudioService.swift
    private func prepareForRecord() {
        var audioFormat = self.audioFormat
    
        AudioQueueNewInput(&audioFormat,
                           AQAudioQueueInputCallback,
                           unsafeBitCast(self, to: UnsafeMutableRawPointer.self),
                           CFRunLoopGetCurrent(),
                           CFRunLoopMode.commonModes.rawValue,
                           0,
                           &audioQueueObject)
        
        startingPacketCount = 0;
        var buffers = Array<AudioQueueBufferRef?>(repeating: nil, count: 3)
        let bufferByteSize: UInt32 = numPacketsToWrite * audioFormat.mBytesPerPacket
        
        for bufferIndex in 0 ..< buffers.count {
            AudioQueueAllocateBuffer(audioQueueObject!, bufferByteSize, &buffers[bufferIndex])
            AudioQueueEnqueueBuffer(audioQueueObject!, buffers[bufferIndex]!, 0, nil)
        }
    }
```
録音用コールバック関数の定義。
コールバックで得たバッファを `writePackets(inBuffer: )` して用意したバッファにデータをコピーして書き込む。
録音位置が最大パケット数に達したら停止する。

```Swift:AudioService.swift
func AQAudioQueueInputCallback(inUserData: UnsafeMutableRawPointer?,
                               inAQ: AudioQueueRef,
                               inBuffer: AudioQueueBufferRef,
                               inStartTime: UnsafePointer<AudioTimeStamp>,
                               inNumberPacketDescriptions: UInt32,
                               inPacketDescs: UnsafePointer<AudioStreamPacketDescription>?) {
    let audioService = unsafeBitCast(inUserData!, to:AudioService.self)
    audioService.writePackets(inBuffer: inBuffer)
    AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nil);
    
    if (audioService.maxPacketCount <= audioService.startingPacketCount) {
        audioService.stopRecord()
    }
}
```
バッファを書き込む部分。
`inBuffer.pointee.mAudioData` を用意したバッファに書き込んで、書き込み位置 `startingPacketCount` を次の位置にインクリメントしておく。

```Swift:AudioService.swift
    func writePackets(inBuffer: AudioQueueBufferRef) {
        var numPackets: UInt32 = (inBuffer.pointee.mAudioDataByteSize / bytesPerPacket)
        if ((maxPacketCount - startingPacketCount) < numPackets) {
            numPackets = (maxPacketCount - startingPacketCount)
        }
        
        if 0 < numPackets {
            memcpy(buffer.advanced(by: Int(bytesPerPacket * startingPacketCount)),
                   inBuffer.pointee.mAudioData,
                   Int(bytesPerPacket * numPackets))
            startingPacketCount += numPackets;:loud_sound:
        }
    }
```

## 開始
`prepareForRecord()` で準備して、`AudioQueueStart` で録音を開始する。

```Swift:AudioService.swift
    func startRecord() {
        guard audioQueueObject == nil else  { return }
        prepareForRecord()
        let err: OSStatus = AudioQueueStart(audioQueueObject!, nil)
        print("err: \(err)")
    }
```

## 停止
保存用に録音したバッファ内容を `data` に格納する。
オーディオキューオブジェクトをStop, Disposeして録音を停止する。

```Swift:AudioService.swift
    func stopRecord() {
        data = NSData(bytesNoCopy: buffer, length: Int(maxPacketCount * bytesPerPacket))
        AudioQueueStop(audioQueueObject!, true)
        AudioQueueDispose(audioQueueObject!, true)
        audioQueueObject = nil
    }
```

# :loud_sound: 再生
大体の流れは録音と同じで、 コールバックが呼ばれたらバッファにデータを読み取り、いっぱいになったら再生位置を戻しておく。
## 準備
`AudioQueueNewOutput` でオーディオフォーマットとコールバック関数を指定して オーディオキューオブジェクトを得る。
録音と同じように３個で確保した `buffers` を用意してオーディオキューが管理するバッファに指定、追加する。

```Swift:AudioService.swift
    private func prepareForPlay() {
        print("prepareForPlay")
        var audioFormat = self.audioFormat
    
        AudioQueueNewOutput(&audioFormat,
                            AQAudioQueueOutputCallback,
                            unsafeBitCast(self, to: UnsafeMutableRawPointer.self),
                            CFRunLoopGetCurrent(),
                            CFRunLoopMode.commonModes.rawValue,
                            0,
                            &audioQueueObject)
        
        startingPacketCount = 0
        var buffers = Array<AudioQueueBufferRef?>(repeating: nil, count: 3)
        let bufferByteSize: UInt32 = numPacketsToRead * audioFormat.mBytesPerPacket
    
        for bufferIndex in 0 ..< buffers.count {
            AudioQueueAllocateBuffer(audioQueueObject!, bufferByteSize, &buffers[bufferIndex])
            AQAudioQueueOutputCallback(inUserData: unsafeBitCast(self, to: UnsafeMutableRawPointer.self),
                                       inAQ: audioQueueObject!,
                                       inBuffer: buffers[bufferIndex]!)
        }
    }
```
再生用コールバック関数の定義。
録音とは逆に `readPackets(inBuffer: )` してコールバックで得たバッファに保持しているバッファを書き込む。
再生位置が最大パケット数に達したら再生位置を最初に戻す。

```Swift:AudioService.swift
func AQAudioQueueOutputCallback(inUserData: UnsafeMutableRawPointer?,
                                inAQ: AudioQueueRef,
                                inBuffer: AudioQueueBufferRef) {
    let audioService = unsafeBitCast(inUserData!, to:AudioService.self)
    audioService.readPackets(inBuffer: inBuffer)
    AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nil);
    
    if (audioService.maxPacketCount <= audioService.startingPacketCount) {
        audioService.startingPacketCount = 0;
    }
}
```
バッファを書き込む部分。
保持しているバッファを `inBuffer.pointee.mAudioData` に書き込んで、書き込み位置 `startingPacketCount` を次の位置にインクリメントしておく。
録音とは逆になっている。

```Swift:AudioService.swift
    func readPackets(inBuffer: AudioQueueBufferRef) {
        var numPackets: UInt32 = maxPacketCount - startingPacketCount
        if numPacketsToRead < numPackets {
            numPackets = numPacketsToRead
        }
        
        if 0 < numPackets {
            memcpy(inBuffer.pointee.mAudioData,
                   buffer.advanced(by: Int(bytesPerPacket * startingPacketCount)),
                   (Int(bytesPerPacket * numPackets)))
            inBuffer.pointee.mAudioDataByteSize = (bytesPerPacket * numPackets)
            inBuffer.pointee.mPacketDescriptionCount = numPackets
            startingPacketCount += numPackets
        }
        else {
            inBuffer.pointee.mAudioDataByteSize = 0;
            inBuffer.pointee.mPacketDescriptionCount = 0;
        }
    }
```

## 開始
`prepareForPlay()` で準備して、`AudioQueueStart` で再生を開始する。

```Swift:AudioService.swift
    func play() {
        guard audioQueueObject == nil else  { return }
        prepareForPlay()
        let err: OSStatus = AudioQueueStart(audioQueueObject!, nil)
        print("err: \(err)")
    }
```
## 停止
オーディオキューオブジェクトをStop, Disposeして再生を停止する。

```Swift:AudioService.swift
    func stop() {
        AudioQueueStop(audioQueueObject!, true)
        AudioQueueDispose(audioQueueObject!, true)
        audioQueueObject = nil
    }
```

# 書き出し
録音停止時に保持した `data` をそのまま書き出している。

```Swift:AppDelegate.swift
    @IBAction func exportData(_ sender: Any) {
        guard let data = audioService.data else { return }
        let savePanel = NSSavePanel()
        savePanel.canCreateDirectories = true
        savePanel.nameFieldStringValue = "sound.raw"
        savePanel.begin { (result) in
            if result.rawValue == NSFileHandlingPanelOKButton {
                guard let url = savePanel.url else { return }
                data.write(to: url, atomically: true)
            }
        }
    }
```
# 読み出し
`AudioService` の `data` にセットしている。

```Swift:AudioService.swift
    @IBAction func importData(_ sender: Any) {
        let openPanel = NSOpenPanel()
        openPanel.allowsMultipleSelection = false
        openPanel.canChooseDirectories = false
        openPanel.canCreateDirectories = false
        openPanel.canChooseFiles = true
        openPanel.allowedFileTypes = ["raw"]
        openPanel.begin { [weak self] (result) in
            if result.rawValue == NSFileHandlingPanelOKButton {
                guard let `self` = self, let url = openPanel.url, let data = NSMutableData(contentsOf: url) else { return }
                self.audioService.setData(data)
            }
        }
    }
```
`data` をコピーして、保持している `buffer` に書き込んでいる。

```Swift:AudioService.swift
    func setData(_ data: NSMutableData) {
        self.data = data.copy() as? NSData
        memcpy(buffer, data.mutableBytes, Int(maxPacketCount * bytesPerPacket))
    }
```

# :wavy_dash: 波形
NSData から波形表示するビューを実装する。
縦方向に中心を0として-128 ~ 128までをパスで描画して波形を表現する。

```Swift:WaveView.swift
final class WaveView: NSView {
    var data: NSData? {
        didSet {
            needsDisplay = true
        }
    }
    
    override func draw(_ dirtyRect: NSRect) {
        NSColor.white.set()
        NSBezierPath(rect: bounds).fill()
        NSColor.darkGray.set()
        NSBezierPath(rect: bounds).stroke()

        guard let data = data else { return }

        let bezierPath = NSBezierPath()
        let length = data.length
        let unit = bounds.width / CGFloat(length)
        let mid = bounds.height / 2
        let bytes = UnsafeBufferPointer(start: data.bytes.assumingMemoryBound(to: Int8.self), count: length)

        bezierPath.move(to: NSPoint(x: 0, y: mid))
        for index in 0 ..< length {
            let normaliedSample = 100 * CGFloat(bytes[index]) / 128
            bezierPath.line(to: NSPoint(x: unit * CGFloat(index), y: (normaliedSample * CGFloat(mid / 128)) + mid))
        }
        NSColor.black.set()
        bezierPath.lineWidth = 1
        bezierPath.stroke()
    }
}
```

録音位置や再生位置を縦線で動かしたり、録音してリアルタイムに波形を表現していったら良いかもしれない。