Help us understand the problem. What is going on with this article?

[iOS]Swift4で音声の入力・出力をする方法

More than 1 year has passed since last update.

はじめに

この記事では、マイクから音声を入力して一旦ファイル化し、その音をスピーカーから出力するまでの処理をSwift4で書いてみることにします。
Swiftのアップデートが早くてSwift4もすぐにオワコンになってしまうかもしれないですが、自分用の備忘も兼ねて記録しておきます:grinning:

使ったもの

  • Xcode 10.1
  • Swift 4.2
  • AVFoundation

共通で必要な処理の実装

AVFoundationのインスタンスを生成する
let session = AVAudioSession.sharedInstance()
再生と録音をすることを宣言してからアクティブ化する
try session.setCategory(.playAndRecord, mode: .default)
try session.setActive(true)

音声入力の実装

まず録音で使用するAVAudioRecorderを宣言する
var audioRecorder: AVAudioRecorder!
録音フォーマットの設定をする
let settings = [
    AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2,
    AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
AVAudioRecorderインスタンスを生成する
audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
audioRecorder.delegate = self as? AVAudioRecorderDelegate
func getAudioFileUrl() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsDirect = paths[0]
    let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

    return audioUrl
}
ボタンを押したら録音開始(録音中に押したら録音停止)
@IBAction func record(_ sender: Any) {
    if !audioRecorder.isRecording {
        audioRecorder.record()
    } else {
        audioRecorder.stop()
    }
}

音声出力の実装

まず再生で使用するAVAudioEngine、AVAudioFile、AVAudioPlayerNodeを宣言する
var audioEngine: AVAudioEngine!
var audioFile: AVAudioFile!
var audioPlayerNode: AVAudioPlayerNode!
AVAudioEngine、AVAudioFile、AVAudioPlayerNode各々のインスタンスを生成する
audioEngine = AVAudioEngine()
audioFile = try AVAudioFile(forReading: getAudioFileUrl())
audioPlayerNode = AVAudioPlayerNode()
//上で書いたfuncと同じ
func getAudioFileUrl() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsDirect = paths[0]
    let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

    return audioUrl
}
Nodeのつなぎ合わせをする
audioEngine.attach(audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
ボタンを押したら再生開始(再生中に押したら初めから再生)
@IBAction func play(_ sender: Any) {
    do {
    //中略(インスタンス生成やNodeのつなぎ合わせはここでやる)
        audioPlayerNode.stop()
        audioPlayerNode.scheduleFile(audioFile, at: nil)

        try audioEngine.start()
        audioPlayerNode.play()
    } catch let error {
        print(error)
    }
}

通しで書くとこんな感じ

import UIKit
import AVFoundation

class ViewController: UIViewController {

    var audioRecorder: AVAudioRecorder!
    var audioEngine: AVAudioEngine!
    var audioFile: AVAudioFile!
    var audioPlayerNode: AVAudioPlayerNode!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let session = AVAudioSession.sharedInstance()

        do {
            try session.setCategory(.playAndRecord, mode: .default)
            try session.setActive(true)

            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]

            audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
        } catch let error {
            print(error)
        }

    }

    @IBAction func record(_ sender: Any) {
        if !audioRecorder.isRecording {
            audioRecorder.record()
        } else {
            audioRecorder.stop()
        }
    }


    @IBAction func play(_ sender: Any) {
        audioEngine = AVAudioEngine()
        do {
            audioFile = try AVAudioFile(forReading: getAudioFileUrl())

            audioPlayerNode = AVAudioPlayerNode()
            audioEngine.attach(audioPlayerNode)
            audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)

            audioPlayerNode.stop()
            audioPlayerNode.scheduleFile(audioFile, at: nil)

            try audioEngine.start()
            audioPlayerNode.play()
        } catch let error {
            print(error)
        }
    }

    func getAudioFileUrl() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docsDirect = paths[0]
        let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

        return audioUrl
    }

}

こんなクラスファイルがあると実装が楽

この辺のユーティリティと言うほどではないけれど、いろんな画面から呼び出しそうな処理はこんな感じで一箇所にまとめて外出ししてしまった方がコードが冗長にならなくていいなあと思います:ok_woman:
あくまで一例ですが…

AudioManager.swift
import UIKit
import AVFoundation

final class AudioManager {

    var audioRecorder: AVAudioRecorder!
    var audioEngine: AVAudioEngine!
    var audioFile: AVAudioFile!
    var audioPlayerNode: AVAudioPlayerNode!
    var audioUnitTimePitch: AVAudioUnitTimePitch!

    init() {}

    func setUpAudioRecorder() {
        let session = AVAudioSession.sharedInstance()

        do {
            try session.setCategory(.playAndRecord, mode: .default)
            try session.setActive(true)

            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]

            audioRecorder = try AVAudioRecorder(url: getAudioFilrUrl(), settings: settings)
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
        } catch let error {
            print(error)
        }
    }

    func playSound(speed: Float, pitch: Float, echo: Bool, reverb: Bool) {
        audioEngine = AVAudioEngine()

        let url = getAudioFilrUrl()

        do {
            audioFile = try AVAudioFile(forReading: url)

            audioPlayerNode = AVAudioPlayerNode()
            audioEngine.attach(audioPlayerNode)

            audioUnitTimePitch = AVAudioUnitTimePitch()
            audioUnitTimePitch.rate = speed
            audioUnitTimePitch.pitch = pitch
            audioEngine.attach(audioUnitTimePitch)

            let echoNode = AVAudioUnitDistortion()
            echoNode.loadFactoryPreset(.multiEcho1)
            audioEngine.attach(echoNode)

            let reverbNode = AVAudioUnitReverb()
            reverbNode.loadFactoryPreset(.cathedral)
            reverbNode.wetDryMix = 50
            audioEngine.attach(reverbNode)

            if echo && reverb {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, echoNode, reverbNode, audioEngine.outputNode)
            } else if echo {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, echoNode, audioEngine.outputNode)
            } else if reverb {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, reverbNode, audioEngine.outputNode)
            } else {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, audioEngine.outputNode)
            }

            audioPlayerNode.stop()
            audioPlayerNode.scheduleFile(audioFile, at: nil)

            try audioEngine.start()
            audioPlayerNode.play()
        } catch let error {
            print(error)
        }
    }

    private func connectAudioNodes(_ nodes: AVAudioNode...) {
        for x in 0..<nodes.count - 1 {
            audioEngine.connect(nodes[x], to: nodes[x+1], format: audioFile.processingFormat)
        }
    }

    func getAudioFilrUrl() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docsDirect = paths[0]
        let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

        return audioUrl
    }
}

おわりに

単に音声の入出力だけでも結構色々やらなきゃいけないことが多いみたいでなれるまでは大変かもしれませんね。
個人的にはNodeをつなぎ合わせるという考え方が、楽器→エフェクター→アンプの接続を想起させるような作りになっていて面白いと思いました:open_mouth:

izey0306
nri
NRIは「コンサルティング」「金融 ITソリューション」「産業 ITソリューション」「IT 基盤サービス」の4事業でお客様のビジネスや快適な社会、暮らしを支えています。※各記事の内容は個人の見解であり、所属する組織の公式見解ではありません。
https://www.nri.com/jp/
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