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:
parent
9fb7185ea4
commit
90eb4ec142
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
@ -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, ¢erBeat);
|
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, ¢erBeat);
|
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
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user