SwiftUI | サイン波の音声を生成し再生する方法

音声・動画

SwiftUIでサイン波の音声を生成し再生する方法を説明する。

全体像

  • bufferは受け取った周波数(freq)のサイン波を生成する。
  • playerはbufferから受け取ったサイン波outputNodeを経由して再生する。

コード

  1. AVAudioEngineとAVAudioPlayerNodeを生成する。
  2. AVAudioPCMBufferを生成する。
  3. AVAudioEngineにAVAudioPlayerNodeを接続する。
  4. outputNodeを接続する。
  5. 再生する。
import Foundation
import AVFoundation

class SoundUnit {
    
    let volume: Double = 1.0
    let sampleRate = 44100.0
    let bufferTimeLength: UInt32 = 5  // sec
    
    private let audioEngine = AVAudioEngine()                             // 1
    private let player = AVAudioPlayerNode()                              // 1
    let session = AVAudioSession.sharedInstance()
    
    init() {
        
    }
    
    deinit {
        stopSound()
    }
    
    private func makeBuffer(freq: Double) -> AVAudioPCMBuffer {
        let audioFormat = AVAudioFormat(
            standardFormatWithSampleRate: sampleRate,
            channels: 1
        )
        guard let buf = AVAudioPCMBuffer(                                 // 2
            pcmFormat: audioFormat!,
            frameCapacity: AVAudioFrameCount(sampleRate) * bufferTimeLength
        ) else {
            fatalError("Error initializing AVAudioPCMBuffer")
        }
        let data = buf.floatChannelData?[0]
        var theta = 0.0
        let deltaTheta = 2.0 * .pi * Double(freq) / self.sampleRate
        let numberFrames = buf.frameCapacity
        buf.frameLength = numberFrames
        
        for frame in 0..<Int(numberFrames) {
            data?[frame] = Float32(sin(theta) * volume)
            
            theta += deltaTheta
            if theta > 2.0 * .pi {
                theta -= 2.0 * .pi
            }
        }
        
        return buf
    }
    
    func startSound(freq: Double) {
        try! session.setCategory(AVAudioSession.Category.playback)
        try! session.setActive(true)
        
        let buffer = makeBuffer(freq: freq)
        audioEngine.attach(player)                                        // 3
        let audioFormat = AVAudioFormat(
            standardFormatWithSampleRate: sampleRate,
            channels: 1
        )
        audioEngine.connect(player, to: audioEngine.outputNode, format: audioFormat)
                                                                          // 4
       
        audioEngine.prepare()
        try! audioEngine.start()
        
        if !player.isPlaying {
            player.play()                                                 // 5
            player.scheduleBuffer(
                buffer,
                at: nil,
                options: .loops,
                completionHandler: nil
            )
        }
        
    }
    
    func stopSound() {
        if player.isPlaying {
            player.stop()
        }
        if audioEngine.isRunning {
            audioEngine.stop()
            try! session.setActive(false)
        }
    }
    
}

使い方

例えばContentView.swift内にSoundUnitクラスのインスタンスを生成し、

let soundUnit = SoundUnit()

例えば440Hzの音を鳴らしたい場合はstartSound(freq: 440)と書く。

soundUnit.startSound(freq: 440)

応用

周波数を決める部分には下記記事の技術を利用してもよい。

下記iOS Appでは本記事の技術を利用した。

メモ

ループ再生時のループのつなぎ目でプツッというノイズが発生する場合がある。

周波数freqが整数でない場合に発生する。

原因は、ループの始めと終わりのサイン波の大きさが異なるため。ループの始めのサイン波の大きさは常に0。ループの終わりはサイン波の大きさは周波数が整数なら0、整数以外なら0以外になる。よって周波数が整数以外のときにループのつなぎ目で音が非連続に変化してしまう。

対処案は、

  • 周波数を整数にする。
  • ループの時間を超えないような短い音だけで使う。
  • ループの時間を長くしてノイズの発生頻度を下げる。
  • この方法は使わずにループの始めと終わりの大きさが同じ音声ファイルを作って使う。

環境

Xcode 13.3, Swift 5.6

まとめ

SwiftUIでサイン波の音声を生成し再生する方法を説明した。

コメント

タイトルとURLをコピーしました