Swiftで書いたコードに音声を再生する機能を追加します。音声の再生にはAVFoundationを使用し、SwiftUIで作成したボタンウィジェットと組み合わせましょう。ここではまず簡単な効果音を鳴らすコードを作成し、次に録音した音をそのまま再生する方法を紹介します。
こんにちは。wat(@watlablog)です。録音の次は音声を再生するSwiftコードを紹介します!
前回までのあらすじ
記事作成の動機
地道に音声系のブログ記事を書いてきて毎回出だしの文が同じになりそうだったので、今回は筆者がここまでにやってきたことを事前知識としてさらっと紹介するに留めます。
最終的には以下記事のような波形分析アプリを作りたいと思いプログラミング方法を調べていました。しかしこの記事で扱った方法ではiOSアプリへ適用するのが難しく、色々と挫折を経て王道であるSwiftプログラミングを始めたという経緯です。
・kivyでピーク検出機能付き簡易FFTアナライザを作ってみた
(Pythonで作成したアプリ。デスクトップアプリとしては使える。)
必要な基礎知識
①基礎文法をさらっと確認し、②録音方法と③グラフ表示方法を扱った後、④録音した波形をグラフに表示させる記事を書きました。
詳細はこれらのリンク先をご確認ください。
動作環境
この記事で紹介しているプログラミング環境を参考に紹介します。Xcodeはバージョンによりバグが出るかもしれません。当ブログのコードをコピペしても動かない場合は先ほど紹介した記事を確認してみると解決するかも…。
Mac | OS | macOS Ventura 13.2.1 |
---|---|---|
CPU | 1.4[GHz] | |
メモリ | 8[GB] | |
Xcode | Version | 14.3(14E222b) |
Swift | Version | 5.8(swiftlang-5.8.0.124.2 clang-1403.0.22.11.100) |
iPhone SE2 | OS | iOS 16.3.1 |
まずは簡単に「タップしたら効果音が鳴るアプリ」を作成する
事前に音声ファイルを準備する
効果音のフリー素材
音声の再生にはAVFoundationのAVAudioPlayerを使います。基本的な機能のみを学習するため、まずは最も簡単な「準備してある音声を再生する」というアプリを作ってみましょう。
ボタンをタップして効果音が鳴るアプリならシンプルで良さそうです。
今回、効果音はフリー素材として効果音ラボさんのを使いました。
※フリー素材といっても著作権は当然あります。必ず利用規約を読んだ方が良いです。
・効果音ラボ:https://soundeffect-lab.info/
Xcodeにファイルを登録する方法
音声ファイルを用意したらあとはXcodeに登録すれば簡単に使うことができます。
Xcodeを開き、ナビゲーションツリーにドラッグ&ドロップすれば登録可能です。これでプログラム上から参照できます。
ファイルは必ずしもプロジェクトファイルの場所になくても良いようですが、一応一緒にしておいた方が良いでしょう。
初期コード
まずはUI部分を作っておきましょう。こちらが今回の初期コードです。ボタンが中央に1つだけ配置されており、「Tap me!」と書かれています。このボタンをクリックしたら用意しておいた効果音が鳴るようにプログラミングします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import SwiftUI struct ContentView: View { var body: some View { VStack { Button("Tap me!"){ } .padding() .background(Color.blue) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } .padding(.all, 10) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
こんな感じ。
全コード
こちらが編集後のコード(ContentView.swift)です。説明は後述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import SwiftUI import AVFoundation struct ContentView: View { // AVAudioPlayerを宣言 @State private var audioPlayer: AVAudioPlayer? func playSound() { // 効果音を再生するメソッド if let soundURL = Bundle.main.url(forResource: "press", withExtension: "mp3") { do { audioPlayer = try AVAudioPlayer(contentsOf: soundURL) audioPlayer?.prepareToPlay() audioPlayer?.play() } catch { print("Error playing sound: \(error)") } } else { print("Sound file not found") } } var body: some View { VStack { Button("Tap me!"){ playSound() } .padding() .font(.largeTitle) .background(Color.blue) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } .padding(.all, 10) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
以下のコーディングを行い、あとはボタンアクション部分でplaySound()を呼び出せば音声が再生されます。
- import AVFoundation
ライブラリを使うためのimport文です。 - @State private var audioPlayer: AVAudioPlayer?
再生に必要なAVAudioPlayerオブジェクトをオプショナル型で宣言します。 - func playSound()
このメソッドで音声を再生する部分を書いています。Bundle.main.url(forResource: "press", withExtension: "mp3")でファイル名と拡張子を定義していますが、これを自分のファイル名に変更してください。
動画で紹介しようと思いましたが、効果音単体の音声動画は効果音ラボさんに記載の再配布に該当するとのことで、ここでは割愛します。ご自分の環境で試してみてください。
録音したデータを再生するSwiftコードの例
それでは本題です。AVAudioPlayerの使い方がわかったので、「SwiftUI/iOSアプリ:録音データをChartsでグラフ化する」の記事で紹介したコードに再生機能を追加してみましょう。
全てをContentView.swiftに記載すると長くなってしまうので、クラス部分をAudio.swift、本文をContentView.swiftに分けました。ここではその2つのファイルをそれぞれ紹介します。
Audio.swift
録音したデータを再生するために、AVAudioPlayerとともにaudioFileURLもクラスの上部で宣言し直しています。また、setUpAudioRecorder()におけるnilチェックを追加しています。playRecording()で音声を再生するという内容です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
import Foundation import AVFoundation class Recorder: NSObject, ObservableObject { // 録音クラス // オプショナル型としてAVAudioRecoderを定義 var audioRecorder: AVAudioRecorder? // 録音の状態を管理するプロパティ @Published var isRecording = false // 録音データの変数を宣言 @Published var waveformData: [Float] = [] // サンプリングレート var sampleRate: Double = 4096 // 再生用のAVAudioPlayerを宣言 private var player: AVAudioPlayer? // audioFileURL をプロパティとして宣言(クラス全体でアクセスするため:再生用) private var audioFileURL: URL? // カスタムクラスのコンストラクタを定義 override init() { super.init() setUpAudioRecorder() } private func setUpAudioRecorder() { // 録音の設定 let recordingSession = AVAudioSession.sharedInstance() // エラーを確認 do { try recordingSession.setCategory(.playAndRecord, mode: .default) try recordingSession.setActive(true) // 辞書型で設定値を変更 let settings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM), AVSampleRateKey: sampleRate, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsFloatKey: false, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // wavファイルのパスを設定する(.wavはリアルタイムに書き込まれる) let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] audioFileURL = documentsPath.appendingPathComponent("recording.wav") // audioFileURL の nil チェック guard let url = audioFileURL else { print("Error: audioFileURL is nil") return } do { audioRecorder = try AVAudioRecorder(url: url, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } catch { print("Error setting up audio recorder: \(error)") } audioRecorder = try AVAudioRecorder(url: url, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } // エラーの場合 catch { print("Error setting up audio recorder: \(error)") } } func startRecording() { // 録音するメソッド audioRecorder?.record() isRecording = true } func stopRecording() { // 録音停止するメソッド audioRecorder?.stop() isRecording = false // 録音停止時にwavファイルのパスをコンソールに表示する if let audioFileURL = audioRecorder?.url { print(audioFileURL)} // 配列データとして取得する getWaveformData { waveformData in print("wave length=", waveformData.count) self.waveformData = waveformData } } func getWaveformData(completion: @escaping ([Float]) -> Void) { // 録音結果の配列データを取得するメソッド guard let audioFileURL = audioRecorder?.url else { return } do { let audioFile = try AVAudioFile(forReading: audioFileURL) let audioFormat = AVAudioFormat(standardFormatWithSampleRate: audioFile.processingFormat.sampleRate, channels: audioFile.processingFormat.channelCount) let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat!, frameCapacity: UInt32(audioFile.length)) try audioFile.read(into: audioBuffer!) let floatArray = Array(UnsafeBufferPointer(start: audioBuffer!.floatChannelData![0], count: Int(audioBuffer!.frameLength))) completion(floatArray) } catch { print("Error getting waveform data: \(error)") } } func playRecording() { guard let url = audioFileURL else { print("Audio file not found") return } do { player = try AVAudioPlayer(contentsOf: url) player?.prepareToPlay() player?.play() } catch { print("Error playing audio: \(error)") } } } struct PointsData: Identifiable { // 点群データの構造体 var xValue: Float var yValue: Float var id = UUID() } |
ContentView.swift
Button("Play")をHStackで既存のStart Recordingボタンに横付けし、その部分でrecorder.playRecording()を呼び出しています。ContentView.swiftは簡単な内容ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
import SwiftUI import Charts struct ContentView: View { // データ構造関係の宣言 @State private var data: [PointsData] = [] @State private var x: [Float] = [] @State private var y: [Float] = [] // 録音関係の宣言 @StateObject private var recorder = Recorder() private var dt: Float { Float(1.0 / recorder.sampleRate) } @State private var isDisplayingData = false var body: some View { // UI VStack{ Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in // 折れ線グラフをプロット LineMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } .padding(.all, 10) HStack{ if recorder.isRecording { // 録音している時 Button(action: { // 停止ボタンが押されたらデータをChartsに表示させる // 録音の実行 print("Stop") recorder.stopRecording() // バックグラウンドでデータ処理を行う DispatchQueue.global(qos: .userInitiated).async { // データ取得 y = recorder.waveformData let samplePoints = Float(y.count) x = Array(stride(from: 0.0, to: samplePoints * dt, by: dt)) // プロットデータの追加 data.removeAll() // メインと同期させる DispatchQueue.main.async { data = zip(x, y).map { PointsData(xValue: $0, yValue: $1) } isDisplayingData = false } } }) { Text("Stop Recording") .padding() .background(Color.red) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } } else { // 録音していない時 Button(action: { print("Start") isDisplayingData = true recorder.startRecording() }) { Text("Start Recording") .padding() .background(Color.green) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } .opacity(isDisplayingData ? 0.5 : 1.0) .disabled(isDisplayingData) } Button("Play"){ // 音声を再生する recorder.playRecording() } .padding() .background(Color.blue) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
実行結果
こちらが実行結果です。音声付きなのでYouTubeに投稿してみました。
PCの内部音声のみ録音しているため、録音している前半は無音ですが、Playボタンを押した後は筆者の口笛が聴けます。
もちろんiPhone実機にインストールして使用することもできました。
iPhone実機で再生音が小さくなる問題の解決法
作ったアプリで遊んでいると、Macbookで再生する時とiPhoneで再生する時で、音量に明確な差があることがわかりました。iPhoneのスピーカーを最大音量に設定しても、録音した音声がか細い感じです。
これはAIの力により解決したのですが、「func playRecording() {}」を以下のように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func playRecording() { guard let url = audioFileURL else { print("Audio file not found") return } do { try? AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: .defaultToSpeaker) try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) try? AVAudioSession.sharedInstance().setActive(true) player = try AVAudioPlayer(contentsOf: url) player?.prepareToPlay() player?.play() } catch { print("Error playing audio: \(error)") } } |
3つのtryを追加するだけ。ここに至るまで色々なプロンプトを試しましたが、きちんと情報を伝えることで適切な回答が返ってきます。以下は敬意を表してChatGPT-4に自ら説明していただきましょう。
まとめ
このページではSwiftUIで作成したボタンをクリックするとAVFoundationによる音声再生が行われるアプリの作成方法を紹介しました。効果音単体のシンプルな再生方法で基礎を学び、その後録音したデータを再生するという2つのコード紹介の構成をとっています。
録音と再生ができるようになったので、ちょっとしたアプリ感が出てきました。
ここから先の波形分析にはもう少し越えなきゃいけないハードルがありそうですが、そういったところをクリアしていくのが趣味プログラミングの醍醐味なのかもしれません。
ただ、難しいところはチャットAIに聞きながらやっているので、僕はここに書いてあるコードの実力まではまだありません。AIにより(趣味の)コピペプログラマの未来は明るそうです。
録音と再生を覚えることができました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!