从 Python 搬到 Rust:pyglet MIDI DAW 变成 egui 鬼畜采样器的迁移复盘

起因

去年用 Python + pyglet 写了一个钢琴卷帘 MIDI DAW:鼠标在网格上画音符、拖拽、改长度,支持播放。功能跑通之后,把它重写成了 Rust + egui 的鬼畜采样器,原仓库随后删除。这篇复盘记录 "为什么迁、迁了什么、丢掉了什么、代价是什么"。

原版做了什么

Python 版一共约 765 行,核心结构:

  • editor.py:钢琴卷帘编辑器,431 行。网格绘制、左侧键盘、鼠标添加 / 删除 / 拖动 / 改长度音符、播放头

  • midi_player.py:播放器。mido 走 MIDI 端口 + pyglet 合成正弦波双模式

  • playback.py:播放控制器,事件驱动调度

功能上是一个能用的演示级 DAW:有钢琴卷帘、有网格对齐、有播放头、能出声音。但播放部分踩了一串坑,详见另一篇《用 time.sleep 播 MIDI 踩的坑》。

撑不下去的三个边界

  • 实时音频:MIDI 端口依赖设备,合成器用 time.sleep 和 GUI 事件循环调度,节奏对不上、卡顿会传导

  • 分发成本:uv add pyglet mido pygame rtmidi,装依赖、跑 Python 环境,别人拿到手要先折腾环境

  • 扩展方向:想把 "钢琴卷帘" 从 MIDI 扩展到 "人声切片 + 歌词 + 鬼畜排列",Python 这边每加一个音频处理能力都要绑一个 C 扩展

为什么选 Rust + egui

  • egui 是 immediate mode,绘制和交互写在一个 show() 里,状态自持,和 pyglet 手动维护 Batch、shapes、坐标换算相比,改动界面的成本低一个量级

  • cpal 直接控制音频输出,回调里混音,不依赖任何系统播放器或合成器

  • symphonia 纯 Rust 解码 wav/ogg,不需要 ffmpeg

  • 编译产物单文件分发,无运行时依赖

迁移不是移植:常量继承与方向改变

重写时最意外的发现:钢琴卷帘的布局常量几乎原样搬了过来。

参数 Python Rust
每拍宽度 90 px 80 px
每半音高度 20 px 18 px
可见节拍 32 32
可见音符 36 36
最低音 C2 (36) C3 (48)
黑键集合 {1,3,6,8,10} {1,3,6,8,10}

网格模型、坐标换算、点击判定这些交互逻辑,在 Rust 版 piano_roll.rs 里基本是逐行对应。但音符的语义变了:Python 版每个音符是一个 MIDI 音号 + 时长,Rust 版每个音符引用一个切片(歌词字 + 音频区域)------ 这就是 "DAW 变鬼畜采样器" 的方向变化。网格是骨架,音符是内容,骨架可以继承,内容换了物种。

音频线程模型:从 sleep 到 ringbuf

Python 版最大的病根是时序调度,Rust 版直接换了架构:

  • 音频线程由 cpal 回调驱动,不 sleep

  • GUI 到音频线程走无锁环形队列(ringbuf),只传命令

  • 回调里维护 voice 列表,逐帧混音

    enum Cmd {

    Trigger { samples: Arc<Vec<f32>>, gain: f32 },

    }

    // 回调里:先排空命令,再混音

    while let Some(cmd) = cons.try_pop() {

    match cmd {

    Cmd::Trigger { samples, gain } => voices.push(Voice { samples, pos: 0, gain }),

    }

    }

    for frame in out.chunks_mut(channels) {

    let mut acc = 0.0f32;

    for voice in voices.iter_mut() {

    if voice.pos < voice.samples.len() {

    acc += voice.samples[voice.pos] * voice.gain;

    voice.pos += 1;

    }

    }

    ...

    }

三个细节值得抄:

  • 无锁队列的容量按命令数设计(64 条),不是按采样数,内存可控

  • 采样格式用泛型分发,SampleFormat::F32 / I16 / U16 各生成一路构建函数,避免回调里每帧做运行时分支

  • 音频初始化失败时返回 null() 引擎,GUI 照常运行,不会一启动就崩

迁移代价

  • Rust 所有权和借用规则有学习曲线,初期写 UI 状态(选中的切片、拖拽状态)比 Python 慢

  • egui 的 immediate mode 没有 "窗口控件对象",状态要么存在 App 结构体里,要么每次重画重建,思维要切换

  • MIDI 端口播放能力被丢掉了。如果以后要接硬件合成器或 MIDI 输出,得另引 midir 之类的库

  • 音频解码从 "合成正弦波" 变成 "读真实文件",symphonia 的多声道转单声道、采样率保留这些都要自己处理

给想迁移的人

  • 如果原项目有 "布局常量" 这类设计沉淀,重写时先抄常量再改逻辑,能省一半工作量

  • 音频这种实时场景,先定线程模型再写功能,别先功能后架构

  • 迁移不是 1:1 移植,趁机把 "用不上的能力"(MIDI 端口)和 "撑不住的能力"(sleep 调度)一起丢掉,比原样搬过来更有价值

  • Rust 版的音频线程模型、命令队列设计,可以原样复用到任何需要 "GUI + 实时音频" 的项目里

相关推荐
李航198310 分钟前
给自己的图形引擎,配上了AI渲染,做设计真是太方便了
人工智能·python·计算机视觉·ai·ai编程
L@ncor12 分钟前
第六章 框架开发实践 · 学习笔记(AutoGen / AgentScope / CAMEL / LangGraph)
python·框架·autogen·langgraph·agentscope
Zhou14113620 分钟前
SpringSecurity_02_授权与高级功能
开发语言·python
码匠许师傅1 小时前
【C++三方组件】cpp-httplib:一个头文件起 HTTP 服务
开发语言·c++·http
摇滚侠2 小时前
《On Java 中文版 基础卷》阅读笔记 对象无处不在 03
java·笔记·python
小小小小钰儿2 小时前
1.5-Python网络编程
开发语言·网络·python·web安全·计算机·网络安全
hiahiahia1232 小时前
实现完整 Tool Dispatcher
开发语言·前端
keke.shengfengpolang2 小时前
2026年金融风控岗技术能力拆解:SQL取数、Python建模、评分卡项目,校招面试到底考什
python