はじめに
iPhone上でボタンを押している間サイン波を鳴らしたいと思った。しかし、綺麗に鳴らすことが難しかったため、同志のためにも記事にまとめておく。なお、もっと単純に音を鳴らすだけならば、こちらの記事を参照いただきたい。
ソースコード
任意の音量と周波数のサイン波を鳴らすためのClass SineWaveを以下に載せる。
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()
}
}
}
使用例
// 音量は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