
1. 项目概述当数学的优雅遇见声音的律动作为一名在音乐科技和创意编程领域摸爬滚打了十多年的老玩家我见过太多将算法与艺术结合的尝试。有的过于晦涩成了数学家的自娱自乐有的又过于肤浅只是给随机音符套了个“智能”的壳子。直到我深入实践了“用斐波那契数列进行算法作曲”才真正体会到一种恰到好处的平衡——它既有严谨的数学内核作为骨架又能催生出充满惊喜和音乐性的听觉体验。这个项目听起来可能有点“极客”但它的魅力在于你不需要是数学博士或作曲大师只要会用一些基础的编程工具比如我们今天要重点聊的minim音频库就能亲手创造出独一无二的算法音乐。简单来说这个项目的核心就是利用斐波那契数列0, 1, 1, 2, 3, 5, 8, 13, 21...中蕴含的“黄金比例”美感来驱动音乐中的各种参数比如音符的音高、时值、节奏间隔甚至是和声的构成。斐波那契数列在自然界中无处不在从向日葵的种子排列到鹦鹉螺的螺旋这种内在的和谐与美感正是我们希望在音乐中捕捉和再现的。而minim作为一个轻量级且易于上手的Java音频库为我们提供了将数字序列实时转化为可听声音的完美桥梁。它处理音频输入输出、生成合成器、加载音源文件的能力让音乐逻辑的实现变得直接而高效。无论你是对生成式艺术感兴趣的创意程序员想为游戏或新媒体装置设计动态音效的开发者还是单纯想探索音乐创作新可能的音乐爱好者这个项目都能为你打开一扇新的大门。它解决的不仅仅是一个“如何用代码写歌”的技术问题更是一个“如何为抽象规则赋予情感和生命力”的创意问题。接下来我会带你从设计思路到代码实现完整走一遍这个融合了数学、编程与音乐的创作之旅。2. 核心思路与设计哲学为何是斐波那契在动手写代码之前我们必须先想清楚为什么选择斐波那契数列它到底能给音乐带来什么别的随机数或序列无法替代的特质盲目地把数列数字映射到音符上只会得到一堆杂乱无章的声音那不是音乐是噪音。我们的目标是“算法作曲”核心在“作曲”算法只是工具。2.1 斐波那契数列的音乐性潜力解析斐波那契数列的魅力在于其相邻两项的比值会越来越趋近于黄金比例φ约1.618。这个比例在视觉艺术、建筑学中被公认为能带来美感。在音乐中这种比例关系可以转化为令人舒适的张力与解决、期待与满足。结构生成我们可以用数列中的数字来决定乐句的长度。例如前几个数字[1, 1, 2, 3, 5, 8]可以定义一个段落的结构1小节的引子1小节的主题陈述2小节的展开3小节的对比5小节的华彩8小节的再现与结尾。这种非对称但又有内在联系的结构比常见的44对称结构更具流动性和叙事感。音高与音阶映射直接将大数字如13, 21映射到钢琴键序号0-127会超出常用音域且毫无调性可言。更聪明的做法是取模运算。例如对一个MIDI音阶数组比如C大调音阶[60C4 62D4 64E4 65F4 67G4 69A4 71B4]进行索引。计算斐波那契数 % 音阶数组长度这样得到的索引永远在音阶内保证了旋律的调性中心同时数列的增长带来了音高循环的“滞后”效果产生既熟悉又新奇的旋律线。节奏与时值设计数列本身就可以作为时值的比例基础。假设一个基础时间单位是八分音符0.5秒那么数列[1, 1, 2, 3, 5]就可以生成一组时值为[0.5s 0.5s 1.0s 1.5s 2.5s]的节奏型。长短音符的分布自然形成一种张弛有度的律动避免了机械的均匀节奏。动态与音色参数数列还可以控制音量velocity、滤波器截止频率filter cutoff或效果器参数。例如将数列值归一化到0-1之间然后映射到音量上可以创造出一种有数学依据的、波浪起伏般的动态变化比简单的随机摆动更有逻辑和层次。注意切忌将数列直接、线性地映射到所有参数。音乐需要对比、重复和变化。通常我会用数列的一个子集或经过变换如取模、归一化、差分后的结果来控制某一类参数同时结合一些简单的音乐规则如避免过大的音程跳跃、解决到主音等。2.2 Minim库在项目中的角色与优势选择为什么选用Minim而不是其他音频库如Tone.jsWeb、SuperCollider或Pure Data这基于几个实际的考量生态与语言亲和性Minim基于Java与Processing创意编程环境集成度极高。Processing本身就是为了视觉艺术和交互设计而生社区庞大有大量图形化示例可供参考。对于想要同时实现视听联觉例如用图形可视化音乐的创作者来说MinimProcessing是黄金组合。上手门槛低Minim的API设计非常直观。创建一个音频输出、加载一个采样、播放一个音符往往只需要几行代码。它帮你封装了底层复杂的音频线程管理和资源加载让你能快速聚焦在音乐算法逻辑本身。功能足够全面对于这个项目我们需要的基本功能Minim都具备音频输出AudioOutput声音的最终出口。合成器Synthesizer可以生成正弦波、方波、锯齿波等基本波形用于创作电子音色。采样播放器AudioPlayer可以加载WAV、AIFF等格式的采样文件如钢琴、鼓组音色实现更真实的乐器声音。效果器Effects如混响Reverb、延迟Delay、滤波器Filter能为生成的干涩声音添加空间感和色彩。实时性与交互性Minim能很好地处理实时音频流。这意味着我们可以根据鼠标位置、键盘按压或者传感器数据实时改变斐波那契数列的生成规则或映射参数让音乐变成可交互的体验而不仅仅是预渲染的音频文件。当然Minim也有其局限比如对非常复杂的合成、粒子合成或频谱操作支持较弱。但对于我们“算法作曲”的探索阶段它的轻便和直接是最大的优点。3. 系统构建与核心模块实现理论说得再多不如一行代码。我们来搭建一个最核心的、能够播放由斐波那契数列生成旋律的Minim应用。我会分步骤拆解并解释每一部分的设计意图。3.1 环境准备与Minim基础设置首先确保你有一个Java开发环境。我强烈推荐使用Processing IDEprocessing.org因为它默认就包含了Minim库无需额外配置。如果你用其他IDE如IntelliJ IDEA或Eclipse需要手动将Minim的JAR包添加到项目依赖中。创建一个新的Processing项目第一件事就是导入Minim并初始化音频对象。import ddf.minim.*; import ddf.minim.ugens.*; Minim minim; AudioOutput out; void setup() { size(800, 600); // 创建一个窗口用于可能的可视化 // 初始化Minim this代表当前的PApplet对象 minim new Minim(this); // 获取系统的音频输出采样率44100缓冲区大小1024是个不错的起点 out minim.getLineOut(Minim.STEREO, 2048); // 这里可以初始化你的斐波那契数列生成器和音乐参数 setupMusic(); } void draw() { // 主循环可以在这里进行可视化绘制或实时参数更新 background(0); // ... 可视化代码 ... } // 记得在程序结束时关闭Minim释放音频资源 void stop() { out.close(); minim.stop(); super.stop(); }实操心得getLineOut的缓冲区大小第二个参数需要权衡。较小的缓冲区如512能降低音频延迟对交互实时性要求高的应用有利但可能增加CPU负载并在某些机器上导致爆音。较大的缓冲区如2048或4096更稳定。建议从2048开始如果听到爆音就调大如果感觉交互有延迟就调小直到找到平衡点。3.2 斐波那契数列生成器与音乐参数映射接下来我们构建数列生成的核心逻辑。我们不一次性生成无限长的数列而是按需生成并定义一个映射函数将数列值转化为音乐参数。// 定义一些音乐相关的全局变量 int fibIndex 0; int[] fibSequence; // 存储生成的数列 int scale[] {60, 62, 64, 65, 67, 69, 71, 72}; // C大调音阶 MIDI音符编号 float baseDuration 0.3; // 基础时值单位秒 float lastNoteTime 0; // 记录上一个音符触发的时间 float noteInterval 0.5; // 音符触发的基本间隔 void setupMusic() { // 初始化一个足够长的斐波那契数组这里生成前20项 fibSequence generateFibonacci(20); println(斐波那契数列前20项: ); println(fibSequence); } // 生成斐波那契数列的函数 int[] generateFibonacci(int n) { int[] fib new int[n]; if (n 0) fib[0] 0; if (n 1) fib[1] 1; for (int i 2; i n; i) { fib[i] fib[i-1] fib[i-2]; } return fib; } // 核心映射函数将斐波那契数映射到MIDI音符 int mapFibToNote(int fibValue) { // 方法1取模确保音符在音阶内循环 int scaleIndex fibValue % scale.length; int midiNote scale[scaleIndex]; // 方法2进阶利用数列增长创造音区变化 // int octaveShift (fibValue / scale.length) % 3; // 限制在3个八度内变化 // int midiNote scale[scaleIndex] (octaveShift * 12); return midiNote; } // 映射函数将斐波那契数映射到时值秒 float mapFibToDuration(int fibValue) { // 归一化到0.5到2.5秒之间避免时值太短或太长 // 简单方法利用取模后的小范围变化 float duration baseDuration (fibValue % 5) * 0.1; return duration; } // 映射函数将斐波那契数映射到音量力度 0-127 int mapFibToVelocity(int fibValue) { // 产生波浪形力度变化 int velocity 70 (fibValue % 30); return constrain(velocity, 50, 100); // 限制在50-100之间避免过轻或过响 }3.3 声音引擎与播放调度逻辑有了参数映射我们需要一个“播放器”来按照节奏触发音符。这里我们用Minim的Oscil振荡器合成一个简单的正弦波音色并用一个定时逻辑来控制播放。// 在setupMusic中初始化一个振荡器作为音源 Oscil osc; float frequency 440; void setupMusic() { // ... 之前的数列初始化代码 ... // 创建一个频率为440HzA4的正弦波振荡器 osc new Oscil(440, 0.8, Waves.SINE); // 将这个振荡器连接到音频输出 osc.patch(out); // 初始时振幅为0不发声 osc.setAmplitude(0); } // 在draw循环中检查是否到了播放下一个音符的时间 void draw() { background(0); float currentTime millis() / 1000.0; // 获取当前时间秒 // 如果距离上次播放的时间超过了设定的间隔就播放下一个音符 if (currentTime - lastNoteTime noteInterval) { playNextNote(); lastNoteTime currentTime; // 动态改变间隔让节奏也有变化可选 // noteInterval 0.3 (fibSequence[fibIndex] % 5) * 0.05; } // 这里可以添加根据当前音符参数进行的可视化比如画一个圆其大小对应音量y位置对应音高 drawVisualization(); } // 播放下一个音符的核心函数 void playNextNote() { if (fibIndex fibSequence.length) { // 如果数列用完可以循环或者重新生成 fibIndex 0; // fibSequence generateFibonacci(20); // 或者重新生成 } int fibValue fibSequence[fibIndex]; // 1. 映射得到音乐参数 int midiNote mapFibToNote(fibValue); float duration mapFibToDuration(fibValue); int velocity mapFibToVelocity(fibValue); // 2. 将MIDI音符编号转换为频率Hz frequency midiToFreq(midiNote); // 3. 更新振荡器参数并触发音符 osc.setFrequency(frequency); // 将力度0-127转换为振幅0.0-1.0 float amplitude velocity / 127.0; osc.setAmplitude(amplitude); // 4. 打印调试信息可选 println(Index: fibIndex , Fib: fibValue , Note: midiNote ( frequency Hz) , Dur: duration s, Vel: velocity); // 5. 设置一个定时器在音符时值结束后停止发声振幅归零 // 这里使用一个简单的线程延迟模拟更优雅的方式是用Minim的ADSR包络或Schedule new Thread(new Runnable() { public void run() { try { Thread.sleep((int)(duration * 1000)); // 转换为毫秒 osc.setAmplitude(0); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 6. 索引指向下一个斐波那契数 fibIndex; } // MIDI音符编号转频率的标准公式 float midiToFreq(int note) { return (pow(2, ((note-69)/12.0))) * 440.0; } void drawVisualization() { // 简单的可视化一个圆y坐标随音高变化半径随音量变化 float y map(frequency, 200, 800, height, 0); // 频率映射到屏幕高度 float r map(osc.getAmplitude()*127, 0, 127, 10, 100); fill(255, 100, 100, 200); noStroke(); ellipse(width/2, y, r, r); }至此一个最基础的、由斐波那契数列驱动旋律、节奏和力度的算法音乐生成器就完成了。运行程序你会听到一个由正弦波演奏的、带有数学美感的旋律不断演进。4. 进阶音色设计与多声部编排基础的单音旋律只是开始。要让音乐真正丰富起来我们需要在音色和多声部上做文章。Minim提供了强大的工具来实现这些。4.1 从基础波形到复杂音色合成一直听正弦波肯定会腻。Minim的Waves类提供了多种基础波形我们可以混合使用或者动态切换。// 在全局变量中定义 int currentWaveform 0; Waves[] waveTypes {Waves.SINE, Waves.SQUARE, Waves.SAW, Waves.TRIANGLE}; // 在playNextNote函数中可以定期或根据数列值切换波形 void playNextNote() { // ... 获取fibValue等参数 ... // 每5个音符切换一次波形示例 if (fibIndex % 5 0) { currentWaveform (currentWaveform 1) % waveTypes.length; osc.setWaveform(waveTypes[currentWaveform]); println(切换波形为: currentWaveform); } // ... 设置频率和振幅 ... }更进阶的方法是使用加法合成或频率调制FM。Minim的Oscil可以相互调制。例如创建一个低频振荡器LFO来调制主振荡器的频率制造颤音效果。// 在setupMusic中创建LFO Oscil lfo; float modulationDepth 10; // 调制深度单位Hz void setupMusic() { // ... 主振荡器初始化 ... // 创建一个频率为5Hz的正弦波LFO lfo new Oscil(5, modulationDepth, Waves.SINE); // 将LFO的输出加到主振荡器的频率上 lfo.offset.setLastValue(frequency); // 设置偏移量为当前基础频率 lfo.patch(osc.frequency); }4.2 使用采样与多轨道实现和声单旋律线是单薄的。我们可以利用斐波那契数列同时生成多个声部如旋律、贝斯、和弦。Minim的AudioPlayer可以加载采样音色如钢琴WAV文件实现更真实的乐器声音。首先将你的采样文件如piano_c4.wav放到项目的data文件夹内。import ddf.minim.spi.*; import ddf.minim.signals.*; // 全局变量 AudioSample pianoSample; AudioOutput out; void setup() { // ... minim初始化 ... // 加载采样文件 pianoSample minim.loadSample(piano_c4.wav, 512); // 512是缓冲区大小 if (pianoSample null) { println(无法加载采样文件请检查data文件夹。); } setupMusic(); } // 修改playNextNote用采样播放代替振荡器 void playNextNote() { // ... 获取midiNote ... // 采样播放器需要知道播放速率来实现音高变化 // 原理播放速率 目标频率 / 采样原始频率 // 假设我们的piano_c4.wav是C4261.63Hz float originalFreq 261.63; float targetFreq midiToFreq(midiNote); float playbackRate targetFreq / originalFreq; // 触发采样播放同时设置播放速率和音量 pianoSample.setGain(map(velocity, 0, 127, -20, 0)); // 增益单位是分贝(dB) // 注意Minim的AudioSample的trigger()方法可能不直接支持rate需要检查文档。 // 另一种更灵活的方式是使用Minim的AudioPlayer和Playable接口或使用MultiChannelBuffer进行更底层的音高变换。 // 这里为了简化我们先播放原速。更复杂的音高变换可能需要用到AudioSource和重采样技术。 pianoSample.trigger(); }对于多声部一个简单的思路是创建多个独立的音符生成逻辑每个声部使用斐波那契数列的不同偏移量或不同的映射规则。// 定义两个声部 int fibIndexMelody 0; int fibIndexBass 5; // 贝斯声部从数列第5项开始产生旋律滞后感 float bassNoteInterval 1.0; // 贝斯音符间隔更长 void scheduleNotes() { // 在主循环或定时器中分别调度两个声部 float currentTime millis() / 1000.0; // 旋律声部 if (currentTime - lastMelodyTime noteInterval) { playNoteForVoice(fibSequence, fibIndexMelody, “melody”); fibIndexMelody (fibIndexMelody 1) % fibSequence.length; lastMelodyTime currentTime; } // 贝斯声部 if (currentTime - lastBassTime bassNoteInterval) { // 贝斯音高可以映射到更低的八度 int bassNote mapFibToNote(fibSequence[fibIndexBass]) - 12; // 降低一个八度 playNoteForVoice(bassNote, ...); // 调用一个通用的播放函数指定音色 fibIndexBass (fibIndexBass 2) % fibSequence.length; // 步进为2产生不同序列 lastBassTime currentTime; } }5. 实战优化与创意扩展一个能跑通的程序只是原型要让它成为一件有趣的作品还需要在细节和创意上下功夫。5.1 节奏感强化与动态变化最初的版本节奏是固定间隔的。我们可以让斐波那契数列也参与控制节奏甚至引入“节拍”的概念。// 用斐波那契数动态计算下一个音符的等待时间 void calculateNextInterval() { int fibValue fibSequence[fibIndex % fibSequence.length]; // 映射到一个合理的间隔范围例如0.1秒到1.5秒 // 使用取模和缩放避免间隔过长导致音乐停滞 noteInterval 0.1 (fibValue % 14) * 0.1; // 0.1s ~ 1.5s } // 在playNextNote中调用 void playNextNote() { calculateNextInterval(); // ... 播放音符 ... // 注意这样lastNoteTime和noteInterval就变成了动态的音乐节奏会随着数列变化。 }还可以引入“小节”和“拍子”的概念。例如设定每小节4拍每个音符的时值必须是基础时值如四分音符的整数倍或分数由斐波那契数决定。这需要更复杂的调度器可能要用到Scheduler或基于音乐时间musical time 以节拍为单位的逻辑。5.2 可视化与交互设计音乐是听觉艺术但加上可视化能极大提升体验。Processing的图形能力正好与此互补。音高瀑布图在屏幕上画一条从左到右移动的线其y坐标对应当前音高颜色或粗细对应音量。数列可视化将斐波那契数列本身用图形表示如经典的黄金螺旋并高亮显示当前用于生成音符的那一项。交互控制允许用户通过鼠标或键盘实时调整参数。鼠标X坐标控制数列生成的起始项。鼠标Y坐标控制映射到音高的缩放比例音域范围。键盘按键切换音阶从C大调到五声音阶。空格键暂停/继续生成。void keyPressed() { if (key ) { isPlaying !isPlaying; // 全局布尔变量控制播放状态 } else if (key c) { // 切换音阶 currentScale (currentScale 1) % scales.length; // scales是预定义的不同音阶数组 scale scales[currentScale]; } else if (key r) { // 重置数列索引 fibIndex 0; } } void mouseDragged() { // 用鼠标水平位置影响音符间隔的基数 baseDuration map(mouseX, 0, width, 0.1, 1.0); // 用鼠标垂直位置影响音量动态范围 velocityRange (int)map(mouseY, 0, height, 20, 80); }5.3 参数化与序列化保存你的“数学种子”一个迷人的想法是每一组斐波那契数列的起始项、映射规则、音阶选择共同定义了一首独一无二的“数学乐曲”。我们可以将这些参数保存下来。// 定义一个配置类或简单用几个全局变量 class MusicConfig { int fibSeedA 0; int fibSeedB 1; int scaleType 0; // 0: Major, 1: Minor, 2: Pentatonic float tempo 120; // ... 其他参数 } MusicConfig config; void saveConfig(String filename) { // 使用Processing的JSON或简单字符串保存 String[] data new String[4]; data[0] str(config.fibSeedA); data[1] str(config.fibSeedB); data[2] str(config.scaleType); data[3] str(config.tempo); saveStrings(filename, data); println(配置已保存: filename); } void loadConfig(String filename) { String[] data loadStrings(filename); if (data ! null data.length 4) { config.fibSeedA int(data[0]); config.fibSeedB int(data[1]); config.scaleType int(data[2]); config.tempo float(data[3]); // 根据加载的配置重新初始化数列和音乐参数 reinitializeMusic(); println(配置已加载: filename); } }这样你可以创建一系列不同的“种子”每次加载都能重现同一首算法生成的乐曲或者分享给他人让他们听到完全一样的数学旋律。6. 常见问题、调试技巧与性能优化在实际操作中你肯定会遇到各种问题。这里记录了我踩过的一些坑和解决方法。6.1 音频相关问题与排查没有声音/爆音/卡顿检查Minim初始化确保setup()中成功获取了AudioOutput(out)。检查系统音频设备是否正常或尝试minim.getLineOut(Minim.STEREO, 2048, 44100, 16);显式指定参数。检查音量与增益振荡器的振幅setAmplitude是否大于0采样播放器的增益setGain是否设置合理分贝值0表示无衰减负值表示衰减AudioOutput本身也有一个setGain方法检查是否被设得太低。缓冲区大小爆音通常由缓冲区下溢underrun引起。尝试在getLineOut中增加缓冲区大小如从1024改为2048或4096。代价是音频延迟会增加。CPU过载过于密集的音频事件如每秒触发上百个极短音符可能导致处理不过来。优化代码避免在draw()循环中做繁重的音频计算。考虑使用音符队列和调度器。音高不准或音色奇怪MIDI转换公式再次确认midiToFreq公式是否正确。标准公式是440.0 * pow(2, (midiNote - 69) / 12.0)。采样播放速率如果使用采样并改变音高播放速率playbackRate的计算必须基于采样的原始根音频率。如果采样是A4440Hz你想播放C5523.25Hz速率应为523.25 / 440.0 ≈ 1.189。波形选择方波、锯齿波含有丰富的泛音听起来比正弦波“亮”甚至“刺耳”这是正常的。可以通过添加低通滤波器LowPassSP来软化音色。6.2 逻辑与音乐性问题旋律过于跳跃或平淡调整映射函数如果旋律跳进太大尝试在mapFibToNote函数中加入平滑处理。例如不是直接使用当前斐波那契数而是使用与前一个音符的差值来生成音程并限制最大跳进如纯五度以内。引入音乐规则在算法中嵌入简单的作曲规则。例如如果连续两个音符向上大跳则下一个音符倾向于向下级进解决。这需要维护一个简单的状态机。混合多种序列不要只用一个斐波那契数列。可以同时用卢卡斯数列、帕多瓦数列等或者将斐波那契数列进行反转、切片用不同部分控制不同参数增加变化。节奏缺乏律动感量化到网格将计算出的音符触发时间对齐到一个虚拟的“节拍网格”上。例如以120BPM每分钟120拍为例每拍的时间是0.5秒。将所有noteInterval舍入到0.5秒的整数倍附近如0.45秒舍入到0.5秒0.7秒舍入到1.0秒。分层节奏设计多个声部每个声部有不同的节奏循环周期。例如旋律声部用数列前6项控制贝斯声部用每隔一项控制打击乐声部用数列值模4的结果控制产生规律的强拍弱拍。6.3 代码结构与性能优化避免阻塞主线程在playNextNote中我们用了Thread.sleep来模拟音符时值。这在简单 demo 中可以但在复杂项目中会阻塞Processing的绘图线程draw。更好的方法是使用状态机和时间戳。float noteOnTime 0; // 音符开始时间 boolean isNoteOn false; int currentNote 0; void draw() { float currentTime millis() / 1000.0; // 检查是否该触发下一个音符 if (!isNoteOn (currentTime - lastNoteTime noteInterval)) { triggerNote(); noteOnTime currentTime; isNoteOn true; lastNoteTime currentTime; } // 检查当前音符是否该结束 if (isNoteOn (currentTime - noteOnTime currentNoteDuration)) { releaseNote(); isNoteOn false; } } void triggerNote() { // 计算并播放音符记录currentNoteDuration // osc.setAmplitude(amplitude); } void releaseNote() { // 停止发声 // osc.setAmplitude(0); }内存管理如果动态加载大量音频采样记得在不需要时用.close()方法释放资源。对于AudioSample多次触发播放是高效的但对于长音频AudioPlayer注意不要创建太多实例。使用ADSR包络对于更真实的乐器音色应该使用ADSR起音、衰减、保持、释音包络来控制振幅随时间的变化而不是简单地开/关。Minim提供了ADSR类可以连接到振荡器或采样播放器上让每个音符都有自然的起音和释音尾音极大提升音乐质感。import ddf.minim.ugens.*; // 在setup中创建ADSR包络 ADSR adsr; void setupMusic() { // ... 初始化osc ... adsr new ADSR(0.5, 0.1, 0.5, 0.5, 0.5); // 参数起音时间衰减时间保持电平释音时间 // 将振荡器连接到包络再将包络连接到输出 osc.patch(adsr).patch(out); } void triggerNote() { // 设置振荡器频率... osc.setFrequency(frequency); // 触发ADSR包络的“按下”事件 adsr.noteOn(); // 不需要手动设置osc的振幅ADSR会控制 } void releaseNote() { // 触发ADSR包络的“释放”事件它会按照释音时间淡出 adsr.noteOff(); // 注意需要在ADSR释音完全结束后才能触发下一个音符的noteOn否则声音会叠加。 // 更严谨的做法是监听ADSR的完成状态。 }走完这一整套流程从最基础的数列映射到拥有多声部、ADSR包络和可视交互的完整作品你会发现算法作曲不再是冰冷代码的堆砌。每一次参数的微调每一次映射规则的改变都像是在与一个由数学规律构成的“音乐精灵”对话它总能给你出乎意料却又合乎逻辑的回应。这其中的乐趣远不止于最终听到的那段旋律更在于探索和发现的过程本身。我自己的习惯是每完成一个有趣的参数组合就用屏幕录制软件把视听效果保存下来久而久之就积累了一个属于自己的“数学音乐灵感库”。