dot_multisample/
lib.rs

1//! .multisample format domain model
2//!
3//! Matches schema on [GitHub](https://github.com/bitwig/multisample) as of commit `4e7971f1`.
4//!
5//! ## Example
6//!
7//! ```
8//! # use std::path::Path;
9//! # use dot_multisample::*;
10//! # let path = std::env::current_dir().unwrap();
11//! let multi = Multisample::default()
12//!     .with_name("My Instrument")
13//!     .with_generator("Rust")
14//!     .with_category("Piano")
15//!     .with_creator("Me")
16//!     .with_description("Toy piano I found at the second hand shop")
17//!     .with_keywords(["noisy", "dirty", "metallic"])
18//!     .with_samples([
19//!         Sample::default()
20//!             .with_file(path.join("C2.wav"))
21//!             .with_key(Key::default().with_root(36)),
22//!         Sample::default()
23//!             .with_file(path.join("C3.wav"))
24//!             .with_key(Key::default().with_root(48)),
25//!         Sample::default()
26//!             .with_file(path.join("C4.wav"))
27//!             .with_key(Key::default().with_root(60)),
28//!     ]);
29//! ```
30
31#![warn(missing_docs)]
32
33use std::borrow::Cow;
34
35/// A multi-sample mapping for an instrument
36#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
37#[serde(rename = "multisample")]
38pub struct Multisample<'a> {
39    #[serde(
40        borrow,
41        default,
42        rename = "@name",
43        skip_serializing_if = "str::is_empty"
44    )]
45    name: Cow<'a, str>,
46    #[serde(borrow, default, skip_serializing_if = "str::is_empty")]
47    generator: Cow<'a, str>,
48    #[serde(borrow, default, skip_serializing_if = "str::is_empty")]
49    category: Cow<'a, str>,
50    #[serde(borrow, default, skip_serializing_if = "str::is_empty")]
51    creator: Cow<'a, str>,
52    #[serde(borrow, default, skip_serializing_if = "str::is_empty")]
53    description: Cow<'a, str>,
54    #[serde(borrow, default, skip_serializing_if = "Keywords::is_empty")]
55    keywords: Keywords<'a>,
56    #[serde(borrow, default, rename = "group")]
57    groups: Cow<'a, [Group<'a>]>,
58    #[serde(borrow, default, rename = "sample")]
59    samples: Cow<'a, [Sample<'a>]>,
60}
61
62impl<'a> Multisample<'a> {
63    /// Clones any borrowed data and returns a copy with a `'static` lifetime
64    pub fn to_owned(self) -> Multisample<'static> {
65        Multisample {
66            name: Cow::Owned(self.name.into_owned()),
67            generator: Cow::Owned(self.generator.into_owned()),
68            category: Cow::Owned(self.category.into_owned()),
69            creator: Cow::Owned(self.creator.into_owned()),
70            description: Cow::Owned(self.description.into_owned()),
71            keywords: Keywords {
72                list: self
73                    .keywords
74                    .list
75                    .iter()
76                    .map(|s| Cow::Owned(s.to_string()))
77                    .collect(),
78            },
79            groups: self
80                .groups
81                .iter()
82                .map(|g| Group {
83                    name: Cow::Owned(g.name.to_string()),
84                    color: g.color,
85                })
86                .collect(),
87            samples: self
88                .samples
89                .iter()
90                .map(|s| Sample {
91                    file: Cow::Owned(s.file.to_path_buf()),
92                    ..s.clone()
93                })
94                .collect(),
95        }
96    }
97
98    /// Set the name of the multi-sampled instrument
99    pub fn with_name(self, name: impl Into<Cow<'a, str>>) -> Self {
100        Self {
101            name: name.into(),
102            ..self
103        }
104    }
105
106    /// Set the name of the software tool generating the mapping
107    pub fn with_generator(self, generator: impl Into<Cow<'a, str>>) -> Self {
108        Self {
109            generator: generator.into(),
110            ..self
111        }
112    }
113
114    /// Set the general kind of instrument this is
115    pub fn with_category(self, category: impl Into<Cow<'a, str>>) -> Self {
116        Self {
117            category: category.into(),
118            ..self
119        }
120    }
121
122    /// Set the user who is creating the mapping
123    pub fn with_creator(self, creator: impl Into<Cow<'a, str>>) -> Self {
124        Self {
125            creator: creator.into(),
126            ..self
127        }
128    }
129
130    /// Provide a longer-form text description of the instrument
131    pub fn with_description(self, description: impl Into<Cow<'a, str>>) -> Self {
132        Self {
133            description: description.into(),
134            ..self
135        }
136    }
137
138    /// Set the keywords associated with this instrument
139    pub fn with_keywords<S: Into<Cow<'a, str>>>(
140        self,
141        keywords: impl IntoIterator<Item = S>,
142    ) -> Self {
143        Self {
144            keywords: Keywords {
145                list: keywords.into_iter().map(Into::into).collect(),
146            },
147            ..self
148        }
149    }
150
151    /// Set the list of sample groups
152    pub fn with_groups(self, groups: impl IntoIterator<Item = Group<'a>>) -> Self {
153        Self {
154            groups: groups.into_iter().collect(),
155            ..self
156        }
157    }
158
159    /// Set the list of sample mappings
160    pub fn with_samples(self, samples: impl IntoIterator<Item = Sample<'a>>) -> Self {
161        Self {
162            samples: samples.into_iter().collect(),
163            ..self
164        }
165    }
166
167    /// Name of the multi-sampled instrument
168    pub fn name(&self) -> &str {
169        &self.name
170    }
171
172    /// Name of the software tool generating the mapping
173    pub fn generator(&self) -> &str {
174        &self.generator
175    }
176
177    /// General kind of instrument
178    pub fn category(&self) -> &str {
179        &self.category
180    }
181
182    /// User who created the mapping
183    pub fn creator(&self) -> &str {
184        &self.creator
185    }
186
187    /// Longer-form text description of the instrument
188    pub fn description(&self) -> &str {
189        &self.description
190    }
191
192    /// Keywords to aid in finding and organizing instruments
193    pub fn keywords(&self) -> &[Cow<'a, str>] {
194        &self.keywords.list
195    }
196
197    /// Groups that can be referenced from the sample list
198    pub fn groups(&self) -> &[Group] {
199        &self.groups
200    }
201
202    /// Sample mappings in this instrument
203    pub fn samples(&self) -> &[Sample] {
204        &self.samples
205    }
206}
207
208#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
209struct Keywords<'a> {
210    #[serde(borrow, default, rename = "keyword")]
211    list: Cow<'a, [Cow<'a, str>]>,
212}
213
214impl Keywords<'_> {
215    fn is_empty(&self) -> bool {
216        self.list.is_empty()
217    }
218}
219
220/// A sample group (for presentation purposes only)
221#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
222pub struct Group<'a> {
223    #[serde(
224        borrow,
225        default,
226        rename = "@name",
227        skip_serializing_if = "str::is_empty"
228    )]
229    name: Cow<'a, str>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    color: Option<Color>,
232}
233
234impl<'a> Group<'a> {
235    /// Give a name to the group
236    pub fn with_name(self, name: impl Into<Cow<'a, str>>) -> Self {
237        Self {
238            name: name.into(),
239            ..self
240        }
241    }
242
243    /// Provide a color to associate with the group
244    pub fn with_color(self, color: impl Into<Option<Color>>) -> Self {
245        Self {
246            color: color.into(),
247            ..self
248        }
249    }
250
251    /// Get the name of the group
252    pub fn name(&self) -> &str {
253        &self.name
254    }
255
256    /// Get the color associated with the group, if any
257    pub fn color(&self) -> Option<Color> {
258        self.color
259    }
260}
261
262/// RGB hex value
263pub type Color = [u8; 3];
264
265/// Mapping information for a sample file
266#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
267pub struct Sample<'a> {
268    #[serde(borrow, rename = "@file")]
269    file: Cow<'a, std::path::Path>,
270    #[serde(rename = "@sample-start", skip_serializing_if = "Option::is_none")]
271    sample_start: Option<f64>,
272    #[serde(rename = "@sample-stop", skip_serializing_if = "Option::is_none")]
273    sample_stop: Option<f64>,
274    #[serde(rename = "@gain", skip_serializing_if = "Option::is_none")]
275    gain: Option<f64>,
276    #[serde(rename = "@group", skip_serializing_if = "Option::is_none")]
277    group: Option<isize>,
278    #[serde(rename = "@parameter-1", skip_serializing_if = "Option::is_none")]
279    parameter_1: Option<f64>,
280    #[serde(rename = "@parameter-2", skip_serializing_if = "Option::is_none")]
281    parameter_2: Option<f64>,
282    #[serde(rename = "@parameter-3", skip_serializing_if = "Option::is_none")]
283    parameter_3: Option<f64>,
284    #[serde(rename = "@reverse", skip_serializing_if = "Option::is_none")]
285    reverse: Option<bool>,
286    #[serde(rename = "@zone-logic", skip_serializing_if = "Option::is_none")]
287    zone_logic: Option<ZoneLogic>,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    key: Option<Key>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    velocity: Option<ZoneInfo>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    select: Option<ZoneInfo>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    r#loop: Option<Loop>,
296}
297
298impl<'a> Sample<'a> {
299    /// Set the file path of the sample
300    pub fn with_file(self, file: impl Into<Cow<'a, std::path::Path>>) -> Self {
301        Self {
302            file: file.into(),
303            ..self
304        }
305    }
306
307    /// Set the start point for the sample (in frames)
308    pub fn with_sample_start(self, sample_start: impl Into<Option<f64>>) -> Self {
309        Self {
310            sample_start: sample_start.into(),
311            ..self
312        }
313    }
314
315    /// Set the end point for the sample (in frames)
316    pub fn with_sample_stop(self, sample_stop: impl Into<Option<f64>>) -> Self {
317        Self {
318            sample_stop: sample_stop.into(),
319            ..self
320        }
321    }
322
323    /// Set the gain for the sample
324    pub fn with_gain(self, gain: impl Into<Option<f64>>) -> Self {
325        Self {
326            gain: gain.into(),
327            ..self
328        }
329    }
330
331    /// Put the sample in a group
332    pub fn with_group(self, group: impl Into<Option<isize>>) -> Self {
333        Self {
334            group: group.into(),
335            ..self
336        }
337    }
338
339    /// Set the first parameter
340    pub fn with_parameter_1(self, parameter_1: impl Into<Option<f64>>) -> Self {
341        Self {
342            parameter_1: parameter_1.into(),
343            ..self
344        }
345    }
346
347    /// Set the second parameter
348    pub fn with_parameter_2(self, parameter_2: impl Into<Option<f64>>) -> Self {
349        Self {
350            parameter_2: parameter_2.into(),
351            ..self
352        }
353    }
354
355    /// Set the third parameter
356    pub fn with_parameter_3(self, parameter_3: impl Into<Option<f64>>) -> Self {
357        Self {
358            parameter_3: parameter_3.into(),
359            ..self
360        }
361    }
362
363    /// Set whether the sample should be played in reverse
364    pub fn with_reverse(self, reverse: impl Into<Option<bool>>) -> Self {
365        Self {
366            reverse: reverse.into(),
367            ..self
368        }
369    }
370
371    /// Choose an algorithm for sample selection when zones overlap
372    pub fn with_zone_logic(self, zone_logic: impl Into<Option<ZoneLogic>>) -> Self {
373        Self {
374            zone_logic: zone_logic.into(),
375            ..self
376        }
377    }
378
379    /// Set the key range for the sample
380    pub fn with_key(self, key: impl Into<Option<Key>>) -> Self {
381        Self {
382            key: key.into(),
383            ..self
384        }
385    }
386
387    /// Set the velocity range for the sample
388    pub fn with_velocity(self, velocity: impl Into<Option<ZoneInfo>>) -> Self {
389        Self {
390            velocity: velocity.into(),
391            ..self
392        }
393    }
394
395    /// Set the "select" range for the sample
396    pub fn with_select(self, select: impl Into<Option<ZoneInfo>>) -> Self {
397        Self {
398            select: select.into(),
399            ..self
400        }
401    }
402
403    /// Set the loop behavior of the sample
404    pub fn with_loop(self, r#loop: impl Into<Option<Loop>>) -> Self {
405        Self {
406            r#loop: r#loop.into(),
407            ..self
408        }
409    }
410
411    /// Get the path to the sample file
412    pub fn file(&self) -> &std::path::Path {
413        &self.file
414    }
415
416    /// Get the sample's start point (in frames)
417    pub fn sample_start(&self) -> Option<f64> {
418        self.sample_start
419    }
420
421    /// Get the sample's end point (in frames)
422    pub fn sample_stop(&self) -> Option<f64> {
423        self.sample_stop
424    }
425
426    /// Get the sample's gain
427    pub fn gain(&self) -> Option<f64> {
428        self.gain
429    }
430
431    /// Get the group associated with the sample, if any
432    pub fn group(&self) -> Option<isize> {
433        self.group
434    }
435
436    /// Get the value of the first parameter
437    pub fn parameter_1(&self) -> Option<f64> {
438        self.parameter_1
439    }
440
441    /// Get the value of the second parameter
442    pub fn parameter_2(&self) -> Option<f64> {
443        self.parameter_2
444    }
445
446    /// Get the value of the third parameter
447    pub fn parameter_3(&self) -> Option<f64> {
448        self.parameter_3
449    }
450
451    /// Get the playback reversal for the sample
452    pub fn reverse(&self) -> Option<bool> {
453        self.reverse
454    }
455
456    /// Get the overlap behavior for the sample
457    pub fn zone_logic(&self) -> Option<ZoneLogic> {
458        self.zone_logic
459    }
460
461    /// Get the sample's key range
462    pub fn key(&self) -> &Option<Key> {
463        &self.key
464    }
465
466    /// Get the sample's velocity range
467    pub fn velocity(&self) -> &Option<ZoneInfo> {
468        &self.velocity
469    }
470
471    /// Get the sample's "select" range
472    pub fn select(&self) -> &Option<ZoneInfo> {
473        &self.select
474    }
475
476    /// Get the sample's loop behavior
477    pub fn r#loop(&self) -> &Option<Loop> {
478        &self.r#loop
479    }
480}
481
482/// Specify behavior when multiple samples occupy the same zone
483#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
484#[serde(rename_all = "kebab-case")]
485pub enum ZoneLogic {
486    /// Play this sample regardless of zone overlap
487    AlwaysPlay,
488    /// Alternate this sample with others in the overlapping region
489    RoundRobin,
490}
491
492/// Mapping data relating to notes played
493#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
494pub struct Key {
495    #[serde(rename = "@root", default, skip_serializing_if = "Option::is_none")]
496    root: Option<u8>,
497    #[serde(rename = "@track", default, skip_serializing_if = "Option::is_none")]
498    track: Option<f64>,
499    #[serde(rename = "@tune", default, skip_serializing_if = "Option::is_none")]
500    tune: Option<f64>,
501    #[serde(rename = "@low", default, skip_serializing_if = "Option::is_none")]
502    low: Option<u8>,
503    #[serde(rename = "@high", default, skip_serializing_if = "Option::is_none")]
504    high: Option<u8>,
505    #[serde(rename = "@low-fade", default, skip_serializing_if = "Option::is_none")]
506    low_fade: Option<u8>,
507    #[serde(
508        rename = "@high-fade",
509        default,
510        skip_serializing_if = "Option::is_none"
511    )]
512    high_fade: Option<u8>,
513}
514
515impl Key {
516    /// Set the root pitch of the sample
517    pub fn with_root(self, root: impl Into<Option<u8>>) -> Self {
518        Self {
519            root: root.into(),
520            ..self
521        }
522    }
523
524    /// Set the keytrack amount (0 to 2)
525    pub fn with_track(self, track: impl Into<Option<f64>>) -> Self {
526        Self {
527            track: track.into(),
528            ..self
529        }
530    }
531
532    /// Set the fine tuning for the sample
533    pub fn with_tune(self, tune: impl Into<Option<f64>>) -> Self {
534        Self {
535            tune: tune.into(),
536            ..self
537        }
538    }
539
540    /// Set the lower end of the pitch range
541    pub fn with_low(self, low: impl Into<Option<u8>>) -> Self {
542        Self {
543            low: low.into(),
544            ..self
545        }
546    }
547
548    /// Set the upper end of the pitch range
549    pub fn with_high(self, high: impl Into<Option<u8>>) -> Self {
550        Self {
551            high: high.into(),
552            ..self
553        }
554    }
555
556    /// Set the length of the lower fade region
557    pub fn with_low_fade(self, low_fade: impl Into<Option<u8>>) -> Self {
558        Self {
559            low_fade: low_fade.into(),
560            ..self
561        }
562    }
563
564    /// Set the length of the upper fade region
565    pub fn with_high_fade(self, high_fade: impl Into<Option<u8>>) -> Self {
566        Self {
567            high_fade: high_fade.into(),
568            ..self
569        }
570    }
571
572    /// Get the sample's root pitch
573    pub fn root(&self) -> Option<u8> {
574        self.root
575    }
576
577    /// Get the sample's keytrack amount
578    pub fn track(&self) -> Option<f64> {
579        self.track
580    }
581
582    /// Get the sample's fine tuning
583    pub fn tune(&self) -> Option<f64> {
584        self.tune
585    }
586
587    /// Get the lower end of the pitch range
588    pub fn low(&self) -> Option<u8> {
589        self.low
590    }
591
592    /// Get the upper end of the pitch range
593    pub fn high(&self) -> Option<u8> {
594        self.high
595    }
596
597    /// Get the length of the lower fade region
598    pub fn low_fade(&self) -> Option<u8> {
599        self.low_fade
600    }
601
602    /// Get the length of the upper fade region
603    pub fn high_fade(&self) -> Option<u8> {
604        self.high_fade
605    }
606}
607
608/// Generic mapping with endpoints and fade distances
609#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
610pub struct ZoneInfo {
611    #[serde(rename = "@low", default, skip_serializing_if = "Option::is_none")]
612    low: Option<u8>,
613    #[serde(rename = "@high", default, skip_serializing_if = "Option::is_none")]
614    high: Option<u8>,
615    #[serde(rename = "@low-fade", default, skip_serializing_if = "Option::is_none")]
616    low_fade: Option<u8>,
617    #[serde(
618        rename = "@high-fade",
619        default,
620        skip_serializing_if = "Option::is_none"
621    )]
622    high_fade: Option<u8>,
623}
624
625impl ZoneInfo {
626    /// Set the lower end of the region
627    pub fn with_low(self, low: impl Into<Option<u8>>) -> Self {
628        Self {
629            low: low.into(),
630            ..self
631        }
632    }
633
634    /// Set the upper end of the region
635    pub fn with_high(self, high: impl Into<Option<u8>>) -> Self {
636        Self {
637            high: high.into(),
638            ..self
639        }
640    }
641
642    /// Set the length of the lower fade region
643    pub fn with_low_fade(self, low_fade: impl Into<Option<u8>>) -> Self {
644        Self {
645            low_fade: low_fade.into(),
646            ..self
647        }
648    }
649
650    /// Set the length of the upper fade region
651    pub fn with_high_fade(self, high_fade: impl Into<Option<u8>>) -> Self {
652        Self {
653            high_fade: high_fade.into(),
654            ..self
655        }
656    }
657
658    /// Get the lower end of the region
659    pub fn low(&self) -> Option<u8> {
660        self.low
661    }
662
663    /// Get the upper end of the region
664    pub fn high(&self) -> Option<u8> {
665        self.high
666    }
667
668    /// Get the length of the lower fade region
669    pub fn low_fade(&self) -> Option<u8> {
670        self.low_fade
671    }
672
673    /// Get the length of the upper fade region
674    pub fn high_fade(&self) -> Option<u8> {
675        self.high_fade
676    }
677}
678
679/// Looping behavior for a sample
680#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
681pub struct Loop {
682    #[serde(rename = "@mode", skip_serializing_if = "Option::is_none")]
683    mode: Option<LoopMode>,
684    #[serde(rename = "@start", skip_serializing_if = "Option::is_none")]
685    start: Option<f64>,
686    #[serde(rename = "@stop", skip_serializing_if = "Option::is_none")]
687    stop: Option<f64>,
688    #[serde(rename = "@fade", skip_serializing_if = "Option::is_none")]
689    fade: Option<f64>,
690}
691
692impl Loop {
693    /// Set the sample's loop mode
694    pub fn with_mode(self, mode: impl Into<Option<LoopMode>>) -> Self {
695        Self {
696            mode: mode.into(),
697            ..self
698        }
699    }
700
701    /// Set the start point of the loop
702    pub fn with_start(self, start: impl Into<Option<f64>>) -> Self {
703        Self {
704            start: start.into(),
705            ..self
706        }
707    }
708
709    /// Set the end point of the loop
710    pub fn with_stop(self, stop: impl Into<Option<f64>>) -> Self {
711        Self {
712            stop: stop.into(),
713            ..self
714        }
715    }
716
717    /// Set the amount of crossfade when crossing the loop point
718    pub fn with_fade(self, fade: impl Into<Option<f64>>) -> Self {
719        Self {
720            fade: fade.into(),
721            ..self
722        }
723    }
724
725    /// Get the loop mode
726    pub fn mode(&self) -> Option<LoopMode> {
727        self.mode
728    }
729
730    /// Get the start point of the loop
731    pub fn start(&self) -> Option<f64> {
732        self.start
733    }
734
735    /// Get the end point of the loop
736    pub fn stop(&self) -> Option<f64> {
737        self.stop
738    }
739
740    /// Get the crossfade amount
741    pub fn fade(&self) -> Option<f64> {
742        self.fade
743    }
744}
745
746/// Traversal mode
747#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
748#[serde(rename_all = "kebab-case")]
749pub enum LoopMode {
750    /// Do not loop
751    #[default]
752    Off,
753    /// Loop in the playback direction
754    Loop,
755    /// Loop in alternating directions
756    PingPong,
757}