├── Cable ├── .gitignore ├── Resources │ ├── ctia.png │ ├── cable.jpg │ ├── schematic.png │ └── cable_heads.jpg └── README.md ├── .gitignore ├── Resources ├── nintendo_switch.jpg └── inflight_entertainment.jpg ├── Echo ├── Resources │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Classes │ ├── AppDelegate.swift │ ├── ViewModel.swift │ ├── MainView.swift │ ├── SceneDelegate.swift │ └── AudioEchoSession.swift └── Info.plist ├── README.md ├── LICENSE └── Echo.xcodeproj └── project.pbxproj /Cable/.gitignore: -------------------------------------------------------------------------------- 1 | *.s#* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | project.xcworkspace/ 3 | xcuserdata/ 4 | build/ 5 | -------------------------------------------------------------------------------- /Cable/Resources/ctia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niw/Echo/HEAD/Cable/Resources/ctia.png -------------------------------------------------------------------------------- /Cable/Resources/cable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niw/Echo/HEAD/Cable/Resources/cable.jpg -------------------------------------------------------------------------------- /Cable/Resources/schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niw/Echo/HEAD/Cable/Resources/schematic.png -------------------------------------------------------------------------------- /Resources/nintendo_switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niw/Echo/HEAD/Resources/nintendo_switch.jpg -------------------------------------------------------------------------------- /Cable/Resources/cable_heads.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niw/Echo/HEAD/Cable/Resources/cable_heads.jpg -------------------------------------------------------------------------------- /Resources/inflight_entertainment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niw/Echo/HEAD/Resources/inflight_entertainment.jpg -------------------------------------------------------------------------------- /Echo/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Echo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Echo/Classes/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Echo 4 | // 5 | // Created by Yoshimasa Niwa on 12/23/19. 6 | // Copyright © 2019 Yoshimasa Niwa. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @main 12 | final class AppDelegate: UIResponder, UIApplicationDelegate { 13 | } 14 | -------------------------------------------------------------------------------- /Echo/Classes/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.swift 3 | // Echo 4 | // 5 | // Created by Yoshimasa Niwa on 5/1/21. 6 | // Copyright © 2021 Yoshimasa Niwa. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | final class ViewModel: ObservableObject { 13 | @Published 14 | var isRunning: Bool = false 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Echo 2 | ==== 3 | 4 | A simple iOS application that redirects microphone input to the Bluetooth 5 | audio device such as AirPods so that we can use AirPods always not just only for musics on iPhone, but also for the in-flight entertainment or Nintendo Switch. 6 | 7 | ![In-flight entertainment](Resources/inflight_entertainment.jpg) 8 | 9 | ![Nintendo Switch](Resources/nintendo_switch.jpg) 10 | 11 | In another words, basically this makes iPhone as a Bluetooth emitter. 12 | 13 | Usage 14 | ----- 15 | 16 | It requires the latet iOS and Xcode. Build the app and install it to your iOS device. 17 | 18 | You may need to switch audio output route to AirPods by using Control Center. 19 | 20 | ### Cable specification 21 | 22 | To audio input, you need to buy a specific cable or implement it manually that connects the headphone audio to the microphone. 23 | 24 | See the [cable documentation](Cable/README.md) for the details. 25 | -------------------------------------------------------------------------------- /Echo/Classes/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // Echo 4 | // 5 | // Created by Yoshimasa Niwa on 12/23/19. 6 | // Copyright © 2019 Yoshimasa Niwa. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MainView: View { 12 | @EnvironmentObject 13 | var viewModel: ViewModel 14 | 15 | var body: some View { 16 | NavigationView { 17 | Form { 18 | Section { 19 | Toggle(isOn: $viewModel.isRunning) { 20 | Text("Microphone to headphones") 21 | } 22 | Text("Use Control Center to change output device from headphones to Bluetooth audio device such as AirPods.") 23 | .foregroundColor(.secondary) 24 | } 25 | } 26 | .navigationBarTitle("Echo") 27 | } 28 | } 29 | } 30 | 31 | struct MainView_Previews: PreviewProvider { 32 | static var previews: some View { 33 | MainView() 34 | .environmentObject(ViewModel()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Yoshimasa Niwa 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Echo/Classes/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Echo 4 | // 5 | // Created by Yoshimasa Niwa on 12/23/19. 6 | // Copyright © 2019 Yoshimasa Niwa. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import SwiftUI 11 | import UIKit 12 | 13 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 14 | var window: UIWindow? 15 | 16 | var cancellableSet: Set = [] 17 | 18 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 19 | let viewModel = ViewModel() 20 | viewModel.$isRunning.sink { isRunning in 21 | if isRunning { 22 | AudioEchoSession.shared.start() 23 | } else { 24 | AudioEchoSession.shared.stop() 25 | } 26 | }.store(in: &cancellableSet) 27 | 28 | let mainView = MainView() 29 | .environmentObject(viewModel) 30 | 31 | if let windowScene = scene as? UIWindowScene { 32 | let window = UIWindow(windowScene: windowScene) 33 | window.rootViewController = UIHostingController(rootView: mainView) 34 | self.window = window 35 | window.makeKeyAndVisible() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Echo/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Echo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Echo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSMicrophoneUsageDescription 24 | The application redirects microphone input to the Bluetooth audio device. 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UIBackgroundModes 43 | 44 | audio 45 | 46 | UILaunchStoryboardName 47 | LaunchScreen 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Cable/README.md: -------------------------------------------------------------------------------- 1 | Echo Cable 2 | ========== 3 | 4 | To input headphone audio to the microphone, it requires a special cable with a few resistors and capacitors. 5 | 6 | There are a few such commercial products that we can buy on Amazon such as [3.5mm TRS Microphone Input Jack to TRRS Smartphone Adapter](https://www.amazon.com/-/de/dp/B01DVCA53U). 7 | 8 | Note that there are many similar products but not working as expected. Most of the products are for output iOS device music to the speaker or headphone, which doesn’t work for this purpose, such as [3.5mm Audio Cable with Lightning Connector](https://www.apple.com/shop/product/HM792ZM/A/belkin-35mm-audio-cable-with-lightning-connector). 9 | 10 | If you want to make it manually, refer to the following schematic. 11 | 12 | You may also use [Lightning to 3.5 mm Headphone Jack Adapter](https://www.apple.com/shop/product/MMX62AM/A/lightning-to-35-mm-headphone-jack-adapter) or [USB-C to 3.5 mm Headphone Jack Adapter](https://www.apple.com/shop/product/MU7E2AM/A/usb-c-to-35-mm-headphone-jack-adapter), depends on your iOS device. 13 | 14 | Schematic 15 | --------- 16 | 17 | Here is a schematic of the circuit of the cable. 18 | 19 | ![Schematic](Resources/schematic.png) 20 | 21 | - `JP1` is for input from 3.5mm audio headphone jack. 22 | - `JP2` is for output to 3.5mm audio headphone jack with microphone input. See the following section about the details. 23 | - `R1`, `R2` and `R3` are to reduce headphone level about -20dB. 24 | - `C1` cuts direct current. 25 | - `R5` is to let iOS devices recognize there is a microphone connected. 26 | 27 | You can omit `R1`, `R2`, `R3`, `R4` if you want to make it simple, however, `C1` and `R5` are required at a minimum. 28 | 29 | This diagram is based on [Audio Loopback Dongle](https://source.android.com/devices/audio/latency/loopback). 30 | 31 | iOS device 3.5mm headphone jack 32 | ------------------------------- 33 | 34 | All iOS devices use 4 pins 3.5mm Tip Ring Ring Shield (TRRS) headphone jack that comes with left, right audio output with a monaural microphone and control input, which is based on CTIA pinout. 35 | 36 | That means, each pin is for left, right, GND then microphone in this order from tip to shield. 37 | 38 | ![CTIA](Resources/ctia.png) 39 | 40 | Implementation 41 | -------------- 42 | 43 | You can implement this circuit inside of each end of the 3.5mm audio jack header. 44 | 45 | ![Cable implementation example](Resources/cable.jpg) 46 | 47 | ![Cable heads](Resources/cable_heads.jpg) 48 | -------------------------------------------------------------------------------- /Echo/Classes/AudioEchoSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioEchoSession.swift 3 | // Echo 4 | // 5 | // Created by Yoshimasa Niwa on 12/23/19. 6 | // Copyright © 2019 Yoshimasa Niwa. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | import Foundation 11 | 12 | final class AudioEchoSession { 13 | static let shared: AudioEchoSession = .init() 14 | 15 | private var audioEngine: AVAudioEngine? 16 | 17 | init() { 18 | NotificationCenter.default.addObserver(self, selector: #selector(audioSessionDidInterrupt(_:)), name: AVAudioSession.interruptionNotification, object: nil) 19 | NotificationCenter.default.addObserver(self, selector: #selector(audioSessionRouteDidChanged(_:)), name: AVAudioSession.routeChangeNotification, object: nil) 20 | NotificationCenter.default.addObserver(self, selector: #selector(audioSessionMediaServicesDidLost(_:)), name: AVAudioSession.mediaServicesWereLostNotification, object: nil) 21 | NotificationCenter.default.addObserver(self, selector: #selector(audioSessionMediaServicesDidReset(_:)), name: AVAudioSession.mediaServicesWereResetNotification, object: nil) 22 | 23 | do { 24 | try resetAudioSession() 25 | resetAudioEngine() 26 | } catch { 27 | print(error) 28 | } 29 | } 30 | 31 | enum AudioSessionPortDescriptionUID: String { 32 | case WiredMicrophone = "Wired Microphone" 33 | } 34 | 35 | private func resetAudioSession() throws { 36 | let session = AVAudioSession.sharedInstance() 37 | try session.setCategory(.playAndRecord, mode: .default, options: [.mixWithOthers, .allowBluetoothA2DP]) 38 | try session.setPreferredInput(session.availableInputs?.first(where: { (description) -> Bool in 39 | return description.uid == AudioSessionPortDescriptionUID.WiredMicrophone.rawValue 40 | })) 41 | } 42 | 43 | private func resetAudioEngine() { 44 | if let audioEngine = audioEngine { 45 | audioEngine.stop() 46 | self.audioEngine = nil 47 | } 48 | 49 | let audioEngine = AVAudioEngine() 50 | audioEngine.connect(audioEngine.inputNode, to: audioEngine.mainMixerNode, format: nil) 51 | audioEngine.connect(audioEngine.mainMixerNode, to: audioEngine.outputNode, format: nil) 52 | self.audioEngine = audioEngine 53 | } 54 | 55 | private(set) var isRunning: Bool = false 56 | 57 | func start() { 58 | guard !isRunning else { return } 59 | 60 | do { 61 | try startSession() 62 | isRunning = true 63 | } catch { 64 | print(error) 65 | } 66 | } 67 | 68 | private func startSession() throws { 69 | try AVAudioSession.sharedInstance().setActive(true) 70 | try audioEngine!.start() // Intentionally forcibly unwrap or assert crash. 71 | } 72 | 73 | func stop() { 74 | guard isRunning else { return } 75 | 76 | do { 77 | try stopSession() 78 | isRunning = false 79 | } catch { 80 | print(error) 81 | } 82 | } 83 | 84 | private func stopSession() throws { 85 | audioEngine!.stop() // Intentionally forcibly unwrap or assert crash. 86 | try AVAudioSession.sharedInstance().setActive(false) 87 | } 88 | 89 | // MARK: - Notifications 90 | 91 | @objc 92 | public func audioSessionDidInterrupt(_ notification: NSNotification) { 93 | // Making a phone call can reach here. 94 | 95 | guard let userInfo = notification.userInfo, 96 | let interruptionTypeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, 97 | let interruptionType = AVAudioSession.InterruptionType(rawValue: interruptionTypeValue) else { 98 | return 99 | } 100 | 101 | switch interruptionType { 102 | case .began: 103 | print("interruption began") 104 | case .ended: 105 | print("interruption end") 106 | 107 | // NOTE: Based on the documentation, apps that don’t require user input to begin audio 108 | // playback can ignore `shouldResume` flag in `AVAudioSession.InterruptionOptions` 109 | // for `AVAudioSessionInterruptionOptionKey` and resume playback when an interruption ends. 110 | // See `AVAudioSession.InterruptionOptions.shouldResume` 111 | if isRunning { 112 | // TODO: do we really need to reconstruct audio engine? probably not. 113 | resumeSession() 114 | } 115 | default: 116 | break 117 | } 118 | } 119 | 120 | @objc 121 | public func audioSessionRouteDidChanged(_ notification: NSNotification) { 122 | print(AVAudioSession.sharedInstance().currentRoute) 123 | } 124 | 125 | @objc 126 | public func audioSessionMediaServicesDidLost(_ notification: NSNotification) { 127 | // TODO: implement this. 128 | } 129 | 130 | @objc 131 | public func audioSessionMediaServicesDidReset(_ notification: NSNotification) { 132 | // See General recommendations for handling `AVAudioSessionMediaServicesWereResetNotification` 133 | // at 134 | resumeSession() 135 | } 136 | 137 | private func resumeSession() { 138 | do { 139 | try resetAudioSession() 140 | resetAudioEngine() 141 | if (isRunning) { 142 | try startSession() 143 | } 144 | } catch { 145 | print(error) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Echo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5434B09D23CCD95A003B78E4 /* AudioEchoSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5434B09C23CCD95A003B78E4 /* AudioEchoSession.swift */; }; 11 | 5434B09E23CCDB1A003B78E4 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3C45323CCD7CA00FEFABE /* MainView.swift */; }; 12 | 54782591263D34C700626A7C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54782590263D34C700626A7C /* ViewModel.swift */; }; 13 | 54D3C45023CCD7CA00FEFABE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3C44F23CCD7CA00FEFABE /* AppDelegate.swift */; }; 14 | 54D3C45223CCD7CA00FEFABE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3C45123CCD7CA00FEFABE /* SceneDelegate.swift */; }; 15 | 54D3C45623CCD7CB00FEFABE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54D3C45523CCD7CB00FEFABE /* Assets.xcassets */; }; 16 | 54D3C45923CCD7CB00FEFABE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 54D3C45823CCD7CB00FEFABE /* Preview Assets.xcassets */; }; 17 | 54D3C45C23CCD7CB00FEFABE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54D3C45A23CCD7CB00FEFABE /* LaunchScreen.storyboard */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 5434B09C23CCD95A003B78E4 /* AudioEchoSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioEchoSession.swift; sourceTree = ""; }; 22 | 54782590263D34C700626A7C /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 23 | 54D3C44C23CCD7CA00FEFABE /* Echo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Echo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | 54D3C44F23CCD7CA00FEFABE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | 54D3C45123CCD7CA00FEFABE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 26 | 54D3C45323CCD7CA00FEFABE /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 27 | 54D3C45523CCD7CB00FEFABE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 54D3C45823CCD7CB00FEFABE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 29 | 54D3C45B23CCD7CB00FEFABE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 54D3C45D23CCD7CB00FEFABE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 54D3C44923CCD7CA00FEFABE /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 540DB52F23CCD856001A8D0A /* Classes */ = { 45 | isa = PBXGroup; 46 | children = ( 47 | 54D3C44F23CCD7CA00FEFABE /* AppDelegate.swift */, 48 | 5434B09C23CCD95A003B78E4 /* AudioEchoSession.swift */, 49 | 54D3C45323CCD7CA00FEFABE /* MainView.swift */, 50 | 54D3C45123CCD7CA00FEFABE /* SceneDelegate.swift */, 51 | 54782590263D34C700626A7C /* ViewModel.swift */, 52 | ); 53 | path = Classes; 54 | sourceTree = ""; 55 | }; 56 | 540DB53023CCD85C001A8D0A /* Resources */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 54D3C45523CCD7CB00FEFABE /* Assets.xcassets */, 60 | 54D3C45A23CCD7CB00FEFABE /* LaunchScreen.storyboard */, 61 | ); 62 | path = Resources; 63 | sourceTree = ""; 64 | }; 65 | 54D3C44323CCD7CA00FEFABE = { 66 | isa = PBXGroup; 67 | children = ( 68 | 54D3C44E23CCD7CA00FEFABE /* Echo */, 69 | 54D3C44D23CCD7CA00FEFABE /* Products */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | 54D3C44D23CCD7CA00FEFABE /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 54D3C44C23CCD7CA00FEFABE /* Echo.app */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | 54D3C44E23CCD7CA00FEFABE /* Echo */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 540DB52F23CCD856001A8D0A /* Classes */, 85 | 54D3C45723CCD7CB00FEFABE /* Preview Content */, 86 | 540DB53023CCD85C001A8D0A /* Resources */, 87 | 54D3C45D23CCD7CB00FEFABE /* Info.plist */, 88 | ); 89 | path = Echo; 90 | sourceTree = ""; 91 | }; 92 | 54D3C45723CCD7CB00FEFABE /* Preview Content */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 54D3C45823CCD7CB00FEFABE /* Preview Assets.xcassets */, 96 | ); 97 | path = "Preview Content"; 98 | sourceTree = ""; 99 | }; 100 | /* End PBXGroup section */ 101 | 102 | /* Begin PBXNativeTarget section */ 103 | 54D3C44B23CCD7CA00FEFABE /* Echo */ = { 104 | isa = PBXNativeTarget; 105 | buildConfigurationList = 54D3C46023CCD7CB00FEFABE /* Build configuration list for PBXNativeTarget "Echo" */; 106 | buildPhases = ( 107 | 54D3C44823CCD7CA00FEFABE /* Sources */, 108 | 54D3C44923CCD7CA00FEFABE /* Frameworks */, 109 | 54D3C44A23CCD7CA00FEFABE /* Resources */, 110 | ); 111 | buildRules = ( 112 | ); 113 | dependencies = ( 114 | ); 115 | name = Echo; 116 | productName = Echo; 117 | productReference = 54D3C44C23CCD7CA00FEFABE /* Echo.app */; 118 | productType = "com.apple.product-type.application"; 119 | }; 120 | /* End PBXNativeTarget section */ 121 | 122 | /* Begin PBXProject section */ 123 | 54D3C44423CCD7CA00FEFABE /* Project object */ = { 124 | isa = PBXProject; 125 | attributes = { 126 | LastSwiftUpdateCheck = 1130; 127 | LastUpgradeCheck = 1310; 128 | ORGANIZATIONNAME = "Yoshimasa Niwa"; 129 | TargetAttributes = { 130 | 54D3C44B23CCD7CA00FEFABE = { 131 | CreatedOnToolsVersion = 11.3; 132 | }; 133 | }; 134 | }; 135 | buildConfigurationList = 54D3C44723CCD7CA00FEFABE /* Build configuration list for PBXProject "Echo" */; 136 | compatibilityVersion = "Xcode 9.3"; 137 | developmentRegion = en; 138 | hasScannedForEncodings = 0; 139 | knownRegions = ( 140 | en, 141 | Base, 142 | ); 143 | mainGroup = 54D3C44323CCD7CA00FEFABE; 144 | productRefGroup = 54D3C44D23CCD7CA00FEFABE /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | 54D3C44B23CCD7CA00FEFABE /* Echo */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | 54D3C44A23CCD7CA00FEFABE /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 54D3C45C23CCD7CB00FEFABE /* LaunchScreen.storyboard in Resources */, 159 | 54D3C45923CCD7CB00FEFABE /* Preview Assets.xcassets in Resources */, 160 | 54D3C45623CCD7CB00FEFABE /* Assets.xcassets in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | 54D3C44823CCD7CA00FEFABE /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 54782591263D34C700626A7C /* ViewModel.swift in Sources */, 172 | 54D3C45023CCD7CA00FEFABE /* AppDelegate.swift in Sources */, 173 | 5434B09E23CCDB1A003B78E4 /* MainView.swift in Sources */, 174 | 54D3C45223CCD7CA00FEFABE /* SceneDelegate.swift in Sources */, 175 | 5434B09D23CCD95A003B78E4 /* AudioEchoSession.swift in Sources */, 176 | ); 177 | runOnlyForDeploymentPostprocessing = 0; 178 | }; 179 | /* End PBXSourcesBuildPhase section */ 180 | 181 | /* Begin PBXVariantGroup section */ 182 | 54D3C45A23CCD7CB00FEFABE /* LaunchScreen.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | 54D3C45B23CCD7CB00FEFABE /* Base */, 186 | ); 187 | name = LaunchScreen.storyboard; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXVariantGroup section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | 54D3C45E23CCD7CB00FEFABE /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | CLANG_ANALYZER_NONNULL = YES; 198 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 200 | CLANG_CXX_LIBRARY = "libc++"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = dwarf; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_TESTABILITY = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu11; 231 | GCC_DYNAMIC_NO_PIC = NO; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_OPTIMIZATION_LEVEL = 0; 234 | GCC_PREPROCESSOR_DEFINITIONS = ( 235 | "DEBUG=1", 236 | "$(inherited)", 237 | ); 238 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 239 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 240 | GCC_WARN_UNDECLARED_SELECTOR = YES; 241 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 242 | GCC_WARN_UNUSED_FUNCTION = YES; 243 | GCC_WARN_UNUSED_VARIABLE = YES; 244 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 245 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 246 | MTL_FAST_MATH = YES; 247 | ONLY_ACTIVE_ARCH = YES; 248 | SDKROOT = iphoneos; 249 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 250 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 251 | }; 252 | name = Debug; 253 | }; 254 | 54D3C45F23CCD7CB00FEFABE /* Release */ = { 255 | isa = XCBuildConfiguration; 256 | buildSettings = { 257 | ALWAYS_SEARCH_USER_PATHS = NO; 258 | CLANG_ANALYZER_NONNULL = YES; 259 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 261 | CLANG_CXX_LIBRARY = "libc++"; 262 | CLANG_ENABLE_MODULES = YES; 263 | CLANG_ENABLE_OBJC_ARC = YES; 264 | CLANG_ENABLE_OBJC_WEAK = YES; 265 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 266 | CLANG_WARN_BOOL_CONVERSION = YES; 267 | CLANG_WARN_COMMA = YES; 268 | CLANG_WARN_CONSTANT_CONVERSION = YES; 269 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 270 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 271 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 272 | CLANG_WARN_EMPTY_BODY = YES; 273 | CLANG_WARN_ENUM_CONVERSION = YES; 274 | CLANG_WARN_INFINITE_RECURSION = YES; 275 | CLANG_WARN_INT_CONVERSION = YES; 276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 278 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 280 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 282 | CLANG_WARN_STRICT_PROTOTYPES = YES; 283 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 285 | CLANG_WARN_UNREACHABLE_CODE = YES; 286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 287 | COPY_PHASE_STRIP = NO; 288 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 289 | ENABLE_NS_ASSERTIONS = NO; 290 | ENABLE_STRICT_OBJC_MSGSEND = YES; 291 | GCC_C_LANGUAGE_STANDARD = gnu11; 292 | GCC_NO_COMMON_BLOCKS = YES; 293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 295 | GCC_WARN_UNDECLARED_SELECTOR = YES; 296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 297 | GCC_WARN_UNUSED_FUNCTION = YES; 298 | GCC_WARN_UNUSED_VARIABLE = YES; 299 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 300 | MTL_ENABLE_DEBUG_INFO = NO; 301 | MTL_FAST_MATH = YES; 302 | SDKROOT = iphoneos; 303 | SWIFT_COMPILATION_MODE = wholemodule; 304 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 305 | VALIDATE_PRODUCT = YES; 306 | }; 307 | name = Release; 308 | }; 309 | 54D3C46123CCD7CB00FEFABE /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 313 | CODE_SIGN_STYLE = Manual; 314 | DEVELOPMENT_ASSET_PATHS = "\"Echo/Preview Content\""; 315 | DEVELOPMENT_TEAM = ""; 316 | ENABLE_PREVIEWS = YES; 317 | INFOPLIST_FILE = Echo/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = ( 319 | "$(inherited)", 320 | "@executable_path/Frameworks", 321 | ); 322 | MARKETING_VERSION = 0.1.0; 323 | PRODUCT_BUNDLE_IDENTIFIER = at.niw.Echo; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | PROVISIONING_PROFILE_SPECIFIER = ""; 326 | SWIFT_VERSION = 5.0; 327 | TARGETED_DEVICE_FAMILY = "1,2"; 328 | }; 329 | name = Debug; 330 | }; 331 | 54D3C46223CCD7CB00FEFABE /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 335 | CODE_SIGN_STYLE = Manual; 336 | DEVELOPMENT_ASSET_PATHS = "\"Echo/Preview Content\""; 337 | DEVELOPMENT_TEAM = ""; 338 | ENABLE_PREVIEWS = YES; 339 | INFOPLIST_FILE = Echo/Info.plist; 340 | LD_RUNPATH_SEARCH_PATHS = ( 341 | "$(inherited)", 342 | "@executable_path/Frameworks", 343 | ); 344 | MARKETING_VERSION = 0.1.0; 345 | PRODUCT_BUNDLE_IDENTIFIER = at.niw.Echo; 346 | PRODUCT_NAME = "$(TARGET_NAME)"; 347 | PROVISIONING_PROFILE_SPECIFIER = ""; 348 | SWIFT_VERSION = 5.0; 349 | TARGETED_DEVICE_FAMILY = "1,2"; 350 | }; 351 | name = Release; 352 | }; 353 | /* End XCBuildConfiguration section */ 354 | 355 | /* Begin XCConfigurationList section */ 356 | 54D3C44723CCD7CA00FEFABE /* Build configuration list for PBXProject "Echo" */ = { 357 | isa = XCConfigurationList; 358 | buildConfigurations = ( 359 | 54D3C45E23CCD7CB00FEFABE /* Debug */, 360 | 54D3C45F23CCD7CB00FEFABE /* Release */, 361 | ); 362 | defaultConfigurationIsVisible = 0; 363 | defaultConfigurationName = Release; 364 | }; 365 | 54D3C46023CCD7CB00FEFABE /* Build configuration list for PBXNativeTarget "Echo" */ = { 366 | isa = XCConfigurationList; 367 | buildConfigurations = ( 368 | 54D3C46123CCD7CB00FEFABE /* Debug */, 369 | 54D3C46223CCD7CB00FEFABE /* Release */, 370 | ); 371 | defaultConfigurationIsVisible = 0; 372 | defaultConfigurationName = Release; 373 | }; 374 | /* End XCConfigurationList section */ 375 | }; 376 | rootObject = 54D3C44423CCD7CA00FEFABE /* Project object */; 377 | } 378 | --------------------------------------------------------------------------------