rytm_rs/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
2#![allow(
3    clippy::module_name_repetitions,
4    clippy::wildcard_imports,
5    clippy::similar_names
6)]
7// TODO: Re-check later.
8#![allow(clippy::must_use_candidate, clippy::unsafe_derive_deserialize)]
9#![doc(
10    html_logo_url = "https://raw.githubusercontent.com/alisomay/rytm-rs/main/assets/logo.png",
11    html_favicon_url = "https://raw.githubusercontent.com/alisomay/rytm-rs/main/assets/favicon/favicon.ico"
12)]
13
14//! # rytm-rs
15//!
16//! More than safe rust abstractions over [rytm-sys](https://github.com/alisomay/rytm-sys), an unofficial SDK for writing software for Analog Rytm running on firmware 1.70.
17//!
18//! On top of `CC` and `NRPN` messages, Rytm also accepts sysex messages which are undocumented and not officially supported by Elektron.
19//!
20//! The effort of reverse engineering the sysex format started with [libanalogrytm](https://github.com/bsp2/libanalogrytm) which is a `C` library powers parts of `rytm-rs` through `rytm-sys` bindings.
21//!
22//! [libanalogrytm](https://github.com/bsp2/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](https://github.com/bsp2/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.
23//!
24//! `rytm-rs` builds on top of [libanalogrytm](https://github.com/bsp2/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.
25//!
26//! ## Features
27//!
28//! - All structures in a Rytm project is completely represented with [`RytmProject`] including all the necessary fields and methods to receive manipulate and send the project to the device.
29//! - All getter and setter methods have range and validity checks including comments about the range and validity of the values.
30//! - The Rytm device project defaults are represented in all the struct `Default` implementations.
31//! - Sysex encoding and decoding is completely abstracted away. Update the project with a single method call.
32//! - Convert parts of the project to sysex with one method call and send it to the device with your choice of transport.
33//! - Separate query types provided for [`Pattern`](crate::object::Pattern), [`Kit`](crate::object::Kit), [`Sound`](crate::object::Sound), [`Settings`](crate::object::Settings) and [`Global`](crate::object::Global) types which covers the entire Rytm project parameters except songs.
34//! - Different methods provided for setting, getting, clearing parameter locks exhaustively and available in [`Trig`](crate::object::pattern::track::trig::Trig) struct.
35//! - All 34 machine types are represented including parameter lock setters getters and clearers.
36//! - All getters and setters use the actual range of values on the device not the internal ranges which are used in the sysex protocol.
37//!
38//! ## Purpose
39//!
40//! 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.
41//!
42//! The first priority for this crate is to provide an easy to use api for developers who would like to
43//!
44//! - Develop a software products for Analog Rytm
45//! - Develop custom creative software for artistic purposes
46//! - Discover and experiment with generative and algorithmic music but don't want to deal with the low level details of the sysex protocol communicating with the device.
47//!
48//! 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.
49//!
50//! 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.
51//!
52//! ## Layers
53//!
54//! `rytm-rs` is composed of 3 main layers.
55//!
56//! ### [`rytm-sys`](https://docs.rs/rytm-sys/latest/rytm_sys/#)
57//!
58//! - Encoding/decoding sysex messages
59//! - Providing `#[repr(C,packed)]` structs to identically represent the sysex messages in memory keeping the original memory layout of the messages.
60//! - Exposing types from [libanalogrytm](https://github.com/bsp2/libanalogrytm) through `rytm-sys` bindings. Which is the main hub for reverse engineering.
61//!
62//! ### `rytm-rs`
63//!
64//! Internal layer which deals with communicating with `rytm-sys` and deals with conversion from/to raw types (`#[repr(C,packed)]` structs).
65//!
66//! User facing layer which provides high level abstractions for common tasks. Getters, setters etc.
67//!
68//!
69//! ## Usage
70//!
71//! Starting with importing the prelude is a good idea since it brings the necessary traits and types into scope.
72//!
73//! Also the [`midir`](https://github.com/Boddlnagg/midir) library will be used for midi communication with the device in these examples but you can use any midi library you want.
74//!
75//! ```
76//! use std::sync::Arc; use parking_lot::Mutex;
77//! use midir::{Ignore, MidiInputConnection, MidiOutputConnection};
78//! use rytm_rs::prelude::*;
79//!
80//! // We'll be using this connection for sending sysex messages to the device.
81//! //
82//! // Using an Arc<Mutex<MidiOutputConnection>> is a good idea since you can share the connection between threads.
83//! // Which will be common in this context.
84//! fn get_connection_to_rytm() -> Arc<Mutex<MidiOutputConnection>> {
85//!     let output = port::MidiOut::new("rytm_test_out").unwrap();
86//!     let rytm_out_identifier = "Elektron Analog Rytm MKII";
87//!     let rytm_output_port = output.find_output_port(rytm_out_identifier).unwrap();
88//!
89//!     Arc::new(Mutex::new(
90//!         output.make_output_connection(&rytm_output_port, 0).unwrap(),
91//!     ))
92//! }
93//!
94//! // We'll be using this connection for receiving sysex messages from the device and forwarding them to our main thread.
95//! pub fn make_input_message_forwarder() -> (
96//!     MidiInputConnection<()>,
97//!     std::sync::mpsc::Receiver<(Vec<u8>, u64)>,
98//! ) {
99//!     let mut input = crate::port::MidiIn::new("rytm_test_in").unwrap();
100//!     input.ignore(Ignore::None);
101//!     let rytm_in_identifier = "Elektron Analog Rytm MKII";
102//!     let rytm_input_port = input.find_input_port(rytm_in_identifier).unwrap();
103//!
104//!     let (tx, rx) = std::sync::mpsc::channel::<(Vec<u8>, u64)>();
105//!
106//!     let conn_in: midir::MidiInputConnection<()> = input
107//!         .into_inner()
108//!         .connect(
109//!             &rytm_input_port,
110//!             "rytm_test_in",
111//!             move |stamp, message, _| {
112//!                 // Do some filtering here if you like.
113//!                 tx.send((message.to_vec(), stamp)).unwrap();
114//!             },
115//!             (),
116//!         )
117//!         .unwrap();
118//!
119//!     (conn_in, rx)
120//! }
121//!
122//! fn main() {
123//!     // Make a default rytm project
124//!     let mut rytm = RytmProject::default();
125//!
126//!     // Get a connection to the device
127//!     let conn_out = get_connection_to_rytm();
128//!
129//!     // Listen for incoming messages from the device
130//!     let (_conn_in, rx) = make_input_message_forwarder();
131//!
132//!     // Make a query for the pattern in the work buffer
133//!     let query = PatternQuery::new_targeting_work_buffer();
134//!
135//!     // Send the query to the device
136//!     conn_out
137//!         .lock()
138//!         .unwrap()
139//!         .send(&query.as_sysex().unwrap())
140//!         .unwrap();
141//!
142//!     // Wait for the response
143//!     match rx.recv() {
144//!         Ok((message, _stamp)) => {
145//!             match rytm.update_from_sysex_response(&message) {
146//!                 Ok(_) => {
147//!                     for track in rytm.work_buffer_mut().pattern_mut().tracks_mut() {
148//!                         // Set the number of steps to 64
149//!                         track.set_number_of_steps(64).unwrap();
150//!                         for (i, trig) in track.trigs_mut().iter_mut().enumerate() {
151//!                             // Enable every 4th trig.
152//!                             // Set retrig on.
153//!                             if i % 4 == 0 {
154//!                                 trig.set_trig_enable(true);
155//!                                 trig.set_retrig(true);
156//!                             }
157//!                         }
158//!                     }
159//!
160//!                     // Send the updated pattern to the device if you like
161//!                     conn_out
162//!                         .lock()
163//!                         .unwrap()
164//!                         .send(&rytm.work_buffer().pattern().as_sysex().unwrap())
165//!                         .unwrap();
166//!                 }
167//!                 Err(err) => {
168//!                     println!("Error: {:?}", err);
169//!                 }
170//!             }
171//!         }
172//!         Err(err) => {
173//!             println!("Error: {:?}", err);
174//!         }
175//!     }
176//! }
177//! ```
178//!
179//! ## Remarks
180//!
181//! The people mentioned here are major contributors to the reverse engineering effort and I would like to thank them for their work.
182//! This crate would not be possible in this form and time frame without their work.
183//!
184//! ### bsp2
185//!
186//! The maintainer of [libanalogrytm](https://github.com/bsp2/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`.
187//!
188//! - <https://github.com/bsp2>
189//!
190//! ### mekohler
191//!
192//! Author of the [Collider](https://www.elektronauts.com/t/collider-for-the-ipad/27479) app which is available for iPad in the app store.
193//! Another contributor to the reverse engineering effort.
194//!
195//! - <https://marcoskohler.com/>
196//! - <https://github.com/mekohler>
197//! - <https://www.elektronauts.com/u/mekohler/summary>
198//!
199//! ### void
200//!
201//! Author of the [STROM](https://apps.apple.com/us/app/strom/id907044543) app which is available for iPad in the app store.
202//! Another contributor to the reverse engineering effort.
203//!
204//! - <https://www.elektronauts.com/u/void/summary>
205//! - <https://soundcloud.com/jakob-penca>
206//!
207//!
208//! Many thanks to [Başak Ünal](https://basakunal.design) for the logo.
209
210pub(crate) mod defaults;
211pub mod error;
212pub mod object;
213pub mod prelude;
214pub mod query;
215pub(crate) mod sysex;
216pub(crate) mod util;
217
218use self::error::RytmError;
219use crate::error::ParameterError;
220use defaults::*;
221use error::SysexConversionError;
222use object::{
223    global::Global,
224    kit::Kit,
225    pattern::Pattern,
226    settings::Settings,
227    sound::{Sound, SoundType},
228};
229use rytm_sys::{ar_global_t, ar_kit_t, ar_pattern_t, ar_settings_t, ar_sound_t};
230use serde::{Deserialize, Serialize};
231use sysex::decode_sysex_response_to_raw;
232pub use sysex::{AnySysexType, SysexCompatible, SysexType};
233
234/// [`RytmProject`] represents the state of the analog rytm.
235///
236/// It contains all structures scoped to an Analog Rytm MKII FW 1.70 project.
237#[derive(Clone, Debug, Serialize, Deserialize)]
238pub struct RytmProject {
239    work_buffer: RytmProjectWorkBuffer,
240    patterns: Vec<Pattern>,
241    pool_sounds: Vec<Sound>,
242    kits: Vec<Kit>,
243    globals: Vec<Global>,
244    // TODO: Songs (16)
245    settings: Settings,
246
247    pub(crate) last_queried_pattern_index: Option<usize>,
248    pub(crate) last_queried_kit_index: Option<usize>,
249    pub(crate) last_queried_work_buffer_pattern_index: Option<usize>,
250    pub(crate) last_queried_work_buffer_kit_index: Option<usize>,
251}
252
253impl RytmProject {
254    /// Try to construct a project from a JSON string.
255    ///
256    /// # Errors
257    /// - Project or JSON might be corrupted.
258    pub fn try_from_str(project_content: &str) -> Result<Self, RytmError> {
259        let project = serde_json::from_str(project_content)?;
260        Ok(project)
261    }
262
263    /// Try to convert the project to a JSON string.
264    ///
265    /// # Errors
266    /// - Project or JSON might be corrupted.
267    pub fn try_to_string(&self) -> Result<String, RytmError> {
268        let project_content = serde_json::to_string(self)?;
269        Ok(project_content)
270    }
271
272    pub fn set_device_id(&mut self, device_id: u8) {
273        // wb
274        self.work_buffer_mut()
275            .pattern_mut()
276            .set_device_id(device_id);
277        self.work_buffer_mut().kit_mut().set_device_id(device_id);
278        for sound in self.work_buffer_mut().sounds_mut().iter_mut() {
279            sound.set_device_id(device_id);
280        }
281        self.work_buffer_mut().global_mut().set_device_id(device_id);
282
283        // Normal
284        for pattern in self.patterns_mut().iter_mut() {
285            pattern.set_device_id(device_id);
286        }
287
288        for kit in self.kits_mut().iter_mut() {
289            kit.set_device_id(device_id);
290        }
291
292        for sound in self.pool_sounds_mut().iter_mut() {
293            sound.set_device_id(device_id);
294        }
295
296        for global in self.globals_mut().iter_mut() {
297            global.set_device_id(device_id);
298        }
299
300        self.settings_mut().set_device_id(device_id);
301    }
302
303    #[allow(clippy::missing_errors_doc)]
304    pub fn try_default() -> Result<Self, RytmError> {
305        Self::try_default_with_device_id(0)
306    }
307
308    #[allow(clippy::missing_errors_doc)]
309    pub fn try_default_with_device_id(device_id: u8) -> Result<Self, RytmError> {
310        let mut patterns = Vec::with_capacity(PATTERN_MAX_COUNT);
311        patterns.reserve_exact(PATTERN_MAX_COUNT);
312        let mut kits = Vec::with_capacity(KIT_MAX_COUNT);
313        kits.reserve_exact(KIT_MAX_COUNT);
314
315        // PATTERN_MAX_COUNT == KIT_MAX_COUNT is true.
316        for i in 0..PATTERN_MAX_COUNT {
317            let pattern = Pattern::try_default_with_device_id(i, device_id)?;
318            let mut kit = Kit::try_default_with_device_id(i, device_id)?;
319            kit.link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
320            patterns.push(pattern);
321            kits.push(kit);
322        }
323
324        // TODO: ALSO FOR THE TRACK AND TRIG TYPES!
325
326        Ok(Self {
327            work_buffer: RytmProjectWorkBuffer::try_default_with_device_id(device_id)?,
328            patterns,
329            pool_sounds: default_pool_sounds_with_device_id(device_id),
330            kits,
331            globals: default_globals_with_device_id(device_id),
332            settings: Settings::try_default_with_device_id(device_id)?,
333
334            last_queried_pattern_index: None,
335            last_queried_kit_index: None,
336            last_queried_work_buffer_pattern_index: None,
337            last_queried_work_buffer_kit_index: None,
338        })
339    }
340
341    #[allow(clippy::too_many_lines)]
342    /// Updates the Rytm struct from a sysex response.
343    ///
344    /// All encoding/decoding is done in [`RytmProject`], so this is the only method that needs to be called to update the struct when a sysex response is received.
345    ///
346    /// # Important
347    ///
348    /// This method will act as a no-op if the given slice does not contain a valid sysex message.
349    /// The check is performed by checking the first and last byte of the slice.
350    /// This behaviour is preferred to returning an error, as it is expected to be called in a midi callback which receives not just sysex messages.
351    ///
352    /// # Errors
353    ///
354    /// This method will return an error
355    ///
356    /// - If the sysex message is invalid in the context of Rytm
357    /// - If the sysex message is valid, but the object type is not supported or implemented yet. Example: [`crate::error::RytmError::SysexConversionError::Unimplemented`] variant.
358    /// - If the sysex message is incomplete, this sometimes happens in the initial parts of the transmission and is a behaviour of Rytm. You may check for the error [`crate::error::RytmError::SysexConversionError::ShortRead`] and ignore it.
359    /// - If the sysex message is valid, but the size of the expected object does not match the size of the received object. This may happen if the firmware version of Rytm is different than the one this library supports which is currently FW 1.70 only. Never happened to me in practice but a cut transmission may also cause this in theory.
360    /// - All other  [`crate::error::RytmError::SysexConversionError`] variants are possible which are inherited from [libanalogrytm](https://github.com/bsp2/libanalogrytm).
361    pub fn update_from_sysex_response(&mut self, response: &[u8]) -> Result<(), RytmError> {
362        if response.len() < 2 {
363            return Err(SysexConversionError::ShortRead.into());
364        }
365
366        // Ignore non-sysex messages.
367        if !(response[0] == 0xF0 && response[response.len() - 1] == 0xF7) {
368            return Ok(());
369        }
370
371        // Raw buffer and metadata about the sysex message.
372        let (mut raw, meta) = decode_sysex_response_to_raw(response)?;
373
374        // `& 0b0111_1111` is to get rid of the bit that indicates if the object is in the work buffer or not.
375        // `- 0x80` is to get the actual index of the object in the work buffer. Example, there could be 12 sounds in the work buffer.
376        match meta.object_type()? {
377            SysexType::Pattern => {
378                let raw_pattern: &ar_pattern_t =
379                    unsafe { &*(raw.as_mut_ptr() as *const ar_pattern_t) };
380                let mut pattern = Pattern::try_from_raw(meta, raw_pattern)?;
381
382                // Once a pattern is retrieved it's parameter lock pool will be linked to the kit we assume that it uses.
383                // This doesn't mean the kit itself is updated, it can be as well out of sync from the device but it's the best we can do.
384                // It is the responsibility of the user to update the kit if it is out of sync.
385
386                let kit_number = pattern.kit_number();
387
388                if kit_number == 0xFF {
389                    // When the kit is not set, we assume that the pattern uses the kit in the work buffer.
390                    let work_buffer_pattern_kit_number = self.work_buffer().pattern().kit_number();
391
392                    // We update the kit which has than number with the parameter lock pool of the pattern.
393                    self.kits_mut()[work_buffer_pattern_kit_number]
394                        .link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
395
396                    // We also update the work buffer kit if it is the same kit.
397                    let work_buffer_kit_mut = self.work_buffer_mut().kit_mut();
398                    if work_buffer_kit_mut.index() == work_buffer_pattern_kit_number {
399                        work_buffer_kit_mut
400                            .link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
401                    }
402                } else {
403                    // When the kit is set then we directly update that kit.
404                    self.kits_mut()[kit_number]
405                        .link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
406
407                    // We also update the work buffer kit if it is the same kit.
408                    let work_buffer_kit_mut = self.work_buffer_mut().kit_mut();
409                    if work_buffer_kit_mut.index() == kit_number {
410                        work_buffer_kit_mut
411                            .link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
412                    }
413                }
414
415                if meta.is_targeting_work_buffer() {
416                    let index = meta.get_normalized_object_index();
417                    pattern.index = index;
418                    self.work_buffer.pattern = pattern;
419
420                    return Ok(());
421                }
422
423                let index = meta.get_normalized_object_index();
424                self.patterns[index] = pattern;
425                Ok(())
426            }
427
428            SysexType::Kit => {
429                let raw_kit: &ar_kit_t = unsafe { &*(raw.as_mut_ptr() as *const ar_kit_t) };
430                let mut kit = Kit::try_from_raw(meta, raw_kit)?;
431
432                // When a kit is received, we check all the existing patterns to see if any of them is linked to the kit index queried or not.
433                // Then we link the plock pool of those patterns to the updated kit.
434
435                for pattern in self.patterns() {
436                    if pattern.kit_number() == kit.index() {
437                        kit.link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
438                    }
439                }
440
441                if self.work_buffer().pattern().kit_number() == kit.index() {
442                    kit.link_parameter_lock_pool(
443                        &self.work_buffer().pattern().parameter_lock_pool,
444                    )?;
445                }
446
447                if meta.is_targeting_work_buffer() {
448                    let index = meta.get_normalized_object_index();
449                    kit.index = index;
450                    self.work_buffer.kit = kit;
451                    return Ok(());
452                }
453
454                let index = meta.get_normalized_object_index();
455                self.kits[index] = kit;
456                Ok(())
457            }
458
459            SysexType::Sound => {
460                let raw_sound: &ar_sound_t = unsafe { &*(raw.as_mut_ptr() as *const ar_sound_t) };
461                let mut sound = Sound::try_from_raw(meta, raw_sound, None)?;
462
463                let index = meta.get_normalized_object_index();
464                match sound.sound_type() {
465                    SoundType::Pool => {
466                        // Pool sounds will not have a linked parameter lock pool.
467                        // TODO: Would we need linking here?
468                        self.pool_sounds[index] = sound;
469                    }
470                    SoundType::WorkBuffer => {
471                        // Work buffer sounds though will be linked to the work buffer pattern's parameter lock pool.
472                        sound.link_parameter_lock_pool(
473                            &self.work_buffer().pattern().parameter_lock_pool,
474                        )?;
475                        self.work_buffer.sounds[index] = sound;
476                    }
477                    SoundType::KitQuery => {
478                        unreachable!("A response from a sound query can not contain a sound which is part of a kit.")
479                    }
480                };
481
482                Ok(())
483            }
484
485            SysexType::Global => {
486                let raw_global: &ar_global_t =
487                    unsafe { &*(raw.as_mut_ptr() as *const ar_global_t) };
488                let global = Global::try_from_raw(meta, raw_global)?;
489
490                if meta.is_targeting_work_buffer() {
491                    self.work_buffer.global = global;
492                    return Ok(());
493                }
494
495                let index = (meta.obj_nr & 0b0111_1111) as usize;
496                self.globals[index] = global;
497                Ok(())
498            }
499
500            // There is only a single settings object for a project.
501            SysexType::Settings => {
502                let raw_settings: &ar_settings_t =
503                    unsafe { &*(raw.as_mut_ptr() as *const ar_settings_t) };
504                let settings = Settings::try_from_raw(meta, raw_settings)?;
505                self.settings = settings;
506                Ok(())
507            }
508
509            // TODO: Implement Song
510            SysexType::Song => Err(SysexConversionError::Unimplemented("Song".to_owned()).into()),
511        }
512    }
513
514    /// Get all patterns.
515    ///
516    /// Total of 128 patterns.
517    pub fn patterns(&self) -> &[Pattern] {
518        &self.patterns
519    }
520
521    /// Get all kits.
522    ///
523    /// Total of 128 kits.
524    pub fn kits(&self) -> &[Kit] {
525        &self.kits
526    }
527
528    /// Get all sounds in the pool.
529    ///
530    /// Total of 128 sounds.
531    pub fn pool_sounds(&self) -> &[Sound] {
532        &self.pool_sounds
533    }
534
535    /// Get all global slots.
536    ///
537    /// Total of 4 global slots.
538    pub fn globals(&self) -> &[Global] {
539        &self.globals
540    }
541
542    /// Get the settings.
543    pub const fn settings(&self) -> &Settings {
544        &self.settings
545    }
546
547    /// Get all patterns mutably.
548    ///
549    /// Total of 128 patterns.
550    pub fn patterns_mut(&mut self) -> &mut [Pattern] {
551        &mut self.patterns
552    }
553
554    /// Get all kits mutably.
555    ///
556    /// Total of 128 kits.
557    pub fn kits_mut(&mut self) -> &mut [Kit] {
558        &mut self.kits
559    }
560
561    /// Get all sounds in the pool mutably.
562    ///
563    /// Total of 128 sounds.
564    pub fn pool_sounds_mut(&mut self) -> &mut [Sound] {
565        &mut self.pool_sounds
566    }
567
568    /// Get all global slots mutably.
569    ///
570    /// Total of 4 global slots.
571    pub fn globals_mut(&mut self) -> &mut [Global] {
572        &mut self.globals
573    }
574
575    /// Get the settings mutably.
576    pub fn settings_mut(&mut self) -> &mut Settings {
577        &mut self.settings
578    }
579
580    /// Get the work buffer structures.
581    pub const fn work_buffer(&self) -> &RytmProjectWorkBuffer {
582        &self.work_buffer
583    }
584
585    /// Get the work buffer structures mutably.
586    pub fn work_buffer_mut(&mut self) -> &mut RytmProjectWorkBuffer {
587        &mut self.work_buffer
588    }
589}
590
591/// [`RytmProjectWorkBuffer`] represents the state of the analog rytm work buffer.
592///
593/// It contains all structures scoped to an Analog Rytm MKII FW 1.70 project work buffer.
594#[derive(Clone, Debug, Serialize, Deserialize)]
595pub struct RytmProjectWorkBuffer {
596    pattern: Pattern,
597    kit: Kit,
598    sounds: Vec<Sound>,
599    global: Global,
600    // TODO: Work buffer song
601}
602
603impl RytmProjectWorkBuffer {
604    #[allow(clippy::missing_errors_doc)]
605    pub fn try_default() -> Result<Self, RytmError> {
606        Self::try_default_with_device_id(0)
607    }
608
609    #[allow(clippy::missing_errors_doc)]
610    pub fn try_default_with_device_id(device_id: u8) -> Result<Self, RytmError> {
611        let pattern = Pattern::work_buffer_default_with_device_id(device_id);
612        let mut kit = Kit::work_buffer_default_with_device_id(device_id);
613        kit.link_parameter_lock_pool(&pattern.parameter_lock_pool)?;
614
615        // TODO: What are work buffer sounds.. hmm..
616        // Should we link their parameter lock pool also?
617
618        Ok(Self {
619            pattern,
620            kit,
621            sounds: default_work_buffer_sounds_with_device_id(device_id),
622            global: Global::work_buffer_default_with_device_id(device_id),
623        })
624    }
625
626    /// Get the pattern in the work buffer.
627    pub const fn pattern(&self) -> &Pattern {
628        &self.pattern
629    }
630
631    /// Get the kit in the work buffer.
632    pub const fn kit(&self) -> &Kit {
633        &self.kit
634    }
635
636    /// Get the sounds in the work buffer.
637    ///
638    /// Total of 12 sounds for 12 tracks.
639    pub fn sounds(&self) -> &[Sound] {
640        &self.sounds
641    }
642
643    /// Get the global in the work buffer.
644    pub const fn global(&self) -> &Global {
645        &self.global
646    }
647
648    /// Get the pattern in the work buffer mutably.
649    pub fn pattern_mut(&mut self) -> &mut Pattern {
650        &mut self.pattern
651    }
652
653    /// Get the kit in the work buffer mutably.
654    pub fn kit_mut(&mut self) -> &mut Kit {
655        &mut self.kit
656    }
657
658    /// Get the sounds in the work buffer mutably.
659    ///
660    /// Total of 12 sounds for 12 tracks.
661    pub fn sounds_mut(&mut self) -> &mut [Sound] {
662        &mut self.sounds
663    }
664
665    /// Get the global in the work buffer mutably.
666    pub fn global_mut(&mut self) -> &mut Global {
667        &mut self.global
668    }
669}