Skip to main content

music21_rs/braille/
translate.rs

1//! Scores, parts and measures in braille: music21's `braille.translate`.
2
3use super::basic::word_to_braille;
4use super::lookup;
5use super::segment::{BrailleGrandSegment, Hand, SegmentOptions, find_segments};
6use crate::{
7    error::{Error, Result},
8    metadata::Metadata,
9    stream::{Stream, StreamElement, StreamKind},
10};
11
12/// How music21's braille translation is asked to write: its keyword
13/// arguments.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct BrailleOptions {
16    /// Whether the stream is written as it stands, or first given the
17    /// notation it leaves unsaid as music21's `makeNotation` gives it.
18    pub in_place: bool,
19    /// Whether each segment is written as music21's English listing of its
20    /// groupings instead of braille.
21    pub debug: bool,
22    /// The options each segment is written with.
23    pub segment: SegmentOptions,
24}
25
26/// A score, part or measure in braille, as music21's `objectToBraille`
27/// writes one: a score part by part, two piano staves side by side, a part
28/// segment by segment.
29///
30/// # Errors
31///
32/// A stream of another kind, or music that music21 cannot write.
33pub fn stream_to_braille(stream: &Stream, options: &BrailleOptions) -> Result<String> {
34    match stream.kind() {
35        StreamKind::Part | StreamKind::PartStaff => part_to_braille(stream, options),
36        StreamKind::Measure => measure_to_braille(stream, options),
37        StreamKind::Score => score_to_braille(stream, options),
38        StreamKind::Opus => {
39            let scores: Vec<String> = stream
40                .events()
41                .iter()
42                .filter_map(|event| event.element().as_stream())
43                .filter(|inner| inner.kind() == StreamKind::Score)
44                .map(|score| score_to_braille(score, options))
45                .collect::<Result<_>>()?;
46            Ok(scores.join("\n\n"))
47        }
48        _ => {
49            let staves: Vec<&Stream> = stream
50                .events()
51                .iter()
52                .filter_map(|event| event.element().as_stream())
53                .filter(|inner| inner.kind() == StreamKind::PartStaff)
54                .collect();
55            if staves.len() == 2 {
56                score_to_braille(stream, options)
57            } else {
58                Err(Error::Notation(
59                    "Stream cannot be translated to Braille.".to_string(),
60                ))
61            }
62        }
63    }
64}
65
66/// A score in braille: its metadata, then each part, a piano's two staves
67/// together: music21's `scoreToBraille`.
68///
69/// # Errors
70///
71/// Music music21 cannot write.
72pub fn score_to_braille(score: &Stream, options: &BrailleOptions) -> Result<String> {
73    let mut lines = Vec::new();
74    if let Some(metadata) = score.metadata() {
75        lines.push(metadata_to_string(metadata, !options.debug)?);
76    }
77    let mut pending: Option<Stream> = None;
78    let mut first_leaf = 0;
79    for event in score.events() {
80        let leaves = match event.element() {
81            StreamElement::Stream(inner) => inner.leaves().len(),
82            _ => 1,
83        };
84        let start = first_leaf;
85        first_leaf += leaves;
86        let Some(part) = event.element().as_stream() else {
87            continue;
88        };
89        let part = &with_own_spanners(score, part, start);
90        match part.kind() {
91            StreamKind::PartStaff => {
92                if let Some(upper) = pending.take() {
93                    lines.push(keyboard_parts_to_braille(&upper, part, options)?);
94                } else {
95                    pending = Some(part.clone());
96                }
97            }
98            StreamKind::Part => {
99                if let Some(alone) = pending.take() {
100                    lines.push(part_to_braille(&alone, options)?);
101                }
102                lines.push(part_to_braille(part, options)?);
103            }
104            _ => {}
105        }
106    }
107    if let Some(alone) = pending {
108        lines.push(part_to_braille(&alone, options)?);
109    }
110    Ok(lines.join("\n"))
111}
112
113/// A part of a score with the spanners music21 keeps in it: those of the
114/// score that start and end among the part's elements, braille reading a
115/// slur by its ends alone. music21 keeps every spanner of a part written on
116/// several staves on its first staff, so a later staff has none. `start` is
117/// where the part's leaves start among the score's.
118fn with_own_spanners(score: &Stream, part: &Stream, start: usize) -> Stream {
119    let mut part = part.clone();
120    let later_staff = part.kind() == StreamKind::PartStaff
121        && part
122            .id()
123            .and_then(|id| id.rsplit_once("-Staff"))
124            .is_some_and(|(_, number)| number != "1");
125    if later_staff {
126        return part;
127    }
128    let end = start + part.leaves().len();
129    let inside = |place: Option<&Option<usize>>| {
130        place.is_some_and(|place| place.is_some_and(|place| (start..end).contains(&place)))
131    };
132    for spanner in score.spanners() {
133        let places = spanner.spanned();
134        if inside(places.first()) && inside(places.last()) {
135            let mut own = spanner.clone();
136            let moved: Vec<Option<usize>> =
137                (0..end).map(|place| place.checked_sub(start)).collect();
138            own.move_places(&moved);
139            part.add_spanner(own);
140        }
141    }
142    part
143}
144
145/// A score's metadata, a line each sorted, as `Name: value` or its braille:
146/// music21's `metadataToString`.
147///
148/// # Errors
149///
150/// A character braille has no sign for.
151pub fn metadata_to_string(metadata: &Metadata, braille: bool) -> Result<String> {
152    let mut lines = Vec::new();
153    for (unique_name, value) in metadata.all() {
154        if unique_name == "software" {
155            continue;
156        }
157        let Some(namespace) = crate::metadata::namespace_name(unique_name) else {
158            continue;
159        };
160        if namespace.starts_with("m21FileInfo:") {
161            continue;
162        }
163        let words = split_camel_case(unique_name);
164        let mut line = format!("{}: {}", title_case(&words.join(" ")), value.text());
165        if braille {
166            let written: Vec<String> = line
167                .split_whitespace()
168                .map(|word| word_to_braille(word, false))
169                .collect::<Result<_>>()?;
170            line = written.join(&lookup::alphabet(' ').unwrap_or_default());
171        }
172        lines.push(line);
173    }
174    lines.sort();
175    Ok(lines.join("\n"))
176}
177
178/// The words of a camel-cased name, as music21 finds them with
179/// `([A-Z]*[a-z]+)`.
180fn split_camel_case(name: &str) -> Vec<String> {
181    let characters: Vec<char> = name.chars().collect();
182    let mut words = Vec::new();
183    let mut index = 0;
184    while index < characters.len() {
185        let start = index;
186        let mut upper_end = index;
187        while upper_end < characters.len() && characters[upper_end].is_ascii_uppercase() {
188            upper_end += 1;
189        }
190        let mut lower_end = upper_end;
191        while lower_end < characters.len() && characters[lower_end].is_ascii_lowercase() {
192            lower_end += 1;
193        }
194        if lower_end > upper_end {
195            words.push(characters[start..lower_end].iter().collect());
196            index = lower_end;
197        } else {
198            index = start + 1;
199        }
200    }
201    words
202}
203
204/// Text in title case as Python's `str.title` writes it: each run of
205/// letters capitalised.
206fn title_case(text: &str) -> String {
207    let mut out = String::new();
208    let mut previous_letter = false;
209    for character in text.chars() {
210        if character.is_alphabetic() {
211            if previous_letter {
212                out.extend(character.to_lowercase());
213            } else {
214                out.extend(character.to_uppercase());
215            }
216            previous_letter = true;
217        } else {
218            out.push(character);
219            previous_letter = false;
220        }
221    }
222    out
223}
224
225/// A copy of a part with the notation it leaves unsaid made, as music21's
226/// `makeNotation(cautionaryNotImmediateRepeat=False)` makes it.
227fn notated(part: &Stream, options: &BrailleOptions) -> Result<Stream> {
228    let mut part = part.clone();
229    if !options.in_place {
230        crate::makenotation::make_part_notation_keeping_spanners(&mut part, false)?;
231    }
232    Ok(part)
233}
234
235/// A part in braille, segment by segment: music21's `partToBraille`.
236///
237/// # Errors
238///
239/// Music music21 cannot write.
240pub fn part_to_braille(part: &Stream, options: &BrailleOptions) -> Result<String> {
241    let part = notated(part, options)?;
242    let mut segments = find_segments(&part, None, options.segment);
243    let mut written = Vec::new();
244    for segment in &mut segments {
245        let braille = segment.transcribe()?;
246        written.push(if options.debug {
247            segment.english()
248        } else {
249            braille
250        });
251    }
252    Ok(written.join("\n"))
253}
254
255/// A measure in braille, as the one measure of a part with no heading or
256/// measure number unless asked: music21's `measureToBraille`.
257///
258/// # Errors
259///
260/// Music music21 cannot write.
261pub fn measure_to_braille(measure: &Stream, options: &BrailleOptions) -> Result<String> {
262    let mut measure = measure.clone();
263    if !options.in_place {
264        crate::makenotation::make_part_notation_keeping_spanners(&mut measure, false)?;
265    }
266    let mut part = Stream::with_kind(StreamKind::Part);
267    part.push(measure);
268    part_to_braille(
269        &part,
270        &BrailleOptions {
271            in_place: true,
272            ..*options
273        },
274    )
275}
276
277/// A piano's two staves in braille, side by side measure by measure:
278/// music21's `keyboardPartsToBraille`.
279///
280/// # Errors
281///
282/// Music music21 cannot write.
283pub fn keyboard_parts_to_braille(
284    upper: &Stream,
285    lower: &Stream,
286    options: &BrailleOptions,
287) -> Result<String> {
288    let upper = notated(upper, options)?;
289    let lower = notated(lower, options)?;
290    let right = find_segments(&upper, Some(Hand::Right), options.segment);
291    let left = find_segments(&lower, Some(Hand::Left), options.segment);
292    let mut written = Vec::new();
293    for (right, left) in right.into_iter().zip(left) {
294        let mut grand = BrailleGrandSegment {
295            groupings: right.groupings.into_iter().chain(left.groupings).collect(),
296            line_length: options.segment.max_line_length,
297            right_contexts: right.contexts,
298            left_contexts: left.contexts,
299        };
300        let braille = grand.transcribe()?;
301        written.push(if options.debug {
302            grand.english()
303        } else {
304            braille
305        });
306    }
307    Ok(written.join("\n"))
308}
309
310/// An element that is not a stream, in braille as the one element of a
311/// measure: music21's `objectToBraille` given one.
312///
313/// # Errors
314///
315/// Music music21 cannot write.
316pub fn element_to_braille(element: StreamElement, options: &BrailleOptions) -> Result<String> {
317    if element.as_stream().is_some() {
318        return Err(Error::Notation(
319            "Stream cannot be translated to Braille.".to_string(),
320        ));
321    }
322    let mut measure = Stream::with_kind(StreamKind::Measure);
323    measure.push(element);
324    let mut part = Stream::with_kind(StreamKind::Part);
325    part.push(measure);
326    let mut segment = options.segment;
327    segment.show_first_measure_number = false;
328    segment.show_heading = false;
329    part_to_braille(
330        &part,
331        &BrailleOptions {
332            in_place: true,
333            segment,
334            ..*options
335        },
336    )
337}