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