swift4
サイン波
鳴らす

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

はじめに

iPhone上でボタンを押している間サイン波を鳴らしたいと思った。意外と綺麗に鳴らすことが難しかったため、同志のためにも記事にまとめておく。

ソースコード

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

SineWave.swift
import AVFoundation

class SineWave {
    let audioEngine = AVAudioEngine()
    let player = AVAudioPlayerNode()
    var cnt: Int = 0
    var timer: Timer?

    init(volume: Float = 0.5, hz: Float = 440) {
        let audioFormat = player.outputFormat(forBus: 0)
        let sampleRate: Float = 44100.0
        let length = UInt32(sampleRate)
        if let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: length) {
            buffer.frameLength = length
            for n in (0 ..< Int(length)) {
                let val = sinf(hz * Float(n) * 2.0 * Float.pi / sampleRate)
                buffer.floatChannelData?.advanced(by: 0).pointee[n] = volume / 5 * val
                buffer.floatChannelData?.advanced(by: 1).pointee[n] = volume / 5 * val
            }
            audioEngine.attach(player)
            audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFormat)
            player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
            do {
                try audioEngine.start()
            } catch {
                Swift.print(error.localizedDescription)
            }
        }
    }

    deinit {
        stopEngine()
    }

    func play() {
        if audioEngine.isRunning {
            Timer.scheduledTimer(withTimeInterval: 0.005, repeats: true, block: { (t) in
                if self.timer == nil || !self.timer!.isValid {
                    t.invalidate()
                    self.cnt = 0
                    self.player.prepare(withFrameCount: 0)
                    self.player.volume = 1.0
                    self.audioEngine.mainMixerNode.outputVolume = 1.0
                    self.player.play()
                }
            })
        }
    }

    func pause() {
        if player.isPlaying {
            timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { (t) in
                self.cnt += 1
                if self.player.volume > 0 {
                    self.player.volume -= 0.33
                    self.audioEngine.mainMixerNode.outputVolume -= 0.33
                } else if self.cnt > 5 {
                    self.player.volume = 0
                    self.audioEngine.mainMixerNode.outputVolume = 0
                    t.invalidate()
                    self.player.pause()
                }
            })
        }
    }

    func stop() {
        if player.isPlaying {
            timer = Timer.scheduledTimer(withTimeInterval: 0.008, repeats: true, block: { (t) in
                self.cnt += 1
                if self.player.volume > 0 {
                    self.player.volume -= 0.33
                    self.audioEngine.mainMixerNode.outputVolume -= 0.33
                } else if self.cnt > 5 {
                    self.player.volume = 0
                    self.audioEngine.mainMixerNode.outputVolume = 0
                    t.invalidate()
                    self.player.stop()
                }
            })
        }
    }

    func stopEngine() {
        stop()
        if audioEngine.isRunning {
            audioEngine.stop()
        }
    }
}

このClassの使い方

ViewController.swift
import UIKit

class ViewController: UIViewController {

    var sinewave: SineWave!

    override func viewDidLoad() {
        super.viewDidLoad()
        sinewave = SineWave(valume: 0.7, hz: 600) //音量0.7、周波数600Hzのサイン波を鳴らす準備
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        sinewave?.stopEngine() //Viewが消える前にaudioEngineを止める(重要)
    }

    //サイン波を再生開始(初回と一時停止している時のみ可)
    @IBAction func start(_ sender: UIButton) {
        sinewave.play()
    }

    //サイン波を一時停止
    @IBAction func pause(_ sender: UIButton) {
        sinewave.pause()
    }

    //サイン波を停止(再びplayをしても再生されない 再生したい場合はもう一度インスタンス化する必要あり)
    @IBAction func stop(_ sender: UIButton) {
        sinewave.stop()
    }

}

※音量は0〜1の間で設定する

大雑把な解説

AVAudioPlayerNodeを使ってPCM形式のサイン波の音源データを生成し、AVAudioEngineにて再生を行なっている。
buffer.floatChannelDataに0と1の二つがあるのは左耳と右耳の両方で鳴らすため。
player.volumeとaudioEngine.mainMixerNode.outputVolumeをタイマーで制御しているのは、そうしないと再生/一時停止の際にぶつ切り音がなってしまうため。綺麗なサイン波を再生する上で必要だった。

参考にしたサイト

タコさんブログ