1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 EnvelopeHandle.cpp
6 
7 Paul Licameli split from TrackPanel.cpp
8 
9 **********************************************************************/
10 
11 
12 #include "EnvelopeHandle.h"
13 
14 #include "TrackView.h"
15 
16 #include "../../Envelope.h"
17 #include "../../EnvelopeEditor.h"
18 #include "../../HitTestResult.h"
19 #include "../../prefs/WaveformSettings.h"
20 #include "../../ProjectAudioIO.h"
21 #include "../../ProjectHistory.h"
22 #include "../../RefreshCode.h"
23 #include "../../TimeTrack.h"
24 #include "../../TrackArtist.h"
25 #include "../../TrackPanelMouseEvent.h"
26 #include "ViewInfo.h"
27 #include "../../WaveTrack.h"
28 #include "../../../images/Cursors.h"
29 
EnvelopeHandle(Envelope * pEnvelope)30 EnvelopeHandle::EnvelopeHandle( Envelope *pEnvelope )
31    : mEnvelope{ pEnvelope }
32 {
33 }
34 
Enter(bool,AudacityProject *)35 void EnvelopeHandle::Enter(bool, AudacityProject *)
36 {
37 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
38    mChangeHighlight = RefreshCode::RefreshCell;
39 #endif
40 }
41 
~EnvelopeHandle()42 EnvelopeHandle::~EnvelopeHandle()
43 {}
44 
HitAnywhere(std::weak_ptr<EnvelopeHandle> & holder,Envelope * envelope,bool timeTrack)45 UIHandlePtr EnvelopeHandle::HitAnywhere
46 (std::weak_ptr<EnvelopeHandle> &holder, Envelope *envelope, bool timeTrack)
47 {
48    auto result = AssignUIHandlePtr(holder, std::make_shared<EnvelopeHandle>(envelope));
49    result->mTimeTrack = timeTrack;
50    return result;
51 }
52 
53 namespace {
GetTimeTrackData(const AudacityProject & project,const TimeTrack & tt,double & dBRange,bool & dB,float & zoomMin,float & zoomMax)54    void GetTimeTrackData
55       (const AudacityProject &project, const TimeTrack &tt,
56        double &dBRange, bool &dB, float &zoomMin, float &zoomMax)
57    {
58       const auto &viewInfo = ViewInfo::Get( project );
59       dBRange = viewInfo.dBr;
60       dB = tt.GetDisplayLog();
61       zoomMin = tt.GetRangeLower(), zoomMax = tt.GetRangeUpper();
62       if (dB) {
63          // MB: silly way to undo the work of GetWaveYPos while still getting a logarithmic scale
64          zoomMin = LINEAR_TO_DB(std::max(1.0e-7, double(zoomMin))) / dBRange + 1.0;
65          zoomMax = LINEAR_TO_DB(std::max(1.0e-7, double(zoomMax))) / dBRange + 1.0;
66       }
67    }
68 }
69 
TimeTrackHitTest(std::weak_ptr<EnvelopeHandle> & holder,const wxMouseState & state,const wxRect & rect,const AudacityProject * pProject,const std::shared_ptr<TimeTrack> & tt)70 UIHandlePtr EnvelopeHandle::TimeTrackHitTest
71 (std::weak_ptr<EnvelopeHandle> &holder,
72  const wxMouseState &state, const wxRect &rect,
73  const AudacityProject *pProject, const std::shared_ptr<TimeTrack> &tt)
74 {
75    auto envelope = tt->GetEnvelope();
76    if (!envelope)
77       return {};
78    bool dB;
79    double dBRange;
80    float zoomMin, zoomMax;
81    GetTimeTrackData( *pProject, *tt, dBRange, dB, zoomMin, zoomMax);
82    return EnvelopeHandle::HitEnvelope
83       (holder, state, rect, pProject, envelope, zoomMin, zoomMax, dB, dBRange,
84        true);
85 }
86 
WaveTrackHitTest(std::weak_ptr<EnvelopeHandle> & holder,const wxMouseState & state,const wxRect & rect,const AudacityProject * pProject,const std::shared_ptr<WaveTrack> & wt)87 UIHandlePtr EnvelopeHandle::WaveTrackHitTest
88 (std::weak_ptr<EnvelopeHandle> &holder,
89  const wxMouseState &state, const wxRect &rect,
90  const AudacityProject *pProject, const std::shared_ptr<WaveTrack> &wt)
91 {
92    /// method that tells us if the mouse event landed on an
93    /// envelope boundary.
94    auto &viewInfo = ViewInfo::Get(*pProject);
95    auto time = viewInfo.PositionToTime(state.m_x, rect.GetX());
96    Envelope *const envelope = wt->GetEnvelopeAtTime(time);
97 
98    if (!envelope)
99       return {};
100 
101    // Get envelope point, range 0.0 to 1.0
102    const bool dB = !wt->GetWaveformSettings().isLinear();
103 
104    float zoomMin, zoomMax;
105    wt->GetDisplayBounds(&zoomMin, &zoomMax);
106 
107    const float dBRange = wt->GetWaveformSettings().dBRange;
108 
109    return EnvelopeHandle::HitEnvelope
110        (holder, state, rect, pProject, envelope, zoomMin, zoomMax, dB, dBRange, false);
111 }
112 
HitEnvelope(std::weak_ptr<EnvelopeHandle> & holder,const wxMouseState & state,const wxRect & rect,const AudacityProject * pProject,Envelope * envelope,float zoomMin,float zoomMax,bool dB,float dBRange,bool timeTrack)113 UIHandlePtr EnvelopeHandle::HitEnvelope
114 (std::weak_ptr<EnvelopeHandle> &holder,
115  const wxMouseState &state, const wxRect &rect, const AudacityProject *pProject,
116  Envelope *envelope, float zoomMin, float zoomMax,
117  bool dB, float dBRange, bool timeTrack)
118 {
119    const auto &viewInfo = ViewInfo::Get( *pProject );
120 
121    const double envValue =
122       envelope->GetValue(viewInfo.PositionToTime(state.m_x, rect.x));
123 
124    // Get y position of envelope point.
125    int yValue = GetWaveYPos(envValue,
126       zoomMin, zoomMax,
127       rect.height, dB, true, dBRange, false) + rect.y;
128 
129    // Get y position of center line
130    int ctr = GetWaveYPos(0.0,
131       zoomMin, zoomMax,
132       rect.height, dB, true, dBRange, false) + rect.y;
133 
134    // Get y distance of mouse from center line (in pixels).
135    int yMouse = abs(ctr - state.m_y);
136    // Get y distance of envelope from center line (in pixels)
137    yValue = abs(ctr - yValue);
138 
139    // JKC: It happens that the envelope is actually drawn offset from its
140    // 'true' position (it is 3 pixels wide).  yMisalign is really a fudge
141    // factor to allow us to hit it exactly, but I wouldn't dream of
142    // calling it yFudgeFactor :)
143    const int yMisalign = 2;
144    // Perhaps yTolerance should be put into preferences?
145    const int yTolerance = 5; // how far from envelope we may be and count as a hit.
146    int distance;
147 
148    // For amplification using the envelope we introduced the idea of contours.
149    // The contours have the same shape as the envelope, which may be partially off-screen.
150    // The contours are closer in to the center line.
151    int ContourSpacing = (int)(rect.height / (2 * (zoomMax - zoomMin)));
152    const int MaxContours = 2;
153 
154    // Adding ContourSpacing/2 selects a region either side of the contour.
155    int yDisplace = yValue - yMisalign - yMouse + ContourSpacing / 2;
156    if (yDisplace > (MaxContours * ContourSpacing))
157       return {};
158    // Subtracting the ContourSpacing/2 we added earlier ensures distance is centred on the contour.
159    distance = abs((yDisplace % ContourSpacing) - ContourSpacing / 2);
160    if (distance >= yTolerance)
161       return {};
162 
163    return HitAnywhere(holder, envelope, timeTrack);
164 }
165 
Click(const TrackPanelMouseEvent & evt,AudacityProject * pProject)166 UIHandle::Result EnvelopeHandle::Click
167 (const TrackPanelMouseEvent &evt, AudacityProject *pProject)
168 {
169    using namespace RefreshCode;
170    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
171    if ( unsafe )
172       return Cancelled;
173 
174    const wxMouseEvent &event = evt.event;
175    const auto &viewInfo = ViewInfo::Get( *pProject );
176    const auto pView = std::static_pointer_cast<TrackView>(evt.pCell);
177    const auto pTrack = pView ? pView->FindTrack().get() : nullptr;
178 
179    mEnvelopeEditors.clear();
180 
181    unsigned result = Cancelled;
182    if (pTrack)
183       result = pTrack->TypeSwitch< decltype(RefreshNone) >(
184       [&](WaveTrack *wt) {
185          if (!mEnvelope)
186             return Cancelled;
187 
188          mLog = !wt->GetWaveformSettings().isLinear();
189          wt->GetDisplayBounds(&mLower, &mUpper);
190          mdBRange = wt->GetWaveformSettings().dBRange;
191          auto channels = TrackList::Channels( wt );
192          for ( auto channel : channels ) {
193             if (channel == wt)
194                mEnvelopeEditors.push_back(
195                   std::make_unique< EnvelopeEditor >( *mEnvelope, true ) );
196             else {
197                auto time =
198                   viewInfo.PositionToTime(event.GetX(), evt.rect.GetX());
199                auto e2 = channel->GetEnvelopeAtTime(time);
200                if (e2)
201                   mEnvelopeEditors.push_back(
202                      std::make_unique< EnvelopeEditor >( *e2, true ) );
203                else {
204                    // There isn't necessarily an envelope there; no guarantee a
205                    // linked track has the same WaveClip structure...
206                 }
207             }
208          }
209 
210          return RefreshNone;
211       },
212       [&](TimeTrack *tt) {
213          if (!mEnvelope)
214             return Cancelled;
215          GetTimeTrackData( *pProject, *tt, mdBRange, mLog, mLower, mUpper);
216          mEnvelopeEditors.push_back(
217             std::make_unique< EnvelopeEditor >( *mEnvelope, false )
218          );
219 
220          return RefreshNone;
221       },
222       [](Track *) {
223          return Cancelled;
224       }
225    );
226 
227    if (result & Cancelled)
228       return result;
229 
230    mRect = evt.rect;
231 
232    const bool needUpdate = ForwardEventToEnvelopes(event, viewInfo);
233    return needUpdate ? RefreshCell : RefreshNone;
234 }
235 
Drag(const TrackPanelMouseEvent & evt,AudacityProject * pProject)236 UIHandle::Result EnvelopeHandle::Drag
237 (const TrackPanelMouseEvent &evt, AudacityProject *pProject)
238 {
239    using namespace RefreshCode;
240    const wxMouseEvent &event = evt.event;
241    const auto &viewInfo = ViewInfo::Get( *pProject );
242    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
243    if (unsafe) {
244       this->Cancel(pProject);
245       return RefreshCell | Cancelled;
246    }
247 
248    const bool needUpdate = ForwardEventToEnvelopes(event, viewInfo);
249    return needUpdate ? RefreshCell : RefreshNone;
250 }
251 
Preview(const TrackPanelMouseState &,AudacityProject * pProject)252 HitTestPreview EnvelopeHandle::Preview
253 (const TrackPanelMouseState &, AudacityProject *pProject)
254 {
255    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
256    static auto disabledCursor =
257       ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
258    static auto envelopeCursor =
259       ::MakeCursor(wxCURSOR_ARROW, EnvCursorXpm, 16, 16);
260 
261    auto message = mTimeTrack
262       ? XO("Click and drag to warp playback time")
263       : XO("Click and drag to edit the amplitude envelope");
264 
265    return {
266       message,
267       (unsafe
268        ? &*disabledCursor
269        : &*envelopeCursor)
270    };
271 }
272 
Release(const TrackPanelMouseEvent & evt,AudacityProject * pProject,wxWindow *)273 UIHandle::Result EnvelopeHandle::Release
274 (const TrackPanelMouseEvent &evt, AudacityProject *pProject,
275  wxWindow *)
276 {
277    const wxMouseEvent &event = evt.event;
278    const auto &viewInfo = ViewInfo::Get( *pProject );
279    const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
280    if (unsafe)
281       return this->Cancel(pProject);
282 
283    const bool needUpdate = ForwardEventToEnvelopes(event, viewInfo);
284 
285    ProjectHistory::Get( *pProject ).PushState(
286       /* i18n-hint: (verb) Audacity has just adjusted the envelope .*/
287       XO("Adjusted envelope."),
288       /* i18n-hint: The envelope is a curve that controls the audio loudness.*/
289       XO("Envelope")
290    );
291 
292    mEnvelopeEditors.clear();
293 
294    using namespace RefreshCode;
295    return needUpdate ? RefreshCell : RefreshNone;
296 }
297 
Cancel(AudacityProject * pProject)298 UIHandle::Result EnvelopeHandle::Cancel(AudacityProject *pProject)
299 {
300    ProjectHistory::Get( *pProject ).RollbackState();
301    mEnvelopeEditors.clear();
302    return RefreshCode::RefreshCell;
303 }
304 
ForwardEventToEnvelopes(const wxMouseEvent & event,const ViewInfo & viewInfo)305 bool EnvelopeHandle::ForwardEventToEnvelopes
306    (const wxMouseEvent &event, const ViewInfo &viewInfo)
307 {
308    /// The Envelope class actually handles things at the mouse
309    /// event level, so we have to forward the events over.  Envelope
310    /// will then tell us whether or not we need to redraw.
311 
312    // AS: I'm not sure why we can't let the Envelope take care of
313    //  redrawing itself.  ?
314    bool needUpdate = false;
315    for (const auto &pEditor : mEnvelopeEditors) {
316       needUpdate =
317          pEditor->MouseEvent(
318             event, mRect, viewInfo, mLog, mdBRange, mLower, mUpper)
319          || needUpdate;
320    }
321 
322    return needUpdate;
323 }
324