Edited at

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

More than 1 year has passed since last update.


はじめに

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をタイマーで制御しているのは、そうしないと再生/一時停止の際にぶつ切り音がなってしまうため。綺麗なサイン波を再生する上で必要だった。


参考にしたサイト

タコさんブログ