Swift：綺麗なサイン波音をその場で生成して鳴らす方法

# はじめに

iPhone上でボタンを押している間サイン波を鳴らしたいと思った。しかし、**綺麗に鳴らす**ことが難しかったため、同志のためにも記事にまとめておく。なお、もっと単純に音を鳴らすだけならば、[こちらの記事](https://qiita.com/Kyome/items/0b514d8b799b956dc73d)を参照いただきたい。

# ソースコード

任意の音量と周波数のサイン波を鳴らすためのClass SineWaveを以下に載せる。

```swift:SineWave.swift
import AVFoundation

class SineWave {
    
    private enum Fade {
        case none
        case `in`
        case out
    }
    
    private let audioEngine = AVAudioEngine()
    private let player = AVAudioPlayerNode()
    private var buffer: AVAudioPCMBuffer!
    private var fadeInBuffer: AVAudioPCMBuffer!
    private var fadeOutBuffer: AVAudioPCMBuffer!
    private let semaphore = DispatchSemaphore(value: 0)
    
    var volume: Float = 0.1 {
        didSet { updateBuffers() }
    }
    
    var hz: Float = 600 {
        didSet { updateBuffers() }
    }
    
    init(volume: Float = 0.1, hz: Float = 600) {
        self.volume = volume
        self.hz = hz
        let audioFormat = player.outputFormat(forBus: 0)
        updateBuffers()
        audioEngine.attach(player)
        audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFormat)
        audioEngine.prepare()
        do {
            try self.audioEngine.start()
        } catch {
            print(error.localizedDescription)
        }
    }
    
    deinit {
        stopEngine()
    }
    
    private func updateBuffers() {
        buffer = makeBuffer()
        fadeInBuffer = makeBuffer(fade: .in)
        fadeOutBuffer = makeBuffer(fade: .out)
    }
    
    private func makeBuffer(fade: Fade = .none) -> AVAudioPCMBuffer {
        let audioFormat = player.outputFormat(forBus: 0)
        let sampleRate = Float(audioFormat.sampleRate) // 44100.0
        let length = AVAudioFrameCount(sampleRate / hz)
        let capacity = fade == .none ? length : 15 * length
        guard let buf = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: capacity) else {
            fatalError("Error initializing AVAudioPCMBuffer")
        }
        buf.frameLength = capacity
        let u = Float.pi / Float(capacity)
        for n in (0 ..< Int(capacity)) {
            let power: Float
            switch fade {
            case .none: power = 1.0
            case .in:   power = 0.5 * (1.0 - cosf(Float(n) * u))
            case .out:  power = 0.5 * (1.0 + cosf(Float(n) * u))
            }
            let value = power * volume * sinf(Float(n) * 2.0 * Float.pi / Float(length))
            buf.floatChannelData?.advanced(by: 0).pointee[n] = value
            buf.floatChannelData?.advanced(by: 1).pointee[n] = value
        }
        return buf
    }
    
    func play() {
        if audioEngine.isRunning && !player.isPlaying {
            player.play()
            player.scheduleBuffer(fadeInBuffer) { [weak self] in
                self?.semaphore.signal()
            }
            player.scheduleBuffer(buffer, at: nil, options: .loops)
        }
    }
    
    func pause() {
        if player.isPlaying {
            switch semaphore.wait(timeout: .now() + 0.1) {
            case .success:
                player.scheduleBuffer(fadeOutBuffer, at: nil,
                                      options: .interruptsAtLoop,
                                      completionHandler: { [weak self] in
                                        self?.player.pause()
                                      })
            case .timedOut:
                break
            }
        }
    }
    
    func stopEngine() {
        pause()
        if audioEngine.isRunning {
            audioEngine.disconnectNodeOutput(player)
            audioEngine.detach(player)
            audioEngine.stop()
        }
    }
    
}
```

# 使用例

```swift
// 音量は0〜1の間で設定する
let sineWave = SineWave(volume: 0.1, hz: 650.0)

// 再生
sineWave.play()
// ポーズ
sineWave.pause()
```

# 大雑把な解説

- AVAudioPlayerNodeを使ってPCM形式のサイン波の音源データを生成し、AVAudioEngineにて再生を行なっている。
- buffer.floatChannelDataに0と1の二つがあるのは左耳と右耳の両方で音を鳴らすため。
- 単純に再生/一時停止/停止を行うと、ぶつ切り音のようなノイズが入るため、fade in/outのためのBufferを用意している。
- AVAudioFrameCountの整数の丸め込みにより、厳密には指定されたHzの音を鳴らすことはできていない。
- GitHubにサンプルのソースあります。[Kyome22/SineWaveSound](https://github.com/Kyome22/SineWaveSound)

# 参考文献

[タコさんブログ](http://tiny-wing.hatenablog.com/entry/2016/06/15/093656)