1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-01-10 01:10:30 +00:00

Send entire DFPWM encoder state to the client

This ensures the client decoder is in sync with the server. Well, mostly
- we don't handle the anti-jerk, but that should correct itself within a
few samples.

Fixes #1748
This commit is contained in:
Jonathan Coates 2024-03-15 18:25:57 +00:00
parent ffb62dfa02
commit 61f9b1d0c6
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
11 changed files with 111 additions and 28 deletions

View File

@ -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);
}

View File

@ -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());

View File

@ -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();

View File

@ -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);

View File

@ -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<ClientNetworkContext> {
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<ClientNetworkCo
source = buf.readUUID();
pos = SpeakerPosition.Message.read(buf);
volume = buf.readFloat();
var bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
content = ByteBuffer.wrap(bytes);
content = EncodedAudio.read(buf);
}
@Override
@ -49,7 +46,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
buf.writeUUID(source);
pos.write(buf);
buf.writeFloat(volume);
buf.writeBytes(content.duplicate());
content.write(buf);
}
@Override

View File

@ -37,7 +37,7 @@ class DfpwmState {
private boolean unplayed = true;
private long clientEndTime = PauseAwareTimer.getTime();
private float pendingVolume = 1.0f;
private @Nullable ByteBuffer pendingAudio;
private @Nullable EncodedAudio pendingAudio;
synchronized boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double> 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;
}

View File

@ -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));
}
}

View File

@ -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");

View File

@ -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);

View File

@ -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<EncodedAudio> audio() {
return Combinators.combine(
Arbitraries.integers(),
Arbitraries.integers(),
Arbitraries.of(true, false),
ArbitraryByteBuffer.bytes().ofMaxSize(1000)
).as(EncodedAudio::new);
}
}

View File

@ -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",
)