summary refs log tree commit diff stats
path: root/src/main/kotlin/space/autistic/radio/cli
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2025-03-04 22:45:19 -0300
committerSoniEx2 <endermoneymod@gmail.com>2025-03-04 22:45:19 -0300
commit79ff3692b9462fc79d93bd74213ce6904340fc13 (patch)
tree22055c038783b87cceffe3d2220cc2b568a4493d /src/main/kotlin/space/autistic/radio/cli
First public commit
Diffstat (limited to 'src/main/kotlin/space/autistic/radio/cli')
-rw-r--r--src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt208
1 files changed, 208 insertions, 0 deletions
diff --git a/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt b/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt
new file mode 100644
index 0000000..517957b
--- /dev/null
+++ b/src/main/kotlin/space/autistic/radio/cli/OfflineSimulator.kt
@@ -0,0 +1,208 @@
+package space.autistic.radio.cli
+
+import org.joml.Vector2f
+import space.autistic.radio.complex.cmul
+import space.autistic.radio.fmsim.FmFullConstants
+import space.autistic.radio.fmsim.FmFullModulator
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.net.URI
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+import kotlin.io.path.inputStream
+import kotlin.io.path.toPath
+import kotlin.math.min
+import kotlin.system.exitProcess
+
+fun printUsage() {
+    println("Usage: OfflineSimulator -o OUTFILE.raw {[-p POWER] [-l|-h] file:///FILE.raw}")
+    println("    file:///FILE.raw (or ./FILE.raw - the ./ is required)")
+    println("        The raw input file. 2x48kHz 32-bit float")
+    println("    -o OUTFILE.raw")
+    println("        The raw RF stream to output, 2x200kHz 32-bit float")
+    println("    -p POWER")
+    println("        The signal amplitude (power level), e.g. 1.0")
+    println("    -l")
+    println("        Simulate a partial overlap on the lower half of the tuned-into frequency.")
+    println("    -h")
+    println("        Simulate a partial overlap on the upper half of the tuned-into frequency.")
+}
+
+class SimFile(val power: Float, val band: Int, val filename: String) {
+    var closed: Boolean = false
+    val buffer: FloatBuffer = FloatBuffer.allocate(8192)
+    val modulator = FmFullModulator()
+    var stream: InputStream? = null
+}
+
+fun main(args: Array<String>) {
+    if (args.isEmpty()) {
+        printUsage()
+        exitProcess(1)
+    }
+    var hasOutput = false
+    var inArg = ""
+    var output = ""
+    var power = 1.0f
+    var band = 2
+    val files: ArrayList<SimFile> = ArrayList()
+    for (arg in args) {
+        if (!hasOutput) {
+            if (arg == "-o") {
+                hasOutput = true
+                inArg = "-o"
+            } else {
+                printUsage()
+                exitProcess(1)
+            }
+        } else {
+            when (inArg) {
+                "-o" -> {
+                    output = arg
+                    inArg = ""
+                }
+
+                "-p" -> {
+                    power = arg.toFloatOrNull() ?: run {
+                        println("Error processing -p argument: not a valid float")
+                        printUsage()
+                        exitProcess(1)
+                    }
+                    inArg = ""
+                }
+
+                "" -> {
+                    if (!arg.startsWith("-")) {
+                        files.add(SimFile(power, band, arg))
+                        inArg = ""
+                        band = 2
+                        power = 1.0f
+                    } else {
+                        when (arg) {
+                            "-p" -> inArg = "-p"
+                            "-l" -> band = 1
+                            "-h" -> band = 3
+                            else -> {
+                                println("Unknown option")
+                                printUsage()
+                                exitProcess(1)
+                            }
+                        }
+                    }
+                }
+
+                else -> throw NotImplementedError(inArg)
+            }
+        }
+    }
+
+    if (files.isEmpty()) {
+        printUsage()
+        exitProcess(1)
+    }
+
+    println(ProcessHandle.current().pid())
+
+    FileOutputStream(output).buffered().use { outputStream ->
+        for (inputFile in files) {
+            if (inputFile.filename != "file:///dev/zero") {
+                if (inputFile.filename.startsWith("./")) {
+                    inputFile.stream = FileInputStream(inputFile.filename)
+                } else {
+                    inputFile.stream = URI(inputFile.filename).toPath().inputStream()
+                }
+            }
+        }
+
+        val buffer = ByteBuffer.allocate(2 * 4 * FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1)
+        val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K)
+        val minus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K)
+        while (true) {
+            // initialized to maximum buffer size, trimmed down later
+            var minBuffer = 8192
+            for (inputFile in files) {
+                val stream = inputFile.stream
+                if (stream == null) {
+                    if (inputFile.buffer.remaining() > 2 * FmFullConstants.IFFT_DATA_BLOCK_SIZE_48K_300K) {
+                        inputFile.modulator.flush(inputFile.power) {
+                            inputFile.buffer.put(it)
+                        }
+                    }
+                } else {
+                    val bytes = stream.read(buffer.array())
+                    if (bytes <= 0) {
+                        stream.close()
+                        inputFile.stream = null
+                        inputFile.closed = true
+                        inputFile.modulator.flush(inputFile.power) {
+                            inputFile.buffer.put(it)
+                        }
+                    } else {
+                        val floats = buffer.slice(0, bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
+                        var shouldFlush = true
+                        inputFile.modulator.process(floats, inputFile.power) {
+                            inputFile.buffer.put(it)
+                            shouldFlush = false
+                        }
+                        if (shouldFlush) {
+                            inputFile.modulator.flush(inputFile.power) {
+                                inputFile.buffer.put(it)
+                            }
+                        }
+                    }
+                }
+                minBuffer = min(minBuffer, inputFile.buffer.position())
+            }
+
+            val outputBuffer = ByteBuffer.allocate(minBuffer * 4)
+            val floatView = outputBuffer.order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
+            val floatBufferLo = FloatBuffer.allocate(minBuffer)
+            val floatBufferHi = FloatBuffer.allocate(minBuffer)
+            for (inputFile in files) {
+                inputFile.buffer.flip()
+                val floatBuffer = when (inputFile.band) {
+                    1 -> floatBufferLo
+                    2 -> floatView
+                    3 -> floatBufferHi
+                    else -> throw IllegalStateException()
+                }
+                for (i in 0 until floatBuffer.capacity()) {
+                    floatBuffer.put(i, floatBuffer.get(i) + inputFile.buffer.get())
+                }
+                inputFile.buffer.compact()
+            }
+            val z = Vector2f()
+            val w = Vector2f()
+            for (i in 0 until floatBufferHi.capacity() step 2) {
+                z.x = floatBufferHi.get(i)
+                z.y = floatBufferHi.get(i + 1)
+                if (!plus100k.hasRemaining()) {
+                    plus100k.clear()
+                }
+                w.x = plus100k.get()
+                w.y = plus100k.get()
+                z.cmul(w)
+                floatView.put(i, floatView.get(i) + z.x)
+                floatView.put(i, floatView.get(i) + z.y)
+            }
+            for (i in 0 until floatBufferLo.capacity() step 2) {
+                z.x = floatBufferLo.get(i)
+                z.y = floatBufferLo.get(i + 1)
+                if (!minus100k.hasRemaining()) {
+                    minus100k.clear()
+                }
+                w.x = minus100k.get()
+                w.y = -minus100k.get()
+                z.cmul(w)
+                floatView.put(i, floatView.get(i) + z.x)
+                floatView.put(i, floatView.get(i) + z.y)
+            }
+            outputStream.write(outputBuffer.array())
+            if (files.all { it.closed }) {
+                break
+            }
+        }
+    }
+}
\ No newline at end of file