# VoicePlay — настольное приложение Windows без браузера
import math
import struct
import threading
import time
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import wave
import ctypes
from ctypes import wintypes

CHUNK = 960
BITS = 16
WAVE_MAPPER = 0xFFFFFFFF
WHDR_DONE = 0x00000001
WHDR_PREPARED = 0x00000002
WHDR_INQUEUE = 0x00000010
PRESETS = [
    ("Обычный", 1.00, 0, 0, 0, 8),
    ("Гелий", 1.45, 6, 0, 4, 6),
    ("Великан", 0.68, -8, 0, 18, 22),
    ("Робот", 0.96, 2, 72, 10, 8),
    ("Рация", 1.02, 4, 8, 46, 0),
    ("Пришелец", 1.30, 9, 28, 12, 18),
    ("Монстр", 0.60, -10, 16, 55, 20),
    ("Детский", 1.36, 8, 0, 0, 10),
    ("Телефон", 1.04, 3, 0, 22, 0),
    ("Космос", 0.84, -2, 12, 8, 70),
]


class WAVEFORMATEX(ctypes.Structure):
    _fields_ = [
        ("wFormatTag", wintypes.WORD),
        ("nChannels", wintypes.WORD),
        ("nSamplesPerSec", wintypes.DWORD),
        ("nAvgBytesPerSec", wintypes.DWORD),
        ("nBlockAlign", wintypes.WORD),
        ("wBitsPerSample", wintypes.WORD),
        ("cbSize", wintypes.WORD),
    ]


class WAVEHDR(ctypes.Structure):
    pass


WAVEHDR._fields_ = [
    ("lpData", ctypes.c_void_p),
    ("dwBufferLength", wintypes.DWORD),
    ("dwBytesRecorded", wintypes.DWORD),
    ("dwUser", ctypes.c_void_p),
    ("dwFlags", wintypes.DWORD),
    ("dwLoops", wintypes.DWORD),
    ("lpNext", ctypes.POINTER(WAVEHDR)),
    ("reserved", ctypes.c_void_p),
]


class WAVEOUTCAPSW(ctypes.Structure):
    _fields_ = [
        ("wMid", wintypes.WORD),
        ("wPid", wintypes.WORD),
        ("vDriverVersion", wintypes.UINT),
        ("szPname", ctypes.c_wchar * 32),
        ("dwFormats", wintypes.DWORD),
        ("wChannels", wintypes.WORD),
        ("wReserved1", wintypes.WORD),
        ("dwSupport", wintypes.DWORD),
    ]


class WAVEINCAPSW(ctypes.Structure):
    _fields_ = [
        ("wMid", wintypes.WORD),
        ("wPid", wintypes.WORD),
        ("vDriverVersion", wintypes.UINT),
        ("szPname", ctypes.c_wchar * 32),
        ("dwFormats", wintypes.DWORD),
        ("wChannels", wintypes.WORD),
        ("wReserved1", wintypes.WORD),
    ]


HWAVE = ctypes.c_void_p
winmm = ctypes.WinDLL("winmm")
UINT = wintypes.UINT
winmm.waveInGetNumDevs.restype = UINT
winmm.waveOutGetNumDevs.restype = UINT
winmm.waveInGetDevCapsW.argtypes = [ctypes.c_void_p, ctypes.POINTER(WAVEINCAPSW), UINT]
winmm.waveOutGetDevCapsW.argtypes = [ctypes.c_void_p, ctypes.POINTER(WAVEOUTCAPSW), UINT]
winmm.waveInOpen.argtypes = [ctypes.POINTER(HWAVE), UINT, ctypes.POINTER(WAVEFORMATEX), ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD]
winmm.waveOutOpen.argtypes = [ctypes.POINTER(HWAVE), UINT, ctypes.POINTER(WAVEFORMATEX), ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD]
winmm.waveInOpen.restype = UINT
winmm.waveOutOpen.restype = UINT
for name in (
    "waveInPrepareHeader", "waveInUnprepareHeader", "waveInAddBuffer",
    "waveOutPrepareHeader", "waveOutUnprepareHeader", "waveOutWrite",
):
    fn = getattr(winmm, name)
    fn.argtypes = [HWAVE, ctypes.POINTER(WAVEHDR), UINT]
    fn.restype = UINT
winmm.waveInStart.argtypes = [HWAVE]
winmm.waveInReset.argtypes = [HWAVE]
winmm.waveOutReset.argtypes = [HWAVE]
winmm.waveInClose.argtypes = [HWAVE]
winmm.waveOutClose.argtypes = [HWAVE]
winmm.waveOutSetVolume.argtypes = [HWAVE, wintypes.DWORD]
winmm.waveInStart.restype = UINT
winmm.waveOutSetVolume.restype = UINT


def pcm_format(channels, rate):
    block = channels * BITS // 8
    return WAVEFORMATEX(1, channels, rate, rate * block, block, BITS, 0)


def list_in_devices():
    items = [(WAVE_MAPPER, "Микрофон по умолчанию")]
    n = winmm.waveInGetNumDevs()
    for i in range(n):
        caps = WAVEINCAPSW()
        if winmm.waveInGetDevCapsW(i, ctypes.byref(caps), ctypes.sizeof(caps)) == 0:
            items.append((i, caps.szPname))
    return items


def list_out_devices():
    items = [(WAVE_MAPPER, "Динамики по умолчанию")]
    n = winmm.waveOutGetNumDevs()
    for i in range(n):
        caps = WAVEOUTCAPSW()
        if winmm.waveOutGetDevCapsW(i, ctypes.byref(caps), ctypes.sizeof(caps)) == 0:
            items.append((i, caps.szPname))
    return items


class Buffer:
    def __init__(self, size):
        self.size = size
        self.raw = (ctypes.c_char * size)()
        self.hdr = WAVEHDR()
        self.hdr.lpData = ctypes.addressof(self.raw)
        self.hdr.dwBufferLength = size


class VoiceFX:
    def __init__(self, rate):
        self.rate = rate
        self.pitch = 1.0
        self.tone = 0.0
        self.robot = 0.0
        self.drive = 0.0
        self.room = 0.0
        self.vol = 1.0
        self.hear = True
        self.lock = threading.Lock()
        self.n = 16384
        self.buf = [0.0] * self.n
        self.w = 0
        self.r = 0.0
        self.phase = 0.0
        self.delay = [0.0] * max(64, int(rate * 0.12))
        self.di = 0
        self.z1 = self.z2 = 0.0

    def set(self, **kw):
        with self.lock:
            self.__dict__.update(kw)

    def process(self, x):
        with self.lock:
            pitch, tone, robot, drive, room, vol, hear = (
                self.pitch, self.tone, self.robot, self.drive, self.room, self.vol, self.hear
            )
            rate = self.rate
        y = self._pitch(x, pitch)
        y = self._tone(y, tone, rate)
        if drive > 0.01:
            k = 1 + drive * 8
            den = math.tanh(k)
            y = [math.tanh(s * k) / den for s in y]
        if robot > 0.01:
            step = 2 * math.pi * 35 / rate
            out = []
            ph = self.phase
            mix = robot * 0.85
            for s in y:
                out.append(s * ((1 - mix) + mix * math.sin(ph)))
                ph += step
            self.phase = ph % (2 * math.pi)
            y = out
        if room > 0.01:
            d = self.delay
            n = len(d)
            i = self.di
            fb = 0.35 + room * 0.25
            mix = room * 0.45
            out = []
            for s in y:
                echo = d[i]
                d[i] = s + echo * fb
                out.append(s * (1 - mix) + echo * mix)
                i += 1
                if i >= n:
                    i = 0
            self.di = i
            y = out
        y = [s * vol * 1.35 for s in y]
        peak_in = max((abs(s) for s in x), default=0.0)
        if not hear:
            return [0.0] * len(y), peak_in
        peak = max((abs(s) for s in y), default=0.0)
        return [max(-1.0, min(1.0, s)) for s in y], max(peak, peak_in)

    def _pitch(self, x, pitch):
        if abs(pitch - 1) < 0.03:
            for s in x:
                self.buf[self.w] = s
                self.w = (self.w + 1) % self.n
            self.r = float(self.w)
            return list(x)
        out = []
        n = self.n
        for s in x:
            self.buf[self.w] = s
            self.w = (self.w + 1) % n
            i0 = int(self.r) % n
            i1 = (i0 + 1) % n
            f = self.r - math.floor(self.r)
            out.append(self.buf[i0] * (1 - f) + self.buf[i1] * f)
            self.r += pitch
            if self.r >= n:
                self.r -= n
        return out

    def _tone(self, x, gain_db, rate):
        if abs(gain_db) < 0.2:
            return list(x)
        A = 10 ** (gain_db / 40)
        w0 = 2 * math.pi * 1200 / rate
        alpha = math.sin(w0) / (2 * 0.9)
        b0 = 1 + alpha * A
        b1 = -2 * math.cos(w0)
        b2 = 1 - alpha * A
        a0 = 1 + alpha / A
        a1 = -2 * math.cos(w0)
        a2 = 1 - alpha / A
        b0, b1, b2, a1, a2 = b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0
        y = []
        z1, z2 = self.z1, self.z2
        for s in x:
            o = b0 * s + z1
            z1 = b1 * s - a1 * o + z2
            z2 = b2 * s - a2 * o
            y.append(o)
        self.z1, self.z2 = z1, z2
        return y


def pack_stereo(samples):
    out = bytearray()
    for s in samples:
        v = max(-32767, min(32767, int(s * 32767)))
        out += struct.pack("<hh", v, v)
    return bytes(out)


class Engine:
    def __init__(self, fx):
        self.fx = fx
        self.running = False
        self.thread = None
        self.peak = 0.0
        self.rec = []
        self.recording = False
        self.hin = HWAVE()
        self.hout = HWAVE()
        self.inbufs = []
        self.outbufs = []
        self.oi = 0
        self.in_ch = 1
        self.out_ch = 2
        self.rate = 48000
        self.in_dev = WAVE_MAPPER
        self.out_dev = WAVE_MAPPER

    def start(self):
        if self.running:
            return
        opened = False
        last_err = "нет устройства"
        for rate in (48000, 44100):
            for in_ch in (1, 2):
                fmt_in = pcm_format(in_ch, rate)
                hin = HWAVE()
                err = winmm.waveInOpen(ctypes.byref(hin), self.in_dev, ctypes.byref(fmt_in), None, None, 0)
                if err:
                    last_err = "микрофон, код %s" % err
                    continue
                hout = HWAVE()
                fmt_out = pcm_format(2, rate)
                err = winmm.waveOutOpen(ctypes.byref(hout), self.out_dev, ctypes.byref(fmt_out), None, None, 0)
                if err:
                    winmm.waveInClose(hin)
                    fmt_out = pcm_format(1, rate)
                    hout = HWAVE()
                    err = winmm.waveOutOpen(ctypes.byref(hout), self.out_dev, ctypes.byref(fmt_out), None, None, 0)
                    if err:
                        winmm.waveInClose(hin)
                        last_err = "динамики, код %s" % err
                        continue
                    self.out_ch = 1
                else:
                    self.out_ch = 2
                self.hin, self.hout = hin, hout
                self.in_ch, self.rate = in_ch, rate
                self.fx.rate = rate
                self.fx.delay = [0.0] * max(64, int(rate * 0.12))
                opened = True
                break
            if opened:
                break
        if not opened:
            raise RuntimeError("Не удалось открыть звук (%s). Выберите другие устройства." % last_err)

        winmm.waveOutSetVolume(self.hout, 0xFFFFFFFF)
        in_bytes = CHUNK * self.in_ch * 2
        out_bytes = CHUNK * self.out_ch * 2
        self.inbufs = [Buffer(in_bytes) for _ in range(6)]
        self.outbufs = [Buffer(out_bytes) for _ in range(8)]
        for b in self.inbufs:
            winmm.waveInPrepareHeader(self.hin, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
            winmm.waveInAddBuffer(self.hin, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
        for b in self.outbufs:
            winmm.waveOutPrepareHeader(self.hout, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
        winmm.waveInStart(self.hin)
        self.running = True
        self.thread = threading.Thread(target=self._loop, daemon=True)
        self.thread.start()
        self._beep()

    def _beep(self):
        n = int(self.rate * 0.22)
        samples = [0.22 * math.sin(2 * math.pi * 880 * i / self.rate) for i in range(n)]
        self._write_pcm(samples)

    def _write_pcm(self, samples):
        if self.out_ch == 2:
            pcm = pack_stereo(samples)
        else:
            pcm = b"".join(struct.pack("<h", max(-32767, min(32767, int(s * 32767)))) for s in samples)
        step = CHUNK * self.out_ch * 2
        for off in range(0, len(pcm), step):
            self._write_chunk(pcm[off:off + step])

    def _write_chunk(self, pcm):
        if not pcm:
            return
        deadline = time.time() + 0.25
        while time.time() < deadline:
            ob = self.outbufs[self.oi]
            flags = ob.hdr.dwFlags
            busy = flags & WHDR_INQUEUE and not (flags & WHDR_DONE)
            if not busy:
                break
            time.sleep(0.001)
        else:
            return
        ob = self.outbufs[self.oi]
        self.oi = (self.oi + 1) % len(self.outbufs)
        n = min(len(pcm), ob.size)
        ob.raw[:n] = pcm[:n]
        ob.hdr.dwBufferLength = n
        ob.hdr.dwFlags = ob.hdr.dwFlags & ~WHDR_DONE
        winmm.waveOutWrite(self.hout, ctypes.byref(ob.hdr), ctypes.sizeof(WAVEHDR))

    def stop(self):
        self.running = False
        if self.thread:
            self.thread.join(timeout=1)
        try:
            winmm.waveInReset(self.hin)
            winmm.waveOutReset(self.hout)
            for b in self.inbufs:
                winmm.waveInUnprepareHeader(self.hin, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
            for b in self.outbufs:
                winmm.waveOutUnprepareHeader(self.hout, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
            winmm.waveInClose(self.hin)
            winmm.waveOutClose(self.hout)
        except Exception:
            pass
        self.peak = 0
        self.inbufs = []
        self.outbufs = []

    def _loop(self):
        while self.running:
            did = False
            for b in self.inbufs:
                if not (b.hdr.dwFlags & WHDR_DONE):
                    continue
                did = True
                n = b.hdr.dwBytesRecorded or b.size
                raw = bytes(b.raw)[:n]
                samples = [struct.unpack_from("<h", raw, i)[0] / 32768.0 for i in range(0, len(raw) - 1, 2)]
                if self.in_ch == 2 and len(samples) >= 2:
                    samples = [(samples[i] + samples[i + 1]) * 0.5 for i in range(0, len(samples) - 1, 2)]
                y, peak = self.fx.process(samples)
                self.peak = peak
                if self.recording:
                    rec = b"".join(struct.pack("<h", max(-32767, min(32767, int(s * 32767)))) for s in y)
                    self.rec.append(rec)
                self._write_pcm(y)
                winmm.waveInUnprepareHeader(self.hin, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
                b.hdr.dwFlags = 0
                b.hdr.dwBytesRecorded = 0
                b.hdr.dwBufferLength = b.size
                winmm.waveInPrepareHeader(self.hin, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
                winmm.waveInAddBuffer(self.hin, ctypes.byref(b.hdr), ctypes.sizeof(WAVEHDR))
            if not did:
                time.sleep(0.002)


class App:
    def __init__(self):
        self.fx = VoiceFX(48000)
        self.eng = Engine(self.fx)
        self.root = tk.Tk()
        self.root.title("VoicePlay")
        self.root.geometry("860x820")
        self.root.configure(bg="#0b0d16")
        self.root.minsize(720, 680)
        self.vars = {
            "pitch": tk.DoubleVar(value=1.0),
            "tone": tk.DoubleVar(value=0),
            "robot": tk.DoubleVar(value=0),
            "drive": tk.DoubleVar(value=0),
            "room": tk.DoubleVar(value=8),
            "vol": tk.DoubleVar(value=100),
        }
        self.hear = True
        self.status = tk.StringVar(value="Выкл")
        self.in_devs = list_in_devices()
        self.out_devs = list_out_devices()
        self.in_name = tk.StringVar(value=self.in_devs[0][1])
        self.out_name = tk.StringVar(value=self.out_devs[0][1])
        self._ui()
        self.root.protocol("WM_DELETE_WINDOW", self.close)
        self.root.after(50, self._meter)

    def _ui(self):
        bg, card, ink, muted, acc = "#0b0d16", "#161b2c", "#f3f1ff", "#9aa3c2", "#7c5cff"
        f = ("Segoe UI", 11)

        top = tk.Frame(self.root, bg=bg)
        top.pack(fill="x", padx=22, pady=16)
        tk.Label(top, text="VoicePlay", font=("Segoe UI Semibold", 22), bg=bg, fg=ink).pack(side="left")
        tk.Label(top, textvariable=self.status, font=f, bg=bg, fg="#4de1c1").pack(side="right")

        box = tk.Frame(self.root, bg=card)
        box.pack(fill="x", padx=22, pady=8)
        tk.Label(box, text="Наденьте наушники. При включении должен пикнуть динамик, потом слышен ваш голос.", font=f, bg=card, fg=muted).pack(anchor="w", padx=16, pady=(14, 6))

        dev = tk.Frame(box, bg=card)
        dev.pack(fill="x", padx=16, pady=4)
        tk.Label(dev, text="Микрофон", font=f, bg=card, fg=muted, width=12, anchor="w").pack(side="left")
        ttk.Combobox(dev, textvariable=self.in_name, values=[n for _, n in self.in_devs], state="readonly", width=42).pack(side="left", fill="x", expand=True)
        dev2 = tk.Frame(box, bg=card)
        dev2.pack(fill="x", padx=16, pady=4)
        tk.Label(dev2, text="Динамики", font=f, bg=card, fg=muted, width=12, anchor="w").pack(side="left")
        ttk.Combobox(dev2, textvariable=self.out_name, values=[n for _, n in self.out_devs], state="readonly", width=42).pack(side="left", fill="x", expand=True)

        row = tk.Frame(box, bg=card)
        row.pack(fill="x", padx=16, pady=8)
        self.b_on = tk.Button(row, text="Включить микрофон", command=self.start, bg=acc, fg="white", relief="flat", font=f, padx=14, pady=8)
        self.b_on.pack(side="left", padx=(0, 8))
        self.b_off = tk.Button(row, text="Стоп", command=self.stop, bg="#2a3148", fg=ink, relief="flat", font=f, padx=14, pady=8, state="disabled")
        self.b_off.pack(side="left", padx=(0, 8))
        self.b_rec = tk.Button(row, text="Запись WAV", command=self.rec, bg="#2a3148", fg=ink, relief="flat", font=f, padx=14, pady=8, state="disabled")
        self.b_rec.pack(side="left")

        row2 = tk.Frame(box, bg=card)
        row2.pack(fill="x", padx=16, pady=(0, 8))
        self.b_hear = tk.Button(row2, text="1. Тест: слышу себя", command=lambda: self.set_hear(True), bg="#4de1c1", fg="#06241d", relief="flat", font=f, padx=12, pady=8)
        self.b_hear.pack(side="left", padx=(0, 8))
        self.b_mute = tk.Button(row2, text="2. Готово: себя не слышу", command=lambda: self.set_hear(False), bg="#2a3148", fg=ink, relief="flat", font=f, padx=12, pady=8)
        self.b_mute.pack(side="left")
        self.hint = tk.Label(box, text="Сначала подберите голос. Если пика нет — выберите другие динамики выше.", font=f, bg=card, fg=muted)
        self.hint.pack(anchor="w", padx=16)
        self.meter_bg = tk.Frame(box, bg="#0c101c", height=10)
        self.meter_bg.pack(fill="x", padx=16, pady=14)
        self.meter = tk.Frame(self.meter_bg, bg="#4de1c1", width=0, height=10)
        self.meter.place(x=0, y=0, relheight=1)

        pr = tk.Frame(self.root, bg=card)
        pr.pack(fill="x", padx=22, pady=8)
        tk.Label(pr, text="ПРЕСЕТЫ", font=("Segoe UI", 9), bg=card, fg=muted).pack(anchor="w", padx=16, pady=(12, 4))
        grid = tk.Frame(pr, bg=card)
        grid.pack(fill="x", padx=12, pady=(0, 12))
        for i, p in enumerate(PRESETS):
            b = tk.Button(grid, text=p[0], command=lambda pp=p: self.preset(pp), bg="#0f1320", fg=ink, relief="flat", font=f, padx=10, pady=7)
            b.grid(row=i // 5, column=i % 5, padx=4, pady=4, sticky="ew")
        for c in range(5):
            grid.grid_columnconfigure(c, weight=1)

        sl = tk.Frame(self.root, bg=card)
        sl.pack(fill="both", expand=True, padx=22, pady=(8, 20))
        names = [("pitch", "Высота", 0.55, 1.7, 0.01), ("tone", "Тембр", -12, 12, 1), ("robot", "Робот", 0, 100, 1), ("drive", "Искажение", 0, 100, 1), ("room", "Зал", 0, 100, 1), ("vol", "Громкость", 0, 100, 1)]
        for key, title, a, b, st in names:
            line = tk.Frame(sl, bg=card)
            line.pack(fill="x", padx=16, pady=4)
            lab = tk.Label(line, text=title, font=f, bg=card, fg=muted, width=14, anchor="w")
            lab.pack(side="left")
            s = tk.Scale(line, from_=a, to=b, resolution=st, orient="horizontal", variable=self.vars[key], bg=card, fg=ink, troughcolor="#0f1320", highlightthickness=0, command=lambda _e: self.sync())
            s.pack(side="left", fill="x", expand=True)

    def _pick(self, items, name):
        for i, n in items:
            if n == name:
                return i
        return WAVE_MAPPER

    def sync(self):
        self.fx.set(
            pitch=float(self.vars["pitch"].get()),
            tone=float(self.vars["tone"].get()),
            robot=float(self.vars["robot"].get()) / 100,
            drive=float(self.vars["drive"].get()) / 100,
            room=float(self.vars["room"].get()) / 100,
            vol=max(0.15, float(self.vars["vol"].get()) / 100),
            hear=self.hear,
        )

    def preset(self, p):
        _, pitch, tone, robot, drive, room = p
        self.vars["pitch"].set(pitch)
        self.vars["tone"].set(tone)
        self.vars["robot"].set(robot)
        self.vars["drive"].set(drive)
        self.vars["room"].set(room)
        self.sync()

    def set_hear(self, on):
        self.hear = on
        self.sync()
        ink = "#f3f1ff"
        if on:
            self.b_hear.configure(bg="#4de1c1", fg="#06241d")
            self.b_mute.configure(bg="#2a3148", fg=ink)
            self.hint.configure(text="Тест: вы слышите обработанный голос. Полоска должна прыгать, когда говорите.")
            self.status.set("Тест")
        else:
            self.b_mute.configure(bg="#4de1c1", fg="#06241d")
            self.b_hear.configure(bg="#2a3148", fg=ink)
            self.hint.configure(text="Себя не слышите. Эффекты продолжают работать.")
            self.status.set("Без прослушки")

    def start(self):
        self.eng.in_dev = self._pick(self.in_devs, self.in_name.get())
        self.eng.out_dev = self._pick(self.out_devs, self.out_name.get())
        self.sync()
        try:
            self.eng.start()
        except Exception as e:
            messagebox.showerror("VoicePlay", str(e))
            return
        self.b_on.configure(state="disabled")
        self.b_off.configure(state="normal")
        self.b_rec.configure(state="normal")
        self.set_hear(True)

    def stop(self):
        self.eng.stop()
        self.b_on.configure(state="normal")
        self.b_off.configure(state="disabled")
        self.b_rec.configure(state="disabled")
        self.status.set("Выкл")

    def rec(self):
        if not self.eng.recording:
            self.eng.rec = []
            self.eng.recording = True
            self.b_rec.configure(text="Стоп и сохранить")
            return
        self.eng.recording = False
        self.b_rec.configure(text="Запись WAV")
        data = b"".join(self.eng.rec)
        if not data:
            messagebox.showinfo("VoicePlay", "Пустая запись")
            return
        path = filedialog.asksaveasfilename(defaultextension=".wav", filetypes=[("WAV", "*.wav")])
        if not path:
            return
        with wave.open(path, "wb") as w:
            w.setnchannels(1)
            w.setsampwidth(2)
            w.setframerate(self.eng.rate)
            w.writeframes(data)

    def _meter(self):
        w = int(self.meter_bg.winfo_width() * min(1, self.eng.peak * 2.4))
        self.meter.place(x=0, y=0, width=max(0, w), relheight=1)
        self.root.after(50, self._meter)

    def close(self):
        self.eng.stop()
        self.root.destroy()

    def run(self):
        self.root.mainloop()


if __name__ == "__main__":
    App().run()
