1
0
mirror of https://github.com/cookiengineer/audacity synced 2025-07-17 09:07:41 +02:00

Make MIDI track stretch path-independent

This commit is contained in:
Paul Licameli 2017-05-11 13:41:26 -04:00
parent 9fb7185ea4
commit 90eb4ec142
4 changed files with 95 additions and 76 deletions

View File

@ -562,29 +562,30 @@ bool NoteTrack::Shift(double t) // t is always seconds
return true; return true;
} }
double NoteTrack::NearestBeatTime(double time, double *beat) const QuantizedTimeAndBeat NoteTrack::NearestBeatTime( double time ) const
{ {
wxASSERT(mSeq); wxASSERT(mSeq);
// Alg_seq knows nothing about offset, so remove offset time // Alg_seq knows nothing about offset, so remove offset time
double seq_time = time - GetOffset(); 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 // 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); bool result = mSeq->stretch_region( t0.second, t1.second, newDur );
// 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);
if (result) { 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->convert_to_seconds();
mSeq->set_dur(mSeq->get_dur() + dur - (t1 - t0)); mSeq->set_dur(mSeq->get_dur() + newDur - oldDur);
#endif
} }
return result; return result;
} }

View File

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

View File

@ -2238,22 +2238,29 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
const auto nt = static_cast<NoteTrack *>(pTrack); const auto nt = static_cast<NoteTrack *>(pTrack);
// find nearest beat to sel0, sel1 // find nearest beat to sel0, sel1
double minPeriod = 0.05; // minimum beat period double minPeriod = 0.05; // minimum beat period
double qBeat0, qBeat1; mStretchState.mBeatCenter = { 0, 0 };
double centerBeat = 0.0f;
mStretchState.mSel0 = nt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); mStretchState.mBeat0 =
mStretchState.mSel1 = nt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1); nt->NearestBeatTime( mViewInfo->selectedRegion.t0() );
mStretchState.mBeat1 =
nt->NearestBeatTime( mViewInfo->selectedRegion.t1() );
// If there is not (almost) a beat to stretch that is slower // If there is not (almost) a beat to stretch that is slower
// than 20 beats per second, don't stretch // than 20 beats per second, don't stretch
if (within(qBeat0, qBeat1, 0.9) || if ( within( mStretchState.mBeat0.second,
(mStretchState.mSel1 - mStretchState.mSel0) / (qBeat1 - qBeat0) < minPeriod) return; 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 if (startNewSelection) { // mouse is not at an edge, but after
// quantization, we could be indicating the selection edge // quantization, we could be indicating the selection edge
mSelStartValid = true; mSelStartValid = true;
mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, rect.x)); mSelStart = std::max(0.0, mViewInfo->PositionToTime(event.m_x, rect.x));
mStretchState.mStart = nt->NearestBeatTime(mSelStart, &centerBeat); mStretchState.mBeatCenter = nt->NearestBeatTime( mSelStart );
if (within(qBeat0, centerBeat, 0.1)) { if ( within( mStretchState.mBeat0.second,
mStretchState.mBeatCenter.second, 0.1 ) ) {
mListener->TP_DisplayStatusMessage( mListener->TP_DisplayStatusMessage(
_("Click and drag to stretch selected region.")); _("Click and drag to stretch selected region."));
SetCursor(*mStretchLeftCursor); SetCursor(*mStretchLeftCursor);
@ -2261,7 +2268,9 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
mSelStart = mViewInfo->selectedRegion.t1(); mSelStart = mViewInfo->selectedRegion.t1();
// condition that implies stretchLeft // condition that implies stretchLeft
startNewSelection = false; startNewSelection = false;
} else if (within(qBeat1, centerBeat, 0.1)) { }
else if ( within( mStretchState.mBeat1.second,
mStretchState.mBeatCenter.second, 0.1 ) ) {
mListener->TP_DisplayStatusMessage( mListener->TP_DisplayStatusMessage(
_("Click and drag to stretch selected region.")); _("Click and drag to stretch selected region."));
SetCursor(*mStretchRightCursor); SetCursor(*mStretchRightCursor);
@ -2274,22 +2283,28 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
if (startNewSelection) { if (startNewSelection) {
mStretchState.mMode = stretchCenter; mStretchState.mMode = stretchCenter;
mStretchState.mLeftBeats = qBeat1 - centerBeat; mStretchState.mLeftBeats =
mStretchState.mRightBeats = centerBeat - qBeat0; mStretchState.mBeat1.second - mStretchState.mBeatCenter.second;
} else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) { mStretchState.mRightBeats =
mStretchState.mBeatCenter.second - mStretchState.mBeat0.second;
}
else if (mSelStartValid && mViewInfo->selectedRegion.t1() == mSelStart) {
// note that at this point, mSelStart is at the opposite // note that at this point, mSelStart is at the opposite
// end of the selection from the cursor. If the cursor is // end of the selection from the cursor. If the cursor is
// over sel0, then mSelStart is at sel1. // over sel0, then mSelStart is at sel1.
mStretchState.mMode = stretchLeft; mStretchState.mMode = stretchLeft;
} else { }
else {
mStretchState.mMode = stretchRight; mStretchState.mMode = stretchRight;
} }
if (mStretchState.mMode == stretchLeft) { if (mStretchState.mMode == stretchLeft) {
mStretchState.mLeftBeats = 0; mStretchState.mLeftBeats = 0;
mStretchState.mRightBeats = qBeat1 - qBeat0; mStretchState.mRightBeats =
mStretchState.mBeat1.second - mStretchState.mBeat0.second;
} else if (mStretchState.mMode == stretchRight) { } else if (mStretchState.mMode == stretchRight) {
mStretchState.mLeftBeats = qBeat1 - qBeat0; mStretchState.mLeftBeats =
mStretchState.mBeat1.second - mStretchState.mBeat0.second;
mStretchState.mRightBeats = 0; mStretchState.mRightBeats = 0;
} }
@ -2297,7 +2312,7 @@ void TrackPanel::SelectionHandleClick(wxMouseEvent & event,
MakeParentModifyState( false ); MakeParentModifyState( false );
mViewInfo->selectedRegion.setTimes mViewInfo->selectedRegion.setTimes
(mStretchState.mSel0, mStretchState.mSel1); ( mStretchState.mBeat0.first, mStretchState.mBeat1.first );
mStretchState.mStretching = true; mStretchState.mStretching = true;
// Full refresh since the label area may need to indicate // Full refresh since the label area may need to indicate
@ -2784,65 +2799,62 @@ void TrackPanel::Stretch(int mouseXCoordinate, int trackLeftEdge,
return; return;
} }
NoteTrack *pNt = (NoteTrack *) pTrack; NoteTrack *pNt = static_cast< NoteTrack * >( pTrack );
double moveto = std::max(0.0, mViewInfo->PositionToTime(mouseXCoordinate, trackLeftEdge));
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 // check to make sure tempo is not higher than 20 beats per second
// (In principle, tempo can be higher, but not infinity.) // (In principle, tempo can be higher, but not infinity.)
double minPeriod = 0.05; // minimum beat period const double minPeriod = 0.05; // minimum beat period
double qBeat0, qBeat1;
pNt->NearestBeatTime(mViewInfo->selectedRegion.t0(), &qBeat0); // get beat
pNt->NearestBeatTime(mViewInfo->selectedRegion.t1(), &qBeat1);
// We could be moving 3 things: left edge, right edge, a point between // make sure target duration is not too short
// Take quick exit if so, without changing the selection.
switch (mStretchState.mMode) { switch (mStretchState.mMode) {
case stretchLeft: { case stretchLeft: {
// make sure target duration is not too short dur = t1 - moveto;
double dur = mViewInfo->selectedRegion.t1() - moveto; if (dur < mStretchState.mRightBeats * minPeriod)
if (dur < mStretchState.mRightBeats * minPeriod) { return;
dur = mStretchState.mRightBeats * minPeriod; pNt->StretchRegion
moveto = mViewInfo->selectedRegion.t1() - dur; ( mStretchState.mBeat0, mStretchState.mBeat1, dur );
} pNt->Offset( moveto - t0 );
if (pNt->StretchRegion(mStretchState.mSel0, mStretchState.mSel1, dur)) { mStretchState.mBeat0.first = moveto;
pNt->SetOffset(pNt->GetOffset() + moveto - mStretchState.mSel0); mViewInfo->selectedRegion.setT0(moveto);
mViewInfo->selectedRegion.setT0(moveto);
}
break; break;
} }
case stretchRight: { case stretchRight: {
// make sure target duration is not too short dur = moveto - t0;
double dur = moveto - mViewInfo->selectedRegion.t0(); if (dur < mStretchState.mLeftBeats * minPeriod)
if (dur < mStretchState.mLeftBeats * minPeriod) { return;
dur = mStretchState.mLeftBeats * minPeriod; pNt->StretchRegion
moveto = mStretchState.mSel0 + dur; ( mStretchState.mBeat0, mStretchState.mBeat1, dur );
} mViewInfo->selectedRegion.setT1(moveto);
if (pNt->StretchRegion(mStretchState.mSel0, mStretchState.mSel1, dur)) { mStretchState.mBeat1.first = moveto;
mViewInfo->selectedRegion.setT1(moveto);
}
break; break;
} }
case stretchCenter: { case stretchCenter: {
// make sure both left and right target durations are not too short left_dur = moveto - t0;
double left_dur = moveto - mViewInfo->selectedRegion.t0(); right_dur = t1 - moveto;
double right_dur = mViewInfo->selectedRegion.t1() - moveto;
double centerBeat; if (left_dur < mStretchState.mLeftBeats * minPeriod ||
pNt->NearestBeatTime(mSelStart, &centerBeat); right_dur < mStretchState.mRightBeats * minPeriod)
if (left_dur < mStretchState.mLeftBeats * minPeriod) { return;
left_dur = mStretchState.mLeftBeats * minPeriod; pNt->StretchRegion
moveto = mStretchState.mSel0 + left_dur; ( mStretchState.mBeatCenter, mStretchState.mBeat1, right_dur );
} pNt->StretchRegion
if (right_dur < mStretchState.mRightBeats * minPeriod) { ( mStretchState.mBeat0, mStretchState.mBeatCenter, left_dur );
right_dur = mStretchState.mRightBeats * minPeriod; mStretchState.mBeatCenter.first = moveto;
moveto = mStretchState.mSel1 - right_dur;
}
pNt->StretchRegion(mStretchState.mStart, mStretchState.mSel1, right_dur);
pNt->StretchRegion(mStretchState.mSel0, mStretchState.mStart, left_dur);
break; break;
} }
default: default:
wxASSERT(false); wxASSERT(false);
break; break;
} }
Refresh(false); Refresh(false);
} }
#endif #endif

View File

@ -326,11 +326,13 @@ class AUDACITY_DLL_API TrackPanel final : public OverlayPanel {
}; };
struct StretchState { struct StretchState {
StretchEnum mMode { stretchCenter }; // remembers what to drag StretchEnum mMode { stretchCenter }; // remembers what to drag
using QuantizedTimeAndBeat = std::pair< double, double >;
bool mStretching {}; // true between mouse down and mouse up bool mStretching {}; // true between mouse down and mouse up
double mStart {}; // time of initial mouse position, quantized QuantizedTimeAndBeat mBeatCenter { 0, 0 };
// to the nearest beat QuantizedTimeAndBeat mBeat0 { 0, 0 };
double mSel0 {}; // initial sel0 (left) quantized to nearest beat QuantizedTimeAndBeat mBeat1 { 0, 0 };
double mSel1 {}; // initial sel1 (left) quantized to nearest beat
double mLeftBeats {}; // how many beats from left to cursor double mLeftBeats {}; // how many beats from left to cursor
double mRightBeats {}; // how many beats from cursor to right double mRightBeats {}; // how many beats from cursor to right
} mStretchState; } mStretchState;