
简介这是清华电子系大二小学期MATLAB音乐合成大作业的完整资源包面向电子信息类本科生、MATLAB初学者及音乐合成爱好者。资源以傅里叶级数理解和MATLAB基本指令训练为主线涵盖从单个do音生成、采样频率设定、包络控制到谐波叠加与多音轨合成的完整实验流程。包内共52个文件包括15个m源文件、10个wav音频结果、3个mat数据文件、2个pdf报告、2个json配置、mlx/mlapp交互程序及16张说明图片整体8.29MB目录按src、results、images清晰划分便于对照学习。已有1220人学习下载。除课程报告外还提供可运行的hw_x_x.m作业脚本、音乐合成所需Guitar.MAT等数据资源以及自定义函数读者可据此复现音乐片段、修改参数生成自己的作品并参考项目结构完成类似大作业。 但凡在电子系待过大概率都躲不开小学期大作业这几个字。我那年抽到的题目是用MATLAB做音乐合成要求最终交一段由程序生成的完整乐曲以及带声音的源代码。当时的第一反应是这玩意儿到底该从哪下手是找现成音频拼接还是能把简谱直接变成能响的音乐等到真正把流程跑通才发现这个作业本质上不是音乐题而是一道把信号系统、采样定理、离散波形构造串起来的综合题。这篇文章就把我当时的完整思路和可复现代码整理出来给正在被同款作业折磨的同学做一个参考。1. 接到题目先别急着写代码需求拆解与方案设计1.1 老师到底想考察什么很多同学把这类作业理解成写一个能播放音符的程序于是直接去搜别人做好的音乐库或者用midi之类的现成工具包生成音频。但我劝你冷静一下这不是产品开发是课设考核。它的核心目的不是让你做出一个能听的成品而是考察你有没有真正理解声音在计算机里是怎么被表达和重建的。用一句话概括音乐合成的本质是在离散时间轴上构造出符合听觉预期的连续信号近似。这里涉及三个关键考点采样率与奈奎斯特定理要知道为什么CD音质是 44100Hz而不是随便定的离散信号构造如何把频率、时长、音色这些音乐参数映射成一维数字数组信号叠加与归一化多声部合奏、和弦、左右声道的最终混音本质上是一次多信号叠加。如果你在答辩时能把这三个点说清楚哪怕曲子简单一点成绩都不会差。反之即便你用外部库把《卡农》渲染得再像被问到一个你是如何生成正弦波就卡壳那场面就很尴尬了。1.2 技术方案选型脚本架构怎么搭我当时的目标很明确既要一个像样的成品也要一份能被追问细节的代码。所以我选择了纯函数化脚本整个工程拆成三层乐谱解析层把简谱/音名表示转换成音符对象频率 起始时刻 时长 波形合成层根据音符对象生成对应离散波形 混音输出层多声部叠加、归一化、写入WAVMATLAB 的矩阵运算特性决定了它不像 C 语言那样适合写长循环所以核心合成函数一定要向量化。比如生成一段正弦波直接用sin(2*pi*f*t)这种写法而不是for循环逐点计算。至于曲子选什么我的建议是选一首你熟悉结构的、主旋律清晰的曲子。别上来就挑战管弦乐总谱那会让你在混音阶段怀疑人生。我选的是《小星星》加一段简单的八度伴奏既能把所有技术点覆盖又不会让声部堆叠失控。2. 从频率到声波合成一段能响的单音2.1 采样率、频点与时间轴的生成这是整个作业里最不能跳过的一步。计算机没法存连续的声音只能按固定时间间隔对声波拍照这个间隔的倒数就是采样率fs。采样率fs 44100意味着每秒钟要生成 44100 个离散点。为什么选这个数因为人耳听觉上限大约是 20kHz按奈奎斯特定理采样率必须大于两倍的最高频率也就是至少 40kHz才能无失真重建。惠普和索尼当年定 CD 标准时留了 1kHz 余量定成 44100这个数字后来成了音频行业的默认值。在代码里时间轴的生成方式是fs 44100; % 采样率单位 Hz duration 1.0; % 声音时长单位秒 t 0 : 1/fs : duration - 1/fs;这个t是一个长度为 44100 的向量代表从 0 秒到 1 秒内每隔 1/44100 秒采一个点。只要有了它频率为f的纯音就是y sin(2 * pi * f * t);2.2 音名频率映射从简谱到Hz乐谱里的 do re mi 只是相对音高真正给到波形合成器的是一个具体频率值。业内通用做法是使用 MIDI 标准把 A4标准音定为 440HzMIDI 编号为 69。任意音高的频率可以用这个公式算出function f midi2freq(midiNum) f 440 * 2^((midiNum - 69) / 12); end十二平均律里每差一个半音频率相差2^(1/12)倍。这个公式直接反映了音乐的十二平均律设计逻辑相邻半音之间是等比例关系而不是等差关系。这也是为什么我们听到的音阶从低到高越高越挤其实是听起来均匀物理上相邻频率间距却越来越大。实际写代码时我建了一个查找表把 C 调的七个音固定映射到频率names {C4,D4,E4,F4,G4,A4,B4,C5}; freqs [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]; freqMap containers.Map(names, freqs);C4 就是中央 C261.63Hz 是它的基频。为什么不是整数因为十二平均律是按倍频关系对数的等比切分出来的频率天生就带小数。你当然可以强行凑整但那样音准会有轻微偏差对于作业来说问题不大不过建议还是用标准值答辩时更经得起问。2.3 节拍换算用采样点数而不是秒来累计节拍的计算是很多人翻车的高发区。BPM 的意思是每分钟节拍数所以一拍时长为bpm 100; beatDuration 60 / bpm; % 秒/拍一个四分音符占一拍二分音符占两拍八分音符占半拍以此类推。我推荐在实际生成时把秒数先换算成采样点数再处理。原因是浮点数累加秒数会产生不可忽略的误差。假设曲长 3 分钟float 累加了上万个小数到结尾可能偏掉几十毫秒听感上就是节奏越来越慢或者越来越快。更稳的写法是全程用整数采样点推进samplesPerBeat round(beatDuration * fs); startSample round(startTime * fs); noteSamples round(noteDuration * fs);所有音的开头结尾都对齐到整数采样点上节奏的精确性在数学上是严格成立的。调试时只要打印每个音符的startSample和endSample就能一眼看出是否有重叠或空隙。3. 让声音有质感谐波叠加、包络与演奏感3.1 为什么纯正弦波听起来像电子闹钟如果你直接把上一步的正弦波拼成一段旋律播放你会得到一个非常电子、非常单薄的声音就像老式手机铃声。原因很简单真实乐器的声音从来不是单一频率而是由基频加一系列谐波组成的复合信号。基频决定音高谐波决定音色这是音乐合成里最重要的一句话。以钢琴为例按下 A4 琴键时弦并不是只有 440Hz 一个振动模式它同时还在 880Hz、1320Hz、1760Hz 等多个频率上振动只是幅度依次递减。人的听觉系统把基频 各次谐波的幅度比例综合起来解读就产生了这是钢琴的判断。所以在作业里想让合成旋律更像乐器一个性价比极高的做法是用多个整数倍频率的正弦波叠出一个复合波形。freq 261.63; % C4 基频 harmonics [1 2 3 4 5 6]; % 1~6 次谐波 amps [1 0.5 0.25 0.12 0.06 0.03]; t 0 : 1/fs : 0.5 - 1/fs; y zeros(size(t)); for k 1:length(harmonics) y y amps(k) * sin(2*pi*freq*harmonics(k)*t); end谐波次数越多、幅度衰减规律越靠近 1/n声音听感就越丰满。但谐波数量也不是越多越好超过 8 次以后人耳对这些极高频成分的敏感度已经很低反而容易引入刺耳感。做作业的话4 到 7 个谐波就足够撑起乐器感了。3.2 包络控制让音符冲出来而不是冒出来谐波能解决音色问题却解决不了起音太突兀的问题。如果一段正弦波从零瞬间跳到满幅度再瞬间掉回零会产生明显的咔哒声click这是因为波形在两端发生了阶跃跳变。这也是专业合成器和我们随手正弦波的最大差别他们用一条包络线控制幅度的变化轨迹。最经典的模型叫 ADSR即 Attack起音、Decay衰减、Sustain保持、Release释音。function env adsr_env(nTotal, attackSamples, decaySamples, sustainLevel, sustainSamples, releaseSamples) env zeros(1, nTotal); idx 1; % Attack: 线性上升 for n 1:attackSamples env(idx) n / attackSamples; idx idx 1; end % Decay: 从1降到sustainLevel for n 1:decaySamples env(idx) 1 - (1 - sustainLevel) * n / decaySamples; idx idx 1; end % Sustain: 保持 env(idx:idxsustainSamples-1) sustainLevel; idx idx sustainSamples; % Release: 降到0 for n 1:releaseSamples env(idx) sustainLevel * (1 - n / releaseSamples); idx idx 1; end endAttack 时间很短钢琴 5~10ms 就能感知到琴槌击弦的冲击感弦乐的 Attack 则慢得多可能有 100~300ms这决定了发音乐器的起奏软硬。Release 时间也不能忽略遇到连续快速音符时如果 Release 太长前一个音的尾巴会和后一个音的头部叠在一起糊成一片。3.3 加一点颤音旋律立刻活过来纯粹控制幅度还不够演奏感还来自音高上的微小波动。真实人声或弦乐在长音上不会完全平稳而是有 ±0.3%~0.5% 的周期性摆动这个现象叫做颤音。给信号加入轻微颤音后长音的死感会大幅减少。代码层面可以在相位中加一个正弦调制项vibratoRate 5; % 颤音速率 5Hz vibratoDepth 4; % 频偏 4Hz t 0 : 1/fs : dur - 1/fs; y sin(2*pi*freq*t 2*pi*vibratoDepth/vibratoRate * sin(2*pi*vibratoRate*t));这里的原理是把相位做一个周期为1/vibratoRate的摆动使瞬时频率围绕freq上下浮动。幅度我建议用频率偏移量Hz而不是百分比这样对高音和低音的感知影响比较均匀。钢琴一般不用颤音但长笛、小提琴非常依赖这个如果你做的曲子里有长音加上它效果特别明显。4. 从旋律到编曲和弦、多声部与归一化4.1 用音程堆出和弦音乐不是单线条的给你作业加分的一个方向就是加上伴奏或和弦。和弦的本质是多个频率按一定音程关系同时发声。C 大三和弦由 C、E、G 三个音构成即根音、大三度、纯五度频率比大约是 4:5:6。生成和弦有个细节如果三个音都从零开始立刻满幅它们叠加后的总幅值可能是单个音的 3 倍如果你后面不对整体做归一化音量直接爆掉。建议给和弦内每个音符做独立的、极短的 Attack 窗1~3ms并适当降低每路幅度确保叠加后不削波。如果曲子里的伴奏是根音 五音 高八度根音本质上就是两次叠加可以在一个循环里生成三条波形再相加最后统一归一化。4.2 立体声混音左右声道怎么摆单声道能听但听起来层次感弱。建议把主旋律放右声道、伴奏放左声道这样二者在频段上有重叠时并不会因为相位干涉而互相吃掉。MATLAB 中生成立体声的写法是构造一个2 x N的矩阵第一行左声道、第二行右声道stereo [leftChannel; rightChannel];需要注意audiowrite对输入格式很挑剔矩阵必须是N x 2行是采样点列是声道所以要把上面的2 x N转置一下stereo [leftChannel; rightChannel]; audiowrite(output.wav, stereo, fs);如果你想让伴奏更远一点可以再给伴奏声部加一个低通滤波比如用lowpass函数高频细节少一些听感上就靠后了。这一步不是必须但属于不错的加分细节。4.3 归一化爆音的根源与解法把多个声部直接相加几乎一定会有某些瞬间总幅度超过 1。而 WAV 文件在写入时超过 1 的采样点会被直接削平这个现象叫 clipping听感就是破音。解决办法分两步。第一步在所有声部叠加前给每个声部乘一个小于 1 的权重比如旋律 0.7、伴奏 0.5保证总响度相对均衡。第二步在最终输出前做一次整体归一化y y / max(abs(y)) * 0.9;把全曲峰值修正到 0.9然后用这个结果写入 WAV。注意归一化必须在混音之后做不能每个音符单独做。每个音单独归一化会抹平音符间的音量对比听感变成机械的一刀切毫无强弱起伏。5. 一份能跑的完整代码简谱到WAV的流水线下面这份代码是我那版作业的简化版本但核心流程是完整的。它读取一段用矩阵表示的简谱生成旋律和伴奏两个声部混音后输出 WAV 文件。%% 基本参数 fs 44100; bpm 90; beat 60 / bpm; %% 音名频率表 noteName {C3,G3,C4,D4,E4,F4,G4,A4,B4,C5}; freq [130.81, 196.00, 261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]; noteFreq containers.Map(noteName, freq); %% 旋律简谱: {音名, 相对拍数} melody { C4, 1; C4, 1; G4, 1; G4, 1; A4, 1; A4, 1; G4, 2; F4, 1; F4, 1; E4, 1; E4, 1; D4, 1; D4, 1; C4, 2 }; %% 伴奏简谱: {音名, 相对拍数} bass { C3, 2; G3, 2; C3, 2; C3, 2 }; %% 旋律合成 melodyWave []; for i 1:size(melody, 1) f noteFreq(melody{i, 1}); dur melody{i, 2} * beat; t 0 : 1/fs : dur - 1/fs; y synth_tone(f, t, fs, piano); melodyWave [melodyWave, y]; end %% 伴奏合成 bassWave []; for i 1:size(bass, 1) f noteFreq(bass{i, 1}); dur bass{i, 2} * beat; t 0 : 1/fs : dur - 1/fs; y synth_tone(f, t, fs, bass); bassWave [bassWave, y]; end %% 时间对齐把旋律循环两遍伴奏重复四遍 melodyLoop repmat(melodyWave, 1, 2); bassLoop repmat(bassWave, 1, 4); len min(length(melodyLoop), length(bassLoop)); leftChannel bassLoop(1:len) * 0.6; rightChannel melodyLoop(1:len) * 0.8; stereo [leftChannel; rightChannel]; stereo stereo / max(abs(stereo)) * 0.9; audiowrite(output.wav, stereo, fs); disp(WAV 文件已生成);核心合成函数synth_tonefunction y synth_tone(f, t, fs, type) switch type case piano harmonics [1 2 3 4 5]; amps [1 0.4 0.2 0.1 0.05]; y zeros(size(t)); for k 1:length(harmonics) y y amps(k) * sin(2*pi*f*harmonics(k)*t); end % 指数衰减包络模拟钢琴 env exp(-2.5 * t / max(t)); y y .* env; case bass % 低音区减少谐波避免混浊 y sin(2*pi*f*t) 0.3 * sin(2*pi*2*f*t); env exp(-1.5 * t / max(t)); y y .* env; otherwise y sin(2*pi*f*t); end y y / max(abs(y)) * 0.8; end运行这段代码前先把所有自定义函数放在同一个目录或者作为脚本体后面的局部函数保存。输出output.wav后你可以用任何播放器听也可以用sound(stereo, fs)直接在 MATLAB 里播放。实际的乐谱解析可以比这个更工程化比如支持点音符6.、连音线-、休止符0但核心判断逻辑都是一样的遇到一个音符算出它的频率、起始采样点和持续采样点然后生成波形并填入对应的时间槽位。你完全可以在这个框架上扩展成完整简谱解析器。6. 实测验证与高频翻车现场6.1 用波形和频谱验证你的合成结果音频类作业最怕听起来还行但说不出所以然。答辩前建议用三个工具自检。第一个是时域波形图。用plot(t, stereo(:,1))查看左声道波形你应该能看到每个音符的幅度包络变化。如果波形在某处突然截成平顶说明削波了去检查归一化。第二个是频域分析。对音频信号做 FFT观察频谱峰值是否落在预期的基频位置。比如 C4 是 261.63Hz频谱图应该在 261Hz 附近出现第一个峰在 523Hz、784Hz 附近有逐渐衰减的谐波峰。这一步正好用上了信号课里的 DFT 知识答辩时非常加分。如果波形被打包导成 CSV用readmatrix读进来再fft完全绕得通我就试过把导出的音频数据丢进脚本里做频谱分析。第三个是听感检查。戴上耳机而非外放因为笔记本扬声器对低频响应差低音伴奏可能完全听不见让你误判混音比例。6.2 音符衔接爆音的排查链路我最开始衔接音符时出现了一个非常典型的问题每个音符明明是正弦波但两个音之间总有一声清脆的咔嗒。排查过程是这样的。首先打印出故障处的波形发现后续波形在拼接点左右产生了幅度跳变——前一个音结尾时幅度是 0.7后一个音开头幅度是 0.1硬接在一起就形成了向下的阶跃。阶跃在频域上对应着一堆高频分量所以耳朵听到的是咔嗒而不是过渡。定位后我用了两个方法修复。第一确保每个音符都从包络 0 开始、到包络 0 结束这样即使相位不连续幅度也已经归零不会有阶跃。第二给每个音符开头加一个极短的线性 attack 窗即便包络没归零也能抹平跳变。这两招叠加后咔嗒声彻底消失。6.3 数据保存与别人机器的兼容性坑还有个小坑值得提一下MATLAB 版本不同某些音频函数的行为会有差异。audiowrite在 R2016a 之后才推荐使用旧版用的是wavwrite。如果你要在学校机房的旧版本上跑演示最好提前检查ver确认版本或者用audiowrite并在注释里标明需要的版本。再有就是.wav文件的位深。audiowrite默认可能是 16bit如果你传入的是 double 类型且数值范围在 [-1,1]它内部会帮你转成整型。如果播放时发现音量很小或波形明显失真多半是数值范围没归一化到 [-1,1] 就写出了。最后再说一个我自己的体会做完这个作业后我对数字音频这回事彻底祛魅了。它无非就是以固定间隔记录振幅的一长串数字至于它是音乐还是噪音完全取决于你怎样组织这些数字。花两天时间把正弦波、谐波、包络、混音这些概念串起来后面你再碰到任何音频处理相关的项目看到采样率和 FFT 就觉得老朋友见面。如果这文章帮你少走了几步弯路那这份作业就值得了。本文还有配套的精品资源点击获取