LoginSignup
14
8

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-12-26

はじめに

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

参考文献

タコさんブログ

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8