2 个版本
0.1.1 | 2023 年 12 月 22 日 |
---|---|
0.1.0 | 2023 年 12 月 15 日 |
#359 在 编码
1MB
22K SLoC
rytm-rs
rytm-rs 在 rytm-sys 的基础上提供了 Rust 的安全抽象,rytm-sys 是用于编写 Analog Rytm 软件的非官方 SDK,该软件运行在 1.70 固件上。
除了 CC 和 NRPN 消息之外,Rytm 还接受 sysex 消息,这些消息未经文档记录,并且没有得到 Elektron 的官方支持。
sysex 格式的逆向工程工作始于 libanalogrytm,这是一个 C 库,通过 rytm-sys 绑定为 rytm-rs 的部分功能提供动力。
libanalogrytm 虽然是一个很好的基础,但由于其低级性质,并且缺乏对常见任务的高级抽象,因此对许多开发者来说并不易于访问。libanalogrytm 的范围是提供编码和解码 sysex 消息所需的必要类型,并关注 sysex 协议的低级细节。
rytm-rs
基于 libanalogrytm 构建,并为常见任务提供高级抽象,旨在为开发者提供类似 SDK 的体验,同时考虑到易用性,并完全抽象了低级细节。
它有详细的文档,可以立即开始使用。
特性
- Rytm 项目的所有结构都完全由一个嵌套结构
RytmProject
表示,其中包含所有必要的字段和方法,以接收、操作和将项目发送到设备。 - 所有获取和设置方法都具有范围和有效性检查,包括关于值范围和有效性的注释。
- Rytm 设备项目的默认值在所有
Default
实现中表示。 - sysex 编码和解码完全抽象。使用单个方法调用更新项目。
- 使用一个方法调用将项目的一部分转换为 sysex,并通过您选择的方式将其发送到设备。
- 为
Pattern
、Kit
、Sound
、Settings
和Global
类型提供了单独的查询类型,涵盖整个Rytm项目参数,除了歌曲。 - 提供了不同的方法来设置、获取、清除参数锁定,这些方法在
Trig
结构体中可用。 - 包括参数锁定的设置器、获取器和清除器,所有34种机器类型都有表示。
- 所有获取器和设置器都使用设备上的实际值范围,而不是在sysex协议中使用内部值范围。
- 提供了将项目序列化和反序列化为
JSON
的功能。但实际上这是实验性的,我认为它并不有用,因为序列化的项目大约32MB,太大了。
目的
此crate的目的是为希望为Analog Rytm编写软件的开发者提供安全且易于使用的SDK类似体验。
此crate的首要任务是向希望开发Analog Rytm软件的开发者提供易于使用的API。
- 开发Analog Rytm的软件产品
- 开发用于艺术目的的定制创意软件
- 探索和实验生成性和算法音乐,但不想处理与设备通信的sysex协议的底层细节。
此crate没有针对最佳性能或内存进行优化。另一方面,内存占用不大,性能足够好,因为性能瓶颈在于设备本身在sysex通信时。
我相信Rytm在他们的内部RTOS中为sysex通信使用了一个低优先级的线程。如果您向Rytm发送大量sysex消息,它会将响应排队,并在可以时再回复您。这在大多数用例中都不是问题,但这是一个值得知道的信息。
层
rytm-rs
由3个主要层组成。
rytm-sys
- 编码/解码sysex消息
- 提供用于在内存中相同地表示sysex消息的
#[repr(C,packed)]
结构体,以保持消息的原始内存布局。 - 通过
rytm-sys
绑定公开来自libanalogrytm的类型。这是逆向工程的主要枢纽。
rytm-rs
处理与rytm-sys
通信的内部层,并处理从/到原始类型(#[repr(C,packed)]
结构体)的转换。
面向用户的层,为常见任务提供高级抽象。获取器、设置器等。
用法
从导入预处理器开始是一个好主意,因为它将必要的特性和类型引入作用域。
此外,这些示例中将使用midir
库来与设备进行midi通信,但您可以使用任何midi库。
use std::sync::{Arc, Mutex};
use midir::{Ignore, MidiInputConnection, MidiOutputConnection};
use rytm_rs::prelude::*;
// We'll be using this connection for sending sysex messages to the device.
//
// Using an Arc<Mutex<MidiOutputConnection>> is a good idea since you can share the connection between threads.
// Which will be common in this context.
fn get_connection_to_rytm() -> Arc<Mutex<MidiOutputConnection>> {
let output = port::MidiOut::new("rytm_test_out").unwrap();
let rytm_out_identifier = "Elektron Analog Rytm MKII";
let rytm_output_port = output.find_output_port(rytm_out_identifier).unwrap();
Arc::new(Mutex::new(
output.make_output_connection(&rytm_output_port, 0).unwrap(),
))
}
// We'll be using this connection for receiving sysex messages from the device and forwarding them to our main thread.
pub fn make_input_message_forwarder() -> (
MidiInputConnection<()>,
std::sync::mpsc::Receiver<(Vec<u8>, u64)>,
) {
let mut input = crate::port::MidiIn::new("rytm_test_in").unwrap();
input.ignore(Ignore::None);
let rytm_in_identifier = "Elektron Analog Rytm MKII";
let rytm_input_port = input.find_input_port(rytm_in_identifier).unwrap();
let (tx, rx) = std::sync::mpsc::channel::<(Vec<u8>, u64)>();
let conn_in: midir::MidiInputConnection<()> = input
.into_inner()
.connect(
&rytm_input_port,
"rytm_test_in",
move |stamp, message, _| {
// Do some filtering here if you like.
tx.send((message.to_vec(), stamp)).unwrap();
},
(),
)
.unwrap();
(conn_in, rx)
}
fn main() {
// Make a default rytm project
let mut rytm = RytmProject::default();
// Get a connection to the device
let conn_out = get_connection_to_rytm();
// Listen for incoming messages from the device
let (_conn_in, rx) = make_input_message_forwarder();
// Make a query for the pattern in the work buffer
let query = PatternQuery::new_targeting_work_buffer();
// Send the query to the device
conn_out
.lock()
.unwrap()
.send(&query.as_sysex().unwrap())
.unwrap();
// Wait for the response
match rx.recv() {
Ok((message, _stamp)) => {
match rytm.update_from_sysex_response(&message) {
Ok(_) => {
for track in rytm.work_buffer_mut().pattern_mut().tracks_mut() {
// Set the number of steps to 64
track.set_number_of_steps(64).unwrap();
for (i, trig) in track.trigs_mut().iter_mut().enumerate() {
// Enable every 4th trig.
// Set retrig on.
if i % 4 == 0 {
trig.set_trig_enable(true);
trig.set_retrig(true);
}
}
}
// Send the updated pattern to the device if you like
conn_out
.lock()
.unwrap()
.send(&rytm.work_buffer().pattern().as_sysex().unwrap())
.unwrap();
}
Err(err) => {
println!("Error: {:?}", err);
}
}
}
Err(err) => {
println!("Error: {:?}", err);
}
}
}
测试
测试目前一团糟。它们不是为了运行而设计的,而是作为逆向工程和手动测试库的游乐场。
我将编写一些自动集成测试,这需要连接到设备。这同样需要手动运行,但可以以更自动化的方式测试库。
贡献
欢迎贡献!
我独自一人完成了这项工作,真是不容易。这中间包含了许多劳动和爱。还有数周单调的逆向工程和手动测试工作。所以我很乐意看到一些贡献。
由于我独自一人,即使我已经彻底测试了库多次,可能仍然存在一些错误。所以如果你发现任何问题,请提出一个issue。我会非常感激。
还有一些想法,可能会在未来对社区很有用。
- 人们对Rytm的Max/MSP外部扩展感到非常兴奋。你可以在本crate的基础上构建这个外部扩展。查看median。
- Neon绑定可能非常有用,这样人们就可以使用Node for Max轻松地在crate上构建Max补丁或Live设备。
- 将crate扩展以支持轻松与
CC
和NRPN
消息接口的想法。
你还可以在代码库中搜索TODO
以找到一些想法。
对于此repo的所有通信和贡献,均适用Rust行为准则。
许可
此crate采用MIT许可。你可以基本上用它做任何你想做的事,但如果你能从中获得丰厚的利润或用于重大商业项目,我很乐意你联系我。
备注
这里提到的人是逆向工程工作的主要贡献者,我要感谢他们的工作。没有他们的工作,这个crate不可能以这种形式和在这个时间框架内完成。
bsp2
libanalogrytm的维护者以及逆向工程工作的原始作者。他是开始逆向工程工作并提供初始C
库的人,这是rytm-rs
的基础。
mekohler
Collider应用的作者,可在App Store中的iPad应用中找到。逆向工程工作的另一位贡献者。
- https://marcoskohler.com/
- https://github.com/mekohler
- https://www.elektronauts.com/u/mekohler/summary
void
STROM应用的作者,可在App Store中的iPad应用中找到。逆向工程工作的另一位贡献者。
依赖
~1.2–4MB
~80K SLoC