Skip to main content

music21_rs/stream/
opus.rs

1//! The scores a multi-work file holds: music21's `stream.Opus`.
2//!
3//! An ABC or Essen collection is one file holding many tunes, read into one
4//! stream of kind [`StreamKind::Opus`] whose elements are scores. These
5//! methods look a score up in it and merge the scores that are really one
6//! work's parts.
7
8use super::{Stream, StreamKind};
9use crate::error::{Error, Result};
10use crate::makenotation::make_notation;
11use crate::metadata::{Metadata, MetadataQuery};
12
13impl Stream {
14    /// The scores this opus holds: music21's `Opus.scores`.
15    pub fn scores(&self) -> Vec<&Stream> {
16        self.streams_of_kind(StreamKind::Score)
17    }
18
19    /// Each score's number, as its metadata gives it: music21's
20    /// `Opus.getNumbers`. A score whose metadata says nothing has none,
21    /// where music21 raises on metadata it does not have.
22    pub fn score_numbers(&self) -> Vec<Option<String>> {
23        self.scores()
24            .into_iter()
25            .map(|score| {
26                score
27                    .metadata()
28                    .and_then(|metadata| metadata.string_value("number"))
29            })
30            .collect()
31    }
32
33    /// The first score whose number a query matches: music21's
34    /// `Opus.getScoreByNumber`.
35    ///
36    /// ```
37    /// use music21_rs::metadata::{Metadata, MetadataQuery};
38    /// use music21_rs::{Stream, StreamKind};
39    ///
40    /// let mut opus = Stream::with_kind(StreamKind::Opus);
41    /// for number in ["1", "2"] {
42    ///     let mut score = Stream::with_kind(StreamKind::Score);
43    ///     let mut metadata = Metadata::new();
44    ///     metadata.add_text("number", number);
45    ///     metadata.add_text("title", format!("Tune {number}"));
46    ///     score.set_metadata(Some(metadata));
47    ///     opus.push(score);
48    /// }
49    ///
50    /// let second = opus.score_by_number(&MetadataQuery::Text("2")).unwrap();
51    /// assert_eq!(second.metadata().unwrap().title().as_deref(), Some("Tune 2"));
52    /// ```
53    pub fn score_by_number(&self, query: &MetadataQuery<'_>) -> Option<&Stream> {
54        self.score_by_field(query, "number")
55    }
56
57    /// The first score whose title a query matches: music21's
58    /// `Opus.getScoreByTitle`. music21 reads a regular expression out of a
59    /// title query that holds one; here that is a
60    /// [`MetadataQuery::Matcher`] the caller builds.
61    pub fn score_by_title(&self, query: &MetadataQuery<'_>) -> Option<&Stream> {
62        self.score_by_field(query, "title")
63    }
64
65    /// The first score whose metadata matches a query in one field.
66    fn score_by_field(&self, query: &MetadataQuery<'_>, field: &str) -> Option<&Stream> {
67        self.scores().into_iter().find(|score| {
68            score
69                .metadata()
70                .is_some_and(|metadata| metadata.search(query, Some(field)).is_some())
71        })
72    }
73
74    /// One score of the opus's scores laid out as its parts: music21's
75    /// `Opus.mergeScores`, for a collection whose scores are each one part
76    /// of the same work.
77    ///
78    /// Each score's first part is written out as notation and inserted at
79    /// the start of the new score, which takes the first title and the
80    /// first composer any of them names.
81    ///
82    /// # Errors
83    ///
84    /// A score holding no part, which music21 raises on as well, or a part
85    /// that cannot be written as notation.
86    pub fn merge_scores(&self) -> Result<Stream> {
87        let mut merged = Stream::with_kind(StreamKind::Score);
88        let mut metadata = Metadata::new();
89        for score in self.scores() {
90            let Some(part) = score.parts().first().copied() else {
91                return Err(Error::Stream(
92                    "an opus score has no part to merge".to_string(),
93                ));
94            };
95            // music21 assumes the one part, and writes it out as notation.
96            let mut part = part.clone();
97            make_notation(&mut part)?;
98            merged.insert(0.0, part);
99
100            let Some(found) = score.metadata() else {
101                continue;
102            };
103            // The first of each is kept, as music21 keeps it.
104            if metadata.get("title").is_empty()
105                && let Some(title) = found.string_value("title")
106            {
107                metadata.add_text("title", title);
108            }
109            if metadata.get("composer").is_empty()
110                && let Some(composer) = found.string_value("composer")
111            {
112                metadata.add_contributor("composer", composer);
113            }
114        }
115        merged.set_metadata(Some(metadata));
116        Ok(merged)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::defaults::FloatType;
124    use crate::metadata::MetadataQuery;
125    use crate::note::Note;
126    use crate::{Duration, TimeSignature};
127
128    fn tune(number: &str, title: &str, composer: Option<&str>) -> Stream {
129        let mut score = Stream::with_kind(StreamKind::Score);
130        let mut metadata = Metadata::new();
131        metadata.add_text("number", number);
132        metadata.add_text("title", title);
133        if let Some(composer) = composer {
134            metadata.add_contributor("composer", composer);
135        }
136        score.set_metadata(Some(metadata));
137        let mut part = Stream::with_kind(StreamKind::Part);
138        part.insert(0.0, TimeSignature::new(2, 4).expect("2/4 is a meter"));
139        for (place, name) in ["C4", "D4", "E4", "F4"].iter().enumerate() {
140            let note = Note::from_name(name)
141                .expect("a note name")
142                .with_duration(Duration::eighth());
143            part.insert(place as FloatType * 0.5, note);
144        }
145        score.push(part);
146        score
147    }
148
149    fn opus() -> Stream {
150        let mut opus = Stream::with_kind(StreamKind::Opus);
151        opus.push(tune("1", "Vrienden, kommt alle gaere", Some("Anonymous")));
152        opus.push(tune("2", "O Venus bant", None));
153        opus
154    }
155
156    #[test]
157    fn an_opus_names_the_numbers_its_scores_carry() {
158        assert_eq!(
159            opus().score_numbers(),
160            [Some("1".to_string()), Some("2".to_string())]
161        );
162        assert_eq!(Stream::with_kind(StreamKind::Opus).score_numbers(), []);
163    }
164
165    #[test]
166    fn a_score_is_found_by_its_number_and_by_its_title() {
167        let opus = opus();
168        let second = opus
169            .score_by_number(&MetadataQuery::Text("2"))
170            .expect("the second tune");
171        assert_eq!(
172            second.metadata().and_then(Metadata::title).as_deref(),
173            Some("O Venus bant")
174        );
175        let first = opus
176            .score_by_title(&MetadataQuery::Text("kommt"))
177            .expect("the first tune");
178        assert_eq!(
179            first.metadata().and_then(Metadata::title).as_deref(),
180            Some("Vrienden, kommt alle gaere")
181        );
182        assert!(opus.score_by_number(&MetadataQuery::Text("9")).is_none());
183    }
184
185    #[test]
186    fn a_matcher_stands_in_for_music21s_regular_expression() {
187        let opus = opus();
188        let kommt = |text: &str| text.starts_with("Vrienden") && text.ends_with("gaere");
189        let found = opus
190            .score_by_title(&MetadataQuery::Matcher(&kommt))
191            .expect("the first tune");
192        assert_eq!(
193            found.metadata().and_then(Metadata::title).as_deref(),
194            Some("Vrienden, kommt alle gaere")
195        );
196    }
197
198    #[test]
199    fn merging_the_scores_makes_one_score_of_parts() {
200        let merged = opus().merge_scores().expect("two parts to merge");
201        assert_eq!(merged.kind(), StreamKind::Score);
202        assert_eq!(merged.parts().len(), 2);
203        let metadata = merged.metadata().expect("the merged metadata");
204        assert_eq!(
205            metadata.title().as_deref(),
206            Some("Vrienden, kommt alle gaere")
207        );
208        assert_eq!(
209            metadata.string_value("composer").as_deref(),
210            Some("Anonymous")
211        );
212        // Each part was written out as notation: music21's makeNotation.
213        assert_eq!(merged.parts()[0].measures().len(), 1);
214    }
215
216    #[test]
217    fn a_score_without_a_part_cannot_be_merged() {
218        let mut opus = Stream::with_kind(StreamKind::Opus);
219        opus.push(Stream::with_kind(StreamKind::Score));
220        assert!(opus.merge_scores().is_err());
221    }
222}