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
+
+
+
+