# -*- 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 (C460) 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_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) # 如果带升号忽略八度标记 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, padding4) top.pack(filltk.X) # 第一行调式 风格 小节数 row1 ttk.Frame(top) row1.pack(filltk.X, pady2) ttk.Label(row1, text调式:).pack(sidetk.LEFT) self.cmb_key ttk.Combobox(row1, valuesALL_KEYS, width8) self.cmb_key.current(0) self.cmb_key.pack(sidetk.LEFT, padx2) ttk.Label(row1, text风格:).pack(sidetk.LEFT, padx(6, 0)) self.cmb_style ttk.Combobox(row1, valuesALL_STYLES, width6) self.cmb_style.current(0) self.cmb_style.pack(sidetk.LEFT, padx2) ttk.Label(row1, text小节:).pack(sidetk.LEFT, padx(6, 0)) self.spn_bars tk.Spinbox(row1, from_4, to32, width4) self.spn_bars.delete(0, tk.END) self.spn_bars.insert(0, 88) self.spn_bars.pack(sidetk.LEFT, padx2) # 第二行BPM 乐器 按钮 row2 ttk.Frame(top) row2.pack(filltk.X, pady2) ttk.Label(row2, textBPM:).pack(sidetk.LEFT) self.spn_bpm tk.Spinbox(row2, from_60, to240, width5) self.spn_bpm.delete(0, tk.END) self.spn_bpm.insert(0, 100) self.spn_bpm.pack(sidetk.LEFT, padx2) ttk.Label(row2, text乐器:).pack(sidetk.LEFT, padx(6, 0)) self.cmb_inst ttk.Combobox(row2, values[str(i) for i in range(128)], width5) self.cmb_inst.current(0) self.cmb_inst.pack(sidetk.LEFT, padx2) # 随机复选框 self.chk_random tk.IntVar(value1) # 默认开启随机 ttk.Checkbutton(row2, text随机, variableself.chk_random).pack(sidetk.LEFT, padx4) self.btn ttk.Button(row2, text生成旋律, commandself._do_async) self.btn.pack(sidetk.LEFT, padx4) ttk.Button(row2, text再来一首, commandself._do_again).pack(sidetk.LEFT, padx2) self.lbl_status ttk.Label(row2, text就绪, foregroundgreen) self.lbl_status.pack(sidetk.LEFT, padx6) # 第三行保存按钮 row3 ttk.Frame(top) row3.pack(filltk.X, pady2) ttk.Button(row3, text保存简谱, commandself._save).pack(sidetk.LEFT, padx2) ttk.Button(row3, text复制简谱, commandself._copy).pack(sidetk.LEFT, padx2) # 简谱显示区域 self.txt_score scrolledtext.ScrolledText(self.root, font(Consolas, 14), wraptk.WORD) self.txt_score.pack(filltk.BOTH, expandTrue, padx4, pady4) 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(statetk.DISABLED) self.lbl_status.config(text生成中..., foregroundorange) Thread(targetself._do, daemonTrue).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(textstatus_msg, foregroundgreen) self.btn.config(statetk.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, initialfilef{key}_{style}_{bars}小节_简谱.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: messagebox.showinfo(提示, 请先生成旋律) return self.root.clipboard_clear() self.root.clipboard_append(self._last_score) self.lbl_status.config(text✓ 已复制到剪贴板, foregroundgreen) # # 主入口 # if __name__ __main__: root tk.Tk() app App(root) root.mainloop()