๋ฉ์์ด์ฌ์์ฒ๋ผ iOS ์ฑ ์ค์ฟจ์์ ์ต์ข ํ๋ก์ ํธ๋ก ๋จ์ด์ฅ ์ฑ์ ๋ง๋ค๋ฉด์,
AVSpeechSynthesizer๋ฅผ ํ์ฉํ์ฌ TTS ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ค๊ณ ํ๋ค.
ํ์ง๋ง ๊ตฌํ ๊ณผ์ ์์ ์์์น ๋ชปํ? ์ค๋ฅ๋ฅผ ๋ง๋ฌ๊ณ ์๊ฐ๋ณด๋ค ๊ณจ๋จธ๋ฆฌ๋ฅผ ์ฉ์๋ค.
์ฝ๋ ์์ผ๋ก๋ ์ ํ ๋ฌธ์ ๊ฐ ์์๋๋ฐ ๋ง์ด๋ค. (์ง์ง๋ก? ๋ฌธ์ ์๋ ๊ฑฐ ๋ง์?)
๊ทธ๋ ๊ฒ ๊ณ ๋ฏผํ๋ ์ค, ๊ฒฐ๊ตญ View Memory Graph Hierarchy๋ฅผ ํตํด ์ค๋ฅ๋ฅผ ํด๊ฒฐํ ์ ์์๋ค.
์ฒ์ ์จ๋ดค๋๋ฐ ์ ๋ง ์ ์ฉํ ์น๊ตฌ๋ผ๋ ๊ฑธ ๊นจ๋ฌ์๋ค.
์๋ฌดํผ ์ค๋์ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ จ ์ด์๊ฐ ์์ ๋ ์ฌ์ฉํ๋ฉด ์ ๋ง ์ ์ฉํ ์น๊ตฌ๋ฅผ ์๊ฐํ๊ณ ์ ํ๋ค.
๊ทธ ์ ์, AVSpeechSynthesizer๋ ์ด๋ป๊ฒ ๋์ํ๋์ง์ ๋ํด ์์๋ณด๋๋ก ํ์.
AVSpeechSynthesizer์ ๋์ ๊ณผ์
์ฝ๋๋ก๋ ์ด๋ ๊ฒ ํํํ ์ ์๋ค.
์ด ์ฝ๋๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ๋์ํ ๊น?
์ฐ์ AVSpeechSynthesizer์๋ AVSpeechUtterance๋ฅผ ๋ด์ ์ ์๋ ๊ทธ๋ฆ(Queue)์ด ์กด์ฌํ๋ค.
์ด๊ณณ์ AVSpeechUtterance ์ธ์คํด์ค๊ฐ ๋ด๊ธฐ๊ณ ,
speak ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด Queue์์ Pop๋๋ฉด์ TTS๊ฐ ์๋ํ๊ฒ ๋๋ค.
์ ๋๋ฉ์ด์ ์ผ๋ก๋ ์ด๋ ๊ฒ ํํํ ์ ์๋ค.
๋ฌธ์ ์ํฉ ๐ง
๋ญ, ์ ๋ง ๋ณ ๊ฑฐ ์๋ค.
์ ์ ๋๋ฉ์ด์ ๋ง ๋ณด๋ฉด '์... ์ด๋ ๊ฒ ๋์ํ๋๊ตฌ๋...' ์ ๋๋ก ์๊ฐํ ์๋ ์๋ค.
๊ฐ๋จํ ์ ๋๋ฉ์ด์
์ฒ๋ผ ์ค์ ๋ก ์ฝ๋ ์์ผ๋ก๋ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ค. (๊ฐ๋จํ ์ค ์์๋ค...)
์ฐ์ , ์ฐ๋ฆฌ ์ฑ์์์ ๋จ์ด ๋ฃ๊ธฐ ๋ชจ๋์๋ ์ธ ๊ฐ์ง ํ์ ์ด ์กด์ฌํ๋ค.
1. ์ ํํ ๋จ์ด ๋ฃ๊ธฐ(1๊ฐ)
2. ์ ํํ ๋จ์ด ๋ฃ๊ธฐ(์ฌ๋ฌ ๊ฐ)
3. ์ ์ฒด ๋จ์ด ๋ฃ๊ธฐ
1๊ฐ์ ๋จ์ด๋ง ๋ค์ ๋(1๋ฒ)๋ TTS๊ฐ ์คํ, ์ข ๋ฃ๋ ์ดํ์ ๋ค์ TTS๋ฅผ ์คํํ์ ๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์์๋ค.
ํ์ง๋ง 2๋ฒ๊ณผ 3๋ฒ์ฒ๋ผ, TTS๊ฐ ์จ์ ํ ์ข ๋ฃํ๊ธฐ ์ ์ NavigationItems์ ์๋ "์ทจ์" ๋ฒํผ์ผ๋ก TTS๋ฅผ ๊ฐ์ ์ข ๋ฃํ๋ ์๊ฐ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
์ด๋ ๊ฒ TTS๋ฅผ ๊ฐ์ ๋ก ์ข ๋ฃํ๊ฒ ๋๋ฉด ์ดํ์ TTS๊ฐ ์คํ๋์ง ์๋ ์น๋ช ์ ์ธ ์ค๋ฅ์๋ค.
์๋์ ๊ฐ์ ์ค๋ฅ๋ฅผ ๋ด๋ฟ์ผ๋ฉฐ TTS๊ฐ ์๋ํ์ง ์์๋ค.
์, ์ด๊ฑฐ๋ค!
๊ตฌ๊ธ๋ง์ ํด๋ด๋ ๋ด๊ฐ ์ํ๋ ๋ต๋ณ์ ์ฐพ์ ์ ์์๋ค.
๊ทธ๋ฌ๋ ์ค, ๋ฉํ ๋๊ป์ ํ์ ๋ง์์ด ๋จธ๋ฆฌ๋ฅผ ์ค์ณ ์ง๋๊ฐ๋ค.
'Xcode์์ ๋ฉ๋ชจ๋ฆฌ์ ๊ด๋ จ๋ ๋๋ฒ๊ทธ ํด์ ์ ๊ณตํ๋ค...'
์, ์ด๊ฑฐ๋ค!
ํ์์ ์์ฃผ ์ฌ์ฉํ๋ View Hierarchy์ ๊ฐ์ ๊ณณ์ ์กด์ฌํ๋ค.
์๋ฌดํผ ์ด ์น๊ตฌ๋ฅผ ํตํด์ ๋ฉ๋ชจ๋ฆฌ์ ์ด๋ค ์ธ์คํด์ค๋ค์ด ์ฌ๋ผ๊ฐ์๋ ์ง์ ํ์ธํด๋ดค๊ณ ,
๋ฌธ์ ๊ฐ ๋ฐ์ํ ๋์ ๋ฐ์ํ์ง ์์ ๋๋ฅผ ๋น๊ตํด๋ดค๋ค.
์ผ์ชฝ ์ฌ์ง์ ์ ์์ ์ผ๋ก TTS๊ฐ ์ข ๋ฃ๋์ ๋์ ๋ชจ์ต์ด๊ณ ,
์ค๋ฅธ์ชฝ์ ์ทจ์ ๋ฒํผ์ ๋๋ฌ์ TTS๋ฅผ ๊ฐ์ ์ข ๋ฃ๋์ ๋์ ๋ชจ์ต์ด๋ค.
๋นจ๊ฐ ๋ฐ์ค ์น ์ ๋, AVSpeechUtterance๊ฐ ์์ํ๊ฒ ์ด์์๋ ๊ฒ์ ๋ชฉ๊ฒฉํ๋ค.
๊ทธ๋์ ์๋ฅผ ๋ฉ๋ชจ๋ฆฌ์์ ์ ๊ฑฐํ๋ฉด TTS๊ฐ ์ ์์ ์ผ๋ก ์คํ๋ ๊ฑฐ๋ผ๋ ํ์ ์ด ์๊ฒผ๋ค.
๊ทธ๋์, ์ด๋ป๊ฒ ์ ๊ฑฐํ ๊ฑด๋ฐ?
ARC๋ฅผ ํ์ฉํ๊ณ ์ ํ๋ค.
wordUtterance๋ฅผ nil๋ก ๋ง๋ค์ด ์ค์ผ๋ก์จ,
ARC์ ์ํด ๋ฉ๋ชจ๋ฆฌ์์ ์๋์ผ๋ก ์ ๊ฑฐ๋๋ ๋ฐฉํฅ์ผ๋ก ์ ๊ทผํ๋ค.
ํ์ง๋ง ์๊น ์์์ ๋์ ์๋ฆฌ์์ ๋ดค๋ ๊ฒ์ฒ๋ผ
AVSpeechSynthesizer์์ wordUtterance์ ๋ํ ์ฐธ์กฐ๋ฅผ ๊ฐ๊ณ ์์๊ณ ,
์ด๋ก ์ธํด RC๊ฐ ๋ฐ์ํ์ฌ ARC๊ฐ ํด๋น ์ธ์คํด์ค๋ฅผ ์ ๊ฑฐํ์ง ์์๋ค.
๊ทธ๋์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ ๊ทผํ๋ค.
wordUtterance๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ด ์๋๋ผ ์ด๋ฅผ ๋ด๊ณ ์๋ AVSpeechSynthesizer ์์ฒด๋ฅผ ์ ๊ฑฐํ๋ ๋ฐฉํฅ์ผ๋ก ๋ง์ด๋ค.
๊ฒฐ๊ณผ๋ ์ฑ๊ณต์ ์ด์๋ค.
์์ ๋ฐ์ํ ๋ฌธ์ ๋ฅผ ๋ง๋ํ๊ฒ ํด๊ฒฐํ ์ ์์๋ค! ๐
๋๋ฒ๊น ์ด ์๋์๋ค๋ฉด ํด๊ฒฐํ์ง ๋ชปํ ์ด์๋ก ๋จ์์ ๊ฒ์ด๋ค...
์์ผ๋ก๋ ์์ฃผ ํ์ฉํด์, ๋ฉ๋ชจ๋ฆฌ ๊ด๋ จ ์ด์๋ฅผ ๋ง๋ํ๊ฒ ํด๊ฒฐํด๋ณด์! ๐ช
์ง์ ๋ถ๋ชํ๊ณ , ์ฐพ์๋ณด๋ฉด์ ์์ฑํ ๊ธ์ด๋ผ ์๋ชป๋ ์ ๋ณด๊ฐ ์์ ์ ์์ต๋๋ค.
ํผ๋๋ฐฑ์ ์ธ์ ๋ ์ง ํ์์ ๋๋ค. ๊ฐ์ฌํฉ๋๋ค. ๐
์ ์ฒด ์ฝ๋
import AVFoundation
import Foundation
enum SpeechType {
case single
case many
}
protocol TTSProtocol {
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordAndMeaning(_ word: Word, to language: String, _ type: SpeechType)
/// ์ฌ๋ฌ๊ฐ์ ๋จ์ด๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordsAndMeanings(_ words: [Word], to language: String)
/// ๋จ์ด ์ฝ๊ธฐ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋
func stopSpeaking()
}
final class SpeechSynthesizer: NSObject, ObservableObject, TTSProtocol {
// ์ฑ๊ธํค์ผ๋ก ์ ์
private var instance: AVSpeechSynthesizer? = AVSpeechSynthesizer()
// ๋จ์ด์ ์๋ฏธ ์ฌ์ด์ ๊ฐ๊ฒฉ (๊ฐ์ ๋จ์ด ๋ด์์)
private let intervalOfWordAndMeaning = 0.3
// ์๋ฏธ์ ์๋ฏธ ์ฌ์ด์ ๊ฐ๊ฒฉ (๊ฐ์ ๋จ์ด ๋ด์์)
private let intervalOfMeaningAndMeaning = 0.1
// ๋จ์ด์ ๋จ์ด ์ฌ์ด์ ๊ฐ๊ฒฉ (๋ค๋ฅธ ๋จ์ด๋ผ๋ฆฌ)
private let intervalOfWordAndWord = 0.5
// TODO: - ์ฌ์ฉ์๊ฐ ์ค์ ํ ์ธ์ด์ ๋ฐ๋ผ ๋์ ์ผ๋ก ๋ฑ๋๋ ์ฝ๋ ์ถ๊ฐ
private let meaningUtteranceLanguage = "ko-KR"
// ์ฌ์ํ ๋จ์ด์ ๊ฐ์๋ฅผ ์ ์ฅ
private var totalWordsCount: Int = 0
private var spokenWordsCount: Int = 1
// ๋จ์ด ๋ฃ๊ธฐ์ ์ฌ์ ์ฌ๋ถ๋ฅผ ํ์ธ
@Published var isPlaying = false
override init() {
super.init()
instance?.delegate = self
}
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
func speakWordAndMeaning(_ word: Word, to language: String, _ type: SpeechType) {
if instance == nil {
instance = AVSpeechSynthesizer()
instance?.delegate = self
}
let wordUtterance = AVSpeechUtterance(string: word.word ?? "") // TTS๋ก ๋ค๋ ค์ค ๋จ์ด ์ค์
wordUtterance.voice = AVSpeechSynthesisVoice(language: language) // ์ธ์ด ์ค์
wordUtterance.postUtteranceDelay = intervalOfWordAndMeaning // ๋ค์ ๋จ์ด์์ ์๊ฐ ๊ฐ๊ฒฉ ์ค์
instance?.speak(wordUtterance) // TTS ์๋
if type == SpeechType.single { return } // contextMenu๋ก ์ ๊ทผํ ๊ฒฝ์ฐ
// meaning ํ์
์ด [String?]๋ผ์ ์ํํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ
word.meaning?.forEach { meaning in
let meaningUtterance = AVSpeechUtterance(string: meaning)
meaningUtterance.voice = AVSpeechSynthesisVoice(language: meaningUtteranceLanguage)
meaningUtterance.postUtteranceDelay = intervalOfMeaningAndMeaning
instance?.speak(meaningUtterance)
}
}
/// ์ฌ๋ฌ ๊ฐ์ ๋จ์ด๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋
/// ๋จ์ด ํ๋๋ฅผ ์ฝ์ด์ฃผ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ
func speakWordsAndMeanings(_ words: [Word], to language: String) {
totalWordsCount = calculateWordsAndMeanings(words) // ๋จ์ด ๊ฐ์ ์นด์ดํธ
spokenWordsCount = 1
isPlaying = true // ์ฌ์ ์ํ๋ก ๋ณ๊ฒฝ
words.forEach { word in
self.speakWordAndMeaning(word, to: language, .many)
}
}
/// ๋จ์ด ์ฝ๊ธฐ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋ (onDisapper)
/// TTS๊ฐ ์๋ ์ค์ธ ์ํ์์ผ๋ฉด (์ ํ ๋ฃ๊ธฐ, ์ ์ฒด ๋ฃ๊ธฐ ๋์ค ์ข
๋ฃ) AVSpeechSynthesizer๋ฅผ ์ด๊ธฐํํ์ฌ ์ค๋ฅ๋ฅผ ๋ฐฉ์ง
func stopSpeaking() {
instance?.stopSpeaking(at: .immediate)
instance = nil // ์ธ์คํด์ค ์ด๊ธฐํ
isPlaying = false // ์ ์ง ์ํ๋ก ๋ณ๊ฒฝ
}
/// ์ฌ์ํ ๋จ์ด์ ๊ฐ์๋ฅผ ๊ณ์ฐํ๋ ๋ฉ์๋
private func calculateWordsAndMeanings(_ words: [Word]) -> Int {
var cnt = 0
for word in words {
cnt += 1
for _ in word.meaning ?? [] {
cnt += 1
}
}
return cnt
}
}
extension SpeechSynthesizer: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
// ์ฌ์์ด ์๋ฃ๋ ๋จ์ด์ ๊ฐ์๊ฐ ์ด ๋จ์ด์ ๊ฐ์์ ์ผ์นํ๋ฉด ์ ์ง
if totalWordsCount == spokenWordsCount {
isPlaying = false
} else {
spokenWordsCount += 1
}
}
}
๊ด๊ณ
๊ธฐ๋ณธ ์ฑ์ค๋ฝ์ง๋ง(์๋ํ ๋์์ธ์
๋๋ค ใ
ใ
) ์จ๊ฒจ์ง ๊ธฐ๋ฅ์ด ๋ง์ต๋๋ค.
๋ง์ด ์ฌ์ฉํด์ฃผ์๊ณ , ์ ํฌ๊ฐ ๋ฐ๊ฒฌํ์ง ๋ชปํ ๋ฒ๊ทธ๊ฐ ์์ ๊ฒฝ์ฐ์ ์ธ์ ๋ ์ง ์ ๋ณดํด์ฃผ์๋ฉด ๊ฐ์ฌํ๊ฒ ์ต๋๋ค. ๐ฝ