// voice_bridge_v8.js — Voice Bridge with Audio Injection (attach after voice chat opened)
//
// STARTUP ORDER:
// 1. Launch app: adb shell monkey -p com.antgroup.aijk.android ...
// 2. Open voice chat manually or via adb tap
// 3. Wait for libantaudio.so to load
// 4. Attach frida with this script
//
// Hook point: libantaudio.so MFAntAudio3AV2Filter::process(micIn, spkRef, out, size, &result)
// TCP :18901
// Frame: 4-byte len + 1-byte type + payload
//   type 0: speaker/AI audio (spkRef, downstream to client)
//   type 1: text/JSON command
//   type 2: mic audio (micIn, downstream to client)
//   type 3: inject audio (upstream from client, replaces micIn)

var voiceActive = false;
var clientOS = null;
var capturedSpk = 0, capturedMic = 0, spkBytes = 0, micBytes = 0;
var injectMode = false;
var injectQueue = [];

function wf(os, type, jArr) {
    try {
        var len = jArr.length;
        var h = Java.array("byte", [(len>>24)&0xFF,(len>>16)&0xFF,(len>>8)&0xFF,len&0xFF, type]);
        os.write(h); os.write(jArr); os.flush();
    } catch(e) {}
}
function wt(os, text) {
    wf(os, 1, Java.use("java.lang.String").$new(text).getBytes("UTF-8"));
}

// === Hook libantaudio.so (should already be loaded) ===
var hooked = false;
function tryHook() {
    if (hooked) return;
    var m = Process.findModuleByName("libantaudio.so");
    if (!m) return;
    var addr = m.findExportByName("_ZN8antaudio20MFAntAudio3AV2Filter7processEPhS1_S1_iRi");
    if (!addr) return;
    hooked = true;

    Interceptor.attach(addr, {
        onEnter: function(args) {
            if (!voiceActive || !clientOS) return;
            var size = args[4].toInt32();
            if (size <= 0) return;

            try {
                if (injectMode) {
                    if (injectQueue.length > 0) {
                        var frame = injectQueue.shift();
                        if (frame.byteLength === size) {
                            args[1].writeByteArray(frame);
                        } else {
                            var buf = new ArrayBuffer(size);
                            var dst = new Uint8Array(buf);
                            var src = new Uint8Array(frame);
                            var copyLen = Math.min(size, frame.byteLength);
                            for (var k = 0; k < copyLen; k++) dst[k] = src[k];
                            args[1].writeByteArray(buf);
                        }
                    } else {
                        var silence = new ArrayBuffer(size);
                        args[1].writeByteArray(silence);
                    }
                }

                // Always capture speaker/AI output (type 0)
                var spkPcm = args[2].readByteArray(size);
                var spkArr = Java.array("byte", Array.from(new Uint8Array(spkPcm)));
                wf(clientOS, 0, spkArr);
                capturedSpk++; spkBytes += size;

                if (!injectMode) {
                    var micPcm = args[1].readByteArray(size);
                    var micArr = Java.array("byte", Array.from(new Uint8Array(micPcm)));
                    wf(clientOS, 2, micArr);
                }
                capturedMic++; micBytes += size;

                if (capturedMic <= 3 || capturedMic % 500 === 0)
                    console.log("[VOICE] mic=" + capturedMic + " spk=" + capturedSpk + " inject=" + injectQueue.length);
            } catch(e) {}
        }
    });
    console.log("[VOICE] process hooked @ " + addr);
}

// Hook immediately — lib should already be loaded since voice chat is open
tryHook();
if (!hooked) {
    // Retry a few times in case of timing
    [500, 1000, 2000, 5000].forEach(function(ms) { setTimeout(tryHook, ms); });
}

// === TCP Server ===
Java.perform(function() {
    var SS = Java.use("java.net.ServerSocket");
    var JS = Java.use("java.lang.String");
    var server = SS.$new(18901);
    console.log("[VOICE] Listening :18901");

    var Srv = Java.registerClass({
        name: "com.antaf.voice.S8",
        implements: [Java.use("java.lang.Runnable")],
        methods: {
            run: function() {
                while (true) {
                    try {
                        console.log("[VOICE] Waiting for client...");
                        var c = server.accept();
                        var is = c.getInputStream();
                        var os = c.getOutputStream();
                        clientOS = os;
                        console.log("[VOICE] Client connected");
                        wt(os, JSON.stringify({
                            event:"connected", protocol:"antaf-voice-v8",
                            hooked: hooked,
                            commands:["start","stop","status","inject_on","inject_off"],
                            audio:"pcm-16bit-960b-frames",
                            frameTypes:{0:"spk_ai",1:"text",2:"mic",3:"inject"}
                        }));

                        while (true) {
                            var hb = [];
                            for (var i=0;i<5;i++) { var b=is.read(); if(b<0) throw "EOF"; hb.push(b); }
                            var fl=(hb[0]<<24)|(hb[1]<<16)|(hb[2]<<8)|hb[3], ft=hb[4];
                            if (fl>1048576) break;
                            var pb = [];
                            for (var i=0;i<fl;i++) { var b=is.read(); if(b<0) throw "EOF"; pb.push(b&0xFF); }

                            if (ft === 3) {
                                var arr = new ArrayBuffer(pb.length);
                                var view = new Uint8Array(arr);
                                for (var j=0;j<pb.length;j++) view[j] = pb[j];
                                injectQueue.push(arr);
                            }
                            else if (ft === 1) {
                                var pl = Java.array("byte", pb);
                                var cmd = JSON.parse(JS.$new(pl,"UTF-8").toString());
                                console.log("[VOICE] Cmd: " + JSON.stringify(cmd));
                                if (cmd.cmd === "start") {
                                    voiceActive = true;
                                    capturedSpk=0;capturedMic=0;spkBytes=0;micBytes=0;
                                    injectQueue = [];
                                    wt(os, JSON.stringify({event:"started",hooked:hooked}));
                                }
                                else if (cmd.cmd === "stop") {
                                    voiceActive = false;
                                    injectMode = false;
                                    injectQueue = [];
                                    wt(os, JSON.stringify({event:"stopped",spk:{frames:capturedSpk,bytes:spkBytes},mic:{frames:capturedMic,bytes:micBytes}}));
                                }
                                else if (cmd.cmd === "inject_on") {
                                    injectMode = true;
                                    injectQueue = [];
                                    wt(os, JSON.stringify({event:"inject_on"}));
                                    console.log("[VOICE] Inject ON");
                                }
                                else if (cmd.cmd === "inject_off") {
                                    injectMode = false;
                                    injectQueue = [];
                                    wt(os, JSON.stringify({event:"inject_off"}));
                                    console.log("[VOICE] Inject OFF");
                                }
                                else if (cmd.cmd === "status") {
                                    wt(os, JSON.stringify({event:"status",active:voiceActive,hooked:hooked,inject:injectMode,queue:injectQueue.length,spk:{frames:capturedSpk,bytes:spkBytes},mic:{frames:capturedMic,bytes:micBytes}}));
                                }
                            }
                        }
                    } catch(e) { console.log("[VOICE] Client disconnected: "+e); }
                    finally { voiceActive=false; clientOS=null; injectMode=false; injectQueue=[]; }
                }
            }
        }
    });
    Java.use("java.lang.Thread").$new(Srv.$new()).start();
    console.log("[VOICE] Ready (v8, no open_voice)");
});
