diff options
Diffstat (limited to 'src')
41 files changed, 1239 insertions, 0 deletions
diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt new file mode 100644 index 0000000..54b7640 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt @@ -0,0 +1,35 @@ +package space.autistic.radio.client + +import com.mojang.brigadier.CommandDispatcher +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry +import net.minecraft.client.MinecraftClient +import net.minecraft.command.CommandRegistryAccess +import org.slf4j.LoggerFactory +import space.autistic.radio.PirateRadio +import space.autistic.radio.PirateRadio.MOD_ID +import space.autistic.radio.PirateRadioEntityTypes +import space.autistic.radio.client.entity.ElectronicsTraderEntityRenderer +import space.autistic.radio.client.gui.FmReceiverScreen + +object PirateRadioClient : ClientModInitializer { + private val logger = LoggerFactory.getLogger(MOD_ID) + + override fun onInitializeClient() { + EntityRendererRegistry.register(PirateRadioEntityTypes.ELECTRONICS_TRADER, ::ElectronicsTraderEntityRenderer) + PirateRadioEntityModelLayers.initialize() + ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ -> + dispatcher.register( + ClientCommandManager.literal("fm").executes { + it.source.client.send { + it.source.client.setScreen(FmReceiverScreen()) + } + 0 + } + ) + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt new file mode 100644 index 0000000..b5130a1 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioDataGenerator.kt @@ -0,0 +1,62 @@ +package space.autistic.radio.client + +import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint +import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput +import net.fabricmc.fabric.api.datagen.v1.provider.FabricModelProvider +import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider +import net.minecraft.data.client.BlockStateModelGenerator +import net.minecraft.data.client.ItemModelGenerator +import net.minecraft.data.client.Models +import net.minecraft.data.server.recipe.RecipeExporter +import net.minecraft.data.server.recipe.RecipeProvider +import net.minecraft.data.server.recipe.ShapelessRecipeJsonBuilder +import net.minecraft.item.ItemStack +import net.minecraft.recipe.Ingredient +import net.minecraft.recipe.Recipe +import net.minecraft.recipe.ShapelessRecipe +import net.minecraft.recipe.book.CraftingRecipeCategory +import net.minecraft.recipe.book.RecipeCategory +import net.minecraft.registry.RegistryWrapper +import net.minecraft.util.Identifier +import net.minecraft.util.collection.DefaultedList +import space.autistic.radio.PirateRadio.MOD_ID +import space.autistic.radio.PirateRadioItems +import java.util.concurrent.CompletableFuture + +class PirateRadioItemModelGenerator(output: FabricDataOutput) : FabricModelProvider(output) { + override fun generateBlockStateModels(modelGenerator: BlockStateModelGenerator) { + } + + override fun generateItemModels(modelGenderator: ItemModelGenerator) { + modelGenderator.register(PirateRadioItems.SBC, Models.GENERATED) + modelGenderator.register(PirateRadioItems.WIRE, Models.GENERATED) + modelGenderator.register(PirateRadioItems.POWERBANK, Models.GENERATED) + modelGenderator.register(PirateRadioItems.FM_RECEIVER, Models.GENERATED) + modelGenderator.register(PirateRadioItems.STORAGE_CARD, Models.GENERATED) + modelGenderator.register(PirateRadioItems.DISPOSABLE_TRANSMITTER, Models.GENERATED) + } + +} + +class PirateRadioRecipeGenerator( + output: FabricDataOutput?, + registriesFuture: CompletableFuture<RegistryWrapper.WrapperLookup>? +) : FabricRecipeProvider(output, registriesFuture) { + override fun generate(exporter: RecipeExporter) { + ShapelessRecipeJsonBuilder.create(RecipeCategory.MISC, PirateRadioItems.DISPOSABLE_TRANSMITTER) + .input(PirateRadioItems.SBC).input(PirateRadioItems.WIRE).input(PirateRadioItems.POWERBANK) + .input(PirateRadioItems.STORAGE_CARD) + .criterion("has_sbc", RecipeProvider.conditionsFromItem(PirateRadioItems.SBC)).offerTo(exporter) + } + +} + +object PirateRadioDataGenerator : DataGeneratorEntrypoint { + override fun onInitializeDataGenerator(fabricDataGenerator: FabricDataGenerator) { + val pack = fabricDataGenerator.createPack() + + pack.addProvider(::PirateRadioItemModelGenerator) + pack.addProvider(::PirateRadioRecipeGenerator) + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt new file mode 100644 index 0000000..765912d --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioEntityModelLayers.kt @@ -0,0 +1,18 @@ +package space.autistic.radio.client + +import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry +import net.minecraft.client.model.TexturedModelData +import net.minecraft.client.render.entity.model.EntityModelLayer +import net.minecraft.client.render.entity.model.VillagerResemblingModel +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio + +object PirateRadioEntityModelLayers { + val ELECTRONICS_TRADER = EntityModelLayer(Identifier.of(PirateRadio.MOD_ID, "electronics-trader"), "main") + + fun initialize() { + EntityModelLayerRegistry.registerModelLayer(ELECTRONICS_TRADER) { + TexturedModelData.of(VillagerResemblingModel.getModelData(), 64, 64) + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt new file mode 100644 index 0000000..74a7c96 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModel.kt @@ -0,0 +1,19 @@ +package space.autistic.radio.client.antenna + +import org.joml.Vector3d + +interface AntennaModel { + /** + * Returns the linear power level/gain to apply for a receiver at the given position. The receiver is assumed to be + * vertically oriented. + * + * Note: 1.0f = 0dB, 0.5f = -3dB (approx.), 0.1f = -10dB. + */ + fun apply(position: Vector3d): Float + + /** + * Returns whether to process block/material attenuation. Useful for "global" antennas (i.e. those that return a + * constant for all positions given to [apply]). + */ + fun shouldAttenuate(): Boolean +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModelFactory.kt b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModelFactory.kt new file mode 100644 index 0000000..33a7087 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/AntennaModelFactory.kt @@ -0,0 +1,7 @@ +package space.autistic.radio.client.antenna + +import org.joml.Quaterniond + +interface AntennaModelFactory { + fun create(orientation: Quaterniond): AntennaModel +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt b/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt new file mode 100644 index 0000000..3c188b6 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/NullModel.kt @@ -0,0 +1,13 @@ +package space.autistic.radio.client.antenna + +import org.joml.Vector3d + +class NullModel : AntennaModel { + override fun apply(position: Vector3d): Float { + return 0f + } + + override fun shouldAttenuate(): Boolean { + return false + } +} diff --git a/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt b/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt new file mode 100644 index 0000000..7181e95 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/antenna/WasmAntennaFactory.kt @@ -0,0 +1,97 @@ +package space.autistic.radio.client.antenna + +import com.dylibso.chicory.experimental.aot.AotMachineFactory +import com.dylibso.chicory.runtime.ExportFunction +import com.dylibso.chicory.runtime.ImportValues +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.wasm.ChicoryException +import com.dylibso.chicory.wasm.InvalidException +import com.dylibso.chicory.wasm.Parser +import com.dylibso.chicory.wasm.types.MemoryLimits +import com.dylibso.chicory.wasm.types.Value +import com.dylibso.chicory.wasm.types.ValueType +import org.joml.Quaterniond +import org.joml.Vector3d +import space.autistic.radio.PirateRadio +import java.util.logging.Level +import java.util.logging.Logger + +class WasmAntennaFactory(moduleBytes: ByteArray) : AntennaModelFactory { + var failing = false + private val instanceBuilder = run { + try { + val module = Parser.parse(moduleBytes) + Instance.builder(module).withMachineFactory(AotMachineFactory(module)).withImportValues(defaultImports) + // capped at 1MB per antenna + .withMemoryLimits(MemoryLimits(0, 16)) + } catch (e: ChicoryException) { + logger.log(Level.SEVERE, "Error while trying to parse antenna model.", e) + failing = true + null + } + } + + override fun create(orientation: Quaterniond): AntennaModel { + if (failing) { + return NullModel() + } + try { + val instance = instanceBuilder!!.build() + // see basic module abi convention: https://github.com/WebAssembly/tool-conventions/blob/4487bbc2f5a0ad6b5ca76e233bdfa5ed4513dd8c/BasicModuleABI.md + var initialize: ExportFunction? = null + try { + initialize = instance.export("_initialize") + } catch (_: InvalidException) { + // export may not exist, it's fine + } + initialize?.apply() + // initialize antenna orientation + instance.export("set-orientation").apply( + orientation.x.toRawBits(), + orientation.y.toRawBits(), + orientation.z.toRawBits(), + orientation.w.toRawBits() + ) + if (instance.exports().global("should-attenuate").type != ValueType.I32) { + logger.log( + Level.SEVERE, "Error while trying to initialize antenna model: missing 'should-attenuate'" + ) + failing = true + return NullModel() + } + val shouldAttenuate = instance.exports().global("should-attenuate").value != 0L + val apply = instance.export("apply") + return object : AntennaModel { + override fun apply(position: Vector3d): Float { + if (failing) { + return 0f + } + try { + return Value.longToFloat( + apply.apply( + position.x.toRawBits(), position.y.toRawBits(), position.z.toRawBits() + )[0] + ) + } catch (e: ChicoryException) { + logger.log(Level.SEVERE, "Error while trying to evaluate antenna model.", e) + failing = true + return 0f + } + } + + override fun shouldAttenuate(): Boolean { + return shouldAttenuate + } + } + } catch (e: ChicoryException) { + logger.log(Level.SEVERE, "Error while trying to initialize antenna model.", e) + failing = true + return NullModel() + } + } + + companion object { + private val defaultImports = ImportValues.builder().build() + private val logger = Logger.getLogger(PirateRadio.MOD_ID) + } +} \ No newline at end of file diff --git a/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt b/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt new file mode 100644 index 0000000..91c29db --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/entity/ElectronicsTraderEntityRenderer.kt @@ -0,0 +1,23 @@ +package space.autistic.radio.client.entity + +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.MobEntityRenderer +import net.minecraft.client.render.entity.model.VillagerResemblingModel +import net.minecraft.util.Identifier +import space.autistic.radio.PirateRadio +import space.autistic.radio.client.PirateRadioEntityModelLayers +import space.autistic.radio.entity.ElectronicsTraderEntity + +class ElectronicsTraderEntityRenderer(context: EntityRendererFactory.Context) : + MobEntityRenderer<ElectronicsTraderEntity, VillagerResemblingModel<ElectronicsTraderEntity>>( + context, + VillagerResemblingModel(context.getPart(PirateRadioEntityModelLayers.ELECTRONICS_TRADER)), + 0.5f + ) { + + companion object { + val TEXTURE = Identifier.of(PirateRadio.MOD_ID, "electronics-trader") + } + + override fun getTexture(entity: ElectronicsTraderEntity?): Identifier = TEXTURE +} \ 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 new file mode 100644 index 0000000..4bd4db2 --- /dev/null +++ b/src/client/kotlin/space/autistic/radio/client/gui/FmReceiverScreen.kt @@ -0,0 +1,11 @@ +package space.autistic.radio.client.gui + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text + +class FmReceiverScreen : Screen(Text.translatable("pirate-radio.fm-receiver")) { + + override fun init() { + // TODO + } +} \ No newline at end of file diff --git a/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b b/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b new file mode 100644 index 0000000..072c021 --- /dev/null +++ b/src/main/generated/.cache/4145a4ade350d062a154f42d7ad0d98fb52bf04b @@ -0,0 +1,3 @@ +// 1.21.1 2025-02-09T00:02:42.294183715 Pirate Radio/Recipes +84f8cd2b2d9d1afcf2a5cf000905c264a6d8267c data/pirate-radio/recipe/disposable-transmitter.json +86e73a1d034dc407ce65e0e61af19b1db43e1939 data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json diff --git a/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 b/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 new file mode 100644 index 0000000..cf1f8c7 --- /dev/null +++ b/src/main/generated/.cache/bd1ee27e4c10ec669c0e0894b64dd83a58902c72 @@ -0,0 +1,7 @@ +// 1.21.1 2025-02-09T00:02:42.294917543 Pirate Radio/Model Definitions +3507512497435bf1047ebd71ae1f4881ceb67f44 assets/pirate-radio/models/item/fm-receiver.json +ab60b602066c94b5746065e1b139a383a6c26429 assets/pirate-radio/models/item/powerbank.json +fb8af1b0939020c3a89a7736e47d9f688b38a2c9 assets/pirate-radio/models/item/storage-card.json +dbc04d664dacd99a76580bcff2c5b944abb0730e assets/pirate-radio/models/item/sbc.json +4ec0ecb715a1eec2f90f47221614e09a4c5b8f65 assets/pirate-radio/models/item/disposable-transmitter.json +2d14f0908eb7b92790cb29b141e4150c2d1f4a16 assets/pirate-radio/models/item/wire.json diff --git a/src/main/generated/assets/pirate-radio/models/item/disposable-transmitter.json b/src/main/generated/assets/pirate-radio/models/item/disposable-transmitter.json new file mode 100644 index 0000000..5eda62b --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/disposable-transmitter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/disposable-transmitter" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json b/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json new file mode 100644 index 0000000..71813c4 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/fm-receiver.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/fm-receiver" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/powerbank.json b/src/main/generated/assets/pirate-radio/models/item/powerbank.json new file mode 100644 index 0000000..90149f7 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/powerbank.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/powerbank" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/sbc.json b/src/main/generated/assets/pirate-radio/models/item/sbc.json new file mode 100644 index 0000000..caa25b1 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/sbc.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/sbc" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/storage-card.json b/src/main/generated/assets/pirate-radio/models/item/storage-card.json new file mode 100644 index 0000000..6b56c92 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/storage-card.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/storage-card" + } +} \ No newline at end of file diff --git a/src/main/generated/assets/pirate-radio/models/item/wire.json b/src/main/generated/assets/pirate-radio/models/item/wire.json new file mode 100644 index 0000000..8c26725 --- /dev/null +++ b/src/main/generated/assets/pirate-radio/models/item/wire.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "pirate-radio:item/wire" + } +} \ No newline at end of file diff --git a/src/main/generated/data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json b/src/main/generated/data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json new file mode 100644 index 0000000..fca182d --- /dev/null +++ b/src/main/generated/data/pirate-radio/advancement/recipes/misc/disposable-transmitter.json @@ -0,0 +1,32 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_sbc": { + "conditions": { + "items": [ + { + "items": "pirate-radio:sbc" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "pirate-radio:disposable-transmitter" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_sbc" + ] + ], + "rewards": { + "recipes": [ + "pirate-radio:disposable-transmitter" + ] + } +} \ No newline at end of file diff --git a/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json b/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json new file mode 100644 index 0000000..2a1d645 --- /dev/null +++ b/src/main/generated/data/pirate-radio/recipe/disposable-transmitter.json @@ -0,0 +1,22 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + { + "item": "pirate-radio:sbc" + }, + { + "item": "pirate-radio:wire" + }, + { + "item": "pirate-radio:powerbank" + }, + { + "item": "pirate-radio:storage-card" + } + ], + "result": { + "count": 1, + "id": "pirate-radio:disposable-transmitter" + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadio.kt b/src/main/kotlin/space/autistic/radio/PirateRadio.kt new file mode 100644 index 0000000..54d0b9f --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadio.kt @@ -0,0 +1,17 @@ +package space.autistic.radio + +import net.fabricmc.api.ModInitializer +import org.slf4j.LoggerFactory + +object PirateRadio : ModInitializer { + const val MOD_ID = "pirate-radio" + private val logger = LoggerFactory.getLogger(MOD_ID) + + override fun onInitialize() { + logger.info("This project is made with love by a queer trans person.\n" + + "The folks of these identities who have contributed to the project would like to make their identities known:\n" + + "Autgender; Not a person; Anticapitalist; Genderqueer; Trans.") + PirateRadioItems.initialize() + PirateRadioEntityTypes.initialize() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt b/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt new file mode 100644 index 0000000..f147394 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioEntityTypes.kt @@ -0,0 +1,26 @@ +package space.autistic.radio + +import net.fabricmc.fabric.api.`object`.builder.v1.entity.FabricDefaultAttributeRegistry +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnGroup +import net.minecraft.entity.mob.MobEntity +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier +import space.autistic.radio.entity.ElectronicsTraderEntity + +object PirateRadioEntityTypes { + val ELECTRONICS_TRADER_KEY = RegistryKey.of(RegistryKeys.ENTITY_TYPE, Identifier.of(PirateRadio.MOD_ID, "electronics-trader")) + val ELECTRONICS_TRADER = register(EntityType.Builder.create(::ElectronicsTraderEntity, SpawnGroup.MISC).dimensions(0.6F, 1.95F).eyeHeight(1.62F).maxTrackingRange(10), ELECTRONICS_TRADER_KEY) + + fun <T : Entity> register(entityTypeBuilder: EntityType.Builder<T>, registryKey: RegistryKey<EntityType<*>>): EntityType<T> { + return Registry.register(Registries.ENTITY_TYPE, registryKey.value, entityTypeBuilder.build(registryKey.value.path)) + } + + fun initialize() { + FabricDefaultAttributeRegistry.register(ELECTRONICS_TRADER, MobEntity.createMobAttributes()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt b/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt new file mode 100644 index 0000000..490acaf --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/PirateRadioItems.kt @@ -0,0 +1,38 @@ +package space.autistic.radio + +import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents +import net.minecraft.item.Item +import net.minecraft.item.ItemGroups +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier + +object PirateRadioItems { + val SBC_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "sbc")) + val SBC = register(Item(Item.Settings()), SBC_KEY) + val WIRE_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "wire")) + val WIRE = register(Item(Item.Settings()), WIRE_KEY) + val POWERBANK_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "powerbank")) + val POWERBANK = register(Item(Item.Settings()), POWERBANK_KEY) + val STORAGE_CARD_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "storage-card")) + val STORAGE_CARD = register(Item(Item.Settings()), STORAGE_CARD_KEY) + val DISPOSABLE_TRANSMITTER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "disposable-transmitter")) + val DISPOSABLE_TRANSMITTER = register(Item(Item.Settings()), DISPOSABLE_TRANSMITTER_KEY) + val FM_RECEIVER_KEY = RegistryKey.of(RegistryKeys.ITEM, Identifier.of(PirateRadio.MOD_ID, "fm-receiver")) + val FM_RECEIVER = register(Item(Item.Settings()), FM_RECEIVER_KEY) + + fun register(item: Item, registryKey: RegistryKey<Item>): Item { + return Registry.register(Registries.ITEM, registryKey.value, item) + } + + fun initialize() { + ItemGroupEvents.modifyEntriesEvent(ItemGroups.INGREDIENTS).register { + it.add(SBC) + it.add(WIRE) + it.add(POWERBANK) + it.add(STORAGE_CARD) + } + } +} \ No newline at end of file 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 diff --git a/src/main/kotlin/space/autistic/radio/complex/Complex.kt b/src/main/kotlin/space/autistic/radio/complex/Complex.kt new file mode 100644 index 0000000..918dac2 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/complex/Complex.kt @@ -0,0 +1,32 @@ +package space.autistic.radio.complex + +import org.joml.Vector2f +import org.joml.Vector2fc + +fun Vector2f.cmul(v: Vector2fc): Vector2f { + return this.cmul(v, this) +} + +fun Vector2f.cmul(v: Vector2fc, dest: Vector2f): Vector2f { + val a = this.x * v.x() + val b = this.y * v.y() + val c = (this.x() + this.y()) * (v.x() + v.y()) + val x = a - b + val y = c - a - b + dest.x = x + dest.y = y + return dest +} + +fun Vector2f.conjugate(): Vector2f { + return this.conjugate(this) +} + +fun Vector2f.conjugate(dest: Vector2f): Vector2f { + dest.x = this.x() + dest.y = -this.y() + return dest +} + +val I + get() = Vector2f(0f, 1f) \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt b/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt new file mode 100644 index 0000000..8f86218 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/dsp/Biquad1stOrder.kt @@ -0,0 +1,11 @@ +package space.autistic.radio.dsp + +class Biquad1stOrder(private val b0: Float, private val b1: Float, private val a1: Float) { + private var delaySlot = 0f + + fun process(samp: Float): Float { + val out = samp * b0 + delaySlot + delaySlot = samp * b1 - out * a1 + return out + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt b/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt new file mode 100644 index 0000000..3aa53b1 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/entity/ElectronicsTraderEntity.kt @@ -0,0 +1,36 @@ +package space.autistic.radio.entity + +import net.minecraft.entity.EntityType +import net.minecraft.entity.ai.goal.HoldInHandsGoal +import net.minecraft.entity.passive.WanderingTraderEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.village.TradeOffer +import net.minecraft.village.TradedItem +import net.minecraft.world.World +import space.autistic.radio.PirateRadioItems + +class ElectronicsTraderEntity(entityType: EntityType<out ElectronicsTraderEntity>, world: World) : + WanderingTraderEntity(entityType, world) { + + override fun initGoals() { + super.initGoals() + goalSelector.goals.removeIf { it.goal is HoldInHandsGoal<*> } + } + + override fun fillRecipes() { + val offers = this.getOffers() + offers.add(TradeOffer(TradedItem(Items.EMERALD, 5), ItemStack(PirateRadioItems.POWERBANK), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 10), ItemStack(PirateRadioItems.FM_RECEIVER), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 15), ItemStack(PirateRadioItems.SBC), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 5), ItemStack(PirateRadioItems.STORAGE_CARD), 3, 0, 0f)) + offers.add(TradeOffer(TradedItem(Items.EMERALD, 1), ItemStack(PirateRadioItems.WIRE), 3, 0, 0f)) + } + + override fun tickMovement() { + if (!this.world.isClient) { + super.setDespawnDelay(1000) + } + super.tickMovement() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt new file mode 100644 index 0000000..5874166 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/fmsim/FmFullConstants.kt @@ -0,0 +1,109 @@ +package space.autistic.radio.fmsim + +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.sin + +object FmFullConstants { + // tau = 75us, fh = 20396.25Hz + const val FM_PREEMPAHSIS_B0_48K = 6.7639647f + const val FM_PREEMPHASIS_B1_48K = -4.975628f + + /* const val FM_PREEMPHASIS_A0_48K = 1f */ + const val FM_PREEMPHASIS_A1_48K = 0.78833646f + + const val FM_DEEMPAHSIS_B0_48K = 1f / FM_PREEMPAHSIS_B0_48K + const val FM_DEEMPHASIS_B1_48K = FM_PREEMPHASIS_A1_48K / FM_PREEMPAHSIS_B0_48K + + /* const val FM_DEEMPHASIS_A0_48K = 1f */ + const val FM_DEEMPHASIS_A1_48K = FM_PREEMPHASIS_B1_48K / FM_PREEMPAHSIS_B0_48K + + val FIR_LPF_48K_15K_3K1 = floatArrayOf( + -0.0010006913216784596f, + 0.001505308784544468f, + -2.625857350794219e-18f, + -0.002777613466605544f, + 0.0030173989944159985f, + 0.002290070755407214f, + -0.008225799538195133f, + 0.004239063244313002f, + 0.010359899140894413f, + -0.017650796100497246f, + 1.510757873119297e-17f, + 0.029305754229426384f, + -0.02889496460556984f, + -0.020366130396723747f, + 0.07103750854730606f, + -0.03811456635594368f, + -0.10945471376180649f, + 0.29212409257888794f, + 0.6252123713493347f, + 0.29212409257888794f, + -0.10945471376180649f, + -0.03811456635594368f, + 0.07103750854730606f, + -0.020366130396723747f, + -0.02889496460556984f, + 0.029305754229426384f, + 1.510757873119297e-17f, + -0.017650796100497246f, + 0.010359899140894413f, + 0.004239063244313002f, + -0.008225799538195133f, + 0.002290070755407214f, + 0.0030173989944159985f, + -0.002777613466605544f, + -2.625857350794219e-18f, + 0.001505308784544468f, + -0.0010006913216784596f, + ) + + // chosen such that we can easily do 38kHz mixing in frequency (750*38k/300k = shift of 95 bins, where 750 comes + // from the 4/25 ratio 48k/300k i.e. 120*25/4) + // (the theoretical optimum, as per above, would be around 180) + // (we could have fudged the carrier frequency a bit but we chose not to) + // NOTE: latency = (data block size / 48000) seconds (84 -> 1.75 ms) + const val FFT_SIZE_LPF_48K_15K_3K1 = 120 + const val FFT_OVERLAP_LPF_48K_15K_3K1 = 36 + const val FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 = FFT_SIZE_LPF_48K_15K_3K1 - FFT_OVERLAP_LPF_48K_15K_3K1 + + init { + assert(FFT_OVERLAP_LPF_48K_15K_3K1 >= FIR_LPF_48K_15K_3K1.size - 1) + } + + const val DECIMATION_48K_300K = 4 + const val INTERPOLATION_48K_300K = 25 + + const val IFFT_SIZE_48K_300K = FFT_SIZE_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K + const val IFFT_OVERLAP_48K_300K = FFT_OVERLAP_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K + const val IFFT_DATA_BLOCK_SIZE_48K_300K = IFFT_SIZE_48K_300K - IFFT_OVERLAP_48K_300K + + // how many bins to shift for 38kHz mixing + // assuming FFT_SIZE_LPF_48K_15K_3K1 *bins* (complex) + // 19 / 150 is the ratio between 38k/300k + const val FREQUENCY_MIXING_BINS_38K = + FFT_SIZE_LPF_48K_15K_3K1 * INTERPOLATION_48K_300K / DECIMATION_48K_300K * 19 / 150 + + // a single cycle of a 19kHz signal takes (1/19k)/(1/300k) or 300k/19k samples. + // since that number isn't exact, buffer an entire 19 cycles. + const val BUFFER_SIZE_19K_300K = 300 + + val BUFFER_19K_300K = FloatArray(BUFFER_SIZE_19K_300K) { + 0.1f * sin(2 * PI * 19000.0 * it.toDouble() / 300000.0).toFloat() + } + + // we want a carrier deviation of +-75kHz, at a sampling rate of 300kHz + const val CORRECTION_FACTOR = (75000.0 / (300000.0 / (2.0 * PI))).toFloat() + + // these are used for "low/high" mixing + const val CBUFFER_SIZE_100K_300K = 3 + + val CBUFFER_100K_300K = FloatArray(2 * CBUFFER_SIZE_100K_300K) { + val index = it / 2 + if (it and 1 == 0) { + 1f * sin(2 * PI * 100000.0 * index.toDouble() / 300000.0).toFloat() + } else { + 1f * cos(2 * PI * 100000.0 * index.toDouble() / 300000.0).toFloat() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt new file mode 100644 index 0000000..654d50f --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/fmsim/FmFullMixer.kt @@ -0,0 +1,4 @@ +package space.autistic.radio.fmsim + +class FmFullMixer { +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt b/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt new file mode 100644 index 0000000..96ad186 --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/fmsim/FmFullModulator.kt @@ -0,0 +1,176 @@ +package space.autistic.radio.fmsim + +import org.joml.Vector2f +import space.autistic.radio.complex.cmul +import space.autistic.radio.complex.conjugate +import space.autistic.radio.dsp.Biquad1stOrder +import java.nio.FloatBuffer +import java.util.function.Consumer +import org.jtransforms.fft.FloatFFT_1D +import kotlin.math.max +import kotlin.math.min + +class FmFullModulator { + private val leftPlusRight = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val leftMinusRight = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val biquadLeft = Biquad1stOrder( + FmFullConstants.FM_PREEMPAHSIS_B0_48K, + FmFullConstants.FM_PREEMPHASIS_B1_48K, + FmFullConstants.FM_PREEMPHASIS_A1_48K + ) + private val biquadRight = Biquad1stOrder( + FmFullConstants.FM_PREEMPAHSIS_B0_48K, + FmFullConstants.FM_PREEMPHASIS_B1_48K, + FmFullConstants.FM_PREEMPHASIS_A1_48K + ) + private val fft48kBuffer = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val fir48kLpf = FloatBuffer.allocate(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + private val mixingBuffer = FloatBuffer.allocate(FmFullConstants.IFFT_SIZE_48K_300K) + private val outputBuffer = FloatBuffer.allocate(2 * FmFullConstants.IFFT_DATA_BLOCK_SIZE_48K_300K) + private val stereoPilot = FloatBuffer.wrap(FmFullConstants.BUFFER_19K_300K) + + private var cycle = -1f + private var lastSum = 0f + + init { + fir48kLpf.put(0, FmFullConstants.FIR_LPF_48K_15K_3K1) + Companion.fft48k.realForward(fir48kLpf.array()) + + // pre-pad the buffers + while (leftPlusRight.position() < FmFullConstants.FFT_OVERLAP_LPF_48K_15K_3K1) { + leftPlusRight.put(0f) + leftMinusRight.put(0f) + } + } + + /** + * Takes in samples at 48kHz, interleaved stereo and processes them for output. + * + * Calls consumer with processed samples in I/Q format. + */ + fun process(input: FloatBuffer, power: Float, consumer: Consumer<FloatBuffer>) { + while (input.remaining() >= 2) { + while (input.remaining() >= 2 && leftPlusRight.hasRemaining()) { + // FIXME AGC (currently clamping/clipping) + val left = min(max(biquadLeft.process(input.get()), -1f), 1f) + val right = min(max(biquadRight.process(input.get()), -1f), 1f) + leftPlusRight.put(left + right) + leftMinusRight.put(left - right) + } + if (!leftPlusRight.hasRemaining()) { + // zero the mixing buffer + for (i in 0 until mixingBuffer.capacity()) { + mixingBuffer.put(i, 0f) + } + fft48kBuffer.put(0, leftPlusRight, 0, FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + Companion.fft48k.realForward(fft48kBuffer.array()) + fft48kBuffer.array().forEachIndexed { index, fl -> + fft48kBuffer.put( + index, + 0.4f / FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 * fl + ) + } + val z = Vector2f() + val w = Vector2f() + for (i in 2 until FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 step 2) { + z.x = fft48kBuffer.get(i) + z.y = fft48kBuffer.get(i + 1) + w.x = fir48kLpf.get(i) + w.y = fir48kLpf.get(i + 1) + z.cmul(w) + fft48kBuffer.put(i, z.x) + fft48kBuffer.put(i + 1, z.y) + } + fft48kBuffer.put(0, fft48kBuffer.get(0) * fir48kLpf.get(0)) + fft48kBuffer.put(1, fft48kBuffer.get(1) * fir48kLpf.get(1)) + // copy only around 19kHz of bandwidth + mixingBuffer.put(0, fft48kBuffer, 0, FmFullConstants.FREQUENCY_MIXING_BINS_38K or 1) + // zero out nyquist frequency bucket + mixingBuffer.put(1, 0f) + fft48kBuffer.put(0, leftMinusRight, 0, FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1) + Companion.fft48k.realForward(fft48kBuffer.array()) + fft48kBuffer.array().forEachIndexed { index, fl -> + fft48kBuffer.put( + index, + 0.2f / FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 * fl + ) + } + for (i in 2 until FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1 step 2) { + z.x = fft48kBuffer.get(i) + z.y = fft48kBuffer.get(i + 1) + w.x = fir48kLpf.get(i) + w.y = fir48kLpf.get(i + 1) + z.cmul(w) + fft48kBuffer.put(i, z.x) + fft48kBuffer.put(i + 1, z.y) + } + fft48kBuffer.put(0, fft48kBuffer.get(0) * fir48kLpf.get(0)) + // (unnecessary) + //fft48kBuffer.put(1, fft48kBuffer.get(1) * fir48kLpf.get(1)) + mixingBuffer.put( + FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2 + 2, + fft48kBuffer, + 2, + // number of floats to copy + // bins are complex, so this halves the bins (~19kHz bandwidth) + // length should be even (for an exact number of complex bins) + FmFullConstants.FREQUENCY_MIXING_BINS_38K and 1.inv() + ) + // the actual 38k bin is at this offset, account for jt convention (buf[0 until 3] = R0,Rn,R1) + mixingBuffer.put(FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2, fft48kBuffer.get(0)) + val base = FmFullConstants.FREQUENCY_MIXING_BINS_38K * 2 + // phase correction factor (due to dropping 150 bins) + // TODO figure out if phase is correct + cycle = -cycle + // bandwidth we care about is about half of 38k, so just, well, half it + for (i in 2 until FmFullConstants.FREQUENCY_MIXING_BINS_38K step 2) { + z.x = mixingBuffer.get(base + i) + z.y = mixingBuffer.get(base + i + 1) + // we also need the conjugate + z.conjugate(w) + mixingBuffer.put(base + i, z.y * -cycle) + mixingBuffer.put(base + i + 1, z.x * cycle) + mixingBuffer.put(base - i, mixingBuffer.get(base - i - 2) - w.y * cycle) + mixingBuffer.put(base - i + 1, mixingBuffer.get(base - i - 1) + w.x * cycle) + } + // handle 38kHz itself + z.x = mixingBuffer.get(base) + z.y = mixingBuffer.get(base + 1) + mixingBuffer.put(base, z.y * -cycle) + mixingBuffer.put(base + 1, z.x * cycle) + // (don't need to handle nyquist) + // mark data block as processed + leftPlusRight.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) + leftMinusRight.position(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1) + leftPlusRight.compact() + leftMinusRight.compact() + Companion.fft300k.realInverse(mixingBuffer.array(), false) + outputBuffer.clear() + var sum = lastSum + for (i in FmFullConstants.IFFT_OVERLAP_48K_300K until FmFullConstants.IFFT_SIZE_48K_300K) { + if (!stereoPilot.hasRemaining()) { + stereoPilot.clear() + } + val result = mixingBuffer.get(i) + stereoPilot.get() + sum += result * FmFullConstants.CORRECTION_FACTOR + val sin = org.joml.Math.sin(sum) + outputBuffer.put(sin * power) + outputBuffer.put(org.joml.Math.cos(sum) * power) + } + lastSum = sum % (2 * Math.PI).toFloat() + outputBuffer.clear() + consumer.accept(outputBuffer) + } + } + input.compact() + } + + fun flush(power: Float, consumer: Consumer<FloatBuffer>) { + process(FloatBuffer.allocate(2 * leftPlusRight.remaining()), power, consumer) + } + + companion object { + private val fft48k = FloatFFT_1D(FmFullConstants.FFT_SIZE_LPF_48K_15K_3K1.toLong()) + private val fft300k = FloatFFT_1D(FmFullConstants.IFFT_SIZE_48K_300K.toLong()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt b/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt new file mode 100644 index 0000000..56fce2b --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/opus/OpusDecoder.kt @@ -0,0 +1,77 @@ +package space.autistic.radio.opus + +import com.dylibso.chicory.runtime.ByteBufferMemory +import space.autistic.radio.reflection.getBuffer +import java.nio.ByteOrder + +class OpusDecoder(sampleRate: Int, private val channels: Int) { + private val instance = OpusFactory() + + init { + instance.export("_initialize").apply() + } + + private val errorPtr = instance.export("malloc").apply(4)[0] + + init { + if (errorPtr == 0L) { + throw IllegalStateException() + } + instance.memory().writeI32(errorPtr.toInt(), 0) + } + + private val decoder = + instance.export("opus_decoder_create").apply(sampleRate.toLong(), channels.toLong(), errorPtr)[0] + + init { + val error = instance.memory().readI32(errorPtr.toInt()) + if (error < 0) { + throw IllegalStateException( + instance.memory().readCString(instance.export("opus_strerror").apply(error)[0].toInt()) + ) + } + } + + private val opusDecodeFloat = instance.export("opus_decode_float") + + private val outBuf = instance.export("malloc").apply((4 * MAX_FRAME_SIZE * channels).toLong())[0] + + init { + if (outBuf == 0L) { + throw IllegalStateException() + } + } + + private val cbits = instance.export("malloc").apply(MAX_PACKET_SIZE.toLong())[0] + + init { + if (cbits == 0L) { + throw IllegalStateException() + } + } + + private val memory = instance.memory() as ByteBufferMemory + + fun decode(packet: ByteArray): FloatArray { + if (packet.size > MAX_PACKET_SIZE) { + throw IllegalArgumentException("packet too big") + } + memory.getBuffer().put(cbits.toInt(), packet) + val decoded = + opusDecodeFloat.apply(decoder, cbits, packet.size.toLong(), outBuf, MAX_FRAME_SIZE.toLong(), 0L)[0] + if (decoded < 0L) { + throw IllegalStateException( + instance.memory().readCString(instance.export("opus_strerror").apply(decoded)[0].toInt()) + ) + } + val out = FloatArray(decoded.toInt()) + memory.getBuffer().slice(outBuf.toInt(), outBuf.toInt() + 4 * channels * decoded.toInt()) + .order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(out) + return out + } + + companion object { + const val MAX_FRAME_SIZE = 6 * 960 + const val MAX_PACKET_SIZE = 3 * 1276 + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt b/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt new file mode 100644 index 0000000..70e0c3c --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/opus/OpusFactory.kt @@ -0,0 +1,26 @@ +package space.autistic.radio.opus + +import com.dylibso.chicory.experimental.aot.AotMachineFactory +import com.dylibso.chicory.runtime.ImportValues +import com.dylibso.chicory.runtime.Instance +import com.dylibso.chicory.wasm.Parser +import net.fabricmc.loader.api.FabricLoader +import java.io.InputStream + +object OpusFactory : () -> Instance { + private val defaultImports = ImportValues.builder().build() + private val module = Parser.parse(getModuleInputStream()) + private val instanceBuilder = + Instance.builder(module) + .withMachineFactory(AotMachineFactory(module)) + .withImportValues(defaultImports) + + override fun invoke(): Instance = instanceBuilder.build() + + private fun getModuleInputStream(): InputStream { + return FabricLoader.getInstance().getModContainer("pirate-radio").flatMap { it.findPath("opus.wasm") } + .map<InputStream?> { it.toFile().inputStream() }.orElseGet { + this.javaClass.getResourceAsStream("/opus.wasm") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/autistic/radio/reflection/MemoryReflection.kt b/src/main/kotlin/space/autistic/radio/reflection/MemoryReflection.kt new file mode 100644 index 0000000..78961da --- /dev/null +++ b/src/main/kotlin/space/autistic/radio/reflection/MemoryReflection.kt @@ -0,0 +1,14 @@ +package space.autistic.radio.reflection + +import com.dylibso.chicory.runtime.ByteBufferMemory +import java.lang.invoke.MethodHandles +import java.nio.ByteBuffer + +fun ByteBufferMemory.getBuffer(): ByteBuffer { + return MemoryReflection.buffer.get(this) as ByteBuffer +} + +object MemoryReflection { + val buffer = MethodHandles.privateLookupIn(ByteBufferMemory::class.java, MethodHandles.lookup()) + .findVarHandle(ByteBufferMemory::class.java, "buffer", ByteBuffer::class.java) +} \ No newline at end of file diff --git a/src/main/resources/assets/pirate-radio/icon.png b/src/main/resources/assets/pirate-radio/icon.png new file mode 100644 index 0000000..62adcdd --- /dev/null +++ b/src/main/resources/assets/pirate-radio/icon.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/lang/en_us.json b/src/main/resources/assets/pirate-radio/lang/en_us.json new file mode 100644 index 0000000..9627729 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/lang/en_us.json @@ -0,0 +1,10 @@ +{ + "item.pirate-radio.sbc": "Raspberry Pi", + "item.pirate-radio.wire": "Piece of Wire", + "item.pirate-radio.powerbank": "Powerbank", + "item.pirate-radio.storage-card": "SD Card", + "item.pirate-radio.disposable-transmitter": "Disposable Pirate Radio Transmitter", + "item.pirate-radio.fm-receiver": "FM Receiver", + "entity.pirate-radio.electronics-trader": "Microcenter", + "pirate-radio.fm-receiver": "FM Receiver" +} \ No newline at end of file diff --git a/src/main/resources/assets/pirate-radio/textures/item/powerbank.png b/src/main/resources/assets/pirate-radio/textures/item/powerbank.png new file mode 100644 index 0000000..0f1685f --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/powerbank.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/sbc.png b/src/main/resources/assets/pirate-radio/textures/item/sbc.png new file mode 100644 index 0000000..38a90a4 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/sbc.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/storage-card.png b/src/main/resources/assets/pirate-radio/textures/item/storage-card.png new file mode 100644 index 0000000..bf4b60b --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/storage-card.png Binary files differdiff --git a/src/main/resources/assets/pirate-radio/textures/item/wire.png b/src/main/resources/assets/pirate-radio/textures/item/wire.png new file mode 100644 index 0000000..8b5b330 --- /dev/null +++ b/src/main/resources/assets/pirate-radio/textures/item/wire.png Binary files differdiff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..71f2518 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "id": "pirate-radio", + "version": "${version}", + "name": "Pirate Radio", + "description": "This is an example description! Tell everyone what your mod is about!", + "authors": [ + "Me!" + ], + "contact": { + "homepage": "https://fabricmc.net/", + "sources": "https://github.com/FabricMC/fabric-example-mod" + }, + "license": "LGPL-2.1-or-later", + "icon": "assets/pirate-radio/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + { + "value": "space.autistic.radio.PirateRadio", + "adapter": "kotlin" + } + ], + "client": [ + { + "value": "space.autistic.radio.client.PirateRadioClient", + "adapter": "kotlin" + } + ], + "fabric-datagen": [ + { + "value": "space.autistic.radio.client.PirateRadioDataGenerator", + "adapter": "kotlin" + } + ] + }, + "depends": { + "fabricloader": ">=0.16.10", + "minecraft": "~1.21.1", + "java": ">=21", + "fabric-api": "*", + "fabric-language-kotlin": "*" + } +} \ No newline at end of file diff --git a/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt b/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt new file mode 100644 index 0000000..a4dfe91 --- /dev/null +++ b/src/test/kotlin/space/autistic/radio/complex/ComplexKtTest.kt @@ -0,0 +1,13 @@ +package space.autistic.radio.complex + +import org.joml.Vector2f +import org.junit.jupiter.api.Assertions.* +import kotlin.test.Test + +class ComplexKtTest { + @Test + fun testI() { + assertEquals(I.cmul(I), Vector2f(-1f, 0f)) + assertNotSame(I, I) + } +} \ No newline at end of file diff --git a/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt b/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt new file mode 100644 index 0000000..8a4862c --- /dev/null +++ b/src/test/kotlin/space/autistic/radio/fmsim/TestAsserts.kt @@ -0,0 +1,13 @@ +package space.autistic.radio.fmsim + +import kotlin.test.Test + +class TestAsserts { + @Test + fun testFmFullSim() { + // initialize and flush an FM modulator + // if anything asserts, this should catch it + val fmFullModulator = FmFullModulator() + fmFullModulator.flush(1f) {} + } +} \ No newline at end of file |