# -*- 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_shift0): 半音 → 简谱记号 规则禁止 # 和八度标记同时出现 - 如果音符带 #不添加任何八度标记 - 只有不带 # 的音符才能添加 或 . 八度标记 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, paddingself.pad_size) main_frame.pack(filltk.BOTH, expandTrue) top_frame ttk.Frame(main_frame) top_frame.pack(filltk.X, pady2) row1 ttk.Frame(top_frame) row1.pack(filltk.X, pady1) ttk.Label(row1, text调式:, font(, self.font_size)).pack(sidetk.LEFT, padx1) self.cmb_key ttk.Combobox(row1, valueslist(KEY_OFFSET.keys()), widthself.control_width, font(, self.font_size)) self.cmb_key.current(7) self.cmb_key.pack(sidetk.LEFT, padx1) ttk.Label(row1, text风格:, font(, self.font_size)).pack(sidetk.LEFT, padx1) self.cmb_style ttk.Combobox(row1, valueslist(STYLES.keys()), widthself.control_width, font(, self.font_size)) self.cmb_style.current(1) self.cmb_style.pack(sidetk.LEFT, padx1) ttk.Label(row1, text小节:, font(, self.font_size)).pack(sidetk.LEFT, padx1) self.spn_bars tk.Spinbox(row1, from_4, to64, widthself.spin_width, font(, self.font_size)) self.spn_bars.delete(0, tk.END) self.spn_bars.insert(0, 66) self.spn_bars.pack(sidetk.LEFT, padx1) row2 ttk.Frame(top_frame) row2.pack(filltk.X, pady1) ttk.Label(row2, text乐器:, font(, self.font_size)).pack(sidetk.LEFT, padx1) self.cmb_inst ttk.Combobox(row2, values[str(i) for i in range(128)], widthself.control_width, font(, self.font_size)) self.cmb_inst.current(0) self.cmb_inst.pack(sidetk.LEFT, padx1) ttk.Label(row2, textBPM:, font(, self.font_size)).pack(sidetk.LEFT, padx1) self.spn_bpm tk.Spinbox(row2, from_60, to240, widthself.spin_width, font(, self.font_size)) self.spn_bpm.delete(0, tk.END) self.spn_bpm.insert(0, 120) self.spn_bpm.pack(sidetk.LEFT, padx1) ttk.Label(row2, text随机:, font(, self.font_size)).pack(sidetk.LEFT, padx1) self.chk_random tk.IntVar(value0) ttk.Checkbutton(row2, variableself.chk_random).pack(sidetk.LEFT, padx1) btn_frame ttk.Frame(top_frame) btn_frame.pack(filltk.X, pady2) self.btn ttk.Button(btn_frame, text生成旋律, commandself._do_async, width8 if self.is_mobile else 10) self.btn.pack(sidetk.LEFT, padxself.btn_pad) ttk.Button(btn_frame, text再来一首, commandself._do_again, width8 if self.is_mobile else 10).pack(sidetk.LEFT, padxself.btn_pad) ttk.Button(btn_frame, text保存, commandself._save, width6 if self.is_mobile else 8).pack(sidetk.LEFT, padxself.btn_pad) ttk.Button(btn_frame, text复制, commandself._copy, width6 if self.is_mobile else 8).pack(sidetk.LEFT, padxself.btn_pad) self.lbl_status ttk.Label(btn_frame, text就绪, foregroundgreen, font(, self.font_size)) self.lbl_status.pack(sidetk.LEFT, padx4) 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), wraptk.WORD, heighttxt_height) self.txt.pack(filltk.BOTH, expandTrue, pady2) bot ttk.Frame(main_frame, padding2) bot.pack(filltk.X) tip_text 格式: 数字→#→八度(可多个) | 如 1 (高两个八度) 1# (升1不带八度标记) if self.is_mobile: tip_text 数字→#→八度 | 如 1 1# ttk.Label(bot, texttip_text, foregroundgray, font(, self.font_size - 1 if self.is_mobile else self.font_size), wraplengthself.root.winfo_screenwidth() - 20 if self.is_mobile else 480).pack(sidetk.LEFT, padx4) self._last_score def _do_async(self): self.btn.config(statetk.DISABLED) self.lbl_status.config(text生成中..., foregroundorange) Thread(targetself._do, daemonTrue).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(textf✓ 完成 ({n_notes} 音符), foregroundgreen) self.btn.config(statetk.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, encodingutf-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已复制, foregroundblue) if __name__ __main__: root tk.Tk() App(root) root.mainloop()