amm_sdk_netsblox/
lib.rs

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; // re-export for lib users
15
16use 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("&lt;"),
28            '>' => result.push_str("&gt;"),
29            '\'' => result.push_str("&apos;"),
30            '"' => result.push_str("&quot;"),
31            '\n' => result.push_str("&#xD;"),
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    // in the future, beatblox will support grace notes - but for now, just ignore them
180    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}