From: Dan Eble Date: Sat, 25 Jun 2016 13:04:13 +0000 (-0400) Subject: Issue 4048 (4/5) Handle multiple (de)crescendi in depart/return groups X-Git-Url: https://git.donarmstrong.com/?a=commitdiff_plain;h=3f196c939480dbb2b82c4aeb5692c90c0677e67d;p=lilypond.git Issue 4048 (4/5) Handle multiple (de)crescendi in depart/return groups --- diff --git a/lily/audio-item.cc b/lily/audio-item.cc index 06603584b9..2c5d691656 100644 --- a/lily/audio-item.cc +++ b/lily/audio-item.cc @@ -132,7 +132,7 @@ moment_to_ticks (Moment m) return int (moment_to_real (m) * 384 * 4); } -void Audio_span_dynamic::set_end_moment(Moment mom) +void Audio_span_dynamic::set_end_moment (Moment mom) { if (mom < start_moment_) { diff --git a/lily/dynamic-performer.cc b/lily/dynamic-performer.cc index ab0005b8f4..6acf7d00a1 100644 --- a/lily/dynamic-performer.cc +++ b/lily/dynamic-performer.cc @@ -40,65 +40,273 @@ protected: void listen_absolute_dynamic (Stream_event *); private: + void close_and_enqueue_span (); + Real compute_departure_volume (Direction depart_dir, + Real start_vol, + Real end_vol, + Real min_vol, + Real max_vol); + bool drive_state_machine (Direction next_grow_dir); // next_vol < 0 means select a target dynamic based on growth direction. // return actual next volume (computed if not provided) - Real end_span (Real next_vol = -1.0); + Real finish_queued_spans (Real next_vol = -1.0); + Real look_up_absolute_volume (SCM dynamicString, + Real defaultValue); + +private: + // This performer queues a number of dynamic spans waiting for the following + // pattern before computing their volume levels. + // + // 1. the first (de)crescendo, followed by ... + // 2. zero or more spans that either change in the same direction as the + // first or do not change, followed by ... + // 3. zero or more spans that either change in the opposite direction as the + // first or do not change + // + // The search may be cut short by an absolute dynamic or the end of the + // context. + enum State + { + STATE_INITIAL = 0, // waiting for a (de)crescendo + STATE_DEPART, // enqueued the first span, gathering same-direction spans + STATE_RETURN // gathering opposite-direction spans + }; + + struct UnfinishedSpan + { + Audio_span_dynamic *dynamic_; + Direction grow_dir_; + + UnfinishedSpan () : dynamic_ (0), grow_dir_ (CENTER) {} + }; + + struct DynamicQueue + { + vector spans_; + // total duration of (de)crescendi (i.e. excluding fixed-volume spans) + Real change_duration_; + Real min_target_vol_; + Real max_target_vol_; + + DynamicQueue () : change_duration_ (0) {} + + void clear () + { + spans_.clear (); + change_duration_ = 0; + } + + void push_back (const UnfinishedSpan &span, + Real min_target_vol, + Real max_target_vol) + { + if (span.grow_dir_ != CENTER) + change_duration_ += span.dynamic_->get_duration (); + min_target_vol_ = min_target_vol; + max_target_vol_ = max_target_vol; + spans_.push_back (span); + } + + void set_volume (Real start_vol, Real target_vol); + }; private: Stream_event *script_event_; Drul_array span_events_; Direction next_grow_dir_; - Audio_span_dynamic *span_dynamic_; - Direction grow_dir_; // of span_dynamic_ + Direction depart_dir_; + UnfinishedSpan open_span_; + DynamicQueue depart_queue_; + DynamicQueue return_queue_; + State state_; }; Dynamic_performer::Dynamic_performer () + : script_event_ (0), + next_grow_dir_ (CENTER), + depart_dir_ (CENTER), + state_ (STATE_INITIAL) { - script_event_ = 0; span_events_[LEFT] - = span_events_[RIGHT] = 0; - next_grow_dir_ = CENTER; - span_dynamic_ = 0; - grow_dir_ = CENTER; + = span_events_[RIGHT] = 0; +} + +bool +Dynamic_performer::drive_state_machine (Direction next_grow_dir) +{ + switch (state_) + { + case STATE_INITIAL: + if (next_grow_dir != CENTER) + { + state_ = STATE_DEPART; + depart_dir_ = next_grow_dir; + } + break; + + case STATE_DEPART: + if (next_grow_dir == -depart_dir_) + state_ = STATE_RETURN; + break; + + case STATE_RETURN: + if (next_grow_dir == depart_dir_) + { + state_ = STATE_DEPART; + return true; + } + break; + } + + return false; +} + +void +Dynamic_performer::close_and_enqueue_span () +{ + if (!open_span_.dynamic_) + programming_error ("no open dynamic span"); + else + { + DynamicQueue &dq + = (state_ == STATE_RETURN) ? return_queue_ : depart_queue_; + + // Changing equalizer settings in the course of the performance does not + // seem very likely. This is a fig leaf: Equalize these limit volumes + // now as the required context properties are current. Note that only + // the limits at the end of the last span in the queue are kept. + + // Resist diminishing to silence. (Idea: Look up "ppppp" + // with dynamicAbsoluteVolumeFunction, however that would yield 0.25.) + const Real min_target = equalize_volume (0.1); + const Real max_target + = equalize_volume (Audio_span_dynamic::MAXIMUM_VOLUME); + + open_span_.dynamic_->set_end_moment (now_mom ()); + dq.push_back (open_span_, min_target, max_target); + } + + open_span_ = UnfinishedSpan (); +} + +// Set the starting and target volume for each span in the queue. The gain +// (loss) of any (de)crescendo is proportional to its share of the total time +// spent changing. +void +Dynamic_performer::DynamicQueue::set_volume (Real start_vol, + Real target_vol) +{ + const Real gain = target_vol - start_vol; + Real dur = 0; // duration of (de)crescendi processed so far + Real vol = start_vol; + for (vector::iterator it = spans_.begin (); + it != spans_.end (); ++it) + { + const Real prev_vol = vol; + if (it->grow_dir_ != CENTER) + { + // grant this (de)crescendo its portion of the gain + dur += it->dynamic_->get_duration (); + vol = start_vol + gain * (dur / change_duration_); + } + it->dynamic_->set_volume (prev_vol, vol); + } } -Real Dynamic_performer::end_span (Real next_vol) +// Return a volume which is reasonably distant from the given start and end +// volumes in the given direction, for use as a peak volume in a passage with a +// crescendo followed by a decrescendo (or vice versa). If the given volumes +// are equal, the returned volume is a also reasonable target volume for a +// single (de)crescendo. +// +// The given minimum and maximum volumes are the allowable dynamic range. +Real +Dynamic_performer::compute_departure_volume (Direction depart_dir, + Real start_vol, + Real end_vol, + Real min_vol, + Real max_vol) { - if (!span_dynamic_) + if (depart_dir == CENTER) + return start_vol; + + // Try to find a volume that is a minimum distance from the starting and + // ending volumes. If the endpoint volumes differ, the nearer one is padded + // less than the farther one. + // + // Example: mf < ... > p. The legacy behavior was to use a 25% of the + // dynamic range for a (de)crescendo to an unspecified target, and this tries + // to preserve that, but is not possible to use a 25% change for both the + // crescendo and the decrescendo and meet the constraints of this example. + // The decrescendo is a greater change than the crescendo. Believing that + // 25% is already more than enough for either, pad using 25% for the greater + // change and 7% for the lesser change. + // + // Idea: Use a context property or callback, e.g. the difference between two + // dynamics in dynamicAbsoluteVolumeFunction. 0.25 is the default difference + // between "p" and "ff". (Isn't that rather wide for this purpose?) 0.07 is + // the default difference between "mp" and "mf". + const Real far_padding = 0.25; + const Real near_padding = 0.07; + + // If for some reason one of the endpoints is already below the supposed + // minimum or maximum, just accept it. + min_vol = min (min (min_vol, start_vol), end_vol); + max_vol = max (max (max_vol, start_vol), end_vol); + + const Real vol_range = max_vol - min_vol; + + const Real near = minmax (depart_dir, start_vol, end_vol) + + depart_dir * near_padding * vol_range; + const Real far = minmax (-depart_dir, start_vol, end_vol) + + depart_dir * far_padding * vol_range; + const Real depart_vol = minmax (depart_dir, near, far); + return max (min (depart_vol, max_vol), min_vol); +} + +Real +Dynamic_performer::finish_queued_spans (Real next_vol) +{ + if (depart_queue_.spans_.empty ()) { - programming_error("no dynamic span to end"); + programming_error ("no dynamic span to finish"); return next_vol; } - Real start_vol = span_dynamic_->get_start_volume (); - Real target_vol = start_vol; - - if (grow_dir_ != CENTER) { - // If the target dynamic is not specified, grow to a reasonable target - // in the desired direction. Do the same for cases like mf < p. - // - // TODO To improve on this, keep a queue of Audio_span_dynamics and compute - // multiple intermediate targets based on the next explicit dynamic. - // Consider cases like mf < ... < ff with only mf and ff specified. - // Consider growing in proportion to the duration of each (de)crescendo in - // the sequence, which may be separated by spans with no change in volume. - if ((next_vol < 0) || (sign(next_vol - start_vol) != grow_dir_)) - { - Real min_vol = equalize_volume (0.1); - Real max_vol = equalize_volume (Audio_span_dynamic::MAXIMUM_VOLUME); - target_vol = max (min (start_vol + grow_dir_ * (max_vol - min_vol) * 0.25, max_vol), min_vol); - } - else - { - target_vol = next_vol; - } - } - - span_dynamic_->set_end_moment (now_mom ()); - span_dynamic_->set_volume (start_vol, target_vol); - span_dynamic_ = 0; - - return (next_vol >= 0) ? next_vol : target_vol; + const Real start_vol = depart_queue_.spans_.front ().dynamic_->get_start_volume (); + + if (return_queue_.spans_.empty ()) + { + Real depart_vol = next_vol; + + // If the next dynamic is not specified or is inconsistent with the + // direction of growth, choose a reasonable target. + if ((next_vol < 0) || (depart_dir_ != sign (next_vol - start_vol))) + { + depart_vol = compute_departure_volume (depart_dir_, + start_vol, start_vol, + depart_queue_.min_target_vol_, + depart_queue_.max_target_vol_); + } + + depart_queue_.set_volume (start_vol, depart_vol); + depart_queue_.clear (); + return (next_vol >= 0) ? next_vol : depart_vol; + } + else + { + // If the next dynamic is not specified, return to the starting volume. + const Real return_vol = (next_vol >= 0) ? next_vol : start_vol; + Real depart_vol = compute_departure_volume (depart_dir_, + start_vol, return_vol, + depart_queue_.min_target_vol_, + depart_queue_.max_target_vol_); + depart_queue_.set_volume (start_vol, depart_vol); + depart_queue_.clear (); + return_queue_.set_volume (depart_vol, return_vol); + return_queue_.clear (); + return return_vol; + } } Real @@ -149,10 +357,22 @@ Dynamic_performer::equalize_volume (Real volume) void Dynamic_performer::finalize () { - if (span_dynamic_) - { - end_span (); - } + if (open_span_.dynamic_) + close_and_enqueue_span (); + finish_queued_spans (); +} + +Real +Dynamic_performer::look_up_absolute_volume (SCM dynamicString, + Real defaultValue) +{ + SCM proc = get_property ("dynamicAbsoluteVolumeFunction"); + + SCM svolume = SCM_EOL; + if (ly_is_procedure (proc)) + svolume = scm_call_1 (proc, dynamicString); + + return robust_scm2double (svolume, defaultValue); } void @@ -160,43 +380,49 @@ Dynamic_performer::process_music () { Real volume = -1; - if (script_event_) + if (script_event_) // explicit dynamic { - // Explicit dynamic script event: determine the volume. - SCM proc = get_property ("dynamicAbsoluteVolumeFunction"); - - SCM svolume = SCM_EOL; - if (ly_is_procedure (proc)) - { - // urg - svolume = scm_call_1 (proc, script_event_->get_property ("text")); - } - - volume = equalize_volume (robust_scm2double (svolume, Audio_span_dynamic::DEFAULT_VOLUME)); + volume = look_up_absolute_volume (script_event_->get_property ("text"), + Audio_span_dynamic::DEFAULT_VOLUME); + volume = equalize_volume (volume); } - else if (!span_dynamic_) // first time through + else if (!open_span_.dynamic_) // first time only { + // Idea: look_up_absolute_volume (ly_symbol2scm ("mf")). + // It is likely to change regtests. volume = equalize_volume (Audio_span_dynamic::DEFAULT_VOLUME); } // end the current span at relevant points - if (span_dynamic_ + if (open_span_.dynamic_ && (span_events_[START] || span_events_[STOP] || script_event_)) { - volume = end_span (volume); + close_and_enqueue_span (); + if (script_event_) + { + state_ = STATE_INITIAL; + volume = finish_queued_spans (volume); + } } // start a new span so that some dynamic is always in effect - if (!span_dynamic_) + if (!open_span_.dynamic_) { - Stream_event *cause = - span_events_[START] ? span_events_[START] : - script_event_ ? script_event_ : - span_events_[STOP]; - - span_dynamic_ = new Audio_span_dynamic (now_mom (), volume); - grow_dir_ = next_grow_dir_; - announce_element (Audio_element_info (span_dynamic_, cause)); + if (drive_state_machine (next_grow_dir_)) + volume = finish_queued_spans (volume); + + // if not known by now, use a default volume for robustness + if (volume < 0) + volume = equalize_volume (Audio_span_dynamic::DEFAULT_VOLUME); + + Stream_event *cause + = span_events_[START] ? span_events_[START] + : script_event_ ? script_event_ + : span_events_[STOP]; + + open_span_.dynamic_ = new Audio_span_dynamic (now_mom (), volume); + open_span_.grow_dir_ = next_grow_dir_; + announce_element (Audio_element_info (open_span_.dynamic_, cause)); } } @@ -205,7 +431,7 @@ Dynamic_performer::stop_translation_timestep () { script_event_ = 0; span_events_[LEFT] - = span_events_[RIGHT] = 0; + = span_events_[RIGHT] = 0; next_grow_dir_ = CENTER; } diff --git a/lily/include/audio-item.hh b/lily/include/audio-item.hh index ae5c96ae95..597fa6a812 100644 --- a/lily/include/audio-item.hh +++ b/lily/include/audio-item.hh @@ -59,7 +59,9 @@ private: Real gain_; // = target volume - start volume public: + Moment get_start_moment () const { return start_moment_; } Real get_start_volume () const { return start_volume_; } + Real get_duration () const { return duration_; } void set_end_moment (Moment); void set_volume (Real start, Real target); Real get_volume (Moment) const;