OSネイティブによる音声合成
iOS / iPadOS / macOS は、数ステップのコードにて、日本語を簡単にしゃべらせることができます。
その方法を説明します。
Speech Synthesis
OS標準のAVFoundationライブラリで提供されているSpeech Synthesis機能を使います。
API
たった3つのAPIを呼び出すだけです。
使用例1
最も簡単なAPI使用例は以下の通り。
import AVFoundation
let speechText = "この言葉をしゃべります"
let utterance = AVSpeechUtterance(string: speechText)
utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP") //日本語で
//utterance.volume = 1.0
//utterance.rate = 0.5
//utterance.pitchMultiplier = 2.0
let speechSynthesizer = AVSpeechSynthesizer()
speechSynthesizer.speak(utterance) //しゃべります
//RunLoop.current.run(until: Date(timeIntervalSinceNow: 5.0)) //for swift repl
SpeechUtteranceのパタメタは以下の通り。必要により、しゃべらせる前に設定します。
パタメタ | 内容 | 範囲 |
---|---|---|
volume | 音量 | 無音:0.0 〜 最大:1.0 デフォルト:1.0 |
rate | 速度 | AVSpeechUtteranceMinimumSpeechRate 〜 AVSpeechUtteranceMaximumSpeechRate デフォルト:AVSpeechUtteranceDefaultSpeechRate |
pitchMultiplier | ピッチ | 低い:0.4 〜 高い:2.0 デフォルト:1.0 |
自分の環境だと次の値でした。
AVSpeechUtteranceMinimumSpeechRate:0.0
AVSpeechUtteranceMaximumSpeechRate:1.0
AVSpeechUtteranceDefaultSpeechRate:0.5
speechSynthesizer.speak()は非同期にしゃべります。
このため、swift repl で実行する場合は、RunLoop.current.run()
が必要。
また、しゃべっている時に続けてspeak()すると(要求はキューイングされ)、前の言葉がしゃべり終わると、続けてしゃべります。
しゃべり終わりやその他のタイミングをつかみたい場合は、AVSpeechSynthesizerDelegate を実装します。
//おしゃべりを開始するタイミング
func speechSynthesizer(AVSpeechSynthesizer, didStart: AVSpeechUtterance)
//おしゃべりテキストの一部をしゃべろうとしているとき
func speechSynthesizer(AVSpeechSynthesizer, willSpeakRangeOfSpeechString: NSRange, utterance: AVSpeechUtterance)
//おしゃべりのマーカーをしゃべろうとしているとき
func speechSynthesizer(AVSpeechSynthesizer, willSpeak: AVSpeechSynthesisMarker, utterance: AVSpeechUtterance)
//おしゃべり中に一時停止するタイミング
func speechSynthesizer(AVSpeechSynthesizer, didPause: AVSpeechUtterance)
//一時停止後におしゃべりを再開するタイミング
func speechSynthesizer(AVSpeechSynthesizer, didContinue: AVSpeechUtterance)
//おしゃべりを終了したとき
func speechSynthesizer(AVSpeechSynthesizer, didFinish: AVSpeechUtterance)
//おしゃべりをキャンセルするタイミング
func speechSynthesizer(AVSpeechSynthesizer, didCancel: AVSpeechUtterance)
使用例2
AVSpeechSynthesizerDelegate の実装例は以下の通り。
import AVFoundation
let speechText = "エーヴイスピーチシンセサイザーは、テキストの発話から合成音声を生成し、進行中の音声の監視または制御を可能にするオブジェクトです"
let utterance = AVSpeechUtterance(string: speechText)
utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
let delegate = SpeechSynthesizerDelegateImplement()
let speechSynthesizer = AVSpeechSynthesizer()
speechSynthesizer.delegate = delegate
speechSynthesizer.speak(utterance)
// AVSpeechSynthesizerDelegate
class SpeechSynthesizerDelegateImplement: NSObject, AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
print("didStart")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
let start = speechText.index(speechText.startIndex, offsetBy: characterRange.location)
let end = speechText.index(start, offsetBy: characterRange.length)
let textPart = speechText[start ..< end]
print("willSpeakRangeOfSpeechString", characterRange, textPart)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeak marker: AVSpeechSynthesisMarker, utterance: AVSpeechUtterance) {
print("willSpeak")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
print("didPause")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
print("didContinue")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
print("didFinish")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
print("didCancel")
}
}
おしゃべりの途中で停止、一時停止 / 再開させる場合は、AVSpeechSynthesizerの次の関数を使います。
//指定した区切りでしゃべりを停止する
func stopSpeaking(at: AVSpeechBoundary) -> Bool
//指定した区切りでしゃべりを一時停止する
func pauseSpeaking(at: AVSpeechBoundary) -> Bool
//一時停止した時点からしゃべりを再開する
func continueSpeaking() -> Bool
//それぞれ、成功するとtrue
停止や一時停止する時の区切り (AVSpeechBoundary) は、次の2通りが指定できます。
- immediate:しゃべりをただちに一時停止または停止する
- word:現在の単語の発話を終了した後に一時停止または停止する
また、おしゃべり中と一時停止中を判定するには、AVSpeechSynthesizerの次のプロパティを参照します。
var isSpeaking: Bool //おしゃべり中はtrue
var isPaused: Bool //一時停止中はtrue
使用例3
これまでのすべてのAPIを試すことができるアプリのコードを以下に示します。
Xcodeのプレビューでも動作確認できます。
import AVFoundation
import SwiftUI
struct ContentView: View {
let speechSynthesizer = SpeechSynthesizer()
@State var text = ""
@State var isPaused = false
@State var speechString = ""
@State var speechText: AttributedString = ""
@State var volume: Float = 1.0
@State var rate: Float = AVSpeechUtteranceDefaultSpeechRate
@State var pitchMultiplier: Float = 1.0
@State var speechBoundary = AVSpeechBoundary.immediate
@FocusState var focus: Bool
var body: some View {
List {
Stepper(value: $volume, in: 0.0 ... 1.0, step: 0.1) {
HStack {
Text("Volume:")
Spacer()
Text("\(volume, specifier: "%.1f")")
}
}
Stepper(value: $rate, in: AVSpeechUtteranceMinimumSpeechRate ... AVSpeechUtteranceMaximumSpeechRate, step: 0.1) {
HStack {
Text("Rate:")
Spacer()
Text("\(rate, specifier: "%.1f")")
}
}
Stepper(value: $pitchMultiplier, in: 0.4 ... 2.0, step: 0.1) {
HStack {
Text("PitchMultiplier:")
Spacer()
Text("\(pitchMultiplier, specifier: "%.1f")")
}
}
HStack {
Text("")
Spacer()
Text("今日の日付 を しゃべる")
.foregroundColor(Color.accentColor)
.onTapGesture {
let now = Date.now
let calendar = Calendar(identifier: .japanese)
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ja_JP")
dateFormatter.calendar = calendar
dateFormatter.dateFormat = "Gy EEEE"
let wareki = dateFormatter.string(from: now).split(separator: " ")
let dateComponents = calendar.dateComponents([.year, .month, .day, .weekday], from: now)
guard let month = dateComponents.month, let day = dateComponents.day else { return }
speechString = "\(wareki[0])年\(correctionMonth(month))\(correctionDay(day))\(wareki[1])"
speechSynthesizer.speak(string: speechString, volume: volume, rate: rate, pitchMultiplier: pitchMultiplier)
}
}
HStack {
Text("")
Spacer()
Text("現在時刻 を しゃべる")
.foregroundColor(Color.accentColor)
.onTapGesture {
let calendar = Calendar.current
let dateComponents = calendar.dateComponents([.hour, .minute, .second], from: Date.now)
guard let hour = dateComponents.hour, let minute = dateComponents.minute, let second = dateComponents.second else { return }
speechString = minute == 0 && second == 0 ? "\(hour)時ちょうど" : "\(hour)時\(minute)分\(second)秒"
speechSynthesizer.speak(string: speechString, volume: volume, rate: rate, pitchMultiplier: pitchMultiplier)
}
}
HStack {
Text("")
TextField("自由テキスト", text: $text)
.textFieldStyle(.roundedBorder)
.focused($focus)
Spacer()
Text("を しゃべる")
.foregroundColor(Color.accentColor)
.onTapGesture {
focus = false
speechString = text
if text.isEmpty { speechString = "空欄です。テキストを入力してください" }
speechSynthesizer.speak(string: speechString, volume: volume, rate: rate, pitchMultiplier: pitchMultiplier)
}
}
Picker("SpeechBoundary", selection: $speechBoundary) {
Text("immediate").tag(AVSpeechBoundary.immediate)
Text("word").tag(AVSpeechBoundary.word)
}
.pickerStyle(.menu)
HStack {
Text("isSpeaking:")
Text("\(speechSynthesizer.speechSynthe.isSpeaking)")
.foregroundColor(speechSynthesizer.speechSynthe.isSpeaking ? .red : .textColor)
Spacer()
Text("Stop")
.foregroundColor(speechSynthesizer.speechSynthe.isSpeaking ? .accentColor : .gray)
.disabled(!speechSynthesizer.speechSynthe.isSpeaking)
.onTapGesture {
if speechSynthesizer.speechSynthe.isSpeaking {
speechSynthesizer.speechSynthe.stopSpeaking(at: speechBoundary)
isPaused = false
}
}
}
HStack {
Text("isPause:")
Text("\(isPaused)")
.foregroundColor(isPaused ? .red : .textColor)
Spacer()
Text(isPaused ? "Continue" : "Pause")
.foregroundColor(Color.accentColor)
.onTapGesture {
isPaused = speechSynthesizer.speechSynthe.isPaused
if isPaused {
speechSynthesizer.speechSynthe.continueSpeaking()
} else {
speechSynthesizer.speechSynthe.pauseSpeaking(at: speechBoundary)
}
isPaused.toggle()
}
}
Text(speechText)
}
.listStyle(.plain)
.padding()
.onAppear {
speechSynthesizer.set(text: $speechText)
}
}
private func correctionMonth(_ month: Int) -> String {
let tsuki = "0 1 2 3 し 5 6 7 8 9 10 11 12".split(separator: " ")
return "\(tsuki[month])がつ"
}
private func correctionDay(_ day: Int) -> String {
let hiduke = ",一日,二日,三日,四日,五日,六日,七日,八日,九日,とうか"
.split(separator: ",", omittingEmptySubsequences: false)
return switch day {
case 1 ..< 11: "\(hiduke[day])"
case 20: "二十日"
case 24: "二十よっか"
default: "\(day)にち"
}
}
}
class SpeechSynthesizer: NSObject {
let speechSynthe = AVSpeechSynthesizer()
var speakText: Binding<AttributedString>? = nil
override init() {
super.init()
speechSynthe.delegate = self
}
func set(text: Binding<AttributedString>) {
speakText = text
}
func speak(string: String, volume: Float, rate: Float, pitchMultiplier: Float) {
let utterance = AVSpeechUtterance(string: string)
utterance.volume = volume
utterance.rate = rate
utterance.pitchMultiplier = pitchMultiplier
utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
speechSynthe.speak(utterance)
}
}
extension SpeechSynthesizer: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) {
print("didStart")
guard speakText != nil else { return }
speakText!.wrappedValue = AttributedString(utterance.speechString)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) {
let speechText = utterance.speechString
let textPart = speechText[Range(characterRange, in: speechText)!]
print("willSpeakRangeOfSpeechString", characterRange, textPart)
var attributeString = AttributedString(utterance.speechString)
guard speakText != nil else { return }
speakText!.wrappedValue = attributeString
if let range = Range(characterRange, in: attributeString) {
attributeString[range].foregroundColor = .red
speakText!.wrappedValue = attributeString
print(range, attributeString[range])
}
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeak marker: AVSpeechSynthesisMarker, utterance: AVSpeechUtterance) {
print("willSpeak", marker)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) {
print("didPause")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) {
print("didContinue")
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
print("didFinish")
guard speakText != nil else { return }
speakText!.wrappedValue = ""
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) {
print("didCancel")
guard speakText != nil else { return }
speakText!.wrappedValue = ""
}
}
extension Color {
#if os(macOS)
static let textColor = Color(nsColor: .textColor)
#else
static let textColor = Color(.label)
#endif
}
#Preview {
ContentView()
#if os(macOS)
.frame(width: 300)
#endif
}


しゃべらせてみ
次の文章をしゃべらせてみました。
- アプリおよびWebサイトの中には、Macの画面やオーディオにアクセスしたりそれを収録したりできるものがあります。画面収録やオーディオ録音を許可するアプリおよびWebサイトを指定できます。
少しぎこちないですね。
しかし、同じ文章でも 『翻訳後のスピーチ』(↓)では、もう少し上手にしゃべります。音量も大きい。サーバで処理しているからでしょうか、しゃべり出すまでに少し待たされます。(下記動画は待ち無しに加工済み)
この違いは、Mac も iPhone もまったく同じでした。
以上です