Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What is going on with this article?
@Kyome

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

はじめに

iPhone上でボタンを押している間サイン波を鳴らしたいと思った。しかし、綺麗に鳴らすことが難しかったため、同志のためにも記事にまとめておく。なお、もっと単純に音を鳴らすだけならば、こちらの記事を参照いただきたい。

ソースコード

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

SineWave.swift
import AVFoundation

class SineWave {

    private enum Fade {
        case none
        case `in`
        case out
    }

    var volume: Float = 0.1 {
        didSet { updateBuffers() }
    }
    var hz: Float = 600 {
        didSet { updateBuffers() }
    }

    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)

    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 {
            logput(error)
        }
    }

    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, completionHandler: { [weak self] in
                self?.semaphore.signal()
            })
            player.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
        }
    }

    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 stop() {
        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() {
        stop()
        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()
// 停止
sineWave.stop()

大雑把な解説

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

参考文献

タコさんブログ

3
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Kyome
火星に住む犬です。 趣味でmacOS アプリを開発しています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
3
Help us understand the problem. What is going on with this article?