midi_msg/system_exclusive/file_reference.rs
1use crate::parse_error::*;
2use crate::util::*;
3use alloc::vec::Vec;
4use bstr::BString;
5
6/// The set of messages used for accessing files on a shared file system or network
7/// so they can be used to play sounds without transferring the file contents.
8/// Used by [`UniversalNonRealTimeMsg::FileReference`](crate::UniversalNonRealTimeMsg::FileReference).
9///
10/// As defined in CA-018.
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum FileReferenceMsg {
14 /// Describe where a file is located for opening, but must be followed by a `SelectContents`
15 /// message if any sounds are to play.
16 Open {
17 /// A number 0-16383 used to distinguish between multiple file operations on the same device
18 ctx: u16,
19 file_type: FileReferenceType,
20 /// Max 260 character url.
21 url: BString,
22 },
23 /// Given the pointer to a file, prepare it so its sounds can be loaded.
24 SelectContents {
25 /// A number 0-16383 used to distinguish between multiple file operations on the same device
26 ctx: u16,
27 /// How to map the file's sounds onto MIDI banks/programs.
28 map: SelectMap,
29 },
30 /// The equivalent of an `Open` and `SelectContents` messages in succession.
31 OpenSelectContents {
32 /// A number 0-16383 used to distinguish between multiple file operations on the same device
33 ctx: u16,
34 file_type: FileReferenceType,
35 /// Max 260 character url.
36 url: BString,
37 /// How to map the file's sounds onto MIDI banks/programs.
38 map: SelectMap,
39 },
40 /// Close the file and deallocate the data related to it, such that its sounds should
41 /// no longer play.
42 Close {
43 /// A number 0-16383 used to distinguish between multiple file operations on the same device
44 ctx: u16,
45 },
46}
47
48impl FileReferenceMsg {
49 pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
50 match self {
51 Self::Open {
52 ctx,
53 file_type,
54 url,
55 } => {
56 push_u14(*ctx, v);
57 let len = 4 + url.len().min(260) + 1;
58 push_u14(len as u16, v);
59 file_type.extend_midi(v);
60 v.extend_from_slice(&url[0..url.len().min(260)]);
61 v.push(0); // Null terminate URL
62 }
63 Self::SelectContents { ctx, map } => {
64 push_u14(*ctx, v);
65 push_u14(map.len() as u16, v);
66 map.extend_midi(v);
67 }
68 Self::OpenSelectContents {
69 ctx,
70 file_type,
71 url,
72 map,
73 } => {
74 push_u14(*ctx, v);
75 let len = 4 + url.len().min(260) + 1 + map.len();
76 push_u14(len as u16, v);
77 file_type.extend_midi(v);
78 v.extend_from_slice(&url[0..url.len().min(260)]);
79 v.push(0); // Null terminate URL
80 map.extend_midi(v);
81 }
82 Self::Close { ctx } => {
83 push_u14(*ctx, v);
84 v.push(0); // Len is zero
85 v.push(0); // And here's another byte for some reason ¯\_(ツ)_/¯
86 }
87 }
88 }
89
90 #[allow(dead_code)]
91 pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
92 Err(ParseError::NotImplemented("FileReferenceMsg"))
93 }
94}
95
96/// The file type of a given file, as used by [`FileReferenceMsg`].
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98#[derive(Debug, Copy, Clone, PartialEq, Eq)]
99pub enum FileReferenceType {
100 DLS,
101 SF2,
102 WAV,
103}
104
105impl FileReferenceType {
106 fn extend_midi(&self, v: &mut Vec<u8>) {
107 match self {
108 Self::DLS => b"DLS ".iter().for_each(|c| v.push(*c)),
109 Self::SF2 => b"SF2 ".iter().for_each(|c| v.push(*c)),
110 Self::WAV => b"WAV ".iter().for_each(|c| v.push(*c)),
111 }
112 }
113}
114
115/// How to map a `DLS` or `SF2` file for MIDI reference. Used by [`SelectMap`].
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117#[derive(Debug, Copy, Clone, PartialEq, Eq)]
118pub struct SoundFileMap {
119 /// MIDI bank number required to select sound for playing. 0-16383
120 pub dst_bank: u16,
121 /// MIDI program number required to select sound for playing. 0-127
122 pub dst_prog: u8,
123 /// MIDI bank number referenced in file's instrument header. 0-16383
124 pub src_bank: u16,
125 /// MIDI program number referenced in file's instrument header. 0-127
126 pub src_prog: u8,
127 /// The selected instrument is a drum instrument
128 pub src_drum: bool,
129 /// The selected instrument should be loaded as a drum instrument
130 pub dst_drum: bool,
131 /// Initial volume 0-127
132 pub volume: u8,
133}
134
135impl Default for SoundFileMap {
136 fn default() -> Self {
137 Self {
138 dst_bank: 0,
139 dst_prog: 0,
140 src_bank: 0,
141 src_prog: 0,
142 src_drum: false,
143 dst_drum: false,
144 volume: 0x7F,
145 }
146 }
147}
148
149impl SoundFileMap {
150 fn extend_midi(&self, v: &mut Vec<u8>) {
151 push_u14(self.dst_bank, v);
152 push_u7(self.dst_prog, v);
153 push_u14(self.src_bank, v);
154 push_u7(self.src_prog, v);
155 let mut flags: u8 = 0;
156 if self.src_drum {
157 flags += 1 << 0;
158 }
159 if self.dst_drum {
160 flags += 1 << 1;
161 }
162 v.push(flags);
163 push_u7(self.volume, v);
164 }
165}
166
167/// How to map a `WAV` file for MIDI reference. Used by [`SelectMap`].
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169#[derive(Debug, Copy, Clone, PartialEq, Eq)]
170pub struct WAVMap {
171 /// MIDI bank number required to select sound for playing. 0-16383
172 pub dst_bank: u16,
173 /// MIDI program number required to select sound for playing. 0-127
174 pub dst_prog: u8,
175 /// MIDI note where sound plays at original pitch
176 pub base: u8,
177 /// Lowest MIDI note that plays
178 pub lokey: u8,
179 /// Highest MIDI note that plays
180 pub hikey: u8,
181 /// Fine tuning offset -8192-8191, representing the fractional cents to shift
182 /// in 1/8192ths of a cent
183 pub fine: i16,
184 /// Initial volume 0-127
185 pub volume: u8,
186}
187
188impl WAVMap {
189 fn extend_midi(&self, v: &mut Vec<u8>) {
190 push_u14(self.dst_bank, v);
191 push_u7(self.dst_prog, v);
192 push_u7(self.base, v);
193 push_u7(self.lokey, v);
194 push_u7(self.hikey, v);
195 let [msb, lsb] = i_to_u14(self.fine);
196 v.push(lsb);
197 v.push(msb);
198 push_u7(self.volume, v);
199 }
200}
201
202impl Default for WAVMap {
203 fn default() -> Self {
204 Self {
205 dst_bank: 0,
206 dst_prog: 0,
207 base: 60,
208 lokey: 0,
209 hikey: 0x7F,
210 fine: 0,
211 volume: 0x7F,
212 }
213 }
214}
215
216/// How to map a file for MIDI reference. Used by [`FileReferenceMsg::SelectContents`].
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum SelectMap {
220 /// Used for DLS or SF2 files. No more than 127 `SoundFileMap`s.
221 ///
222 /// 0 `SoundFileMap`s indicates "use the map provided in the file".
223 SoundFile(Vec<SoundFileMap>),
224 /// Used for WAV files.
225 WAV(WAVMap),
226 /// Used for DLS or SF2 files. Use the mapping provided by the file,
227 /// but offset the given MIDI bank by `bank_offset`.
228 ///
229 /// Defined in CA-028
230 SoundFileBankOffset {
231 bank_offset: u16,
232 /// The selected instrument is a drum instrument
233 src_drum: bool,
234 },
235 /// Used for WAV files. Offset the dest MIDI bank by `bank_offset`.
236 ///
237 /// Defined in CA-028.
238 WAVBankOffset {
239 map: WAVMap,
240 bank_offset: u16,
241 /// The selected instrument is a drum instrument
242 src_drum: bool,
243 },
244}
245
246impl SelectMap {
247 fn extend_midi(&self, v: &mut Vec<u8>) {
248 match self {
249 Self::WAV(m) => m.extend_midi(v),
250 Self::WAVBankOffset {
251 map,
252 bank_offset,
253 src_drum,
254 } => {
255 map.extend_midi(v);
256 v.push(0); // count
257 v.push(0); // Extension ID 1
258 v.push(1); // Extension ID 2
259 v.push(3); // len
260 push_u14(*bank_offset, v);
261 let mut flags: u8 = 0;
262 if *src_drum {
263 flags += 1 << 0;
264 }
265 push_u7(flags, v);
266 }
267 Self::SoundFileBankOffset {
268 bank_offset,
269 src_drum,
270 } => {
271 v.push(0); // count
272 v.push(0); // Extension ID 1
273 v.push(1); // Extension ID 2
274 v.push(3); // len
275 push_u14(*bank_offset, v);
276 let mut flags: u8 = 0;
277 if *src_drum {
278 flags += 1 << 0;
279 }
280 push_u7(flags, v);
281 }
282 Self::SoundFile(maps) => {
283 let count = maps.len().min(127);
284 push_u7(count as u8, v);
285 for m in maps[0..count].iter() {
286 m.extend_midi(v);
287 }
288 }
289 }
290 }
291
292 fn len(&self) -> usize {
293 match self {
294 Self::WAV(_) => 9,
295 Self::WAVBankOffset { .. } => 9 + 6,
296 Self::SoundFileBankOffset { .. } => 7,
297 Self::SoundFile(maps) => {
298 let count = maps.len().min(127);
299 1 + count * 8
300 }
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::*;
309 use alloc::vec;
310
311 #[test]
312 fn serialize_sample_dump_msg() {
313 assert_eq!(
314 MidiMsg::SystemExclusive {
315 msg: SystemExclusiveMsg::UniversalNonRealTime {
316 device: DeviceID::AllCall,
317 msg: UniversalNonRealTimeMsg::FileReference(
318 FileReferenceMsg::OpenSelectContents {
319 ctx: 44,
320 file_type: FileReferenceType::DLS,
321 url: BString::from("file://foo.dls"),
322 map: SelectMap::SoundFile(vec![SoundFileMap {
323 dst_bank: 1 << 10,
324 src_prog: 1,
325 ..Default::default()
326 }]),
327 }
328 ),
329 },
330 }
331 .to_midi(),
332 vec![
333 0xF0, 0x7E, 0x7F, // All call
334 0x0B, 0x03, // ExtendedSampleDump header
335 44, 00, // ctx
336 28, 0, // len,
337 b"D"[0], b"L"[0], b"S"[0], b" "[0], // Start URL
338 b"f"[0], b"i"[0], b"l"[0], b"e"[0], b":"[0], b"/"[0], b"/"[0], b"f"[0], b"o"[0],
339 b"o"[0], b"."[0], b"d"[0], b"l"[0], b"s"[0], 0, // End of url
340 1, // count
341 0, 8, // dst_bank
342 0, //dst_prog
343 0, 0, // src_bank
344 1, // src_prog
345 0, // flags
346 0x7f, // vol
347 0xF7
348 ]
349 );
350 }
351}