mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-11 11:04:05 +09:00
638 lines
23 KiB
Kotlin
638 lines
23 KiB
Kotlin
package net.torvald.terrarum.audio
|
|
|
|
import com.badlogic.gdx.Gdx
|
|
import com.badlogic.gdx.Input
|
|
import com.badlogic.gdx.backends.lwjgl3.audio.Lwjgl3Audio
|
|
import com.badlogic.gdx.utils.Disposable
|
|
import com.jme3.math.FastMath
|
|
import net.torvald.spriteanimation.AssembledSpriteAnimation
|
|
import net.torvald.terrarum.*
|
|
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE
|
|
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATED
|
|
import net.torvald.terrarum.audio.dsp.*
|
|
import net.torvald.terrarum.concurrent.ThreadExecutor
|
|
import net.torvald.terrarum.gameactors.ActorWithBody
|
|
import java.lang.Thread.MAX_PRIORITY
|
|
import java.util.*
|
|
import kotlin.math.*
|
|
|
|
/**
|
|
* Any audio reference fed into this manager will get lost; you must manually store and dispose of them on your own.
|
|
*
|
|
* Created by minjaesong on 2023-11-07.
|
|
*/
|
|
class AudioMixer : Disposable {
|
|
|
|
companion object {
|
|
const val SPEED_OF_SOUND_AIR = 340f
|
|
const val SPEED_OF_SOUND_WATER = 1480f
|
|
const val SPEED_OF_SOUND = 340f
|
|
|
|
const val DEFAULT_FADEOUT_LEN = 1.8
|
|
|
|
internal const val DS_FLTIDX_PAN = 2
|
|
internal const val DS_FLTIDX_LOW = 3
|
|
}
|
|
|
|
|
|
val masterVolume: Double
|
|
get() = App.getConfigDouble("mastervolume")
|
|
|
|
val musicVolume: Double
|
|
get() = App.getConfigDouble("bgmvolume")
|
|
|
|
val ambientVolume: Double
|
|
get() = App.getConfigDouble("ambientvolume")
|
|
|
|
val sfxVolume: Double
|
|
get() = App.getConfigDouble("sfxvolume")
|
|
|
|
val guiVolume: Double
|
|
get() = App.getConfigDouble("guivolume")
|
|
|
|
val dynamicSourceCount: Int
|
|
get() = App.getConfigInt("audio_dynamic_source_max")
|
|
|
|
val tracks = Array(12) { TerrarumAudioMixerTrack(
|
|
if (it == 0) "Music"
|
|
else if (it == 1) "Amb1"
|
|
else if (it == 2) "Amb2"
|
|
else if (it == 3) "AMB1+2"
|
|
else if (it == 4) "Amb3"
|
|
else if (it == 5) "Amb4"
|
|
else if (it == 6) "GUI"
|
|
else if (it == 7) "\u00E4SFX"
|
|
else if (it == 8) "\u00F0 \u00E4 \u00F0" // summation
|
|
else if (it == 9) "\u00D9Open\u00D9" // convolution1
|
|
else if (it == 10) "\u00D9Cave\u00D9" // convolution2
|
|
else if (it == 11) "\u00F0 \u00DA \u00F0" // fade
|
|
else "Trk${it+1}", trackType = if (it >= 6 || it == 3) TrackType.BUS else TrackType.STATIC_SOURCE, maxVolumeFun = {
|
|
when (it) {
|
|
0 -> { musicVolume }
|
|
4 -> { ambientVolume }
|
|
2 -> { sfxVolume }
|
|
3 -> { guiVolume }
|
|
else -> { 1.0 }
|
|
}
|
|
}
|
|
) }
|
|
|
|
val dynamicTracks = Array(dynamicSourceCount) { TerrarumAudioMixerTrack(
|
|
"DS${(it + 1).toString().padStart(3, '0')}",
|
|
TrackType.DYNAMIC_SOURCE
|
|
) }
|
|
|
|
val masterTrack = TerrarumAudioMixerTrack("\u00DBMASTER", TrackType.MASTER) { masterVolume }
|
|
|
|
val musicTrack: TerrarumAudioMixerTrack
|
|
get() = tracks[0]
|
|
val ambientTrack1: TerrarumAudioMixerTrack
|
|
get() = tracks[1]
|
|
val ambientTrack2: TerrarumAudioMixerTrack
|
|
get() = tracks[2]
|
|
val amb1plus2: TerrarumAudioMixerTrack
|
|
get() = tracks[3]
|
|
val ambientTrack3: TerrarumAudioMixerTrack
|
|
get() = tracks[4]
|
|
val ambientTrack4: TerrarumAudioMixerTrack
|
|
get() = tracks[5]
|
|
|
|
val guiTrack: TerrarumAudioMixerTrack
|
|
get() = tracks[6]
|
|
|
|
val sfxSumBus: TerrarumAudioMixerTrack
|
|
get() = tracks[7]
|
|
val sumBus: TerrarumAudioMixerTrack
|
|
get() = tracks[8]
|
|
val convolveBusOpen: TerrarumAudioMixerTrack
|
|
get() = tracks[9]
|
|
val convolveBusCave: TerrarumAudioMixerTrack
|
|
get() = tracks[10]
|
|
val fadeBus: TerrarumAudioMixerTrack
|
|
get() = tracks[11]
|
|
|
|
val ambientTracks = arrayOf(
|
|
ambientTrack1, ambientTrack2, ambientTrack3, ambientTrack4
|
|
)
|
|
|
|
val guiTracks = Array(4) { TerrarumAudioMixerTrack("GUI${it+1}", TrackType.STATIC_SOURCE) }
|
|
|
|
var processing = false
|
|
|
|
var actorNowPlaying = Terrarum.ingame?.actorNowPlaying; private set
|
|
|
|
fun getFreeGuiTrackNoMatterWhat(): TerrarumAudioMixerTrack {
|
|
synchronized(this) {
|
|
return getFreeGuiTrack() ?: guiTracks.minByOrNull { it.playStartedTime }.also { it!!.checkedOutTime = System.nanoTime() }!!
|
|
}
|
|
}
|
|
|
|
fun getFreeGuiTrack(): TerrarumAudioMixerTrack? {
|
|
synchronized(this) {
|
|
return guiTracks.minByOrNull { maxOf(it.checkedOutTime, it.playStartedTime) }
|
|
.also {
|
|
it?.checkedOutTime = System.nanoTime()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return oldest dynamic track, even if the track is currently playing
|
|
*/
|
|
fun getFreeTrackNoMatterWhat(): TerrarumAudioMixerTrack {
|
|
synchronized(this) {
|
|
return getFreeTrack() ?: dynamicTracks.minByOrNull { it.playStartedTime }.also { it!!.checkedOutTime = System.nanoTime() }!!
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return oldest dynamic track that is not playing
|
|
*/
|
|
fun getFreeTrack(): TerrarumAudioMixerTrack? {
|
|
synchronized(this) {
|
|
return dynamicTracks.filter { it.trackingTarget == null && !it.isPlaying }
|
|
.minByOrNull { maxOf(it.checkedOutTime, it.playStartedTime) }
|
|
.also {
|
|
it?.checkedOutTime = System.nanoTime()
|
|
}
|
|
}
|
|
}
|
|
|
|
var listenerHeadSize = BinoPan.EARDIST_DEFAULT; private set
|
|
|
|
private fun setHeadSize(actor: ActorWithBody?): Float {
|
|
val scale = actor?.scale?.toFloat() ?: 1f
|
|
val headSize0 = if (actor?.sprite is AssembledSpriteAnimation)
|
|
(actor.sprite as AssembledSpriteAnimation).headSizeInMeter
|
|
else null
|
|
|
|
return (headSize0 ?: 0f).times(scale).coerceAtLeast(BinoPan.EARDIST_DEFAULT)
|
|
}
|
|
|
|
private val millisecUnitTime = 100L // 48 * p, p is multiplied to compensate the time takes for writing samples
|
|
private val sleepMS = App.audioBufferSize / millisecUnitTime
|
|
private val sleepNS = (App.audioBufferSize / millisecUnitTime * 1000000).toInt() % 1000000
|
|
|
|
fun createProcessingThread(): Thread = Thread {
|
|
// serial precessing
|
|
while (processing) {
|
|
actorNowPlaying = Terrarum.ingame?.actorNowPlaying
|
|
listenerHeadSize = setHeadSize(actorNowPlaying)
|
|
|
|
// process
|
|
dynamicTracks.forEach {
|
|
if (!it.processor.paused) {
|
|
try { it.processor.run() }
|
|
catch (e: Throwable) { e.printStackTrace() }
|
|
}
|
|
}
|
|
guiTracks.forEach {
|
|
if (!it.processor.paused) {
|
|
try { it.processor.run() }
|
|
catch (e: Throwable) { e.printStackTrace() }
|
|
}
|
|
}
|
|
tracks.forEach {
|
|
if (!it.processor.paused) {
|
|
try { it.processor.run() }
|
|
catch (e: Throwable) { e.printStackTrace() }
|
|
}
|
|
}
|
|
masterTrack.processor.run()
|
|
|
|
while (processing && !masterTrack.pcmQueue.isEmpty) {
|
|
masterTrack.adev!!.writeSamples(masterTrack.pcmQueue.removeFirst()) // it blocks until the queue is consumed
|
|
}
|
|
|
|
Thread.sleep(sleepMS, sleepNS)
|
|
}
|
|
|
|
// parallel processing, it seems even on the 7950X this is less efficient than serial processing...
|
|
/*while (processing) {
|
|
actorNowPlaying = Terrarum.ingame?.actorNowPlaying
|
|
listenerHeadSize = setHeadSize(actorNowPlaying)
|
|
|
|
for (tracks in parallelProcessingSchedule) {
|
|
if (!processing) break
|
|
|
|
val callables = tracks.map { Callable {
|
|
if (!it.processor.paused) {
|
|
try { it.processor.run() }
|
|
catch (_: InterruptedException) {}
|
|
catch (e: Throwable) { e.printStackTrace() }
|
|
}
|
|
} }
|
|
|
|
try {
|
|
processingExecutor.renew()
|
|
processingExecutor.submitAll(callables)
|
|
processingExecutor.join()
|
|
}
|
|
catch (_: InterruptedException) {}
|
|
catch (e: Throwable) { e.printStackTrace() }
|
|
}
|
|
|
|
while (processing && !masterTrack.pcmQueue.isEmpty) {
|
|
masterTrack.adev!!.writeSamples(masterTrack.pcmQueue.removeFirst()) // it blocks until the queue is consumed
|
|
}
|
|
}*/
|
|
}.also {
|
|
it.priority = MAX_PRIORITY // higher = more predictable; audio delay is very noticeable so it gets high priority
|
|
}
|
|
|
|
private val processingExecutor = ThreadExecutor()
|
|
lateinit var processingThread: Thread
|
|
|
|
// val parallelProcessingSchedule: Array<Array<TerrarumAudioMixerTrack>>
|
|
|
|
|
|
init {
|
|
// initialise audio paths //
|
|
|
|
listOf(musicTrack, ambientTrack1, ambientTrack2, ambientTrack3, ambientTrack4, guiTrack).forEach {
|
|
it.filters[0] = PreGain(1f)
|
|
}
|
|
|
|
guiTracks.forEach {
|
|
guiTrack.addSidechainInput(it, 1.0)
|
|
it.filters[0] = BinoPan(0f)
|
|
}
|
|
|
|
masterTrack.filters[0] = SoftClp
|
|
masterTrack.filters[1] = Buffer
|
|
masterTrack.filters[2] = Vecto(1.4142f)
|
|
masterTrack.filters[3] = Spectro()
|
|
|
|
musicTrack.filters[1] = Vecto()
|
|
musicTrack.filters[2] = Spectro()
|
|
ambientTracks.forEach {
|
|
it.filters[1] = Vecto(decibelsToFullscale(24.0).toFloat())
|
|
it.filters[2] = Spectro()
|
|
}
|
|
|
|
amb1plus2.addSidechainInput(ambientTrack1, 1.0)
|
|
amb1plus2.addSidechainInput(ambientTrack2, 1.0)
|
|
|
|
sfxSumBus.filters[1] = Vecto(0.7071f)
|
|
sfxSumBus.filters[2] = Spectro()
|
|
|
|
listOf(sumBus, convolveBusOpen, convolveBusCave).forEach {
|
|
it.addSidechainInput(musicTrack, 1.0)
|
|
it.addSidechainInput(amb1plus2, 1.0)
|
|
it.addSidechainInput(ambientTrack3, 1.0)
|
|
it.addSidechainInput(ambientTrack4, 1.0)
|
|
it.addSidechainInput(sfxSumBus, 1.0)
|
|
}
|
|
|
|
convolveBusOpen.filters[1] = Convolv("basegame", "audio/convolution/EchoThief - PurgatoryChasm.bin", decibelsToFullscale(-6.0).toFloat())
|
|
convolveBusOpen.filters[2] = PreGain(decibelsToFullscale(17.0).toFloat()) // don't make it too loud; it'll sound like a shit
|
|
convolveBusOpen.volume = 0.5 // will be controlled by the other updater which surveys the world
|
|
|
|
convolveBusCave.filters[1] = Convolv("basegame", "audio/convolution/EchoThief - WaterplacePark-trimmed.bin", decibelsToFullscale(-3.0).toFloat())
|
|
convolveBusCave.filters[2] = PreGain(decibelsToFullscale(16.0).toFloat())
|
|
convolveBusCave.volume = 0.5 // will be controlled by the other updater which surveys the world
|
|
|
|
fadeBus.addSidechainInput(sumBus, 1.0 / 3.0)
|
|
fadeBus.addSidechainInput(convolveBusOpen, 2.0 / 3.0)
|
|
fadeBus.addSidechainInput(convolveBusCave, 2.0 / 3.0)
|
|
fadeBus.filters[0] = Lowpass(SAMPLING_RATE / 2f)
|
|
|
|
masterTrack.addSidechainInput(fadeBus, 1.0)
|
|
masterTrack.addSidechainInput(guiTrack, 1.0)
|
|
|
|
|
|
dynamicTracks.forEach {
|
|
it.filters[DS_FLTIDX_PAN] = BinoPan(0f)
|
|
it.filters[DS_FLTIDX_LOW] = Lowpass(SAMPLING_RATE / 2f)
|
|
sfxSumBus.addSidechainInput(it, 1.0)
|
|
}
|
|
|
|
// unused for now
|
|
/*parallelProcessingSchedule =
|
|
arrayOf(musicTrack, ambientTrack, guiTrack).sliceEvenly(THREAD_COUNT / 2).toTypedArray() +
|
|
dynamicTracks.sliceEvenly(THREAD_COUNT / 2).toTypedArray() +
|
|
guiTracks +
|
|
arrayOf(sfxSumBus, sumBus, convolveBusOpen, convolveBusCave).sliceEvenly(THREAD_COUNT / 2).toTypedArray() +
|
|
arrayOf(fadeBus) +
|
|
arrayOf(masterTrack)*/
|
|
|
|
processingThread = createProcessingThread()
|
|
processing = true
|
|
processingThread.start()
|
|
// feedingThread.priority = MAX_PRIORITY
|
|
// feedingThread.start()
|
|
}
|
|
|
|
|
|
data class FadeRequest(
|
|
var fadeAkku: Double = 0.0,
|
|
var fadeLength: Double = DEFAULT_FADEOUT_LEN,
|
|
var fadeoutFired: Boolean = false,
|
|
var fadeinFired: Boolean = false,
|
|
var fadeTarget: Double = 0.0,
|
|
var fadeStart: Double = 0.0,
|
|
var callback: () -> Unit = {},
|
|
)
|
|
|
|
private val fadeReqs = HashMap<TerrarumAudioMixerTrack, FadeRequest>().also { map ->
|
|
listOf(musicTrack, ambientTrack1, ambientTrack2, guiTrack, amb1plus2, fadeBus).forEach {
|
|
map[it] = FadeRequest()
|
|
}
|
|
}
|
|
|
|
private var lpAkku = 0.0
|
|
private var lpLength = 0.4
|
|
private var lpOutFired = false
|
|
private var lpInFired = false
|
|
private var lpStart = SAMPLING_RATED / 2.0
|
|
private var lpTarget = SAMPLING_RATED / 2.0
|
|
|
|
private var testAudioMixRatio = 0.0
|
|
|
|
private var muteLatched = false
|
|
|
|
fun update(delta: Float) {
|
|
// enable manual mixer on BuildingMaker
|
|
if (Terrarum.ingame?.javaClass?.canonicalName == "net.torvald.terrarum.modulebasegame.BuildingMaker") {
|
|
val mixDelta = if (testAudioMixRatio >= 0.0) 0.001 else (0.001 * MaterialCodex["AIIR"].sondrefl).absoluteValue
|
|
|
|
if (Gdx.input.isKeyPressed(Input.Keys.UP))
|
|
testAudioMixRatio += mixDelta
|
|
if (Gdx.input.isKeyPressed(Input.Keys.DOWN))
|
|
testAudioMixRatio -= mixDelta
|
|
if (Gdx.input.isKeyPressed(Input.Keys.NUM_1))
|
|
testAudioMixRatio = -1.0
|
|
if (Gdx.input.isKeyPressed(Input.Keys.NUM_2))
|
|
testAudioMixRatio = 0.0
|
|
if (Gdx.input.isKeyPressed(Input.Keys.NUM_3))
|
|
testAudioMixRatio = 1.0
|
|
if (!muteLatched && Gdx.input.isKeyPressed(Input.Keys.NUM_4)) {
|
|
sumBus.volume = 1.0 - sumBus.volume
|
|
muteLatched = true
|
|
}
|
|
else if (!Gdx.input.isKeyPressed(Input.Keys.NUM_4))
|
|
muteLatched = false
|
|
|
|
testAudioMixRatio = testAudioMixRatio.coerceIn(MaterialCodex["AIIR"].sondrefl.absoluteValue * -1.0, 1.0)
|
|
|
|
if (testAudioMixRatio >= 0.0) {
|
|
val ratio1 = testAudioMixRatio.coerceIn(0.0, 1.0)
|
|
convolveBusCave.volume = ratio1
|
|
convolveBusOpen.volume = 1.0 - ratio1
|
|
}
|
|
else {
|
|
val ratio1 = (testAudioMixRatio / MaterialCodex["AIIR"].sondrefl).absoluteValue.coerceIn(0.0, 1.0)
|
|
convolveBusOpen.volume = (1.0 - ratio1).pow(0.75)
|
|
convolveBusCave.volume = 0.0
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// the real updates
|
|
(Gdx.audio as? Lwjgl3Audio)?.update()
|
|
masterTrack.volume = masterVolume
|
|
musicTrack.getFilter<PreGain>().gain = musicVolume.toFloat() * 0.5f
|
|
ambientTracks.forEach {
|
|
it.getFilter<PreGain>().gain = ambientVolume.toFloat()
|
|
}
|
|
sfxSumBus.volume = sfxVolume
|
|
guiTrack.getFilter<PreGain>().gain = guiVolume.toFloat()
|
|
|
|
|
|
// process fades
|
|
fadeReqs.entries.forEach { val track = it.key; val req = it.value
|
|
if (req.fadeoutFired) {
|
|
req.fadeAkku += delta
|
|
val step = req.fadeAkku / req.fadeLength
|
|
track.volume = FastMath.interpolateLinear(step, req.fadeStart, req.fadeTarget)
|
|
|
|
if (req.fadeAkku >= req.fadeLength) {
|
|
req.fadeoutFired = false
|
|
track.volume = req.fadeTarget
|
|
|
|
// stop streaming if the track or the fader track is muted
|
|
if (req.fadeTarget == 0.0 && (track == musicTrack || track == fadeBus)) {
|
|
musicTrack.stop()
|
|
musicTrack.currentTrack = null
|
|
}
|
|
/*ambientTracks.forEach {
|
|
if (req.fadeTarget == 0.0 && (track == it || track == fadeBus)) {
|
|
it.stop()
|
|
it.currentTrack = null
|
|
}
|
|
}*/
|
|
req.callback()
|
|
}
|
|
}
|
|
else if (req.fadeinFired) {
|
|
req.fadeAkku += delta
|
|
val step = req.fadeAkku / req.fadeLength
|
|
track.volume = FastMath.interpolateLinear(step, req.fadeStart, req.fadeTarget)
|
|
|
|
if (req.fadeAkku >= req.fadeLength) {
|
|
track.volume = req.fadeTarget
|
|
req.fadeinFired = false
|
|
}
|
|
|
|
req.callback
|
|
}
|
|
}
|
|
|
|
|
|
if (lpOutFired) {
|
|
lpAkku += delta
|
|
val cutoff = linPercToLog(lpAkku / lpLength, lpStart, lpTarget)
|
|
fadeBus.getFilter<Lowpass>().setCutoff(cutoff)
|
|
|
|
if (lpAkku >= lpLength) {
|
|
lpOutFired = false
|
|
fadeBus.getFilter<Lowpass>().setCutoff(lpTarget)
|
|
}
|
|
}
|
|
else if (lpInFired) {
|
|
lpAkku += delta
|
|
val cutoff = linPercToLog(lpAkku / lpLength, lpStart, lpTarget)
|
|
fadeBus.getFilter<Lowpass>().setCutoff(cutoff)
|
|
|
|
if (lpAkku >= lpLength) {
|
|
fadeBus.getFilter<Lowpass>().setCutoff(lpTarget)
|
|
lpInFired = false
|
|
}
|
|
}
|
|
|
|
|
|
if (!musicTrack.isPlaying && musicTrack.nextTrack != null) {
|
|
musicTrack.queueNext(null)
|
|
fadeBus.volume = 1.0
|
|
musicTrack.volume = 1.0
|
|
musicTrack.play()
|
|
}
|
|
|
|
ambientTracks.forEach {
|
|
if (!it.isPlaying && it.nextTrack != null) {
|
|
it.queueNext(null)
|
|
if (ambientStopped) {
|
|
requestFadeIn(it, DEFAULT_FADEOUT_LEN * 4, 1.0, 0.00001)
|
|
}
|
|
it.play()
|
|
ambientStopped = false
|
|
}
|
|
}
|
|
}
|
|
|
|
private var ambientStopped = true
|
|
|
|
/**
|
|
* Don't call this function from the application (fixture, user-made music player mod, etc.) directly!
|
|
* Use transactions from the MusicService.
|
|
*/
|
|
internal fun startMusic(song: AudioBank) {
|
|
if (musicTrack.isPlaying) {
|
|
requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN)
|
|
}
|
|
musicTrack.nextTrack = song
|
|
}
|
|
|
|
/**
|
|
* Don't call this function from the application (fixture, user-made music player mod, etc.) directly!
|
|
* Use transactions from the MusicService.
|
|
*/
|
|
internal fun stopMusic() {
|
|
requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN)
|
|
}
|
|
|
|
internal fun startAmb(song: AudioBank) {
|
|
val ambientTrack = if (!ambientTrack1.streamPlaying.get())
|
|
ambientTrack1
|
|
else if (!ambientTrack2.streamPlaying.get())
|
|
ambientTrack2
|
|
else if (ambientTrack1.playStartedTime < ambientTrack2.playStartedTime)
|
|
ambientTrack1
|
|
else
|
|
ambientTrack2
|
|
|
|
if (ambientTrack.isPlaying == true) {
|
|
requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN)
|
|
}
|
|
ambientTrack.nextTrack = song
|
|
// fade will be processed by the update()
|
|
}
|
|
|
|
internal fun startAmb1(song: AudioBank) {
|
|
if (ambientTrack1.isPlaying == true) {
|
|
requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN)
|
|
}
|
|
ambientTrack1.nextTrack = song
|
|
// fade will be processed by the update()
|
|
}
|
|
|
|
internal fun startAmb2(song: AudioBank) {
|
|
if (ambientTrack2.isPlaying == true) {
|
|
requestFadeOut(musicTrack, DEFAULT_FADEOUT_LEN)
|
|
}
|
|
ambientTrack2.nextTrack = song
|
|
// fade will be processed by the update()
|
|
}
|
|
|
|
/**
|
|
* Preferably, audio apps should NOT call this function directly to change music, [MusicService] must be used
|
|
* to control the music playback instead.
|
|
*/
|
|
internal fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double = DEFAULT_FADEOUT_LEN, target: Double = 0.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) {
|
|
val req = fadeReqs[track]!!
|
|
if (!req.fadeoutFired) {
|
|
req.fadeLength = length.coerceAtLeast(1.0/1024.0)
|
|
req.fadeAkku = 0.0
|
|
req.fadeoutFired = true
|
|
req.fadeTarget = target * track.maxVolume
|
|
req.fadeStart = source ?: fadeBus.volume
|
|
req.callback = jobAfterFadeout
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preferably, audio apps should NOT call this function directly to change music, [MusicService] must be used
|
|
* to control the music playback instead.
|
|
*/
|
|
internal fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) {
|
|
// printdbg(this, "fadein called by")
|
|
// printStackTrace(this)
|
|
|
|
val req = fadeReqs[track]!!
|
|
if (!req.fadeinFired) {
|
|
req.fadeLength = length.coerceAtLeast(1.0/1024.0)
|
|
req.fadeAkku = 0.0
|
|
req.fadeinFired = true
|
|
req.fadeTarget = target * track.maxVolume
|
|
req.fadeStart = source ?: fadeBus.volume
|
|
req.callback = jobAfterFadeout
|
|
}
|
|
}
|
|
|
|
|
|
fun requestLowpassOut(length: Double) {
|
|
if (!lpOutFired) {
|
|
lpLength = length.coerceAtLeast(1.0/1024.0)
|
|
lpAkku = 0.0
|
|
lpOutFired = true
|
|
lpStart = fadeBus.getFilter<Lowpass>().cutoff
|
|
lpTarget = SAMPLING_RATED / 2.0
|
|
}
|
|
}
|
|
|
|
fun requestLowpassIn(length: Double) {
|
|
if (!lpInFired) {
|
|
lpLength = length.coerceAtLeast(1.0/1024.0)
|
|
lpAkku = 0.0
|
|
lpInFired = true
|
|
lpStart = fadeBus.getFilter<Lowpass>().cutoff
|
|
lpTarget = SAMPLING_RATED / 100.0
|
|
}
|
|
}
|
|
|
|
internal fun reset() {
|
|
ambientStopped = true
|
|
dynamicTracks.forEach { it.stop() }
|
|
guiTracks.forEach { it.stop() }
|
|
tracks.filter { it.trackType == TrackType.STATIC_SOURCE }.forEach { it.stop() }
|
|
tracks.forEach {
|
|
it.processor.purgeBuffer()
|
|
}
|
|
fadeBus.getFilter<Lowpass>().setCutoff(TerrarumAudioMixerTrack.SAMPLING_RATEF / 2)
|
|
// give some time for the cave bus to decay before ramping the volume up
|
|
Timer().schedule(object : TimerTask() {
|
|
override fun run() {
|
|
fadeBus.volume = 1.0
|
|
}
|
|
}, 500L)
|
|
}
|
|
|
|
override fun dispose() {
|
|
processingExecutor.killAll()
|
|
// processingSubthreads.forEach { it.interrupt() }
|
|
processing = false
|
|
processingThread.interrupt()
|
|
// feeder.stop()
|
|
// feedingThread.join()
|
|
tracks.forEach { it.tryDispose() }
|
|
dynamicTracks.forEach { it.tryDispose() }
|
|
guiTracks.forEach { it.tryDispose() }
|
|
masterTrack.tryDispose()
|
|
}
|
|
}
|
|
|
|
fun linToLogPerc(value: Double, low: Double, high: Double): Double {
|
|
// https://www.desmos.com/calculator/dmhve2awxm
|
|
val b = -ln(low / high)
|
|
val a = low
|
|
return ln(value / a) / b
|
|
}
|
|
|
|
fun linPercToLog(perc: Double, low: Double, high: Double): Double {
|
|
// https://www.desmos.com/calculator/dmhve2awxm
|
|
val t = perc.coerceIn(0.0, 1.0)
|
|
val b = -ln(low / high)
|
|
val a = low
|
|
return a * exp(b * t)
|
|
} |