void _u_ReceiveResponse()
    {
        responsesReceived++;

        // Requies convention public variable and event names
        UdonSharpBehaviour usb = connectionRequesters[currentID];

        if (connectionIsWebSocket[currentID])
        {
            // First byte is the text/binary flag, with highest bit indiciating
            // that this is a dummy 'connection closed' response.  Second highest
            // bit indicates a websocket opened message.
            if ((connectionData[currentID][0] & 0x40) == 0x40)
            {
                usb.SetProgramVariable("connectionID", currentID);
                usb.SendCustomEvent("_u_WebSocketOpened");
            }
            else if ((connectionData[currentID][0] & 0x80) == 0x80)
            {
                usb.SetProgramVariable("connectionID", currentID);
                usb.SendCustomEvent("_u_WebSocketClosed");
                connectionRequesters[currentID] = null;
                connectionsOpen--;
                _u_SendCallback("_u_OnUdonMIDIWebHandlerConnectionCountChanged");
            }
            else
            {
                usb.SetProgramVariable("connectionID", currentID);
                bool   messageIsText = (connectionData[currentID][0] & 0x1) == 0x0;
                byte[] messsageData  = new byte[connectionData[currentID].Length - 1];
                Array.Copy(connectionData[currentID], 1, messsageData, 0, connectionData[currentID].Length - 1);
                usb.SetProgramVariable("messageIsText", messageIsText);
                if (messageIsText && connectionReturnsStrings[currentID])
                {
                    usb.SetProgramVariable("connectionString", new string(connectionDataChars[currentID]));
                }
                else
                {
                    usb.SetProgramVariable("connectionData", messsageData);
                }
                usb.SendCustomEvent("_u_WebSocketReceive");
            }
        }
        else
        {
            // Non-websocket response
            // First 4 bytes of data are an int for HTTP response code or request error code
            int    responseCode = _u_BitConverterToInt32(connectionData[currentID], 0);
            byte[] responseData = new byte[connectionData[currentID].Length - 4];
            Array.Copy(connectionData[currentID], 4, responseData, 0, connectionData[currentID].Length - 4);
            // Process loopback connection
            if (currentID == LOOPBACK_CONNECTION_ID)
            {
                string connectionString = new string(connectionDataChars[currentID]);
                // responseCode determines what kind of message it is, rather than typical HTTP
                // or extra Udon handler response codes.
                if (responseCode == 0) // ping response
                {
                    if (online == false)
                    {
                        online = true;
                        playersOnline++;
                        _u_SendCallback("_u_OnUdonMIDIWebHandlerOnlineChanged");
                    }
                    awaitingPong            = false;
                    framesSinceAwaitingPong = 0;
                    string[] split = connectionString.Split(' ');
                    queuedResponsesCount = Int32.Parse(split[0]);
                    queuedBytesCount     = Int32.Parse(split[1]);
                }
                else if (responseCode == 1) // avatar change
                {
                    string[] split = connectionString.Split('\n');
                    _u_SendAvatarChangedCallback(split[0], split[1]);
                }
            }
            else
            {
                usb.SetProgramVariable("connectionID", currentID);
                usb.SetProgramVariable("responseCode", responseCode);
                if (connectionReturnsStrings[currentID])
                {
                    usb.SetProgramVariable("connectionString", new string(connectionDataChars[currentID]));
                }
                else
                {
                    usb.SetProgramVariable("connectionData", responseData);
                }
                usb.SendCustomEvent("_u_WebRequestReceived");
                connectionRequesters[currentID] = null;
                connectionsOpen--;
                _u_SendCallback("_u_OnUdonMIDIWebHandlerConnectionCountChanged");
            }
        }

        // Reset response arary
        connectionData[currentID]        = null;
        connectionDataOffsets[currentID] = 0;
    }
    void Update()
    {
        secondsSinceLastPing += Time.deltaTime;
        if (secondsSinceLastPing > PING_INTERVAL_SECONDS)
        {
            // Ping the helper program using a dedicated command, on a dedicated connection ID.
            // The loopback response from this has priority over all 255 other connections.
            // This is done on a dedicated connectionID to ensure it is received like all other
            // MIDI frames, and to make sure there's always a free, prioritized line for auxiliary data.
            Debug.Log("[Udon-MIDI-Web-Helper] PING");
            commandsSent++;
            secondsSinceLastPing = 0;
            awaitingPong         = true;
        }

        if (awaitingPong)
        {
            // Measure time since ping response in number of frames, as the response should be
            // nearly instantaneous.  VRChat may lag and drop connection for a while, but the
            // helper program never should.  If VRChat is running successfully and pong isn't
            // received, its safe to assume the connection is dead.
            if (++framesSinceAwaitingPong > PONG_TIMEOUT_FRAMES && online)
            {
                online = false;
                playersOnline--;
                _u_SendCallback("_u_OnUdonMIDIWebHandlerOnlineChanged");
                // If the handler goes offline, there is no hope of reconnecting until restart.
                // Cancel/close all open connections with a failure response code.
                for (int i = 0; i < MAX_USABLE_CONNECTIONS; i++)
                {
                    if (connectionRequesters[i] != null)
                    {
                        UdonSharpBehaviour usb = connectionRequesters[i];
                        usb.SetProgramVariable("connectionID", i);
                        if (connectionIsWebSocket[i])
                        {
                            usb.SendCustomEvent("_u_WebSocketClosed");
                        }
                        else
                        {
                            usb.SetProgramVariable("responseCode", RESPONSE_CODE_DISCONNECTED);
                            usb.SendCustomEvent("_u_WebRequestReceived");
                        }
                        connectionRequesters[i] = null;
                        connectionsOpen--;
                        _u_SendCallback("_u_OnUdonMIDIWebHandlerConnectionCountChanged");
                    }
                }
            }
        }

        // VRChat's update order for Udon appears to go: Update(), LateUpdate(), MidiNoteX(), at least
        // with VRC Midi Listener after Udon Behaviour on the same gameobject.
        // Therefore, it should be safe to print a READY or ACK each frame without waiting a game tick.
        // (This whole protocol is built around VRChat crashing if more than 1 midi frame of data is recevied per game tick)
        // Hopefully this means the Log-MIDI communication is fast enough to send a new frame
        // before the game loops around again, ideally resulting in one MIDI frame every game tick.
        if (currentFrameOffset == usableBytesThisFrame)
        {
            Debug.Log("[Udon-MIDI-Web-Helper] ACK");
            currentFrameOffset    = 0;
            secondsSinceLastReady = 0; // Don't send a ready, ACK doubles as a ready
        }

        secondsSinceLastReady += Time.deltaTime;
        if (secondsSinceLastReady > READY_TIMEOUT_SECONDS)
        {
            // Only send a RDY exactly READY_TIMEOUT_SECONDS after at least one connection was opened
            Debug.Log("[Udon-MIDI-Web-Helper] READY"); // Ready message lets the helper know its safe to send a new frame OR if a frame was dropped and never received
            secondsSinceLastReady = 0;
        }
    }