audacia/src/tracks/playabletrack/wavetrack/ui/SampleHandle.cpp

483 lines
17 KiB
C++

/**********************************************************************
Audacity: A Digital Audio Editor
SampleHandle.cpp
Paul Licameli split from TrackPanel.cpp
**********************************************************************/
#include "../../../../Audacity.h"
#include "SampleHandle.h"
#include "../../../../Experimental.h"
#include <algorithm>
#include "../../../../MemoryX.h"
#include <wx/gdicmn.h>
#include "../../../../Envelope.h"
#include "../../../../HitTestResult.h"
#include "../../../../prefs/WaveformSettings.h"
#include "../../../../Project.h"
#include "../../../../RefreshCode.h"
#include "../../../../TrackArtist.h"
#include "../../../../TrackPanelMouseEvent.h"
#include "../../../../UndoManager.h"
#include "../../../../ViewInfo.h"
#include "../../../../WaveTrack.h"
#include "../../../../../images/Cursors.h"
static const int SMOOTHING_KERNEL_RADIUS = 3;
static const int SMOOTHING_BRUSH_RADIUS = 5;
static const double SMOOTHING_PROPORTION_MAX = 0.7;
static const double SMOOTHING_PROPORTION_MIN = 0.0;
SampleHandle::SampleHandle( const std::shared_ptr<WaveTrack> &pTrack )
: mClickedTrack{ pTrack }
{
}
void SampleHandle::Enter(bool)
{
#ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
mChangeHighlight = RefreshCode::RefreshCell;
#endif
}
HitTestPreview SampleHandle::HitPreview
(const wxMouseState &state, const AudacityProject *WXUNUSED(pProject), bool unsafe)
{
static auto disabledCursor =
::MakeCursor(wxCURSOR_NO_ENTRY, DisabledCursorXpm, 16, 16);
static wxCursor smoothCursor{ wxCURSOR_SPRAYCAN };
static auto pencilCursor =
::MakeCursor(wxCURSOR_PENCIL, DrawCursorXpm, 12, 22);
// TODO: message should also mention the brush. Describing the modifier key
// (alt, or other) varies with operating system.
auto message = _("Click and drag to edit the samples");
return {
message,
(unsafe
? &*disabledCursor
: (state.AltDown()
? &smoothCursor
: &*pencilCursor))
};
}
UIHandlePtr SampleHandle::HitAnywhere
(std::weak_ptr<SampleHandle> &holder,
const wxMouseState &WXUNUSED(state), const std::shared_ptr<WaveTrack> &pTrack)
{
auto result = std::make_shared<SampleHandle>( pTrack );
result = AssignUIHandlePtr(holder, result);
return result;
}
namespace {
inline double adjustTime(const WaveTrack *wt, double time)
{
// Round to an exact sample time
return wt->LongSamplesToTime(wt->TimeToLongSamples(time));
}
// Is the sample horizontally nearest to the cursor sufficiently separated
// from its neighbors that the pencil tool should be allowed to drag it?
bool SampleResolutionTest
( const ViewInfo &viewInfo, const WaveTrack *wt, double time, int width )
{
// Require more than 3 pixels per sample
const wxInt64 xx = std::max(wxInt64(0), viewInfo.TimeToPosition(time));
ZoomInfo::Intervals intervals;
const double rate = wt->GetRate();
viewInfo.FindIntervals(rate, intervals, width);
ZoomInfo::Intervals::const_iterator it = intervals.begin(),
end = intervals.end(), prev;
wxASSERT(it != end && it->position == 0);
do
prev = it++;
while (it != end && it->position <= xx);
const double threshold = 3 * rate;
// three times as many pixels per second, as samples
return prev->averageZoom > threshold;
}
}
UIHandlePtr SampleHandle::HitTest
(std::weak_ptr<SampleHandle> &holder,
const wxMouseState &state, const wxRect &rect,
const AudacityProject *pProject, const std::shared_ptr<WaveTrack> &pTrack)
{
const ViewInfo &viewInfo = pProject->GetViewInfo();
WaveTrack *wavetrack = pTrack.get();
const int displayType = wavetrack->GetDisplay();
if (WaveTrack::Waveform != displayType)
return {}; // Not a wave, so return.
const double tt =
adjustTime(wavetrack, viewInfo.PositionToTime(state.m_x, rect.x));
if (!SampleResolutionTest(viewInfo, wavetrack, tt, rect.width))
return {};
// Just get one sample.
float oneSample;
const double rate = wavetrack->GetRate();
const auto s0 = (sampleCount)(tt * rate + 0.5);
if (! wavetrack->Get((samplePtr)&oneSample, floatSample, s0, 1, fillZero,
// Do not propagate exception but return a failure value
false) )
return {};
// Get y distance of envelope point from center line (in pixels).
float zoomMin, zoomMax;
wavetrack->GetDisplayBounds(&zoomMin, &zoomMax);
double envValue = 1.0;
Envelope* env = wavetrack->GetEnvelopeAtX(state.GetX());
if (env)
// Calculate sample as it would be rendered, so quantize time
envValue = env->GetValue( tt, 1.0 / wavetrack->GetRate() );
const bool dB = !wavetrack->GetWaveformSettings().isLinear();
int yValue = GetWaveYPos(oneSample * envValue,
zoomMin, zoomMax,
rect.height, dB, true,
wavetrack->GetWaveformSettings().dBRange, false) + rect.y;
// Get y position of mouse (in pixels)
int yMouse = state.m_y;
// Perhaps yTolerance should be put into preferences?
const int yTolerance = 10; // More tolerance on samples than on envelope.
if (abs(yValue - yMouse) >= yTolerance)
return {};
return HitAnywhere(holder, state, pTrack);
}
SampleHandle::~SampleHandle()
{
}
namespace {
/// Determines if we can edit samples in a wave track.
/// Also pops up warning messages in certain cases where we can't.
/// @return true if we can edit the samples, false otherwise.
bool IsSampleEditingPossible
(const wxMouseEvent &event,
const wxRect &rect, const ViewInfo &viewInfo, Track *pTrack, int width)
{
if (pTrack->GetKind() != Track::Wave)
return false;
WaveTrack *wt = static_cast<WaveTrack*>(pTrack);
//Get out of here if we shouldn't be drawing right now:
//If we aren't displaying the waveform, Display a message dialog
const int display = wt->GetDisplay();
if (WaveTrack::Waveform != display)
{
wxMessageBox(_(
"To use Draw, choose 'Waveform' or 'Waveform (dB)' in the Track Dropdown Menu."),
_("Draw Tool"));
return false;
}
//If we aren't zoomed in far enough, show a message dialog.
const double time = adjustTime(wt, viewInfo.PositionToTime(event.m_x, rect.x));
if (!SampleResolutionTest(viewInfo, wt, time, width))
{
wxMessageBox(_(
"To use Draw, zoom in further until you can see the individual samples."),
_("Draw Tool"));
return false;
}
return true;
}
}
UIHandle::Result SampleHandle::Click
(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
{
using namespace RefreshCode;
const bool unsafe = pProject->IsAudioActive();
if ( unsafe )
return Cancelled;
const wxMouseEvent &event = evt.event;
const wxRect &rect = evt.rect;
const ViewInfo &viewInfo = pProject->GetViewInfo();
const auto pTrack = mClickedTrack.get();
/// Someone has just clicked the mouse. What do we do?
if (!IsSampleEditingPossible(
event, rect, viewInfo, pTrack, rect.width))
return Cancelled;
/// We're in a track view and zoomed enough to see the samples.
mRect = rect;
//If we are still around, we are drawing in earnest. Set some member data structures up:
//First, calculate the starting sample. To get this, we need the time
const double t0 =
adjustTime(mClickedTrack.get(), viewInfo.PositionToTime(event.m_x, rect.x));
//convert t0 to samples
mClickedStartSample = mClickedTrack->TimeToLongSamples(t0);
//Determine how drawing should occur. If alt is down,
//do a smoothing, instead of redrawing.
if (event.m_altDown)
{
mAltKey = true;
//*************************************************
//*** ALT-DOWN-CLICK (SAMPLE SMOOTHING) ***
//*************************************************
//
// Smoothing works like this: There is a smoothing kernel radius constant that
// determines how wide the averaging window is. Plus, there is a smoothing brush radius,
// which determines how many pixels wide around the selected pixel this smoothing is applied.
//
// Samples will be replaced by a mixture of the original points and the smoothed points,
// with a triangular mixing probability whose value at the center point is
// SMOOTHING_PROPORTION_MAX and at the far bounds is SMOOTHING_PROPORTION_MIN
//Get the region of samples around the selected point
size_t sampleRegionSize = 1 + 2 * (SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS);
Floats sampleRegion{ sampleRegionSize };
Floats newSampleRegion{ 1 + 2 * (size_t)SMOOTHING_BRUSH_RADIUS };
//Get a sample from the track to do some tricks on.
mClickedTrack->Get((samplePtr)sampleRegion.get(), floatSample,
mClickedStartSample - SMOOTHING_KERNEL_RADIUS - SMOOTHING_BRUSH_RADIUS,
sampleRegionSize);
//Go through each point of the smoothing brush and apply a smoothing operation.
for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
float sumOfSamples = 0;
for (auto ii = -SMOOTHING_KERNEL_RADIUS; ii <= SMOOTHING_KERNEL_RADIUS; ++ii) {
//Go through each point of the smoothing kernel and find the average
//The average is a weighted average, scaled by a weighting kernel that is simply triangular
// A triangular kernel across N items, with a radius of R ( 2 R + 1 points), if the farthest:
// points have a probability of a, the entire triangle has total probability of (R + 1)^2.
// For sample number ii and middle brush sample M, (R + 1 - abs(M-ii))/ ((R+1)^2) gives a
// legal distribution whose total probability is 1.
//
//
// weighting factor value
sumOfSamples +=
(SMOOTHING_KERNEL_RADIUS + 1 - abs(ii)) *
sampleRegion[ii + jj + SMOOTHING_KERNEL_RADIUS + SMOOTHING_BRUSH_RADIUS];
}
newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
sumOfSamples /
((SMOOTHING_KERNEL_RADIUS + 1) *(SMOOTHING_KERNEL_RADIUS + 1));
}
// Now that the NEW sample levels are determined, go through each and mix it appropriately
// with the original point, according to a 2-part linear function whose center has probability
// SMOOTHING_PROPORTION_MAX and extends out SMOOTHING_BRUSH_RADIUS, at which the probability is
// SMOOTHING_PROPORTION_MIN. _MIN and _MAX specify how much of the smoothed curve make it through.
float prob;
for (auto jj = -SMOOTHING_BRUSH_RADIUS; jj <= SMOOTHING_BRUSH_RADIUS; ++jj) {
prob =
SMOOTHING_PROPORTION_MAX -
(float)abs(jj) / SMOOTHING_BRUSH_RADIUS *
(SMOOTHING_PROPORTION_MAX - SMOOTHING_PROPORTION_MIN);
newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] =
newSampleRegion[jj + SMOOTHING_BRUSH_RADIUS] * prob +
sampleRegion[SMOOTHING_BRUSH_RADIUS + SMOOTHING_KERNEL_RADIUS + jj] *
(1 - prob);
}
//Set the sample to the point of the mouse event
mClickedTrack->Set((samplePtr)newSampleRegion.get(), floatSample,
mClickedStartSample - SMOOTHING_BRUSH_RADIUS, 1 + 2 * SMOOTHING_BRUSH_RADIUS);
// mLastDragSampleValue will not be used
}
else
{
mAltKey = false;
//*************************************************
//*** PLAIN DOWN-CLICK (NORMAL DRAWING) ***
//*************************************************
//Otherwise (e.g., the alt button is not down) do normal redrawing, based on the mouse position.
const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
//Set the sample to the point of the mouse event
mClickedTrack->Set((samplePtr)&newLevel, floatSample, mClickedStartSample, 1);
mLastDragSampleValue = newLevel;
}
//Set the member data structures for drawing
mLastDragSample = mClickedStartSample;
// Sample data changed on either branch, so refresh the track display.
return RefreshCell;
}
UIHandle::Result SampleHandle::Drag
(const TrackPanelMouseEvent &evt, AudacityProject *pProject)
{
using namespace RefreshCode;
const wxMouseEvent &event = evt.event;
const ViewInfo &viewInfo = pProject->GetViewInfo();
const bool unsafe = pProject->IsAudioActive();
if (unsafe) {
this->Cancel(pProject);
return RefreshCell | Cancelled;
}
//*************************************************
//*** DRAG-DRAWING ***
//*************************************************
//No dragging effects if the alt key is down--
//Don't allow left-right dragging for smoothing operation
if (mAltKey)
return RefreshNone;
sampleCount s0; //declare this for use below. It designates which sample number to draw.
// Figure out what time the click was at
//Find the point that we want to redraw at. If the control button is down,
//adjust only the originally clicked-on sample
if (event.m_controlDown) {
//*************************************************
//*** CTRL-DOWN (Hold Initial Sample Constant ***
//*************************************************
s0 = mClickedStartSample;
}
else {
//*************************************************
//*** Normal CLICK-drag (Normal drawing) ***
//*************************************************
//Otherwise, adjust the sample you are dragging over right now.
//convert this to samples
const double tt = viewInfo.PositionToTime(event.m_x, mRect.x);
s0 = mClickedTrack->TimeToLongSamples(tt);
}
const double t0 = mClickedTrack->LongSamplesToTime(s0);
// Do redrawing, based on the mouse position.
// Calculate where the mouse is located vertically (between +/- 1)
const float newLevel = FindSampleEditingLevel(event, viewInfo, t0);
//Now, redraw all samples between current and last redrawn sample, inclusive
//Go from the smaller to larger sample.
const auto start = std::min(s0, mLastDragSample);
const auto end = std::max(s0, mLastDragSample);
// Few enough samples to be drawn individually on screen will not
// overflow size_t:
const auto size = ( end - start + 1 ).as_size_t();
if (size == 1) {
mClickedTrack->Set((samplePtr)&newLevel, floatSample, start, size);
}
else {
std::vector<float> values(size);
for (auto ii = start; ii <= end; ++ii) {
//This interpolates each sample linearly:
// i - start will not overflow size_t either:
values[( ii - start ).as_size_t()] =
mLastDragSampleValue + (newLevel - mLastDragSampleValue) *
(ii - mLastDragSample).as_float() /
(s0 - mLastDragSample).as_float();
}
mClickedTrack->Set((samplePtr)&values[0], floatSample, start, size);
}
//Update the member data structures.
mLastDragSample = s0;
mLastDragSampleValue = newLevel;
return RefreshCell;
}
HitTestPreview SampleHandle::Preview
(const TrackPanelMouseState &st, const AudacityProject *pProject)
{
const bool unsafe = pProject->IsAudioActive();
return HitPreview(st.state, pProject, unsafe);
}
UIHandle::Result SampleHandle::Release
(const TrackPanelMouseEvent &, AudacityProject *pProject,
wxWindow *)
{
const bool unsafe = pProject->IsAudioActive();
if (unsafe)
return this->Cancel(pProject);
//*************************************************
//*** UP-CLICK (Finish drawing) ***
//*************************************************
//On up-click, send the state to the undo stack
mClickedTrack.reset(); //Set this to NULL so it will catch improper drag events.
pProject->PushState(_("Moved Samples"),
_("Sample Edit"),
UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE);
// No change to draw since last drag
return RefreshCode::RefreshNone;
}
UIHandle::Result SampleHandle::Cancel(AudacityProject *pProject)
{
pProject->RollbackState();
mClickedTrack.reset();
return RefreshCode::RefreshCell;
}
float SampleHandle::FindSampleEditingLevel
(const wxMouseEvent &event, const ViewInfo &, double t0)
{
// Calculate where the mouse is located vertically (between +/- 1)
float zoomMin, zoomMax;
mClickedTrack->GetDisplayBounds(&zoomMin, &zoomMax);
const int yy = event.m_y - mRect.y;
const int height = mRect.GetHeight();
const bool dB = !mClickedTrack->GetWaveformSettings().isLinear();
float newLevel =
::ValueOfPixel(yy, height, false, dB,
mClickedTrack->GetWaveformSettings().dBRange, zoomMin, zoomMax);
//Take the envelope into account
Envelope *const env = mClickedTrack->GetEnvelopeAtX(event.m_x);
if (env)
{
// Calculate sample as it would be rendered, so quantize time
double envValue = env->GetValue( t0, 1.0 / mClickedTrack->GetRate());
if (envValue > 0)
newLevel /= envValue;
else
newLevel = 0;
//Make sure the NEW level is between +/-1
newLevel = std::max(-1.0f, std::min(1.0f, newLevel));
}
return newLevel;
}