これという仕様の記載を見つけられなかったのですがtts.speak()では同時にテキストの再生はできないと思われます。
試しにTTSのインスタンスをアプリ内で二つ作ってspeak()させてみましたが同時再生はできませんでした。STREAM_TYPEをSYSTEMとMUSICのように変えて見ましたがそれでもダメでした。また複数のTTSアプリを使ってどうなるかも試しましたが同時再生はできませんでした。
が仕様的にがんばれば実現できそうに思ったのでこんな感じで作って見ました。
色々自分で試しやすいようにKotlinを使いたかったので、共有していただいているコードに追加する形になっていないのはご容赦下さい。もしJavaのコードで確認したい場合はAndroidStudioでKotlin->Java変換を試して見て下さい。
tts.speak()ではなくtts.synthesizeToFileを使って音声ファイルを作成し、それをMediaPlayerで再生させる形で同時読み上げっぽい挙動を実装しました。
コードをシンプルにするために読み上げる文字列は固定、読み上げのpitchなども固定にしています。またminSdk>=21です。
MainActivity.kt
Kotlin
1package 自分の開発環境に合わせて下さい
2
3import androidx.appcompat.app.AppCompatActivity
4import android.os.Bundle
5import android.speech.tts.TextToSpeech
6import android.speech.tts.UtteranceProgressListener
7import android.util.Log
8import android.widget.Button
9import android.widget.TextView
10import android.media.MediaPlayer
11import android.net.Uri
12import java.io.File
13
14private val TAG = MainActivity::class.java.simpleName
15
16class MainActivity : AppCompatActivity() {
17 companion object {
18 private const val TEXT1_ID = "TEXT1_ID"
19 private const val TEXT2_ID = "TEXT2_ID"
20 }
21
22 /** テキスト読み上げ */
23 private lateinit var tts: TextToSpeech
24 lateinit var text1: TextView
25 lateinit var button1: Button
26 lateinit var text2: TextView
27 lateinit var button2: Button
28
29 private var mediaPlayer1: MediaPlayer? = null
30 private var mediaPlayer2: MediaPlayer? = null
31
32 override fun onCreate(savedInstanceState: Bundle?) {
33 super.onCreate(savedInstanceState)
34 setContentView(R.layout.activity_main)
35
36 text1 = findViewById(R.id.text1)
37 text2 = findViewById(R.id.text2)
38 button1 = findViewById(R.id.button1)
39 button2 = findViewById(R.id.button2)
40
41 // TTS初期化
42 tts = TextToSpeech(this, TextToSpeech.OnInitListener { status ->
43 if (TextToSpeech.SUCCESS != status) {
44 Log.e(TAG, "TextToSpeech init error status=$status")
45 }
46 }).apply {
47 setSpeechRate(2.0f)
48 setPitch(1.0f)
49 }
50
51 val file1 = File(cacheDir, TEXT1_ID)
52 val file2 = File(cacheDir, TEXT2_ID)
53 tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
54 override fun onDone(utteranceId: String?) {
55 // TextToSpeech.synthesizeToFile完了時もspeak時と同様にこのコールバックが実行される
56 Log.d(TAG, "onDone")
57
58 when (utteranceId) {
59 TEXT1_ID -> {
60 // synthesizeToFileによって生成されたファイルでMediaPlayerを生成し再生する
61 mediaPlayer1 = MediaPlayer.create(this@MainActivity, (Uri.fromFile(file1)))
62
63 // 再生後の後始末
64 mediaPlayer1!!.setOnCompletionListener {
65 Log.d(TAG, "end of audio mediaPlayer1")
66 releaseMediaPlayer(mediaPlayer1)
67 mediaPlayer1 = null
68 }
69
70 mediaPlayer1!!.start()
71 }
72 TEXT2_ID -> {
73 mediaPlayer2 = MediaPlayer.create(this@MainActivity, (Uri.fromFile(file2)))
74
75 mediaPlayer2!!.setOnCompletionListener {
76 Log.d(TAG, "end of audio mediaPlayer2")
77 releaseMediaPlayer(mediaPlayer2)
78 mediaPlayer2 = null
79 }
80
81 mediaPlayer2!!.start()
82 }
83 }
84 }
85
86 override fun onError(utteranceId: String?) {
87 Log.w(TAG, "onError")
88 }
89
90 override fun onStart(utteranceId: String?) {
91 // NOP
92 }
93 })
94
95 button1.setOnClickListener {
96 releaseMediaPlayer(mediaPlayer1)
97 val param = Bundle().apply {
98 putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, 1.0f)
99 }
100
101 // tts.speak()させずにファイルに書き出す
102 // ref: http://www.dre.vanderbilt.edu/~schmidt/android/android-4.0/out/target/common/docs/doc-comment-check/resources/articles/tts.html
103 tts.synthesizeToFile(text1.text.toString(), param, file1, TEXT1_ID)
104
105 }
106
107 button2.setOnClickListener {
108 releaseMediaPlayer(mediaPlayer2)
109 val param = Bundle().apply {
110 putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, 1.0f)
111 }
112
113 tts.synthesizeToFile(text2.text.toString(), param, file2, TEXT2_ID)
114 }
115 }
116
117 // ref: https://developer.android.com/guide/topics/media/mediaplayer?hl=ja#releaseplayer
118 override fun onStop() {
119 super.onStop()
120 releaseMediaPlayer(mediaPlayer1)
121 mediaPlayer1 = null
122 releaseMediaPlayer(mediaPlayer2)
123 mediaPlayer2 = null
124 }
125
126 override fun onDestroy() {
127 super.onDestroy()
128 tts.shutdown()
129 }
130
131 private fun releaseMediaPlayer(mediaPlayer: MediaPlayer?) {
132 if (mediaPlayer?.isPlaying == true) {
133 mediaPlayer.stop()
134 }
135 mediaPlayer?.reset()
136 mediaPlayer?.release()
137 }
138}
139
activity_main.xml
xml
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 tools:context=".MainActivity">
8
9 <TextView
10 android:id="@+id/text1"
11 android:layout_width="wrap_content"
12 android:layout_height="wrap_content"
13 android:text="1111"
14 android:textSize="16sp"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toTopOf="parent" />
17
18 <Button
19 android:id="@+id/button1"
20 android:layout_width="wrap_content"
21 android:layout_height="wrap_content"
22 android:layout_marginTop="8dp"
23 android:background="@color/colorPrimary"
24 android:text="button1"
25 app:layout_constraintStart_toStartOf="parent"
26 app:layout_constraintTop_toBottomOf="@+id/text1" />
27
28 <TextView
29 android:id="@+id/text2"
30 android:layout_width="wrap_content"
31 android:layout_height="wrap_content"
32 android:layout_marginTop="20dp"
33 android:text="2222"
34 android:textSize="16sp"
35 app:layout_constraintStart_toStartOf="parent"
36 app:layout_constraintTop_toBottomOf="@+id/button1" />
37
38 <Button
39 android:id="@+id/button2"
40 android:layout_width="wrap_content"
41 android:layout_height="wrap_content"
42 android:layout_marginTop="8dp"
43 android:background="@color/colorPrimary"
44 android:text="button2"
45 app:layout_constraintStart_toStartOf="parent"
46 app:layout_constraintTop_toBottomOf="@+id/text2" />
47
48
49</androidx.constraintlayout.widget.ConstraintLayout>
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。