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

# Constants
RATE = 44100  # Sample rate
DURATION = 60  # Default duration in seconds
CHANNELS = 1  # Mono audio
FORMAT = pyaudio.paInt16  # Audio format
CHUNK = 1024  # Frames per buffer
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

    # Function to capture audio from the microphone
    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("Error", f"An error occurred during recording: {e}")
        finally:
            if stream is not None:
                stream.stop_stream()
                stream.close()
            p.terminate()
            self.is_recording = False

    # Save raw audio data to a WAV file
    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()

    # Function to play the recorded audio
    def play_audio(self, progress_callback):
        if self.audio_data is None:
            messagebox.showwarning("Warning", "No audio has been recorded yet.")
            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("Error", f"An error occurred during playback: {e}")
        finally:
            self.is_playing = False
            if stream is not None:
                stream.stop_stream()
                stream.close()
            p.terminate()

    # Function to perform frequency analysis
    def analyze_frequency(self):
        if self.audio_data is None:
            messagebox.showwarning("Warning", "No audio has been recorded yet.")
            return

        # Convert byte data to numpy array
        audio_array = np.frombuffer(self.audio_data, dtype=np.int16)

        # Apply Hamming window
        window = windows.hamming(len(audio_array))
        windowed_signal = audio_array * window

        # Perform FFT
        N = len(windowed_signal)
        yf = fft(windowed_signal)
        xf = np.fft.fftfreq(N, 1 / RATE)

        # Get the magnitude spectrum
        magnitude_spectrum = np.abs(yf[:N // 2])
        frequencies = xf[:N // 2]

        self.frequencies = frequencies
        self.magnitudes = magnitude_spectrum

    # Function to identify missing frequencies
    def identify_missing_frequencies(self):
        if self.frequencies is None or self.magnitudes is None:
            messagebox.showwarning("Warning", "Frequency analysis has not been performed yet.")
            return

        # Focus on human audible range
        idx = np.where((self.frequencies >= 20) & (self.frequencies <= 20000))
        frequencies = self.frequencies[idx]
        magnitudes = self.magnitudes[idx]

        # Calculate dynamic threshold based on mean magnitude
        threshold = np.mean(magnitudes) * 0.5
        missing_freqs = frequencies[magnitudes < threshold]

        # Limit the number of frequencies to prevent overloading
        max_freqs = 50  # Maximum number of frequencies to generate
        if len(missing_freqs) > max_freqs:
            missing_freqs = missing_freqs[:max_freqs]

        self.missing_freqs = missing_freqs

    # Function to generate a sound wave with missing frequencies
    def generate_missing_frequencies(self):
        if self.missing_freqs is None or len(self.missing_freqs) == 0:
            messagebox.showinfo("Info", "No missing frequencies identified.")
            return

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

        # Create an empty signal
        signal = np.zeros_like(t)

        # Adjust amplitude to prevent clipping
        amplitude = 1.0 / len(self.missing_freqs)

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

        # Normalize the signal
        signal /= np.max(np.abs(signal))

        self.generated_signal = signal

    # Save the generated signal as a FLAC file
    def save_generated_audio(self):
        if self.generated_signal is not None:
            sf.write('missing_frequencies.flac', self.generated_signal, RATE)
            messagebox.showinfo("Success", "Generated audio saved as 'missing_frequencies.flac'.")
        else:
            messagebox.showwarning("Warning", "No audio generated due to lack of missing frequencies.")

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

        self.root.title("Audio Frequency Analyzer")
        self.create_widgets()

    def create_widgets(self):
        # Duration selection
        duration_frame = ttk.Frame(self.root)
        duration_frame.pack(pady=10)
        ttk.Label(duration_frame, text="Recording Duration (seconds):").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)

        # Record button
        self.record_button = ttk.Button(self.root, text="Record Audio", command=self.start_recording)
        self.record_button.pack(pady=5)

        # Progress bar for recording
        self.record_progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=300, mode='determinate')
        self.record_progress.pack()

        # Play button
        self.play_button = ttk.Button(self.root, text="Play Recorded Audio", command=self.start_playback)
        self.play_button.pack(pady=5)
        self.play_button.config(state=tk.DISABLED)

        # Progress bar for playback
        self.play_progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=300, mode='determinate')
        self.play_progress.pack()

        # Analyze button
        self.analyze_button = ttk.Button(self.root, text="Analyze Frequency", command=self.analyze_frequency)
        self.analyze_button.pack(pady=5)
        self.analyze_button.config(state=tk.DISABLED)

        # Generate button
        self.generate_button = ttk.Button(self.root, text="Generate Missing Frequencies", 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("Invalid Duration", "Please enter a valid positive number for the duration.")
            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("Analysis Complete", "Frequency analysis completed.")

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

    def on_close(self):
        # Clean up temporary audio file
        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()    сделай максимально качественное распознавание и синтез  очень слабо выраженных и практически отсутствующих частот в скрипте примени подход триз опиши подробно что сделал