1.0 으로 업그레이드

NUGU SDK iOS 를 0.x 에서 1.0 으로 업그레이드 하기위한 가이드입니다.

자세한 source code 변경사항은 github release note 를 참고해주세요.(https://github.com/nugu-developers/nugu-ios/releases/tag/1.0.)

NotificationCenter 사용

NUGU SDK 의 구성요소에 대한 변경사항을 전달하기 위해 DelegateSet 대신 NotificationCenter 를 사용합니다.

DelegateSet 은 삭제되었으며, 기존에 DelegateSet 이 하던 역할을 delegate pattern 과 observer pattern 으로 구분하였습니다.

ASRAgent 변경사항 감지

  • 이전코드
MainViewController.swift
복사성공!
1
NuguCentralManager.shared.client.asrAgent.add(delegate: self)
  • 1.0 에서 변경된 코드
MainViewController.swift
복사성공!
1
2
3
4
5
asrResultObserver = object.observe(NuguAgentNotification.ASR.Result.self, queue: .main) { (notification) in
    switch notification.result {
        ...
    }
}

SystemAgent 변경사항 감지

  • 이전코드
NuguCentralManager.swift
복사성공!
1
client.systemAgent.add(systemAgentDelegate: self)
  • 1.0 에서 변경된 코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
systemAgentExceptionObserver = object.observe(NuguAgentNotification.System.Exception.self, queue: nil) { (notification) in
    guard let self = self else { return }
    
    switch notification.code {
        ...
    }
}

systemAgentRevokeObserver = object.observe(NuguAgentNotification.System.RevokeDevice.self, queue: nil) { (notification) in
    ...
}

DialogStateAggregator 변경사항 감지

  • 이전코드
MainViewController.swift
복사성공!
1
NuguCentralManager.shared.client.dialogStateAggregator.add(delegate: self)
  • 1.0 에서 변경된 코드
MainViewController.swift
복사성공!
1
2
3
4
5
dialogStateObserver = object.observe(NuguClientNotification.DialogState.State.self, queue: nil) { (notification) in
    switch notification.state {
        ...
    }
}

NuguClient.Builder 추가

NuguClient 의 필수/선택 요소를 보다 효율적으로 생성하기 위해 builder pattern 이 적용되었습니다.

  • 이전코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
lazy private(set) var client: NuguClient = {
    let client = NuguClient(delegate: self)
    
    ...
    
    return client
}()
  • 1.0 에서 변경된 코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lazy private(set) var client: NuguClient = {
    let nuguBuilder = NuguClient.Builder()
    
    ...
    
    // Set DataSource for SoundAgent
    nuguBuilder.setDataSource(self)
    // Set Delegate for PhoneCallAgent, MessageAgent, MediaPlayerAgent,
    // ExtensionAgent, LocationAgent, PermissionAgent
    nuguBuilder.setDelegate(self)
    
    let client = nuguBuilder.build()
    client.delegate = self
    
    return client
}()

SpeechRecognizerAggregator 추가

NUGU SDK 의 SpeechRecognizerAggregatorMicInputProvider, ASRAgent, KeywordDetector 를 통합 관리합니다.

  • 이전코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
private var startMicWorkItem: DispatchWorkItem?

// Audio input source
private let micQueue = DispatchQueue(label: "central_manager_mic_input_queue")
private let micInputProvider = MicInputProvider()
 
func enable() {
    ...

     // Set Last WakeUp Keyword
     // If you don't want to use saved wakeup-word, don't need to be implemented
     if UserDefaults.Standard.useWakeUpDetector,
         let keyword = Keyword(rawValue: UserDefaults.Standard.wakeUpWord) {
         client.keywordDetector.keywordSource = keyword.keywordSource
         startWakeUpDetector()

         startMicWorkItem?.cancel()
         startMicWorkItem = DispatchWorkItem(block: { [weak self] in
             log.debug("startMicWorkItem start")
             self?.startMicInputProvider(requestingFocus: false) { (success) in
                 guard success else {
                     log.debug("startMicWorkItem failed!")
                     return
                 }
             }
         })
         guard let startMicWorkItem = startMicWorkItem else { return }
         // When mic has been activated before interruption end notification has been fired,
         // Option's .shouldResume factor never comes in. (even when it has to be)
         // Giving small delay for starting mic can be a solution for this situation
         DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: startMicWorkItem)
     } else {
         stopWakeUpDetector()
         stopMicInputProvider()
     }
 }

func startMicInputProvider(requestingFocus: Bool, completion: @escaping (Bool) -> Void) {
     startMicWorkItem?.cancel()
     DispatchQueue.main.async {
         guard UIApplication.shared.applicationState == .active else {
             completion(false)
             return
         }

         NuguAudioSessionManager.shared.requestRecordPermission { [unowned self] isGranted in
             guard isGranted else {
                 log.error("Record permission denied")
                 completion(false)
                 return
             }
             self.micQueue.async { [unowned self] in
                 defer {
                     log.debug("addEngineConfigurationChangeNotification")
                     NuguAudioSessionManager.shared.registerAudioEngineConfigurationObserver()
                 }
                 self.micInputProvider.stop()

                 // Control center does not work properly when mixWithOthers option has been included.
                 // To avoid adding mixWithOthers option when audio player is in paused state,
                 // update audioSession should be done only when requesting focus
                 if requestingFocus {
                     NuguAudioSessionManager.shared.updateAudioSession(requestingFocus: requestingFocus)
                 }
                 do {
                     try self.micInputProvider.start()
                     completion(true)
                 } catch {
                     log.error(error)
                     completion(false)
                 }
             }
         }
     }
 }

 func stopMicInputProvider() {
     micQueue.sync {
         startMicWorkItem?.cancel()
         micInputProvider.stop()
         NuguAudioSessionManager.shared.removeAudioEngineConfigurationObserver()
     }
 }
 
 func startWakeUpDetector() {
     client.keywordDetector.start()
 }
 
 func stopWakeUpDetector() {
     client.keywordDetector.stop()
 }
 
 func startRecognition(initiator: ASRInitiator) {
     client.asrAgent.startRecognition(initiator: initiator)
 }

 func stopRecognition() {
     client.asrAgent.stopRecognition()
 }
  • 1.0 에서 변경된 코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
func startListening(initiator: ASRInitiator) {
    client.speechRecognizerAggregator.startListening(initiator: initiator)
}

func startListeningWithTrigger() {
    client.speechRecognizerAggregator.startListeningWithTrigger()
}

func stopListening() {
    client.speechRecognizerAggregator.stopListening()
}

KeywordDetector, MicInputProvider , ASRAgent 시작/종료와 관련된 코드를 모두 제거하고SpeechRecognizerAggregatorstartListening(initiator:), startListeningWithTrigger, stopListening 함수 호출로 대체합니다.

Keyword 설정

  • 이전코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
 // Set Last WakeUp Keyword
 // If you don't want to use saved wakeup-word, don't need to be implemented
 if UserDefaults.Standard.useWakeUpDetector,
     let keyword = Keyword(rawValue: UserDefaults.Standard.wakeUpWord) {
     client.keywordDetector.keywordSource = keyword.keywordSource
 }
  • 1.0 에서 변경된 코드
NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
// Set Last WakeUp Keyword
// If you don't want to use saved wakeup-word, don't need to be implemented.
// Because `aria` is set as a default keyword
if let keyword = Keyword(rawValue: UserDefaults.Standard.wakeUpWord) {
    nuguBuilder.keywordDetector.keyword = keyword
}

// If you want to use built-in keyword detector, set this value as true
nuguBuilder.speechRecognizerAggregator.useKeywordDetector = UserDefaults.Standard.useWakeUpDetector

Keyword(e.g. “아리아”) 관련 설정을 KeywordDetector 대신 SpeechRecognizerAggregator 에 적용합니다.

SDK 에서 AVAudioSessionManager 를 관리

NUGU SDK 를 사용하기 위해서는 시스템, Application, NUGU SDK 의 복합적인 상황에 맞춰 AVAudioSession 의 category, categoryOptions 와 AVAudioSession.interruptionNotification , AVAudioSession.routeChangeNotification 등을 적절하게 처리해 주어야 합니다.

SDK 1.0 에 추가된 AudioSessionManager 는 NUGU SDK 를 사용하기 위한 기본적인 AVAudioSession 처리를 구현하고 있습니다.

AVAudioSession 이 이미 Application 에서 충분히 복잡하게 관리되고 있다면, AudioSessionManager 를 사용하는 것보다 기존의 관리 로직에 NUGU SDK 를 위한 로직을 추가하는 방향이 더욱 효율적일 수 있습니다.

Custom AudioSessionManager 를 참고해주세요.

SDK 의 AudioSessionManager 사용

Application 의 NuguAudioSessionManager.swift 파일 및 관련 코드를 삭제합니다.

NuguCentralManager.swift
복사성공!
1
client.audioSessionManager?.enable()

AudioSessionManagerAVAudioSession.interruptionNotification , AVAudioSession.routeChangeNotification event 를 감지합니다.

NUGU SDK 를 사용하는 동안 event 를 감지하여 SDK 동작에 필요한 기능을 수행합니다.

NuguCentralManager.swift
복사성공!
1
client.audioSessionManager?.disable()

AudioSessionManagerAVAudioSession.interruptionNotification , AVAudioSession.routeChangeNotification event 감지를 중지합니다.

NUGU SDK 를 사용하지 않을 때 호출하여 SDK 가 불필요한 동작을 수행하지 않도록합니다.

Custom AudioSessionManager 사용

NUGU SDK 의 AudioSessionManager 를 사용하지 않고 Application 에서 AVAudioSession 을 직접 처리할 수 있습니다.

NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lazy private(set) var client: NuguClient = {
    let nuguBuilder = NuguClient.Builder()
    
    ...
    
    nuguBuilder.audioSessionManager = nil
    
    ...
    
    let client = nuguBuilder.build()
    client.delegate = self
    
    ...
    
    return client
}()

NuguClient 생성시 audioSessionManager 를 nil 처리합니다.

NuguCentralManager.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension NuguCentralManager: NuguClientDelegate {
    func nuguClientWillUseMic() {
        if requestingFocus {
            NuguAudioSessionManager.shared.updateAudioSession(requestingFocus: true)
        }
    }
    
    func nuguClientWillRequireAudioSession() -> Bool {
        return NuguAudioSessionManager.shared.updateAudioSession(requestingFocus: true)
    }
    
    func nuguClientDidReleaseAudioSession() {
        NuguAudioSessionManager.shared.notifyAudioSessionDeactivation()
    }
    
    ...
}

NUGU SDK iOS 0.x 의 NuguAudioSessionManager.swift 를 참고하여 Application 에서 AudioSessionManager 를 직접 구현하고 NuguClientDelegate 의 일부 함수를 구현해 주어야 합니다.

VoiceChromePresenter 기능 추가

UITapGestureRecognizer 구현 이동

  • 삭제해야 하는 코드
MainViewController.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@objc private extension MainViewController {
    func didTapForStopRecognition() {
        guard [.listening, .recognizing].contains(NuguCentralManager.shared.client.asrAgent.asrState) else { return }
        NuguCentralManager.shared.client.asrAgent.stopRecognition()
    }
}
func initializeNugu() {
    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapForStopRecognition))
    tapGestureRecognizer.delegate = self
    tapGestureRecognizer.cancelsTouchesInView = false
    view.addGestureRecognizer(tapGestureRecognizer)
}
extension MainViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        let touchLocation = touch.location(in: gestureRecognizer.view)
        return !nuguVoiceChrome.frame.contains(touchLocation)
    }
    
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

UITapGestureRecognizer 관련 기능을 VoiceChromePresenter 에서 처리하기 때문에 Application 에서 관련 코드를 삭제합니다.

ASRBeepPlayer 구현 이동

  • 삭제해야 하는 코드
NuguCentralManager.swift
복사성공!
1
lazy private(set) var asrBeepPlayer: ASRBeepPlayer = ASRBeepPlayer(focusManager: client.focusManager)
MainViewController.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
extension MainViewController: DialogStateDelegate {
    func dialogStateDidChange(_ state: DialogState, isMultiturn: Bool, chips: [ChipsAgentItem.Chip]?, sessionActivated: Bool) {
        switch state {
        case .listening:
            DispatchQueue.main.async {
                NuguCentralManager.shared.asrBeepPlayer.beep(type: .start)
            }
        case .thinking:
            DispatchQueue.main.async { [weak self] in
                self?.nuguButton.pauseDeactivateAnimation()
            }
        default:
            break
        }
    }
}

extension MainViewController: ASRAgentDelegate {
    func asrAgentDidReceive(result: ASRResult, dialogRequestId: String) {
        switch result {
        case .complete:
            DispatchQueue.main.async {
                NuguCentralManager.shared.asrBeepPlayer.beep(type: .success)
            }
        case .error(let error, _):
            DispatchQueue.main.async {
                switch error {
                case ASRError.listenFailed:
                    NuguCentralManager.shared.asrBeepPlayer.beep(type: .fail)
                case ASRError.recognizeFailed:
                    NuguCentralManager.shared.localTTSAgent.playLocalTTS(type: .deviceGatewayRequestUnacceptable)
                default:
                    NuguCentralManager.shared.asrBeepPlayer.beep(type: .fail)
                }
            }
        default: break
        }
    }
}

Beep 음 재생과 관련된 기능을 VoiceChromePresenter 에서 처리하기 때문에 Application 의 ASRBeepPlayer.swift 파일 및 asrBeepPlayer.beep(type:) 호출 코드를 삭제합니다.

ControlCenterManager 추가

AudioPlayerAgent 를 사용하여 음악을 재생하는 경우 SDK 의 ControlCenterManager 에서 MPNowPlayingInfoCenter 를 통해 재생정보를 iOS platform 으로 전달합니다.

  • 삭제해야 하는 코드
NuguCentralManager.swift
복사성공!
1
let displayPlayerController = NuguDisplayPlayerController()
MainViewController.swift
복사성공!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension MainViewController: AudioDisplayViewPresenterDelegate {
     func displayControllerShouldUpdateTemplate(template: AudioPlayerDisplayTemplate) {
         NuguCentralManager.shared.displayPlayerController.update(template)
     }

     func displayControllerShouldUpdateState(state: AudioPlayerState) {
         NuguCentralManager.shared.displayPlayerController.update(state)
     }

     func displayControllerShouldUpdateDuration(duration: Int) {
         NuguCentralManager.shared.displayPlayerController.update(duration)
     }

     func displayControllerShouldRemove() {
         NuguCentralManager.shared.displayPlayerController.remove()
     }
    ...
}

MPNowPlayingInfoCenter 관련 기능을 AudioDisplayViewPresenterControlCenterManager 에서 처리하기 때문에 Application 의 NuguDisplayPlayerController.swift 및 관련 코드를 삭제합니다.