Audacity 3.2.0
SampleHandle.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3Audacity: A Digital Audio Editor
4
5SampleHandle.cpp
6
7Paul Licameli split from TrackPanel.cpp
8
9**********************************************************************/
10
11
12#include "SampleHandle.h"
13
14#include <algorithm>
15#include <wx/gdicmn.h>
16
17#include "Envelope.h"
18#include "../../../../HitTestResult.h"
19#include "../../../../prefs/WaveformSettings.h"
20#include "ProjectAudioIO.h"
21#include "ProjectHistory.h"
22#include "../../../../RefreshCode.h"
23#include "../../../../TrackArt.h"
24#include "../../../../TrackArtist.h"
25#include "../../../../TrackPanelMouseEvent.h"
26#include "UndoManager.h"
27#include "ViewInfo.h"
28#include "WaveClip.h"
29#include "WaveTrack.h"
30#include "../../../../../images/Cursors.h"
31#include "AudacityMessageBox.h"
32
33
34static const int SMOOTHING_KERNEL_RADIUS = 3;
35static const int SMOOTHING_BRUSH_RADIUS = 5;
36static const double SMOOTHING_PROPORTION_MAX = 0.7;
37static const double SMOOTHING_PROPORTION_MIN = 0.0;
38
39SampleHandle::SampleHandle( const std::shared_ptr<WaveTrack> &pTrack )
40 : mClickedTrack{ pTrack }
41{
42}
43
45{
46#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
48#endif
49}
50
52(const wxMouseState &state, const AudacityProject *WXUNUSED(pProject), bool unsafe)
53{
54 static auto disabledCursor =
55 ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
56 static wxCursor smoothCursor{ wxCURSOR_SPRAYCAN };
57 static auto pencilCursor =
58 ::MakeCursor(wxCURSOR_PENCIL, DrawCursorXpm, 12, 22);
59
60 // TODO: message should also mention the brush. Describing the modifier key
61 // (alt, or other) varies with operating system.
62 auto message = XO("Click and drag to edit the samples");
63
64 return {
65 message,
66 (unsafe
67 ? &*disabledCursor
68 : (state.AltDown()
69 ? &smoothCursor
70 : &*pencilCursor))
71 };
72}
73
75(std::weak_ptr<SampleHandle> &holder,
76 const wxMouseState &WXUNUSED(state), const std::shared_ptr<WaveTrack> &pTrack)
77{
78 auto result = std::make_shared<SampleHandle>( pTrack );
79 result = AssignUIHandlePtr(holder, result);
80 return result;
81}
82
83namespace {
84 inline double adjustTime(const WaveTrack& wt, double time)
85 {
86 // Round to an exact sample time
87 const auto clip = wt.GetClipAtTime(time);
88 if (!clip)
89 return wt.SnapToSample(time);
90 const auto sampleOffset =
91 clip->TimeToSamples(time - clip->GetPlayStartTime());
92 return clip->SamplesToTime(sampleOffset) + clip->GetPlayStartTime();
93 }
94
95 // Is the sample horizontally nearest to the cursor sufficiently separated
96 // from its neighbors that the pencil tool should be allowed to drag it?
98 const ViewInfo& viewInfo, const WaveClip& clip,
99 const ZoomInfo::Intervals& intervals)
100 {
101 // Require more than 3 pixels per sample
102 const auto xx = std::max<ZoomInfo::int64>(
103 0, viewInfo.TimeToPosition(clip.GetPlayStartTime()));
104 const double rate = clip.GetRate() / clip.GetStretchRatio();
105 ZoomInfo::Intervals::const_iterator it = intervals.begin(),
106 end = intervals.end(), prev;
107 wxASSERT(it != end && it->position == 0);
108 do
109 prev = it++;
110 while (it != end && it->position <= xx);
111 const double threshold = 3 * rate;
112 // three times as many pixels per second, as samples
113 return prev->averageZoom > threshold;
114 }
115}
116
118(std::weak_ptr<SampleHandle> &holder,
119 const wxMouseState &state, const wxRect &rect,
120 const AudacityProject *pProject, const std::shared_ptr<WaveTrack> &pTrack)
121{
122 const auto &viewInfo = ViewInfo::Get( *pProject );
123
126 const auto wavetrack = pTrack.get();
127 const auto time = viewInfo.PositionToTime(state.m_x, rect.x);
128 const auto clickedClip = wavetrack->GetClipAtTime(time);
129 if (!clickedClip)
130 return {};
131
132 const double tt = adjustTime(*wavetrack, time);
133 const auto intervals = viewInfo.FindIntervals(rect.width);
134 if (!SampleResolutionTest(viewInfo, *clickedClip, intervals))
135 return {};
136
137 // Just get one sample.
138 float oneSample;
139 constexpr auto iChannel = 0u;
140 constexpr auto mayThrow = false;
141 if (!wavetrack->GetFloatAtTime(tt, iChannel, oneSample, mayThrow))
142 return {};
143
144 // Get y distance of envelope point from center line (in pixels).
145 auto &cache = WaveformScale::Get(*wavetrack);
146 float zoomMin, zoomMax;
147 cache.GetDisplayBounds(zoomMin, zoomMax);
148
149 double envValue = 1.0;
150 Envelope* env = wavetrack->GetEnvelopeAtTime(time);
151 if (env)
152 // Calculate sample as it would be rendered
153 envValue = env->GetValue(tt);
154
155 auto &settings = WaveformSettings::Get(*wavetrack);
156 const bool dB = !settings.isLinear();
157 int yValue = GetWaveYPos(oneSample * envValue,
158 zoomMin, zoomMax,
159 rect.height, dB, true,
160 settings.dBRange, false) + rect.y;
161
162 // Get y position of mouse (in pixels)
163 int yMouse = state.m_y;
164
165 // Perhaps yTolerance should be put into preferences?
166 const int yTolerance = 10; // More tolerance on samples than on envelope.
167 if (abs(yValue - yMouse) >= yTolerance)
168 return {};
169
170 return HitAnywhere(holder, state, pTrack);
171}
172
174{
175}
176
177std::shared_ptr<const Channel> SampleHandle::FindChannel() const
178{
179 return mClickedTrack;
180}
181
183(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
184{
185 using namespace RefreshCode;
186 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
187 if ( unsafe )
188 return Cancelled;
189
190 const wxMouseEvent &event = evt.event;
191 const wxRect &rect = evt.rect;
192 const auto &viewInfo = ViewInfo::Get( *pProject );
193
194 const double t0 = viewInfo.PositionToTime(event.m_x, rect.x);
195 const auto pTrack = mClickedTrack.get();
196 mClickedClip = pTrack->GetClipAtTime(t0);
197 if (!mClickedClip)
198 return Cancelled;
199
200 const auto intervals = viewInfo.FindIntervals(rect.width);
202 if (!SampleResolutionTest(viewInfo, *mClickedClip, intervals))
203 {
205 XO("To use Draw, zoom in further until you can see the individual samples."),
206 XO("Draw Tool"));
207 return Cancelled;
208 }
209
211 mRect = rect;
212
213 //convert t0 to samples
214 mClickedStartPixel = viewInfo.TimeToPosition(t0);
215
216 //Determine how drawing should occur. If alt is down,
217 //do a smoothing, instead of redrawing.
218 if (event.m_altDown)
219 {
220 mAltKey = true;
221 //*************************************************
222 //*** ALT-DOWN-CLICK (SAMPLE SMOOTHING) ***
223 //*************************************************
224 //
225 // Smoothing works like this: There is a smoothing kernel radius constant that
226 // determines how wide the averaging window is. Plus, there is a smoothing brush radius,
227 // which determines how many pixels wide around the selected pixel this smoothing is applied.
228 //
229 // Samples will be replaced by a mixture of the original points and the smoothed points,
230 // with a triangular mixing probability whose value at the center point is
231 // SMOOTHING_PROPORTION_MAX and at the far bounds is SMOOTHING_PROPORTION_MIN
232
233 //Get the region of samples around the selected point
234 size_t sampleRegionSize = 1 + 2 * (SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS);
235 Floats sampleRegion{ sampleRegionSize };
236 Floats newSampleRegion{ 1 + 2 * (size_t)SMOOTHING_BRUSH_RADIUS };
237
238 //Get a sample from the clip to do some tricks on.
239 constexpr auto iChannel = 0u;
240 constexpr auto mayThrow = false;
241 const auto sampleRegionRange = mClickedTrack->GetFloatsCenteredAroundTime(
242 t0, iChannel, sampleRegion.get(),
244
245 //Go through each point of the smoothing brush and apply a smoothing operation.
246 for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
247 float sumOfSamples = 0;
248 for (auto ii = -SMOOTHING_KERNEL_RADIUS; ii <= SMOOTHING_KERNEL_RADIUS; ++ii) {
249 //Go through each point of the smoothing kernel and find the average
250 const auto sampleRegionIndex =
252 const auto inRange = sampleRegionRange.first <= sampleRegionIndex &&
253 sampleRegionIndex < sampleRegionRange.second;
254 if (!inRange)
255 continue;
256 //The average is a weighted average, scaled by a weighting kernel that is simply triangular
257 // A triangular kernel across N items, with a radius of R ( 2 R + 1 points), if the farthest:
258 // points have a probability of a, the entire triangle has total probability of (R + 1)^2.
259 // For sample number ii and middle brush sample M, (R + 1 - abs(M-ii))/ ((R+1)^2) gives a
260 // legal distribution whose total probability is 1.
261 //
262 //
263 // weighting factor value
264 sumOfSamples += (SMOOTHING_KERNEL_RADIUS + 1 - abs(ii)) *
265 sampleRegion[sampleRegionIndex];
266 }
267 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
268 sumOfSamples /
270 }
271
272
273 // Now that the NEW sample levels are determined, go through each and mix it appropriately
274 // with the original point, according to a 2-part linear function whose center has probability
275 // SMOOTHING_PROPORTION_MAX and extends out SMOOTHING_BRUSH_RADIUS, at which the probability is
276 // SMOOTHING_PROPORTION_MIN. _MIN and _MAX specify how much of the smoothed curve make it through.
277
278 float prob;
279
280 for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
281
282 prob =
284 (float)abs(jj) / SMOOTHING_BRUSH_RADIUS *
286
287 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
288 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] * prob +
290 (1 - prob);
291 }
292 // Set a range of samples around the mouse event
293 // Don't require dithering later
294 mClickedTrack->SetFloatsCenteredAroundTime(
295 t0, iChannel, newSampleRegion.get(), SMOOTHING_BRUSH_RADIUS,
297
298 // mLastDragSampleValue will not be used
299 }
300 else
301 {
302 mAltKey = false;
303 //*************************************************
304 //*** PLAIN DOWN-CLICK (NORMAL DRAWING) ***
305 //*************************************************
306
307 //Otherwise (e.g., the alt button is not down) do normal redrawing, based on the mouse position.
308 const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
309
310 //Set the sample to the point of the mouse event
311 // Don't require dithering later
312
313 constexpr auto iChannel = 0u;
314 mClickedTrack->SetFloatAtTime(
315 t0, iChannel, newLevel, narrowestSampleFormat);
316
317 mLastDragSampleValue = newLevel;
318 }
319
320 //Set the member data structures for drawing
322
323 // Sample data changed on either branch, so refresh the track display.
324 return RefreshCell;
325}
326
327namespace
328{
330 size_t n, bool forward, const WaveClipPointers& sortedClips,
331 const ViewInfo& viewInfo, const ZoomInfo::Intervals& intervals)
332{
333 assert(n < sortedClips.size());
334 const auto increment = forward ? 1 : -1;
335 int last = n + increment;
336 const auto limit = forward ? sortedClips.size() : -1;
337 while (last != limit)
338 {
339 if (!SampleResolutionTest(viewInfo, *sortedClips[last], intervals))
340 break;
341 last += increment;
342 }
343 last -= increment;
344 assert(last >= 0 && last < sortedClips.size());
345 return last;
346}
347} // namespace
348
350(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
351{
352 using namespace RefreshCode;
353 const wxMouseEvent &event = evt.event;
354 const auto &viewInfo = ViewInfo::Get( *pProject );
355
356 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
357 if (unsafe)
358 {
359 this->Cancel(pProject);
360 return RefreshCell | Cancelled;
361 }
362
363 // There must have been some clicking before dragging ...
364 assert(mClickedTrack && mClickedClip);
365 if (!(mClickedTrack && mClickedClip))
366 return Cancelled;
367
368 //*************************************************
369 //*** DRAG-DRAWING ***
370 //*************************************************
371
372 //No dragging effects if the alt key is down--
373 //Don't allow left-right dragging for smoothing operation
374 if (mAltKey)
375 return RefreshNone;
376
377 const double t0 = viewInfo.PositionToTime(mLastDragPixel);
378 const double t1 = viewInfo.PositionToTime(event.m_x, mRect.x);
379
380 const int x1 =
381 event.m_controlDown ? mClickedStartPixel : viewInfo.TimeToPosition(t1);
382 const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
383 const auto start = std::min(t0, t1);
384 const auto end = std::max(t0, t1);
385
386 // Starting from the originally clicked clip, restrict the editing boundaries
387 // to the succession of clips with visible samples. If, of clips A, B and C,
388 // only B had invisible samples, it'd mean one could not drag-draw from A
389 // into C, but that probably isn't a behavior worthwhile much implementation
390 // complications.
391 const auto clips = mClickedTrack->SortedClipArray();
392 const auto clickedClipIndex = std::distance(
393 clips.begin(), std::find(clips.begin(), clips.end(), mClickedClip));
394 constexpr auto forward = true;
395 const auto intervals = viewInfo.FindIntervals(mRect.width);
396 const size_t leftmostEditable = GetLastEditableClipStartingFromNthClip(
397 clickedClipIndex, !forward, clips, viewInfo, intervals);
398 const size_t rightmostEditable = GetLastEditableClipStartingFromNthClip(
399 clickedClipIndex, forward, clips, viewInfo, intervals);
400
401 const auto editStart =
402 std::max(start, clips[leftmostEditable]->GetPlayStartTime());
403 const auto editEnd =
404 std::min(end, clips[rightmostEditable]->GetPlayEndTime());
405
406 // For fast pencil movements covering more than one sample between two
407 // updates, we draw a line going from v0 at t0 to v1 at t1.
408 const auto interpolator = [t0, t1, v0 = mLastDragSampleValue,
409 v1 = newLevel](double t) {
410 if (t0 == t1)
411 return v1;
412 const auto gradient = (v1 - v0) / (t1 - t0);
413 const auto value = static_cast<float>(gradient * (t - t0) + v0);
414 // t may be outside t0 and t1, and we don't want to extrapolate.
415 return std::clamp(value, std::min(v0, v1), std::max(v0, v1));
416 };
417 constexpr auto iChannel = 0u;
418 mClickedTrack->SetFloatsWithinTimeRange(
419 editStart, editEnd, iChannel, interpolator, narrowestSampleFormat);
420
421 mLastDragPixel = x1;
422 mLastDragSampleValue = newLevel;
423
424 return RefreshCell;
425}
426
428(const TrackPanelMouseState &st, AudacityProject *pProject)
429{
430 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
431 return HitPreview(st.state, pProject, unsafe);
432}
433
435(const TrackPanelMouseEvent &, AudacityProject *pProject,
436 wxWindow *)
437{
438 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
439 if (unsafe)
440 return this->Cancel(pProject);
441
442 //*************************************************
443 //*** UP-CLICK (Finish drawing) ***
444 //*************************************************
445 //On up-click, send the state to the undo stack
446 mClickedTrack.reset(); //Set this to NULL so it will catch improper drag events.
447 mClickedClip = nullptr;
448 ProjectHistory::Get( *pProject ).PushState(XO("Moved Samples"),
449 XO("Sample Edit"),
451
452 // No change to draw since last drag
454}
455
457{
458 mClickedTrack.reset();
459 ProjectHistory::Get( *pProject ).RollbackState();
461}
462
464 (const wxMouseEvent &event, const ViewInfo &viewInfo, double t0)
465{
466 // Calculate where the mouse is located vertically (between +/- 1)
467 float zoomMin, zoomMax;
468 auto &cache = WaveformScale::Get(*mClickedTrack);
469 cache.GetDisplayBounds(zoomMin, zoomMax);
470
471 const int yy = event.m_y - mRect.y;
472 const int height = mRect.GetHeight();
474 const bool dB = !settings.isLinear();
475 float newLevel =
476 ::ValueOfPixel(yy, height, false, dB,
477 settings.dBRange, zoomMin, zoomMax);
478
479 //Take the envelope into account
480 const auto time = viewInfo.PositionToTime(event.m_x, mRect.x);
481 Envelope *const env = mClickedTrack->GetEnvelopeAtTime(time);
482 if (env)
483 {
484 // Calculate sample as it would be rendered
485 double envValue = env->GetValue(t0);
486 if (envValue > 0)
487 newLevel /= envValue;
488 else
489 newLevel = 0;
490
491 //Make sure the NEW level is between +/-1
492 newLevel = std::max(-1.0f, std::min(1.0f, newLevel));
493 }
494
495 return newLevel;
496}
int AudacityMessageBox(const TranslatableString &message, const TranslatableString &caption, long style, wxWindow *parent, int x, int y)
std::shared_ptr< UIHandle > UIHandlePtr
Definition: CellularPanel.h:28
int min(int a, int b)
XO("Cut/Copy/Paste")
@ narrowestSampleFormat
Two synonyms for previous values that might change if more values were added.
static const int SMOOTHING_BRUSH_RADIUS
static const double SMOOTHING_PROPORTION_MAX
static const int SMOOTHING_KERNEL_RADIUS
static const double SMOOTHING_PROPORTION_MIN
int GetWaveYPos(float value, float min, float max, int height, bool dB, bool outer, float dBr, bool clip)
Definition: TrackArt.cpp:66
float ValueOfPixel(int yy, int height, bool offset, bool dB, double dBRange, float zoomMin, float zoomMax)
Definition: TrackArt.cpp:122
static Settings & settings()
Definition: TrackInfo.cpp:69
std::unique_ptr< wxCursor > MakeCursor(int WXUNUSED(CursorId), const char *const pXpm[36], int HotX, int HotY)
Definition: TrackPanel.cpp:188
std::shared_ptr< Subclass > AssignUIHandlePtr(std::weak_ptr< Subclass > &holder, const std::shared_ptr< Subclass > &pNew)
Definition: UIHandle.h:159
std::vector< WaveClip * > WaveClipPointers
Definition: WaveTrack.h:49
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
Piecewise linear or piecewise exponential function from double to double.
Definition: Envelope.h:72
double GetValue(double t, double sampleDur=0) const
Get envelope value at time t.
Definition: Envelope.cpp:837
bool IsAudioActive() const
static ProjectAudioIO & Get(AudacityProject &project)
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
static ProjectHistory & Get(AudacityProject &project)
Result Release(const TrackPanelMouseEvent &event, AudacityProject *pProject, wxWindow *pParent) override
virtual ~SampleHandle()
float FindSampleEditingLevel(const wxMouseEvent &event, const ViewInfo &viewInfo, double t0)
WaveClip * mClickedClip
Definition: SampleHandle.h:73
float mLastDragSampleValue
Definition: SampleHandle.h:78
int mClickedStartPixel
Definition: SampleHandle.h:76
Result Cancel(AudacityProject *pProject) override
static HitTestPreview HitPreview(const wxMouseState &state, const AudacityProject *pProject, bool unsafe)
static UIHandlePtr HitAnywhere(std::weak_ptr< SampleHandle > &holder, const wxMouseState &state, const std::shared_ptr< WaveTrack > &pTrack)
void Enter(bool forward, AudacityProject *) override
static UIHandlePtr HitTest(std::weak_ptr< SampleHandle > &holder, const wxMouseState &state, const wxRect &rect, const AudacityProject *pProject, const std::shared_ptr< WaveTrack > &pTrack)
Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
wxRect mRect
Definition: SampleHandle.h:74
std::shared_ptr< WaveTrack > mClickedTrack
Definition: SampleHandle.h:72
HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
SampleHandle(const SampleHandle &)=delete
std::shared_ptr< const Channel > FindChannel() const override
int mLastDragPixel
Definition: SampleHandle.h:77
Result mChangeHighlight
Definition: UIHandle.h:147
unsigned Result
Definition: UIHandle.h:39
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
This allows multiple clips to be a part of one WaveTrack.
Definition: WaveClip.h:113
double GetStretchRatio() const override
Definition: WaveClip.cpp:348
double GetPlayStartTime() const noexcept override
Definition: WaveClip.cpp:1305
int GetRate() const override
Definition: WaveClip.h:166
A Track that contains audio waveform data.
Definition: WaveTrack.h:227
const WaveClip * GetClipAtTime(double time) const
Definition: WaveTrack.cpp:3759
static WaveformScale & Get(const WaveTrack &track)
Mutative access to attachment even if the track argument is const.
static WaveformSettings & Get(const WaveTrack &track)
double SnapToSample(double t) const
double PositionToTime(int64 position, int64 origin=0, bool ignoreFisheye=false) const
Definition: ZoomInfo.cpp:34
std::vector< Interval > Intervals
Definition: ZoomInfo.h:144
int64 TimeToPosition(double time, int64 origin=0, bool ignoreFisheye=false) const
STM: Converts a project time to screen x position.
Definition: ZoomInfo.cpp:44
auto end(const Ptr< Type, BaseDeleter > &p)
Enables range-for.
Definition: PackedArray.h:159
Namespace containing an enum 'what to do on a refresh?'.
Definition: RefreshCode.h:16
double adjustTime(const WaveTrack &wt, double time)
bool SampleResolutionTest(const ViewInfo &viewInfo, const WaveClip &clip, const ZoomInfo::Intervals &intervals)
size_t GetLastEditableClipStartingFromNthClip(size_t n, bool forward, const WaveClipPointers &sortedClips, const ViewInfo &viewInfo, const ZoomInfo::Intervals &intervals)