2 lyric-phrasing-engraver.cc -- implement Lyric_phrasing_engraver
4 source file of the GNU LilyPond music typesetter
6 (c) 2000 Glen Prideaux <glenprideaux@iname.com>
10 #include "lyric-phrasing-engraver.hh"
11 #include "note-head.hh"
12 #include "translator-group.hh"
13 #include "side-position-interface.hh"
15 String get_context_id(Translator_group * ancestor, const char * type);
16 String trim_suffix(String &id);
18 ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
21 We find start and end of phrases, and align lyrics accordingly.
22 Also, lyrics at start of melismata should be left aligned.
24 Alignment and melismata
26 I've taken [a different] approach:
29 O O <-- second note throws a melisma score element
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.
42 Lyric_phrasing_engraver::Lyric_phrasing_engraver()
44 voice_alist_ = SCM_EOL;
48 Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
51 No need to delete alist_; that's what Garbage collection is for.
56 Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
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))) {
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));
69 else { // the entry is current ... return it.
70 SCM entry_scm = gh_caadr(s);
71 return unsmob_voice_entry(entry_scm);
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 ());
79 voice_alist_ = scm_acons(key, val, voice_alist_);
80 return unsmob_voice_entry (gh_caar(val));
85 Lyric_phrasing_engraver::record_notehead(const String &context_id,
86 Score_element * notehead)
88 Voice_alist_entry * v = lookup_context_id(context_id);
89 v->set_notehead(notehead);
91 any_notehead_l_ = notehead;
95 Lyric_phrasing_engraver::record_lyric(const String &context_id, Score_element * lyric)
97 Voice_alist_entry * v = lookup_context_id(context_id);
102 Lyric_phrasing_engraver::record_melisma(const String &context_id)
104 Voice_alist_entry * v = lookup_context_id(context_id);
109 Lyric_phrasing_engraver::acknowledge_element(Score_element_info i)
111 SCM p = get_property("automaticPhrasing");
116 Score_element *h = i.elem_l_;
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);
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);
132 /* now try for a lyric */
133 if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
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);
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);
150 get_context_id(Translator_group * ancestor, const char *type)
152 while(ancestor != 0 && ancestor->type_str_ != type) {
153 ancestor = ancestor->daddy_trans_l_;
157 return ancestor->id_str_;
164 trim_suffix(String &id)
166 int index = id.index_i('-');
168 return id.left_str(index);
174 void Lyric_phrasing_engraver::process_acknowledged ()
176 /* iterate through entries in voice_alist_
177 for each, call set_lyric_align(alignment). Issue a warning if this returns false.
180 SCM sp = get_property("phrasingPunctuation");
181 punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\"";
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"));
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();
206 Lyric_phrasing_engraver::do_pre_move_processing ()
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));
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);
228 /*=========================================================================================*/
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.
234 Voice_alist_entry::Voice_alist_entry()
236 first_in_phrase_b_=true;
242 Voice_alist_entry::clear()
252 Voice_alist_entry::copy( Voice_alist_entry *from)
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_;
264 Voice_alist_entry::set_first_in_phrase(bool f)
266 first_in_phrase_b_ = f;
270 Voice_alist_entry::set_notehead(Score_element * notehead)
273 /* there should only be a single notehead, so silently ignore any extras */
274 notehead_l_=notehead;
279 Voice_alist_entry::add_lyric(Score_element * lyric)
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;
290 longest_lyric_l_ = shortest_lyric_l_ = lyric;
294 Voice_alist_entry::set_melisma()
300 Voice_alist_entry::set_lyric_align(const char *punc, Score_element *default_notehead_l)
302 if(lyric_list_.size()==0) {
303 // No lyrics: nothing to do.
307 Score_element * lyric;
308 alignment_i_ = appropriate_alignment(punc);
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).
313 notehead_l_ = default_notehead_l;
315 Real translation = amount_to_translate();
316 for(int l = 0; l < lyric_list_.size(); l++) {
317 /** set the x alignment of each lyric
319 lyric = lyric_list_[l];
320 lyric->set_elt_property("self-alignment-X", gh_int2scm(alignment_i_));
322 // centre on notehead ... if we have one.
324 /* set the parent of each lyric to the notehead,
325 set the offset callback of each lyric to centered_on_parent,
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);
333 return (notehead_l_);
337 Voice_alist_entry::amount_to_translate()
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
345 - move so shortest lyric just reaches notehead centre
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;
352 translate = alignment_i_*(shortest_lyric_l_->extent(X_AXIS)).length();
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.
364 Voice_alist_entry::appropriate_alignment(const char *punc)
366 if(first_in_phrase_b_)
369 Score_element * lyric;
370 bool end_phrase = true;
371 /* use a property to determine what constitutes punctuation */
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):"";
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.
390 FIXME: The extra space throws alignment out a bit.
392 if(lastchar == ' ') {
393 if(lyric_str.length_i()>2) {
394 lastchar = lyric_str[lyric_str.length_i()-3];
395 if(strchr(punc, lastchar))
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.
414 Voice_alist_entry::adjust_melisma_align()
417 // undo what we did before ...
418 Real translation = -amount_to_translate();
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;
425 case RIGHT: // move right so smallest lyric is left-aligned on notehead
426 translation += (shortest_lyric_l_->extent(X_AXIS)).length();
429 for(int l = 0; l < lyric_list_.size(); l++) {
430 lyric_list_[l]->translate_axis (translation, X_AXIS);
437 Voice_alist_entry::is_empty()
439 return lyric_list_.size()==0;
443 Voice_alist_entry::next_lyric()
445 first_in_phrase_b_ = (alignment_i_ == RIGHT);
451 #include "ly-smobs.icc"
454 Voice_alist_entry::mark_smob (SCM)
460 Voice_alist_entry::print_smob (SCM, SCM port, scm_print_state * )
462 scm_puts ("#<Voice_alist_entry>", port);
466 IMPLEMENT_UNSMOB(Voice_alist_entry, voice_entry);
467 IMPLEMENT_SIMPLE_SMOBS(Voice_alist_entry);
468 IMPLEMENT_DEFAULT_EQUAL_P(Voice_alist_entry);
471 Voice_alist_entry::make_entry ()
473 Voice_alist_entry *vi = new Voice_alist_entry;
474 return vi->smobbed_self ();