Last active
January 30, 2026 00:03
-
-
Save AdelMmdi/ad1e8bb0fc480d203ec0ea09266ea009 to your computer and use it in GitHub Desktop.
While watching videos and various sounds that are played from the output of the device, you need to save them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import tkinter as tk | |
| from tkinter import messagebox | |
| import threading | |
| import pyaudiowpatch as pyaudio # pip install PyAudioWPatch | |
| import wave | |
| import time | |
| import sys | |
| import os | |
| import numpy as np | |
| from matplotlib.figure import Figure | |
| from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg | |
| FORMAT = pyaudio.paInt16 | |
| CHUNK = 1024 | |
| # Quality presets | |
| QUALITY_PRESETS = { | |
| "Low (8 kHz)": 8000, | |
| "Medium (44.1 kHz)": 44100, | |
| "High (48 kHz)": 48000 | |
| } | |
| # Channel presets | |
| CHANNEL_PRESETS = { | |
| "Mono": 1, | |
| "Stereo": 2 | |
| } | |
| class WASAPIRecorder: | |
| def __init__(self): | |
| self.pa = pyaudio.PyAudio() | |
| self.stream = None | |
| self.frames = [] | |
| self.recording = False | |
| self.device_info = self.pa.get_default_wasapi_loopback() | |
| if self.device_info is None: | |
| messagebox.showerror("Error", "No WASAPI loopback device found.\nEnable 'Stereo Mix' or use headphones that support WASAPI.") | |
| sys.exit(1) | |
| def start_recording(self, rate, channels, update_waveform_callback): | |
| if self.recording: | |
| return | |
| self.frames = [] | |
| self.recording = True | |
| self.stream = self.pa.open(format=FORMAT, | |
| channels=channels, | |
| rate=rate, | |
| input=True, | |
| frames_per_buffer=CHUNK, | |
| input_device_index=self.device_info["index"]) | |
| while self.recording: | |
| data = self.stream.read(CHUNK, exception_on_overflow=False) | |
| self.frames.append(data) | |
| # Convert to numpy array | |
| audio_data = np.frombuffer(data, dtype=np.int16) | |
| if channels == 2: | |
| audio_data = audio_data[::2] # left channel only | |
| update_waveform_callback(audio_data) | |
| def stop_recording(self, filename, rate, channels): | |
| if not self.recording: | |
| return | |
| self.recording = False | |
| time.sleep(0.1) | |
| self.stream.stop_stream() | |
| self.stream.close() | |
| with wave.open(filename, 'wb') as wf: | |
| wf.setnchannels(channels) | |
| wf.setsampwidth(self.pa.get_sample_size(FORMAT)) | |
| wf.setframerate(rate) | |
| wf.writeframes(b''.join(self.frames)) | |
| def terminate(self): | |
| self.pa.terminate() | |
| class RecorderApp: | |
| def __init__(self, root): | |
| self.root = root | |
| self.root.title("WASAPI Recorder with Channel & Quality Selection") | |
| self.root.geometry("520x500") | |
| self.root.resizable(False, False) | |
| self.recorder = WASAPIRecorder() | |
| self.is_recording = False | |
| tk.Label(root, text="🎙 WASAPI Recorder", font=("Arial", 14)).pack(pady=5) | |
| # File name entry (no .wav shown) | |
| tk.Label(root, text="Output File Name (no extension):").pack() | |
| self.filename_entry = tk.Entry(root, width=40) | |
| self.filename_entry.insert(0, "system_audio") | |
| self.filename_entry.pack(pady=5) | |
| # Channel selection | |
| tk.Label(root, text="Select Channels:").pack() | |
| self.channel_var = tk.StringVar(value="Stereo") | |
| tk.OptionMenu(root, self.channel_var, *CHANNEL_PRESETS.keys()).pack(pady=5) | |
| # Quality selection | |
| tk.Label(root, text="Select Quality:").pack() | |
| self.quality_var = tk.StringVar(value="High (48 kHz)") | |
| tk.OptionMenu(root, self.quality_var, *QUALITY_PRESETS.keys()).pack(pady=5) | |
| self.label = tk.Label(root, text="Ready to record", font=("Arial", 12)) | |
| self.label.pack(pady=5) | |
| # Side-by-side buttons | |
| btn_frame = tk.Frame(root) | |
| btn_frame.pack(pady=5) | |
| self.start_btn = tk.Button(btn_frame, text="Start Recording", command=self.start_recording, bg="green", fg="white", width=15) | |
| self.start_btn.grid(row=0, column=0, padx=5) | |
| self.stop_btn = tk.Button(btn_frame, text="Stop Recording", command=self.stop_recording, bg="red", fg="white", width=15, state=tk.DISABLED) | |
| self.stop_btn.grid(row=0, column=1, padx=5) | |
| # Matplotlib figure for waveform | |
| self.fig = Figure(figsize=(5, 2), dpi=100) | |
| self.ax = self.fig.add_subplot(111) | |
| self.ax.set_ylim([-32768, 32767]) | |
| self.ax.set_xlim([0, CHUNK]) | |
| self.ax.set_title("Live Waveform") | |
| self.ax.set_xticks([]) | |
| self.ax.set_yticks([]) | |
| self.line, = self.ax.plot(np.zeros(CHUNK), color='blue') | |
| self.canvas = FigureCanvasTkAgg(self.fig, master=root) | |
| self.canvas.get_tk_widget().pack(pady=10) | |
| def update_waveform(self, audio_data): | |
| self.line.set_ydata(audio_data) | |
| self.canvas.draw_idle() | |
| def start_recording(self): | |
| if self.is_recording: | |
| return | |
| filename_base = self.filename_entry.get().strip() | |
| if not filename_base: | |
| messagebox.showerror("Error", "Please enter a file name.") | |
| return | |
| self.output_filename = filename_base + ".wav" | |
| self.selected_channels = CHANNEL_PRESETS[self.channel_var.get()] | |
| requested_rate = QUALITY_PRESETS[self.quality_var.get()] | |
| # Get default WASAPI loopback device info | |
| device_info = self.recorder.device_info | |
| default_rate = int(device_info["defaultSampleRate"]) | |
| # If requested rate is not supported, use default | |
| if requested_rate != default_rate: | |
| try: | |
| # Try opening with requested rate to test | |
| test_stream = self.recorder.pa.open( | |
| format=FORMAT, | |
| channels=self.selected_channels, | |
| rate=requested_rate, | |
| input=True, | |
| frames_per_buffer=CHUNK, | |
| input_device_index=device_info["index"] | |
| ) | |
| test_stream.close() | |
| except Exception: | |
| messagebox.showwarning( | |
| "Sample Rate Unsupported", | |
| f"{requested_rate} Hz not supported. Using default {default_rate} Hz." | |
| ) | |
| requested_rate = default_rate | |
| self.selected_rate = requested_rate | |
| self.is_recording = True | |
| self.label.config(text="🔴 Recording...", fg="red") | |
| self.start_btn.config(state=tk.DISABLED) | |
| self.stop_btn.config(state=tk.NORMAL) | |
| threading.Thread( | |
| target=self.recorder.start_recording, | |
| args=(self.selected_rate, self.selected_channels, self.update_waveform), | |
| daemon=True | |
| ).start() | |
| def stop_recording(self): | |
| if not self.is_recording: | |
| return | |
| self.is_recording = False | |
| self.recorder.stop_recording(self.output_filename, self.selected_rate, self.selected_channels) | |
| # Copy just the base name (no .wav) to clipboard | |
| base_name = os.path.splitext(self.output_filename)[0] | |
| self.root.clipboard_clear() | |
| self.root.clipboard_append(base_name) | |
| self.root.update() | |
| self.label.config(text=f"✅ Saved: {self.output_filename} (name copied)", fg="green") | |
| self.start_btn.config(state=tk.NORMAL) | |
| self.stop_btn.config(state=tk.DISABLED) | |
| def on_close(self): | |
| if self.is_recording: | |
| self.recorder.stop_recording(self.output_filename, self.selected_rate, self.selected_channels) | |
| self.recorder.terminate() | |
| self.root.destroy() | |
| if __name__ == "__main__": | |
| root = tk.Tk() | |
| app = RecorderApp(root) | |
| root.protocol("WM_DELETE_WINDOW", app.on_close) | |
| root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment