rytm_rs/object/
sound.rs

1pub(crate) mod de;
2/// All structures related to machines and their parameters.
3pub mod machine;
4/// Holds the page settings of the sound. Like `[AMP]`, `[FLT]`, `[LFO]`, `[SAMP]` on the device.
5pub mod page;
6/// Holds the structures which represent the settings of the sound.
7pub mod settings;
8/// Types which are relevant to sounds.
9pub mod types;
10pub(crate) mod unknown;
11
12use self::{
13    machine::MachineParameters,
14    page::{Amplitude, Filter, Lfo, Sample},
15    settings::SoundSettings,
16    types::MachineType,
17    unknown::SoundUnknown,
18};
19use super::pattern::plock::ParameterLockPool;
20use crate::{
21    error::{RytmError, SysexConversionError},
22    impl_sysex_compatible,
23    object::types::ObjectName,
24    sysex::{SysexCompatible, SysexMeta, SysexType, SOUND_SYSEX_SIZE},
25    util::{arc_mutex_owner, assemble_u32_from_u8_array_be},
26    AnySysexType, ParameterError,
27};
28use derivative::Derivative;
29use parking_lot::Mutex;
30use rytm_rs_macro::parameter_range;
31use rytm_sys::{ar_sound_raw_to_syx, ar_sound_t, ar_sysex_meta_t};
32use serde::{Deserialize, Serialize};
33use std::sync::Arc;
34
35/// An enum to understand where the sound is coming from.
36///
37/// The sound can be a pool sound, the work buffer or as a part of a kit.
38#[derive(
39    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
40)]
41pub enum SoundType {
42    Pool,
43    #[default]
44    WorkBuffer,
45    KitQuery,
46}
47
48impl_sysex_compatible!(
49    Sound,
50    ar_sound_t,
51    ar_sound_raw_to_syx,
52    SysexType::Sound,
53    SOUND_SYSEX_SIZE
54);
55
56/// Represents a sound in the analog rytm.
57///
58/// This structure does not map identically to the relevant structure in the firmware.
59#[derive(Derivative, Clone, Serialize)]
60#[derivative(Debug)]
61pub struct Sound {
62    #[derivative(Debug = "ignore")]
63    sysex_meta: SysexMeta,
64    /// Version of the sound structure.
65    version: u32,
66
67    /// Index of the sound.
68    ///
69    /// This can mean various things depending on the context
70    ///
71    /// - If this sound is retrieved from the sound pool, this is the index of the sound in the pool.
72    /// - If this sound is retrieved from a track from the work buffer or a kit query, this is the index of the track.
73    index: usize,
74    /// Index of the sound if it was retrieved from the sound pool.
75    pool_index: Option<usize>,
76    /// Kit number if this sound is retrieved from a kit query
77    kit_number: Option<usize>,
78    /// Index of the sound if it was retrieved from a track from the work buffer.
79    assigned_track: Option<usize>,
80
81    /// Name of the sound.
82    name: ObjectName,
83
84    machine_parameters: MachineParameters,
85
86    sample: Sample,
87    filter: Filter,
88    amplitude: Amplitude,
89    lfo: Lfo,
90    settings: SoundSettings,
91
92    accent_level: u8,
93    // TODO: Understand what this corresponds to.
94    // Currently not implementing it.
95    def_note: u8,
96
97    #[derivative(Debug = "ignore")]
98    __unknown: SoundUnknown,
99
100    #[derivative(Debug = "ignore")]
101    #[serde(serialize_with = "arc_mutex_owner::opt_serialize")]
102    pub(crate) parameter_lock_pool: Option<Arc<Mutex<ParameterLockPool>>>,
103}
104
105impl From<&Sound> for ar_sound_t {
106    fn from(sound: &Sound) -> Self {
107        let mut raw_sound = Self {
108            name: sound.name.copy_inner(),
109            accent_level: sound.accent_level,
110            def_note: sound.def_note,
111            ..Default::default()
112        };
113
114        sound.sample.apply_to_raw_sound(&mut raw_sound);
115        sound.filter.apply_to_raw_sound(&mut raw_sound);
116        sound.amplitude.apply_to_raw_sound(&mut raw_sound);
117        sound.lfo.apply_to_raw_sound(&mut raw_sound);
118        sound.settings.apply_to_raw_sound(&mut raw_sound);
119        sound.machine_parameters.apply_to_raw_sound(&mut raw_sound);
120
121        sound.__unknown.apply_to_raw_sound(&mut raw_sound);
122
123        raw_sound
124    }
125}
126
127impl Sound {
128    // This can never fail.
129    #[allow(clippy::missing_panics_doc)]
130    /// Links a pattern's parameter lock pool to this sound.
131    ///
132    /// This way, one can set parameter locks for trigs for the machine assigned to this sound.
133    ///
134    /// # Errors
135    ///
136    /// Sound must be a track sound. This is necessary because the pattern's parameter lock pool
137    /// belongs to a pattern but sounds are not. Sounds are received with different query compared to patterns.
138    ///
139    /// Calling this method on a pool sound will result in an error.
140    pub fn link_parameter_lock_pool(
141        &mut self,
142        parameter_lock_pool: &Arc<Mutex<ParameterLockPool>>,
143    ) -> Result<(), RytmError> {
144        let compatibility_error =  ParameterError::Compatibility {
145                value: "ParameterLockPool".into(),
146                parameter_name: "parameter_lock_pool".into(),
147                reason: Some("The sound you're trying to link the parameter lock pool is a pool sound. Pool sounds cannot have parameter locks.".into()),
148        };
149        if self.is_pool_sound() {
150            return Err(compatibility_error.into());
151        }
152        self.parameter_lock_pool = Some(Arc::clone(parameter_lock_pool));
153        let parameter_lock_pool_ref = Arc::clone(
154            self.parameter_lock_pool
155                .as_ref()
156                .ok_or(compatibility_error)?,
157        );
158
159        self.machine_parameters
160            .link_parameter_lock_pool(parameter_lock_pool_ref);
161        Ok(())
162    }
163
164    // The panics in this function should be basically unreachable when this function is used correctly.
165    pub(crate) fn try_from_raw(
166        sysex_meta: SysexMeta,
167        raw_sound: &ar_sound_t,
168        kit_number_and_assigned_track: Option<(usize, usize)>,
169    ) -> Result<Self, RytmError> {
170        let mut index: usize = 0;
171        let mut assigned_track = None;
172        let mut kit_number = None;
173        let mut pool_index = None;
174
175        match sysex_meta.object_type()? {
176            SysexType::Sound => {
177                if sysex_meta.is_targeting_work_buffer() {
178                    index = (sysex_meta.obj_nr & 0b0111_1111) as usize;
179                    assigned_track = Some(index);
180                }
181
182                if let Some((kit_n, assigned_t)) = kit_number_and_assigned_track {
183                    index = assigned_t;
184                    assigned_track = Some(assigned_t);
185                    kit_number = Some(kit_n);
186                }
187
188                if kit_number_and_assigned_track.is_none() && !sysex_meta.is_targeting_work_buffer()
189                {
190                    index = (sysex_meta.obj_nr & 0b0111_1111) as usize;
191                    pool_index = Some(index);
192                }
193            }
194            SysexType::Kit => {
195                // When this sound is part of a kit query...
196                if let Some((kit_n, assigned_t)) = kit_number_and_assigned_track {
197                    index = assigned_t;
198                    assigned_track = Some(assigned_t);
199                    kit_number = Some(kit_n);
200                } else {
201                    panic!("This is not a sound query. Kit queries should provide the kit number and assigned track.")
202                }
203            }
204            _ => panic!(" This is not a sound or kit query."),
205        }
206
207        Ok(Self {
208            index,
209            pool_index,
210            kit_number,
211            assigned_track,
212            sysex_meta,
213            version: assemble_u32_from_u8_array_be(&raw_sound.__unknown_arr1[4..=7]),
214
215            name: ObjectName::from_u8_array(raw_sound.name),
216
217            sample: raw_sound.try_into()?,
218            filter: raw_sound.try_into()?,
219            amplitude: raw_sound.try_into()?,
220            lfo: raw_sound.try_into()?,
221            settings: raw_sound.try_into()?,
222            machine_parameters: MachineParameters::try_from_raw_sound(raw_sound, assigned_track)?,
223
224            accent_level: raw_sound.accent_level,
225            def_note: raw_sound.def_note,
226
227            __unknown: raw_sound.into(),
228
229            parameter_lock_pool: None,
230        })
231    }
232
233    pub(crate) fn as_raw_parts(&self) -> (SysexMeta, ar_sound_t) {
234        (self.sysex_meta, self.into())
235    }
236
237    /// Returns the type of the sound.
238    pub const fn sound_type(&self) -> SoundType {
239        if self.is_pool_sound() {
240            SoundType::Pool
241        } else if self.is_work_buffer_sound() {
242            SoundType::WorkBuffer
243        } else {
244            SoundType::KitQuery
245        }
246    }
247
248    /// Returns if the sound is coming from the sound pool.
249    pub const fn is_pool_sound(&self) -> bool {
250        self.pool_index.is_some()
251    }
252
253    /// Returns if the sound is coming from the work buffer and assigned to a track.
254    pub const fn is_work_buffer_sound(&self) -> bool {
255        self.assigned_track().is_some() && self.kit_number.is_none()
256    }
257
258    /// Returns if the sound is coming from a kit query and loaded as a part of a kit.
259    pub const fn is_part_of_a_kit_query(&self) -> bool {
260        self.kit_number.is_some()
261    }
262
263    /// Sets the name of the sound.
264    ///
265    /// # Errors
266    ///
267    /// The name must be ASCII and have a length of 15 characters or less. Other cases will result in an error.
268    pub fn set_name(&mut self, name: &str) -> Result<(), RytmError> {
269        self.name = name.try_into()?;
270        Ok(())
271    }
272
273    /// Sets the accent level of the sound.
274    ///
275    /// Range: `0..=127`
276    #[parameter_range(range = "accent_level:0..=127")]
277    // Range is checked
278    #[allow(clippy::cast_possible_truncation)]
279    pub fn set_accent_level(&mut self, accent_level: usize) -> Result<(), RytmError> {
280        self.accent_level = accent_level as u8;
281        Ok(())
282    }
283
284    /// Returns the assigned track if this is a track sound.
285    ///
286    /// Returns `None` if this is not a track sound.
287    ///
288    /// Range: `0..=11`
289    pub const fn assigned_track(&self) -> Option<usize> {
290        self.assigned_track
291    }
292
293    /// Returns the kit number if this sound is a part of a kit.
294    ///
295    /// Returns `None` if this is not a kit sound.
296    ///
297    /// Range: `0..=127`
298    pub const fn kit_number(&self) -> Option<usize> {
299        self.kit_number
300    }
301
302    /// Returns the kit number if this sound is a part of a kit.
303    ///
304    /// Returns `None` if this is not a kit sound.
305    ///
306    /// Range: `0..=127`
307    pub const fn pool_index(&self) -> Option<usize> {
308        self.pool_index
309    }
310
311    /// Returns the accent level of the sound.
312    ///
313    /// Range: `0..=127`
314    pub const fn accent_level(&self) -> usize {
315        self.accent_level as usize
316    }
317
318    /// Returns the name of the sound.
319    pub fn name(&self) -> &str {
320        self.name.as_str()
321    }
322
323    /// Returns the sample page parameters of the sound.
324    pub const fn sample(&self) -> &Sample {
325        &self.sample
326    }
327
328    /// Returns the filter page parameters of the sound.
329    pub const fn filter(&self) -> &Filter {
330        &self.filter
331    }
332
333    /// Returns the amplitude page parameters of the sound.
334    pub const fn amplitude(&self) -> &Amplitude {
335        &self.amplitude
336    }
337
338    /// Returns the LFO page parameters of the sound.
339    pub const fn lfo(&self) -> &Lfo {
340        &self.lfo
341    }
342
343    /// Returns sound settings of the sound.
344    pub const fn settings(&self) -> &SoundSettings {
345        &self.settings
346    }
347
348    /// Returns the machine parameters of the sound.
349    pub const fn machine_parameters(&self) -> &MachineParameters {
350        &self.machine_parameters
351    }
352
353    /// Returns the sample page parameters of the sound mutably.
354    pub fn sample_mut(&mut self) -> &mut Sample {
355        &mut self.sample
356    }
357
358    /// Returns the filter page parameters of the sound mutably.
359    pub fn filter_mut(&mut self) -> &mut Filter {
360        &mut self.filter
361    }
362
363    /// Returns the amplitude page parameters of the sound mutably.
364    pub fn amplitude_mut(&mut self) -> &mut Amplitude {
365        &mut self.amplitude
366    }
367
368    /// Returns the LFO page parameters of the sound mutably.
369    pub fn lfo_mut(&mut self) -> &mut Lfo {
370        &mut self.lfo
371    }
372
373    /// Returns sound settings of the sound mutably.
374    pub fn settings_mut(&mut self) -> &mut SoundSettings {
375        &mut self.settings
376    }
377
378    /// Returns the machine parameters of the sound mutably.
379    pub fn machine_parameters_mut(&mut self) -> &mut MachineParameters {
380        &mut self.machine_parameters
381    }
382
383    /// Returns the version of the sound structure.
384    pub const fn structure_version(&self) -> u32 {
385        self.version
386    }
387
388    /// Makes a new pool sound with the given index complying to project defaults.
389    ///
390    /// Range: `0..=127`
391    #[parameter_range(range = "sound_index:0..=127")]
392    pub fn try_default(sound_index: usize) -> Result<Self, RytmError> {
393        Self::try_default_with_device_id(sound_index, 0)
394    }
395
396    /// Makes a new pool sound with the given index complying to project defaults.
397    ///
398    /// Sound index range: 0..=12`
399    /// Device id range: `0..=127`
400    #[parameter_range(range = "sound_index:0..=127", range = "device_id:0..=127")]
401    pub fn try_default_with_device_id(
402        sound_index: usize,
403        device_id: u8,
404    ) -> Result<Self, RytmError> {
405        Ok(Self {
406            sysex_meta: SysexMeta::try_default_for_sound(sound_index, Some(device_id))?,
407            index: sound_index,
408            pool_index: Some(sound_index),
409            kit_number: None,
410            assigned_track: None,
411
412            version: 4,
413            // TODO: Decide default name.
414            name: ObjectName::try_from(format!("POOL_SOUND {sound_index}"))?,
415            accent_level: 32,
416
417            sample: Sample::default(),
418            filter: Filter::default(),
419            amplitude: Amplitude::default(),
420            lfo: Lfo::default(),
421            settings: SoundSettings::default(),
422            machine_parameters: MachineParameters::default(),
423
424            // Don't know what this is still..
425            def_note: 0,
426
427            __unknown: SoundUnknown::default(),
428
429            parameter_lock_pool: None,
430        })
431    }
432
433    /// Makes a new sound with the given index complying to project defaults as if it comes from a part of a kit.
434    ///
435    /// Track index range: `0..=11`
436    /// Kit index range: `0..=127`
437    #[parameter_range(range = "track_index:0..=11", range = "kit_index:0..=127")]
438    pub fn try_kit_default(
439        track_index: usize,
440        kit_index: usize,
441        sysex_meta: SysexMeta,
442    ) -> Result<Self, RytmError> {
443        // TODO: Do we need a work buffer | here?
444        let index = track_index;
445        Ok(Self {
446            sysex_meta,
447            index,
448            pool_index: None,
449            kit_number: Some(kit_index),
450            assigned_track: Some(track_index),
451
452            version: 4,
453            name: ObjectName::try_from(format!("KIT_SOUND {track_index}"))?,
454            accent_level: 32,
455
456            sample: Sample::default(),
457            filter: Filter::default(),
458            amplitude: Amplitude::default(),
459            lfo: Lfo::default(),
460            settings: SoundSettings::try_default_for_track(track_index)?,
461            machine_parameters: MachineParameters::try_default_for_track(track_index)?,
462
463            // Don't know what this is still..
464            def_note: 0,
465
466            __unknown: SoundUnknown::default(),
467
468            parameter_lock_pool: None,
469        })
470    }
471
472    /// Makes a new sound with the given index complying to project defaults as if it comes from the work buffer.
473    ///
474    /// Range: `0..=11`
475    #[allow(clippy::missing_panics_doc)]
476    pub fn work_buffer_default(track_index: usize) -> Self {
477        Self::try_work_buffer_default_with_device_id(track_index, 0).unwrap()
478    }
479
480    /// Makes a new sound with the given index complying to project defaults as if it comes from the work buffer.
481    ///
482    /// Track index range: `0..=11`
483    /// Device id range: `0..=127`
484    #[parameter_range(range = "track_index:0..=11", range = "device_id:0..=127")]
485    pub fn try_work_buffer_default_with_device_id(
486        track_index: usize,
487        device_id: u8,
488    ) -> Result<Self, RytmError> {
489        // Continue indexing from 128 since this is in work buffer.
490        let index = track_index | 0b1000_0000;
491        Ok(Self {
492            sysex_meta: SysexMeta::default_for_sound_in_work_buffer(track_index, Some(device_id)),
493            index,
494            pool_index: None,
495            kit_number: None,
496            assigned_track: Some(track_index),
497
498            version: 4,
499            name: ObjectName::try_from(format!("SOUND {track_index}"))?,
500            accent_level: 32,
501
502            sample: Sample::default(),
503            filter: Filter::default(),
504            amplitude: Amplitude::default(),
505            lfo: Lfo::default(),
506            settings: SoundSettings::try_default_for_track(track_index)?,
507            machine_parameters: MachineParameters::try_default_for_track(track_index)?,
508
509            // Don't know what this is still..
510            def_note: 0,
511
512            __unknown: SoundUnknown::default(),
513
514            parameter_lock_pool: None,
515        })
516    }
517
518    /// Sets the machine type of the sound.
519    ///
520    /// # Errors
521    ///
522    /// Not every machine type could be set for every sound if they're assigned to a track.
523    ///
524    /// For the sounds which are assigned to a track, the machine type must be compatible with the track or an error will be returned.
525    ///
526    /// For pool sounds this function will always succeed.
527    pub fn set_machine_type(&mut self, machine_type: MachineType) -> Result<(), RytmError> {
528        if let Some(assigned_track) = self.assigned_track() {
529            if !crate::util::is_machine_compatible_for_track(assigned_track, machine_type) {
530                return Err(ParameterError::Compatibility {
531                    value: machine_type.to_string(),
532                    parameter_name: "Machine".to_string(),
533                    reason: Some(format!(
534                        "Given machine {} is not compatible for track {}",
535                        machine_type, self.index
536                    )),
537                }
538                .into());
539            }
540        }
541
542        self.settings_mut().machine_type = machine_type;
543        self.machine_parameters = machine_type.into();
544        Ok(())
545    }
546
547    /// Returns the machine type of the sound.
548    pub const fn machine_type(&self) -> MachineType {
549        self.settings().machine_type
550    }
551
552    /// Returns the index of the sound.
553    ///
554    /// Normalized index if this sound is a work buffer sound.
555    pub const fn index(&self) -> usize {
556        if self.sysex_meta.is_targeting_work_buffer() {
557            return self.sysex_meta.get_normalized_object_index();
558        }
559        self.index
560    }
561
562    pub(crate) fn set_device_id(&mut self, device_id: u8) {
563        self.sysex_meta.set_device_id(device_id);
564    }
565}