diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2025-03-04 22:45:19 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2025-03-04 22:45:19 -0300 |
commit | 79ff3692b9462fc79d93bd74213ce6904340fc13 (patch) | |
tree | 22055c038783b87cceffe3d2220cc2b568a4493d /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.kt | 208 |
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 |