2 lyric-phrasing-engraver.cc -- implement Lyric_phrasing_engraver
4 source file of the GNU LilyPond music typesetter
6 (c) 2000 Glen Prideaux <glenprideaux@iname.com>
10 #include "lyric-phrasing-engraver.hh"
11 #include "note-head.hh"
12 #include "translator-group.hh"
16 String get_context_id(Translator_group * ancestor, const char * type);
17 String trim_suffix(String &id);
19 ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
22 TODO: this code is too hairy, and does things that should be in the
29 shared lyrics should be vertically centered:
33 > About lyrics, it happens that there are common words for many bars, like
34 > for a refrain say. When there is an even number of lyrics lines, I do not
35 > know how to force the positioning of the common lyric line in the plain
36 > middle of the others, because this is in between lines. Not a big matter,
37 > but it would be a bit nicer if this was doable.
42 We find start and end of phrases, and align lyrics accordingly.
43 Also, lyrics at start of melismata should be left aligned.
45 Alignment and melismata
47 I've taken [a different] approach:
50 O O <-- second note throws a melisma score element
57 Lyric_phrasing_engraver keeps track of the current and previous notes and
58 lyrics for each voice, and when it catches a melisma, it adjusts the
59 alignment of the lyrics of the previous note. I hope this isn't
60 unnecessarily convoluted.
63 Lyric_phrasing_engraver::Lyric_phrasing_engraver()
65 voice_alist_ = SCM_EOL;
69 Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
72 No need to delete alist_; that's what Garbage collection is for.
77 Lyric_phrasing_engraver::finalize ()
80 but do need to unprotect alist_, since Engravers are gc'd now.
83 voice_alist_ = SCM_EOL;
88 Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
90 SCM key = ly_str02scm(context_id.ch_C());
91 if( ! gh_null_p(voice_alist_) ) {
92 SCM s = scm_assoc(key, voice_alist_);
93 if(! (gh_boolean_p(s) && !to_boolean(s))) {
95 // ( key . ( (alist_entry . old_entry) . previous_entry) )
96 if(to_boolean(gh_cdadr(s))) { // it's an old entry ... make it a new one
97 SCM val = gh_cons(gh_cons(gh_caadr(s), SCM_BOOL_F), gh_cddr(s));
98 voice_alist_ = scm_assoc_set_x(voice_alist_, gh_car(s), val);
99 return unsmob_voice_entry (gh_caar(val));
101 else { // the entry is current ... return it.
102 SCM entry_scm = gh_caadr(s);
103 return unsmob_voice_entry(entry_scm);
107 // ( ( alist_entry . old_entry ) . previous_entry )
108 SCM val = gh_cons(gh_cons(Syllable_group::make_entry (), SCM_BOOL_F),
109 Syllable_group::make_entry ());
111 voice_alist_ = scm_acons(key, val, voice_alist_);
112 return unsmob_voice_entry (gh_caar(val));
117 Lyric_phrasing_engraver::record_notehead(const String &context_id,
120 Syllable_group * v = lookup_context_id(context_id);
121 v->set_notehead(notehead);
123 any_notehead_l_ = notehead;
127 Lyric_phrasing_engraver::record_lyric(const String &context_id, Grob * lyric)
129 Syllable_group * v = lookup_context_id(context_id);
134 Lyric_phrasing_engraver::record_extender(const String &context_id, Grob * extender)
136 SCM key = ly_str02scm(context_id.ch_C());
137 if( ! gh_null_p(voice_alist_) ) {
138 SCM s = scm_assoc(key, voice_alist_);
139 if(! (gh_boolean_p(s) && !to_boolean(s))) {
141 // ( key . ( (alist_entry . old_entry) . previous_entry) )
142 SCM previous_scm = gh_cddr(s);
143 if(previous_scm != SCM_EOL) {
144 Syllable_group * v = unsmob_voice_entry(previous_scm);
145 v->add_extender(extender);
152 Lyric_phrasing_engraver::record_melisma(const String &context_id)
154 Syllable_group * v = lookup_context_id(context_id);
159 Lyric_phrasing_engraver::acknowledge_grob(Grob_info i)
161 SCM p = get_property("automaticPhrasing");
168 if (Note_head::has_interface(h)) {
169 /* caught a note head ... do something with it */
170 /* ... but not if it's a grace note ... */
171 bool grace= to_boolean (i.elem_l_->get_grob_property ("grace"));
172 SCM wg = get_property ("weAreGraceContext");
173 bool wgb = to_boolean (wg);
177 /* what's its Voice context name? */
178 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
179 record_notehead(voice_context_id, h);
181 /* is it in a melisma ? */
182 if(to_boolean(i.origin_trans_l_->get_property("melismaEngraverBusy"))) {
183 record_melisma(voice_context_id);
188 /* now try for a lyric */
189 if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
191 /* what's its LyricsVoice context name? */
192 String voice_context_id;
193 SCM voice_context_scm = i.origin_trans_l_->get_property("associatedVoice");
194 if (gh_string_p (voice_context_scm)) {
195 voice_context_id = ly_scm2string(voice_context_scm);
198 voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricsVoice");
199 voice_context_id = trim_suffix(voice_context_id);
201 record_lyric(voice_context_id, h);
205 /* Catch any extender items and then if we have a melisma,
206 set the RIGHT item of the extender spanner to the melismatic note in
207 the corresponding context (if any).
208 This has the effect of finishing the extender under the last note
209 of the melisma, instead of extending it to the next lyric.
211 Problem: the extender request is thrown at the same moment as the next lyric,
212 by which time we have already passed the last note of the melisma.
213 However, the Lyric_phrasing_engraver remembers the last note, so just
214 attach it to that, provided it was melismatic. If it was not melismatic,
215 then ignore it and let the Extender_engraver take care of it (i.e. finish at next
218 if(h->has_interface (ly_symbol2scm ("lyric-extender-interface"))) {
219 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricsVoice");
220 record_extender(trim_suffix(voice_context_id), h);
226 get_context_id(Translator_group * ancestor, const char *type)
228 while(ancestor != 0 && ancestor->type_str_ != type) {
229 ancestor = ancestor->daddy_trans_l_;
233 return ancestor->id_str_;
240 trim_suffix(String &id)
242 int index = id.index_i('-');
244 return id.left_str(index);
250 void Lyric_phrasing_engraver::create_grobs ()
252 /* iterate through entries in voice_alist_
253 for each, call set_lyric_align(alignment). Issue a warning if this returns false.
256 SCM sp = get_property("phrasingPunctuation");
257 punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\"";
259 for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
260 SCM v_entry = gh_cdar(v);
261 // ((current . oldflag) . previous)
262 if(!to_boolean(gh_cdar(v_entry))) { // not an old entry left over from a prior note ...
263 Syllable_group *entry = unsmob_voice_entry(gh_caar(v_entry));
266 TODO: give context for warning.
268 if(! entry->set_lyric_align(punc.ch_C(), any_notehead_l_))
269 warning (_ ("lyrics found without any matching notehead"));
271 // is this note melismatic? If so adjust alignment of previous one.
272 if(entry->get_melisma()) {
273 if(entry->lyric_count())
274 warning (_ ("Huh? Melismatic note found to have associated lyrics."));
275 SCM previous_scm = gh_cdr(v_entry);
276 if(previous_scm != SCM_EOL) {
277 Syllable_group *previous = unsmob_voice_entry(previous_scm);
278 if (previous->lyric_count())
279 previous->adjust_melisma_align();
288 Lyric_phrasing_engraver::stop_translation_timestep ()
290 for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
291 SCM entry_scm = gh_cdar(v);
292 // ((alist_entry . entry_is_old) . previous_entry)
293 Syllable_group * entry = unsmob_voice_entry(gh_caar(entry_scm));
295 // set previous_entry, set entry_is_old, and resave it to alist_
296 // but only change if this current was not old.
297 if(! to_boolean(gh_cdar(entry_scm))) {
298 Syllable_group * previous_entry = unsmob_voice_entry(gh_cdr(entry_scm));
299 previous_entry->copy(entry);
300 entry_scm = gh_cons(gh_cons(gh_caar(entry_scm), SCM_BOOL_T), gh_cdr(entry_scm));
301 voice_alist_ = scm_assoc_set_x(voice_alist_, gh_caar(v), entry_scm);