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"
14 #include "ly-smobs.icc"
16 #include "paper-def.hh"
19 String get_context_id(Translator_group * ancestor, const char * type);
20 String trim_suffix(String &id);
22 ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
25 We find start and end of phrases, and align lyrics accordingly.
26 Also, lyrics at start of melismata should be left aligned.
28 Alignment and melismata
30 I've taken [a different] approach:
33 O O <-- second note throws a melisma score element
40 Lyric_phrasing_engraver keeps track of the current and previous notes and
41 lyrics for each voice, and when it catches a melisma, it adjusts the
42 alignment of the lyrics of the previous note. I hope this isn't
43 unnecessarily convoluted.
46 Lyric_phrasing_engraver::Lyric_phrasing_engraver()
48 voice_alist_ = SCM_EOL;
52 Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
55 No need to delete alist_; that's what Garbage collection is for.
60 Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
62 SCM key = ly_str02scm(context_id.ch_C());
63 if( ! gh_null_p(voice_alist_) ) {
64 SCM s = scm_assoc(key, voice_alist_);
65 if(! (gh_boolean_p(s) && !to_boolean(s))) {
67 // ( key . ( (alist_entry . old_entry) . previous_entry) )
68 if(to_boolean(gh_cdadr(s))) { // it's an old entry ... make it a new one
69 SCM val = gh_cons(gh_cons(gh_caadr(s), SCM_BOOL_F), gh_cddr(s));
70 voice_alist_ = scm_assoc_set_x(voice_alist_, gh_car(s), val);
71 return unsmob_voice_entry (gh_caar(val));
73 else { // the entry is current ... return it.
74 SCM entry_scm = gh_caadr(s);
75 return unsmob_voice_entry(entry_scm);
79 // ( ( alist_entry . old_entry ) . previous_entry )
80 SCM val = gh_cons(gh_cons(Voice_alist_entry::make_entry (), SCM_BOOL_F),
81 Voice_alist_entry::make_entry ());
83 voice_alist_ = scm_acons(key, val, voice_alist_);
84 return unsmob_voice_entry (gh_caar(val));
89 Lyric_phrasing_engraver::record_notehead(const String &context_id,
90 Score_element * notehead)
92 Voice_alist_entry * v = lookup_context_id(context_id);
93 v->set_notehead(notehead);
95 any_notehead_l_ = notehead;
99 Lyric_phrasing_engraver::record_lyric(const String &context_id, Score_element * lyric)
101 Voice_alist_entry * v = lookup_context_id(context_id);
106 Lyric_phrasing_engraver::record_extender(const String &context_id, Score_element * extender)
108 SCM key = ly_str02scm(context_id.ch_C());
109 if( ! gh_null_p(voice_alist_) ) {
110 SCM s = scm_assoc(key, voice_alist_);
111 if(! (gh_boolean_p(s) && !to_boolean(s))) {
113 // ( key . ( (alist_entry . old_entry) . previous_entry) )
114 SCM previous_scm = gh_cddr(s);
115 if(previous_scm != SCM_EOL) {
116 Voice_alist_entry * v = unsmob_voice_entry(previous_scm);
117 v->add_extender(extender);
124 Lyric_phrasing_engraver::record_melisma(const String &context_id)
126 Voice_alist_entry * v = lookup_context_id(context_id);
131 Lyric_phrasing_engraver::acknowledge_element(Score_element_info i)
133 SCM p = get_property("automaticPhrasing");
138 Score_element *h = i.elem_l_;
140 if (Note_head::has_interface(h)) {
141 /* caught a note head ... do something with it */
142 /* ... but not if it's a grace note ... */
143 bool grace= to_boolean (i.elem_l_->get_elt_property ("grace"));
144 SCM wg = get_property ("weAreGraceContext");
145 bool wgb = to_boolean (wg);
149 /* what's its Voice context name? */
150 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
151 record_notehead(voice_context_id, h);
154 /* now try for a lyric */
155 if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
157 /* what's its LyricVoice context name? */
158 String lyric_voice_context_id =
159 get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricVoice");
160 record_lyric(trim_suffix(lyric_voice_context_id), h);
163 /* finally for a melisma */
164 if(h->has_interface (ly_symbol2scm ("melisma-interface"))) {
165 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
166 record_melisma(voice_context_id);
169 /* How about catching any extender items and then if we have a melisma,
170 set the RIGHT item of the extender spanner to the melismatic note in
171 the corresponding context (if any).
172 This has the effect of finishing the extender under the last note
173 of the melisma, instead of extending it to the next lyric.
175 Problem: the extender request is thrown at the same moment as the next lyric,
176 by which time we have already passed the last note of the melisma.
177 However, the Lyric_phrasing_engraver remembers the last note, so just
178 attach it to that, provided it was melismatic. If it was not melismatic,
179 then ignore it and let the Extender_engraver take care of it (i.e. finish at next
182 if(h->has_interface (ly_symbol2scm ("lyric-extender-interface"))) {
183 String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricVoice");
184 record_extender(trim_suffix(voice_context_id), h);
190 get_context_id(Translator_group * ancestor, const char *type)
192 while(ancestor != 0 && ancestor->type_str_ != type) {
193 ancestor = ancestor->daddy_trans_l_;
197 return ancestor->id_str_;
204 trim_suffix(String &id)
206 int index = id.index_i('-');
208 return id.left_str(index);
214 void Lyric_phrasing_engraver::process_acknowledged ()
216 /* iterate through entries in voice_alist_
217 for each, call set_lyric_align(alignment). Issue a warning if this returns false.
220 SCM sp = get_property("phrasingPunctuation");
221 punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\"";
223 for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
224 SCM v_entry = gh_cdar(v);
225 // ((current . oldflag) . previous)
226 if(!to_boolean(gh_cdar(v_entry))) { // not an old entry left over from a prior note ...
227 Voice_alist_entry *entry = unsmob_voice_entry(gh_caar(v_entry));
228 if(! entry->set_lyric_align(punc.ch_C(), any_notehead_l_))
229 warning (_ ("lyrics found without any matching notehead"));
231 // is this note melismatic? If so adjust alignment of previous one.
232 if(entry->get_melisma()) {
233 if(entry->lyric_count())
234 warning (_ ("Huh? Melismatic note found to have associated lyrics."));
235 SCM previous_scm = gh_cdr(v_entry);
236 if(previous_scm != SCM_EOL) {
237 Voice_alist_entry *previous = unsmob_voice_entry(previous_scm);
238 if (previous->lyric_count())
239 previous->adjust_melisma_align();
248 Lyric_phrasing_engraver::do_pre_move_processing ()
250 for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
251 SCM entry_scm = gh_cdar(v);
252 // ((alist_entry . entry_is_old) . previous_entry)
253 Voice_alist_entry * entry = unsmob_voice_entry(gh_caar(entry_scm));
255 // set previous_entry, set entry_is_old, and resave it to alist_
256 // but only change if this current was not old.
257 if(! to_boolean(gh_cdar(entry_scm))) {
258 Voice_alist_entry * previous_entry = unsmob_voice_entry(gh_cdr(entry_scm));
259 previous_entry->copy(entry);
260 entry_scm = gh_cons(gh_cons(gh_caar(entry_scm), SCM_BOOL_T), gh_cdr(entry_scm));
261 voice_alist_ = scm_assoc_set_x(voice_alist_, gh_caar(v), entry_scm);
270 /*=========================================================================================*/
272 /** Voice_alist_entry is a class to be smobbed and entered as data in the association list
273 member of the Lyric_phrasing_engraver class.
276 Voice_alist_entry::Voice_alist_entry()
278 first_in_phrase_b_=true;
284 Voice_alist_entry::clear()
294 Voice_alist_entry::copy( Voice_alist_entry *from)
296 notehead_l_ = from->notehead_l_;
297 lyric_list_ = from->lyric_list_;
298 longest_lyric_l_ = from->longest_lyric_l_;
299 shortest_lyric_l_ = from->shortest_lyric_l_;
300 melisma_b_ = from->melisma_b_;
301 alignment_i_ = from->alignment_i_;
302 first_in_phrase_b_ = from->first_in_phrase_b_;
306 Voice_alist_entry::set_first_in_phrase(bool f)
308 first_in_phrase_b_ = f;
312 Voice_alist_entry::set_notehead(Score_element * notehead)
315 /* there should only be a single notehead, so silently ignore any extras */
316 notehead_l_=notehead;
321 Voice_alist_entry::add_lyric(Score_element * lyric)
323 lyric_list_.push(lyric);
324 /* record longest and shortest lyrics */
325 if( longest_lyric_l_ ) {
326 if(lyric->extent(X_AXIS).length() > (longest_lyric_l_->extent(X_AXIS)).length())
327 longest_lyric_l_ = lyric;
328 if(lyric->extent(X_AXIS).length() < (shortest_lyric_l_->extent(X_AXIS)).length())
329 shortest_lyric_l_ = lyric;
332 longest_lyric_l_ = shortest_lyric_l_ = lyric;
336 Voice_alist_entry::add_extender(Score_element * extender)
338 if(notehead_l_ && melisma_b_) {
339 dynamic_cast<Spanner*>(extender)->set_bound (RIGHT, notehead_l_);
340 // should the extender finish at the right of the last note of the melisma, or the left?
341 // Comments in lyric-extender.hh say left, but right looks better to me. GP.
344 // extender->set_elt_property("right-trim-amount", gh_double2scm(0.0));
347 Real ss = extender->paper_l ()->get_var ("staffspace");
348 extender->set_elt_property("right-trim-amount",
349 gh_double2scm(-notehead_l_->extent(X_AXIS).length()/ss));
354 Voice_alist_entry::set_melisma()
360 Voice_alist_entry::set_lyric_align(const char *punc, Score_element *default_notehead_l)
362 if(lyric_list_.size()==0) {
363 // No lyrics: nothing to do.
367 Score_element * lyric;
368 alignment_i_ = appropriate_alignment(punc);
370 // If there was no notehead in the matching voice context, use the first
371 // notehead caught from any voice context (any port in a storm).
373 notehead_l_ = default_notehead_l;
375 Real translation = amount_to_translate();
376 for(int l = 0; l < lyric_list_.size(); l++) {
377 /** set the x alignment of each lyric
379 lyric = lyric_list_[l];
380 lyric->set_elt_property("self-alignment-X", gh_int2scm(alignment_i_));
382 // centre on notehead ... if we have one.
384 /* set the parent of each lyric to the notehead,
385 set the offset callback of each lyric to centered_on_parent,
387 lyric->set_parent(notehead_l_, X_AXIS);
388 lyric->add_offset_callback (Side_position::centered_on_parent, X_AXIS);
389 /* reference is on the right of the notehead; move it left half way, then centralise */
390 lyric->translate_axis (translation-(notehead_l_->extent(X_AXIS)).center(), X_AXIS);
393 return (notehead_l_);
397 Voice_alist_entry::amount_to_translate()
399 Real translate = 0.0;
400 if(alignment_i_ != CENTER) {
401 // right or left align ...
402 /* If length of longest lyric < 2 * length of shortest lyric,
403 - centre longest lyric on notehead
405 - move so shortest lyric just reaches notehead centre
407 // FIXME: do we really know the lyric extent here? Some font sizing comes later?
408 if((longest_lyric_l_->extent(X_AXIS)).length() <
409 (shortest_lyric_l_->extent(X_AXIS)).length() * 2 )
410 translate = alignment_i_*(longest_lyric_l_->extent(X_AXIS)).length()/2;
412 translate = alignment_i_*(shortest_lyric_l_->extent(X_AXIS)).length();
418 /** determine what alignment we want.
419 Rules: if first_in_phrase_b_ is set, then alignment is LEFT.
420 otherwise if each syllable ends in punctuation, then alignment is RIGHT
421 otherwise alignment is centre.
424 Voice_alist_entry::appropriate_alignment(const char *punc)
426 if(first_in_phrase_b_)
429 Score_element * lyric;
430 bool end_phrase = true;
431 /* use a property to determine what constitutes punctuation */
433 for(int l = 0; l < lyric_list_.size() && end_phrase; l++) {
434 lyric = lyric_list_[l];
435 SCM lyric_scm = lyric->get_elt_property("text");
436 String lyric_str = gh_string_p(lyric_scm)?ly_scm2string(lyric_scm):"";
438 if(lyric_str.length_i()>0) {
439 lastchar = lyric_str[lyric_str.length_i()-1];
440 /* If it doesn't end in punctuation then it ain't an end of phrase */
441 if(! strchr(punc, lastchar)) {
442 /* Special case: trailing space. Here examine the previous character and reverse the
443 sense of the test (i.e. trailing space makes a break without punctuation, or
444 suppresses a break with punctuation).
445 This behaviour can be suppressed by including a space in the
446 phrasingPunctuation property, in which case trailing space always means
447 the same as punctuation.
449 FIXME: The extra space throws alignment out a bit.
451 if(lastchar == ' ') {
452 if(lyric_str.length_i()>1) {
453 lastchar = lyric_str[lyric_str.length_i()-2];
454 if(strchr(punc, lastchar))
469 /** We don't know about the melisma until after the initial alignment work is done, so go
470 back and fix the alignment when we DO know.
473 Voice_alist_entry::adjust_melisma_align()
476 // undo what we did before ...
477 Real translation = -amount_to_translate();
479 switch (alignment_i_) {
480 // case LEFT: // that's all
481 case CENTER: // move right so smallest lyric is left-aligned on notehead
482 translation += (shortest_lyric_l_->extent(X_AXIS)).length()/2;
484 case RIGHT: // move right so smallest lyric is left-aligned on notehead
485 translation += (shortest_lyric_l_->extent(X_AXIS)).length();
488 for(int l = 0; l < lyric_list_.size(); l++) {
489 lyric_list_[l]->translate_axis (translation, X_AXIS);
496 Voice_alist_entry::is_empty()
498 return lyric_list_.size()==0;
502 Voice_alist_entry::next_lyric()
504 first_in_phrase_b_ = (alignment_i_ == RIGHT);
513 Voice_alist_entry::mark_smob (SCM)
519 Voice_alist_entry::print_smob (SCM, SCM port, scm_print_state * )
521 scm_puts ("#<Voice_alist_entry>", port);
525 IMPLEMENT_UNSMOB(Voice_alist_entry, voice_entry);
526 IMPLEMENT_SIMPLE_SMOBS(Voice_alist_entry);
527 IMPLEMENT_DEFAULT_EQUAL_P(Voice_alist_entry);
530 Voice_alist_entry::make_entry ()
532 Voice_alist_entry *vi = new Voice_alist_entry;
533 return vi->smobbed_self ();