
假设你在 DAW 里拖过十几个轨道最后发现真正卡住你的不是混音也不是音色而是脑子里那段节奏到底怎么落到时间轴上——那么 Strudel 这个项目值得你花半小时看一眼。它是一个跑在浏览器里的 live coding实时编码音乐环境你把一小段类似正则表达式的字符串敲进编辑器按下快捷键声音立刻响起来改一个字符下一轮循环就变了。最近流传的一段带中文字幕的 Strudel REPL 体验视频把不少人第一次带进了 live coding 的门槛但视频看得爽自己上手时往往第一时间就卡在“这串符号到底在说什么”上。这篇内容不打算复述视频而是把 Strudel 当成一个需要被拆解的技术系统来看它的 Pattern 模型是什么、mini-notation 这门微型语言有哪些规则、浏览器里声音是怎么被调度出来的、想嵌入到自己项目里又该怎么接。我的判断是Strudel 真正的价值不在于“用代码做音乐”这个噱头而在于它把“音乐结构”抽象成了一个可组合、可变换、可测试的数据结构——这一点对写代码的人是天然的亲和力也是它比图形化音序器更适合练结构思维的地方。但它也有明确的边界它不是编曲工具不适合做需要精细混音和多轨自动化的成品它的采样和音源依赖网络加载现场的稳定性需要提前准备它的 API 在快速演进你从不同地方抄来的片段很可能因为版本差异跑不起来。读完这篇你应该能做到三件事独立跑通一段多声部循环、看懂别人写的 Strudel 代码、把它以最小成本嵌进自己的网页或练手项目里。1. 先搞清楚Strudel 到底解决了什么问题要理解一个工具最好的切入点是问“没有它的时候这件事是怎么做的”。传统音乐制作有两条路一条是 DAW 路线用鼠标在钢琴卷帘上画音符好处是直观坏处是每次改动都要重新画一遍一个 8 小节的循环改到第 30 版时你自己都记不清哪一版是好的另一条是脚本路线用 MIDI 文件或某种 DSL 生成音符数据好处是可复用坏处是反馈慢你得先导出、再播放、再回来改。Strudel 站的位置很巧它既有脚本的可复用性又有接近 DAW 的实时反馈。关键在于“实时”这两个字。Strudel 的 REPL 采用了一种在 live coding 圈子里很成熟的玩法编辑器里始终只有一份当前正在播放的代码你按下求值快捷键新的代码会在下一个循环边界接管音频。这意味着你不需要“停止—修改—重新播放”音乐是连续的改动是渐入的。这个交互模型改变的不只是效率而是创作方式本身——你可以在音乐响着的时候试错错了立刻改回来听觉判断和代码修改在同一秒内完成。第二个被解决的问题是“音乐结构的数据化”。在 Strudel 里一段鼓点不是一个波形文件也不是一堆鼠标位置而是一个 Pattern 对象Pattern 可以被加速、减速、反转、随机丢弃、条件替换。你在写代码时积累的那套思维方式——函数组合、纯函数变换、关注点分离——可以直接迁移到音乐上。这是它和普通音序器最本质的区别音序器让你摆放事件Strudel 让你描述事件的生成规则。第三件事是门槛。Strudel 完全跑在浏览器里打开网页就能出声不需要装宿主、不需要配驱动、不需要买音源。对于想验证“我到底对音乐编程有没有兴趣”的人来说这个试错成本几乎为零。但也正因为门槛低很多人第一次打开 REPL看到默认代码里那一堆s(bd sd)、note(c3)就懵了——这不是语法难而是缺少对 mini-notation 这门微型语言的解释。那么谁最应该读这篇三类人。第一类是有编程基础、想找一个能立刻出声的练手项目的人第二类是做前端或创意编程想把音频能力接进网页的开发者第三类是想理解“模式Pattern驱动”这套抽象因为它和响应式编程、流处理、定时任务调度都有相通之处。如果你只是想批量生产音乐成品那还是打开 DAW 更划算这篇可以跳过。2. Live Coding、Pattern 与 mini-notation三个必须建立的心智模型学 Strudel 最容易走的弯路是一上来就背函数名。函数是查文档的事真正需要先建立的是心智模型。Strudel 里只有三个概念值得你先想明白live coding 的执行模型、Pattern 的时间语义、mini-notation 的表达粒度。这三件事想通了后面所有函数都只是参数问题。2.1 Live Coding 不是“用代码生成音乐”那么简单很多人把 live coding 理解成“写个脚本把 MIDI 导出来”。这是误解。真正的 live coding 环境有三个硬性要求一是求值时不能中断正在播放的音频二是新代码必须在某个明确的时间边界生效而不是立刻掐断当前声音三是编辑器要能承载“一边跑一边改”的工作流。Strudel 的做法是维护一个“当前活动的 Pattern”。你按下求值编辑器把代码字符串编译成新的 Pattern调度器会在下一个循环起点把它换上去旧 Pattern 自然结束。这个过程通常几毫秒就完成但听觉上是无缝的。理解这一点很重要因为它解释了为什么你改代码之后声音没有立刻变化——不是没生效是在等循环边界。如果你把速度调得很慢比如setcps(0.1)那一个循环十秒等待感就会非常明显新手很容易误以为代码报错了。还有一个常见困惑为什么我写了s(bd)但它一直在响因为 Strudel 里的 Pattern 是循环播放的一个 cycle 结束就从头再来。这不是 bug是设计。你写的每一行代码描述的都不是“播放一次”而是“每一轮循环该做什么”。带着这个前提去看代码很多写法就顺了。2.2 Pattern 是“时间到事件”的函数Pattern 的本质可以做这样一个通俗类比它像一份没有具体日期的日程表你问它“今天 9 点到 10 点之间有什么安排”它告诉你“9:15 有一个会议9:45 有一个电话”。你问它别的时间段它也能算出来。Pattern 就是这样一种东西——它不存事件列表它存的是从时间区间查询事件的规则。这个抽象带来的直接好处是“变换的自由”。因为 Pattern 是一个查询函数你可以对它做任意包装fast(2)返回的是一个新函数它把你问的时间区间除以 2 再转发给原 Patternrev()返回的是把区间镜像翻转后转发的新函数。原始 Pattern 完全没被修改所有变换都是纯的、可组合的。这和你在函数式编程里用map、filter链式处理数组是同一套思路。理解这一点之后你就能解释一些看起来奇怪的现象。比如s(bd).fast(2).slow(2)和s(bd)听起来是一样的因为两次变换互相抵消了比如stack(a, b)不是把 a 和 b 拼起来而是把两个 Pattern 的查询结果合并到同一时间轴上——所以它等价于“同时播放”。这些行为都不是魔法全是从“Pattern 是个查询函数”这个定义推出来的。时间在 Strudel 里用有理数表示一个完整的循环叫一个 cycle。默认情况下一个 cycle 大约对应两秒左右的时长具体取决于当前速度设置你写的所有 mini-notation 都是在“把一个 cycle 切分成若干份”。这也是为什么改速度不会改变结构只会改变播放快慢——结构描述的是比例关系不是绝对秒数。2.3 mini-notation 是一门微型鼓机语言如果 Pattern 是数据结构那 mini-notation 就是它的字面量语法。这门语言很小但它把“节奏切分”这件事压缩到了极致。字符串bd sd的意思是把一个 cycle 平均切成两份前半放 bd后半放 sd。字符串bd sd hh cp就是切成四份。你不需要声明拍号不需要设置网格分隔符本身就是切分。下面这张表是上手必须记住的核心符号覆盖了日常 90% 的表达需求符号含义示例说明空格顺序切分s(bd sd cp)一个 cycle 均分成三份~休止符s(bd ~ sd ~)占位但不发声用来做空拍*n在该步内加速 n 次s(hh*8)八分音符式的连续敲击/n放慢 n 倍s(bd/2)一个音跨越多个步[ ]分组把多个事件挤进一步s([bd sd] hh)前半步内再切两半 每次循环交替一项s(bd sd cp)第一轮 bd第二轮 sd,并行叠加s(bd, hh*8)两个声部同时进行?随机丢弃事件s(hh*8?)概率性留白制造呼吸感!复制事件s(bd!3)等价于bd bd bdn权重延音s(bd3 sd)让 bd 占据更多的时长比例这里真正容易踩坑的是[ ]和 的区别。前者是“在这一步内部再切分”改变的是时间粒度后者是“这一个位置每次循环换一个值”改变的是循环间的变化。新手常把 当成数组写成bd, sd结果发现是并行而不是交替——顺序交替要用空格分隔。3. 环境准备三种运行方式怎么选Strudel 的运行形态比想象中多选错方式会让你在无关的地方浪费大量时间。下面三种是最常见的路径按“想干什么”来选就行。3.1 浏览器 REPL零安装适合学习和现场最直接的方式是打开官方 REPL 站点地址以项目官网为准目前主站是strudel.cc。打开后会看到一个左右分栏的编辑器左侧是代码右侧是输出与日志。按下Ctrl EntermacOS 上是Cmd Enter对当前代码求值音频开始播放再按一次新代码在下一个循环接管。浏览器路径有几个必须知道的前置条件。第一浏览器对自动播放有严格限制页面必须先获得一次用户手势才能启动音频上下文所以你打开页面后点的第一下“播放”或第一次按快捷键本质上是在解锁音频。第二声音依赖采样库默认音色是内置的合成器鼓组需要额外加载第一次加载会有网络等待。第三长时间运行会积累音频节点极少数情况下需要刷新页面重置。版本方面REPL 是一个持续部署的站点没有固定的“稳定版”概念功能随时可能更新。这意味着你从论坛或视频里抄来的代码可能因为 API 调整跑不通——这不是你的问题查一下最新文档就好。我一般建议把 REPL 当成“快速验证想法的草稿纸”真正的作品还是落盘成本地文件。3.2 本地嵌入npm 方式接进网页如果你要做的不是“自己写音乐”而是“让网页能出声”那就要走 npm 嵌入路线。Strudel 把能力拆成了若干包常见的有核心模式计算、mini-notation 解析、Web Audio 输出几个部分另有一个面向网页的整合包。下面是一个最小可用的package.json版本号请以 npm 上的最新发布为准不要直接照抄{ name: strudel-demo, private: true, type: module, scripts: { dev: vite, build: vite build, preview: vite preview }, dependencies: { strudel/web: latest }, devDependencies: { vite: latest } }安装依赖npm install npm run dev需要说明的是Strudel 生态里的包名和导出函数在不同阶段有过调整strudel/web、strudel/core、strudel/mini、strudel/webaudio这几个包的职责边界也可能随版本变化。如果你发现某个导出不存在第一步不是改代码而是打开node_modules里对应包的类型声明或入口文件看一眼实际导出了什么——这比在网上翻半天快得多。3.3 采样与音源的前置条件不管走哪条路音源都是绕不开的一环。Strudel 的音源分两类一类是内置合成器通过s(sawtooth)、s(triangle)这样的波形名直接调用无需加载另一类是采样需要从远程音源库拉取。鼓组名字bd、sd、hh、cp等默认指向一套内置采样但如果你想用更丰富的音色库需要显式声明来源。在实际项目里这一步最容易被忽略的细节是“加载是异步的”。如果你在采样还没就绪时就触发了播放那一拍会静音日志里通常不会有明显报错新手很难定位。稳妥的做法是在页面初始化阶段就发起加载等完成后再允许用户点击播放REPL 里则建议第一次求值后等一两秒再判断效果。4. 核心流程拆解从一行节奏到一段完整 Loop这一节把 “一行代码到一段循环” 拆成五个步骤。每一步都说明它做什么、为什么这么做、做错会怎样。建议按顺序在 REPL 里逐段试不要一次性粘贴全部。4.1 第一步跑通第一行确认音频链路先写最朴素的鼓点确认浏览器能出声、快捷键生效、日志没有异常// 最小的四拍鼓组逗号表示并行声部 s(bd sd [~ bd] sd, hh*8)这一行同时做了两件事逗号左侧是一个四拍的底鼓与军鼓组合其中第三步被[~ bd]拆成“前半休止、后半底鼓”形成一个切分逗号右侧是踩镲连打八下。如果你听到的是规律的“咚—哒—咚—哒”加上密集的镲声说明音频链路正常。如果完全没声音先看三个地方页面是否已经通过点击解锁音频、编辑器日志区有没有红色报错、采样名是否拼错bd写成bd2之类。如果只有某一层没声音比如鼓有但镲没有那大概率是该采样没加载成功而不是语法问题。4.2 第二步加旋律理解音名与音色分离Strudel 里“音高”和“音色”是两条独立的线note()负责音高s()负责波形或采样。这种分离让你可以把同一段旋律换到不同音色上试听// 用锯齿波演奏一段下行旋律低通滤波压掉高频毛刺 note(c3 eb3 g3 bb3) .s(sawtooth) .lpf(800) .gain(0.6)音名写法是标准的小写字母加可选升降号和八度数c3是中央 C 下方一个八度附近的位置eb3是降 E。这里的.lpf()是低通滤波器参数是截止频率值越低声音越闷。锯齿波本身泛音很丰富不加滤波会显得刺耳这也是很多人第一次写旋律觉得“难听”的主要原因——不是音高错了是没做音色塑形。4.3 第三步加和声用调式而不是手写音符手写和弦的每个音很累而且改调时要全改。Strudel 提供了和弦与音阶的抽象让你用符号描述和声进行// 用 voicing 自动把和弦符号展开成具体的音高排列 chord(Cm7 Fm7 Bb7 Ebmaj7) .voicing() .s(sawtooth) .gain(0.25) .room(0.4)...表示每个循环切换一个和弦所以这段代码会播放一个四小节的 ii–v–I 走向。.voicing()做的是“声部排列”它会把和弦音安排在一个合适的音域内避免所有音挤在一起或者跨度太大。这一步的常见错误是漏掉.voicing()直接对和弦符号调用.s()结果声音不对或者干脆报错——和弦符号本身不是音高得先展开。4.4 第四步用变换函数制造变化一段循环听四遍就腻。变化不一定要重写代码用变换函数叠加条件即可// 每 4 个循环反转一次踩镲制造段落感 s(hh*8).every(4, x x.rev()) // 30% 概率把底鼓加速一倍制造随机推进 s(bd*4).sometimesBy(0.3, x x.fast(2))every(n, fn)的语义是“每 n 个循环应用一次变换”注意它作用在整条 pattern 上你可以把它理解为一种低频调制。sometimesBy(概率, fn)更随机适合做那种“听起来像人在打鼓”的轻微失误感。这两个函数的区别值得强调前者是可预期的、周期性的后者是概率性的、不可预期的。做电子乐时两者搭配用效果比单纯堆音符好得多。4.5 第五步处理空间与电平声音挤在一起的原因通常不是音符太多而是没有空间感。给不同声部安排不同的声像、混响和延迟立刻就有层次// 各声部分别处理鼓干、和弦湿、旋律带延迟 stack( s(bd*4).gain(0.9), s(~ cp).gain(0.55).room(0.2), s(hh*8).gain(0.3).pan(sine.range(-0.5, 0.5)), chord(Cm7 Fm7).voicing().s(sawtooth).gain(0.2).room(0.6) ).pan()控制左右声像这里传入的是一个连续信号sine.range(-0.5, 0.5)意思是让踩镲的声像随时间缓慢左右摆动产生一种“流动”的听感。.room()是混响量。这里有个硬性经验所有声部的gain加起来最好控制在 1 附近超过太多会出现削波失真听感上是“糊成一团”很多新手以为是音色不好其实是电平爆了。5. 完整示例一段可直接粘贴运行的循环把上面五步合起来就是下面这段代码。它包含鼓组、贝斯、和弦、主旋律四个声部加了一次周期性反转。整段代码可以直接粘进 REPL 求值// 1. 设定速度每秒 0.5 个循环 // 在 4/4 拍、一个循环等于一小节的前提下相当于 120 BPM setcps(0.5) // 2. 鼓组底鼓、军鼓、踩镲 const drums stack( s(bd*4).gain(0.9), s(~ cp).gain(0.55), s(hh*8).gain(0.3).pan(sine.range(-0.5, 0.5)) ) // 3. 贝斯每循环换一个根音低通压暗 const bass note(c2 c2 eb2 g1) .s(sawtooth) .lpf(400) .gain(0.55) // 4. 和弦四小节进行加混响拉开空间 const chords chord(Cm7 Fm7 Bb7 Ebmaj7) .voicing() .s(sawtooth) .gain(0.22) .room(0.45) // 5. 主旋律三角波更柔和加延迟 const lead note(c4 eb4 g4 bb4 ab4 g4 eb4 c4) .s(triangle) .gain(0.28) .delay(0.3) .room(0.5) // 6. 合并所有声部每 4 个循环整体反转一次 stack(drums, bass, chords, lead) .every(4, x x.rev())这段代码里有几个设计决定值得说明。第一用const声明子声部而不是把所有东西塞进一个巨大的stack()。原因是可维护性当你想单独调试贝斯时只要把最后的stack()临时改成bass就行不用从一长串调用里挑出那几行。第二速度用setcps(0.5)而不是setcpm()。这两个函数容易混setcps是“每秒多少个循环”setcpm是“每分钟多少个循环”。很多人以为setcpm(120)就是 120 BPM其实不是——120 个循环每分钟意味着每个循环只有半秒节奏会快到离谱。如果确实想用setcpm表达 120 BPM在“一个循环等于四拍”的前提下应该写setcpm(120/4)。这个换算是新手最容易栽的坑之一。第三.every(4, x x.rev())放在最后的stack()上作用对象是一整个复合 Pattern。反转会让所有声部的时间轴同时镜像听起来像一次“倒带”作为段落过渡很有效。但不要滥用每 2 个循环反转一次会让人完全失去节奏锚点。6. 运行结果与效果验证代码粘贴进去后按Ctrl EntermacOS 用Cmd Enter。正常情况下你会听到稳定的四拍底鼓、第二拍和第四拍的军鼓、连续的踩镲在左右声道之间缓慢摆动、一条低沉的贝斯线跟着和弦根音走、中间层有和弦铺底、最上面是三角波旋律带一点回声。整体速度接近 120 BPM四小节一个循环。判断“跑通了”的标准不应该是“好听”而是这几条底鼓每循环响四次且间隔均匀、军鼓只在二四拍出现、和弦每循环换一次且没有明显爆音、整段音量没有忽大忽小。如果这四条都满足说明结构和电平都对了。如果失败按下面这个顺序排查不要跳步。第一步看编辑器右侧或下方的输出区有没有红色错误信息有的话直接读错误内容Strudel 的报错通常会说清楚是哪个函数不认识或者哪个参数类型不对。第二步把代码精简到只剩s(bd*4)单独求值确认基础音频能出声这一步排除的是浏览器音频上下文和环境问题。第三步逐段加回声部每次加一个看是哪一段引入的异常这一步排除的是语法和采样加载问题。第四步如果声音出来了但很难听先查电平——把所有gain减半再听八成的“音质问题”其实是削波。还有一种情况是“求值了但声音没变”。先确认你是不是在一个很慢的速度下工作循环边界还没到再确认你按的是求值快捷键而不是保存最后确认编辑器里没有多份代码同时处于活动状态REPL 只认最后一次求值的结果。7. 常见问题与排查思路Strudel 的坑集中在音频权限、采样加载、速度换算、版本差异这四类。下面这张表覆盖了实际使用中最高频的问题问题现象可能原因排查方式解决方案打开页面完全没声音浏览器自动播放策略限制音频上下文未解锁看页面是否有“点击播放”提示检查控制台是否有音频上下文挂起警告先点击页面任意按钮或按一次求值快捷键用用户手势解锁音频求值后有一两秒静音才出声采样库异步加载未完成观察网络面板是否有音频资源请求未返回首次加载时先求值一个最简单的合成器音色等采样就绪再播放鼓组鼓有声音但某个采样如 cp没声该采样名不存在或未加载单独求值s(cp)确认查官方采样名列表改用内置存在的采样名或显式加载包含该采样的音源库节奏明显过快或过慢setcps/setcpm单位搞混打印当前速度设置核对“每个循环对应多少拍”4/4 拍下一个循环一小节时120 BPM 对应setcps(0.5)或setcpm(30)声音糊成一团、有破音多声部 gain 叠加超过 1 造成削波逐个声部单独播放观察是否单声部正常所有声部 gain 之和控制在 1 以内必要时加压缩或降低整体电平报错xxx is not a function抄来的代码与当前版本 API 不匹配读报错行查官方文档对应函数的最新签名按当前版本文档改写不要照搬旧教程的写法修改代码后音乐不更新未重新求值或处于很慢的速度下等循环边界确认快捷键是否触发观察是否超过一个循环时长重新求值调试阶段先把速度调快缩短反馈周期移动端浏览器无声移动端音频策略更严格需显式恢复上下文检查是否有用户手势触发把播放绑定到明确的按钮点击事件上不要依赖页面加载即播放长时间运行后卡顿音频节点累积、日志堆积观察内存占用与日志区增长定期重新求值或刷新页面演出前重启一次嵌入网页后报模块解析错误包名或导出名与版本不符查看node_modules中该包的实际入口导出按实际导出改写 import或锁定到已知可用的版本这张表里最值得多说一句的是“速度单位”。Strudel 的时间单位是循环不是拍。你写的bd sd是把一个循环切成两半具体多快取决于setcps。所以同一段代码在不同速度下会呈现完全不同的风格——慢速下是氛围快速下是碎拍。理解这一点之后你会发现自己调速度的频率会高很多因为速度本身就是编排手段。另一个值得展开的是“为什么嵌入网页会比 REPL 麻烦”。REPL 帮你处理了音频上下文解锁、采样预加载、编辑器绑定这一整套事而嵌入时这些都要你自己做。尤其是音频上下文浏览器要求必须由用户手势触发如果你在DOMContentLoaded里直接调播放大概率是静默失败。这个坑几乎每个人第一次嵌入都会踩。8. 工程化最佳实践把 Strudel 当玩具玩和当项目用差别主要在工程习惯上。下面几条是从实际踩坑里总结出来的按重要性排序。8.1 代码组织按声部分组用变量隔离一段超过 20 行的 Strudel 代码如果全塞进一个stack()调试成本会指数上升。推荐的做法是每个声部一个const命名用英文小写加下划线比如drums、bass、pad、lead。这样你可以随时把最后的组合改成单个声部单独试听也可以在演出时快速注释掉某一层。更进一步的技巧是把“变换”和“内容”分开写。内容层只描述音符和节奏变换层单独一行追加比如// 内容层只写音符 const melody note(c4 eb4 g4 bb4) // 变换层单独追加效果方便开关 const melodyFx melody .s(triangle) .gain(0.28) .delay(0.3)这样当你想试不同音色时只要改melodyFx那一行旋律本身完全不动。这个习惯在长时间调试里能省下大量时间。8.2 电平预算把 gain 当成一个需要分配的预算前面提过一次这里再强调Strudel 没有自动混音所有声部叠加就是简单相加。一个稳妥的经验是给每个声部分配一个预算底鼓 0.8 到 0.9军鼓 0.5 到 0.6踩镲 0.25 到 0.35贝斯 0.5 到 0.6和弦铺底 0.2 到 0.3主旋律 0.25 到 0.35。加起来刚好在 1 附近留一点余量给混响和延迟的尾巴。如果发现某一段特别响不要第一反应去压其他声部先看这个声部是不是用了波形很丰富的音色比如锯齿波那种音色本身就比三角波响需要单独降。这是一个听感校准问题没有公式只能靠耳朵调。8.3 演出与演示的容错准备如果你打算在别人面前演示有两件事必须提前做。第一把采样提前加载完。方法是开场前先求值一段包含所有采样名的静音代码gain(0)让浏览器把资源拉下来正式开始时再去掉静音。第二准备一份“降级版本”——把所有复杂变换去掉只保留最基本的节奏和和声。真出问题时立刻切到降级版本比现场 debug 体面得多。另外建议把作品保存成独立的文本文件用 git 管理。Strudel 代码本质上就是几行 JS版本控制完全适用。你会惊讶地发现一段循环改到第十版时前面某一版往往才是最好的没有版本记录就只能凭记忆重写。8.4 网页嵌入的注意点嵌入到自己的网页时结构上建议做成“一个按钮 一个隐藏的代码区”。按钮负责用户手势解锁音频和触发求值代码区可以用script typetext/plain存放避免被浏览器当 JS 执行。下面是一个最小可用的骨架实际使用时请对照strudel/web的最新文档确认 API!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / titleStrudel 最小嵌入示例/title /head body button idstart开始播放/button button idstop停止/button !-- 代码以纯文本形式存放避免被当作脚本执行 -- script idpattern typetext/plain setcps(0.5) s(bd*4, hh*8).gain(0.7) /script script typemodule import { initStrudel } from strudel/web; let repl null; document.getElementById(start).addEventListener(click, async () { // 首次点击时才初始化满足浏览器的用户手势要求 if (!repl) { repl await initStrudel({ prebake: () samples(github:tidalcycles/dirt-samples) }); } const code document.getElementById(pattern).textContent; repl.evaluate(code); }); document.getElementById(stop).addEventListener(click, () { if (repl) { repl.stop(); } }); /script /body /html这段骨架的关键点是初始化时机。initStrudel必须在用户点击之后调用不能放在模块顶层否则音频上下文拿不到权限。prebake用来指定采样预加载如果你只用合成器音色这一项可以省掉能显著缩短首屏等待。9. 学习路径与后续方向回到最开始那个判断Strudel 值得花时间不是因为它能“用代码做音乐”而是因为它把音乐结构变成一个可组合的对象。你在它身上练的其实是一套“用函数描述时间”的思维——这段思维在音频之外也用得上比如动画时间轴、任务调度、状态机流转本质都是同一类问题。如果要把这条路走下去我建议按这个顺序推进。第一步把 mini-notation 的十个符号练到不用查表方法是拿一段自己喜欢的鼓点试着用字符串复刻出来。第二步理解 Pattern 的组合语义重点练stack并行、cat串行、fast/slow时间缩放、every/sometimesBy条件变换这四组它们是所有复杂编排的积木。第三步开始做完整的段落结构——用变量把前奏、主歌、副歌分开写用arrange之类的函数串起来这一步会让你从“循环”进入“作品”。第四步才是考虑嵌入和工程化。最后提醒三件事。一是不要在深夜用耳机大音量调鼓点听觉疲劳会让你做出错误的电平判断。二是所有涉及采样版权的内容正式发布前务必确认授权社区音源库的许可条款各不相同。三是现场演出前一定要在目标设备上完整跑一遍浏览器版本、网络状况、音频输出设备都会影响结果本地测试通过不等于现场能跑。如果你现在就想动手最省事的做法是打开 REPL把第 5 节那段完整示例粘进去按一次求值然后从删掉主旋律开始一个一个声部往里加。听感和代码的对应关系只有在反复修改中才会真正建立起来。