Python mido库实战:从零解析与生成MIDI音乐文件

📅 2026/8/25 18:24:16
Python mido库实战:从零解析与生成MIDI音乐文件
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或播放器里实际播放是检验代码效果最直接的方法。