You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从MIDI文件提取实际音符起始时间:解析器节拍时长适配问题

优化MIDI文件解析器:修复BPM与拍号处理问题

原代码核心问题分析

  1. 时间计算逻辑错误:
    • 仅用当前current_tempo转换所有累积ticks,忽略了tempo变化对历史ticks的影响——不同时间段的ticks必须用对应时刻的tempo转换。
    • 注释掉了last_msg_time的更新逻辑,导致delta_time计算完全失效,无法正确识别音符的时间间隔。
  2. 完全忽略拍号处理:
    • 未解析time_signature元消息,无法根据拍号(如4/4、3/4)划分正确的节拍边界,导致beat分组混乱。
  3. 累积ticks管理不当:
    • 所有非元消息的time都累积到accumulated_ticks,没有结合tempo变化分段处理,时间转换必然失真。

优化方案与代码实现

以下是修复后的完整代码,解决了BPM缩放异常和拍号处理问题,同时保证输出结构符合需求:

import os
import mido

def midi_to_array(src_folder):
    files = [f for f in os.listdir(src_folder) if f.endswith(('.mid', '.midi'))][-5:]
    songs_data = []
    
    for file in files:
        mid = mido.MidiFile(os.path.join(src_folder, file))
        tracks = []
        
        for track in mid.tracks:
            # 初始化轨道基础数据
            track_name = ""
            track_beats = []
            # 跟踪时间相关状态:当前tick位置、已转换的总毫秒数、tempo变化记录
            current_tick = 0
            total_ms = 0.0
            tempo_events = [(0, 500000)]  # (tick位置, tempo值,单位微秒/拍)
            # 跟踪拍号:默认4/4拍
            time_sig_num = 4
            time_sig_den = 4
            # 当前beat的临时存储
            current_notes = []
            last_beat_end_ms = 0.0
            
            # 第一步:先扫描轨道,收集所有tempo和拍号变化事件
            for msg in track:
                current_tick += msg.time
                
                if msg.type == 'track_name':
                    track_name = msg.name.strip().lower()
                elif msg.type == 'set_tempo':
                    tempo_events.append((current_tick, msg.tempo))
                elif msg.type == 'time_signature':
                    time_sig_num = msg.numerator
                    time_sig_den = msg.denominator
            
            # 重置tick位置,重新扫描处理音符
            current_tick = 0
            total_ms = 0.0
            tempo_idx = 0  # 当前使用的tempo事件索引
            current_tempo = tempo_events[0][1]
            # 计算每个节拍对应的tick数(基于拍号和ticks_per_beat)
            ticks_per_beat = mid.ticks_per_beat
            # 拍号分母是2的幂,转化为四分音符的倍数:如8分音符=0.5个四分音符
            beat_unit = 2 ** (time_sig_den - 2)
            next_beat_tick = ticks_per_beat * time_sig_num * beat_unit
            
            for msg in track:
                # 先处理当前消息的tick增量,转换为实际毫秒
                if msg.time > 0:
                    # 检查是否需要切换tempo
                    while tempo_idx + 1 < len(tempo_events) and current_tick + msg.time >= tempo_events[tempo_idx+1][0]:
                        # 计算到下一个tempo变化点的tick数
                        delta_tick = tempo_events[tempo_idx+1][0] - current_tick
                        # 转换为毫秒并累加
                        total_ms += mido.tick2second(delta_tick, ticks_per_beat, current_tempo) * 1000
                        current_tick = tempo_events[tempo_idx+1][0]
                        tempo_idx += 1
                        current_tempo = tempo_events[tempo_idx][1]
                    # 处理剩余的tick
                    delta_tick = msg.time - (tempo_events[tempo_idx+1][0] - current_tick if tempo_idx+1 < len(tempo_events) else 0)
                    total_ms += mido.tick2second(delta_tick, ticks_per_beat, current_tempo) * 1000
                    current_tick += msg.time
                
                # 处理note_on消息(忽略velocity=0的情况,视为note_off)
                if msg.type == 'note_on' and msg.velocity > 0:
                    current_notes.append([msg.note % 12, msg.note // 12])
                
                # 检查是否到达节拍边界(基于拍号的tick间隔)
                while current_tick >= next_beat_tick:
                    # 计算当前beat的时长(到下一个beat的间隔)
                    delta_ms = total_ms - last_beat_end_ms
                    if current_notes:
                        track_beats.append({
                            'notes': current_notes.copy(),
                            'delta_ms': delta_ms
                        })
                    # 更新状态
                    last_beat_end_ms = total_ms
                    current_notes.clear()
                    next_beat_tick += ticks_per_beat * time_sig_num * beat_unit
            
            # 添加最后一个未完成的beat
            if current_notes:
                delta_ms = total_ms - last_beat_end_ms
                track_beats.append({
                    'notes': current_notes,
                    'delta_ms': delta_ms
                })
            
            # 组装轨道数据
            tracks.append({
                'track_name': track_name,
                'track_beats': track_beats
            })
        
        songs_data.append(tracks)
    
    return songs_data

关键改动说明

  1. 分段时间转换:
    • 先扫描轨道收集所有set_tempo事件,记录每个tempo生效的tick位置。
    • 处理消息时,按tempo变化点分段计算tick到毫秒的转换,确保每个时间段用正确的tempo计算时间。
  2. 拍号处理:
    • 解析time_signature元消息,计算每个节拍对应的tick数,以此为依据划分beat边界。
    • 支持不同拍号(如3/4、6/8)的节拍分组,符合音乐逻辑。
  3. 时间状态修正:
    • 新增last_beat_end_ms跟踪节拍结束时间,正确计算每个beat到下一个beat的毫秒间隔delta_ms。
    • 区分note_on消息中velocity=0的情况(视为note_off),避免无效音符加入。
  4. 输出结构调整:
    • 每个beat包含notes(半音+八度)和delta_ms(到下一节拍的实际毫秒间隔),完全符合需求。

内容的提问来源于stack exchange,提问作者Maxim Mashkov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:15:12