├── images
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── 6.png
├── Simple Recorder
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── RecordingState.swift
├── SimpleRecorder.swift
├── RecorderView.swift
└── AudioManager.swift
├── Simple Recorder.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── xiaoquan.xcuserdatad
│ │ ├── xcschemes
│ │ └── xcschememanagement.plist
│ │ └── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
└── project.pbxproj
└── README.md
/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/all-laugh/AudioRecorder-TextToSpeech-SwiftUI/HEAD/images/1.png
--------------------------------------------------------------------------------
/images/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/all-laugh/AudioRecorder-TextToSpeech-SwiftUI/HEAD/images/2.png
--------------------------------------------------------------------------------
/images/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/all-laugh/AudioRecorder-TextToSpeech-SwiftUI/HEAD/images/3.png
--------------------------------------------------------------------------------
/images/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/all-laugh/AudioRecorder-TextToSpeech-SwiftUI/HEAD/images/4.png
--------------------------------------------------------------------------------
/images/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/all-laugh/AudioRecorder-TextToSpeech-SwiftUI/HEAD/images/5.png
--------------------------------------------------------------------------------
/images/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/all-laugh/AudioRecorder-TextToSpeech-SwiftUI/HEAD/images/6.png
--------------------------------------------------------------------------------
/Simple Recorder/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Simple Recorder/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Simple Recorder/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Simple Recorder.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Simple Recorder.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Simple Recorder/RecordingState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordingState.swift
3 | // SimpleRecorder
4 | //
5 | // Created by Xiao Quan on 1/18/22.
6 | //
7 |
8 | import Foundation
9 |
10 | enum RecordingState: Int, CustomStringConvertible {
11 | case recording,
12 | paused,
13 | stopped,
14 | playing,
15 | playingSpeech
16 |
17 | var stateName: String {
18 | let states = ["Audio: Recording", "Audio: Paused", "Audio: Stopped", "Audio: Playing", "Audio: Playing Speech"]
19 |
20 | return states[self.rawValue]
21 | }
22 |
23 | var description: String {
24 | return stateName
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Simple Recorder.xcodeproj/xcuserdata/xiaoquan.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | LearningAVFoundation.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | Simple Recorder.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Simple Recorder/SimpleRecorder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SimpleRecorderApp.swift
3 | // SimpleRecorder
4 | //
5 | // Created by Xiao Quan on 1/18/22.
6 | //
7 |
8 | import SwiftUI
9 | import AVFoundation
10 |
11 |
12 | class AppDelegate: NSObject, UIApplicationDelegate {
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
14 |
15 | let session = AVAudioSession.sharedInstance()
16 | do {
17 | try session.setCategory(.playAndRecord, options: [.allowBluetooth])
18 | try session.setActive(true)
19 | } catch {
20 | print("Error setting AVAudioSession Category", error.localizedDescription)
21 | }
22 |
23 | print("Simulator Directory: \(NSHomeDirectory())")
24 |
25 | return true
26 | }
27 | }
28 |
29 | @main
30 | struct LearningAVFoundationApp: App {
31 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
32 |
33 | var body: some Scene {
34 | WindowGroup {
35 | RecorderView()
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Audio Recorder + Speech Synthesizer
2 |
3 | A simple audio recorder + speech synthesizer that utilizes essential functionalities of AVFoundation, and a little bit of AVAudioEngine for real-time audio effect processing.
4 |
5 | Hacked in a day while learning more about AVFoundation.
6 |
7 | ### Can:
8 |
9 | - Record 2 channel audio at 44100, linear PCM
10 | - Play back what just recorded immediately
11 | - Toggle a text to speech player with an Australian accent
12 | - Change the pitch and speed of the speech synthesizer in settings
13 | - Change the pitch and reverb dry/wet of normal recordings in real-time during playback
14 | - View and play recorded file in a separate tab
15 | - Delete recorded files
16 | - Handle audio route changes or app state changes accordingly using AVAudioSession and notification observers
17 |
18 | interface
19 |
20 |
21 |
22 | recording
23 |
24 |
25 |
26 | recorded files, tap to play
27 |
28 |
29 |
30 | speech synthesizer
31 |
32 |
33 |
34 | settings page
35 |
36 |
37 |
38 | ### Cannot:
39 |
40 | - alter speech synth speed/pitch in real-time
41 |
42 | ### Future work (probably in a another project):
43 |
44 | - refactor the heck out
45 | - nice to support recording in different formats/samplerates
46 |
--------------------------------------------------------------------------------
/Simple Recorder/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Simple Recorder.xcodeproj/xcuserdata/xiaoquan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
23 |
25 |
37 |
38 |
39 |
41 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/Simple Recorder/RecorderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SimpleRecorder
4 | //
5 | // Created by Xiao Quan on 1/18/22.
6 | //
7 |
8 | import SwiftUI
9 | import AVFAudio
10 |
11 | struct RecorderView: View {
12 | @ObservedObject var audioManager = AudioManager()
13 | @AppStorage("textToSpeak") var textToSpeak: String = "Hello there"
14 | @State var requestPermission: Bool = false
15 | @State var permissionGranted: Bool = false
16 |
17 | var body: some View {
18 | TabView {
19 | // MARK: - Play Tab
20 | VStack {
21 | Spacer()
22 |
23 | if !audioManager.playFromSpeechSynthesizer {
24 | // Record
25 | Button {
26 | if audioManager.recordingStatus == .stopped {
27 | if permissionGranted {
28 | audioManager.record()
29 | } else {
30 | requestAudioPermission()
31 | }
32 | } else {
33 | audioManager.stop()
34 | }
35 | } label: {
36 | Image(systemName: audioManager.recordingStatus == .recording ?
37 | "stop.circle.fill" : "record.circle.fill")
38 | .resizable()
39 | .aspectRatio(contentMode: .fit)
40 | .frame(width: 100, height: 100)
41 | .padding()
42 | }
43 | } else {
44 | if #available(iOS 15.0, *) {
45 | TextEditor(text: $textToSpeak)
46 | .frame(maxHeight: 250)
47 | .cornerRadius(10)
48 | .shadow(radius: 10)
49 | .padding()
50 | } else {
51 | // Fallback on earlier versions
52 | }
53 | }
54 |
55 | // Play
56 | if audioManager.previousRecordingUrl != nil {
57 | Button {
58 | if audioManager.recordingStatus == .stopped {
59 | print("current status: stopped. Will play recorded audio")
60 | audioManager.play()
61 | } else if audioManager.recordingStatus == .playing {
62 | print("current status: playing. Will stop playing")
63 | audioManager.stopPlayback()
64 | } else { // recording
65 | print("current status: recoding. Will stop recording and play")
66 | audioManager.stop()
67 | audioManager.play()
68 | }
69 | } label: {
70 | Image(systemName: audioManager.recordingStatus == .playing ||
71 | audioManager.recordingStatus == .playingSpeech ?
72 | "stop.circle":
73 | "play.circle")
74 | .resizable()
75 | .aspectRatio(contentMode: .fit)
76 | .frame(width: 100, height: 100)
77 | .padding()
78 | }
79 | }
80 |
81 | Spacer()
82 |
83 | if audioManager.recordingStatus == .recording ||
84 | audioManager.recordingStatus == .playing {
85 | Text(audioManager.time)
86 | .font(.custom("courier", size: 18))
87 | .bold()
88 | }
89 |
90 | Toggle("Speech Synthesizer", isOn: $audioManager.playFromSpeechSynthesizer)
91 | .padding(80)
92 | }
93 | .alert(isPresented: $requestPermission) {
94 | Alert(title: Text("Permission Denied"),
95 | message: Text("Got to settings to enable microphone access"),
96 | dismissButton: .default(Text("Ok")))
97 | }
98 | .tabItem {
99 | VStack {
100 | Image(systemName: "play")
101 | Text("Play")
102 | }
103 | }
104 | .tag(0)
105 |
106 | // MARK: - Files Tab
107 | List {
108 | ForEach (audioManager.recordedFileNames, id: \.self) { name in
109 | Button {
110 | audioManager.playFile(named: name)
111 | } label: {
112 | Text(name)
113 | }
114 | }
115 | .onDelete { indexSet in
116 | audioManager.deleteFile(at: indexSet)
117 | }
118 |
119 |
120 | }
121 | .tabItem {
122 | VStack {
123 | Image(systemName: "folder")
124 | Text("Recordings")
125 | }
126 | }
127 | .tag(1)
128 |
129 | // MARK: - Settings Tab
130 | List {
131 | Text("Settings")
132 | .font(.headline)
133 | .bold()
134 |
135 | Section(header: Text("Recording Effects")) {
136 | VStack {
137 | Slider(value: $audioManager.pitchShift.pitch, in: -2400...2400, step: 100, onEditingChanged: {_ in }, minimumValueLabel: Text("-24"), maximumValueLabel: Text("24"), label: {})
138 |
139 | Text("Pitch shift: \(String(format: "%4.0f", audioManager.pitchShift.pitch))")
140 | }
141 |
142 | VStack {
143 | Slider(value: $audioManager.reverb.wetDryMix, in:0...100, step: 1.0, onEditingChanged: {_ in }, minimumValueLabel: Text("0"), maximumValueLabel: Text("100"), label: {})
144 | Text("Reverb: \(String(format: "%2.0f", audioManager.reverb.wetDryMix))")
145 | }
146 | }
147 |
148 | Section(header: Text("Synthesizer Params")) {
149 | VStack {
150 | Slider(value: $audioManager.speechRate,
151 | in: AVSpeechUtteranceMinimumSpeechRate...AVSpeechUtteranceMaximumSpeechRate,
152 | label: {})
153 |
154 | Text("Speech Speed: \(String(format: "%1.2f", audioManager.speechRate + 0.5))")
155 | }
156 |
157 | VStack {
158 | Slider(value: $audioManager.speechPitch,
159 | in: 0.5...2.0,
160 | label: {})
161 |
162 | Text("Speech Pitch: \(String(format: "%1.2f", audioManager.speechPitch))")
163 | }
164 | }
165 |
166 | }
167 | .tabItem {
168 | VStack {
169 | Image(systemName: "gear")
170 | Text("Settings")
171 | }
172 | }
173 | .tag(2)
174 | }
175 | .accentColor(.red)
176 | .foregroundColor(.red)
177 | .onAppear {
178 | // audioManager.setupRecorder()
179 | audioManager.prepareEngine()
180 | }
181 | }
182 |
183 | private func requestAudioPermission() {
184 | let session = AVAudioSession.sharedInstance()
185 | session.requestRecordPermission { granted in
186 | self.permissionGranted = granted
187 | if granted {
188 | audioManager.record()
189 | } else {
190 | requestPermission = true
191 | }
192 | }
193 | }
194 | }
195 |
196 | struct ContentView_Previews: PreviewProvider {
197 | static var previews: some View {
198 | RecorderView()
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Simple Recorder/AudioManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AudioRecorder.swift
3 | // LearningAVFoundation
4 | //
5 | // Created by Xiao Quan on 1/18/22.
6 | //
7 |
8 | import Foundation
9 | import AVFoundation
10 | import SwiftUI
11 |
12 | class AudioManager: NSObject, ObservableObject {
13 |
14 | var audioRecorder: AVAudioRecorder?
15 | var audioPlayer: AVAudioPlayer?
16 | var updateTimer: CADisplayLink?
17 | var audioEngine = AVAudioEngine()
18 | var audioEnginePlayer = AVAudioPlayerNode()
19 | @Published var pitchShift = AVAudioUnitTimePitch()
20 | @Published var reverb = AVAudioUnitReverb()
21 | var recordTime: TimeInterval = 0.0
22 | var playbackTime: TimeInterval = 0.0
23 | var fileToPlay: AVAudioFile?
24 |
25 | @Published var playFromSpeechSynthesizer = false
26 | var speechSynthesizer = AVSpeechSynthesizer()
27 | @AppStorage("textToSpeak") var textToSpeak: String = "Hello there"
28 | @Published var speechPitch: Float = 1.0
29 | @Published var speechRate: Float = AVSpeechUtteranceDefaultSpeechRate
30 |
31 | @Published var recordingStatus: RecordingState = .stopped
32 | @Published var time = "00:00:00"
33 |
34 | var previousRecordingUrl: URL?
35 |
36 | override init() {
37 | super.init()
38 | let notificationCenter = NotificationCenter.default
39 | notificationCenter.addObserver(self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: nil)
40 | notificationCenter.addObserver(self, selector: #selector(handleInteruption), name: AVAudioSession.interruptionNotification, object: nil)
41 | notificationCenter.addObserver(self, selector: #selector(handleAEChange), name: .AVAudioEngineConfigurationChange, object: nil)
42 | speechSynthesizer.delegate = self
43 |
44 | loadFileNames()
45 | }
46 |
47 | deinit {
48 | NotificationCenter.default.removeObserver(self)
49 | }
50 |
51 | @objc func handleAEChange(notification: Notification) {
52 | if let info = notification.userInfo {
53 | print(info)
54 | }
55 | }
56 |
57 | @objc func handleRouteChange(notification: Notification) {
58 | if let info = notification.userInfo,
59 | let rawValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt {
60 | let reason = AVAudioSession.RouteChangeReason(rawValue: rawValue)
61 | if reason == .oldDeviceUnavailable {
62 | guard let previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription,
63 | let previousOutput = previousRoute.outputs.first else {
64 | return
65 | }
66 | if previousOutput.portType == .headphones {
67 | if recordingStatus == .playing {
68 | stopPlayback()
69 | } else if recordingStatus == .recording {
70 | stop()
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | @objc func handleInteruption(notification: Notification) {
78 | if let info = notification.userInfo,
79 | let rawValue = info[AVAudioSessionInterruptionTypeKey] as? UInt {
80 | let type = AVAudioSession.InterruptionType(rawValue: rawValue)
81 | if type == .began {
82 | if recordingStatus == .playing {
83 | stopPlayback()
84 | } else if recordingStatus == .recording {
85 | stop()
86 | }
87 | } else {
88 | if let rawValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
89 | let options = AVAudioSession.InterruptionOptions(rawValue: rawValue)
90 | if options == .shouldResume {
91 | // restart audio or restart recording
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
98 | func prepareEngine() {
99 | let format = audioEngine.outputNode.inputFormat(forBus: 0) // Something is fishy
100 | print(format.sampleRate)
101 | print(AVAudioSession.sharedInstance().sampleRate)
102 | audioEngine.attach(audioEnginePlayer)
103 | audioEngine.attach(pitchShift)
104 | audioEngine.attach(reverb)
105 | audioEngine.connect(audioEnginePlayer, to: pitchShift, format: format)
106 | audioEngine.connect(pitchShift, to: reverb, format: format)
107 | audioEngine.connect(reverb, to: audioEngine.outputNode, format: format)
108 |
109 | reverb.loadFactoryPreset(.cathedral)
110 | pitchShift.pitch = 0.0
111 | reverb.wetDryMix = 50
112 |
113 | audioEngine.prepare()
114 | print("AudioEngine is prepare to start")
115 | }
116 |
117 | func startEngine() {
118 | do {
119 | try audioEngine.start()
120 | print("Audio Engine prepared and running: ", audioEngine.isRunning)
121 | } catch {
122 | print("AudioEngine start failed: ", error.localizedDescription)
123 | }
124 | }
125 |
126 | func setupRecorder() {
127 | let settings: [String: Any] = [
128 | AVFormatIDKey : Int(kAudioFormatLinearPCM),
129 | AVSampleRateKey : 44100.0,
130 | AVNumberOfChannelsKey : 2,
131 | AVEncoderAudioQualityKey : AVAudioQuality.high.rawValue
132 | ]
133 | do {
134 | try audioRecorder = AVAudioRecorder(url: generateFileUrl(), settings: settings)
135 | audioRecorder?.delegate = self
136 | } catch {
137 | print("Error creating audio recorder with format: \(error.localizedDescription)")
138 | }
139 | }
140 |
141 | func record() {
142 | setupRecorder()
143 |
144 | if let recorder = audioRecorder {
145 | recorder.record()
146 | }
147 | recordingStatus = .recording
148 | startUpdateLoop()
149 | print("record(): ", audioEngine.isRunning)
150 | }
151 |
152 | func stop() {
153 | if let recorder = audioRecorder {
154 | recorder.stop()
155 |
156 | }
157 | recordingStatus = .stopped
158 | stopLoop()
159 | print("stop(): ", audioEngine.isRunning)
160 | }
161 |
162 | func play() {
163 | if playFromSpeechSynthesizer {
164 | let utterance = AVSpeechUtterance(string: textToSpeak)
165 | utterance.voice = AVSpeechSynthesisVoice(language: "en-AU")
166 | utterance.pitchMultiplier = speechPitch
167 | utterance.rate = speechRate
168 |
169 | speechSynthesizer.speak(utterance)
170 |
171 | } else {
172 | print("play() 1. Engine running? ", audioEngine.isRunning)
173 | do {
174 | fileToPlay = try AVAudioFile(forReading: previousRecordingUrl!)
175 | } catch {
176 | print("Error Loading Audio File as AVAudioFile")
177 | }
178 |
179 | guard fileToPlay != nil else { return }
180 | audioEnginePlayer.scheduleFile(fileToPlay!, at: nil, completionHandler: nil)
181 |
182 | do {
183 | try audioPlayer = AVAudioPlayer(contentsOf: previousRecordingUrl!)
184 | audioPlayer?.delegate = self
185 | } catch {
186 | print("Error loading audio file from temp directory")
187 | }
188 |
189 | guard audioPlayer != nil else { return }
190 | if audioPlayer!.duration > 0 {
191 | audioPlayer!.volume = 0
192 | audioPlayer!.prepareToPlay()
193 | }
194 |
195 | startEngine()
196 | print("play() 2. Engine running? ", audioEngine.isRunning)
197 |
198 | guard audioEngine.isRunning else { return }
199 |
200 | audioPlayer!.play()
201 | audioEnginePlayer.play()
202 | recordingStatus = .playing
203 | startUpdateLoop()
204 | }
205 | }
206 |
207 | func stopPlayback() {
208 | audioPlayer?.stop()
209 | audioEnginePlayer.stop()
210 | speechSynthesizer.stopSpeaking(at: .immediate)
211 | recordingStatus = .stopped
212 | stopLoop()
213 | }
214 |
215 | func startUpdateLoop() {
216 | if let updateTimer = updateTimer {
217 | updateTimer.invalidate()
218 | }
219 | updateTimer = CADisplayLink(target: self, selector: #selector(updateLoop))
220 | updateTimer?.add(to: .current, forMode: .common)
221 | }
222 |
223 | func stopLoop() {
224 | updateTimer?.invalidate()
225 | updateTimer = nil
226 | time = "00:00:00"
227 | }
228 |
229 | @objc func updateLoop() {
230 | if recordingStatus == .recording {
231 | if CFAbsoluteTimeGetCurrent() - recordTime > 0.5 {
232 | // print(audioRecorder?.currentTime)
233 | time = formatTime(UInt(audioRecorder?.currentTime ?? 0))
234 | recordTime = CFAbsoluteTimeGetCurrent()
235 | }
236 | } else if recordingStatus == .playing {
237 | if CFAbsoluteTimeGetCurrent() - playbackTime > 0.5 {
238 | // print(audioEnginePlayer.lastRenderTime?.audioTimeStamp.mHostTime)
239 | time = formatTime(UInt(audioPlayer?.currentTime ?? 0))
240 | playbackTime = CFAbsoluteTimeGetCurrent()
241 | }
242 | }
243 | }
244 |
245 | private func formatTime(_ time: UInt) -> String {
246 | let hour = time / 3600
247 | let minute = (time / 60) % 60
248 | let seconds = time % 60
249 |
250 | return String(format: "%02i:%02i:%02i", hour, minute, seconds)
251 | }
252 |
253 | private func generateFileUrl() -> URL {
254 | var url = previousRecordingUrl ?? documentFolderUrl
255 |
256 | do {
257 | let documentDirectory = try FileManager.default.url(for: .documentDirectory,
258 | in: .userDomainMask,
259 | appropriateFor: nil,
260 | create: false)
261 | let formatter = DateFormatter()
262 | formatter.dateFormat = "MM-dd-yyyy_HH-mm-ss"
263 |
264 | let time = formatter.string(from: Date())
265 |
266 | url = documentDirectory.appendingPathComponent("Recording_" + time + ".caf", isDirectory: false)
267 |
268 | print(url)
269 | } catch {
270 | print(error.localizedDescription)
271 | }
272 |
273 | previousRecordingUrl = url
274 |
275 | return url
276 | }
277 |
278 | var documentFolderUrl: URL {
279 | FileManager.default.urls(for: .documentDirectory,
280 | in: .userDomainMask)[0]
281 | }
282 |
283 | @Published var recordedFileNames: [String] = []
284 |
285 | private func loadFileNames() {
286 | do {
287 | recordedFileNames = try FileManager.default.contentsOfDirectory(atPath: documentFolderUrl.path).filter { $0.hasSuffix(".caf") }
288 | print("Loaded: ", recordedFileNames)
289 | } catch {
290 | print("Error loading document folder")
291 | }
292 | }
293 |
294 | func playFile(named name: String) {
295 | stop()
296 | let temp = previousRecordingUrl
297 | previousRecordingUrl = documentFolderUrl.appendingPathComponent(name, isDirectory: false)
298 | play()
299 | previousRecordingUrl = temp
300 | }
301 |
302 | func deleteFile(at offsets: IndexSet) {
303 | // var filenames = recordedFileNames
304 | // print("DELETE offsets? -> \(offsets.min())")
305 | // filenames.remove(atOffsets: offsets)
306 | if let indexToDelete = offsets.min() {
307 | let filename = recordedFileNames[indexToDelete]
308 | print("Deleting: ", filename)
309 | let url = documentFolderUrl.appendingPathComponent(filename, isDirectory: false)
310 | // print(url.path)
311 | try? FileManager.default.removeItem(at: url)
312 | }
313 | loadFileNames()
314 | }
315 |
316 | }
317 |
318 | extension AudioManager: AVAudioRecorderDelegate {
319 | func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
320 | recordingStatus = .stopped
321 | loadFileNames()
322 | }
323 | }
324 |
325 | extension AudioManager: AVAudioPlayerDelegate {
326 | func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
327 | print("Audio Player finished playing")
328 | recordingStatus = .stopped
329 | }
330 |
331 | }
332 |
333 | extension AudioManager: AVSpeechSynthesizerDelegate {
334 | func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
335 | }
336 |
337 | func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
338 | recordingStatus = .playingSpeech
339 | }
340 |
341 | func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
342 | recordingStatus = .stopped
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/Simple Recorder.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 506F85F22797450A00EF20AB /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506F85F12797450A00EF20AB /* AudioManager.swift */; };
11 | 506F85F427975B3A00EF20AB /* RecordingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506F85F327975B3A00EF20AB /* RecordingState.swift */; };
12 | 5098A60E27973F0200F608A7 /* SimpleRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098A60D27973F0200F608A7 /* SimpleRecorder.swift */; };
13 | 5098A61027973F0200F608A7 /* RecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098A60F27973F0200F608A7 /* RecorderView.swift */; };
14 | 5098A61227973F0300F608A7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5098A61127973F0300F608A7 /* Assets.xcassets */; };
15 | 5098A61527973F0300F608A7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5098A61427973F0300F608A7 /* Preview Assets.xcassets */; };
16 | 50D955F92798CA2800E88104 /* 4.png in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F22798CA2800E88104 /* 4.png */; };
17 | 50D955FA2798CA2800E88104 /* 5.png in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F32798CA2800E88104 /* 5.png */; };
18 | 50D955FB2798CA2800E88104 /* 6.png in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F42798CA2800E88104 /* 6.png */; };
19 | 50D955FC2798CA2800E88104 /* 2.png in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F52798CA2800E88104 /* 2.png */; };
20 | 50D955FD2798CA2800E88104 /* 3.png in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F62798CA2800E88104 /* 3.png */; };
21 | 50D955FE2798CA2800E88104 /* 1.png in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F72798CA2800E88104 /* 1.png */; };
22 | 50D955FF2798CA2800E88104 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 50D955F82798CA2800E88104 /* README.md */; };
23 | /* End PBXBuildFile section */
24 |
25 | /* Begin PBXFileReference section */
26 | 506F85F12797450A00EF20AB /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; };
27 | 506F85F327975B3A00EF20AB /* RecordingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingState.swift; sourceTree = ""; };
28 | 5098A60A27973F0200F608A7 /* Simple Recorder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Simple Recorder.app"; sourceTree = BUILT_PRODUCTS_DIR; };
29 | 5098A60D27973F0200F608A7 /* SimpleRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleRecorder.swift; sourceTree = ""; };
30 | 5098A60F27973F0200F608A7 /* RecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecorderView.swift; sourceTree = ""; };
31 | 5098A61127973F0300F608A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
32 | 5098A61427973F0300F608A7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
33 | 50D955F22798CA2800E88104 /* 4.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 4.png; sourceTree = ""; };
34 | 50D955F32798CA2800E88104 /* 5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 5.png; sourceTree = ""; };
35 | 50D955F42798CA2800E88104 /* 6.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 6.png; sourceTree = ""; };
36 | 50D955F52798CA2800E88104 /* 2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 2.png; sourceTree = ""; };
37 | 50D955F62798CA2800E88104 /* 3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 3.png; sourceTree = ""; };
38 | 50D955F72798CA2800E88104 /* 1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 1.png; sourceTree = ""; };
39 | 50D955F82798CA2800E88104 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
40 | /* End PBXFileReference section */
41 |
42 | /* Begin PBXFrameworksBuildPhase section */
43 | 5098A60727973F0200F608A7 /* Frameworks */ = {
44 | isa = PBXFrameworksBuildPhase;
45 | buildActionMask = 2147483647;
46 | files = (
47 | );
48 | runOnlyForDeploymentPostprocessing = 0;
49 | };
50 | /* End PBXFrameworksBuildPhase section */
51 |
52 | /* Begin PBXGroup section */
53 | 5098A60127973F0200F608A7 = {
54 | isa = PBXGroup;
55 | children = (
56 | 5098A60C27973F0200F608A7 /* Simple Recorder */,
57 | 5098A60B27973F0200F608A7 /* Products */,
58 | );
59 | sourceTree = "";
60 | };
61 | 5098A60B27973F0200F608A7 /* Products */ = {
62 | isa = PBXGroup;
63 | children = (
64 | 5098A60A27973F0200F608A7 /* Simple Recorder.app */,
65 | );
66 | name = Products;
67 | sourceTree = "";
68 | };
69 | 5098A60C27973F0200F608A7 /* Simple Recorder */ = {
70 | isa = PBXGroup;
71 | children = (
72 | 5098A60D27973F0200F608A7 /* SimpleRecorder.swift */,
73 | 506F85F12797450A00EF20AB /* AudioManager.swift */,
74 | 506F85F327975B3A00EF20AB /* RecordingState.swift */,
75 | 5098A60F27973F0200F608A7 /* RecorderView.swift */,
76 | 5098A61127973F0300F608A7 /* Assets.xcassets */,
77 | 5098A61327973F0300F608A7 /* Preview Content */,
78 | 50D955F12798CA2800E88104 /* images */,
79 | 50D955F82798CA2800E88104 /* README.md */,
80 | );
81 | path = "Simple Recorder";
82 | sourceTree = "";
83 | };
84 | 5098A61327973F0300F608A7 /* Preview Content */ = {
85 | isa = PBXGroup;
86 | children = (
87 | 5098A61427973F0300F608A7 /* Preview Assets.xcassets */,
88 | );
89 | path = "Preview Content";
90 | sourceTree = "";
91 | };
92 | 50D955F12798CA2800E88104 /* images */ = {
93 | isa = PBXGroup;
94 | children = (
95 | 50D955F22798CA2800E88104 /* 4.png */,
96 | 50D955F32798CA2800E88104 /* 5.png */,
97 | 50D955F42798CA2800E88104 /* 6.png */,
98 | 50D955F52798CA2800E88104 /* 2.png */,
99 | 50D955F62798CA2800E88104 /* 3.png */,
100 | 50D955F72798CA2800E88104 /* 1.png */,
101 | );
102 | path = images;
103 | sourceTree = SOURCE_ROOT;
104 | };
105 | /* End PBXGroup section */
106 |
107 | /* Begin PBXNativeTarget section */
108 | 5098A60927973F0200F608A7 /* Simple Recorder */ = {
109 | isa = PBXNativeTarget;
110 | buildConfigurationList = 5098A61827973F0300F608A7 /* Build configuration list for PBXNativeTarget "Simple Recorder" */;
111 | buildPhases = (
112 | 5098A60627973F0200F608A7 /* Sources */,
113 | 5098A60727973F0200F608A7 /* Frameworks */,
114 | 5098A60827973F0200F608A7 /* Resources */,
115 | );
116 | buildRules = (
117 | );
118 | dependencies = (
119 | );
120 | name = "Simple Recorder";
121 | productName = LearningAVFoundation;
122 | productReference = 5098A60A27973F0200F608A7 /* Simple Recorder.app */;
123 | productType = "com.apple.product-type.application";
124 | };
125 | /* End PBXNativeTarget section */
126 |
127 | /* Begin PBXProject section */
128 | 5098A60227973F0200F608A7 /* Project object */ = {
129 | isa = PBXProject;
130 | attributes = {
131 | BuildIndependentTargetsInParallel = 1;
132 | LastSwiftUpdateCheck = 1320;
133 | LastUpgradeCheck = 1320;
134 | TargetAttributes = {
135 | 5098A60927973F0200F608A7 = {
136 | CreatedOnToolsVersion = 13.2.1;
137 | };
138 | };
139 | };
140 | buildConfigurationList = 5098A60527973F0200F608A7 /* Build configuration list for PBXProject "Simple Recorder" */;
141 | compatibilityVersion = "Xcode 13.0";
142 | developmentRegion = en;
143 | hasScannedForEncodings = 0;
144 | knownRegions = (
145 | en,
146 | Base,
147 | );
148 | mainGroup = 5098A60127973F0200F608A7;
149 | productRefGroup = 5098A60B27973F0200F608A7 /* Products */;
150 | projectDirPath = "";
151 | projectRoot = "";
152 | targets = (
153 | 5098A60927973F0200F608A7 /* Simple Recorder */,
154 | );
155 | };
156 | /* End PBXProject section */
157 |
158 | /* Begin PBXResourcesBuildPhase section */
159 | 5098A60827973F0200F608A7 /* Resources */ = {
160 | isa = PBXResourcesBuildPhase;
161 | buildActionMask = 2147483647;
162 | files = (
163 | 50D955FC2798CA2800E88104 /* 2.png in Resources */,
164 | 50D955FE2798CA2800E88104 /* 1.png in Resources */,
165 | 50D955F92798CA2800E88104 /* 4.png in Resources */,
166 | 50D955FA2798CA2800E88104 /* 5.png in Resources */,
167 | 5098A61527973F0300F608A7 /* Preview Assets.xcassets in Resources */,
168 | 5098A61227973F0300F608A7 /* Assets.xcassets in Resources */,
169 | 50D955FF2798CA2800E88104 /* README.md in Resources */,
170 | 50D955FD2798CA2800E88104 /* 3.png in Resources */,
171 | 50D955FB2798CA2800E88104 /* 6.png in Resources */,
172 | );
173 | runOnlyForDeploymentPostprocessing = 0;
174 | };
175 | /* End PBXResourcesBuildPhase section */
176 |
177 | /* Begin PBXSourcesBuildPhase section */
178 | 5098A60627973F0200F608A7 /* Sources */ = {
179 | isa = PBXSourcesBuildPhase;
180 | buildActionMask = 2147483647;
181 | files = (
182 | 5098A61027973F0200F608A7 /* RecorderView.swift in Sources */,
183 | 506F85F427975B3A00EF20AB /* RecordingState.swift in Sources */,
184 | 5098A60E27973F0200F608A7 /* SimpleRecorder.swift in Sources */,
185 | 506F85F22797450A00EF20AB /* AudioManager.swift in Sources */,
186 | );
187 | runOnlyForDeploymentPostprocessing = 0;
188 | };
189 | /* End PBXSourcesBuildPhase section */
190 |
191 | /* Begin XCBuildConfiguration section */
192 | 5098A61627973F0300F608A7 /* Debug */ = {
193 | isa = XCBuildConfiguration;
194 | buildSettings = {
195 | ALWAYS_SEARCH_USER_PATHS = NO;
196 | CLANG_ANALYZER_NONNULL = YES;
197 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
199 | CLANG_CXX_LIBRARY = "libc++";
200 | CLANG_ENABLE_MODULES = YES;
201 | CLANG_ENABLE_OBJC_ARC = YES;
202 | CLANG_ENABLE_OBJC_WEAK = YES;
203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
204 | CLANG_WARN_BOOL_CONVERSION = YES;
205 | CLANG_WARN_COMMA = YES;
206 | CLANG_WARN_CONSTANT_CONVERSION = YES;
207 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
208 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
209 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
210 | CLANG_WARN_EMPTY_BODY = YES;
211 | CLANG_WARN_ENUM_CONVERSION = YES;
212 | CLANG_WARN_INFINITE_RECURSION = YES;
213 | CLANG_WARN_INT_CONVERSION = YES;
214 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
215 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
218 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
219 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
220 | CLANG_WARN_STRICT_PROTOTYPES = YES;
221 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
222 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
223 | CLANG_WARN_UNREACHABLE_CODE = YES;
224 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
225 | COPY_PHASE_STRIP = NO;
226 | DEBUG_INFORMATION_FORMAT = dwarf;
227 | ENABLE_STRICT_OBJC_MSGSEND = YES;
228 | ENABLE_TESTABILITY = YES;
229 | GCC_C_LANGUAGE_STANDARD = gnu11;
230 | GCC_DYNAMIC_NO_PIC = NO;
231 | GCC_NO_COMMON_BLOCKS = YES;
232 | GCC_OPTIMIZATION_LEVEL = 0;
233 | GCC_PREPROCESSOR_DEFINITIONS = (
234 | "DEBUG=1",
235 | "$(inherited)",
236 | );
237 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
238 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
239 | GCC_WARN_UNDECLARED_SELECTOR = YES;
240 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
241 | GCC_WARN_UNUSED_FUNCTION = YES;
242 | GCC_WARN_UNUSED_VARIABLE = YES;
243 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
244 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
245 | MTL_FAST_MATH = YES;
246 | ONLY_ACTIVE_ARCH = YES;
247 | SDKROOT = iphoneos;
248 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
249 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
250 | };
251 | name = Debug;
252 | };
253 | 5098A61727973F0300F608A7 /* Release */ = {
254 | isa = XCBuildConfiguration;
255 | buildSettings = {
256 | ALWAYS_SEARCH_USER_PATHS = NO;
257 | CLANG_ANALYZER_NONNULL = YES;
258 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
260 | CLANG_CXX_LIBRARY = "libc++";
261 | CLANG_ENABLE_MODULES = YES;
262 | CLANG_ENABLE_OBJC_ARC = YES;
263 | CLANG_ENABLE_OBJC_WEAK = YES;
264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
265 | CLANG_WARN_BOOL_CONVERSION = YES;
266 | CLANG_WARN_COMMA = YES;
267 | CLANG_WARN_CONSTANT_CONVERSION = YES;
268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
270 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
271 | CLANG_WARN_EMPTY_BODY = YES;
272 | CLANG_WARN_ENUM_CONVERSION = YES;
273 | CLANG_WARN_INFINITE_RECURSION = YES;
274 | CLANG_WARN_INT_CONVERSION = YES;
275 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
276 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
277 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
279 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
280 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
281 | CLANG_WARN_STRICT_PROTOTYPES = YES;
282 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
283 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
284 | CLANG_WARN_UNREACHABLE_CODE = YES;
285 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
286 | COPY_PHASE_STRIP = NO;
287 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
288 | ENABLE_NS_ASSERTIONS = NO;
289 | ENABLE_STRICT_OBJC_MSGSEND = YES;
290 | GCC_C_LANGUAGE_STANDARD = gnu11;
291 | GCC_NO_COMMON_BLOCKS = YES;
292 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
293 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
294 | GCC_WARN_UNDECLARED_SELECTOR = YES;
295 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
296 | GCC_WARN_UNUSED_FUNCTION = YES;
297 | GCC_WARN_UNUSED_VARIABLE = YES;
298 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
299 | MTL_ENABLE_DEBUG_INFO = NO;
300 | MTL_FAST_MATH = YES;
301 | SDKROOT = iphoneos;
302 | SWIFT_COMPILATION_MODE = wholemodule;
303 | SWIFT_OPTIMIZATION_LEVEL = "-O";
304 | VALIDATE_PRODUCT = YES;
305 | };
306 | name = Release;
307 | };
308 | 5098A61927973F0300F608A7 /* Debug */ = {
309 | isa = XCBuildConfiguration;
310 | buildSettings = {
311 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
312 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
313 | CODE_SIGN_STYLE = Automatic;
314 | CURRENT_PROJECT_VERSION = 1;
315 | DEVELOPMENT_ASSET_PATHS = "\"Simple Recorder/Preview Content\"";
316 | DEVELOPMENT_TEAM = XDPTRV2722;
317 | ENABLE_PREVIEWS = YES;
318 | GENERATE_INFOPLIST_FILE = YES;
319 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app uses the device to record audio.";
320 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
321 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
322 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
323 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
324 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
325 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
326 | LD_RUNPATH_SEARCH_PATHS = (
327 | "$(inherited)",
328 | "@executable_path/Frameworks",
329 | );
330 | MARKETING_VERSION = 1.0;
331 | PRODUCT_BUNDLE_IDENTIFIER = ninja.xquan.MDX;
332 | PRODUCT_NAME = "$(TARGET_NAME)";
333 | SWIFT_EMIT_LOC_STRINGS = YES;
334 | SWIFT_VERSION = 5.0;
335 | TARGETED_DEVICE_FAMILY = "1,2";
336 | };
337 | name = Debug;
338 | };
339 | 5098A61A27973F0300F608A7 /* Release */ = {
340 | isa = XCBuildConfiguration;
341 | buildSettings = {
342 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
343 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
344 | CODE_SIGN_STYLE = Automatic;
345 | CURRENT_PROJECT_VERSION = 1;
346 | DEVELOPMENT_ASSET_PATHS = "\"Simple Recorder/Preview Content\"";
347 | DEVELOPMENT_TEAM = XDPTRV2722;
348 | ENABLE_PREVIEWS = YES;
349 | GENERATE_INFOPLIST_FILE = YES;
350 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "This app uses the device to record audio.";
351 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
352 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
353 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
354 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
355 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
356 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
357 | LD_RUNPATH_SEARCH_PATHS = (
358 | "$(inherited)",
359 | "@executable_path/Frameworks",
360 | );
361 | MARKETING_VERSION = 1.0;
362 | PRODUCT_BUNDLE_IDENTIFIER = ninja.xquan.MDX;
363 | PRODUCT_NAME = "$(TARGET_NAME)";
364 | SWIFT_EMIT_LOC_STRINGS = YES;
365 | SWIFT_VERSION = 5.0;
366 | TARGETED_DEVICE_FAMILY = "1,2";
367 | };
368 | name = Release;
369 | };
370 | /* End XCBuildConfiguration section */
371 |
372 | /* Begin XCConfigurationList section */
373 | 5098A60527973F0200F608A7 /* Build configuration list for PBXProject "Simple Recorder" */ = {
374 | isa = XCConfigurationList;
375 | buildConfigurations = (
376 | 5098A61627973F0300F608A7 /* Debug */,
377 | 5098A61727973F0300F608A7 /* Release */,
378 | );
379 | defaultConfigurationIsVisible = 0;
380 | defaultConfigurationName = Release;
381 | };
382 | 5098A61827973F0300F608A7 /* Build configuration list for PBXNativeTarget "Simple Recorder" */ = {
383 | isa = XCConfigurationList;
384 | buildConfigurations = (
385 | 5098A61927973F0300F608A7 /* Debug */,
386 | 5098A61A27973F0300F608A7 /* Release */,
387 | );
388 | defaultConfigurationIsVisible = 0;
389 | defaultConfigurationName = Release;
390 | };
391 | /* End XCConfigurationList section */
392 | };
393 | rootObject = 5098A60227973F0200F608A7 /* Project object */;
394 | }
395 |
--------------------------------------------------------------------------------