手机端python语言作曲软件代码最新版ZXQZQ
📅 2026/7/22 6:47:22
👁️ 阅读次数
📝 编程学习
# -*- coding: utf-8 -*- """作曲软件 v2 - 标准简谱格式 完全支持规则:数字 + # + 八度标记(可多个) + 延长符 + 分组标记 """ import random import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog from threading import Thread # 调式 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, "E小调": 4, "D小调": 2, } MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11] MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10] PENTATONIC = [0, 2, 4, 7, 9] ALL_SEMITONES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # 半音 → 简谱 (包含 #) 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", } STYLES = { "欢快": {"scale": MAJOR_SCALE, "rhythms": [0.25, 0.5, 0.5, 0.25, 1.0, 0.5], "step_pref": 2, "oct_range": (-2, 2), "chromatic_prob": 0.2}, "抒情": {"scale": MAJOR_SCALE, "rhythms": [0.5, 1.0, 1.0, 0.5, 2.0], "step_pref": 1, "oct_range": (-2, 2), "chromatic_prob": 0.1}, "激昂": {"scale": MAJOR_SCALE, "rhythms": [0.5, 0.5, 0.25, 0.25, 0.5, 1.0], "step_pref": 3, "oct_range": (-2, 3), "chromatic_prob": 0.25}, "平静": {"scale": PENTATONIC, "rhythms": [1.0, 1.0, 0.5, 0.5, 2.0], "step_pref": 1, "oct_range": (-2, 1), "chromatic_prob": 0.05}, "古风": {"scale": PENTATONIC, "rhythms": [1.0, 0.5, 0.5, 1.0, 0.5, 0.5], "step_pref": 1, "oct_range": (-2, 1), "chromatic_prob": 0.05}, "进行曲": {"scale": MAJOR_SCALE, "rhythms": [0.5, 0.5, 1.0, 0.5, 0.5, 1.0], "step_pref": 2, "oct_range": (-2, 2), "chromatic_prob": 0.15}, "舞曲": {"scale": MAJOR_SCALE, "rhythms": [0.25, 0.25, 0.5, 0.25, 0.25, 0.5, 1.0], "step_pref": 2, "oct_range": (-2, 2), "chromatic_prob": 0.2}, "摇滚": {"scale": MAJOR_SCALE, "rhythms": [0.5, 0.25, 0.25, 0.5, 1.0, 0.5], "step_pref": 3, "oct_range": (-2, 3), "chromatic_prob": 0.3}, "爵士": {"scale": MAJOR_SCALE, "rhythms": [0.5, 0.25, 0.5, 0.25, 0.5, 1.0], "step_pref": 2, "oct_range": (-2, 2), "chromatic_prob": 0.5}, } 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) # 低八度:. .. ... # oct_shift == 0 时不添加任何标记 # 如果带升号,忽略八度标记,只返回带 # 的基础音符 return base def format_with_group(base_jianpu, dur): """处理拍长和分组标记 规则: - 无后缀 = 1拍 - - = 延长1拍 - -- = 延长2拍 - (x) = 拍长减半(0.5拍) - (x/) = 拍长减半再减半(0.25拍) - [xx] = 组内音符共占1拍 """ 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 + ")" + "/" * (half_count - 1) elif half_count == 1: return "(" + base_jianpu + ")" return base_jianpu def gen_melody(n_bars, style, oct_lo, oct_hi, step_pref): """生成旋律,支持多八度变化""" scale = style["scale"] chromatic_prob = style.get("chromatic_prob", 0.15) cur_semi = scale[0] cur_oct = 0 out = [] for bar in range(n_bars): beats = 0.0 bar_notes = [] attempts = 0 while beats < 4.0 and attempts < 50: attempts += 1 dur = random.choice(style["rhythms"]) if beats + dur > 4.0: dur = 4.0 - beats if dur < 0.125: break # 决定是否使用升降音 use_chromatic = random.random() < chromatic_prob if use_chromatic: non_scale = [s for s in ALL_SEMITONES if s not in scale] if non_scale: new_semi = random.choice(non_scale) else: new_semi = random.choice(scale) else: if random.random() < 0.6 and cur_semi in scale: cur_idx = scale.index(cur_semi) step = random.choice([-1, 1]) * random.randint(1, min(step_pref, 3)) new_idx = max(0, min(len(scale) - 1, cur_idx + step)) new_semi = scale[new_idx] else: new_semi = random.choice(scale) # 八度变化:可以跳多个八度 # 注意:如果这个音符是升降音(chromatic),我们仍然允许八度变化 # 但最终在 semi_to_jianpu 中,如果音符带 #,八度标记会被忽略 if random.random() < 0.15: oct_change = random.choice([-2, -1, 0, 0, 0, 1, 2]) cur_oct += oct_change cur_oct = max(oct_lo, min(oct_hi, cur_oct)) bar_notes.append((new_semi, cur_oct, dur)) cur_semi = new_semi beats += dur # 每小节回到主音(主音不带 #,所以可以有八度标记) if bar == 0 and bar_notes: bar_notes[0] = (scale[0], 0, bar_notes[0][2]) cur_semi = scale[0] cur_oct = 0 if bar_notes and random.random() < 0.25: bar_notes[-1] = (scale[0], 0, max(0.5, bar_notes[-1][2])) cur_semi = scale[0] cur_oct = 0 out.extend(bar_notes) return out def melody_to_score(melody): """旋律 → 标准简谱字符串 (带 | 分小节)""" bar_lines = [] cur_bar = [] cur_dur = 0.0 bar_size = 4.0 for semi, octv, dur in melody: cur_bar.append((semi, octv, dur)) cur_dur += dur if cur_dur >= bar_size - 0.001: s = "".join( format_with_group(semi_to_jianpu(semi, oo), du) for semi, oo, du in cur_bar ) bar_lines.append(s) cur_bar = [] cur_dur = 0.0 if cur_bar: s = "".join( format_with_group(semi_to_jianpu(semi, oo), du) for semi, oo, du in cur_bar ) bar_lines.append(s) return " | ".join(bar_lines) class App: def __init__(self, root): self.root = root root.title("作曲软件 v2 - 标准简谱格式") screen_width = root.winfo_screenwidth() if screen_width < 768: root.attributes('-fullscreen', True) self.font_size = 10 self.pad_size = 2 self.control_width = 6 self.spin_width = 3 self.btn_pad = 2 self.is_mobile = True else: root.geometry("500x650") self.font_size = 10 self.pad_size = 4 self.control_width = 8 self.spin_width = 4 self.btn_pad = 4 self.is_mobile = False self._build() def _build(self): main_frame = ttk.Frame(self.root, padding=self.pad_size) main_frame.pack(fill=tk.BOTH, expand=True) top_frame = ttk.Frame(main_frame) top_frame.pack(fill=tk.X, pady=2) row1 = ttk.Frame(top_frame) row1.pack(fill=tk.X, pady=1) ttk.Label(row1, text="调式:", font=("", self.font_size)).pack(side=tk.LEFT, padx=1) self.cmb_key = ttk.Combobox(row1, values=list(KEY_OFFSET.keys()), width=self.control_width, font=("", self.font_size)) self.cmb_key.current(7) self.cmb_key.pack(side=tk.LEFT, padx=1) ttk.Label(row1, text="风格:", font=("", self.font_size)).pack(side=tk.LEFT, padx=1) self.cmb_style = ttk.Combobox(row1, values=list(STYLES.keys()), width=self.control_width, font=("", self.font_size)) self.cmb_style.current(1) self.cmb_style.pack(side=tk.LEFT, padx=1) ttk.Label(row1, text="小节:", font=("", self.font_size)).pack(side=tk.LEFT, padx=1) self.spn_bars = tk.Spinbox(row1, from_=4, to=64, width=self.spin_width, font=("", self.font_size)) self.spn_bars.delete(0, tk.END) self.spn_bars.insert(0, "66") self.spn_bars.pack(side=tk.LEFT, padx=1) row2 = ttk.Frame(top_frame) row2.pack(fill=tk.X, pady=1) ttk.Label(row2, text="乐器:", font=("", self.font_size)).pack(side=tk.LEFT, padx=1) self.cmb_inst = ttk.Combobox(row2, values=[str(i) for i in range(128)], width=self.control_width, font=("", self.font_size)) self.cmb_inst.current(0) self.cmb_inst.pack(side=tk.LEFT, padx=1) ttk.Label(row2, text="BPM:", font=("", self.font_size)).pack(side=tk.LEFT, padx=1) self.spn_bpm = tk.Spinbox(row2, from_=60, to=240, width=self.spin_width, font=("", self.font_size)) self.spn_bpm.delete(0, tk.END) self.spn_bpm.insert(0, "120") self.spn_bpm.pack(side=tk.LEFT, padx=1) ttk.Label(row2, text="随机:", font=("", self.font_size)).pack(side=tk.LEFT, padx=1) self.chk_random = tk.IntVar(value=0) ttk.Checkbutton(row2, variable=self.chk_random).pack(side=tk.LEFT, padx=1) btn_frame = ttk.Frame(top_frame) btn_frame.pack(fill=tk.X, pady=2) self.btn = ttk.Button(btn_frame, text="生成旋律", command=self._do_async, width=8 if self.is_mobile else 10) self.btn.pack(side=tk.LEFT, padx=self.btn_pad) ttk.Button(btn_frame, text="再来一首", command=self._do_again, width=8 if self.is_mobile else 10).pack(side=tk.LEFT, padx=self.btn_pad) ttk.Button(btn_frame, text="保存", command=self._save, width=6 if self.is_mobile else 8).pack(side=tk.LEFT, padx=self.btn_pad) ttk.Button(btn_frame, text="复制", command=self._copy, width=6 if self.is_mobile else 8).pack(side=tk.LEFT, padx=self.btn_pad) self.lbl_status = ttk.Label(btn_frame, text="就绪", foreground="green", font=("", self.font_size)) self.lbl_status.pack(side=tk.LEFT, padx=4) txt_height = 8 if self.is_mobile else 10 txt_font_size = 14 if self.is_mobile else 14 self.txt = scrolledtext.ScrolledText(main_frame, font=("Consolas", txt_font_size), wrap=tk.WORD, height=txt_height) self.txt.pack(fill=tk.BOTH, expand=True, pady=2) bot = ttk.Frame(main_frame, padding=2) bot.pack(fill=tk.X) tip_text = "格式: 数字→#→八度(可多个) | 如 1'' (高两个八度) 1# (升1,不带八度标记)" if self.is_mobile: tip_text = "数字→#→八度 | 如 1'' 1#" ttk.Label(bot, text=tip_text, foreground="gray", font=("", self.font_size - 1 if self.is_mobile else self.font_size), wraplength=self.root.winfo_screenwidth() - 20 if self.is_mobile else 480).pack(side=tk.LEFT, padx=4) self._last_score = "" 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): key = self.cmb_key.get() style_name = self.cmb_style.get() try: bars = int(self.spn_bars.get()) except: bars = 16 try: bpm = int(self.spn_bpm.get()) except: bpm = 120 try: inst = int(self.cmb_inst.get()) except: inst = 0 is_random = self.chk_random.get() == 1 if is_random: key = random.choice(list(KEY_OFFSET.keys())) inst = random.randint(0, 127) bpm = random.randint(30, 900) self.root.after(0, self._update_random_values, key, inst, bpm) style = STYLES.get(style_name, STYLES["抒情"]) if key.endswith("大调"): scale = MAJOR_SCALE elif key.endswith("小调"): scale = MINOR_SCALE else: scale = MAJOR_SCALE if style_name in ("平静", "古风"): scale = PENTATONIC oct_lo, oct_hi = style["oct_range"] melody = gen_melody(bars, style, oct_lo, oct_hi, style["step_pref"]) score_body = melody_to_score(melody) header = f"[{inst},{bpm},{key}]\n" full = header + score_body + "\n" self._last_score = full self.root.after(0, self._render, full, key, inst, bpm, len(melody)) def _update_random_values(self, key, inst, bpm): self.cmb_key.set(key) self.cmb_inst.set(str(inst)) self.spn_bpm.delete(0, tk.END) self.spn_bpm.insert(0, str(bpm)) def _do_again(self): self._do_async() def _render(self, full, key, inst, bpm, n_notes): self.txt.delete("1.0", tk.END) self.txt.insert(tk.END, full) self.lbl_status.config(text=f"✓ 完成 ({n_notes} 音符)", foreground="green") self.btn.config(state=tk.NORMAL) def _save(self): if not self._last_score: messagebox.showinfo("提示", "请先生成旋律") return path = filedialog.asksaveasfilename(defaultextension=".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: return self.root.clipboard_clear() self.root.clipboard_append(self._last_score) self.lbl_status.config(text="已复制", foreground="blue") if __name__ == "__main__": root = tk.Tk() App(root) root.mainloop()
编程学习
技术分享
实战经验