如何用Python Mido按小节分割MIDI文件并随机重组
MIDI分割与随机重组解决方案
问题背景
我有一个包含8小节、4/4拍、100BPM的MIDI文件,需要完成两个任务:
- 将原文件分割为8个等长的单小节MIDI文件
- 使用Python的Mido库将这些小节随机组合成新的MIDI文件
尝试了ChatGPT提供的代码但无法正常运行,原代码如下:
import mido import os # Load the MIDI file mid = mido.MidiFile('input_file.mid') # Set the time signature to 4/4 time_signature = mido.MetaMessage('time_signature', numerator=4, denominator=4, clocks_per_click=24, notated_32nd_notes_per_beat=8) mid.tracks[0].insert(0, time_signature) # Set the tempo to 100 BPM tempo = mido.bpm2tempo(100) set_tempo = mido.MetaMessage('set_tempo', tempo=tempo) mid.tracks[0].insert(0, set_tempo) # Calculate the number of ticks per beat and per bar ticks_per_beat = mid.ticks_per_beat ticks_per_bar = ticks_per_beat * 4 # Keep track of the current tick position current_tick = 0 # Keep track of the messages for the current bar current_bar_messages = [] # Iterate over each message in the MIDI file for message in mid: # Append the message to the current bar current_bar_messages.append(message) # Increment the current tick position by the message time current_tick += message.time # If the current tick position is greater than or equal to the number of ticks per bar if current_tick >= ticks_per_bar: # Create a new MIDI file output_file_name = f"bar_{len(os.listdir())}.mid" output_mid = mido.MidiFile(type=mid.type) # Add a new track to the output MIDI file output_mid.add_track() # Append the time signature and tempo messages to the new track output_mid.tracks[0].append(time_signature) output_mid.tracks[0].append(set_tempo) # Append all messages up to the current tick position to the new track for msg in current_bar_messages: output_mid.tracks[0].append(msg) # Reset the current tick position and current bar messages current_tick = 0 current_bar_messages = [] # Save the new MIDI file output_mid.save(output_file_name)
原代码问题分析
- 错误直接遍历
mid对象,正确逻辑应遍历每个轨道内的消息 - 使用
os.listdir()生成文件名会受目录内其他文件干扰,导致编号混乱 - 未处理消息时间超出小节长度的情况,小节边界划分不准确
- 强制插入时间签名和Tempo消息可能覆盖原文件设置,引发冲突
- 缺失多轨MIDI的处理逻辑,仅支持单轨道文件
- 未处理最后剩余的消息片段
修正后的分割代码
import mido import os # 配置参数 INPUT_MIDI = 'input_file.mid' OUTPUT_DIR = 'split_bars' BARS_COUNT = 8 TIME_SIGNATURE = (4, 4) TARGET_BPM = 100 # 创建输出目录 os.makedirs(OUTPUT_DIR, exist_ok=True) # 加载原MIDI文件 mid = mido.MidiFile(INPUT_MIDI) ticks_per_beat = mid.ticks_per_beat ticks_per_bar = ticks_per_beat * TIME_SIGNATURE[0] # 获取原文件的元消息(优先用原文件配置,无则用目标值) def get_meta_messages(track): tempo_msg = None ts_msg = None for msg in track: if msg.type == 'set_tempo': tempo_msg = msg elif msg.type == 'time_signature': ts_msg = msg if tempo_msg and ts_msg: break if not tempo_msg: tempo_msg = mido.MetaMessage('set_tempo', tempo=mido.bpm2tempo(TARGET_BPM)) if not ts_msg: ts_msg = mido.MetaMessage('time_signature', numerator=TIME_SIGNATURE[0], denominator=TIME_SIGNATURE[1], clocks_per_click=24, notated_32nd_notes_per_beat=8) return tempo_msg, ts_msg # 按小节分割每个轨道的消息 tracks_bars = [] for track in mid.tracks: tempo_msg, ts_msg = get_meta_messages(track) bar_tracks = [[] for _ in range(BARS_COUNT)] current_tick = 0 current_bar_idx = 0 for msg in track: if current_bar_idx >= BARS_COUNT: break # 处理跨小节的消息 if current_tick + msg.time > ticks_per_bar: time_in_current = ticks_per_bar - current_tick time_in_next = msg.time - time_in_current # 当前小节的消息片段 current_msg = msg.copy(time=time_in_current) bar_tracks[current_bar_idx].append(current_msg) # 剩余部分放入下一小节 next_msg = msg.copy(time=time_in_next) current_bar_idx += 1 bar_tracks[current_bar_idx].append(next_msg) current_tick = time_in_next else: bar_tracks[current_bar_idx].append(msg) current_tick += msg.time # 切换到下一小节 if current_tick >= ticks_per_bar: current_tick -= ticks_per_bar current_bar_idx += 1 tracks_bars.append(bar_tracks) # 保存每个小节的MIDI文件 for bar_idx in range(BARS_COUNT): bar_mid = mido.MidiFile(type=mid.type, ticks_per_beat=ticks_per_beat) # 保留原轨道结构 for track_idx in range(len(mid.tracks)): track = bar_mid.add_track() # 添加元消息 tempo_msg, ts_msg = get_meta_messages(mid.tracks[track_idx]) track.append(ts_msg) track.append(tempo_msg) # 添加当前小节的消息 for msg in tracks_bars[track_idx][bar_idx]: track.append(msg) # 生成有序文件名 output_path = os.path.join(OUTPUT_DIR, f'bar_{bar_idx+1}.mid') bar_mid.save(output_path)
随机组合小节的代码
import mido import os import random # 配置参数 SPLIT_DIR = 'split_bars' OUTPUT_MIDI = 'random_combined.mid' TARGET_BARS = 8 # 组合为8小节 # 获取所有分割后的小节文件 bar_files = [f for f in os.listdir(SPLIT_DIR) if f.endswith('.mid')] if not bar_files: raise ValueError('未找到分割后的小节文件') # 随机选择小节 selected_bars = random.sample(bar_files, TARGET_BARS) # 以第一个小节为模板创建组合文件 first_bar = mido.MidiFile(os.path.join(SPLIT_DIR, selected_bars[0])) combined_mid = mido.MidiFile(type=first_bar.type, ticks_per_beat=first_bar.ticks_per_beat) # 为每个轨道构建组合内容 for track_idx in range(len(first_bar.tracks)): combined_track = combined_mid.add_track() # 仅在第一个轨道添加元消息(避免重复) if track_idx == 0: for msg in first_bar.tracks[track_idx]: if msg.is_meta: combined_track.append(msg) # 拼接选中小节的轨道消息(跳过元消息) for bar_file in selected_bars: bar_mid = mido.MidiFile(os.path.join(SPLIT_DIR, bar_file)) for msg in bar_mid.tracks[track_idx]: if not msg.is_meta: combined_track.append(msg) # 保存组合后的MIDI文件 combined_mid.save(OUTPUT_MIDI)
内容的提问来源于stack exchange,提问作者memes garage
相关产品推荐
相关产品推荐

