]> git.donarmstrong.com Git - lilypond.git/blob - lily/lyric-phrasing-engraver.cc
af66efdcfa7be5760a4ab82efe8bf09e3132fa05
[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
15 String get_context_id(Translator_group * ancestor, const char * type);
16 String trim_suffix(String &id);
17
18 ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
19
20 /*
21   We find start and end of phrases, and align lyrics accordingly.
22   Also, lyrics at start of melismata should be left aligned.
23
24   Alignment and melismata
25
26   I've taken [a different] approach:
27           |      |
28           |      |
29          O      O  <-- second note throws a melisma score element
30           \____/
31
32          ^      ^
33          |      |
34        Lyric (None)
35
36   Lyric_phrasing_engraver keeps track of the current and previous notes and
37   lyrics for each voice, and when it catches a melisma, it adjusts the
38   alignment of the lyrics of the previous note. I hope this isn't
39   unnecessarily convoluted.
40  */
41
42 Lyric_phrasing_engraver::Lyric_phrasing_engraver()
43 {
44   voice_alist_ = SCM_EOL;
45   any_notehead_l_ = 0;
46 }
47
48 Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
49 {
50   /*
51     No need to delete alist_; that's what Garbage collection is for.
52    */
53 }
54
55 Voice_alist_entry * 
56 Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
57 {
58   SCM key = ly_str02scm(context_id.ch_C());
59   if( ! gh_null_p(voice_alist_) ) {
60     SCM s = scm_assoc(key, voice_alist_);
61     if(! (gh_boolean_p(s) && !to_boolean(s))) {
62       /* match found */
63       // ( key . ( (alist_entry . old_entry) . previous_entry) )
64       if(to_boolean(gh_cdadr(s))) { // it's an old entry ... make it a new one
65         SCM val = gh_cons(gh_cons(gh_caadr(s), SCM_BOOL_F), gh_cddr(s)); 
66         voice_alist_ = scm_assoc_set_x(voice_alist_, gh_car(s), val);
67         return unsmob_voice_entry (gh_caar(val));
68       }
69       else { // the entry is current ... return it.
70         SCM entry_scm = gh_caadr(s);
71         return unsmob_voice_entry(entry_scm);
72       }
73     }
74   }
75   // ( ( alist_entry . old_entry ) . previous_entry )
76   SCM val = gh_cons(gh_cons(Voice_alist_entry::make_entry (), SCM_BOOL_F), 
77                     Voice_alist_entry::make_entry ()); 
78
79   voice_alist_ = scm_acons(key, val, voice_alist_);
80   return unsmob_voice_entry (gh_caar(val));
81 }
82
83
84 void 
85 Lyric_phrasing_engraver::record_notehead(const String &context_id, 
86                                          Score_element * notehead)
87 {
88   Voice_alist_entry * v = lookup_context_id(context_id);
89   v->set_notehead(notehead);
90   if(!any_notehead_l_)
91     any_notehead_l_ = notehead;
92 }
93   
94 void 
95 Lyric_phrasing_engraver::record_lyric(const String &context_id, Score_element * lyric)
96 {
97   Voice_alist_entry * v = lookup_context_id(context_id);
98   v->add_lyric(lyric);
99 }
100
101 void 
102 Lyric_phrasing_engraver::record_melisma(const String &context_id)
103 {
104   Voice_alist_entry * v = lookup_context_id(context_id);
105   v->set_melisma();
106 }
107   
108 void
109 Lyric_phrasing_engraver::acknowledge_element(Score_element_info i)
110 {
111   SCM p = get_property("automaticPhrasing");
112   if(!to_boolean(p))
113     return;
114
115
116   Score_element *h = i.elem_l_;
117
118   if (Note_head::has_interface(h)) {
119     /* caught a note head ... do something with it */
120     /* ... but not if it's a grace note ... */
121     bool grace= to_boolean (i.elem_l_->get_elt_property ("grace"));
122     SCM wg = get_property ("weAreGraceContext");
123     bool wgb = to_boolean (wg);
124     if (grace != wgb)
125       return;
126
127     /* what's its Voice context name? */
128     String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
129     record_notehead(voice_context_id, h);
130     return;
131   }
132   /* now try for a lyric */
133   if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
134
135     /* what's its LyricVoice context name? */
136     String lyric_voice_context_id = 
137       get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricVoice");
138     record_lyric(trim_suffix(lyric_voice_context_id), h);
139     return;
140   }
141   /* finally for a melisma */
142   if(h->has_interface (ly_symbol2scm ("melisma-interface"))) {
143     String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
144     record_melisma(voice_context_id);
145     return;
146   }
147 }
148
149 String 
150 get_context_id(Translator_group * ancestor, const char *type)
151 {
152   while(ancestor != 0 && ancestor->type_str_ != type) {
153     ancestor = ancestor->daddy_trans_l_;
154   }
155
156   if(ancestor != 0) {
157     return ancestor->id_str_;
158   }
159
160   return "";
161 }
162
163 String 
164 trim_suffix(String &id)
165 {
166   int index = id.index_i('-');
167   if(index >= 0) {
168     return id.left_str(index);
169   }
170   return id;
171 }
172
173
174 void Lyric_phrasing_engraver::process_acknowledged () 
175 {
176   /* iterate through entries in voice_alist_
177      for each, call set_lyric_align(alignment). Issue a warning if this returns false.
178   */
179   String punc;
180   SCM sp = get_property("phrasingPunctuation");
181   punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\""; 
182   
183   for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
184     SCM v_entry = gh_cdar(v);
185     // ((current . oldflag) . previous)
186     Voice_alist_entry *entry = unsmob_voice_entry(gh_caar(v_entry));
187     if(! entry->set_lyric_align(punc.ch_C(), any_notehead_l_))
188       warning (_ ("lyrics found without any matching notehead"));
189
190     // is this note melismatic? If so adjust alignment of previous one.
191     if(entry->get_melisma()) {
192       if(entry->lyric_count())
193         warning (_ ("Huh? Melismatic note found to have associated lyrics."));
194       SCM previous_scm = gh_cdr(v_entry);
195       if(previous_scm != SCM_EOL) {
196         Voice_alist_entry *previous = unsmob_voice_entry(previous_scm);
197         if (previous->lyric_count())
198           previous->adjust_melisma_align();
199       }
200     }
201   }
202 }
203
204
205 void
206 Lyric_phrasing_engraver::do_pre_move_processing ()
207 {
208   for(SCM v=voice_alist_; gh_pair_p(v); v = gh_cdr(v)) {
209     SCM entry_scm = gh_cdar(v);
210     // ((alist_entry . entry_is_old) . previous_entry)
211     Voice_alist_entry * entry = unsmob_voice_entry(gh_caar(entry_scm));
212
213     // set previous_entry, set entry_is_old, and resave it to alist_
214     // but only change if this current was not old.
215     if(! to_boolean(gh_cdar(entry_scm))) { 
216       Voice_alist_entry * previous_entry = unsmob_voice_entry(gh_cdr(entry_scm));
217       previous_entry->copy(entry);
218       entry_scm = gh_cons(gh_cons(gh_caar(entry_scm), SCM_BOOL_T), gh_cdr(entry_scm));
219       voice_alist_ = scm_assoc_set_x(voice_alist_, gh_caar(v), entry_scm);
220     }
221     entry->next_lyric();
222   }
223   any_notehead_l_ = 0;
224 }
225
226
227
228 /*=========================================================================================*/
229
230 /** Voice_alist_entry is a class to be smobbed and entered as data in the association list
231     member of the Lyric_phrasing_engraver class.
232 */
233
234 Voice_alist_entry::Voice_alist_entry()
235 {
236   first_in_phrase_b_=true;
237   melisma_b_ = false;
238   clear();
239 }
240
241 void 
242 Voice_alist_entry::clear()
243 {
244   notehead_l_=0;
245   lyric_list_.clear();
246   longest_lyric_l_=0;
247   shortest_lyric_l_=0;
248   melisma_b_ = false;
249 }
250   
251 void
252 Voice_alist_entry::copy( Voice_alist_entry *from)
253 {
254   notehead_l_ = from->notehead_l_;
255   lyric_list_ = from->lyric_list_;
256   longest_lyric_l_ = from->longest_lyric_l_;
257   shortest_lyric_l_ = from->shortest_lyric_l_;
258   melisma_b_ = from->melisma_b_;
259   alignment_i_ = from->alignment_i_;
260   first_in_phrase_b_ = from->first_in_phrase_b_;
261 }
262
263 void 
264 Voice_alist_entry::set_first_in_phrase(bool f) 
265
266   first_in_phrase_b_ = f; 
267 }
268
269 void 
270 Voice_alist_entry::set_notehead(Score_element * notehead)
271 {
272   if(!notehead_l_) {
273     /* there should only be a single notehead, so silently ignore any extras */
274     notehead_l_=notehead;
275   }
276 }
277
278 void 
279 Voice_alist_entry::add_lyric(Score_element * lyric)
280 {
281   lyric_list_.push(lyric);
282   /* record longest and shortest lyrics */
283   if( longest_lyric_l_ ) {
284     if(lyric->extent(X_AXIS).length() > (longest_lyric_l_->extent(X_AXIS)).length())
285       longest_lyric_l_ = lyric;
286     if(lyric->extent(X_AXIS).length() < (shortest_lyric_l_->extent(X_AXIS)).length())
287       shortest_lyric_l_ = lyric;
288   }
289   else
290     longest_lyric_l_ = shortest_lyric_l_ = lyric;
291 }
292
293  void 
294 Voice_alist_entry::set_melisma()
295 {
296   melisma_b_ = true;
297 }
298  
299 bool 
300 Voice_alist_entry::set_lyric_align(const char *punc, Score_element *default_notehead_l)
301 {
302   if(lyric_list_.size()==0) {
303     // No lyrics: nothing to do.
304     return true;
305   }
306
307   Score_element * lyric;
308   alignment_i_ = appropriate_alignment(punc);
309
310   // If there was no notehead in the matching voice context, use the first 
311   // notehead caught from any voice context (any port in a storm).
312   if(!notehead_l_) {
313     notehead_l_ = default_notehead_l;
314   }
315   Real translation = amount_to_translate();
316   for(int l = 0; l < lyric_list_.size(); l++) {
317     /** set the x alignment of each lyric
318      */
319     lyric = lyric_list_[l];
320     lyric->set_elt_property("self-alignment-X", gh_int2scm(alignment_i_));
321
322     // centre on notehead ... if we have one. 
323     if(notehead_l_) {
324       /* set the parent of each lyric to the notehead,
325          set the offset callback of each lyric to centered_on_parent,
326       */
327       lyric->set_parent(notehead_l_, X_AXIS);
328       lyric->add_offset_callback (Side_position::centered_on_parent, X_AXIS);
329       /* reference is on the right of the notehead; move it left half way, then centralise */
330       lyric->translate_axis (translation-(notehead_l_->extent(X_AXIS)).center(), X_AXIS);
331     }
332   }
333   return (notehead_l_);
334 }
335
336 Real 
337 Voice_alist_entry::amount_to_translate()
338 {
339   Real translate = 0.0;
340   if(alignment_i_ != CENTER) {
341     // right or left align ... 
342     /* If length of longest lyric < 2 * length of shortest lyric,
343        - centre longest lyric on notehead
344        Otherwise
345        - move so shortest lyric just reaches notehead centre
346     */
347     // FIXME: do we really know the lyric extent here? Some font sizing comes later?
348     if((longest_lyric_l_->extent(X_AXIS)).length() <
349        (shortest_lyric_l_->extent(X_AXIS)).length() * 2 )
350       translate = alignment_i_*(longest_lyric_l_->extent(X_AXIS)).length()/2;
351     else
352       translate = alignment_i_*(shortest_lyric_l_->extent(X_AXIS)).length();
353   }
354   return translate;
355 }
356
357
358 /** determine what alignment we want.
359     Rules: if first_in_phrase_b_ is set, then alignment is LEFT.
360            otherwise if each syllable ends in punctuation, then alignment is RIGHT
361            otherwise alignment is centre.
362 */
363 int 
364 Voice_alist_entry::appropriate_alignment(const char *punc)
365 {
366   if(first_in_phrase_b_)
367     return LEFT;
368
369   Score_element * lyric;
370   bool end_phrase = true;
371   /* use a property to determine what constitutes punctuation */
372
373   for(int l = 0; l < lyric_list_.size() && end_phrase; l++) {
374     lyric = lyric_list_[l];
375     SCM lyric_scm = lyric->get_elt_property("text");
376     String lyric_str = gh_string_p(lyric_scm)?ly_scm2string(lyric_scm):"";
377     char lastchar;
378     if(lyric_str.length_i()>1) {
379       lastchar = lyric_str[lyric_str.length_i()-2];
380       /* We look at the second last character, because lily always appends a space. */
381       /* If it doesn't end in punctuation then it ain't an end of phrase */
382       if(! strchr(punc, lastchar)) {
383         /* Special case: trailing space. Here examine the previous character and reverse the
384            sense of the test (i.e. trailing space makes a break without punctuation, or 
385            suppresses a break with punctuation).
386            This behaviour can be suppressed by including a space in the 
387            phrasingPunctuation property, in which case trailing space always means 
388            the same as punctuation.
389
390            FIXME: The extra space throws alignment out a bit.
391         */
392         if(lastchar == ' ') {
393           if(lyric_str.length_i()>2) {
394             lastchar = lyric_str[lyric_str.length_i()-3];
395             if(strchr(punc, lastchar))
396               end_phrase=false;
397           }
398         }
399         else
400           end_phrase=false;
401       }
402     }
403   }
404   if(end_phrase)
405     return RIGHT;
406
407   return CENTER;
408 }
409
410 /** We don't know about the melisma until after the initial alignment work is done, so go
411     back and fix the alignment when we DO know.
412 */
413 void
414 Voice_alist_entry::adjust_melisma_align()
415 {
416   if(notehead_l_) {
417     // undo what we did before ...
418     Real translation = -amount_to_translate();
419     // melisma aligning:
420     switch (alignment_i_) {
421       //  case LEFT: // that's all
422     case CENTER: // move right so smallest lyric is left-aligned on notehead
423       translation += (shortest_lyric_l_->extent(X_AXIS)).length()/2;
424       break;
425     case RIGHT: // move right so smallest lyric is left-aligned on notehead
426       translation += (shortest_lyric_l_->extent(X_AXIS)).length();
427       break;
428     }
429     for(int l = 0; l < lyric_list_.size(); l++) {
430       lyric_list_[l]->translate_axis (translation, X_AXIS);
431     }
432   }
433 }
434
435
436 bool
437 Voice_alist_entry::is_empty()
438 {
439   return lyric_list_.size()==0;
440 }
441
442 void
443 Voice_alist_entry::next_lyric()
444 {
445   first_in_phrase_b_ = (alignment_i_ == RIGHT);
446   clear();
447 }
448
449 /* SMOB */
450
451 #include "ly-smobs.icc"
452
453 SCM
454 Voice_alist_entry::mark_smob (SCM)
455 {
456   return SCM_EOL;
457 }
458
459 int
460 Voice_alist_entry::print_smob (SCM, SCM port, scm_print_state * )
461 {
462   scm_puts ("#<Voice_alist_entry>", port);
463   return 1;
464 }
465
466 IMPLEMENT_UNSMOB(Voice_alist_entry, voice_entry);
467 IMPLEMENT_SIMPLE_SMOBS(Voice_alist_entry);
468 IMPLEMENT_DEFAULT_EQUAL_P(Voice_alist_entry);
469
470 SCM
471 Voice_alist_entry::make_entry ()
472 {
473   Voice_alist_entry *vi = new Voice_alist_entry;
474   return vi->smobbed_self ();
475 }