mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-11 11:28:03 +07:00
Fade in and out for City Ambiance Sounds (#10230)
* Extend MusicController to allow one extra 'overlay' track * Reduce CityAmbiencePlayer to a proxy for MusicController's 'overlay' * Treat all the LongLine complaints
This commit is contained in:
parent
a4e3617037
commit
5541407a3a
@ -1,72 +1,24 @@
|
||||
package com.unciv.ui.audio
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.audio.Music
|
||||
import com.badlogic.gdx.files.FileHandle
|
||||
import com.badlogic.gdx.utils.Disposable
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.utils.Log
|
||||
|
||||
/** Must be [disposed][dispose]. Starts playing an ambience sound for the city when created. Stops playing the ambience sound when [disposed][dispose]. */
|
||||
/** Must be [disposed][dispose].
|
||||
* Starts playing an ambience sound for the city when created.
|
||||
* Stops playing the ambience sound when [disposed][dispose]. */
|
||||
class CityAmbiencePlayer(
|
||||
city: City
|
||||
) : Disposable {
|
||||
private var playingCitySound: Music? = null
|
||||
|
||||
init {
|
||||
play(city)
|
||||
}
|
||||
|
||||
private fun getModsFolder(): FileHandle {
|
||||
val path = "mods"
|
||||
val internal = Gdx.files.internal(path)
|
||||
if (internal.exists()) return internal
|
||||
return Gdx.files.local(path)
|
||||
}
|
||||
|
||||
private fun getModSoundFolders(): Sequence<FileHandle> {
|
||||
val visualMods = UncivGame.Current.settings.visualMods
|
||||
val mods = UncivGame.Current.gameInfo!!.gameParameters.getModsAndBaseRuleset()
|
||||
return (visualMods + mods).asSequence()
|
||||
.map { modName ->
|
||||
getModsFolder()
|
||||
.child(modName)
|
||||
.child("sounds")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSoundFile(fileName: String): FileHandle {
|
||||
val fileFromMods = getModSoundFolders()
|
||||
.filter { it.isDirectory }
|
||||
.flatMap { it.list().asSequence() }
|
||||
.filter { !it.isDirectory && it.extension() in MusicController.gdxSupportedFileExtensions }
|
||||
.firstOrNull { it.nameWithoutExtension() == fileName }
|
||||
|
||||
return fileFromMods ?: Gdx.files.internal("sounds/$fileName.ogg")
|
||||
}
|
||||
|
||||
private fun play(city: City) {
|
||||
if (UncivGame.Current.settings.citySoundsVolume == 0f) return
|
||||
|
||||
if (playingCitySound != null)
|
||||
stop()
|
||||
try {
|
||||
playingCitySound = Gdx.audio.newMusic(getSoundFile(city.civ.getEra().citySound))
|
||||
playingCitySound?.volume = UncivGame.Current.settings.citySoundsVolume
|
||||
playingCitySound?.isLooping = true
|
||||
playingCitySound?.play()
|
||||
} catch (ex: Throwable) {
|
||||
playingCitySound?.dispose()
|
||||
Log.error("Error while playing city sound: ", ex)
|
||||
val volume = UncivGame.Current.settings.citySoundsVolume
|
||||
if (volume > 0f) {
|
||||
UncivGame.Current.musicController
|
||||
.playOverlay("sounds", city.civ.getEra().citySound, volume)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stop() {
|
||||
playingCitySound?.dispose()
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
stop()
|
||||
UncivGame.Current.musicController.stopOverlay()
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import com.unciv.logic.multiplayer.storage.DropBox
|
||||
import com.unciv.models.metadata.GameSettings
|
||||
import com.unciv.utils.Concurrency
|
||||
import com.unciv.utils.Log
|
||||
import java.io.File
|
||||
import java.util.EnumSet
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.thread
|
||||
@ -20,6 +21,10 @@ import kotlin.math.roundToInt
|
||||
* Play, choose, fade-in/out and generally manage music track playback.
|
||||
*
|
||||
* Main methods: [chooseTrack], [pause], [resume], [setModList], [isPlaying], [gracefulShutdown]
|
||||
*
|
||||
* City ambience feature: [playOverlay], [stopOverlay]
|
||||
* * This plays entirely independent of all other functionality as linked above.
|
||||
* * Can load from internal (jar,apk) - music is always local, nothing is packaged into a release.
|
||||
*/
|
||||
class MusicController {
|
||||
companion object {
|
||||
@ -27,16 +32,24 @@ class MusicController {
|
||||
private val musicLocation = FileType.Local
|
||||
private const val musicPath = "music"
|
||||
private const val modPath = "mods"
|
||||
private const val musicFallbackLocation = "/music/thatched-villagers.mp3" // Dropbox path
|
||||
private const val musicFallbackLocalName = "music/Thatched Villagers - Ambient.mp3" // Name we save it to
|
||||
private const val maxVolume = 0.6f // baseVolume has range 0.0-1.0, which is multiplied by this for the API
|
||||
private const val ticksPerSecondGdx = 58.3f // *Observed* frequency of Gdx app loop
|
||||
private const val ticksPerSecondOwn = 20f // Timer frequency when we use our own
|
||||
private const val defaultFadeDuration = 0.9f // in seconds
|
||||
/** Dropbox path of default download offer */
|
||||
private const val musicFallbackLocation = "/music/thatched-villagers.mp3"
|
||||
/** Name we save the default download offer to */
|
||||
private const val musicFallbackLocalName = "music/Thatched Villagers - Ambient.mp3"
|
||||
/** baseVolume has range 0.0-1.0, which is multiplied by this for the API */
|
||||
private const val maxVolume = 0.6f
|
||||
/** *Observed* frequency of Gdx app loop - theoretically this should reach 60fps */
|
||||
private const val ticksPerSecondGdx = 58.3f
|
||||
/** Timer frequency when we use our own */
|
||||
private const val ticksPerSecondOwn = 20f
|
||||
/** Default fade duration in seconds used to calculate the step per tick */
|
||||
private const val defaultFadeDuration = 0.9f
|
||||
private const val defaultFadingStepGdx = 1f / (defaultFadeDuration * ticksPerSecondGdx)
|
||||
private const val defaultFadingStepOwn = 1f / (defaultFadeDuration * ticksPerSecondOwn)
|
||||
private const val musicHistorySize = 8 // number of names to keep to avoid playing the same in short succession
|
||||
val gdxSupportedFileExtensions = listOf("mp3", "ogg", "wav") // All Gdx formats
|
||||
/** Number of names to keep, to avoid playing the same in short succession */
|
||||
private const val musicHistorySize = 8
|
||||
/** All Gdx-supported sound formats (file extensions) */
|
||||
val gdxSupportedFileExtensions = listOf("mp3", "ogg", "wav")
|
||||
|
||||
private fun getFile(path: String) =
|
||||
if (musicLocation == FileType.External && Gdx.files.isExternalStorageAvailable)
|
||||
@ -75,9 +88,13 @@ class MusicController {
|
||||
if (fileName.isEmpty())
|
||||
return MusicTrackInfo("", "", "")
|
||||
val fileNameParts = fileName.split('/')
|
||||
val modName = if (fileNameParts.size > 1 && fileNameParts[0] == "mods") fileNameParts[1] else ""
|
||||
var trackName = fileNameParts[if (fileNameParts.size > 3 && fileNameParts[2] == "music") 3 else 1]
|
||||
val type = gdxSupportedFileExtensions.firstOrNull {trackName.endsWith(".$it") } ?: ""
|
||||
val modName = if (fileNameParts.size > 1 && fileNameParts[0] == modPath)
|
||||
fileNameParts[1] else ""
|
||||
var trackName = fileNameParts[
|
||||
if (fileNameParts.size > 3 && fileNameParts[2] == musicPath) 3 else 1
|
||||
]
|
||||
val type = gdxSupportedFileExtensions
|
||||
.firstOrNull {trackName.endsWith(".$it") } ?: ""
|
||||
trackName = trackName.removeSuffix(".$type")
|
||||
return MusicTrackInfo(modName, trackName, type)
|
||||
}
|
||||
@ -93,7 +110,8 @@ class MusicController {
|
||||
get() = silenceLengthInTicks.toFloat() / ticksPerSecond
|
||||
set(value) { silenceLengthInTicks = (ticksPerSecond * value).toInt() }
|
||||
|
||||
private var silenceLengthInTicks = (UncivGame.Current.settings.pauseBetweenTracks * ticksPerSecond).roundToInt()
|
||||
private var silenceLengthInTicks =
|
||||
(UncivGame.Current.settings.pauseBetweenTracks * ticksPerSecond).roundToInt()
|
||||
|
||||
private var mods = HashSet<String>()
|
||||
|
||||
@ -106,7 +124,7 @@ class MusicController {
|
||||
private enum class ControllerState {
|
||||
/** Own timer stopped, if using the HardenedGdxAudio callback just do nothing */
|
||||
Idle,
|
||||
/** As the name says. Loop will release everything and go [Idle] if it encounters this state. */
|
||||
/** Loop will release everything and go [Idle] if it encounters this state. */
|
||||
Cleanup,
|
||||
/** Play a track to its end, then silence for a while, then choose another track */
|
||||
Playing,
|
||||
@ -124,6 +142,9 @@ class MusicController {
|
||||
private var current: MusicTrackController? = null
|
||||
private var next: MusicTrackController? = null
|
||||
|
||||
/** One entry only for 'overlay' tracks in addition to and independent of normal music */
|
||||
private var overlay: MusicTrackController? = null
|
||||
|
||||
/** Keeps paths of recently played track to reduce repetition */
|
||||
private val musicHistory = ArrayDeque<String>(musicHistorySize)
|
||||
|
||||
@ -198,7 +219,8 @@ class MusicController {
|
||||
|
||||
private fun startTimer() {
|
||||
if (!needOwnTimer || musicTimer != null) return
|
||||
// Start background TimerTask which manages track changes - on desktop, we get callbacks from the app.loop instead
|
||||
// Start background TimerTask which manages track changes and fades -
|
||||
// on desktop, we get callbacks from the app.loop instead
|
||||
val timerPeriod = (1000f / ticksPerSecond).roundToInt().toLong()
|
||||
musicTimer = timer("MusicTimer", daemon = true, period = timerPeriod ) {
|
||||
musicTimerTask()
|
||||
@ -213,6 +235,9 @@ class MusicController {
|
||||
|
||||
private fun musicTimerTask() {
|
||||
// This ticks [ticksPerSecond] times per second. Runs on Gdx main thread in desktop only
|
||||
|
||||
overlay?.overlayTick()
|
||||
|
||||
when (state) {
|
||||
ControllerState.Idle -> return
|
||||
|
||||
@ -221,10 +246,12 @@ class MusicController {
|
||||
if (next == null) {
|
||||
// no music to play - begin silence or shut down
|
||||
ticksOfSilence = 0
|
||||
state = if (state == ControllerState.PlaySingle) ControllerState.Shutdown else ControllerState.Silence
|
||||
state = if (state == ControllerState.PlaySingle) ControllerState.Shutdown
|
||||
else ControllerState.Silence
|
||||
fireOnChange()
|
||||
} else if (next!!.state.canPlay) {
|
||||
// Next track - if top slot empty and a next exists, move it to top and start
|
||||
// Next track -
|
||||
// if top slot empty and a next exists, move it to top and start
|
||||
current = next
|
||||
next = null
|
||||
if (!current!!.play()) {
|
||||
@ -269,13 +296,14 @@ class MusicController {
|
||||
stopTimer()
|
||||
clearNext()
|
||||
clearCurrent()
|
||||
clearOverlay()
|
||||
musicHistory.clear()
|
||||
Log.debug("MusicController shut down.")
|
||||
}
|
||||
|
||||
private fun audioExceptionHandler(ex: Throwable, music: Music) {
|
||||
// Should run only in exceptional cases when the Gdx codecs actually have trouble with a file.
|
||||
// Most playback problems are caught by the similar handler in MusicTrackController
|
||||
// Should run only in exceptional cases when the Gdx codecs actually have trouble with a
|
||||
// file. Most playback problems are caught by the similar handler in MusicTrackController.
|
||||
|
||||
// Gdx _will_ try to read more data from file in Lwjgl3Application.loop even for
|
||||
// Music instances that already have thrown an exception.
|
||||
@ -283,6 +311,7 @@ class MusicController {
|
||||
music.dispose()
|
||||
if (music == next?.music) clearNext()
|
||||
if (music == current?.music) clearCurrent()
|
||||
if (music == overlay?.music) clearOverlay()
|
||||
|
||||
Log.error("Error playing music", ex)
|
||||
|
||||
@ -295,35 +324,49 @@ class MusicController {
|
||||
}
|
||||
}
|
||||
|
||||
/** Get sequence of potential music locations */
|
||||
private fun getMusicFolders() = sequence {
|
||||
/** Get sequence of potential music locations when called without parameters.
|
||||
* @param folder a folder name relative to mod/assets/local root
|
||||
* @param getDefault builds the default (not modded) `FileHandle`,
|
||||
* allows fallback to internal assets
|
||||
* @return a Sequence of `FileHandle`s describing potential existing directories
|
||||
*/
|
||||
private fun getMusicFolders(
|
||||
folder: String = musicPath,
|
||||
getDefault: () -> FileHandle = { getFile(folder) }
|
||||
) = sequence<FileHandle> {
|
||||
yieldAll(
|
||||
(UncivGame.Current.settings.visualMods + mods).asSequence()
|
||||
.map { getFile(modPath).child(it).child(musicPath) }
|
||||
.map { getFile(modPath).child(it).child(folder) }
|
||||
)
|
||||
yield(getFile(musicPath))
|
||||
}
|
||||
yield(getDefault())
|
||||
}.filter { it.exists() && it.isDirectory }
|
||||
|
||||
/** Get a 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 gdxSupportedFileExtensions }
|
||||
|
||||
/** Choose adequate entry from [getAllMusicFiles] */
|
||||
private fun chooseFile(prefix: String, suffix: String, flags: EnumSet<MusicTrackChooserFlags>): FileHandle? {
|
||||
private fun chooseFile(
|
||||
prefix: String,
|
||||
suffix: String,
|
||||
flags: EnumSet<MusicTrackChooserFlags>
|
||||
): FileHandle? {
|
||||
if (flags.contains(MusicTrackChooserFlags.PlayDefaultFile)) {
|
||||
val defaultFile = getFile(musicFallbackLocalName)
|
||||
// Test so if someone never downloaded Thatched Villagers, their volume slider will still play music
|
||||
// Test so if someone never downloaded Thatched Villagers,
|
||||
// their volume slider will still play music
|
||||
if (defaultFile.exists()) return defaultFile
|
||||
}
|
||||
// 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
|
||||
val prefixMustMatch = flags.contains(MusicTrackChooserFlags.PrefixMustMatch)
|
||||
val suffixMustMatch = flags.contains(MusicTrackChooserFlags.SuffixMustMatch)
|
||||
return getAllMusicFiles()
|
||||
.filter {
|
||||
(!flags.contains(MusicTrackChooserFlags.PrefixMustMatch) || it.nameWithoutExtension().startsWith(prefix))
|
||||
&& (!flags.contains(MusicTrackChooserFlags.SuffixMustMatch) || it.nameWithoutExtension().endsWith(suffix))
|
||||
(!prefixMustMatch || it.nameWithoutExtension().startsWith(prefix))
|
||||
&& (!suffixMustMatch || it.nameWithoutExtension().endsWith(suffix))
|
||||
}
|
||||
// randomize
|
||||
.shuffled()
|
||||
@ -332,10 +375,12 @@ class MusicController {
|
||||
{ if (it.nameWithoutExtension().startsWith(prefix)) 0 else 1 }
|
||||
, { if (it.nameWithoutExtension().endsWith(suffix)) 0 else 1 }
|
||||
, { if (it.path() in musicHistory) 1 else 0 }
|
||||
// Then just pick the first one. Not as wasteful as it looks - need to check all names anyway
|
||||
// Then just pick the first one.
|
||||
// Not as wasteful as it looks - need to check all names anyway
|
||||
)).firstOrNull()
|
||||
// Note: shuffled().sortedWith(), ***not*** .sortedWith(.., Random)
|
||||
// the latter worked with older JVM's, current ones *crash* you when a compare is not transitive.
|
||||
// the latter worked with older JVM's,
|
||||
// current ones *crash* you when a compare is not transitive.
|
||||
}
|
||||
|
||||
private fun fireOnChange() {
|
||||
@ -368,12 +413,14 @@ class MusicController {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 or the master volume is zero.
|
||||
*
|
||||
* @param prefix file name prefix, meant to represent **Context** - in most cases a Civ name
|
||||
* @param suffix file name suffix, meant to represent **Mood** - e.g. Peace, War, Theme, Defeat, Ambient
|
||||
* (Ambient is the default when a track ends and exists so War Peace and the others are not chosen in that case)
|
||||
* @param suffix file name suffix, meant to represent **Mood** -
|
||||
* e.g. Peace, War, Theme, Defeat, Ambient (Ambient is the default when
|
||||
* a track ends and exists so War Peace and the others are not chosen in that case)
|
||||
* @param flags a set of optional flags to tune the choice and playback.
|
||||
* @return `true` = success, `false` = no match, no playback change
|
||||
*/
|
||||
@ -388,10 +435,12 @@ class MusicController {
|
||||
|
||||
if (musicFile == null) {
|
||||
// MustMatch flags at work or Music folder empty
|
||||
Log.debug("No music found for prefix=%s, suffix=%s, flags=%s", prefix, suffix, flags)
|
||||
Log.debug("No music found for prefix=%s, suffix=%s, flags=%s",
|
||||
prefix, suffix, flags)
|
||||
return false
|
||||
}
|
||||
Log.debug("Track chosen: %s for prefix=%s, suffix=%s, flags=%s", musicFile.path(), prefix, suffix, flags)
|
||||
Log.debug("Track chosen: %s for prefix=%s, suffix=%s, flags=%s",
|
||||
musicFile.path(), prefix, suffix, flags)
|
||||
return startTrack(musicFile, flags)
|
||||
}
|
||||
|
||||
@ -420,10 +469,12 @@ class MusicController {
|
||||
if (musicHistory.size >= musicHistorySize) musicHistory.removeFirst()
|
||||
musicHistory.addLast(musicFile.path())
|
||||
|
||||
// This is what makes a track change fade _over_ current fading out and next fading in at the same time.
|
||||
// This is what makes a track change fade _over_:
|
||||
// current fading out and next fading in at the same time.
|
||||
it.play()
|
||||
|
||||
val fadingStep = defaultFadingStep / (if (flags.contains(MusicTrackChooserFlags.SlowFade)) 5 else 1)
|
||||
val fadingStep = defaultFadingStep /
|
||||
(if (flags.contains(MusicTrackChooserFlags.SlowFade)) 5 else 1)
|
||||
it.startFade(MusicTrackController.State.FadeIn, fadingStep)
|
||||
|
||||
when (state) {
|
||||
@ -436,7 +487,8 @@ class MusicController {
|
||||
})
|
||||
|
||||
// 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
|
||||
state = if (flags.contains(MusicTrackChooserFlags.PlaySingle)) ControllerState.PlaySingle
|
||||
else ControllerState.Playing
|
||||
startTimer()
|
||||
return true
|
||||
}
|
||||
@ -470,8 +522,8 @@ class MusicController {
|
||||
*/
|
||||
fun pause(speedFactor: Float = 1f) {
|
||||
Log.debug("MusicTrackController.pause called")
|
||||
val controller = current
|
||||
if ((state != ControllerState.Playing && state != ControllerState.PlaySingle) || controller == null) return
|
||||
val controller = current ?: return
|
||||
if (state != ControllerState.Playing && state != ControllerState.PlaySingle) return
|
||||
val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f)
|
||||
controller.startFade(MusicTrackController.State.FadeOut, fadingStep)
|
||||
if (next?.state == MusicTrackController.State.FadeIn)
|
||||
@ -490,14 +542,17 @@ class MusicController {
|
||||
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
|
||||
// this may circumvent a PlaySingle, but -
|
||||
// currently only the main menu resumes, and then it's perfect:
|
||||
state = ControllerState.Playing
|
||||
current!!.play()
|
||||
} else if (state == ControllerState.Cleanup) {
|
||||
chooseTrack()
|
||||
}
|
||||
}
|
||||
|
||||
/** Fade out then shutdown with a given [duration] in seconds, defaults to a 'slow' fade (4.5s) */
|
||||
/** Fade out then shutdown with a given [duration] in seconds,
|
||||
* defaults to a 'slow' fade (4.5s) */
|
||||
@Suppress("unused") // might be useful instead of gracefulShutdown
|
||||
fun fadeoutToSilence(duration: Float = defaultFadeDuration * 5) {
|
||||
val fadingStep = 1f / ticksPerSecond / duration
|
||||
@ -529,5 +584,53 @@ class MusicController {
|
||||
fun isDefaultFileAvailable() =
|
||||
getFile(musicFallbackLocalName).exists()
|
||||
|
||||
//endregion
|
||||
//region Overlay track
|
||||
|
||||
/** Scans all mods [folder]s for [name] with a supported sound extension,
|
||||
* with fallback to _internal_ assets [folder] */
|
||||
private fun getMatchingFiles(folder: String, name: String) =
|
||||
getMusicFolders(folder) { Gdx.files.internal(folder) }
|
||||
.flatMap {
|
||||
it.list { file: File ->
|
||||
file.nameWithoutExtension == name && file.exists() && !file.isDirectory &&
|
||||
file.extension in gdxSupportedFileExtensions
|
||||
}.asSequence()
|
||||
}
|
||||
|
||||
/** Play [name] from any mod's [folder] or internal assets,
|
||||
* fading in to [volume] then looping */
|
||||
fun playOverlay(folder: String, name: String, volume: Float) {
|
||||
val file = getMatchingFiles(folder, name).firstOrNull() ?: return
|
||||
playOverlay(file, volume)
|
||||
}
|
||||
|
||||
/** Play [file], fading in to [volume] then looping */
|
||||
@Suppress("MemberVisibilityCanBePrivate") // open to future use
|
||||
fun playOverlay(file: FileHandle, volume: Float) {
|
||||
clearOverlay()
|
||||
MusicTrackController(volume, initialFadeVolume = 0f).load(file) {
|
||||
it.music?.isLooping = true
|
||||
it.play()
|
||||
it.startFade(MusicTrackController.State.FadeIn)
|
||||
overlay = it
|
||||
}
|
||||
}
|
||||
|
||||
/** Fade out any playing overlay then clean up */
|
||||
fun stopOverlay() {
|
||||
overlay?.startFade(MusicTrackController.State.FadeOut)
|
||||
}
|
||||
|
||||
private fun MusicTrackController.overlayTick() {
|
||||
if (timerTick() == MusicTrackController.State.Idle)
|
||||
clearOverlay() // means FadeOut finished
|
||||
}
|
||||
|
||||
private fun clearOverlay() {
|
||||
overlay?.clear()
|
||||
overlay = null
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import com.unciv.utils.Log
|
||||
import com.unciv.utils.debug
|
||||
|
||||
/** Wraps one Gdx Music instance and manages loading, playback, fading and cleanup */
|
||||
internal class MusicTrackController(private var volume: Float) {
|
||||
internal class MusicTrackController(private var volume: Float, initialFadeVolume: Float = 1f) {
|
||||
|
||||
/** Internal state of this Music track */
|
||||
enum class State(val canPlay: Boolean) {
|
||||
@ -25,7 +25,7 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
var music: Music? = null
|
||||
private set
|
||||
private var fadeStep = MusicController.defaultFadingStep
|
||||
private var fadeVolume: Float = 1f
|
||||
private var fadeVolume: Float = initialFadeVolume
|
||||
|
||||
//region Functions for MusicController
|
||||
|
||||
@ -47,7 +47,9 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
onError: ((MusicTrackController)->Unit)? = null,
|
||||
onSuccess: ((MusicTrackController)->Unit)? = null
|
||||
) {
|
||||
check(state == State.None && music == null) { "MusicTrackController.load should only be called once" }
|
||||
check(state == State.None && music == null) {
|
||||
"MusicTrackController.load should only be called once"
|
||||
}
|
||||
|
||||
state = State.Loading
|
||||
try {
|
||||
@ -75,12 +77,13 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
|
||||
/** Starts fadeIn or fadeOut.
|
||||
*
|
||||
* Note this does _not_ set the current fade "percentage" to allow smoothly changing direction mid-fade
|
||||
* 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
|
||||
if (step > 0f) fadeStep = step
|
||||
state = fade
|
||||
}
|
||||
|
||||
@ -97,7 +100,8 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
return timerTick() == State.Idle
|
||||
}
|
||||
|
||||
/** @return [Music.isPlaying] (Gdx music stream is playing) unless [state] says it won't make sense */
|
||||
/** @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.
|
||||
@ -105,11 +109,14 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
* @throws IllegalStateException if called on uninitialized instance
|
||||
*/
|
||||
fun play(): Boolean {
|
||||
check(state.canPlay && music != null) { "MusicTrackController.play called on uninitialized instance" }
|
||||
check(state.canPlay && music != null) {
|
||||
"MusicTrackController.play called on uninitialized instance"
|
||||
}
|
||||
|
||||
// Unexplained observed exception: Gdx.Music.play fails with
|
||||
// "Unable to allocate audio buffers. AL Error: 40964" (AL_INVALID_OPERATION)
|
||||
// Approach: This track dies, parent controller will enter state Silence thus retry after a while.
|
||||
// Approach: This track dies, parent controller will enter state Silence thus
|
||||
// retry after a while.
|
||||
if (tryPlay(music!!)) return true
|
||||
state = State.Error
|
||||
return false
|
||||
@ -136,7 +143,8 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
state = State.Playing
|
||||
}
|
||||
private fun fadeOutStep() {
|
||||
// fade-out: linearly ramp fadeVolume to 0.0, then act according to Status (Playing->Silence/Pause/Shutdown)
|
||||
// fade-out: linearly ramp fadeVolume to 0.0, then act according to Status
|
||||
// (Playing->Silence/Pause/Shutdown)
|
||||
// This needs to guard against the music backend breaking mid-fade away during game shutdown
|
||||
fadeVolume -= fadeStep
|
||||
try {
|
||||
@ -153,9 +161,9 @@ internal class MusicTrackController(private var volume: Float) {
|
||||
|
||||
private fun tryPlay(music: Music): Boolean {
|
||||
return try {
|
||||
music.volume = volume
|
||||
if (!music.isPlaying) // for fade-over this could be called by the end of the previous track
|
||||
music.play()
|
||||
music.volume = volume * fadeVolume
|
||||
// for fade-over this could be called by the end of the previous track:
|
||||
if (!music.isPlaying) music.play()
|
||||
true
|
||||
} catch (ex: Throwable) {
|
||||
audioExceptionHandler(ex)
|
||||
|
Loading…
Reference in New Issue
Block a user