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