WWDC Quick Look 💓 By SwiftGGTeam
Extend Speech Synthesis with personal and custom voices

Extend Speech Synthesis with personal and custom voices

元の動画を見る

ハイライト

iOS 17 と macOS Sonoma の Speech Synthesis フレームワークに 3 つの能力が追加されました。SSML 対応でより表現豊かな合成、Speech Synthesis Provider でカスタム音声エンジンを Audio Unit Extension としてシステムに接続、Personal Voice でユーザー自身の声から合成音声を生成して補助コミュニケーションに使えます。

主要内容

多くの人がデバイスとのやり取りに音声合成に依存しています。合成音声の選択は非常に個人的なものです。Apple システムには豊富な音声がありますが、サードパーティ開発者や特別なニーズを持つユーザーにはさらに多くが必要です。

SSML(Speech Synthesis Markup Language)は W3C 標準で、XML で音声属性を記述します。以前 AVSpeechSynthesizer はプレーンテキストのみ受け付け、休止、話速、音高などの細部を制御できませんでした。今は SSML を直接渡し、より自然で表現豊かな合成が可能です。

Speech Synthesis Provider は今年最大の新機能です。Audio Unit Extension アーキテクチャに基づき、開発者は自社の音声合成エンジンを Extension としてパッケージし、システム音声リストに接続できます。ユーザーはシステム設定であなたの音声を選べ、VoiceOver、Siri、サードパーティ App も利用できます。

Personal Voice はアクセシビリティ機能です。ユーザーが自分の声を録音すると、デバイス上でローカルにパーソナライズされた合成音声を生成します。ALS など発声能力に影響する疾患のユーザーにとって、自分の声で外界とコミュニケーションできることは大きな意味を持ちます。

詳細

SSML 音声マークアップ

02:10)SSML は XML タグで音声属性を制御します。<break> で休止、<prosody> で話速と音高を制御します。

<speak>
    Hello
    <break time="1s"/>
    <prosody rate="200%">nice to meet you!</prosody>
</speak>

02:29)SSML を AVSpeechUtterance に渡します。

let ssml = """
    <speak>
        Hello
        <break time="1s" />
        <prosody rate="200%">nice to meet you!</prosody>
    </speak>
"""

guard let ssmlUtterance = AVSpeechUtterance(ssmlRepresentation: ssml) else {
    return
}

self.synthesizer.speak(ssmlUtterance)

キーポイント:

  • AVSpeechUtterance(ssmlRepresentation:) は SSML 文字列を受け取る新しいイニシャライザ
  • 戻り値は Optional。SSML 形式エラー時は nil
  • SSML は WebSpeech と Apple 純正合成器の標準入力形式
  • サポートタグに breakprosody など。詳細は W3C 仕様を参照

Speech Synthesis Provider アーキテクチャ

03:21)Provider は Audio Unit Extension ベースで、独立した Extension プロセスで動作します。システムがオーディオセッション管理と再生を担当し、Extension は SSML を音声にレンダリングするだけです。

プロジェクト作成時は Audio Unit Extension を選び、タイプは「Speech Synthesizer」、4 文字の subtype と manufacturer 識別子を指定します。

ホスト App が利用可能音声リストと購入フローを表示します。

struct ContentView: View {
    var body: some View {
        List {
            Section("My Awesome Voices") {
                ForEach(availableVoices) { voice in
                    HStack {
                        Text(voice.name)
                        Spacer()
                        Button("Buy") {
                            // Buy this voice...
                        }
                    }
                }
            }
        }
    }

    var availableVoices: [WWDCVoice] {
        return [
            WWDCVoice(name: "Screen Reader Voice", id: "com.example.screen-reader-voice"),
            WWDCVoice(name: "Reading Voice", id: "com.example.reading-voice")
        ]
    }
}

キーポイント:

  • WWDCVoice は例のカスタム構造体で、音声名と識別子を保持
  • ホスト App と Extension は App Group の UserDefaults で購入済み音声リストを共有
  • 購入後は AVSpeechSynthesisProviderVoice.updateSpeechVoices() でシステム音声リストを更新

05:13)音声購入後にシステム音声リストを更新します。

func purchase(voice: WWDCVoice) {
    purchasedVoices.append(voice)
    
    // 共有 UserDefaults に書き込み
    updatePurchasedVoices()
    
    // システムに音声リスト変更を通知
    AVSpeechSynthesisProviderVoice.updateSpeechVoices()
}

06:25)システム音声リストの変化を監視します。

.onReceive(NotificationCenter.default
    .publisher(for: AVSpeechSynthesizer.availableVoicesDidChangeNotification)) { _ in
        systemVoices = AVSpeechSynthesisVoice.speechVoices()
}

Audio Unit Extension の実装

06:53)Extension の核心は AVSpeechSynthesisProviderAudioUnit を継承し、4 つの重要部分を実装することです。

利用可能音声の宣言:

public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit {
    public override var speechVoices: [AVSpeechSynthesisProviderVoice] {
        get {
            let voices: [String : String] = groupDefaults.value(forKey: "voices") as? [String : String] ?? [:]
            return voices.map { key, value in
                return AVSpeechSynthesisProviderVoice(
                    name: value,
                    identifier: key,
                    primaryLanguages: ["en-US"],
                    supportedLanguages: ["en-US"]
                )
            }
        }
    }
}

キーポイント:

  • speechVoicesAVSpeechSynthesisProviderVoice 配列を返す
  • App Group の UserDefaults から購入済み音声リストを読み取る
  • 各音声に名称、識別子、主言語、サポート言語を指定

合成リクエストの受信:

public override func synthesizeSpeechRequest(speechRequest: AVSpeechSynthesisProviderRequest) {
    currentBuffer = getAudioBuffer(for: speechRequest.voice, with: speechRequest.ssmlRepresentation)
    framePosition = 0
}

キーポイント:

  • synthesizeSpeechRequest はシステムからの合成リクエストを受信
  • speechRequest.ssmlRepresentation に SSML テキストが含まれる
  • speechRequest.voice で使用する音声を指定
  • オーディオバッファを生成し、現在のフレーム位置を記録

リクエストのキャンセル:

public override func cancelSpeechRequest() {
    currentBuffer = nil
}

オーディオフレームのレンダリング:

public override var internalRenderBlock: AUInternalRenderBlock {
    return { [weak self]
        actionFlags, timestamp, frameCount, outputBusNumber, outputAudioBufferList, _, _ in
        guard let self else { return kAudio_ParamError }

        var unsafeBuffer = UnsafeMutableAudioBufferListPointer(outputAudioBufferList)[0]
        let frames = unsafeBuffer.mData!.assumingMemoryBound(to: Float32.self)
             
        var sourceBuffer = UnsafeMutableAudioBufferListPointer(self.currentBuffer!.mutableAudioBufferList)[0]
        let sourceFrames = sourceBuffer.mData!.assumingMemoryBound(to: Float32.self)

        for frame in 0..<frameCount {
            if frames.count > frame && sourceFrames.count > self.framePosition {
                frames[Int(frame)] = sourceFrames[Int(self.framePosition)]
                self.framePosition += 1
                if self.framePosition >= self.currentBuffer!.frameLength {
                    break
                }
            }
        }
             
        return noErr
    }
}

キーポイント:

  • internalRenderBlock はシステムが周期的に呼び出し、指定フレーム数の音声を埋める
  • 事前生成したオーディオバッファデータを出力バッファにコピー
  • 全フレームレンダリング完了後、actionFlagsofflineUnitRenderAction_Complete に設定してシステムに通知

Personal Voice

11:10)Personal Voice はユーザーが自分の声を録音し、デバイス上でローカルに合成音声を生成します。使用前に認可をリクエストします。

func fetchPersonalVoices() async {
    AVSpeechSynthesizer.requestPersonalVoiceAuthorization() { status in
        if status == .authorized {
            personalVoices = AVSpeechSynthesisVoice.speechVoices().filter {
                $0.voiceTraits.contains(.isPersonalVoice)
            }
        }
    }
}

11:34)Personal Voice で音声を合成します。

func speakUtterance(string: String) {
    let utterance = AVSpeechUtterance(string: string)
    if let voice = personalVoices.first {
        utterance.voice = voice
        synthesizer.speak(utterance)
    }
}

キーポイント:

  • requestPersonalVoiceAuthorization で Personal Voice へのアクセス許可をリクエスト
  • 認可後、Personal Voice は AVSpeechSynthesisVoice.speechVoices() に現れる
  • voiceTraits.contains(.isPersonalVoice) で Personal Voice をフィルタ
  • Personal Voice は主に AAC(補強代替コミュニケーション)App で使用され、センシティブな権限

重要ポイント

  1. SSML 対応の読み上げ App を作る

    • テキスト読み上げで SSML マークアップをサポートし、休止、話速、音高をユーザーが制御
    • 価値:SSML で機械読み上げがより人間らしくなり、長文読書体験が大幅向上
    • 始め方:テキストエディタに SSML タグ挿入ボタンを追加し、AVSpeechUtterance(ssmlRepresentation:) で合成
  2. 自社 TTS エンジンを iOS システムに接続

    • 自社音声合成技術があれば、Speech Synthesis Provider で Audio Unit Extension 化し、システムの全音声機能で利用可能に
    • 価値:システム音声の制限を超え、VoiceOver や Siri などでもあなたの音声を使える
    • 始め方:Audio Unit Extension プロジェクトを作成し、タイプは Speech Synthesizer、AVSpeechSynthesisProviderAudioUnit の 4 つの核心メソッドを実装
  3. AAC 補助コミュニケーション App に Personal Voice を統合

    • 補助コミュニケーション App でユーザーが自分の Personal Voice を合成音声として選択
    • 価値:発声能力を失う可能性のあるユーザーにとって、自分の声で話すことは大きな心理的価値
    • 始め方:requestPersonalVoiceAuthorization で認可を取得し、isPersonalVoice 音声をフィルタして AVSpeechUtterance に渡す

関連セッション

コメント

GitHub Issues · utterances