]> git.donarmstrong.com Git - lilypond.git/blob - lily/lyric-phrasing-engraver.cc
''
[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_l_ = 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 = ly_str02scm (context_id.ch_C ());
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             { // it's an old entry ... make it a new one
116               SCM val = gh_cons (gh_cons (ly_caadr (s), SCM_BOOL_F), ly_cddr (s)); 
117               voice_alist_ = scm_assoc_set_x (voice_alist_, ly_car (s), val);
118               return unsmob_voice_entry (ly_caar (val));
119             }
120           else { // the entry is current ... return it.
121             SCM entry_scm = ly_caadr (s);
122             return unsmob_voice_entry (entry_scm);
123           }
124         }
125     }
126   // ((alist_entry . old_entry) . previous_entry)
127   SCM val = gh_cons (gh_cons (Syllable_group::make_entry (), SCM_BOOL_F), 
128                      Syllable_group::make_entry ()); 
129
130   voice_alist_ = scm_acons (key, val, voice_alist_);
131   return unsmob_voice_entry (ly_caar (val));
132 }
133
134
135 void 
136 Lyric_phrasing_engraver::record_notehead (const String &context_id, 
137                                           Grob * notehead)
138 {
139   Syllable_group * v = lookup_context_id (context_id);
140   v->set_notehead (notehead);
141   if (!any_notehead_l_)
142     any_notehead_l_ = notehead;
143 }
144   
145 void 
146 Lyric_phrasing_engraver::record_lyric (const String &context_id, Grob * lyric)
147 {
148   Syllable_group * v = lookup_context_id (context_id);
149   v->add_lyric (lyric);
150 }
151
152 void 
153 Lyric_phrasing_engraver::record_extender (const String &context_id, Grob * extender)
154 {
155   SCM key = ly_str02scm (context_id.ch_C ());
156   if (! gh_null_p (voice_alist_))
157     {
158       SCM s = scm_assoc (key, voice_alist_);
159       if (! (gh_boolean_p (s) && !to_boolean (s)))
160         {
161           /* match found */
162           // (key . ((alist_entry . old_entry) . previous_entry))
163           SCM previous_scm = ly_cddr (s);
164           if (previous_scm != SCM_EOL)
165             {
166               Syllable_group * v = unsmob_voice_entry (previous_scm);
167               v->add_extender (extender);
168             }
169         }
170     }
171 }
172
173 void 
174 Lyric_phrasing_engraver::record_melisma (const String &context_id)
175 {
176   Syllable_group * v = lookup_context_id (context_id);
177   v->set_melisma ();
178 }
179   
180 void
181 Lyric_phrasing_engraver::acknowledge_grob (Grob_info i)
182 {
183   SCM p = get_property ("automaticPhrasing");
184   if (!to_boolean (p))
185     return;
186
187
188   Grob *h = i.grob_l_;
189
190   if (Note_head::has_interface (h))
191     {
192       /* caught a note head ... do something with it */
193
194       /* what's its Voice context name? */
195       String voice_context_id = get_context_id (i.origin_trans_l_->daddy_trans_l_, "Voice");
196       record_notehead (voice_context_id, h);
197
198       /* is it in a melisma ? */
199       if (to_boolean (i.origin_trans_l_->get_property ("melismaEngraverBusy")))
200         {
201           record_melisma (voice_context_id);
202         }
203       return;
204     }
205
206   /* now try for a lyric */
207   if (h->internal_has_interface (ly_symbol2scm ("lyric-syllable-interface")))
208     {
209
210       /* what's its LyricsVoice context name? */
211       String voice_context_id;
212       SCM voice_context_scm = i.origin_trans_l_->get_property ("associatedVoice");
213       if (gh_string_p (voice_context_scm))
214         {
215           voice_context_id = ly_scm2string (voice_context_scm);
216         }
217       else {
218         voice_context_id = get_context_id (i.origin_trans_l_->daddy_trans_l_, "LyricsVoice");
219         voice_context_id = trim_suffix (voice_context_id);
220       }
221       record_lyric (voice_context_id, h);
222       return;
223     }
224
225   /* Catch any extender items and then if we have a melisma, 
226      set the RIGHT item of the extender spanner to the melismatic note in 
227      the corresponding context (if any).
228      This has the effect of finishing the extender under the last note
229      of the melisma, instead of extending it to the next lyric.
230      
231      Problem: the extender request is thrown at the same moment as the next lyric,
232      by which time we have already passed the last note of the melisma.
233      However, the Lyric_phrasing_engraver remembers the last note, so just 
234      attach it to that, provided it was melismatic. If it was not melismatic, 
235      then ignore it and let the Extender_engraver take care of it (i.e. finish at next
236      lyric).
237   */
238   if (h->internal_has_interface (ly_symbol2scm ("lyric-extender-interface")))
239     {
240       String voice_context_id = get_context_id (i.origin_trans_l_->daddy_trans_l_, "LyricsVoice");
241       record_extender (trim_suffix (voice_context_id), h);
242       return;
243     }
244 }
245
246 String 
247 get_context_id (Translator_group * ancestor, const char *type)
248 {
249   while (ancestor != 0 && ancestor->type_str_ != type)
250     {
251       ancestor = ancestor->daddy_trans_l_;
252     }
253
254   if (ancestor != 0)
255     {
256       return ancestor->id_str_;
257     }
258
259   return "";
260 }
261
262 String 
263 trim_suffix (String &id)
264 {
265   int index = id.index_i ('-');
266   if (index >= 0)
267     {
268       return id.left_str (index);
269     }
270   return id;
271 }
272
273
274 void
275 Lyric_phrasing_engraver::create_grobs () 
276 {
277   SCM p = get_property ("automaticPhrasing");
278   if (!to_boolean (p))
279     return;
280
281   
282   /* iterate through entries in voice_alist_
283      for each, call set_lyric_align (alignment). Issue a warning if this returns false.
284   */
285   String punc;
286   SCM sp = get_property ("phrasingPunctuation");
287   punc = gh_string_p (sp) ? ly_scm2string (sp) : ".,;:?!\""; 
288   
289   for (SCM v=voice_alist_; gh_pair_p (v); v = ly_cdr (v))
290     {
291       SCM v_entry = ly_cdar (v);
292       // ((current . oldflag) . previous)
293       if (!to_boolean (ly_cdar (v_entry)))
294         { // not an old entry left over from a prior note ...
295           Syllable_group *entry = unsmob_voice_entry (ly_caar (v_entry));
296
297           /*
298             TODO: give context for warning.
299           */
300           if (! entry->set_lyric_align (punc.ch_C (), any_notehead_l_))
301             warning (_ ("lyrics found without any matching notehead"));
302
303           // is this note melismatic? If so adjust alignment of previous one.
304           if (entry->get_melisma ())
305             {
306               if (entry->lyric_count ())
307                 warning (_ ("Huh? Melismatic note found to have associated lyrics."));
308               SCM previous_scm = ly_cdr (v_entry);
309               if (previous_scm != SCM_EOL)
310                 {
311                   Syllable_group *previous = unsmob_voice_entry (previous_scm);
312                   if (previous->lyric_count ())
313                     previous->adjust_melisma_align ();
314                 }
315             }
316         }
317     }
318 }
319
320
321 void
322 Lyric_phrasing_engraver::stop_translation_timestep ()
323 {
324   for (SCM v=voice_alist_; gh_pair_p (v); v = ly_cdr (v))
325     {
326       SCM entry_scm = ly_cdar (v);
327       // ((alist_entry . entry_is_old) . previous_entry)
328       Syllable_group * entry = unsmob_voice_entry (ly_caar (entry_scm));
329
330       // set previous_entry, set entry_is_old, and resave it to alist_
331       // but only change if this current was not old.
332       if (! to_boolean (ly_cdar (entry_scm)))
333         { 
334           Syllable_group * previous_entry = unsmob_voice_entry (ly_cdr (entry_scm));
335           previous_entry->copy (entry);
336           entry_scm = gh_cons (gh_cons (ly_caar (entry_scm), SCM_BOOL_T), ly_cdr (entry_scm));
337           voice_alist_ = scm_assoc_set_x (voice_alist_, ly_caar (v), entry_scm);
338         }
339       entry->next_lyric ();
340     }
341   any_notehead_l_ = 0;
342 }
343
344
345
346 ENTER_DESCRIPTION(Lyric_phrasing_engraver,
347                   /* descr */       "",
348                   /* creats*/       "",
349                   /* acks  */       "lyric-syllable-interface note-head-interface lyric-extender-interface",
350                   /* reads */       "automaticPhrasing melismaEngraverBusy associatedVoice phrasingPunctuation",
351                   /* write */       "");