news 2026/9/16 19:16:53

基于十二平均律与ADSR包络的Matlab音乐合成实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于十二平均律与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基准音实际频率,单位 Hz
ref_midi69基准音符号,固定对应 A4

调用方式可以直接传入一句旋律的 MIDI 编号数组:

melody_midi = [60, 62, 64, 62, 60, 60, 62]; freqs = midi2freq(melody_midi);

这就是《小星星》前两句的音高序列。把乐谱存成 MIDI 编号而不是直接存频率,好处是转调时只需要整体加减一个数:全部加 12 就是高八度,全部减 2 就是降一个全音,不用重新计算频率表。我在写大作业时会专门用一个变量保存整个旋律的 MIDI 序列,频率只在使用时换算,这样报告里写“转调实验”也更容易演示。

2.3 常用音符的 MIDI 编号与频率对照表

做文档说明时,放一张对照表比放一大段公式直观得多。下表按 A4=440Hz 计算:

MIDI 编号音名频率(Hz)
60C4261.63
62D4293.66
64E4329.63
65F4349.23
67G4392.00
69A4440.00
71B4493.88
72C5523.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(pos+1:pos+nd) = linspace(1, S, nd); pos = pos + nd; if sustain_len > 0 env(pos+1:pos+sustain_len) = S; pos = pos + sustain_len; end if pos < n env(pos+1:n) = linspace(S, 0, n - pos); end s = x .* env; s = s / max(abs(s)) * 0.8; end

逻辑说明:包络的每个阶段都用linspace生成,而不是用oneszeros硬拼接,目的是保证相邻两段的端点连续。数字音频里,端点跳变会产生高频分量,听感就是爆音。函数里还处理了一个边界:当音符时值很短,A、D、R 之和超过总时长时,压缩 decay 长度来保证 release 存在,否则env会出现索引越界或全零段。这个边界处理是答辩时老师最喜欢问的点之一。

末尾归一化到 0.8 而不是 1.0,是因为后面多个音叠加时,峰值可能超过 1,提前留出余量可以避免最终削波。如果每个音都先归一到 1,再叠加,混音结果几乎必然 clip。

参数推荐起始值:

参数推荐起始值调参方向
A0.01 s调大让起音更柔和
D0.15 s调大让音头更圆润
S0.6调小更像弹拨乐器
R0.2 s调小防糊,调大防 click

3.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 对中文路径支持不稳定,文件路径里带中文时,soundaudiowrite可能无声报错,这也是源码包最常见的问题之一。

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(delay+1:end) = wet(delay+1:end) + 0.35 * mix; wet = wet / max(abs(wet));

这段代码把原信号延迟 60ms 后以 0.35 的系数叠加回原信号,相当于一次早期反射。参数不要调大:延迟超过 100ms 会变成明显的回声,反馈系数超过 0.5 会出现金属感共振。作为答辩试听 demo,这个效果足够说明你理解“反射和混响在时域上就是对原信号的延迟叠加”。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 19:16:38

YOLO11-seg结合CBAM与GhostConv的裂缝检测分割轻量化实践

裂缝检测在桥梁、隧道、路面养护里一直是刚需&#xff0c;传统做法要么靠人工目检&#xff0c;要么用U-Net这类全卷积网络做像素级分割。人工效率低&#xff0c;U-Net虽然精度还行&#xff0c;但模型重、推理慢&#xff0c;放到边缘设备上很容易吃瘪。所以当我决定做一个既能分…

作者头像 李华
网站建设 2026/9/16 19:15:21

解决realme手机微信多文件分享限制的3种方法

1. 问题背景与场景还原作为一名长期使用realme手机的老用户&#xff0c;我最近遇到了一个相当恼人的文件分享问题。事情发生在2026年3月18日晚上&#xff0c;当时我需要通过微信给同事发送多个工作文档——包括PDF报告、Excel表格和Word文档。按照常规操作&#xff0c;我打开了…

作者头像 李华
网站建设 2026/9/16 19:14:37

Codex 跑 Git 项目管理 Agent:Key 用 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华