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 "WaveTrack.h"
29#include "../../../../../images/Cursors.h"
30#include "AudacityMessageBox.h"
31
32
33static const int SMOOTHING_KERNEL_RADIUS = 3;
34static const int SMOOTHING_BRUSH_RADIUS = 5;
35static const double SMOOTHING_PROPORTION_MAX = 0.7;
36static const double SMOOTHING_PROPORTION_MIN = 0.0;
37
38SampleHandle::SampleHandle( const std::shared_ptr<WaveTrack> &pTrack )
39 : mClickedTrack{ pTrack }
40{
41}
42
44{
45#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
47#endif
48}
49
51(const wxMouseState &state, const AudacityProject *WXUNUSED(pProject), bool unsafe)
52{
53 static auto disabledCursor =
54 ::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
55 static wxCursor smoothCursor{ wxCURSOR_SPRAYCAN };
56 static auto pencilCursor =
57 ::MakeCursor(wxCURSOR_PENCIL, DrawCursorXpm, 12, 22);
58
59 // TODO: message should also mention the brush. Describing the modifier key
60 // (alt, or other) varies with operating system.
61 auto message = XO("Click and drag to edit the samples");
62
63 return {
64 message,
65 (unsafe
66 ? &*disabledCursor
67 : (state.AltDown()
68 ? &smoothCursor
69 : &*pencilCursor))
70 };
71}
72
74(std::weak_ptr<SampleHandle> &holder,
75 const wxMouseState &WXUNUSED(state), const std::shared_ptr<WaveTrack> &pTrack)
76{
77 auto result = std::make_shared<SampleHandle>( pTrack );
78 result = AssignUIHandlePtr(holder, result);
79 return result;
80}
81
82namespace {
83 inline double adjustTime(const WaveTrack *wt, double time)
84 {
85 // Round to an exact sample time
86 return wt->LongSamplesToTime(wt->TimeToLongSamples(time));
87 }
88
89 // Is the sample horizontally nearest to the cursor sufficiently separated
90 // from its neighbors that the pencil tool should be allowed to drag it?
92 ( const ViewInfo &viewInfo, const WaveTrack *wt, double time, int width )
93 {
94 // Require more than 3 pixels per sample
95 const auto xx = std::max<ZoomInfo::int64>(0, viewInfo.TimeToPosition(time));
96 ZoomInfo::Intervals intervals;
97 const double rate = wt->GetRate();
98 viewInfo.FindIntervals(rate, intervals, width);
99 ZoomInfo::Intervals::const_iterator it = intervals.begin(),
100 end = intervals.end(), prev;
101 wxASSERT(it != end && it->position == 0);
102 do
103 prev = it++;
104 while (it != end && it->position <= xx);
105 const double threshold = 3 * rate;
106 // three times as many pixels per second, as samples
107 return prev->averageZoom > threshold;
108 }
109}
110
112(std::weak_ptr<SampleHandle> &holder,
113 const wxMouseState &state, const wxRect &rect,
114 const AudacityProject *pProject, const std::shared_ptr<WaveTrack> &pTrack)
115{
116 const auto &viewInfo = ViewInfo::Get( *pProject );
117
120 const auto wavetrack = pTrack.get();
121 const auto time = viewInfo.PositionToTime(state.m_x, rect.x);
122
123 const double tt = adjustTime(wavetrack, time);
124 if (!SampleResolutionTest(viewInfo, wavetrack, tt, rect.width))
125 return {};
126
127 // Just get one sample.
128 float oneSample;
129 const double rate = wavetrack->GetRate();
130 const auto s0 = (sampleCount)(tt * rate + 0.5);
131 if (! wavetrack->GetFloats(&oneSample, s0, 1, fillZero,
132 // Do not propagate exception but return a failure value
133 false) )
134 return {};
135
136 // Get y distance of envelope point from center line (in pixels).
137 auto &cache = WaveformScale::Get(*wavetrack);
138 float zoomMin, zoomMax;
139 cache.GetDisplayBounds(zoomMin, zoomMax);
140
141 double envValue = 1.0;
142 Envelope* env = wavetrack->GetEnvelopeAtTime(time);
143 if (env)
144 // Calculate sample as it would be rendered, so quantize time
145 envValue = env->GetValue( tt, 1.0 / wavetrack->GetRate() );
146
147 auto &settings = WaveformSettings::Get(*wavetrack);
148 const bool dB = !settings.isLinear();
149 int yValue = GetWaveYPos(oneSample * envValue,
150 zoomMin, zoomMax,
151 rect.height, dB, true,
152 settings.dBRange, false) + rect.y;
153
154 // Get y position of mouse (in pixels)
155 int yMouse = state.m_y;
156
157 // Perhaps yTolerance should be put into preferences?
158 const int yTolerance = 10; // More tolerance on samples than on envelope.
159 if (abs(yValue - yMouse) >= yTolerance)
160 return {};
161
162 return HitAnywhere(holder, state, pTrack);
163}
164
166{
167}
168
169namespace {
174 (const wxMouseEvent &event,
175 const wxRect &rect, const ViewInfo &viewInfo, WaveTrack *wt, int width)
176 {
177 //If we aren't zoomed in far enough, show a message dialog.
178 const double time = adjustTime(wt, viewInfo.PositionToTime(event.m_x, rect.x));
179 if (!SampleResolutionTest(viewInfo, wt, time, width))
180 {
182 XO(
183"To use Draw, zoom in further until you can see the individual samples."),
184 XO("Draw Tool"));
185 return false;
186 }
187 return true;
188 }
189}
190
192(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
193{
194 using namespace RefreshCode;
195 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
196 if ( unsafe )
197 return Cancelled;
198
199 const wxMouseEvent &event = evt.event;
200 const wxRect &rect = evt.rect;
201 const auto &viewInfo = ViewInfo::Get( *pProject );
202 const auto pTrack = mClickedTrack.get();
203
206 event, rect, viewInfo, pTrack, rect.width))
207 return Cancelled;
208
210 mRect = rect;
211
212 //If we are still around, we are drawing in earnest. Set some member data structures up:
213 //First, calculate the starting sample. To get this, we need the time
214 const double t0 =
215 adjustTime(mClickedTrack.get(), viewInfo.PositionToTime(event.m_x, rect.x));
216
217 //convert t0 to samples
218 mClickedStartSample = mClickedTrack->TimeToLongSamples(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 track to do some tricks on.
243 mClickedTrack->GetFloats(sampleRegion.get(),
245 sampleRegionSize);
246
247 //Go through each point of the smoothing brush and apply a smoothing operation.
248 for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
249 float sumOfSamples = 0;
250 for (auto ii = -SMOOTHING_KERNEL_RADIUS; ii <= SMOOTHING_KERNEL_RADIUS; ++ii) {
251 //Go through each point of the smoothing kernel and find the average
252
253 //The average is a weighted average, scaled by a weighting kernel that is simply triangular
254 // A triangular kernel across N items, with a radius of R ( 2 R + 1 points), if the farthest:
255 // points have a probability of a, the entire triangle has total probability of (R + 1)^2.
256 // For sample number ii and middle brush sample M, (R + 1 - abs(M-ii))/ ((R+1)^2) gives a
257 // legal distribution whose total probability is 1.
258 //
259 //
260 // weighting factor value
261 sumOfSamples +=
262 (SMOOTHING_KERNEL_RADIUS + 1 - abs(ii)) *
263 sampleRegion[ii + jj + SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS];
264
265 }
266 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
267 sumOfSamples /
269 }
270
271
272 // Now that the NEW sample levels are determined, go through each and mix it appropriately
273 // with the original point, according to a 2-part linear function whose center has probability
274 // SMOOTHING_PROPORTION_MAX and extends out SMOOTHING_BRUSH_RADIUS, at which the probability is
275 // SMOOTHING_PROPORTION_MIN. _MIN and _MAX specify how much of the smoothed curve make it through.
276
277 float prob;
278
279 for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
280
281 prob =
283 (float)abs(jj) / SMOOTHING_BRUSH_RADIUS *
285
286 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
287 newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] * prob +
289 (1 - prob);
290 }
291 //Set the sample to the point of the mouse event
292 // Don't require dithering later
293 mClickedTrack->Set((samplePtr)newSampleRegion.get(), floatSample,
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 mClickedTrack->Set(
315
316 mLastDragSampleValue = newLevel;
317 }
318
319 //Set the member data structures for drawing
321
322 // Sample data changed on either branch, so refresh the track display.
323 return RefreshCell;
324}
325
327(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
328{
329 using namespace RefreshCode;
330 const wxMouseEvent &event = evt.event;
331 const auto &viewInfo = ViewInfo::Get( *pProject );
332
333 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
334 if (unsafe) {
335 this->Cancel(pProject);
336 return RefreshCell | Cancelled;
337 }
338
339 //*************************************************
340 //*** DRAG-DRAWING ***
341 //*************************************************
342
343 //No dragging effects if the alt key is down--
344 //Don't allow left-right dragging for smoothing operation
345 if (mAltKey)
346 return RefreshNone;
347
348 sampleCount s0; //declare this for use below. It designates which sample number to draw.
349
350 // Figure out what time the click was at
351 //Find the point that we want to redraw at. If the control button is down,
352 //adjust only the originally clicked-on sample
353
354 if (event.m_controlDown) {
355 //*************************************************
356 //*** CTRL-DOWN (Hold Initial Sample Constant ***
357 //*************************************************
358
360 }
361 else {
362 //*************************************************
363 //*** Normal CLICK-drag (Normal drawing) ***
364 //*************************************************
365
366 //Otherwise, adjust the sample you are dragging over right now.
367 //convert this to samples
368 const double tt = viewInfo.PositionToTime(event.m_x, mRect.x);
369 s0 = mClickedTrack->TimeToLongSamples(tt);
370 }
371
372 const double t0 = mClickedTrack->LongSamplesToTime(s0);
373
374 // Do redrawing, based on the mouse position.
375 // Calculate where the mouse is located vertically (between +/- 1)
376
377 const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
378
379 //Now, redraw all samples between current and last redrawn sample, inclusive
380 //Go from the smaller to larger sample.
381 const auto start = std::min(s0, mLastDragSample);
382 const auto end = std::max(s0, mLastDragSample);
383 // Few enough samples to be drawn individually on screen will not
384 // overflow size_t:
385 const auto size = ( end - start + 1 ).as_size_t();
386 if (size == 1) {
387 // Don't require dithering later
388 mClickedTrack->Set(
389 (samplePtr)&newLevel, floatSample, start, size, narrowestSampleFormat);
390 }
391 else {
392 std::vector<float> values(size);
393 for (auto ii = start; ii <= end; ++ii) {
394 //This interpolates each sample linearly:
395 // i - start will not overflow size_t either:
396 values[( ii - start ).as_size_t()] =
398 (ii - mLastDragSample).as_float() /
399 (s0 - mLastDragSample).as_float();
400 }
401 // Don't require dithering later
402 mClickedTrack->Set(
404 }
405
406 //Update the member data structures.
407 mLastDragSample = s0;
408 mLastDragSampleValue = newLevel;
409
410 return RefreshCell;
411}
412
414(const TrackPanelMouseState &st, AudacityProject *pProject)
415{
416 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
417 return HitPreview(st.state, pProject, unsafe);
418}
419
421(const TrackPanelMouseEvent &, AudacityProject *pProject,
422 wxWindow *)
423{
424 const bool unsafe = ProjectAudioIO::Get( *pProject ).IsAudioActive();
425 if (unsafe)
426 return this->Cancel(pProject);
427
428 //*************************************************
429 //*** UP-CLICK (Finish drawing) ***
430 //*************************************************
431 //On up-click, send the state to the undo stack
432 mClickedTrack.reset(); //Set this to NULL so it will catch improper drag events.
433 ProjectHistory::Get( *pProject ).PushState(XO("Moved Samples"),
434 XO("Sample Edit"),
436
437 // No change to draw since last drag
439}
440
442{
443 mClickedTrack.reset();
444 ProjectHistory::Get( *pProject ).RollbackState();
446}
447
449 (const wxMouseEvent &event, const ViewInfo &viewInfo, double t0)
450{
451 // Calculate where the mouse is located vertically (between +/- 1)
452 float zoomMin, zoomMax;
453 auto &cache = WaveformScale::Get(*mClickedTrack);
454 cache.GetDisplayBounds(zoomMin, zoomMax);
455
456 const int yy = event.m_y - mRect.y;
457 const int height = mRect.GetHeight();
459 const bool dB = !settings.isLinear();
460 float newLevel =
461 ::ValueOfPixel(yy, height, false, dB,
462 settings.dBRange, zoomMin, zoomMax);
463
464 //Take the envelope into account
465 const auto time = viewInfo.PositionToTime(event.m_x, mRect.x);
466 Envelope *const env = mClickedTrack->GetEnvelopeAtTime(time);
467 if (env)
468 {
469 // Calculate sample as it would be rendered, so quantize time
470 double envValue = env->GetValue( t0, 1.0 / mClickedTrack->GetRate());
471 if (envValue > 0)
472 newLevel /= envValue;
473 else
474 newLevel = 0;
475
476 //Make sure the NEW level is between +/-1
477 newLevel = std::max(-1.0f, std::min(1.0f, newLevel));
478 }
479
480 return newLevel;
481}
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)
const wxChar * values
XO("Cut/Copy/Paste")
@ narrowestSampleFormat
Two synonyms for previous values that might change if more values were added.
char * samplePtr
Definition: SampleFormat.h:55
@ fillZero
Definition: SampleFormat.h:60
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:46
float ValueOfPixel(int yy, int height, bool offset, bool dB, double dBRange, float zoomMin, float zoomMax)
Definition: TrackArt.cpp:102
static Settings & settings()
Definition: TrackInfo.cpp:87
std::unique_ptr< wxCursor > MakeCursor(int WXUNUSED(CursorId), const char *const pXpm[36], int HotX, int HotY)
Definition: TrackPanel.cpp:186
std::shared_ptr< Subclass > AssignUIHandlePtr(std::weak_ptr< Subclass > &holder, const std::shared_ptr< Subclass > &pNew)
Definition: UIHandle.h:151
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:828
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)
float mLastDragSampleValue
Definition: SampleHandle.h:76
sampleCount mClickedStartSample
Definition: SampleHandle.h:74
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
sampleCount mLastDragSample
Definition: SampleHandle.h:75
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:72
std::shared_ptr< WaveTrack > mClickedTrack
Definition: SampleHandle.h:71
HitTestPreview Preview(const TrackPanelMouseState &state, AudacityProject *pProject) override
Result Drag(const TrackPanelMouseEvent &event, AudacityProject *pProject) override
SampleHandle(const SampleHandle &)=delete
double LongSamplesToTime(sampleCount pos) const
Convert correctly between a number of samples and an (absolute) time in seconds.
Definition: SampleTrack.cpp:48
sampleCount TimeToLongSamples(double t0) const
Convert correctly between an (absolute) time in seconds and a number of samples.
Definition: SampleTrack.cpp:43
Result mChangeHighlight
Definition: UIHandle.h:139
unsigned Result
Definition: UIHandle.h:38
static ViewInfo & Get(AudacityProject &project)
Definition: ViewInfo.cpp:235
A Track that contains audio waveform data.
Definition: WaveTrack.h:51
double GetRate() const override
Definition: WaveTrack.cpp:360
static WaveformScale & Get(const WaveTrack &track)
Mutative access to attachment even if the track argument is const.
static WaveformSettings & Get(const WaveTrack &track)
double PositionToTime(int64 position, int64 origin=0, bool ignoreFisheye=false) const
Definition: ZoomInfo.cpp:35
void FindIntervals(double rate, Intervals &results, int64 width, int64 origin=0) const
Definition: ZoomInfo.cpp:104
std::vector< Interval > Intervals
Definition: ZoomInfo.h:136
int64 TimeToPosition(double time, int64 origin=0, bool ignoreFisheye=false) const
STM: Converts a project time to screen x position.
Definition: ZoomInfo.cpp:45
Positions or offsets within audio files need a wide type.
Definition: SampleCount.h:19
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
bool SampleResolutionTest(const ViewInfo &viewInfo, const WaveTrack *wt, double time, int width)
double adjustTime(const WaveTrack *wt, double time)
bool IsSampleEditingPossible(const wxMouseEvent &event, const wxRect &rect, const ViewInfo &viewInfo, WaveTrack *wt, int width)