source file of the GNU LilyPond music typesetter
- (c) 2000 Glen Prideaux <glenprideaux@iname.com>
+ (c) 2000--2003 Glen Prideaux <glenprideaux@iname.com>
*/
#include <string.h>
#include "lyric-phrasing-engraver.hh"
#include "note-head.hh"
#include "translator-group.hh"
-#include "side-position-interface.hh"
+#include "spanner.hh"
+#include "warn.hh"
-String get_context_id(Translator_group * ancestor, const char * type);
-String trim_suffix(String &id);
-ADD_THIS_TRANSLATOR (Lyric_phrasing_engraver);
+String get_context_id (Translator_group * ancestor, SCM);
+String trim_suffix (String &id);
-Lyric_phrasing_engraver::Lyric_phrasing_engraver()
-{
- voice_alist_ = SCM_EOL;
- any_notehead_l_ = 0;
-}
-Lyric_phrasing_engraver::~Lyric_phrasing_engraver()
-{
- /*
- No need to delete alist_; that's what Garbage collection is for.
- */
-}
+/*
+ TODO: this code is too hairy, and does things that should be in the
+ backend. Fixme.
+*/
-Voice_alist_entry *
-Lyric_phrasing_engraver::lookup_context_id(const String &context_id)
-{
- SCM key = ly_str02scm(context_id.ch_C());
- if( ! gh_null_p(voice_alist_) ) {
- SCM s = scm_assoc(key, voice_alist_);
- if(! (gh_boolean_p(s) && !to_boolean(s))) {
- /* match found */
- return unsmob_voice_entry(gh_cdr(s));
- }
- }
- SCM val = Voice_alist_entry::make_entry ();
- voice_alist_ = scm_acons(key, val, voice_alist_);
- return unsmob_voice_entry (val);
-}
+/*
+ TODO:
+ shared lyrics should be vertically centered:
-void
-Lyric_phrasing_engraver::record_notehead(const String &context_id, Score_element * notehead)
-{
- Voice_alist_entry * v = lookup_context_id(context_id);
- v->set_notehead(notehead);
- if(!any_notehead_l_)
- any_notehead_l_ = notehead;
-}
-void
-Lyric_phrasing_engraver::record_lyric(const String &context_id, Score_element * lyric)
-{
- Voice_alist_entry * v = lookup_context_id(context_id);
- v->add_lyric(lyric);
-}
-
-
-
-
-void
-Lyric_phrasing_engraver::acknowledge_element(Score_element_info i)
-{
- SCM p = get_property("automaticPhrasing");
- if(!to_boolean(p))
- return;
+ > About lyrics, it happens that there are common words for many bars, like
+ > for a refrain say. When there is an even number of lyrics lines, I do not
+ > know how to force the positioning of the common lyric line in the plain
+ > middle of the others, because this is in between lines. Not a big matter,
+ > but it would be a bit nicer if this was doable.
- Score_element *h = i.elem_l_;
+*/
- if (Note_head::has_interface(h)) {
- /* caught a note head ... do something with it */
- /* ... but not if it's a grace note ... */
- bool grace= to_boolean (i.elem_l_->get_elt_property ("grace"));
- SCM wg = get_property ("weAreGraceContext");
- bool wgb = to_boolean (wg);
- if (grace != wgb)
- return;
+/*
+ We find start and end of phrases, and align lyrics of multiple stanzas
+ accordingly.
- /* what's its Voice context name? */
- String voice_context_id = get_context_id(i.origin_trans_l_->daddy_trans_l_, "Voice");
- record_notehead(voice_context_id, h);
- return;
- }
- /* now try for a lyric */
- if (h->has_interface (ly_symbol2scm ("lyric-syllable-interface"))) {
+ Also, lyrics at start of melismata should be left aligned.
+ (is that only lyrics that are followed by `__'? Because
+ that seems to be the case now -- jcn)
- /* what's its LyricVoice context name? */
- String lyric_voice_context_id =
- get_context_id(i.origin_trans_l_->daddy_trans_l_, "LyricVoice");
- record_lyric(trim_suffix(lyric_voice_context_id), h);
- return;
- }
-}
+ | | | | |
+ x| x| x| x| x|
-String
-get_context_id(Translator_group * ancestor, const char *type)
-{
- while(ancestor != 0 && ancestor->type_str_ != type) {
- ancestor = ancestor->daddy_trans_l_;
- }
+ 1: Start sentence melisma end.
+ 2: x x x_____ x
- if(ancestor != 0) {
- return ancestor->id_str_;
- }
+ Only lyrics that are followed by '__' while there's a melisma,
+ are left-aligned, in this case the third x.
- return "";
-}
+
+ Alignment and melismata
+
+ I've taken [a different] approach:
+ | |
+ | |
+ O O <-- second note throws a melisma score element
+ \____/
+
+ ^ ^
+ | |
+ Lyric (None)
+
+ Lyric_phrasing_engraver keeps track of the current and previous notes and
+ lyrics for each voice, and when it catches a melisma, it adjusts the
+ alignment of the lyrics of the previous note. I hope this isn't
+ unnecessarily convoluted.
+*/
-String
-trim_suffix(String &id)
+Lyric_phrasing_engraver::Lyric_phrasing_engraver ()
{
- int index = id.index_i('-');
- if(index >= 0) {
- return id.left_str(index);
- }
- return id;
+ voice_alist_ = SCM_EOL;
+ any_notehead_ = 0;
}
-
-void Lyric_phrasing_engraver::process_acknowledged ()
+Lyric_phrasing_engraver::~Lyric_phrasing_engraver ()
{
- /* iterate through entries in voice_alist_
- for each, call set_lyric_align(alignment). Issue a warning if this returns false.
+ /*
+ No need to delete alist_; that's what Garbage collection is for.
*/
- Voice_alist_entry *entry;
- String punc;
- SCM sp = get_property("phrasingPunctuation");
- punc = gh_string_p(sp) ? ly_scm2string(sp) : ".,;:?!\"";
-
- for(unsigned v=0; v < gh_length(voice_alist_); v++) {
- entry = unsmob_voice_entry(gh_cdr(gh_list_ref(voice_alist_, gh_int2scm(v))));
- if(! entry->set_lyric_align(punc.ch_C(), any_notehead_l_))
- warning (_ ("lyrics found without any matching notehead"));
- }
}
-
void
-Lyric_phrasing_engraver::do_pre_move_processing ()
+Lyric_phrasing_engraver::finalize ()
{
- Voice_alist_entry * entry;
- for(unsigned v=0; v < gh_length(voice_alist_); v++) {
- entry = unsmob_voice_entry(gh_cdr(gh_list_ref(voice_alist_, gh_int2scm(v))));
- entry->next_lyric();
- }
- any_notehead_l_ = 0;
-}
-
-
+ /*
+ but do need to unprotect alist_, since Engravers are gc'd now.
+ */
-/*=========================================================================================*/
+ voice_alist_ = SCM_EOL;
+}
-/** Voice_alist_entry is a class to be smobbed and entered as data in the association list
- member of the Lyric_phrasing_engraver class.
-*/
-Voice_alist_entry::Voice_alist_entry()
+Syllable_group *
+Lyric_phrasing_engraver::lookup_context_id (const String &context_id)
{
- first_in_phrase_b_=true;
- clear();
-}
-
+ SCM key = scm_makfrom0str (context_id.to_str0 ());
+ if (! gh_null_p (voice_alist_))
+ {
+ SCM s = scm_assoc (key, voice_alist_);
+ if (! (gh_boolean_p (s) && !to_boolean (s)))
+ {
+ /* match found */
+ // (key . ((alist_entry . old_entry) . previous_entry))
+ if (to_boolean (ly_cdadr (s)))
+ {
+ // it's an old entry ... make it a new one
+ SCM val = gh_cons (gh_cons (ly_caadr (s), SCM_BOOL_F), ly_cddr (s));
+ voice_alist_ = scm_assoc_set_x (voice_alist_, ly_car (s), val);
+ return unsmob_voice_entry (ly_caar (val));
+ }
+ else
+ {
+ // the entry is current ... return it.
+ SCM entry_scm = ly_caadr (s);
+ return unsmob_voice_entry (entry_scm);
+ }
+ }
+ }
+ // ((alist_entry . old_entry) . previous_entry)
+ SCM val = gh_cons (gh_cons (Syllable_group::make_entry (), SCM_BOOL_F),
+ Syllable_group::make_entry ());
+ voice_alist_ = scm_acons (key, val, voice_alist_);
+ return unsmob_voice_entry (ly_caar (val));
+}
void
-Voice_alist_entry::clear()
+Lyric_phrasing_engraver::record_notehead (const String &context_id,
+ Grob * notehead)
{
- notehead_l_=0;
- lyric_list_.clear();
- longest_lyric_l_=0;
- shortest_lyric_l_=0;
+ Syllable_group * v = lookup_context_id (context_id);
+ v->set_notehead (notehead);
+ if (!any_notehead_)
+ any_notehead_ = notehead;
}
void
-Voice_alist_entry::set_first_in_phrase(bool f)
-{
- first_in_phrase_b_ = f;
+Lyric_phrasing_engraver::record_lyric (const String &context_id, Grob * lyric)
+{
+ Syllable_group * v = lookup_context_id (context_id);
+ v->add_lyric (lyric);
}
void
-Voice_alist_entry::set_notehead(Score_element * notehead)
+Lyric_phrasing_engraver::record_extender (const String &context_id, Grob * extender)
{
- if(!notehead_l_)
- /* there should only be a single notehead, so silently ignore any extras */
- notehead_l_=notehead;
+ SCM key = scm_makfrom0str (context_id.to_str0 ());
+ if (! gh_null_p (voice_alist_))
+ {
+ SCM s = scm_assoc (key, voice_alist_);
+ if (! (gh_boolean_p (s) && !to_boolean (s)))
+ {
+ /* match found */
+ // (key . ((alist_entry . old_entry) . previous_entry))
+ SCM previous_scm = ly_cddr (s);
+ if (previous_scm != SCM_EOL)
+ {
+ Syllable_group * v = unsmob_voice_entry (previous_scm);
+ v->add_extender (extender);
+ }
+ }
+ }
}
void
-Voice_alist_entry::add_lyric(Score_element * lyric)
+Lyric_phrasing_engraver::record_melisma (const String &context_id)
{
- lyric_list_.push(lyric);
- /* record longest and shortest lyrics */
- if( longest_lyric_l_ ) {
- if(lyric->extent(X_AXIS).length() > (longest_lyric_l_->extent(X_AXIS)).length())
- longest_lyric_l_ = lyric;
- if(lyric->extent(X_AXIS).length() < (shortest_lyric_l_->extent(X_AXIS)).length())
- shortest_lyric_l_ = lyric;
- }
- else
- longest_lyric_l_ = shortest_lyric_l_ = lyric;
+ Syllable_group * v = lookup_context_id (context_id);
+ v->set_melisma ();
}
-
-bool
-Voice_alist_entry::set_lyric_align(const char *punc, Score_element *default_notehead_l)
+
+/*
+ TODO: this engraver is always on, also for orchestral scores. That
+ is a waste of time and space. This should be switched on
+ automatically at the first Lyrics found.
+ */
+void
+Lyric_phrasing_engraver::acknowledge_grob (Grob_info i)
{
- if(lyric_list_.size()<2) {
- /* Only for multi-stanza songs ... if we've only a single lyric (or none at all) we
- do nothing.
- */
- clear();
- return true;
- }
-
- Score_element * lyric;
- alignment_i_ = appropriate_alignment(punc);
-
- for(int l = 0; l < lyric_list_.size(); l++) {
- /** set the x alignment of each lyric
- */
- lyric = lyric_list_[l];
- lyric->set_elt_property("self-alignment-X", gh_int2scm(alignment_i_));
-
- // centre on notehead ... if we have one. If there was no notehead in the matching
- // voice context, use the first notehead caught from any voice context (any port in a storm).
- if(notehead_l_ || default_notehead_l) {
- /* set the parent of each lyric to the notehead,
- set the offset callback of each lyric to centered_on_parent,
- */
- Score_element * parent_nh = notehead_l_ ? notehead_l_ : default_notehead_l;
- lyric->set_parent(parent_nh, X_AXIS);
- lyric->add_offset_callback (Side_position::centered_on_parent, X_AXIS);
- /* reference is on the right of the notehead; move it left half way */
- lyric->translate_axis (-(parent_nh->extent(X_AXIS)).center(), X_AXIS);
-
- if(alignment_i_ != CENTER) {
- // right or left align ...
- /* If length of longest lyric < 2 * length of shortest lyric,
- - centre longest lyric on notehead
- Otherwise
- - move so shortest lyric just reaches notehead centre
- */
- // FIXME: do we really know the lyric extent here? Some font sizing comes later?
- Real translate;
- if((longest_lyric_l_->extent(X_AXIS)).length() <
- (shortest_lyric_l_->extent(X_AXIS)).length() * 2 )
- translate = alignment_i_*(longest_lyric_l_->extent(X_AXIS)).length()/2;
- else
- translate = alignment_i_*(shortest_lyric_l_->extent(X_AXIS)).length();
- lyric->translate_axis (translate, X_AXIS);
- }
+ SCM p = get_property ("automaticPhrasing");
+ if (!to_boolean (p))
+ return;
+
+
+ Grob *h = i.grob_;
+
+ if (Note_head::has_interface (h))
+ {
+ /* caught a note head ... do something with it */
+
+ /* what's its Voice context name? */
+ String voice_context_id = get_context_id (i.origin_trans_->daddy_trans_, ly_symbol2scm ("Voice"));
+ record_notehead (voice_context_id, h);
+
+ /* is it in a melisma ? */
+ if (to_boolean (i.origin_trans_->get_property ("melismaEngraverBusy")))
+ {
+ record_melisma (voice_context_id);
+ }
+ return;
}
- }
- return (notehead_l_ || default_notehead_l);
-}
-/** determine what alignment we want.
- Rules: if first_in_phrase_b_ is set, then alignment is LEFT.
- otherwise if each syllable ends in punctuation, then alignment is RIGHT
- otherwise alignment is centre.
-*/
-int
-Voice_alist_entry::appropriate_alignment(const char *punc)
-{
- if(first_in_phrase_b_)
- return LEFT;
-
- Score_element * lyric;
- bool end_phrase = true;
- /* use a property to determine what constitutes punctuation */
-
- for(int l = 0; l < lyric_list_.size() && end_phrase; l++) {
- lyric = lyric_list_[l];
- SCM lyric_scm = lyric->get_elt_property("text");
- String lyric_str = gh_string_p(lyric_scm)?ly_scm2string(lyric_scm):"";
- char lastchar;
- if(lyric_str.length_i()>1) {
- lastchar = lyric_str[lyric_str.length_i()-2];
- /* We look at the second last character, because lily always appends a space. */
- /* If it doesn't end in punctuation then it ain't an end of phrase */
- if(! strchr(punc, lastchar)) {
- /* Special case: trailing space. Here examine the previous character and reverse the
- sense of the test (i.e. trailing space makes a break without punctuation, or
- suppresses a break with punctuation).
- This behaviour can be suppressed by including a space in the
- phrasingPunctuation property, in which case trailing space always means
- the same as punctuation.
-
- FIXME: The extra space throws alignment out a bit.
- */
- if(lastchar == ' ') {
- if(lyric_str.length_i()>2) {
- lastchar = lyric_str[lyric_str.length_i()-3];
- if(strchr(punc, lastchar))
- end_phrase=false;
- }
+ /* now try for a lyric */
+ if (h->internal_has_interface (ly_symbol2scm ("lyric-syllable-interface")))
+ {
+
+ /* what's its LyricsVoice context name? */
+ String voice_context_id;
+ SCM voice_context_scm = i.origin_trans_->get_property ("associatedVoice");
+ if (gh_string_p (voice_context_scm))
+ {
+ voice_context_id = ly_scm2string (voice_context_scm);
+ }
+ else
+ {
+ voice_context_id = get_context_id (i.origin_trans_->daddy_trans_,ly_symbol2scm ( "LyricsVoice"));
+ voice_context_id = trim_suffix (voice_context_id);
}
- else
- end_phrase=false;
- }
+ record_lyric (voice_context_id, h);
+ return;
}
- }
- if(end_phrase)
- return RIGHT;
- return CENTER;
+ /* Catch any extender items and then if we have a melisma,
+ set the RIGHT item of the extender spanner to the melismatic note in
+ the corresponding context (if any).
+ This has the effect of finishing the extender under the last note
+ of the melisma, instead of extending it to the next lyric.
+
+ Problem: the extender event is thrown at the same moment as the next lyric,
+ by which time we have already passed the last note of the melisma.
+ However, the Lyric_phrasing_engraver remembers the last note, so just
+ attach it to that, provided it was melismatic. If it was not melismatic,
+ then ignore it and let the Extender_engraver take care of it (i.e. finish at next
+ lyric).
+ */
+ if (h->internal_has_interface (ly_symbol2scm ("lyric-extender-interface")))
+ {
+ String voice_context_id = get_context_id (i.origin_trans_->daddy_trans_, ly_symbol2scm ("LyricsVoice"));
+ record_extender (trim_suffix (voice_context_id), h);
+ return;
+ }
}
-bool
-Voice_alist_entry::is_empty()
+String
+get_context_id (Translator_group * ancestor, SCM type)
{
- return lyric_list_.size()==0;
+ while (ancestor != 0 && !ancestor->is_alias_b(type))
+ {
+ ancestor = ancestor->daddy_trans_;
+ }
+
+ if (ancestor != 0)
+ {
+ return ancestor->id_string_;
+ }
+
+ return "";
}
-void
-Voice_alist_entry::next_lyric()
+String
+trim_suffix (String &id)
{
- first_in_phrase_b_ = (alignment_i_ == RIGHT);
- clear();
+ int index = id.index ('-');
+ if (index >= 0)
+ {
+ return id.left_string (index);
+ }
+ return id;
}
-/* SMOB */
-
-#include "ly-smobs.icc"
-SCM
-Voice_alist_entry::mark_smob (SCM)
+void
+Lyric_phrasing_engraver::process_acknowledged_grobs ()
{
- return SCM_EOL;
-}
+ SCM p = get_property ("automaticPhrasing");
+ if (!to_boolean (p))
+ return;
-int
-Voice_alist_entry::print_smob (SCM, SCM port, scm_print_state * )
-{
- scm_puts ("#<Voice_alist_entry>", port);
- return 1;
+
+ /* iterate through entries in voice_alist_
+ for each, call set_lyric_align (alignment). Issue a warning if this returns false.
+ */
+ String punc;
+ SCM sp = get_property ("phrasingPunctuation");
+ punc = gh_string_p (sp) ? ly_scm2string (sp) : ".,;:?!\"";
+
+ for (SCM v=voice_alist_; gh_pair_p (v); v = ly_cdr (v))
+ {
+ SCM v_entry = ly_cdar (v);
+ // ((current . oldflag) . previous)
+ if (!to_boolean (ly_cdar (v_entry)))
+ {
+ // not an old entry left over from a prior note ...
+ Syllable_group *entry = unsmob_voice_entry (ly_caar (v_entry));
+
+ /*
+ TODO: give context for warning.
+ */
+ if (! entry->set_lyric_align (punc.to_str0 (), any_notehead_))
+ warning (_ ("lyrics found without any matching notehead"));
+
+ // is this note melismatic? If so adjust alignment of previous one.
+ if (entry->get_melisma ())
+ {
+ if (entry->lyric_count ())
+ warning (_ ("Huh? Melismatic note found to have associated lyrics."));
+ SCM previous_scm = ly_cdr (v_entry);
+ if (previous_scm != SCM_EOL)
+ {
+ Syllable_group *previous = unsmob_voice_entry (previous_scm);
+ if (previous->lyric_count ())
+ previous->adjust_melisma_align ();
+ }
+ }
+ }
+ }
}
-IMPLEMENT_UNSMOB(Voice_alist_entry, voice_entry);
-IMPLEMENT_SIMPLE_SMOBS(Voice_alist_entry);
-IMPLEMENT_DEFAULT_EQUAL_P(Voice_alist_entry);
-SCM
-Voice_alist_entry::make_entry ()
+void
+Lyric_phrasing_engraver::stop_translation_timestep ()
{
- Voice_alist_entry *vi = new Voice_alist_entry;
- return vi->smobbed_self ();
+ for (SCM v=voice_alist_; gh_pair_p (v); v = ly_cdr (v))
+ {
+ SCM entry_scm = ly_cdar (v);
+ // ((alist_entry . entry_is_old) . previous_entry)
+ Syllable_group * entry = unsmob_voice_entry (ly_caar (entry_scm));
+
+ // set previous_entry, set entry_is_old, and resave it to alist_
+ // but only change if this current was not old.
+ if (! to_boolean (ly_cdar (entry_scm)))
+ {
+ Syllable_group * previous_entry = unsmob_voice_entry (ly_cdr (entry_scm));
+ previous_entry->copy (entry);
+ entry_scm = gh_cons (gh_cons (ly_caar (entry_scm), SCM_BOOL_T), ly_cdr (entry_scm));
+ voice_alist_ = scm_assoc_set_x (voice_alist_, ly_caar (v), entry_scm);
+ }
+ entry->next_lyric ();
+ }
+ any_notehead_ = 0;
}
+
+
+
+ENTER_DESCRIPTION(Lyric_phrasing_engraver,
+ /* descr */
+"This engraver combines note heads and lyrics for alignment. "
+"\n\n"
+"This engraver is switched on by default. Turn it off for faster "
+"processing of orchestral scores. ",
+ /* creats*/ "",
+ /* accepts */ "",
+/* acks */ "lyric-syllable-interface note-head-interface lyric-extender-interface",
+ /* reads */ "automaticPhrasing melismaEngraverBusy associatedVoice phrasingPunctuation",
+ /* write */ "");