Created
April 15, 2026 23:48
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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