diff --git a/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java b/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java index 941c56c73..ebfe918c6 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java +++ b/projects/common/src/client/java/dan200/computercraft/client/platform/ClientNetworkContextImpl.java @@ -17,6 +17,7 @@ import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.computer.upload.UploadResult; import dan200.computercraft.shared.network.client.ClientNetworkContext; import dan200.computercraft.shared.peripheral.monitor.MonitorBlockEntity; +import dan200.computercraft.shared.peripheral.speaker.EncodedAudio; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; @@ -27,7 +28,6 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import javax.annotation.Nullable; -import java.nio.ByteBuffer; import java.util.UUID; /** @@ -79,7 +79,7 @@ public final class ClientNetworkContextImpl implements ClientNetworkContext { } @Override - public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer buffer) { + public void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio buffer) { SpeakerManager.getSound(source).playAudio(reifyPosition(position), volume, buffer); } diff --git a/projects/common/src/client/java/dan200/computercraft/client/sound/DfpwmStream.java b/projects/common/src/client/java/dan200/computercraft/client/sound/DfpwmStream.java index 6cbca4c3a..6ba517b5a 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/sound/DfpwmStream.java +++ b/projects/common/src/client/java/dan200/computercraft/client/sound/DfpwmStream.java @@ -5,6 +5,7 @@ package dan200.computercraft.client.sound; import com.mojang.blaze3d.audio.Channel; +import dan200.computercraft.shared.peripheral.speaker.EncodedAudio; import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import net.minecraft.client.sounds.AudioStream; @@ -36,7 +37,7 @@ class DfpwmStream implements AudioStream { /** * The {@link Channel} which this sound is playing on. * - * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer) + * @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio) */ @Nullable Channel channel; @@ -44,21 +45,23 @@ class DfpwmStream implements AudioStream { /** * The underlying {@link SoundEngine} executor. * - * @see SpeakerInstance#playAudio(SpeakerPosition, float, ByteBuffer) + * @see SpeakerInstance#playAudio(SpeakerPosition, float, EncodedAudio) * @see SoundEngine#executor */ @Nullable Executor executor; - private int charge = 0; // q - private int strength = 0; // s private int lowPassCharge; - private boolean previousBit = false; DfpwmStream() { } - void push(ByteBuffer input) { + void push(EncodedAudio audio) { + var charge = audio.charge(); + var strength = audio.strength(); + var previousBit = audio.previousBit(); + var input = audio.audio(); + var readable = input.remaining(); var output = ByteBuffer.allocate(readable * 8).order(ByteOrder.nativeOrder()); diff --git a/projects/common/src/client/java/dan200/computercraft/client/sound/SpeakerInstance.java b/projects/common/src/client/java/dan200/computercraft/client/sound/SpeakerInstance.java index 472bcffb1..c3957168c 100644 --- a/projects/common/src/client/java/dan200/computercraft/client/sound/SpeakerInstance.java +++ b/projects/common/src/client/java/dan200/computercraft/client/sound/SpeakerInstance.java @@ -6,12 +6,12 @@ package dan200.computercraft.client.sound; import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.core.util.Nullability; +import dan200.computercraft.shared.peripheral.speaker.EncodedAudio; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; import javax.annotation.Nullable; -import java.nio.ByteBuffer; /** * An instance of a speaker, which is either playing a {@link DfpwmStream} stream or a normal sound. @@ -25,7 +25,7 @@ public class SpeakerInstance { SpeakerInstance() { } - private void pushAudio(ByteBuffer buffer) { + private void pushAudio(EncodedAudio buffer) { var sound = this.sound; var stream = currentStream; @@ -43,7 +43,7 @@ public class SpeakerInstance { } } - public void playAudio(SpeakerPosition position, float volume, ByteBuffer buffer) { + public void playAudio(SpeakerPosition position, float volume, EncodedAudio buffer) { pushAudio(buffer); var soundManager = Minecraft.getInstance().getSoundManager(); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java b/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java index d981544d4..cdc598e95 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/client/ClientNetworkContext.java @@ -8,6 +8,7 @@ import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.computer.core.ComputerState; import dan200.computercraft.shared.computer.terminal.TerminalState; import dan200.computercraft.shared.computer.upload.UploadResult; +import dan200.computercraft.shared.peripheral.speaker.EncodedAudio; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; @@ -15,7 +16,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvent; import javax.annotation.Nullable; -import java.nio.ByteBuffer; import java.util.UUID; /** @@ -34,7 +34,7 @@ public interface ClientNetworkContext { void handlePocketComputerDeleted(int instanceId); - void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, ByteBuffer audio); + void handleSpeakerAudio(UUID source, SpeakerPosition.Message position, float volume, EncodedAudio audio); void handleSpeakerMove(UUID source, SpeakerPosition.Message position); diff --git a/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java b/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java index 6de8a0b57..283ff91d1 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/network/client/SpeakerAudioClientMessage.java @@ -7,11 +7,11 @@ package dan200.computercraft.shared.network.client; import dan200.computercraft.shared.network.MessageType; import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.NetworkMessages; +import dan200.computercraft.shared.peripheral.speaker.EncodedAudio; import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity; import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition; import net.minecraft.network.FriendlyByteBuf; -import java.nio.ByteBuffer; import java.util.UUID; /** @@ -24,10 +24,10 @@ import java.util.UUID; public class SpeakerAudioClientMessage implements NetworkMessage { private final UUID source; private final SpeakerPosition.Message pos; - private final ByteBuffer content; + private final EncodedAudio content; private final float volume; - public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, ByteBuffer content) { + public SpeakerAudioClientMessage(UUID source, SpeakerPosition pos, float volume, EncodedAudio content) { this.source = source; this.pos = pos.asMessage(); this.content = content; @@ -38,10 +38,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage table, int size, Optional volume) throws LuaException { if (pendingAudio != null) return false; @@ -45,6 +45,10 @@ class DfpwmState { var outSize = size / 8; var buffer = ByteBuffer.allocate(outSize); + var initialCharge = charge; + var initialStrength = strength; + var initialPreviousBit = previousBit; + for (var i = 0; i < outSize; i++) { var thisByte = 0; for (var j = 1; j <= 8; j++) { @@ -80,7 +84,7 @@ class DfpwmState { buffer.flip(); - pendingAudio = buffer; + pendingAudio = new EncodedAudio(initialCharge, initialStrength, initialPreviousBit, buffer); pendingVolume = (float) clampVolume(volume.orElse((double) pendingVolume)); return true; } @@ -89,12 +93,12 @@ class DfpwmState { return pendingAudio != null && now >= clientEndTime - CLIENT_BUFFER; } - ByteBuffer pullPending(long now) { + EncodedAudio pullPending(long now) { var audio = pendingAudio; if (audio == null) throw new IllegalStateException("Should not pull pending audio yet"); pendingAudio = null; // Compute when we should consider sending the next packet. - clientEndTime = Math.max(now, clientEndTime) + (audio.remaining() * SECOND * 8 / SAMPLE_RATE); + clientEndTime = Math.max(now, clientEndTime) + (audio.audio().remaining() * SECOND * 8 / SAMPLE_RATE); unplayed = false; return audio; } diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudio.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudio.java new file mode 100644 index 000000000..1e6ce9b5e --- /dev/null +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudio.java @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.peripheral.speaker; + +import net.minecraft.network.FriendlyByteBuf; + +import java.nio.ByteBuffer; + +/** + * A chunk of encoded audio, along with the state required for the decoder to reproduce the original audio samples. + * + * @param charge The DFPWM charge. + * @param strength The DFPWM strength. + * @param previousBit The previous bit. + * @param audio The block of encoded audio. + */ +public record EncodedAudio(int charge, int strength, boolean previousBit, ByteBuffer audio) { + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(charge()); + buf.writeVarInt(strength()); + buf.writeBoolean(previousBit()); + buf.writeBytes(audio().duplicate()); + } + + public static EncodedAudio read(FriendlyByteBuf buf) { + var charge = buf.readVarInt(); + var strength = buf.readVarInt(); + var previousBit = buf.readBoolean(); + + var bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + + return new EncodedAudio(charge, strength, previousBit, ByteBuffer.wrap(bytes)); + } +} diff --git a/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java b/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java index de1f89b8e..2e7866501 100644 --- a/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java +++ b/projects/common/src/test/java/dan200/computercraft/client/sound/DfpwmStreamTest.java @@ -4,6 +4,7 @@ package dan200.computercraft.client.sound; +import dan200.computercraft.shared.peripheral.speaker.EncodedAudio; import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; @@ -16,7 +17,7 @@ public class DfpwmStreamTest { var stream = new DfpwmStream(); var input = ByteBuffer.wrap(new byte[]{ 43, -31, 33, 44, 30, -16, -85, 23, -3, -55, 46, -70, 68, -67, 74, -96, -68, 16, 94, -87, -5, 87, 11, -16, 19, 92, 85, -71, 126, 5, -84, 64, 17, -6, 85, -11, -1, -87, -12, 1, 85, -56, 33, -80, 82, 104, -93, 17, 126, 23, 91, -30, 37, -32, 117, -72, -58, 11, -76, 19, -108, 86, -65, -10, -1, -68, -25, 10, -46, 85, 124, -54, 15, -24, 43, -94, 117, 63, -36, 15, -6, 88, 87, -26, -83, 106, 41, 13, -28, -113, -10, -66, 119, -87, -113, 68, -55, 40, -107, 62, 20, 72, 3, -96, 114, -87, -2, 39, -104, 30, 20, 42, 84, 24, 47, 64, 43, 61, -35, 95, -65, 42, 61, 42, -50, 4, -9, 81 }); - stream.push(input); + stream.push(new EncodedAudio(0, 0, false, input)); var buffer = stream.read(1024 + 1); assertEquals(1024, buffer.remaining(), "Must have read 1024 bytes"); diff --git a/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java index 7d73af37e..b3cd5df53 100644 --- a/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java +++ b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/DfpwmStateTest.java @@ -23,7 +23,7 @@ class DfpwmStateTest { var state = new DfpwmState(); state.pushBuffer(new ObjectLuaTable(inputTbl), input.length, Optional.empty()); - var result = state.pullPending(0); + var result = state.pullPending(0).audio(); var contents = new byte[result.remaining()]; result.get(contents); diff --git a/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudioTest.java b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudioTest.java new file mode 100644 index 000000000..18751a82f --- /dev/null +++ b/projects/common/src/test/java/dan200/computercraft/shared/peripheral/speaker/EncodedAudioTest.java @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.shared.peripheral.speaker; + +import dan200.computercraft.test.core.ArbitraryByteBuffer; +import io.netty.buffer.Unpooled; +import net.jqwik.api.*; +import net.minecraft.network.FriendlyByteBuf; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EncodedAudioTest { + /** + * Sends the audio on a roundtrip, ensuring that its contents are reassembled on the other end. + * + * @param audio The message to send. + */ + @Property + public void testRoundTrip(@ForAll("audio") EncodedAudio audio) { + var buffer = new FriendlyByteBuf(Unpooled.directBuffer()); + audio.write(buffer); + + var converted = EncodedAudio.read(buffer); + assertEquals(buffer.readableBytes(), 0, "Whole packet was read"); + + assertThat("Messages are equal", converted, equalTo(converted)); + } + + @Provide + Arbitrary audio() { + return Combinators.combine( + Arbitraries.integers(), + Arbitraries.integers(), + Arbitraries.of(true, false), + ArbitraryByteBuffer.bytes().ofMaxSize(1000) + ).as(EncodedAudio::new); + } +} diff --git a/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt b/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt index eb869e1a5..e2b963e25 100644 --- a/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt +++ b/projects/lints/src/main/kotlin/cc/tweaked/linter/SideChecker.kt @@ -105,7 +105,6 @@ internal class SideProvider { companion object { private val notClientPackages = listOf( // Ugly! But we do what we must. - "net.fabricmc.fabric.api.client.itemgroup", "dan200.computercraft.shared.network.client", )