diff --git a/assets/mods/basegame/audio/effects/notes/spieluhr.ogg b/assets/mods/basegame/audio/effects/notes/spieluhr.ogg new file mode 100644 index 000000000..c7eba9e9b --- /dev/null +++ b/assets/mods/basegame/audio/effects/notes/spieluhr.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7be7ebc1c856a24cf0eb80eb721f7d8e62394eddb2755c829edeec40ca0fd276 +size 27966 diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 984ca42f9..11055e4f8 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -512,7 +512,7 @@ public class App implements ApplicationListener { ); var loadOrder = loadOrderCSVparser.getRecords(); - if (loadOrder.size() > 0) { + if (!loadOrder.isEmpty()) { var modname = loadOrder.get(0).get(0); var textureFile = Gdx.files.internal("assets/mods/"+modname+"/splashback.png"); @@ -531,8 +531,8 @@ public class App implements ApplicationListener { } finally { - try {loadOrderCSVparser.close();} - catch (IOException e) {} + try { loadOrderCSVparser.close(); } + catch (IOException | NullPointerException e) {} } } @@ -740,12 +740,15 @@ public class App implements ApplicationListener { FrameBufferManager.end(); screenshotRequested = false; - Terrarum.INSTANCE.getIngame().sendNotification(msg); + + var ingame = Terrarum.INSTANCE.getIngame(); + if (ingame != null) ingame.sendNotification(msg); } } public static Texture getCurrentDitherTex() { - int hash = (int) (31 + GLOBAL_RENDER_TIMER + 0x165667B1 + GLOBAL_RENDER_TIMER * 0xC2B2AE3D); + var T = (int) GLOBAL_RENDER_TIMER; + int hash = 31 + T + 0x165667B1 + T * 0xC2B2AE3D; hash = Integer.rotateLeft(hash, 17) * 0x27D4EB2F; hash ^= hash >>> 15; hash *= 0x85EBCA77; @@ -1102,12 +1105,7 @@ public class App implements ApplicationListener { } // nullify if not actually connected - try { - if (!((XinputControllerAdapter) gamepad).getC().isConnected()) { - gamepad = null; - } - } - catch (NullPointerException notQuiteWindows) { + if (gamepad != null && !((XinputControllerAdapter) gamepad).getC().isConnected()) { gamepad = null; } } @@ -1946,7 +1944,7 @@ public class App implements ApplicationListener { public static void addDebugTime(String target, String... targets) { long l = 0L; for (String s : targets) { - l += ((long) debugTimers.get(s)); + l += debugTimers.get(s); } debugTimers.put(target, l); } diff --git a/src/net/torvald/terrarum/CreditSingleton.kt b/src/net/torvald/terrarum/CreditSingleton.kt index 55a27a059..647f3ebed 100644 --- a/src/net/torvald/terrarum/CreditSingleton.kt +++ b/src/net/torvald/terrarum/CreditSingleton.kt @@ -343,7 +343,11 @@ Sound from - effects/haptic_*.ogg ℗ 2024 CuriousTorvald -My own recordings +Sound from + + - effects/notes/spieluhr.ogg +℗ 2013 Ubikphonik +Sound from $BULLET Impulse Responses: diff --git a/src/net/torvald/terrarum/audio/AudioHelper.kt b/src/net/torvald/terrarum/audio/AudioHelper.kt index 5ea6eac32..f05fb9fc7 100644 --- a/src/net/torvald/terrarum/audio/AudioHelper.kt +++ b/src/net/torvald/terrarum/audio/AudioHelper.kt @@ -14,6 +14,9 @@ import java.io.File */ object AudioHelper { + /** + * The audio must be in stereo (two channels) + */ fun getIR(module: String, path: String): Array { val id = "convolution$$module.$path" @@ -66,6 +69,9 @@ object AudioHelper { return Array(2) { FFT.fft(conv[it]) } } + /** + * The audio must be in stereo + */ fun getAudioInSamples(module: String, path: String): Array { val id = "audiosamplesf32$$module.$path" diff --git a/src/net/torvald/terrarum/audio/audiobank/AudioBankMusicBox.kt b/src/net/torvald/terrarum/audio/audiobank/AudioBankMusicBox.kt deleted file mode 100644 index a37878346..000000000 --- a/src/net/torvald/terrarum/audio/audiobank/AudioBankMusicBox.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.torvald.terrarum.audio.audiobank - -import com.badlogic.gdx.utils.Queue -import net.torvald.terrarum.INGAME -import net.torvald.terrarum.audio.AudioBank - -/** - * Created by minjaesong on 2024-04-12. - */ -class AudioBankMusicBox(override var songFinishedHook: (AudioBank) -> Unit = {}) : AudioBank() { - - override val name = "spieluhr" - override val samplingRate = 48000 - override val channels = 1 - - override val totalSizeInSamples = Long.MAX_VALUE // TODO length of lowest-pitch note - - private val messageQueue = Queue>() // pair of: absolute tick count, notes (61 notes polyphony) - - override fun currentPositionInSamples(): Long { - TODO("Not yet implemented") - } - - override fun readSamples(bufferL: FloatArray, bufferR: FloatArray): Int { - val tickCount = INGAME.WORLD_UPDATE_TIMER - - TODO("Not yet implemented") - } - - override fun reset() { - TODO("Not yet implemented") - } - - override fun makeCopy(): AudioBank { - TODO("Not yet implemented") - } - - override fun dispose() { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/audio/audiobank/AudioBankMusicBox.kt b/src/net/torvald/terrarum/modulebasegame/audio/audiobank/AudioBankMusicBox.kt new file mode 100644 index 000000000..0fab6c503 --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/audio/audiobank/AudioBankMusicBox.kt @@ -0,0 +1,81 @@ +package net.torvald.terrarum.modulebasegame.audio.audiobank + +import com.badlogic.gdx.utils.Queue +import net.torvald.terrarum.App +import net.torvald.terrarum.INGAME +import net.torvald.terrarum.audio.AudioBank + +/** + * Created by minjaesong on 2024-04-12. + */ +class AudioBankMusicBox(override var songFinishedHook: (AudioBank) -> Unit = {}) : AudioBank() { + + private data class Msg(val tick: Long, val samplesL: FloatArray, val samplesR: FloatArray, var samplesDispatched: Int = 0) // in many cases, samplesL and samplesR will point to the same object + + override val name = "spieluhr" + override val samplingRate = 48000 + override val channels = 1 + + private val getSample = // usage: getSample(noteNum 0..60) + InstrumentLoader.load("spieluhr", "basegame", "audio/effects/notes/spieluhr.ogg", 29) + + private val SAMEPLES_PER_TICK = samplingRate / App.TICK_SPEED // should be 800 on default setting + + override val totalSizeInSamples = getSample(0).first.size.toLong() // length of lowest-pitch note + + private val messageQueue = Queue() + + private fun findSetBits(num: Long): List { + val result = mutableListOf() + for (i in 0 until 61) { + if (num and (1L shl i) != 0L) { + result.add(i) + } + } + return result + } + + /** + * Queues the notes such that they are played on the next tick + */ + fun queuePlay(noteBits: Long) { + if (noteBits == 0L) return + + val tick = INGAME.WORLD_UPDATE_TIMER + 1 + val notes = findSetBits(noteBits) + + val buf = FloatArray(getSample(notes.first()).first.size) + + // combine all those samples + notes.forEach { note -> + getSample(note).first.forEachIndexed { index, fl -> + buf[index] += fl + } + } + + // actually queue it + messageQueue.addLast(Msg(tick, buf, buf)) + } + + override fun currentPositionInSamples(): Long { + TODO("Not yet implemented") + } + + override fun readSamples(bufferL: FloatArray, bufferR: FloatArray): Int { + val tickCount = INGAME.WORLD_UPDATE_TIMER + + TODO("Not yet implemented") + } + + override fun reset() { + TODO("Not yet implemented") + } + + override fun makeCopy(): AudioBank { + TODO("Not yet implemented") + } + + override fun dispose() { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/audio/audiobank/InstrumentLoader.kt b/src/net/torvald/terrarum/modulebasegame/audio/audiobank/InstrumentLoader.kt new file mode 100644 index 000000000..0b67972e7 --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/audio/audiobank/InstrumentLoader.kt @@ -0,0 +1,101 @@ +package net.torvald.terrarum.modulebasegame.audio.audiobank + +import net.torvald.terrarum.CommonResourcePool +import net.torvald.terrarum.ModMgr +import net.torvald.terrarum.audio.AudioProcessBuf +import net.torvald.terrarum.audio.audiobank.MusicContainer +import net.torvald.terrarum.ceilToInt +import net.torvald.terrarum.floorToInt +import java.lang.Math.pow +import kotlin.math.roundToInt + +/** + * Creates 61-note (C1 to C5) pack of samples from a single file. Tuning system is 12-note Equal Temperament. + * + * Created by minjaesong on 2024-04-14. + */ +object InstrumentLoader { + + // 0 is C0 + private fun getStretch(noteNum: Int) = pow(2.0, (noteNum - 29) / 12.0) + + + /** + * Will read the sample and create 61 copies of them. Rendered samples will be stored on the CommonResourcePool + * with the naming rule of `"${idBase}_${noteNumber}"`, with format of Pair + * + * If `isDualMono` option is set, two values of a pair will point to the same FloatArray. + * + * @param idBase Base ID string + * @param module Which module the path must refer to + * @param path path to the audio + * @param initialNote Initial note of the given sample. Ranged from 0 to 60. C1 is 0, F3 is 29 + * @param isDualMono if the input sample is in dual mono + */ + fun load(idBase: String, module: String, path: String, initialNote: Int, isDualMono: Boolean = true): (Int) -> Pair { + if (initialNote !in 0..60) throw IllegalArgumentException("Initial note too low or high ($initialNote not in range of 0..60)") + + val baseResourceName = "inst$$idBase" + if (CommonResourcePool.resourceExists("${baseResourceName}_$initialNote")) return { it -> + CommonResourcePool.getAs>("${baseResourceName}_$it") + } + + val masterFile = MusicContainer("${idBase}_${initialNote}", ModMgr.getFile(module, path)) + val masterSamplesL = FloatArray(masterFile.totalSizeInSamples.toInt()) + val masterSamplesR = FloatArray(masterFile.totalSizeInSamples.toInt()) + masterFile.readSamples(masterSamplesL, masterSamplesR) + + val renderedSamples = Array>(61) { null to null } + + for (j in 0 until 61) { + val i = j - initialNote + val rate = getStretch(i) + val sampleCount = (masterFile.totalSizeInSamples * rate).roundToInt() + + val samplesL = FloatArray(sampleCount) + val samplesR = if (isDualMono) samplesL else FloatArray(sampleCount) + + renderedSamples[j] = samplesL to samplesR + + // do resampling + resample(masterSamplesL, samplesL, rate) + if (!isDualMono) + resample(masterSamplesR, samplesR, rate) + + CommonResourcePool.addToLoadingList("${baseResourceName}_$j") { + samplesL to samplesR + } + } + + CommonResourcePool.loadAll() + + masterFile.dispose() + + return { it -> + CommonResourcePool.getAs>("${baseResourceName}_$it") + } + } + + private val TAPS = 8 + + private fun resample(input: FloatArray, output: FloatArray, rate: Double) { + for (sampleIdx in 0 until output.size) { + val t = sampleIdx.toDouble() * rate + val leftBound = maxOf(0, (t - TAPS + 1).floorToInt()) + val rightBound = minOf(input.size - 1, (t + TAPS).ceilToInt()) + + + var akkuL = 0.0 + var weightedSum = 0.0 + + for (j in leftBound..rightBound) { + val w = AudioProcessBuf.L(t - j.toDouble()) + akkuL += input[j] * w + weightedSum += w + } + + output[sampleIdx] = (akkuL / weightedSum).toFloat() + } + } + +} \ No newline at end of file