A-59工业语音模块:宽温设计约束与十种工作模式的系统集成
2026/7/22 10:42:02
# -*- coding: utf-8 -*- """ 谱曲软件 v2 - 手机版 """ import re import os import time import sys import random import json import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog from threading import Thread from collections import defaultdict # ============================================================ # 14 调式 → 主音 MIDI (C4=60) KEY_OFFSET = { "C大调": 0, "C#大调": 1, "Db大调": 1, "D大调": 2, "D#大调": 3, "Eb大调": 3, "E大调": 4, "F大调": 5, "F#大调": 6, "Gb大调": 6, "G大调": 7, "G#大调": 8, "Ab大调": 8, "A大调": 9, "A#大调": 10, "Bb大调": 10, "B大调": 11, "A小调": 9, } # 所有调式列表(用于随机) ALL_KEYS = list(KEY_OFFSET.keys()) # 所有风格列表(用于随机) ALL_STYLES = ["抒情", "欢快", "激昂", "古风", "平静", "爵士"] # 音阶 MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11] PENTATONIC = [0, 2, 4, 7, 9] BLUES_SCALE = [0, 3, 5, 6, 7, 10] # 布鲁斯音阶 # 半音 → 简谱记号 SEMI_MAP = { 0: "1", 1: "1#", 2: "2", 3: "2#", 4: "3", 5: "4", 6: "4#", 7: "5", 8: "5#", 9: "6", 10: "6#", 11: "7", } # 可以加#的音符(升半音) SHARPABLE = [0, 2, 4, 5, 7, 9] # 1,2,3,4,5,6 可以升半音 def semi_to_jianpu(semi, oct_shift=0): """半音 → 简谱记号 规则:禁止 # 和八度标记同时出现 - 如果音符带 #,不添加任何八度标记 - 只有不带 # 的音符才能添加 ' 或 . 八度标记 """ base = SEMI_MAP.get(semi, "1") # 检查是否带升号(音符中包含 #) has_sharp = '#' in base # 只有不带升号的音符才能添加八度标记 if not has_sharp: if oct_shift > 0: base += "'" * oct_shift elif oct_shift < 0: base += "." * (-oct_shift) # 如果带升号,忽略八度标记 return base def format_with_group(base_jianpu, dur): """基础简谱 + 拍长, 应用 () 减半规则""" if abs(dur - 1.0) < 0.01: return base_jianpu if dur > 1.0: n = int(round(dur)) if n >= 2: return base_jianpu + "-" * (n - 1) # dur < 1.0: 用 () 减半 half_count = 0 d = dur while d < 0.999 and half_count < 5: d *= 2 half_count += 1 if half_count == 1: return "(" + base_jianpu + ")" elif half_count > 1: return "(" + base_jianpu + ")" + "/" * (half_count - 1) return base_jianpu def gen_melody_for_song(n_bars, key, style): """生成包含丰富音符变化的旋律""" # 风格配置 style_config = { "抒情": { "scale": MAJOR_SCALE, "high_prob": 0.20, "low_prob": 0.20, "sharp_prob": 0.05, "durations": [0.5, 1.0, 1.0, 1.5, 2.0], "range": 2, "repeat_prob": 0.3 }, "欢快": { "scale": MAJOR_SCALE, "high_prob": 0.30, "low_prob": 0.10, "sharp_prob": 0.10, "durations": [0.25, 0.5, 0.5, 1.0, 1.0], "range": 3, "repeat_prob": 0.2 }, "激昂": { "scale": MAJOR_SCALE, "high_prob": 0.40, "low_prob": 0.05, "sharp_prob": 0.15, "durations": [0.5, 0.5, 1.0, 1.0, 1.5], "range": 3, "repeat_prob": 0.15 }, "古风": { "scale": PENTATONIC, "high_prob": 0.15, "low_prob": 0.35, "sharp_prob": 0.02, "durations": [0.5, 1.0, 1.0, 2.0, 2.0], "range": 2, "repeat_prob": 0.4 }, "平静": { "scale": MAJOR_SCALE, "high_prob": 0.10, "low_prob": 0.25, "sharp_prob": 0.03, "durations": [1.0, 1.0, 1.5, 2.0, 2.0], "range": 1, "repeat_prob": 0.5 }, "爵士": { "scale": BLUES_SCALE, "high_prob": 0.25, "low_prob": 0.15, "sharp_prob": 0.25, "durations": [0.5, 0.5, 1.0, 1.0, 1.5], "range": 2, "repeat_prob": 0.2 } } config = style_config.get(style, style_config["抒情"]) scale = config["scale"] out = [] cur_degree = 0 bar_octave = 0 phrase_counter = 0 for bar_idx in range(n_bars): bar = [] beats = 0.0 attempts = 0 # 每4小节改变八度,形成段落 if bar_idx % 4 == 0 and bar_idx > 0: if random.random() < 0.4: bar_octave += random.choice([-1, 1]) bar_octave = max(-2, min(3, bar_octave)) # 小节内生成音符 while beats < 4.0 and attempts < 60: attempts += 1 # 选择时长 fits = [d for d in config["durations"] if beats + d <= 4.0 + 0.001] if not fits: break dur = random.choice(fits) # 选择音阶度数(带倾向性) if random.random() < config["repeat_prob"]: # 重复或小幅移动 step = random.choice([-1, 0, 1, 0]) cur_degree = max(0, min(len(scale) - 1, cur_degree + step)) else: # 随机跳跃 if random.random() < 0.4: # 倾向于主音、三音、五音 preferred = [0, 2, 4, 5, 7] cur_degree = random.choice([d for d in preferred if d < len(scale)]) else: cur_degree = random.randint(0, len(scale) - 1) # 计算八度偏移 oct_shift = bar_octave # 根据音符度数调整八度 if cur_degree >= len(scale) - 2: # 高音 if random.random() < config["high_prob"]: oct_shift += 1 elif cur_degree <= 1: # 低音 if random.random() < config["low_prob"]: oct_shift -= 1 # 随机八度变化 if random.random() < 0.12: oct_shift += random.choice([-1, 1]) # 限制八度范围 oct_shift = max(-2, min(3, oct_shift)) # 决定是否升半音 use_sharp = False if cur_degree in SHARPABLE and cur_degree < len(scale) - 1: # 检查升半音后的音是否在音阶中 next_semi = scale[cur_degree] + 1 if next_semi not in scale and random.random() < config["sharp_prob"]: use_sharp = True # 记录音符(使用升半音的话,改变半音值) if use_sharp: # 使用升半音的音符 semi = scale[cur_degree] + 1 # 确保不超过12 if semi >= 12: semi -= 12 oct_shift += 1 bar.append((semi, oct_shift, dur, True)) else: bar.append((scale[cur_degree], oct_shift, dur, False)) beats += dur # 每小节最后一个音处理 if bar: # 最后一个音延长或回到主音 if random.random() < 0.4: # 延长最后一个音 extra = random.choice([0.5, 1.0]) if beats + extra <= 4.0: bar[-1] = (bar[-1][0], bar[-1][1], bar[-1][2] + extra) elif random.random() < 0.3: # 回到主音 main_oct = bar_octave if random.random() < 0.2: main_oct += random.choice([-1, 1]) main_oct = max(-2, min(3, main_oct)) bar[-1] = (0, main_oct, max(0.5, bar[-1][2])) out.append(bar) return out, scale def bar_to_str(bar, scale): """一小节旋律 → 简谱字符串(完整支持各种符号)""" result = [] for item in bar: if len(item) == 4: semi, oct_shift, dur, is_sharp = item else: semi, oct_shift, dur = item is_sharp = False # 获取基础简谱记号 base = SEMI_MAP.get(semi, "1") # 如果是升半音且基础记号不包含#,添加# if is_sharp and "#" not in base: base += "#" # 添加八度标记(使用 semi_to_jianpu 函数,它会自动处理 # 和八度标记的冲突) # 但这里我们需要保留 is_sharp 的信息,所以手动处理 # 检查是否带升号 has_sharp = '#' in base # 只有不带升号的音符才能添加八度标记 if not has_sharp: if oct_shift > 0: base += "'" * oct_shift elif oct_shift < 0: base += "." * (-oct_shift) # 如果带升号,忽略八度标记 # 格式化时长 result.append(format_with_group(base, dur)) return "".join(result) # ============================================================ # GUI - 纯谱曲软件 (手机版) # ============================================================ class App: def __init__(self, root): self.root = root root.title("谱曲软件 v2 - 手机版") # 手机屏幕尺寸适配 screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() win_width = int(screen_width * 0.92) win_height = int(screen_height * 0.92) root.geometry(f"{win_width}x{win_height}") root.minsize(320, 480) self._build() def _build(self): # 顶部控制栏 top = ttk.Frame(self.root, padding=4) top.pack(fill=tk.X) # 第一行:调式 + 风格 + 小节数 row1 = ttk.Frame(top) row1.pack(fill=tk.X, pady=2) ttk.Label(row1, text="调式:").pack(side=tk.LEFT) self.cmb_key = ttk.Combobox(row1, values=ALL_KEYS, width=8) self.cmb_key.current(0) self.cmb_key.pack(side=tk.LEFT, padx=2) ttk.Label(row1, text="风格:").pack(side=tk.LEFT, padx=(6, 0)) self.cmb_style = ttk.Combobox(row1, values=ALL_STYLES, width=6) self.cmb_style.current(0) self.cmb_style.pack(side=tk.LEFT, padx=2) ttk.Label(row1, text="小节:").pack(side=tk.LEFT, padx=(6, 0)) self.spn_bars = tk.Spinbox(row1, from_=4, to=32, width=4) self.spn_bars.delete(0, tk.END) self.spn_bars.insert(0, "88") self.spn_bars.pack(side=tk.LEFT, padx=2) # 第二行:BPM + 乐器 + 按钮 row2 = ttk.Frame(top) row2.pack(fill=tk.X, pady=2) ttk.Label(row2, text="BPM:").pack(side=tk.LEFT) self.spn_bpm = tk.Spinbox(row2, from_=60, to=240, width=5) self.spn_bpm.delete(0, tk.END) self.spn_bpm.insert(0, "100") self.spn_bpm.pack(side=tk.LEFT, padx=2) ttk.Label(row2, text="乐器:").pack(side=tk.LEFT, padx=(6, 0)) self.cmb_inst = ttk.Combobox(row2, values=[str(i) for i in range(128)], width=5) self.cmb_inst.current(0) self.cmb_inst.pack(side=tk.LEFT, padx=2) # 随机复选框 self.chk_random = tk.IntVar(value=1) # 默认开启随机 ttk.Checkbutton(row2, text="随机", variable=self.chk_random).pack(side=tk.LEFT, padx=4) self.btn = ttk.Button(row2, text="生成旋律", command=self._do_async) self.btn.pack(side=tk.LEFT, padx=4) ttk.Button(row2, text="再来一首", command=self._do_again).pack(side=tk.LEFT, padx=2) self.lbl_status = ttk.Label(row2, text="就绪", foreground="green") self.lbl_status.pack(side=tk.LEFT, padx=6) # 第三行:保存按钮 row3 = ttk.Frame(top) row3.pack(fill=tk.X, pady=2) ttk.Button(row3, text="保存简谱", command=self._save).pack(side=tk.LEFT, padx=2) ttk.Button(row3, text="复制简谱", command=self._copy).pack(side=tk.LEFT, padx=2) # 简谱显示区域 self.txt_score = scrolledtext.ScrolledText(self.root, font=("Consolas", 14), wrap=tk.WORD) self.txt_score.pack(fill=tk.BOTH, expand=True, padx=4, pady=4) self._last_score = "" self._last_meta = "" self._last_params = {} def _random_all_params(self): """随机选择所有参数""" # 随机调式 random_key = random.choice(ALL_KEYS) self.cmb_key.set(random_key) # 随机风格 random_style = random.choice(ALL_STYLES) self.cmb_style.set(random_style) # 随机BPM (60-180) random_bpm = random.randint(60, 180) self.spn_bpm.delete(0, tk.END) self.spn_bpm.insert(0, str(random_bpm)) # 随机乐器 (0-127) random_inst = random.randint(0, 127) self.cmb_inst.set(str(random_inst)) # 随机小节数 (8-32) random_bars = random.randint(8, 180) self.spn_bars.delete(0, tk.END) self.spn_bars.insert(0, str(random_bars)) return random_key, random_style, random_bpm, random_inst, random_bars def _do_async(self): self.btn.config(state=tk.DISABLED) self.lbl_status.config(text="生成中...", foreground="orange") Thread(target=self._do, daemon=True).start() def _do(self): # 如果勾选了随机,先随机所有参数 if self.chk_random.get() == 1: key, style, bpm, inst, bars = self._random_all_params() else: key = self.cmb_key.get() style = self.cmb_style.get() try: bars = int(self.spn_bars.get()) except: bars = 8 try: bpm = int(self.spn_bpm.get()) except: bpm = 100 try: inst = int(self.cmb_inst.get()) except: inst = 0 # 生成旋律 melodies, scale = gen_melody_for_song(bars, key, style) # 构建简谱文本 score_text = f"[{inst},{bpm},{key}]\n" for i, bar in enumerate(melodies, 1): bar_str = bar_to_str(bar, scale) score_text += bar_str if i % 4 == 0: score_text += "\n" else: score_text += "|" self._last_score = score_text self._last_meta = f"{key}|{style}|{bpm}|{bars}" self._last_params = {"key": key, "style": style, "bpm": bpm, "inst": inst, "bars": bars} # 更新状态信息 status_msg = f"✓ {key} | {style} | {bpm}BPM | 乐器{inst} | {bars}小节" self.root.after(0, self._render, score_text, status_msg) def _do_again(self): self._do_async() def _render(self, score_text, status_msg): self.txt_score.delete("1.0", tk.END) self.txt_score.insert(tk.END, score_text) self.lbl_status.config(text=status_msg, foreground="green") self.btn.config(state=tk.NORMAL) def _save(self): if not self._last_score: messagebox.showinfo("提示", "请先生成旋律") return meta = self._last_meta.split("|") if self._last_meta else ["C大调", "抒情", "100", "8"] key, style, bpm, bars = meta[0], meta[1], meta[2], meta[3] path = filedialog.asksaveasfilename( defaultextension=".txt", initialfile=f"{key}_{style}_{bars}小节_简谱.txt", filetypes=[("简谱", "*.txt")] ) if path: with open(path, "w", encoding="utf-8") as f: f.write(self._last_score) messagebox.showinfo("保存", f"已保存到 {path}") def _copy(self): if not self._last_score: messagebox.showinfo("提示", "请先生成旋律") return self.root.clipboard_clear() self.root.clipboard_append(self._last_score) self.lbl_status.config(text="✓ 已复制到剪贴板", foreground="green") # ============================================================ # 主入口 # ============================================================ if __name__ == "__main__": root = tk.Tk() app = App(root) root.mainloop()