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