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"
29#include "WaveClip.h"
30#include "WaveTrack.h"
31#include "../../../../../images/Cursors.h"
32#include "AudacityMessageBox.h"
33
34
35static const int SMOOTHING_KERNEL_RADIUS = 3;
36static const int SMOOTHING_BRUSH_RADIUS = 5;
37static const double SMOOTHING_PROPORTION_MAX = 0.7;
38static const double SMOOTHING_PROPORTION_MIN = 0.0;
39
40SampleHandle::SampleHandle(const std::shared_ptr<WaveChannel> &pTrack)
41 : mClickedTrack{ pTrack }
42{
43}
44
46{
47#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
49#endif
50}
51
53(const wxMouseState &state, const AudacityProject *WXUNUSED(pProject), bool unsafe)
54{
55 static auto disabledCursor =
56 ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
57 static wxCursor smoothCursor{ wxCURSOR_SPRAYCAN };
58 static auto pencilCursor =
59 ::MakeCursor(wxCURSOR_PENCIL, DrawCursorXpm, 12, 22);
60
61 // TODO: message should also mention the brush. Describing the modifier key
62 // (alt, or other) varies with operating system.
63 auto message = XO("Click and drag to edit the samples");
64
65 return {
66 message,
67 (unsafe
68 ? &*disabledCursor
69 : (state.AltDown()
70 ? &smoothCursor
71 : &*pencilCursor))
72 };
73}
74
75UIHandlePtr SampleHandle::HitAnywhere(std::weak_ptr<SampleHandle> &holder,
76 const wxMouseState &, const std::shared_ptr<WaveChannel> &pChannel)
77{
78 auto result = std::make_shared<SampleHandle>(pChannel);
79 result = AssignUIHandlePtr(holder, result);
80 return result;
81}
82
83namespace {
84 inline double adjustTime(const WaveChannel& wt, double time)
85 {
86 // Round to an exact sample time
87 const auto clip = WaveChannelUtilities::GetClipAtTime(wt, 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 WaveChannelInterval& 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
117UIHandlePtr SampleHandle::HitTest(std::weak_ptr<SampleHandle> &holder,
118 const wxMouseState &state, const wxRect &rect,
119 const AudacityProject *pProject, const std::shared_ptr<WaveChannel> &pChannel)
120{
121 using namespace WaveChannelUtilities;
122 const auto &viewInfo = ViewInfo::Get( *pProject );
123
126 if (!pChannel)
127 return {};
128 auto &waveChannel = *pChannel;
129 const auto time = viewInfo.PositionToTime(state.m_x, rect.x);
130 const auto clickedClip =
131 WaveChannelUtilities::GetClipAtTime(waveChannel, time);
132 if (!clickedClip)
133 return {};
134
135 const double tt = adjustTime(waveChannel, time);
136 const auto intervals = viewInfo.FindIntervals(rect.width);
137 if (!SampleResolutionTest(viewInfo, *clickedClip, intervals))
138 return {};
139
140 // Just get one sample.
141 float oneSample;
142 constexpr auto mayThrow = false;
144 tt, oneSample, mayThrow))
145 return {};
146
147 // Get y distance of envelope point from center line (in pixels).
148 auto &cache = WaveformScale::Get(waveChannel);
149 float zoomMin, zoomMax;
150 cache.GetDisplayBounds(zoomMin, zoomMax);
151
152 double envValue = 1.0;
153 if (const auto env =
155 // Calculate sample as it would be rendered
156 envValue = env->GetValue(tt);
157
158 auto &settings = WaveformSettings::Get(waveChannel);
159 const bool dB = !settings.isLinear();
160 int yValue = GetWaveYPos(oneSample * envValue,
161 zoomMin, zoomMax,
162 rect.height, dB, true,
163 settings.dBRange, false) + rect.y;
164
165 // Get y position of mouse (in pixels)
166 int yMouse = state.m_y;
167
168 // Perhaps yTolerance should be put into preferences?
169 const int yTolerance = 10; // More tolerance on samples than on envelope.
170 if (abs(yValue - yMouse) >= yTolerance)
171 return {};
172
173 return HitAnywhere(holder, state, pChannel);
174}
175
177{
178}
179
180std::shared_ptr<const Track> SampleHandle::FindTrack() const
181{
183}
184
186(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
187{
188 using namespace RefreshCode;
189 using namespace WaveChannelUtilities;
190 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
191 if ( unsafe )
192 return Cancelled;
193
194 const wxMouseEvent &event = evt.event;
195 const wxRect &rect = evt.rect;
196 const auto &viewInfo = ViewInfo::Get( *pProject );
197
198 const double t0 = viewInfo.PositionToTime(event.m_x, rect.x);
199 const auto pTrack = mClickedTrack.get();
201 if (!mClickedClip)
202 return Cancelled;
203
204 const auto intervals = viewInfo.FindIntervals(rect.width);
206 if (!SampleResolutionTest(viewInfo, *mClickedClip, intervals))
207 {
209 XO("To use Draw, zoom in further until you can see the individual samples."),
210 XO("Draw Tool"));
211 return Cancelled;
212 }
213
215 mRect = rect;
216
217 //convert t0 to samples
218 mClickedStartPixel = viewInfo.TimeToPosition(t0);
219
220 //Determine how drawing should occur. If alt is down,
221 //do a smoothing, instead of redrawing.
222 if (event.m_altDown)
223 {
224 mAltKey = true;
225 //*************************************************
226 //*** ALT-DOWN-CLICK (SAMPLE SMOOTHING) ***
227 //*************************************************
228 //
229 // Smoothing works like this: There is a smoothing kernel radius constant that
230 // determines how wide the averaging window is. Plus, there is a smoothing brush radius,
231 // which determines how many pixels wide around the selected pixel this smoothing is applied.
232 //
233 // Samples will be replaced by a mixture of the original points and the smoothed points,
234 // with a triangular mixing probability whose value at the center point is
235 // SMOOTHING_PROPORTION_MAX and at the far bounds is SMOOTHING_PROPORTION_MIN
236
237 //Get the region of samples around the selected point
238 size_t sampleRegionSize = 1 + 2 * (SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS);
239 Floats sampleRegion{ sampleRegionSize };
240 Floats newSampleRegion{ 1 + 2 * (size_t)SMOOTHING_BRUSH_RADIUS };
241
242 //Get a sample from the clip to do some tricks on.
243 constexpr auto mayThrow = false;
244 const auto sampleRegionRange = GetFloatsCenteredAroundTime(*mClickedTrack,
245 t0, sampleRegion.get(),
247
248 //Go through each point of the smoothing brush and apply a smoothing operation.
249 for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
250 float sumOfSamples = 0;
251 for (auto ii = -SMOOTHING_KERNEL_RADIUS; ii <= SMOOTHING_KERNEL_RADIUS; ++ii) {
252 //Go through each point of the smoothing kernel and find the average
253 const auto sampleRegionIndex =
255 const auto inRange = sampleRegionRange.first <= sampleRegionIndex &&
256 sampleRegionIndex < sampleRegionRange.second;
257 if (!inRange)
258 continue;
259 //The average is a weighted average, scaled by a weighting kernel that is simply triangular
260 // A triangular kernel across N items, with a radius of R ( 2 R + 1 points), if the farthest:
261 // points have a probability of a, the entire triangle has total probability of (R + 1)^2.
262 // For sample number ii and middle brush sample M, (R + 1 - abs(M-ii))/ ((R+1)^2) gives a
263 // legal distribution whose total probability is 1.
264 //
265 //
266 // weighting factor value
267 sumOfSamples += (SMOOTHING_KERNEL_RADIUS + 1 - abs(ii)) *
268 sampleRegion[sampleRegionIndex];
269 }
270 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
271 sumOfSamples /
273 }
274
275
276 // Now that the NEW sample levels are determined, go through each and mix it appropriately
277 // with the original point, according to a 2-part linear function whose center has probability
278 // SMOOTHING_PROPORTION_MAX and extends out SMOOTHING_BRUSH_RADIUS, at which the probability is
279 // SMOOTHING_PROPORTION_MIN. _MIN and _MAX specify how much of the smoothed curve make it through.
280
281 float prob;
282
283 for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
284
285 prob =
287 (float)abs(jj) / SMOOTHING_BRUSH_RADIUS *
289
290 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
291 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] * prob +
293 (1 - prob);
294 }
295 // Set a range of samples around the mouse event
296 // Don't require dithering later
298 t0, newSampleRegion.get(), SMOOTHING_BRUSH_RADIUS,
300
301 // mLastDragSampleValue will not be used
302 }
303 else
304 {
305 mAltKey = false;
306 //*************************************************
307 //*** PLAIN DOWN-CLICK (NORMAL DRAWING) ***
308 //*************************************************
309
310 //Otherwise (e.g., the alt button is not down) do normal redrawing, based on the mouse position.
311 const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
312
313 //Set the sample to the point of the mouse event
314 // Don't require dithering later
315
317
318 mLastDragSampleValue = newLevel;
319 }
320
321 //Set the member data structures for drawing
323
324 // Sample data changed on either branch, so refresh the track display.
325 return RefreshCell;
326}
327
328namespace
329{
330size_t GetLastEditableClipStartingFromNthClip(size_t n, bool forward,
331 const WaveChannelUtilities::ClipPointers& sortedClips,
332 const ViewInfo& viewInfo, const ZoomInfo::Intervals& intervals)
333{
334 assert(n < sortedClips.size());
335 const auto increment = forward ? 1 : -1;
336 int last = n + increment;
337 const auto limit = forward ? sortedClips.size() : -1;
338 while (last != limit)
339 {
340 if (!SampleResolutionTest(viewInfo, *sortedClips[last], intervals))
341 break;
342 last += increment;
343 }
344 last -= increment;
345 assert(last >= 0 && last < sortedClips.size());
346 return last;
347}
348} // namespace
349
351(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
352{
353 using namespace RefreshCode;
354 using namespace WaveChannelUtilities;
355 const wxMouseEvent &event = evt.event;
356 const auto &viewInfo = ViewInfo::Get( *pProject );
357
358 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
359 if (unsafe)
360 {
361 this->Cancel(pProject);
362 return RefreshCell | Cancelled;
363 }
364
365 // There must have been some clicking before dragging ...
366 assert(mClickedTrack && mClickedClip);
367 if (!(mClickedTrack && mClickedClip))
368 return Cancelled;
369
370 //*************************************************
371 //*** DRAG-DRAWING ***
372 //*************************************************
373
374 //No dragging effects if the alt key is down--
375 //Don't allow left-right dragging for smoothing operation
376 if (mAltKey)
377 return RefreshNone;
378
379 const double t0 = viewInfo.PositionToTime(mLastDragPixel);
380 const double t1 = viewInfo.PositionToTime(event.m_x, mRect.x);
381
382 const int x1 =
383 event.m_controlDown ? mClickedStartPixel : viewInfo.TimeToPosition(t1);
384 const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
385 const auto start = std::min(t0, t1);
386 const auto end = std::max(t0, t1);
387
388 // Starting from the originally clicked clip, restrict the editing boundaries
389 // to the succession of clips with visible samples. If, of clips A, B and C,
390 // only B had invisible samples, it'd mean one could not drag-draw from A
391 // into C, but that probably isn't a behavior worthwhile much implementation
392 // complications.
394 const auto iter = std::find_if(clips.begin(), clips.end(),
395 // Compare the intervals, not the pointers to them
396 [this](const auto &pClip){ return *pClip == *mClickedClip; });
397 const auto clickedClipIndex = std::distance(clips.begin(), iter);
398 constexpr auto forward = true;
399 const auto intervals = viewInfo.FindIntervals(mRect.width);
400 const size_t leftmostEditable = GetLastEditableClipStartingFromNthClip(
401 clickedClipIndex, !forward, clips, viewInfo, intervals);
402 const size_t rightmostEditable = GetLastEditableClipStartingFromNthClip(
403 clickedClipIndex, forward, clips, viewInfo, intervals);
404
405 const auto editStart =
406 std::max(start, clips[leftmostEditable]->GetPlayStartTime());
407 const auto editEnd =
408 std::min(end, clips[rightmostEditable]->GetPlayEndTime());
409
410 // For fast pencil movements covering more than one sample between two
411 // updates, we draw a line going from v0 at t0 to v1 at t1.
412 const auto interpolator = [t0, t1, v0 = mLastDragSampleValue,
413 v1 = newLevel](double t) {
414 if (t0 == t1)
415 return v1;
416 const auto gradient = (v1 - v0) / (t1 - t0);
417 const auto value = static_cast<float>(gradient * (t - t0) + v0);
418 // t may be outside t0 and t1, and we don't want to extrapolate.
419 return std::clamp(value, std::min(v0, v1), std::max(v0, v1));
420 };
422 editStart, editEnd, interpolator, narrowestSampleFormat);
423
424 mLastDragPixel = x1;
425 mLastDragSampleValue = newLevel;
426
427 return RefreshCell;
428}
429
431(const TrackPanelMouseState &st, AudacityProject *pProject)
432{
433 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
434 return HitPreview(st.state, pProject, unsafe);
435}
436
438(const TrackPanelMouseEvent &, AudacityProject *pProject,
439 wxWindow *)
440{
441 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
442 if (unsafe)
443 return this->Cancel(pProject);
444
445 //*************************************************
446 //*** UP-CLICK (Finish drawing) ***
447 //*************************************************
448 //On up-click, send the state to the undo stack
449 mClickedTrack.reset(); //Set this to NULL so it will catch improper drag events.
450 mClickedClip = nullptr;
451 ProjectHistory::Get( *pProject ).PushState(XO("Moved Samples"),
452 XO("Sample Edit"),
454
455 // No change to draw since last drag
457}
458
460{
461 mClickedTrack.reset();
462 ProjectHistory::Get( *pProject ).RollbackState();
464}
465
467 (const wxMouseEvent &event, const ViewInfo &viewInfo, double t0)
468{
469 using namespace WaveChannelUtilities;
470 // Calculate where the mouse is located vertically (between +/- 1)
471 float zoomMin, zoomMax;
472 auto &cache = WaveformScale::Get(*mClickedTrack);
473 cache.GetDisplayBounds(zoomMin, zoomMax);
474
475 const int yy = event.m_y - mRect.y;
476 const int height = mRect.GetHeight();
478 const bool dB = !settings.isLinear();
479 float newLevel =
480 ::ValueOfPixel(yy, height, false, dB,
481 settings.dBRange, zoomMin, zoomMax);
482
483 //Take the envelope into account
484 const auto time = viewInfo.PositionToTime(event.m_x, mRect.x);
485 if (const auto env = GetEnvelopeAtTime(*mClickedTrack, time)){
486 // Calculate sample as it would be rendered
487 double envValue = env->GetValue(t0);
488 if (envValue > 0)
489 newLevel /= envValue;
490 else
491 newLevel = 0;
492
493 //Make sure the NEW level is between +/-1
494 newLevel = std::max(-1.0f, std::min(1.0f, newLevel));
495 }
496
497 return newLevel;
498}
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:189
std::shared_ptr< Subclass > AssignUIHandlePtr(std::weak_ptr< Subclass > &holder, const std::shared_ptr< Subclass > &pNew)
Definition: UIHandle.h:164
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
bool IsAudioActive() const
static ProjectAudioIO & Get(AudacityProject &project)
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
static ProjectHistory & Get(AudacityProject &project)
std::shared_ptr< WaveClipChannel > mClickedClip
Definition: SampleHandle.h:74
Result Release(const TrackPanelMouseEvent &event, AudacityProject *pProject, wxWindow *pParent) override
virtual ~SampleHandle()
float FindSampleEditingLevel(const wxMouseEvent &event, const ViewInfo &viewInfo, double t0)
float mLastDragSampleValue
Definition: SampleHandle.h:79
std::shared_ptr< WaveChannel > mClickedTrack
Definition: SampleHandle.h:73
int mClickedStartPixel
Definition: SampleHandle.h:77
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< WaveChannel > &pChannel)
void Enter(bool forward, AudacityProject *) override
Result Click(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
wxRect mRect
Definition: SampleHandle.h:75
std::shared_ptr< const Track > FindTrack() const override
HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
static UIHandlePtr HitTest(std::weak_ptr< SampleHandle > &holder, const wxMouseState &state, const wxRect &rect, const AudacityProject *pProject, const std::shared_ptr< WaveChannel > &pChannel)
SampleHandle(const SampleHandle &)=delete
int mLastDragPixel
Definition: SampleHandle.h:78
static std::shared_ptr< const Track > TrackFromChannel(const std::shared_ptr< const Channel > &pChannel)
A frequent convenience in the definition of UIHandles.
Definition: UIHandle.cpp:63
Result mChangeHighlight
Definition: UIHandle.h:152
unsigned Result
Definition: UIHandle.h:40
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
int GetRate() const override
Definition: WaveClip.cpp:193
double GetPlayStartTime() const override
Definition: WaveClip.cpp:198
double GetStretchRatio() const override
Definition: WaveClip.cpp:218
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
Namespace containing an enum 'what to do on a refresh?'.
Definition: RefreshCode.h:16
WAVE_TRACK_API void SetFloatsCenteredAroundTime(WaveChannel &channel, double t, const float *buffer, size_t numSideSamples, sampleFormat effectiveFormat)
Similar to GetFloatsCenteredAroundTime, but for writing. Sets as many samples as it can according to ...
std::vector< ClipPointer > ClipPointers
WAVE_TRACK_API void SetFloatsWithinTimeRange(WaveChannel &channel, double t0, double t1, const std::function< float(double sampleTime)> &producer, sampleFormat effectiveFormat)
Provides a means of setting clip values as a function of time. Included are closest sample to t0 up t...
WAVE_TRACK_API std::pair< size_t, size_t > GetFloatsCenteredAroundTime(const WaveChannel &channel, double t, float *buffer, size_t numSideSamples, bool mayThrow)
Gets as many samples as it can, but no more than 2 * numSideSamples + 1, centered around t....
WAVE_TRACK_API ClipPointer GetClipAtTime(WaveChannel &channel, double time)
WAVE_TRACK_API bool GetFloatAtTime(const WaveChannel &channel, double t, float &value, bool mayThrow)
WAVE_TRACK_API Envelope * GetEnvelopeAtTime(WaveChannel &channel, double time)
WAVE_TRACK_API void SetFloatAtTime(WaveChannel &channel, double t, float value, sampleFormat effectiveFormat)
Sets sample nearest to t to value. Silently fails if GetClipAtTime(t) == nullptr.
WAVE_TRACK_API ClipPointers SortedClipArray(WaveChannel &channel)
Get clips sorted by play start time.
size_t GetLastEditableClipStartingFromNthClip(size_t n, bool forward, const WaveChannelUtilities::ClipPointers &sortedClips, const ViewInfo &viewInfo, const ZoomInfo::Intervals &intervals)
double adjustTime(const WaveChannel &wt, double time)
bool SampleResolutionTest(const ViewInfo &viewInfo, const WaveChannelInterval &clip, const ZoomInfo::Intervals &intervals)
const char * end(const char *str) noexcept
Definition: StringUtils.h:106