diff options
Diffstat (limited to 'src/client')
9 files changed, 546 insertions, 158 deletions
diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt index b3c9f17..87fd144 100644 --- a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt @@ -20,6 +20,8 @@ import space.autistic.radio.client.fmsim.FmFullThread import space.autistic.radio.client.fmsim.FmSimulatorMode import space.autistic.radio.client.gui.FmReceiverScreen import space.autistic.radio.client.sound.PirateRadioSoundInstance +import space.autistic.radio.client.sound.ReceiverAudioStream +import javax.sound.sampled.Mixer import kotlin.math.max import kotlin.math.min @@ -36,6 +38,14 @@ object PirateRadioClient : ClientModInitializer { } var mode = FmSimulatorMode.FULL + val minecraftAudioDevice = object : Mixer.Info("", "", "", "") { + } + val openAlAudioDevice = object : Mixer.Info("", "", "", "") { + } + val systemDefaultAudioDevice = object : Mixer.Info("", "", "", "") { + } + var audioDevice: Mixer.Info = if (ReceiverAudioStream.useNativeAudio) minecraftAudioDevice else openAlAudioDevice + override fun onInitializeClient() { Thread.ofPlatform().daemon().name("fm-receiver").start(FmFullThread) PirateRadio.proxy = ClientProxy() diff --git a/src/client/kotlin/space/autistic/radio/client/flite/FliteFactory.kt b/src/client/kotlin/space/autistic/radio/client/flite/FliteFactory.kt index 993cbe9..768a040 100644 --- a/src/client/kotlin/space/autistic/radio/client/flite/FliteFactory.kt +++ b/src/client/kotlin/space/autistic/radio/client/flite/FliteFactory.kt @@ -9,23 +9,28 @@ import space.autistic.radio.wasm.Bindings import space.autistic.radio.wasm.WasmExitException import java.io.InputStream import java.lang.invoke.MethodHandles +import kotlin.io.path.inputStream import kotlin.reflect.jvm.javaMethod object FliteFactory : () -> Instance { private fun fd_read(a: Int, b: Int, c: Int, d: Int): Int { - throw UnsupportedOperationException() + // EBADF + return 8 } private fun fd_seek(a: Int, b: Long, c: Int, d: Int): Int { - throw UnsupportedOperationException() + // EBADF + return 8 } private fun fd_close(a: Int): Int { - throw UnsupportedOperationException() + // EBADF + return 8 } private fun fd_write(a: Int, b: Int, c: Int, d: Int): Int { - throw UnsupportedOperationException() + // EBADF + return 8 } private fun proc_exit(status: Int): Nothing { @@ -51,7 +56,7 @@ object FliteFactory : () -> Instance { private fun getModuleInputStream(): InputStream { return FabricLoader.getInstance().getModContainer("pirate-radio").flatMap { it.findPath("flite.wasm") } - .map<InputStream?> { it.toFile().inputStream() }.orElseGet { + .map { it.inputStream() }.orElseGet { this.javaClass.getResourceAsStream("/flite.wasm") } } diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt index 0a6184f..bce7a72 100644 --- a/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt +++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt @@ -1,25 +1,34 @@ package space.autistic.radio.client.fmsim +import net.minecraft.client.sound.SoundSystem +import net.minecraft.util.math.MathHelper import org.joml.Vector2f +import space.autistic.radio.PirateRadio import space.autistic.radio.client.PirateRadioClient import space.autistic.radio.client.complex.cmul import space.autistic.radio.client.sound.PirateRadioSoundInstance import space.autistic.radio.client.sound.ReceiverAudioStream -import space.autistic.radio.entity.DisposableTransmitterEntity +import space.autistic.radio.client.util.LevenshteinDistance +import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.FloatBuffer +import java.util.UUID import java.util.concurrent.ArrayBlockingQueue +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.LineUnavailableException +import javax.sound.sampled.Mixer import kotlin.math.max -import kotlin.math.min object FmFullThread : Runnable { class FmTask( - val trackedTransmitters: Map<DisposableTransmitterEntity, PirateRadioSoundInstance.TrackedTransmitter>, + val trackedTransmitters: Map<UUID, PirateRadioSoundInstance.TrackedTransmitter>, val noiseLevels: FloatArray, + val audioOutput: Mixer.Info, + val minecraftSoundDevice: String?, ) // empty task, marker to shut off the thread - val EMPTY_TASK = FmTask(emptyMap(), FloatArray(3)) + val EMPTY_TASK = FmTask(emptyMap(), FloatArray(3), PirateRadioClient.minecraftAudioDevice, null) val trackedTransmitterQueue = ArrayBlockingQueue<FmTask>(8) @@ -34,9 +43,12 @@ object FmFullThread : Runnable { // 3 seconds private const val REPEAT_TIMEOUT = 8000 * 3 + // default to 0.05s (1/20) + val bufferSize = System.getProperty("space.autistic.radio.buffer.size", "").toIntOrNull() ?: 2400 + override fun run() { var currentTask = EMPTY_TASK - val modulators = HashMap<DisposableTransmitterEntity, TtsModulator>() + val modulators = HashMap<UUID, TtsModulator>() val mixingBuffers = Array(3) { FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K * 2) } val inputBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2) @@ -51,135 +63,232 @@ object FmFullThread : Runnable { val demodulator = FmFullDemodulator() - while (!Thread.interrupted()) { - currentTask = trackedTransmitterQueue.poll() ?: currentTask - if (currentTask === EMPTY_TASK) { - Thread.onSpinWait() - continue - } - modulators.keys.retainAll(currentTask.trackedTransmitters.keys) - currentTask.trackedTransmitters.forEach { (k, v) -> - modulators.compute(k) { _, modulator -> - if (modulator != null) { - modulator.power = v.power - modulator.mixingBuffer = mixingBuffers[v.frequencyOffset + 1] - return@compute modulator + // for native audio only + val outputBytes = ByteBuffer.allocate(bufferSize * 2 * 2).order(ByteOrder.LITTLE_ENDIAN) + var nativeAudio = if (ReceiverAudioStream.useNativeAudio) { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } else { + null + } + var lastOutput = PirateRadioClient.audioDevice + var lastMinecraftSoundDevice: String? = null + + try { + while (!Thread.interrupted()) { + currentTask = trackedTransmitterQueue.poll() ?: currentTask + if (currentTask === EMPTY_TASK) { + Thread.onSpinWait() + continue + } + + val audioOutput = currentTask.audioOutput + if (lastOutput != audioOutput && audioOutput != PirateRadioClient.minecraftAudioDevice) { + nativeAudio = when (audioOutput) { + PirateRadioClient.systemDefaultAudioDevice -> { + try { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } + + PirateRadioClient.openAlAudioDevice -> { + null + } + + else -> { + try { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format, audioOutput) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } + } + } + if (audioOutput == PirateRadioClient.minecraftAudioDevice) { + if (lastMinecraftSoundDevice != currentTask.minecraftSoundDevice || lastOutput != audioOutput) { + lastMinecraftSoundDevice = currentTask.minecraftSoundDevice + nativeAudio = if (lastMinecraftSoundDevice == "") { + try { + AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + .apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } else { + val device = lastMinecraftSoundDevice!!.removePrefix(SoundSystem.OPENAL_SOFT_ON) + try { + AudioSystem.getSourceDataLine( + ReceiverAudioStream.format, + AudioSystem.getMixerInfo().filter { + AudioSystem.getMixer(it).sourceLineInfo.isNotEmpty() + }.map { + it to LevenshteinDistance.calculate(it.description, device) + }.minByOrNull { it.second }?.first + ).apply { open(ReceiverAudioStream.format, FmFullThread.bufferSize * 2 * 2) } + } catch (e: LineUnavailableException) { + null + } + } } - val audioData = v.audio.getNow(null) - if (audioData != null) { - val buf = FloatBuffer.wrap(audioData) - var actualSampleOffset = Math.floorMod(v.sampleOffset, (buf.capacity() + REPEAT_TIMEOUT)) - var repeatTimeout = max(0, actualSampleOffset - buf.capacity()) - if (repeatTimeout > 0) { - actualSampleOffset = 0 - repeatTimeout = REPEAT_TIMEOUT - repeatTimeout + } + lastOutput = audioOutput + + modulators.keys.retainAll(currentTask.trackedTransmitters.keys) + currentTask.trackedTransmitters.forEach { (k, v) -> + modulators.compute(k) { _, modulator -> + if (modulator != null) { + modulator.power = v.power + modulator.mixingBuffer = mixingBuffers[v.frequencyOffset + 1] + return@compute modulator } - if (actualSampleOffset == buf.capacity()) { - actualSampleOffset = 0 - repeatTimeout = REPEAT_TIMEOUT + val audioData = v.audio.getNow(null) + if (audioData != null) { + val buf = FloatBuffer.wrap(audioData) + var actualSampleOffset = Math.floorMod(v.sampleOffset, (buf.capacity() + REPEAT_TIMEOUT)) + var repeatTimeout = max(0, actualSampleOffset - buf.capacity()) + if (repeatTimeout > 0) { + actualSampleOffset = 0 + repeatTimeout = REPEAT_TIMEOUT - repeatTimeout + } + if (actualSampleOffset == buf.capacity()) { + actualSampleOffset = 0 + repeatTimeout = REPEAT_TIMEOUT + } + buf.position(actualSampleOffset) + TtsModulator( + buf, FmFullModulator(), v.power, repeatTimeout, mixingBuffers[v.frequencyOffset + 1] + ) + } else { + null } - buf.position(actualSampleOffset) - TtsModulator( - buf, FmFullModulator(), v.power, repeatTimeout, mixingBuffers[v.frequencyOffset + 1] - ) - } else { - null } } - } - mixingBuffers.forEach { - it.clear() - while (it.hasRemaining()) it.put(0f) - it.clear() - } + mixingBuffers.forEach { + it.clear() + while (it.hasRemaining()) it.put(0f) + it.clear() + } - modulators.values.forEach { - inputBuffer.clear() - while (inputBuffer.hasRemaining()) { - val sample = if (it.repeatTimeout > 0) { - it.repeatTimeout-- - 0f - } else { - it.buffer.get() - } - if (!it.buffer.hasRemaining()) { - it.repeatTimeout = REPEAT_TIMEOUT - it.buffer.clear() + modulators.values.forEach { + inputBuffer.clear() + while (inputBuffer.hasRemaining()) { + val sample = if (it.repeatTimeout > 0) { + it.repeatTimeout-- + 0f + } else { + it.buffer.get() + } + if (!it.buffer.hasRemaining()) { + it.repeatTimeout = REPEAT_TIMEOUT + it.buffer.clear() + } + for (i in 0 until 2 * 6) inputBuffer.put(sample) } - for (i in 0 until 2 * 6) inputBuffer.put(sample) - } - inputBuffer.clear() - val mixingBuffer = it.mixingBuffer - it.modulator.process(inputBuffer, it.power, false) { outputBuffer -> - for (i in 0 until mixingBuffer.capacity()) { - mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + inputBuffer.clear() + val mixingBuffer = it.mixingBuffer + it.modulator.process(inputBuffer, it.power, false) { outputBuffer -> + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + } } } - } - if (modulators.any { it.value.mixingBuffer === mixingBuffers[2] }) { - val floatBufferHi = mixingBuffers[2] - val floatView = mixingBuffers[1] - 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() + if (modulators.any { it.value.mixingBuffer === mixingBuffers[2] }) { + val floatBufferHi = mixingBuffers[2] + val floatView = mixingBuffers[1] + 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 + 1, floatView.get(i + 1) + z.y) } - w.x = plus100k.get() - w.y = plus100k.get() - z.cmul(w) - floatView.put(i, floatView.get(i) + z.x) - floatView.put(i + 1, floatView.get(i + 1) + z.y) } - } - if (modulators.any { it.value.mixingBuffer === mixingBuffers[0] }) { - val floatBufferLo = mixingBuffers[0] - val floatView = mixingBuffers[1] - val z = Vector2f() - val w = Vector2f() - 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() + if (modulators.any { it.value.mixingBuffer === mixingBuffers[0] }) { + val floatBufferLo = mixingBuffers[0] + val floatView = mixingBuffers[1] + val z = Vector2f() + val w = Vector2f() + 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 + 1, floatView.get(i + 1) + z.y) } - w.x = minus100k.get() - w.y = -minus100k.get() - z.cmul(w) - floatView.put(i, floatView.get(i) + z.x) - floatView.put(i + 1, floatView.get(i + 1) + z.y) } - } - noiseGens.forEachIndexed { index, v -> - if (currentTask.noiseLevels[index] != 0f) { - v.generateNoise(currentTask.noiseLevels[index]) { outputBuffer -> - val mixingBuffer = mixingBuffers[1] - for (i in 0 until mixingBuffer.capacity()) { - mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + noiseGens.forEachIndexed { index, v -> + if (currentTask.noiseLevels[index] != 0f) { + v.generateNoise(currentTask.noiseLevels[index]) { outputBuffer -> + val mixingBuffer = mixingBuffers[1] + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + } } } } - } - noiseFloor.noiseBlock { outputBuffer -> - val mixingBuffer = mixingBuffers[1] - for (i in 0 until mixingBuffer.capacity()) { - mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + noiseFloor.noiseBlock { outputBuffer -> + val mixingBuffer = mixingBuffers[1] + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get()) + } } - } - demodulator.process(mixingBuffers[1], PirateRadioClient.stereo) { _, audioBuffer -> - // TODO stereo pilot - // we *want* backpressure - // FIXME use bigger buffers? - ReceiverAudioStream.bufferQueue.put(FloatBuffer.allocate(audioBuffer.capacity()).put(audioBuffer).clear()) + @Suppress("NAME_SHADOWING") + val nativeAudio = nativeAudio + demodulator.process(mixingBuffers[1], PirateRadioClient.stereo) { _, audioBuffer -> + // TODO stereo pilot + // we *want* backpressure + // FIXME use bigger buffers? + if (nativeAudio != null) { + while (audioBuffer.hasRemaining()) { + if (!outputBytes.hasRemaining()) { + val written = nativeAudio.write(outputBytes.array(), 0, outputBytes.capacity()) + outputBytes.position(written).compact() + if (written == 0) { + nativeAudio.start() + continue + } + } + val volume = PirateRadioClient.volume + outputBytes.putShort( + (MathHelper.clamp( + (audioBuffer.get() * 32767.5f - 0.5f).toInt(), -32768, 32767 + ) * volume * volume / 100).toShort() + ) + } + } else { + ReceiverAudioStream.bufferQueue.put( + FloatBuffer.allocate(audioBuffer.capacity()).put(audioBuffer).clear() + ) + } + } } + } catch (e: Throwable) { + PirateRadio.logger.error("Quitting FM simulation thread, as something went wrong!", e) + // for some reason it doesn't print stack trace but we still wanna propagate the exception to the thread + // itself + throw e } } -} \ No newline at end of file +} diff --git a/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt index f5fd729..8886498 100644 --- a/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt +++ b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt @@ -14,12 +14,16 @@ import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.ClickableWidget +import net.minecraft.client.gui.widget.CyclingButtonWidget +import net.minecraft.client.gui.widget.SliderWidget import net.minecraft.client.render.GameRenderer import net.minecraft.client.util.InputUtil +import net.minecraft.screen.ScreenTexts import net.minecraft.text.StringVisitable import net.minecraft.text.Text import net.minecraft.util.Colors import net.minecraft.util.Identifier +import net.minecraft.util.math.MathHelper import org.lwjgl.glfw.GLFW import org.slf4j.LoggerFactory import org.slf4j.event.Level @@ -29,6 +33,9 @@ import space.autistic.radio.client.PirateRadioClient import space.autistic.radio.client.fmsim.FmSimulatorMode import space.autistic.radio.wasm.Bindings.Companion.bindFunc import java.lang.invoke.MethodHandles +import java.util.* +import javax.sound.sampled.AudioSystem +import javax.sound.sampled.Mixer import kotlin.math.max import kotlin.reflect.jvm.javaMethod @@ -55,8 +62,9 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { private var textObjectsFree = ArrayList<Int>() private val frequencyPlusWidget = ButtonWidget.builder(Text.translatable("pirate-radio.frequency.plus")) { - if (InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_LEFT_SHIFT) - || InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT) + if (InputUtil.isKeyPressed( + MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_LEFT_SHIFT + ) || InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT) ) { PirateRadioClient.frequency += 10 } else { @@ -66,8 +74,9 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { .narrationSupplier { ButtonWidget.getNarrationMessage(Text.translatable("pirate-radio.frequency.plus.narrated")) } .build() private val frequencyMinusWidget = ButtonWidget.builder(Text.translatable("pirate-radio.frequency.minus")) { - if (InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_LEFT_SHIFT) - || InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT) + if (InputUtil.isKeyPressed( + MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_LEFT_SHIFT + ) || InputUtil.isKeyPressed(MinecraftClient.getInstance().window.handle, GLFW.GLFW_KEY_RIGHT_SHIFT) ) { PirateRadioClient.frequency -= 10 } else { @@ -87,12 +96,128 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { }.dimensions(20, 20, 20, 20) .narrationSupplier { ButtonWidget.getNarrationMessage(Text.translatable("pirate-radio.volume.minus.narrated")) } .build() + private var frequencySlider = makeFrequencySlider(0, 0, 320, 20) + private fun makeFrequencySlider(x: Int, y: Int, width: Int, height: Int) = object : SliderWidget( + x, y, width, height, ScreenTexts.EMPTY, (PirateRadioClient.frequency - 768).toDouble() / (1080.0 - 768.0) + ) { + init { + updateMessage() + } + + override fun updateMessage() { + message = Text.translatable( + "pirate-radio.frequency.selected", PirateRadioClient.frequency / 10, PirateRadioClient.frequency % 10 + ) + } + + override fun applyValue() { + PirateRadioClient.frequency = MathHelper.clampedLerp(768.0, 1080.0, value).toInt() + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val oldValue = this.value + val oldFrequency = PirateRadioClient.frequency + val changed = super.keyPressed(keyCode, scanCode, modifiers) + val bl = keyCode == GLFW.GLFW_KEY_LEFT + if (bl || keyCode == GLFW.GLFW_KEY_RIGHT) { + if (changed) { + val delta = if (bl) -1 else 1 + val newValue = (oldFrequency + delta - 768).toDouble() / (1080.0 - 768.0) + this.value = MathHelper.clamp(newValue, 0.0, 1.0) + if (oldValue != this.value) { + this.applyValue() + } + + this.updateMessage() + } + } + return changed + } + } + + private var volumeSlider = makeVolumeSlider(0, 20, 320, 20) + private fun makeVolumeSlider(x: Int, y: Int, width: Int, height: Int) = object : SliderWidget( + x, y, width, height, ScreenTexts.EMPTY, PirateRadioClient.volume.toDouble() / 10.0 + ) { + init { + updateMessage() + } + + override fun updateMessage() { + message = if (PirateRadioClient.volume == 0) { + Text.translatable("pirate-radio.volume.selected", Text.translatable("pirate-radio.volume.off")) + } else { + Text.translatable("pirate-radio.volume.selected", PirateRadioClient.volume) + } + } + + override fun applyValue() { + PirateRadioClient.volume = MathHelper.clampedLerp(0.0, 10.0, value).toInt() + } + + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val oldValue = this.value + val oldVolume = PirateRadioClient.volume + val changed = super.keyPressed(keyCode, scanCode, modifiers) + val bl = keyCode == GLFW.GLFW_KEY_LEFT + if (bl || keyCode == GLFW.GLFW_KEY_RIGHT) { + if (changed) { + val delta = if (bl) -1 else 1 + val newValue = (oldVolume + delta).toDouble() / 10.0 + this.value = MathHelper.clamp(newValue, 0.0, 1.0) + if (oldValue != this.value) { + this.applyValue() + } + + this.updateMessage() + } + } + return changed + } + } + private val toggleModes = ButtonWidget.builder(Text.translatable("pirate-radio.mode")) { PirateRadioClient.mode = when (PirateRadioClient.mode) { FmSimulatorMode.FULL -> FmSimulatorMode.FAST else -> FmSimulatorMode.FULL } }.position(0, 40).build() + private val cycleModes = CyclingButtonWidget.builder { info: FmSimulatorMode -> + when (info) { + FmSimulatorMode.FAST -> Text.translatable("pirate-radio.mode.fast") + FmSimulatorMode.FULL -> Text.translatable("pirate-radio.mode.full") + FmSimulatorMode.DEAF -> Text.translatable("pirate-radio.mode.deaf") + } + }.values( + listOf( + FmSimulatorMode.FULL, FmSimulatorMode.FAST + ) + ).initially(PirateRadioClient.mode).build(0, 40, 320, 20, Text.translatable("pirate-radio.mode")) { _, it -> + PirateRadioClient.mode = it + } + private val audioDevice = CyclingButtonWidget.builder { info: Mixer.Info -> + when (info) { + PirateRadioClient.minecraftAudioDevice -> Text.translatable("pirate-radio.device.system-matching-minecraft") + PirateRadioClient.systemDefaultAudioDevice -> Text.translatable("pirate-radio.device.system") + PirateRadioClient.openAlAudioDevice -> Text.translatable("pirate-radio.device.openal") + else -> Text.literal(info.description) + } + }.values( + listOf( + PirateRadioClient.minecraftAudioDevice, + PirateRadioClient.systemDefaultAudioDevice, + PirateRadioClient.openAlAudioDevice, + ), AudioSystem.getMixerInfo().filter { + AudioSystem.getMixer(it).sourceLineInfo.isNotEmpty() + }).initially(PirateRadioClient.audioDevice) + .build(0, 60, 320, 20, Text.translatable("pirate-radio.device.choose")) { _, it -> + PirateRadioClient.audioDevice = it + } + + private val buttons = arrayListOf<ClickableWidget>( + frequencyPlusWidget, frequencyMinusWidget, volumePlusWidget, volumeMinusWidget, toggleModes, audioDevice + ) override fun init() { if (failure == null && instance == null) { @@ -105,13 +230,8 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { } if (failure == null) { try { + setBaseLayout(0) instance!!.export("init").apply() - addDrawableChild(frequencyPlusWidget) - addDrawableChild(frequencyMinusWidget) - addDrawableChild(volumePlusWidget) - addDrawableChild(volumeMinusWidget) - addDrawableChild(toggleModes) - volumePlusWidget.visible } catch (e: ChicoryException) { failure = WasmScreenException("Skin failed to initialize", e) logger.error("Failed to initialize.", failure) @@ -287,6 +407,18 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { toggleModes.height = height } + private fun audioDeviceWidgetDimensions(x: Int, y: Int, width: Int, height: Int) { + audioDevice.setDimensionsAndPosition(width, height, x, y) + } + + private fun buttonsDimensions(index: Int, x: Int, y: Int, width: Int, height: Int): Boolean { + if (index < 0 || index >= buttons.size) { + return false + } + buttons[index].setDimensionsAndPosition(width, height, x, y) + return true + } + private fun getFrequency(): Int = PirateRadioClient.frequency private fun getMode(): Int = PirateRadioClient.mode.ordinal @@ -301,33 +433,61 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { private fun getHeight(): Int = height + private fun setBaseLayout(layout: Int) { + when (layout) { + 0 -> { + clearChildren() + addDrawableChild(frequencyPlusWidget) + addDrawableChild(frequencyMinusWidget) + addDrawableChild(volumePlusWidget) + addDrawableChild(volumeMinusWidget) + addDrawableChild(toggleModes) + addDrawableChild(audioDevice) + buttons.clear() + buttons.addAll( + listOf( + frequencyPlusWidget, + frequencyMinusWidget, + volumePlusWidget, + volumeMinusWidget, + toggleModes, + audioDevice + ) + ) + } + + 1 -> { + clearChildren() + frequencySlider = makeFrequencySlider( + frequencySlider.x, + frequencySlider.y, + frequencySlider.width, + frequencySlider.height + ) + volumeSlider = makeVolumeSlider(volumeSlider.x, volumeSlider.y, volumeSlider.width, volumeSlider.height) + addDrawableChild(frequencySlider) + addDrawableChild(volumeSlider) + addDrawableChild(cycleModes) + addDrawableChild(audioDevice) + cycleModes.value = PirateRadioClient.mode + buttons.clear() + buttons.addAll(listOf(frequencySlider, volumeSlider, cycleModes, audioDevice)) + } + + else -> throw WasmScreenException("Invalid base layout: $layout", null) + } + } + // useful for drawing the stereo pilot, not that it's implemented private fun drawImage( - red: Float, - green: Float, - blue: Float, - alpha: Float, - x: Int, - y: Int, - u: Float, - v: Float, - w: Int, - h: Int + red: Float, green: Float, blue: Float, alpha: Float, x: Int, y: Int, u: Float, v: Float, w: Int, h: Int ) { val drawContext = drawContext ?: throw WasmScreenException("Inappropriate draw call", null) RenderSystem.setShader(GameRenderer::getPositionTexProgram) RenderSystem.setShaderColor(red, green, blue, alpha) RenderSystem.setShaderTexture(0, TEXTURE) drawContext.drawTexture( - TEXTURE, - x, - y, - u, - v, - w, - h, - backgroundTextureWidth, - backgroundTextureHeight + TEXTURE, x, y, u, v, w, h, backgroundTextureWidth, backgroundTextureHeight ) RenderSystem.setShaderColor(1f, 1f, 1f, 1f) } @@ -444,6 +604,21 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { ) importValues.addFunction( bindFunc( + "audio-device-widget", "set-dimensions", lookup, this::audioDeviceWidgetDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "buttons", "set-dimensions", lookup, this::buttonsDimensions.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( + "buttons", "set-base-layout", lookup, this::setBaseLayout.javaMethod!!, this + ) + ) + importValues.addFunction( + bindFunc( "simulator", "get-frequency", lookup, this::getFrequency.javaMethod!!, this ) ) diff --git a/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt b/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt index a1dd5d5..6e5da3a 100644 --- a/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt +++ b/src/client/kotlin/space/autistic/radio/client/gui/StorageCardEditScreen.kt @@ -108,7 +108,7 @@ class StorageCardEditScreen( const val FREQ_WIDTH = 40 const val FREQ_HEIGHT = 20 - val FREQ_REGEX = Regex("^76\\.[8-9]|7[7-9]\\.[0-9]|[8-9][0-9]\\.[0-9]|10[0-7]\\.[0-9]|108\\.0$") + val FREQ_REGEX = Regex("^76\\.[8-9]|7[7-9]\\.[0-9]|[8-9][0-9]\\.[0-9]|100\\.0$") val FREQ_REGEX_CHARACTERS = Regex("^[0-9]*\\.?[0-9]?$") } diff --git a/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt index 5fb80bc..435a488 100644 --- a/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt +++ b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt @@ -1,6 +1,8 @@ package space.autistic.radio.client.sound +import com.dylibso.chicory.wasm.ChicoryException import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance +import net.minecraft.client.MinecraftClient import net.minecraft.client.network.ClientPlayerEntity import net.minecraft.client.sound.* import net.minecraft.sound.SoundCategory @@ -13,8 +15,10 @@ import space.autistic.radio.client.fmsim.FmFullThread import space.autistic.radio.client.fmsim.FmFullThread.trackedTransmitterQueue import space.autistic.radio.client.fmsim.FmSimulatorMode import space.autistic.radio.entity.DisposableTransmitterEntity +import space.autistic.radio.wasm.WasmExitException import java.lang.ref.WeakReference import java.nio.FloatBuffer +import java.util.UUID import java.util.concurrent.CompletableFuture import kotlin.math.PI @@ -47,7 +51,7 @@ class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingS // find relevant entities @Suppress("UNCHECKED_CAST") val trackedEntities: List<DisposableTransmitterEntity> = player.clientWorld.entities.filter { it.type == PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER } - .filter { (it as DisposableTransmitterEntity).frequency <= PirateRadioClient.frequency + 1 && it.frequency >= PirateRadioClient.frequency - 1 } + .filter { (it as DisposableTransmitterEntity).frequency <= PirateRadioClient.frequency + 1 && it.frequency >= PirateRadioClient.frequency - 1 && it.text.isNotEmpty() } .sortedBy { player.pos.squaredDistanceTo(it.pos) } as List<DisposableTransmitterEntity> val main = trackedEntities.filter { it.frequency == PirateRadioClient.frequency } .take(if (PirateRadioClient.mode == FmSimulatorMode.FAST) 1 else 2) @@ -68,32 +72,47 @@ class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingS noise + getPowerReceived(entity.pos.squaredDistanceTo(player.pos)) } // updated tracked transmitters - val trackedTransmitters: MutableMap<DisposableTransmitterEntity, TrackedTransmitter> = HashMap() - listOf(lower, main, upper).flatten().associateWithTo(trackedTransmitters) { + val trackedTransmitters: MutableMap<UUID, TrackedTransmitter> = HashMap() + listOf(lower, main, upper).flatten().associateTo(trackedTransmitters) { val text = it.text val audio = futuresCache[text]?.get() ?: CompletableFuture.supplyAsync { - lateinit var buffer: FloatBuffer - FliteWrapper.textToWave(text) { - buffer = FloatBuffer.allocate(it.capacity()) - while (it.hasRemaining()) { - val sample = (it.get().toFloat() + 0.5f) / 32767.5f - buffer.put(sample) + try { + lateinit var buffer: FloatBuffer + FliteWrapper.textToWave(text) { + buffer = FloatBuffer.allocate(it.capacity()) + while (it.hasRemaining()) { + val sample = (it.get().toFloat() + 0.5f) / 32767.5f + buffer.put(sample) + } } + buffer.array() + } catch (e: ChicoryException) { + floatArrayOf() + } catch (e: WasmExitException) { + floatArrayOf() } - buffer.array() } futuresCache[text] = WeakReference(audio) - TrackedTransmitter( + it.uuid to TrackedTransmitter( getPowerReceived(it.pos.squaredDistanceTo(player.pos)), it.age * (8000 / 20), audio, it.frequency - PirateRadioClient.frequency ) } + + + val audioOutput = PirateRadioClient.audioDevice + val minecraftSoundDevice = if (audioOutput === PirateRadioClient.minecraftAudioDevice) { + MinecraftClient.getInstance().options.soundDevice.value + } else { + null + } + // this can be empty but it is not EMPTY_TASK trackedTransmitterQueue.offer( FmFullThread.FmTask( - trackedTransmitters, floatArrayOf(lowerNoise, mainNoise, upperNoise) + trackedTransmitters, floatArrayOf(lowerNoise, mainNoise, upperNoise), audioOutput, minecraftSoundDevice ) ) volume = PirateRadioClient.volume.toFloat() / 10 diff --git a/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt index 274e727..34d5585 100644 --- a/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt +++ b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt @@ -21,6 +21,8 @@ object ReceiverAudioStream : BufferedAudioStream { ) ) + val useNativeAudio = System.getProperty("space.autistic.radio.output", "native") == "native" + private val skipBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2) get() = field.clear() diff --git a/src/client/kotlin/space/autistic/radio/client/util/LevenshteinDistance.kt b/src/client/kotlin/space/autistic/radio/client/util/LevenshteinDistance.kt new file mode 100644 index 0000000..6f04ce1 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/util/LevenshteinDistance.kt @@ -0,0 +1,54 @@ +//The MIT License (MIT) +// +//Copyright (c) 2024 +// +//Permission is hereby granted, free of charge, to any person obtaining a copy +//of this software and associated documentation files (the "Software"), to deal +//in the Software without restriction, including without limitation the rights +//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: +// +//The above copyright notice and this permission notice shall be included in +//all copies or substantial portions of the Software. +// +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//THE SOFTWARE. + +package space.autistic.radio.client.util + +// taken from https://github.com/Volcano-Bay-Studios/pavlovian-dogs/blob/master/src%2Fmain%2Fjava%2Fxyz%2Fvolcanobay%2Fpavloviandogs%2Futil%2FLevenshteinDistance.java +object LevenshteinDistance { + fun calculate(x: String, y: String): Int { + val dp = Array(x.length + 1) { IntArray(y.length + 1) } + + for (i in 0..x.length) { + for (j in 0..y.length) { + if (i == 0) { + dp[i][j] = j + } else if (j == 0) { + dp[i][j] = i + } else { + dp[i][j] = min( + dp[i - 1][j - 1] + costOfSubstitution(x[i - 1], y[j - 1]), dp[i - 1][j] + 1, dp[i][j - 1] + 1 + ) + } + } + } + + return dp[x.length][y.length] + } + + private fun costOfSubstitution(a: Char, b: Char): Int { + return if (a == b) 0 else 1 + } + + private fun min(vararg numbers: Int): Int { + return numbers.minOrNull() ?: Int.MAX_VALUE + } +} \ No newline at end of file diff --git a/src/client/resources/pirate-radio.client-mixins.json b/src/client/resources/pirate-radio.client-mixins.json new file mode 100644 index 0000000..27a5861 --- /dev/null +++ b/src/client/resources/pirate-radio.client-mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "space.autistic.radio.client.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "client": [ + "BlockStatesLoaderMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file |