diff --git a/.gitignore b/.gitignore index d0ddcf174..da6fbe1e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ target/ logs/ +log/ +logs/ +*.wav + # IntelliJ .DS_Store .idea diff --git a/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/examples/speaker/SpeakerExamples.kt b/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/examples/speaker/SpeakerExamples.kt new file mode 100644 index 000000000..912880e04 --- /dev/null +++ b/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/examples/speaker/SpeakerExamples.kt @@ -0,0 +1,59 @@ +package de.gleex.pltcmd.game.application.examples.speaker + +import de.gleex.pltcmd.game.sound.speech.Speaker +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.hexworks.cobalt.logging.api.LoggerFactory + +private val log = LoggerFactory.getLogger("SpeakerExample") + +private val texts = listOf( + "Hello World!", + "Bravo, engage enemy at (1 1 5| 2 2 4), out." +) + +@ExperimentalCoroutinesApi +fun main() { + + /* + Documentation about maryTTS can be found here: http://marytts.phonetik.uni-muenchen.de:59125/documentation.html + + Available effects: http://marytts.phonetik.uni-muenchen.de:59125/audioeffects + The list is: + Volume amount:2.0; + TractScaler amount:1.5; + F0Scale f0Scale:2.0; + F0Add f0Add:50.0; + Rate durScale:1.5; + Robot amount:100.0; + Whisper amount:100.0; + Stadium amount:100.0 + Chorus delay1:466;amp1:0.54;delay2:600;amp2:-0.10;delay3:250;amp3:0.30 + FIRFilter type:3;fc1:500.0;fc2:2000.0 + JetPilot + + Description for a specific effect (change effect name in the URL): + http://marytts.phonetik.uni-muenchen.de:59125/audioeffect-help?effect=TractScaler + */ + + log.info("Starting Speaker...") + Speaker.startup() + + for (text in texts) { + log.info("Saying '$text'...") + runBlocking { + Speaker.say(text) + delay(100) + Speaker.waitForQueueToEmpty() + } + log.info("") + log.info(" - - -") + log.info("") + } + + runBlocking { + delay(500) + Speaker.waitForQueueToEmpty() + } +} \ No newline at end of file diff --git a/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/main.kt b/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/main.kt index 1c039fc42..1f20cda25 100644 --- a/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/main.kt +++ b/game/application/src/main/kotlin/de/gleex/pltcmd/game/application/main.kt @@ -7,6 +7,7 @@ import de.gleex.pltcmd.game.engine.entities.types.callsign import de.gleex.pltcmd.game.engine.entities.types.element import de.gleex.pltcmd.game.options.GameOptions import de.gleex.pltcmd.game.options.UiOptions +import de.gleex.pltcmd.game.sound.speech.Speaker import de.gleex.pltcmd.game.ticks.Ticker import de.gleex.pltcmd.game.ui.entities.GameWorld import de.gleex.pltcmd.game.ui.mapgeneration.MapGenerationProgressController @@ -22,6 +23,7 @@ import de.gleex.pltcmd.model.world.Sector import de.gleex.pltcmd.model.world.WorldMap import de.gleex.pltcmd.model.world.coordinate.Coordinate import de.gleex.pltcmd.model.world.toSectorOrigin +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.hexworks.amethyst.api.Engine import org.hexworks.cobalt.logging.api.LoggerFactory import org.hexworks.zircon.api.SwingApplications @@ -32,9 +34,11 @@ import org.hexworks.zircon.api.screen.Screen import java.util.concurrent.TimeUnit import kotlin.random.Random +@ExperimentalCoroutinesApi private val log = LoggerFactory.getLogger(::main::class) private val random = Random(GameOptions.MAP_SEED) +@ExperimentalCoroutinesApi fun main() { Main().run() } @@ -42,6 +46,7 @@ fun main() { /** * Setups, starts and runs the game. */ +@ExperimentalCoroutinesApi open class Main { /** @@ -50,6 +55,8 @@ open class Main { open fun run() { val application = SwingApplications.startApplication(UiOptions.buildAppConfig()) + Speaker.startup() + val tileGrid = application.tileGrid val screen = tileGrid.toScreen() diff --git a/game/application/src/main/resources/logback.xml b/game/application/src/main/resources/logback.xml index 577dbbdd3..f6752490b 100644 --- a/game/application/src/main/resources/logback.xml +++ b/game/application/src/main/resources/logback.xml @@ -29,8 +29,7 @@ - - + diff --git a/game/options/src/main/kotlin/de/gleex/pltcmd/game/options/GameOptions.kt b/game/options/src/main/kotlin/de/gleex/pltcmd/game/options/GameOptions.kt index 02c82bce1..534e99eae 100644 --- a/game/options/src/main/kotlin/de/gleex/pltcmd/game/options/GameOptions.kt +++ b/game/options/src/main/kotlin/de/gleex/pltcmd/game/options/GameOptions.kt @@ -57,4 +57,6 @@ object GameOptions { * Vertical number of sectors in the world. */ const val SECTORS_COUNT_V: Int = 10 + + const val SOUND_ENABLED = true } \ No newline at end of file diff --git a/game/pom.xml b/game/pom.xml index 3539bf9ea..2af93f873 100644 --- a/game/pom.xml +++ b/game/pom.xml @@ -21,6 +21,7 @@ ui engine ui-strings + sound @@ -76,6 +77,11 @@ ui-strings ${project.version} + + de.gleex.pltcmd.game + sound + ${project.version} + de.gleex.pltcmd.model diff --git a/game/sound/pom.xml b/game/sound/pom.xml new file mode 100644 index 000000000..21c9c9812 --- /dev/null +++ b/game/sound/pom.xml @@ -0,0 +1,53 @@ + + + + game + de.gleex.pltcmd.game + 0.2.0-SNAPSHOT + + 4.0.0 + + sound + Sound + Everything related to sound output. + + + 5.2 + ${marytts.version}-beta3 + + + + + de.gleex.pltcmd.game + options + + + + de.dfki.mary + voice-cmu-rms-hsmm + ${marytts.version} + + + de.dfki.mary + marytts-runtime + + + de.dfki.mary + marytts-common + + + de.dfki.mary + marytts-signalproc + + + + + + de.dfki.mary + marytts-runtime + ${marytts.version.beta} + + + \ No newline at end of file diff --git a/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/Speaker.kt b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/Speaker.kt new file mode 100644 index 000000000..645875d23 --- /dev/null +++ b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/Speaker.kt @@ -0,0 +1,222 @@ +package de.gleex.pltcmd.game.sound.speech + +import de.gleex.pltcmd.game.options.GameOptions +import de.gleex.pltcmd.game.sound.speech.Speaker.say +import de.gleex.pltcmd.game.sound.speech.Speaker.startup +import de.gleex.pltcmd.game.sound.speech.effects.EffectList +import de.gleex.pltcmd.game.sound.speech.effects.Effects +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import marytts.LocalMaryInterface +import marytts.modules.synthesis.Voice +import marytts.server.Mary +import marytts.util.data.audio.MaryAudioUtils +import org.hexworks.cobalt.logging.api.LoggerFactory +import java.io.File +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import javax.sound.sampled.* + +/** + * Use this object for text-to-speech. All strings given to [say] wll be read aloud one after another. This means + * that further texts coming in will be queued when a sound is currently being played. + * + * The underlying TTS-engine will implicitly be started at the first call of [say] if necessary but it is + * advisable to call [startup] earlier as it might take some time and thus delay the output of the first text. + * + * How this object works in detail: [say] creates an audio file from the given text and saves it locally and then + * queues this file for replay. The file names are generated from the hashcode of the given text so the same + * text does not need to be written to a file again. + */ +@ExperimentalCoroutinesApi +object Speaker { + + private const val FOLDER_SPEECH_FILES = "./speech" + private val IGNORED_PHRASES = listOf( + "come in", + "send it" + ) + + private lateinit var mary: LocalMaryInterface + + /** + * The filenames to play. All entries in this channel are being played one after another. + */ + private val filenames: Channel = Channel() + + private val playingSound = AtomicBoolean(false) + + private var playLoop: Job? = null + + private val log = LoggerFactory.getLogger(Speaker::class) + + /** + * Used to set the effects used by MaryTTS. The next call of [say] will use the given value. + * + * This magic string must be in the form of "Effect(parameter:value)+OtherEffect(parameter:value;parameter2:value2)" + * + * The list of available effects can be found here: http://marytts.phonetik.uni-muenchen.de:59125/audioeffects + * It is: + * - Volume amount:2.0; + * - TractScaler amount:1.5; + * - F0Scale f0Scale:2.0; + * - F0Add f0Add:50.0; + * - Rate durScale:1.5; + * - Robot amount:100.0; + * - Whisper amount:100.0; + * - Stadium amount:100.0 + * - Chorus delay1:466;amp1:0.54;delay2:600;amp2:-0.10;delay3:250;amp3:0.30 + * - FIRFilter type:3;fc1:500.0;fc2:2000.0 + * - JetPilot + */ + internal var effects = EffectList.none + + init { + if (GameOptions.SOUND_ENABLED) { + log.debug("Starting MaryTTS engine...") + mary = LocalMaryInterface() + if(Mary.currentState() == Mary.STATE_RUNNING) { + log.debug("MaryTTS started.") + } + + effects = EffectList.of( + Effects.jetPilot(), + Effects.rate(0.65) + ) + + log.debug("Available voices: ${mary.availableVoices}") + val defaultVoice = Voice.getDefaultVoice(Locale.US) + log.debug("Default US voice: ${defaultVoice.name}") + + + playLoop = GlobalScope.launch { + log.debug("Launching play loop") + while (isActive) { + val fileToPlay = filenames.receive() + log.debug("Received file to play: $fileToPlay") + play(fileToPlay) + } + log.debug("Stopped play loop") + } + } else { + log.info("Sound disabled, Speaker will do nothing.") + } + Runtime.getRuntime() + .addShutdownHook(Thread { + shutdown() + }) + } + + /** + * Explicitly starts the TTS engine. You don't need to call this method, as the engine is also initialized + * at the first call to [say]. But as it may take a little time it is most probably better to call this + * method early. + */ + fun startup() { + // This simply triggers the init block which handles all the initialization + log.info("Speaker initialized.") + } + + /** + * Stops the underlying TTS engine after waiting for the current replay queue to be empty (current playback + * may finish first, too). + */ + fun shutdown() { + log.info("Shutting down Speaker...") + runBlocking { + if (playLoop != null) { + log.debug("Waiting for speaker queue to empty...") + waitForQueueToEmpty() + log.debug("Stopping replay loop...") + playLoop?.cancelAndJoin() + } + if (Mary.currentState() == Mary.STATE_RUNNING) { + log.debug("Shutting down MaryTTS...") + Mary.shutdown() + } + log.info("Speaker shutdown complete!") + } + } + + /** + * Generates text-to-speech audio from the given text, writes it to a file (when the same text has not yet + * been written to a file) and queues it for replay. + */ + suspend fun say(text: String) { + if (GameOptions.SOUND_ENABLED.not()) { + return + } + if (IGNORED_PHRASES.any { + text.contains(it, true) + }) { + log.debug("Ignored phrase detected, not saying '$text'") + return + } + val speechDirectory = File(FOLDER_SPEECH_FILES) + if (!speechDirectory.exists()) { + speechDirectory.mkdir() + } + + // reuse existing texts if they had the same effects + mary.audioEffects = effects.toString() + val path = "${speechDirectory.absolutePath}/${text.hashCode()}_${mary.audioEffects.hashCode()}.wav" + + val soundFile = File(path) + if (!soundFile.exists()) { + log.debug("Creating new sound file $path") + val audio = mary.generateAudio(text) + log.debug("Generated speech audio with effects: ${mary.audioEffects}") + val samples = MaryAudioUtils.getSamplesAsDoubleArray(audio!!) + MaryAudioUtils.writeWavFile(samples, path, audio.format) + } + soundFile.deleteOnExit() + + filenames.send(path) + } + + private fun play(filename: String) { + val soundFile = File(filename) + if (soundFile.exists()) { + val audioStream = AudioSystem.getAudioInputStream(soundFile) + val audioFormat = audioStream!!.format + val info = DataLine.Info(SourceDataLine::class.java, audioFormat) + val sourceDataLine = AudioSystem.getLine(info) as SourceDataLine + try { + playingSound.set(true) + sourceDataLine.use { + play(audioStream, it, audioFormat) + } + } finally { + playingSound.set(false) + } + } else { + log.warn("Speech file $filename does not exist! Can not play it.") + } + } + + private fun play(audioStream: AudioInputStream, line: SourceDataLine, audioFormat: AudioFormat?) { + line.open(audioFormat) + line.start() + + var count = 0 + val buffer = ByteArray(4096) + while (count != -1) { + count = audioStream.read(buffer, 0, buffer.size) + if (count >= 0) { + line.write(buffer, 0, count) + } + } + + line.drain() + } + + /** + * Waits until the current queue of sounds has been played and all playback is finished. + */ + suspend fun waitForQueueToEmpty() { + delay(100) + while (filenames.isEmpty.not() || playingSound.get()) { + delay(100) + } + } +} \ No newline at end of file diff --git a/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/Effect.kt b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/Effect.kt new file mode 100644 index 000000000..b168e2e58 --- /dev/null +++ b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/Effect.kt @@ -0,0 +1,18 @@ +package de.gleex.pltcmd.game.sound.speech.effects + +/** + * This is a simple model class to create the String representation of an [marytts.signalproc.effects.AudioEffect]. + * + * We could directly use the implementations of that class but they have a horrible API and we can only set them + * in [marytts.LocalMaryInterface.effects] as String. That's why we have this simple data class. + * + * To be parsable by MaryTTS it has to be in the form of either: + * - "EffectName" when it has no parameters or + * - "EffectName(parameter1:value1;parameter2:value2)" + */ +internal data class Effect( + val name: String, + val parameters: EffectParameterList = EffectParameterList.empty +) { + override fun toString() = "$name${if (parameters != EffectParameterList.empty) "($parameters)" else ""}" +} \ No newline at end of file diff --git a/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectList.kt b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectList.kt new file mode 100644 index 000000000..d9a76487f --- /dev/null +++ b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectList.kt @@ -0,0 +1,20 @@ +package de.gleex.pltcmd.game.sound.speech.effects + +/** + * A list of [Effect]s. This model class actually creates the string needed by [marytts.LocalMaryInterface.effects] + * to set the currently active effects. + * + * Its string representation is "Effect1+Effect2(p:v)". + */ +internal data class EffectList( + val effects: List +) { + override fun toString() = effects.joinToString("+") + + companion object { + val none = of() + + fun of(vararg effects: Effect) = + EffectList(effects.toList()) + } +} \ No newline at end of file diff --git a/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectParameter.kt b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectParameter.kt new file mode 100644 index 000000000..0b21abb02 --- /dev/null +++ b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectParameter.kt @@ -0,0 +1,13 @@ +package de.gleex.pltcmd.game.sound.speech.effects + +/** + * A key-value pair representing a parameter of an [Effect]. + * + * The string form of it is "key:value". + */ +internal data class EffectParameter( + val name: String, + val value: Double +) { + override fun toString() = "$name:$value" +} diff --git a/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectParameterList.kt b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectParameterList.kt new file mode 100644 index 000000000..1a1ba5196 --- /dev/null +++ b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectParameterList.kt @@ -0,0 +1,15 @@ +package de.gleex.pltcmd.game.sound.speech.effects + +/** + * A list of [EffectParameter]s. + * + * It's string representation is "effect1:value1;effect2:value2". + */ +internal data class EffectParameterList( + val parameters: List +) { + override fun toString() = parameters.joinToString(";") + companion object { + val empty = EffectParameterList(emptyList()) + } +} diff --git a/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/Effects.kt b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/Effects.kt new file mode 100644 index 000000000..51ba03931 --- /dev/null +++ b/game/sound/src/main/kotlin/de/gleex/pltcmd/game/sound/speech/effects/Effects.kt @@ -0,0 +1,123 @@ +package de.gleex.pltcmd.game.sound.speech.effects + +import marytts.signalproc.effects.* + +/** + * This object creates [Effect] model objects that can be used to set the [marytts.LocalMaryInterface.effects] string. + * + * There are actual model objects coming with Mary but they are a PITA to use. + */ +internal object Effects { + /** + * Creates an [Effect] representing [JetPilotEffect]. + */ + fun jetPilot() = effect("JetPilot") + + /** + * Creates an [Effect] representing [VolumeEffect]. + */ + fun volume(amount: Double = 2.0) = + effect("Volume", + "amount" to amount) + + /** + * Creates an [Effect] representing [VocalTractLinearScalerEffect]. + */ + fun tractScaler(amount: Double = 1.5) = + effect("TractScaler", + "amount" to amount) + + /** + * Creates an [Effect] representing [HMMF0ScaleEffect]. + */ + fun f0Scale(f0Scale: Double = 2.0) = + effect("F0Scale", + "f0Scale" to f0Scale) + + /** + * Creates an [Effect] representing [HMMF0AddEffect]. + */ + fun f0Add(f0Add: Double = 50.0) = + effect("F0Add", + "f0Add" to f0Add) + + /** + * Creates an [Effect] representing [HMMDurationScaleEffect]. + */ + fun rate(durScale: Double = 1.5) = + effect("Rate", + "durScale" to durScale) + + /** + * Creates an [Effect] representing [RobotiserEffect]. + */ + fun robot(amount: Double = 100.0) = + effect("Robot", + "amount" to amount) + + /** + * Creates an [Effect] representing [LpcWhisperiserEffect]. + */ + fun whisper(amount: Double = 100.0) = + effect("Whisper", + "amount" to amount) + + /** + * Creates an [Effect] representing [StadiumEffect]. + */ + fun stadium(amount: Double = 100.0) = + effect("Stadium", + "amount" to amount) + + /** + * Creates an [Effect] representing [FilterEffectBase]. + * + * The possible values for [type] are: + * - [FilterEffectBase.NULL_FILTER] + * - [FilterEffectBase.LOWPASS_FILTER] + * - [FilterEffectBase.HIGHPASS_FILTER] + * - [FilterEffectBase.BANDPASS_FILTER] (default) + * - [FilterEffectBase.BANDREJECT_FILTER] + * + */ + fun firFilter( + type: Double = 3.0, + fc1: Double = 500.0, + fc2: Double = 2000.0, + ) = + effect("FIRFilter", + "type" to type, + "fc1" to fc1, + "fc2" to fc2) + + /** + * Creates an [Effect] representing [ChorusEffectBase]. + */ + fun chorus( + delay1: Double = 466.0, + amp1: Double = 0.54, + delay2: Double = 600.0, + amp2: Double = -0.10, + delay3: Double = 250.0, + amp3: Double = 0.30 + ) = + effect("Chorus", + "delay1" to delay1, + "amp1" to amp1, + "delay2" to delay2, + "amp2" to amp2, + "delay3" to delay3, + "amp3" to amp3) + + /** + * Simple builder method to create an [Effect] from a name and some key-value pairs as parameters. + */ + private fun effect(name: String, vararg parameters: Pair): Effect { + return Effect( + name, + EffectParameterList( + parameters.map { (parameterName, parameterValue) -> + EffectParameter(parameterName, parameterValue) + })) + } +} \ No newline at end of file diff --git a/game/sound/src/test/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectsTest.kt b/game/sound/src/test/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectsTest.kt new file mode 100644 index 000000000..408ba2ee6 --- /dev/null +++ b/game/sound/src/test/kotlin/de/gleex/pltcmd/game/sound/speech/effects/EffectsTest.kt @@ -0,0 +1,71 @@ +package de.gleex.pltcmd.game.sound.speech.effects + +import io.kotest.core.spec.style.WordSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.matchers.shouldBe +import marytts.signalproc.effects.* + +class EffectsTest: WordSpec({ + "The toString representation of our effects" should { + forAll( + row( + Effects.jetPilot(), + JetPilotEffect()), + row( + Effects.chorus(199.0, 0.12, 244.0, 0.44, 91.0, 9.1), + ChorusEffectBase() + .apply { setParams("delay1:199;amp1:0.12;delay2:244;amp2:0.44;delay3:91;amp3:9.1") } + ), + row( + Effects.rate(19.99999), + HMMDurationScaleEffect() + .apply { setParams("durScale:19.99999") } + ), + row( + Effects.f0Add(-42.01), + HMMF0AddEffect() + .apply { setParams("f0Add:-42.01") } + ), + row( + Effects.f0Scale(2.0), + HMMF0ScaleEffect() + .apply { setParams("f0Scale:2.0") } + ), + row( + Effects.firFilter(2.0, 420.000, 1337.101), + FilterEffectBase(420.0, 1337.101, 16000, 2) + .apply { setParams("type:2;fc1:420.0;fc2:1337.101") } + ), + row( + Effects.robot(99.1), + RobotiserEffect() + .apply { setParams("amount:99.1") } + ), + row( + Effects.stadium(100.0), + StadiumEffect() + .apply { setParams("amount:100.0") } + ), + row( + Effects.volume(1.0000000000000000000001), + VolumeEffect() + .apply { setParams("amount:1.0000000000000000000001") } + ), + row( + Effects.whisper(1234.5677), + LpcWhisperiserEffect() + .apply { setParams("amount:1234.5677") } + ), + row( + Effects.tractScaler(1.51), + VocalTractLinearScalerEffect() + .apply { setParams("amount:1.51") } + ) + ) { effect, expectedEffect -> + "be equal to the original for effect $effect" { + effect.toString() shouldBe expectedEffect.fullEffectAsString + } + } + } +}) \ No newline at end of file diff --git a/game/ui/pom.xml b/game/ui/pom.xml index 71e8b9738..6ed342c61 100644 --- a/game/ui/pom.xml +++ b/game/ui/pom.xml @@ -27,6 +27,10 @@ de.gleex.pltcmd.game options + + de.gleex.pltcmd.game + sound + de.gleex.pltcmd.model mapgeneration diff --git a/game/ui/src/main/kotlin/de/gleex/pltcmd/game/ui/views/GameView.kt b/game/ui/src/main/kotlin/de/gleex/pltcmd/game/ui/views/GameView.kt index f1457f0f2..ab80ebce2 100644 --- a/game/ui/src/main/kotlin/de/gleex/pltcmd/game/ui/views/GameView.kt +++ b/game/ui/src/main/kotlin/de/gleex/pltcmd/game/ui/views/GameView.kt @@ -7,6 +7,7 @@ import de.gleex.pltcmd.game.options.UiOptions import de.gleex.pltcmd.game.options.UiOptions.MAP_VIEW_HEIGHT import de.gleex.pltcmd.game.options.UiOptions.WINDOW_HEIGHT import de.gleex.pltcmd.game.options.UiOptions.WINDOW_WIDTH +import de.gleex.pltcmd.game.sound.speech.Speaker import de.gleex.pltcmd.game.ticks.Ticker import de.gleex.pltcmd.game.ui.components.CustomComponent import de.gleex.pltcmd.game.ui.components.InfoSidebar @@ -18,6 +19,8 @@ import de.gleex.pltcmd.model.radio.communication.transmissions.decoding.isOpenin import de.gleex.pltcmd.model.radio.subscribeToBroadcasts import de.gleex.pltcmd.model.world.Sector import de.gleex.pltcmd.util.events.globalEventBus +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.hexworks.cobalt.logging.api.LoggerFactory import org.hexworks.zircon.api.ComponentDecorations import org.hexworks.zircon.api.Components @@ -120,12 +123,17 @@ class GameView( private fun LogArea.logRadioCalls() { globalEventBus.subscribeToBroadcasts { event: BroadcastEvent -> - val transmission = event.transmission - val message = "${Ticker.currentTimeString.value}: ${transmission.message}" - if (transmission.isOpening) { - addHeader(message, false) - } else { - addParagraph(message, false, 5) + runBlocking { + val transmission = event.transmission + val message = "${Ticker.currentTimeString.value}: ${transmission.message}" + launch { + Speaker.say(transmission.message) + } + if (transmission.isOpening) { + addHeader(message, false) + } else { + addParagraph(message, false, 5) + } } } } diff --git a/pom.xml b/pom.xml index 33565f123..9f1fa2004 100644 --- a/pom.xml +++ b/pom.xml @@ -1,218 +1,231 @@ - - 4.0.0 + + 4.0.0 - de.gleex.pltcmd - pltcmd - pom - 0.2.0-SNAPSHOT + de.gleex.pltcmd + pltcmd + pom + 0.2.0-SNAPSHOT - PltCmd - PltCmd is a strategy game that puts you in control of a whole army with nothing but a radio at your hands. + PltCmd + PltCmd is a strategy game that puts you in control of a whole army with nothing but a radio at your + hands. + - - scm:git:https://github.com/Baret/pltcmd.git - scm:git:git@github.com:Baret/pltcmd.git - https://github.com/Baret/pltcmd - pltcmd-0.1.0 - + + scm:git:https://github.com/Baret/pltcmd.git + scm:git:git@github.com:Baret/pltcmd.git + https://github.com/Baret/pltcmd + pltcmd-0.1.0 + - - util - model - game + + util + model + game - - UTF-8 + + UTF-8 - 1.8 - 1.8 + 1.8 + 1.8 - 1.8 - 1.4.20 - - 1.3.9 + 1.8 + 1.4.20 + + 1.3.9 - 2020.2.0-RELEASE - 2020.1.2-RELEASE - - 2020.0.19-PREVIEW + 2020.2.0-RELEASE + 2020.1.2-RELEASE + + 2020.0.19-PREVIEW - 5.7.0-M1 - 4.2.5 - + 5.7.0-M1 + 4.2.5 + - - - - kotlinx - kotlinx - https://kotlin.bintray.com/kotlinx/ - - false - - - + + + jcenter + https://jcenter.bintray.com + + + + kotlinx + kotlinx + https://kotlin.bintray.com/kotlinx/ + + false + + + - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - ${kotlin.version} - - - org.hexworks.cobalt - cobalt.core-jvm - ${cobalt.version} - - - org.junit.jupiter - junit-jupiter-api - ${jupiter.version} - - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - io.kotest - kotest-runner-junit5-jvm - ${kotest.version} - test - - - io.kotest - kotest-framework-api-jvm - ${kotest.version} - test - - - io.kotest - kotest-assertions-core-jvm - ${kotest.version} - test - - - io.kotest - kotest-property-jvm - ${kotest.version} - test - - - io.kotest - kotest-assertions-arrow-jvm - ${kotest.version} - test - - - io.mockk - mockk - 1.10.0 - test - - + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.hexworks.cobalt + cobalt.core-jvm + ${cobalt.version} + + + org.junit.jupiter + junit-jupiter-api + ${jupiter.version} + + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + io.kotest + kotest-runner-junit5-jvm + ${kotest.version} + test + + + commons-io + commons-io + + + + + io.kotest + kotest-framework-api-jvm + ${kotest.version} + test + + + io.kotest + kotest-assertions-core-jvm + ${kotest.version} + test + + + io.kotest + kotest-property-jvm + ${kotest.version} + test + + + io.kotest + kotest-assertions-arrow-jvm + ${kotest.version} + test + + + io.mockk + mockk + 1.10.0 + test + + - - - - org.jetbrains.kotlin - kotlin-stdlib-common - ${kotlin.version} - - - org.jetbrains.kotlin - kotlin-reflect - ${kotlin.version} - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - ${kotlinx.coroutines.version} - - - org.hexworks.zircon - zircon.core-jvm - ${zircon.version} - - - org.hexworks.zircon - zircon.jvm.swing - ${zircon.version} - - - org.hexworks.amethyst - amethyst.core-jvm - ${amethyst.version} - - - + + + + org.jetbrains.kotlin + kotlin-stdlib-common + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx.coroutines.version} + + + org.hexworks.zircon + zircon.core-jvm + ${zircon.version} + + + org.hexworks.zircon + zircon.jvm.swing + ${zircon.version} + + + org.hexworks.amethyst + amethyst.core-jvm + ${amethyst.version} + + + - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - - compile - - - - test-compile - - test-compile - - - - - - -Xopt-in=kotlin.RequiresOptIn - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M4 - - - org.apache.maven.plugins - maven-enforcer-plugin - 3.0.0-M3 - - - enforce-upper-bound-deps - - enforce - - - - - - - - - - - org.apache.maven.plugins - maven-release-plugin - 3.0.0-M1 - - true - - install - - - - + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + + + -Xopt-in=kotlin.RequiresOptIn + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-upper-bound-deps + + enforce + + + + + + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.0.0-M1 + + true + + install + + + +