import warnings
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import soundcard as sc
import soundfile as sf
import numpy as np
import threading
import time

# 屏蔽无关警告
warnings.filterwarnings("ignore", category=sc.SoundcardRuntimeWarning)


class AudioRecorderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("多功能录音机 - 系统内录+麦克风")
        self.root.geometry("500x400")
        self.root.resizable(False, False)

        # 核心变量
        self.is_recording = False  # 是否正在录音
        self.is_paused = False  # 是否暂停
        self.recorded_data = []  # 录制的数据
        self.record_thread = None  # 录音线程
        self.samplerate = 44100  # 采样率
        self.selected_device = None  # 选中的录音设备
        self.record_type = tk.StringVar(value="system")  # 录音类型:system(内录)/mic(麦克风)

        # 初始化UI
        self._init_ui()
        # 加载设备列表
        self._load_devices()

    def _init_ui(self):
        # 1. 录音类型选择
        type_frame = ttk.LabelFrame(self.root, text="录音类型")
        type_frame.pack(padx=20, pady=10, fill="x")

        ttk.Radiobutton(type_frame, text="系统内录", variable=self.record_type,
                        value="system", command=self._on_record_type_change).pack(side="left", padx=20, pady=5)
        ttk.Radiobutton(type_frame, text="麦克风录音", variable=self.record_type,
                        value="mic", command=self._on_record_type_change).pack(side="left", padx=20, pady=5)

        # 2. 设备选择
        device_frame = ttk.LabelFrame(self.root, text="录音设备")
        device_frame.pack(padx=20, pady=10, fill="x")

        ttk.Label(device_frame, text="选择设备:").pack(side="left", padx=10, pady=5)
        self.device_combobox = ttk.Combobox(device_frame, state="readonly", width=30)
        self.device_combobox.pack(side="left", padx=10, pady=5)
        self.device_combobox.bind("<<ComboboxSelected>>", self._on_device_selected)

        # 3. 录制时长设置
        time_frame = ttk.LabelFrame(self.root, text="录制设置")
        time_frame.pack(padx=20, pady=10, fill="x")

        ttk.Label(time_frame, text="录制时长(秒):").pack(side="left", padx=10, pady=5)
        self.time_entry = ttk.Entry(time_frame, width=10)
        self.time_entry.insert(0, "5")  # 默认5秒
        self.time_entry.pack(side="left", padx=5, pady=5)
        ttk.Label(time_frame, text="(0为无限时长,手动停止)").pack(side="left", padx=5, pady=5)

        # 4. 状态显示
        status_frame = ttk.LabelFrame(self.root, text="录制状态")
        status_frame.pack(padx=20, pady=10, fill="x")

        self.status_label = ttk.Label(status_frame, text="未开始录制", foreground="gray")
        self.status_label.pack(padx=10, pady=10)

        # 5. 控制按钮
        btn_frame = ttk.Frame(self.root)
        btn_frame.pack(padx=20, pady=20, fill="x")

        self.start_btn = ttk.Button(btn_frame, text="开始录制", command=self.start_recording)
        self.start_btn.pack(side="left", padx=10, pady=5, fill="x", expand=True)

        self.pause_btn = ttk.Button(btn_frame, text="暂停", command=self.pause_recording, state="disabled")
        self.pause_btn.pack(side="left", padx=10, pady=5, fill="x", expand=True)

        self.stop_btn = ttk.Button(btn_frame, text="停止录制", command=self.stop_recording, state="disabled")
        self.stop_btn.pack(side="left", padx=10, pady=5, fill="x", expand=True)

        self.save_btn = ttk.Button(btn_frame, text="保存录音", command=self.save_recording, state="disabled")
        self.save_btn.pack(side="left", padx=10, pady=5, fill="x", expand=True)

    def _load_devices(self):
        """加载录音设备列表(根据录音类型)"""
        if self.record_type.get() == "system":
            # 系统内录:加载扬声器设备
            devices = sc.all_speakers()
        else:
            # 麦克风录音:加载麦克风设备
            devices = sc.all_microphones()

        # 提取设备名
        device_names = [dev.name for dev in devices]
        self.device_combobox["values"] = device_names
        if device_names:
            self.device_combobox.current(0)
            self.selected_device = devices[0]

    def _on_record_type_change(self):
        """切换录音类型时重新加载设备列表"""
        self._load_devices()
        self.status_label.config(text="切换录音类型,请重新选择设备", foreground="blue")

    def _on_device_selected(self, event):
        """选择设备后更新选中的设备"""
        selected_idx = self.device_combobox.current()
        if self.record_type.get() == "system":
            self.selected_device = sc.all_speakers()[selected_idx]
        else:
            self.selected_device = sc.all_microphones()[selected_idx]
        self.status_label.config(text=f"已选中设备:{self.selected_device.name}", foreground="green")

    def _record_task(self):
        """后台录音线程任务"""
        try:
            # 获取录制时长
            try:
                record_seconds = int(self.time_entry.get())
            except ValueError:
                messagebox.showerror("错误", "录制时长请输入数字!")
                self.is_recording = False
                return

            # 创建录音器
            if self.record_type.get() == "system":
                # 系统内录:开启loopback
                mic = sc.get_microphone(id=str(self.selected_device.name), include_loopback=True)
            else:
                # 麦克风录音:普通麦克风
                mic = sc.get_microphone(id=str(self.selected_device.name))

            with mic.recorder(samplerate=self.samplerate, blocksize=1024) as recorder:
                self.recorded_data = []
                total_frames = self.samplerate * record_seconds if record_seconds > 0 else float("inf")
                recorded_frames = 0

                self.status_label.config(text="正在录制...", foreground="red")
                while self.is_recording and recorded_frames < total_frames:
                    if not self.is_paused:
                        # 每次录制1024帧
                        chunk = recorder.record(numframes=1024)
                        self.recorded_data.append(chunk)
                        recorded_frames += len(chunk)
                        # 更新录制时长显示
                        elapsed = recorded_frames / self.samplerate
                        self.status_label.config(text=f"正在录制... 已录制:{elapsed:.1f}秒", foreground="red")
                    else:
                        time.sleep(0.1)  # 暂停时休眠

                # 录制结束
                self.is_recording = False
                self.root.after(0, lambda: self.status_label.config(
                    text=f"录制完成!总时长:{recorded_frames / self.samplerate:.1f}秒", foreground="green"))
                self.root.after(0, lambda: self._update_btn_state(after_stop=True))

        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("录制错误", f"录音失败:{str(e)}"))
            self.root.after(0, lambda: self.status_label.config(text="录制失败", foreground="red"))
            self.is_recording = False
            self.root.after(0, lambda: self._update_btn_state(after_stop=True))

    def start_recording(self):
        """开始录制"""
        if not self.selected_device:
            messagebox.showwarning("警告", "请先选择录音设备!")
            return

        if self.is_recording and not self.is_paused:
            messagebox.showinfo("提示", "已经在录制中了!")
            return

        if self.is_paused:
            # 恢复暂停
            self.is_paused = False
            self.status_label.config(text="恢复录制...", foreground="red")
            self.pause_btn.config(text="暂停")
            return

        # 开始新的录制
        self.is_recording = True
        self.is_paused = False
        self._update_btn_state(after_start=True)
        # 启动后台录音线程
        self.record_thread = threading.Thread(target=self._record_task, daemon=True)
        self.record_thread.start()

    def pause_recording(self):
        """暂停/恢复录制"""
        if not self.is_recording:
            return

        self.is_paused = not self.is_paused
        if self.is_paused:
            self.status_label.config(text="录制已暂停", foreground="orange")
            self.pause_btn.config(text="恢复")
        else:
            self.status_label.config(text="恢复录制...", foreground="red")
            self.pause_btn.config(text="暂停")

    def stop_recording(self):
        """停止录制"""
        if self.is_recording:
            self.is_recording = False
            self.status_label.config(text="正在停止录制...", foreground="orange")
            # 等待线程结束
            if self.record_thread:
                self.record_thread.join(timeout=2)
            self._update_btn_state(after_stop=True)
            self.status_label.config(text="录制已停止", foreground="green")

    def save_recording(self):
        """保存录制的音频"""
        if not self.recorded_data:
            messagebox.showwarning("警告", "没有可保存的录音数据!")
            return

        # 选择保存路径
        file_path = filedialog.asksaveasfilename(
            defaultextension=".wav",
            filetypes=[("WAV音频文件", "*.wav"), ("所有文件", "*.*")],
            title="保存录音"
        )
        if not file_path:
            return

        try:
            # 合并录制的数据
            data = np.concatenate(self.recorded_data, axis=0)
            # 立体声转单声道(兼容更多播放器)
            data = data.mean(axis=1)
            # 保存为WAV文件
            sf.write(file_path, data, self.samplerate)
            messagebox.showinfo("成功", f"录音已保存到:\n{file_path}")
        except Exception as e:
            messagebox.showerror("保存失败", f"保存出错:{str(e)}")

    def _update_btn_state(self, after_start=False, after_stop=False):
        """更新按钮状态"""
        if after_start:
            self.start_btn.config(state="normal")  # 开始按钮仍可用(用于恢复暂停)
            self.pause_btn.config(state="normal")
            self.stop_btn.config(state="normal")
            self.save_btn.config(state="disabled")
        elif after_stop:
            self.start_btn.config(state="normal")
            self.pause_btn.config(state="disabled")
            self.stop_btn.config(state="disabled")
            self.save_btn.config(state="normal")
        else:
            # 默认状态
            self.start_btn.config(state="normal")
            self.pause_btn.config(state="disabled")
            self.stop_btn.config(state="disabled")
            self.save_btn.config(state="disabled")


if __name__ == "__main__":
    root = tk.Tk()
    app = AudioRecorderApp(root)
    root.mainloop()
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐