flipperzero-ocarina:在 Flipper Zero 上复刻《时之笛》五孔奥卡利那笛的完整源码解析
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
导读
flipperzero-ocarina 是运行在 Flipper Zero 上的一个极简"奥卡利那笛"(Ocarina of Time)模拟器,它把 Flipper Zero 的五个物理按键映射为五孔陶笛的五个音孔,按下发声、松开止音,并完全沿用 N64 版《塞尔达传说:时之笛》的演奏逻辑(OK 键替代 A 键)。本文以 ocarina/README.md 为骨架,结合仓库中 ocarina.c 与 application.fam 的完整实现,从按键映射、音高表、事件循环、扬声器驱动到 FAP 打包逐层拆解,读者读完后既能直接玩转这款应用,也能把它当作理解 Flipper Zero GUI + 音频 + 输入子系统的最小范例。
一、应用概述:一个"会响的按键矩阵"
按照原文档的描述,这是一个basic Ocarina(of Time)——一个基础版的、致敬《时之笛》的奥卡利那笛。它不做任何复杂的乐理计算,也没有旋律回放功能,而是把"五孔笛"这个乐器的核心交互还原了出来:
- 按下对应方向的按键 → 持续发出该音孔的音高;
- 松开按键 → 立即停止发声;
- 五个音孔对应五个方向键 + 中央 OK 键,操作方式与 N64 版《时之笛》完全相同,其中OK 键替代了原版 A 按钮。
在 ocarina.c 的顶部,五个音高被直接写成宏常量,构成了整支笛子的音阶基础:
| 按键 | 方向含义(N64 同款) | 频率常量 | 对应音名(近似) |
|---|---|---|---|
InputKeyUp | 上 | NOTE_UP 587.33f | D5 |
InputKeyLeft | 左 | NOTE_LEFT 493.88f | B4 |
InputKeyRight | 右 | NOTE_RIGHT 440.00f | A4(标准音) |
InputKeyDown | 下 | NOTE_DOWN 349.23f | F4 |
InputKeyOk | A 键(中央) | NOTE_OK 293.66f | D4 |
说明:频率数值直接取自源码宏定义,音名标注基于标准十二平均律换算(440 Hz 即 A4),可作为演奏时的参考。
二、按键与音高映射:从输入事件到扬声器
整个应用的"乐器逻辑"集中在主循环里的一个switch语句中(ocarina.c):
if(event.type == InputTypePress) { switch(event.key) { case InputKeyUp: furi_hal_speaker_start(NOTE_UP, volume); break; case InputKeyDown: furi_hal_speaker_start(NOTE_DOWN, volume); break; case InputKeyLeft: furi_hal_speaker_start(NOTE_LEFT, volume); break; case InputKeyRight: furi_hal_speaker_start(NOTE_RIGHT, volume); break; case InputKeyOk: furi_hal_speaker_start(NOTE_OK, volume); break; case InputKeyBack: processing = false; // 按 Back 退出 break; default: break; } } else if(event.type == InputTypeRelease) { furi_hal_speaker_stop(); // 任意键松开即止音 }这里有三个值得注意的设计细节:
- 按下(
InputTypePress)触发发音,松开(InputTypeRelease)统一止音。furi_hal_speaker_stop()被放在Release分支中,因此同时按下多个键时,最后一个松开会导致整体止音——这是单声道蜂鸣器驱动的自然结果,也决定了它"一个音一个音吹"的演奏风格,正好符合奥卡利那笛的单音吹奏特性。 - 音量被硬编码为
1.0f(ocarina.c),即满音量驱动内部扬声器,未提供音量调节入口,体现了"basic"定位。 InputKeyBack被用来优雅退出:它不发声,而是把循环标志processing置为false,随后进入清理流程(见第五节)。
三、事件驱动架构:Flipper Zero 应用的经典四件套
虽然功能简单,但 ocarina 的代码结构非常完整,是 Flipper Zero 原生 GUI 应用的标准范式,包含四个核心组成部分(ocarina.c):
typedef struct { FuriMutex* model_mutex; // 保护共享状态的互斥锁 FuriMessageQueue* event_queue; // 输入事件队列 ViewPort* view_port; // 屏幕绘制视口 Gui* gui; // GUI 服务句柄 } Ocarina;1. 事件队列(FuriMessageQueue)
在ocarina_alloc()中创建了容量为8 条、每条sizeof(InputEvent)的队列(ocarina.c)。硬件输入回调input_callback只做一件事——把输入事件put进队列:
void input_callback(InputEvent* input, void* ctx) { Ocarina* ocarina = ctx; furi_message_queue_put(ocarina->event_queue, input, FuriWaitForever); }而应用主线程通过furi_message_queue_get(..., 100)以100 tick 超时轮询出队(ocarina.c)。这种"回调只入队、主循环处理"的模型把按键处理放回到应用线程,避免在中断上下文或 GUI 线程中做耗时操作,是 FuriOS 上推荐的线程安全写法。
2. 互斥锁(FuriMutex)
model_mutex采用FuriMutexTypeNormal类型(ocarina.c),在主循环的每次迭代前后分别acquire/release,同时保护draw_callback中的绘制访问。虽然本应用没有可变模型数据,但它演示了"渲染与逻辑共享状态时必须加锁"的规范。
3. 视口与 GUI(ViewPort+Gui)
ocarina_alloc()中完成了 GUI 挂载的完整流程:
instance->view_port = view_port_alloc(); view_port_draw_callback_set(instance->view_port, draw_callback, instance); view_port_input_callback_set(instance->view_port, input_callback, instance); instance->gui = furi_record_open("gui"); gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen);- 通过
furi_record_open("gui")获取系统 GUI 服务(Flipper Zero 的"服务注册表"机制); - 将视口注册到
GuiLayerFullscreen(全屏层),因此应用启动后会覆盖整个 128×64 屏幕。
4. 绘制回调(draw_callback)
屏幕内容非常简单,但精确对应了 Flipper Zero 的屏幕规格(128×64):
canvas_draw_frame(canvas, 0, 0, 128, 64); // 全屏边框 canvas_draw_str(canvas, 50, 10, "Ocarina"); // 标题 canvas_draw_str(canvas, 30, 20, "OK button for A"); // 操作提示界面上直接印着"OK button for A"——这正是原文档强调的 N64 键位移植点:用 Flipper Zero 的中央 OK 键替代 N64 手柄的 A 按钮。主循环每次迭代末尾调用view_port_update()触发重绘(ocarina.c)。
四、音频输出:furi_hal_speaker的按下-松开模型
所有发声都基于 Flipper Zero 硬件抽象层的两个 API:
furi_hal_speaker_start(frequency, volume):以指定频率(Hz)和音量(0.0–1.0)启动内部扬声器;furi_hal_speaker_stop():停止发声。
ocarina 的"持续音"效果完全依赖按键按下状态:按住即持续输出方波音调,松开即停。这一实现与该仓库其他音频应用使用了完全相同的 HAL 接口——例如同目录下的 tuning_fork.c(调音叉)、music_beeper_worker.c(蜂鸣器音乐)、morse_code_worker.c(莫尔斯码)以及 speaker_hal.c(音乐追踪器)都基于furi_hal_speaker_start发声。可以推断,furi_hal_speaker是这批老版官方固件应用的通用音频通路。
需要注意的兼容性前提:本目录位于
Applications/Official/source-OLDER/,属于旧版官方固件时期的应用源码归档。仓库 Applications/Official/ReadMe.md 明确提示,"部分使用音频的应用因固件音频访问模式变更而无法正常工作"(原文:Some apps which use audio won't work due to a new pattern for audio access)。因此在新版官方固件上编译运行本应用时,建议先确认当前固件对furi_hal_speaker_start的可用性,并优先考虑使用官方应用商店(FAP Store)中维护的版本。
五、资源释放与退出流程:ocarina_free的逆序清理
ocarina_free()展示了 Flipper Zero 应用的资源管理规范(ocarina.c),与ocarina_alloc()严格逆序:
void ocarina_free(Ocarina* instance) { view_port_enabled_set(instance->view_port, false); // 1. 禁用视口 gui_remove_view_port(instance->gui, instance->view_port); // 2. 从 GUI 摘除视口 furi_record_close("gui"); // 3. 关闭 gui 服务记录 view_port_free(instance->view_port); // 4. 释放视口内存 furi_message_queue_free(instance->event_queue); // 5. 释放事件队列 furi_mutex_free(instance->model_mutex); // 6. 释放互斥锁 furi_hal_speaker_stop(); // 7. 确保退出时扬声器静音 free(instance); // 8. 释放实例 }特别值得注意的是第 7 步:退出前强制调用furi_hal_speaker_stop()。因为本应用的音频输出完全由按键按下状态驱动,若用户在按住某个音孔的瞬间按下 Back 退出,扬声器可能仍在发声;这行代码确保应用无论以何种方式退出,都不会留下"幽灵持续音"。这个细节对任何使用扬声器的应用都具有借鉴意义。
六、FAP 构建配置:application.fam逐项解读
应用通过 FAP(Flipper Application Package)清单 application.fam 描述构建元数据,全文件仅 13 行:
App( appid="Ocarina", name="Ocarina", apptype=FlipperAppType.EXTERNAL, entry_point="ocarina_app", cdefines=["APP_OCARINA"], requires=["gui"], stack_size=1 * 1024, order=30, fap_icon="icons/music_10px.png", fap_category="Music", fap_icon_assets="icons", )各字段的实际影响:
appid/name:应用唯一标识与显示名,均为Ocarina;apptype=FlipperAppType.EXTERNAL:编译为独立 FAP 文件,可放置于 SD 卡/apps目录运行,无需刷入固件;entry_point="ocarina_app":入口函数名,对应源码中的int32_t ocarina_app(void* p)(ocarina.c);requires=["gui"]:声明依赖 GUI 服务,即运行时通过furi_record_open("gui")访问的服务;stack_size=1 * 1024:应用线程栈仅 1 KB——由于本应用不做任何堆上的复杂数据处理,1 KB 栈足以支撑主循环、事件处理与绘制调用;order=30:应用在菜单中的排序权重;fap_icon="icons/music_10px.png":应用图标(10×10 像素的极简音符图标),图标资产目录为同级的icons/;fap_category="Music":在应用菜单中被归类到Music(音乐)分组下。
七、从源码到实操:如何构建与运行
由于source-OLDER是旧版源码归档,官方固件现已改用 FAP 商店分发机制(详见 Applications/Official/ReadMe.md),因此推荐两条使用路径:
- 直接使用编译产物:官方固件用户可前往 Flipper 官方应用商店(lab.flipper.net/apps)或 Flipper 手机应用内的应用目录搜索 "Ocarina" 类音乐应用直接安装 FAP 文件;
- 本地构建:若环境固件兼容(见第四节的音频访问注意事项),可将本目录源码置于固件仓库的
applications_user目录,通过./fbt fap_Ocarina之类的 fbt 命令编译出 FAP,再拷贝到 SD 卡/apps/Music/目录后从应用菜单启动。
启动后,屏幕中央显示 "Ocarina" 标题与 "OK button for A" 提示,玩家即可像吹奏《时之笛》中的奥卡利那笛一样:方向键对应四个音孔,OK 键替代 A 键,按住发音、松开止音、按 Back 退出。
八、延伸思考:从五音笛到旋律
作为"basic"实现,ocarina 仍有明确的扩展空间,且每个方向都有仓库内现成的参考实现:
- 多音阶切换:当前五个固定频率硬编码于宏定义,可参考同作者的 tuning_fork.c 引入按键切换音阶/调式;
- 旋律回放:将按键序列录制为乐谱并自动播放,可复用 music_beeper_worker.c 的节拍与音符队列模式;
- 乐曲文件加载:若希望从 SD 卡读取曲谱,musictracker 中的 speaker_hal.c 展示了更精细的扬声器控制封装;
- 可视化反馈:当前
draw_callback仅绘制静态文字,可参照仓库中其他游戏类应用为按下的音孔增加高亮动画,增强"吹奏"的即时反馈。
总结
flipperzero-ocarina 虽然只有 118 行 C 代码,却完整覆盖了 Flipper Zero 应用的五大核心主题:GUI 视口渲染、输入事件队列、互斥锁同步、HAL 扬声器驱动、FAP 构建打包。它的价值远超"一个能响的玩具"——对想学习 Flipper Zero 应用开发的人来说,这是一份麻雀虽小、五脏俱全的最小可运行示例;而对玩家来说,它把《时之笛》中最具标志性的交互之一原汁原味地搬到了口袋里的 Flipper Zero 上。按下、松开、聆听——就是这么简单。
【免费下载链接】FlipperPlayground (and dump) of stuff I make or modify for the Flipper Zero项目地址: https://gitcode.com/GitHub_Trending/fl/Flipper
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考