Fix my error. The heaphones should work.

✅ Format supported: Microphone (Razer Seiren Mini) (2 ch, 44100 Hz)
🎙️ Recording from: Microphone (Razer Seiren Mini) (Index: 1, Channels: 2, Sample Rate: 44100 Hz)
⚠️ Note: Bluetooth devices like WH-1000XM4 may not support loopback. Consider using VB-Audio Virtual Cable (https://vb-audio.com/Cable/).
⚠️ Format not supported: Headphones (2- WH-1000XM4) (2 ch, 44100 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (1 ch, 44100 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (2 ch, 48000 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (1 ch, 48000 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (2 ch, 44100 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (1 ch, 44100 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (2 ch, 32000 Hz): ('Invalid number of channels', -9998)
⚠️ Format not supported: Headphones (2- WH-1000XM4) (1 ch, 32000 Hz): ('Invalid number of channels', -9998)
❌ Speaker format not supported: Headphones (2- WH-1000XM4) (0 ch, 44100 Hz)
🔈 Disabling speaker recording
🔄 Starting capture loop...
🔄 Loop iteration 1, recording=True

---

import threading
import time
import os
import wave
import subprocess
import sys
import shutil

import numpy as np
import pyaudiowpatch as pyaudio
from pystray import Icon, MenuItem, Menu
from PIL import Image, ImageDraw

# --- Globals ---
recording = False
mic_chunks = []
sr_mic = None
chunk_mic = 8192  # Increased for Bluetooth stability
tray_icon = None
start_time = None
selected_mic = None  # Selected microphone device info
selected_spk = None  # Selected speaker (loopback) device info
p = None  # PyAudio instance for global access

# --- Get available devices ---
def get_available_devices():
    global p, selected_mic, selected_spk
    if p is None:
        p = pyaudio.PyAudio()
    input_devices = []
    output_devices = []
    for i in range(p.get_device_count()):
        dev = p.get_device_info_by_index(i)
        dev_info = {
            'name': dev['name'],
            'index': dev['index'],
            'input_channels': min(dev['maxInputChannels'], 2) if dev['maxInputChannels'] > 0 else 0,
            'output_channels': min(dev['maxOutputChannels'], 2) if dev['maxOutputChannels'] > 0 else 0,
            'sample_rate': int(dev['defaultSampleRate'])
        }
        if dev['maxInputChannels'] > 0:
            input_devices.append(dev_info)
            if 'Razer Seiren Mini' in dev['name'] and not selected_mic:
                selected_mic = dev_info  # Default to Razer Seiren Mini
        if dev['maxOutputChannels'] > 0:
            output_devices.append(dev_info)
    # Validate selected_spk against current devices
    if selected_spk:
        if not any(dev['index'] == selected_spk['index'] for dev in output_devices):
            print(f"⚠️ Previously selected output {selected_spk['name']} no longer available. Resetting to None.")
            selected_spk = None
    return input_devices, output_devices

# --- Check if device is selected ---
def is_input_selected(device):
    return selected_mic and selected_mic['index'] == device['index']

def is_output_selected(device):
    return selected_spk and selected_spk['index'] == device['index'] if device else selected_spk is None

# --- Select device ---
def select_input_device(device):
    def inner(icon, item):
        global selected_mic
        selected_mic = device
        print(f"🎙️ Selected input: {selected_mic['name']} (Index: {selected_mic['index']})")
        icon.title = f"Tray Recorder: {selected_mic['name']}"
        icon.menu = build_menu()  # Rebuild menu with persisted selections
    return inner

def select_output_device(device):
    def inner(icon, item):
        global selected_spk
        old_spk = selected_spk['name'] if selected_spk else "None"
        selected_spk = device
        new_spk = selected_spk['name'] if selected_spk else "None"
        print(f"🔈 Output changed from {old_spk} to {new_spk} (Index: {selected_spk['index'] if selected_spk else 'N/A'})")
        icon.title = f"Tray Recorder: {selected_mic['name'] if selected_mic else 'Idle'}"
        icon.menu = build_menu()  # Rebuild menu with persisted selections
    return inner

# --- Build dynamic menu ---
def build_menu():
    input_devices, output_devices = get_available_devices()
    input_menu = Menu(
        *[MenuItem(f"{dev['name']} ({dev['input_channels']} ch, {dev['sample_rate']} Hz)",
                   select_input_device(dev),
                   checked=lambda item, dev=dev: is_input_selected(dev))
          for dev in input_devices]
    )
    output_menu = Menu(
        MenuItem("None", select_output_device(None), checked=lambda item: is_output_selected(None)),
        *[MenuItem(f"{dev['name']} ({dev['output_channels']} ch, {dev['sample_rate']} Hz)",
                   select_output_device(dev),
                   checked=lambda item, dev=dev: is_output_selected(dev))
          for dev in output_devices]
    )
    return Menu(
        MenuItem("Select Input Device", input_menu),
        MenuItem("Select Output Device", output_menu),
        MenuItem("Start Recording", start_record),
        MenuItem("Stop Recording", stop_record),
        MenuItem("Quit", quit_app)
    )

# --- Test device format ---
def test_device_format(device, sample_rate, channels, is_input=True):
    sample_rates = [sample_rate, 48000, 44100, 32000]  # Try default, then common rates
    for sr in sample_rates:
        for ch in [channels, 1]:  # Try specified channels, then 1
            try:
                p.is_format_supported(
                    sr,
                    input_device=device['index'] if is_input else None,
                    output_device=device['index'] if not is_input else None,
                    input_channels=ch if is_input else None,
                    output_channels=ch if not is_input else None,
                    input_format=pyaudio.paInt16 if is_input else None,
                    output_format=pyaudio.paInt16 if not is_input else None
                )
                print(f"✅ Format supported: {device['name']} ({ch} ch, {sr} Hz)")
                return True, ch, sr
            except ValueError as e:
                print(f"⚠️ Format not supported: {device['name']} ({ch} ch, {sr} Hz): {e}")
    return False, 0, sample_rate

# --- Audio capture worker ---
def record_worker():
    global recording, mic_chunks, sr_mic, chunk_mic, selected_mic, selected_spk, p

    if p is None:
        p = pyaudio.PyAudio()

    # Log all devices
    print("🔍 Available devices:")
    for i in range(p.get_device_count()):
        dev = p.get_device_info_by_index(i)
        print(f"Device {i}: {dev['name']}, in_ch={dev['maxInputChannels']}, "
              f"out_ch={dev['maxOutputChannels']}, sr={dev['defaultSampleRate']}")

    # Validate selected devices
    if not selected_mic:
        print("❌ No microphone selected")
        recording = False
        p.terminate()
        return

    # Microphone setup
    sr_mic = selected_mic['sample_rate']
    mic_idx = selected_mic['index']
    mic_ch = selected_mic['input_channels']
    mic_supported, mic_ch, sr_mic = test_device_format(selected_mic, sr_mic, mic_ch, is_input=True)
    if not mic_supported:
        print(f"❌ Microphone format not supported: {selected_mic['name']} ({mic_ch} ch, {sr_mic} Hz)")
        recording = False
        p.terminate()
        return

    print(f"🎙️ Recording from: {selected_mic['name']} "
          f"(Index: {mic_idx}, Channels: {mic_ch}, Sample Rate: {sr_mic} Hz)")

    mic_stream = None
    try:
        mic_stream = p.open(
            format=pyaudio.paInt16,
            channels=mic_ch,
            rate=sr_mic,
            input=True,
            input_device_index=mic_idx,
            frames_per_buffer=chunk_mic
        )
    except Exception as e:
        print(f"❌ Cannot open microphone {selected_mic['name']}: {e}")
        recording = False
        p.terminate()
        return

    # Speaker loopback setup (if selected)
    spk_stream = None
    spk_chunks = []
    sr_spk = None
    spk_ch = None
    if selected_spk:
        print("⚠️ Note: Bluetooth devices like WH-1000XM4 may not support loopback. Consider using VB-Audio Virtual Cable (https://vb-audio.com/Cable/).")
        sr_spk = selected_spk['sample_rate']
        spk_idx = selected_spk['index']
        spk_ch = selected_spk['output_channels']
        spk_supported, spk_ch, sr_spk = test_device_format(selected_spk, sr_spk, spk_ch, is_input=True)
        if not spk_supported:
            print(f"❌ Speaker format not supported: {selected_spk['name']} ({spk_ch} ch, {sr_spk} Hz)")
            print("🔈 Disabling speaker recording")
            selected_spk = None
        else:
            print(f"🔈 Recording from: {selected_spk['name']} "
                  f"(Index: {spk_idx}, Channels: {spk_ch}, Sample Rate: {sr_spk} Hz)")
            for attempt in range(5):  # Retry up to 5 times
                try:
                    spk_stream = p.open(
                        format=pyaudio.paInt16,
                        channels=spk_ch,
                        rate=sr_spk,
                        input=True,
                        input_device_index=spk_idx,
                        frames_per_buffer=chunk_mic
                    )
                    print(f"✅ Speaker stream opened on attempt {attempt + 1}")
                    break
                except Exception as e:
                    print(f"⚠️ Attempt {attempt + 1} failed to open speaker {selected_spk['name']}: {e}")
                    time.sleep(1.0)  # Longer delay for Bluetooth
                    if attempt == 4:
                        print("❌ Cannot open speaker after 5 attempts. Disabling speaker recording.")
                        selected_spk = None
                        break

    mic_chunks.clear()
    spk_chunks.clear()

    # Capture loop
    loop_count = 0
    timeout = time.time() + 30
    print("🔄 Starting capture loop...")
    while recording and time.time() < timeout:
        loop_count += 1
        print(f"🔄 Loop iteration {loop_count}, recording={recording}")
        try:
            # Check microphone device
            try:
                dev_info = p.get_device_info_by_index(mic_idx)
                if dev_info['maxInputChannels'] <= 0:
                    print(f"⚠️ Microphone {selected_mic['name']} unavailable")
                    recording = False
                    break
            except Exception as e:
                print(f"⚠️ Error checking microphone: {e}")
                recording = False
                break

            # Microphone capture
            if mic_stream and mic_stream.is_active():
                for _ in range(5):
                    try:
                        start_read = time.time()
                        data_mic = mic_stream.read(chunk_mic, exception_on_overflow=False)
                        read_time = time.time() - start_read
                        print(f"🎙️ Read {len(data_mic)} bytes from mic in {read_time:.3f}s")
                        if data_mic:
                            arr_mic = np.frombuffer(data_mic, dtype=np.int16).reshape(-1, mic_ch)
                            mono_mic = arr_mic.mean(axis=1).astype(np.int16).tobytes()
                            mic_chunks.append(mono_mic)
                            mic_max = np.max(np.abs(arr_mic)) if arr_mic.size > 0 else 0
                            print(f"🎙️ Mic chunk {len(mic_chunks)}, max amplitude: {mic_max}")
                            break
                        else:
                            print(f"⚠️ Empty data from {selected_mic['name']}")
                            time.sleep(0.2)
                    except Exception as e:
                        print(f"⚠️ Mic read error: {e}")
                        if "Stream closed" in str(e):
                            break
                        time.sleep(0.2)
                else:
                    print(f"⚠️ Mic read failed after retries")
                    try:
                        mic_stream.close()
                        mic_stream = p.open(
                            format=pyaudio.paInt16,
                            channels=mic_ch,
                            rate=sr_mic,
                            input=True,
                            input_device_index=mic_idx,
                            frames_per_buffer=chunk_mic
                        )
                        print(f"🎙️ Mic stream reinitialized")
                    except Exception as e:
                        print(f"❌ Failed to reinitialize mic stream: {e}")
                        recording = False
                        break

            # Speaker capture (if selected)
            if selected_spk and spk_stream and spk_stream.is_active():
                for _ in range(5):
                    try:
                        start_read = time.time()
                        data_spk = spk_stream.read(chunk_mic, exception_on_overflow=False)
                        read_time = time.time() - start_read
                        print(f"🔈 Read {len(data_spk)} bytes from speaker in {read_time:.3f}s")
                        if data_spk:
                            arr_spk = np.frombuffer(data_spk, dtype=np.int16).reshape(-1, spk_ch)
                            mono_spk = arr_spk.mean(axis=1).astype(np.int16).tobytes()
                            spk_chunks.append(mono_spk)
                            spk_max = np.max(np.abs(arr_spk)) if arr_spk.size > 0 else 0
                            print(f"🔈 Speaker chunk {len(spk_chunks)}, max amplitude: {spk_max}")
                            break
                        else:
                            print(f"⚠️ Empty data from {selected_spk['name']}")
                            time.sleep(0.2)
                    except Exception as e:
                        print(f"⚠️ Speaker read error: {e}")
                        if "Stream closed" in str(e):
                            break
                        time.sleep(0.2)
                else:
                    print(f"⚠️ Speaker read failed after retries")
                    try:
                        spk_stream.close()
                        spk_stream = p.open(
                            format=pyaudio.paInt16,
                            channels=spk_ch,
                            rate=sr_spk,
                            input=True,
                            input_device_index=selected_spk['index'],
                            frames_per_buffer=chunk_mic
                        )
                        print(f"🔈 Speaker stream reinitialized")
                    except Exception as e:
                        print(f"❌ Failed to reinitialize speaker stream: {e}")
                        selected_spk = None
                        break

        except Exception as e:
            print(f"⚠️ Error in capture loop: {e}")
            recording = False
            break

        time.sleep(0.01)

    print(f"🛑 Capture loop exited (reason: {'recording=False' if not recording else 'timeout'}, "
          f"mic_chunks: {len(mic_chunks)}, spk_chunks: {len(spk_chunks) if selected_spk else 0}, loops: {loop_count})")

    # Cleanup
    if mic_stream:
        try:
            if mic_stream.is_active():
                mic_stream.stop_stream()
            mic_stream.close()
        except Exception as e:
            print(f"⚠️ Error closing mic stream: {e}")
    if spk_stream:
        try:
            if spk_stream.is_active():
                spk_stream.stop_stream()
            spk_stream.close()
        except Exception as e:
            print(f"⚠️ Error closing speaker stream: {e}")
    p.terminate()
    p = None

# --- Save & mix via ffmpeg ---
def save_recording():
    global mic_chunks, sr_mic, selected_mic, selected_spk

    print(f"💾 Saving: {len(mic_chunks)} mic chunks from {selected_mic['name']}")
    spk_chunks = []
    if selected_spk:
        print(f"💾 Saving: {len(spk_chunks)} speaker chunks from {selected_spk['name']}")

    if not mic_chunks and not spk_chunks:
        print("❌ No audio recorded—skipping save.")
        return

    mic_silent = True
    if mic_chunks:
        mic_data = np.frombuffer(b"".join(mic_chunks), dtype=np.int16)
        mic_max = np.max(np.abs(mic_data)) if mic_data.size > 0 else 0
        mic_silent = mic_max < 10
        print(f"🎙️ Mic audio max amplitude: {mic_max}")

    spk_silent = True
    if selected_spk and spk_chunks:
        spk_data = np.frombuffer(b"".join(spk_chunks), dtype=np.int16)
        spk_max = np.max(np.abs(spk_data)) if spk_data.size > 0 else 0
        spk_silent = spk_max < 10
        print(f"🔈 Speaker audio max amplitude: {spk_max}")

    if mic_silent and spk_silent:
        print("❌ Audio is silent—skipping save.")
        return

    ts = int(time.time())
    wav_files = []

    # Save microphone audio
    if mic_chunks and not mic_silent:
        wav_mic = f"recording_mic_{time.strftime('%Y%m%d_%H%M%S')}.wav"
        with wave.open(wav_mic, "wb") as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)
            wf.setframerate(sr_mic)
            wf.writeframes(b"".join(mic_chunks))
        wav_files.append(wav_mic)

    # Save speaker audio
    if selected_spk and spk_chunks and not spk_silent:
        wav_spk = f"recording_spk_{time.strftime('%Y%m%d_%H%M%S')}.wav"
        with wave.open(wav_spk, "wb") as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)
            wf.setframerate(sr_spk if sr_spk else sr_mic)
            wf.writeframes(b"".join(spk_chunks))
        wav_files.append(wav_spk)

    if not wav_files:
        print("❌ No valid audio to save.")
        return

    # Check for ffmpeg
    ffmpeg_path = shutil.which("ffmpeg")
    if ffmpeg_path is None:
        # Try local ffmpeg.exe in script directory
        script_dir = os.path.dirname(os.path.abspath(__file__))
        local_ffmpeg = os.path.join(script_dir, "ffmpeg.exe")
        if os.path.exists(local_ffmpeg):
            ffmpeg_path = local_ffmpeg
        else:
            print("❌ FFmpeg not found. Please install FFmpeg and add it to your system PATH, or place ffmpeg.exe in the script directory (C:\\Users\\Radek\\Desktop\\audio).")
            print(f"✅ Audio saved as WAV: {wav_files[0]}")
            return

    out_mp3 = time.strftime("recording_%Y%m%d_%H%M%S.mp3")

    # Mix or save single file
    try:
        if len(wav_files) == 1:
            cmd = [ffmpeg_path, "-y", "-i", wav_files[0], "-codec:a", "libmp3lame", "-qscale:a", "2", out_mp3]
        else:
            cmd = [ffmpeg_path, "-y"]
            for wav in wav_files:
                cmd.extend(["-i", wav])
            cmd.extend(["-filter_complex", "amix=inputs=2:duration=longest", "-codec:a", "libmp3lame", "-qscale:a", "2", out_mp3])

        subprocess.run(cmd, check=True)
        print(f"✅ Saved: {out_mp3}")

        # Clean up WAV files after successful MP3 conversion
        for wf in wav_files:
            try:
                os.remove(wf)
            except OSError:
                print(f"⚠️ Could not delete temporary WAV file: {wf}")
    except (subprocess.CalledProcessError, FileNotFoundError) as e:
        print(f"❌ FFmpeg failed: {e}")
        print(f"✅ Audio saved as WAV: {wav_files[0]}")

# --- Tray & Ctrl+C handling ---
def make_icon_image(is_rec=False):
    img = Image.new("RGB", (64, 64), "black")
    d = ImageDraw.Draw(img)
    d.ellipse((16, 16, 48, 48), fill="red" if is_rec else "grey")
    return img

def start_record(icon, item):
    global recording, start_time
    if recording:
        return
    if not selected_mic:
        print("❌ Please select a microphone first")
        return
    recording = True
    start_time = time.time()
    print("▶ Recording started.")
    threading.Thread(target=record_worker, daemon=True).start()
    icon.icon = make_icon_image(True)
    icon.title = f"Recording: {selected_mic['name']}"
    icon.menu = build_menu()  # Update menu with persisted selections

def stop_record(icon=None, item=None):
    global recording, start_time
    if not recording:
        return
    elapsed = time.time() - start_time if start_time else 0
    if elapsed < 5:
        print("⚠️ Stop ignored: recording too short (< 5s)")
        return
    print(f"🛑 Stop record called (elapsed: {elapsed:.1f}s)")
    recording = False
    if icon:
        icon.icon = make_icon_image(False)
        icon.title = "Saving..."
        time.sleep(1.0)
    save_recording()
    if icon:
        icon.title = f"Tray Recorder: {selected_mic['name'] if selected_mic else 'Idle'}"
        icon.menu = build_menu()  # Update menu with persisted selections

def quit_app(icon, item):
    stop_record(icon, item)
    icon.stop()

# --- Main ---
if __name__ == "__main__":
    # Initialize tray icon
    tray_icon = Icon(
        "TrayRecorder",
        icon=make_icon_image(False),
        title="Tray Recorder: Idle"
    )

    # Set initial menu
    tray_icon.menu = build_menu()

    tray_icon.run_detached()
    print("Tray recorder running. Use tray menu to select devices and Start/Stop, or Ctrl+C to quit.")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\nInterrupted; stopping...")
        stop_record()
        tray_icon.stop()
        sys.exit(0)