Swiftを使ってiOSデバイスで録音アプリを作成しましょう。本記事では、Xcodeでプロジェクトを作成し、マイクを使った録音機能を実装する方法をわかりやすく解説します。iOSシミュレータやiPhoneでのテストまで、初心者でも安心して学べるよう、Swiftの基本操作も紹介します。
こんにちは。wat(@watlablog)です。今回はPythonで苦戦しているiOSデバイスによる録音機能実装をSwiftで行います!
本記事の目標
iOSデバイスで録音機能を実装する
WATLABブログではこれまでPythonだけを使って様々なアプリを作ってきました。特に2023年初頭は実行ファイル化にもトライしており、以下の記事でデスクトップアプリやモバイルアプリに挑戦しました。
・kivyでピーク検出機能付き簡易FFTアナライザを作ってみた
・PythonアプリをiOSで実機テスト:kivy-iosの使い方
デスクトップアプリは問題なく動作しましたが、モバイルアプリについてはpyaudioをはじめとした外部ライブラリが対応していない関係でiPhone実機のテストまで到達していない現状です。
なんとかPythonだけで最後までやってみたかったのですが、録音機能等の外部ライブラリをkivy-iosで使うためには開発ハードルの高いrecipesを自分で作る必要があります。
目的の機能をはやく自分のモバイル端末で動かしたいという気持ちが先行しているため、今回はiOSアプリ開発の王道であるSwiftを使って録音機能の実装を行います。
kivy-iosで使えるrecipesも作ってみたいですが、まずは王道を学びます!
SwiftとXcodeを使ったiOSアプリ開発へ入門する
Swiftとは?
Swiftとは、Appleが2014年に発表したオープンソースのプログラミング言語です。iOS以外にもmacOS、watchOS、tvOSといったApple製品のアプリケーションはこのSwift1つ習得すれば問題なく開発できます。
またSwiftはプログラミング言語の中でも比較的新しく、習得も容易でセキュリティの高さを意識して設計された言語とのことで、Python同様に初心者でも手が出せると思い今回挑戦してみます。
Xcodeを使ったiOSアプリ開発について
PythonではPyCharmをIDE(統合開発環境)として使っていましたが、SwiftではXcodeを使います。
Xcodeを使ったiOSアプリ開発の概要は以下の記事を参照してください。別途Apple IDも必要です。
・PythonアプリをiOSで実機テスト:kivy-iosの使い方
SwiftコードでHello Worldする
動作環境
この記事のコードは以下の環境で動作を確認しました。SwiftはXcodeをインストールしたときにすでに同梱されますが、バージョン確認はターミナルで「swift --version」と打てば確認できます。
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 |
プロジェクトの作成
いきなり録音機能を書く前に、まずは新しいプログラミング言語への入門として定番の「Hello World」をしてみましょう。
まずXcodeを起動します。
次に新規プロジェクトを作成するためにCreate a new Xcode projectをクリックします。
続いてiOSタブを選択し、Application欄からAppを選択したらNextをクリックします。
以下の画面になります。必要な項目を入力し、Nextをクリックします。
- Product Name
アプリの名称であり、必須項目です。 - Team
Apple IDを使って登録したDeveloper Teamのことです。アプリ実行時に設定されていないとエラーとなりますが、設定方法は「PythonアプリをiOSで実機テスト:kivy-iosの使い方」を参照してください。 - Organization Identifier
任意の名称を入力します。この記事の範疇では特に何を入力していても問題ありません。 - Bundle Identifier
Product NameとOrganization Identifierの二つの値を使って自動的に生成されます。このBundle Identifierがアプリ固有のIDとなります。 - Interface
ここではユーザーインターフェースにSwiftUIを選択します。 - Language
言語はSwiftです。 - Use Core Data
チェックを付けることで高度なデータベースを使用できるとのことですが、今回は使用しないのでチェックを外しておきます。 - Include Tests
チェックをつけることでUnit test機能が利用できます。Unit testとは、コードの各部分(関数やメソッド)を分離し、期待通り機能するか確認する手法です。バグの早期発見や品質向上に役立ちます。しかし今回は小規模なコードなのでチェックを外しておきます。
Xcodeのプロジェクトファイルを保存する場所を選択してCreateをクリックします。筆者はiOS-Projectsというフォルダを作りましたが、任意の場所で構いません。Create Git repository on my MacにチェックをつけることでGitリポジトリを作成することができますが、これも今回は外します。
Createをクリックすると、先ほどのフォルダにProduct Nameのフォルダが作成され、その中にXcodeプロジェクトファイルが保存されます。そしてXcodeの画面は以下のようになっているはずです(mac環境によって配置は異なるかもしれません)。
コードの変更方法と実行方法
コードの全体構成
プロジェクトが作成されると、画面にはテンプレートコードとして下画像のコードが既に記載されています。
コードには外部ライブラリをインポートするimport文が書いてある領域、UI(ユーザーインターフェース)やイベント処理等を記述するstruct ContentView: View {}の領域があります。
そして最後にプレビューを実行するためのstruct ContentView_Previews: PreviewProvider {}の領域があり、基本的な最低限の構成として既にHello Worldのテキストが表示されるプログラムとなっています。
オブジェクトのプロパティを変更する
テキストやボタンといったウィジェットオブジェクトは、フォントサイズや背景色といったプロパティ値を持ちます。XcodeではSwiftでコードを書く以外にも、ウィジェットを選択してGUI上で変更できます。
プレビュー画面の隅にある矢印マーク(Selectable)を選択し、ウィジェット選択モードにしましょう。
変更したいウィジェットをプレビュー画面から選択し、ユーティリティエリアに表示されている各種設定値を変更します。ここではFontをLarge Titleに変更しました。Large Titleにすることで、実機の機種違いや文字サイズの違いに応じて自動的にサイズ変更がされます。
ユーティリティエリアで変更した内容は自動的にSwiftコードに追加されます。色々な設定を変更して遊んでみるとコードの内容もより理解できるかもしれません。
ちなみにユーティリティエリアには全てのプロパティ値が表示されているわけではありません。例えば背景色を変更したい場合は、「Add modifier」という欄からBackgroundを検索して選択することで変更可能となります。
ボタンの追加とイベントの設定
次はGUIプログラミングの基本であるボタン設置とイベントの設定方法を覚えることで、Swift入門の仕上げとします。Xcodeにおけるウィジェットの追加はコード上から簡単にできます。今回はTextの下にボタンを配置しましょう。
まずTextのウィジェットが書いてあるコードの下に改行を行い、改行したところにカーソルを合わせてキーボードの「shift+command+L」を同時押しします。するとライブラリ一覧がポップアップされるので、ここから配置したいウィジェットを検索してダブルクリックで追加します。
先ほどと同様にボタンを配置するためのコードが自動追加されます。Actionと書いてあるところにボタンが押された時のイベント処理を設定します。
ウィジェットの設定やイベントの設定が完了したコードを以下に示します。筆者はXcode14.3を使っていますが、.backgroundで背景色を設定する時に、.background(Color.blue)ではなく.background(View)となっていました。色の設定はColorを使うのが正しいようなのでこれはもしかしたらXcode側のバグかもしれません。ご注意ください。
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 |
import SwiftUI struct ContentView: View { // テキストの初期値を設定 @State var outputText = "Hello world" var body: some View { VStack { // テキストに変数を設定する Text(outputText) .font(.largeTitle) .fontWeight(.bold) Button("BUTTON") { // ボタンが押されると「!」を文字列に追加する outputText += "!" } .padding(.all) .background(Color.blue) .foregroundColor(Color.white) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
結果はこちら。ボタンをクリックする度にビックリマークが1つ増える無駄アプリを生み出しました。
ここまでの説明でSwiftとXcodeを使ったプログラミングの概要はわかったと思います。次はいよいよ録音機能を書きましょう。
録音機能を実装するSwiftコード
録音機能を実装するためのポイント
全コードを示す前に、録音機能を実装するためのポイントをメモします。というのも、自分もこのコードをChatGPT-4に聞きながら書いたので、備忘録を残しておこうと思います。
最近話題のチャットAIはプログラミングもできちゃうんですね…おそろしい!
全体構成
全体構成として、クラスやメソッドの構成がわかりやすいように外枠のみのコードを以下に示します。
詳細を書くと長くなりますが、このように大枠だけ用意すると見通しが良くなりますね。
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 |
import SwiftUI import AVFoundation class Recorder: NSObject, ObservableObject { // 録音クラス private func setUpAudioRecorder() { // 録音の設定 } func startRecording() { // 録音するメソッド } func stopRecording() { // 録音停止するメソッド } struct ContentView: View { // UI } struct ContentView_Previews: PreviewProvider { // プレビュー static var previews: some View { ContentView() } } |
録音はclass Recorder{}で行います。privateメソッドで録音に関係する設定を行い、録音開始と録音停止のメソッドを内包しています。あとは先ほどのHello Worldプログラムと同じで、UIを記述するContentViewとプレビューを実行するContentView_Previewsを書いています。
またプログラムの冒頭でAVFoundationをimportしていますが、これがiOSで録音をする時に使う外部ライブラリです。
変数の宣言と初期化
以下のコードはclass Recorderの冒頭部分です。「var audioRecorder: AVAudioRecorder?」と変数に「?」がついているのは、AVAudioRecorderがnilになる可能性を持っていることから、オプショナル型として定義しているためです。
nilとはSwift言語において「値が存在しない」ことを示すもので、オプショナル型を使うことで値が存在しない状態を許容し、今回の場合はAVAudioRecorderがインスタンスを作成する際の初期化に失敗してもアプリがクラッシュしないようにしています。
isRecordingは録音しているかどうかをモニタするための変数です。
override.init()でクラスのオーバーライド(親クラスのイニシャライザを上書き)を行い、super.init()で親クラスのイニシャライザを呼び出しています。この一連の処理の中で最後にsetUpAudioRecorder()を呼びだすことで、本カスタムクラスを定義する際に自動的に録音に関する設定を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Recorder: NSObject, ObservableObject { // 録音クラス // nilになる可能性を考慮しオプショナル型として定義 var audioRecorder: AVAudioRecorder? // 録音の状態を管理するプロパティ @Published var isRecording = false // カスタムクラスのコンストラクタを定義 override init() { super.init() setUpAudioRecorder() } |
録音の設定とwavファイルの保存
次のコードは録音の設定に関する内容です。ここでサンプリングレートを44100Hz、チャンネル設定、縦軸16ビット設定等を行っています。この辺の話はPythonでも「PythonのPyAudioで音声録音する簡単な方法」の記事で扱いました。
またこの部分はdoブロックとcatchブロックでエラー処理を書いています。try文の内容がエラーの場合はcatchに書かれた文章がコンソールに出力されます。
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 |
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: 44100, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsFloatKey: false, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // wavファイルのパスを設定する(.wavはリアルタイムに書き込まれる) let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let audioFileURL = documentsPath.appendingPathComponent("recording.wav") audioRecorder = try AVAudioRecorder(url: audioFileURL, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } // エラーの場合 catch { print("Error setting up audio recorder: \(error)") } } |
今回のプログラムは録音をすることだけに特化していますが、本当に録音されたかどうかを確認するためにwavファイルをつくるようにしてみました(グラフ出力は別記事で行う予定)。
そのため途中でwavファイルのパス設定があります(録音したらrecording.wavというファイルが作成される)。wavは録音中にリアルタイムに保存されます。
録音開始と録音停止
録音開始と録音停止は.record()と.stop()とメソッドを呼び出しているだけです。UIの操作をするためにisRecording変数のtrueとfalseをボタンを押す毎に切り替えます。
録音停止時はwavファイルがどこにできたかをコンソールに表示させるようにしてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func startRecording() { // 録音するメソッド audioRecorder?.record() isRecording = true } func stopRecording() { // 録音停止するメソッド audioRecorder?.stop() isRecording = false // 録音停止時にwavファイルのパスをコンソールに表示する if let audioFileURL = audioRecorder?.url { print(audioFileURL)} } |
UIを含む全コード(コピペ可能)
以下にUI部分を含む全コードを示します。
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 |
import SwiftUI import AVFoundation class Recorder: NSObject, ObservableObject { // 録音クラス // nilになる可能性を考慮しオプショナル型として定義 var audioRecorder: AVAudioRecorder? // 録音の状態を管理するプロパティ @Published var isRecording = false // カスタムクラスのコンストラクタを定義 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: 44100, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsFloatKey: false, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // wavファイルのパスを設定する(.wavはリアルタイムに書き込まれる) let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let audioFileURL = documentsPath.appendingPathComponent("recording.wav") audioRecorder = try AVAudioRecorder(url: audioFileURL, 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)} } } struct ContentView: View { // UI @StateObject private var recorder = Recorder() var body: some View { VStack { if recorder.isRecording { // 録音している時 Button(action: { recorder.stopRecording() }) { Text("Stop Recording") .padding() .background(Color.red) .foregroundColor(Color.white) .cornerRadius(10) } } else { // 録音していない時 Button(action: { recorder.startRecording() }) { Text("Start Recording") .padding() .background(Color.green) .foregroundColor(Color.white) .cornerRadius(10) } } } } } struct ContentView_Previews: PreviewProvider { // プレビュー static var previews: some View { ContentView() } } |
アプリを起動した図がこちらです。録音待機中は緑のボタンで文字が「Start Recording」と書いてありますが、ボタンをクリックすると色が赤く変化し、Stop Recordingに変わります。
録音するとコンソールに以下のメッセージが表示されます。これは人によって変わるところですが、Xcodeのシミュレータの場合かなり深い階層にファイルが保存されるようです。
1 |
file:///Users/wat/Library/Developer/Xcode/UserData/Previews/Simulator%20Devices/1F777D20-4A39-4B28-8277-D3566FF9239F/data/Containers/Data/Application/10814639-AB93-40D6-8207-D494FAE3B21E/Documents/recording.wav |
以下はPythonで作成した波形分析ソフトに今回作成したwavファイルを読ませて波形分析してみた例です。周波数の違う二つの口笛を録音しましたが、ちゃんと波形分析可能なwavファイルとなっているようです。
ちなみにこの波形分析ソフトは「アプリ完成編:wxPythonで信号処理のGUIアプリをつくろう⑥」の記事にあります。
実機テスト時の注意点
プライバシー設定を追加する
シミュレータで実行しているうちは問題ありませんが、実機でマイクを使う場合はプロジェクト上でプライバシーに関する設定を追加する必要があります。
Xcodeのナビゲーションエリアでツリーの最上位を選択し、TARGETSの下に記載されたアプリ名を選択、その後Infoタブを選択してください。
Custom iOS Target Propertiesの項目で、+マークをクリックし「Privacy - Microphone Usage Description」の設定を追加します。この設定のValueにマイクを使う理由を記載します。
あとは「PythonアプリをiOSで実機テスト:kivy-iosの使い方」の記事の通り、ビルドしたアプリをiPhoneへ転送すると、アプリ起動時にマイク使用許可のメッセージが表示されます。ユーザーはこのメッセージのOKをクリックすることで録音機能を使うことができます。
先ほど書いた「マイクを使用する理由」がiPhone上に表示されていますね。
ちなみにこちらのファイルでグラフ化までできました!是非ご参考に。
SwiftUI/iOSアプリ:録音データをChartsでグラフ化する
ファイルをmac側から確認できない
これは現段階の設定や入出力コードの関係かもしれませんが、実機でアプリを実行し得られたファイルへmacOS側からアクセスすることができませんでした。
別途何かファイルへアクセスするためのPrivacy設定があるのか、それとも別の方法で確認するのかは別途記事で検証していきたいと思います。
とはいえ、これでPythonで苦労してまだできていなかったiOSデバイスによる録音ができるようになりました。あとはグラフ表示や波形分析処理を追加していけば目標(自分のモバイル端末で自分の作った波形分析アプリを動かす!)を達成できそうです。
まとめ
この記事ではPythonコードで苦労したiOSデバイスによる録音方法について、王道のSwiftで書いたプログラムをもって実現する方法を紹介しました。
筆者自身は今回Swiftでプログラムをするのが初めてだったため、課金しているChatGPT-4の力を借りながらようやく録音するためのコードを書くことができました(GPT先生に聞くプロンプトで結構試行錯誤しましたが)。
まだまだ完全に理解したわけではないため、この記事は備忘録もかねて基本的なSwiftとXcodeの使い方も含めてメモしました。個人的にはクラスの使い方やUIと関係を持たせる方法が少し理解できたのではないかと思います。
これまでPythonで色々書いてきましたが、Swiftも比較的初心者にやさしい言語なのではないかと思います。
Swiftに入門して録音機能を実装できました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!