Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

iOSの音声読み上げ時のメモリ消費量について

こんにちはミラティブ2年目突入の福山です。 今回は、ユーザーが追加でインストール可能な拡張版の読み上げ音声がiOS 16で多くのメモリを消費するということがわかったので、ライブ配信用のアプリ拡張内で対策を行いました。

メモリ使用量50MB制限

MirrativのiOSアプリは、AVSpeechSynthesizerを使用してライブ配信時のコメントを読み上げる機能を提供しています。この機能は配信用のアプリ拡張であるBroadcast Upload Extension内で動作し、そのメモリ制限は50MBとなっています。もしもこの制限を少しでも超えると、アプリはクラッシュし、ライブ配信は終了してしまいます。したがって、メモリ使用量が増加する可能性のある要素に対しては無視するわけにはいきません。

画像1. Broadcast Upload Extensionのメモリ使用量

拡張版の音声を使用するとすぐに50MBのメモリ制限に達してしまう可能性があります。メモリ使用量が50MBに到達すると、Broadcast Upload Extensionがクラッシュし、「Mirrativへのライブブロードキャストは次の利用により停止しました: 無効なブロードキャストセッションを開始しようとしました」というシステムアラートが表示されます。

画像2. Broadcast Upload Extensionのメモリ使用量が50MBに達して終了したとき

メモリ使用量の計測

音声の変更は、「設定」アプリの「アクセシビリティ」 > 「読み上げコンテンツ」 > 「声」から可能で、日本語には女性音声のKyokoと男性音声のOtoyaがあります(Siriについては後述)。それぞれの拡張版はユーザー側でインストール可能です。

iOS version Kyoko通常 Otoya通常 Kyoko拡張 Otoya拡張
iOS 15.7.1 6.6MB 6.2MB 6.8MB 6.5MB
iOS 16.1.1 6.7MB 6.8MB 23.8MB 21.6MB
iOS 16.5.0 14.7MB 14.9MB 32.1MB 29.9MB

表1. 読み上げ開始時に増えたメモリ使用量 (それぞれ3回計測した平均です)

import SwiftUI
import AVKit

struct ContentView: View {
    @StateObject var speaker = Speaker()

    var body: some View {
        Button {
            speaker.speak(text: "ハロー")
        } label: {
            Text("Speak")
        }
    }
}

class Speaker: ObservableObject {
    private var synthesizer = AVSpeechSynthesizer()

    func speak(text: String) {
        let utterance = AVSpeechUtterance(string: text)
        synthesizer.speak(utterance)
    }
}

コード1. 検証に使用したSwiftコード

対策

ユーザーが拡張版の音声を選択するとライブ配信が突然終了しやすくなるという問題を解消するため、拡張版が選択されていても音声の性別を保ったまま通常版の音声を使うことでメモリ使用量を抑えることにしました。

ちなみに日本語の通常品質のSiri音声はデフォルトでインストールされており、コードからのみアクセス可能です。ユーザーは「設定」アプリから拡張版のSiri音声をインストールし選択可能ですが、それはサードパーティのアプリからは使用することも検出することもできません。

    private lazy var preferredVoice: AVSpeechSynthesisVoice? = {
        // 言語ごとのデフォルトorユーザー選択音声は以下のように取得可能、しかしgenderが常に.unspecifiedとなる
        // ユーザーが日本語のSiriを選択していた場合、サードパーティアプリでは強制的に女性音声Kyokoになる
        let defaultVoice = AVSpeechSynthesisVoice(language: AVSpeechSynthesisVoice.currentLanguageCode())
        // デフォルト音質の場合はそれを使う。
        guard defaultVoice?.quality != .default else {
            return defaultVoice
        }
        // defaultVoiceのgenderが.unspecifiedのため、identifierからユーザーが選択している音声の性別を判定
        let isMaleVoice = defaultVoice?.identifier.contains("Otoya") ?? false
        let gender: AVSpeechSynthesisVoiceGender = isMaleVoice ? .male : .female
        // Siri音声identifierの先頭に使われている文字列
        let siriIdentifierPrefix = "com.apple.ttsbundle.siri"
        let currentDefaultLanguageVoice = AVSpeechSynthesisVoice.speechVoices()
            .first(where: {
                $0.quality == .default
                    && $0.gender == gender
                    && !$0.identifier.hasPrefix(siriIdentifierPrefix)
                    && $0.language == AVSpeechSynthesisVoice.currentLanguageCode()
            })
        return currentDefaultLanguageVoice
    }()

    private var synthesizer = AVSpeechSynthesizer()

    func speak(text: String) {
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = preferredVoice // 声を指定
        synthesizer.speak(utterance)
    }

コード2. 通常版の音声を選択する

おわりに

一般的なアプリでは無視できる程度のメモリ使用量ですが、Broadcast Upload Extensionのような制約がある場合は問題となることがあります。この記事が他の開発者の皆さんの参考になれば幸いです。

We are hiring!

ミラティブでは『好きでつながり、自分の物語(ナラティブ)が生まれる居場所』を実現するエンジニアを募集中です!

www.mirrativ.co.jp

speakerdeck.com

mirrativ.notion.site