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