mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-06 23:02:42 +02:00
1319 lines
33 KiB
C++
1319 lines
33 KiB
C++
/**********************************************************************
|
|
|
|
Audacity: A Digital Audio Editor
|
|
|
|
Track.cpp
|
|
|
|
Dominic Mazzoni
|
|
|
|
*******************************************************************//**
|
|
|
|
\class Track
|
|
\brief Fundamental data object of Audacity, placed in the TrackPanel.
|
|
Classes derived form it include the WaveTrack, NoteTrack, LabelTrack
|
|
and TimeTrack.
|
|
|
|
\class AudioTrack
|
|
\brief A Track that can load/save audio data to/from XML.
|
|
|
|
\class PlayableTrack
|
|
\brief An AudioTrack that can be played and stopped.
|
|
|
|
*//*******************************************************************/
|
|
|
|
#include "Audacity.h" // for USE_* macros
|
|
|
|
#include "Track.h"
|
|
|
|
#include "Experimental.h"
|
|
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
|
|
#include <float.h>
|
|
#include <wx/file.h>
|
|
#include <wx/textfile.h>
|
|
#include <wx/log.h>
|
|
|
|
#include "Project.h"
|
|
#include "ProjectSettings.h"
|
|
#include "DirManager.h"
|
|
|
|
#include "InconsistencyException.h"
|
|
|
|
#ifdef _MSC_VER
|
|
//Disable truncation warnings
|
|
#pragma warning( disable : 4786 )
|
|
#endif
|
|
|
|
Track::Track(const std::shared_ptr<DirManager> &projDirManager)
|
|
: vrulerSize(36,0),
|
|
mDirManager(projDirManager)
|
|
{
|
|
mSelected = false;
|
|
mLinked = false;
|
|
|
|
mIndex = 0;
|
|
|
|
mOffset = 0.0;
|
|
|
|
mChannel = MonoChannel;
|
|
}
|
|
|
|
Track::Track(const Track &orig)
|
|
: vrulerSize( orig.vrulerSize )
|
|
{
|
|
mIndex = 0;
|
|
Init(orig);
|
|
mOffset = orig.mOffset;
|
|
}
|
|
|
|
// Copy all the track properties except the actual contents
|
|
void Track::Init(const Track &orig)
|
|
{
|
|
mId = orig.mId;
|
|
|
|
mDefaultName = orig.mDefaultName;
|
|
mName = orig.mName;
|
|
|
|
mDirManager = orig.mDirManager;
|
|
|
|
mSelected = orig.mSelected;
|
|
mLinked = orig.mLinked;
|
|
mChannel = orig.mChannel;
|
|
}
|
|
|
|
void Track::SetName( const wxString &n )
|
|
{
|
|
if ( mName != n ) {
|
|
mName = n;
|
|
Notify();
|
|
}
|
|
}
|
|
|
|
void Track::SetSelected(bool s)
|
|
{
|
|
if (mSelected != s) {
|
|
mSelected = s;
|
|
auto pList = mList.lock();
|
|
if (pList)
|
|
pList->SelectionEvent( SharedPointer() );
|
|
}
|
|
}
|
|
|
|
void Track::EnsureVisible( bool modifyState )
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList)
|
|
pList->EnsureVisibleEvent( SharedPointer(), modifyState );
|
|
}
|
|
|
|
void Track::Merge(const Track &orig)
|
|
{
|
|
mSelected = orig.mSelected;
|
|
}
|
|
|
|
Track::Holder Track::Duplicate() const
|
|
{
|
|
// invoke "virtual constructor" to copy track object proper:
|
|
auto result = Clone();
|
|
|
|
if (mpView)
|
|
// Copy view state that might be important to undo/redo
|
|
mpView->CopyTo( *result );
|
|
|
|
return result;
|
|
}
|
|
|
|
Track::~Track()
|
|
{
|
|
}
|
|
|
|
|
|
TrackNodePointer Track::GetNode() const
|
|
{
|
|
wxASSERT(mList.lock() == NULL || this == mNode.first->get());
|
|
return mNode;
|
|
}
|
|
|
|
void Track::SetOwner
|
|
(const std::weak_ptr<TrackList> &list, TrackNodePointer node)
|
|
{
|
|
// BUG: When using this function to clear an owner, we may need to clear
|
|
// focussed track too. Otherwise focus could remain on an invisible (or deleted) track.
|
|
mList = list;
|
|
mNode = node;
|
|
}
|
|
|
|
const std::shared_ptr<CommonTrackCell> &Track::GetTrackView()
|
|
{
|
|
return mpView;
|
|
}
|
|
|
|
void Track::SetTrackView( const std::shared_ptr<CommonTrackCell> &pView )
|
|
{
|
|
mpView = pView;
|
|
}
|
|
|
|
const std::shared_ptr<CommonTrackCell> &Track::GetTrackControls()
|
|
{
|
|
return mpControls;
|
|
}
|
|
|
|
void Track::SetTrackControls( const std::shared_ptr<CommonTrackCell> &pControls )
|
|
{
|
|
mpControls = pControls;
|
|
}
|
|
|
|
int Track::GetIndex() const
|
|
{
|
|
return mIndex;
|
|
}
|
|
|
|
void Track::SetIndex(int index)
|
|
{
|
|
mIndex = index;
|
|
}
|
|
|
|
void Track::SetLinked(bool l)
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList && !pList->mPendingUpdates.empty()) {
|
|
auto orig = pList->FindById( GetId() );
|
|
if (orig && orig != this) {
|
|
orig->SetLinked(l);
|
|
return;
|
|
}
|
|
}
|
|
|
|
DoSetLinked(l);
|
|
|
|
if (pList) {
|
|
pList->RecalcPositions(mNode);
|
|
pList->ResizingEvent(mNode);
|
|
}
|
|
}
|
|
|
|
void Track::DoSetLinked(bool l)
|
|
{
|
|
mLinked = l;
|
|
}
|
|
|
|
Track *Track::GetLink() const
|
|
{
|
|
auto pList = mList.lock();
|
|
if (!pList)
|
|
return nullptr;
|
|
|
|
if (!pList->isNull(mNode)) {
|
|
if (mLinked) {
|
|
auto next = pList->getNext( mNode );
|
|
if ( !pList->isNull( next ) )
|
|
return next.first->get();
|
|
}
|
|
|
|
if (mNode.first != mNode.second->begin()) {
|
|
auto prev = pList->getPrev( mNode );
|
|
if ( !pList->isNull( prev ) ) {
|
|
auto track = prev.first->get();
|
|
if (track && track->GetLinked())
|
|
return track;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
namespace {
|
|
inline bool IsSyncLockableNonLabelTrack( const Track *pTrack )
|
|
{
|
|
return nullptr != track_cast< const AudioTrack * >( pTrack );
|
|
}
|
|
|
|
bool IsGoodNextSyncLockTrack(const Track *t, bool inLabelSection)
|
|
{
|
|
if (!t)
|
|
return false;
|
|
const bool isLabel = ( nullptr != track_cast<const LabelTrack*>(t) );
|
|
if (inLabelSection)
|
|
return isLabel;
|
|
else if (isLabel)
|
|
return true;
|
|
else
|
|
return IsSyncLockableNonLabelTrack( t );
|
|
}
|
|
}
|
|
|
|
bool Track::IsSyncLockSelected() const
|
|
{
|
|
#ifdef EXPERIMENTAL_SYNC_LOCK
|
|
auto pList = mList.lock();
|
|
if (!pList)
|
|
return false;
|
|
|
|
auto p = pList->GetOwner();
|
|
if (!p || !ProjectSettings::Get( *p ).IsSyncLocked())
|
|
return false;
|
|
|
|
auto shTrack = this->SubstituteOriginalTrack();
|
|
if (!shTrack)
|
|
return false;
|
|
|
|
const auto pTrack = shTrack.get();
|
|
auto trackRange = TrackList::SyncLockGroup( pTrack );
|
|
|
|
if (trackRange.size() <= 1) {
|
|
// Not in a sync-locked group.
|
|
// Return true iff selected and of a sync-lockable type.
|
|
return (IsSyncLockableNonLabelTrack( pTrack ) ||
|
|
track_cast<const LabelTrack*>( pTrack )) && GetSelected();
|
|
}
|
|
|
|
// Return true iff any track in the group is selected.
|
|
return *(trackRange + &Track::IsSelected).begin();
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
void Track::Notify( int code )
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList)
|
|
pList->DataEvent( SharedPointer(), code );
|
|
}
|
|
|
|
void Track::SyncLockAdjust(double oldT1, double newT1)
|
|
{
|
|
if (newT1 > oldT1) {
|
|
// Insert space within the track
|
|
|
|
if (oldT1 > GetEndTime())
|
|
return;
|
|
|
|
auto tmp = Cut(oldT1, GetEndTime());
|
|
|
|
Paste(newT1, tmp.get());
|
|
}
|
|
else if (newT1 < oldT1) {
|
|
// Remove from the track
|
|
Clear(newT1, oldT1);
|
|
}
|
|
}
|
|
|
|
void PlayableTrack::Init( const PlayableTrack &orig )
|
|
{
|
|
mMute = orig.mMute;
|
|
mSolo = orig.mSolo;
|
|
AudioTrack::Init( orig );
|
|
}
|
|
|
|
void PlayableTrack::Merge( const Track &orig )
|
|
{
|
|
auto pOrig = dynamic_cast<const PlayableTrack *>(&orig);
|
|
wxASSERT( pOrig );
|
|
mMute = pOrig->mMute;
|
|
mSolo = pOrig->mSolo;
|
|
AudioTrack::Merge( *pOrig );
|
|
}
|
|
|
|
void PlayableTrack::SetMute( bool m )
|
|
{
|
|
if ( mMute != m ) {
|
|
mMute = m;
|
|
Notify();
|
|
}
|
|
}
|
|
|
|
void PlayableTrack::SetSolo( bool s )
|
|
{
|
|
if ( mSolo != s ) {
|
|
mSolo = s;
|
|
Notify();
|
|
}
|
|
}
|
|
|
|
// Serialize, not with tags of its own, but as attributes within a tag.
|
|
void PlayableTrack::WriteXMLAttributes(XMLWriter &xmlFile) const
|
|
{
|
|
xmlFile.WriteAttr(wxT("mute"), mMute);
|
|
xmlFile.WriteAttr(wxT("solo"), mSolo);
|
|
AudioTrack::WriteXMLAttributes(xmlFile);
|
|
}
|
|
|
|
// Return true iff the attribute is recognized.
|
|
bool PlayableTrack::HandleXMLAttribute(const wxChar *attr, const wxChar *value)
|
|
{
|
|
const wxString strValue{ value };
|
|
long nValue;
|
|
if (!wxStrcmp(attr, wxT("mute")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue)) {
|
|
mMute = (nValue != 0);
|
|
return true;
|
|
}
|
|
else if (!wxStrcmp(attr, wxT("solo")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue)) {
|
|
mSolo = (nValue != 0);
|
|
return true;
|
|
}
|
|
|
|
return AudioTrack::HandleXMLAttribute(attr, value);
|
|
}
|
|
|
|
bool Track::Any() const
|
|
{ return true; }
|
|
|
|
bool Track::IsSelected() const
|
|
{ return GetSelected(); }
|
|
|
|
bool Track::IsSelectedOrSyncLockSelected() const
|
|
{ return GetSelected() || IsSyncLockSelected(); }
|
|
|
|
bool Track::IsLeader() const
|
|
{ return !GetLink() || GetLinked(); }
|
|
|
|
bool Track::IsSelectedLeader() const
|
|
{ return IsSelected() && IsLeader(); }
|
|
|
|
void Track::FinishCopy
|
|
(const Track *n, Track *dest)
|
|
{
|
|
if (dest) {
|
|
dest->SetChannel(n->GetChannel());
|
|
dest->SetLinked(n->GetLinked());
|
|
dest->SetName(n->GetName());
|
|
}
|
|
}
|
|
|
|
bool Track::LinkConsistencyCheck()
|
|
{
|
|
// Sanity checks for linked tracks; unsetting the linked property
|
|
// doesn't fix the problem, but it likely leaves us with orphaned
|
|
// blockfiles instead of much worse problems.
|
|
bool err = false;
|
|
if (GetLinked())
|
|
{
|
|
Track *l = GetLink();
|
|
if (l)
|
|
{
|
|
// A linked track's partner should never itself be linked
|
|
if (l->GetLinked())
|
|
{
|
|
wxLogWarning(
|
|
wxT("Left track %s had linked right track %s with extra right track link.\n Removing extra link from right track."),
|
|
GetName(), l->GetName());
|
|
err = true;
|
|
l->SetLinked(false);
|
|
}
|
|
|
|
// Channels should be left and right
|
|
if ( !( (GetChannel() == Track::LeftChannel &&
|
|
l->GetChannel() == Track::RightChannel) ||
|
|
(GetChannel() == Track::RightChannel &&
|
|
l->GetChannel() == Track::LeftChannel) ) )
|
|
{
|
|
wxLogWarning(
|
|
wxT("Track %s and %s had left/right track links out of order. Setting tracks to not be linked."),
|
|
GetName(), l->GetName());
|
|
err = true;
|
|
SetLinked(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wxLogWarning(
|
|
wxT("Track %s had link to NULL track. Setting it to not be linked."),
|
|
GetName());
|
|
err = true;
|
|
SetLinked(false);
|
|
}
|
|
}
|
|
|
|
return ! err;
|
|
}
|
|
|
|
std::pair<Track *, Track *> TrackList::FindSyncLockGroup(Track *pMember) const
|
|
{
|
|
if (!pMember)
|
|
return { nullptr, nullptr };
|
|
|
|
// A non-trivial sync-locked group is a maximal sub-sequence of the tracks
|
|
// consisting of any positive number of audio tracks followed by zero or
|
|
// more label tracks.
|
|
|
|
// Step back through any label tracks.
|
|
auto member = pMember;
|
|
while (member && ( nullptr != track_cast<const LabelTrack*>(member) )) {
|
|
member = GetPrev(member);
|
|
}
|
|
|
|
// Step back through the wave and note tracks before the label tracks.
|
|
Track *first = nullptr;
|
|
while (member && IsSyncLockableNonLabelTrack(member)) {
|
|
first = member;
|
|
member = GetPrev(member);
|
|
}
|
|
|
|
if (!first)
|
|
// Can't meet the criteria described above. In that case,
|
|
// consider the track to be the sole member of a group.
|
|
return { pMember, pMember };
|
|
|
|
Track *last = first;
|
|
bool inLabels = false;
|
|
|
|
while (const auto next = GetNext(last)) {
|
|
if ( ! IsGoodNextSyncLockTrack(next, inLabels) )
|
|
break;
|
|
last = next;
|
|
inLabels = (nullptr != track_cast<const LabelTrack*>(last) );
|
|
}
|
|
|
|
return { first, last };
|
|
}
|
|
|
|
|
|
// TrackList
|
|
//
|
|
// The TrackList sends events whenever certain updates occur to the list it
|
|
// is managing. Any other classes that may be interested in get these updates
|
|
// should use TrackList::Connect() or TrackList::Bind().
|
|
//
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_TRACK_DATA_CHANGE, TrackListEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_SELECTION_CHANGE, TrackListEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_TRACK_REQUEST_VISIBLE, TrackListEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_PERMUTED, TrackListEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_RESIZING, TrackListEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_ADDITION, TrackListEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_DELETION, TrackListEvent);
|
|
|
|
// same value as in the default constructed TrackId:
|
|
long TrackList::sCounter = -1;
|
|
|
|
static const AudacityProject::AttachedObjects::RegisteredFactory key{
|
|
[](AudacityProject &project) { return TrackList::Create( &project ); }
|
|
};
|
|
|
|
TrackList &TrackList::Get( AudacityProject &project )
|
|
{
|
|
return project.AttachedObjects::Get< TrackList >( key );
|
|
}
|
|
|
|
const TrackList &TrackList::Get( const AudacityProject &project )
|
|
{
|
|
return Get( const_cast< AudacityProject & >( project ) );
|
|
}
|
|
|
|
TrackList::TrackList( AudacityProject *pOwner )
|
|
: wxEvtHandler()
|
|
, mOwner{ pOwner }
|
|
{
|
|
}
|
|
|
|
// Factory function
|
|
std::shared_ptr<TrackList> TrackList::Create( AudacityProject *pOwner )
|
|
{
|
|
return std::make_shared<TrackList>( pOwner );
|
|
}
|
|
|
|
#if 0
|
|
TrackList &TrackList::operator= (TrackList &&that)
|
|
{
|
|
if (this != &that) {
|
|
this->Clear();
|
|
Swap(that);
|
|
}
|
|
return *this;
|
|
}
|
|
#endif
|
|
|
|
void TrackList::Swap(TrackList &that)
|
|
{
|
|
auto SwapLOTs = [](
|
|
ListOfTracks &a, const std::weak_ptr< TrackList > &aSelf,
|
|
ListOfTracks &b, const std::weak_ptr< TrackList > &bSelf )
|
|
{
|
|
a.swap(b);
|
|
for (auto it = a.begin(), last = a.end(); it != last; ++it)
|
|
(*it)->SetOwner(aSelf, {it, &a});
|
|
for (auto it = b.begin(), last = b.end(); it != last; ++it)
|
|
(*it)->SetOwner(bSelf, {it, &b});
|
|
};
|
|
|
|
const auto self = shared_from_this();
|
|
const auto otherSelf = that.shared_from_this();
|
|
SwapLOTs( *this, self, that, otherSelf );
|
|
SwapLOTs( this->mPendingUpdates, self, that.mPendingUpdates, otherSelf );
|
|
mUpdaters.swap(that.mUpdaters);
|
|
}
|
|
|
|
TrackList::~TrackList()
|
|
{
|
|
Clear(false);
|
|
}
|
|
|
|
void TrackList::RecalcPositions(TrackNodePointer node)
|
|
{
|
|
if ( isNull( node ) )
|
|
return;
|
|
|
|
Track *t;
|
|
int i = 0;
|
|
|
|
auto prev = getPrev( node );
|
|
if ( !isNull( prev ) ) {
|
|
t = prev.first->get();
|
|
i = t->GetIndex() + 1;
|
|
}
|
|
|
|
const auto theEnd = end();
|
|
for (auto n = Find( node.first->get() ); n != theEnd; ++n) {
|
|
t = *n;
|
|
t->SetIndex(i++);
|
|
}
|
|
|
|
UpdatePendingTracks();
|
|
}
|
|
|
|
void TrackList::SelectionEvent( const std::shared_ptr<Track> &pTrack )
|
|
{
|
|
// wxWidgets will own the event object
|
|
QueueEvent(
|
|
safenew TrackListEvent{ EVT_TRACKLIST_SELECTION_CHANGE, pTrack } );
|
|
}
|
|
|
|
void TrackList::DataEvent( const std::shared_ptr<Track> &pTrack, int code )
|
|
{
|
|
// wxWidgets will own the event object
|
|
QueueEvent(
|
|
safenew TrackListEvent{ EVT_TRACKLIST_TRACK_DATA_CHANGE, pTrack, code } );
|
|
}
|
|
|
|
void TrackList::EnsureVisibleEvent(
|
|
const std::shared_ptr<Track> &pTrack, bool modifyState )
|
|
{
|
|
auto pEvent = std::make_unique<TrackListEvent>(
|
|
EVT_TRACKLIST_TRACK_REQUEST_VISIBLE, pTrack, 0 );
|
|
pEvent->SetInt( modifyState ? 1 : 0 );
|
|
// wxWidgets will own the event object
|
|
QueueEvent( pEvent.release() );
|
|
}
|
|
|
|
void TrackList::PermutationEvent(TrackNodePointer node)
|
|
{
|
|
// wxWidgets will own the event object
|
|
QueueEvent( safenew TrackListEvent{ EVT_TRACKLIST_PERMUTED, *node.first } );
|
|
}
|
|
|
|
void TrackList::DeletionEvent(TrackNodePointer node)
|
|
{
|
|
// wxWidgets will own the event object
|
|
QueueEvent( safenew TrackListEvent{
|
|
EVT_TRACKLIST_DELETION,
|
|
node.second && node.first != node.second->end()
|
|
? *node.first
|
|
: nullptr
|
|
} );
|
|
}
|
|
|
|
void TrackList::AdditionEvent(TrackNodePointer node)
|
|
{
|
|
// wxWidgets will own the event object
|
|
QueueEvent( safenew TrackListEvent{ EVT_TRACKLIST_ADDITION, *node.first } );
|
|
}
|
|
|
|
void TrackList::ResizingEvent(TrackNodePointer node)
|
|
{
|
|
// wxWidgets will own the event object
|
|
QueueEvent( safenew TrackListEvent{ EVT_TRACKLIST_RESIZING, *node.first } );
|
|
}
|
|
|
|
auto TrackList::EmptyRange() const
|
|
-> TrackIterRange< Track >
|
|
{
|
|
auto it = const_cast<TrackList*>(this)->getEnd();
|
|
return {
|
|
{ it, it, it, &Track::Any },
|
|
{ it, it, it, &Track::Any }
|
|
};
|
|
}
|
|
|
|
auto TrackList::SyncLockGroup( Track *pTrack )
|
|
-> TrackIterRange< Track >
|
|
{
|
|
auto pList = pTrack->GetOwner();
|
|
auto tracks =
|
|
pList->FindSyncLockGroup( const_cast<Track*>( pTrack ) );
|
|
return pList->Any().StartingWith(tracks.first).EndingAfter(tracks.second);
|
|
}
|
|
|
|
auto TrackList::FindLeader( Track *pTrack )
|
|
-> TrackIter< Track >
|
|
{
|
|
auto iter = Find(pTrack);
|
|
while( *iter && ! ( *iter )->IsLeader() )
|
|
--iter;
|
|
return iter.Filter( &Track::IsLeader );
|
|
}
|
|
|
|
void TrackList::Permute(const std::vector<TrackNodePointer> &permutation)
|
|
{
|
|
for (const auto iter : permutation) {
|
|
ListOfTracks::value_type track = *iter.first;
|
|
erase(iter.first);
|
|
Track *pTrack = track.get();
|
|
pTrack->SetOwner(shared_from_this(),
|
|
{ insert(ListOfTracks::end(), track), this });
|
|
}
|
|
auto n = getBegin();
|
|
RecalcPositions(n);
|
|
PermutationEvent(n);
|
|
}
|
|
|
|
Track *TrackList::FindById( TrackId id )
|
|
{
|
|
// Linear search. Tracks in a project are usually very few.
|
|
// Search only the non-pending tracks.
|
|
auto it = std::find_if( ListOfTracks::begin(), ListOfTracks::end(),
|
|
[=](const ListOfTracks::value_type &ptr){ return ptr->GetId() == id; } );
|
|
if (it == ListOfTracks::end())
|
|
return {};
|
|
return it->get();
|
|
}
|
|
|
|
Track *TrackList::DoAddToHead(const std::shared_ptr<Track> &t)
|
|
{
|
|
Track *pTrack = t.get();
|
|
push_front(ListOfTracks::value_type(t));
|
|
auto n = getBegin();
|
|
pTrack->SetOwner(shared_from_this(), n);
|
|
pTrack->SetId( TrackId{ ++sCounter } );
|
|
RecalcPositions(n);
|
|
AdditionEvent(n);
|
|
return front().get();
|
|
}
|
|
|
|
Track *TrackList::DoAdd(const std::shared_ptr<Track> &t)
|
|
{
|
|
push_back(t);
|
|
|
|
auto n = getPrev( getEnd() );
|
|
|
|
t->SetOwner(shared_from_this(), n);
|
|
t->SetId( TrackId{ ++sCounter } );
|
|
RecalcPositions(n);
|
|
AdditionEvent(n);
|
|
return back().get();
|
|
}
|
|
|
|
void TrackList::GroupChannels(
|
|
Track &track, size_t groupSize, bool resetChannels )
|
|
{
|
|
// If group size is exactly two, group as stereo, else mono (bug 2195).
|
|
auto list = track.mList.lock();
|
|
if ( groupSize > 0 && list.get() == this ) {
|
|
auto iter = track.mNode.first;
|
|
auto after = iter;
|
|
auto end = this->ListOfTracks::end();
|
|
auto count = groupSize;
|
|
for ( ; after != end && count; ++after, --count )
|
|
;
|
|
if ( count == 0 ) {
|
|
auto unlink = [&] ( Track &tr ) {
|
|
if ( tr.GetLinked() ) {
|
|
if ( resetChannels ) {
|
|
auto link = tr.GetLink();
|
|
if ( link )
|
|
link->SetChannel( Track::MonoChannel );
|
|
}
|
|
tr.SetLinked( false );
|
|
}
|
|
if ( resetChannels )
|
|
tr.SetChannel( Track::MonoChannel );
|
|
};
|
|
|
|
// Disassociate previous tracks -- at most one
|
|
auto pLeader = this->FindLeader( &track );
|
|
if ( *pLeader && *pLeader != &track )
|
|
unlink( **pLeader );
|
|
|
|
// First disassociate given and later tracks, then reassociate them
|
|
for ( auto iter2 = iter; iter2 != after; ++iter2 )
|
|
unlink( **iter2 );
|
|
|
|
if ( groupSize > 1 ) {
|
|
const auto channel = *iter++;
|
|
channel->SetLinked( groupSize == 2 );
|
|
channel->SetChannel( groupSize == 2? Track::LeftChannel : Track::MonoChannel );
|
|
(*iter++)->SetChannel( groupSize == 2? Track::RightChannel : Track::MonoChannel );
|
|
while (iter != after)
|
|
(*iter++)->SetChannel( Track::MonoChannel );
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// *this does not contain the track or sufficient following channels
|
|
// or group size is zero
|
|
THROW_INCONSISTENCY_EXCEPTION;
|
|
}
|
|
|
|
auto TrackList::Replace(Track * t, const ListOfTracks::value_type &with) ->
|
|
ListOfTracks::value_type
|
|
{
|
|
ListOfTracks::value_type holder;
|
|
if (t && with) {
|
|
auto node = t->GetNode();
|
|
t->SetOwner({}, {});
|
|
|
|
holder = *node.first;
|
|
|
|
Track *pTrack = with.get();
|
|
*node.first = with;
|
|
pTrack->SetOwner(shared_from_this(), node);
|
|
pTrack->SetId( t->GetId() );
|
|
RecalcPositions(node);
|
|
|
|
DeletionEvent(node);
|
|
AdditionEvent(node);
|
|
}
|
|
return holder;
|
|
}
|
|
|
|
TrackNodePointer TrackList::Remove(Track *t)
|
|
{
|
|
auto result = getEnd();
|
|
if (t) {
|
|
auto node = t->GetNode();
|
|
t->SetOwner({}, {});
|
|
|
|
if ( !isNull( node ) ) {
|
|
ListOfTracks::value_type holder = *node.first;
|
|
|
|
result = getNext( node );
|
|
erase(node.first);
|
|
if ( !isNull( result ) )
|
|
RecalcPositions(result);
|
|
|
|
DeletionEvent(result);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void TrackList::Clear(bool sendEvent)
|
|
{
|
|
// Null out the back-pointers in tracks, in case there are outstanding
|
|
// shared_ptrs to those tracks.
|
|
for ( auto pTrack: *this )
|
|
pTrack->SetOwner( {}, {} );
|
|
for ( auto pTrack: mPendingUpdates )
|
|
pTrack->SetOwner( {}, {} );
|
|
|
|
ListOfTracks tempList;
|
|
tempList.swap( *this );
|
|
|
|
ListOfTracks updating;
|
|
updating.swap( mPendingUpdates );
|
|
|
|
mUpdaters.clear();
|
|
|
|
if (sendEvent)
|
|
DeletionEvent();
|
|
}
|
|
|
|
/// Return a track in the list that comes after Track t
|
|
Track *TrackList::GetNext(Track * t, bool linked) const
|
|
{
|
|
if (t) {
|
|
auto node = t->GetNode();
|
|
if ( !isNull( node ) ) {
|
|
if ( linked && t->GetLinked() )
|
|
node = getNext( node );
|
|
|
|
if ( !isNull( node ) )
|
|
node = getNext( node );
|
|
|
|
if ( !isNull( node ) )
|
|
return node.first->get();
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Track *TrackList::GetPrev(Track * t, bool linked) const
|
|
{
|
|
if (t) {
|
|
TrackNodePointer prev;
|
|
auto node = t->GetNode();
|
|
if ( !isNull( node ) ) {
|
|
// linked is true and input track second in team?
|
|
if (linked) {
|
|
prev = getPrev( node );
|
|
if( !isNull( prev ) &&
|
|
!t->GetLinked() && t->GetLink() )
|
|
// Make it the first
|
|
node = prev;
|
|
}
|
|
|
|
prev = getPrev( node );
|
|
if ( !isNull( prev ) ) {
|
|
// Back up once
|
|
node = prev;
|
|
|
|
// Back up twice sometimes when linked is true
|
|
if (linked) {
|
|
prev = getPrev( node );
|
|
if( !isNull( prev ) &&
|
|
!(*node.first)->GetLinked() && (*node.first)->GetLink() )
|
|
node = prev;
|
|
}
|
|
|
|
return node.first->get();
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool TrackList::CanMoveUp(Track * t) const
|
|
{
|
|
return GetPrev(t, true) != NULL;
|
|
}
|
|
|
|
bool TrackList::CanMoveDown(Track * t) const
|
|
{
|
|
return GetNext(t, true) != NULL;
|
|
}
|
|
|
|
// This is used when you want to swap the channel group starting
|
|
// at s1 with that starting at s2.
|
|
// The complication is that the tracks are stored in a single
|
|
// linked list.
|
|
void TrackList::SwapNodes(TrackNodePointer s1, TrackNodePointer s2)
|
|
{
|
|
// if a null pointer is passed in, we want to know about it
|
|
wxASSERT(!isNull(s1));
|
|
wxASSERT(!isNull(s2));
|
|
|
|
// Deal with first track in each team
|
|
s1 = ( * FindLeader( s1.first->get() ) )->GetNode();
|
|
s2 = ( * FindLeader( s2.first->get() ) )->GetNode();
|
|
|
|
// Safety check...
|
|
if (s1 == s2)
|
|
return;
|
|
|
|
// Be sure s1 is the earlier iterator
|
|
if ((*s1.first)->GetIndex() >= (*s2.first)->GetIndex())
|
|
std::swap(s1, s2);
|
|
|
|
// For saving the removed tracks
|
|
using Saved = std::vector< ListOfTracks::value_type >;
|
|
Saved saved1, saved2;
|
|
|
|
auto doSave = [&] ( Saved &saved, TrackNodePointer &s ) {
|
|
size_t nn = Channels( s.first->get() ).size();
|
|
saved.resize( nn );
|
|
// Save them in backwards order
|
|
while( nn-- )
|
|
saved[nn] = *s.first, s.first = erase(s.first);
|
|
};
|
|
|
|
doSave( saved1, s1 );
|
|
// The two ranges are assumed to be disjoint but might abut
|
|
const bool same = (s1 == s2);
|
|
doSave( saved2, s2 );
|
|
if (same)
|
|
// Careful, we invalidated s1 in the second doSave!
|
|
s1 = s2;
|
|
|
|
// Reinsert them
|
|
auto doInsert = [&] ( Saved &saved, TrackNodePointer &s ) {
|
|
Track *pTrack;
|
|
for (auto & pointer : saved)
|
|
pTrack = pointer.get(),
|
|
// Insert before s, and reassign s to point at the new node before
|
|
// old s; which is why we saved pointers in backwards order
|
|
pTrack->SetOwner(shared_from_this(),
|
|
s = { insert(s.first, pointer), this } );
|
|
};
|
|
// This does not invalidate s2 even when it equals s1:
|
|
doInsert( saved2, s1 );
|
|
// Even if s2 was same as s1, this correctly inserts the saved1 range
|
|
// after the saved2 range, when done after:
|
|
doInsert( saved1, s2 );
|
|
|
|
// Now correct the Index in the tracks, and other things
|
|
RecalcPositions(s1);
|
|
PermutationEvent(s1);
|
|
}
|
|
|
|
bool TrackList::MoveUp(Track * t)
|
|
{
|
|
if (t) {
|
|
Track *p = GetPrev(t, true);
|
|
if (p) {
|
|
SwapNodes(p->GetNode(), t->GetNode());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TrackList::MoveDown(Track * t)
|
|
{
|
|
if (t) {
|
|
Track *n = GetNext(t, true);
|
|
if (n) {
|
|
SwapNodes(t->GetNode(), n->GetNode());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool TrackList::Contains(const Track * t) const
|
|
{
|
|
return make_iterator_range( *this ).contains( t );
|
|
}
|
|
|
|
bool TrackList::empty() const
|
|
{
|
|
return begin() == end();
|
|
}
|
|
|
|
size_t TrackList::size() const
|
|
{
|
|
int cnt = 0;
|
|
|
|
if (!empty())
|
|
cnt = getPrev( getEnd() ).first->get()->GetIndex() + 1;
|
|
|
|
return cnt;
|
|
}
|
|
|
|
namespace {
|
|
// Abstract the common pattern of the following three member functions
|
|
inline double Accumulate
|
|
(const TrackList &list,
|
|
double (Track::*memfn)() const,
|
|
double ident,
|
|
const double &(*combine)(const double&, const double&))
|
|
{
|
|
// Default the answer to zero for empty list
|
|
if (list.empty()) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Otherwise accumulate minimum or maximum of track values
|
|
return list.Any().accumulate(ident, combine, memfn);
|
|
}
|
|
}
|
|
|
|
double TrackList::GetMinOffset() const
|
|
{
|
|
return Accumulate(*this, &Track::GetOffset, DBL_MAX, std::min);
|
|
}
|
|
|
|
double TrackList::GetStartTime() const
|
|
{
|
|
return Accumulate(*this, &Track::GetStartTime, DBL_MAX, std::min);
|
|
}
|
|
|
|
double TrackList::GetEndTime() const
|
|
{
|
|
return Accumulate(*this, &Track::GetEndTime, -DBL_MAX, std::max);
|
|
}
|
|
|
|
std::shared_ptr<Track>
|
|
TrackList::RegisterPendingChangedTrack( Updater updater, Track *src )
|
|
{
|
|
std::shared_ptr<Track> pTrack;
|
|
if (src) {
|
|
pTrack = src->Clone(); // not duplicate
|
|
// Share the satellites with the original, though they do not point back
|
|
// to the pending track
|
|
pTrack->mpView = src->mpView;
|
|
pTrack->mpControls = src->mpControls;
|
|
}
|
|
|
|
if (pTrack) {
|
|
mUpdaters.push_back( updater );
|
|
mPendingUpdates.push_back( pTrack );
|
|
auto n = mPendingUpdates.end();
|
|
--n;
|
|
pTrack->SetOwner(shared_from_this(), {n, &mPendingUpdates});
|
|
}
|
|
|
|
return pTrack;
|
|
}
|
|
|
|
void TrackList::RegisterPendingNewTrack( const std::shared_ptr<Track> &pTrack )
|
|
{
|
|
Add<Track>( pTrack );
|
|
pTrack->SetId( TrackId{} );
|
|
}
|
|
|
|
void TrackList::UpdatePendingTracks()
|
|
{
|
|
auto pUpdater = mUpdaters.begin();
|
|
for (const auto &pendingTrack : mPendingUpdates) {
|
|
// Copy just a part of the track state, according to the update
|
|
// function
|
|
const auto &updater = *pUpdater;
|
|
auto src = FindById( pendingTrack->GetId() );
|
|
if (pendingTrack && src) {
|
|
if (updater)
|
|
updater( *pendingTrack, *src );
|
|
pendingTrack->DoSetLinked(src->GetLinked());
|
|
}
|
|
++pUpdater;
|
|
}
|
|
}
|
|
|
|
void TrackList::ClearPendingTracks( ListOfTracks *pAdded )
|
|
// NOFAIL-GUARANTEE
|
|
{
|
|
for (const auto &pTrack: mPendingUpdates)
|
|
pTrack->SetOwner( {}, {} );
|
|
mPendingUpdates.clear();
|
|
mUpdaters.clear();
|
|
|
|
if (pAdded)
|
|
pAdded->clear();
|
|
|
|
// To find the first node that remains after the first deleted one
|
|
TrackNodePointer node;
|
|
bool findingNode = false;
|
|
bool foundNode = false;
|
|
|
|
for (auto it = ListOfTracks::begin(), stop = ListOfTracks::end();
|
|
it != stop;) {
|
|
if (it->get()->GetId() == TrackId{}) {
|
|
if (pAdded)
|
|
pAdded->push_back( *it );
|
|
(*it)->SetOwner( {}, {} );
|
|
it = erase( it );
|
|
|
|
if (!findingNode)
|
|
findingNode = true;
|
|
if (!foundNode && it != stop)
|
|
node = (*it)->GetNode();
|
|
}
|
|
else {
|
|
if ( findingNode )
|
|
foundNode = true;
|
|
++it;
|
|
}
|
|
}
|
|
|
|
if (!empty()) {
|
|
RecalcPositions(getBegin());
|
|
DeletionEvent( node );
|
|
}
|
|
}
|
|
|
|
bool TrackList::ApplyPendingTracks()
|
|
{
|
|
bool result = false;
|
|
|
|
ListOfTracks additions;
|
|
ListOfTracks updates;
|
|
{
|
|
// Always clear, even if one of the update functions throws
|
|
auto cleanup = finally( [&] { ClearPendingTracks( &additions ); } );
|
|
UpdatePendingTracks();
|
|
updates.swap( mPendingUpdates );
|
|
}
|
|
|
|
// Remaining steps must be NOFAIL-GUARANTEE so that this function
|
|
// gives STRONG-GUARANTEE
|
|
|
|
std::vector< std::shared_ptr<Track> > reinstated;
|
|
|
|
for (auto &pendingTrack : updates) {
|
|
if (pendingTrack) {
|
|
if (pendingTrack->mpView)
|
|
pendingTrack->mpView->Reparent( pendingTrack );
|
|
if (pendingTrack->mpControls)
|
|
pendingTrack->mpControls->Reparent( pendingTrack );
|
|
auto src = FindById( pendingTrack->GetId() );
|
|
if (src)
|
|
this->Replace(src, pendingTrack), result = true;
|
|
else
|
|
// Perhaps a track marked for pending changes got deleted by
|
|
// some other action. Recreate it so we don't lose the
|
|
// accumulated changes.
|
|
reinstated.push_back(pendingTrack);
|
|
}
|
|
}
|
|
|
|
// If there are tracks to reinstate, append them to the list.
|
|
for (auto &pendingTrack : reinstated)
|
|
if (pendingTrack)
|
|
this->Add( pendingTrack ), result = true;
|
|
|
|
// Put the pending added tracks back into the list, preserving their
|
|
// positions.
|
|
bool inserted = false;
|
|
ListOfTracks::iterator first;
|
|
for (auto &pendingTrack : additions) {
|
|
if (pendingTrack) {
|
|
auto iter = ListOfTracks::begin();
|
|
std::advance( iter, pendingTrack->GetIndex() );
|
|
iter = ListOfTracks::insert( iter, pendingTrack );
|
|
pendingTrack->SetOwner( shared_from_this(), {iter, this} );
|
|
pendingTrack->SetId( TrackId{ ++sCounter } );
|
|
if (!inserted) {
|
|
first = iter;
|
|
inserted = true;
|
|
}
|
|
}
|
|
}
|
|
if (inserted) {
|
|
TrackNodePointer node{first, this};
|
|
RecalcPositions(node);
|
|
AdditionEvent(node);
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::shared_ptr<Track> Track::SubstitutePendingChangedTrack()
|
|
{
|
|
// Linear search. Tracks in a project are usually very few.
|
|
auto pList = mList.lock();
|
|
if (pList) {
|
|
const auto id = GetId();
|
|
const auto end = pList->mPendingUpdates.end();
|
|
auto it = std::find_if(
|
|
pList->mPendingUpdates.begin(), end,
|
|
[=](const ListOfTracks::value_type &ptr){ return ptr->GetId() == id; } );
|
|
if (it != end)
|
|
return *it;
|
|
}
|
|
return SharedPointer();
|
|
}
|
|
|
|
std::shared_ptr<const Track> Track::SubstitutePendingChangedTrack() const
|
|
{
|
|
return const_cast<Track*>(this)->SubstitutePendingChangedTrack();
|
|
}
|
|
|
|
std::shared_ptr<const Track> Track::SubstituteOriginalTrack() const
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList) {
|
|
const auto id = GetId();
|
|
const auto pred = [=]( const ListOfTracks::value_type &ptr ) {
|
|
return ptr->GetId() == id; };
|
|
const auto end = pList->mPendingUpdates.end();
|
|
const auto it = std::find_if( pList->mPendingUpdates.begin(), end, pred );
|
|
if (it != end) {
|
|
const auto &list2 = (const ListOfTracks &) *pList;
|
|
const auto end2 = list2.end();
|
|
const auto it2 = std::find_if( list2.begin(), end2, pred );
|
|
if ( it2 != end2 )
|
|
return *it2;
|
|
}
|
|
}
|
|
return SharedPointer();
|
|
}
|
|
|
|
// Serialize, not with tags of its own, but as attributes within a tag.
|
|
void Track::WriteCommonXMLAttributes(
|
|
XMLWriter &xmlFile, bool includeNameAndSelected) const
|
|
{
|
|
if (includeNameAndSelected) {
|
|
xmlFile.WriteAttr(wxT("name"), GetName());
|
|
xmlFile.WriteAttr(wxT("isSelected"), this->GetSelected());
|
|
}
|
|
if ( mpView )
|
|
mpView->WriteXMLAttributes( xmlFile );
|
|
if ( mpControls )
|
|
mpControls->WriteXMLAttributes( xmlFile );
|
|
}
|
|
|
|
// Return true iff the attribute is recognized.
|
|
bool Track::HandleCommonXMLAttribute(const wxChar *attr, const wxChar *value)
|
|
{
|
|
long nValue = -1;
|
|
wxString strValue( value );
|
|
if ( mpView && mpView->HandleXMLAttribute( attr, value ) )
|
|
;
|
|
else if ( mpControls && mpControls->HandleXMLAttribute( attr, value ) )
|
|
;
|
|
else if (!wxStrcmp(attr, wxT("name")) &&
|
|
XMLValueChecker::IsGoodString(strValue)) {
|
|
SetName( strValue );
|
|
return true;
|
|
}
|
|
else if (!wxStrcmp(attr, wxT("isSelected")) &&
|
|
XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue)) {
|
|
this->SetSelected(nValue != 0);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Track::AdjustPositions()
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList) {
|
|
pList->RecalcPositions(mNode);
|
|
pList->ResizingEvent(mNode);
|
|
}
|
|
}
|
|
|
|
bool TrackList::HasPendingTracks() const
|
|
{
|
|
if ( !mPendingUpdates.empty() )
|
|
return true;
|
|
if (end() != std::find_if(begin(), end(), [](const Track *t){
|
|
return t->GetId() == TrackId{};
|
|
}))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
#include "ViewInfo.h"
|
|
static auto TrackFactoryFactory = []( AudacityProject &project ) {
|
|
auto &dirManager = DirManager::Get( project );
|
|
auto &viewInfo = ViewInfo::Get( project );
|
|
return std::make_shared< TrackFactory >(
|
|
ProjectSettings::Get( project ),
|
|
dirManager.shared_from_this(), &viewInfo );
|
|
};
|
|
|
|
static const AudacityProject::AttachedObjects::RegisteredFactory key2{
|
|
TrackFactoryFactory
|
|
};
|
|
|
|
TrackFactory &TrackFactory::Get( AudacityProject &project )
|
|
{
|
|
return project.AttachedObjects::Get< TrackFactory >( key2 );
|
|
}
|
|
|
|
const TrackFactory &TrackFactory::Get( const AudacityProject &project )
|
|
{
|
|
return Get( const_cast< AudacityProject & >( project ) );
|
|
}
|
|
|
|
TrackFactory &TrackFactory::Reset( AudacityProject &project )
|
|
{
|
|
auto result = TrackFactoryFactory( project );
|
|
project.AttachedObjects::Assign( key2, result );
|
|
return *result;
|
|
}
|
|
|
|
void TrackFactory::Destroy( AudacityProject &project )
|
|
{
|
|
project.AttachedObjects::Assign( key2, nullptr );
|
|
}
|