1#![forbid(unsafe_code)]
2#![no_std]
3
4#[macro_use]
5extern crate alloc;
6
7use core::fmt::Write as _;
8use core::iter;
9
10use alloc::vec::Vec;
11use alloc::collections::{BTreeMap, BTreeSet};
12use alloc::string::String;
13
14pub use amm_sdk; use amm_sdk::Composition;
17use amm_sdk::note::{Note, DurationType, Duration, Accidental};
18use amm_sdk::context::{Key, Tempo};
19use amm_sdk::modification::{PhraseModificationType, NoteModificationType, SectionModificationType, DirectionType, NoteModification, ChordModificationType};
20use amm_sdk::structure::{Part, Section, Staff, PartContent, SectionContent, StaffContent, ChordContent, Phrase, PhraseContent};
21
22fn xml_escape(input: &str) -> String {
23 let mut result = String::with_capacity(input.len());
24 for c in input.chars() {
25 match c {
26 '&' => result.push_str("&"),
27 '<' => result.push_str("<"),
28 '>' => result.push_str(">"),
29 '\'' => result.push_str("'"),
30 '"' => result.push_str("""),
31 '\n' => result.push_str("
"),
32 _ => result.push(c),
33 }
34 }
35 result
36}
37fn quarter_note_tempo(tempo: &Tempo) -> f64 {
38 tempo.beats_per_minute as f64 * (tempo.base_note.value() / Duration::new(DurationType::Quarter, 0).value())
39}
40
41#[derive(Debug)]
42pub enum TranslateError {
43 CyclicStructure,
44 UnsupportedDuration { duration: Duration },
45 UnsupportedTuplet { num_beats: u8, into_beats: u8 },
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
49enum Mod {
50 Accent, Staccato, TurnUpper, TurnLower,
51}
52#[derive(Default)]
53struct Modifiers {
54 stack: Vec<Vec<Mod>>,
55 active: BTreeSet<Mod>,
56}
57
58macro_rules! check_modifiers_invariants {
59 ($self:ident) => {{
60 debug_assert!($self.stack.iter().all(|x| !x.is_empty()));
61 debug_assert!($self.stack.iter().map(|x| x.len()).sum::<usize>() == $self.active.len());
62 debug_assert!($self.stack.iter().flat_map(|x| x.iter().copied()).collect::<BTreeSet<_>>() == $self.active);
63 }};
64}
65
66impl Modifiers {
67 fn set(&mut self, new_active: &BTreeSet<Mod>, output: &mut String) {
68 check_modifiers_invariants!(self);
69
70 while !self.active.is_subset(new_active) {
71 for x in self.stack.pop().unwrap() {
72 self.active.remove(&x);
73 }
74 write!(output, r#"</script></block>"#).unwrap();
75 }
76
77 let new = (new_active - &self.active).into_iter().collect::<Vec<_>>();
78
79 if !new.is_empty() {
80 write!(output, r#"<block s="noteMod"><list>"#).unwrap();
81 for x in new.iter() {
82 self.active.insert(*x);
83 write!(output, r#"<l><option>{x:?}</option></l>"#).unwrap();
84 }
85 write!(output, r#"</list><script>"#).unwrap();
86
87 self.stack.push(new);
88 }
89
90 check_modifiers_invariants!(self);
91 }
92 fn unwind_point(&self) -> usize {
93 self.stack.len()
94 }
95 fn unwind_to(&mut self, point: usize, output: &mut String) {
96 check_modifiers_invariants!(self);
97
98 while self.stack.len() > point {
99 for x in self.stack.pop().unwrap() {
100 self.active.remove(&x);
101 }
102 write!(output, r#"</script></block>"#).unwrap();
103 }
104
105 check_modifiers_invariants!(self);
106 }
107}
108
109struct Context {
110 modifiers: Modifiers,
111 sections: BTreeSet<*const Section>,
112 staffs: BTreeSet<*const Staff>,
113 phrases: BTreeSet<*const Phrase>,
114 starting_key: Key,
115 starting_tempo: Tempo,
116 blocks: BTreeMap<String, String>,
117}
118
119fn half_duration_type(duration_type: DurationType) -> Option<DurationType> {
120 match duration_type {
121 DurationType::Maxima => Some(DurationType::Long),
122 DurationType::Long => Some(DurationType::Breve),
123 DurationType::Breve => Some(DurationType::Whole),
124 DurationType::Whole => Some(DurationType::Half),
125 DurationType::Half => Some(DurationType::Quarter),
126 DurationType::Quarter => Some(DurationType::Eighth),
127 DurationType::Eighth => Some(DurationType::Sixteenth),
128 DurationType::Sixteenth => Some(DurationType::ThirtySecond),
129 DurationType::ThirtySecond => Some(DurationType::SixtyFourth),
130 DurationType::SixtyFourth => Some(DurationType::OneHundredTwentyEighth),
131 DurationType::OneHundredTwentyEighth => Some(DurationType::TwoHundredFiftySixth),
132 DurationType::TwoHundredFiftySixth => Some(DurationType::FiveHundredTwelfth),
133 DurationType::FiveHundredTwelfth => Some(DurationType::OneThousandTwentyFourth),
134 DurationType::OneThousandTwentyFourth => Some(DurationType::TwoThousandFortyEighth),
135 DurationType::TwoThousandFortyEighth => None,
136 }
137}
138fn parse_duration(duration: Duration) -> Result<String, TranslateError> {
139 let dots = match duration.dots {
140 0 => "",
141 1 => "Dotted",
142 2 => "DottedDotted",
143 x => {
144 let mut res = String::from(r#"<block s="tieDuration"><list>"#);
145 let mut t = duration.value;
146 for _ in 2..x {
147 res += &parse_duration(Duration::new(t, 0)).map_err(|_| TranslateError::UnsupportedDuration { duration })?;
148 t = half_duration_type(t).ok_or_else(|| TranslateError::UnsupportedDuration { duration })?;
149 }
150 res += &parse_duration(Duration::new(t, 2)).map_err(|_| TranslateError::UnsupportedDuration { duration })?;
151 res += "</list></block>";
152 return Ok(res);
153 }
154 };
155 Ok(match duration.value {
156 DurationType::Maxima => format!(r#"<block s="tieDuration"><list><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l></list></block>"#),
157 DurationType::Long => format!(r#"<block s="tieDuration"><list><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l><l>{dots}Whole</l></list></block>"#),
158 DurationType::Breve => format!(r#"<block s="tieDuration"><list><l>{dots}Whole</l><l>{dots}Whole</l></list></block>"#),
159 DurationType::Whole => format!("<l>{dots}Whole</l>"),
160 DurationType::Half => format!("<l>{dots}Half</l>"),
161 DurationType::Quarter => format!("<l>{dots}Quarter</l>"),
162 DurationType::Eighth => format!("<l>{dots}Eighth</l>"),
163 DurationType::Sixteenth => format!("<l>{dots}Sixteenth</l>"),
164 DurationType::ThirtySecond => format!("<l>{dots}ThirtySecond</l>"),
165 DurationType::SixtyFourth => format!("<l>{dots}SixtyFourth</l>"),
166 _ => return Err(TranslateError::UnsupportedDuration { duration }),
167 })
168}
169fn translate_chord(raw_notes: &[Note], raw_mods: &[ChordModificationType], output: &mut String, context: &mut Context) -> Result<(), TranslateError> {
170 let raw_mods = raw_mods.iter().flat_map(NoteModification::from_chord_modification).map(|x| x.r#type).collect::<Vec<_>>();
171
172 for m in raw_notes.iter().flat_map(|n| n.iter_modifications()).map(|m| &m.r#type).chain(&raw_mods) {
173 match m {
174 NoteModificationType::Dynamic { dynamic } => write!(output, r#"<block s="setAudioEffect"><l>Volume</l><l>{}</l></block>"#, 100.0 * dynamic.value()).unwrap(),
175 _ => (),
176 }
177 }
178
179 let raw_notes = raw_notes.iter().filter(|x| !x.iter_modifications().any(|m| matches!(m.r#type, NoteModificationType::Grace { .. })));
181
182 let (notes, shortest_duration) = match raw_notes.clone().map(|x| x.duration).reduce(|a, b| if a.value() <= b.value() { a } else { b }) {
183 Some(x) => (raw_notes.filter(|x| !x.is_rest()), parse_duration(x)?),
184 None => return Ok(()),
185 };
186
187 if notes.clone().next().is_some() {
188 let mut notes_xml = String::new();
189 let mut durations_xml = vec![];
190 for note in notes.clone() {
191 let accidental = match note.accidental {
192 Accidental::None => "",
193 Accidental::Natural => "n",
194 Accidental::Sharp => "s",
195 Accidental::DoubleSharp => "ss",
196 Accidental::Flat => "b",
197 Accidental::DoubleFlat => "bb",
198 };
199 write!(notes_xml, "<l>{pitch}{accidental}</l>", pitch = note.pitch).unwrap();
200 durations_xml.push(parse_duration(note.duration)?);
201 }
202 if !durations_xml.contains(&shortest_duration) {
203 write!(notes_xml, "<l>rest</l>").unwrap();
204 durations_xml.push(shortest_duration);
205 }
206 let durations_xml = if durations_xml.iter().all(|x| *x == durations_xml[0]) { durations_xml.into_iter().next().unwrap() } else { format!(r#"<block s="reportNewList"><list>{}</list></block>"#, durations_xml.join("")) };
207
208 let mods = notes.flat_map(|n| n.iter_modifications().map(|x| &x.r#type)).chain(&raw_mods).flat_map(|m| Some(match &m {
209 NoteModificationType::Accent | NoteModificationType::SoftAccent => Mod::Accent,
210 NoteModificationType::Staccato | NoteModificationType::Staccatissimo => Mod::Staccato,
211 NoteModificationType::Turn { upper, delayed: _, vertical: _ } => if *upper { Mod::TurnUpper } else { Mod::TurnLower },
212 _ => return None,
213 })).collect();
214 context.modifiers.set(&mods, output);
215
216 write!(output, r#"<block s="playNotes">{durations_xml}<list>{notes_xml}</list></block>"#).unwrap();
217 } else {
218 write!(output, r#"<block s="rest">{shortest_duration}</block>"#).unwrap();
219 }
220
221 Ok(())
222}
223fn translate_phrase(phrase: &Phrase, output: &mut String, context: &mut Context) -> Result<(), TranslateError> {
224 if !context.phrases.insert(phrase as *const _) {
225 return Err(TranslateError::CyclicStructure);
226 }
227
228 let mut tuplet_mod = None;
229 for modification in phrase.iter_modifications() {
230 match modification.r#type {
231 PhraseModificationType::Tuplet { num_beats, into_beats } => match (num_beats, into_beats) {
232 (3, 2) => tuplet_mod = Some("Tuplet 3:2"),
233 (5, 4) => tuplet_mod = Some("Tuplet 5:4"),
234 (6, 4) => tuplet_mod = Some("Tuplet 6:4"),
235 (7, 4) => tuplet_mod = Some("Tuplet 7:4"),
236 _ => return Err(TranslateError::UnsupportedTuplet { num_beats, into_beats }),
237 }
238 _ => (),
239 }
240 }
241
242 let unwind_point = context.modifiers.unwind_point();
243 if let Some(tuplet_mod) = tuplet_mod {
244 write!(output, r#"<block s="noteMod"><list><l><option>{tuplet_mod}</option></l></list><script>"#).unwrap();
245 }
246
247 for content in phrase.iter() {
248 match content {
249 PhraseContent::Note(note) => translate_chord(&[note.clone()], &[], output, context)?,
250 PhraseContent::Chord(chord) => translate_chord(&chord.iter().map(|x| match x { ChordContent::Note(note) => note.clone() }).collect::<Vec<_>>(), &chord.iter_modifications().map(|x| x.r#type).collect::<Vec<_>>(), output, context)?,
251 PhraseContent::Phrase(sub_phrase) => translate_phrase(sub_phrase, output, context)?,
252 PhraseContent::MultiVoice(_) => (),
253 }
254 }
255
256 if tuplet_mod.is_some() {
257 write!(output, r#"</script></block>"#).unwrap();
258 context.modifiers.unwind_to(unwind_point, output);
259 }
260
261 assert!(context.phrases.remove(&(phrase as *const _)));
262 Ok(())
263}
264fn translate_staff(staff: &Staff, output: &mut String, context: &mut Context) -> Result<(), TranslateError> {
265 if !context.staffs.insert(staff as *const _) {
266 return Err(TranslateError::CyclicStructure);
267 }
268
269 for content in staff.iter() {
270 match content {
271 StaffContent::Note(note) => translate_chord(&[note.clone()], &[], output, context)?,
272 StaffContent::Chord(chord) => translate_chord(&chord.iter().map(|x| match x { ChordContent::Note(note) => note.clone() }).collect::<Vec<_>>(), &chord.iter_modifications().map(|x| x.r#type).collect::<Vec<_>>(), output, context)?,
273 StaffContent::Phrase(phrase) => translate_phrase(phrase, output, context)?,
274 StaffContent::Direction(direction) => match &direction.r#type {
275 DirectionType::KeyChange { key } => write!(output, r#"<block s="setKey"><l>{key_sig:?}{key_mode:?}</l></block>"#, key_sig = key.signature, key_mode = key.mode).unwrap(),
276 _ => (),
277 }
278 StaffContent::MultiVoice(_) => (),
279 }
280 }
281
282 assert!(context.staffs.remove(&(staff as *const _)));
283 Ok(())
284}
285fn translate_section(section: &Section, output: &mut String, context: &mut Context) -> Result<(), TranslateError> {
286 if !context.sections.insert(section as *const _) {
287 return Err(TranslateError::CyclicStructure);
288 }
289
290 let mut repetitions = 1;
291 for modification in section.iter_modifications() {
292 match &modification.r#type {
293 SectionModificationType::Repeat { num_times } => repetitions += *num_times as usize,
294 SectionModificationType::TempoExplicit { tempo } => write!(output, r#"<block s="setBPM"><l>{tempo}</l></block>"#, tempo = quarter_note_tempo(tempo)).unwrap(),
295 SectionModificationType::TempoImplicit { tempo } => write!(output, r#"<block s="setBPM"><l>{tempo}</l></block>"#, tempo = tempo.value()).unwrap(),
296 _ => (),
297 }
298 }
299
300 if repetitions != 1 {
301 write!(output, r#"<block s="doRepeat"><l>{repetitions}</l><script>"#).unwrap();
302 }
303
304 for content in section.iter() {
305 match content {
306 SectionContent::Staff(staff) => translate_staff(staff, output, context)?,
307 SectionContent::Section(section) => translate_section(section, output, context)?,
308 }
309 }
310
311 if repetitions != 1 {
312 write!(output, r#"</script></block>"#).unwrap();
313 }
314
315 assert!(context.sections.remove(&(section as *const _)));
316 Ok(())
317}
318fn translate_part(part: &Part, output: &mut String, context: &mut Context) -> Result<(), TranslateError> {
319 let name = xml_escape(part.get_name());
320 let instrument = match part.get_name().to_lowercase().as_str() {
321 x if x.contains("synth") => "Synthesizer",
322 x if x.contains("bassoon") => "Bassoon",
323 x if x.contains("bass") => "Electric Bass",
324 x if x.contains("cello") => "Cello",
325 x if x.contains("guitar") => match x {
326 x if x.contains("elec") => "Electric Guitar",
327 x if x.contains("nylon") => "Nylon Guitar",
328 _ => "Acoustic Guitar",
329 }
330 x if x.contains("harp") => "Harp",
331 x if x.contains("organ") => "Pipe Organ",
332 x if x.contains("violin") => "Violin",
333 _ => "Grand Piano",
334 };
335
336 write!(output, r#"<sprite name="{name}" x="0" y="0" heading="90" scale="1" volume="100" pan="0" rotation="1" draggable="true" costume="0" color="80,80,80,1" pen="tip"><costumes><list struct="atomic"></list></costumes><sounds><list struct="atomic"></list></sounds><blocks></blocks><variables></variables><scripts>"#).unwrap();
337
338 write!(output, r#"<script x="0" y="0"><block s="receiveGo"></block>"#).unwrap();
339 write!(output, r#"<block s="setInstrument"><l>{instrument}</l></block>"#).unwrap();
340 write!(output, r#"<block s="setBPM"><l>{tempo}</l></block>"#, tempo = quarter_note_tempo(&context.starting_tempo)).unwrap();
341 write!(output, r#"<block s="setKey"><l>{key_sig:?}{key_mode:?}</l></block>"#, key_sig = context.starting_key.signature, key_mode = context.starting_key.mode).unwrap();
342
343 for content in part.iter() {
344 debug_assert!(context.modifiers.stack.is_empty() && context.modifiers.active.is_empty());
345 match content {
346 PartContent::Section(section) => {
347 let block_name = iter::once(String::new()).chain((2usize..).map(|x| format!(" {x}"))).map(|x| format!("{}{x}", section.get_name())).find(|x| !context.blocks.contains_key(x)).unwrap();
348 let mut block_def = format!(r#"<block-definition s={block_name:?} type="command" category="music"><inputs></inputs><script>"#);
349 translate_section(section, &mut block_def, context)?;
350 context.modifiers.set(&Default::default(), &mut block_def);
351 write!(block_def, "</script></block-definition>").unwrap();
352 write!(output, r#"<custom-block s={block_name:?}></custom-block>"#).unwrap();
353 context.blocks.insert(block_name, block_def);
354 }
355 }
356 }
357
358 write!(output, r#"</script></scripts></sprite>"#).unwrap();
359 Ok(())
360}
361pub fn translate(composition: &Composition) -> Result<String, TranslateError> {
362 let composition = composition.restructure_staves_as_parts().flatten();
363 let title = xml_escape(composition.get_title());
364 let tempo = quarter_note_tempo(composition.get_tempo());
365
366 let stringify_list = |x: &[String]| if !x.is_empty() { x.join(", ") } else { "N/A".into() };
367 let notes = xml_escape(&format!("title: {title}\ncomposers: {composers}\nlyricists: {lyricists}\narrangers: {arrangers}\npublisher: {publisher}\ncopyright: {copyright}\n\ntempo: {tempo}\ntime signature: {time_signature}\nkey: {key_sig:?}{key_mode:?}",
368 title = composition.get_title(),
369 composers = stringify_list(composition.get_composers()),
370 lyricists = stringify_list(composition.get_lyricists()),
371 arrangers = stringify_list(composition.get_arrangers()),
372 publisher = stringify_list(composition.get_publisher().as_slice()),
373 copyright = stringify_list(composition.get_copyright().as_slice()),
374 time_signature = composition.get_starting_time_signature(),
375 key_sig = composition.get_starting_key().signature,
376 key_mode = composition.get_starting_key().mode,
377 ));
378
379 let mut res = String::new();
380 write!(res, r#"<room name="{title}"><role name="myRole"><project name="myRole"><notes>{notes}</notes><stage name="Stage" width="480" height="360" costume="0" color="255,255,255,1" tempo="{tempo}" threadsafe="false" penlog="false" volume="100" pan="0" lines="round" ternary="false" hyperops="true" codify="false" inheritance="false" sublistIDs="false" scheduled="false"><costumes><list struct="atomic"></list></costumes><sounds><list struct="atomic"></list></sounds><variables></variables><blocks></blocks><messageTypes><messageType><name>message</name><fields><field>msg</field></fields></messageType></messageTypes><scripts></scripts><sprites>"#).unwrap();
381
382 let mut context = Context {
383 modifiers: <_>::default(),
384 sections: <_>::default(),
385 phrases: <_>::default(),
386 staffs: <_>::default(),
387 starting_key: *composition.get_starting_key(),
388 starting_tempo: *composition.get_tempo(),
389 blocks: <_>::default(),
390 };
391 for part in composition.iter() {
392 translate_part(part, &mut res, &mut context)?;
393 }
394
395 write!(res, r#"</sprites></stage><blocks>"#).unwrap();
396 for block_def in context.blocks.values() {
397 res += block_def.as_str();
398 }
399 write!(res, r#"</blocks><variables></variables></project><media name="myRole"></media></role></room>"#).unwrap();
400
401 Ok(res)
402}