基于十二平均律与ADSR包络的Matlab音乐合成实现
简介这份基于Matlab的音乐合成大作业源码与文档包适用于高校信号处理、计算机音乐或MATLAB编程相关课程的期末设计也适合需要参考完整项目思路的学习者。资源已通过本地编译运行评审分达98分难度适中内容经助教审定可直接作为课程设计或大作业的参照方案。压缩包共49个文件、约48.9MB核心为20个m脚本文件涵盖音符生成、旋律编排、播放控制等完整流程另含12张程序运行效果图、3个wav示例音频、5个json配置及mat文件便于观察数据结构和调试结果附带的pdf与md文档对项目背景、实现逻辑和操作方式做了说明fig文件则保存了可视化界面。目前已有124人学习下载。通过这份资源使用者能清晰理解从乐谱解析到波形合成、再到播放输出的全链路实现并可根据注释与文档快速修改参数、替换曲目对掌握Matlab音频处理技巧和提升综合编程能力都有实际帮助。1. 同样是 Matlab 音乐合成大作业为什么有人 95 有人 75最典型的低分实现是先把 1 到 7 写进一个数组再用 for 循环逐个sin播放最后能听出旋律就交卷。这种写法不是不能运行而是把音高、时值、音色全部揉在了一个循环里换一首歌要改代码换一个音色要改代码参数为什么是这个数也说不清楚。课程大作业评到 95 分以上通常不是旋律选得多复杂而是代码结构把“音高怎么算、单音怎么发、整曲怎么拼、文档怎么证”拆开了。这篇内容就按这条路线完整过一遍从十二平均律的频率映射开始写一个带 ADSR 包络的单音合成函数再组织整首旋律和配套文档最后处理采样率、削波和听感微调。适合正在做 Matlab 音乐合成大作业的本科生也适合想看看合成函数怎么写才算有工程感的熟手。2. 十二平均律与音符频率生成的 Matlab 实现先把音高算准2.1 音乐合成里真正要建模的三个量计算机合成音乐最底层只需要三个量音高对应基频频率时值对应持续时长力度对应振幅大小。拿高分的大作业源码一般都把这三件事拆成独立函数一个负责音符到频率的映射一个负责生成单个音符的波形一个主脚本负责按乐谱组装。多写两个函数看似绕远但调试时能单独验证每一层答辩时也能明确说出模块之间的接口这就是工程感和脚本感的区别。采样率是第四个隐藏参数。它不直接出现在乐谱里但决定时间轴怎么离散化。写代码前先定一个fs比如 44100Hz 或 22050Hz后面所有函数都共用这个变量不要在每个函数里散落写 8000、16000。采样率不一致是合成音乐最常见的低级错误声音会变调而且极难排查。2.2 用 midi2freq 一次算出整段旋律的频率十二平均律里每升高一个半音频率乘以 2 的 1/12 次方。国际通用基准是 A4 等于 440Hz。把 MIDI 音符号 69 定义为 A4其余音符号与频率的关系就是一个幂函数function f midi2freq(midi_note, ref_freq, ref_midi) % midi_note 可以是标量也可以是向量 % ref_freq 是基准音频率默认 A4 440Hz % ref_midi 是基准音对应的 MIDI 编号默认 69 if nargin 2 ref_freq 440; end if nargin 3 ref_midi 69; end f ref_freq * 2.^((midi_note - ref_midi) / 12); end逻辑说明2.^用的是点乘方因为midi_note可能是向量需要对每个元素单独求幂如果写成2^((midi_note - ref_midi) / 12)Matlab 会尝试矩阵幂运算输入向量时直接报错。函数里用nargin设置默认值调用者只传一个参数也能工作这属于 MATLAB 函数式编程里值得保留的习惯。各参数含义如下表参数默认值作用midi_note必填MIDI 音符号支持标量或向量ref_freq440基准音实际频率单位 Hzref_midi69基准音符号固定对应 A4调用方式可以直接传入一句旋律的 MIDI 编号数组melody_midi [60, 62, 64, 62, 60, 60, 62]; freqs midi2freq(melody_midi);这就是《小星星》前两句的音高序列。把乐谱存成 MIDI 编号而不是直接存频率好处是转调时只需要整体加减一个数全部加 12 就是高八度全部减 2 就是降一个全音不用重新计算频率表。我在写大作业时会专门用一个变量保存整个旋律的 MIDI 序列频率只在使用时换算这样报告里写“转调实验”也更容易演示。2.3 常用音符的 MIDI 编号与频率对照表做文档说明时放一张对照表比放一大段公式直观得多。下表按 A4440Hz 计算MIDI 编号音名频率Hz60C4261.6362D4293.6664E4329.6365F4349.2367G4392.0069A4440.0071B4493.8872C5523.25注意这张表只对 440Hz 基准成立。如果用 442Hz 或 432Hz 调音表中所有值都会整体平移所以不要在文档里硬背频率直接写midi2freq的计算结果更稳妥。3. 带 ADSR 包络的 Matlab 音符合成函数从响到像乐器3.1 ADSR 每个阶段解决听感上的哪个问题裸的sin波从头到尾振幅恒定听起来是电子蜂鸣器不是乐器。要让声音“像什么”关键在包络。ADSR 是 Attack、Decay、Sustain、Release 四个阶段的缩写Attack 是起音时间决定声音从零到峰值的速度Decay 是峰值回落到延音电平的时间Sustain 是按住音符期间维持的电平Release 是松开后声音衰减到零的时间。这四个参数直接决定音色方向。A 取很短时声音干脆接近钢琴、吉他这类敲击乐器A 拉到 0.1 秒以上就有弦乐缓慢“拉起来”的感觉。D 阶段决定音符的敲击分量D 越大音头越圆。S 设成 1 就是管风琴那种按住一直不衰减的效果S 设成 0 就接近打击乐。R 在课程作业里经常被忽略但它恰恰是消除爆音的关键Release 让波形在结尾平滑归零拼接下一音时不会因为电平跳变产生咔哒声。3.2 一个可复用的 adsr_tone 函数把单音合成封装成一个独立函数输入频率、时长、采样率和 ADSR 参数输出一段带包络的波形。这是整个大作业源代码里的核心模块function s adsr_tone(freq, dur, fs, A, D, S, R) % 生成单音正弦载波 x ADSR 包络 % freq: 基频(Hz) % dur: 音符总时长(s) % fs: 采样率(Hz) % A/D/R: attack, decay, release 时长(s) % S: sustain 电平取值 0~1 n round(dur * fs); t (0:n-1) / fs; x sin(2 * pi * freq * t); na max(1, round(A * fs)); nd max(1, round(D * fs)); nr max(1, round(R * fs)); sustain_len n - na - nd - nr; if sustain_len 0 % 音符太短压缩 decay 给 release 留空间 nd max(1, floor((n - na - nr) / 2)); sustain_len n - na - nd - nr; end env zeros(1, n); env(1:na) linspace(0, 1, na); pos na; env(pos1:posnd) linspace(1, S, nd); pos pos nd; if sustain_len 0 env(pos1:possustain_len) S; pos pos sustain_len; end if pos n env(pos1:n) linspace(S, 0, n - pos); end s x .* env; s s / max(abs(s)) * 0.8; end逻辑说明包络的每个阶段都用linspace生成而不是用ones和zeros硬拼接目的是保证相邻两段的端点连续。数字音频里端点跳变会产生高频分量听感就是爆音。函数里还处理了一个边界当音符时值很短A、D、R 之和超过总时长时压缩 decay 长度来保证 release 存在否则env会出现索引越界或全零段。这个边界处理是答辩时老师最喜欢问的点之一。末尾归一化到 0.8 而不是 1.0是因为后面多个音叠加时峰值可能超过 1提前留出余量可以避免最终削波。如果每个音都先归一到 1再叠加混音结果几乎必然 clip。参数推荐起始值参数推荐起始值调参方向A0.01 s调大让起音更柔和D0.15 s调大让音头更圆润S0.6调小更像弹拨乐器R0.2 s调小防糊调大防 click3.3 拼接音符时别把 click 也拼进去单音函数写好后最容易翻车的地方是拼接。常见误用是把两段波形直接用[y1, y2]连起来如果前一段结尾没有归零拼接点就有电平跳变听感上是“哒”的一声。处理方法是保证每个音符的包络最后都落到零也就是 release 参数必须大于零。休止符确实可以用补零实现但只能加在完整的音符之后如果音符本身没弹完就硬接下一音截断产生的毛刺比休止符缺位更明显。这里有一个更容易忽略的问题拼接循环里不要每拼一个音就整体归一化一次。每次s / max(abs(s))会把弱音也拉满力度信息全丢。正确做法是每个音内部只控制包络和力度系数所有音都拼完后再整体限幅。4. 结构化编排整首旋律与配套文档源代码和答辩各占一半4.1 用 struct 数组写乐谱而不是塞进一堆全局变量主脚本里逐句写mix [mix, adsr_tone(...)]也能跑但换一首歌就要从头改代码。把乐谱定义成一张“音符表”每个音符包含 MIDI 编号、时值、力度代码就变成了纯粹的数据驱动notes struct(midi, {60, 62, 64, 62, 60, 60, 62}, ... dur, {0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0}, ... vel, {0.9, 0.8, 0.8, 0.8, 0.8, 0.7, 0.9}); fs 44100; mix zeros(1, 0); for k 1:numel(notes) f midi2freq(notes(k).midi); s adsr_tone(f, notes(k).dur, fs, 0.01, 0.15, 0.6, 0.2); mix [mix, s * notes(k).vel]; end mix mix / max(abs(mix)); sound(mix, fs);逻辑说明struct(midi, {...}, dur, {...})的写法创建了一个长度为 7 的结构体数组每个元素是一个音符。notes(k).midi取第 k 个音符的音符号notes(k).dur取时值。这种组织方式下乐谱和合成逻辑完全分离演示时改notes数组的内容就能换歌不需要动合成函数。拼接用[mix, s]在数据量小的时候完全够用。课程作业通常只有几十个音符总样本数在几十万量级Matlab 的拼接开销可以忽略。不要为了性能写复杂的预分配逻辑代码可读性更重要。遇到休止符时不要跳过否则后面的音符会提前出现。可以在结构体数组里增加一个rest字段遇到时执行mix [mix, zeros(1, round(rest_dur * fs))]这样节拍才准。4.2 文档说明的章节与评分点对照文档占大作业分数的一半甚至更多。95 分以上的报告通常不需要长篇大论但每一章都要能回答一个具体的“为什么”文档章节应包含内容回答的答辩问题问题描述与乐理基础十二平均律、MIDI 编号、包络概念频率为什么是 2 的 1/12 次方系统设计函数划分、数据流、波形图或频谱图模块接口怎么定义参数实验不同 ADSR 下波形对比、试听结论参数为什么取这些值测试与问题削波、click 噪声、时值偏差的处理出错后怎么排查使用说明如何换歌、换音色、改速度能不能现场演示改参数文档里至少放两张图一张plot(t, s)的时域波形图一张abs(fft(s))的频谱图。这两张图能让老师一眼看出你理解“包络在时域上改变振幅、基频决定频谱峰值位置”这两个基本点。对 95 分以上的作业工程感往往就体现在这种地方。4.3 源代码打包前的文件组织按 zip 提交时文件组织本身就是第一印象。建议结构如下src/ main.m % 入口定义乐谱并调用合成函数 midi2freq.m % 音符到频率映射 adsr_tone.m % 单音合成带 ADSR 包络 play_melody.m % 可选封装整曲生成流程 doc/ 报告.pdf 参数实验记录.md README.md大作业不需要复杂的包管理所有.m文件放在同一目录下即可。注意文件名必须和函数名一致主脚本不要用中文文件名。Matlab 对中文路径支持不稳定文件路径里带中文时sound或audiowrite可能无声报错这也是源码包最常见的问题之一。5. 音乐合成播放前的最后一公里采样率、削波与听感微调5.1 用三个命令判断成品音频能不能交交作业前先输出一份 WAV 文件并检查两个指标audiowrite(result.wav, mix, fs); info audioinfo(result.wav); fprintf(时长 %.2fs峰值 %.3f\n, info.Duration, max(abs(mix)));audiowrite默认按 16 位 PCM 写入超过 0.999 的样本会被硬削波播放时出现失真。因此检查max(abs(mix))是否显著小于 1 是必不可少的步骤。info.Duration用来核对总时长是否等于所有音符时值加上休止符的和如果偏短多半是某个音符被截断了。5.2 加一个不出戏的颤音先频率调制再积分相位给长音加颤音能让听感自然不少。常见误用是直接写成sin(2 * pi * inst_freq .* t)这在数学上是错的因为瞬时频率不能直接乘时间求相位正确做法是对瞬时频率做积分。Matlab 里用cumsum实现最方便vib_freq 5.5; % 颤音频率接近人声自然颤音 vib_depth 0.005; % 频偏比例0.5% 即可 inst_freq freq * (1 vib_depth * sin(2 * pi * vib_freq * t)); phase 2 * pi * cumsum(inst_freq) / fs; wave sin(phase);cumsum(inst_freq) / fs本质上是相位对时间的离散积分结果再用2 * pi缩放得到弧度相位。vib_depth不宜超过 0.02否则音高漂移会明显跑调。对低音区 0.5% 已经足够对高音区可以适当降到 0.003。5.3 混响用一条延迟线就能有空间感最简单有效的空间感来自一条带反馈的延迟线也就是 comb filter 的最简形态delay round(0.06 * fs); % 60ms 延迟 wet zeros(1, numel(mix) delay); wet(1:numel(mix)) mix; wet(delay1:end) wet(delay1:end) 0.35 * mix; wet wet / max(abs(wet));这段代码把原信号延迟 60ms 后以 0.35 的系数叠加回原信号相当于一次早期反射。参数不要调大延迟超过 100ms 会变成明显的回声反馈系数超过 0.5 会出现金属感共振。作为答辩试听 demo这个效果足够说明你理解“反射和混响在时域上就是对原信号的延迟叠加”。本文还有配套的精品资源点击获取