Music controller with fade-over and mod capabilities. (#5273)

* Music controller with fade-over and mod capabilities.
- Preparation for music following game situations
- Minimal in-game hooks for now
- Already allows mods providing music, will play randomly

* Music controller - template
This commit is contained in:
SomeTroglodyte
2021-09-22 08:35:33 +02:00
committed by GitHub
parent b7467d3467
commit 5e4aff90e9
11 changed files with 575 additions and 40 deletions

View File

@ -67,7 +67,6 @@ class MainMenuScreen: CameraStageBaseScreen() {
// If we were in a mod, some of the resource images for the background map we're creating
// will not exist unless we reset the ruleset and images
ImageGetter.ruleset = RulesetCache.getBaseRuleset()
//ImageGetter.refreshAtlas()
thread(name = "ShowMapBackground") {
val newMap = MapGenerator(RulesetCache.getBaseRuleset())

View File

@ -4,7 +4,6 @@ import com.badlogic.gdx.Application
import com.badlogic.gdx.Game
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.audio.Music
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.utils.Align
import com.unciv.logic.GameInfo
@ -15,6 +14,7 @@ import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.tilesets.TileSetCache
import com.unciv.models.translations.Translations
import com.unciv.ui.LanguagePickerScreen
import com.unciv.ui.audio.MusicController
import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.PlayerReadyScreen
import com.unciv.ui.worldscreen.WorldScreen
@ -37,6 +37,8 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
fun isGameInfoInitialized() = this::gameInfo.isInitialized
lateinit var settings: GameSettings
lateinit var crashController: CrashController
lateinit var musicController: MusicController
/**
* This exists so that when debugging we can see the entire map.
* Remember to turn this to false before commit and upload!
@ -57,8 +59,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
lateinit var worldScreen: WorldScreen
var music: Music? = null
val musicLocation = "music/thatched-villagers.mp3"
var isInitialized = false
@ -86,6 +86,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
*/
settings = GameSaver.getGeneralSettings() // needed for the screen
screen = LoadingScreen() // NOT dependent on any atlas or skin
musicController = MusicController() // early, but at this point does only copy volume from settings
ImageGetter.resetAtlases()
ImageGetter.setNewRuleset(ImageGetter.ruleset) // This needs to come after the settings, since we may have default visual mods
@ -110,11 +111,10 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
// This stuff needs to run on the main thread because it needs the GL context
Gdx.app.postRunnable {
musicController.chooseTrack()
ImageGetter.ruleset = RulesetCache.getBaseRuleset() // so that we can enter the map editor without having to load a game first
thread(name="Music") { startMusic() }
if (settings.isFreshlyCreated) {
setScreen(LanguagePickerScreen())
} else { setScreen(MainMenuScreen()) }
@ -127,6 +127,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
fun loadGame(gameInfo: GameInfo) {
this.gameInfo = gameInfo
ImageGetter.setNewRuleset(gameInfo.ruleSet)
musicController.setModList(gameInfo.gameParameters.mods)
Gdx.input.inputProcessor = null // Since we will set the world screen when we're ready,
if (gameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1 && !gameInfo.gameParameters.isOnlineMultiplayer)
setScreen(PlayerReadyScreen(gameInfo, gameInfo.getPlayerToViewAs()))
@ -136,18 +137,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
}
}
fun startMusic() {
if (settings.musicVolume < 0.01) return
val musicFile = Gdx.files.local(musicLocation)
if (musicFile.exists()) {
music = Gdx.audio.newMusic(musicFile)
music!!.isLooping = true
music!!.volume = 0.4f * settings.musicVolume
music!!.play()
}
}
fun setScreen(screen: CameraStageBaseScreen) {
Gdx.input.inputProcessor = screen.stage
super.setScreen(screen)
@ -178,13 +167,14 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
override fun dispose() {
cancelDiscordEvent?.invoke()
Sounds.clearCache()
if (::musicController.isInitialized) musicController.shutdown()
// Log still running threads (on desktop that should be only this one and "DestroyJavaVM")
val numThreads = Thread.activeCount()
val threadList = Array(numThreads) { _ -> Thread() }
Thread.enumerate(threadList)
if (isGameInfoInitialized()){
if (isGameInfoInitialized()) {
val autoSaveThread = threadList.firstOrNull { it.name == "Autosave" }
if (autoSaveThread != null && autoSaveThread.isAlive) {
// auto save is already in progress (e.g. started by onPause() event)
@ -217,4 +207,3 @@ private class LoadingScreen : CameraStageBaseScreen() {
stage.addActor(happinessImage)
}
}

View File

@ -23,8 +23,11 @@ class GameSettings {
var tutorialsShown = HashSet<String>()
var tutorialTasksCompleted = HashSet<String>()
var hasCrashedRecently = false
var soundEffectsVolume = 0.5f
var musicVolume = 0.5f
var pauseBetweenTracks = 10
var turnsBetweenAutosaves = 1
var tileSet: String = "FantasyHex"
var showTutorials: Boolean = true

View File

@ -0,0 +1,335 @@
package com.unciv.ui.audio
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Files.FileType
import com.badlogic.gdx.files.FileHandle
import com.unciv.UncivGame
import com.unciv.models.metadata.GameSettings
import com.unciv.ui.worldscreen.mainmenu.DropBox
import java.util.*
import kotlin.concurrent.timer
/**
* Play, choose, fade-in/out and generally manage music track playback.
*
* Main methods: [chooseTrack], [pause], [resume], [setModList], [isPlaying], [gracefulShutdown]
*/
class MusicController {
companion object {
/** Mods live in Local - but this file prepares for music living in External just in case */
private val musicLocation = FileType.Local
const val musicPath = "music"
const val modPath = "mods"
const val musicFallbackLocation = "music/thatched-villagers.mp3"
const val maxVolume = 0.6f // baseVolume has range 0.0-1.0, which is multiplied by this for the API
private const val ticksPerSecond = 20 // Timer frequency defines smoothness of fade-in/out
private const val timerPeriod = 1000L / ticksPerSecond
const val defaultFadingStep = 0.08f // this means fade is 12 ticks or 0.6 seconds
private const val musicHistorySize = 8 // number of names to keep to avoid playing the same in short succession
private val fileExtensions = listOf("mp3", "ogg") // flac, opus, m4a... blocked by Gdx, `wav` we don't want
internal const val consoleLog = false
private fun getFile(path: String) =
if (musicLocation == FileType.External && Gdx.files.isExternalStorageAvailable)
Gdx.files.external(path)
else Gdx.files.local(path)
}
//region Fields
/** mirrors [GameSettings.musicVolume] - use [setVolume] to update */
var baseVolume: Float = UncivGame.Current.settings.musicVolume
private set
/** Pause in seconds between tracks unless [chooseTrack] is called to force a track change */
var silenceLength: Float
get() = silenceLengthInTicks.toFloat() / ticksPerSecond
set(value) { silenceLengthInTicks = (ticksPerSecond * value).toInt() }
private var silenceLengthInTicks = UncivGame.Current.settings.pauseBetweenTracks * ticksPerSecond
private var mods = HashSet<String>()
private var state = ControllerState.Idle
private var ticksOfSilence: Int = 0
private var musicTimer: Timer? = null
private enum class ControllerState {
/** As the name says. Timer will stop itself if it encounters this state. */
Idle,
/** Play a track to its end, then silence for a while, then choose another track */
Playing,
/** Play a track to its end, then go [Idle] */
PlaySingle,
/** Wait for a while in silence to start next track */
Silence,
/** Music fades to pause or is paused. Continue with chooseTrack or resume. */
Pause,
/** Fade out then stop */
Shutdown
}
/** Simple two-entry only queue, for smooth fade-overs from one track to another */
var current: MusicTrackController? = null
var next: MusicTrackController? = null
/** Keeps paths of recently played track to reduce repetition */
private val musicHistory = ArrayDeque<String>(musicHistorySize)
//endregion
//region Pure functions
/** @return the path of the playing track or null if none playing */
fun currentlyPlaying() = if (state != ControllerState.Playing && state != ControllerState.PlaySingle) null
else musicHistory.peekLast()
/**
* Determines whether any music tracks are available for the options menu
*/
fun isMusicAvailable() = getAllMusicFiles().any()
/** @return `true` if there's a current music track and if it's actively playing */
fun isPlaying(): Boolean {
return current?.isPlaying() == true
}
//endregion
//region Internal helpers
private fun clearCurrent() {
current?.clear()
current = null
}
private fun musicTimerTask() {
// This ticks [ticksPerSecond] times per second
when (state) {
ControllerState.Playing, ControllerState.PlaySingle ->
if (current == null) {
if (next == null) {
// no music to play - begin silence or shut down
ticksOfSilence = 0
state = if (state == ControllerState.PlaySingle) ControllerState.Shutdown else ControllerState.Silence
} else if (next!!.state.canPlay) {
// Next track - if top slot empty and a next exists, move it to top and start
current = next
next = null
if (!current!!.play())
state = ControllerState.Shutdown
} // else wait for the thread of next.load() to finish
} else if (!current!!.isPlaying()) {
// normal end of track
clearCurrent()
// rest handled next tick
} else {
if (current?.timerTick() == MusicTrackController.State.Idle)
clearCurrent()
next?.timerTick()
}
ControllerState.Silence ->
if (++ticksOfSilence > silenceLengthInTicks) {
ticksOfSilence = 0
chooseTrack()
}
ControllerState.Shutdown, ControllerState.Idle -> {
state = ControllerState.Idle
shutdown()
}
ControllerState.Pause ->
current?.timerTick()
}
}
/** Get sequence of potential music locations */
private fun getMusicFolders() = sequence {
yieldAll(
(UncivGame.Current.settings.visualMods + mods).asSequence()
.map { getFile(modPath).child(it).child(musicPath) }
)
yield(getFile(musicPath))
}
/** Get sequence of all existing music files */
private fun getAllMusicFiles() = getMusicFolders()
.filter { it.exists() && it.isDirectory }
.flatMap { it.list().asSequence() }
// ensure only normal files with common sound extension
.filter { it.exists() && !it.isDirectory && it.extension() in fileExtensions }
/** Choose adequate entry from [getAllMusicFiles] */
private fun chooseFile(prefix: String, suffix: String, flags: EnumSet<MusicTrackChooserFlags>): FileHandle? {
if (flags.contains(MusicTrackChooserFlags.PlayDefaultFile))
return getFile(musicFallbackLocation)
// Scan whole music folder and mods to find best match for desired prefix and/or suffix
// get a path list (as strings) of music folder candidates - existence unchecked
return getAllMusicFiles()
.filter {
(!flags.contains(MusicTrackChooserFlags.PrefixMustMatch) || it.nameWithoutExtension().startsWith(prefix))
&& (!flags.contains(MusicTrackChooserFlags.SuffixMustMatch) || it.nameWithoutExtension().endsWith(suffix))
}
// sort them by prefix match / suffix match / not last played / random
.sortedWith(compareBy(
{ if (it.nameWithoutExtension().startsWith(prefix)) 0 else 1 }
, { if (it.nameWithoutExtension().endsWith(suffix)) 0 else 1 }
, { if (it.path() in musicHistory) 1 else 0 }
, { Random().nextInt() }))
// Then just pick the first one. Not as wasteful as it looks - need to check all names anyway
.firstOrNull()
}
//endregion
//region State changing methods
/** This tells the music controller about active mods - all are allowed to provide tracks */
fun setModList ( newMods: HashSet<String> ) {
//todo: Ensure this gets updated where appropriate.
// loadGame; newGame: Choose Map with Mods?; map editor...
// check against "ImageGetter.ruleset=" ?
mods = newMods
}
/**
* Chooses and plays a music track using an adaptable approach - for details see the wiki.
* Called without parameters it will choose a new ambient music track and start playing it with fade-in/out.
* Will do nothing when no music files exist.
*
* @param prefix file name prefix, meant to represent **Context** - in most cases a Civ name or default "Ambient"
* @param suffix file name suffix, meant to represent **Mood** - e.g. Peace, War, Theme...
* @param flags a set of optional flags to tune the choice and playback.
* @return `true` = success, `false` = no match, no playback change
*/
fun chooseTrack (
prefix: String = "",
suffix: String = "",
flags: EnumSet<MusicTrackChooserFlags> = EnumSet.noneOf(MusicTrackChooserFlags::class.java)
): Boolean {
val musicFile = chooseFile(prefix, suffix, flags)
if (musicFile == null) {
// MustMatch flags at work or Music folder empty
if (consoleLog)
println("No music found for prefix=$prefix, suffix=$suffix, flags=$flags")
return false
}
if (musicFile.path() == currentlyPlaying())
return true // picked file already playing
if (!musicFile.exists())
return false // Safety check - nothing to play found?
next?.clear()
next = MusicTrackController(baseVolume * maxVolume)
next!!.load(musicFile, onError = {
ticksOfSilence = 0
state = ControllerState.Silence // will retry after one silence period
next = null
}, onSuccess = {
if (consoleLog)
println("Music queued: ${musicFile.path()} for prefix=$prefix, suffix=$suffix, flags=$flags")
if (musicHistory.size >= musicHistorySize) musicHistory.removeFirst()
musicHistory.addLast(musicFile.path())
val fadingStep = defaultFadingStep / (if (flags.contains(MusicTrackChooserFlags.SlowFade)) 5 else 1)
it.startFade(MusicTrackController.State.FadeIn, fadingStep)
when (state) {
ControllerState.Playing, ControllerState.PlaySingle ->
current?.startFade(MusicTrackController.State.FadeOut, fadingStep)
ControllerState.Pause ->
if (current?.state == MusicTrackController.State.Idle) clearCurrent()
else -> Unit
}
})
// Yes while the loader is doing its thing we wait for it in a Playing state
state = if (flags.contains(MusicTrackChooserFlags.PlaySingle)) ControllerState.PlaySingle else ControllerState.Playing
// Start background TimerTask which manages track changes
if (musicTimer == null)
musicTimer = timer("MusicTimer", true, 0, timerPeriod) {
musicTimerTask()
}
return true
}
/**
* Pause playback with fade-out
*
* @param speedFactor accelerate (>1) or slow down (<1) the fade-out. Clamped to 1/1000..1000.
*/
fun pause(speedFactor: Float = 1f) {
if ((state != ControllerState.Playing && state != ControllerState.PlaySingle) || current == null) return
val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f)
current!!.startFade(MusicTrackController.State.FadeOut, fadingStep)
state = ControllerState.Pause
}
/**
* Resume playback with fade-in - from a pause will resume where playback left off,
* otherwise it will start a new ambient track choice.
*
* @param speedFactor accelerate (>1) or slow down (<1) the fade-in. Clamped to 1/1000..1000.
*/
fun resume(speedFactor: Float = 1f) {
if (state == ControllerState.Pause && current != null) {
val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f)
current!!.startFade(MusicTrackController.State.FadeIn, fadingStep)
state = ControllerState.Playing // this may circumvent a PlaySingle, but, currently only the main menu resumes, and then it's perfect
current!!.play()
} else if (state == ControllerState.Idle) {
chooseTrack()
}
}
/** Fade out then shutdown with a given [duration] in seconds */
fun fadeoutToSilence(duration: Float = 4.0f) {
val fadingStep = 1f / ticksPerSecond / duration
current?.startFade(MusicTrackController.State.FadeOut, fadingStep)
next?.startFade(MusicTrackController.State.FadeOut, fadingStep)
state = ControllerState.Shutdown
}
/** Update playback volume, to be called from options popup */
fun setVolume(volume: Float) {
baseVolume = volume
if ( volume < 0.01 ) shutdown()
else if (isPlaying()) current!!.setVolume(baseVolume * maxVolume)
}
/** Soft shutdown of music playback, with fadeout */
fun gracefulShutdown() {
if (state == ControllerState.Idle) shutdown()
else state = ControllerState.Shutdown
}
/** Forceful shutdown of music playback and timers - see [gracefulShutdown] */
fun shutdown() {
state = ControllerState.Idle
if (musicTimer != null) {
musicTimer!!.cancel()
musicTimer = null
}
if (next != null) {
next!!.clear()
next = null
}
if (current != null) {
current!!.clear()
current = null
}
musicHistory.clear()
if (consoleLog)
println("MusicController shut down.")
}
fun downloadDefaultFile() {
val file = DropBox.downloadFile(musicFallbackLocation)
getFile(musicFallbackLocation).write(file, false)
}
//endregion
}

View File

@ -0,0 +1,14 @@
package com.unciv.ui.audio
enum class MusicTrackChooserFlags {
/** Makes prefix parameter a mandatory match */
PrefixMustMatch,
/** Makes suffix parameter a mandatory match */
SuffixMustMatch,
/** Extends fade duration by factor 5 */
SlowFade,
/** Lets music controller shut down after track ends instead of choosing a random next track */
PlaySingle,
/** directly choose the 'fallback' file for playback */
PlayDefaultFile,
}

View File

@ -0,0 +1,152 @@
package com.unciv.ui.audio
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.audio.Music
import com.badlogic.gdx.files.FileHandle
import kotlin.concurrent.thread
/** Wraps one Gdx Music instance and manages threaded loading, playback, fading and cleanup */
class MusicTrackController(private var volume: Float) {
/** Internal state of this Music track */
enum class State(val canPlay: Boolean) {
None(false),
Loading(false),
Idle(true),
FadeIn(true),
Playing(true),
FadeOut(true),
Error(false)
}
var state = State.None
private set
var music: Music? = null
private set
private var loaderThread: Thread? = null
private var fadeStep = MusicController.defaultFadingStep
private var fadeVolume: Float = 1f
/** Clean up and dispose resources */
fun clear() {
state = State.None
clearLoader()
clearMusic()
}
private fun clearLoader() {
if (loaderThread == null) return
loaderThread!!.interrupt()
loaderThread = null
}
private fun clearMusic() {
if (music == null) return
music!!.stop()
music!!.dispose()
music = null
}
/** Loads [file] into this controller's [music] and optionally calls [onSuccess] when done.
* Failures are silently logged to console, and [onError] is called.
* Callbacks run on the background thread.
* @throws IllegalStateException if called in the wrong state (fresh or cleared instance only)
*/
fun load(
file: FileHandle,
onError: ((MusicTrackController)->Unit)? = null,
onSuccess: ((MusicTrackController)->Unit)? = null
) {
if (state != State.None || loaderThread != null || music != null)
throw IllegalStateException("MusicTrackController.load should only be called once")
loaderThread = thread(name = "MusicLoader") {
state = State.Loading
try {
music = Gdx.audio.newMusic(file)
if (state != State.Loading) { // in case clear was called in the meantime
clearMusic()
} else {
state = State.Idle
if (MusicController.consoleLog)
println("Music loaded: $file")
onSuccess?.invoke(this)
}
} catch (ex: Exception) {
println("Exception loading $file: ${ex.message}")
if (MusicController.consoleLog)
ex.printStackTrace()
state = State.Error
onError?.invoke(this)
}
loaderThread = null
}
}
/** Called by the [MusicController] in its timer "tick" event handler, implements fading */
fun timerTick(): State {
if (state == State.FadeIn) fadeInStep()
if (state == State.FadeOut) fadeOutStep()
return state
}
private fun fadeInStep() {
// fade-in: linearly ramp fadeVolume to 1.0, then continue playing
fadeVolume += fadeStep
if (fadeVolume < 1f && music != null && music!!.isPlaying) {
music!!.volume = volume * fadeVolume
return
}
music!!.volume = volume
fadeVolume = 1f
state = State.Playing
}
private fun fadeOutStep() {
// fade-out: linearly ramp fadeVolume to 0.0, then act according to Status (Playing->Silence/Pause/Shutdown)
fadeVolume -= fadeStep
if (fadeVolume >= 0.001f && music != null && music!!.isPlaying) {
music!!.volume = volume * fadeVolume
return
}
fadeVolume = 0f
state = State.Idle
}
/** Starts fadeIn or fadeOut.
*
* Note this does _not_ set the current fade "percentage" to allow smoothly changing direction mid-fade
* @param step Overrides current fade step only if >0
*/
fun startFade(fade: State, step: Float = 0f) {
if (!state.canPlay) return
if (fadeStep > 0f) fadeStep = step
state = fade
}
/** @return [Music.isPlaying] (Gdx music stream is playing) unless [state] says it won't make sense */
fun isPlaying() = state.canPlay && music?.isPlaying == true
/** Calls play() on the wrapped Gdx Music, catching exceptions to console.
* @return success
* @throws IllegalStateException if called on uninitialized instance
*/
fun play(): Boolean {
if (!state.canPlay || music == null) {
throw IllegalStateException("MusicTrackController.play called on uninitialized instance")
}
return try {
music!!.volume = volume
if (!music!!.isPlaying) // for fade-over this could be called by the end of the previous track
music!!.play()
true
} catch (ex: Exception) {
println("Exception playing music: ${ex.message}")
if (MusicController.consoleLog)
ex.printStackTrace()
state = State.Error
false
}
}
/** Adjust master volume without affecting a fade-in/out */
fun setVolume(newVolume: Float) {
volume = newVolume
music?.volume = volume * fadeVolume
}
}

View File

@ -170,6 +170,7 @@ class GameOptionsTable(
ruleset.modOptions = newRuleset.modOptions
ImageGetter.setNewRuleset(ruleset)
UncivGame.Current.musicController.setModList(gameParameters.mods)
}
fun getModCheckboxes(isPortrait: Boolean = false): Table {

View File

@ -226,6 +226,7 @@ class NewGameScreen(
ruleset.clear()
ruleset.add(RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods))
ImageGetter.setNewRuleset(ruleset)
game.musicController.setModList(gameSetupInfo.gameParameters.mods)
}
fun lockTables() {

View File

@ -21,6 +21,8 @@ import com.unciv.models.tilesets.TileSetCache
import com.unciv.models.translations.TranslationFileWriter
import com.unciv.models.translations.Translations
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicController
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.civilopedia.FormattedLine
import com.unciv.ui.civilopedia.MarkupRenderer
import com.unciv.ui.civilopedia.SimpleCivilopediaText
@ -30,6 +32,8 @@ import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import com.unciv.ui.worldscreen.WorldScreen
import java.util.*
import kotlin.concurrent.thread
import kotlin.math.floor
import kotlin.math.roundToInt
import com.badlogic.gdx.utils.Array as GdxArray
/**
@ -207,11 +211,12 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
addSoundEffectsVolumeSlider()
val musicLocation = Gdx.files.local(previousScreen.game.musicLocation)
if (musicLocation.exists())
if (previousScreen.game.musicController.isMusicAvailable()) {
addMusicVolumeSlider()
else
addDownloadMusic(musicLocation)
addMusicPauseSlider()
} else {
addDownloadMusic()
}
}
private fun getMultiplayerTab(): Table = Table(CameraStageBaseScreen.skin).apply {
@ -247,7 +252,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
addYesNoRow("Enable portrait orientation", settings.allowAndroidPortrait) {
settings.allowAndroidPortrait = it
// Note the following might close the options screen indirectly and delayed
previousScreen.game.limitOrientationsHelper!!.allowPortrait(it)
previousScreen.game.limitOrientationsHelper.allowPortrait(it)
}
}
@ -392,7 +397,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
private fun Table.addSoundEffectsVolumeSlider() {
add("Sound effects volume".tr()).left().fillX()
val soundEffectsVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
val soundEffectsVolumeSlider = UncivSlider(0f, 1.0f, 0.05f,
initial = settings.soundEffectsVolume
) {
settings.soundEffectsVolume = it
@ -404,24 +409,55 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
private fun Table.addMusicVolumeSlider() {
add("Music volume".tr()).left().fillX()
val musicVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
val musicVolumeSlider = UncivSlider(0f, 1.0f, 0.05f,
initial = settings.musicVolume,
sound = UncivSound.Silent
) {
settings.musicVolume = it
settings.save()
val music = previousScreen.game.music
if (music == null) // restart music, if it was off at the app start
thread(name = "Music") { previousScreen.game.startMusic() }
music?.volume = 0.4f * it
val music = previousScreen.game.musicController
music.setVolume(it)
if (!music.isPlaying())
music.chooseTrack(flags = EnumSet.of(MusicTrackChooserFlags.PlayDefaultFile, MusicTrackChooserFlags.PlaySingle))
}
musicVolumeSlider.value = settings.musicVolume
add(musicVolumeSlider).pad(5f).row()
}
private fun Table.addDownloadMusic(musicLocation: FileHandle) {
private fun Table.addMusicPauseSlider() {
val music = previousScreen.game.musicController
// map to/from 0-1-2..10-12-14..30-35-40..60-75-90-105-120
fun posToLength(pos: Float): Float = when (pos) {
in 0f..10f -> pos
in 11f..20f -> pos * 2f - 10f
in 21f..26f -> pos * 5f - 70f
else -> pos * 15f - 330f
}
fun lengthToPos(length: Float): Float = floor(when (length) {
in 0f..10f -> length
in 11f..30f -> (length + 10f) / 2f
in 31f..60f -> (length + 10f) / 5f
else -> (length + 330f) / 15f
})
val getTipText: (Float)->String = {
"%.0f".format(posToLength(it))
}
add("Pause between tracks".tr()).left().fillX()
val pauseLengthSlider = UncivSlider(0f, 30f, 1f,
initial = lengthToPos(music.silenceLength),
sound = UncivSound.Silent,
getTipText = getTipText
) {
music.silenceLength = posToLength(it)
settings.pauseBetweenTracks = music.silenceLength.toInt()
}
add(pauseLengthSlider).pad(5f).row()
}
private fun Table.addDownloadMusic() {
val downloadMusicButton = "Download music".toTextButton()
add(downloadMusicButton).colspan(2).row()
val errorTable = Table()
@ -435,11 +471,10 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
// So the whole game doesn't get stuck while downloading the file
thread(name = "Music") {
try {
val file = DropBox.downloadFile("/Music/thatched-villagers.mp3")
musicLocation.write(file, false)
previousScreen.game.musicController.downloadDefaultFile()
Gdx.app.postRunnable {
tabs.replacePage("Sound", getSoundTab())
previousScreen.game.startMusic()
previousScreen.game.musicController.chooseTrack(flags = EnumSet.of(MusicTrackChooserFlags.PlayDefaultFile, MusicTrackChooserFlags.PlaySingle))
}
} catch (ex: Exception) {
Gdx.app.postRunnable {

View File

@ -14,6 +14,8 @@ import com.unciv.ui.worldscreen.WorldScreen
class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
init {
defaults().fillX()
worldScreen.game.musicController.pause()
addButton("Main menu") { worldScreen.game.setScreen(MainMenuScreen()) }
addButton("Civilopedia") { worldScreen.game.setScreen(CivilopediaScreen(worldScreen.gameInfo.ruleSet)) }
addButton("Save game") { worldScreen.game.setScreen(SaveGameScreen(worldScreen.gameInfo)) }
@ -30,8 +32,11 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
addButton("Options") { worldScreen.openOptionsPopup() }
addButton("Community") {
close()
WorldScreenCommunityPopup(worldScreen).open(force = true) }
addCloseButton()
WorldScreenCommunityPopup(worldScreen).open(force = true)
}
addCloseButton {
worldScreen.game.musicController.resume()
}
pack()
}
}