Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save AdelMmdi/ad1e8bb0fc480d203ec0ea09266ea009 to your computer and use it in GitHub Desktop.

Select an option

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.
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