wangrong
2026-08-24 beef6536e7c8dd3f5cbe8f73585611479910bea7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import threading
import sys
import ffmpeg
 
class AudioConverterApp:
    def __init__(self, master):
        self.master = master
        master.title("小智AI OGG音频批量转换工具")
        master.geometry("680x600")  # 调整窗口高度
 
        # 初始化变量
        self.mode = tk.StringVar(value="audio_to_ogg")
        self.output_dir = tk.StringVar()
        self.output_dir.set(os.path.abspath("output"))
        self.enable_loudnorm = tk.BooleanVar(value=True)
        self.target_lufs = tk.DoubleVar(value=-16.0)
 
        # 创建UI组件
        self.create_widgets()
        self.redirect_output()
 
    def create_widgets(self):
        # 模式选择
        mode_frame = ttk.LabelFrame(self.master, text="转换模式")
        mode_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew")
        
        ttk.Radiobutton(mode_frame, text="音频转到OGG", variable=self.mode,
                        value="audio_to_ogg", command=self.toggle_settings,
                        width=12).grid(row=0, column=0, padx=5)
        ttk.Radiobutton(mode_frame, text="OGG转回音频", variable=self.mode,
                        value="ogg_to_audio", command=self.toggle_settings,
                        width=12).grid(row=0, column=1, padx=5)
 
        # 响度设置
        self.loudnorm_frame = ttk.Frame(self.master)
        self.loudnorm_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew")
        
        ttk.Checkbutton(self.loudnorm_frame, text="启用响度调整", 
                       variable=self.enable_loudnorm, width=15
                       ).grid(row=0, column=0, padx=2)
        ttk.Entry(self.loudnorm_frame, textvariable=self.target_lufs, 
                 width=6).grid(row=0, column=1, padx=2)
        ttk.Label(self.loudnorm_frame, text="LUFS").grid(row=0, column=2, padx=2)
 
        # 文件选择
        file_frame = ttk.LabelFrame(self.master, text="输入文件")
        file_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew")
        
        # 文件操作按钮
        ttk.Button(file_frame, text="选择文件", command=self.select_files,
                  width=12).grid(row=0, column=0, padx=5, pady=2)
        ttk.Button(file_frame, text="移除选中", command=self.remove_selected,
                  width=12).grid(row=0, column=1, padx=5, pady=2)
        ttk.Button(file_frame, text="清空列表", command=self.clear_files,
                  width=12).grid(row=0, column=2, padx=5, pady=2)
 
        # 文件列表(使用Treeview)
        self.tree = ttk.Treeview(file_frame, columns=("selected", "filename"), 
                               show="headings", height=8)
        self.tree.heading("selected", text="选中", anchor=tk.W)
        self.tree.heading("filename", text="文件名", anchor=tk.W)
        self.tree.column("selected", width=60, anchor=tk.W)
        self.tree.column("filename", width=600, anchor=tk.W)
        self.tree.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=5, pady=2)
        self.tree.bind("<ButtonRelease-1>", self.on_tree_click)
 
        # 输出目录
        output_frame = ttk.LabelFrame(self.master, text="输出目录")
        output_frame.grid(row=3, column=0, padx=10, pady=5, sticky="ew")
        
        ttk.Entry(output_frame, textvariable=self.output_dir, width=60
                 ).grid(row=0, column=0, padx=5, sticky="ew")
        ttk.Button(output_frame, text="浏览", command=self.select_output_dir,
                  width=8).grid(row=0, column=1, padx=5)
 
        # 转换按钮区域
        button_frame = ttk.Frame(self.master)
        button_frame.grid(row=4, column=0, padx=10, pady=10, sticky="ew")
        
        ttk.Button(button_frame, text="转换全部文件", command=lambda: self.start_conversion(True),
                  width=15).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="转换选中文件", command=lambda: self.start_conversion(False),
                  width=15).pack(side=tk.LEFT, padx=5)
 
        # 日志区域
        log_frame = ttk.LabelFrame(self.master, text="日志")
        log_frame.grid(row=5, column=0, padx=10, pady=5, sticky="nsew")
        
        self.log_text = tk.Text(log_frame, height=14, width=80)
        self.log_text.pack(fill=tk.BOTH, expand=True)
 
        # 配置布局权重
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(2, weight=1)
        self.master.rowconfigure(5, weight=3)
        file_frame.columnconfigure(0, weight=1)
        file_frame.rowconfigure(1, weight=1)
 
    def toggle_settings(self):
        if self.mode.get() == "audio_to_ogg":
            self.loudnorm_frame.grid()
        else:
            self.loudnorm_frame.grid_remove()
 
    def select_files(self):
        file_types = [
            ("音频文件", "*.wav *.mogg *.ogg *.flac") if self.mode.get() == "audio_to_ogg" 
            else ("ogg文件", "*.ogg")
        ]
        
        files = filedialog.askopenfilenames(filetypes=file_types)
        for f in files:
            self.tree.insert("", tk.END, values=("[ ]", os.path.basename(f)), tags=(f,))
 
    def on_tree_click(self, event):
        """处理复选框点击事件"""
        region = self.tree.identify("region", event.x, event.y)
        if region == "cell":
            col = self.tree.identify_column(event.x)
            item = self.tree.identify_row(event.y)
            if col == "#1":  # 点击的是选中列
                current_val = self.tree.item(item, "values")[0]
                new_val = "[√]" if current_val == "[ ]" else "[ ]"
                self.tree.item(item, values=(new_val, self.tree.item(item, "values")[1]))
 
    def remove_selected(self):
        """移除选中的文件"""
        to_remove = []
        for item in self.tree.get_children():
            if self.tree.item(item, "values")[0] == "[√]":
                to_remove.append(item)
        for item in reversed(to_remove):
            self.tree.delete(item)
 
    def clear_files(self):
        """清空所有文件"""
        for item in self.tree.get_children():
            self.tree.delete(item)
 
    def select_output_dir(self):
        path = filedialog.askdirectory()
        if path:
            self.output_dir.set(path)
 
    def redirect_output(self):
        class StdoutRedirector:
            def __init__(self, text_widget):
                self.text_widget = text_widget
                self.original_stdout = sys.stdout
 
            def write(self, message):
                self.text_widget.insert(tk.END, message)
                self.text_widget.see(tk.END)
                self.original_stdout.write(message)
 
            def flush(self):
                self.original_stdout.flush()
 
        sys.stdout = StdoutRedirector(self.log_text)
 
    def start_conversion(self, convert_all):
        """开始转换"""
        input_files = []
        for item in self.tree.get_children():
            if convert_all or self.tree.item(item, "values")[0] == "[√]":
                input_files.append(self.tree.item(item, "tags")[0])
        
        if not input_files:
            msg = "没有找到可转换的文件" if convert_all else "没有选中任何文件"
            messagebox.showwarning("警告", msg)
            return
        
        os.makedirs(self.output_dir.get(), exist_ok=True)
 
        try:
            if self.mode.get() == "audio_to_ogg":
                target_lufs = self.target_lufs.get() if self.enable_loudnorm.get() else None
                thread = threading.Thread(target=self.convert_audio_to_ogg, args=(target_lufs, input_files))
            else:
                thread = threading.Thread(target=self.convert_ogg_to_audio, args=(input_files,))
            
            thread.start()
        except Exception as e:
            print(f"转换初始化失败: {str(e)}")
 
    def convert_audio_to_ogg(self, target_lufs, input_files):
        """音频转到ogg转换逻辑"""
        for input_path in input_files:
            try:
                filename = os.path.basename(input_path)
                base_name = os.path.splitext(filename)[0]
                output_path = os.path.join(self.output_dir.get(), f"{base_name}.ogg")
                
                print(f"正在转换: {filename}")
                (
                    ffmpeg
                    .input(input_path)
                    .output(output_path, acodec='libopus', audio_bitrate='16k', ac=1, ar=16000, frame_duration=60)
                    .run(overwrite_output=True)
                )
                print(f"转换成功: {filename}\n")
            except Exception as e:
                print(f"转换失败: {str(e)}\n")
 
    def convert_ogg_to_audio(self, input_files):
        """ogg转回音频转换逻辑"""
        for input_path in input_files:
            try:
                filename = os.path.basename(input_path)
                base_name = os.path.splitext(filename)[0]
                output_path = os.path.join(self.output_dir.get(), f"{base_name}.ogg")
                
                print(f"正在转换: {filename}")
                (
                    ffmpeg
                    .input(input_path)
                    .output(output_path, acodec='libopus', audio_bitrate='16k', ac=1, ar=16000, frame_duration=60)
                    .run(overwrite_output=True)
                )
                print(f"转换成功: {filename}\n")
            except Exception as e:
                print(f"转换失败: {str(e)}\n")
 
if __name__ == "__main__":
    root = tk.Tk()
    app = AudioConverterApp(root)
    root.mainloop()