Files
Terrarum/src/net/torvald/terrarum/audio/MixerTrackProcessor.kt
minjaesong 7ed37cf355 new ore?
2024-09-12 20:53:26 +09:00

401 lines
15 KiB
Kotlin

package net.torvald.terrarum.audio
import net.torvald.terrarum.*
import net.torvald.terrarum.audio.AudioMixer.Companion.DS_FLTIDX_LOW
import net.torvald.terrarum.audio.AudioMixer.Companion.DS_FLTIDX_PAN
import net.torvald.terrarum.audio.AudioMixer.Companion.SPEED_OF_SOUND_AIR
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATE
import net.torvald.terrarum.audio.TerrarumAudioMixerTrack.Companion.SAMPLING_RATED
import net.torvald.terrarum.audio.dsp.BinoPan
import net.torvald.terrarum.audio.dsp.Lowpass
import net.torvald.terrarum.audio.dsp.NullFilter
import net.torvald.terrarum.gameactors.ActorWithBody
import net.torvald.terrarum.gameactors.ActorWithBody.Companion.GAME_TO_SI_VELO
import org.dyn4j.geometry.Vector2
import kotlin.math.absoluteValue
import kotlin.math.cosh
import kotlin.math.sqrt
/**
* Created by minjaesong on 2023-11-17.
*/
class MixerTrackProcessor(bufferSize: Int, val rate: Int, val track: TerrarumAudioMixerTrack): Runnable {
private var buffertaille = bufferSize
companion object {
fun getVolFun(x: Double): Double {
// https://www.desmos.com/calculator/blcd4s69gl
// val K = 1.225
// fun q(x: Double) = if (x >= 1.0) 0.5 else (K*x - K).pow(2.0) + 0.5
// val x2 = x.pow(q(x))
// method 1.
// https://www.desmos.com/calculator/uzbjw10lna
// val K = 512.0
// return K.pow(-sqrt(1.0+x.sqr())) * K
// method 2.
// https://www.desmos.com/calculator/3xsac66rsp
// method 3.
// comparison with method 1.
// https://www.desmos.com/calculator/rbteowef8v
val Q = 2.0
return 1.0 / cosh(Q * x).sqr()
}
}
@Volatile var running = true; private set
@Volatile var paused = false; private set
private val pauseLock = java.lang.Object()
private var emptyBuf = FloatArray(buffertaille)
internal var streamBuf: AudioProcessBuf? = null
// internal var jitterMode = 0
// internal var jitterIntensity = 0f
private var fout1 = listOf(emptyBuf, emptyBuf)
val maxSigLevel = arrayOf(0.0, 0.0)
val maxRMS = arrayOf(0.0, 0.0)
val hasClipping = arrayOf(false, false)
internal fun purgeBuffer() {
fout1.forEach { it.fill(0f) }
purgeStreamBuf()
}
private fun purgeStreamBuf() {
track.stop()
streamBuf = null
// printdbg("StreamBuf is now null")
}
private var breakBomb = false
private val distFalloff = 1600.0
private fun printdbg(msg: Any) {
if (true) App.printdbg("MixerTrackProcessor ${track.name}", msg)
}
private fun allocateStreamBuf(track: TerrarumAudioMixerTrack) {
// printdbg("Allocating a StreamBuf with rate ${track.currentTrack!!.samplingRate}")
streamBuf = AudioProcessBuf(track.currentTrack!!.samplingRate, { bufL, bufR ->
var samplesRead = track.currentTrack?.readSamples(bufL, bufR) ?: 0
// do gapless fetch if there is space in the buffer
if (track.doGaplessPlayback && samplesRead < bufL.size) {
track.currentTrack?.reset()
track.pullNextTrack()
samplesRead += read00(bufL, bufR, samplesRead)
}
samplesRead
}, { purgeStreamBuf() }).also {
// it.jitterMode = jitterMode
// it.jitterIntensity = jitterIntensity
}
}
private fun read00(bufL: FloatArray, bufR: FloatArray, samplesRead: Int): Int {
val bufSize = bufL.size - samplesRead
val tmpBufL = FloatArray(bufSize)
val tmpBufR = FloatArray(bufSize)
val newRead = track.currentTrack?.readSamples(tmpBufL, tmpBufR) ?: 0
System.arraycopy(tmpBufL, 0, bufL, samplesRead, tmpBufL.size)
System.arraycopy(tmpBufR, 0, bufR, samplesRead, tmpBufR.size)
return newRead
}
var bufEmpty = true; private set
override fun run() {
// while (running) { // uncomment to multithread
/*synchronized(pauseLock) { // uncomment to multithread
if (!running) { // may have changed while waiting to
// synchronize on pauseLock
breakBomb = true
}
if (paused) {
try {
pauseLock.wait() // will cause this Thread to block until
// another thread calls pauseLock.notifyAll()
// Note that calling wait() will
// relinquish the synchronized lock that this
// thread holds on pauseLock so another thread
// can acquire the lock to call notifyAll()
// (link with explanation below this code)
}
catch (ex: InterruptedException) {
breakBomb = true
}
if (!running) { // running might have changed since we paused
breakBomb = true
}
}
}
if (breakBomb) break*/ // uncomment to multithread
// Your code here
// update panning and shits
if (track.trackType == TrackType.DYNAMIC_SOURCE && track.isPlaying) {
(track.filters[DS_FLTIDX_PAN] as BinoPan).earDist = App.audioMixer.listenerHeadSize
if (App.audioMixer.actorNowPlaying != null) {
val trackingTarget = track.trackingTarget
if (trackingTarget == null || trackingTarget == App.audioMixer.actorNowPlaying) {
// "reset" the track
track.volume = track.maxVolume
(track.filters[DS_FLTIDX_PAN] as BinoPan).pan = 0f
(track.filters[DS_FLTIDX_LOW] as Lowpass).setCutoff(SAMPLING_RATE / 2f)
}
else if (trackingTarget is ActorWithBody) {
val relativeXpos = relativeXposition(App.audioMixer.actorNowPlaying!!, trackingTarget as ActorWithBody)
val distFromActor = distBetweenActors(App.audioMixer.actorNowPlaying!!, trackingTarget as ActorWithBody)
val vol = track.maxVolume * getVolFun(distFromActor / distFalloff).coerceAtLeast(0.0)
track.volume = vol
(track.filters[DS_FLTIDX_PAN] as BinoPan).pan = (1.3f * relativeXpos / distFalloff).toFloat()
(track.filters[DS_FLTIDX_LOW] as Lowpass).setCutoff(
(SAMPLING_RATED*0.5) / (24.0 * (distFromActor / distFalloff).sqr() + 1.0)
)
val sourceVec = (trackingTarget as ActorWithBody).let { it.externalV + it.controllerV }
val listenerVec = App.audioMixer.actorNowPlaying!!.let { it.externalV + it.controllerV }
val distFromActorNext = distBetweenPoints(
App.audioMixer.actorNowPlaying!!.centrePosVector + listenerVec,
(trackingTarget as ActorWithBody).centrePosVector + sourceVec
)
val isApproaching = if (distFromActorNext <= distFromActor) 1.0 else -1.0
val relativeSpeed = (sourceVec - listenerVec).magnitude * GAME_TO_SI_VELO * isApproaching
val soundSpeed = SPEED_OF_SOUND_AIR * 4f // using an arbitrary value for "gamification"
val dopplerFactor = (soundSpeed + relativeSpeed) / soundSpeed // >1: speedup, <1: speeddown
track.processor.streamBuf?.playbackSpeed = dopplerFactor.toFloat()
// printdbg("dist=$distFromActor\tdopplerFactor=$dopplerFactor")
}
}
else {
// "reset" the track
track.volume = track.maxVolume
(track.filters[DS_FLTIDX_PAN] as BinoPan).pan = 0f
(track.filters[DS_FLTIDX_LOW] as Lowpass).setCutoff(SAMPLING_RATE / 2f)
}
}
// fetch deviceBufferSize amount of sample from the disk
if (track.playRequested.get()) {
track.play()
}
if (track.trackType != TrackType.MASTER && track.trackType != TrackType.BUS && track.streamPlaying.get()) {
if (streamBuf == null && track.currentTrack != null) {
allocateStreamBuf(track)
}
if (track.currentTrack == null) throw IllegalStateException("Track ${track.name} is playing but also has null music track")
streamBuf?.fetchBytes() ?: throw NullPointerException("Null StreamBuf for ${track.name}")
}
var samplesL1: FloatArray
var samplesR1: FloatArray
bufEmpty = false
// get samples and apply the fader
if (track.trackType == TrackType.MASTER || track.trackType == TrackType.BUS) {
// combine all the inputs
samplesL1 = FloatArray(buffertaille)
samplesR1 = FloatArray(buffertaille)
val sidechains = track.sidechainInputs
// add all up
sidechains.forEach { (side, mix) ->
for (i in samplesL1.indices) {
samplesL1[i] += side.processor.fout1[0][i] * mix.toFloat()
samplesR1[i] += side.processor.fout1[1][i] * mix.toFloat()
}
}
}
// source channel: skip processing if there's no active input
// else if (track.getSidechains().any { it != null && !it.isBus && !it.isMaster && !it.streamPlaying } && !track.streamPlaying) {
else if (!track.streamPlaying.get() || streamBuf == null || streamBuf!!.validSamplesInBuf < App.audioBufferSize) {
samplesL1 = emptyBuf
samplesR1 = emptyBuf
bufEmpty = true
}
else {
streamBuf!!.getLR().let {
samplesL1 = it.first
samplesR1 = it.second
}
}
if (!bufEmpty) {
// run the input through the stack of filters
val filterStack = track.filters.filter { !it.bypass && it !is NullFilter }
if (filterStack.isEmpty()) {
fout1 = listOf(samplesL1, samplesR1)
}
else {
var fin1 = listOf(samplesL1, samplesR1)
fout1 = listOf(FloatArray(buffertaille), FloatArray(buffertaille))
filterStack.forEachIndexed { index, it ->
it(fin1, fout1)
fin1 = fout1
if (index < filterStack.lastIndex) {
fout1 = listOf(FloatArray(buffertaille), FloatArray(buffertaille))
}
}
}
// apply fader at post
fout1.forEach { ch ->
ch.forEachIndexed { index, sample ->
ch[index] = (sample * track.volume).toFloat()
}
}
// scan the finished sample for mapping signal level and clipping detection
fout1.map { it.maxOf { it.absoluteValue } }.forEachIndexed { index, fl ->
maxSigLevel[index] = fl.toDouble()
}
fout1.map { it.sumOf { it.sqr().toDouble() } }.forEachIndexed { index, fl ->
maxRMS[index] = sqrt(fl / (buffertaille))
}
hasClipping.fill(false)
fout1.forEachIndexed { index, floats ->
var lastSample = floats[0]
for (i in 1 until floats.size) {
val currentSample = floats[i]
if (lastSample * currentSample > 0.0 && lastSample.absoluteValue >= 1.0 && currentSample.absoluteValue >= 1.0) {
hasClipping[index] = true
break
}
lastSample = currentSample
}
}
}
else {
fout1 = listOf(samplesL1, samplesR1) // keep pass the so that long-delay filters can empty out its buffer
maxSigLevel.fill(0.0)
maxRMS.fill(0.0)
hasClipping.fill(false)
}
// by this time, the output buffer is filled with processed results, pause the execution
if (track.trackType != TrackType.MASTER) {
this.pause()
}
else {
// spin until queue is sufficiently empty
/*while (track.pcmQueue.size >= BACK_BUF_COUNT && running) { // uncomment to multithread
Thread.sleep(1)
}*/
// printdbg("PUSHE; Queue size: ${track.pcmQueue.size}")
track.pcmQueue.addLast(fout1)
// spin
// Thread.sleep(((1000*bufferSize) / 8L / rate).coerceAtLeast(1L)) // uncomment to multithread
// wake sidechain processors
resumeSidechainsRecursively(track, track.name)
}
// } // uncomment to multithread
}
private fun FloatArray.applyVolume(volume: Float) = FloatArray(this.size) { (this[it] * volume) }
/*private fun FloatArray.applyVolumeInline(volume: Float): FloatArray {
for (i in this.indices) {
this[i] *= volume
}
return this
}*/
private fun resumeSidechainsRecursively(track: TerrarumAudioMixerTrack?, caller: String) {
track?.getSidechains()?.forEach {
if (it?.processor?.running == true) {
it.processor.resume()
it.getSidechains().forEach {
if (it?.processor?.running == true) {
it.processor.resume()
resumeSidechainsRecursively(it, caller + caller)
}
}
}
}
}
fun stop() {
running = false
// you might also want to interrupt() the Thread that is
// running this Runnable, too, or perhaps call:
resume()
// to unblock
}
fun pause() {
// printdbg("PAUSE")
// you may want to throw an IllegalStateException if !running
paused = true
}
fun resume() {
// printdbg("RESUME")
synchronized(pauseLock) {
paused = false
pauseLock.notifyAll() // Unblocks thread
}
}
}
class FeedSamplesToAdev(val bufferSize: Int, val rate: Int, val track: TerrarumAudioMixerTrack) : Runnable {
init {
if (track.trackType != TrackType.MASTER) throw IllegalArgumentException("Track is not master")
}
override fun run() {
while (!Thread.currentThread().isInterrupted) {
try {
val writeQueue = track.pcmQueue
val queueSize = writeQueue.size
if (queueSize > 0) {
val samples = writeQueue.removeFirst()
track.adev!!.writeSamples(samples)
}
}
catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
}
}