Skip to content

Instantly share code, notes, and snippets.

@ericek111
Created April 15, 2026 23:48
Show Gist options
  • Select an option

  • Save ericek111/18488654aaa2abd208a8adb607fe34e7 to your computer and use it in GitHub Desktop.

Select an option

Save ericek111/18488654aaa2abd208a8adb607fe34e7 to your computer and use it in GitHub Desktop.
A BepInEx plugin for the co-op game In Sink which allows playing the piano in the lobby programatically + a JACK->UDP bridge in Java.
package me.lixko.midiclient;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.EnumSet;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.ShortMessage;
import org.jaudiolibs.jnajack.Jack;
import org.jaudiolibs.jnajack.JackClient;
import org.jaudiolibs.jnajack.JackException;
import org.jaudiolibs.jnajack.JackMidi;
import org.jaudiolibs.jnajack.JackOptions;
import org.jaudiolibs.jnajack.JackPort;
import org.jaudiolibs.jnajack.JackPortFlags;
import org.jaudiolibs.jnajack.JackPortType;
import org.jaudiolibs.jnajack.JackProcessCallback;
import org.jaudiolibs.jnajack.JackShutdownCallback;
import org.jaudiolibs.jnajack.JackStatus;
public class InSinkPianoConsumer implements JackProcessCallback, JackShutdownCallback {
private final JackClient client;
private final JackPort inputPort;
private final JackPort outputPort;
private final JackMidi.Event midiEvent;
PrintWriter socketWriter = null;
private byte[] data;
private DatagramSocket socket;
private InetAddress targetAddress;
private int udpPort = 9150;
private volatile int octaveShift = -3;
private byte[] udpPayload = new byte[1]; // a single note
private DatagramPacket udpPacket;
public InSinkPianoConsumer() throws JackException, UnknownHostException, IOException {
EnumSet<JackStatus> status = EnumSet.noneOf(JackStatus.class);
try {
Jack jack = Jack.getInstance();
client = jack.openClient("ÍnSink Piano", EnumSet.of(JackOptions.JackNoStartServer), status);
if (!status.isEmpty()) {
System.out.println("JACK client status : " + status);
}
inputPort = client.registerPort("MIDI in", JackPortType.MIDI, JackPortFlags.JackPortIsInput);
outputPort = client.registerPort("MIDI out", JackPortType.MIDI, JackPortFlags.JackPortIsOutput);
midiEvent = new JackMidi.Event();
} catch (JackException ex) {
if (!status.isEmpty()) {
System.out.println("JACK exception client status : " + status);
}
throw ex;
}
socket = new DatagramSocket();
targetAddress = InetAddress.getByName("127.0.0.1");
udpPacket = new DatagramPacket(udpPayload, udpPayload.length, targetAddress, udpPort);
}
public void activate() throws JackException {
client.setProcessCallback(this);
client.onShutdown(this);
client.activate();
this.runConsole();
}
@Override
public boolean process(JackClient client, int nframes) {
try {
JackMidi.clearBuffer(outputPort);
int eventCount = JackMidi.getEventCount(inputPort);
for (int i = 0; i < eventCount; ++i) {
JackMidi.eventGet(midiEvent, inputPort, i);
int size = midiEvent.size();
if (data == null || data.length < size) {
data = new byte[size];
}
midiEvent.read(data);
ShortMessage msg = new ShortMessage();
msg.setMessage(data[0] & 0xFF, data[1] & 0xFF, data[2] & 0xFF);
if (msg.getCommand() == ShortMessage.PROGRAM_CHANGE) {
} else if (msg.getCommand() == ShortMessage.NOTE_ON) {
int note = msg.getData1() + octaveShift * 12;
if (msg.getData2() == 0) { // velocity
System.out.println("Zero velocity instr: " + msg.getChannel() + ", note: " + msg.getData1() + ", vel: " + msg.getData2() + " => " + note);
continue;
}
if (msg.getChannel() == 9) { // Percussion
} else {
}
if (note >= 48) { // our piano only has 4 octaves
System.out.println("Out of range instr: " + msg.getChannel() + ", note: " + msg.getData1() + ", vel: " + msg.getData2() + " => " + note);
continue;
}
udpPayload[0] = (byte) note;
System.out.println("Playing instr: " + msg.getChannel() + ", note: " + msg.getData1() + ", vel: " + msg.getData2() + " => " + note);
socket.send(udpPacket);
}
}
return true;
} catch (JackException | InvalidMidiDataException | IOException ex) {
System.out.println("ERROR : " + ex);
return false;
}
}
@Override
public void clientShutdown(JackClient client) {
}
private void runConsole() {
System.out.println("Controls: [+] octave up [-] octave down [q] quit");
try {
while (true) {
int ch = System.in.read();
switch (ch) {
case '+':
octaveShift++;
System.out.printf("octave shift: %d", octaveShift);
break;
case '-': octaveShift--;
System.out.printf("octave shift: %d", octaveShift);
break;
case 'q':
System.out.println("Quitting.");
System.exit(0);
break;
default:
break;
}
}
} catch (Exception e) {
}
}
}
using System.Net;
using System.Net.Sockets;
using System.Threading;
using BepInEx;
using BepInEx.Logging;
using UnityEngine;
namespace PianoSocket
{
[BepInPlugin("com.pianomod.socket", "Piano Socket Listener", "1.0.0")]
public class PianoSocketPlugin : BaseUnityPlugin
{
static ManualLogSource Log;
UdpClient _udp;
Thread _listenThread;
volatile bool _running;
// Thread-safe queue: the listener thread pushes key indices here,
// Update() pops them on the main thread where Unity API calls are safe.
readonly System.Collections.Concurrent.ConcurrentQueue<int> _keyQueue =
new System.Collections.Concurrent.ConcurrentQueue<int>();
const int Port = 9150;
void Awake()
{
Log = Logger;
Log.LogInfo($"Piano socket plugin loaded, listening on UDP port {Port}");
_running = true;
_udp = new UdpClient(Port);
_listenThread = new Thread(ListenLoop) { IsBackground = true };
_listenThread.Start();
}
void ListenLoop()
{
var endpoint = new IPEndPoint(IPAddress.Any, Port);
while (_running)
{
try
{
byte[] data = _udp.Receive(ref endpoint);
foreach (byte b in data) {
if (b >= 48) // this many keys on the keyboard
continue;
_keyQueue.Enqueue(b);
}
}
catch (SocketException) when (!_running)
{
break;
}
}
}
void Update()
{
while (_keyQueue.TryDequeue(out int key))
{
if (NetworkedHubManager.Instance == null)
{
Log.LogWarning("NetworkedHubManager.Instance is null");
return;
}
Log.LogInfo($"Pressing piano key {key}");
NetworkedHubManager.Instance.CmdPlayPianoKey(key);
}
}
void OnDestroy()
{
_running = false;
_udp?.Close();
_listenThread?.Join(500);
}
}
}
/*
mcs -target:library -out:BepInEx/plugins/PianoSocketPlugin.dll \
-r:BepInEx/core/BepInEx.dll \
-r:"In Sink_Data"/Managed/UnityEngine.dll \
-r:"In Sink_Data"/Managed/UnityEngine.CoreModule.dll \
-r:"In Sink_Data"/Managed/Assembly-CSharp.dll \
-r:"In Sink_Data"/Managed/netstandard.dll \
-r:"In Sink_Data"/Managed/Mirror.dll \
BepInEx/plugins/PianoSocketPlugin.cs
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment