Audacity 3.2.0
StretchHandle.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3Audacity: A Digital Audio Editor
4
5StretchHandle.cpp
6
7Paul Licameli split from TrackPanel.cpp
8
9**********************************************************************/
10
11
12
13#ifdef USE_MIDI
14#include "WrapAllegro.h"
15
16#include "StretchHandle.h"
17
18#include "../../../ui/CommonTrackPanelCell.h"
19#include "../../../../HitTestResult.h"
20#include "NoteTrack.h"
21#include "ProjectAudioIO.h"
22#include "ProjectHistory.h"
23#include "../../../../RefreshCode.h"
24#include "SyncLock.h"
25#include "../../../../TrackPanelMouseEvent.h"
26#include "UndoManager.h"
27#include "ViewInfo.h"
28#include "../../../../../images/Cursors.h"
29
30#include <wx/event.h>
31#include <algorithm>
32
34( const std::shared_ptr<NoteTrack> &pTrack, const StretchState &stretchState )
35 : mpTrack{ pTrack }
36 , mStretchState{ stretchState }
37{}
38
40{
41 static auto disabledCursor =
42 ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
43 static auto stretchLeftCursor =
44 ::MakeCursor(wxCURSOR_BULLSEYE, StretchLeftCursorXpm, 16, 16);
45 static auto stretchRightCursor =
46 ::MakeCursor(wxCURSOR_BULLSEYE, StretchRightCursorXpm, 16, 16);
47 static auto stretchCursor =
48 ::MakeCursor(wxCURSOR_BULLSEYE, StretchCursorXpm, 16, 16);
49
50 if (unsafe) {
51 return { {}, &*disabledCursor };
52 }
53 else {
54 wxCursor *pCursor = NULL;
55 switch (stretchMode) {
56 default:
57 wxASSERT(false);
58 case stretchLeft:
59 pCursor = &*stretchLeftCursor; break;
60 case stretchCenter:
61 pCursor = &*stretchCursor; break;
62 case stretchRight:
63 pCursor = &*stretchRightCursor; break;
64 }
65 return {
66 XO("Click and drag to stretch selected region."),
67 pCursor
68 };
69 }
70}
71
73(std::weak_ptr<StretchHandle> &holder,
74 const TrackPanelMouseState &st, const AudacityProject *pProject,
75 const std::shared_ptr<NoteTrack> &pTrack)
76{
77 StretchState stretchState;
78 const wxMouseState &state = st.state;
79
80 // later, we may want a different policy, but for now, stretch is
81 // selected when the cursor is near the center of the track and
82 // within the selection
83 auto &viewInfo = ViewInfo::Get( *pProject );
84
85 if (!pTrack || !pTrack->GetSelected())
86 return {};
87
88 const wxRect &rect = st.rect;
89 int center = rect.y + rect.height / 2;
90 int distance = abs(state.m_y - center);
91 const int yTolerance = 10;
92 wxInt64 leftSel = viewInfo.TimeToPosition(viewInfo.selectedRegion.t0(), rect.x);
93 wxInt64 rightSel = viewInfo.TimeToPosition(viewInfo.selectedRegion.t1(), rect.x);
94 // Something is wrong if right edge comes before left edge
95 wxASSERT(!(rightSel < leftSel));
96 if (!(leftSel <= state.m_x && state.m_x <= rightSel &&
97 distance < yTolerance))
98 return {};
99
100 // find nearest beat to sel0, sel1
101 static const double minPeriod = 0.05; // minimum beat period
102 stretchState.mBeatCenter = { 0, 0 };
103
104 auto t0 = GetT0(*pTrack, viewInfo);
105 auto t1 = GetT1(*pTrack, viewInfo);
106
107 if (t0 >= t1)
108 return {};
109
110 stretchState.mBeat0 = pTrack->NearestBeatTime( t0 );
111 stretchState.mOrigSel0Quantized = stretchState.mBeat0.first;
112
113 stretchState.mBeat1 = pTrack->NearestBeatTime( t1 );
114 stretchState.mOrigSel1Quantized = stretchState.mBeat1.first;
115
116 // If there is not (almost) a beat to stretch that is slower
117 // than 20 beats per second, don't stretch
118 if ( within( stretchState.mBeat0.second,
119 stretchState.mBeat1.second, 0.9 ) ||
120 ( stretchState.mBeat1.first - stretchState.mBeat0.first ) /
121 ( stretchState.mBeat1.second - stretchState.mBeat0.second )
122 < minPeriod )
123 return {};
124
125 auto selStart = viewInfo.PositionToTime( state.m_x, rect.x );
126 selStart = std::max(t0, std::min(t1, selStart));
127 stretchState.mBeatCenter = pTrack->NearestBeatTime( selStart );
128 if ( within( stretchState.mBeat0.second,
129 stretchState.mBeatCenter.second, 0.1 ) ) {
130 stretchState.mMode = stretchLeft;
131 stretchState.mLeftBeats = 0;
132 stretchState.mRightBeats =
133 stretchState.mBeat1.second - stretchState.mBeat0.second;
134 }
135 else if ( within( stretchState.mBeat1.second,
136 stretchState.mBeatCenter.second, 0.1 ) ) {
137 stretchState.mMode = stretchRight;
138 stretchState.mLeftBeats =
139 stretchState.mBeat1.second - stretchState.mBeat0.second;
140 stretchState.mRightBeats = 0;
141 }
142 else {
143 stretchState.mMode = stretchCenter;
144 stretchState.mLeftBeats =
145 stretchState.mBeat1.second - stretchState.mBeatCenter.second;
146 stretchState.mRightBeats =
147 stretchState.mBeatCenter.second - stretchState.mBeat0.second;
148 }
149
150 auto result = std::make_shared<StretchHandle>( pTrack, stretchState );
151 result = AssignUIHandlePtr(holder, result);
152 return result;
153}
154
156{
157}
158
159std::shared_ptr<const Channel> StretchHandle::FindChannel() const
160{
161 return mpTrack;
162}
163
165(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
166{
167 using namespace RefreshCode;
168 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
169 if ( unsafe )
170 return Cancelled;
171
172 const wxMouseEvent &event = evt.event;
173
174 if (event.LeftDClick() ||
175 !event.LeftDown() ||
176 evt.pCell == NULL)
177 return Cancelled;
178
179
180 mLeftEdge = evt.rect.GetLeft();
181 auto &viewInfo = ViewInfo::Get( *pProject );
182
183 viewInfo.selectedRegion.setTimes
184 ( mStretchState.mBeat0.first, mStretchState.mBeat1.first );
185
186 // Full refresh since the label area may need to indicate
187 // newly selected tracks. (I'm really not sure if the label area
188 // needs to be refreshed or how to just refresh non-label areas.-RBD)
189
190 return RefreshAll;
191}
192
194(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
195{
196 using namespace RefreshCode;
197 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
198 if (unsafe) {
199 this->Cancel(pProject);
200 return RefreshAll | Cancelled;
201 }
202
203 const wxMouseEvent &event = evt.event;
204 const int x = event.m_x;
205
206 Track *clickedTrack=nullptr;
207 if (evt.pCell)
208 clickedTrack =
209 static_cast<CommonTrackPanelCell*>(evt.pCell.get())->FindTrack().get();
210
211 if (clickedTrack == nullptr && mpTrack != nullptr)
212 clickedTrack = mpTrack.get();
213 Stretch(pProject, x, mLeftEdge, clickedTrack);
214 return RefreshAll;
215}
216
218(const TrackPanelMouseState &, AudacityProject *pProject)
219{
220 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
221 return HitPreview( mStretchState.mMode, unsafe );
222}
223
225(const TrackPanelMouseEvent &, AudacityProject *pProject,
226 wxWindow *)
227{
228 using namespace RefreshCode;
229
230 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
231 if (unsafe) {
232 this->Cancel(pProject);
233 return RefreshAll | Cancelled;
234 }
235
236 bool left = mStretchState.mMode == stretchLeft;
237 bool right = mStretchState.mMode == stretchRight;
238 auto &viewInfo = ViewInfo::Get( *pProject );
239 if (SyncLockState::Get(*pProject).IsSyncLocked() && (left || right)) {
240 for (auto track : SyncLock::Group(mpTrack.get())) {
241 if (track != mpTrack.get()) {
242 if (left) {
243 auto origT0 = mStretchState.mOrigSel0Quantized;
244 auto diff = viewInfo.selectedRegion.t0() - origT0;
245 if (diff > 0)
246 track->SyncLockAdjust(origT0 + diff, origT0);
247 else
248 track->SyncLockAdjust(origT0, origT0 - diff);
249 track->ShiftBy(diff);
250 }
251 else {
252 auto origT1 = mStretchState.mOrigSel1Quantized;
253 auto diff = viewInfo.selectedRegion.t1() - origT1;
254 track->SyncLockAdjust(origT1, origT1 + diff);
255 }
256 }
257 }
258 }
259
260 /* i18n-hint: (noun) The track that is used for MIDI notes which can be
261 dragged to change their duration.*/
262 ProjectHistory::Get( *pProject ).PushState(XO("Stretch Note Track"),
263 /* i18n-hint: In the history list, indicates a MIDI note has
264 been dragged to change its duration (stretch it). Using either past
265 or present tense is fine here. If unsure, go for whichever is
266 shorter.*/
267 XO("Stretch"),
269 return RefreshAll;
270}
271
273{
274 ProjectHistory::Get( *pProject ).RollbackState();
276}
277
278double StretchHandle::GetT0(const Track &track, const ViewInfo &viewInfo)
279{
280 return std::max(track.GetStartTime(), viewInfo.selectedRegion.t0());
281}
282
283double StretchHandle::GetT1(const Track &track, const ViewInfo &viewInfo)
284{
285 return std::min(track.GetEndTime(), viewInfo.selectedRegion.t1());
286}
287
288void StretchHandle::Stretch(AudacityProject *pProject, int mouseXCoordinate, int trackLeftEdge,
289 Track *pTrack)
290{
291 auto &viewInfo = ViewInfo::Get( *pProject );
292
293 if (pTrack == NULL && mpTrack != NULL)
294 pTrack = mpTrack.get();
295
296 if (pTrack) pTrack->TypeSwitch( [&](NoteTrack &nt) {
297 double moveto =
298 std::max(0.0, viewInfo.PositionToTime(mouseXCoordinate, trackLeftEdge));
299
300 double dur, left_dur, right_dur;
301
302 // check to make sure tempo is not higher than 20 beats per second
303 // (In principle, tempo can be higher, but not infinity.)
304 double minPeriod = 0.05; // minimum beat period
305
306 // make sure target duration is not too short
307 // Take quick exit if so, without changing the selection.
308 auto t0 = mStretchState.mBeat0.first;
309 auto t1 = mStretchState.mBeat1.first;
310 switch ( mStretchState.mMode ) {
311 case stretchLeft: {
312 dur = t1 - moveto;
313 if (dur < mStretchState.mRightBeats * minPeriod)
314 return;
317 nt.ShiftBy(moveto - t0);
318 mStretchState.mBeat0.first = moveto;
319 viewInfo.selectedRegion.setT0(moveto);
320 break;
321 }
322 case stretchRight: {
323 dur = moveto - t0;
324 if (dur < mStretchState.mLeftBeats * minPeriod)
325 return;
328 viewInfo.selectedRegion.setT1(moveto);
329 mStretchState.mBeat1.first = moveto;
330 break;
331 }
332 case stretchCenter: {
333 moveto = std::max(t0, std::min(t1, moveto));
334 left_dur = moveto - t0;
335 right_dur = t1 - moveto;
336 if ( left_dur < mStretchState.mLeftBeats * minPeriod ||
337 right_dur < mStretchState.mRightBeats * minPeriod )
338 return;
343 mStretchState.mBeatCenter.first = moveto;
344 break;
345 }
346 default:
347 wxASSERT(false);
348 break;
349 }
350 });
351}
352#endif
std::shared_ptr< UIHandle > UIHandlePtr
Definition: CellularPanel.h:28
int min(int a, int b)
XO("Cut/Copy/Paste")
std::unique_ptr< wxCursor > MakeCursor(int WXUNUSED(CursorId), const char *const pXpm[36], int HotX, int HotY)
Definition: TrackPanel.cpp:188
bool within(A a, B b, DIST d)
Definition: TrackPanel.cpp:169
std::shared_ptr< Subclass > AssignUIHandlePtr(std::weak_ptr< Subclass > &holder, const std::shared_ptr< Subclass > &pNew)
Definition: UIHandle.h:159
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
double GetEndTime() const
Get the maximum of End() values of intervals, or 0 when none.
Definition: Channel.cpp:135
double GetStartTime() const
Get the minimum of Start() values of intervals, or 0 when none.
Definition: Channel.cpp:124
void ShiftBy(double t)
Change start time by given duration.
Definition: Channel.h:317
A Track that is used for Midi notes. (Somewhat old code).
Definition: NoteTrack.h:86
bool StretchRegion(QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur)
Definition: NoteTrack.cpp:632
double t1() const
Definition: ViewInfo.h:36
double t0() const
Definition: ViewInfo.h:35
bool IsAudioActive() const
static ProjectAudioIO & Get(AudacityProject &project)
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
static ProjectHistory & Get(AudacityProject &project)
Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
StretchState mStretchState
static double GetT0(const Track &track, const ViewInfo &viewInfo)
std::shared_ptr< NoteTrack > mpTrack
Definition: StretchHandle.h:98
std::shared_ptr< const Channel > FindChannel() const override
static double GetT1(const Track &track, const ViewInfo &viewInfo)
Result Release(const TrackPanelMouseEvent &event, AudacityProject *pProject, wxWindow *pParent) override
void Stretch(AudacityProject *pProject, int mouseXCoordinate, int trackLeftEdge, Track *pTrack)
HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
StretchHandle(const StretchHandle &)
static HitTestPreview HitPreview(StretchEnum stretchMode, bool unsafe)
virtual ~StretchHandle()
static UIHandlePtr HitTest(std::weak_ptr< StretchHandle > &holder, const TrackPanelMouseState &state, const AudacityProject *pProject, const std::shared_ptr< NoteTrack > &pTrack)
Result Cancel(AudacityProject *pProject) override
static TrackIterRange< Track > Group(Track *pTrack)
Definition: SyncLock.cpp:161
bool IsSyncLocked() const
Definition: SyncLock.cpp:43
static SyncLockState & Get(AudacityProject &project)
Definition: SyncLock.cpp:26
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:122
R TypeSwitch(const Functions &...functions)
Definition: Track.h:418
unsigned Result
Definition: UIHandle.h:39
NotifyingSelectedRegion selectedRegion
Definition: ViewInfo.h:215
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
Namespace containing an enum 'what to do on a refresh?'.
Definition: RefreshCode.h:16
std::shared_ptr< Track > FindTrack(TrackPanelCell *pCell)
Definition: TrackPanel.cpp:537
QuantizedTimeAndBeat mBeatCenter
Definition: StretchHandle.h:44
QuantizedTimeAndBeat mBeat0
Definition: StretchHandle.h:45
QuantizedTimeAndBeat mBeat1
Definition: StretchHandle.h:46
std::shared_ptr< TrackPanelCell > pCell