diff --git a/README.md b/README.md index b55b8eb..e1e5753 100644 --- a/README.md +++ b/README.md @@ -41,21 +41,22 @@ old PC-speakers you are able to connect your device to them via line port and get a complete speech-based notification system. -## The `/wave` endpoint +## The `/{wave,acc,mp3,m4a,wma,flac}` endpoint ``` -POST /wave +POST /{wave,acc,mp3,m4a,wma,flac} { "text": "The text to be spoken", "language": "en_US" } ``` -*Returns:* `200 OK` with wave file (`Content-Type: audio/x-wav`) +*Returns:* `200 OK` with proper audio file -The `/wave` endpoint enables you to download a wav file containing speech of the +This endpoint enables you to download an audio file file containing speech of the provided text. The goal of this endpoint is to provide interface allowing you establishment of the connection between the TTS Server and some other kind of already running TTS system, which can invoke the HTTP request to your Android device and do something with returned -wav file. +audio file. Note that all files but wav originates right from the wav file through FFmpeg +converter. ## The `/sonos` endpoint ``` diff --git a/app/build.gradle b/app/build.gradle index 8c696ae..7a99dd6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation 'com.bartlomiejpluta:sonos-controller:v.0.2' implementation 'com.google.dagger:dagger-android:2.15' implementation 'com.google.dagger:dagger-android-support:2.15' + implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8' kapt 'com.google.dagger:dagger-android-processor:2.15' kapt 'com.google.dagger:dagger-compiler:2.15' testImplementation 'junit:junit:4.12' diff --git a/app/src/main/assets/help/help.html b/app/src/main/assets/help/help.html index aa7638e..4dfe2d3 100644 --- a/app/src/main/assets/help/help.html +++ b/app/src/main/assets/help/help.html @@ -58,21 +58,22 @@ POST /say get a complete speech-based notification system.

-

The /wave endpoint

+

The /{wave,acc,mp3,m4a,wma,flac} endpoint

-POST /wave
+POST /{wave,acc,mp3,m4a,wma,flac}
 {
     "text": "The text to be spoken",
     "language": "en_US"
 }
 
-

Returns: 200 OK with wave file (Content-Type: audio/x-wav)

+

Returns: 200 OK with proper audio file

- The /wave endpoint enables you to download a wav file containing speech of the + This endpoint enables you to download an audio file file containing speech of the provided text. The goal of this endpoint is to provide interface allowing you establishment of the connection between the TTS Server and some other kind of already running TTS system, which can invoke the HTTP request to your Android device and do something with returned - wav file. + audio file. Note that all files but wav originates right from the wav file through FFmpeg + converter.

The /sonos endpoint

diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt index 62143f1..ad100e6 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/sonos/worker/SonosWorker.kt @@ -1,6 +1,7 @@ package com.bartlomiejpluta.ttsserver.core.sonos.worker import android.content.SharedPreferences +import cafe.adriel.androidaudioconverter.model.AudioFormat import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint @@ -40,7 +41,7 @@ class SonosWorker( } private fun prepareTTSFile(data: SonosDTO): String { - val filename = tts.createTTSFile(data.text, data.language).name + val filename = tts.createTTSFile(data.text, data.language, AudioFormat.MP3).name return "$announcementUrl$filename" } diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/engine/TTSEngine.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/engine/TTSEngine.kt index 9357046..c59c1b9 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/engine/TTSEngine.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/engine/TTSEngine.kt @@ -6,12 +6,14 @@ import android.media.AudioAttributes import android.media.MediaPlayer import android.net.Uri import android.speech.tts.TextToSpeech +import cafe.adriel.androidaudioconverter.model.AudioFormat import com.bartlomiejpluta.ttsserver.core.tts.exception.TTSException import com.bartlomiejpluta.ttsserver.core.tts.listener.GongListener import com.bartlomiejpluta.ttsserver.core.tts.listener.TTSProcessListener import com.bartlomiejpluta.ttsserver.core.tts.model.TTSStream import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder +import com.bartlomiejpluta.ttsserver.core.util.AudioConverter import com.bartlomiejpluta.ttsserver.ui.preference.PreferenceKey import java.io.BufferedInputStream import java.io.File @@ -23,29 +25,41 @@ class TTSEngine( private val context: Context, private val tts: TextToSpeech, private val ttsStatusHolder: TTSStatusHolder, - private val preferences: SharedPreferences + private val preferences: SharedPreferences, + private val audioConverter: AudioConverter ) { private val messageDigest = MessageDigest.getInstance("SHA-256") val status: TTSStatus get() = ttsStatusHolder.status - fun createTTSFile(text: String, language: Locale): File { + fun createTTSFile(text: String, language: Locale, audioFormat: AudioFormat = AudioFormat.WAV): File { val digest = hash(text, language) - val filename = "tts_$digest.wav" - val file = File(context.cacheDir, filename) + val targetFilename = "tts_$digest.${audioFormat.format}" + val wavFilename = "tts_$digest.wav" + val wavFile = File(context.cacheDir, wavFilename) + val targetFile = File(context.cacheDir, targetFilename) - file.takeIf { it.exists() }?.let { return it } + targetFile.takeIf { it.exists() }?.let { return it } val uuid = UUID.randomUUID().toString() val listener = TTSProcessListener(uuid) tts.setOnUtteranceProgressListener(listener) tts.language = language - tts.synthesizeToFile(text, null, file, uuid) + tts.synthesizeToFile(text, null, wavFile, uuid) listener.await() - return file + return convertFile(wavFile, audioFormat) + } + + private fun convertFile(file: File, audioFormat: AudioFormat): File { + if (audioFormat == AudioFormat.WAV) { + return file + } + + + return audioConverter.convert(file, audioFormat).also { file.delete() } } private fun hash(text: String, language: Locale): String { @@ -54,7 +68,7 @@ class TTSEngine( return digest.fold("", { str, it -> str + "%02x".format(it) }) } - fun fetchTTSStream(text: String, language: Locale): TTSStream { + fun fetchTTSStream(text: String, language: Locale, audioFormat: AudioFormat = AudioFormat.WAV): TTSStream { val file = createTempFile("tmp_tts_server", ".wav") val uuid = UUID.randomUUID().toString() @@ -65,8 +79,9 @@ class TTSEngine( tts.synthesizeToFile(text, null, file, uuid) listener.await() - val stream = BufferedInputStream(FileInputStream(file)) - val length = file.length() + val converted = convertFile(file, audioFormat) + val stream = BufferedInputStream(FileInputStream(converted)) + val length = converted.length() file.delete() diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/exception/AudioConversionException.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/exception/AudioConversionException.kt new file mode 100644 index 0000000..e48674d --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/exception/AudioConversionException.kt @@ -0,0 +1,6 @@ +package com.bartlomiejpluta.ttsserver.core.tts.exception + +class AudioConversionException : Exception { + constructor(message: String) : super(message) + constructor(message: String, cause: Throwable) : super(message, cause) +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/listener/ConverterListener.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/listener/ConverterListener.kt new file mode 100644 index 0000000..a90eae0 --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/tts/listener/ConverterListener.kt @@ -0,0 +1,20 @@ +package com.bartlomiejpluta.ttsserver.core.tts.listener + +import cafe.adriel.androidaudioconverter.callback.IConvertCallback +import com.bartlomiejpluta.ttsserver.core.tts.exception.AudioConversionException +import java.io.File +import java.util.concurrent.LinkedBlockingQueue + +class ConverterListener : IConvertCallback { + private val queue = LinkedBlockingQueue() + + fun await() = queue.take() + + override fun onSuccess(convertedFile: File?) { + queue.add(convertedFile) + } + + override fun onFailure(error: Exception?) { + error?.let { throw AudioConversionException("Conversion failed", error) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/util/AudioConverter.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/util/AudioConverter.kt new file mode 100644 index 0000000..2e1f0ba --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/util/AudioConverter.kt @@ -0,0 +1,47 @@ +package com.bartlomiejpluta.ttsserver.core.util + +import android.content.Context +import cafe.adriel.androidaudioconverter.AndroidAudioConverter +import cafe.adriel.androidaudioconverter.callback.ILoadCallback +import cafe.adriel.androidaudioconverter.model.AudioFormat +import com.bartlomiejpluta.ttsserver.core.tts.exception.AudioConversionException +import com.bartlomiejpluta.ttsserver.core.tts.listener.ConverterListener +import java.io.File + +class AudioConverter(private val context: Context) { + var state: State = State.UNLOADED + private set + + enum class State { + READY, + ERROR, + UNLOADED + } + + init { + AndroidAudioConverter.load(context, object : ILoadCallback { + override fun onSuccess() { + state = State.READY + } + + override fun onFailure(error: Exception?) { + state = State.ERROR + } + }) + } + + fun convert(file: File, format: AudioFormat): File { + if(state != State.READY) { + throw AudioConversionException("Converter is not ready") + } + + val listener = ConverterListener() + AndroidAudioConverter.with(context) + .setFile(file) + .setFormat(format) + .setCallback(listener) + .convert() + + return listener.await() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt index 50b92ae..b10cb45 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/Endpoint.kt @@ -4,14 +4,19 @@ enum class Endpoint(val uri: String, val id: Int) { UNKNOWN("/", 1), SAY("/say", 2), WAVE("/wave", 3), - SONOS("/sonos", 4), - SONOS_CACHE("/sonos/*", 5), - GONG("/gong.wav", 6); + AAC("/aac", 4), + MP3("/mp3", 5), + M4A("/m4a", 6), + WMA("/wma", 7), + FLAC("/flac", 8), + SONOS("/sonos", 9), + SONOS_CACHE("/sonos/*", 10), + GONG("/gong.wav", 11); val trimmedUri: String get() = uri.replace("*", "") companion object { - fun of(id: Int) = values().firstOrNull { it.id == id } ?: UNKNOWN + fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN } } \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt index 91d311f..1eb1626 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/endpoint/EndpointMatcher.kt @@ -8,7 +8,7 @@ object EndpointMatcher { init { Endpoint.values().forEach { - uriMatcher.addURI("", it.uri, it.id) + uriMatcher.addURI("", it.uri, it.ordinal) } } diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/mime/MimeType.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/mime/MimeType.kt new file mode 100644 index 0000000..7756de4 --- /dev/null +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/mime/MimeType.kt @@ -0,0 +1,28 @@ +package com.bartlomiejpluta.ttsserver.core.web.mime + +import cafe.adriel.androidaudioconverter.model.AudioFormat +import java.io.File +import java.util.* + +enum class MimeType(val mimeType: String) { + AAC("audio/aac"), + MP3("audio/mpeg"), + M4A("audio/m4a"), + WMA("audio/x-ms-wma"), + WAV("audio/x-wav"), + FLAC("audio/x-wav"), + JSON("application/json"); + + companion object { + fun forAudioFormat(audioFormat: AudioFormat) = when(audioFormat) { + AudioFormat.AAC -> AAC + AudioFormat.MP3 -> MP3 + AudioFormat.M4A -> M4A + AudioFormat.WMA -> WMA + AudioFormat.WAV -> WAV + AudioFormat.FLAC -> FLAC + } + + fun forFile(file: File) = valueOf(file.extension.toUpperCase(Locale.ROOT)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt index 268c279..85e682f 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/core/web/server/WebServer.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.content.SharedPreferences import android.net.Uri import androidx.localbroadcastmanager.content.LocalBroadcastManager +import cafe.adriel.androidaudioconverter.model.AudioFormat import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.tts.exception.TTSException @@ -14,6 +15,7 @@ import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointMatcher import com.bartlomiejpluta.ttsserver.core.web.exception.WebException +import com.bartlomiejpluta.ttsserver.core.web.mime.MimeType import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService import com.bartlomiejpluta.ttsserver.service.state.ServiceState import com.bartlomiejpluta.ttsserver.ui.preference.PreferenceKey @@ -57,7 +59,12 @@ class WebServer( private fun dispatch(it: IHTTPSession): Response { return when (EndpointMatcher.match(it.uri)) { Endpoint.SAY -> say(it) - Endpoint.WAVE -> wave(it) + Endpoint.WAVE -> file(it, AudioFormat.WAV) + Endpoint.AAC -> file(it, AudioFormat.AAC) + Endpoint.MP3 -> file(it, AudioFormat.MP3) + Endpoint.M4A -> file(it, AudioFormat.M4A) + Endpoint.WMA -> file(it, AudioFormat.WMA) + Endpoint.FLAC -> file(it, AudioFormat.FLAC) Endpoint.SONOS -> sonos(it) Endpoint.SONOS_CACHE -> sonosCache(it) Endpoint.GONG -> gong(it) @@ -103,8 +110,8 @@ class WebServer( } } - private fun wave(session: IHTTPSession): Response { - if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) { + private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response { + if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) { throw WebException(NOT_FOUND) } @@ -118,8 +125,8 @@ class WebServer( val dto = extractBody(session) { BaseDTO(it) } - val (stream, size) = tts.fetchTTSStream(dto.text, dto.language) - return newFixedLengthResponse(OK, MIME_WAVE, stream, size) + val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat) + return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size) } private fun sonos(session: IHTTPSession): Response { @@ -160,7 +167,7 @@ class WebServer( val stream = BufferedInputStream(FileInputStream(file)) val size = file.length() - return newFixedLengthResponse(OK, MIME_WAVE, stream, size) + return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size) } private fun gong(session: IHTTPSession): Response { @@ -183,7 +190,7 @@ class WebServer( context.contentResolver.openInputStream(uri) ?: throw TTSException() ) - return newFixedLengthResponse(OK, MIME_WAVE, stream, size) + return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size) } override fun start() { @@ -208,7 +215,6 @@ class WebServer( companion object { private const val MIME_JSON = "application/json" - private const val MIME_WAVE = "audio/x-wav" private const val CONTENT_TYPE = "content-type" private val SUCCESS_RESPONSE = response("Request has been completed") private val QUEUED_RESPONSE = response("Request has been queued") diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt index 35839de..4a4f9b4 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/di/module/TTSModule.kt @@ -7,6 +7,7 @@ import androidx.preference.PreferenceManager import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder +import com.bartlomiejpluta.ttsserver.core.util.AudioConverter import com.bartlomiejpluta.ttsserver.core.util.NetworkUtil import com.bartlomiejpluta.ttsserver.core.web.server.WebServerFactory import com.bartlomiejpluta.ttsserver.service.notification.ForegroundNotificationFactory @@ -32,8 +33,9 @@ class TTSModule { context: Context, textToSpeech: TextToSpeech, ttsStatusHolder: TTSStatusHolder, - preferences: SharedPreferences - ) = TTSEngine(context, textToSpeech, ttsStatusHolder, preferences) + preferences: SharedPreferences, + converter: AudioConverter + ) = TTSEngine(context, textToSpeech, ttsStatusHolder, preferences, converter) @Provides @Singleton @@ -58,6 +60,10 @@ class TTSModule { fun networkUtil(context: Context, preferences: SharedPreferences) = NetworkUtil(context, preferences) + @Provides + @Singleton + fun adudioConverter(context: Context) = AudioConverter(context) + @Provides @Singleton fun sonosQueue(tts: TTSEngine, preferences: SharedPreferences, networkUtil: NetworkUtil) = diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/Preference.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/Preference.kt index 171810d..da588c9 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/Preference.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/Preference.kt @@ -5,7 +5,7 @@ object PreferenceKey { const val PORT = "preference_port" const val ENABLE_HTTP_DEBUG = "preference_http_debug" const val ENABLE_SAY_ENDPOINT = "preference_enable_say_endpoint" - const val ENABLE_WAVE_ENDPOINT = "preference_enable_wave_endpoint" + const val ENABLE_FILE_ENDPOINTS = "preference_enable_file_endpoints" const val ENABLE_SONOS_ENDPOINT = "preference_enable_sonos_endpoint" const val ENABLE_GONG = "preference_enable_gong" const val GONG = "preference_gong" diff --git a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/PreferencesFragment.kt b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/PreferencesFragment.kt index f138ab7..754e0cb 100644 --- a/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/bartlomiejpluta/ttsserver/ui/preference/PreferencesFragment.kt @@ -12,6 +12,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference import com.bartlomiejpluta.R +import com.bartlomiejpluta.ttsserver.core.web.mime.MimeType import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService import com.bartlomiejpluta.ttsserver.service.state.ServiceState @@ -19,7 +20,7 @@ import com.bartlomiejpluta.ttsserver.service.state.ServiceState class PreferencesFragment : PreferenceFragmentCompat() { private lateinit var portPreference: IntEditTextPreference private lateinit var sayEndpointPreference: SwitchPreference - private lateinit var waveEndpointPreference: SwitchPreference + private lateinit var fileEndpointPreference: SwitchPreference private lateinit var sonosEndpointPreference: SwitchPreference private lateinit var httpDebugPreference: SwitchPreference private lateinit var enableGongPreference: SwitchPreference @@ -61,7 +62,7 @@ class PreferencesFragment : PreferenceFragmentCompat() { portPreference.setOnBindEditTextListener { it.inputType = InputType.TYPE_CLASS_NUMBER } httpDebugPreference = findPreference(PreferenceKey.ENABLE_HTTP_DEBUG)!! sayEndpointPreference = findPreference(PreferenceKey.ENABLE_SAY_ENDPOINT)!! - waveEndpointPreference = findPreference(PreferenceKey.ENABLE_WAVE_ENDPOINT)!! + fileEndpointPreference = findPreference(PreferenceKey.ENABLE_FILE_ENDPOINTS)!! sonosEndpointPreference = findPreference(PreferenceKey.ENABLE_SONOS_ENDPOINT)!! enableGongPreference = findPreference(PreferenceKey.ENABLE_GONG)!! enableGongPreference.setOnPreferenceClickListener { preference -> @@ -90,7 +91,7 @@ class PreferencesFragment : PreferenceFragmentCompat() { private fun openFilePicker(preference: Preference?) { if ((preference as SwitchPreference).isChecked) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - .apply { type = "audio/x-wav" } + .apply { type = MimeType.WAV.mimeType } .let { Intent.createChooser(it, getString(R.string.preference_gong_picker_prompt)) } startActivityForResult(intent, PICKFILE_RESULT_CODE) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c007723..0964b6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,8 +19,8 @@ Server port Allow HTTP clients to use /say endpoint which enables them to say message using builtin speakers or external ones connected to the device Enable /say endpoint - Allow HTTP clients to use /wave endpoint which enables them to retrieve TTS message as wave file - Enable /wave endpoint + Allow HTTP clients to use file endpoints (/wav, /mp3, /flac etc.) which enables them to retrieve TTS message as audio file + Enable file endpoints Allow HTTP clients to use /sonos endpoint which enables them to send TTS messages directly to Sonos devices Enable /sonos endpoint Attach the stacktrace to each error HTTP response if available diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index cbcfca7..0e05302 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -27,9 +27,9 @@