Skip to content

Commit

Permalink
v2.1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
Apehum authored Oct 5, 2024
2 parents 26667e9 + 4ffbfce commit 474be4a
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 38 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/prerelease-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Publish
on:
push:
branches:
- v1.1
- v2.1

jobs:
build:
Expand All @@ -23,6 +23,7 @@ jobs:
8
16
17
21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
Expand Down
4 changes: 2 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
- Update to Plasmo Voice 2.1.0. This version is not compatible with PV 2.0.x.
- Pitch is now used to control the playback speed of the source.
- Voice audio render to AAC format using OpenAL loopback device.
Rendered audio is saved in the same location and the same name (but with .aac extension) as the video.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version=2.1.0
version=2.1.1

org.gradle.jvmargs=-Xmx4G
kotlin.stdlib.default.dependency=false
2 changes: 0 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ A fork of [Replay Voice Chat](https://github.com/henkelmax/replay-voice-chat) th

## Installation

Both Fabric and Forge are supported. The same add-on file works on both platforms.

1. Install [Plasmo Voice](https://modrinth.com/plugin/plasmo-voice) and [ReplayMod](https://www.replaymod.com/)
1. Download the add-on from [Modrinth](https://modrinth.com/mod/pv-addon-replaymod/)
1. Drop it into the `~/mods` folder
Expand Down
4 changes: 2 additions & 2 deletions versions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
base.archivesName.set("${rootProject.name}-${platform.mcVersionStr}")

val minecraftSupportedVersions = mapOf(
11605 to "[\">=1.16.5\", \"<=1.20.4\"]",
11605 to "\">=1.16.5 <=1.20.4\"",
12100 to "\">=1.21\""
)

Expand Down Expand Up @@ -39,7 +39,7 @@ dependencies {

annotationProcessor(libs.lombok)

// modImplementation("maven.modrinth:plasmo-voice:fabric-${platform.mcVersionStr}-2.0.10")
modImplementation("maven.modrinth:plasmo-voice:fabric-${platform.mcVersionStr}-2.1.0")

if (platform.mcVersion >= 12100) {
modImplementation("maven.modrinth:replaymod:${platform.mcVersionStr}-2.6.17")
Expand Down
11 changes: 10 additions & 1 deletion versions/src/main/java/su/plo/replayvoice/ReplayVoiceAddon.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import su.plo.voice.api.addon.annotation.Addon;
import su.plo.voice.api.client.PlasmoVoiceClient;
import su.plo.voice.api.client.audio.source.ClientAudioSource;
import su.plo.voice.api.client.event.audio.capture.AudioCaptureEvent;
import su.plo.voice.api.client.event.audio.capture.AudioCaptureInitializeEvent;
import su.plo.voice.api.client.event.audio.device.source.AlSourceWriteEvent;
import su.plo.voice.api.client.event.audio.source.AudioSourceResetEvent;
Expand Down Expand Up @@ -54,6 +55,7 @@
@Addon(id = "pv-addon-replaymod", scope = AddonLoaderScope.CLIENT, version = BuildConstants.VERSION, authors = "Apehum")
public class ReplayVoiceAddon implements ClientModInitializer, AddonInitializer {

public static ReplayVoiceAddon INSTANCE = new ReplayVoiceAddon();
public static final Logger LOGGER = LogManager.getLogger();
public static final ResourceLocation SELF_AUDIO_PACKET = ResourceLocation.tryParse("plasmo:voice/v2/self_audio");
public static final ResourceLocation SELF_AUDIO_INFO_PACKET = ResourceLocation.tryParse("plasmo:voice/v2/self_audio_info");
Expand All @@ -63,7 +65,7 @@ public class ReplayVoiceAddon implements ClientModInitializer, AddonInitializer
private final Minecraft minecraft = Minecraft.getInstance();

@InjectPlasmoVoice
private PlasmoVoiceClient voiceClient;
public PlasmoVoiceClient voiceClient;

@Override
public void onAddonInitialize() {
Expand Down Expand Up @@ -108,6 +110,7 @@ public void onAddonInitialize() {

@Override
public void onInitializeClient() {
INSTANCE = this;
ClientAddonsLoader.INSTANCE.load(this);
}

Expand All @@ -120,6 +123,12 @@ public void onDistanceRender(@NotNull VoiceDistanceRenderEvent event) {
}
}

@EventSubscribe
public void onAudioCapture(@NotNull AudioCaptureEvent event) {
if (!ReplayInterface.INSTANCE.isInReplayEditor) return;
event.setCancelled(true);
}

@EventSubscribe
public void onHudActivationRender(@NotNull HudActivationRenderEvent event) {
if (!ReplayInterface.INSTANCE.isInReplayEditor || event.isRender()) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package su.plo.replayvoice.mixin;

import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.ALC11;
import org.lwjgl.openal.SOFTLoopback;
import org.lwjgl.system.MemoryStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import su.plo.replayvoice.render.VoiceAudioRender;
import su.plo.voice.client.audio.device.AlOutputDevice;

import java.nio.Buffer;
import java.nio.IntBuffer;

@Mixin(value = AlOutputDevice.class, remap = false)
public final class MixinAlOutputDevice {

@Inject(method = "openDevice", at = @At("HEAD"), cancellable = true)
private void openDevice(String deviceName, CallbackInfoReturnable<Long> cir) {
if (!VoiceAudioRender.isRendering()) return;

long devicePointer = SOFTLoopback.alcLoopbackOpenDeviceSOFT((String) null);
cir.setReturnValue(devicePointer);
}

@Redirect(method = "openSync", at = @At(value = "INVOKE", target = "Lorg/lwjgl/openal/ALC11;alcCreateContext(JLjava/nio/IntBuffer;)J"))
private long createContext(long devicePointer, IntBuffer attrList) {
if (!VoiceAudioRender.isRendering()) {
return ALC11.alcCreateContext(devicePointer, attrList);
}

try (MemoryStack memoryStack = MemoryStack.stackPush()) {
IntBuffer intBuffer = memoryStack.callocInt(7)
.put(ALC10.ALC_FREQUENCY).put(48000)
.put(SOFTLoopback.ALC_FORMAT_CHANNELS_SOFT).put(SOFTLoopback.ALC_STEREO_SOFT)
.put(SOFTLoopback.ALC_FORMAT_TYPE_SOFT).put(SOFTLoopback.ALC_SHORT_SOFT)
.put(0);
((Buffer) intBuffer).flip();
return ALC10.alcCreateContext(devicePointer, intBuffer);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package su.plo.replayvoice.mixin;

import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import su.plo.replayvoice.render.VoiceAudioRender;

@Mixin(targets = "com.replaymod.render.rendering.Pipeline$ProcessTask", remap = false)
public final class MixinProcessTask {

@Inject(method = "run", at = @At(value = "INVOKE", target = "Lcom/replaymod/render/rendering/FrameConsumer;consume(Ljava/util/Map;)V", shift = At.Shift.BEFORE))
public void run(CallbackInfo ci) {
VoiceAudioRender.AUDIO_RENDER.render();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import lombok.RequiredArgsConstructor;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.FriendlyByteBuf;
import org.apache.logging.log4j.LogManager;
import su.plo.replayvoice.CameraUtil;
import su.plo.voice.api.client.PlasmoVoiceClient;
Expand All @@ -24,7 +20,6 @@
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package su.plo.replayvoice.render;

import com.replaymod.lib.org.apache.commons.exec.CommandLine;
import org.jetbrains.annotations.NotNull;
import su.plo.replayvoice.ReplayVoiceAddon;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

public final class FfmpegAudioWriter implements AutoCloseable {

private final @NotNull Process process;
private final @NotNull InputStream inputStream;
private final @NotNull OutputStream outputStream;

public FfmpegAudioWriter(
@NotNull String ffmpegCommand,
@NotNull File outputFile,
@NotNull String outputFormat,
int sampleRate,
int channels
) {
List<String> commandArguments = new ArrayList<>();
commandArguments.add("-y");
commandArguments.add("-f s16le");
commandArguments.add("-ar " + sampleRate);
commandArguments.add("-ac " + channels);
commandArguments.add("-i -");
commandArguments.add("-c:a " + outputFormat);
commandArguments.add(outputFile.getName());

String[] commandLine = (new CommandLine(ffmpegCommand))
.addArguments(String.join(" ", commandArguments), false)
.toStrings();

ReplayVoiceAddon.LOGGER.info("Starting ffmpeg process: {}", String.join(" ", commandLine));

try {
process = (new ProcessBuilder(commandLine))
.directory(outputFile.getParentFile())
.redirectErrorStream(true)
.start();

inputStream = process.getInputStream();
outputStream = process.getOutputStream();
} catch (IOException e) {
ReplayVoiceAddon.LOGGER.info("Failed to create ffmpeg process", e);
throw new RuntimeException(e);
}
}

public void write(byte[] samples) {
try {
outputStream.write(samples);

byte[] available = new byte[inputStream.available()];
inputStream.read(available);
} catch (IOException e) {
ReplayVoiceAddon.LOGGER.info("Failed to write to ffmpeg stdin", e);
}
}

@Override
public void close() throws Exception {
try {
outputStream.flush();
outputStream.close();
inputStream.close();
process.waitFor();
} catch (InterruptedException | IOException e) {
ReplayVoiceAddon.LOGGER.info("Failed to exit ffmpeg process", e);
}
process.destroy();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package su.plo.replayvoice.render;

import com.replaymod.render.rendering.VideoRenderer;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.openal.SOFTLoopback;
import su.plo.voice.api.client.PlasmoVoiceClient;
import su.plo.voice.api.client.audio.device.AlContextOutputDevice;
import su.plo.voice.api.client.connection.ServerInfo;
import su.plo.voice.api.util.AudioUtil;

import java.io.File;
import java.util.Arrays;

@RequiredArgsConstructor
public class LoopbackAudioRender implements AutoCloseable {

private final @NotNull PlasmoVoiceClient voiceClient;
private final @NotNull VideoRenderer videoRenderer;

private FfmpegAudioWriter writer;
private int frameSize;
private short[] shortsBuffer;

private boolean initialized = false;

public synchronized void render() {
if (!initialized) {
initializeWriter();
return;
}

AlContextOutputDevice outputDevice = voiceClient.getDeviceManager()
.getOutputDevice()
.orElse(null);
if (outputDevice == null) return;

long devicePointer = outputDevice.getDevicePointer();

outputDevice.runInContextBlocking(() -> {
SOFTLoopback.alcRenderSamplesSOFT(devicePointer, shortsBuffer, frameSize);
writer.write(AudioUtil.shortsToBytes(shortsBuffer));
});
}

@Override
public synchronized void close() throws Exception {
if (!initialized) return;

writer.close();
}

private void initializeWriter() {
ServerInfo serverInfo = voiceClient.getServerInfo().orElse(null);
if (serverInfo == null) return;

VoiceAudioRender.reloadDevice();

File outputVideoFile = videoRenderer.getRenderSettings().getOutputFile();
File outputFolder = outputVideoFile.getParentFile();
String[] outputFileNameSplit = outputVideoFile.getName().split("\\.");
String outputFileName = String.join(".", Arrays.copyOf(outputFileNameSplit, outputFileNameSplit.length - 1)) + "-voice.aac";
String ffmpegCommand = videoRenderer.getRenderSettings().getExportCommandOrDefault();

int sampleRate = serverInfo.getVoiceInfo().getCaptureInfo().getSampleRate();
int fps = videoRenderer.getRenderSettings().getFramesPerSecond();
this.frameSize = sampleRate / fps;
int channels = 2;
this.shortsBuffer = new short[frameSize * channels];

this.writer = new FfmpegAudioWriter(
ffmpegCommand,
new File(outputFolder, outputFileName),
"aac",
sampleRate,
channels
);
this.initialized = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package su.plo.replayvoice.render;

import com.replaymod.render.rendering.VideoRenderer;
import org.jetbrains.annotations.NotNull;
import su.plo.replayvoice.ReplayVoiceAddon;
import su.plo.voice.api.client.audio.device.DeviceException;
import su.plo.voice.api.client.audio.device.DeviceManager;

public final class VoiceAudioRender {

public static LoopbackAudioRender AUDIO_RENDER;

public static boolean isRendering() {
return AUDIO_RENDER != null;
}

public static void startRender(@NotNull VideoRenderer renderer) {
if (AUDIO_RENDER != null) return;

AUDIO_RENDER = new LoopbackAudioRender(ReplayVoiceAddon.INSTANCE.voiceClient, renderer);
}

public static void stopRender(@NotNull VideoRenderer videoRenderer) {
if (AUDIO_RENDER == null) return;

try {
AUDIO_RENDER.close();
} catch (Exception e) {
ReplayVoiceAddon.LOGGER.error("Failed to close audio renderer", e);
}
AUDIO_RENDER = null;

reloadDevice();
}

public static void reloadDevice() {
DeviceManager devices = ReplayVoiceAddon.INSTANCE.voiceClient.getDeviceManager();

devices.getOutputDevice().ifPresent((device) -> {
try {
device.reload();
} catch (DeviceException e) {
ReplayVoiceAddon.LOGGER.error("Failed to reload output device", e);
}
});
}
}
Loading

0 comments on commit 474be4a

Please sign in to comment.