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