import numpy as np
import pyaudio
import soundfile as sf
from scipy.fft import fft
from scipy.signal import windows, butter, lfilter, spectrogram
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
import threading
import wave
import os

# Constants
RATE = 44100  # Частота сэмплирования
DURATION = 60  # Длительность записи по умолчанию в секундах
CHANNELS = 1  # Моно
FORMAT = pyaudio.paInt16  # Формат аудио
CHUNK = 1024  # Количество кадров в буфере
AUDIO_FILE = 'recorded_audio.wav'

class AudioProcessor:
    def __init__(self):
        self.audio_data = None
        self.frequencies = None
        self.magnitudes = None
        self.missing_freqs = None
        self.generated_signal = None
        self.is_recording = False
        self.is_playing = False

    # Функция записи аудио с микрофона
    def record_audio(self, duration, progress_callback):
        p = pyaudio.PyAudio()
        frames = []
        stream = None

        try:
            stream = p.open(format=FORMAT,
                            channels=CHANNELS,
                            rate=RATE,
                            input=True,
                            frames_per_buffer=CHUNK)

            self.is_recording = True
            total_chunks = int(RATE / CHUNK * duration)

            for i in range(total_chunks):
                if not self.is_recording:
                    break
                data = stream.read(CHUNK, exception_on_overflow=False)
                frames.append(data)
                progress = int((i + 1) / total_chunks * 100)
                progress_callback(progress)

            self.audio_data = b''.join(frames)
            self.save_audio_to_file(self.audio_data)

        except Exception as e:
            messagebox.showerror("Ошибка", f"Произошла ошибка во время записи: {e}")
        finally:
            if stream is not None:
                stream.stop_stream()
                stream.close()
            p.terminate()
            self.is_recording = False

    # Сохранение аудио данных в WAV файл
    def save_audio_to_file(self, audio_data):
        wf = wave.open(AUDIO_FILE, 'wb')
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(pyaudio.PyAudio().get_sample_size(FORMAT))
        wf.setframerate(RATE)
        wf.writeframes(audio_data)
        wf.close()

    # Функция воспроизведения записанного аудио
    def play_audio(self, progress_callback):
        if self.audio_data is None:
            messagebox.showwarning("Предупреждение", "Аудио не было записано.")
            return

        p = pyaudio.PyAudio()
        stream = None

        try:
            wf = wave.open(AUDIO_FILE, 'rb')
            stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                            channels=wf.getnchannels(),
                            rate=wf.getframerate(),
                            output=True)
            self.is_playing = True
            data = wf.readframes(CHUNK)
            total_frames = wf.getnframes()
            frames_played = 0

            while data and self.is_playing:
                stream.write(data)
                data = wf.readframes(CHUNK)
                frames_played += CHUNK
                progress = int((frames_played) / total_frames * 100)
                progress_callback(progress)

        except Exception as e:
            messagebox.showerror("Ошибка", f"Произошла ошибка во время воспроизведения: {e}")
        finally:
            self.is_playing = False
            if stream is not None:
                stream.stop_stream()
                stream.close()
            p.terminate()

    # Функция анализа частот
    def analyze_frequency(self):
        if self.audio_data is None:
            messagebox.showwarning("Предупреждение", "Аудио не было записано.")
            return

        # Преобразование байтовых данных в numpy массив
        audio_array = np.frombuffer(self.audio_data, dtype=np.int16)

        # Предварительная фильтрация для удаления шумов
        audio_array = self.noise_reduction(audio_array)

        # Применение окна Хэмминга
        window = windows.hamming(len(audio_array))
        windowed_signal = audio_array * window

        # Увеличение разрешения FFT
        N = 4 * len(windowed_signal)  # Увеличиваем размер FFT для повышения разрешения
        yf = fft(windowed_signal, n=N)
        xf = np.fft.fftfreq(N, 1 / RATE)

        # Получение спектра амплитуд
        magnitude_spectrum = np.abs(yf[:N // 2])
        frequencies = xf[:N // 2]

        # Сохраняем результаты
        self.frequencies = frequencies
        self.magnitudes = magnitude_spectrum

    # Функция фильтрации и подавления шума
    def noise_reduction(self, signal):
        # Применяем полосовой фильтр
        lowcut = 20.0
        highcut = 20000.0
        nyq = 0.5 * RATE
        low = lowcut / nyq
        high = highcut / nyq
        b, a = butter(5, [low, high], btype='band')
        filtered_signal = lfilter(b, a, signal)

        # Применяем адаптивное подавление шума
        # Здесь можно добавить более сложные алгоритмы подавления шума

        return filtered_signal

    # Функция идентификации отсутствующих частот
    def identify_missing_frequencies(self):
        if self.frequencies is None or self.magnitudes is None:
            messagebox.showwarning("Предупреждение", "Частотный анализ не был выполнен.")
            return

        # Фокусируемся на слышимом человеком диапазоне
        idx = np.where((self.frequencies >= 20) & (self.frequencies <= 20000))
        frequencies = self.frequencies[idx]
        magnitudes = self.magnitudes[idx]

        # Нормализация спектра
        magnitudes /= np.max(magnitudes)

        # Используем динамический порог на основе средней мощности и стандартного отклонения
        mean_magnitude = np.mean(magnitudes)
        std_magnitude = np.std(magnitudes)
        threshold = mean_magnitude + std_magnitude * 0.5

        # Идентифицируем слабые (отсутствующие) частоты
        missing_freqs = frequencies[magnitudes < threshold]

        # Используем метод кластеризации для группировки близких частот
        from sklearn.cluster import DBSCAN

        if len(missing_freqs) > 0:
            freq_reshaped = missing_freqs.reshape(-1, 1)
            clustering = DBSCAN(eps=50, min_samples=1).fit(freq_reshaped)
            labels = clustering.labels_

            # Выбираем центральную частоту каждого кластера
            unique_labels = set(labels)
            representative_freqs = []
            for label in unique_labels:
                cluster_freqs = missing_freqs[labels == label]
                center_freq = np.mean(cluster_freqs)
                representative_freqs.append(center_freq)

            self.missing_freqs = np.array(representative_freqs)
        else:
            self.missing_freqs = np.array([])

    # Функция генерации звуковой волны с отсутствующими частотами
    def generate_missing_frequencies(self):
        if self.missing_freqs is None or len(self.missing_freqs) == 0:
            messagebox.showinfo("Информация", "Отсутствующие частоты не идентифицированы.")
            return

        t = np.linspace(0, DURATION, int(RATE * DURATION), endpoint=False)

        # Создаем пустой сигнал
        signal = np.zeros_like(t)

        # Увеличиваем амплитуду для слабых частот
        amplitude = 0.8 / len(self.missing_freqs)

        for freq in self.missing_freqs:
            signal += amplitude * np.sin(2 * np.pi * freq * t)

        # Нормализуем сигнал
        signal /= np.max(np.abs(signal))

        # Применяем оконную функцию и плавные атаки/затухания
        fade_in_out = windows.tukey(len(signal), alpha=0.1)
        signal *= fade_in_out

        self.generated_signal = signal

    # Сохранение сгенерированного сигнала в аудио файл
    def save_generated_audio(self):
        if self.generated_signal is not None:
            sf.write('missing_frequencies.flac', self.generated_signal, RATE)
            messagebox.showinfo("Успех", "Сгенерированное аудио сохранено как 'missing_frequencies.flac'.")
        else:
            messagebox.showwarning("Предупреждение", "Аудио не было сгенерировано из-за отсутствия отсутствующих частот.")

class App:
    def __init__(self, root):
        self.root = root
        self.processor = AudioProcessor()
        self.duration = DURATION

        self.root.title("Анализатор Аудио Частот")
        self.create_widgets()

    def create_widgets(self):
        # Выбор длительности
        duration_frame = ttk.Frame(self.root)
        duration_frame.pack(pady=10)
        ttk.Label(duration_frame, text="Длительность записи (секунды):").pack(side=tk.LEFT)
        self.duration_entry = ttk.Entry(duration_frame, width=5)
        self.duration_entry.insert(0, str(DURATION))
        self.duration_entry.pack(side=tk.LEFT)

        # Кнопка записи
        self.record_button = ttk.Button(self.root, text="Записать Аудио", command=self.start_recording)
        self.record_button.pack(pady=5)

        # Полоса прогресса записи
        self.record_progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=300, mode='determinate')
        self.record_progress.pack()

        # Кнопка воспроизведения
        self.play_button = ttk.Button(self.root, text="Воспроизвести Записанное Аудио", command=self.start_playback)
        self.play_button.pack(pady=5)
        self.play_button.config(state=tk.DISABLED)

        # Полоса прогресса воспроизведения
        self.play_progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=300, mode='determinate')
        self.play_progress.pack()

        # Кнопка анализа
        self.analyze_button = ttk.Button(self.root, text="Анализ Частот", command=self.analyze_frequency)
        self.analyze_button.pack(pady=5)
        self.analyze_button.config(state=tk.DISABLED)

        # Кнопка генерации
        self.generate_button = ttk.Button(self.root, text="Сгенерировать Отсутствующие Частоты", command=self.generate_audio)
        self.generate_button.pack(pady=5)
        self.generate_button.config(state=tk.DISABLED)

    def start_recording(self):
        try:
            duration = float(self.duration_entry.get())
            if duration <= 0:
                raise ValueError
            self.duration = duration
        except ValueError:
            messagebox.showerror("Неверная Длительность", "Введите корректное положительное число для длительности.")
            return

        self.record_button.config(state=tk.DISABLED)
        self.play_button.config(state=tk.DISABLED)
        self.analyze_button.config(state=tk.DISABLED)
        self.generate_button.config(state=tk.DISABLED)
        self.record_progress['value'] = 0

        recording_thread = threading.Thread(target=self.processor.record_audio, args=(self.duration, self.update_record_progress))
        recording_thread.start()

        self.root.after(100, self.check_recording_thread, recording_thread)

    def update_record_progress(self, value):
        self.record_progress['value'] = value

    def check_recording_thread(self, thread):
        if thread.is_alive():
            self.root.after(100, self.check_recording_thread, thread)
        else:
            self.record_button.config(state=tk.NORMAL)
            self.play_button.config(state=tk.NORMAL)
            self.analyze_button.config(state=tk.NORMAL)
            self.record_progress['value'] = 100

    def start_playback(self):
        self.play_button.config(state=tk.DISABLED)
        self.record_button.config(state=tk.DISABLED)
        self.analyze_button.config(state=tk.DISABLED)
        self.generate_button.config(state=tk.DISABLED)
        self.play_progress['value'] = 0

        playback_thread = threading.Thread(target=self.processor.play_audio, args=(self.update_play_progress,))
        playback_thread.start()

        self.root.after(100, self.check_playback_thread, playback_thread)

    def update_play_progress(self, value):
        self.play_progress['value'] = value

    def check_playback_thread(self, thread):
        if thread.is_alive():
            self.root.after(100, self.check_playback_thread, thread)
        else:
            self.play_button.config(state=tk.NORMAL)
            self.record_button.config(state=tk.NORMAL)
            self.analyze_button.config(state=tk.NORMAL)
            self.generate_button.config(state=tk.NORMAL)
            self.play_progress['value'] = 100

    def analyze_frequency(self):
        self.processor.analyze_frequency()
        self.processor.identify_missing_frequencies()
        self.generate_button.config(state=tk.NORMAL)
        messagebox.showinfo("Анализ Завершен", "Частотный анализ завершен.")

    def generate_audio(self):
        self.processor.generate_missing_frequencies()
        self.processor.save_generated_audio()

    def on_close(self):
        # Удаление временного аудио файла
        if os.path.exists(AUDIO_FILE):
            os.remove(AUDIO_FILE)
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.protocol("WM_DELETE_WINDOW", app.on_close)
    root.mainloop() скрипт зависает при анализе частот адаптируй его производительность для 8 гибайт оперативной памяти на убунту 22.04