Crates.io | rytm-rs |
lib.rs | rytm-rs |
version | 0.1.1 |
source | src |
created_at | 2023-12-15 12:50:13.301988 |
updated_at | 2023-12-22 23:48:02.882112 |
description | More than safe rust abstractions over rytm-sys, an unofficial SDK for Analog Rytm MKII running firmware 1.70 |
homepage | https://github.com/alisomay/rytm-rs |
repository | https://github.com/alisomay/rytm-rs |
max_upload_size | |
id | 1070967 |
size | 935,289 |
More than safe rust abstractions over rytm-sys, an unofficial SDK for writing software for Analog Rytm running on firmware 1.70.
On top of CC
and NRPN
messages, Rytm also accepts sysex messages which are undocumented and not officially supported by Elektron.
The effort of reverse engineering the sysex format started with libanalogrytm which is a C
library powers parts of rytm-rs
through rytm-sys
bindings.
libanalogrytm though a great foundation, is not accessible to many developers due to its low level nature and also lacks high level abstractions for common tasks. The scope of the libanalogrytm is to provide the necessary types for the encoded and decoded sysex messages and focus on the low level details of the sysex protocol.
rytm-rs
builds on top of libanalogrytm and provides high level abstractions for common tasks and designed to provide an SDK like experience for developers with ease of use in mind abstracting the low level details completely.
It is thoroughly documented, to get you started right away.
RytmProject
with all the necessary fields and methods to receive manipulate and send the project to the device.Default
implementations.Pattern
, Kit
, Sound
, Settings
and Global
types which covers the entire Rytm project parameters except songs.Trig
struct.JSON
is provided. But that was experimental actually and I don't think it is useful since a serialized project is around 32mb which is too large.The purpose of this crate is to provide a safe and easy to use SDK like experience for developers who would like to write software for Analog Rytm.
The first priority for this crate is to provide an easy to use api for developers who would like to
The crate is not optimized for the best performance or memory. On the other hand the memory footprint is not that big and the performance is good enough since the performance bottleneck is the device itself when it comes to sysex communication.
I believe that Rytm uses a low priority thread for sysex communication in their internal RTOS. If you flood Rytm with sysex messages it will queue the responses and get back to you when it can. This is not an issue for most use cases but it is a nice to know.
rytm-rs
is composed of 3 main layers.
rytm-sys
#[repr(C,packed)]
structs to identically represent the sysex messages in memory keeping the original memory layout of the messages.rytm-sys
bindings. Which is the main hub for reverse engineering.rytm-rs
Internal layer which deals with communicating with rytm-sys
and deals with conversion from/to raw types (#[repr(C,packed)]
structs).
User facing layer which provides high level abstractions for common tasks. Getters, setters etc.
Starting with importing the prelude is a good idea since it brings the necessary traits and types into scope.
Also the midir
library will be used for midi communication with the device in these examples but you can use any midi library you want.
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);
}
}
}
Tests are currently a mess. They're not meant to be run but used as a playground for reverse engineering and testing the library manually.
I'll write some automated integration tests in the future which requires a connection to the device. Which again should be run manually but could test the library in a more automated way.
Contributions are welcome!
I did this as a single individual and phew.. it was a lot of labour of love. Also weeks of tedious reverse engineering and manual testing work has gone into it. So I would be happy to see some contributions.
Also since I'm alone even if I tested the library thoroughly many times there might be some bugs. So if you find any please open an issue. I'd be grateful.
There are also some ideas which may be nice for the community in the future.
CC
and NRPN
messages would be an idea.You can also search for TODO
in the codebase to find some ideas.
For all communication and contributions for this repo the Rust Code of Conduct applies.
This crate is licensed under the MIT license. You can basically do whatever you want with it but I'd be glad if you reach me out if you make good profit from it or use it for major commercial projects.
The people mentioned here are major contributors to the reverse engineering effort and I would like to thank them for their work. This crate would not be possible in this form and time frame without their work.
The maintainer of libanalogrytm and the original author of the reverse engineering effort. He is the one who started the reverse engineering effort and provided the initial C
library which is the foundation of rytm-rs
.
Author of the Collider app which is available for iPad in the app store. Another contributor to the reverse engineering effort.
Author of the STROM app which is available for iPad in the app store. Another contributor to the reverse engineering effort.