From 489c1038841f6b09cd912ae4b766f9fa7af73c81 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 14:04:51 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E9=9F=B3=E6=95=88=E3=80=81=E9=9F=B3?= =?UTF-8?q?=E4=B9=90=E4=B8=8E=E7=B4=A0=E6=9D=90=E5=B7=A5=E5=85=B7=EF=BC=88?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=20D=EF=BC=8C#14=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 预设、包络和波形与保存共用同一份 WAV。官方音效和孩子收录的声音走同一套素材清单,不兼容格式在问题里说明,导出后 pygame-web 可播放。 Co-authored-by: Jett Wang --- docs/ai-rules.md | 3 +- docs/roadmap.md | 31 +- docs/runtime.md | 6 +- .../asset-packs/forest-sounds/manifest.json | 140 ++++++ .../asset-packs/forest-sounds/sounds/bump.wav | Bin 0 -> 7100 bytes .../asset-packs/forest-sounds/sounds/coin.wav | Bin 0 -> 7982 bytes .../forest-sounds/sounds/jump-soft.wav | Bin 0 -> 11510 bytes .../asset-packs/forest-sounds/sounds/jump.wav | Bin 0 -> 9746 bytes .../asset-packs/forest-sounds/sounds/walk.wav | Bin 0 -> 211724 bytes public/asset-packs/index.json | 12 + scripts/write-forest-pack.ts | 30 +- src/lib/ai.test.ts | 5 + src/lib/ai.ts | 9 +- src/lib/assets/copy.ts | 4 + src/lib/assets/filter.ts | 9 + src/lib/assets/forest.ts | 33 +- src/lib/assets/schema.ts | 2 +- src/lib/assets/snippet.ts | 48 ++ src/lib/assets/sounds.ts | 63 +++ src/lib/assets/validate.test.ts | 30 +- src/lib/assets/validate.ts | 9 +- src/lib/audio/edit.test.ts | 114 +++++ src/lib/audio/edit.ts | 112 +++++ src/lib/audio/format.ts | 95 ++++ src/lib/audio/kept.ts | 109 +++++ src/lib/audio/play.ts | 56 +++ src/lib/audio/presets.ts | 106 +++++ src/lib/audio/render.ts | 9 + src/lib/audio/save.ts | 60 +++ src/lib/diagnostics.test.ts | 19 + src/lib/diagnostics.ts | 23 +- src/lib/storage.ts | 33 +- src/studio/store.tsx | 79 +++- src/styles.css | 4 + src/ui/LibraryDialog.tsx | 96 +++- src/ui/Overlays.tsx | 81 +--- src/ui/SoundEditor.tsx | 425 ++++++++++++++++++ src/ui/sound-editor.css | 25 ++ src/ui/text.ts | 51 ++- tests/e2e/sounds.spec.ts | 160 ++++++- worker/mcp.test.ts | 23 + worker/mcp.ts | 3 + 42 files changed, 1951 insertions(+), 166 deletions(-) create mode 100644 public/asset-packs/forest-sounds/manifest.json create mode 100644 public/asset-packs/forest-sounds/sounds/bump.wav create mode 100644 public/asset-packs/forest-sounds/sounds/coin.wav create mode 100644 public/asset-packs/forest-sounds/sounds/jump-soft.wav create mode 100644 public/asset-packs/forest-sounds/sounds/jump.wav create mode 100644 public/asset-packs/forest-sounds/sounds/walk.wav create mode 100644 src/lib/assets/sounds.ts create mode 100644 src/lib/audio/edit.test.ts create mode 100644 src/lib/audio/edit.ts create mode 100644 src/lib/audio/format.ts create mode 100644 src/lib/audio/kept.ts create mode 100644 src/lib/audio/play.ts create mode 100644 src/lib/audio/presets.ts create mode 100644 src/lib/audio/save.ts create mode 100644 src/ui/SoundEditor.tsx create mode 100644 src/ui/sound-editor.css diff --git a/docs/ai-rules.md b/docs/ai-rules.md index ffbbec7..9e170ee 100644 --- a/docs/ai-rules.md +++ b/docs/ai-rules.md @@ -71,6 +71,7 @@ - 【已实现】生成的图片先预览,接受后才存入 `assets/`。 - 【已实现】声音也是这样:模型只挑参数(`op: "sound"`),采样在浏览器里合成(`src/lib/audio/render.ts`), 预听的 WAV 就是最后存进 `assets/` 的那一份。参数一律夹紧到安全范围(音高 20–8000 Hz、增益 ≤ 0.6、时长 ≤ 4 秒)。 + 不登录也能用预设和编辑器做出同一份 WAV。音频文件(`*.wav`、`*.ogg`、`*.mp3`、`*.aiff`)和小助手、WebMCP 都改不了。 - 【目标】删除文件必须在改动列表里单独列出并单独确认。 - 【目标】来自 WebMCP 外部 Agent 的写入走同一个候选与采用流程,没有「自动采用」。 - 【已实现】模型结果不合规时整体丢弃,不展示半成品,候选也不会生成(E2E:`a broken model answer changes nothing`)。 @@ -83,7 +84,7 @@ - 【已实现】普通 pygame-ce,不发明 GameKit API。 - 【已实现】帧循环里有 `await asyncio.sleep(0)`(通常紧跟 `pygame.display.flip()`); 不用 `time.sleep`、`pygame.time.wait`、`pygame.time.delay`。 -- 【已实现】字体用 `pygame.font.Font(None, size)` 或项目里的 `.ttf`;网页音频用 OGG Vorbis。 +- 【已实现】字体用 `pygame.font.Font(None, size)` 或项目里的 `.ttf`;网页音频用 16-bit PCM WAV 或 OGG Vorbis。 - 【提示词】代码面向孩子阅读:变量名有意义、函数短、关键处有简短中文注释;不引入孩子还没学过的高级写法, 除非项目里已经在用。 - 【提示词】沿用项目已有的结构和命名;只改被要求的范围。 diff --git a/docs/roadmap.md b/docs/roadmap.md index fced21c..5c10143 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -181,9 +181,8 @@ Worker 对每个 `/api/*` 请求校验 Access JWT(签名、aud、iss、过期 - 声音生成(仅授权用户) 音效与音乐两种。模型只挑参数(振荡器/包络/扫频,或 bpm/调式/和弦/三个声部),**不写采样**; -浏览器按同一份参数确定性地合成 PCM 并写成 16-bit 单声道 22.05 kHz WAV,预览播放的就是最后存进 assets/ 的那一份, -面板还会写上「在代码里这样用:pygame.mixer.Sound(...)」。入口 `worker/ai.ts`(提示词与参数校验)、 -`src/lib/audio/spec.ts`、`src/lib/audio/render.ts`(纯函数合成)、`src/lib/audio/wav.ts`、`src/ui/Overlays.tsx`。 +浏览器按同一份参数确定性地合成 PCM 并写成 16-bit 单声道 22.05 kHz WAV。不登录时用预设和滑杆做同一件事。 +入口 `worker/ai.ts`(提示词与参数校验)、`src/lib/audio/`、`src/ui/SoundEditor.tsx`。编辑器见下一条。 - AI 素材生成(仅授权用户) @@ -227,7 +226,7 @@ Worker 对每个 `/api/*` 请求校验 Access JWT(签名、aud、iss、过期 小助手和 WebMCP 不能改写 `*.anim.json`。入口 `src/ui/PixelAnimPanel.tsx`、`src/lib/pixel/anim.ts`、`src/lib/pixel/film.ts`;保存仍走 `savePixelImage`。 -还没做:音效编辑器、让小助手按这段说明检索素材。 +还没做:让小助手按这段说明检索素材。音效编辑器已在阶段 D 交付。 - 瓦片地图编辑器(阶段 C,不登录可用) @@ -241,13 +240,23 @@ Worker 对每个 `/api/*` 请求校验 Access JWT(签名、aud、iss、过期 入口 `src/ui/MapEditor.tsx`、`src/ui/MapNewDialog.tsx`、`src/lib/map/`;写入走 `src/studio/store.tsx` 的 `saveMap`。 -还没做:用项目里已有的 PNG 换瓦片图、音效编辑器。 +还没做:用项目里已有的 PNG 换瓦片图。 + +- 音效编辑器(阶段 D,不登录可用) + +「看代码」的素材区能打开声音。预设有跳跃、轻跳、金币、碰到蘑菇和一段走路音乐,不登录也能用。滑杆改波形、音高、包络、音量,也能截取一头一尾。勾上循环时,首尾会接上,免得 `play(-1)` 爆音。音乐能改速度和小节,主旋律、低音、踩镲可以单独听。画面上的包络和波形就是正在听的那一份。点保存,写进项目的 WAV 和刚才试听的字节相同,可以撤销。写入失败时项目不变,声音还留在编辑器里。 + +「在舞台上听听」会先给看 `main.py` 变成什么样,确认后才写。那段代码是普通 pygame:点一下画面再 `mixer.init()` 和 `Sound.play`,并且 `await asyncio.sleep(0)`。官方音效在素材库的「森林声音」包里,和森林的图用同一套标签连着(跳跃对角色,金币对金币,蘑菇对蘑菇)。轻跳是跳跃的一种变体,改参数就行,不用重新画图。孩子收录的声音存在这台电脑上,清单字段和官方包一样(许可、作者、来源、署名、hash),格式不对的不会收。复制进项目之后,游戏读的是项目里的文件。 + +浏览器能播的是 16-bit PCM WAV,和通过检查的 OGG Vorbis。MP3、AIFF、坏 WAV、Opus 会在问题里说明。小助手和 WebMCP 不能改这些音频文件,也不能改素材库记录。 + +入口 `src/ui/SoundEditor.tsx`、`src/lib/audio/`、`src/lib/assets/sounds.ts`;写入走 `src/studio/store.tsx` 的 `saveSound`。 - 内置素材库(阶段 A,不登录可用) 顶栏和「看代码」的素材区都能打开素材库。包的说明是版本化的 `asset-pack` manifest(稳定 id、版本、路径、媒体类型、像素尺寸、动画帧和碰撞、中文标签、主题、预览、许可/作者/来源/署名、内容 hash),语义不靠文件名猜。现在收录一套 GameKit 原创、CC0 的「森林冒险」(角色、地形、道具、界面)。缩略图和包说明在打开素材库之后才加载,不进首屏脚本。 复制进项目的是文件本身,记在 `assets/library/<包>/…`,版本写在 `assets/library.json`。运行和导出用的是这份副本,不访问素材库。同名、重复、更新和替换都会先说明,不会静默盖掉;盖掉之后可以撤销。加载示例是普通的 `pygame.image.load`。小助手和 WebMCP 不能把素材库文件写进项目,也不能改那份版本记录。 -入口 `src/ui/LibraryDialog.tsx`、`src/lib/assets/`、`public/asset-packs/`。还没做:更多主题包、孩子自己的跨项目素材、音频库、配方,以及让小助手检索素材库(本阶段只保证它不能绕过确认去写)。动画编辑器已在阶段 B 交付,地图编辑器已在阶段 C 交付。 +入口 `src/ui/LibraryDialog.tsx`、`src/lib/assets/`、`public/asset-packs/`。还没做:更多主题包、孩子自己跨项目的图片、配方,以及让小助手检索素材库(本阶段只保证它不能绕过确认去写)。动画编辑器已在阶段 B 交付,地图编辑器已在阶段 C 交付。官方音效和孩子收录的声音已在阶段 D 放进同一套清单。 - 导出 @@ -257,7 +266,7 @@ Worker 对每个 `/api/*` 请求校验 Access JWT(签名、aud、iss、过期 推送 `main` 后 CI 单元测试、类型检查、构建、E2E,全部通过后 `wrangler deploy`(`.github/workflows/ci.yml`)。 -尚不存在:云同步、PWA 清单与离线外壳、教程与配方、音效编辑器(单张像素图、阶段 B 精灵动画、阶段 C 瓦片地图和阶段 A 素材库已有)、AI 按人配额、 +尚不存在:云同步、PWA 清单与离线外壳、教程与配方(单张像素图、阶段 B 精灵动画、阶段 C 瓦片地图、阶段 D 音效编辑和阶段 A 素材库已有)、AI 按人配额、 对话历史持久化(一轮的对话刷新后就没了,设计卡和作品在)。 ## L0 分阶段(Issue #14) @@ -269,7 +278,7 @@ Worker 对每个 `/api/*` 请求校验 Access JWT(签名、aud、iss、过期 | A. 统一内置资产库闭环 | 已交付 | 版本化 manifest、一个 CC0 主题包、匿名浏览/预览、复制进项目并固定版本、pygame 示例、离线导出。见上「内置素材库」 | | B. 精灵与动画编辑器 | 已交付 | 帧时间轴、精灵表、多动作、选区与图层。产物是 PNG + 可读 JSON,播放是普通 pygame。见上「精灵与动画编辑器」 | | C. 可视化瓦片地图 | 已交付 | 瓦片调色板、点绘/矩形/填充/擦除、图层、背景、出生点、物件标记、碰撞标注和预览。产物是 JSON + CSV + PNG,并附可读的 pygame 加载示例。见上「瓦片地图编辑器」 | -| D. 音效与音乐编辑 | 未做 | 在现有合成上加试听和裁剪,官方与生成的声音进同一套索引,导出后离线可播 | +| D. 音效与音乐编辑 | 已交付 | 预设、包络和波形、裁剪、音量、循环、音乐的节奏和声部。官方音效与收录的声音走同一套清单。导出的 WAV 离线可播。见上「音效编辑器」 | | E. 游戏配方与分步挑战 | 未做 | 完整可玩配方、分步目标,用静态检查或运行证据判断,不覆盖旧项目 | | F. AI / WebMCP 资源检索 | 未做 | 只读检索和提议;写入仍走候选和采用。阶段 A 只守住了「不能直接写素材库」 | | G. 离线与发布验证 | 未做 | 在阶段 A 的导出契约之上,补发布演练和断网运行已缓存游戏 | @@ -313,7 +322,7 @@ Worker 对每个 `/api/*` 请求校验 Access JWT(签名、aud、iss、过期 - 内置创作工具 -像素精灵编辑器(单张图片的编辑与新建已完成;多帧、精灵表和动作说明已在阶段 B 交付)、瓦片地图编辑器(阶段 C 已交付)、音效合成器(本地参数化生成 OGG)。产物是普通的 +像素精灵编辑器(单张图片的编辑与新建已完成;多帧、精灵表和动作说明已在阶段 B 交付)、瓦片地图编辑器(阶段 C 已交付)、音效编辑器(阶段 D 已交付,本地参数化生成 WAV)。产物是普通的 PNG/OGG/数据文件,代码用标准 pygame 读取。不依赖 AI,未登录也可用。服务于「打开就能做游戏」。 - 小助手覆盖全流程 @@ -410,9 +419,9 @@ PNG/OGG/数据文件,代码用标准 pygame 读取。不依赖 AI,未登录 | 游戏配方与教程 | 未实现 | 中 | ❌ 缺口 | ❌ 缺口 | 不适用 | 不适用(加载总是新建项目) | 无 | | 内置素材库 | 已有(阶段 A:一个「森林冒险」包。更多主题、音频库、配方、AI 检索未做;动画见阶段 B,地图见阶段 C) | 高(覆盖作品、版权) | ✅(匿名浏览、筛选、预览许可与来源 → 复制进项目 → pygame 在舞台加载 → 源码包和网页包里都有这份文件;加入时没有 `/api/` 请求) | ✅(同名先问再保留/另存/替换;重复复制不写第二份;hash 不符或许可/来源为空则校验失败,构建会被挡住) | ✅ 匿名可用。小助手和 WebMCP 仍不能改 `assets/` 里的二进制,也不能改 `assets/library.json` | ✅(替换后撤销回到旧文件;复制后撤销去掉这份;删进回收站可以恢复) | `tests/e2e/library.spec.ts`;`src/lib/assets/*.test.ts`;构建时 `vite.config.ts` 的素材包检查 | | 像素编辑器 / 新建图片 | 已有(单张图片的画笔、保存和撤销;多帧见下一行) | 高(覆盖孩子已有素材、作品丢失) | ✅(新建 → 画 → 保存 → 项目里出现 PNG、预览里 pygame 加载;打开已有 PNG → 改 → 保存 → 撤销还原;另存为新图片;触控与笔;导出包字节一致;生成素材继续改像素) | ✅(非法尺寸被拒;同名不覆盖、自动加序号;未保存关闭先问、继续画/不保存/保存;写入失败后画布保留、可重试;缓冲区/填充越界与死循环、PNG 往返逐字节、alpha 保持、解压体积受表头限制、撤销占用有上限的单元测试;生成素材里「保存并关闭」不会再弹出空的生成面板) | ✅ 匿名可用(不发任何 `/api/` 请求;不新增 AI 工具,小助手与 WebMCP 没有改 assets/ 的能力) | ✅(写入失败不留半成品,重试成功;覆盖可用工作室撤销还原;新图保存后可撤销) | `tests/e2e/pixel.spec.ts`(14 条);`src/lib/pixel/*.test.ts`(单张图画笔仍由 `buffer`、`png`、`rules`、`history`、`session`、`save`、`export` 覆盖;多帧见下一行) | -| 精灵与动画编辑器 | 已有(阶段 B。音效编辑器仍未做) | 高(覆盖已有素材、帧和说明对不上) | ✅(匿名:画多帧 → 设时长和动作 → 保存 → 重新打开后帧序和时长还在 → PNG 与 JSON 的像素、alpha、帧序可比对 → 源码包里两份文件都在 → pygame 在舞台打出 `ANIM-OK` 和 `ANIM-PIXEL`) | ✅(坏的动画说明按整张图打开且不改文件;写入失败时 PNG 和 JSON 都不留下,画布还在,重试成功;选区移动、图层显隐、洋葱皮、复制/删除帧、精灵表导入可撤销) | ✅ 匿名可用,不发 `/api/`。小助手和 WebMCP 不能改写 `*.anim.json` | ✅(失败写入不留半成品,重试成功;试播先确认,工作室撤销回到试播前的代码,精灵表还在) | `tests/e2e/anim.spec.ts`;`src/lib/pixel/anim.test.ts`、`film.test.ts`、`save.test.ts`;`src/lib/ai.test.ts`;`worker/mcp.test.ts` | +| 精灵与动画编辑器 | 已有(阶段 B) | 高(覆盖已有素材、帧和说明对不上) | ✅(匿名:画多帧 → 设时长和动作 → 保存 → 重新打开后帧序和时长还在 → PNG 与 JSON 的像素、alpha、帧序可比对 → 源码包里两份文件都在 → pygame 在舞台打出 `ANIM-OK` 和 `ANIM-PIXEL`) | ✅(坏的动画说明按整张图打开且不改文件;写入失败时 PNG 和 JSON 都不留下,画布还在,重试成功;选区移动、图层显隐、洋葱皮、复制/删除帧、精灵表导入可撤销) | ✅ 匿名可用,不发 `/api/`。小助手和 WebMCP 不能改写 `*.anim.json` | ✅(失败写入不留半成品,重试成功;试播先确认,工作室撤销回到试播前的代码,精灵表还在) | `tests/e2e/anim.spec.ts`;`src/lib/pixel/anim.test.ts`、`film.test.ts`、`save.test.ts`;`src/lib/ai.test.ts`;`worker/mcp.test.ts` | | 瓦片地图编辑器 | 已有(阶段 C) | 高(覆盖已有关卡、半截文件) | ✅(匿名:画关 → 碰撞、出生点、物件 → 保存 → 重新打开还原 → 导出地图包再导入一致 → 源码包里有 JSON/CSV/PNG → pygame 打出 `MAP-OK` 碰撞和出生点;加入时没有 `/api/` 请求) | ✅(非法地图说明原因且原文件不变;写入失败时不留半截文件,画面还在,重试成功) | ✅ 匿名可用,不发 `/api/`。小助手和 WebMCP 不能改写 `*.map.json`、`maps/*.csv`、`*.tileset.png` | ✅(失败写入不留半成品,重试成功;试玩先确认,工作室撤销回到试玩前的代码,地图文件还在) | `tests/e2e/map.spec.ts`;`src/lib/map/*.test.ts`;`src/lib/ai.test.ts`;`worker/mcp.test.ts` | -| 音效编辑器 | 未实现 | 中 | ❌ 缺口 | ❌ 缺口 | 不适用 | ❌ 缺口 | 无。地图编辑器已单独成行。参数化声音生成见「声音生成」 | +| 音效编辑器 | 已有(阶段 D) | 中(覆盖作品、噪音、格式播不出) | ✅(匿名:选预设 → 波形和包络 → 试听的 WAV 与保存的字节相同 → 撤销去掉;官方音效进素材库 → pygame 打出 `SOUND-OK` → 源码包和网页包里都是这份 WAV;收录进本机素材库) | ✅(坏 WAV / MP3 在问题里说明;写入失败时项目不变,再试能保存) | ✅ 匿名可用,不发 `/api/`。生成参数仍要登录。小助手和 WebMCP 不能改 `*.wav` / `*.ogg` / `*.mp3` / `*.aiff` | ✅(保存后可撤销;写入失败不改项目) | `tests/e2e/sounds.spec.ts`;`src/lib/audio/*.test.ts`;`src/lib/assets/validate.test.ts`;`src/lib/diagnostics.test.ts`;`src/lib/ai.test.ts`;`worker/mcp.test.ts` | 缺口的最低期望: diff --git a/docs/runtime.md b/docs/runtime.md index 2a4588c..797d467 100644 --- a/docs/runtime.md +++ b/docs/runtime.md @@ -11,7 +11,7 @@ GameKit 没有自己实现 Pygame。预览和导出使用同一套 [pygbag 0.9.3 | 导出后不依赖 GameKit | 成品必须能单独打开 | ZIP 里只有 `index.html`、`favicon.png` 和 `gamekit.apk` | 运行时脚本仍从 pygame-web CDN 加载。GameKit 下线不影响;pygame-web CDN 下线会影响。把整份 CPython/pygame WASM 打进每个 ZIP 大约几十 MB,而且要自己跟上游版本,所以没有内置 | | 桌面项目直接拷进来 | pygbag 不能在一个同步死循环里刷新页面 | 模板诊断 `await asyncio.sleep(0)`、`time.sleep` 和 `pygame.time.wait` | 这不是私有 API。同一份 async 循环也能在安装了 pygame 的桌面 Python 上运行。GameKit 不会在构建时偷偷改写用户源码 | | 音频格式 | 项目里生成的声音是 WAV(浏览器里合成,无需服务端) | 实测 pygame-web 运行时能 `pygame.mixer.Sound("assets/probe.wav")` 并读到正确时长(0.3 秒),所以 WAV 可用;OGG 仍是体积上更好的选择 | MP3/AIFF 仍给警告。声音要等孩子点一下画面才会响 | -| 音频 | 浏览器需要用户手势,且 pygame-web 支持的是 OGG Vorbis | 示例把 `mixer.init()` 放在第一次跳跃;WAV/MP3 只给出警告 | 打开预览后要点一下画面,声音才会响。桌面 WAV 文件不会被自动转码 | +| 音频 | 浏览器需要用户手势。pygame-web 能播 8/16-bit PCM WAV 和 OGG Vorbis | 声音编辑器存 WAV;OGG 要过文件头检查才当能播。MP3、AIFF、坏 WAV、Opus 在问题里说明 | 打开预览后要点一下画面,声音才会响。桌面文件不会被自动转码 | | 停止游戏 | WASM 进程活在 iframe 里 | Stop 把 iframe 设为 `about:blank`,重新 Run 使用新的会话 | 没有进程级调试器,也不能在任意行暂停 | | 离开页面 | pygbag 运行时在 `can_close` 为假时会注册 `window.onbeforeunload`(`pythons.js`: `if (!vm.config.can_close)`),浏览器就会弹「Leave site? Changes you made may not be saved.」 | 构建时渲染 `can_close = 1`(`src/lib/build.ts`),预览与导出都不再出现这个弹窗 | 游戏自己不再拦住关闭页面。游戏里没有未保存的数据,工作室的保存由 store 负责,所以这是想要的 | | AI | 不能让模型自己改项目、构建或发布 | Worker 只提供补全和生图两个接口,只对授权用户开放(见 [ai-rules.md](ai-rules.md))。产品逻辑在编辑器里,生成结果要先看 diff 再接受 | 系统提示词仍在浏览器里拼装,授权用户可以改写它;还没有按人配额 | @@ -35,6 +35,10 @@ await asyncio.sleep(0) 预览和导出都打包项目里的这份副本(和别的 `assets/` 文件一样)。成品不请求素材库目录,也不请求 GameKit 站点。素材库以后升级,不会改掉已经复制进项目、或已经导出的那一份。模板本身没有为素材库改过。 +## 声音 + +声音编辑器把参数合成成 16-bit 单声道 22.05 kHz WAV。试听播放的字节和保存进 `assets/` 的字节是同一份,所以导出的游戏不调用生成接口。pygame-web 能播这种 PCM WAV,也能播通过文件头检查的 OGG Vorbis。MP3、AIFF、坏掉的 WAV、以及 Ogg 里的 Opus 会在问题面板里说明,不会假装能播。官方音效在素材库里,复制进项目之后和别的 `assets/` 文件一样打包。播放用普通 `pygame.mixer.Sound`。模板没有为声音改过。声音要等孩子点一下画面才会响。 + ## 精灵动画 精灵动画是项目里的两份普通文件:一张 PNG 精灵表,和旁边的 `.anim.json`。说明里的 `animation` 字段和素材库清单相同(帧宽高、帧序号、毫秒、动作)。预览和导出把它们和其他 `assets/` 文件一起打包。播放用普通 pygame(`image.load`、`subsurface`、`json`),可以是孩子复制的片段,或项目里可见的 `sprite_anim.py`。模板没有为动画改过。 diff --git a/public/asset-packs/forest-sounds/manifest.json b/public/asset-packs/forest-sounds/manifest.json new file mode 100644 index 0000000..71b99cf --- /dev/null +++ b/public/asset-packs/forest-sounds/manifest.json @@ -0,0 +1,140 @@ +{ + "format": "gamekit.asset-pack", + "formatVersion": 1, + "id": "forest-sounds", + "version": "1.0.0", + "title": "森林声音", + "theme": "森林冒险", + "summary": "跳跃、轻跳、金币、碰到蘑菇,和一段可以循环的走路音乐。和森林的角色、金币、蘑菇用同一套标签连在一起。改参数就能做出另一种,不用重新画图。", + "license": { + "spdx": "CC0-1.0", + "name": "CC0 1.0 Universal", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "assets": [ + { + "id": "jump-sfx", + "version": "1.0.0", + "title": "跳跃", + "path": "sounds/jump.wav", + "mediaType": "audio/wav", + "tags": [ + "音效", + "跳跃", + "角色" + ], + "theme": "森林冒险", + "category": "sfx", + "preview": "sounds/jump.wav", + "license": { + "spdx": "CC0-1.0", + "name": "CC0 1.0 Universal", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "sha256": "86c53bb89599dfdc7ae13cacd2d6dc0811e5e4c57de6e6ea8b07c442bc37f8bd" + }, + { + "id": "jump-soft", + "version": "1.0.0", + "title": "轻跳", + "path": "sounds/jump-soft.wav", + "mediaType": "audio/wav", + "tags": [ + "音效", + "跳跃", + "角色", + "变体" + ], + "theme": "森林冒险", + "category": "sfx", + "preview": "sounds/jump-soft.wav", + "license": { + "spdx": "CC0-1.0", + "name": "CC0 1.0 Universal", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "sha256": "90d5e223acc4f35c4cb7e1b403e5406687f09bcc3ed3525c6157356073ed0124" + }, + { + "id": "coin-sfx", + "version": "1.0.0", + "title": "金币声", + "path": "sounds/coin.wav", + "mediaType": "audio/wav", + "tags": [ + "音效", + "金币", + "道具" + ], + "theme": "森林冒险", + "category": "sfx", + "preview": "sounds/coin.wav", + "license": { + "spdx": "CC0-1.0", + "name": "CC0 1.0 Universal", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "sha256": "8bd3c7a9ee7ac7069dde9e7f6a23e82c8659995ec82020bf9ba4c5453f80796f" + }, + { + "id": "bump-sfx", + "version": "1.0.0", + "title": "碰到蘑菇", + "path": "sounds/bump.wav", + "mediaType": "audio/wav", + "tags": [ + "音效", + "撞击", + "蘑菇" + ], + "theme": "森林冒险", + "category": "sfx", + "preview": "sounds/bump.wav", + "license": { + "spdx": "CC0-1.0", + "name": "CC0 1.0 Universal", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "sha256": "3eb5e1dd1152529ed7cd7054582e0f5701d40ec441e98d5603d5bfc471409eb5" + }, + { + "id": "walk-music", + "version": "1.0.0", + "title": "走路音乐", + "path": "sounds/walk.wav", + "mediaType": "audio/wav", + "tags": [ + "音乐", + "森林", + "循环" + ], + "theme": "森林冒险", + "category": "music", + "preview": "sounds/walk.wav", + "license": { + "spdx": "CC0-1.0", + "name": "CC0 1.0 Universal", + "url": "https://creativecommons.org/publicdomain/zero/1.0/legalcode" + }, + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "sha256": "57e88b1634d720e891835872cbb41e9f3a9b2efe0f83414601e2cc8345d6fe65" + } + ] +} diff --git a/public/asset-packs/forest-sounds/sounds/bump.wav b/public/asset-packs/forest-sounds/sounds/bump.wav new file mode 100644 index 0000000000000000000000000000000000000000..20b86f5b81810ac448b0ba9f0d195cf5ad7b981c GIT binary patch literal 7100 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 literal 0 HcmV?d00001 diff --git a/public/asset-packs/forest-sounds/sounds/jump-soft.wav b/public/asset-packs/forest-sounds/sounds/jump-soft.wav new file mode 100644 index 0000000000000000000000000000000000000000..2c15d3c71353eb2b3caa1220b69e0a18d0b4442a GIT binary patch literal 11510 zcmX|nWq1{5*Yzzkb0jD3A;BfMI}~?!EACdL1lOX)U5gfXFYfMcK?8)i9i5q5zWqKw zzAHc4E9C66b?vp+Ove^Yn`V_ks9S@sO@@q~P%sD~gyE-fSA-fLMu?ywG^qcC{tw`9 z5GsP|p)P0;8ioEq!%-j93ROm-C{tdO%VZ}RA|Ht9qJ((FM{vwnvshN2jbr=RPgaso z;J0~x_}3)SR$iC2(M8k{Kg9!yAUkMt#j6}qhihdl9?LX~R~w}zs*Th=ilm+BPVx(v z!2Qu0c~$)2l*h0#W;LUhUfoyL8|zWsey)$M)9y8%$=*S}Zh8l!gV~)8;ZwzW`5*d< z18EcGFZGTVWbJP|Z;y0L_fH9U6?imYj(=N6sNW0Q5^EjHFLj9$Pp{&3=$+`v-PCGOxJnrhBM2M89cFW3>cAkMIsURqbhMXsh5? z%)d}zk>IkSb;CPHiion|T|y@YuL|7lf5Pva?LW(Lbthel$H}(5nCbRi@yvGB$;-{& zp4l`#Ic0Wo=#Lja^N8zH;T{D%l|!QK?bM!QZ34 zR{WIjv+bMbds6b_)N>jCW-rZ4aQE>wHOq=nY|t;-E!%#_lE6PgI)qn>3eD$=dsQ%h zk?I9I#Epzu5VXGD^-;J+@KA-)( z{#(M2&c7<91!iVs-^<(Wp5^OdRuT^UgI?0s*hV<&2Zo2FgrHk=<~7frnsFrcD`cqow;7**e)%tH(a#=VLlM+a6hG##c8|bm>1YOBsBbeB-F zu#*2s`)uk&vgWDC6!J~gURzSOww1<&O7=Bph&Gva2b6(U&cmuM}l zji9Fx=grL3zEAFw&V-zsnZfCOQ;z-=No~It`1bnylH>-dp7euRy>o+HH$4;dV(g8W zkEm5&m~n(R^3qR^`vm*Eae{gn!uR;3okU9BXVIt%q_27mHzqZ|4(eH+T$DnT^ zW5Z?S(wOMDvvGyfI{8yd>-3A6C3809 zDeke}M7=3HE@JRpnyz-TUa%MRUl5oc+%D`?L`3wIe2FpRqihj7Lu&@V2pHwC*mqfe zQ$LVNC|sN}JL!3zHLfaoud^p-=1aelGBDZtV{r0a=>5MlQ?py-?RRP3f%-KwR!l}O zNLh7(<%_MlV?{u6P@T|K;mMJ;V|K(eiAoD!8CpH~bHD;edHXxdUus$U4$Tu4*=K#J zx3)ViZ*xw|ES7#GwcoGMpD&V&r%p+Kl$k$gOx{&jptp~H#58$xw3d8T%2+1auKC#l zItFbH`4$!*IXc=Ey)Uv?cu?r=ps4{B9Vxc$mhNgWeTe3Z`i$x4yb12g&fJ{iSz|LQ zq~-oP_4Dwr+|+6rQ?oATAZJ7OT<XK_yt-xA|8%M@|A4V|VQu)MWVNBMwWLGwe7g}sl!(PJX(hX;oy1)T|4U-mk7HGt>5FJkE0E#5n7@2YDCjhs?)3 zTLzJeN_%arb-Dep<9@);ARHPSUOn=D3o}eBl}3^rt}4=l~P)#4au09wISzd-fdT+C)cN%F}$p-k2}(V>IBO?+giU} z{>KBa1V0FU6Mi9bU-;V4xxwQD`}=qFYhU12!L1Dqpr$7fH^T#&vpH7MPYY0aj& zJkR6G_WbgFG2XC8;yOA@4l3KUmDa!P6C8sAItMilsT5W)Vo}(dknKT-0?s+^*k4i>R{5f<$__WaV!N&t1`KS1)_QKW%T0dnL*@3Q$B&M42dTZ}k z_d4gr+@x$h2F>BTeE?4+Eld0SkQJRN;y42^x}XXSD{nATQ`NVe0xAUI zkQbpMv_wdI=SY3ZiUr>F6E87dmHiN=1&cc8nGb8_y{?4Oyj8ILml$r_nc zGtc6B<=){PsaG>Ge<0SNUZk*+svfgUu+{YAj%xvPgPMeBVZB1u1U(8+{pP|xs|^Ne$y$an`Jy3VC0hE8KOx>AsCd6PC;Wkxg+9*`zekQYGZq(PWdh2n-`Uy-Im{4mUzifJG*oe;Qa{=`G+n29{ss4$8^N8Isqp9NtGhL^a568mLayepuSr&e-D|^Ze5SLId~u*K|Cw548C#E41S3721vDpvAJ7 zxW;;$hQ7g9&HLJ&;0kyCm&`@s#n6)lV8OnCt{`M!HZXGKL;j zQq&@rKGu!4=XRT;zW*jiPro?(SL?r)VOm+$NiUE&xCIK4ANgK3&a7+L^|#(Vo(b;8 zt}y2fr|v568RXsQd!$RFA{)rph?~-h3Xyhny0Tw=rAbRkTSxmezkeO?{0`Ws+1gqQ zS={OaWe1%|+TvnJh`0O%TWSt78ta98((}oE*|pWV(RJ1R(_{CQ*E<;#&5i5=PZXxi zhij5-p+_=XvJ6QcXG4=ksxTWtxp z`PTK8ecA=}vGR?&i61G1YoK;=n3%)2urua!BU88QC45c2gFQ981$`DhQ-5jvXKrCL zd4JJR7C{t$$Jfbrs9F!TiWX=|wcN1k)@W-@OD}DPxwY{ZyZ=1sPq4->VdA4)`r@Kn;AhAZFT+MKL4v^cCit&1%e zv~)F2ZLiFu=SU_lf_tJ>^1dLV4xh+Qn3+Z?W2nBz_rp8bS5|icip@5gFdKiwmy7l? z7(GX;aC_oUAJT7jaK zJxAYav@|{DzpOoH{D9~xDLRIJ$2M}744{F^C1to8q21JywOZOT^{Y}@SwP>DGGrEh zfr_EY@}bBt#_?M$k_|U68G*(C-R;|@w==kL(Co$(@U*XR$n$b23d7g&7!phG(@9EE z^|?ArD`lCkJyi>-6O=nNnvNt_a1b7d&Pc25DUNW-Iw$Vp-KXz0x|o!mV10RjxFkl%Xml4%#6`&qnAq~lXJx5cTT9ihXhE=L zPAQ7gmF^=x+!AjAh2%H6LVV+u_+0kdEN)IR?&&dlJv|FD+S)YC1FSo@h_hm_3_~~3 zI9!lCB{OMR<)gAht@S^f0a{=6xPq0A^k3q_P4Ie@EUU{!;yo|Lr?Q7;tT`I|4A5KY zrhd@qVcOU^Hk3z-J7S_NhF+t2xH9=pR?~(`uCfziN0z;sS8b+lP*P|Ox`@1o%1=QL zWQ-gkE^SJ|= z77w_0PmNNCDd(sacGf=ZMa|F#nJlV_1^hKD#uCiiMwl^3_xkqf-3+UF&K%C7`2)~v zO3OsK6xG3*cq?g31wE$pRfDwa+E1;DHdlSA6jsJT*9Vcl_$U&xjr>QX^ICi{dvC@= zHt*=+dQ;u2?}y0k>;fCfW5r`JQ&vFV&}!U>xX2#bRk5h&KxN8jS**QRo(Y$4ZLU$UzLwau`z`SOTV@3EYzCcu$sd5WyjRihN`qNP5 zmNHR|*WPOnwP{fJ>K|WEH!^) zIcyj2EHrsejzF>a37$mpz# zrz;iI@9KKZA1v-wok~4rDa?5>G9F(=eyF?LEpmBXsLflmh&k4{q}#wpn{mk)YZhT| z*%JPn$P;^H59AM1K7qv3k8}lKr&m3sc_HeR>PIDBnLw`r#`nN`P_C>i{}!)#AwHU& zH&wHPu~kp?CFmuMPsS> zT|++OQaAx!m;SQ5*v+$9ZMMLCVZ<53bPv?JuMuY6H)pU){1@K>=&qvk=nq_kydz6# z1HgywIZ~YEh__X1oK%Nt`7M8GkJJdYpK=Jc zZ$0uieu?7HP8p6uzNQfKlHMC6+KPgX>>OO*ljkQ zR~D&ahwO#|@GU$IdOMZwP`au9+AS?nE2@o#X_2%!T}3{?&K(6vE<`i2lD}t#*+}!0 z!Sp72to~A8Y&0}=^EhBxe(^>ulZ}yyPT>)x5PeTqD$OB-XSEcqqBcdnq1cs9^dIsA zmxUT%mYQrUHt;Vjo{crn8Q5r{N9)h@MMizoYaRo81;E~N*%S$M7LO*y=qI{PX``yz zW$l|*QX8jUP>9l!t_J@K;}Phj)I|gFH-FBeS%33@k*n9#OX*4aW}~C&$8NG|5O0Rq zEq{mI^%&15b*YCQRfehsw71$FEkNs{ZdHEJGW1V!5fj`JIL3QXPz>eAn8&PZ&Nm+E zVR}25fHTnnN-+DuX8E4EtSPA}(Zx)@TKe~fv0dvhGhv;A>zxq~P z1-Q^r+oXO`iYue&DWc>0cmaAWLuC)KgD11H>`(K&fsCfm(eLzCMk`ZgSJ`A<38LK# z3@{qM#LGxCij|8>f?6JGaauFf`sxDZ5e=c;$u|5Ql|*CY8R)(Sd?9;mhML`tZTb)2 zY`vD@HjcptD8fHOH+KZ@?xH!sdpzVg9jO#kzp9%xY-y$~RbMDkN^iQ0q(E)|MCYJa z8i_^x2@7RC%x%VZy`)|T(Ep?{$}GW>*jC;hdik+jgc<=-pC^CPa!M-PLVdLu%N*^F z>IZCZ9q`=(fT{<8@m7|T#U*%R(UdJQpBdr6(jEF;{V$^~blzz;hL;k_VkcBK3SwPO zT2fWHu1r^}Yff#ymZ4Tu6O;>7kVa66hk&S^p&>Ohz8*?>l%l*V%F;~_HbUud@NJW}S4=O|cXYGO})CTH&;V$Y6_1yCC8z1 zfG7vZ5a{4<$~N_PE!r|myP>LTGi5P-L;^_%yao_2O7;?4`B#|#LFPUqRWGgA)eZd| z++3AeE<46Y14l@ad*MPVOumwBw3iYM{ku-1mL}Rl^}gb#w1La$IS$2LfOEVT`2df% zv9Ex3gN(g;if@iy518gfa|)}$J^ZxzQ&vRTz+py_Qh-hSl)-9IElE2Atf+=MRk=tx z`HjrR*Aay{7l^yu##=&vJT?N1c7P=B;3n>3hOw7yHE%BhVN#a?FR+vQWD#wqXzFcs zo)&89tgTbuLhtsVn@J*$#l6v1`B@YY{rFCn1lw_tu?OloPj6%p^Ezk(^||C%fVb3z zdR_$7tVIoa9x6}+Hsf|+>V?$)$}XBjijaYLH~Jxq$-!bT|HVqO5#}MdtIEUfwi)-0 zMP@Um@mqYZXdtD$jHbcNd&y}!PAR9RtNVdhQLPDd+Jpb~#%lZ;MWJ4DyZFwF^AYTr z=`^Yv(*fZ;FjHg9((D)e7r1$hd?QyulooQGOsCa=Ngh%MYxgvNt%JH=`A7@Vq2w^m zLp9J$d0Y62j(ij3zXaU9XJI})`Y~fTaPp6A9d84?=c=59%EP|i4!b5$xueX0TkjJ1 z)k<9h*GLgMik!wAHAO4rJ5flCgbPQQ&CNB&C%psUxeqL_FFA-ks3BS|KZxRDJip2uth>3#aO#uwGR9Y9E#%7t z9?@Tf$-8n2DuX}cmC*k#x)*kqO&gReGy_S_N&k`b;ULjHfqA7#WPuA}i`C z55v{np6_CAh;fUNrMvaruvrj03R7u=%V3}kLYL8S96@f7KWGf(YrGn#C8<@_MUeRl zbS`;|%i!7Ql`JV|ikCc|&txynlIASqwcg6`7(30@%nOXOHO%Br*$TPPHr$-#!Zp-L z$$-65L0bU4tCI3J{X(jdW%viG1J7L30cW>xCu;@Q6*IcPb$Qh23b*QBKpqd@3W`Mr zL|7Al#S1|PcuA)y1)%e9sMKW&&yv+s^^vI}hd@*L3`2oFp!-cz9)Rb@KMA+yjaXXSp7Sj^IiTbMvFw-@a zEr1aH$PHW!FGA_Etvo3r#1#ICHDr5DyE(>q4;>l;^=rwK`ClSG+_ha$4#ZcSTqC`x z8#r%Cbv#(AqwJ&_bo+B$9d8A+87`l}y|fkL8_8anwalHyH)9$g%~95jf8|qPvmJy? zC89sEAK3*6@th7*BzSdSsR-H-rX$D;TpQ5AA5D~BL<@0}$MOX%*X#wim(N&k7GoD# zN1g%-PN+N}e?zaJs{~$6O3;h6o$?)+_z^&?xipLPB)4%z@X3Y}2Ao9X%{QcFY}hb?FPymIUQN?TDYbUlj`&)ZL7Rj zwkaXd13LMGB;jtbuj>QzEG>6H?Un!on8dQoL1shqG3XIVY!vkPY|u{D$~d$SRlw(Q z1M+}$q=|H(k_aB%qxI-nQkLw&ad<6qpm~yu1d+vu!WQcRwQFszHDlRsR+^vUwZwJN z47NmP^a1t5$*^_u$Rx_>9OW;C(TOyNi~#1^7r#fH(Noz<-VqJOWnL3duM+I)k!FsW zz;sZ^h*%(O@U}%TT8kp_W*kShlfra2Ew1baOxZ^B(T$L!HK2|x2ags@8|-LJ%!h88 z3zhrJd|`HCuUS|Aj`sjvq$gb6y`YzR;Y8evd>}pOJK9aD0rz_iP{V4F^SBnifNG;l zpkG`C{(BQHh`X#Mdj#969!yCcei63(e}Hr+C8*@6B;E`6(sod8HqtPq5ok3nV8gc~ zFL77=0p1r$l0yJf#tJu|3YyJ4W@neo8tfF*>>w{Dc8Gj(oeV^ak%DJnFCGU9*g&`r z8$zbr(pS)rU-3|!g%S{xbEThL1Ngg(Lv~pmcFU~8PQzWa2fAn-s7UjqDaWH!$n{&? zhTJ4I=`mVNxdK<> zwgNwx4R!{gm#8t^<;BQ4qS6HVgD$6WaP!qB4{|Hv}(GR)dnK8{nEw&IW_ z7iifpcmr_^P-Lkz)$r9otgZ~c!s6mzjpFM#a~h-~&-r?f^w}BB&#+@L8CT#l%T^(>=5n%<5ob;FY*IzJxlVRPZ}e z9u-Z1(N5qFzMD;CUs(%K>hg)XZ~=6dmt;}21UXSp$WJ_30o?s}I-06ZZ(=cBj<_S{Jp#b@##yahxRCa1{HfNA@HcaFucaCNc;-U}K)%h3nm z4JKQ0O<=F%Q6M@jTY#E05BR`U-kY0j6LUf@p9gPdim#v@@0J=GiteFOcnNUM)-d<} zv^7m9f0KA}7Z1T2=m|~GPdOi$(`_+CSj2wbf~T?ftR~;YeQ-DY2TDSMd?hQPmB0?$ z;Dgu-KHMY)0a3@{AA4bu^sat|P)PwWz~;rUMJ<^ntc zRO-TD_lYP98GZtlnv5Q!!gwNn0KGGw+##pHijADX{jmd|gZH|E;cgxQ6~86Mfj5s} zO5^!7{sgQ{fIA~bj*?e^hYUcckqY0uAM3aU*-Z9BL@GIj2jF0Q4UK`T<%ygLD(Yvk zLevvE;7v!q2oNV8e7PqgVF#Rr4c!Utf_?ZKUJAS|o+Oa1GjC3)>%C93{nYdv<0^ZmEs~) zsW@zk)v~#iz@bKqq9T#6=Af&HmSPQLx40Z5|C0zchf2Ih1)*P$0q*=p`jSZU1TVn# z040v2VW<%L09&Aqw7|}pCg8~{;KOUs7zc_2A{(Y@8oU9g!P{^vL1~S~eef#T#fbcr#sk6mR;KMB-+9#0Rj%W>7iNM|QCdf<- zaKsmQ6>bMvd5o6Aci2It{Tu$;2K;leXeAtg6@^59u~U2#g=9au9cC#GzHJLc6p6dw zHTXFWAZhpz9)-){9CQ**fUeVFa%ahgppx7d%R~neE&{;TB5_*~$oW*LP#!#o7>@R! zuP7f_SqVMkgNt146Q>wQ9;n456khgCS>;xSm`Wc#cxpKlOkOdlYQg{`3xqY z0h)qN01_93h*sb`_%>b!_TunQbOaEn9SgX)Arl(T?Y zq{)Kt*5P89Bqu5XNVx*vgpB+S?*%&HcZ*S1R2Z)Ki*ljt47yo1X!DCjH!(*13oO7U z>p+cn%9l`|>JZNs^azyeabqz@;|05ZosZp-U!0g$p4su%LgT3LQX0mJr1e zaZ&sf0kVedFBi*Gu(cIb2AINhv>QH^2R&CGH0N-fiEg80q<~rfC*U!)7C)7 zN{@IBx`PvDxIXOQrSb$YTL#fIg>A6{okDLxS1SOKDEJF#it7Pw+oDoPL*JpZw}L0# zAf8Z}16r^K7e;Hy=_;`PM*3t7MAI8`vK?K6NyLBwIp_)0b}3kH3EGbhw#a>8ntubb zw1VyCFHM-<+Oii+&1PW5@1$3T!;a_()4CiTLibTJ@vW%{*cq+U?W35g}B#) zCw-u6O8`byk?rMZu(}7{C;bF|grah&1z^}rv2p^}Rro-=T9?E#-AqP~C-6+u6tVB;-( z-y!g1wj3$D%8r0TGod#Q!ae&MI#qy;c(|R~gC~<=Gj0Pri^0MG$Y(8xBoyAF&6FSD z$~+@?$(7LGqvbf*0P7(`7oqB3fF}?X2IyA}o}F|9?3oO{%!VlXpf+H)42lI_$KdYz z1b=-+9)oyR!{=AXZSttREFVE{q(U|{uu&MUrMggqZs5yU@SrnTtO*thpb+SF9c+As zjNFG$os|245gdl@yahA+5jIYqg^_#)R%|Ucd2guUZz|9@9yc{l2kR*vqRgqHemqtX%XFK^u!rTCjbCrK@|-^>k9xt z-~>hvn=vdq)bars3qFJT%n?Qbr@(mF9GyW<>>M_oZI7>DFK!8!%XPQ9W2IEgsdTD5 zUD2!}u6%O2Rr%qvdS#zV$CT2N-6hpaUYUoOD~mT5tBW5O^(k_8p6i_9+`;9li?{1i z*F4uAZg<^++}FCB-TQew_0V{3_2fN=dA;$f;XNjs8g?718(w7%&Z^LF(ue6EWkzS3 zGFD{-WZX*cnx37uD9t17Qfk{&m1>=;MAbj|S#U(iwh$p?c<9^Enqm9GpnAOetGZ$M zv2YvBG)f*7)OOVN(Dv01 z)sE3l*3Qx{)UMQS&~Df6*B;ZJ)n3)!(LT|>(tgxNL%tHcf=Ik9Nl8wYTYK?PTfJ>3Eg?!b=^JPQ{5ZgXI;E5 zO_!}R>B@DYPSliZjGAmsnkG*3S@TBoRC7;rU2|S@LUTZ~Q?p64O0!rqM>9n;PIEZ+ zRP4pro3RgKpU1w7{TiDPn-QBETO3;%E5~wi_Hk};esRHZk#QfxUxhyozY~5n{H$ea z`@^?~ZwOxzzA$`N_@wYL;X}jwhW7~X5Z*GpQTT^AZCp~EJ}y7b9LL9jctyNpyhnUM zd}w@BeC_x~@vY)J#`lQt7k@;(SG`rePQ6?`Up+%TK|M-6SRJkIrf#opu5O^Np^i`o zt9{jOYI`+_hY8jR&Iw)#$^>=79|?66nk2MN=$!CZ!hnR~3F8u`Cd^4#oUl4!TG-!V zBf60fY7ET|4NBA`{+U=mu~}l<#IA|G z69*=aO#C}>dg8pqrHN}3wAt@ockdGm+LLP^-PwJl3Cuwlf=%k5BGm;i0El*mPv@L0G($S5MmVqLaKtz!TG_N!AZd}!S91#20skG6?{4PbnubjJ;9TcXC*I6 zUYWchc}Md8F`Oy;VI| z-B;aET~wV^9a8O9ZC0&OEm6%?O;hbkIhb-Hg zDQv1;s%xrGswy=u=yTBPpr=9ig02Of4>}%nAZTaM#-LR}i-TqdO$izsG%TopP|u)F zK@U=2q`pi2nwprJk(!rUoLZSmQ@J$zH1{;Ww2-vOw3=xR(^{mpPrIT#t30OMr`)bw zuUw&AsGO;sq#UChqU@{euI!*}scfjMrHoQ0r|Hx3(@N6#G$vha;mlK+J^efRxAJf7U&p_if0%!uzo);GzrvsK7yL^63jDJC zlKpgkAN?BYTj)FJyX*Vthv-M^C+cVF7wA{$*Xy_G_vw%7&*-n{Z|fiF|Iyd-i}VZe z^Y?T2bMUkBqrO$XX5T#DOy5M`7~l84FMJ>R-txWV+b?Ta*4V7cS+lbiWv$HGn6)Eo zf7bD=b6MB2?q)s7dY$ztD>f@N%h%V{*UlIFNZUGHn&=e&=5AMoDcz0rG>_ag7v-dnTxWFO8xoqZ|$X7 zJexe1JnuYZUU**hyt;W!^X_|G_qgD3(&M1VE|1L~t38%@%=MV+G2Uai#{iFB9-Tef zcr^8>>+vNoAul~IC$A{4B2UW0`F8oP`M&w8{D}M-`StUg=eNu6njf8i+;(Q`oq$Rbi*Xo`wAjhZT-3oKiTua8cnr*XgbkTt~VN za_#Nf)wP{#GuQgAHC#2WDpwy@S64e%)>U$`F}fJNjY?ynae#4z zalCPwajtQRagA}aalFfLmjN!lTsph7acSaG*QL6P+C}N&?c(fW;{siXbD49Yv#&{I ziZInM)i*UawKH`yMVkhhMwupwwm^sj+pv6_jK;)+{(GJb8Y8p&Y{i$ z&YsSW&I)J7nRhC2%6HN`B|AkG)hcRO)Uv2UQTL+0MMH|l6iq6cS+uZdMbY}A?M3^F zjuo9P>h9FusfANRr5SvB@;?!l*}(#RS zYssFHBPFLxE|=UYd06tYadxt8 zrERfou5E^GqU~4PceXEVAK2csy=Z&N_OR`^vMFV=%NCcdD%)7Lv+O|GiL&!$*UIjd zJuQ1(_PH#sEUhfN%+=P`mbH~^%56+GIW}oF@iw1r-q<{|xo30T=7P-$n}aqxZKjpa zEnix`rhIex?(#$BC(AFE-za}j{=EEc`PcG<^7QiDa%UTB8)!qUORWp74b~~vvDTlg zUs*q~zGHpW`keJK>;2X{tY=iruUKBOu3~G&-ijj?rz7p+cN9k$wS zwZ&?U)#9pERhz1IRvoB1QFXrRdeyzEr&VvNK3Bz6rB!8D8LP^x#43?1=S*BSm&V0& zpSd^OQ|=yj=R4Z-zvg?z&#YhPXwUyl_r33bSC;R4|C#k)wV$8W{@(ZNPQH)-nf3oU zzMabdjA{IS7r&q34)?1Z-_Gr4>tE@A{oV2J&+xOpzxVyBhwr0*w0>9r?|%+I_WZr) zcX#lATmSv6ezg9*yZ^D?_nzOY{(I_Qy}93a`lI!)tN$4Pz30DA{jV$k>Raz$tH0ml zkJj&~e@FX!&-!;N|G)Oz3Hy1kI~nhb zp6|eS@NpYq>f69DXi8kDt#k;1}|X z_{IDZeks3TobM_*NAJ#HLy^htH;&l>TvJ*5Bx{|6aSh2 z!hhvscrCBvWBE8fo=@Nt`A{x|3+7Z@5U1nzJM>}jl9W1 z5ns%k`4YaAFXPL38_t?ja8?}0VUFbx2RViV9K|x0u!yn1308tauoi3tTft7S7aRmf z!AWoyTm&O7#05CtLLSb=IXD{|a2D3%Oq_w!agd-Af`t$vR0tE)Lb#w2B7{gGN~k9M zA$-A~@hAKdf57kYJNy>E!LRWv{11MKU*PAsiO^JNCNvjX2rY$HLTjOo&{k+Cv==%E z9fh0t2ELB3;j5POq09IZzKAd2^Y|P-i_hSGLVsa^Fi;pI3>Jn6Lxo|&aAAZn(!wZV zw6GWN!MpJ;yc6%h+wnHM6>q_t@g}?xZ@@E!S;A~#jxbl4C(IWX2n&Tp!eU{GuvAzk zEW``&d^``&#dGj%JPXgnGb~KU)9_ThMc68A6SfOGgq^}JVYjeH*emQ4_6rAugTf(U z7#@m;;K6th9*76v{tij<} zjl(R2;t(8+RX7MMaUc%BdLc_N2-!l8kSpW~`9gtEC>RBkP$U!!X2Aj5V>@h%ZLl>~ zU@OdFj9H8@#0&<6A>>6bdw|`~?qm0|d)VFVE_NrogPlkwk;!BV znM$US>0}0(NoJARWR8WoWEH!TUBNDAm$6IPCG28$5xbCGz|Lpqv2)oqWGz`o){_ln zBiTeYlPzQ`*+#aL9b_jN&yHiqvSZlM>?n35JAxg~4znbwj0}(?ZS3uJFy+v4s3h29ov>|!?tEyv5&}O@`OAk&&YG~g1jXE zSa?NVlQ-losl(Q0Yq2%i8tk8Jb@mUo8XLt%vJtF?4QCTbB1t02B!#4sG?Gp-NG8#f zEMg$p#GCbEJy{RdopobfSr-e=tP|_VICr)rMOC*i{_x&Xcn4jVFsFxrlF~53Yv^2q3z-hai_RT z+%4`A_lo<({o(=fpm<0;EFKYupuuPm8i)p<{-_`7i~68w)Eo6ef1yj_W$}u5RlH{5 zx_CppDc%xqi+9Ak;ytkqYK>Z$y@S~d?i20UkZ={C8ZQ3sia^j879F*m|(d95D(*EEYv|Q zjDcU_7x)?0kZMY`q}oy)sjgH{sxLK=8cL0%#!?gM8GH(#z{l_rdD!&Pu4TmhHEWpJ^yL|Q5> zla@;>q?OVtX|=RQS}U!S)=M+sbT|!8wcKo&3@5>fa02`rj)&vmSh!c(C+(LGNC%}u z77j~Cq@&U?>9}-48UXvley}g>1EXPY*bDvzd%_;DJM0FpN!O(t(oN}>bX&S3-IeZ1 z_oWBYL+O$9SZWTN!KSbYYz!O0h87yY`mi3X3+ur5(g*3I^hx?GeUZLOF_Ko&NwHF# z6fdb^7z~9WFc_*}5LCiI7y$jDAM}OUQjU}>^BmMU06tGKCfjn0zLW z$z{A{AK6#-ll|oYIZ#&0L9$8?mP6!FIgv?V;+Z%mmeDagOTI1Nk?+d)mE}@_YG%{89cSf0n<M~S;K@gY71dZ zC=|;7UV!J|8F&hwfXCnwcnBVV``{jEN88g5v?J|AJJT+-EA3{XJMBSx(u?2%I1kQ& zv)~Lk4NifR-~>1hj)9|KC>=(J(-Cwe9YsgeF?1{)N5|8@=?<_RYy(@t7O)v?0vo{w zupX?luoldrbLl)fpDv&a=_0zAE}={5GP<0up!2~zFc-`Lv%xGd6U+e9!89-xOaWWz zHoBeepgZX2NR{3K5HJ`F0t3MS&>!>zeL){^j-IC%=tX*o zUZz**ReFtHr#I+L+7WaB?Lj-x7PJAaK`YP_v;fUPGtd+~r!VMB3;)np^fi4$-_m#V zJ^esyf*RmYP#ydMs(~mF2_k?7gab7Q14%TQrqEQHM$>5q&7^voMGZ8Y=1?!-2|R#1 laI@eFT!6FXK9?hK0Hw5ymeUGaNvkMN1xlz$B`Q-&{{wLy{YwA< literal 0 HcmV?d00001 diff --git a/public/asset-packs/forest-sounds/sounds/walk.wav b/public/asset-packs/forest-sounds/sounds/walk.wav new file mode 100644 index 0000000000000000000000000000000000000000..d0e7f8f5ec311868bdf6f6b8b5716c377931461e GIT binary patch literal 211724 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

16si=yys%(2b^)Dp`DKq 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 literal 0 HcmV?d00001 diff --git a/public/asset-packs/index.json b/public/asset-packs/index.json index a17a96c..989b859 100644 --- a/public/asset-packs/index.json +++ b/public/asset-packs/index.json @@ -13,6 +13,18 @@ "source": "https://github.com/talkincode/GameKit", "attribution": "本包由 GameKit 原创绘制,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的。", "assetCount": 7 + }, + { + "id": "forest-sounds", + "version": "1.0.0", + "title": "森林声音", + "theme": "森林冒险", + "summary": "跳跃、轻跳、金币、碰到蘑菇,和一段可以循环的走路音乐。和森林的角色、金币、蘑菇用同一套标签连在一起。改参数就能做出另一种,不用重新画图。", + "license": "CC0-1.0", + "author": "GameKit", + "source": "https://github.com/talkincode/GameKit", + "attribution": "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。", + "assetCount": 5 } ] } diff --git a/scripts/write-forest-pack.ts b/scripts/write-forest-pack.ts index 8b73d4e..9ad003c 100644 --- a/scripts/write-forest-pack.ts +++ b/scripts/write-forest-pack.ts @@ -1,16 +1,24 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { buildForestPack, catalogIndexFor } from "../src/lib/assets/forest"; +import { buildForestPack, catalogIndex } from "../src/lib/assets/forest"; +import { buildSoundPack } from "../src/lib/assets/sounds"; -/** Writes the committed starter pack under public/asset-packs. Run from the repo root after changing the drawings. */ +/** Writes the committed packs under public/asset-packs. Run from the repo root after changing a drawing or a sound preset. */ const root = join(process.cwd(), "public", "asset-packs"); -const built = await buildForestPack(); -const packDir = join(root, built.manifest.id); -for (const [path, bytes] of Object.entries(built.files)) { - const file = join(packDir, path); - await mkdir(dirname(file), { recursive: true }); - await writeFile(file, bytes); + +async function writePack(manifest: Awaited>["manifest"], files: Record) { + const packDir = join(root, manifest.id); + for (const [path, bytes] of Object.entries(files)) { + const file = join(packDir, path); + await mkdir(dirname(file), { recursive: true }); + await writeFile(file, bytes); + } + await writeFile(join(packDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); + console.log(`Wrote ${manifest.assets.length} assets to ${packDir}`); } -await writeFile(join(packDir, "manifest.json"), `${JSON.stringify(built.manifest, null, 2)}\n`); -await writeFile(join(root, "index.json"), `${JSON.stringify(catalogIndexFor(built.manifest), null, 2)}\n`); -console.log(`Wrote ${built.manifest.assets.length} assets to ${packDir}`); + +const forest = await buildForestPack(); +const sounds = await buildSoundPack(); +await writePack(forest.manifest, forest.files); +await writePack(sounds.manifest, sounds.files); +await writeFile(join(root, "index.json"), `${JSON.stringify(catalogIndex([forest.manifest, sounds.manifest]), null, 2)}\n`); diff --git a/src/lib/ai.test.ts b/src/lib/ai.test.ts index 7d0b1d4..07ff4af 100644 --- a/src/lib/ai.test.ts +++ b/src/lib/ai.test.ts @@ -34,6 +34,7 @@ describe("requests to the model", () => { expect(system.content).toContain("assets/library.json"); expect(system.content).toContain("*.anim.json"); expect(system.content).toContain("*.map.json"); + expect(system.content).toContain("*.wav"); expect(user.content).toContain("一个在太空里躲陨石的游戏"); }); @@ -45,6 +46,7 @@ describe("requests to the model", () => { expect(user.content).toContain("assets/library.json"); expect(user.content).toContain("*.anim.json"); expect(user.content).toContain("*.map.json"); + expect(user.content).toContain("*.wav"); }); it("carries the traceback into a repair request", () => { @@ -123,6 +125,9 @@ describe("answers from the model", () => { expect(() => readCandidateFiles('{"files":[{"path":"maps/level-1.collision.csv","content":"0"}]}', project, "main.py"), ).toThrow(/tile map/); + expect(() => + readCandidateFiles('{"files":[{"path":"assets/jump.wav","content":"no"}]}', project, "main.py"), + ).toThrow(/audio file/); }); it("rejects an oversized file", () => { diff --git a/src/lib/ai.ts b/src/lib/ai.ts index 5a0c93e..d4fa85f 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -1,5 +1,6 @@ import { designFromRecord, designText, type DesignCard } from "./design"; import type { Problem } from "./diagnostics"; +import { isAudioPath } from "./audio/format"; import { PINS_PATH } from "./assets/copy"; import { isMapDataPath } from "./map/document"; import { normalizePath, type Project, type ProjectFile } from "./project"; @@ -42,9 +43,10 @@ Code rules: - The browser runtime is pygame-ce via pygbag. A frame loop must include await asyncio.sleep(0), usually right after pygame.display.flip(). That form also runs on desktop Python. - Do not use time.sleep, pygame.time.wait, or pygame.time.delay. - Prefer pygame.font.Font(None, size) or a project .ttf over SysFont. -- Audio for the web build should be OGG Vorbis. +- Audio for the web build should be 16-bit PCM WAV or OGG Vorbis. The sound editor saves WAV. - Never write to assets/ in a reply: images, sounds and fonts are binary files the child owns. -- The built-in asset library is read-only for you. You cannot copy a library file into the project, and you cannot rewrite assets/library.json. The child adds those files. You may load a picture that is already in the project with pygame.image.load. +- The built-in asset library is read-only for you. You cannot copy a library file into the project, and you cannot rewrite assets/library.json. The child adds those files. You may load a picture or a sound that is already in the project with pygame.image.load or pygame.mixer.Sound. +- Audio files (*.wav, *.ogg, *.mp3, *.aiff) belong to the child. Do not rewrite them. - Sprite animation notes (*.anim.json) belong to the child. Do not rewrite them. Play a saved sheet with ordinary pygame: load the PNG, read the JSON, and blit frames by durationMs. - Tile maps (*.map.json, maps/*.csv, and *.tileset.png) belong to the child. Do not rewrite them. Load a saved map with ordinary pygame: read the JSON, the collision CSV, and the tileset PNG. - Write code a kid can read: meaningful names, short functions, a few short comments in simple Simplified Chinese. Avoid advanced features the project does not already use. @@ -114,6 +116,7 @@ export function readCandidateFiles(raw: string, project: Project, fallbackPath: if (path === PINS_PATH) throw new Error("AI cannot rewrite the asset library record."); if (path.endsWith(".anim.json")) throw new Error("AI cannot rewrite a sprite animation note."); if (isMapDataPath(path)) throw new Error("AI cannot rewrite a tile map."); + if (isAudioPath(path)) throw new Error("AI cannot rewrite an audio file."); const allowed = [".py", ".txt", ".toml", ".json", ".md"].some((ext) => path.endsWith(ext)); if (!allowed) throw new Error(`Rejected a file outside the text-edit scope: ${String(entry.path)}`); if (project.files.some((file) => file.path === path && file.bytes)) { @@ -167,7 +170,7 @@ export function buildMessages(input: { request: string; design: DesignCard; base `${designBlock(input.design)}\n` + `现在项目里的文件:\n${filesForPrompt(input.base.files, MESSAGE_BUDGET - 4_000) || "(空项目)"}\n` + `把这张设计卡做成能玩的游戏,写出完整的文件(不是片段)。必须包含 main.py。\n` + - `沿用项目里已经在用的结构和命名;可以新增 .py 文件;不要改动 assets/ 里的二进制素材,也不要改 assets/library.json、*.anim.json 或地图文件(*.map.json、maps/*.csv、*.tileset.png)。素材库里的图要孩子自己复制进项目。动画帧和地图也要孩子自己在编辑器里改。\n` + + `沿用项目里已经在用的结构和命名;可以新增 .py 文件;不要改动 assets/ 里的二进制素材,也不要改 assets/library.json、*.anim.json、音频文件(*.wav、*.ogg、*.mp3、*.aiff)或地图文件(*.map.json、maps/*.csv、*.tileset.png)。素材库里的图和声音要孩子自己复制进项目。动画帧、地图和声音也要孩子自己在编辑器里改。\n` + `返回 {"say":"给孩子的一句话","files":[{"path":"main.py","content":"整个文件"}]},最多 6 个文件。`, }, ]; diff --git a/src/lib/assets/copy.ts b/src/lib/assets/copy.ts index 477e26b..62518a3 100644 --- a/src/lib/assets/copy.ts +++ b/src/lib/assets/copy.ts @@ -52,6 +52,8 @@ export type LibraryAssetCopy = { author: string; frameWidth: number; frameHeight: number; + media: "image" | "audio"; + loop: boolean; }; export type LibraryChoice = "add" | "keep" | "save-as" | "replace"; @@ -115,6 +117,8 @@ export function libraryCopyFrom(pack: AssetPack, asset: CatalogAsset, bytes: Uin author: asset.author, frameWidth: asset.animation?.frameWidth ?? asset.width ?? 0, frameHeight: asset.animation?.frameHeight ?? asset.height ?? 0, + media: asset.mediaType.startsWith("audio/") ? "audio" : "image", + loop: asset.category === "music", }; } diff --git a/src/lib/assets/filter.ts b/src/lib/assets/filter.ts index 7b29fb3..b8ab6cc 100644 --- a/src/lib/assets/filter.ts +++ b/src/lib/assets/filter.ts @@ -7,6 +7,15 @@ export type AssetQuery = { tag: string | "all"; }; +/** + * Other assets that share a tag. Style variants stay editable copies linked by + * tags, so a sound and a picture can belong to one theme without a new drawing. + */ +export function relatedAssets(assets: CatalogAsset[], asset: CatalogAsset): CatalogAsset[] { + const tags = new Set(asset.tags); + return assets.filter((other) => other !== asset && other.id !== asset.id && other.tags.some((tag) => tags.has(tag))); +} + /** Client-side filter. The pack JSON is already loaded; this does not fetch. */ export function filterAssets(assets: CatalogAsset[], query: AssetQuery): CatalogAsset[] { const needle = query.text.trim().toLowerCase(); diff --git a/src/lib/assets/forest.ts b/src/lib/assets/forest.ts index 2ef28af..7b0138a 100644 --- a/src/lib/assets/forest.ts +++ b/src/lib/assets/forest.ts @@ -304,23 +304,26 @@ export async function buildForestPack(): Promise<{ manifest: AssetPack; files: R return { manifest, files }; } -export function catalogIndexFor(pack: AssetPack): CatalogIndex { +/** One catalog index for every pack that ships under public/asset-packs. */ +export function catalogIndex(packs: AssetPack[]): CatalogIndex { return catalogIndexSchema.parse({ format: "gamekit.asset-catalog", formatVersion: 1, - packs: [ - { - id: pack.id, - version: pack.version, - title: pack.title, - theme: pack.theme, - summary: pack.summary, - license: pack.license.spdx, - author: pack.author, - source: pack.source, - attribution: pack.attribution, - assetCount: pack.assets.length, - }, - ], + packs: packs.map((pack) => ({ + id: pack.id, + version: pack.version, + title: pack.title, + theme: pack.theme, + summary: pack.summary, + license: pack.license.spdx, + author: pack.author, + source: pack.source, + attribution: pack.attribution, + assetCount: pack.assets.length, + })), }); } + +export function catalogIndexFor(pack: AssetPack): CatalogIndex { + return catalogIndex([pack]); +} diff --git a/src/lib/assets/schema.ts b/src/lib/assets/schema.ts index 8cd6e46..4c3dd16 100644 --- a/src/lib/assets/schema.ts +++ b/src/lib/assets/schema.ts @@ -15,7 +15,7 @@ const httpsUrl = z.string().regex(/^https:\/\/\S+$/); const relativePath = z.string().regex(/^[a-z0-9][a-z0-9/_-]*\.[a-z0-9]+$/); const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/); -export const ASSET_CATEGORIES = ["character", "terrain", "prop", "ui", "effect", "tileset"] as const; +export const ASSET_CATEGORIES = ["character", "terrain", "prop", "ui", "effect", "tileset", "sfx", "music"] as const; export const MEDIA_TYPES = ["image/png", "audio/ogg", "audio/wav", "application/json"] as const; export const licenseSchema = z.object({ diff --git a/src/lib/assets/snippet.ts b/src/lib/assets/snippet.ts index c39f2e4..eb9e1b5 100644 --- a/src/lib/assets/snippet.ts +++ b/src/lib/assets/snippet.ts @@ -8,10 +8,26 @@ export type SnippetAsset = { attribution: string; frameWidth: number; frameHeight: number; + media?: "image" | "audio"; + loop?: boolean; }; /** Standard pygame the child can copy. No GameKit API, no network path. */ export function pygameSnippet(copy: SnippetAsset, path: string): string { + if (copy.media === "audio") { + return [ + "import pygame", + "", + `# ${copy.title}`, + `# 许可:${copy.licenseName}(${copy.licenseSpdx}) · 作者:${copy.author}`, + `# 来源:${copy.source}`, + `# 署名:${copy.attribution}`, + "pygame.mixer.init()", + `sound = pygame.mixer.Sound("${path}")`, + copy.loop ? "sound.play(-1)" : "sound.play()", + "", + ].join("\n"); + } const lines = [ "import pygame", "", @@ -33,6 +49,38 @@ export function pygameSnippet(copy: SnippetAsset, path: string): string { /** A whole main.py that loads the copied file and prints a size the preview can show. */ export function pygameDemoMain(copy: SnippetAsset, path: string): string { + if (copy.media === "audio") { + const play = copy.loop ? "sound.play(-1)" : "sound.play()"; + return `import asyncio +import pygame + +# ${copy.title} · ${copy.licenseSpdx} · ${copy.author} +pygame.init() +screen = pygame.display.set_mode((320, 240)) +clock = pygame.time.Clock() +print("SOUND-WAIT") + + +async def main(): + sound = None + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + if event.type == pygame.MOUSEBUTTONDOWN and sound is None: + pygame.mixer.init() + sound = pygame.mixer.Sound("${path}") + print("SOUND-OK", int(round(sound.get_length() * 1000))) + ${play} + screen.fill((46, 92, 74)) + pygame.display.flip() + clock.tick(60) + await asyncio.sleep(0) + + +asyncio.run(main()) +`; + } const frame = copy.frameWidth > 0 && copy.frameHeight > 0 ? `frame = image.subsurface((0, 0, ${copy.frameWidth}, ${copy.frameHeight}))` diff --git a/src/lib/assets/sounds.ts b/src/lib/assets/sounds.ts new file mode 100644 index 0000000..0f28537 --- /dev/null +++ b/src/lib/assets/sounds.ts @@ -0,0 +1,63 @@ +/** + * Official forest sounds. + * + * Rendered by the same synthesizer the editor uses, then checked with the same + * pack schema as the pictures. CC0, written by GameKit, not downloaded and not + * produced by a generation API. Tags overlap the forest pictures so a jump + * sound and the explorer sit in one set. + */ +import { renderEdited } from "../audio/edit"; +import { SOUND_PRESETS, type SoundPreset } from "../audio/presets"; +import { sha256Hex } from "./hash"; +import { FOREST_AUTHOR, FOREST_LICENSE, FOREST_SOURCE } from "./forest"; +import type { AssetPack, CatalogAsset } from "./schema"; + +export const SOUND_PACK_ID = "forest-sounds"; + +const ATTRIBUTION = + "这些声音由 GameKit 用参数合成,以 CC0 1.0 贡献到公共领域。使用时无需署名。不是从第三方素材站搬运的,也不是调用生成接口做的。"; + +function filePath(preset: SoundPreset): string { + return `sounds/${preset.spec.name}.wav`; +} + +export async function buildSoundPack(): Promise<{ manifest: AssetPack; files: Record }> { + const files: Record = {}; + const assets: CatalogAsset[] = []; + for (const preset of SOUND_PRESETS) { + const rendered = renderEdited(preset.spec, preset.edit); + const path = filePath(preset); + files[path] = rendered.wav; + assets.push({ + id: preset.id, + version: "1.0.0", + title: preset.title, + path, + mediaType: "audio/wav", + tags: preset.tags, + theme: preset.theme, + category: preset.kind, + preview: path, + license: FOREST_LICENSE, + author: FOREST_AUTHOR, + source: FOREST_SOURCE, + attribution: ATTRIBUTION, + sha256: await sha256Hex(rendered.wav), + }); + } + const manifest: AssetPack = { + format: "gamekit.asset-pack", + formatVersion: 1, + id: SOUND_PACK_ID, + version: "1.0.0", + title: "森林声音", + theme: "森林冒险", + summary: "跳跃、轻跳、金币、碰到蘑菇,和一段可以循环的走路音乐。和森林的角色、金币、蘑菇用同一套标签连在一起。改参数就能做出另一种,不用重新画图。", + license: FOREST_LICENSE, + author: FOREST_AUTHOR, + source: FOREST_SOURCE, + attribution: ATTRIBUTION, + assets, + }; + return { manifest, files }; +} diff --git a/src/lib/assets/validate.test.ts b/src/lib/assets/validate.test.ts index d0d6fe5..41069e7 100644 --- a/src/lib/assets/validate.test.ts +++ b/src/lib/assets/validate.test.ts @@ -2,7 +2,8 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { checkAssetPacks } from "./check-disk"; -import { buildForestPack, catalogIndexFor } from "./forest"; +import { buildForestPack, catalogIndex } from "./forest"; +import { buildSoundPack } from "./sounds"; import { validateAssetPack } from "./validate"; const root = join(process.cwd(), "public", "asset-packs"); @@ -10,15 +11,29 @@ const root = join(process.cwd(), "public", "asset-packs"); describe("forest adventure pack", () => { it("matches the generator and passes the ship gate", async () => { const built = await buildForestPack(); + const sounds = await buildSoundPack(); const manifest = JSON.parse(readFileSync(join(root, "forest-adventure", "manifest.json"), "utf8")); const index = JSON.parse(readFileSync(join(root, "index.json"), "utf8")); expect(manifest).toEqual(built.manifest); - expect(index).toEqual(catalogIndexFor(built.manifest)); + expect(index).toEqual(catalogIndex([built.manifest, sounds.manifest])); for (const asset of built.manifest.assets) { const bytes = readFileSync(join(root, "forest-adventure", asset.path)); expect(Buffer.from(bytes).equals(Buffer.from(built.files[asset.path]))).toBe(true); } expect(await checkAssetPacks(root)).toEqual([]); + const soundManifest = JSON.parse(readFileSync(join(root, "forest-sounds", "manifest.json"), "utf8")); + expect(soundManifest).toEqual(sounds.manifest); + for (const asset of sounds.manifest.assets) { + const bytes = readFileSync(join(root, "forest-sounds", asset.path)); + expect(Buffer.from(bytes).equals(Buffer.from(sounds.files[asset.path]))).toBe(true); + expect(asset.license.spdx).toBe("CC0-1.0"); + expect(asset.author).toBe("GameKit"); + expect(asset.source).toMatch(/^https:\/\//); + expect(asset.attribution.length).toBeGreaterThan(0); + expect(asset.tags.some((tag: string) => /[\u4e00-\u9fff]/.test(tag))).toBe(true); + } + const soundChecked = await validateAssetPack(sounds.manifest, sounds.files); + expect(soundChecked.ok).toBe(true); const checked = await validateAssetPack(built.manifest, built.files); expect(checked.ok).toBe(true); }); @@ -66,4 +81,15 @@ describe("forest adventure pack", () => { expect(tagged.issues.map((issue) => issue.code)).toEqual(expect.arrayContaining(["tag-language", "animation"])); } }); + + it("blocks an audio file the browser runtime cannot play", async () => { + const sounds = await buildSoundPack(); + const manifest = structuredClone(sounds.manifest); + const files = { ...sounds.files, [manifest.assets[0].path]: new Uint8Array([1, 2, 3, 4]) }; + const { sha256Hex } = await import("./hash"); + manifest.assets[0].sha256 = await sha256Hex(files[manifest.assets[0].path]); + const checked = await validateAssetPack(manifest, files); + expect(checked.ok).toBe(false); + if (!checked.ok) expect(checked.issues.map((issue) => issue.code)).toContain("audio-format"); + }); }); diff --git a/src/lib/assets/validate.ts b/src/lib/assets/validate.ts index ea492a2..d59935d 100644 --- a/src/lib/assets/validate.ts +++ b/src/lib/assets/validate.ts @@ -1,3 +1,4 @@ +import { inspectAudio } from "../audio/format"; import { sha256Hex } from "./hash"; import { assetPackSchema, type AssetPack, type CatalogAsset } from "./schema"; @@ -18,7 +19,8 @@ export type PackIssueCode = | "collision" | "preview" | "tag-language" - | "title-language"; + | "title-language" + | "audio-format"; export type PackIssue = { code: PackIssueCode; message: string; path?: string }; @@ -123,6 +125,11 @@ export async function validateAssetPack( issues.push({ code: "missing-file", path: asset.path, message: `Missing file ${asset.path}.` }); } else if ((await sha256Hex(bytes)) !== asset.sha256) { issues.push({ code: "hash-mismatch", path: asset.path, message: `Hash does not match ${asset.path}.` }); + } else if (asset.mediaType === "audio/wav" || asset.mediaType === "audio/ogg") { + const verdict = inspectAudio(asset.path, bytes); + if (!verdict?.ok) { + issues.push({ code: "audio-format", path: asset.path, message: `${asset.id} is not audio the browser runtime can play.` }); + } } checkAnimation(asset, issues); diff --git a/src/lib/audio/edit.test.ts b/src/lib/audio/edit.test.ts new file mode 100644 index 0000000..0277257 --- /dev/null +++ b/src/lib/audio/edit.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from "vitest"; +import { IDENTITY_EDIT, applyEdit, musicVoices, renderEdited, waveformPeaks } from "./edit"; +import { inspectAudio } from "./format"; +import { pygameSoundDemo, pygameSoundSnippet } from "./play"; +import { SOUND_PRESETS, soundPreset } from "./presets"; +import { renderMusic, renderSfx } from "./render"; +import { readMusic, readSfx } from "./spec"; +import { planSoundSave } from "./save"; +import { pcmToWav } from "./wav"; +import type { Project } from "../project"; + +function project(files: Project["files"] = []): Project { + return { id: "p", name: "game", createdAt: 0, updatedAt: 0, files }; +} + +describe("edited sound", () => { + it("previews and saves the same bytes as the synthesizer when nothing is trimmed", () => { + const spec = readSfx({ layers: [{ wave: "square", from: 440, to: 880, duration: 0.2, gain: 0.4 }] }, "jump"); + const edited = renderEdited(spec, IDENTITY_EDIT); + const raw = pcmToWav(renderSfx(spec)); + expect(Array.from(edited.wav)).toEqual(Array.from(raw)); + expect(inspectAudio("assets/jump.wav", edited.wav)).toEqual({ ok: true, format: "wav" }); + }); + + it("makes a quieter, shorter file when volume and trim change", () => { + const spec = readSfx({ layers: [{ duration: 0.4, gain: 0.5 }] }, "jump"); + const full = renderEdited(spec, IDENTITY_EDIT); + const cut = renderEdited(spec, { volume: 0.5, trimStart: 0.05, trimEnd: 0.2, loop: false }); + expect(cut.pcm.samples.length).toBeLessThan(full.pcm.samples.length); + let fullPeak = 0; + let cutPeak = 0; + for (const sample of full.pcm.samples) fullPeak = Math.max(fullPeak, Math.abs(sample)); + for (const sample of cut.pcm.samples) cutPeak = Math.max(cutPeak, Math.abs(sample)); + expect(cutPeak).toBeLessThan(fullPeak); + expect(Array.from(renderEdited(spec, { volume: 0.5, trimStart: 0.05, trimEnd: 0.2, loop: false }).wav)).toEqual(Array.from(cut.wav)); + }); + + it("changes the join when a loop fade is asked for, and keeps the length", () => { + const spec = readSfx({ layers: [{ duration: 0.3, gain: 0.4 }] }, "jump"); + const plain = renderEdited(spec, IDENTITY_EDIT); + const looped = renderEdited(spec, { ...IDENTITY_EDIT, loop: true }); + expect(looped.pcm.samples.length).toBe(plain.pcm.samples.length); + expect(Array.from(looped.wav)).not.toEqual(Array.from(plain.wav)); + }); + + it("draws a waveform and solos a music voice without changing the bar length", () => { + const music = readMusic({ bpm: 120, bars: 2, chords: [1, 5], tracks: [{ role: "lead" }, { role: "bass" }, { role: "hat" }] }, "walk"); + const mix = renderEdited(music, IDENTITY_EDIT); + const lead = renderEdited(musicVoices(music, ["lead"]), IDENTITY_EDIT); + expect(mix.pcm.samples.length).toBe(4 * 22_050); + expect(lead.pcm.samples.length).toBe(mix.pcm.samples.length); + expect(Array.from(lead.wav)).not.toEqual(Array.from(pcmToWav(renderMusic(music)))); + expect(waveformPeaks(mix.pcm).length).toBe(80); + expect(waveformPeaks(mix.pcm).some((peak) => peak > 0)).toBe(true); + }); + + it("refuses to save audio the browser cannot play", () => { + const spec = soundPreset("jump-sfx"); + const wav = renderEdited(spec.spec, spec.edit).wav; + const saved = planSoundSave(project([{ path: "main.py", text: "print(1)\n" }]), { + bytes: wav, + suggested: "assets/jump.wav", + }); + expect(saved.ok).toBe(true); + if (saved.ok) { + expect(saved.path).toBe("assets/jump.wav"); + expect(saved.next.files.find((file) => file.path === "assets/jump.wav")?.bytes).toEqual(wav); + } + const bad = planSoundSave(project(), { bytes: new Uint8Array([1, 2, 3]), suggested: "assets/nope.wav" }); + expect(bad).toEqual({ ok: false, reason: "bad-audio" }); + const named = planSoundSave(project(), { bytes: wav, suggested: "../nope.wav" }); + expect(named).toEqual({ ok: false, reason: "name" }); + }); + + it("writes a pygame example that loads the saved file and yields each frame", () => { + const snippet = pygameSoundSnippet("assets/jump.wav", false); + const demo = pygameSoundDemo("assets/library/forest-sounds/sounds/jump.wav", true); + expect(snippet).toContain('pygame.mixer.Sound("assets/jump.wav")'); + expect(snippet).toContain("sound.play()"); + expect(demo).toContain("sound.play(-1)"); + expect(demo).toContain("await asyncio.sleep(0)"); + expect(demo).toContain("SOUND-OK"); + expect(demo).not.toContain("/asset-packs/"); + expect(demo).not.toContain("/api/"); + }); +}); + +describe("sound presets", () => { + it("shares tags with the forest pictures and renders a playable wav", () => { + const jump = soundPreset("jump-sfx"); + const soft = soundPreset("jump-soft"); + expect(jump.tags).toEqual(expect.arrayContaining(["跳跃", "角色"])); + expect(soft.tags).toContain("变体"); + expect(soft.tags).toEqual(expect.arrayContaining(["跳跃"])); + expect(soundPreset("coin-sfx").tags).toContain("金币"); + expect(soundPreset("bump-sfx").tags).toContain("蘑菇"); + expect(soundPreset("walk-music").spec.kind).toBe("music"); + for (const preset of SOUND_PRESETS) { + const wav = renderEdited(preset.spec, preset.edit).wav; + expect(inspectAudio(`assets/${preset.spec.name}.wav`, wav)?.ok).toBe(true); + } + }); + + it("does not change samples when the same preset is rendered twice", () => { + const preset = soundPreset("coin-sfx"); + const a = renderEdited(preset.spec, preset.edit).wav; + const b = applyEdit( + { samples: renderEdited(preset.spec, IDENTITY_EDIT).pcm.samples, sampleRate: 22_050 }, + IDENTITY_EDIT, + ); + expect(a.length).toBeGreaterThan(44); + expect(b.samples.length).toBeGreaterThan(10); + }); +}); diff --git a/src/lib/audio/edit.ts b/src/lib/audio/edit.ts new file mode 100644 index 0000000..078635e --- /dev/null +++ b/src/lib/audio/edit.ts @@ -0,0 +1,112 @@ +/** + * Turns a validated patch into the exact samples the child hears and saves. + * + * Trim, volume, and an optional loop fade happen after the synthesizer, so the + * preview player and the WAV in assets/ are one buffer. The same spec and edit + * always produce the same bytes. + */ +import { envelopeAt, renderMusic, renderSfx, type Pcm } from "./render"; +import type { MusicSpec, MusicTrack, SfxLayer, SfxSpec, SoundSpec } from "./spec"; +import { pcmToWav } from "./wav"; + +export type SoundEdit = { + /** 0 is silent, 1 leaves the synthesizer's level alone. */ + volume: number; + /** Seconds from the start. */ + trimStart: number; + /** Seconds. 0 means "through the end". */ + trimEnd: number; + /** Fade the join so pygame's play(-1) does not click. */ + loop: boolean; +}; + +export const IDENTITY_EDIT: SoundEdit = { volume: 1, trimStart: 0, trimEnd: 0, loop: false }; + +const DEFAULT_VOICE: Record = { + lead: { wave: "triangle", gain: 0.25, role: "lead", octave: 0 }, + bass: { wave: "triangle", gain: 0.2, role: "bass", octave: -1 }, + hat: { wave: "noise", gain: 0.15, role: "hat", octave: 0 }, +}; + +export function soundFileStem(value: string, fallback: string): string { + const text = value.trim().toLowerCase().replace(/[^a-z0-9-]/g, ""); + return (text || fallback).slice(0, 24); +} + +/** Keeps the requested voices, filling a missing role with a quiet default. */ +export function musicVoices(spec: MusicSpec, roles: MusicTrack["role"][]): MusicSpec { + const wanted = roles.filter((role, index) => roles.indexOf(role) === index); + const picked = (wanted.length ? wanted : (["lead"] as const)).map( + (role) => spec.tracks.find((track) => track.role === role) ?? DEFAULT_VOICE[role], + ); + return { ...spec, tracks: picked }; +} + +export function clampEdit(edit: SoundEdit, duration: number): SoundEdit { + const volume = Math.min(1, Math.max(0, Number.isFinite(edit.volume) ? edit.volume : 1)); + const full = Math.max(0.02, duration); + const start = Math.min(Math.max(0, edit.trimStart), full - 0.02); + const requestedEnd = edit.trimEnd <= 0 ? full : edit.trimEnd; + const end = Math.min(full, Math.max(start + 0.02, requestedEnd)); + return { volume, trimStart: start, trimEnd: end, loop: edit.loop }; +} + +function fadeLoop(samples: Float32Array, sampleRate: number): void { + const fade = Math.min(Math.floor(samples.length / 8), Math.floor(0.01 * sampleRate)); + if (fade < 2) return; + const head = samples.slice(0, fade); + for (let index = 0; index < fade; index += 1) { + const blend = index / (fade - 1); + const tailIndex = samples.length - fade + index; + const tail = samples[tailIndex]; + samples[index] = head[index] * blend + tail * (1 - blend); + samples[tailIndex] = tail * (1 - blend); + } +} + +/** Slice, scale, and optionally prepare a loop. Does not re-normalize, so volume is audible. */ +export function applyEdit(pcm: Pcm, edit: SoundEdit): Pcm { + const duration = pcm.samples.length / pcm.sampleRate; + const clamped = clampEdit(edit, duration); + const start = Math.floor(clamped.trimStart * pcm.sampleRate); + const end = Math.min(pcm.samples.length, Math.ceil(clamped.trimEnd * pcm.sampleRate)); + const sliced = pcm.samples.slice(start, Math.max(start + 1, end)); + const samples = new Float32Array(sliced.length); + for (let index = 0; index < sliced.length; index += 1) samples[index] = sliced[index] * clamped.volume; + if (clamped.loop) fadeLoop(samples, pcm.sampleRate); + return { samples, sampleRate: pcm.sampleRate }; +} + +export function renderEdited(spec: SoundSpec, edit: SoundEdit = IDENTITY_EDIT): { pcm: Pcm; wav: Uint8Array; seconds: number } { + const raw = spec.kind === "sfx" ? renderSfx(spec) : renderMusic(spec); + const pcm = applyEdit(raw, edit); + return { pcm, wav: pcmToWav(pcm), seconds: pcm.samples.length / pcm.sampleRate }; +} + +/** The envelope drawn in the editor. Same curve the synthesizer applies. */ +export function envelopeCurve(layer: SfxLayer, steps = 48): number[] { + const points: number[] = []; + const count = Math.max(2, steps); + for (let index = 0; index < count; index += 1) { + const time = (index / (count - 1)) * layer.duration; + points.push(envelopeAt(layer, layer.duration, time)); + } + return points; +} + +/** Absolute peaks for the waveform, one number per column. */ +export function waveformPeaks(pcm: Pcm, buckets = 80): number[] { + const count = Math.max(1, buckets); + const size = Math.max(1, Math.floor(pcm.samples.length / count)); + const peaks: number[] = []; + for (let bucket = 0; bucket < count; bucket += 1) { + const start = bucket * size; + const end = Math.min(pcm.samples.length, start + size); + let peak = 0; + for (let index = start; index < end; index += 1) peak = Math.max(peak, Math.abs(pcm.samples[index])); + peaks.push(peak); + } + return peaks; +} + +export type { SfxSpec }; diff --git a/src/lib/audio/format.ts b/src/lib/audio/format.ts new file mode 100644 index 0000000..efcdc89 --- /dev/null +++ b/src/lib/audio/format.ts @@ -0,0 +1,95 @@ +/** + * Which audio files the browser runtime can play. + * + * pygame-web plays 8/16-bit PCM WAV (checked with a probe) and OGG Vorbis. + * MP3, AIFF, Opus, and a WAV that is not PCM stay silent, so they are diagnosed + * instead of being treated as a working game asset. See docs/runtime.md. + */ + +export type AudioFormat = "wav" | "ogg" | "mp3" | "aiff" | "unknown"; + +export type AudioReason = "mp3" | "aiff" | "wav-bad" | "ogg-bad" | "unknown"; + +export type AudioVerdict = { ok: true; format: "wav" | "ogg" } | { ok: false; format: AudioFormat; reason: AudioReason }; + +const AUDIO_EXT = /\.(wav|ogg|mp3|aiff)$/i; + +export function isAudioPath(path: string): boolean { + return AUDIO_EXT.test(path); +} + +function ascii(bytes: Uint8Array, at: number, length: number): string { + let text = ""; + const end = Math.min(bytes.length, at + length); + for (let index = at; index < end; index += 1) text += String.fromCharCode(bytes[index]); + return text; +} + +function pcmWav(bytes: Uint8Array): boolean { + if (bytes.length < 44) return false; + if (ascii(bytes, 0, 4) !== "RIFF" || ascii(bytes, 8, 4) !== "WAVE") return false; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + let offset = 12; + let format = 0; + let channels = 0; + let rate = 0; + let bits = 0; + let dataBytes = -1; + while (offset + 8 <= bytes.length) { + const id = ascii(bytes, offset, 4); + const size = view.getUint32(offset + 4, true); + const body = offset + 8; + if (id === "fmt ") { + if (body + 16 > bytes.length) return false; + format = view.getUint16(body, true); + channels = view.getUint16(body + 2, true); + rate = view.getUint32(body + 4, true); + bits = view.getUint16(body + 14, true); + } else if (id === "data") { + dataBytes = size; + if (body + size > bytes.length) return false; + } + const step = 8 + size + (size % 2); + if (step <= 0) return false; + offset += step; + } + return format === 1 && channels >= 1 && channels <= 2 && (bits === 8 || bits === 16) && rate >= 8_000 && rate <= 48_000 && dataBytes >= 2; +} + +/** OGG container plus a Vorbis identification packet. Opus in an Ogg page is not enough. */ +function oggVorbis(bytes: Uint8Array): boolean { + if (bytes.length < 64 || ascii(bytes, 0, 4) !== "OggS") return false; + const head = ascii(bytes, 0, Math.min(bytes.length, 128)); + if (head.includes("OpusHead")) return false; + return head.includes("vorbis"); +} + +/** + * Null when the path is not audio. Otherwise a yes/no the diagnostics and the + * asset-pack gate can both use. + */ +export function inspectAudio(path: string, bytes: Uint8Array | undefined): AudioVerdict | null { + const ext = path.split(".").pop()?.toLowerCase(); + if (!ext || !["wav", "ogg", "mp3", "aiff"].includes(ext)) return null; + const data = bytes ?? new Uint8Array(); + if (ext === "mp3") return { ok: false, format: "mp3", reason: "mp3" }; + if (ext === "aiff") return { ok: false, format: "aiff", reason: "aiff" }; + if (ext === "wav") return pcmWav(data) ? { ok: true, format: "wav" } : { ok: false, format: "wav", reason: "wav-bad" }; + return oggVorbis(data) ? { ok: true, format: "ogg" } : { ok: false, format: "ogg", reason: "ogg-bad" }; +} + +/** What the Problems panel says. Short, and it names a format the runtime can play. */ +export function audioProblemMessage(verdict: Extract): string { + switch (verdict.reason) { + case "mp3": + return "浏览器里的 pygame 播不了 MP3。请换成 WAV,或 OGG Vorbis。"; + case "aiff": + return "浏览器里的 pygame 播不了 AIFF。请换成 WAV,或 OGG Vorbis。"; + case "wav-bad": + return "这个 WAV 不是浏览器能播的 PCM(8 位或 16 位)。可以用声音编辑器重新存一份。"; + case "ogg-bad": + return "这个 OGG 没通过 Vorbis 检查。浏览器播的是 OGG Vorbis,不是 Opus。"; + default: + return "这种音频在浏览器里播不了。请换成 WAV,或 OGG Vorbis。"; + } +} diff --git a/src/lib/audio/kept.ts b/src/lib/audio/kept.ts new file mode 100644 index 0000000..0015952 --- /dev/null +++ b/src/lib/audio/kept.ts @@ -0,0 +1,109 @@ +/** + * Sounds the child kept on this computer. + * + * Official packs live in public/asset-packs. A sound made in the editor (a + * preset or a spec the model picked) uses the same catalog fields and the same + * format check, then sits in IndexedDB until the child copies it into a project. + */ +import { loadKeptSounds, removeKeptSound, saveKeptSound } from "../storage"; +import { sha256Hex } from "../assets/hash"; +import { FOREST_LICENSE, FOREST_SOURCE } from "../assets/forest"; +import { assetPackSchema, catalogAssetSchema, type AssetPack, type CatalogAsset } from "../assets/schema"; +import { inspectAudio } from "./format"; + +type SoundKind = "sfx" | "music"; + +export const KEPT_PACK_ID = "my-sounds"; + +const ATTRIBUTION = "这是你在 GameKit 里调出来的声音,存在这台电脑上。听到的和保存的是同一份,不经过网上的生成接口。"; + +function hasCjk(value: string): boolean { + return /[\u4e00-\u9fff]/.test(value); +} + +export async function buildKeptAsset(input: { + bytes: Uint8Array; + title: string; + kind: SoundKind; + tags: string[]; +}): Promise { + const verdict = inspectAudio("assets/kept.wav", input.bytes); + if (!verdict?.ok) return null; + const hash = await sha256Hex(input.bytes); + const id = `s${hash.slice(0, 10)}`; + const tags = [...new Set([...input.tags, input.kind === "music" ? "音乐" : "音效", "我做的"].filter(hasCjk))]; + if (!tags.length) return null; + const title = hasCjk(input.title) ? input.title.trim().slice(0, 24) : input.kind === "music" ? "我的音乐" : "我的音效"; + const path = `sounds/${id}.wav`; + const parsed = catalogAssetSchema.safeParse({ + id, + version: "1.0.0", + title, + path, + mediaType: "audio/wav", + tags, + theme: "我的声音", + category: input.kind, + preview: path, + license: FOREST_LICENSE, + author: "你", + source: FOREST_SOURCE, + attribution: ATTRIBUTION, + sha256: hash, + }); + return parsed.success ? parsed.data : null; +} + +export async function keepSound(input: { + bytes: Uint8Array; + title: string; + kind: SoundKind; + tags: string[]; +}): Promise<{ ok: true; asset: CatalogAsset } | { ok: false; reason: "bad-audio" | "write-failed" }> { + const asset = await buildKeptAsset(input); + if (!asset) return { ok: false, reason: "bad-audio" }; + try { + await saveKeptSound({ id: asset.id, createdAt: Date.now(), bytes: new Uint8Array(input.bytes), asset }); + return { ok: true, asset }; + } catch (error) { + console.warn("Keeping the sound failed:", error); + return { ok: false, reason: "write-failed" }; + } +} + +export async function forgetKeptSound(id: string): Promise { + await removeKeptSound(id); +} + +/** A pack the library dialog can merge with the official ones. Empty when nothing was kept. */ +export async function loadKeptPack(): Promise<{ pack: AssetPack; files: Record } | null> { + const rows = await loadKeptSounds(); + const files: Record = {}; + const assets: CatalogAsset[] = []; + for (const row of rows) { + const parsed = catalogAssetSchema.safeParse(row.asset); + if (!parsed.success) continue; + const bytes = row.bytes instanceof Uint8Array ? row.bytes : new Uint8Array(); + if ((await sha256Hex(bytes)) !== parsed.data.sha256) continue; + const verdict = inspectAudio(parsed.data.path, bytes); + if (!verdict?.ok) continue; + files[parsed.data.path] = bytes; + assets.push(parsed.data); + } + if (!assets.length) return null; + const pack = assetPackSchema.parse({ + format: "gamekit.asset-pack", + formatVersion: 1, + id: KEPT_PACK_ID, + version: "1.0.0", + title: "我做的声音", + theme: "我的声音", + summary: "你在声音编辑器里收录的声音,存在这台电脑上。复制进项目之后,游戏不再来这里取。", + license: FOREST_LICENSE, + author: "你", + source: FOREST_SOURCE, + attribution: ATTRIBUTION, + assets, + }); + return { pack, files }; +} diff --git a/src/lib/audio/play.ts b/src/lib/audio/play.ts new file mode 100644 index 0000000..f8c91a7 --- /dev/null +++ b/src/lib/audio/play.ts @@ -0,0 +1,56 @@ +/** + * Pygame the child can copy. The file is a normal WAV or OGG in the project. + * No GameKit import, and nothing is fetched when the game runs. + */ + +function quote(path: string): string { + return `"${path}"`; +} + +/** A few lines for mixer.Sound. `loops` is -1 when the child asked for a loop. */ +export function pygameSoundSnippet(path: string, loop: boolean): string { + return [ + "import pygame", + "", + "pygame.mixer.init()", + `sound = pygame.mixer.Sound(${quote(path)})`, + loop ? "sound.play(-1)" : "sound.play()", + "", + ].join("\n"); +} + +/** + * A whole main.py. Sound is loaded after a click, because the browser stays + * quiet until someone taps the game. The print is the length pygame read. + */ +export function pygameSoundDemo(path: string, loop: boolean): string { + const play = loop ? "sound.play(-1)" : "sound.play()"; + return `import asyncio +import pygame + +pygame.init() +screen = pygame.display.set_mode((320, 240)) +clock = pygame.time.Clock() +print("SOUND-WAIT") + + +async def main(): + sound = None + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + if event.type == pygame.MOUSEBUTTONDOWN and sound is None: + pygame.mixer.init() + sound = pygame.mixer.Sound(${quote(path)}) + print("SOUND-OK", int(round(sound.get_length() * 1000))) + ${play} + screen.fill((46, 92, 74)) + pygame.display.flip() + clock.tick(60) + await asyncio.sleep(0) + + +asyncio.run(main()) +`; +} diff --git a/src/lib/audio/presets.ts b/src/lib/audio/presets.ts new file mode 100644 index 0000000..641931a --- /dev/null +++ b/src/lib/audio/presets.ts @@ -0,0 +1,106 @@ +/** + * Sounds a child can hear without logging in. + * + * Each preset is an ordinary spec the synthesizer already knows. Variants share + * tags with the forest pictures (角色、金币、蘑菇、森林) so the library can show + * them as one set. Nothing here calls a model. + */ +import { IDENTITY_EDIT, type SoundEdit } from "./edit"; +import type { MusicSpec, SfxSpec } from "./spec"; + +export type SoundPreset = { + id: string; + title: string; + kind: "sfx" | "music"; + tags: string[]; + /** Shares the forest picture pack's theme, so one filter shows both. */ + theme: string; + spec: SfxSpec | MusicSpec; + edit: SoundEdit; +}; + +const theme = "森林冒险"; + +function sfx(id: string, file: string, title: string, tags: string[], layer: SfxSpec["layers"][number]): SoundPreset { + const spec: SfxSpec = { kind: "sfx", say: title, name: file, layers: [layer] }; + return { id, title, kind: "sfx", tags, theme, spec, edit: IDENTITY_EDIT }; +} + +export const SOUND_PRESETS: SoundPreset[] = [ + sfx("jump-sfx", "jump", "跳跃", ["音效", "跳跃", "角色"], { + wave: "square", + from: 520, + to: 880, + gain: 0.35, + attack: 0.005, + decay: 0.06, + sustain: 0.15, + release: 0.08, + duration: 0.22, + }), + sfx("jump-soft", "jump-soft", "轻跳", ["音效", "跳跃", "角色", "变体"], { + wave: "triangle", + from: 440, + to: 660, + gain: 0.28, + attack: 0.01, + decay: 0.08, + sustain: 0.2, + release: 0.1, + duration: 0.26, + }), + sfx("coin-sfx", "coin", "金币声", ["音效", "金币", "道具"], { + wave: "square", + from: 880, + to: 1320, + gain: 0.3, + attack: 0.002, + decay: 0.04, + sustain: 0.1, + release: 0.06, + duration: 0.18, + }), + sfx("bump-sfx", "bump", "碰到蘑菇", ["音效", "撞击", "蘑菇"], { + wave: "noise", + from: 240, + to: 90, + gain: 0.4, + attack: 0.001, + decay: 0.05, + sustain: 0.05, + release: 0.08, + duration: 0.16, + cutoff: 1400, + }), + { + id: "walk-music", + title: "走路音乐", + kind: "music", + tags: ["音乐", "森林", "循环"], + theme, + edit: IDENTITY_EDIT, + spec: { + kind: "music", + say: "一段可以循环的走路音乐。", + name: "walk", + bpm: 100, + bars: 2, + root: 57, + scale: "minor", + chords: [1, 4], + tracks: [ + { wave: "triangle", gain: 0.22, role: "lead", octave: 0 }, + { wave: "triangle", gain: 0.18, role: "bass", octave: -1 }, + { wave: "noise", gain: 0.12, role: "hat", octave: 0 }, + ], + }, + }, +]; + +export function soundPreset(id: string): SoundPreset { + return SOUND_PRESETS.find((preset) => preset.id === id) ?? SOUND_PRESETS[0]; +} + +export function presetsFor(kind: "sfx" | "music"): SoundPreset[] { + return SOUND_PRESETS.filter((preset) => preset.kind === kind); +} diff --git a/src/lib/audio/render.ts b/src/lib/audio/render.ts index 3ea535d..698d9df 100644 --- a/src/lib/audio/render.ts +++ b/src/lib/audio/render.ts @@ -54,6 +54,15 @@ function lowpass(input: Float32Array, cutoff: number): Float32Array { return out; } +/** attack → decay → sustain → release,音量只往下降。编辑器画出的曲线用的是同一个函数。 */ +export function envelopeAt( + layer: { attack: number; decay: number; sustain: number; release: number }, + duration: number, + time: number, +): number { + return envelope(layer, duration)(time); +} + /** attack → decay → sustain → release,音量只往下降。 */ function envelope(layer: { attack: number; decay: number; sustain: number; release: number }, duration: number) { const attack = Math.min(layer.attack, duration); diff --git a/src/lib/audio/save.ts b/src/lib/audio/save.ts new file mode 100644 index 0000000..bdbc355 --- /dev/null +++ b/src/lib/audio/save.ts @@ -0,0 +1,60 @@ +/** + * The only plan for putting a sound into a project. + * + * The bytes are the WAV the editor already previewed. A bad file is refused + * before anything is written. main.py changes only when the child confirmed + * the stage demo. + */ +import { inspectAudio } from "./format"; +import { normalizePath, uniquePath, upsertFile, type Project } from "../project"; + +export type SoundSaveInput = { + bytes: Uint8Array; + suggested: string; + /** Overwrite this path when the editor already saved it once. */ + reusePath?: string; + /** Set only after the child confirms replacing main.py. */ + mainText?: string; +}; + +export type SoundSaveFailure = "no-project" | "bad-audio" | "name"; + +export type SoundSavePlan = + | { ok: true; next: Project; path: string; changed: boolean } + | { ok: false; reason: SoundSaveFailure }; + +export type SoundSaveResult = + | { ok: true; path: string; changed: boolean } + | { ok: false; reason: SoundSaveFailure | "write-failed" }; + +function sameBytes(project: Project, path: string, bytes: Uint8Array): boolean { + const existing = project.files.find((file) => file.path === path)?.bytes; + if (!existing || existing.length !== bytes.length) return false; + for (let index = 0; index < bytes.length; index += 1) { + if (existing[index] !== bytes[index]) return false; + } + return true; +} + +function wavPath(value: string | null): value is string { + return !!value && value.startsWith("assets/") && value.endsWith(".wav") && !value.includes(".."); +} + +export function planSoundSave(project: Project | null, input: SoundSaveInput): SoundSavePlan { + if (!project) return { ok: false, reason: "no-project" }; + const suggested = normalizePath(input.suggested); + if (!wavPath(suggested)) return { ok: false, reason: "name" }; + const verdict = inspectAudio(suggested, input.bytes); + if (!verdict?.ok || verdict.format !== "wav") return { ok: false, reason: "bad-audio" }; + + const reuse = input.reusePath ? normalizePath(input.reusePath) : null; + const path = wavPath(reuse) && project.files.some((file) => file.path === reuse) ? reuse : uniquePath(project.files.map((file) => file.path), suggested); + if (!wavPath(path)) return { ok: false, reason: "name" }; + + let next = upsertFile(project, { path, bytes: new Uint8Array(input.bytes) }); + if (input.mainText !== undefined) next = upsertFile(next, { path: "main.py", text: input.mainText }); + + const sameMain = input.mainText === undefined || project.files.find((file) => file.path === "main.py")?.text === input.mainText; + if (sameBytes(project, path, input.bytes) && sameMain) return { ok: true, next: project, path, changed: false }; + return { ok: true, next, path, changed: true }; +} diff --git a/src/lib/diagnostics.test.ts b/src/lib/diagnostics.test.ts index 8fd1891..d8f28d9 100644 --- a/src/lib/diagnostics.test.ts +++ b/src/lib/diagnostics.test.ts @@ -1,4 +1,7 @@ +import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { renderEdited } from "./audio/edit"; +import { soundPreset } from "./audio/presets"; import { diagnoseProject, problemsFromConsole } from "./diagnostics"; import type { Project } from "./project"; @@ -30,6 +33,22 @@ describe("diagnoseProject", () => { project("import time\ntime.sleep(1)\n", [{ path: "assets/jump.wav", bytes: new Uint8Array([1]) }]), ); expect(problems.map((item) => item.id)).toEqual(expect.arrayContaining(["sleep-2", "audio-assets/jump.wav"])); + expect(problems.find((item) => item.id === "audio-assets/jump.wav")?.message).toContain("WAV"); + }); + + it("accepts PCM WAV and OGG Vorbis, and flags MP3", () => { + const jump = soundPreset("jump-sfx"); + const wav = renderEdited(jump.spec, jump.edit).wav; + const ogg = new Uint8Array(readFileSync("src/lib/samples/jump.ogg")); + const problems = diagnoseProject( + project("import asyncio\nimport pygame\npygame.display.flip()\nawait asyncio.sleep(0)\n", [ + { path: "assets/jump.wav", bytes: wav }, + { path: "assets/jump.ogg", bytes: ogg }, + { path: "assets/song.mp3", bytes: new Uint8Array([0x49, 0x44, 0x33]) }, + ]), + ); + expect(problems.map((item) => item.id)).toEqual(["audio-assets/song.mp3"]); + expect(problems[0]?.message).toContain("MP3"); }); }); diff --git a/src/lib/diagnostics.ts b/src/lib/diagnostics.ts index e7f4b0c..c2d9b21 100644 --- a/src/lib/diagnostics.ts +++ b/src/lib/diagnostics.ts @@ -1,3 +1,4 @@ +import { audioProblemMessage, inspectAudio } from "./audio/format"; import type { Project } from "./project"; export type Problem = { @@ -65,20 +66,14 @@ export function diagnoseProject(project: Project): Problem[] { } for (const file of project.files) { - const ext = file.path.split(".").pop()?.toLowerCase(); - if (ext && ["wav", "mp3", "aiff"].includes(ext)) { - problems.push({ - id: `audio-${file.path}`, - severity: "warning", - path: file.path, - // WAV loads in the browser runtime (verified with a probe); MP3/AIFF may - // not decode. OGG is still the safest choice for a web build. - message: - ext === "wav" - ? "WAV loads in the browser runtime too. Sound only starts after someone taps the game once." - : "The browser runtime decodes OGG Vorbis and WAV. This format may stay silent on the web, but still plays on desktop Pygame.", - }); - } + const verdict = inspectAudio(file.path, file.bytes); + if (!verdict || verdict.ok) continue; + problems.push({ + id: `audio-${file.path}`, + severity: "warning", + path: file.path, + message: audioProblemMessage(verdict), + }); } return problems; diff --git a/src/lib/storage.ts b/src/lib/storage.ts index eddf17f..a93e43d 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -4,6 +4,8 @@ const DB_NAME = "gamekit"; const STORE = "projects"; /** One agent conversation per project (src/lib/adk/session.ts). */ const SESSION_STORE = "sessions"; +/** Sounds the child kept from the editor, separate from any one project. */ +const SOUND_STORE = "sounds"; /** * Schema rule: object stores and indexes need a version bump (plus a migration in @@ -11,8 +13,16 @@ const SESSION_STORE = "sessions"; * not: old records simply lack it, and `fromStored` tolerates that. `design` * (src/lib/design.ts) and `trash` (src/lib/project.ts) were added that way. * Version 2 added the `sessions` store for agent conversations. + * Version 3 added the `sounds` store for editor sounds kept on this computer. */ -const DB_VERSION = 2; +const DB_VERSION = 3; + +export type StoredKeptSound = { + id: string; + createdAt: number; + bytes: Uint8Array; + asset: unknown; +}; function openDb(): Promise { return new Promise((resolve, reject) => { @@ -21,6 +31,7 @@ function openDb(): Promise { const db = request.result; if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: "id" }); if (!db.objectStoreNames.contains(SESSION_STORE)) db.createObjectStore(SESSION_STORE, { keyPath: "id" }); + if (!db.objectStoreNames.contains(SOUND_STORE)) db.createObjectStore(SOUND_STORE, { keyPath: "id" }); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); @@ -93,3 +104,23 @@ export async function removeSession(id: string): Promise { await requestToPromise(db.transaction(SESSION_STORE, "readwrite").objectStore(SESSION_STORE).delete(id)); db.close(); } + +/** Editor sounds kept on this computer. Missing store on an old database is created by version 3. */ +export async function loadKeptSounds(): Promise { + const db = await openDb(); + const rows = await requestToPromise(db.transaction(SOUND_STORE).objectStore(SOUND_STORE).getAll() as IDBRequest); + db.close(); + return rows.sort((a, b) => b.createdAt - a.createdAt); +} + +export async function saveKeptSound(row: StoredKeptSound): Promise { + const db = await openDb(); + await requestToPromise(db.transaction(SOUND_STORE, "readwrite").objectStore(SOUND_STORE).put(row)); + db.close(); +} + +export async function removeKeptSound(id: string): Promise { + const db = await openDb(); + await requestToPromise(db.transaction(SOUND_STORE, "readwrite").objectStore(SOUND_STORE).delete(id)); + db.close(); +} diff --git a/src/studio/store.tsx b/src/studio/store.tsx index 481c0ef..8e226d3 100644 --- a/src/studio/store.tsx +++ b/src/studio/store.tsx @@ -70,9 +70,9 @@ import { cleanMapName, createMap, isMapSlug } from "../lib/map/model"; import { planMapExample, planMapSave, type MapExampleInput, type MapExampleResult, type MapSaveFailure, type MapSaveInput } from "../lib/map/save"; import { discardPlay, downloadBytes, ensurePlayerServiceWorker, publishPlay } from "../lib/session"; import { budgetFor, loadSettings, saveSettings, type Settings } from "../lib/settings"; -import { renderMusic, renderSfx } from "../lib/audio/render"; -import { readSoundSpec } from "../lib/audio/spec"; -import { pcmToWav } from "../lib/audio/wav"; +import { forgetKeptSound as forgetKeptRecord, keepSound as keepSoundRecord } from "../lib/audio/kept"; +import { planSoundSave, type SoundSaveInput, type SoundSaveResult } from "../lib/audio/save"; +import { readSoundSpec, type SoundSpec } from "../lib/audio/spec"; import { prepareAsset } from "../lib/sprite"; import { createBlankProject, createStarterProject } from "../lib/starter"; import { applyLibraryCopy, type LibraryCopyRequest, type LibraryCopyResult } from "../lib/assets/copy"; @@ -277,6 +277,16 @@ type StudioValue = { saveMapExample: (input: MapExampleInput) => Promise; soundKind: SoundKind; setSoundKind: (kind: SoundKind) => void; + /** Set when the model picks numbers. The editor renders this spec itself. */ + soundRecipe: { token: number; spec: SoundSpec } | null; + clearSoundRecipe: () => void; + /** Writes the previewed WAV. Replaces main.py only when mainText is set. */ + saveSound: (input: SoundSaveInput) => Promise; + keepSound: (input: { bytes: Uint8Array; title: string; kind: SoundKind; tags: string[] }) => Promise< + { ok: true } | { ok: false; reason: "bad-audio" | "write-failed" } + >; + forgetKeptSound: (id: string) => Promise; + soundLibraryVersion: number; aiBusy: boolean; notice: string; dismissNotice: () => void; @@ -398,6 +408,8 @@ export function StudioProvider({ children }: { children: ReactNode }) { const [assetOpen, setAssetOpen] = useState(false); const [soundOpen, setSoundOpen] = useState(false); const [soundKind, setSoundKind] = useState("sfx"); + const [soundRecipe, setSoundRecipe] = useState<{ token: number; spec: SoundSpec } | null>(null); + const [soundLibraryVersion, setSoundLibraryVersion] = useState(0); const [pixelNewOpen, setPixelNewOpen] = useState(false); const [libraryOpen, setLibraryOpen] = useState(false); const [pixelSession, setPixelSession] = useState(null); @@ -833,6 +845,45 @@ export function StudioProvider({ children }: { children: ReactNode }) { setMapSession(null); }, []); + const saveSound = useCallback( + async (input: SoundSaveInput): Promise => { + const base = projectRef.current; + if (!base) return { ok: false, reason: "no-project" }; + const plan = planSoundSave(base, input); + if (!plan.ok) return plan; + if (!plan.changed) return { ok: true, path: plan.path, changed: false }; + flushSave(); + const written = saveChain.current.then(() => saveProject(plan.next)); + saveChain.current = written.then( + () => undefined, + () => undefined, + ); + try { + await written; + } catch (error) { + console.warn("Saving the sound failed:", error); + return { ok: false, reason: "write-failed" }; + } + setUndoStack((stack) => [...stack, { projectId: base.id, files: base.files, design: base.design, trash: base.trash }].slice(-20)); + if (projectRef.current?.id === plan.next.id) projectRef.current = plan.next; + setProjects((current) => current.map((item) => (item.id === plan.next.id ? plan.next : item))); + setPath(input.mainText !== undefined ? "main.py" : plan.path); + return { ok: true, path: plan.path, changed: true }; + }, + [flushSave], + ); + + const keepSound = useCallback(async (input: { bytes: Uint8Array; title: string; kind: SoundKind; tags: string[] }) => { + const outcome = await keepSoundRecord(input); + if (outcome.ok) setSoundLibraryVersion((version) => version + 1); + return outcome.ok ? { ok: true as const } : { ok: false as const, reason: outcome.reason }; + }, []); + + const forgetKeptSound = useCallback(async (id: string) => { + await forgetKeptRecord(id); + setSoundLibraryVersion((version) => version + 1); + }, []); + const saveMap = useCallback( async (input: MapSaveInput): Promise => { const session = mapSessionRef.current; @@ -1415,6 +1466,12 @@ export function StudioProvider({ children }: { children: ReactNode }) { setSoundOpen, soundKind, setSoundKind, + soundRecipe, + clearSoundRecipe: () => setSoundRecipe(null), + saveSound, + keepSound, + forgetKeptSound, + soundLibraryVersion, aiBusy, notice, dismissNotice: () => setNotice(""), @@ -1677,16 +1734,7 @@ export function StudioProvider({ children }: { children: ReactNode }) { if (!payload?.text) throw new Error("Sound generation failed."); // The model chose numbers; we render the samples here, deterministically. const spec = readSoundSpec(payload.text, soundKind, soundKind === "sfx" ? "sound" : "music"); - const pcm = spec.kind === "sfx" ? renderSfx(spec) : renderMusic(spec); - const bytes = pcmToWav(pcm); - setAssetDraft({ - bytes, - mediaType: "audio/wav", - suggested: `assets/${spec.name}.wav`, - width: 0, - height: 0, - sound: { kind: soundKind, say: spec.say, seconds: pcm.samples.length / pcm.sampleRate }, - }); + setSoundRecipe({ token: variation.current, spec }); } catch (error) { console.warn("Sound generation failed:", error); setNotice(text.ai.soundFailed); @@ -1840,6 +1888,11 @@ export function StudioProvider({ children }: { children: ReactNode }) { assetOpen, soundOpen, soundKind, + soundRecipe, + soundLibraryVersion, + saveSound, + keepSound, + forgetKeptSound, pixelNewOpen, libraryOpen, pixelSession, diff --git a/src/styles.css b/src/styles.css index e9419ba..69ea13a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -842,6 +842,10 @@ button:disabled { opacity: 0.45; cursor: default; } font-size: 12px; } .library-card.on { border-color: var(--pine); box-shadow: inset 3px 0 0 var(--pine); } +.library-audio { + width: 64px; height: 64px; display: grid; place-items: center; + background: var(--paper); border-radius: 8px; font-size: 13px; color: var(--ink); +} .library-card img, .library-detail img.preview { image-rendering: pixelated; object-fit: contain; diff --git a/src/ui/LibraryDialog.tsx b/src/ui/LibraryDialog.tsx index 7540eef..038691f 100644 --- a/src/ui/LibraryDialog.tsx +++ b/src/ui/LibraryDialog.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import { KEPT_PACK_ID, loadKeptPack } from "../lib/audio/kept"; import { clearCatalogCache, loadAssetBytes, loadLibrary, packAssetUrl } from "../lib/assets/catalog"; import { describeLibraryCopy, @@ -9,7 +10,7 @@ import { type LibraryExample, type LibraryPlan, } from "../lib/assets/copy"; -import { filterAssets } from "../lib/assets/filter"; +import { filterAssets, relatedAssets } from "../lib/assets/filter"; import type { AssetCategory, AssetPack, CatalogAsset } from "../lib/assets/schema"; import { pygameDemoMain, pygameSnippet, type SnippetAsset } from "../lib/assets/snippet"; import { useStudio } from "../studio/store"; @@ -33,6 +34,8 @@ function snippetOf(asset: CatalogAsset): SnippetAsset { attribution: asset.attribution, frameWidth: asset.animation?.frameWidth ?? asset.width ?? 0, frameHeight: asset.animation?.frameHeight ?? asset.height ?? 0, + media: asset.mediaType.startsWith("audio/") ? "audio" : "image", + loop: asset.category === "music", }; } @@ -53,16 +56,20 @@ export function LibraryDialog() { const [busy, setBusy] = useState(false); const [status, setStatus] = useState(""); const [copied, setCopied] = useState(false); + const [localFiles, setLocalFiles] = useState>({}); + const [removeKept, setRemoveKept] = useState(false); + const [keptUrl, setKeptUrl] = useState(""); useEffect(() => { if (!open) return; let cancel = false; setLoading(true); setError(""); - void loadLibrary() - .then((loaded) => { + void Promise.all([loadLibrary(), loadKeptPack()]) + .then(([loaded, kept]) => { if (cancel) return; - setPacks(loaded); + setPacks(kept ? [...loaded, kept.pack] : loaded); + setLocalFiles(kept?.files ?? {}); setLoading(false); }) .catch(() => { @@ -73,7 +80,7 @@ export function LibraryDialog() { return () => { cancel = true; }; - }, [open, reload]); + }, [open, reload, studio.soundLibraryVersion]); useEffect(() => { if (open) return; @@ -108,6 +115,15 @@ export function LibraryDialog() { ); const visible = rows.filter((row) => shown.includes(row.asset)); const selected = visible.find((row) => row.asset.id === selectedId) ?? visible[0] ?? null; + const keptPath = selected?.pack.id === KEPT_PACK_ID && selected.asset.mediaType.startsWith("audio/") ? selected.asset.path : ""; + useEffect(() => { + if (!keptPath) return; + const bytes = localFiles[keptPath]; + if (!bytes) return; + const url = URL.createObjectURL(new Blob([new Uint8Array(bytes)], { type: "audio/wav" })); + setKeptUrl(url); + return () => URL.revokeObjectURL(url); + }, [keptPath, localFiles]); if (!open) return null; @@ -126,7 +142,9 @@ export function LibraryDialog() { }; const prepare = async (row: Row): Promise => { - const bytes = await loadAssetBytes(row.pack.id, row.asset.path); + const bytes = + row.pack.id === KEPT_PACK_ID ? localFiles[row.asset.path] : await loadAssetBytes(row.pack.id, row.asset.path); + if (!bytes) throw new Error("Missing sound"); const copy = libraryCopyFrom(row.pack, row.asset, bytes); if (!studio.project) { setStatus(text.library.message({ code: "no-project", path: "", pinsSkipped: false, exampleSkipped: false })); @@ -201,6 +219,13 @@ export function LibraryDialog() { const dest = selected ? libraryDest({ packId: selected.pack.id, assetPath: selected.asset.path }) : ""; const snippet = selected ? pygameSnippet(snippetOf(selected.asset), dest) : ""; + const related = selected ? relatedAssets(rows.map((row) => row.asset), selected.asset).slice(0, 8) : []; + const audioPreview = + selected && selected.asset.mediaType.startsWith("audio/") + ? selected.pack.id === KEPT_PACK_ID + ? "" + : packAssetUrl(selected.pack.id, selected.asset.path) + : ""; return (

{selected ? (
- {selected.asset.title} + {selected.asset.mediaType.startsWith("audio/") ? ( +
) : null} + {status ?

{status}

: null} diff --git a/src/ui/Overlays.tsx b/src/ui/Overlays.tsx index 507ef38..5678fea 100644 --- a/src/ui/Overlays.tsx +++ b/src/ui/Overlays.tsx @@ -7,6 +7,7 @@ import { PixelEditor } from "./PixelEditor"; import { PixelNewDialog } from "./PixelNewDialog"; import { MapEditor } from "./MapEditor"; import { MapNewDialog } from "./MapNewDialog"; +import { SoundDialog } from "./SoundEditor"; import { text } from "./text"; export function Overlays() { @@ -24,86 +25,6 @@ export function Overlays() { ); } -/** - * 生成声音: the child describes a sound, the model picks the numbers, and the - * browser renders the samples (src/lib/audio). The preview plays exactly the file - * that will be stored. - */ -function SoundDialog() { - const studio = useStudio(); - const [prompt, setPrompt] = useState(""); - const [url, setUrl] = useState(""); - - useEffect(() => { - if (!studio.assetDraft?.sound) return; - const copy = new Uint8Array(studio.assetDraft.bytes); - const next = URL.createObjectURL(new Blob([copy], { type: studio.assetDraft.mediaType })); - setUrl(next); - return () => URL.revokeObjectURL(next); - }, [studio.assetDraft]); - - const draft = studio.assetDraft?.sound ? studio.assetDraft : null; - - return ( -
-
{ - event.preventDefault(); - if (prompt.trim().length >= 2) void studio.generateSound(prompt); - }} - > -

{text.sounds.title}

-

{text.sounds.copy}

-
- {(["sfx", "music"] as const).map((kind) => ( - - ))} -
-