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"
13 #include "side-position-interface.hh"
15 #include "paper-def.hh"
17 String get_context_id(Translator_group * ancestor, const char * type);
18 String trim_suffix(String &id);
20 ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
23 We find start and end of phrases, and align lyrics accordingly.
24 Also, lyrics at start of melismata should be left aligned.
26 Alignment and melismata
28 I've taken [a different] approach:
31 O O <-- second note throws a melisma score element
38 Lyric_phrasing_engraver keeps track of the current and previous notes and
39 lyrics for each voice, and when it catches a melisma, it adjusts the
40 alignment of the lyrics of the previous note. I hope this isn't
41 unnecessarily convoluted.
44 Lyric_phrasing_engraver::Lyric_phrasing_engraver()
46 voice_alist_ = SCM_EOL;
50 Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
53 No need to delete alist_; that's what Garbage collection is for.
58 Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
60 SCM key = ly_str02scm(context_id.ch_C());
61 if( ! gh_null_p(voice_alist_) ) {
62 SCM s = scm_assoc(key, voice_alist_);
63 if(! (gh_boolean_p(s) && !to_boolean(s))) {
65 // ( key . ( (alist_entry . old_entry) . previous_entry) )
66 if(to_boolean(gh_cdadr(s))) { // it's an old entry ... make it a new one
67 SCM val = gh_cons(gh_cons(gh_caadr(s), SCM_BOOL_F), gh_cddr(s));
68 voice_alist_ = scm_assoc_set_x(voice_alist_, gh_car(s), val);
69 return unsmob_voice_entry (gh_caar(val));
71 else { // the entry is current ... return it.
72 SCM entry_scm = gh_caadr(s);
73 return unsmob_voice_entry(entry_scm);
77 // ( ( alist_entry . old_entry ) . previous_entry )
78 SCM val = gh_cons(gh_cons(Voice_alist_entry::make_entry (), SCM_BOOL_F),
79 Voice_alist_entry::make_entry ());
81 voice_alist_ = scm_acons(key, val, voice_alist_);
82 return unsmob_voice_entry (gh_caar(val));
87 Lyric_phrasing_engraver::record_notehead(const String &context_id,
88 Score_element * notehead)
90 Voice_alist_entry * v = lookup_context_id(context_id);
91 v->set_notehead(notehead);
93 any_notehead_l_ = notehead;
97 Lyric_phrasing_engraver::record_lyric(const String &context_id, Score_element * lyric)
99 Voice_alist_entry * v = lookup_context_id(context_id);
104 Lyric_phrasing_engraver::record_extender(const String &context_id, Score_element * extender)
106 SCM key = ly_str02scm(context_id.ch_C());
107 if( ! gh_null_p(voice_alist_) ) {
108 SCM s = scm_assoc(key, voice_alist_);
109 if(! (gh_boolean_p(s) && !to_boolean(s))) {
111 // ( key . ( (alist_entry . old_entry) . previous_entry) )
112 SCM previous_scm = gh_cddr(s);
113 if(previous_scm != SCM_EOL) {
114 Voice_alist_entry * v = unsmob_voice_entry(previous_scm);
115 v->add_extender(extender);
122 Lyric_phrasing_engraver::record_melisma(const String &context_id)
124 Voice_alist_entry * v = lookup_context_id(context_id);
129 Lyric_phrasing_engraver::acknowledge_element(Score_element_info i)
131 SCM p = get_property("automaticPhrasing");
136 Score_element *h = i.elem_l_;
138 if (Note_head::has_interface(h)) {
139 /* caught a note head ... do something with it */
140 /* ... but not if it's a grace note ... */
141 bool grace= to_boolean (i.elem_l_->get_elt_property ("grace"));
142 SCM wg = get_property ("weAreGraceContext");
143 bool wgb = to_boolean (wg);
147 /* what's its Voice context name? */
148 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
149 record_notehead(voice_context_id, h);
152 /* now try for a lyric */
153 if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
155 /* what's its LyricVoice context name? */
156 String lyric_voice_context_id =
157 get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricVoice");
158 record_lyric(trim_suffix(lyric_voice_context_id), h);
161 /* finally for a melisma */
162 if(h->has_interface (ly_symbol2scm ("melisma-interface"))) {
163 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
164 record_melisma(voice_context_id);
167 /* How about catching any extender items and then if we have a melisma,
168 set the RIGHT item of the extender spanner to the melismatic note in
169 the corresponding context (if any).
170 This has the effect of finishing the extender under the last note
171 of the melisma, instead of extending it to the next lyric.
173 Problem: the extender request is thrown at the same moment as the next lyric,
174 by which time we have already passed the last note of the melisma.
175 However, the Lyric_phrasing_engraver remembers the last note, so just
176 attach it to that, provided it was melismatic. If it was not melismatic,
177 then ignore it and let the Extender_engraver take care of it (i.e. finish at next
180 if(h->has_interface (ly_symbol2scm ("lyric-extender-interface"))) {
181 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricVoice");
182 record_extender(trim_suffix(voice_context_id), h);
188 get_context_id(Translator_group * ancestor, const char *type)
190 while(ancestor != 0 && ancestor->type_str_ != type) {
191 ancestor = ancestor->daddy_trans_l_;
195 return ancestor->id_str_;
202 trim_suffix(String &id)
204 int index = id.index_i('-');
206 return id.left_str(index);
212 void Lyric_phrasing_engraver::process_acknowledged ()
214 /* iterate through entries in voice_alist_
215 for each, call set_lyric_align(alignment). Issue a warning if this returns false.
218 SCM sp = get_property("phrasingPunctuation");
219 punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\"";
221 for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
222 SCM v_entry = gh_cdar(v);
223 // ((current . oldflag) . previous)
224 if(!to_boolean(gh_cdar(v_entry))) { // not an old entry left over from a prior note ...
225 Voice_alist_entry *entry = unsmob_voice_entry(gh_caar(v_entry));
226 if(! entry->set_lyric_align(punc.ch_C(), any_notehead_l_))
227 warning (_ ("lyrics found without any matching notehead"));
229 // is this note melismatic? If so adjust alignment of previous one.
230 if(entry->get_melisma()) {
231 if(entry->lyric_count())
232 warning (_ ("Huh? Melismatic note found to have associated lyrics."));
233 SCM previous_scm = gh_cdr(v_entry);
234 if(previous_scm != SCM_EOL) {
235 Voice_alist_entry *previous = unsmob_voice_entry(previous_scm);
236 if (previous->lyric_count())
237 previous->adjust_melisma_align();
246 Lyric_phrasing_engraver::do_pre_move_processing ()
248 for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
249 SCM entry_scm = gh_cdar(v);
250 // ((alist_entry . entry_is_old) . previous_entry)
251 Voice_alist_entry * entry = unsmob_voice_entry(gh_caar(entry_scm));
253 // set previous_entry, set entry_is_old, and resave it to alist_
254 // but only change if this current was not old.
255 if(! to_boolean(gh_cdar(entry_scm))) {
256 Voice_alist_entry * previous_entry = unsmob_voice_entry(gh_cdr(entry_scm));
257 previous_entry->copy(entry);
258 entry_scm = gh_cons(gh_cons(gh_caar(entry_scm), SCM_BOOL_T), gh_cdr(entry_scm));
259 voice_alist_ = scm_assoc_set_x(voice_alist_, gh_caar(v), entry_scm);
268 /*=========================================================================================*/
270 /** Voice_alist_entry is a class to be smobbed and entered as data in the association list
271 member of the Lyric_phrasing_engraver class.
274 Voice_alist_entry::Voice_alist_entry()
276 first_in_phrase_b_=true;
282 Voice_alist_entry::clear()
292 Voice_alist_entry::copy( Voice_alist_entry *from)
294 notehead_l_ = from->notehead_l_;
295 lyric_list_ = from->lyric_list_;
296 longest_lyric_l_ = from->longest_lyric_l_;
297 shortest_lyric_l_ = from->shortest_lyric_l_;
298 melisma_b_ = from->melisma_b_;
299 alignment_i_ = from->alignment_i_;
300 first_in_phrase_b_ = from->first_in_phrase_b_;
304 Voice_alist_entry::set_first_in_phrase(bool f)
306 first_in_phrase_b_ = f;
310 Voice_alist_entry::set_notehead(Score_element * notehead)
313 /* there should only be a single notehead, so silently ignore any extras */
314 notehead_l_=notehead;
319 Voice_alist_entry::add_lyric(Score_element * lyric)
321 lyric_list_.push(lyric);
322 /* record longest and shortest lyrics */
323 if( longest_lyric_l_ ) {
324 if(lyric->extent(X_AXIS).length() > (longest_lyric_l_->extent(X_AXIS)).length())
325 longest_lyric_l_ = lyric;
326 if(lyric->extent(X_AXIS).length() < (shortest_lyric_l_->extent(X_AXIS)).length())
327 shortest_lyric_l_ = lyric;
330 longest_lyric_l_ = shortest_lyric_l_ = lyric;
334 Voice_alist_entry::add_extender(Score_element * extender)
336 if(notehead_l_ && melisma_b_) {
337 dynamic_cast<Spanner*>(extender)->set_bound (RIGHT, notehead_l_);
338 // should the extender finish at the right of the last note of the melisma, or the left?
339 // Comments in lyric-extender.hh say left, but right looks better to me. GP.
342 // extender->set_elt_property("right-trim-amount", gh_double2scm(0.0));
345 Real ss = extender->paper_l ()->get_var ("staffspace");
346 extender->set_elt_property("right-trim-amount",
347 gh_double2scm(-notehead_l_->extent(X_AXIS).length()/ss));
352 Voice_alist_entry::set_melisma()
358 Voice_alist_entry::set_lyric_align(const char *punc, Score_element *default_notehead_l)
360 if(lyric_list_.size()==0) {
361 // No lyrics: nothing to do.
365 Score_element * lyric;
366 alignment_i_ = appropriate_alignment(punc);
368 // If there was no notehead in the matching voice context, use the first
369 // notehead caught from any voice context (any port in a storm).
371 notehead_l_ = default_notehead_l;
373 Real translation = amount_to_translate();
374 for(int l = 0; l < lyric_list_.size(); l++) {
375 /** set the x alignment of each lyric
377 lyric = lyric_list_[l];
378 lyric->set_elt_property("self-alignment-X", gh_int2scm(alignment_i_));
380 // centre on notehead ... if we have one.
382 /* set the parent of each lyric to the notehead,
383 set the offset callback of each lyric to centered_on_parent,
385 lyric->set_parent(notehead_l_, X_AXIS);
386 lyric->add_offset_callback (Side_position::centered_on_parent, X_AXIS);
387 /* reference is on the right of the notehead; move it left half way, then centralise */
388 lyric->translate_axis (translation-(notehead_l_->extent(X_AXIS)).center(), X_AXIS);
391 return (notehead_l_);
395 Voice_alist_entry::amount_to_translate()
397 Real translate = 0.0;
398 if(alignment_i_ != CENTER) {
399 // right or left align ...
400 /* If length of longest lyric < 2 * length of shortest lyric,
401 - centre longest lyric on notehead
403 - move so shortest lyric just reaches notehead centre
405 // FIXME: do we really know the lyric extent here? Some font sizing comes later?
406 if((longest_lyric_l_->extent(X_AXIS)).length() <
407 (shortest_lyric_l_->extent(X_AXIS)).length() * 2 )
408 translate = alignment_i_*(longest_lyric_l_->extent(X_AXIS)).length()/2;
410 translate = alignment_i_*(shortest_lyric_l_->extent(X_AXIS)).length();
416 /** determine what alignment we want.
417 Rules: if first_in_phrase_b_ is set, then alignment is LEFT.
418 otherwise if each syllable ends in punctuation, then alignment is RIGHT
419 otherwise alignment is centre.
422 Voice_alist_entry::appropriate_alignment(const char *punc)
424 if(first_in_phrase_b_)
427 Score_element * lyric;
428 bool end_phrase = true;
429 /* use a property to determine what constitutes punctuation */
431 for(int l = 0; l < lyric_list_.size() && end_phrase; l++) {
432 lyric = lyric_list_[l];
433 SCM lyric_scm = lyric->get_elt_property("text");
434 String lyric_str = gh_string_p(lyric_scm)?ly_scm2string(lyric_scm):"";
436 if(lyric_str.length_i()>0) {
437 lastchar = lyric_str[lyric_str.length_i()-1];
438 /* If it doesn't end in punctuation then it ain't an end of phrase */
439 if(! strchr(punc, lastchar)) {
440 /* Special case: trailing space. Here examine the previous character and reverse the
441 sense of the test (i.e. trailing space makes a break without punctuation, or
442 suppresses a break with punctuation).
443 This behaviour can be suppressed by including a space in the
444 phrasingPunctuation property, in which case trailing space always means
445 the same as punctuation.
447 FIXME: The extra space throws alignment out a bit.
449 if(lastchar == ' ') {
450 if(lyric_str.length_i()>1) {
451 lastchar = lyric_str[lyric_str.length_i()-2];
452 if(strchr(punc, lastchar))
467 /** We don't know about the melisma until after the initial alignment work is done, so go
468 back and fix the alignment when we DO know.
471 Voice_alist_entry::adjust_melisma_align()
474 // undo what we did before ...
475 Real translation = -amount_to_translate();
477 switch (alignment_i_) {
478 // case LEFT: // that's all
479 case CENTER: // move right so smallest lyric is left-aligned on notehead
480 translation += (shortest_lyric_l_->extent(X_AXIS)).length()/2;
482 case RIGHT: // move right so smallest lyric is left-aligned on notehead
483 translation += (shortest_lyric_l_->extent(X_AXIS)).length();
486 for(int l = 0; l < lyric_list_.size(); l++) {
487 lyric_list_[l]->translate_axis (translation, X_AXIS);
494 Voice_alist_entry::is_empty()
496 return lyric_list_.size()==0;
500 Voice_alist_entry::next_lyric()
502 first_in_phrase_b_ = (alignment_i_ == RIGHT);
508 #include "ly-smobs.icc"
511 Voice_alist_entry::mark_smob (SCM)
517 Voice_alist_entry::print_smob (SCM, SCM port, scm_print_state * )
519 scm_puts ("#<Voice_alist_entry>", port);
523 IMPLEMENT_UNSMOB(Voice_alist_entry, voice_entry);
524 IMPLEMENT_SIMPLE_SMOBS(Voice_alist_entry);
525 IMPLEMENT_DEFAULT_EQUAL_P(Voice_alist_entry);
528 Voice_alist_entry::make_entry ()
530 Voice_alist_entry *vi = new Voice_alist_entry;
531 return vi->smobbed_self ();