Cara Mengontrol HP Android dari Linux: Screen Mirroring, Rekam Layar, Reverse Internet, dan Audio DSP
Apakah Anda mencari cara praktis untuk mengelola HP Android langsung dari PC/Laptop Linux? Script All-in-One berbasis Python GUI (Tkinter) ini menggabungkan keandalan Scrcpy, Gnirehtet, dan FFmpeg DSP Audio ke dalam satu tampilan ringkas dan intuitif.
Fitur Utama Aplikasi
- Screen Mirroring & Recording: Tampilkan layar HP di PC dengan kontrol penuh (kustomisasi resolusi, bitrate, FPS) serta opsi rekam layar otomatis (.mp4/.mkv).
- Reverse Internet (Gnirehtet): Alirkan koneksi internet PC ke HP via kabel USB tanpa root atau hotspot. Dilengkapi fitur Auto-Reconnect.
- Audio DSP & Vocal Control: Equalizer audio real-time untuk Linux dengan 15 preset, penguat bass, serta fitur isolasi/penghilang vokal.
1. Persiapan Dependensi (Prasyarat)
Sebelum menjalankan script, pastikan sistem Linux Anda telah terpasang aplikasi pendukung berikut. Buka terminal dan jalankan:
# Update & install dependensi dasar pada Ubuntu/Debian/Linux Mint
sudo apt update
sudo apt install python3 python3-tk adb scrcpy ffmpeg pulseaudio-utils -y
Khusus untuk fitur Reverse Internet (Gnirehtet):
- Unduh binary
gnirehtet-rust-linux-x86_64-v...zipdari halaman GitHub resmi Gnirehtet. - Ekstrak dan letakkan file executable
gnirehtetdi folderDownloads,Desktop, atau dalam satu folder yang sama dengan script Python ini.
2. Langkah Pemasangan & Menjalankan Aplikasi
- Buat file baru bernama
android_suite.pydi komputer Anda. - Salin seluruh kode Python di bawah ini lalu simpan ke dalam file tersebut:
#!/usr/bin/env python3
import os
import sys
import time
import shutil
import queue
import threading
import subprocess
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
# ============================================================
# AUDIO DSP CONFIG & CONSTANTS
# ============================================================
RATE = 48000
CHANNELS = 2
FFMPEG_PCM_CODEC = "pcm_f32le"
INPUT_FRAME_BYTES = 512
INPUT_FRAGMENT_BYTES = 1024
OUTPUT_BUFFER_BYTES = 4096
OUTPUT_PREBUF_BYTES = 0
OUTPUT_MINREQ_BYTES = 512
PULSE_LATENCY_MSEC = 8
LIVE_UPDATE_DELAY_MSEC = 100
# ============================================================
# SYSTEM & AUDIO HELPERS
# ============================================================
def run_cmd(cmd, check=True):
return subprocess.run(
cmd,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=check
)
def pactl(args, check=True):
return run_cmd(["pactl"] + args, check=check)
def have_cmd(name):
return shutil.which(name) is not None
def clamp(v, lo, hi):
return max(lo, min(hi, float(v)))
def ffmpeg_has_pulse_io():
demux = run_cmd(["ffmpeg", "-hide_banner", "-h", "demuxer=pulse"], check=False)
mux = run_cmd(["ffmpeg", "-hide_banner", "-h", "muxer=pulse"], check=False)
demux_text = ((demux.stdout or "") + (demux.stderr or "")).lower()
mux_text = ((mux.stdout or "") + (mux.stderr or "")).lower()
has_input = "demuxer pulse" in demux_text or "pulse audio input" in demux_text
has_output = "muxer pulse" in mux_text or "pulse audio output" in mux_text
return has_input and has_output
def get_default_sink():
try:
result = pactl(["get-default-sink"], check=False)
output = result.stdout.strip()
if output:
return output
except Exception:
pass
try:
result = pactl(["info"], check=False)
for line in result.stdout.splitlines():
if line.lower().startswith("default sink:"):
return line.split(":", 1)[1].strip()
except Exception:
pass
return ""
def list_sinks():
result = pactl(["list", "short", "sinks"], check=False)
sinks = []
if result and result.stdout:
for line in result.stdout.splitlines():
parts = line.split("\t")
if len(parts) < 2:
continue
name = parts[1]
if name.startswith("pybass_"):
continue
sinks.append(name)
return sinks
def move_all_sink_inputs(target_sink):
try:
result = pactl(["list", "short", "sink-inputs"], check=False)
if result and result.stdout:
for line in result.stdout.splitlines():
parts = line.split("\t")
if not parts:
continue
sink_input_id = parts[0].strip()
if sink_input_id:
pactl(["move-sink-input", sink_input_id, target_sink], check=False)
except Exception:
pass
def cleanup_old_pybass(output_sink=None):
try:
if output_sink:
pactl(["set-default-sink", output_sink], check=False)
move_all_sink_inputs(output_sink)
time.sleep(0.15)
result = pactl(["list", "short", "modules"], check=False)
modules_to_remove = []
if result and result.stdout:
for line in result.stdout.splitlines():
parts = line.split("\t")
if len(parts) < 3:
continue
module_id, module_name, module_args = parts[0], parts[1], parts[2]
if module_name == "module-null-sink" and "sink_name=pybass_" in module_args:
modules_to_remove.append(module_id)
for module_id in modules_to_remove:
pactl(["unload-module", module_id], check=False)
except Exception:
pass
def find_gnirehtet_binary():
found = shutil.which("gnirehtet")
if found:
return found
home = os.path.expanduser("~")
script_dir = os.path.dirname(os.path.abspath(__file__))
candidates = [
os.path.join(script_dir, "gnirehtet"),
os.path.join(home, "gnirehtet"),
os.path.join(home, "Desktop", "gnirehtet"),
os.path.join(home, "Downloads", "gnirehtet"),
os.path.join(home, "Unduhan", "gnirehtet"),
"/usr/bin/gnirehtet",
"/usr/local/bin/gnirehtet",
"/opt/gnirehtet/gnirehtet"
]
for candidate in candidates:
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
for search_dir in [script_dir, os.path.join(home, "Desktop"), os.path.join(home, "Downloads"), os.path.join(home, "Unduhan"), home]:
if os.path.exists(search_dir):
for root, _, files in os.walk(search_dir):
if "gnirehtet" in files:
full_p = os.path.join(root, "gnirehtet")
if os.access(full_p, os.X_OK):
return full_p
if root.count(os.path.sep) - search_dir.count(os.path.sep) >= 2:
break
return ""
def build_ffmpeg_filter(p):
preamp = clamp(p["preamp_db"], -24, -4)
bass_db = clamp(p["bass_db"], 0, 20)
bass_freq = clamp(p["bass_freq"], 55, 120)
chest_db = clamp(p["chest_db"], 0, 12)
chest_freq = clamp(p["chest_freq"], 65, 90)
kick_db = clamp(p["kick_db"], 0, 8)
kick_freq = clamp(p["kick_freq"], 100, 130)
mud_cut = clamp(p["mud_cut_db"], -9, 0)
rumble = clamp(p["rumble_freq"], 25, 45)
clarity = clamp(p["clarity_db"], 0, 4)
output = clamp(p["output_db"], -6, 3)
filters = []
vocal_mode = p.get("vocal_mode", "normal")
vocal_strength = clamp(p.get("vocal_strength", 1.0), 0.0, 1.0)
if vocal_mode == "remove":
filters.append(f"extrastereo=m={vocal_strength * 2.5}:c=0")
filters.append(f"pan=stereo|c0=c0-{vocal_strength}*c1|c1=c1-{vocal_strength}*c0")
filters.append(f"volume={1.5 + vocal_strength * 0.5}")
elif vocal_mode == "isolate":
filters.append(f"pan=stereo|c0=c0+{vocal_strength}*c1|c1=c0+{vocal_strength}*c1")
filters.append("bandpass=f=800:width_type=h:width=2500")
filters.append(f"volume={2.0 + vocal_strength}")
elif vocal_mode == "enhance":
filters.append(f"equalizer=f=1200:t=q:w=1.5:g={3 + vocal_strength * 4}")
filters.append(f"equalizer=f=3500:t=q:w=1.2:g={2 + vocal_strength * 3}")
filters.append("compand=attacks=0.1:decays=0.2:points=-80/-80|-45/-30|-27/-20|0/-10")
if vocal_mode != "isolate":
filters.append(f"volume={preamp:.2f}dB")
filters.append(f"highpass=f={rumble:.1f}")
filters.append(f"bass=g={bass_db:.2f}:f={bass_freq:.1f}:t=q:w=0.70")
filters.append(f"equalizer=f={chest_freq:.1f}:t=q:w=1.05:g={chest_db:.2f}")
filters.append(f"equalizer=f={kick_freq:.1f}:t=q:w=1.20:g={kick_db:.2f}")
filters.append(f"equalizer=f=245:t=q:w=0.85:g={mud_cut:.2f}")
if clarity > 0.01:
filters.append(f"treble=g={clarity:.2f}:f=6500:t=q:w=0.70")
filters.append(f"volume={output:.2f}dB")
if p["limiter"]:
filters.append("alimiter=limit=0.93:attack=1:release=30")
return ",".join(filters)
# ============================================================
# WORKER THREAD FOR AUDIO PIPELINE
# ============================================================
class PipelineWorker(threading.Thread):
def __init__(self, virtual_sink, output_sink, ff_filter, msg_queue):
super().__init__(daemon=True)
self.virtual_sink = virtual_sink
self.output_sink = output_sink
self.ff_filter = ff_filter
self.msg_queue = msg_queue
self.stop_event = threading.Event()
self.reload_event = threading.Event()
self.filter_lock = threading.Lock()
self.fx_proc = None
def update_filter(self, new_filter):
with self.filter_lock:
self.ff_filter = new_filter
self.reload_event.set()
def stop(self):
self.stop_event.set()
self._kill_process()
def _kill_process(self):
proc = self.fx_proc
if not proc:
return
if proc.poll() is None:
try:
proc.terminate()
proc.wait(timeout=0.30)
except subprocess.TimeoutExpired:
try:
proc.kill()
proc.wait(timeout=0.30)
except Exception:
pass
except Exception:
try:
proc.kill()
except Exception:
pass
self.fx_proc = None
def _read_error(self, proc):
if not proc or not proc.stderr:
return ""
try:
return proc.stderr.read().decode(errors="ignore").strip()
except Exception:
return ""
def _start_pipeline(self):
with self.filter_lock:
current_filter = self.ff_filter
monitor_source = f"{self.virtual_sink}.monitor"
env = os.environ.copy()
env["PULSE_LATENCY_MSEC"] = str(PULSE_LATENCY_MSEC)
env["PIPEWIRE_LATENCY"] = "128/48000"
command = [
"ffmpeg", "-nostdin", "-hide_banner", "-nostats", "-loglevel", "error",
"-filter_threads", "1", "-fflags", "nobuffer", "-probesize", "32",
"-analyzeduration", "0", "-thread_queue_size", "64", "-f", "pulse",
"-sample_rate", str(RATE), "-channels", str(CHANNELS),
"-frame_size", str(INPUT_FRAME_BYTES), "-fragment_size", str(INPUT_FRAGMENT_BYTES),
"-i", monitor_source, "-map", "0:a:0", "-vn", "-sn", "-dn",
"-af", current_filter, "-ar", str(RATE), "-ac", str(CHANNELS),
"-c:a", FFMPEG_PCM_CODEC, "-flush_packets", "1", "-f", "pulse",
"-device", self.output_sink, "-buffer_size", str(OUTPUT_BUFFER_BYTES),
"-prebuf", str(OUTPUT_PREBUF_BYTES), "-minreq", str(OUTPUT_MINREQ_BYTES),
"-stream_name", "PyBass Direct DSP", "PyBass_Direct_DSP"
]
self.fx_proc = subprocess.Popen(
command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE, bufsize=0, env=env
)
def run(self):
try:
self._start_pipeline()
time.sleep(0.15)
if self.fx_proc and self.fx_proc.poll() is not None:
error = self._read_error(self.fx_proc) or "FFmpeg direct DSP gagal dijalankan."
raise RuntimeError(error)
self.msg_queue.put(("status", "DSP direct aktif - mode low latency."))
while not self.stop_event.is_set():
if self.reload_event.is_set():
self.reload_event.clear()
self.msg_queue.put(("status", "Memperbarui filter..."))
self._kill_process()
if self.stop_event.is_set():
break
time.sleep(0.02)
self._start_pipeline()
time.sleep(0.08)
if self.fx_proc and self.fx_proc.poll() is not None:
error = self._read_error(self.fx_proc) or "FFmpeg berhenti saat update."
raise RuntimeError(error)
self.msg_queue.put(("status", "Filter berhasil diperbarui."))
continue
if self.fx_proc and self.fx_proc.poll() is not None:
error = self._read_error(self.fx_proc) or "FFmpeg berhenti tanpa pesan error."
raise RuntimeError(error)
time.sleep(0.05)
except Exception as e:
if not self.stop_event.is_set():
self.msg_queue.put(("error", str(e)))
finally:
self._kill_process()
if self.stop_event.is_set():
self.msg_queue.put(("status", "DSP berhenti."))
# ============================================================
# TAB 1: SCREEN MIRRORING & RECORDING (SCRCPY)
# ============================================================
class MirrorTab(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.auto_connect = False
self.auto_thread = None
self.running_scrcpy_procs = []
self.build_ui()
def build_ui(self):
frame_dev = ttk.LabelFrame(self, text="Perangkat Terhubung")
frame_dev.pack(fill="x", padx=10, pady=5)
self.device_combo = ttk.Combobox(frame_dev, state="readonly")
self.device_combo.pack(side="left", fill="x", expand=True, padx=5, pady=5)
btn_refresh = ttk.Button(frame_dev, text="Refresh Perangkat", command=self.refresh_devices)
btn_refresh.pack(side="right", padx=5, pady=5)
# --- MENU KONEKSI SCRCPY VIA WIFI ---
frame_wifi = ttk.LabelFrame(self, text="Koneksi Scrcpy via WiFi (ADB Wireless)")
frame_wifi.pack(fill="x", padx=10, pady=5)
wifi_inner = ttk.Frame(frame_wifi)
wifi_inner.pack(fill="x", padx=5, pady=5)
ttk.Label(wifi_inner, text="IP:Port HP:").pack(side="left", padx=(0, 5))
self.wifi_ip_var = tk.StringVar(value="192.168.1.X:5555")
ttk.Entry(wifi_inner, textvariable=self.wifi_ip_var, width=18).pack(side="left", padx=5)
ttk.Button(wifi_inner, text="Connect WiFi", command=self.connect_wifi).pack(side="left", padx=5)
ttk.Button(wifi_inner, text="Disconnect WiFi", command=self.disconnect_wifi).pack(side="left", padx=5)
# --- PENGATURAN REKAM LAYAR ---
frame_rec = ttk.LabelFrame(self, text="Pengaturan Perekaman Layar (Screen Recording)")
frame_rec.pack(fill="x", padx=10, pady=5)
self.enable_record_var = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_rec, text="Aktifkan Perekaman Layar Saat Mirroring (-r)", variable=self.enable_record_var).pack(anchor="w", padx=5, pady=4)
rec_dir_frame = ttk.Frame(frame_rec)
rec_dir_frame.pack(fill="x", padx=5, pady=4)
ttk.Label(rec_dir_frame, text="Folder Simpan:").pack(side="left")
default_dir = os.path.expanduser("~/Videos") if os.path.exists(os.path.expanduser("~/Videos")) else os.path.expanduser("~")
self.rec_dir_var = tk.StringVar(value=default_dir)
ttk.Entry(rec_dir_frame, textvariable=self.rec_dir_var).pack(side="left", fill="x", expand=True, padx=5)
ttk.Button(rec_dir_frame, text="Pilih Folder", command=self.browse_rec_dir).pack(side="right")
rec_fmt_frame = ttk.Frame(frame_rec)
rec_fmt_frame.pack(fill="x", padx=5, pady=4)
ttk.Label(rec_fmt_frame, text="Format File Rekaman:").pack(side="left")
self.rec_format_var = tk.StringVar(value="mp4")
self.rec_fmt_combo = ttk.Combobox(rec_fmt_frame, textvariable=self.rec_format_var, values=["mp4", "mkv"], width=8, state="readonly")
self.rec_fmt_combo.pack(side="left", padx=5)
# --- PENGATURAN SCRCPY LENGKAP ---
frame_opts = ttk.LabelFrame(self, text="Pengaturan Display & Performa")
frame_opts.pack(fill="x", padx=10, pady=5)
frame_opts.columnconfigure(1, weight=1)
ttk.Label(frame_opts, text="Resolusi Maksimal:").grid(row=0, column=0, sticky="w", padx=5, pady=5)
self.size_var = tk.StringVar(value="1024")
self.size_combo = ttk.Combobox(frame_opts, textvariable=self.size_var, values=[
"Asli / Max (Original)", "1920 (Full HD)", "1600 (2K Scaling)",
"1440", "1280 (HD 720p)", "1024 (Standard)", "800 (Low)", "720"
])
self.size_combo.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
ttk.Label(frame_opts, text="Bitrate Video:").grid(row=1, column=0, sticky="w", padx=5, pady=5)
self.bitrate_var = tk.StringVar(value="8 Mbps")
self.bitrate_combo = ttk.Combobox(frame_opts, textvariable=self.bitrate_var, values=[
"2 Mbps", "4 Mbps", "8 Mbps", "12 Mbps", "16 Mbps", "24 Mbps", "32 Mbps"
])
self.bitrate_combo.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
ttk.Label(frame_opts, text="Max FPS:").grid(row=2, column=0, sticky="w", padx=5, pady=5)
self.fps_var = tk.StringVar(value="60")
self.fps_combo = ttk.Combobox(frame_opts, textvariable=self.fps_var, values=[
"30", "60", "90", "120", "Tanpa Batas (Uncapped)"
])
self.fps_combo.grid(row=2, column=1, sticky="ew", padx=5, pady=5)
frame_checks = ttk.Frame(frame_opts)
frame_checks.grid(row=3, column=0, columnspan=2, sticky="w", padx=5, pady=5)
self.stay_awake_var = tk.BooleanVar(value=True)
ttk.Checkbutton(frame_checks, text="Layar Tetap Menyala (-w)", variable=self.stay_awake_var).pack(anchor="w", pady=2)
self.turn_off_screen_var = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_checks, text="Matikan Layar HP Saat Mirroring (-S)", variable=self.turn_off_screen_var).pack(anchor="w", pady=2)
self.show_touches_var = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_checks, text="Tampilkan Sentuhan Layar (--show-touches)", variable=self.show_touches_var).pack(anchor="w", pady=2)
self.always_on_top_var = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_checks, text="Jendela Selalu di Atas (--always-on-top)", variable=self.always_on_top_var).pack(anchor="w", pady=2)
self.fullscreen_var = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_checks, text="Mode Layar Penuh / Fullscreen (-f)", variable=self.fullscreen_var).pack(anchor="w", pady=2)
self.no_audio_var = tk.BooleanVar(value=False)
ttk.Checkbutton(frame_checks, text="Matikan Audio Transmit (--no-audio)", variable=self.no_audio_var).pack(anchor="w", pady=2)
frame_act = ttk.Frame(self)
frame_act.pack(fill="x", padx=10, pady=10)
btn_start = ttk.Button(frame_act, text="MIRROR / REKAM LAYAR SEKARANG", command=self.start_scrcpy)
btn_start.pack(fill="x", pady=2)
self.btn_auto = ttk.Button(frame_act, text="START AUTO CONNECT MIRROR", command=self.toggle_auto_connect)
self.btn_auto.pack(fill="x", pady=2)
self.refresh_devices()
def connect_wifi(self):
ip_port = self.wifi_ip_var.get().strip()
if not ip_port:
messagebox.showwarning("Peringatan", "Masukkan alamat IP & Port perangkat terlebih dahulu!")
return
try:
res = subprocess.run(["adb", "connect", ip_port], capture_output=True, text=True)
output = res.stdout.strip()
if "connected" in output.lower():
messagebox.showinfo("Berhasil", f"Berhasil terhubung ke {ip_port}")
self.refresh_devices()
else:
messagebox.showerror("Gagal", f"Gagal terhubung:\n{output}")
except Exception as e:
messagebox.showerror("Error", f"Terjadi kesalahan:\n{e}")
def disconnect_wifi(self):
ip_port = self.wifi_ip_var.get().strip()
if not ip_port:
return
try:
subprocess.run(["adb", "disconnect", ip_port], capture_output=True, text=True)
messagebox.showinfo("Informasi", f"Koneksi ke {ip_port} diputus.")
self.refresh_devices()
except Exception as e:
messagebox.showerror("Error", f"Terjadi kesalahan:\n{e}")
def browse_rec_dir(self):
selected = filedialog.askdirectory(title="Pilih Folder Penyimpanan Rekaman")
if selected:
self.rec_dir_var.set(selected)
def build_cmd(self, device=None):
cmd = ["scrcpy"]
if device:
cmd.extend(["-s", device])
size_raw = self.size_var.get().split()[0]
if size_raw.isdigit():
cmd.extend(["-m", size_raw])
bitrate_raw = self.bitrate_var.get().split()[0]
if bitrate_raw.isdigit():
cmd.extend(["-b", f"{bitrate_raw}M"])
fps_raw = self.fps_var.get().split()[0]
if fps_raw.isdigit():
cmd.extend(["--max-fps", fps_raw])
if self.stay_awake_var.get():
cmd.append("-w")
if self.turn_off_screen_var.get():
cmd.append("-S")
if self.show_touches_var.get():
cmd.append("--show-touches")
if self.always_on_top_var.get():
cmd.append("--always-on-top")
if self.fullscreen_var.get():
cmd.append("-f")
if self.no_audio_var.get():
cmd.append("--no-audio")
if self.enable_record_var.get():
save_dir = self.rec_dir_var.get().strip()
if not save_dir:
save_dir = os.path.expanduser("~")
os.makedirs(save_dir, exist_ok=True)
timestamp = time.strftime("%Y%m%d_%H%M%S")
fmt = self.rec_format_var.get().strip().lower()
rec_filename = os.path.join(save_dir, f"scrcpy_rec_{timestamp}.{fmt}")
cmd.extend(["-r", rec_filename])
return cmd
def refresh_devices(self):
devices = []
try:
res = subprocess.run(["adb", "devices"], capture_output=True, text=True)
for line in res.stdout.splitlines()[1:]:
if "\tdevice" in line:
devices.append(line.split("\t")[0])
except Exception:
pass
self.device_combo["values"] = devices
if devices:
self.device_combo.current(0)
else:
self.device_combo.set("")
def start_scrcpy(self):
if not have_cmd("scrcpy"):
messagebox.showerror("Error", "Perintah 'scrcpy' tidak terinstall atau tidak ditemukan di sistem!")
return
device = self.device_combo.get()
cmd = self.build_cmd(device)
try:
p = subprocess.Popen(cmd)
self.running_scrcpy_procs.append(p)
except Exception as e:
messagebox.showerror("Error", f"Gagal menjalankan Scrcpy:\n{e}")
def toggle_auto_connect(self):
self.auto_connect = not self.auto_connect
if self.auto_connect:
self.btn_auto.config(text="STOP AUTO CONNECT MIRROR")
self.auto_thread = threading.Thread(target=self._auto_connect_loop, daemon=True)
self.auto_thread.start()
else:
self.btn_auto.config(text="START AUTO CONNECT MIRROR")
def _auto_connect_loop(self):
connected = set()
while self.auto_connect:
try:
res = subprocess.run(["adb", "devices"], capture_output=True, text=True)
current = set()
for line in res.stdout.splitlines()[1:]:
if "\tdevice" in line:
current.add(line.split("\t")[0])
new_devs = current - connected
for dev in new_devs:
cmd = self.build_cmd(dev)
subprocess.Popen(cmd)
connected = current
except Exception:
pass
time.sleep(2)
# ============================================================
# TAB 2: REVERSE INTERNET (GNIREHTET WITH AUTO-RECONNECT)
# ============================================================
class ReverseNetTab(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.process = None
self.auto_reconnect = False
self.auto_thread = None
self.gnirehtet_path = find_gnirehtet_binary()
self.build_ui()
def build_ui(self):
frame_bin = ttk.LabelFrame(self, text="Lokasi Executable Gnirehtet")
frame_bin.pack(fill="x", padx=10, pady=5)
self.path_var = tk.StringVar(value=self.gnirehtet_path)
ttk.Entry(frame_bin, textvariable=self.path_var).pack(side="left", fill="x", expand=True, padx=5, pady=5)
btn_box = ttk.Frame(frame_bin)
btn_box.pack(side="right", padx=5, pady=5)
ttk.Button(btn_box, text="Deteksi Ulang", command=self.auto_detect).pack(side="left", padx=2)
ttk.Button(btn_box, text="Cari Manual", command=self.browse_binary).pack(side="left", padx=2)
frame_ctrl = ttk.LabelFrame(self, text="Koneksi Internet HP via PC")
frame_ctrl.pack(fill="x", padx=10, pady=10)
self.btn_toggle = ttk.Button(frame_ctrl, text="Mulai Internet Manual (Gnirehtet Run)", command=self.toggle_internet)
self.btn_toggle.pack(fill="x", padx=10, pady=5)
self.btn_auto = ttk.Button(frame_ctrl, text="AKTIFKAN AUTO RECONNECT (Autorun Kabel USB)", command=self.toggle_auto_reconnect)
self.btn_auto.pack(fill="x", padx=10, pady=5)
self.status_label = ttk.Label(frame_ctrl, text="Status: Berhenti", foreground="red")
self.status_label.pack(pady=5)
def auto_detect(self):
found = find_gnirehtet_binary()
if found:
self.path_var.set(found)
self.gnirehtet_path = found
messagebox.showinfo("Deteksi Berhasil", f"Executable ditemukan di:\n{found}")
else:
messagebox.showwarning("Peringatan", "File 'gnirehtet' tidak ditemukan secara otomatis. Silakan tentukan lokasi file secara manual.")
def browse_binary(self):
filename = filedialog.askopenfilename(title="Pilih Executable Gnirehtet")
if filename:
self.path_var.set(filename)
self.gnirehtet_path = filename
def toggle_internet(self):
path = self.path_var.get()
if not path or not os.path.exists(path):
messagebox.showerror("Error", "File executable 'gnirehtet' tidak ditemukan!")
return
if self.process is None:
try:
self.process = subprocess.Popen([path, "run"])
self.status_label.config(text="Status: Berjalan (Mengalirkan Internet)", foreground="green")
self.btn_toggle.config(text="Hentikan Internet Manual")
except Exception as e:
messagebox.showerror("Error", f"Gagal menjalankan Gnirehtet:\n{e}")
else:
self.stop_internet()
def toggle_auto_reconnect(self):
path = self.path_var.get()
if not path or not os.path.exists(path):
messagebox.showerror("Error", "File executable 'gnirehtet' tidak ditemukan!")
return
self.auto_reconnect = not self.auto_reconnect
if self.auto_reconnect:
self.btn_auto.config(text="MATIKAN AUTO RECONNECT")
self.status_label.config(text="Status: Auto Reconnect Aktif (Menunggu Perangkat)", foreground="blue")
self.auto_thread = threading.Thread(target=self._auto_reconnect_loop, daemon=True)
self.auto_thread.start()
else:
self.btn_auto.config(text="AKTIFKAN AUTO RECONNECT (Autorun Kabel USB)")
self.stop_internet()
def _auto_reconnect_loop(self):
path = self.path_var.get()
while self.auto_reconnect:
if self.process is None or self.process.poll() is not None:
try:
res = subprocess.run(["adb", "devices"], capture_output=True, text=True)
has_device = any("\tdevice" in l for l in res.stdout.splitlines()[1:])
if has_device:
self.process = subprocess.Popen([path, "autorun"])
self.root_update_status("Status: Auto Reconnect Aktif (Internet Terhubung)", "green")
except Exception:
pass
else:
try:
res = subprocess.run(["adb", "devices"], capture_output=True, text=True)
has_device = any("\tdevice" in l for l in res.stdout.splitlines()[1:])
if not has_device:
self.stop_internet()
self.root_update_status("Status: Perangkat Terputus (Menunggu Sambungan USB)", "orange")
except Exception:
pass
time.sleep(2)
def root_update_status(self, text, color):
try:
self.after(0, lambda: self.status_label.config(text=text, foreground=color))
except Exception:
pass
def stop_internet(self):
if self.process:
try:
self.process.terminate()
self.process.wait(timeout=0.5)
except Exception:
try:
self.process.kill()
except Exception:
pass
self.process = None
self.status_label.config(text="Status: Berhenti", foreground="red")
self.btn_toggle.config(text="Mulai Internet Manual (Gnirehtet Run)")
# ============================================================
# TAB 3: AUDIO EQUALIZER & VOCAL CONTROL (PYBASS)
# ============================================================
class AudioDSPTab(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.msg_queue = queue.Queue()
self.worker = None
self.module_id = None
self.virtual_sink = None
self.output_sink = None
self.running = False
self.update_timer = None
self.params = {
"preamp_db": -16.0, "bass_db": 15.0, "bass_freq": 82.0,
"chest_db": 8.0, "chest_freq": 76.0, "kick_db": 4.0,
"kick_freq": 116.0, "mud_cut_db": -5.0, "rumble_freq": 32.0,
"clarity_db": 1.0, "output_db": 0.0, "limiter": True,
"vocal_mode": "normal", "vocal_strength": 1.0,
}
self.vars = {}
self.labels = {}
self.units = {}
self.output_var = tk.StringVar()
self.status_var = tk.StringVar(value="Siap. Pilih output asli lalu Start.")
self.build_ui()
self.refresh_sinks()
self.after(200, self.poll_messages)
def build_ui(self):
canvas = tk.Canvas(self, highlightthickness=0)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
canvas_window = canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.bind("<Configure>", lambda e: canvas.itemconfigure(canvas_window, width=e.width))
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
pad = {"padx": 10, "pady": 4}
# --- OUTPUT SINK ---
frame_out = ttk.LabelFrame(scrollable_frame, text="Output Speaker / Headset")
frame_out.pack(fill="x", **pad)
out_inner = ttk.Frame(frame_out)
out_inner.pack(fill="x", padx=8, pady=6)
self.sink_combo = ttk.Combobox(out_inner, textvariable=self.output_var, state="readonly", width=50)
self.sink_combo.pack(side="left", fill="x", expand=True, padx=(0, 5))
ttk.Button(out_inner, text="Refresh", command=self.refresh_sinks, width=12).pack(side="right")
# --- VOCAL CONTROL ---
vocal_frame = ttk.LabelFrame(scrollable_frame, text="Vocal Control")
vocal_frame.pack(fill="x", **pad)
mode_frame = ttk.Frame(vocal_frame)
mode_frame.pack(fill="x", padx=8, pady=6)
ttk.Label(mode_frame, text="Mode:", width=10, font=("Sans", 9, "bold")).pack(side="left")
self.vocal_mode_var = tk.StringVar(value="normal")
modes = [("Normal", "normal"), ("Remove", "remove"), ("Isolate", "isolate"), ("Enhance", "enhance")]
for label, val in modes:
ttk.Radiobutton(mode_frame, text=label, variable=self.vocal_mode_var, value=val, command=self.on_vocal_mode_change).pack(side="left", padx=5)
strength_frame = ttk.Frame(vocal_frame)
strength_frame.pack(fill="x", padx=8, pady=(0, 6))
ttk.Label(strength_frame, text="Strength:", width=10, font=("Sans", 9, "bold")).pack(side="left")
self.vocal_strength_var = tk.DoubleVar(value=1.0)
self.vocal_strength_label = ttk.Label(strength_frame, width=8, text="100%")
self.vocal_strength_label.pack(side="right")
def on_strength_change(v):
st = float(v)
self.params["vocal_strength"] = st
self.vocal_strength_label.config(text=f"{int(st * 100)}%")
self.schedule_live_update()
tk.Scale(
strength_frame, from_=0.0, to=1.0, orient="horizontal", resolution=0.05,
variable=self.vocal_strength_var, showvalue=False, command=on_strength_change
).pack(side="left", fill="x", expand=True, padx=5)
# --- BASS & EQ CONTROL ---
bass_frame = ttk.LabelFrame(scrollable_frame, text="Bass & EQ Control")
bass_frame.pack(fill="x", **pad)
self.add_slider(bass_frame, "Preamp", "preamp_db", -24.0, -4.0, 0.5, "dB")
self.add_slider(bass_frame, "Bass Brutal", "bass_db", 0.0, 20.0, 0.5, "dB")
self.add_slider(bass_frame, "Bass Freq", "bass_freq", 55.0, 120.0, 1.0, "Hz")
self.add_slider(bass_frame, "Chest Punch", "chest_db", 0.0, 12.0, 0.5, "dB")
self.add_slider(bass_frame, "Chest Freq", "chest_freq", 65.0, 90.0, 1.0, "Hz")
self.add_slider(bass_frame, "Kick Attack", "kick_db", 0.0, 8.0, 0.5, "dB")
self.add_slider(bass_frame, "Kick Freq", "kick_freq", 100.0, 130.0, 1.0, "Hz")
self.add_slider(bass_frame, "Mud Cut", "mud_cut_db", -9.0, 0.0, 0.5, "dB")
self.add_slider(bass_frame, "Rumble Cut", "rumble_freq", 25.0, 45.0, 1.0, "Hz")
self.add_slider(bass_frame, "Clarity", "clarity_db", 0.0, 4.0, 0.5, "dB")
self.add_slider(bass_frame, "Output", "output_db", -6.0, 3.0, 0.5, "dB")
self.limiter_var = tk.BooleanVar(value=True)
ttk.Checkbutton(bass_frame, text="Limiter Anti-Clipping", variable=self.limiter_var, command=self.schedule_live_update).pack(anchor="w", padx=10, pady=4)
# --- PRESETS ---
preset_frame = ttk.LabelFrame(scrollable_frame, text="15 Presets")
preset_frame.pack(fill="x", **pad)
r1 = ttk.Frame(preset_frame)
r1.pack(fill="x", padx=5, pady=2)
for lbl, name in [("Flat", "flat"), ("Clean", "clean"), ("Nendang", "nendang"), ("Brutal", "brutal"), ("Max", "max")]:
ttk.Button(r1, text=lbl, command=lambda n=name: self.apply_preset(n)).pack(side="left", expand=True, fill="x", padx=2)
r2 = ttk.Frame(preset_frame)
r2.pack(fill="x", padx=5, pady=2)
for lbl, name in [("EDM", "edm"), ("Hip-Hop", "hiphop"), ("Rock", "rock"), ("Cinema", "cinema"), ("Gaming", "gaming")]:
ttk.Button(r2, text=lbl, command=lambda n=name: self.apply_preset(n)).pack(side="left", expand=True, fill="x", padx=2)
r3 = ttk.Frame(preset_frame)
r3.pack(fill="x", padx=5, pady=2)
for lbl, name in [("Jazz", "jazz"), ("Classical", "classical"), ("Podcast", "podcast"), ("Bass Test", "basstest"), ("Acoustic", "acoustic")]:
ttk.Button(r3, text=lbl, command=lambda n=name: self.apply_preset(n)).pack(side="left", expand=True, fill="x", padx=2)
# --- BOTTOM CONTROLS ---
btn_box = ttk.Frame(scrollable_frame)
btn_box.pack(fill="x", padx=10, pady=10)
self.start_btn = ttk.Button(btn_box, text="START DSP", command=self.start)
self.start_btn.pack(side="left", expand=True, fill="x", padx=(0, 5), ipady=5)
self.stop_btn = ttk.Button(btn_box, text="STOP DSP", command=self.stop, state="disabled")
self.stop_btn.pack(side="left", expand=True, fill="x", ipady=5)
ttk.Label(scrollable_frame, textvariable=self.status_var, relief="sunken", anchor="w").pack(fill="x", padx=10, pady=(0, 10))
def add_slider(self, parent, text, key, minimum, maximum, resolution, unit):
row = ttk.Frame(parent)
row.pack(fill="x", padx=8, pady=2)
ttk.Label(row, text=text, width=12).pack(side="left")
variable = tk.DoubleVar(value=self.params[key])
self.vars[key] = variable
self.units[key] = unit
val_label = ttk.Label(row, width=8)
val_label.pack(side="right")
self.labels[key] = val_label
def on_change(val):
self.params[key] = float(val)
self.update_label(key)
self.schedule_live_update()
tk.Scale(row, from_=minimum, to=maximum, orient="horizontal", resolution=resolution, variable=variable, showvalue=False, command=on_change).pack(side="left", fill="x", expand=True, padx=5)
self.update_label(key)
def update_label(self, key):
val = self.params[key]
unit = self.units[key]
text = f"{val:.0f} Hz" if unit == "Hz" else f"{val:.1f} dB"
self.labels[key].config(text=text)
def on_vocal_mode_change(self):
mode = self.vocal_mode_var.get()
self.params["vocal_mode"] = mode
if self.running:
self.schedule_live_update()
def schedule_live_update(self):
self.params["limiter"] = bool(self.limiter_var.get())
if not self.running:
return
if self.update_timer:
try:
self.after_cancel(self.update_timer)
except Exception:
pass
self.update_timer = self.after(LIVE_UPDATE_DELAY_MSEC, self.apply_live_update)
def apply_live_update(self):
self.update_timer = None
if not self.running or not self.worker:
return
try:
new_filter = build_ffmpeg_filter(self.params)
self.worker.update_filter(new_filter)
except Exception as e:
self.status_var.set(f"Gagal update filter: {e}")
def apply_preset(self, name):
presets = {
"flat": {"preamp_db": -4.0, "bass_db": 0.0, "bass_freq": 85.0, "chest_db": 0.0, "chest_freq": 78.0, "kick_db": 0.0, "kick_freq": 115.0, "mud_cut_db": 0.0, "rumble_freq": 25.0, "clarity_db": 0.0, "output_db": 0.0, "limiter": True},
"clean": {"preamp_db": -9.0, "bass_db": 7.0, "bass_freq": 88.0, "chest_db": 4.0, "chest_freq": 78.0, "kick_db": 2.0, "kick_freq": 115.0, "mud_cut_db": -3.5, "rumble_freq": 32.0, "clarity_db": 1.0, "output_db": 0.0, "limiter": True},
"nendang": {"preamp_db": -13.0, "bass_db": 12.0, "bass_freq": 84.0, "chest_db": 6.5, "chest_freq": 77.0, "kick_db": 3.0, "kick_freq": 116.0, "mud_cut_db": -4.5, "rumble_freq": 32.0, "clarity_db": 1.0, "output_db": 0.0, "limiter": True},
"brutal": {"preamp_db": -16.0, "bass_db": 15.0, "bass_freq": 82.0, "chest_db": 8.0, "chest_freq": 76.0, "kick_db": 4.0, "kick_freq": 116.0, "mud_cut_db": -5.0, "rumble_freq": 32.0, "clarity_db": 1.0, "output_db": 0.0, "limiter": True},
"max": {"preamp_db": -19.0, "bass_db": 18.0, "bass_freq": 80.0, "chest_db": 10.0, "chest_freq": 75.0, "kick_db": 5.0, "kick_freq": 118.0, "mud_cut_db": -6.0, "rumble_freq": 35.0, "clarity_db": 1.5, "output_db": 0.0, "limiter": True},
"edm": {"preamp_db": -17.0, "bass_db": 16.0, "bass_freq": 65.0, "chest_db": 9.0, "chest_freq": 75.0, "kick_db": 5.5, "kick_freq": 120.0, "mud_cut_db": -6.5, "rumble_freq": 28.0, "clarity_db": 2.5, "output_db": 0.0, "limiter": True},
"hiphop": {"preamp_db": -15.0, "bass_db": 14.0, "bass_freq": 70.0, "chest_db": 10.0, "chest_freq": 85.0, "kick_db": 3.5, "kick_freq": 110.0, "mud_cut_db": -5.5, "rumble_freq": 30.0, "clarity_db": 2.0, "output_db": 0.0, "limiter": True},
"rock": {"preamp_db": -12.0, "bass_db": 10.0, "bass_freq": 95.0, "chest_db": 7.0, "chest_freq": 78.0, "kick_db": 6.0, "kick_freq": 115.0, "mud_cut_db": -4.0, "rumble_freq": 35.0, "clarity_db": 3.0, "output_db": 0.0, "limiter": True},
"cinema": {"preamp_db": -11.0, "bass_db": 11.0, "bass_freq": 75.0, "chest_db": 5.0, "chest_freq": 85.0, "kick_db": 4.5, "kick_freq": 110.0, "mud_cut_db": -3.0, "rumble_freq": 25.0, "clarity_db": 2.5, "output_db": 0.5, "limiter": True},
"gaming": {"preamp_db": -14.0, "bass_db": 13.0, "bass_freq": 80.0, "chest_db": 6.0, "chest_freq": 90.0, "kick_db": 5.0, "kick_freq": 120.0, "mud_cut_db": -4.5, "rumble_freq": 32.0, "clarity_db": 3.5, "output_db": 0.0, "limiter": True},
"jazz": {"preamp_db": -8.0, "bass_db": 6.0, "bass_freq": 92.0, "chest_db": 5.0, "chest_freq": 80.0, "kick_db": 1.5, "kick_freq": 110.0, "mud_cut_db": -2.5, "rumble_freq": 35.0, "clarity_db": 2.5, "output_db": 0.0, "limiter": True},
"classical": {"preamp_db": -6.0, "bass_db": 4.0, "bass_freq": 90.0, "chest_db": 2.0, "chest_freq": 85.0, "kick_db": 0.5, "kick_freq": 105.0, "mud_cut_db": -1.5, "rumble_freq": 30.0, "clarity_db": 3.0, "output_db": 0.0, "limiter": True},
"podcast": {"preamp_db": -7.0, "bass_db": 2.0, "bass_freq": 100.0, "chest_db": 8.0, "chest_freq": 88.0, "kick_db": 0.0, "kick_freq": 115.0, "mud_cut_db": -6.0, "rumble_freq": 40.0, "clarity_db": 3.5, "output_db": 1.0, "limiter": True},
"basstest": {"preamp_db": -21.0, "bass_db": 20.0, "bass_freq": 60.0, "chest_db": 12.0, "chest_freq": 70.0, "kick_db": 8.0, "kick_freq": 125.0, "mud_cut_db": -7.0, "rumble_freq": 25.0, "clarity_db": 0.5, "output_db": -1.0, "limiter": True},
"acoustic": {"preamp_db": -8.5, "bass_db": 5.0, "bass_freq": 95.0, "chest_db": 3.5, "chest_freq": 82.0, "kick_db": 1.5, "kick_freq": 112.0, "mud_cut_db": -2.0, "rumble_freq": 32.0, "clarity_db": 3.5, "output_db": 0.0, "limiter": True},
}
if name not in presets:
return
data = presets[name]
for key, value in data.items():
self.params[key] = value
if key == "limiter":
self.limiter_var.set(bool(value))
elif key in self.vars:
self.vars[key].set(float(value))
self.update_label(key)
if self.running:
self.schedule_live_update()
self.status_var.set(f"Preset {name.upper()} diterapkan.")
else:
self.status_var.set(f"Preset {name.upper()} aktif.")
def refresh_sinks(self):
try:
sinks = list_sinks()
self.sink_combo["values"] = sinks
default_sink = get_default_sink()
if default_sink in sinks:
self.output_var.set(default_sink)
elif sinks:
self.output_var.set(sinks[0])
else:
self.output_var.set("")
self.status_var.set(f"{len(sinks)} output ditemukan.")
except Exception as e:
messagebox.showerror("Error Audio", str(e))
def check_requirements(self):
missing = [cmd for cmd in ["pactl", "ffmpeg"] if not have_cmd(cmd)]
if missing:
raise RuntimeError(f"Command tidak ditemukan: {', '.join(missing)}")
if not ffmpeg_has_pulse_io():
raise RuntimeError("FFmpeg tidak memiliki dukungan PulseAudio input/output.")
def start(self):
if self.running:
return
try:
self.check_requirements()
output_sink = self.output_var.get().strip()
if not output_sink:
raise RuntimeError("Pilih output speaker/headset asli.")
if output_sink.startswith("pybass_"):
raise RuntimeError("Jangan pilih virtual sink PyBass sebagai output.")
self.output_sink = output_sink
cleanup_old_pybass(self.output_sink)
self.virtual_sink = f"pybass_direct_{os.getpid()}"
load_result = pactl([
"load-module", "module-null-sink", f"sink_name={self.virtual_sink}",
f"rate={RATE}", f"channels={CHANNELS}", "channel_map=front-left,front-right",
"sink_properties=device.description=PyBass_Direct_LowLatency"
])
self.module_id = load_result.stdout.strip()
if not self.module_id:
raise RuntimeError("Gagal membuat virtual sink PyBass.")
time.sleep(0.20)
pactl(["set-default-sink", self.virtual_sink])
move_all_sink_inputs(self.virtual_sink)
time.sleep(0.08)
ff_filter = build_ffmpeg_filter(self.params)
self.worker = PipelineWorker(self.virtual_sink, self.output_sink, ff_filter, self.msg_queue)
self.worker.start()
self.running = True
self.start_btn.config(state="disabled")
self.stop_btn.config(state="normal")
self.status_var.set("Memulai DSP direct low latency...")
except Exception as e:
self.cleanup_audio()
self.running = False
self.start_btn.config(state="normal")
self.stop_btn.config(state="disabled")
messagebox.showerror("Gagal Start Audio DSP", str(e))
def stop(self):
if not self.running and not self.module_id:
return
if self.update_timer:
try:
self.after_cancel(self.update_timer)
except Exception:
pass
self.update_timer = None
if self.worker:
self.worker.stop()
self.worker.join(timeout=2.0)
self.worker = None
self.cleanup_audio()
self.running = False
self.start_btn.config(state="normal")
self.stop_btn.config(state="disabled")
self.status_var.set("Stop. Audio dikembalikan ke output asli.")
def cleanup_audio(self):
if self.output_sink:
try:
pactl(["set-default-sink", self.output_sink], check=False)
move_all_sink_inputs(self.output_sink)
time.sleep(0.05)
except Exception:
pass
if self.module_id:
try:
pactl(["unload-module", self.module_id], check=False)
except Exception:
pass
self.module_id = None
self.virtual_sink = None
def poll_messages(self):
try:
while True:
msg_type, msg = self.msg_queue.get_nowait()
if msg_type == "status":
self.status_var.set(msg)
elif msg_type == "error":
messagebox.showerror("DSP Error", msg)
self.stop()
except queue.Empty:
pass
self.after(200, self.poll_messages)
# ============================================================
# MAIN APPLICATION WINDOW
# ============================================================
class UnifiedUtilityApp:
def __init__(self, root):
self.root = root
self.root.title("System & Android Utility Suite By Fi'i")
self.root.geometry("850x850")
self.root.minsize(750, 600)
notebook = ttk.Notebook(self.root)
notebook.pack(fill="both", expand=True)
self.tab_mirror = MirrorTab(notebook)
notebook.add(self.tab_mirror, text=" Screen Mirror & Record ")
self.tab_reverse = ReverseNetTab(notebook)
notebook.add(self.tab_reverse, text=" Reverse Internet ")
self.tab_audio = AudioDSPTab(notebook)
notebook.add(self.tab_audio, text=" Audio DSP & Vocal ")
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def on_close(self):
self.tab_audio.stop()
self.tab_reverse.stop_internet()
self.root.destroy()
def main():
root = tk.Tk()
UnifiedUtilityApp(root)
root.mainloop()
if __name__ == "__main__":
main()
- Buka terminal di folder tempat file berada, lalu jalankan perintah:
chmod +x android_suite.py
python3 android_suite.py
3. Panduan Penggunaan Fitur
- Persiapan Awal: Aktifkan USB Debugging di menu Opsi Pengembang (Developer Options) pada HP Android Anda, lalu sambungkan ke PC via kabel USB.
- Tab Screen Mirror & Record: Pilih ID perangkat pada menu drop-down. Jika ingin merekam video layar saat mirroring, centang opsi Aktifkan Perekaman Layar, tentukan folder simpan, lalu klik MIRROR / REKAM LAYAR SEKARANG.
- Tab Reverse Internet: Klik Deteksi Ulang untuk memastikan lokasi binary
gnirehtetterbaca. Tekan tombol AKTIFKAN AUTO RECONNECT agar internet PC langsung mengalir ke HP setiap kali kabel USB dicolok. - Tab Audio DSP & Vocal: Pilih perangkat Output Speaker/Headset Anda, tentukan mode vokal atau preset equalizer yang diinginkan (misal: EDM, Brutal, atau Podcast), lalu tekan START DSP.
DOWNLOAD FILE JADI Download file terbaru + browser DOWNLOADTips Tambahan untuk Blog: Tambahkan tangkapan layar (screenshot) dari tiap tab aplikasi saat dipublikasikan di Blogger agar artikel Anda makin menarik dan mudah dipahami pembaca!
User Reviews & Discussion
Tidak ada komentar:
Posting Komentar