diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2025-03-20 15:12:37 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2025-03-20 15:12:37 -0300 |
commit | 32f4966e8d3e824619fabbd225ee8e74b62be7c7 (patch) | |
tree | 1bd61a9459b32b48e81279e99c33180312f7d1a5 | |
parent | b1b2b791f55c808483e404e5c35045506432a400 (diff) |
Add a button to select audio output
9 files changed, 216 insertions, 39 deletions
diff --git a/gradle.properties b/gradle.properties index cbc5cac..080b6d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ loader_version=0.16.10 fabric_kotlin_version=1.13.1+kotlin.2.1.10 # Mod Properties -mod_version=1.0.3 +mod_version=1.0.4 maven_group=space.autistic.radio archives_base_name=pirate-radio diff --git a/gui/radio-receiver.c b/gui/radio-receiver.c index 136b45f..defd419 100644 --- a/gui/radio-receiver.c +++ b/gui/radio-receiver.c @@ -61,6 +61,8 @@ extern void fp_set_dimensions(int x, int y, int width, int height); extern void fm_set_dimensions(int x, int y, int width, int height); [[clang::import_module("toggle-widget")]] [[clang::import_name("set-dimensions")]] extern void mode_set_dimensions(int x, int y, int width, int height); +[[clang::import_module("audio-device-widget")]] [[clang::import_name("set-dimensions")]] +extern void audio_device_set_dimensions(int x, int y, int width, int height); #define BACKGROUND_WIDTH 200 #define BACKGROUND_HEIGHT 100 @@ -79,6 +81,7 @@ void init() { vp_set_dimensions(base_x + 100 - 40, base_y + 20, 20, 20); vm_set_dimensions(base_x + 120 - 40, base_y + 20, 20, 20); mode_set_dimensions(base_x + 100 - 100, base_y + 40, 100, 20); + audio_device_set_dimensions(base_x, base_y + 60, 200, 20); } void render(int mouseX, int mouseY, float delta) { 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/fmsim/FmFullThread.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt index d3b39b8..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,5 +1,6 @@ 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 @@ -7,22 +8,27 @@ 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 object FmFullThread : Runnable { class FmTask( 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) @@ -38,7 +44,7 @@ object FmFullThread : Runnable { private const val REPEAT_TIMEOUT = 8000 * 3 // default to 0.05s (1/20) - private val bufferSize = System.getProperty("space.autistic.radio.buffer.size", "").toIntOrNull() ?: 2400 + val bufferSize = System.getProperty("space.autistic.radio.buffer.size", "").toIntOrNull() ?: 2400 override fun run() { var currentTask = EMPTY_TASK @@ -59,12 +65,14 @@ object FmFullThread : Runnable { // for native audio only val outputBytes = ByteBuffer.allocate(bufferSize * 2 * 2).order(ByteOrder.LITTLE_ENDIAN) - val nativeAudio = if (ReceiverAudioStream.useNativeAudio) { - javax.sound.sampled.AudioSystem.getSourceDataLine(ReceiverAudioStream.format) + 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()) { @@ -73,6 +81,62 @@ object FmFullThread : Runnable { 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 + } + } + } + } + lastOutput = audioOutput + modulators.keys.retainAll(currentTask.trackedTransmitters.keys) currentTask.trackedTransmitters.forEach { (k, v) -> modulators.compute(k) { _, modulator -> @@ -190,14 +254,16 @@ object FmFullThread : Runnable { } } + @Suppress("NAME_SHADOWING") + val nativeAudio = nativeAudio demodulator.process(mixingBuffers[1], PirateRadioClient.stereo) { _, audioBuffer -> // TODO stereo pilot // we *want* backpressure // FIXME use bigger buffers? - if (ReceiverAudioStream.useNativeAudio) { + if (nativeAudio != null) { while (audioBuffer.hasRemaining()) { if (!outputBytes.hasRemaining()) { - val written = nativeAudio!!.write(outputBytes.array(), 0, outputBytes.capacity()) + val written = nativeAudio.write(outputBytes.array(), 0, outputBytes.capacity()) outputBytes.position(written).compact() if (written == 0) { nativeAudio.start() 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..e4ea8ea 100644 --- a/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt +++ b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt @@ -14,6 +14,7 @@ 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.render.GameRenderer import net.minecraft.client.util.InputUtil import net.minecraft.text.StringVisitable @@ -29,6 +30,10 @@ 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.collections.ArrayList import kotlin.math.max import kotlin.reflect.jvm.javaMethod @@ -55,8 +60,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 +72,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 { @@ -93,6 +100,28 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { else -> FmSimulatorMode.FULL } }.position(0, 40).build() + 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 = arrayOf( + frequencyPlusWidget, frequencyMinusWidget, volumePlusWidget, volumeMinusWidget, toggleModes, audioDevice + ) override fun init() { if (failure == null && instance == null) { @@ -111,7 +140,7 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { addDrawableChild(volumePlusWidget) addDrawableChild(volumeMinusWidget) addDrawableChild(toggleModes) - volumePlusWidget.visible + addDrawableChild(audioDevice) } catch (e: ChicoryException) { failure = WasmScreenException("Skin failed to initialize", e) logger.error("Failed to initialize.", failure) @@ -287,6 +316,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 @@ -303,31 +344,14 @@ class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { // 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 +468,16 @@ 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( "simulator", "get-frequency", lookup, this::getFrequency.javaMethod!!, this ) ) 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 98db4d6..435a488 100644 --- a/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt +++ b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt @@ -2,6 +2,7 @@ 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 @@ -99,10 +100,19 @@ class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingS 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 dc6d9b7..34d5585 100644 --- a/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt +++ b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt @@ -36,11 +36,7 @@ object ReceiverAudioStream : BufferedAudioStream { } override fun read(channelList: FloatConsumer): Boolean { - val buffer = if (useNativeAudio) { - bufferQueue.poll() ?: skipBuffer - } else { - skipBuffer - } + val buffer = bufferQueue.poll() ?: skipBuffer while (buffer.hasRemaining()) { channelList.accept(buffer.get()) } 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/main/resources/assets/pirate-radio/lang/en_us.json b/src/main/resources/assets/pirate-radio/lang/en_us.json index 81145f0..974deb9 100644 --- a/src/main/resources/assets/pirate-radio/lang/en_us.json +++ b/src/main/resources/assets/pirate-radio/lang/en_us.json @@ -26,5 +26,9 @@ "pirate-radio.frequency.selected": "Frequency: %s.%s MHz", "pirate-radio.storage-card": "SD Card", "pirate-radio.message": "Message...", - "pirate-radio.frequency.edit": "Frequency" + "pirate-radio.frequency.edit": "Frequency", + "pirate-radio.device.choose": "Audio output", + "pirate-radio.device.system": "Direct (System default)", + "pirate-radio.device.openal": "Minecraft sound system", + "pirate-radio.device.system-matching-minecraft": "Direct (guess from Minecraft)" } \ No newline at end of file |