diff --git a/docs-design/夜市风格版-批次二/音效CC0采样台账.md b/docs-design/夜市风格版-批次二/音效CC0采样台账.md new file mode 100644 index 00000000..ee9a7db8 --- /dev/null +++ b/docs-design/夜市风格版-批次二/音效CC0采样台账.md @@ -0,0 +1,55 @@ +# 音效 CC0 真采样台账 · 批次二(2026-07-07) + +《夜市一条街》的真实世界厨房 foley 从分层合成换成 CC0/公有领域真采样。起因是创始人 2026-07-06 拍定夜市 SFX 走真采样而非合成——烤肉的滋滋声、收银的钱箱声,真录音的质感比振荡器合成贴得多。本批把七个经营内容事件逐个去找干净、可核验授权的真录音,找得到就换采样,找不到就如实退回合成,绝不为了凑齐而糊弄授权。 + +机器可读的逐文件授权清单是采样目录里的 `manifest.json`(源页 URL、直链、license、作者、字节、裁剪窗全留痕);本页是给人看的一页数与决策留痕。 + +## 采样来源与授权(逐条核验) + +七个目标里六个拿到了干净的 CC0 或公有领域真录音,全部来自 Wikimedia Commons(其 license 字段可经 API 机器核验,`extmetadata.LicenseShortName` + `License` 两栏在下载当刻复核过一次,不是笼统的“免费”)。授权口径只收 CC0 与 Public domain(免授权、免署名);过程中命中的几个 CC-BY / CC-BY-SA 素材(一段现成的 “Cha Ching” 收银机、一段掌声、一个压电蜂鸣器)因为要求署名,一律不取。 + +| 音效 | 文件 | 采样内容 | License | 作者 | 源页 | 字节 / 时长 | +|---|---|---|---|---|---|---| +| 烤肉滋滋 | `sizzle.mp3` | 油锅煎炸 | **Public domain** | ezwa | [Frying fries…ogg](https://commons.wikimedia.org/wiki/File:Frying_fries_going_in_and_out_of_oil.ogg) | 22.3 KB / 1.79s | +| 收银钱箱 | `cash.mp3` | 硬币落入金属钱箱 | **Public domain** | ezwa | [Coins…metallic moneybox.ogg](https://commons.wikimedia.org/wiki/File:Coins_dropped_in_metallic_moneybox.ogg) | 17.0 KB / 1.35s | +| 金币叮当 | `coin.mp3` | 硬币落入木钱箱 | **Public domain** | ezwa | [Coins…wooden moneybox.ogg](https://commons.wikimedia.org/wiki/File:Coins_dropped_in_wooden_moneybox.ogg) | 13.5 KB / 1.05s | +| 下单铃 | `bell.mp3` | 自行车铃清脆一响 | **CC0 1.0** | qubodup | [Bell-ring.flac](https://commons.wikimedia.org/wiki/File:Bell-ring.flac) | 17.8 KB / 1.40s | +| 顾客满意 | `applause.mp3` | 鼓掌加欢呼 | **Public domain** | starlite | [Clapping hurray.ogg](https://commons.wikimedia.org/wiki/File:Clapping_hurray.ogg) | 24.8 KB / 2.00s | +| 失败差评 | `buzzer.mp3` | 蜂鸣器 | **CC0 1.0** | David Pride | [Buzzer.wav](https://commons.wikimedia.org/wiki/File:Buzzer.wav) | 11.0 KB / 0.85s | + +四个来自 pdsounds.org 早年释入公有领域的现场录音(ezwa、starlite),两个来自 freesound 与 Cotswold Jam 的 CC0 释出(qubodup 的自行车铃、David Pride 的蜂鸣器),均经 Wikimedia Commons 转存并保留原始出处。收银用的其实是“硬币落金属钱箱”的真声——比一段合成 cha-ching 更贴夜市小摊那种把零钱往铁盒里一撂的动作,也正好对上工单里“收银 cha-ching/钱箱”的口径。 + +每个文件下载后统一走一道加工:curl 直下原始 ogg/flac/wav,ffmpeg 裁到两秒以内的活跃窗口(连续声如煎炸、鼓掌取最响的一段,脉冲声如落币、铃响、蜂鸣取起音到自然衰减),下混单声道 44.1kHz,首尾各加几十毫秒淡入淡出消除裁剪爆音,最后 libmp3lame 96k 编码。六个文件合计 106 KB,加清单 111 KB。 + +## 采样 vs 合成的分布 + +真实世界 foley 事件换采样,抽象与 UI 反馈音保留合成——后者本就是设计音,没有“真录音更贴”的诉求。 + +- **改播采样(6 类事件 → 6 个样本)**:下锅 `order`(下单铃 + 起锅滋滋两层走采样)、收银三档 `cash_low/mid/high`(高档额外叠一层散币叮当)、小费 `tip`、好评 `praise`、差评 `blame`。 +- **保留合成(切菜 chop,1 类)**:见下节,如实退回。 +- **本就合成、不在采样范围**:翻台、连击、断链、进货、缺货、解锁号角、升级、研发、雇佣、技能、潮汐、评论家登场、雨声、任务、成就 fanfare、图鉴、UI 点击、弹窗、广告位金光,以及出餐 `serve_ready`。这些是设计音或抽象反馈,合成足够。 + +接线上采样是懒加载:首个用户手势解锁 AudioContext 后才 fetch 解码六个样本,不进首屏字节账;解码完成前、文件缺失或加载失败,对应事件自动回落到原有合成声部,一行都没删,缺音不崩的红线不破。node 与 headless 环境没有 fetch 与 AudioContext,整条采样加载跳过,纯逻辑与门的契约不变。 + +## 未竟:切菜 chop 退回合成(如实) + +切菜没找到干净的 CC0 真录音,退回合成。Wikimedia Commons 搜遍 chopping / cutting / cleaver / knife-cutting-board 全部落空;freesound 上确有 CC0 的切菜录音,但其原文件下载要 OAuth token,内网凭据档里没有,不硬凑;OpenGameArt 的 CC0 合集要逐层下钻且难以在无法试听的机器上核验音色。唯一现成的公有领域近似物是一段 “Knocking on wood or door”,但它是连续几下敲击、带空心门腔体的共鸣、没有切割的嘶声,在 6c6g 上没法试听确认它读起来像不像“刀落砧板”——按诚实协议,音色不可核验的样本不采。保留的合成 chop 本来就是照刀落砧板建模的(短噪声穿切 + 低频木墩 thock),比一段敲门声更贴,作为这一个音的兜底站得住。 + +结论:七个目标,六个真采样、一个合成兜底。 + +## 成本 + +零采买。六个样本都是 CC0 或公有领域,无授权费、无素材费。开销只有下载与转码的本机算力和时间,没有调用任何付费模型或接口(本批音效线与批次二美术线的 mmx 生图预算无关,不占其额度)。 + +## 验收 + +| 门 | 结果 | +|---|---| +| logic-smoke(95 断言,纯逻辑核) | exit 0,全过 | +| tree-check(15 静态可达查) | exit 0,全过 | +| 单测(30 用例:经济骨架/九门 driver/富三门/M2 系统/M3 注入缝) | 30 pass / 0 fail / 0 skipped | +| 构建 + 两本包体账 | build exit 0;账一总 4.336 MB ≤ 10 MB(过),首屏 1.13 MB ≤ 2 MB(过) | + +采样落在 `assets/audio/` 下,构建记账里本就归 BGM 同级的懒加载项、不计首屏,所以首屏字节与改动前一字不差(1.13 MB),总账只多了这 111 KB。三个纯逻辑门与单测都不碰音效模块,零倒退。 + +浏览器里真实的 decodeAudioData 播放与听感未在本机验证——6c6g 禁 chrome,音频真播归 mini-desktop 的 CDP 窗口,与 M4 换装的九门回归一并排。样本文件本身由 ffmpeg 编码、ffprobe 复核为合法 MP3,路径解析沿用 Phaser 既有的相对 `assets/…` 惯例(M2 CDP 已 ACCEPT 过同款路径),接线正确性有据。 diff --git a/tier2/games/night-market/assets/audio/sfx/applause.mp3 b/tier2/games/night-market/assets/audio/sfx/applause.mp3 new file mode 100644 index 00000000..2da42f53 Binary files /dev/null and b/tier2/games/night-market/assets/audio/sfx/applause.mp3 differ diff --git a/tier2/games/night-market/assets/audio/sfx/bell.mp3 b/tier2/games/night-market/assets/audio/sfx/bell.mp3 new file mode 100644 index 00000000..03a88e00 Binary files /dev/null and b/tier2/games/night-market/assets/audio/sfx/bell.mp3 differ diff --git a/tier2/games/night-market/assets/audio/sfx/buzzer.mp3 b/tier2/games/night-market/assets/audio/sfx/buzzer.mp3 new file mode 100644 index 00000000..408c214c Binary files /dev/null and b/tier2/games/night-market/assets/audio/sfx/buzzer.mp3 differ diff --git a/tier2/games/night-market/assets/audio/sfx/cash.mp3 b/tier2/games/night-market/assets/audio/sfx/cash.mp3 new file mode 100644 index 00000000..e2ca7c8c Binary files /dev/null and b/tier2/games/night-market/assets/audio/sfx/cash.mp3 differ diff --git a/tier2/games/night-market/assets/audio/sfx/coin.mp3 b/tier2/games/night-market/assets/audio/sfx/coin.mp3 new file mode 100644 index 00000000..2a8312e6 Binary files /dev/null and b/tier2/games/night-market/assets/audio/sfx/coin.mp3 differ diff --git a/tier2/games/night-market/assets/audio/sfx/manifest.json b/tier2/games/night-market/assets/audio/sfx/manifest.json new file mode 100644 index 00000000..64fb6179 --- /dev/null +++ b/tier2/games/night-market/assets/audio/sfx/manifest.json @@ -0,0 +1,59 @@ +{ + "_说明": "《夜市一条街》真实世界 foley 事件的 CC0/公有领域真采样清单(逐个记 source URL + license,授权干净留痕红线)。抽象/UI 反馈音(点击/弹层/连击/解锁号角等)仍走 src/audio/sfx.js 分层合成,不在此清单。采样懒加载(首个用户手势后 fetch+decodeAudioData),缺采样回退合成。", + "_生成": { "日期": "2026-07-07", "工序": "curl 直下(Wikimedia upload)→ ffmpeg trim ≤2s + 单声道 44.1k + 首尾淡入淡出 → libmp3lame 96k", "口径": "只收 CC0 或 Public domain(免授权免署名);CC-BY / CC-BY-SA 一律不取(不满足免署名)" }, + "samples": [ + { + "key": "sizzle", "file": "sizzle.mp3", "音效": "烤肉滋滋(起锅煎炸)", "用于事件": ["order 下锅第三层"], + "license": "Public domain (PD)", "licenseCode": "pd", "author": "ezwa", + "sourcePage": "https://commons.wikimedia.org/wiki/File:Frying_fries_going_in_and_out_of_oil.ogg", + "downloadUrl": "https://upload.wikimedia.org/wikipedia/commons/4/4e/Frying_fries_going_in_and_out_of_oil.ogg", + "origin": "pdsounds.org/audio/download/150 → Wikimedia Commons", + "trim": "源 2.0–3.8s(取活跃煎炸窗)", "durationS": 1.79, "bytes": 22301 + }, + { + "key": "cash", "file": "cash.mp3", "音效": "收银/钱箱(硬币入金属钱箱)", "用于事件": ["cash_low", "cash_mid", "cash_high"], + "license": "Public domain (PD)", "licenseCode": "pd", "author": "ezwa", + "sourcePage": "https://commons.wikimedia.org/wiki/File:Coins_dropped_in_metallic_moneybox.ogg", + "downloadUrl": "https://upload.wikimedia.org/wikipedia/commons/2/2d/Coins_dropped_in_metallic_moneybox.ogg", + "origin": "pdsounds.org/audio/download/135 → Wikimedia Commons", + "trim": "源 0.0–1.35s", "durationS": 1.35, "bytes": 16972 + }, + { + "key": "coin", "file": "coin.mp3", "音效": "金币叮当(硬币入木钱箱)", "用于事件": ["tip", "cash_high 散币层"], + "license": "Public domain (PD)", "licenseCode": "pd", "author": "ezwa", + "sourcePage": "https://commons.wikimedia.org/wiki/File:Coins_dropped_in_wooden_moneybox.ogg", + "downloadUrl": "https://upload.wikimedia.org/wikipedia/commons/c/c1/Coins_dropped_in_wooden_moneybox.ogg", + "origin": "pdsounds.org/audio/download/136 → Wikimedia Commons", + "trim": "源 0.28–1.33s(去前导静音)", "durationS": 1.05, "bytes": 13524 + }, + { + "key": "bell", "file": "bell.mp3", "音效": "下单铃(自行车铃清脆一响)", "用于事件": ["order 下单铃层"], + "license": "CC0 1.0", "licenseCode": "cc0", "author": "qubodup", + "sourcePage": "https://commons.wikimedia.org/wiki/File:Bell-ring.flac", + "downloadUrl": "https://upload.wikimedia.org/wikipedia/commons/b/b2/Bell-ring.flac", + "origin": "freesound.org/people/qubodup/sounds/168709 → Wikimedia Commons", + "trim": "源 0.0–1.4s(响+自然衰减)", "durationS": 1.4, "bytes": 17761 + }, + { + "key": "applause", "file": "applause.mp3", "音效": "顾客满意(鼓掌+欢呼)", "用于事件": ["praise 好评欢呼"], + "license": "Public domain (PD)", "licenseCode": "pd", "author": "starlite", + "sourcePage": "https://commons.wikimedia.org/wiki/File:Clapping_hurray.ogg", + "downloadUrl": "https://upload.wikimedia.org/wikipedia/commons/a/a8/Clapping_hurray.ogg", + "origin": "pdsounds.org/audio/download/38 → Wikimedia Commons", + "trim": "源 1.8–3.8s(密集鼓掌窗)", "durationS": 2.0, "bytes": 24809 + }, + { + "key": "buzzer", "file": "buzzer.mp3", "音效": "失败/差评(蜂鸣器)", "用于事件": ["blame 差评"], + "license": "CC0 1.0", "licenseCode": "cc0", "author": "David Pride", + "sourcePage": "https://commons.wikimedia.org/wiki/File:Buzzer.wav", + "downloadUrl": "https://upload.wikimedia.org/wikipedia/commons/5/55/Buzzer.wav", + "origin": "cotswoldjam.org(David Pride)→ Wikimedia Commons", + "trim": "源 0.0–0.85s", "durationS": 0.85, "bytes": 11016 + } + ], + "fallbackSynth": [ + { + "音效": "切菜 chop", "退回原因": "Wikimedia Commons 无真实切菜/刀落砧板录音(chopping/cutting/cleaver/knife-cutting-board 全空);freesound CC0 需 OAuth token(内网凭据档无);OpenGameArt CC0 合集需逐层下钻且难眼验。仅有 PD『Knocking on wood or door』(敲门/敲木)——为多下连敲、空心门腔体、无切割嘶声,不能听验其读作『刀落砧板』,按诚实协议不采不可核验音色的样本。保留 src/audio/sfx.js 既有合成 chop(短噪声穿切+低频木墩 thock,本就贴刀落砧板)。" + } + ] +} diff --git a/tier2/games/night-market/assets/audio/sfx/sizzle.mp3 b/tier2/games/night-market/assets/audio/sfx/sizzle.mp3 new file mode 100644 index 00000000..06705a0c Binary files /dev/null and b/tier2/games/night-market/assets/audio/sfx/sizzle.mp3 differ diff --git a/tier2/games/night-market/src/audio/sfx.js b/tier2/games/night-market/src/audio/sfx.js index 89500310..a03a322a 100644 --- a/tier2/games/night-market/src/audio/sfx.js +++ b/tier2/games/night-market/src/audio/sfx.js @@ -1,13 +1,17 @@ /** - * src/audio/sfx.js —— 音效系统(M4 内容化:分层 WebAudio foley 合成,零二进制资产) + * src/audio/sfx.js —— 音效系统(M4 内容化:CC0/PD 真采样 + 分层 WebAudio 合成兜底) * owner:北极星顶级线①(W-POLARIS)· 设计书 §6 音效 24 类 * - * 【为什么自著分层合成而非 CC0 采样】设计书要求内容贴合音效(烤肉滋滋/切菜/下单铃/收银/顾客满意…), - * 旧实现是 ZzFX 单振荡器芯片音(几十年前 8-bit 味,不贴内容)。CC0 采样评估结论:保证 CC0 的源 - * (Kenney)偏 UI 蜂鸣、缺真实厨房 foley;真实 foley 源(freesound)需逐条 OAuth + 逐条授权核验, - * 触「零外采 = CC0 授权干净留痕」红线。故按设计书 fallback 走「分层 WebAudio 合成(噪声+滤波+包络 - * 近 foley,非单振荡器)」:自著 = 零授权风险、零二进制字节(不吃包体账)、延迟最低、headless 静默。 - * 【合成分层】每个音由多路声部叠加:滤波噪声(滋滋/切菜质感)+ 非谐分音钟(下单/收银铃)+ + * 【采样 vs 合成分工】创始人 2026-07-06 拍定:真实世界厨房 foley 走 CC0 真采样(真录音比合成贴)。 + * 6 类经营内容事件改播 assets/audio/sfx/ 下的 CC0/公有领域真采样(清单 + 逐条授权见同目录 manifest.json): + * 烤肉滋滋 sizzle(order)/ 收银钱箱 cash(cash_*)/ 金币叮当 coin(tip)/ 下单铃 bell(order)/ + * 顾客满意掌声 applause(praise)/ 差评蜂鸣 buzzer(blame)。切菜 chop 未找到干净 CC0 真录音 + * (Commons 无切菜录音、freesound 需 OAuth token、见 manifest.fallbackSynth)→ 退回合成;抽象/UI + * 反馈音(点击/弹层/连击/解锁号角/成就 fanfare 等)本是设计音、无"真采样更贴"诉求,全部保留合成。 + * 【采样懒加载】首个用户手势(initAudio)后 fetch + decodeAudioData 六个样本,非首屏(不进首屏字节账, + * assets/audio 在构建 §5 记账里本就归懒加载);解码前/文件缺失/加载失败 → 该事件 playBuf 返 false + * 回退合成,绝不阻塞可玩。node/headless 无 fetch/AudioContext,采样加载整体跳过,契约不变。 + * 【合成兜底分层】每音由多路声部叠加:滤波噪声(滋滋/切菜质感)+ 非谐分音钟(下单/收银铃)+ * 包络塑形(上升=正反馈/下降=负反馈)。总线经压缩器防叠加削波。 * 【手势解锁】浏览器策略要求 AudioContext 在用户手势后 resume;initAudio() 挂首个 pointerdown。 * 【静默降级】无 WebAudio(headless/门)或未解锁时 play 静默返回,绝不抛错阻塞可玩(缺音不崩红线)。 @@ -39,10 +43,55 @@ export function initAudio() { for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1; } if (X.state === 'suspended') X.resume(); + loadSamples(); // 手势解锁后懒加载 CC0 真采样(非首屏;失败静默回退合成) unlocked = true; } catch (_) { /* 解锁失败:静默降级,不阻塞 */ } } +// ── CC0/PD 真采样(懒加载;缺样本回退合成;逐条授权见 assets/audio/sfx/manifest.json)──────────── +const SAMPLE_FILES = Object.freeze({ + sizzle: 'sizzle.mp3', // 烤肉滋滋(PD · ezwa) + cash: 'cash.mp3', // 收银/钱箱(PD · ezwa) + coin: 'coin.mp3', // 金币叮当(PD · ezwa) + bell: 'bell.mp3', // 下单铃(CC0 · qubodup) + applause: 'applause.mp3', // 顾客满意/掌声(PD · starlite) + buzzer: 'buzzer.mp3', // 差评蜂鸣(CC0 · David Pride) +}); +// 各样本相对增益:样本近满幅,而合成 peak 刻意压低(0.1–0.28);此处把样本拉回同量级(再过 master 0.34 + 压缩器) +const SAMPLE_GAIN = Object.freeze({ sizzle: 0.85, cash: 0.42, coin: 0.25, bell: 0.35, applause: 0.22, buzzer: 0.24 }); +const buffers = Object.create(null); // key → 解码后 AudioBuffer 缓存 +let samplesKicked = false; + +/** 懒加载六个真采样(首个手势后触发,非首屏);缺文件/解码失败该键静默回退合成——外部资源加载失败不阻塞。 */ +function loadSamples() { + if (samplesKicked || !X || typeof fetch === 'undefined') return; // 已加载过 / node·headless 无 fetch:整体跳过 + samplesKicked = true; + const base = 'assets/audio/sfx/'; // 相对文档根(index.html 同 Phaser 资源惯例;正式装载随 GamePackage 同源) + for (const key of Object.keys(SAMPLE_FILES)) { + // 外部资源:网络失败(404/离线)与解码错都 catch 静默,该键 playBuf 返 false → 合成兜底(缺音不崩红线) + Promise.resolve() + .then(() => fetch(base + SAMPLE_FILES[key])) + .then((r) => (r && r.ok ? r.arrayBuffer() : Promise.reject(new Error('sfx http ' + (r && r.status))))) + .then((ab) => X.decodeAudioData(ab)) + .then((buf) => { buffers[key] = buf; }) + .catch(() => { /* 采样不可用:保持 buffers[key] 缺席 → 合成兜底 */ }); + } +} + +/** 播放一个采样声部(缓冲就绪且成功起播返 true;否则 false → 调用方合成兜底)。t 绝对秒,gainMul 叠加系数,rate 变速。 */ +function playBuf(key, t, gainMul = 1, rate = 1) { + const buf = buffers[key]; + if (!buf || !X || !master) return false; + try { + const src = X.createBufferSource(); src.buffer = buf; + if (rate !== 1) src.playbackRate.value = rate; + const g = X.createGain(); g.gain.value = (SAMPLE_GAIN[key] || 0.5) * gainMul; + src.connect(g); g.connect(master); + src.start(t); + return true; + } catch (_) { return false; } // 起播异常:回退合成 +} + // ── 声部原语(全部连到 master;t 为绝对起始秒)────────────────────────────── /** 带 ADSR 包络的振荡器声部(下单/收银铃、连击 pluck、号角)。 */ @@ -100,15 +149,19 @@ function coins(t, n = 4, peak = 0.14) { // ── 24 类音效(设计书 §6;每类分层 foley,语义:高频清脆/低频沉/上升正反馈/下降负反馈)── const SFX = Object.freeze({ // 下锅 = 下单铃 + 切菜 + 起锅滋滋(核心经营动作,内容三合一) - order: (t) => { bell(1050, t, { peak: 0.2, dur: 0.28, partials: [1, 2.4, 4.1] }); chop(t + 0.06, 0.22); sizzle(t + 0.12, 0.34, 0.1); }, + order: (t) => { // 下单铃(采样→合成)+ 切菜(无 CC0,合成)+ 起锅滋滋(采样→合成) + if (!playBuf('bell', t, 0.8)) bell(1050, t, { peak: 0.2, dur: 0.28, partials: [1, 2.4, 4.1] }); + chop(t + 0.06, 0.22); + if (!playBuf('sizzle', t + 0.12)) sizzle(t + 0.12, 0.34, 0.1); + }, serve_ready: (t) => { chop(t, 0.18); osc('sine', 720, t + 0.04, { a: 0.002, d: 0.09, s: 0, r: 0.05, peak: 0.22 }); }, // 出餐嗒 // 收银 cha-ching(两声钟由低到高 + 抽屉机械 clack;客单越高越亮) - cash_low: (t) => { bell(560, t, { peak: 0.22, dur: 0.22, partials: [1, 2.7] }); fnoise(t + 0.02, 0.05, { type: 'bandpass', freq: 1400, q: 1.5, peak: 0.12, a: 0.001, r: 0.05 }); }, - cash_mid: (t) => { bell(620, t, { peak: 0.22, dur: 0.2, partials: [1, 2.7] }); bell(900, t + 0.1, { peak: 0.22, dur: 0.34, partials: [1, 2.76, 5.4] }); fnoise(t + 0.02, 0.05, { type: 'bandpass', freq: 1500, q: 1.4, peak: 0.12, a: 0.001, r: 0.05 }); }, - cash_high: (t) => { bell(680, t, { peak: 0.22, dur: 0.2, partials: [1, 2.7] }); bell(1020, t + 0.1, { peak: 0.24, dur: 0.4, partials: [1, 2.76, 5.4, 8.9] }); coins(t + 0.18, 4, 0.12); fnoise(t + 0.02, 0.05, { type: 'bandpass', freq: 1600, q: 1.4, peak: 0.12, a: 0.001, r: 0.05 }); }, - tip: (t) => coins(t, 5, 0.15), // 小费叮当 - praise: (t) => { [523, 659, 784, 1047].forEach((f, i) => osc('triangle', f, t + i * 0.07, { a: 0.004, d: 0.12, s: 0.1, r: 0.12, peak: 0.2 })); }, // 好评欢呼(上行琶音) - blame: (t) => { osc('sawtooth', 240, t, { a: 0.005, d: 0.18, s: 0.2, r: 0.14, peak: 0.16, slideTo: 150 }); fnoise(t, 0.16, { type: 'lowpass', freq: 600, q: 0.7, peak: 0.08 }); }, // 差评低鸣 + cash_low: (t) => { if (playBuf('cash', t, 0.9)) return; bell(560, t, { peak: 0.22, dur: 0.22, partials: [1, 2.7] }); fnoise(t + 0.02, 0.05, { type: 'bandpass', freq: 1400, q: 1.5, peak: 0.12, a: 0.001, r: 0.05 }); }, + cash_mid: (t) => { if (playBuf('cash', t, 1)) return; bell(620, t, { peak: 0.22, dur: 0.2, partials: [1, 2.7] }); bell(900, t + 0.1, { peak: 0.22, dur: 0.34, partials: [1, 2.76, 5.4] }); fnoise(t + 0.02, 0.05, { type: 'bandpass', freq: 1500, q: 1.4, peak: 0.12, a: 0.001, r: 0.05 }); }, + cash_high: (t) => { if (playBuf('cash', t, 1.05)) { playBuf('coin', t + 0.22, 0.7); return; } bell(680, t, { peak: 0.22, dur: 0.2, partials: [1, 2.7] }); bell(1020, t + 0.1, { peak: 0.24, dur: 0.4, partials: [1, 2.76, 5.4, 8.9] }); coins(t + 0.18, 4, 0.12); fnoise(t + 0.02, 0.05, { type: 'bandpass', freq: 1600, q: 1.4, peak: 0.12, a: 0.001, r: 0.05 }); }, + tip: (t) => { if (!playBuf('coin', t)) coins(t, 5, 0.15); }, // 小费叮当(采样→合成) + praise: (t) => { if (playBuf('applause', t)) return; [523, 659, 784, 1047].forEach((f, i) => osc('triangle', f, t + i * 0.07, { a: 0.004, d: 0.12, s: 0.1, r: 0.12, peak: 0.2 })); }, // 好评(采样掌声→合成上行琶音) + blame: (t) => { if (playBuf('buzzer', t)) return; osc('sawtooth', 240, t, { a: 0.005, d: 0.18, s: 0.2, r: 0.14, peak: 0.16, slideTo: 150 }); fnoise(t, 0.16, { type: 'lowpass', freq: 600, q: 0.7, peak: 0.08 }); }, // 差评(采样蜂鸣→合成低鸣) leave: (t) => { osc('sine', 420, t, { a: 0.003, d: 0.16, s: 0, r: 0.12, peak: 0.2, slideTo: 180 }); osc('sine', 280, t + 0.08, { a: 0.003, d: 0.14, s: 0, r: 0.12, peak: 0.16, slideTo: 120 }); }, // 翻台走人(下滑) combo: (t) => { osc('triangle', 880, t, { a: 0.002, d: 0.08, s: 0, r: 0.06, peak: 0.22, slideTo: 1200 }); coins(t + 0.02, 2, 0.08); }, // 连击(上跳) combo_break: (t) => osc('sine', 520, t, { a: 0.004, d: 0.14, s: 0, r: 0.1, peak: 0.18, slideTo: 300 }), // 断链