]> git.donarmstrong.com Git - lilypond.git/blob - lily/lyric-phrasing-engraver.cc
99eb55515b56a758d549e75de0c72d3b46849930
[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 "spanner.hh"
14 #include "warn.hh"
15
16
17 String get_context_id (Translator_group * ancestor, const char * type);
18 String trim_suffix (String &id);
19
20
21
22 /*
23   TODO: this code is too hairy, and does things that should be in the
24   backend. Fixme.
25 */
26
27 /*
28   TODO:
29
30   shared lyrics should be vertically centered:
31
32   
33
34   > About lyrics, it happens that there are common words for many bars, like
35   > for a refrain say.  When there is an even number of lyrics lines, I do not
36   > know how to force the positioning of the common lyric line in the plain
37   > middle of the others, because this is in between lines.  Not a big matter,
38   > but it would be a bit nicer if this was doable.
39
40 */
41
42 /*
43   We find start and end of phrases, and align lyrics of multiple stanzas
44   accordingly.
45
46   Also, lyrics at start of melismata should be left aligned.
47   (is that only lyrics that are followed by `__'?  Because
48   that seems to be the case now -- jcn)
49
50
51   |        |        |     |      |
52   x|       x|       x|    x|     x|
53
54   1:  Start  sentence  melisma      end.
55   2:  x         x         x_____       x
56
57   Only lyrics that are followed by '__' while there's a melisma,
58   are left-aligned, in this case the third x.
59
60   
61   Alignment and melismata
62
63   I've taken [a different] approach:
64   |      |
65   |      |
66   O      O  <-- second note throws a melisma score element
67   \____/
68
69   ^      ^
70   |      |
71   Lyric (None)
72
73   Lyric_phrasing_engraver keeps track of the current and previous notes and
74   lyrics for each voice, and when it catches a melisma, it adjusts the
75   alignment of the lyrics of the previous note. I hope this isn't
76   unnecessarily convoluted.
77 */
78
79 Lyric_phrasing_engraver::Lyric_phrasing_engraver ()
80 {
81   voice_alist_ = SCM_EOL;
82   any_notehead_ = 0;
83 }
84
85 Lyric_phrasing_engraver::~Lyric_phrasing_engraver ()
86 {
87   /*
88     No need to delete alist_; that's what Garbage collection is for.
89   */
90 }
91
92 void
93 Lyric_phrasing_engraver::finalize ()
94 {
95   /*
96     but do need to unprotect alist_, since Engravers are gc'd now.
97   */
98
99   voice_alist_ = SCM_EOL;
100 }
101
102
103 Syllable_group * 
104 Lyric_phrasing_engraver::lookup_context_id (const String &context_id)
105 {
106   SCM key = scm_makfrom0str (context_id.to_str0 ());
107   if (! gh_null_p (voice_alist_))
108     {
109       SCM s = scm_assoc (key, voice_alist_);
110       if (! (gh_boolean_p (s) && !to_boolean (s)))
111         {
112           /* match found */
113           // (key . ((alist_entry . old_entry) . previous_entry))
114           if (to_boolean (ly_cdadr (s)))
115             {
116               // it's an old entry ... make it a new one
117               SCM val = gh_cons (gh_cons (ly_caadr (s), SCM_BOOL_F), ly_cddr (s)); 
118               voice_alist_ = scm_assoc_set_x (voice_alist_, ly_car (s), val);
119               return unsmob_voice_entry (ly_caar (val));
120             }
121           else
122             {
123               // the entry is current ... return it.
124             SCM entry_scm = ly_caadr (s);
125             return unsmob_voice_entry (entry_scm);
126           }
127         }
128     }
129   // ((alist_entry . old_entry) . previous_entry)
130   SCM val = gh_cons (gh_cons (Syllable_group::make_entry (), SCM_BOOL_F), 
131                      Syllable_group::make_entry ()); 
132
133   voice_alist_ = scm_acons (key, val, voice_alist_);
134   return unsmob_voice_entry (ly_caar (val));
135 }
136
137
138 void 
139 Lyric_phrasing_engraver::record_notehead (const String &context_id, 
140                                           Grob * notehead)
141 {
142   Syllable_group * v = lookup_context_id (context_id);
143   v->set_notehead (notehead);
144   if (!any_notehead_)
145     any_notehead_ = notehead;
146 }
147   
148 void 
149 Lyric_phrasing_engraver::record_lyric (const String &context_id, Grob * lyric)
150 {
151   Syllable_group * v = lookup_context_id (context_id);
152   v->add_lyric (lyric);
153 }
154
155 void 
156 Lyric_phrasing_engraver::record_extender (const String &context_id, Grob * extender)
157 {
158   SCM key = scm_makfrom0str (context_id.to_str0 ());
159   if (! gh_null_p (voice_alist_))
160     {
161       SCM s = scm_assoc (key, voice_alist_);
162       if (! (gh_boolean_p (s) && !to_boolean (s)))
163         {
164           /* match found */
165           // (key . ((alist_entry . old_entry) . previous_entry))
166           SCM previous_scm = ly_cddr (s);
167           if (previous_scm != SCM_EOL)
168             {
169               Syllable_group * v = unsmob_voice_entry (previous_scm);
170               v->add_extender (extender);
171             }
172         }
173     }
174 }
175
176 void 
177 Lyric_phrasing_engraver::record_melisma (const String &context_id)
178 {
179   Syllable_group * v = lookup_context_id (context_id);
180   v->set_melisma ();
181 }
182
183 /*
184   TODO: this engraver is always on, also for orchestral scores. That
185   is a waste of time and space. This should be switched on
186   automatically at the first Lyrics found.
187  */
188 void
189 Lyric_phrasing_engraver::acknowledge_grob (Grob_info i)
190 {
191   SCM p = get_property ("automaticPhrasing");
192   if (!to_boolean (p))
193     return;
194
195
196   Grob *h = i.grob_;
197
198   if (Note_head::has_interface (h))
199     {
200       /* caught a note head ... do something with it */
201
202       /* what's its Voice context name? */
203       String voice_context_id = get_context_id (i.origin_trans_->daddy_trans_, "Voice");
204       record_notehead (voice_context_id, h);
205
206       /* is it in a melisma ? */
207       if (to_boolean (i.origin_trans_->get_property ("melismaEngraverBusy")))
208         {
209           record_melisma (voice_context_id);
210         }
211       return;
212     }
213
214   /* now try for a lyric */
215   if (h->internal_has_interface (ly_symbol2scm ("lyric-syllable-interface")))
216     {
217
218       /* what's its LyricsVoice context name? */
219       String voice_context_id;
220       SCM voice_context_scm = i.origin_trans_->get_property ("associatedVoice");
221       if (gh_string_p (voice_context_scm))
222         {
223           voice_context_id = ly_scm2string (voice_context_scm);
224         }
225       else
226         {
227           voice_context_id = get_context_id (i.origin_trans_->daddy_trans_, "LyricsVoice");
228           voice_context_id = trim_suffix (voice_context_id);
229         }
230       record_lyric (voice_context_id, h);
231       return;
232     }
233
234   /* Catch any extender items and then if we have a melisma, 
235      set the RIGHT item of the extender spanner to the melismatic note in 
236      the corresponding context (if any).
237      This has the effect of finishing the extender under the last note
238      of the melisma, instead of extending it to the next lyric.
239      
240      Problem: the extender event is thrown at the same moment as the next lyric,
241      by which time we have already passed the last note of the melisma.
242      However, the Lyric_phrasing_engraver remembers the last note, so just 
243      attach it to that, provided it was melismatic. If it was not melismatic, 
244      then ignore it and let the Extender_engraver take care of it (i.e. finish at next
245      lyric).
246   */
247   if (h->internal_has_interface (ly_symbol2scm ("lyric-extender-interface")))
248     {
249       String voice_context_id = get_context_id (i.origin_trans_->daddy_trans_, "LyricsVoice");
250       record_extender (trim_suffix (voice_context_id), h);
251       return;
252     }
253 }
254
255 String 
256 get_context_id (Translator_group * ancestor, const char *type)
257 {
258   while (ancestor != 0 && ancestor->type_string_ != type)
259     {
260       ancestor = ancestor->daddy_trans_;
261     }
262
263   if (ancestor != 0)
264     {
265       return ancestor->id_string_;
266     }
267
268   return "";
269 }
270
271 String 
272 trim_suffix (String &id)
273 {
274   int index = id.index ('-');
275   if (index >= 0)
276     {
277       return id.left_string (index);
278     }
279   return id;
280 }
281
282
283 void
284 Lyric_phrasing_engraver::process_acknowledged_grobs () 
285 {
286   SCM p = get_property ("automaticPhrasing");
287   if (!to_boolean (p))
288     return;
289
290   
291   /* iterate through entries in voice_alist_
292      for each, call set_lyric_align (alignment). Issue a warning if this returns false.
293   */
294   String punc;
295   SCM sp = get_property ("phrasingPunctuation");
296   punc = gh_string_p (sp) ? ly_scm2string (sp) : ".,;:?!\""; 
297   
298   for (SCM v=voice_alist_; gh_pair_p (v); v = ly_cdr (v))
299     {
300       SCM v_entry = ly_cdar (v);
301       // ((current . oldflag) . previous)
302       if (!to_boolean (ly_cdar (v_entry)))
303         { 
304           // not an old entry left over from a prior note ...
305           Syllable_group *entry = unsmob_voice_entry (ly_caar (v_entry));
306
307           /*
308             TODO: give context for warning.
309           */
310           if (! entry->set_lyric_align (punc.to_str0 (), any_notehead_))
311             warning (_ ("lyrics found without any matching notehead"));
312
313           // is this note melismatic? If so adjust alignment of previous one.
314           if (entry->get_melisma ())
315             {
316               if (entry->lyric_count ())
317                 warning (_ ("Huh? Melismatic note found to have associated lyrics."));
318               SCM previous_scm = ly_cdr (v_entry);
319               if (previous_scm != SCM_EOL)
320                 {
321                   Syllable_group *previous = unsmob_voice_entry (previous_scm);
322                   if (previous->lyric_count ())
323                     previous->adjust_melisma_align ();
324                 }
325             }
326         }
327     }
328 }
329
330
331 void
332 Lyric_phrasing_engraver::stop_translation_timestep ()
333 {
334   for (SCM v=voice_alist_; gh_pair_p (v); v = ly_cdr (v))
335     {
336       SCM entry_scm = ly_cdar (v);
337       // ((alist_entry . entry_is_old) . previous_entry)
338       Syllable_group * entry = unsmob_voice_entry (ly_caar (entry_scm));
339
340       // set previous_entry, set entry_is_old, and resave it to alist_
341       // but only change if this current was not old.
342       if (! to_boolean (ly_cdar (entry_scm)))
343         { 
344           Syllable_group * previous_entry = unsmob_voice_entry (ly_cdr (entry_scm));
345           previous_entry->copy (entry);
346           entry_scm = gh_cons (gh_cons (ly_caar (entry_scm), SCM_BOOL_T), ly_cdr (entry_scm));
347           voice_alist_ = scm_assoc_set_x (voice_alist_, ly_caar (v), entry_scm);
348         }
349       entry->next_lyric ();
350     }
351   any_notehead_ = 0;
352 }
353
354
355
356 ENTER_DESCRIPTION(Lyric_phrasing_engraver,
357                   /* descr */       
358 "This engraver combines note heads and lyrics for alignment. "
359 "\n\n"
360 "This engraver is switched on by default. Turn it off for faster " 
361 "processing of orchestral scores. ",
362                   /* creats*/       "",
363                   /* accepts */     "",
364 /* acks  */      "lyric-syllable-interface note-head-interface lyric-extender-interface",
365                   /* reads */       "automaticPhrasing melismaEngraverBusy associatedVoice phrasingPunctuation",
366                   /* write */       "");