# -*- coding: utf-8 -*-
"""
作曲软件 v3 – 整合经典作曲技术
支持:对位法、和声学、曲式学、配器法、现代技法
输出:多声部简谱 + MIDI多轨
"""
import random
import math
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,
"G小调": 7, "C小调": 0, "F小调": 5,
"Bb小调": 10, "Eb小调": 3,
}
# 音阶类型
MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11]
MINOR_SCALE = [0, 2, 3, 5, 7, 8, 10]
HARMONIC_MINOR = [0, 2, 3, 5, 7, 8, 11]
MELODIC_MINOR_UP = [0, 2, 3, 5, 7, 9, 11]
PENTATONIC_MAJOR = [0, 2, 4, 7, 9]
PENTATONIC_MINOR = [0, 3, 5, 7, 10]
BLUES_SCALE = [0, 3, 5, 6, 7, 10]
WHOLE_TONE = [0, 2, 4, 6, 8, 10]
OCTATONIC = [0, 1, 3, 4, 6, 7, 9, 10]
ALL_SEMITONES = list(range(12))
# 半音 → 简谱
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",
}
# 简谱 → MIDI音高(C4=60,即简谱1=C)
def jianpu_to_midi(note_str, base_octave=4):
"""将简谱字符串转为MIDI音高数字"""
if not note_str or note_str in ['|', ' ', '-', '_']:
return None
num = None
sharp = 0
oct_shift = 0
for ch in note_str:
if ch in '1234567':
num = int(ch)
elif ch == '#':
sharp = 1
elif ch == "'":
oct_shift += 1
elif ch == '.':
oct_shift -= 1
if num is None:
return None
num_to_semi = {1: 0, 2: 2, 3: 4, 4: 5, 5: 7, 6: 9, 7: 11}
semi = num_to_semi[num] + sharp
midi = (base_octave + oct_shift) * 12 + 12 + semi
return midi
# ==================== 风格配置 ====================
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_MAJOR, "rhythms": [1.0, 1.0, 0.5, 0.5, 2.0],
"step_pref": 1, "oct_range": (-2, 1), "chromatic_prob": 0.05},
"古风": {"scale": PENTATONIC_MAJOR, "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},
"蓝调": {"scale": BLUES_SCALE, "rhythms": [0.5, 0.5, 1.0, 0.5, 0.5, 1.0],
"step_pref": 2, "oct_range": (-2, 2), "chromatic_prob": 0.3},
"全音阶": {"scale": WHOLE_TONE, "rhythms": [0.5, 1.0, 0.5, 1.0, 0.5],
"step_pref": 1, "oct_range": (-2, 2), "chromatic_prob": 0.0},
"八声音阶": {"scale": OCTATONIC, "rhythms": [0.25, 0.5, 0.25, 0.5, 1.0],
"step_pref": 1, "oct_range": (-2, 2), "chromatic_prob": 0.0},
}
# ==================== 作曲技术引擎 ====================
class CounterpointEngine:
"""对位法引擎 — 支持二声部、三声部、卡农、赋格"""
INTERVAL_NAMES = {
0: "纯一度", 1: "小二度", 2: "大二度", 3: "小三度",
4: "大三度", 5: "纯四度", 6: "增四度/减五度", 7: "纯五度",
8: "小六度", 9: "大六度", 10: "小七度", 11: "大七度", 12: "纯八度"
}
# 协和度:3=完全协和, 2=不完全协和, 1=不协和, 0=禁用
CONSONANCE = {
0: 3, 7: 3, 12: 3,
3: 2, 4: 2, 8: 2, 9: 2,
5: 2,
2: 1, 10: 1,
1: 1, 11: 1,
6: 0,
}
def __init__(self, key_offset=0, scale=None):
self.key = key_offset
self.scale = scale or MAJOR_SCALE
self.voices = []
self.errors = []
def get_interval(self, p1, p2):
return abs(p1 – p2) % 12
def is_parallel_fifth(self, v1_prev, v1_curr, v2_prev, v2_curr):
prev_int = self.get_interval(v1_prev, v2_prev)
curr_int = self.get_interval(v1_curr, v2_curr)
return prev_int == 7 and curr_int == 7 and v1_prev != v1_curr
def is_parallel_octave(self, v1_prev, v1_curr, v2_prev, v2_curr):
prev_int = self.get_interval(v1_prev, v2_prev)
curr_int = self.get_interval(v1_curr, v2_curr)
return prev_int == 0 and curr_int == 0 and v1_prev != v1_curr
def generate_cantus_firmus(self, n_notes, start_note, scale=None):
"""生成定旋律(Cantus Firmus)"""
scale = scale or self.scale
notes = [start_note]
for i in range(1, n_notes):
prev = notes[-1]
step_weights = [0.5, 0.3, 0.15, 0.05]
step_type = random.choices([1, 3, 5, random.randint(2, 7)], weights=step_weights)[0]
direction = random.choice([-1, 1])
new_note = prev + direction * step_type
new_semi = new_note % 12
if new_semi not in scale:
candidates = [(s, abs(s – new_semi)) for s in scale]
candidates.sort(key=lambda x: x[1])
new_note = (new_note // 12) * 12 + candidates[0][0]
new_note = max(start_note – 12, min(start_note + 12, new_note))
notes.append(new_note)
final = start_note
if notes[-1] != final:
notes[-1] = final
return notes
def generate_counterpoint(self, cantus, voice_type="above", species=1):
"""生成对位旋律"""
cp = []
octave_offset = 12 if voice_type == "above" else -12
for i, cf_note in enumerate(cantus):
target = cf_note + octave_offset
if species == 1:
candidates = []
for offset in [-3, -4, -5, 3, 4, 5, 7, 8, 12]:
cand = target + offset
if cand % 12 in self.scale:
candidates.append(cand)
if candidates:
cp.append(random.choice(candidates))
else:
cp.append(target + random.choice([3, 4, 5]))
elif species == 2:
if i < len(cantus) – 1:
first = target + random.choice([3, 4, 5, 7, 8])
step = random.choice([-1, 1])
second = first + step
cp.extend([first, second])
else:
cp.append(target + 3)
return cp
def generate_canon(self, theme, interval=4, delay=2):
"""生成卡农"""
voices = [theme]
imitation = [(n + interval) % 12 + (n // 12) * 12 for n in theme]
voices.append(imitation)
return voices
def generate_fugue_subject(self, scale, length=8):
"""生成赋格主题"""
subject = []
tonic = scale[0]
dominant = scale[4] if len(scale) >= 5 else scale[0] + 7
subject.append(tonic)
subject.append(dominant)
direction = -1 if random.random() < 0.5 else 1
current = dominant
for i in range(length – 3):
step = random.choice([1, 2]) * direction
current += step
current_semi = current % 12
if current_semi not in scale:
current = (current // 12) * 12 + min(scale, key=lambda s: abs(s – current_semi))
subject.append(current)
subject.append(tonic)
return subject
class HarmonyEngine:
"""和声学引擎 — 四部和声、和弦进行、调式交替"""
CHORD_TYPES = {
"I": [0, 4, 7],
"ii": [2, 5, 9],
"iii": [4, 7, 11],
"IV": [5, 9, 0],
"V": [7, 11, 2],
"vi": [9, 0, 4],
"vii°": [11, 2, 5],
"V7": [7, 11, 2, 5],
"ii7": [2, 5, 9, 0],
"viio7": [11, 2, 5, 8],
"V/V": [2, 6, 9],
"V/ii": [9, 1, 4],
}
PROGRESSIONS = {
"古典终止": ["I", "IV", "V", "I"],
"正格进行": ["I", "V", "I"],
"变格进行": ["I", "IV", "I"],
"半终止": ["I", "IV", "V"],
"阻碍终止": ["I", "V", "vi"],
"完全进行": ["I", "vi", "ii", "V", "I"],
"爵士ii-V-I": ["ii7", "V7", "I"],
"流行进行": ["I", "V", "vi", "IV"],
"蓝调进行": ["I", "IV", "I", "V", "IV", "I"],
"调式交替": ["I", "ii", "iii", "IV", "V", "vi", "vii°", "I"],
}
def __init__(self, key_offset=0, scale=None):
self.key = key_offset
self.scale = scale or MAJOR_SCALE
self.chords = []
def get_chord_pitches(self, chord_name, inversion=0):
base_pitches = self.CHORD_TYPES.get(chord_name, [0, 4, 7])
if inversion > 0:
base_pitches = base_pitches[inversion:] + [p + 12 for p in base_pitches[:inversion]]
return [(p + self.key) % 12 for p in base_pitches]
def generate_four_part(self, progression_name, n_bars=4):
progression = self.PROGRESSIONS.get(progression_name, self.PROGRESSIONS["古典终止"])
ranges = {'S': (60, 81), 'A': (55, 74), 'T': (48, 67), 'B': (40, 60)}
voices = {'S': [], 'A': [], 'T': [], 'B': []}
for bar in range(n_bars):
chord_name = progression[bar % len(progression)]
chord_pitches = self.get_chord_pitches(chord_name)
for voice_name in ['B', 'T', 'A', 'S']:
v_range = ranges[voice_name]
candidates = []
for p in chord_pitches:
for octv in range(3, 7):
midi = p + octv * 12
if v_range[0] <= midi <= v_range[1]:
candidates.append(midi)
if candidates:
if voices[voice_name]:
prev = voices[voice_name][-1]
candidates.sort(key=lambda x: abs(x – prev))
chosen = candidates[0]
else:
chosen = random.choice(candidates)
voices[voice_name].append(chosen)
else:
voices[voice_name].append(v_range[0] + 4)
return voices
def generate_jazz_changes(self, n_bars=8):
jazz_progressions = [
["ii7", "V7", "I", "I"],
["I", "vi", "ii7", "V7"],
["ii7", "V7", "iii", "vi"],
["I", "IV", "ii7", "V7"],
]
result = []
for bar in range(n_bars):
prog = jazz_progressions[bar % len(jazz_progressions)]
chord = prog[bar % len(prog)]
result.append(chord)
return result
class FormEngine:
"""曲式学引擎"""
def __init__(self, melody_engine=None):
self.melody_engine = melody_engine
def generate_binary(self, theme, n_bars_a=8, n_bars_b=8, style=None):
style = style or STYLES["抒情"]
section_a = theme[:n_bars_a] if len(theme) >= n_bars_a else theme
dominant_shift = 7
section_b = []
for semi, octv, dur in theme[:n_bars_b]:
new_semi = (semi + dominant_shift) % 12
section_b.append((new_semi, octv, dur))
return {"A": section_a, "B": section_b}
def generate_ternary(self, theme, n_bars=8, style=None):
style = style or STYLES["抒情"]
section_a = theme[:n_bars] if len(theme) >= n_bars else theme
section_b = []
for semi, octv, dur in section_a:
new_oct = octv + random.choice([-1, 1])
new_dur = dur / 2 if dur >= 0.5 else dur
section_b.append((semi, new_oct, new_dur))
section_a_prime = []
for semi, octv, dur in section_a:
if random.random() < 0.3:
neighbor = semi + random.choice([-1, 1])
section_a_prime.append((neighbor, octv, dur * 0.25))
section_a_prime.append((semi, octv, dur * 0.75))
else:
section_a_prime.append((semi, octv, dur))
return {"A": section_a, "B": section_b, "A'": section_a_prime}
def generate_rondo(self, theme, episodes=2, style=None):
style = style or STYLES["欢快"]
result = {"A": theme}
for i in range(episodes):
episode = []
for semi, octv, dur in theme:
new_semi = (semi + 2) % 12
new_oct = octv + random.choice([-1, 0, 1])
episode.append((new_semi, new_oct, dur))
result[f"插部{i+1}"] = episode
return result
def generate_variations(self, theme, n_variations=3, style=None):
style = style or STYLES["抒情"]
variations = {"主题": theme}
techniques = ["装饰变奏", "节奏变奏", "调式变奏", "音区变奏"]
for i in range(n_variations):
tech = techniques[i % len(techniques)]
var = []
for semi, octv, dur in theme:
if tech == "装饰变奏":
if random.random() < 0.4:
passing = semi + random.choice([-1, 1])
var.append((passing, octv, dur * 0.3))
var.append((semi, octv, dur * 0.7))
else:
var.append((semi, octv, dur))
elif tech == "节奏变奏":
new_dur = dur * random.choice([0.5, 1.0, 2.0])
var.append((semi, octv, new_dur))
elif tech == "调式变奏":
new_semi = semi
if semi in [4, 9, 11]:
new_semi = semi – 1
var.append((new_semi, octv, dur))
elif tech == "音区变奏":
new_oct = octv + random.choice([-1, 1])
var.append((semi, new_oct, dur))
else:
var.append((semi, octv, dur))
variations[f"变奏{i+1}({tech})"] = var
return variations
def generate_sonata(self, theme_group1, theme_group2, n_bars=16, style=None):
style = style or STYLES["激昂"]
exposition = {
"主部(主调)": theme_group1[:n_bars//2],
"连接部": theme_group1[n_bars//2:n_bars//2+4],
"副部(属调)": [(semi+7, octv, dur) for semi, octv, dur in theme_group2[:n_bars//2]],
}
development = []
for semi, octv, dur in theme_group1 + theme_group2:
new_semi = (semi + random.randint(1, 6)) % 12
new_dur = dur * random.choice([0.5, 0.5, 0.25])
development.append((new_semi, octv, new_dur))
recapitulation = {
"主部再现": theme_group1[:n_bars//2],
"副部再现(主调)": theme_group2[:n_bars//2],
"尾声": theme_group1[:4],
}
return {"呈示部": exposition, "展开部": development, "再现部": recapitulation}
class OrchestrationEngine:
"""配器法引擎"""
INSTRUMENTS = {
"钢琴": 0, "明亮钢琴": 1, "电钢琴": 2, "酒吧钢琴": 3,
"颤音琴": 11, "马林巴": 12, "木琴": 13, "管钟": 14,
"风琴": 16, "教堂风琴": 19, "手风琴": 21, "口琴": 22,
"尼龙吉他": 24, "钢弦吉他": 25, "电吉他": 27, "失真吉他": 29,
"贝司": 32, "电贝司": 33, "无品贝司": 35,
"小提琴": 40, "中提琴": 41, "大提琴": 42, "低音提琴": 43,
"弦乐合奏": 48, "弦乐拨奏": 45, "竖琴": 46,
"长笛": 73, "双簧管": 68, "单簧管": 71, "巴松": 70,
"圆号": 60, "小号": 56, "长号": 57, "大号": 58,
"铜管合奏": 61, "合成器主音": 80, "合成器铺底": 88,
"管弦乐打击": 47, "定音鼓": 47,
}
RANGES = {
"钢琴": (21, 108), "小提琴": (55, 103), "中提琴": (48, 91),
"大提琴": (36, 76), "低音提琴": (28, 67),
"长笛": (60, 96), "双簧管": (58, 91), "单簧管": (50, 94),
"巴松": (34, 75), "圆号": (31, 77), "小号": (54, 86),
"长号": (40, 72), "大号": (28, 58),
"人声-女高": (60, 84), "人声-女低": (53, 77),
"人声-男高": (48, 72), "人声-男低": (40, 64),
}
ENSEMBLES = {
"弦乐四重奏": ["小提琴", "小提琴", "中提琴", "大提琴"],
"钢琴三重奏": ["钢琴", "小提琴", "大提琴"],
"铜管五重奏": ["小号", "小号", "圆号", "长号", "大号"],
"木管五重奏": ["长笛", "双簧管", "单簧管", "巴松", "圆号"],
"管弦乐队": ["小提琴", "中提琴", "大提琴", "低音提琴",
"长笛", "双簧管", "单簧管", "巴松",
"圆号", "小号", "长号", "大号", "钢琴"],
"流行乐队": ["钢琴", "电吉他", "贝司", "合成器主音", "弦乐合奏"],
}
def __init__(self):
self.assignments = []
def assign_instruments(self, melody, ensemble_name="弦乐四重奏"):
instruments = self.ENSEMBLES.get(ensemble_name, ["钢琴"])
tracks = []
for i, inst_name in enumerate(instruments):
inst_id = self.INSTRUMENTS.get(inst_name, 0)
inst_range = self.RANGES.get(inst_name, (36, 84))
track = []
for semi, octv, dur in melody:
midi = (4 + octv) * 12 + semi
if midi < inst_range[0]:
midi += 12
elif midi > inst_range[1]:
midi -= 12
oct_adjust = 0
if inst_name in ["低音提琴", "大号", "人声-男低"]:
oct_adjust = -1
elif inst_name in ["小提琴", "长笛", "小号"]:
oct_adjust = 1
new_semi = midi % 12
new_oct = (midi // 12) – 4 + oct_adjust
track.append((new_semi, new_oct, dur, inst_id, inst_name))
tracks.append(track)
return tracks
def generate_texture(self, melody, texture_type="主调"):
if texture_type == "主调":
accompaniment = []
for semi, octv, dur in melody:
third = (semi + 4) % 12
fifth = (semi + 7) % 12
accompaniment.append([
(semi, octv – 1, dur, "旋律"),
(third, octv – 1, dur, "三音"),
(fifth, octv – 1, dur, "五音"),
])
return {"旋律": melody, "伴奏": accompaniment}
elif texture_type == "复调":
cp = CounterpointEngine()
cantus = [semi + (4 + octv) * 12 for semi, octv, dur in melody]
counter = cp.generate_counterpoint(cantus, "below", 1)
counter_melody = [(c % 12, c // 12 – 4, 1.0) for c in counter]
return {"高音": melody, "低音": counter_melody}
elif texture_type == "分解和弦":
arpeggio = []
for semi, octv, dur in melody:
chord_tones = [semi, (semi + 4) % 12, (semi + 7) % 12]
for i, tone in enumerate(chord_tones):
arpeggio.append((tone, octv – 1, dur / 3))
return {"旋律": melody, "琶音": arpeggio}
elif texture_type == "持续音":
drone = []
tonic = melody[0][0] if melody else 0
for _ in range(len(melody)):
drone.append((tonic, -2, 4.0))
return {"旋律": melody, "持续音": drone}
return {"旋律": melody}
class TwelveToneEngine:
"""十二音技法引擎"""
def __init__(self):
self.row = None
self.matrix = None
def generate_row(self):
row = list(range(12))
random.shuffle(row)
self.row = row
return row
def build_matrix(self):
if not self.row:
self.generate_row()
matrix = []
for i in range(12):
transposed = [(x + i) % 12 for x in self.row]
matrix.append(transposed)
self.matrix = matrix
return matrix
def get_transformations(self):
if not self.row:
self.generate_row()
P = self.row
I = [(-x) % 12 for x in self.row]
R = list(reversed(self.row))
RI = [(-x) % 12 for x in reversed(self.row)]
return {"P": P, "I": I, "R": R, "RI": RI}
def generate_piece(self, n_notes=48, use_rhythm=True):
transforms = self.get_transformations()
forms = ["P", "I", "R", "RI"]
piece = []
for i in range(n_notes // 12):
form = forms[i % 4]
row = transforms[form]
transposition = random.randint(0, 11)
transposed = [(x + transposition) % 12 for x in row]
for semi in transposed:
dur = random.choice([0.5, 1.0, 0.25]) if use_rhythm else 1.0
octv = random.randint(-1, 2)
piece.append((semi, octv, dur))
return piece
class RhythmEngine:
"""节奏引擎"""
def __init__(self):
self.patterns = []
def generate_polyrhythm(self, base_pulse, against_pulse, n_beats=4):
base_notes = []
against_notes = []
for i in range(base_pulse):
base_notes.append((i * n_beats / base_pulse, n_beats / base_pulse))
for i in range(against_pulse):
against_notes.append((i * n_beats / against_pulse, n_beats / against_pulse))
return {"基础": base_notes, "对位": against_notes}
def generate_syncopation(self, pattern, intensity=0.5):
syncopated = []
for dur in pattern:
if random.random() < intensity and dur >= 0.5:
syncopated.append(dur * 0.6)
syncopated.append(dur * 0.4)
else:
syncopated.append(dur)
return syncopated
def develop_rhythm(self, motif, technique="重复"):
if technique == "重复":
return motif * 2
elif technique == "增值":
return [d * 2 for d in motif]
elif technique == "减值":
return [d / 2 for d in motif]
elif technique == "逆行":
return list(reversed(motif))
elif technique == "交错":
return motif + [max(motif) – d + min(motif) for d in motif]
elif technique == "分裂":
result = []
for d in motif:
result.extend([d/2, d/2])
return result
return motif
def generate_groove(self, style_name="funk", n_bars=4):
grooves = {
"funk": [0.25, 0.25, 0.25, 0.25, 0.5, 0.5, 0.25, 0.25, 0.5],
"bossa": [0.5, 0.25, 0.25, 0.5, 0.25, 0.25, 0.5, 0.5],
"shuffle": [0.66, 0.33, 0.66, 0.33, 0.66, 0.33, 0.66, 0.33],
"latin": [0.25, 0.25, 0.5, 0.25, 0.25, 0.5, 0.25, 0.25, 0.25, 0.25],
"disco": [0.5, 0.5, 0.5, 0.5, 0.25, 0.25, 0.5, 0.5],
}
pattern = grooves.get(style_name, [0.5, 0.5, 0.5, 0.5])
return pattern * n_bars
class ChanceMusicEngine:
"""偶然音乐引擎"""
def __init__(self):
self.parameters = {}
def dice_composition(self, n_bars=8):
fragments = {
1: [(0, 0, 1.0), (2, 0, 1.0), (4, 0, 1.0), (5, 0, 1.0)],
2: [(0, 0, 0.5), (2, 0, 0.5), (4, 0, 1.0), (7, 0, 1.0), (5, 0, 1.0)],
3: [(4, 0, 1.0), (2, 0, 1.0), (0, 0, 2.0)],
4: [(0, 0, 0.5), (4, 0, 0.5), (7, 0, 0.5), (4, 0, 0.5), (5, 0, 1.0), (2, 0, 1.0)],
5: [(2, 0, 1.0), (4, 0, 0.5), (5, 0, 0.5), (7, 0, 1.0), (5, 0, 0.5), (4, 0, 0.5)],
6: [(7, 0, 0.5), (5, 0, 0.5), (4, 0, 1.0), (2, 0, 0.5), (0, 0, 0.5), (2, 0, 0.5), (4, 0, 0.5)],
}
result = []
for bar in range(n_bars):
dice = random.randint(1, 6)
fragment = fragments[dice]
result.extend(fragment)
return result
def aleatoric_piece(self, n_events=50, pitch_range=(48, 84)):
piece = []
for _ in range(n_events):
pitch = random.randint(*pitch_range)
dur = random.choice([0.25, 0.5, 1.0, 2.0, 0.125])
velocity = random.randint(30, 120)
piece.append((pitch % 12, pitch // 12 – 4, dur, velocity))
return piece
def cellular_automata_music(self, rule=30, n_generations=32):
cells = [0] * n_generations
cells[n_generations // 2] = 1
piece = []
for gen in range(n_generations):
active_cells = [i for i, c in enumerate(cells) if c == 1]
for ac in active_cells:
semi = ac % 12
octv = ac // 12
piece.append((semi, octv, 0.5))
new_cells = cells[:]
for i in range(1, n_generations – 1):
pattern = (cells[i-1], cells[i], cells[i+1])
new_cells[i] = self._apply_rule(rule, pattern)
cells = new_cells
return piece
def _apply_rule(self, rule, pattern):
idx = pattern[0] * 4 + pattern[1] * 2 + pattern[2]
return (rule >> idx) & 1
# ==================== 简谱工具函数 ====================
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)
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 melody_to_score(melody, bar_size=4.0):
"""旋律 → 标准简谱字符串 (带 | 分小节)"""
bar_lines = []
cur_bar = []
cur_dur = 0.0
for item in melody:
if len(item) >= 3:
semi, octv, dur = item[0], item[1], item[2]
else:
continue
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)
def multi_voice_to_score(voices_dict, bar_size=4.0):
"""多声部 → 简谱"""
lines = []
for name, melody in voices_dict.items():
score = melody_to_score(melody, bar_size)
lines.append(f"【{name}】{score}")
return "\\n".join(lines)
# ==================== 基础旋律生成 ====================
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)
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
# ==================== MIDI导出 ====================
def export_midi(tracks, bpm=120, filename="output.mid", time_signature=(4, 4)):
"""使用mido导出MIDI文件(支持多轨、多通道、完整Meta信息)
Args:
tracks: [(instrument_id, notes), …]
notes: [(pitch, duration, time_offset), …] 或 [(pitch, duration), …]
bpm: 速度
filename: 输出路径
time_signature: (分子, 分母),默认(4,4)
Returns:
(bool, str): (是否成功, 状态信息)
"""
try:
import mido
from mido import Message, MidiFile, MidiTrack, MetaMessage
except ImportError:
return False, "需要安装 mido 库: pip install mido"
try:
mid = MidiFile(type=1)
ticks_per_beat = mid.ticks_per_beat
# 控制轨:全局Meta信息
control_track = MidiTrack()
mid.tracks.append(control_track)
control_track.append(MetaMessage('time_signature',
numerator=time_signature[0],
denominator=time_signature[1],
clocks_per_click=24,
notated_32nd_notes_per_beat=8, time=0))
control_track.append(MetaMessage('set_tempo', tempo=mido.bpm2tempo(bpm), time=0))
control_track.append(MetaMessage('track_name', name='Control', time=0))
for track_idx, (inst_id, notes) in enumerate(tracks):
track = MidiTrack()
mid.tracks.append(track)
channel = track_idx % 16
track.append(MetaMessage('track_name', name=f'Track{track_idx+1}', time=0))
track.append(Message('program_change', program=inst_id % 128,
channel=channel, time=0))
events = []
current_time = 0
for note_data in notes:
if len(note_data) >= 3:
pitch, duration, time_offset = int(note_data[0]), float(note_data[1]), float(note_data[2])
else:
pitch, duration = int(note_data[0]), float(note_data[1])
time_offset = current_time
pitch = max(0, min(127, pitch))
start_tick = int(time_offset * ticks_per_beat)
end_tick = int((time_offset + duration) * ticks_per_beat)
events.append((start_tick, 'note_on', pitch, 100, channel))
events.append((end_tick, 'note_off', pitch, 0, channel))
current_time = time_offset + duration
events.sort(key=lambda x: x[0])
prev_tick = 0
for tick, evt_type, pitch, velocity, ch in events:
delta = max(0, tick – prev_tick)
if evt_type == 'note_on':
track.append(Message('note_on', note=pitch, velocity=velocity,
channel=ch, time=delta))
else:
track.append(Message('note_off', note=pitch, velocity=velocity,
channel=ch, time=delta))
prev_tick = tick
track.append(MetaMessage('end_of_track', time=0))
control_track.append(MetaMessage('end_of_track', time=0))
mid.save(filename)
return True, f"MIDI已导出: {filename} ({len(tracks)}轨, {bpm}BPM, 4/4拍)"
except Exception as e:
return False, f"导出失败: {str(e)}"
# ==================== 主应用 ====================
class CompositionApp:
"""作曲软件主应用 v3"""
COMPOSITION_TECHNIQUES = {
"旋律生成": "基于风格参数的随机旋律生成",
"对位法": "二声部/三声部对位、卡农、赋格",
"和声学": "四部和声、爵士和弦进行",
"曲式学": "二段式、三段式、回旋曲、变奏、奏鸣曲",
"配器法": "乐器分配、织体生成、音域控制",
"十二音": "序列主义作曲、音列变形",
"节奏技法": "复节奏、切分、律动生成",
"偶然音乐": "掷骰子作曲、元胞自动机",
}
def __init__(self, root):
self.root = root
root.title("作曲软件 v3 – 经典作曲技术整合")
screen_width = root.winfo_screenwidth()
if screen_width < 768:
root.attributes('-fullscreen', True)
self.font_size = 10
self.pad_size = 2
self.is_mobile = True
else:
root.geometry("900×750")
self.font_size = 10
self.pad_size = 4
self.is_mobile = False
self._build()
self._last_score = ""
self._current_melody = []
self._midi_tracks = []
def _build(self):
main_frame = ttk.Frame(self.root, padding=self.pad_size)
main_frame.pack(fill=tk.BOTH, expand=True)
top_frame = ttk.LabelFrame(main_frame, text="作曲参数", padding=5)
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=2)
self.cmb_key = ttk.Combobox(row1, values=list(KEY_OFFSET.keys()),
width=8, 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=2)
self.cmb_style = ttk.Combobox(row1, values=list(STYLES.keys()),
width=8, 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=2)
self.spn_bars = tk.Spinbox(row1, from_=4, to=64, width=4,
font=("", self.font_size))
self.spn_bars.delete(0, tk.END)
self.spn_bars.insert(0, "16")
self.spn_bars.pack(side=tk.LEFT, padx=1)
ttk.Label(row1, text="BPM:", font=("", self.font_size)).pack(side=tk.LEFT, padx=2)
self.spn_bpm = tk.Spinbox(row1, from_=30, to=300, width=4,
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)
row2 = ttk.Frame(top_frame)
row2.pack(fill=tk.X, pady=2)
ttk.Label(row2, text="作曲技术:", font=("", self.font_size)).pack(side=tk.LEFT, padx=2)
self.cmb_technique = ttk.Combobox(row2, values=list(self.COMPOSITION_TECHNIQUES.keys()),
width=10, font=("", self.font_size))
self.cmb_technique.current(0)
self.cmb_technique.pack(side=tk.LEFT, padx=1)
ttk.Label(row2, text="子类型:", font=("", self.font_size)).pack(side=tk.LEFT, padx=2)
self.cmb_sub = ttk.Combobox(row2, values=["默认"], width=10,
font=("", self.font_size))
self.cmb_sub.current(0)
self.cmb_sub.pack(side=tk.LEFT, padx=1)
ttk.Label(row2, text="参数:", font=("", self.font_size)).pack(side=tk.LEFT, padx=2)
self.cmb_param = ttk.Combobox(row2, values=["默认"], width=8,
font=("", self.font_size))
self.cmb_param.current(0)
self.cmb_param.pack(side=tk.LEFT, padx=1)
self.cmb_technique.bind("<<ComboboxSelected>>", self._update_subtypes)
btn_frame = ttk.Frame(top_frame)
btn_frame.pack(fill=tk.X, pady=3)
self.btn_generate = ttk.Button(btn_frame, text="生成作品",
command=self._do_async, width=10)
self.btn_generate.pack(side=tk.LEFT, padx=3)
ttk.Button(btn_frame, text="再来一首",
command=self._do_again, width=10).pack(side=tk.LEFT, padx=3)
ttk.Button(btn_frame, text="保存简谱",
command=self._save, width=10).pack(side=tk.LEFT, padx=3)
ttk.Button(btn_frame, text="复制",
command=self._copy, width=8).pack(side=tk.LEFT, padx=3)
ttk.Button(btn_frame, text="导出MIDI",
command=self._export_midi, width=10).pack(side=tk.LEFT, padx=3)
self.lbl_status = ttk.Label(btn_frame, text="就绪",
foreground="green", font=("", self.font_size))
self.lbl_status.pack(side=tk.LEFT, padx=5)
txt_height = 20 if self.is_mobile else 25
self.txt = scrolledtext.ScrolledText(main_frame,
font=("Consolas", 13),
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 = "作曲技术: 旋律|对位|和声|曲式|配器|十二音|节奏|偶然音乐 | 输出: 多声部简谱 + MIDI"
ttk.Label(bot, text=tip, foreground="gray",
font=("", self.font_size – 1 if self.is_mobile else self.font_size),
wraplength=800).pack(side=tk.LEFT, padx=4)
def _update_subtypes(self, event=None):
tech = self.cmb_technique.get()
subtypes = {
"旋律生成": ["单旋律", "多声部旋律"],
"对位法": ["二声部对位", "三声部对位", "卡农", "赋格主题"],
"和声学": ["四部和声", "爵士和弦", "功能和声进行"],
"曲式学": ["二段式", "三段式", "回旋曲", "变奏曲", "奏鸣曲式"],
"配器法": ["弦乐四重奏", "钢琴三重奏", "管弦乐队", "流行乐队", "织体生成"],
"十二音": ["基础序列", "完整作品", "音列矩阵"],
"节奏技法": ["复节奏", "切分节奏", "律动生成", "节奏动机发展"],
"偶然音乐": ["掷骰子作曲", "完全随机", "元胞自动机"],
}
self.cmb_sub['values'] = subtypes.get(tech, ["默认"])
self.cmb_sub.current(0)
params = {
"对位法": ["一音对一音", "二音对一音", "华彩对位"],
"曲式学": ["8小节", "16小节", "32小节"],
"配器法": ["主调织体", "复调织体", "分解和弦", "持续音"],
"十二音": ["带节奏", "纯音列", "矩阵展示"],
"节奏技法": ["3对2", "4对3", "5对4", "切分", "Shuffle", "Funk", "Bossa"],
}
self.cmb_param['values'] = params.get(tech, ["默认"])
self.cmb_param.current(0)
def _do_async(self):
self.btn_generate.config(state=tk.DISABLED)
self.lbl_status.config(text="生成中…", foreground="orange")
Thread(target=self._do, daemon=True).start()
def _do(self):
try:
key = self.cmb_key.get()
style_name = self.cmb_style.get()
technique = self.cmb_technique.get()
subtype = self.cmb_sub.get()
param = self.cmb_param.get()
try:
bars = int(self.spn_bars.get())
except:
bars = 16
try:
bpm = int(self.spn_bpm.get())
except:
bpm = 120
style = STYLES.get(style_name, STYLES["抒情"])
key_offset = KEY_OFFSET.get(key, 0)
if key.endswith("小调"):
scale = MINOR_SCALE
elif style_name in ("平静", "古风"):
scale = PENTATONIC_MAJOR
elif style_name == "蓝调":
scale = BLUES_SCALE
elif style_name == "全音阶":
scale = WHOLE_TONE
elif style_name == "八声音阶":
scale = OCTATONIC
else:
scale = MAJOR_SCALE
oct_lo, oct_hi = style["oct_range"]
melody = gen_melody(bars, style, oct_lo, oct_hi, style["step_pref"])
self._current_melody = melody
result = self._apply_technique(technique, subtype, param, melody,
key_offset, scale, style, bars)
header = f"【{technique} – {subtype}】 | 风格: {style_name}\\n"
header += "=" * 50 + "\\n"
header += f"[0,{bpm},{key}]\\n"
full = header + result
self._last_score = full
self.root.after(0, self._render, full)
except Exception as e:
self.root.after(0, self._render, f"错误: {str(e)}")
def _melody_to_midi_notes(self, melody):
"""将旋律数据转为MIDI音符列表 [(pitch, duration, time_offset), …]"""
notes = []
time = 0
for item in melody:
if len(item) >= 3:
semi, octv, dur = item[0], item[1], item[2]
else:
continue
pitch = (4 + octv) * 12 + semi
notes.append((pitch, dur, time))
time += dur
return notes
def _build_tracks_from_voices(self, voices_dict, instrument_map=None):
"""从多声部字典构建MIDI轨道列表"""
defaults = [0, 32, 40, 48, 56, 60, 73, 80]
tracks = []
for i, (name, melody) in enumerate(voices_dict.items()):
inst = instrument_map.get(name, defaults[i % len(defaults)]) if instrument_map else defaults[i % len(defaults)]
tracks.append((inst, self._melody_to_midi_notes(melody)))
return tracks
def _apply_technique(self, technique, subtype, param, melody, key_offset, scale, style, bars):
"""应用选定的作曲技术,同时保存MIDI多轨数据"""
self._midi_tracks = []
if technique == "旋律生成":
self._midi_tracks = [(0, self._melody_to_midi_notes(melody))]
if subtype == "单旋律":
return melody_to_score(melody)
else:
melody2 = gen_melody(bars, style, -1, 1, style["step_pref"])
self._midi_tracks = [
(0, self._melody_to_midi_notes(melody)),
(48, self._melody_to_midi_notes(melody2)),
]
return multi_voice_to_score({"旋律1": melody, "旋律2": melody2})
elif technique == "对位法":
cp = CounterpointEngine(key_offset, scale)
if subtype == "二声部对位":
cantus = [semi + (4 + octv) * 12 for semi, octv, dur in melody]
counter = cp.generate_counterpoint(cantus, "above", 1)
counter_melody = [(c % 12, c // 12 – 4, 1.0) for c in counter[:len(melody)]]
self._midi_tracks = [
(0, self._melody_to_midi_notes(melody)),
(40, self._melody_to_midi_notes(counter_melody)),
]
return multi_voice_to_score({"定旋律": melody, "对位声部": counter_melody})
elif subtype == "三声部对位":
cantus = [semi + (4 + octv) * 12 for semi, octv, dur in melody]
counter1 = cp.generate_counterpoint(cantus, "above", 1)
counter2 = cp.generate_counterpoint(cantus, "below", 1)
c1 = [(c % 12, c // 12 – 4, 1.0) for c in counter1[:len(melody)]]
c2 = [(c % 12, c // 12 – 4, 1.0) for c in counter2[:len(melody)]]
self._midi_tracks = [
(40, self._melody_to_midi_notes(c1)),
(0, self._melody_to_midi_notes(melody)),
(32, self._melody_to_midi_notes(c2)),
]
return multi_voice_to_score({"高音": c1, "中音": melody, "低音": c2})
elif subtype == "卡农":
cantus = [semi + (4 + octv) * 12 for semi, octv, dur in melody[:16]]
voices = cp.generate_canon(cantus, interval=7, delay=2)
v1 = [(v % 12, v // 12 – 4, 1.0) for v in voices[0]]
v2 = [(v % 12, v // 12 – 4, 1.0) for v in voices[1]]
self._midi_tracks = [
(0, self._melody_to_midi_notes(v1)),
(48, self._melody_to_midi_notes(v2)),
]
header = "【卡农 — 八度模仿,延迟2拍】"
return header + "\\n" + multi_voice_to_score({"先导": v1, "模仿": v2})
elif subtype == "赋格主题":
subject = cp.generate_fugue_subject(scale, length=12)
subject_melody = [(s % 12, 0, 1.0) for s in subject]
self._midi_tracks = [(0, self._melody_to_midi_notes(subject_melody))]
return "【赋格主题】\\n" + melody_to_score(subject_melody) + "\\n\\n"
elif technique == "和声学":
he = HarmonyEngine(key_offset, scale)
if subtype == "四部和声":
progression = random.choice(list(he.PROGRESSIONS.keys()))
voices = he.generate_four_part(progression, n_bars=min(bars, 8))
result = "【四部和声 — 和弦进行: " + progression + "】\\n"
inst_map = {'S': 0, 'A': 48, 'T': 56, 'B': 32}
tracks = []
for v_name in ['S', 'A', 'T', 'B']:
voice_melody = [(p % 12, p // 12 – 4, 1.0) for p in voices[v_name]]
tracks.append((inst_map[v_name], self._melody_to_midi_notes(voice_melody)))
result += "【" + v_name + "】" + melody_to_score(voice_melody) + "\\n"
self._midi_tracks = tracks
return result
elif subtype == "爵士和弦":
changes = he.generate_jazz_changes(n_bars=min(bars, 8))
result = "【爵士和弦进行】\\n"
for i, ch in enumerate(changes):
result += "第" + str(i+1) + "小节: " + ch + " "
if (i + 1) % 4 == 0:
result += "\\n"
self._midi_tracks = [(0, self._melody_to_midi_notes(melody))]
return result
elif subtype == "功能和声进行":
progression = random.choice(list(he.PROGRESSIONS.keys()))
prog = he.PROGRESSIONS[progression]
result = "【功能和声 — " + progression + "】\\n"
result += " → ".join(prog) + "\\n"
chord_melody = []
for chord_name in prog * (bars // len(prog) + 1):
root = he.CHORD_TYPES[chord_name][0]
chord_melody.append(((root + key_offset) % 12, 0, 1.0))
self._midi_tracks = [(0, self._melody_to_midi_notes(chord_melody[:bars]))]
result += "\\n根音进行: " + melody_to_score(chord_melody[:bars])
return result
elif technique == "曲式学":
fe = FormEngine()
if subtype == "二段式":
form = fe.generate_binary(melody, n_bars_a=bars//2, n_bars_b=bars//2)
self._midi_tracks = self._build_tracks_from_voices(form)
return "【二段式 A-B】\\n" + multi_voice_to_score(form)
elif subtype == "三段式":
form = fe.generate_ternary(melody, n_bars=bars//3)
self._midi_tracks = self._build_tracks_from_voices(form)
return "【三段式 A-B-A'】\\n" + multi_voice_to_score(form)
elif subtype == "回旋曲":
form = fe.generate_rondo(melody, episodes=2)
self._midi_tracks = self._build_tracks_from_voices(form)
return "【回旋曲 A-B-A-C-A】\\n" + multi_voice_to_score(form)
elif subtype == "变奏曲":
n_var = 3 if param == "默认" else int(param[0]) if param[0].isdigit() else 3
form = fe.generate_variations(melody, n_variations=n_var)
self._midi_tracks = self._build_tracks_from_voices(form)
return "【变奏曲】\\n" + multi_voice_to_score(form)
elif subtype == "奏鸣曲式":
melody2 = gen_melody(bars, style, -1, 2, style["step_pref"])
form = fe.generate_sonata(melody, melody2, n_bars=bars)
result = "【奏鸣曲式】\\n"
all_tracks = []
defaults = [0, 48, 40, 56]
idx = 0
for section_name, section in form["呈示部"].items():
result += "— " + section_name + " —\\n"
result += "【" + section_name + "】" + melody_to_score(section) + "\\n"
all_tracks.append((defaults[idx % len(defaults)], self._melody_to_midi_notes(section)))
idx += 1
result += "\\n— 展开部 —\\n"
result += melody_to_score(form["展开部"]) + "\\n"
all_tracks.append((defaults[idx % len(defaults)], self._melody_to_midi_notes(form["展开部"])))
idx += 1
for section_name, section in form["再现部"].items():
result += "— " + section_name + " —\\n"
result += "【" + section_name + "】" + melody_to_score(section) + "\\n"
all_tracks.append((defaults[idx % len(defaults)], self._melody_to_midi_notes(section)))
idx += 1
self._midi_tracks = all_tracks
return result
elif technique == "配器法":
oe = OrchestrationEngine()
if subtype in ["弦乐四重奏", "钢琴三重奏", "管弦乐队", "流行乐队"]:
tracks_data = oe.assign_instruments(melody, subtype)
result = "【" + subtype + "】\\n"
midi_tracks = []
for i, track in enumerate(tracks_data):
track_melody = [(t[0], t[1], t[2]) for t in track]
inst_name = track[0][4] if track else "未知"
inst_id = track[0][3] if track else 0
result += "【" + inst_name + "】" + melody_to_score(track_melody) + "\\n"
midi_tracks.append((inst_id, self._melody_to_midi_notes(track_melody)))
self._midi_tracks = midi_tracks
return result
elif subtype == "织体生成":
texture_type = param if param in ["主调", "复调", "分解和弦", "持续音"] else "主调"
texture = oe.generate_texture(melody, texture_type)
result = "【" + texture_type + "织体】\\n"
midi_tracks = []
defaults = [0, 48, 40, 32]
idx = 0
for name, content in texture.items():
if name == "伴奏":
result += "【" + name + "】(和弦伴奏)\\n"
if content and isinstance(content[0], list):
flat = []
for chord_notes in content:
if chord_notes:
flat.extend(chord_notes)
midi_tracks.append((defaults[idx % len(defaults)], self._melody_to_midi_notes(flat)))
else:
midi_tracks.append((defaults[idx % len(defaults)], self._melody_to_midi_notes(content)))
else:
result += "【" + name + "】" + melody_to_score(content) + "\\n"
midi_tracks.append((defaults[idx % len(defaults)], self._melody_to_midi_notes(content)))
idx += 1
self._midi_tracks = midi_tracks
return result
elif technique == "十二音":
tte = TwelveToneEngine()
if subtype == "基础序列":
row = tte.generate_row()
self._midi_tracks = [(0, self._melody_to_midi_notes([(r, 0, 0.5) for r in row]))]
return "【十二音基础序列】\\n" + " ".join(str(x) for x in row) + "\\n"
elif subtype == "完整作品":
piece = tte.generate_piece(n_notes=min(bars * 4, 48))
transforms = tte.get_transformations()
result = "【十二音技法作品】\\n"
result += "P: " + " ".join(str(x) for x in transforms['P']) + "\\n"
result += "I: " + " ".join(str(x) for x in transforms['I']) + "\\n"
result += "R: " + " ".join(str(x) for x in transforms['R']) + "\\n"
result += "RI: " + " ".join(str(x) for x in transforms['RI']) + "\\n\\n"
result += melody_to_score(piece)
self._midi_tracks = [(0, self._melody_to_midi_notes(piece))]
return result
elif subtype == "音列矩阵":
tte.generate_row()
matrix = tte.build_matrix()
result = "【十二音音列矩阵】\\n"
result += " " + " ".join(f"{x:2d}" for x in range(12)) + "\\n"
result += " " + "-" * 37 + "\\n"
for i, row in enumerate(matrix):
result += f"{i:2d}" + " | " + " ".join(f"{x:2d}" for x in row) + "\\n"
self._midi_tracks = [(0, self._melody_to_midi_notes([(r, 0, 0.5) for r in tte.row]))]
return result
elif technique == "节奏技法":
re = RhythmEngine()
if subtype == "复节奏":
if "对" in param:
parts = param.split("对")
p1, p2 = int(parts[0]), int(parts[1])
else:
p1, p2 = 3, 2
poly = re.generate_polyrhythm(p1, p2, n_beats=4)
result = "【复节奏 " + str(p1) + "对" + str(p2) + "】\\n"
result += "基础拍: " + " | ".join([f"{n:.2f}" for n, d in poly["基础"]]) + "\\n"
result += "对位拍: " + " | ".join([f"{n:.2f}" for n, d in poly["对位"]]) + "\\n"
self._midi_tracks = [(0, self._melody_to_midi_notes(melody))]
return result
elif subtype == "切分节奏":
base_pattern = [1.0, 1.0, 1.0, 1.0]
syncopated = re.generate_syncopation(base_pattern, intensity=0.6)
result = "【切分节奏】\\n"
result += "原始: " + " | ".join([f"{d:.2f}" for d in base_pattern]) + "\\n"
result += "切分: " + " | ".join([f"{d:.2f}" for d in syncopated]) + "\\n"
sync_melody = []
for i, (semi, octv, _) in enumerate(melody[:len(syncopated)]):
sync_melody.append((semi, octv, syncopated[i]))
result += "\\n旋律: " + melody_to_score(sync_melody)
self._midi_tracks = [(0, self._melody_to_midi_notes(sync_melody))]
return result
elif subtype == "律动生成":
groove_style = param.lower() if param != "默认" else "funk"
groove = re.generate_groove(groove_style, n_bars=bars // 4)
result = "【" + groove_style.title() + " 律动】\\n"
result += "节奏型: " + " ".join([f"{d:.2f}" for d in groove[:16]]) + "…\\n"
self._midi_tracks = [(0, self._melody_to_midi_notes(melody))]
return result
elif subtype == "节奏动机发展":
motif = [1.0, 0.5, 0.5]
techniques = ["重复", "增值", "减值", "逆行", "分裂"]
result = "【节奏动机发展】\\n"
result += "原始动机: " + " | ".join([str(d) for d in motif]) + "\\n\\n"
for tech in techniques:
developed = re.develop_rhythm(motif, tech)
result += tech + ": " + " | ".join([f"{d:.2f}" for d in developed]) + "\\n"
self._midi_tracks = [(0, self._melody_to_midi_notes(melody))]
return result
elif technique == "偶然音乐":
cme = ChanceMusicEngine()
if subtype == "掷骰子作曲":
piece = cme.dice_composition(n_bars=min(bars, 8))
result = "【掷骰子作曲(莫扎特风格)】\\n"
result += melody_to_score(piece)
self._midi_tracks = [(0, self._melody_to_midi_notes(piece))]
return result
elif subtype == "完全随机":
piece = cme.aleatoric_piece(n_events=min(bars * 4, 50))
result = "【完全随机偶然音乐】\\n"
result += melody_to_score([(p[0], p[1], p[2]) for p in piece])
self._midi_tracks = [(0, self._melody_to_midi_notes([(p[0], p[1], p[2]) for p in piece]))]
return result
elif subtype == "元胞自动机":
piece = cme.cellular_automata_music(rule=30, n_generations=32)
result = "【元胞自动机音乐 (Rule 30)】\\n"
result += melody_to_score(piece)
self._midi_tracks = [(0, self._melody_to_midi_notes(piece))]
return result
self._midi_tracks = [(0, self._melody_to_midi_notes(melody))]
return melody_to_score(melody)
def _render(self, text): return melody_to_score(melody)
def _render(self, text):
self.txt.delete("1.0", tk.END)
self.txt.insert(tk.END, text)
n_lines = text.count('\\n') + 1
self.lbl_status.config(text=f"✓ 完成 ({n_lines} 行)", foreground="green")
self.btn_generate.config(state=tk.NORMAL)
def _do_again(self):
self._do_async()
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")
def _export_midi(self):
if not self._current_melody:
messagebox.showinfo("提示", "请先生成旋律")
return
path = filedialog.asksaveasfilename(defaultextension=".mid",
filetypes=[("MIDI文件", "*.mid")])
if not path:
return
try:
bpm = int(self.spn_bpm.get())
except:
bpm = 120
# 优先使用已保存的多轨数据,支持多声部导出
if self._midi_tracks and len(self._midi_tracks) > 0:
tracks = self._midi_tracks
else:
notes = []
time = 0
for semi, octv, dur in self._current_melody:
pitch = (4 + octv) * 12 + semi
notes.append((pitch, dur, time))
time += dur
tracks = [(0, notes)]
success, msg = export_midi(tracks, bpm, path)
if success:
self.lbl_status.config(text=msg, foreground="green")
messagebox.showinfo("导出成功", msg)
else:
self.lbl_status.config(text=msg, foreground="red")
messagebox.showerror("导出失败", msg)
if __name__ == "__main__":
root = tk.Tk()
app = CompositionApp(root)
root.mainloop()
网硕互联帮助中心


评论前必须登录!
注册