]> git.donarmstrong.com Git - lilypond.git/blob - lily/lyric-phrasing-engraver.cc
b9a33c0c2bb5ff9c48e5ada9ce791c31eae75fe1
[lilypond.git] / lily / lyric-phrasing-engraver.cc
1 /*
2   lyric-phrasing-engraver.cc -- implement Lyric_phrasing_engraver
3
4   source file of the GNU LilyPond music typesetter
5
6   (c)  2000 Glen Prideaux <glenprideaux@iname.com>
7 */
8 #include <string.h>
9
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"
15 #include "spanner.hh"
16 #include "paper-def.hh"
17
18
19 String get_context_id(Translator_group * ancestor, const char * type);
20 String trim_suffix(String &id);
21
22 ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
23
24 /*
25   We find start and end of phrases, and align lyrics accordingly.
26   Also, lyrics at start of melismata should be left aligned.
27
28   Alignment and melismata
29
30   I've taken [a different] approach:
31           |      |
32           |      |
33          O      O  <-- second note throws a melisma score element
34           \____/
35
36          ^      ^
37          |      |
38        Lyric (None)
39
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.
44  */
45
46 Lyric_phrasing_engraver::Lyric_phrasing_engraver()
47 {
48   voice_alist_ = SCM_EOL;
49   any_notehead_l_ = 0;
50 }
51
52 Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
53 {
54   /*
55     No need to delete alist_; that's what Garbage collection is for.
56    */
57 }
58
59 Voice_alist_entry * 
60 Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
61 {
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))) {
66       /* match found */
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));
72       }
73       else { // the entry is current ... return it.
74         SCM entry_scm = gh_caadr(s);
75         return unsmob_voice_entry(entry_scm);
76       }
77     }
78   }
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 ()); 
82
83   voice_alist_ = scm_acons(key, val, voice_alist_);
84   return unsmob_voice_entry (gh_caar(val));
85 }
86
87
88 void 
89 Lyric_phrasing_engraver::record_notehead(const String &context_id, 
90                                          Score_element * notehead)
91 {
92   Voice_alist_entry * v = lookup_context_id(context_id);
93   v->set_notehead(notehead);
94   if(!any_notehead_l_)
95     any_notehead_l_ = notehead;
96 }
97   
98 void 
99 Lyric_phrasing_engraver::record_lyric(const String &context_id, Score_element * lyric)
100 {
101   Voice_alist_entry * v = lookup_context_id(context_id);
102   v->add_lyric(lyric);
103 }
104
105 void 
106 Lyric_phrasing_engraver::record_extender(const String &context_id, Score_element * extender)
107 {
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))) {
112       /* match found */
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);
118       }
119     }
120   }
121 }
122
123 void 
124 Lyric_phrasing_engraver::record_melisma(const String &context_id)
125 {
126   Voice_alist_entry * v = lookup_context_id(context_id);
127   v->set_melisma();
128 }
129   
130 void
131 Lyric_phrasing_engraver::acknowledge_element(Score_element_info i)
132 {
133   SCM p = get_property("automaticPhrasing");
134   if(!to_boolean(p))
135     return;
136
137
138   Score_element *h = i.elem_l_;
139
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);
146     if (grace != wgb)
147       return;
148
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);
152     return;
153   }
154   /* now try for a lyric */
155   if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
156
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);
161     return;
162   }
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);
167     return;
168   }
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.
174
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
180        lyric).
181     */
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);
185     return;
186   }
187 }
188
189 String 
190 get_context_id(Translator_group * ancestor, const char *type)
191 {
192   while(ancestor != 0 && ancestor->type_str_ != type) {
193     ancestor = ancestor->daddy_trans_l_;
194   }
195
196   if(ancestor != 0) {
197     return ancestor->id_str_;
198   }
199
200   return "";
201 }
202
203 String 
204 trim_suffix(String &id)
205 {
206   int index = id.index_i('-');
207   if(index >= 0) {
208     return id.left_str(index);
209   }
210   return id;
211 }
212
213
214 void Lyric_phrasing_engraver::process_acknowledged () 
215 {
216   /* iterate through entries in voice_alist_
217      for each, call set_lyric_align(alignment). Issue a warning if this returns false.
218   */
219   String punc;
220   SCM sp = get_property("phrasingPunctuation");
221   punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\""; 
222   
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"));
230
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();
240         }
241       }
242     }
243   }
244 }
245
246
247 void
248 Lyric_phrasing_engraver::do_pre_move_processing ()
249 {
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));
254
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);
262     }
263     entry->next_lyric();
264   }
265   any_notehead_l_ = 0;
266 }
267
268
269
270 /*=========================================================================================*/
271
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.
274 */
275
276 Voice_alist_entry::Voice_alist_entry()
277 {
278   first_in_phrase_b_=true;
279   melisma_b_ = false;
280   clear();
281 }
282
283 void 
284 Voice_alist_entry::clear()
285 {
286   notehead_l_=0;
287   lyric_list_.clear();
288   longest_lyric_l_=0;
289   shortest_lyric_l_=0;
290   melisma_b_ = false;
291 }
292   
293 void
294 Voice_alist_entry::copy( Voice_alist_entry *from)
295 {
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_;
303 }
304
305 void 
306 Voice_alist_entry::set_first_in_phrase(bool f) 
307
308   first_in_phrase_b_ = f; 
309 }
310
311 void 
312 Voice_alist_entry::set_notehead(Score_element * notehead)
313 {
314   if(!notehead_l_) {
315     /* there should only be a single notehead, so silently ignore any extras */
316     notehead_l_=notehead;
317   }
318 }
319
320 void 
321 Voice_alist_entry::add_lyric(Score_element * lyric)
322 {
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;
330   }
331   else
332     longest_lyric_l_ = shortest_lyric_l_ = lyric;
333 }
334
335 void 
336 Voice_alist_entry::add_extender(Score_element * extender)
337 {
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.
342
343     // Left:
344 //     extender->set_elt_property("right-trim-amount", gh_double2scm(0.0));
345
346     // Right:
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));
350   }
351 }
352
353 void 
354 Voice_alist_entry::set_melisma()
355 {
356   melisma_b_ = true;
357 }
358  
359 bool 
360 Voice_alist_entry::set_lyric_align(const char *punc, Score_element *default_notehead_l)
361 {
362   if(lyric_list_.size()==0) {
363     // No lyrics: nothing to do.
364     return true;
365   }
366
367   Score_element * lyric;
368   alignment_i_ = appropriate_alignment(punc);
369
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).
372   if(!notehead_l_) {
373     notehead_l_ = default_notehead_l;
374   }
375   Real translation = amount_to_translate();
376   for(int l = 0; l < lyric_list_.size(); l++) {
377     /** set the x alignment of each lyric
378      */
379     lyric = lyric_list_[l];
380     lyric->set_elt_property("self-alignment-X", gh_int2scm(alignment_i_));
381
382     // centre on notehead ... if we have one. 
383     if(notehead_l_) {
384       /* set the parent of each lyric to the notehead,
385          set the offset callback of each lyric to centered_on_parent,
386       */
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);
391     }
392   }
393   return (notehead_l_);
394 }
395
396 Real 
397 Voice_alist_entry::amount_to_translate()
398 {
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
404        Otherwise
405        - move so shortest lyric just reaches notehead centre
406     */
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;
411     else
412       translate = alignment_i_*(shortest_lyric_l_->extent(X_AXIS)).length();
413   }
414   return translate;
415 }
416
417
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.
422 */
423 int 
424 Voice_alist_entry::appropriate_alignment(const char *punc)
425 {
426   if(first_in_phrase_b_)
427     return LEFT;
428
429   Score_element * lyric;
430   bool end_phrase = true;
431   /* use a property to determine what constitutes punctuation */
432
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):"";
437     char lastchar;
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.
448
449            FIXME: The extra space throws alignment out a bit.
450         */
451         if(lastchar == ' ') {
452           if(lyric_str.length_i()>1) {
453             lastchar = lyric_str[lyric_str.length_i()-2];
454             if(strchr(punc, lastchar))
455               end_phrase=false;
456           }
457         }
458         else
459           end_phrase=false;
460       }
461     }
462   }
463   if(end_phrase)
464     return RIGHT;
465
466   return CENTER;
467 }
468
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.
471 */
472 void
473 Voice_alist_entry::adjust_melisma_align()
474 {
475   if(notehead_l_) {
476     // undo what we did before ...
477     Real translation = -amount_to_translate();
478     // melisma aligning:
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;
483       break;
484     case RIGHT: // move right so smallest lyric is left-aligned on notehead
485       translation += (shortest_lyric_l_->extent(X_AXIS)).length();
486       break;
487     }
488     for(int l = 0; l < lyric_list_.size(); l++) {
489       lyric_list_[l]->translate_axis (translation, X_AXIS);
490     }
491   }
492 }
493
494
495 bool
496 Voice_alist_entry::is_empty()
497 {
498   return lyric_list_.size()==0;
499 }
500
501 void
502 Voice_alist_entry::next_lyric()
503 {
504   first_in_phrase_b_ = (alignment_i_ == RIGHT);
505   clear();
506 }
507
508 /* SMOB */
509
510
511
512 SCM
513 Voice_alist_entry::mark_smob (SCM)
514 {
515   return SCM_EOL;
516 }
517
518 int
519 Voice_alist_entry::print_smob (SCM, SCM port, scm_print_state * )
520 {
521   scm_puts ("#<Voice_alist_entry>", port);
522   return 1;
523 }
524
525 IMPLEMENT_UNSMOB(Voice_alist_entry, voice_entry);
526 IMPLEMENT_SIMPLE_SMOBS(Voice_alist_entry);
527 IMPLEMENT_DEFAULT_EQUAL_P(Voice_alist_entry);
528
529 SCM
530 Voice_alist_entry::make_entry ()
531 {
532   Voice_alist_entry *vi = new Voice_alist_entry;
533   return vi->smobbed_self ();
534 }