20241307 实验四 《Python程序设计》实验报告
20241307 2024-2025-2 《Python程序设计》实验四报告
课程:《Python程序设计》
班级: 2413
姓名: 张岳峰
学号:20241307
实验教师:王志强
实验日期:2025年5月14日
必修/选修: 公选课
一、实验内容
(一)项目概述
这是一个基于 PyQt 界面的虚拟键盘和马尔科夫链预测模型,根据导入的呃数据集训练LSTM模型的和弦预测工具,包含界面布局、事件处理、和弦预测逻辑等功能模块的和弦预测系统。
(二)实验功能
1.和弦检测功能
(1)基础和弦识别
支持识别 C Major、G Major、F Major、A Minor、D Major 等常见三和弦
通过音符集合匹配和弦结构(如 {C, E, G} 对应 C Major)。
(2)实时检测能力
模拟 MIDI 输入(测试按钮)或扩展链接 MIDI 键盘,实时分析按住的音符,输出当前和弦。
2.和弦预测功能
(1)马尔可夫链预测
支持 一阶、二阶、三阶 马尔科夫链模型切换,学习历史和弦序列的转移规律。
输出预测和弦及概率(如 “下一个和弦:G Major,概率 67.2%”)。
(2)多风格扩展潜力
可加载流行、爵士、古典等风格的和弦数据集,适配不同音乐类型的预测逻辑模型。
3.PyQt界面交互
(1)界面交互
测试按钮:模拟弹奏 C、G、F 等和弦,进行检测与预测。
序列管理:实时显示最近 12 条和弦,点击可删除历史记录。
风格切换:支持选择音乐风格(默认、流行、爵士等),影响预测模型和预测结果。
4.数据管理功能
(1)和弦序列保存 / 加载
支持将当前和弦序列保存为 JSON 文件(含风格、阶数、序列)。
加载历史保存过的序列
数据集支持
预设流行、爵士、古典风格的和弦数据集,可扩展自定义数据集训练模型。
5.程序可扩展功能
(1)在学习的过程中,我学习到了智齿MIDI的扩展接口,只要接入rtmidi库搭建环境,就能进行和弦的实时检测与预测功能
实现代码如下:
import rtmidi
midi_in = rtmidi.MidiIn()
ports = midi_in.get_ports()
if ports:
midi_in.open_port(0) # 连接第一个 MIDI 设备
(2)可以扩展更多的音乐类型数据集,如dorian、rock等等,实现音乐类型内更精准的预测
(3)在交互界面概率输出时,引入预测概率柱状图等可视化模块
(4)综合马尔科夫链模型、向量匹配算法、LSTM模型、Transformer模型的实时匹配综合预测系统(我们在项目中实现了,但是我还没有学会,具体内容且看下文)
基于以上内容,我在我目前的代码基础上实现了一个较为完善的基于 PyQt 的虚拟键盘和弦预测工具开发的和弦预测系统。
二、实验过程
1.实验思路
作为一个资深的吉他爱好者,我喜欢弹电吉他,对音乐创作也有着喜爱和热情。有幸在乐队碰到了一群志同道合的学长,我们一同商讨开发基于马尔科夫链向量匹配算法的和弦预测系统。得知我们想用Python来实现开发,我毫不犹豫地选择的了王志强老师的Python课程,也打开了我Python编程的大门。在项目研发中,我负责算法的解读和实现过程的详细讲解,学长们在之前也选择了志强老师的Python课程,这一次由大三有丰富编程经验的学长负责代码的编写。也正是在这次项目的开发中,我接触到了马尔科夫链向量匹配算法、LSTM、Transformer模型,并且啃资料学习研读,试图更加清晰的解读代码(真的很难,学不明白)。与此同时,我也一步一步的进行我的Python学习,我也希望有一天我有能力实现代码的编写。很快我的Python结课了,这一次我想去自己独立在自己目前的能力下实现这个程序的基本功能。由于心中对整个程序的架构和算法很熟悉,我展开对细节的学习便开始了我的程序开发。
2.项目结构
├── main.py # 主程序,这是程序的入口,包含PyQt5主界面和交互功能
├── musicutils.py # 和弦检测工具,负责识别和弦和加载不同音乐风格的数据集
├── chord_predictor.py # 预测模型(基于马尔可夫链的预测模型)
├── datasets/ # 音乐风格数据集
│ ├── pop.json # 流行音乐和弦序列
│ ├── jazz.json # 爵士乐和弦序列
│ └── classical.json # 古典音乐和弦序列(这里还可以扩展多个音乐风格的数据集)
└── saved_sequences/ # 保存的和弦序列
3.实验准备
(一)环境搭建
pip install pyqt5 # PyQ5交互界面
pip install numpy # 概率计算
搭建成功:

4.实验流程及代码展示
(一)项目架构构建
创建和弦预测项目,根据项目结构,在根目录下创建python代码及和弦保存目录文件

(二)分布构建程序
1.musicutils.py:和弦检测与音乐风格数据集加载
(1)和弦检测ChordDetector函数
class ChordDetector:
def getNormalChord(self):
# 通过音符集合匹配和弦类型(如 {C,E,G} → "C Major")
note_names = {n.split('_')[0] for n in self.notes}
if note_names == {'C','E','G'}: return "C Major"
# ... 其他和弦同理(G Major、F Major 等)
功能为:解析输入音符(如 ["C_4", "E_4", "G_4"])并识别对应的和弦名称。
(2)数据集load_music_style函数
def load_music_style(style_name):
# 从 datasets/ 加载 JSON 格式的和弦序列(如 pop.json)
dataset_path = os.path.join("datasets", f"{style_name}.json")
return json.load(f) if os.path.exists(dataset_path) else []
功能为:为预测模型提供训练数据,目前我在datasets目录下创建了 “流行、爵士、古典” 三种风格如下,还可以引入更多的音乐风格训练数据
流行(pop):
[
["C Major", "G Major", "Am", "F Major"],
["D Major", "G Major", "Bm", "A Major"],
["G Major", "D Major", "Em", "C Major"],
["A Minor", "F Major", "C Major", "G Major"]
]
爵士(jazz):
[
["Cmaj7", "G7", "Cmaj7", "Fmaj7"],
["Dm7", "G7", "Cmaj7", "Am7"],
["Gmaj7", "C7", "Fmaj7", "Bm7-5"],
["Fmaj7", "Bm7-5", "E7", "Am7"]
]
古典(classical):
[
["C Major", "G Major", "Am", "E7", "Am"],
["G Major", "Em", "C Major", "D7", "G Major"],
["F Major", "Dm", "G7", "C Major"],
["D Minor", "G Minor", "C Major", "Bdim", "C Major"]
]
2.chord_predictor.py:和弦预测模型
(1)基础的马尔科夫链模型预测器(ChordPredictor):
马尔可夫链模型,我们在处理不同长度的序列依赖时考虑了当前状态和前 n-1 个状态之间的关系,使得模型能够更好地捕捉系统状态之间的长程依赖关系。具体来说,n 阶马尔可夫链模型考虑了系统在每个时刻的状态,以及前 n-1 个时刻的状态序列,称为历史。通过考虑历史信息,该模型可以更准确地估计系统在给定当前状态下的下一个状态。
这是当时我写的项目计划书查询的资料内容,但是马尔科夫链模型对于短序列依赖的处理更具优势,用人话来讲,在和弦预测中,他比较适合相邻和弦的预测,他考虑的范围比较近,对于距离较远的和弦对于后续和弦的影响计算没有后面引入的LSTM模型更有优势。
class ChordPredictor:
def build_model(self):
# 用马尔可夫链统计和弦转移次数(如 "C Major" → "G Major" 的概率)
for seq in self.chord_sequences:
for i in range(len(seq)-self.order):
current_state = tuple(seq[i:i+self.order])
next_chord = seq[i+self.order]
self.transition_counts[current_state][next_chord] += 1
def predict_chord(self, current_chords):
# 根据历史和弦序列,预测下一个和弦及概率
current_state = tuple(current_chords[-self.order:])
next_chords = self.transition_counts.get(current_state, {})
predicted_chord = max(next_chords, key=next_chords.get)
probability = next_chords[predicted_chord]/sum(next_chords.values())
return (predicted_chord, probability)
功能为:用马尔可夫链学习和弦转移规律(一阶 / 二阶 / 三阶),输出预测结果
(2)音乐类型数据集训练后的混合风格预测器 MultiStylePredictor:
def add_style(self, style_name, chord_sequences, weight=1.0):
# 为每种风格(流行、爵士)创建独立预测器,并设置占比权重
self.style_predictors[style_name] = ChordPredictor(...)
self.style_weights[style_name] = weight
def predict_chord(self, current_chords):
# 综合所有风格的预测结果(加权平均)
predictions = {}
for style, predictor in self.style_predictors.items():
predicted, prob = predictor.predict_chord(current_chords)
predictions[predicted] += prob * self.style_weights[style]
# 同意结果,取概率最高的和弦并输出概率
predicted_chord = max(predictions, key=predictions.get)
return (predicted_chord, predictions[predicted_chord])
功能为:这里再不选则特定数据集时,在默认预测模型中可以设置不同数据集的权重,来混和多种数据集考虑预测和弦
(3)引入LSTM比较简单情况下的算法(简略版),可以考虑距离较远的和弦对于下一和弦的影响(下文中的长序列依赖就是和个意思),学到的LSTM模型运算机理如下:LSTM 能够捕捉到相隔较远的和弦和音符之间的依赖关系。例如在一些古典音乐的乐章中,后续的和弦可能与前面一个乃至多个和弦相关联,LSTM 的记忆单元可以保留这些关键的历史信息并记忆,使得模型能够根据更丰富的上下文的和弦关联来预测下一个和弦。在数学表达上,输入门决定了多少新输入的信息会被添加到记忆单元中,遗忘门控制着记忆单元中旧信息的保留,输出门则决定了记忆单元中的哪些信息会被输出到下一个时间步。在乐曲创作中,将尽量长的音符串加入输入门的记忆单元中,由遗忘门将乐曲的音符和和弦加以保留,用输出门将经过马尔科夫链和向量匹配算法分析预测的和弦及音符信息输出,从而实现长字符串的和弦预测,即处理相隔较远的和弦音符的依赖关系,即处理和弦预测中的长系列依赖。
代码如下:
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
def build_lstm_model(input_shape, num_chords):
model = Sequential()
model.add(LSTM(64, input_shape=input_shape, return_sequences=True))
model.add(LSTM(32))
model.add(Dense(num_chords, activation="softmax"))
model.compile(loss="categorical_crossentropy", optimizer="adam")
return model
功能:用 LSTM替代马尔可夫链,学习更长的和弦依赖关系
3.main.py:界面交互与函数调度来实现程序
那么下面来到了主程序,这里有着PyQt5界面设计和上面两个模块的调度,还有交互功能
一步一步来
(一)首先是,界面设计,包含以下六个小板块:
ef initUI(self):
# 构建界面组件:
# 1. 和弦序列显示区(12 个按钮,展示历史和弦)
# 2. 音乐风格选择(下拉菜单:default/pop/jazz/classical)
# 3. 预测阶数选择(单选按钮:一阶/二阶/三阶)
# 4. 状态显示(当前和弦、预测和弦)
# 5. 测试按钮(C/G/F/A Minor/D Major)
# 6. 序列管理(保存、加载、删除)
分布代码如下:
(1)和弦显示区域
ch_title = QLabel(f"实时记录最新{self.MAX_QUEUE}条和弦")
self.vbox.addWidget(ch_title)
self.chords_queue = QHBoxLayout()
self.chord_btns = []
for i in range(self.MAX_QUEUE):
btn = QPushButton("")
btn.setMinimumHeight(40)
btn.clicked.connect(lambda _, idx=i: self.on_chord_clicked(idx))
self.chords_queue.addWidget(btn)
self.chord_btns.append(btn)
self.vbox.addLayout(self.chords_queue)
(2) 音乐风格选择区域
style_layout = QHBoxLayout()
self.style_label = QLabel("选择音乐风格:")
self.style_combo = QComboBox()
self.style_combo.addItems(self.available_styles)
self.style_combo.currentIndexChanged.connect(self.on_style_changed)
# 风格权重选择,三个单选(pop、jazz、classical)
style_layout.addWidget(self.style_label)
style_layout.addWidget(self.style_combo)
self.vbox.addLayout(style_layout)
(3)预测阶数选择区域(三个单选,三阶马尔科夫链预测模型)
radio_layout = QHBoxLayout()
self.radio1 = QRadioButton("一阶")
self.radio2 = QRadioButton("二阶")
self.radio3 = QRadioButton("三阶")
self.radio2.setChecked(True)
radio_layout.addWidget(QLabel("预测模型阶数:"))
radio_layout.addWidget(self.radio1)
radio_layout.addWidget(self.radio2)
radio_layout.addWidget(self.radio3)
self.vbox.addLayout(radio_layout)
(4)状态显示(当前检测到什么和弦、预测到的下一个和弦,chord_predictor.py模型里能显示概率)
radio_layout = QHBoxLayout()
self.radio1 = QRadioButton("一阶")
self.radio2 = QRadioButton("二阶")
self.radio3 = QRadioButton("三阶")
self.radio2.setChecked(True)
radio_layout.addWidget(QLabel("预测模型阶数:"))
radio_layout.addWidget(self.radio1)
radio_layout.addWidget(self.radio2)
radio_layout.addWidget(self.radio3)
self.vbox.addLayout(radio_layout)
(5)和弦测试按钮(在没有接入MIDI键盘时,可以通过虚拟键盘输入和弦序列,这个部分是我的虚拟键盘)
test_layout = QHBoxLayout()
self.test_c_btn = QPushButton("测试C和弦")
self.test_g_btn = QPushButton("测试G和弦")
self.test_f_btn = QPushButton("测试F和弦")
self.test_a_btn = QPushButton("测试A Minor") # 新增和弦
self.test_d_btn = QPushButton("测试D Major") # 新增和弦
self.test_c_btn.clicked.connect(lambda: self.test_chord("C"))
self.test_g_btn.clicked.connect(lambda: self.test_chord("G"))
self.test_f_btn.clicked.connect(lambda: self.test_chord("F"))
self.test_a_btn.clicked.connect(lambda: self.test_chord("A"))
self.test_d_btn.clicked.connect(lambda: self.test_chord("D"))
test_layout.addWidget(self.test_c_btn)
test_layout.addWidget(self.test_g_btn)
test_layout.addWidget(self.test_f_btn)
test_layout.addWidget(self.test_a_btn)
test_layout.addWidget(self.test_d_btn)
self.vbox.addLayout(test_layout)
(6)和弦序列保存与读出按钮(文件保存让我学明白了,跟c语言和Linux操作一样一样的)
load_delete_layout = QHBoxLayout()
self.load_btn = QPushButton("加载选中序列")
self.delete_btn = QPushButton("删除选中序列")
self.load_btn.clicked.connect(self.load_sequence)
self.delete_btn.clicked.connect(self.delete_sequence)
load_delete_layout.addWidget(self.load_btn)
load_delete_layout.addWidget(self.delete_btn)
self.vbox.addLayout(load_delete_layout)
(二)和弦检测与预测
这部分调用前两个模块实现和弦预测
def test_chord(self, chord_type):
# 模拟按下和弦(如 "C" → 生成音符 ["C_4","E_4","G_4"])
notes = ["C_4","E_4","G_4"] if chord_type=="C" else ...
self.detect_current_chord(notes) # 调用 musicutils 识别和弦
QTimer.singleShot(2000, self.release_chord) # 模拟松开按键
def release_chord(self):
# 将识别的和弦加入历史队列,更新预测结果
self.QUEUE.append(self.PRE_CHORD)
self.update_prediction() # 调用 chord_predictor 预测下一个和弦
预测流程为:按下按钮测试 → 和弦识别 → 加入队列 → 预测下一和弦
(三)和弦记录管理,就是和弦序列(保存 / 加载 / 删除)
def save_sequence(self):
# 将当前和弦记录、风格、阶数等保存为 JSON 文件
sequence_data = {
"name": name,
"style": self.current_style,
"order": order,
"chords": self.QUEUE.copy(),
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
with open(file_path, "w") as f:
json.dump(sequence_data, f)
def load_sequence(self):
# 加载 JSON 文件,还原和弦队列、风格、阶数
with open(file_path, "r") as f:
data = json.load(f)
self.QUEUE = data["chords"]
self.current_style = data["style"]
# 更新界面和预测模型
self.update_chords_display()
self.update_prediction()
这一部分将之前预测和检测输入的和弦结果保存,以便后续的读入继续创作,可以在操作界面对整条和弦记录命名、保存、删除
5.代码展示
展示一下全部代码吧
(一)musicutils.py(音乐模型数据集)
# musicutils.py
import json
import os
class ChordDetector:
def __init__(self, notes):
self.notes = notes
def getNormalChord(self):
note_names = {n.split('_')[0] for n in self.notes}
if note_names == {'C', 'E', 'G'}:
return "C Major"
elif note_names == {'G', 'B', 'D'}:
return "G Major"
elif note_names == {'F', 'A', 'C'}:
return "F Major"
elif note_names == {'A', 'C#', 'E'}:
return "A Minor"
elif note_names == {'D', 'F#', 'A'}:
return "D Major"
return "Unknown"
def detectElement(notes):
return ChordDetector(notes)
# 加载音乐风格数据集
def load_music_style(style_name):
"""加载音乐风格的和弦序列数据"""
dataset_path = os.path.join("datasets", f"{style_name}.json")
if not os.path.exists(dataset_path):
return []
with open(dataset_path, "r", encoding="utf-8") as f:
return json.load(f)
(二)chord_predictor.py(马尔科夫链预测模型)
# chord_predictor.py
import numpy as np
class ChordPredictor:
def __init__(self, model_type, chord_sequences, order):
self.model_type = model_type
self.chord_sequences = chord_sequences
self.order = order
self.transition_counts = {}
self.build_model()
def build_model(self):
for seq in self.chord_sequences:
for i in range(len(seq) - self.order):
current_state = tuple(seq[i:i+self.order])
next_chord = seq[i+self.order]
if current_state not in self.transition_counts:
self.transition_counts[current_state] = {}
self.transition_counts[current_state][next_chord] = (
self.transition_counts[current_state].get(next_chord, 0) + 1
)
def predict_chord(self, current_chords):
if len(current_chords) < self.order:
return ("", 0.0)
current_state = tuple(current_chords[-self.order:])
if current_state not in self.transition_counts:
return ("", 0.0)
next_chords = self.transition_counts[current_state]
predicted_chord = max(next_chords, key=next_chords.get)
probability = next_chords[predicted_chord] / sum(next_chords.values())
return (predicted_chord, probability)
# default:多风格混合预测器
class MultiStylePredictor:
def __init__(self, order=2):
self.order = order
self.style_predictors = {}
self.style_weights = {}
def add_style(self, style_name, chord_sequences, weight=1.0):
"""添加音乐风格及其权重"""
predictor = ChordPredictor("base", chord_sequences, self.order)
self.style_predictors[style_name] = predictor
self.style_weights[style_name] = weight
def predict_chord(self, current_chords):
"""综合多风格预测结果"""
if len(current_chords) < self.order:
return ("", 0.0)
# 收集所有风格的预测结果
predictions = {}
for style, predictor in self.style_predictors.items():
predicted, prob = predictor.predict_chord(current_chords)
if predicted and prob > 0:
if predicted not in predictions:
predictions[predicted] = 0
predictions[predicted] += prob * self.style_weights[style]
if not predictions:
return ("", 0.0)
# 概率处理,输出最匹配和弦
total_weight = sum(predictions.values())
for chord in predictions:
predictions[chord] /= total_weight
predicted_chord = max(predictions, key=predictions.get)
probability = predictions[predicted_chord]
return (predicted_chord, probability)
(三)main.py
# main.py
import sys
import os
import json
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QRadioButton, QCheckBox, QScrollArea,
QLineEdit, QFileDialog, QComboBox, QMessageBox, QListWidget
)
from PyQt5.QtCore import QTimer
from musicutils import detectElement, load_music_style
from chord_predictor import ChordPredictor, MultiStylePredictor
class VirtualKeyboard(QMainWindow):
def __init__(self):
super().__init__()
self.MAX_QUEUE = 12 # 和弦显示数量为12个,十二个小方块
self.QUEUE = []
self.PRE_CHORD = None
self.current_style = "default" # 当前选择的音乐风格
self.available_styles = ["default", "pop", "jazz", "classical"]
self.style_weights = {style: 1.0 for style in self.available_styles}
self.saved_sequences_dir = "saved_sequences"
# 创建保存序列的目录
if not os.path.exists(self.saved_sequences_dir):
os.makedirs(self.saved_sequences_dir)
self.initUI()
def initUI(self):
self.setWindowTitle("和弦预测工具")
self.setGeometry(100, 100, 900, 700)
central_widget = QWidget()
self.setCentralWidget(central_widget)
self.vbox = QVBoxLayout(central_widget)
# 1. 和弦序列显示区域
ch_title = QLabel(f"实时记录最新{self.MAX_QUEUE}条和弦")
self.vbox.addWidget(ch_title)
self.chords_queue = QHBoxLayout()
self.chord_btns = []
for i in range(self.MAX_QUEUE):
btn = QPushButton("")
btn.setMinimumHeight(40)
btn.clicked.connect(lambda _, idx=i: self.on_chord_clicked(idx))
self.chords_queue.addWidget(btn)
self.chord_btns.append(btn)
self.vbox.addLayout(self.chords_queue)
# 2. 音乐风格选择区域
style_layout = QHBoxLayout()
self.style_label = QLabel("选择音乐风格:")
self.style_combo = QComboBox()
self.style_combo.addItems(self.available_styles)
self.style_combo.currentIndexChanged.connect(self.on_style_changed)
# 风格权重滑块,单选为三个,在datasets目录里,可以根据需要增加
style_layout.addWidget(self.style_label)
style_layout.addWidget(self.style_combo)
self.vbox.addLayout(style_layout)
# 3. 预测阶数选择
radio_layout = QHBoxLayout()
self.radio1 = QRadioButton("一阶")
self.radio2 = QRadioButton("二阶")
self.radio3 = QRadioButton("三阶")
self.radio2.setChecked(True)
radio_layout.addWidget(QLabel("预测模型阶数:"))
radio_layout.addWidget(self.radio1)
radio_layout.addWidget(self.radio2)
radio_layout.addWidget(self.radio3)
self.vbox.addLayout(radio_layout)
# 4. 状态显示
self.current_chord_label = QLabel("当前和弦: ")
self.predicted_chord_label = QLabel("预测下一个和弦: ")
self.vbox.addWidget(self.current_chord_label)
self.vbox.addWidget(self.predicted_chord_label)
# 5. 和弦测试按钮
test_layout = QHBoxLayout()
self.test_c_btn = QPushButton("测试C和弦")
self.test_g_btn = QPushButton("测试G和弦")
self.test_f_btn = QPushButton("测试F和弦")
self.test_a_btn = QPushButton("测试A Minor") # 新增和弦
self.test_d_btn = QPushButton("测试D Major") # 新增和弦
self.test_c_btn.clicked.connect(lambda: self.test_chord("C"))
self.test_g_btn.clicked.connect(lambda: self.test_chord("G"))
self.test_f_btn.clicked.connect(lambda: self.test_chord("F"))
self.test_a_btn.clicked.connect(lambda: self.test_chord("A"))
self.test_d_btn.clicked.connect(lambda: self.test_chord("D"))
test_layout.addWidget(self.test_c_btn)
test_layout.addWidget(self.test_g_btn)
test_layout.addWidget(self.test_f_btn)
test_layout.addWidget(self.test_a_btn)
test_layout.addWidget(self.test_d_btn)
self.vbox.addLayout(test_layout)
# 6. 和弦序列保存功能
save_layout = QHBoxLayout()
self.save_name_input = QLineEdit()
self.save_name_input.setPlaceholderText("序列名称")
self.save_btn = QPushButton("保存当前序列")
self.save_btn.clicked.connect(self.save_sequence)
save_layout.addWidget(self.save_name_input)
save_layout.addWidget(self.save_btn)
self.vbox.addLayout(save_layout)
# 7. 已保存序列列表
saved_title = QLabel("已保存的和弦序列:")
self.vbox.addWidget(saved_title)
self.saved_list = QListWidget()
self.saved_list.setMinimumHeight(100)
self.vbox.addWidget(self.saved_list)
# 加载已保存的序列
self.load_saved_sequences()
# 8. 加载和删除序列按钮
load_delete_layout = QHBoxLayout()
self.load_btn = QPushButton("加载选中序列")
self.delete_btn = QPushButton("删除选中序列")
self.load_btn.clicked.connect(self.load_sequence)
self.delete_btn.clicked.connect(self.delete_sequence)
load_delete_layout.addWidget(self.load_btn)
load_delete_layout.addWidget(self.delete_btn)
self.vbox.addLayout(load_delete_layout)
def on_style_changed(self, index):
"""音乐风格变更"""
self.current_style = self.available_styles[index]
self.update_prediction()
def on_chord_clicked(self, index):
if 0 <= index < len(self.QUEUE):
self.QUEUE.pop(index)
self.update_chords_display()
self.update_prediction()
def update_chords_display(self):
for i, btn in enumerate(self.chord_btns):
if i < len(self.QUEUE):
btn.setText(self.QUEUE[i])
else:
btn.setText("")
def test_chord(self, chord_type):
# 模拟按下和弦对应的按钮
if chord_type == "C":
notes = ["C_4", "E_4", "G_4"] # C Major
elif chord_type == "G":
notes = ["G_3", "B_3", "D_4"] # G Major
elif chord_type == "F":
notes = ["F_3", "A_3", "C_4"] # F Major
elif chord_type == "A":
notes = ["A_3", "C#_4", "E_4"] # A Minor
elif chord_type == "D":
notes = ["D_4", "F#_4", "A_4"] # D Major
else:
return
self.detect_current_chord(notes)
# 释放按键,2s钟
QTimer.singleShot(2000, self.release_chord)
def detect_current_chord(self, notes):
detector = detectElement(notes)
current_chord = detector.getNormalChord()
self.current_chord_label.setText(f"当前和弦: {current_chord}")
self.PRE_CHORD = current_chord
def release_chord(self):
if self.PRE_CHORD:
self.QUEUE.append(self.PRE_CHORD)
if len(self.QUEUE) > self.MAX_QUEUE:
self.QUEUE.pop(0)
self.PRE_CHORD = None
self.current_chord_label.setText("当前和弦: ")
self.update_chords_display()
self.update_prediction()
def update_prediction(self):
if len(self.QUEUE) < 2:
self.predicted_chord_label.setText("预测下一个和弦: ")
return
order = 1 if self.radio1.isChecked() else 2 if self.radio2.isChecked() else 3
if len(self.QUEUE) < order:
self.predicted_chord_label.setText("预测下一个和弦: 数据不足")
return
# 加载所选风格的训练数据
if self.current_style == "default":
training_data = [
["C Major", "G Major", "C Major", "F Major"],
["G Major", "C Major", "G Major", "D Major"],
["C Major", "F Major", "G Major", "C Major"],
["F Major", "G Major", "C Major", "F Major"]
]
else:
training_data = load_music_style(self.current_style)
if not training_data: # 如果没有风格数据,使用默认default
training_data = [
["C Major", "G Major", "C Major", "F Major"],
["G Major", "C Major", "G Major", "D Major"],
["C Major", "F Major", "G Major", "C Major"],
["F Major", "G Major", "C Major", "F Major"]
]
predictor = ChordPredictor("base", training_data, order)
current_sequence = self.QUEUE[-order:]
next_chord, prob = predictor.predict_chord(current_sequence)
if prob > 0:
self.predicted_chord_label.setText(
f"预测下一个和弦: {next_chord} ({prob*100:.1f}%)"
)
else:
self.predicted_chord_label.setText("预测下一个和弦: 无匹配")
def save_sequence(self):
"""保存当前和弦序列到文件"""
name = self.save_name_input.text().strip()
if not name:
name = f"Sequence_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
sequence_data = {
"name": name,
"style": self.current_style,
"order": 2 if self.radio2.isChecked() else 1 if self.radio1.isChecked() else 3,
"chords": self.QUEUE.copy(),
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
file_path = os.path.join(self.saved_sequences_dir, f"{name}.json")
with open(file_path, "w", encoding="utf-8") as f:
json.dump(sequence_data, f, ensure_ascii=False, indent=2)
# 更新已保存序列列表
self.load_saved_sequences()
self.save_name_input.clear()
QMessageBox.information(self, "保存成功", f"和弦序列 '{name}' 已保存")
def load_saved_sequences(self):
"""加载已保存的和弦序列列表"""
self.saved_list.clear()
for file in os.listdir(self.saved_sequences_dir):
if file.endswith(".json"):
self.saved_list.addItem(file.replace(".json", ""))
def load_sequence(self):
"""加载选中的和弦序列"""
selected_items = self.saved_list.selectedItems()
if not selected_items:
QMessageBox.warning(self, "错误", "请先选择一个序列")
return
sequence_name = selected_items[0].text()
file_path = os.path.join(self.saved_sequences_dir, f"{sequence_name}.json")
try:
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
self.QUEUE = data["chords"]
self.current_style = data.get("style", "default")
order = data.get("order", 2)
# 更新界面
self.update_chords_display()
self.style_combo.setCurrentText(self.current_style)
# 更新预测阶数
if order == 1:
self.radio1.setChecked(True)
elif order == 2:
self.radio2.setChecked(True)
else:
self.radio3.setChecked(True)
self.update_prediction()
QMessageBox.information(self, "加载成功", f"已加载和弦序列 '{sequence_name}'")
except Exception as e:
QMessageBox.critical(self, "加载失败", f"无法加载序列: {str(e)}")
def delete_sequence(self):
"""删除选中的和弦序列"""
selected_items = self.saved_list.selectedItems()
if not selected_items:
QMessageBox.warning(self, "错误", "请先选择一个序列")
return
sequence_name = selected_items[0].text()
reply = QMessageBox.question(
self, "确认删除",
f"确定要删除和弦序列 '{sequence_name}' 吗?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
file_path = os.path.join(self.saved_sequences_dir, f"{sequence_name}.json")
try:
os.remove(file_path)
self.load_saved_sequences()
QMessageBox.information(self, "删除成功", f"已删除和弦序列 '{sequence_name}'")
except Exception as e:
QMessageBox.critical(self, "删除失败", f"无法删除序列: {str(e)}")
if __name__ == "__main__":
try:
app = QApplication(sys.argv)
window = VirtualKeyboard()
window.show()
sys.exit(app.exec_())
except Exception as e:
print(f"程序异常、退出: {e}")
import traceback
traceback.print_exc()
(四)dataset数据集
(1)流行(pop)pop.json
[
["C Major", "G Major", "Am", "F Major"],
["D Major", "G Major", "Bm", "A Major"],
["G Major", "D Major", "Em", "C Major"],
["A Minor", "F Major", "C Major", "G Major"]
]
(2)爵士(jazz)jazz.json
[
["Cmaj7", "G7", "Cmaj7", "Fmaj7"],
["Dm7", "G7", "Cmaj7", "Am7"],
["Gmaj7", "C7", "Fmaj7", "Bm7-5"],
["Fmaj7", "Bm7-5", "E7", "Am7"]
]
(3) 古典(classical)classical.json
古典(classical):
[
["C Major", "G Major", "Am", "E7", "Am"],
["G Major", "Em", "C Major", "D7", "G Major"],
["F Major", "Dm", "G7", "C Major"],
["D Minor", "G Minor", "C Major", "Bdim", "C Major"]
]
6.实验结果展示
运行界面:

音乐类型选择:

读入序列:

7.实验运行视频
三、实验过程中遇到的问题和解决过程
实验中遇到好多好多问题,我以为我对代码的设计胸有成竹,但是还是对于Python编程不够熟练,反复报错报错报错,是一个一个不眠的夜,好在查资料、看报错提示、问AI学习,我的程序终于能跑了。
首先搭建环境就反复报错,在终端下载的PyQt5,告诉我需要更新终端库、复杂和弦计算music21,根目录文件创建路径也不对,主函数因为目录命名错误运行找不到datasets音乐类型数据集,代码也是反复报错。
简单展示一下让我遇到头疼的错误吧




......
好在,经过了爆肝,在端午节假期的最后一天闭馆之后幽暗的教室(灯都关了),我成功搭建了我的程序

四、参考资料
- PyQt5 快速开发实战
- PyQt5保姆级教程-- 从入门到精通
- PyQt5保姆级入门教程——从安装到使用
- 【基础算法】马尔科夫模型
- python:music21 与 AI 结合应用探讨
- LSTM模型全面解析
- Python基础语法体系(详细)
- PyQt5 快速入门
- python模拟马尔科夫链原理
- 还有亲爱的豆包老师
- deepseek
五、感悟与思考
作为一个资深的吉他爱好者,我喜欢弹电吉他,对音乐创作也有着喜爱和热情。有幸在乐队碰到了一群志同道合的学长,我们一同商讨开发基于马尔科夫链向量匹配算法的和弦预测系统。得知我们想用Python来实现开发,我毫不犹豫地选择的了王志强老师的Python课程,也打开了我Python编程的大门。在项目研发中,我负责算法的解读和实现过程的详细讲解,学长们在之前也选择了志强老师的Python课程,这一次由大三有丰富编程经验的学长负责代码的编写。也正是在这次项目的开发中,我接触到了马尔科夫链向量匹配算法、LSTM、Transformer模型,并且啃资料学习研读、学习乐理与计算,试图更加清晰的解读代码(真的很难,学不明白)。与此同时,我也一步一步的进行我的Python学习,我也希望有一天我有能力实现代码的编写。很快我的Python结课实验来了,这一次我想去自己独立在自己目前的能力下实现这个程序的基本功能。当时我分析项目算法、实现过程的时候花了很长时间,这一次编写代码也花了很久很久,这是一个对我来说很费心血的实验,我觉得在我目前的能力范围中,这是令我比较满意的程序,我也将继续学习不断学习,争取在自己的能力范围中完成完善度更高的代码。
六、课程总结
还记得最后一次课时,我坐在第一排,王老师的右手边,大屏幕的正前方听最后一次总结课。也非常有幸在抢答环节凭借手速和网速,抢到了课程回顾的问题回答。课程的节奏很快,从Python的课程简介、Python简介,基础语法,再到类与对象,再到文件操作、模块、网络爬虫。每次课程签到时候的连线九宫格往往其他课程简单的'L'型不一样,快速看着大屏幕按着连线推导怎么连这九个点。刚上课时回顾上一次课程的内容时,都会忘记上一次课的知识点,点名提问时候快速的翻书、翻笔记复习。记忆深刻的还有王老师课堂的举例,学类时“王凯”用数值对打“曾楷”,做石头剪刀布实验时逗我们说输了就挂科。王凯实验写的不好直接挂科,哈哈哈哈,轻松愉悦的氛围消除了课程的枯燥,让每一次周三的晚课都充满热情。总结课上,王老师PPT的那句“那些看似波澜不惊的日复一日,终将在某一天,让我们看到坚持的意义。” 没错,做项目时熬夜查资料,泡图书馆问AI学习马尔科夫链算法,学习LSTM输入门、遗忘门和输出门信息控制处理中长序列依赖,学习Transfomer模型嵌入层、多头自注意力层、前馈神经网络层和输出层Transformer 网络结构,用softmax函数输出下一个和弦的概率分布结果。做程序时搭建不明白环境、代码报错,程序能运行打开就闪退。就是这些波澜不惊的日复一日,让我在端午节最后一天的晚上成功运行程序,看到坚持的意义。我觉得这句话不止适用于短期,对未来的我们更是使用,那些看似波澜不惊的日复一日,终将在某一天,让我们看到坚持的意义。
还记得王老师给我们看往届学长学姐的实验时,王艺瑶学姐的虚拟试衣并对外观打分的程序让我印象很深,最终王艺瑶学姐和她的团队把这个程序做成项目搬上了“红星杯”大学生创新大赛决赛取得了一等奖。也正是因为这个,让我决心把我的大创项目做成实验,我也希望未来我会像优秀的学姐一样,共同用代码编程能力完善自己的项目,我会不断学习。王老师说他只是带我们领进门,真正的学习还要靠我们自己,很感谢王老师把我带进Python编程的大门,我也切实的感受到了真正的学习过程是我日如一日查资料研究算法,在一次次报错中找视频、问AI,我再一次一次编写中更加熟练,这是我真真正正的学习过程。
脚踏实地时,抬头看路!
“那些看似波澜不惊的日复一日,终将在某一天,让我们看到坚持的意义。”
很荣幸能抢到王老师的Python课程,今后的学习和生活中,仍需要王老师的指点与帮助。
未来的人生,一路繁花似锦。
那么,完结撒花!!!
附上照片:

