
1. 項目概述從數字樂譜到可編程聲音如果你玩過音樂游戲或者用過電子琴的自動伴奏功能那你已經接觸過MIDI了。它不像MP3或WAV那樣直接記錄聲音的波形而更像是一份極其詳盡的“數字樂譜”。這份樂譜里不包含任何實際的聲音只記錄了一系列指令什么時間、哪個音符被按下Note On、力度多大、什么時候松開Note Off以及音色、音量、彎音等控制信息。這種設計讓MIDI文件體積非常小一個幾分鐘的復雜樂章可能只有幾十KB并且修改起來異常靈活——你可以輕松地把鋼琴曲換成吉他音色或者改變整個曲子的速度而無需重新錄制。MIDI的全稱是Musical Instrument Digital Interface樂器數字接口它誕生于上世紀80年代初衷是為了讓不同廠家的電子樂器能夠互相“對話”。今天它的應用早已超出專業音樂制作的范疇滲透到游戲音效、手機鈴聲、多媒體演示乃至嵌入式系統的提示音中。對于程序員和創作者來說MIDI的魅力在于它的“可編程性”。你可以用代碼來生成、解析、修改這些樂譜數據從而實現自動作曲、音樂可視化、算法生成背景音樂等創意項目。Python的mido庫就是我們操作這份“數字樂譜”的瑞士軍刀。它不像一些龐大的音頻處理庫那樣復雜而是專注于MIDI協議本身提供了清晰、Pythonic的API來讀寫.mid文件和處理MIDI消息。無論是想批量修改一批MIDI文件的音量還是從零生成一段旋律抑或是做一個根據你的鍵盤打字節奏來生成音樂的趣味程序mido都是一個絕佳的起點。接下來我會帶你深入MIDI的結構并用mido庫進行實戰操作分享一些我處理音樂數據時踩過的坑和總結的技巧。2. MIDI文件結構深度解析要熟練地用代碼操作MIDI不能只停留在“它是一組指令”的概念上必須理解其內部是如何組織的。一個標準的MIDI文件SMF就像一部音樂劇的完整劇本它包含了所有演員音符的出場時間、臺詞音高、情緒力度以及舞臺燈光和布景的變換控制信息。2.1 核心構成軌道與消息一個MIDI文件主要由兩部分構成軌道Track和消息Message。你可以把整個MIDI文件想象成一個多軌錄音機。文件頭Header Chunk定義了全局信息比如格式是單軌、多軌同步還是多軌獨立、軌道數量以及時間基準Ticks per quarter note 即一個四分音符包含多少個“時間滴答”這決定了時間的精度。緊接著的是一個個軌道塊Track Chunk。通常第一個軌道Track 0是特殊的它不包含音符只存放元消息Meta Message比如曲目標題、版權信息、速度Tempo 單位是微秒每拍和拍號Time Signature等。從Track 1開始才是真正的音樂數據軌道比如鋼琴軌、鼓軌等。每個軌道由一系列按時間順序排列的消息組成。這是MIDI的精髓。消息分為幾大類通道消息Channel Messages這是最主要的部分需要指定一個通道0-15。最常見的就是note_on 音符開啟。參數包括channel通道note音高編號60代表中央Cvelocity觸鍵力度0-127。note_off 音符關閉。參數包括channelnotevelocity釋放力度通常也用來表現觸感。control_change 控制改變。用于調制輪、音量CC7、聲像CC10、延音踏板CC64等。參數是channelcontrol控制器編號value值0-127。program_change 音色改變。用來切換樂器音色例如從鋼琴program 0切換到弦樂program 48。系統消息System Messages與特定通道無關如系統專用信息SysEx常用于傳輸廠商特定的數據。元消息Meta Messages如前所述存放文本、速度、拍號、音軌結束等非演奏信息。這里有一個關鍵概念增量時間Delta Time。MIDI文件中的消息并不是存儲絕對時間而是存儲距離上一條消息過去了多少“滴答數”ticks。這種相對時間的存儲方式非常高效。例如一條消息的time為120意味著從上一條消息執行后等待120個ticks再執行本條消息。2.2 時間與節奏的量化Ticks與Tempo理解時間系統是避免音樂播放速度混亂的關鍵。文件頭中的ticks_per_beatPPQ是分辨率。假設ticks_per_beat 480那么一個四分音符就占據480個ticks。而實際播放速度由速度元消息set_tempo決定它的單位是微秒每四分音符microseconds per quarter note。一個常見的默認速度是tempo 500000即50萬微秒0.5秒一個四分音符換算過來就是每分鐘120拍BPM 60,000,000 / 500,000。所以一條time240的消息在ticks_per_beat480且tempo500000的情況下它的實際延遲時間是(240 / 480) * (500000 / 1,000,000) 0.5 * 0.5 0.25秒。注意很多新手會混淆time字段的含義。在從文件讀取時message.time是增量時間單位是ticks。但在構建消息準備寫入時mido允許你直接使用time作為增量時間或者使用MidiFile的tick相對時間。在實時播放或生成文件時必須確保時間系統的一致性。3. 使用mido庫進行MIDI文件操作安裝mido非常簡單pip install mido。它還有一個可選依賴python-rtmidi用于實時MIDI端口輸入輸出如果不需要實時功能僅讀寫文件則不需要安裝。3.1 讀取與解析窺探MIDI的細節讓我們從一個簡單的讀取例子開始看看如何獲取文件的全局信息和遍歷所有消息。import mido # 1. 讀取MIDI文件 midi_file mido.MidiFile(example.mid) # 2. 打印文件頭信息 print(f文件類型: {midi_file.type}) print(f軌道數: {len(midi_file.tracks)}) print(f時間分辨率 (ticks per beat): {midi_file.ticks_per_beat}) # 3. 遍歷所有軌道和消息 for i, track in enumerate(midi_file.tracks): print(f\n--- 軌道 {i}: {track.name if hasattr(track, name) else 未命名} ---) # 累計絕對時間單位ticks用于分析 absolute_time 0 for msg in track: absolute_time msg.time # 格式化輸出顯示絕對時間和消息內容 print(f[{absolute_time:6d} ticks] {msg})這段代碼會輸出類似這樣的內容文件類型: 1 軌道數: 3 時間分辨率 (ticks per beat): 480 --- 軌道 0 --- [ 0 ticks] meta message track_name namePiano time0 [ 0 ticks] meta message time_signature numerator4 denominator4 clocks_per_click24 notated_32nd_notes_per_beat8 time0 [ 0 ticks] meta message set_tempo tempo500000 time0 [ 0 ticks] meta message end_of_track time0 --- 軌道 1 --- [ 0 ticks] meta message track_name namePiano Right Hand time0 [ 0 ticks] meta message program_change channel0 program0 time0 [ 0 ticks] control_change channel0 control7 value100 time0 [ 120 ticks] note_on channel0 note60 velocity90 time120 [ 360 ticks] note_off channel0 note60 velocity64 time240 ...通過遍歷你可以清晰地看到音樂是如何被“搭建”起來的。track.name屬性通常由第一個track_name元消息設置。program_change消息將通道0的音色設置為0通常是原聲鋼琴。control_change消息將通道0的音量CC7設置為100。隨后在120 ticks后中央Cnote 60以90的力度被按下再經過240 ticks后松開。3.2 創建與編輯從零構建你的旋律創建新的MIDI文件給了你最大的自由度。下面我們創建一個簡單的C大調音階。import mido from mido import MidiFile, MidiTrack, Message, MetaMessage # 1. 創建MIDI文件和軌道 mid MidiFile() track MidiTrack() mid.tracks.append(track) # 2. 在軌道0第一個軌道設置元信息 # 設置軌道名稱 track.append(MetaMessage(track_name, nameMy Scale, time0)) # 設置速度120 BPM (500000 microseconds per beat) track.append(MetaMessage(set_tempo, tempo500000, time0)) # 設置拍號4/4拍 track.append(MetaMessage(time_signature, numerator4, denominator4, time0)) # 3. 選擇音色Program Change0是原聲鋼琴 track.append(Message(program_change, channel0, program0, time0)) # 4. 添加音符消息C大調音階C4, D4, E4, F4, G4, A4, B4, C5 notes [60, 62, 64, 65, 67, 69, 71, 72] ticks_per_note mid.ticks_per_beat # 一個四分音符的長度默認480 current_time 0 for i, note in enumerate(notes): # 音符開啟力度100 track.append(Message(note_on, channel0, notenote, velocity100, timecurrent_time)) # 音符持續一個四分音符480 ticks后關閉 track.append(Message(note_off, channel0, notenote, velocity64, timeticks_per_note)) # 重置當前時間為0以便下個note_on緊接著上一個note_off發生 current_time 0 # 5. 必須添加軌道結束標志 track.append(MetaMessage(end_of_track, time0)) # 6. 保存文件 mid.save(my_c_scale.mid)關鍵點解析time參數的理解在append消息時time參數表示距離上一條消息的延遲。在上面的循環中第一個note_on的time0表示立即觸發。note_off的time480表示在note_on之后480ticks觸發。下一個note_on的time0意味著它緊跟著上一個note_off發生從而形成連貫的音階。必須添加end_of_track沒有它一些播放器可能無法正確識別文件結束。通道選擇通道0-15通常通道9保留給打擊樂在GM標準下。其他通道可以自由分配不同音色。編輯現有文件是另一個常見需求。例如將所有音符的力度整體提高20%但不超過127def boost_velocity(midi_file_path, output_path, factor1.2): mid MidiFile(midi_file_path) for track in mid.tracks: for msg in track: if msg.type note_on and msg.velocity 0: # 注意velocity0的note_on有時用作note_off new_vel int(msg.velocity * factor) msg.velocity min(new_vel, 127) mid.save(output_path)實操心得在修改note_on消息時一定要小心處理velocity0的情況。在MIDI標準中一個力度為0的note_on事件等價于note_off事件。有些文件會用它來結束音符。如果你盲目地將所有note_on的力度都乘以系數會把原本為0的力度代表音符結束也改變導致音符無法正常結束產生持續的鳴響。一個健壯的修改器應該判斷if msg.type note_on and msg.velocity 0。3.3 高級操作合并、切片與信息提取合并多個軌道如果你有兩個獨立的MIDI文件想把它們的鋼琴軌合并到一起你需要處理時間同步問題。更簡單的情況是在同一個文件內合并兩個音樂軌道。def merge_tracks(source_midi, track_index1, track_index2, output_path): mid MidiFile(source_midi) track1 mid.tracks[track_index1] track2 mid.tracks[track_index2] # 創建一個新軌道將所有消息按時間順序合并是一項復雜任務。 # 更簡單直接的方法新建一個文件將兩個軌道作為獨立軌道加入類型需為1或2 new_mid MidiFile(type1, ticks_per_beatmid.ticks_per_beat) # 添加元信息軌道 meta_track MidiTrack() meta_track.extend([msg for msg in mid.tracks[0] if msg.is_meta]) # 復制原文件的元信息 meta_track.append(MetaMessage(end_of_track, time0)) new_mid.tracks.append(meta_track) # 添加第一個音樂軌道 new_mid.tracks.append(track1) # 添加第二個音樂軌道 new_mid.tracks.append(track2) new_mid.save(output_path)提取旋律輪廓音高序列這對于音樂分析或機器學習非常有用。def extract_note_sequence(midi_file_path, track_num1, channel0): 提取指定軌道和通道的音符序列音高 開始時間 持續時間 mid MidiFile(midi_file_path) track mid.tracks[track_num] notes [] # 存儲 (note, start_tick, duration_tick) active_notes {} # 記錄正在響應的音符 {note_number: start_tick} current_tick 0 for msg in track: current_tick msg.time if msg.type note_on and msg.velocity 0 and msg.channel channel: # 記錄音符開始 active_notes[msg.note] current_tick elif (msg.type note_off or (msg.type note_on and msg.velocity 0)) and msg.channel channel: # 音符結束計算持續時間 if msg.note in active_notes: start_tick active_notes.pop(msg.note) duration current_tick - start_tick notes.append((msg.note, start_tick, duration)) return notes, mid.ticks_per_beat這個函數返回一個列表包含了每個音符的音高、開始的絕對時間ticks和持續的ticks數。結合ticks_per_beat和tempo信息你可以輕松地將它們轉換成秒。4. 實戰應用場景與代碼示例掌握了基礎操作后我們可以看看mido能實現哪些有趣的應用。4.1 場景一自動生成隨機旋律或伴奏利用隨機數生成符合音樂理論的簡單模式。例如生成一個在C大調內隨機漫步的旋律import random import mido from mido import MidiFile, MidiTrack, Message, MetaMessage def generate_random_walk_melody(output_path, num_notes50, key_root60): 生成一個隨機漫步旋律限制在某個調式內 # C大調音階內的音高相對于根音 scale_intervals [0, 2, 4, 5, 7, 9, 11] # C, D, E, F, G, A, B mid MidiFile() track MidiTrack() mid.tracks.append(track) track.append(MetaMessage(track_name, nameRandom Walk, time0)) track.append(MetaMessage(set_tempo, tempo500000, time0)) track.append(Message(program_change, channel0, program48, time0)) # 弦樂合奏 current_note key_root random.choice(scale_intervals) # 從調內隨機一個音開始 for _ in range(num_notes): # 隨機決定下一步向上、向下或重復一個步進-1, 0, 1 step random.choice([-1, 0, 1]) current_index (scale_intervals.index((current_note - key_root) % 12) step) % len(scale_intervals) current_note key_root scale_intervals[current_index] # 隨機力度和時長 velocity random.randint(70, 110) duration random.choice([120, 240, 480]) # 八分、四分、二分音符 track.append(Message(note_on, channel0, notecurrent_note, velocityvelocity, time0)) track.append(Message(note_off, channel0, notecurrent_note, velocity64, timeduration)) track.append(MetaMessage(end_of_track, time0)) mid.save(output_path) generate_random_walk_melody(random_walk.mid)4.2 場景二音樂可視化數據預處理將MIDI文件轉換為按時間排列的事件列表供前端如p5.js或圖形庫如matplotlib繪制。def midi_to_timeline(midi_file_path, resolution_ms10): 將MIDI轉換為一個時間線列表每個時間單位包含當前正在發聲的音符 mid MidiFile(midi_file_path) # 首先我們需要解析出所有音符的起止時間單位毫秒 # 這是一個簡化版本假設整個文件只有一個速度 tempo 500000 # 默認120 BPM for msg in mid.tracks[0]: if msg.type set_tempo: tempo msg.tempo break ticks_per_beat mid.ticks_per_beat ms_per_tick (tempo / 1000.0) / ticks_per_beat # 每tick的毫秒數 notes [] # (start_ms, end_ms, note, velocity) active_notes {} current_tick 0 # 遍歷所有軌道跳過元信息軌道 for track in mid.tracks[1:]: current_tick 0 for msg in track: current_tick msg.time current_ms current_tick * ms_per_tick if msg.type note_on and msg.velocity 0: active_notes[msg.note] (current_ms, msg.velocity) elif (msg.type note_off or (msg.type note_on and msg.velocity 0)): if msg.note in active_notes: start_ms, velocity active_notes.pop(msg.note) notes.append((start_ms, current_ms, msg.note, velocity)) # 按開始時間排序 notes.sort(keylambda x: x[0]) # 生成時間線這里只是一個示例輸出每個時間片段內的音符數 if not notes: return [] end_time max(note[1] for note in notes) timeline [] t 0 while t end_time: active_count sum(1 for start, end, note, vel in notes if start t end) timeline.append((t, active_count)) t resolution_ms return timeline, notes # 使用matplotlib簡單繪制一個鋼琴卷簾視圖 import matplotlib.pyplot as plt import matplotlib.patches as patches def plot_piano_roll(notes): 繪制簡單的鋼琴卷簾圖 if not notes: print(沒有音符數據) return fig, ax plt.subplots(figsize(12, 6)) for start_ms, end_ms, note, velocity in notes: # 將音高轉換為Y軸位置時間轉換為X軸 height 1 width end_ms - start_ms rect patches.Rectangle((start_ms, note - 0.5), width, height, linewidth1, edgecolorblack, facecolorplt.cm.plasma(velocity/127.0), alpha0.7) ax.add_patch(rect) ax.set_xlabel(時間 (毫秒)) ax.set_ylabel(音高) ax.set_title(鋼琴卷簾視圖) ax.set_ylim(30, 90) # 顯示一個常見的音區 ax.grid(True, whichboth, axisy, linestyle--, linewidth0.5) plt.tight_layout() plt.show() # 使用 timeline, note_list midi_to_timeline(example.mid) plot_piano_roll(note_list[:50]) # 只繪制前50個音符以免過于密集4.3 場景三批量處理與文件格式轉換你可能有一堆游戲音效MIDI文件需要統一降低音量或者想將Type 0單軌文件轉換成Type 1多軌以便在DAW中編輯。mido非常適合這種腳本化的批量操作。import os from pathlib import Path def batch_adjust_volume(input_folder, output_folder, volume_cc7, multiplier0.8): 批量調整MIDI文件的音量控制信息 input_path Path(input_folder) output_path Path(output_folder) output_path.mkdir(parentsTrue, exist_okTrue) for midi_file in input_path.glob(*.mid): try: mid MidiFile(midi_file) for track in mid.tracks: for msg in track: if msg.type control_change and msg.control volume_cc: msg.value int(msg.value * multiplier) new_name output_path / fvol_adjusted_{midi_file.name} mid.save(new_name) print(f已處理: {midi_file.name}) except Exception as e: print(f處理 {midi_file.name} 時出錯: {e}) def convert_to_type1(input_file, output_file): 將MIDI文件轉換為Type 1多軌格式。 注意這是一個概念性示例。簡單地將Type 0文件的所有消息塞進一個軌道并設為Type 1可能不準確 因為Type 0所有通道消息都在一個軌道里。更嚴謹的做法需要按通道分離消息到不同軌道。 mid MidiFile(input_file) if mid.type 0: # 創建一個新的Type 1文件 new_mid MidiFile(type1, ticks_per_beatmid.ticks_per_beat) # 創建元信息軌道 meta_track MidiTrack() # 嘗試從原文件復制全局元信息如速度、拍號 # 這里簡化處理直接使用默認或從第一個消息中提取 meta_track.append(MetaMessage(set_tempo, tempo500000, time0)) meta_track.append(MetaMessage(time_signature, numerator4, denominator4, time0)) meta_track.append(MetaMessage(end_of_track, time0)) new_mid.tracks.append(meta_track) # 將原Type 0的唯一軌道作為第一個音樂軌道加入 new_mid.tracks.append(mid.tracks[0]) new_mid.save(output_file) print(f已轉換并保存為: {output_file}) else: print(文件不是Type 0格式無需轉換。)5. 常見問題、排查技巧與性能優化在實際使用mido處理MIDI文件尤其是來源復雜、由不同軟件生成的文件時會遇到各種奇怪的問題。這里記錄了一些典型坑點和解決方案。5.1 文件讀取與播放異常問題1讀取文件時報錯“無效的MIDI文件”或“損壞的文件頭”。排查首先用十六進制編輯器或xxd命令查看文件頭幾個字節。標準MIDI文件應以“MThd”開頭。常見原因是文件被截斷、或根本不是MIDI文件可能是誤改了擴展名。解決嘗試用專業音頻軟件如Ableton Live, MuseScore或MIDI編輯器打開并重新導出。如果文件來自網絡重新下載。問題2用某些播放器播放時無聲或音色錯亂。排查檢查音色號program_change消息的音色號program是否超出了GMGeneral MIDI標準范圍0-127。一些播放器只支持GM音色表。檢查通道鼓組通常固定在通道10GM標準下是通道9因為從0開始計數。如果你的音樂數據誤用了通道10播放旋律就會聽到打擊樂聲。檢查音量確認是否有control_change消息將音量CC7設置為0或表達式CC11過低。檢查音符結束是否有note_on消息沒有對應的note_off消息導致音符一直持續可以用我們之前寫的extract_note_sequence函數檢查是否有音符的持續時間異常地長。解決寫一個診斷腳本打印出所有非元消息檢查通道、音色、控制器值。def diagnose_midi(file_path): mid MidiFile(file_path) print(f診斷文件: {file_path}) for i, track in enumerate(mid.tracks): print(f\n軌道 {i}:) for msg in track: if not msg.is_meta: # 打印關鍵信息 print(f {msg}) # 特別關注特定消息 if msg.type program_change: print(f - 音色改變為: {msg.program}) elif msg.type control_change and msg.control in [7, 10, 11]: ctrl_name {7: 音量, 10: 聲像, 11: 表達式}.get(msg.control, fCC{msg.control}) print(f - {ctrl_name}: {msg.value})5.2 時間與節奏處理陷阱問題3自己生成的MIDI文件播放速度不對或者音符時長感覺不對。原因混淆了增量時間Delta Time和絕對時間Absolute Time或者錯誤理解了ticks_per_beat與tempo的關系。黃金法則讀取時msg.time是距離上一條消息的增量ticks。寫入/構建時當你append一條消息你設置的time參數是它距離你添加的上一條消息的延遲ticks。計算實際時間實際秒數 (累計ticks / ticks_per_beat) * (tempo / 1,000,000)。技巧在構建復雜節奏時可以先用絕對時間以ticks為單位計算好每個事件的時刻再轉換為增量時間列表。def build_from_absolute_times(absolute_times, messages): 根據絕對時間列表和消息列表構建軌道 track MidiTrack() last_time 0 for abs_time, msg in zip(absolute_times, messages): delta abs_time - last_time msg.time delta track.append(msg) last_time abs_time track.append(MetaMessage(end_of_track, time0)) return track問題4合并或剪切MIDI片段后開頭有奇怪的停頓或速度突變。原因每個軌道開頭可能有一個初始的time值即第一條消息的delta time如果這個值很大就會產生停頓。另外速度元消息set_tempo可能被錯誤地復制或丟失。解決在合并軌道時確保正確處理軌道開頭的初始time。通常在拼接時第二個軌道的第一個消息的time應該設為0或根據需要調整并將其delta time加到后續消息的時間上。同時確保最終文件只有一個全局速度軌道通常是第一個軌道或者所有軌道有統一的速度設置。5.3 性能與內存優化問題5處理非常大的MIDI文件如包含整個交響樂樂譜時速度慢或內存占用高。mido在讀取文件時會一次性將所有消息加載到內存的MidiTrack對象中。對于超大型文件這可能成為瓶頸。優化策略流式處理如果不需要隨機訪問所有消息可以邊讀取邊處理。mido的MidiFile在迭代時就是流式的但track對象已經加載完畢。對于極端情況可以考慮使用更低層的庫如python-midi但已不維護或直接解析二進制文件。選擇性加載如果你只關心特定通道或特定類型的消息可以在遍歷時快速跳過不關心的消息。使用copy()方法當需要修改消息時如調整力度直接修改msg.velocity會改變原始對象。如果后續還需要原始數據使用msg.copy()來創建副本進行操作。避免深度嵌套循環如果需要對音符進行復雜的關聯分析如查找和弦先將音符數據提取到更高效的數據結構如Pandas DataFrame或列表的列表中再進行操作比在龐大的消息列表上多次遍歷要快得多。# 示例流式處理僅統計C5以上高音音符的數量 def count_high_notes(file_path, threshold_note72): mid MidiFile(file_path) high_note_count 0 for track in mid.tracks: for msg in track: if msg.type note_on and msg.velocity 0 and msg.note threshold_note: high_note_count 1 return high_note_count5.4 與其他庫的協作問題6如何將MIDI數據用于機器學習或更復雜的音樂分析mido提供了底層的MIDI消息訪問。對于更高層的音樂表示如音符序列、和弦、節奏模式通常需要轉換成其他格式。常用方案Music21一個強大的音樂學分析庫可以直接讀取MIDI文件進行樂理分析調性、和弦、音階等。你可以用mido做預處理再用music21做分析。PrettyMIDI另一個優秀的庫專注于將MIDI文件轉換為易于操作的音符起止時間列表單位是秒并提供了生成、操作這些數據的接口非常適合音樂信息檢索MIR和機器學習。自定義表示如前文extract_note_sequence函數所示你可以將MIDI轉換成(pitch, start_time, duration, velocity)的列表或者進一步量化為基于固定時間網格的鋼琴卷簾矩陣一個二維數組行是音高列是時間步這是很多深度學習模型如MusicVAE, MuseNet的輸入格式。# 使用pretty_midi庫進行轉換示例 import pretty_midi import numpy as np def midi_to_piano_roll_matrix(midi_path, fs100): 將MIDI轉換為鋼琴卷簾矩陣每秒fs幀 pm pretty_midi.PrettyMIDI(midi_path) # 將所有樂器合成一個鋼琴卷簾 piano_roll pm.get_piano_roll(fsfs) # 形狀為 (128, 時間幀數) # 鋼琴卷簾的值是力度可以二值化或保留力度值 binary_pr (piano_roll 0).astype(np.float32) return binary_pr, fs處理MIDI文件就像在指揮一個數字樂隊mido給了你指揮棒和樂譜。從簡單的文件信息讀取到復雜的音樂生成算法理解其消息機制和時間系統是關鍵。我個人的體會是在開始一個音樂編程項目前先用mido寫個小腳本把目標MIDI文件的結構和內容打印出來仔細看看往往能發現一些意想不到的細節比如那些奇怪的控制器事件這比直接悶頭寫代碼要高效得多。最后多聽多試把生成的文件丟進DAW或播放器里實際播放是檢驗代碼效果最直接的方法。