Skip to main content

music21_rs/
tinynotation.rs

1//! TinyNotation: music21's `tinyNotation`, a line of notes written as
2//! letters and numbers.
3//!
4//! `4/4 c4 d8 e f#2 trip{g8 a b} c'1` is a bar of quarter, two eighths and a
5//! half, then a triplet and a whole note an octave up. A capital letter is
6//! the octave below middle C and each letter more an octave lower; a small
7//! one is the octave from middle C and each `'` an octave higher. A number
8//! is the note value, kept until the next one; `r` is a rest; `~` ties a
9//! note to the next; `_word` is a lyric.
10
11use crate::defaults::{FloatType, IntegerType};
12use crate::duration::{Duration, DurationType, Tuplet, TupletType};
13use crate::error::{Error, Result};
14use crate::expressions::{Expression, Fermata};
15use crate::makenotation::{make_measures, op_frac};
16use crate::meter::TimeSignature;
17use crate::notation::{Tie, TieType};
18use crate::note::Note;
19use crate::pitch::{Accidental, PitchOptions};
20use crate::rest::Rest;
21use crate::stream::{Stream, StreamElement, StreamEvent, StreamKind};
22
23/// Something a bracket or a tie holds open over the tokens after it.
24enum State {
25    /// `trip{` or `quad{`: the notes inside are so many in the time of so
26    /// many.
27    Tuplet {
28        actual: u32,
29        normal: u32,
30        tokens: Vec<usize>,
31    },
32    /// `~`: this note and the next are tied.
33    Tie { tokens: Vec<usize> },
34}
35
36fn is_modifier_free(character: char) -> bool {
37    !character.is_whitespace() && character != '_'
38}
39
40/// Every place the modifiers starting at `at` may end, as music21's
41/// modifier pattern has it, down to the end of the token.
42fn modifiers_reach_end(text: &[char], at: usize) -> bool {
43    if at == text.len() {
44        return true;
45    }
46    let closed_by = |close: char| -> bool {
47        (at + 1..text.len())
48            .filter(|index| text[*index] == close)
49            .any(|index| modifiers_reach_end(text, index + 1))
50    };
51    match text[at] {
52        '=' => {
53            let mut end = at + 1;
54            while end < text.len() && is_modifier_free(text[end]) {
55                end += 1;
56            }
57            (at + 1..=end)
58                .rev()
59                .any(|stop| modifiers_reach_end(text, stop))
60        }
61        '*' => closed_by('*'),
62        '<' => closed_by('>'),
63        '(' => closed_by(')'),
64        '[' => closed_by(']'),
65        '_' => {
66            at + 1 < text.len()
67                && !text[at + 1].is_whitespace()
68                && text[at + 1] != '='
69                && modifiers_reach_end(text, at + 2)
70        }
71        _ => false,
72    }
73}
74
75/// Where a duration written at `at` may end: a number and dots, dots and a
76/// number, dots alone, or nothing.
77fn duration_ends(text: &[char], at: usize) -> Vec<usize> {
78    let run = |from: usize, test: fn(&char) -> bool| -> usize {
79        from + text[from..].iter().take_while(|c| test(c)).count()
80    };
81    let digit: fn(&char) -> bool = |c| c.is_ascii_digit();
82    let dot: fn(&char) -> bool = |c| *c == '.';
83    let mut ends = vec![at];
84    let digits = run(at, digit);
85    if digits > at {
86        for stop in at + 1..=digits {
87            for dots in stop..=run(stop, dot) {
88                ends.push(dots);
89            }
90        }
91    }
92    let dots = run(at, dot);
93    if dots > at {
94        for stop in at + 1..=dots {
95            ends.push(stop);
96            for number in stop + 1..=run(stop, digit) {
97                ends.push(number);
98            }
99        }
100    }
101    ends
102}
103
104/// Where an accidental at `at` may end: sharps, flats or an `n`, bare or in
105/// parentheses, or nothing.
106fn accidental_ends(text: &[char], at: usize) -> Vec<usize> {
107    let mut ends = vec![at];
108    let bare = |from: usize| -> Vec<usize> {
109        match text.get(from) {
110            Some('n') => vec![from + 1],
111            Some(sign @ ('#' | '-')) => {
112                let run = text[from..].iter().take_while(|c| *c == sign).count();
113                (from + 1..=from + run).collect()
114            }
115            _ => Vec::new(),
116        }
117    };
118    ends.extend(bare(at));
119    if text.get(at) == Some(&'(') {
120        ends.extend(
121            bare(at + 1)
122                .into_iter()
123                .filter(|end| text.get(*end) == Some(&')'))
124                .map(|end| end + 1),
125        );
126    }
127    ends
128}
129
130/// Where a note's name may end.
131fn name_ends(text: &[char]) -> Vec<usize> {
132    let Some(first) = text.first() else {
133        return Vec::new();
134    };
135    if ('A'..='G').contains(first) {
136        let run = text.iter().take_while(|c| *c == first).count();
137        return (1..=run)
138            .flat_map(|stop| accidental_ends(text, stop))
139            .collect();
140    }
141    if ('a'..='g').contains(first) {
142        let ticks = |from: usize| text[from..].iter().take_while(|c| **c == '\'').count();
143        let mut ends = Vec::new();
144        for after in accidental_ends(text, 1) {
145            for stop in after..=after + ticks(after) {
146                ends.push(stop);
147            }
148        }
149        for stop in 1..=1 + ticks(1) {
150            ends.extend(accidental_ends(text, stop));
151        }
152        return ends;
153    }
154    Vec::new()
155}
156
157/// Whether the text is a note as music21's token pattern reads one.
158fn is_note_token(text: &[char]) -> bool {
159    name_ends(text).into_iter().any(|name| {
160        duration_ends(text, name)
161            .into_iter()
162            .any(|end| modifiers_reach_end(text, end))
163    })
164}
165
166/// Whether the text after an `r` is what a rest may carry.
167fn is_rest_body(text: &[char]) -> bool {
168    duration_ends(text, 0)
169        .into_iter()
170        .any(|end| modifiers_reach_end(text, end))
171}
172
173fn is_meter_token(text: &str) -> bool {
174    text.split_once('/').is_some_and(|(top, bottom)| {
175        !top.is_empty()
176            && !bottom.is_empty()
177            && top.chars().all(|c| c.is_ascii_digit())
178            && bottom.chars().all(|c| c.is_ascii_digit())
179    })
180}
181
182/// The first run of characters passing a test, as a search for `(x+)`
183/// finds it.
184fn first_run(text: &str, test: impl Fn(char) -> bool) -> Option<&str> {
185    let start = text.find(&test)?;
186    let rest = &text[start..];
187    let end = rest.find(|c| !test(c)).unwrap_or(rest.len());
188    Some(&rest[..end])
189}
190
191fn without(text: &str, test: impl Fn(char) -> bool) -> String {
192    text.chars().filter(|c| !test(*c)).collect()
193}
194
195/// The reader's state: music21's `Converter`.
196struct Reader {
197    elements: Vec<StreamElement>,
198    meter: Option<TimeSignature>,
199    last_length: FloatType,
200    states: Vec<State>,
201}
202
203fn with_duration(element: &mut StreamElement, change: impl FnOnce(&mut Duration)) {
204    match element {
205        StreamElement::Note(note) => {
206            let mut duration = note.duration().cloned().unwrap_or_else(Duration::quarter);
207            change(&mut duration);
208            note.set_duration(duration);
209        }
210        StreamElement::Rest(rest) => {
211            let mut duration = rest.duration().clone();
212            change(&mut duration);
213            rest.set_duration(duration);
214        }
215        _ => {}
216    }
217}
218
219/// The written value a duration is, a quarter where it is no one value.
220fn written_type(duration: &Duration) -> DurationType {
221    match duration.components().as_slice() {
222        [(kind, _)] => *kind,
223        _ => DurationType::Quarter,
224    }
225}
226
227impl Reader {
228    /// music21's `applyDuration`: the length a token says, or the last one
229    /// said. The second answer is whether a whole-bar fermata was asked for.
230    fn duration_of(&mut self, text: &str) -> Result<Option<(Duration, bool)>> {
231        let mut duration = Duration::quarter();
232        let mut fermata = false;
233        let mut found = false;
234        if let Some(digits) = first_run(text, |c| c.is_ascii_digit()) {
235            found = true;
236            let number: u64 = digits.parse().map_err(|_| {
237                Error::Duration(format!("Cannot parse token with duration {digits}"))
238            })?;
239            if number == 0 {
240                if let Some(meter) = &self.meter {
241                    duration = meter.bar_duration();
242                    fermata = true;
243                }
244            } else {
245                let kind = (number <= 2048 && number.is_power_of_two())
246                    .then(|| {
247                        DurationType::ALL
248                            .into_iter()
249                            .find(|kind| kind.quarter_length() == 4.0 / number as FloatType)
250                    })
251                    .flatten();
252                // A number that is no note value leaves the token unread.
253                let Some(kind) = kind else {
254                    return Ok(None);
255                };
256                duration = Duration::from_type(kind);
257            }
258        }
259        if let Some(dots) = first_run(text, |c| c == '.') {
260            let kind = written_type(&duration);
261            duration = Duration::from_type_with_dots(kind, dots.len() as u32);
262        }
263        if !found {
264            duration = Duration::new(self.last_length)?;
265        }
266        self.last_length = duration.quarter_length();
267        Ok(Some((duration, fermata)))
268    }
269
270    /// music21's `NoteToken.parse`.
271    fn note(&mut self, token: &str) -> Result<Option<StreamElement>> {
272        let mut text = token.to_string();
273        let mut step = 'C';
274        let mut octave: IntegerType = 4;
275        let low = |c: char| ('A'..='G').contains(&c);
276        if let Some(run) = first_run(&text, low) {
277            step = run.chars().next().unwrap_or('C');
278            octave = 4 - run.chars().count() as IntegerType;
279            text = without(&text, low);
280        }
281        let high = |c: char| ('a'..='g').contains(&c);
282        if let Some(start) = text.find(high) {
283            let letter = text[start..].chars().next().unwrap_or('c');
284            let ticks = text[start + 1..].chars().take_while(|c| *c == '\'').count();
285            step = letter.to_ascii_uppercase();
286            octave = 4 + ticks as IntegerType;
287            // Every small letter goes, with the marks after it.
288            let mut kept = String::new();
289            let mut after_letter = false;
290            for character in text.chars() {
291                if high(character) {
292                    after_letter = true;
293                } else if character == '\'' && after_letter {
294                } else {
295                    after_letter = false;
296                    kept.push(character);
297                }
298            }
299            text = kept;
300        }
301        // An accidental in parentheses is editorial, and is not the note's.
302        let mut editorial = false;
303        let signs = |c: char| matches!(c, '#' | '-' | 'n');
304        let chars: Vec<char> = text.chars().collect();
305        let found = (0..chars.len()).find_map(|open| {
306            if chars[open] != '(' {
307                return None;
308            }
309            let run = chars[open + 1..].iter().take_while(|c| signs(**c)).count();
310            (run > 0 && chars.get(open + 1 + run) == Some(&')')).then_some((open, run))
311        });
312        if let Some((open, run)) = found {
313            editorial = true;
314            text = chars[open + 1..open + 1 + run]
315                .iter()
316                .chain(&chars[open + run + 2..])
317                .collect();
318        }
319        let mut alter: Option<IntegerType> = None;
320        for (sign, direction) in [('#', 1), ('-', -1), ('n', 0)] {
321            let is_sign = |c: char| c == sign;
322            if let Some(run) = first_run(&text, is_sign) {
323                let count = if sign == 'n' { 1 } else { run.len() };
324                alter = Some(direction * count as IntegerType);
325                text = without(&text, is_sign);
326            }
327        }
328        let mut pitch = PitchOptions::new().step(step).octave(octave).build()?;
329        if let Some(alter) = alter
330            && !editorial
331        {
332            pitch.set_written_accidental(Some(Accidental::new(alter)?));
333        }
334        let Some((duration, fermata)) = self.duration_of(&text)? else {
335            return Ok(None);
336        };
337        let mut note = Note::from_pitch(pitch).with_duration(duration);
338        if fermata {
339            note.expressions_mut()
340                .push(Expression::Fermata(Fermata::new()));
341        }
342        Ok(Some(note.into()))
343    }
344
345    fn rest(&mut self, body: &str) -> Result<Option<StreamElement>> {
346        let Some((duration, fermata)) = self.duration_of(body)? else {
347            return Ok(None);
348        };
349        let mut rest = Rest::new(duration);
350        if fermata {
351            rest.expressions_mut()
352                .push(Expression::Fermata(Fermata::new()));
353        }
354        Ok(Some(rest.into()))
355    }
356
357    /// What closing a state does: music21's `State.end`.
358    fn end(&mut self, state: State) -> Result<()> {
359        match state {
360            State::Tie { tokens } => {
361                let opened = |tie: Option<&Tie>| match tie.cloned() {
362                    None => Tie::new(TieType::Start),
363                    Some(mut tie) => {
364                        tie.set_tie_type(TieType::Continue);
365                        tie
366                    }
367                };
368                match tokens.first().map(|index| &mut self.elements[*index]) {
369                    Some(StreamElement::Note(first)) => {
370                        let tie = opened(first.tie());
371                        first.set_tie(Some(tie));
372                    }
373                    Some(StreamElement::Rest(first)) => {
374                        let tie = opened(first.tie());
375                        first.set_tie(Some(tie));
376                    }
377                    Some(_) => {
378                        return Err(Error::Meter("a time signature cannot be tied".to_string()));
379                    }
380                    None => {}
381                }
382                match tokens.get(1).map(|index| &mut self.elements[*index]) {
383                    Some(StreamElement::Note(second)) => {
384                        second.set_tie(Some(Tie::new(TieType::Stop)));
385                    }
386                    Some(StreamElement::Rest(second)) => {
387                        second.set_tie(Some(Tie::new(TieType::Stop)));
388                    }
389                    _ => {}
390                }
391            }
392            State::Tuplet { tokens, .. } => {
393                let mark = |element: &mut StreamElement, kind: TupletType| {
394                    with_duration(element, |duration| {
395                        let mut tuplets = duration.tuplets();
396                        if let Some(first) = tuplets.first_mut() {
397                            first.set_tuplet_type(Some(kind));
398                        }
399                        duration.set_tuplets(tuplets);
400                    });
401                };
402                if let (Some(first), Some(last)) = (tokens.first(), tokens.last()) {
403                    mark(&mut self.elements[*first], TupletType::Start);
404                    mark(&mut self.elements[*last], TupletType::Stop);
405                }
406            }
407        }
408        Ok(())
409    }
410
411    /// music21's `parseOne`.
412    fn token(&mut self, token: &str) -> Result<()> {
413        let mut text = token.to_string();
414
415        // Brackets opening: a word and a brace, taken out one at a time.
416        loop {
417            let chars: Vec<char> = text.chars().collect();
418            let Some(brace) = (1..chars.len()).find(|index| {
419                chars[*index] == '{'
420                    && (chars[index - 1].is_alphanumeric() || chars[index - 1] == '_')
421            }) else {
422                break;
423            };
424            let start = (0..brace)
425                .rev()
426                .take_while(|index| chars[*index].is_alphanumeric() || chars[*index] == '_')
427                .last()
428                .unwrap_or(brace);
429            let word: String = chars[start..brace].iter().collect();
430            text = chars[..start].iter().chain(&chars[brace + 1..]).collect();
431            let ratio = match word.as_str() {
432                "trip" => Some((3, 2)),
433                "quad" => Some((4, 3)),
434                // A bracket of no known kind is dropped.
435                _ => None,
436            };
437            if let Some((actual, normal)) = ratio {
438                self.states.push(State::Tuplet {
439                    actual,
440                    normal,
441                    tokens: Vec::new(),
442                });
443            }
444        }
445        if text.contains('~') {
446            text = text.replace('~', "");
447            self.states.push(State::Tie { tokens: Vec::new() });
448        }
449
450        let closing = text.matches('}').count();
451        text = text.replace('}', "");
452
453        // `=name` names the element.
454        let mut id: Option<String> = None;
455        if text.contains('=') {
456            let mut kept = String::new();
457            let mut rest = &text[..];
458            while let Some(at) = rest.find('=') {
459                kept.push_str(&rest[..at]);
460                let after = &rest[at + 1..];
461                let end = after
462                    .find(|c: char| !c.is_ascii_alphanumeric())
463                    .unwrap_or(after.len());
464                id.get_or_insert_with(|| after[..end].to_string());
465                rest = &after[end..];
466            }
467            kept.push_str(rest);
468            text = kept;
469        }
470        let mut lyric: Option<String> = None;
471        if let Some(start) = text.find('_') {
472            lyric = Some(text[start + 1..].to_string());
473            text.truncate(start);
474        }
475
476        let chars: Vec<char> = text.chars().collect();
477        let mut made: Option<StreamElement> = None;
478        if is_meter_token(&text) {
479            let meter = TimeSignature::from_ratio_string(&text)?;
480            self.meter = Some(meter.clone());
481            made = Some(meter.into());
482        } else if chars.first() == Some(&'r') && is_rest_body(&chars[1..]) {
483            made = self.rest(&text[1..])?;
484        }
485        if made.is_none() && is_note_token(&chars) {
486            made = self.note(&text)?;
487        }
488
489        if let Some(mut element) = made {
490            if let (Some(id), StreamElement::Note(note)) = (&id, &mut element) {
491                note.set_id(Some(id.clone()));
492            }
493            if let Some(lyric) = &lyric {
494                match &mut element {
495                    StreamElement::Note(note) => note.set_lyric(Some(lyric))?,
496                    StreamElement::Rest(rest) => rest.set_lyric(Some(lyric)),
497                    _ => {}
498                }
499            }
500            // Only notes and rests count towards a tie or go in a bracket;
501            // a meter passes the open states by.
502            let index = self.elements.len();
503            let counts = !matches!(element, StreamElement::TimeSignature(_));
504            self.elements.push(element);
505            if counts {
506                let mut place = 0;
507                while place < self.states.len() {
508                    let expired = match &mut self.states[place] {
509                        State::Tuplet {
510                            actual,
511                            normal,
512                            tokens,
513                        } => {
514                            tokens.push(index);
515                            let (actual, normal) = (*actual, *normal);
516                            let element = &mut self.elements[index];
517                            let values = element
518                                .duration()
519                                .map_or(1, |duration| duration.components().len());
520                            if values != 1 {
521                                return Err(Error::Duration("Unknown type: complex".to_string()));
522                            }
523                            with_duration(element, |duration| {
524                                let kind = written_type(duration);
525                                duration.append_tuplet(Tuplet::new(actual, normal, kind, 0));
526                            });
527                            false
528                        }
529                        State::Tie { tokens } => {
530                            tokens.push(index);
531                            tokens.len() == 2
532                        }
533                    };
534                    if expired {
535                        let state = self.states.remove(place);
536                        self.end(state)?;
537                    } else {
538                        place += 1;
539                    }
540                }
541            }
542        }
543
544        // A closing brace ends the last bracket opened, not a tie.
545        for _ in 0..closing.min(self.states.len()) {
546            let mut at = self.states.len() - 1;
547            while at > 0 && matches!(self.states[at], State::Tie { .. }) {
548                at -= 1;
549            }
550            let state = self.states.remove(at);
551            self.end(state)?;
552        }
553        Ok(())
554    }
555}
556
557/// Reads a line of TinyNotation into a part of measures: music21's
558/// `converter.parse('tinyNotation: ...')`.
559///
560/// The text may start with `tinyNotation:`. A token that is not a meter, a
561/// note or a rest is passed over, as music21 passes over it. The part is cut
562/// into measures by [`make_measures`]: a line with no meter is in `4/4`,
563/// and a note running past a barline is left whole in the measure it starts
564/// in.
565///
566/// ```
567/// use music21_rs::tinynotation::from_tiny_notation;
568///
569/// let part = from_tiny_notation("tinyNotation: 3/4 E4 r f# g=lastG trip{b-8 a g} c'2.")?;
570/// let measures = part.measures();
571/// assert_eq!(measures.len(), 3);
572/// let names: Vec<String> = measures[1]
573///     .pitches()
574///     .iter()
575///     .map(|pitch| pitch.name_with_octave())
576///     .collect();
577/// // The last note starts in this measure and runs on past its barline.
578/// assert_eq!(names, ["G4", "Bb4", "A4", "G4", "C5"]);
579/// # Ok::<(), music21_rs::Error>(())
580/// ```
581pub fn from_tiny_notation(text: &str) -> Result<Stream> {
582    let text = text.trim_start();
583    let body = match text.split_once(':') {
584        Some((head, body)) if head.eq_ignore_ascii_case("tinynotation") => body,
585        _ => text,
586    };
587    let mut reader = Reader {
588        elements: Vec::new(),
589        meter: None,
590        last_length: 1.0,
591        states: Vec::new(),
592    };
593    for token in body.split_whitespace() {
594        reader.token(token)?;
595    }
596    let mut events = Vec::new();
597    let mut offset = 0.0;
598    for element in reader.elements {
599        let length = element.quarter_length();
600        events.push(StreamEvent::new(offset, element));
601        offset = op_frac(offset + length);
602    }
603    let mut part = Stream::from_events(events);
604    part.set_kind(StreamKind::Part);
605    make_measures(&part)
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    #[test]
613    fn a_closing_brace_ends_the_bracket_rather_than_a_tie() {
614        let part = from_tiny_notation("4/4 trip{c8 d e~} e4 f2").unwrap();
615        let held: Vec<(FloatType, Option<TieType>, usize)> = part
616            .notes()
617            .into_iter()
618            .filter_map(|(_, element)| match element {
619                StreamElement::Note(note) => Some((
620                    note.duration().map_or(0.0, Duration::quarter_length),
621                    note.tie().map(|tie| tie.tie_type()),
622                    note.duration()
623                        .map_or(0, |duration| duration.tuplets().len()),
624                )),
625                _ => None,
626            })
627            .collect();
628        let third = 1.0 / 3.0;
629        assert_eq!(
630            held,
631            [
632                (third, None, 1),
633                (third, None, 1),
634                (third, Some(TieType::Start), 1),
635                (1.0, Some(TieType::Stop), 0),
636                (2.0, None, 0),
637            ]
638        );
639    }
640
641    #[test]
642    fn a_meter_passes_open_states_by() {
643        let part = from_tiny_notation("2/4 trip{c8 d 4/4} e4 f4 g2").unwrap();
644        let lengths: Vec<FloatType> = notes(&part).into_iter().map(|(_, length)| length).collect();
645        let third = 1.0 / 3.0;
646        assert_eq!(lengths, [third, third, 1.0, 1.0, 2.0]);
647    }
648
649    fn notes(part: &Stream) -> Vec<(String, FloatType)> {
650        part.notes()
651            .into_iter()
652            .filter_map(|(_, element)| match element {
653                StreamElement::Note(note) => Some((
654                    note.pitch().name_with_octave(),
655                    note.duration().map_or(0.0, Duration::quarter_length),
656                )),
657                _ => None,
658            })
659            .collect()
660    }
661
662    #[test]
663    fn letters_and_marks_say_the_octave() {
664        // music21: CC is two octaves below middle C's octave, c'' two above.
665        let part = from_tiny_notation("CC4 C c c' c''").unwrap();
666        let names: Vec<String> = notes(&part).into_iter().map(|(name, _)| name).collect();
667        assert_eq!(names, ["C2", "C3", "C4", "C5", "C6"]);
668    }
669
670    #[test]
671    fn a_length_lasts_until_the_next_one() {
672        let part = from_tiny_notation("4/4 c4 d8 e f#2 g-4. an16").unwrap();
673        assert_eq!(
674            notes(&part),
675            [
676                ("C4".to_string(), 1.0),
677                ("D4".to_string(), 0.5),
678                ("E4".to_string(), 0.5),
679                ("F#4".to_string(), 2.0),
680                ("Gb4".to_string(), 1.5),
681                ("A4".to_string(), 0.25),
682            ]
683        );
684    }
685
686    #[test]
687    fn a_triplet_is_bracketed_from_its_first_note_to_its_last() {
688        let part = from_tiny_notation("2/4 trip{c8 d e} f4").unwrap();
689        let measure = part.measures()[0];
690        let tuplets: Vec<Option<TupletType>> = measure
691            .events()
692            .iter()
693            .filter_map(|event| match event.element() {
694                StreamElement::Note(note) => note
695                    .duration()
696                    .map(|duration| duration.tuplets().first().and_then(Tuplet::tuplet_type)),
697                _ => None,
698            })
699            .collect();
700        assert_eq!(
701            tuplets,
702            [Some(TupletType::Start), None, Some(TupletType::Stop), None]
703        );
704        assert!((measure.end_offset() - 2.0).abs() < 1e-9);
705    }
706
707    #[test]
708    fn a_tie_joins_a_note_to_the_next() {
709        let part = from_tiny_notation("c2~ c~ c4 d").unwrap();
710        let ties: Vec<Option<TieType>> = part
711            .notes()
712            .into_iter()
713            .filter_map(|(_, element)| match element {
714                StreamElement::Note(note) => Some(note.tie().map(Tie::tie_type)),
715                _ => None,
716            })
717            .collect();
718        assert_eq!(
719            ties,
720            [
721                Some(TieType::Start),
722                Some(TieType::Continue),
723                Some(TieType::Stop),
724                None
725            ]
726        );
727    }
728
729    #[test]
730    fn what_is_no_token_is_passed_over() {
731        let part = from_tiny_notation("c4 hello d r4 3").unwrap();
732        assert_eq!(notes(&part).len(), 2);
733    }
734}