1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-07-04 14:39:08 +02:00

Bug1646: Fix many defects in the Note Track stretching tool

This commit is contained in:
Paul Licameli 2017-05-22 23:25:57 -04:00
commit 646c285913
4 changed files with 227 additions and 165 deletions

View File

@ -562,29 +562,30 @@ bool NoteTrack::Shift(double t) // t is always seconds
return true;
}
double NoteTrack::NearestBeatTime(double time, double *beat) const
QuantizedTimeAndBeat NoteTrack::NearestBeatTime( double time ) const
{
wxASSERT(mSeq);
// Alg_seq knows nothing about offset, so remove offset time
double seq_time = time - GetOffset();
seq_time = mSeq->nearest_beat_time(seq_time, beat);
double beat;
seq_time = mSeq->nearest_beat_time(seq_time, &beat);
// add the offset back in to get "actual" audacity track time
return seq_time + GetOffset();
return { seq_time + GetOffset(), beat };
}
bool NoteTrack::StretchRegion(double t0, double t1, double dur)
bool NoteTrack::StretchRegion
( QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur )
{
wxASSERT(mSeq);
// Alg_seq::stretch_region uses beats, so we translate time
// to beats first:
t0 -= GetOffset();
t1 -= GetOffset();
double b0 = mSeq->get_time_map()->time_to_beat(t0);
double b1 = mSeq->get_time_map()->time_to_beat(t1);
bool result = mSeq->stretch_region(b0, b1, dur);
bool result = mSeq->stretch_region( t0.second, t1.second, newDur );
if (result) {
const auto oldDur = t1.first - t0.first;
#if 0
// PRL: Would this be better ?
mSeq->set_real_dur(mSeq->get_real_dur() + newDur - oldDur);
#else
mSeq->convert_to_seconds();
mSeq->set_dur(mSeq->get_dur() + dur - (t1 - t0));
mSeq->set_dur(mSeq->get_dur() + newDur - oldDur);
#endif
}
return result;
}

View File

@ -11,6 +11,7 @@
#ifndef __AUDACITY_NOTETRACK__
#define __AUDACITY_NOTETRACK__
#include <utility>
#include <wx/string.h>
#include "Audacity.h"
#include "Experimental.h"
@ -58,6 +59,8 @@ using NoteTrackBase =
#endif
;
using QuantizedTimeAndBeat = std::pair< double, double >;
class AUDACITY_DLL_API NoteTrack final
: public NoteTrackBase
{
@ -112,8 +115,9 @@ class AUDACITY_DLL_API NoteTrack final
void SetVelocity(float velocity) { mVelocity = velocity; }
#endif
double NearestBeatTime(double time, double *beat) const;
bool StretchRegion(double b0, double b1, double dur);
QuantizedTimeAndBeat NearestBeatTime( double time ) const;
bool StretchRegion
( QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur );
int GetBottomNote() const { return mBottomNote; }
int GetPitchHeight() const { return mPitchHeight; }

View File

@ -480,10 +480,6 @@ TrackPanel::TrackPanel(wxWindow * parent, wxWindowID id,
#endif
#ifdef USE_MIDI
mStretchMode = stretchCenter;
mStretching = false;
mStretched = false;
mStretchStart = 0;
mStretchCursor = MakeCursor( wxCURSOR_BULLSEYE, StretchCursorXpm, 16, 16);
mStretchLeftCursor = MakeCursor( wxCURSOR_BULLSEYE,
StretchLeftCursorXpm, 16, 16);
@ -1363,7 +1359,9 @@ bool TrackPanel::SetCursorByActivity( )
return true;
#ifdef USE_MIDI
case IsStretching:
SetCursor( unsafe ? *mDisabledCursor : *mStretchCursor);
SetCursor( unsafe
? *mDisabledCursor
: *ChooseStretchCursor( mStretchState.mMode ) );
return true;
#endif
default:
@ -1631,13 +1629,14 @@ void TrackPanel::SetCursorAndTipWhenSelectTool( Track * t,
switch( boundary) {
case SBNone:
case SBLeft:
case SBRight:
if ( HitTestStretch(t, rect, event)) {
case SBRight: {
if ( auto stretchMode = HitTestStretch( t, rect, event ) ) {
tip = _("Click and drag to stretch within selected region.");
*ppCursor = mStretchCursor.get();
*ppCursor = ChooseStretchCursor( stretchMode );
return;
}
break;
}
default:
break;
}
@ -1841,6 +1840,41 @@ void TrackPanel::HandleSelect(wxMouseEvent & event)
SelectionHandleClick(event, t, rect);
} else if (event.LeftUp() || event.RightUp()) {
mSnapManager.reset();
bool left;
if ( GetProject()->IsSyncLocked() &&
( ( left = mStretchState.mMode == stretchLeft ) ||
mStretchState.mMode == stretchRight ) ) {
auto pNt = static_cast< NoteTrack * >( mCapturedTrack );
SyncLockedTracksIterator syncIter( GetTracks() );
for ( auto track = syncIter.StartWith(pNt); track != nullptr;
track = syncIter.Next() ) {
if ( track != pNt ) {
if ( left ) {
auto origT0 = mStretchState.mOrigT0;
auto diff = mViewInfo->selectedRegion.t0() - origT0;
if ( diff > 0)
track->SyncLockAdjust( origT0 + diff, origT0 );
else
track->SyncLockAdjust( origT0, origT0 - diff );
track->Offset( diff );
}
else {
auto origT1 = mStretchState.mOrigT1;
auto diff = mViewInfo->selectedRegion.t1() - origT1;
track->SyncLockAdjust( origT1, origT1 + diff );
}
}
}
}
if ( mStretchState.mStretching ) {
MakeParentPushState(_("Stretch Note Track"), _("Stretch"),
UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE);
Refresh(false);
SetCapturedTrack( NULL );
}
else {
// Do not draw yellow lines
if (mSnapLeft != -1 || mSnapRight != -1) {
mSnapLeft = mSnapRight = -1;
@ -1855,6 +1889,7 @@ void TrackPanel::HandleSelect(wxMouseEvent & event)
// This stops center snapping with mouse movement
mFreqSelMode = FREQ_SEL_INVALID;
#endif
}
} else if (event.LeftDClick() && !event.ShiftDown()) {
if (!mCapturedTrack) {
@ -2065,8 +2100,8 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
mSnapLeft = mSnapRight = -1;
#ifdef USE_MIDI
mStretching = false;
bool stretch = HitTestStretch(pTrack, rect, event);
mStretchState = StretchState{};
auto stretch = HitTestStretch( pTrack, rect, event, &mStretchState );
#endif
bool bShiftDown = event.ShiftDown();
@ -2234,72 +2269,57 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
const auto nt = static_cast<NoteTrack *>(pTrack);
// find nearest beat to sel0, sel1
double minPeriod = 0.05; // minimum beat period
double qBeat0, qBeat1;
double centerBeat = 0.0f;
mStretchSel0 = nt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0);
mStretchSel1 = nt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1);
// If there is not (almost) a beat to stretch that is slower
// than 20 beats per second, don't stretch
if (within(qBeat0, qBeat1, 0.9) ||
(mStretchSel1 - mStretchSel0) / (qBeat1 - qBeat0) < minPeriod) return;
if ( within( mStretchState.mBeat0.second,
mStretchState.mBeat1.second, 0.9 ) ||
( mStretchState.mBeat1.first - mStretchState.mBeat0.first ) /
( mStretchState.mBeat1.second - mStretchState.mBeat0.second )
< minPeriod )
return;
if (startNewSelection) { // mouse is not at an edge, but after
// quantization, we could be indicating the selection edge
mSelStartValid = true;
mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, rect.x));
mStretchStart = nt->NearestBeatTime(mSelStart, &centerBeat);
if (within(qBeat0, centerBeat, 0.1)) {
if ( stretch == stretchLeft ) {
mListener->TP_DisplayStatusMessage(
_("Click and drag to stretch selected region."));
SetCursor(*mStretchLeftCursor);
// mStretchMode = stretchLeft;
mSelStart = mViewInfo->selectedRegion.t1();
// condition that implies stretchLeft
startNewSelection = false;
} else if (within(qBeat1, centerBeat, 0.1)) {
}
else if ( stretchRight ) {
mListener->TP_DisplayStatusMessage(
_("Click and drag to stretch selected region."));
SetCursor(*mStretchRightCursor);
// mStretchMode = stretchRight;
mSelStart = mViewInfo->selectedRegion.t0();
// condition that implies stretchRight
startNewSelection = false;
}
}
if (startNewSelection) {
mStretchMode = stretchCenter;
mStretchLeftBeats = qBeat1 - centerBeat;
mStretchRightBeats = centerBeat - qBeat0;
} else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) {
// note that at this point, mSelStart is at the opposite
// end of the selection from the cursor. If the cursor is
// over sel0, then mSelStart is at sel1.
mStretchMode = stretchLeft;
} else {
mStretchMode = stretchRight;
mStretchState.mMode = stretch;
if ( mStretchState.mMode == stretchCenter ) {
mStretchState.mLeftBeats =
mStretchState.mBeat1.second - mStretchState.mBeatCenter.second;
mStretchState.mRightBeats =
mStretchState.mBeatCenter.second - mStretchState.mBeat0.second;
}
else if (mStretchState.mMode == stretchLeft) {
mStretchState.mLeftBeats = 0;
mStretchState.mRightBeats =
mStretchState.mBeat1.second - mStretchState.mBeat0.second;
} else if (mStretchState.mMode == stretchRight) {
mStretchState.mLeftBeats =
mStretchState.mBeat1.second - mStretchState.mBeat0.second;
mStretchState.mRightBeats = 0;
}
if (mStretchMode == stretchLeft) {
mStretchLeftBeats = 0;
mStretchRightBeats = qBeat1 - qBeat0;
} else if (mStretchMode == stretchRight) {
mStretchLeftBeats = qBeat1 - qBeat0;
mStretchRightBeats = 0;
}
mViewInfo->selectedRegion.setTimes(mStretchSel0, mStretchSel1);
mStretching = true;
mStretched = false;
// Do this before we change the selection
MakeParentModifyState( false );
/* i18n-hint: (noun) The track that is used for MIDI notes which can be
dragged to change their duration.*/
MakeParentPushState(_("Stretch Note Track"),
/* i18n-hint: In the history list, indicates a MIDI note has
been dragged to change its duration (stretch it). Using either past
or present tense is fine here. If unsure, go for whichever is
shorter.*/
_("Stretch"));
mViewInfo->selectedRegion.setTimes
( mStretchState.mBeat0.first, mStretchState.mBeat1.first );
mStretchState.mStretching = true;
// Full refresh since the label area may need to indicate
// newly selected tracks. (I'm really not sure if the label area
@ -2775,35 +2795,55 @@ void TrackPanel::ResetFreqSelectionPin(double hintFrequency, bool logF)
#endif
#ifdef USE_MIDI
wxCursor *TrackPanel::ChooseStretchCursor( StretchEnum mode )
{
switch ( mode ) {
case stretchCenter: return mStretchCursor.get();
case stretchLeft: return mStretchLeftCursor.get();
case stretchRight: return mStretchRightCursor.get();
default: return nullptr;
}
}
auto TrackPanel::ChooseStretchMode
( const wxMouseEvent &event, const wxRect &rect, const ViewInfo &viewInfo,
const NoteTrack *nt, StretchState *pState ) -> StretchEnum
{
// Assume x coordinate is in the selection and y is appropriate for stretch
// -- and then decide whether x is near enough to either edge or neither.
Maybe< StretchState > state;
if ( !pState )
state.create(), pState = state.get();
if ( nt ) {
pState->mBeat0 =
nt->NearestBeatTime( viewInfo.selectedRegion.t0() );
pState->mOrigT0 = pState->mBeat0.first;
pState->mBeat1 =
nt->NearestBeatTime( viewInfo.selectedRegion.t1() );
pState->mOrigT1 = pState->mBeat1.first;
auto selStart = viewInfo.PositionToTime(event.m_x, rect.x);
pState->mBeatCenter = nt->NearestBeatTime( selStart );
if ( within( pState->mBeat0.second, pState->mBeatCenter.second, 0.1 ) )
return stretchLeft;
else if ( within( pState->mBeat1.second, pState->mBeatCenter.second, 0.1 ) )
return stretchRight;
}
else {
pState->mBeat0 = pState->mBeat1 = pState->mBeatCenter = { 0, 0 };
return stretchNone;
}
return stretchCenter;
}
void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge,
Track *pTrack)
{
if (mStretched) { // Undo stretch and redo it with NEW mouse coordinates
// Drag handling was not originally implemented with Undo in mind --
// there are saved pointers to tracks that are not supposed to change.
// Undo will change tracks, so convert pTrack, mCapturedTrack to index
// values, then look them up after the Undo
TrackListIterator iter(GetTracks());
int pTrackIndex = pTrack->GetIndex();
int capturedTrackIndex =
(mCapturedTrack ? mCapturedTrack->GetIndex() : 0);
GetProject()->OnUndo();
// Undo brings us back to the pre-click state, but we want to
// quantize selected region to integer beat boundaries. These
// were saved in mStretchSel[12] variables:
mViewInfo->selectedRegion.setTimes(mStretchSel0, mStretchSel1);
mStretched = false;
int index = 0;
for (Track *t = iter.First(GetTracks()); t; t = iter.Next()) {
if (index == pTrackIndex) pTrack = t;
if (mCapturedTrack && index == capturedTrackIndex) mCapturedTrack = t;
index++;
}
}
if (pTrack == NULL && mCapturedTrack != NULL)
pTrack = mCapturedTrack;
@ -2811,68 +2851,63 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge,
return;
}
NoteTrack *pNt = (NoteTrack *) pTrack;
double moveto = std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
NoteTrack *pNt = static_cast< NoteTrack * >( pTrack );
double moveto =
std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
auto t1 = mViewInfo->selectedRegion.t1();
auto t0 = mViewInfo->selectedRegion.t0();
double dur, left_dur, right_dur;
// check to make sure tempo is not higher than 20 beats per second
// (In principle, tempo can be higher, but not infinity.)
double minPeriod = 0.05; // minimum beat period
double qBeat0, qBeat1;
pNt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); // get beat
pNt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1);
const double minPeriod = 0.05; // minimum beat period
// We could be moving 3 things: left edge, right edge, a point between
switch (mStretchMode) {
case stretchLeft: {
// make sure target duration is not too short
double dur = mViewInfo->selectedRegion.t1() - moveto;
if (dur < mStretchRightBeats * minPeriod) {
dur = mStretchRightBeats * minPeriod;
moveto = mViewInfo->selectedRegion.t1() - dur;
}
if (pNt->StretchRegion(mStretchSel0, mStretchSel1, dur)) {
pNt->SetOffset(pNt->GetOffset() + moveto - mStretchSel0);
// Take quick exit if so, without changing the selection.
switch (mStretchState.mMode) {
case stretchLeft: {
dur = t1 - moveto;
if (dur < mStretchState.mRightBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeat1, dur );
mStretchState.mBeat0.first = moveto;
pNt->Offset( moveto - t0 );
mViewInfo->selectedRegion.setT0(moveto);
}
break;
}
case stretchRight: {
// make sure target duration is not too short
double dur = moveto - mViewInfo->selectedRegion.t0();
if (dur < mStretchLeftBeats * minPeriod) {
dur = mStretchLeftBeats * minPeriod;
moveto = mStretchSel0 + dur;
}
if (pNt->StretchRegion(mStretchSel0, mStretchSel1, dur)) {
dur = moveto - t0;
if (dur < mStretchState.mLeftBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeat1, dur );
mViewInfo->selectedRegion.setT1(moveto);
}
mStretchState.mBeat1.first = moveto;
break;
}
case stretchCenter: {
// make sure both left and right target durations are not too short
double left_dur = moveto - mViewInfo->selectedRegion.t0();
double right_dur = mViewInfo->selectedRegion.t1() - moveto;
double centerBeat;
pNt->NearestBeatTime(mSelStart, &centerBeat);
if (left_dur < mStretchLeftBeats * minPeriod) {
left_dur = mStretchLeftBeats * minPeriod;
moveto = mStretchSel0 + left_dur;
}
if (right_dur < mStretchRightBeats * minPeriod) {
right_dur = mStretchRightBeats * minPeriod;
moveto = mStretchSel1 - right_dur;
}
pNt->StretchRegion(mStretchStart, mStretchSel1, right_dur);
pNt->StretchRegion(mStretchSel0, mStretchStart, left_dur);
left_dur = moveto - t0;
right_dur = t1 - moveto;
if (left_dur < mStretchState.mLeftBeats * minPeriod ||
right_dur < mStretchState.mRightBeats * minPeriod)
return;
pNt->StretchRegion
( mStretchState.mBeatCenter, mStretchState.mBeat1, right_dur );
pNt->StretchRegion
( mStretchState.mBeat0, mStretchState.mBeatCenter, left_dur );
mStretchState.mBeatCenter.first = moveto;
break;
}
default:
wxASSERT(false);
break;
}
MakeParentPushState(_("Stretch Note Track"), _("Stretch"),
UndoPush::CONSOLIDATE | UndoPush::AUTOSAVE);
mStretched = true;
Refresh(false);
}
#endif
@ -2920,7 +2955,7 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
// Abandon this drag if selecting < 5 pixels.
if (wxLongLong(SelStart-x).Abs() < minimumSizedSelection
#ifdef USE_MIDI // limiting selection size is good, and not starting
&& !mStretching // stretch unless mouse moves 5 pixels is good, but
&& !mStretchState.mStretching // stretch unless mouse moves 5 pixels is good, but
#endif // once stretching starts, it's ok to move even 1 pixel
)
return;
@ -2933,7 +2968,7 @@ void TrackPanel::SelectionHandleDrag(wxMouseEvent & event, Track *clickedTrack)
SelectRangeOfTracks(sTrack, eTrack);
#ifdef USE_MIDI
if (mStretching) {
if (mStretchState.mStretching) {
// the following is also in ExtendSelection, called below
// probably a good idea to "hoist" the code to before this "if" stmt
if (clickedTrack == NULL && mCapturedTrack != NULL)
@ -6817,14 +6852,17 @@ int TrackPanel::DetermineToolToUse( ToolsToolBar * pTtb, const wxMouseEvent & ev
#ifdef USE_MIDI
bool TrackPanel::HitTestStretch(Track *track, const wxRect &rect, const wxMouseEvent & event)
auto TrackPanel::HitTestStretch
( const Track *track, const wxRect &rect, const wxMouseEvent & event,
StretchState *pState )
-> StretchEnum
{
// later, we may want a different policy, but for now, stretch is
// selected when the cursor is near the center of the track and
// within the selection
if (!track || !track->GetSelected() || track->GetKind() != Track::Note ||
IsUnsafe()) {
return false;
return stretchNone;
}
int center = rect.y + rect.height / 2;
int distance = abs(event.m_y - center);
@ -6833,8 +6871,14 @@ bool TrackPanel::HitTestStretch(Track *track, const wxRect &rect, const wxMouseE
wxInt64 rightSel = mViewInfo->TimeToPosition(mViewInfo->selectedRegion.t1(), rect.x);
// Something is wrong if right edge comes before left edge
wxASSERT(!(rightSel < leftSel));
return (leftSel <= event.m_x && event.m_x <= rightSel &&
distance < yTolerance);
if (leftSel <= event.m_x && event.m_x <= rightSel &&
distance < yTolerance)
return ChooseStretchMode
( event, rect, *mViewInfo,
static_cast< const NoteTrack * >( track ), pState );
return stretchNone;
}
#endif

View File

@ -320,20 +320,33 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel {
// part shrinks, keeping the leftmost and rightmost boundaries
// fixed.
enum StretchEnum {
stretchNone = 0, // false value!
stretchLeft,
stretchCenter,
stretchRight
};
StretchEnum mStretchMode; // remembers what to drag
bool mStretching; // true between mouse down and mouse up
bool mStretched; // true after drag has pushed state
double mStretchStart; // time of initial mouse position, quantized
// to the nearest beat
double mStretchSel0; // initial sel0 (left) quantized to nearest beat
double mStretchSel1; // initial sel1 (left) quantized to nearest beat
double mStretchLeftBeats; // how many beats from left to cursor
double mStretchRightBeats; // how many beats from cursor to right
virtual bool HitTestStretch(Track *track, const wxRect &rect, const wxMouseEvent & event);
struct StretchState {
StretchEnum mMode { stretchCenter }; // remembers what to drag
using QuantizedTimeAndBeat = std::pair< double, double >;
bool mStretching {}; // true between mouse down and mouse up
double mOrigT0 {};
double mOrigT1 {};
QuantizedTimeAndBeat mBeatCenter { 0, 0 };
QuantizedTimeAndBeat mBeat0 { 0, 0 };
QuantizedTimeAndBeat mBeat1 { 0, 0 };
double mLeftBeats {}; // how many beats from left to cursor
double mRightBeats {}; // how many beats from cursor to right
} mStretchState;
virtual StretchEnum HitTestStretch
( const Track *track, const wxRect &rect, const wxMouseEvent & event,
StretchState *pState = nullptr );
wxCursor *ChooseStretchCursor( StretchEnum mode );
static StretchEnum ChooseStretchMode
( const wxMouseEvent &event, const wxRect &rect, const ViewInfo &viewInfo,
const NoteTrack *nt, StretchState *pState = nullptr );
virtual void Stretch(int mouseXCoordinate, int trackLeftEdge, Track *pTrack);
#endif