개인 프로젝트 기록 - 1 (Ear Speaker)

2023. 4. 1. 18:53iOS/프로젝트

Ear Speaker 테스트

귀로 전화를 받아야 하는 것이 주 아이디어이기에, Ear Speaker를 사용하는 것을 가장 먼저 테스트 해 보려 한다.

 

목표 달성


처음에 검색한 결과

/// Ear speaker로 재생하기 위한 설정 ?
try audioSession.overrideOutputAudioPort(.none)

 

오디오세션을 overrideOutputAudioPort(.none) 으로 설정해준다.

overrideOutputAudioPort 는 “Temporarily changes the current audio route”, 즉 현재의 오디오 루트를 일시적으로 변경시켜주는 것인데 정확한 설명은 없지만 .none 으로 설정 시 Ear Speaker에서 오디오가 나오는… 것인 줄 알았고 성공한 줄 알았다.

 

왜냐면 실제로 전화할 때 듣는 (귀가 닿는) 스피커에서 소리가 나왔기 때문이다.

 

근데 갑자기 싸해서 계속 테스트를 해 봤더니,

(편의상 귀 닿는 부분을 전화 스피커로, 충전 포트가 있는 부분을 하단 스피커로 통칭하겠다.)

 

  • 전화 스피커와 하단 스피커 모두에서 소리가 나오고 있었다.
  • 음악 앱을 재생해 보니 역시 전화 스피커와 하단 스피커 모두에서 소리가 나온다.

 

그래서 다시 찾아봤다.

 

최종 결과물

/// 재생 + 녹음 모드
try audioSession.setCategory(.playAndRecord)
/// Ear speaker로'만' 재생하기 위한 '진짜' 설정
try audioSession.setMode(.voiceChat)

 

AVAudioSession에는 mode라는 게 있다. 공식 문서에 따르면 You can use a mode to configure the audio system for specific use cases such as video recording, voice or video chat, or audio analysis. 라고 한다.

중요한 건 어떤 용도로 쓸 것인지 모드를 지정할 수 있다는 것.

 

그래서 voiceChat 모드를 사용했더니, 원했던 동작 대로 전화 스피커에서만 소리가 나왔다.

 

그리고 카테고리를 playAndRecord 로 설정해야 한다.

그 요구사항은 아래 링크에 자세히 나온다.

https://developer.apple.com/documentation/avfaudio/avaudiosession/mode/1616455-voicechat

 

voiceChat | Apple Developer Documentation

A mode that indicates that your app is performing two-way voice communication, such as using Voice over Internet Protocol (VoIP).

developer.apple.com

사실 정확한 설명은 VoIP 프로토콜을 사용하는 경우를 암시하는 모드라고 나와 있는데, 나는 오직 전화 스피커를 사용하기 위해 썼기 때문에 다른 사이드 이펙트가 없는지 나중에 좀 더 알아봐야겠다. 

overrideOutputAudioPort 는 왜 여러 검색 결과에서 나왔는지 모르겠다. 내가 ear speaker 라는 명칭을 잘못 이해했을 수도 있을 것 같다. 이 개념은 왠지 블루투스 이어폰 다룰 나중에 다시 나올 것 같아서 조금 미뤄두려고 한다.

 

목표 달성을 위해 필요한 것들


AudioManager

오디오 관련한 기능들을 관리할 수 있는 클래스. 어디서든 접근 가능하도록 싱글톤으로 구현했다.

 

  • 전역변수로 shared 라는 변수를 만들고 자기 자신의 인스턴스를 할당한다
  • 전역변수는 메모리의 데이터 공간에 저장된다
  • 싱글톤은 한 번만 초기화되어야 하기 때문에 init을 private으로 설정한다

 

final class AudioManager {
    /// Singleton: 전역변수로 할당(메모리의 데이터 공간에 저장)
    static let shared = AudioManager()

    /// MARK: Singleton: 다른 곳에서 초기화됨을 막기 위해 private으로 선언
    private init() { }

    // ...
}

AVAudioSession

앱에서 오디오를 어떻게 설정할 지 시스템에 알려주는 것 == 오디오 설정과 같은 느낌이다.

 

  • AVAudioSession 의 shared 인스턴스를 가져와 변수에 할당하여 사용한다
  • setCategory 를 통해 재생 모드를 설정한다 (우선은 재생만)
  • setMode 를 통해 오디오 모드를 설정한다
  • 앱의 AudioSession 을 활성화시킨다.
  • AudioSession은 한 번에 하나만 존재해야 하기에 지금은 이 앱에서 AudioSession 을 사용하겠다는 설정 (다른 우선순위가 높은 작업 - 예를 들어 전화 받기 - 이 있을 경우 활성화는 실패한다)

 

final class AudioManager {
    // ...

    let audioSession = AVAudioSession.sharedInstance()

    private init() {
        do {
            /// 재생 + 녹음 모드
            try audioSession.setCategory(.playAndRecord)

            /// Ear speaker로'만' 재생하기 위한 '진짜' 설정
            try audioSession.setMode(.voiceChat)

            /// 앱 자체의 AudioSession을 활성화 (다른 우선순위가 높은 AudioSession이 있으면 - 예를 들어 전화 - 활성화 실패 -> AudioSession은 하나씩만 가능)
            try audioSession.setActive(true)
        } catch {
        	print("Failed to set audio session.")
        }
    }

    // ...
}

 

AVAudioPlayer

AVAudioPlayer란 파일이나 버퍼에서 오디오를 불러와 재생하는 객체이다.

 

  • AVAudioPlayer 를 담을 변수를 선언한다
  • 재생될 오디오 파일의 url 을 가지고 AVAudioPlayer 를 할당한다
  • 파일 이름과 확장자를 가지고 url을 가져온다
  • 지금은 Xcode 프로젝트 내에 있는 테스트 파일(번들 안에 포함된 파일)을 가져온다
  • play() 함수로 player 를 실행한다

 

final class AudioManager {
    // ...

    var player: AVAudioPlayer!

    /// URL을 편하게 가져오기 위한 함수
    private func getResourceURL(forResource: String, musicExtension: String) -> URL? {
	    /// Bundle: 프로젝트 내의 리소스를 나타내는 디렉토리
    	/// main 앱 번들에서 해당 경로에 파일이 존재한다면 URL을 리턴한다
	    return Bundle.main.url(forResource: forResource, withExtension: musicExtension) ?? nil
    }

    // 오디오 파일을 가지고 player를 설정한다
    func setAudio(forResource res: String, musicExtension ext: String) {
        // 테스트 파일 사용
        guard let url = getResourceURL(forResource: res, musicExtension: ext) else {
            return
        }

        do {
            // player 할당
            player = try AVAudioPlayer(contentsOf: url)
        } catch {
            print(error)
        }
    }

    func playAudio() {
	    player.play()
    }
}

 

 

남아있는 질문들

  • 싱글톤의 인스턴스는 언제 생성될까?
  • 이어폰이나 외부 악세서리를 사용하고 있는 경우에도 Ear speaker에서 소리가 나올까?
  • AudioSession을 활성화 한 뒤 deactive 시켜주어야 할까?
  • AudioSession 설정(재생모드, 오디오 포트, 활성화)에 실패한 경우, 어떻게 처리해 주어야 할까?
  • AVAudioPlayer는 다른 오디오 파일 마다 매번 새로 할당해 주어야 하나?
  • AVAudioPlayer 할당에 실패할 경우 어떤 에러가 나오고, 어떻게 처리해 주어야 할까?
  • 앱 번들과 main 번들에 관하여 자세히 알아보기
  • voiceChat 모드의 side effect가 있을까?
  • AVFoundation과 AudioKit의 차이는?

 

이전 이야기


https://mila00a.tistory.com/54

 

개인 프로젝트 기록 - 0 (시작)

개인 프로젝트를 시작하다 취업 준비로 CS나 알고리즘만 하다가는 Swift, SwiftUI, Xcode 죄다 까먹을 것 같고 새로운 기술을 경험할 기회도 없을 것 같아 개인 프로젝트를 시작하려고 합니다. Hear My Vo

mila00a.tistory.com

 

다음 이야기


https://mila00a.tistory.com/56

 

개인 프로젝트 기록 - 3 (Recording)

녹음 테스트 재생을 테스트 했으니, 음성 메시지를 녹음할 수 있도록 그 기능을 구현해 보는 것이 목표 목표 달성 AudioSession 카테고리 설정 (앞선 설정과 동일) /// 재생 + 녹음 (playAndRecord) 모드 try

mila00a.tistory.com