mirror of
https://github.com/cookiengineer/audacity
synced 2025-05-08 15:52:53 +02:00
1591 lines
37 KiB
C++
1591 lines
37 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 <algorithm>
|
|
#include <numeric>
|
|
#include "Track.h"
|
|
|
|
#include <float.h>
|
|
#include <wx/file.h>
|
|
#include <wx/textfile.h>
|
|
#include <wx/log.h>
|
|
|
|
#include "TimeTrack.h"
|
|
#include "WaveTrack.h"
|
|
#include "NoteTrack.h"
|
|
#include "LabelTrack.h"
|
|
#include "Project.h"
|
|
#include "DirManager.h"
|
|
|
|
#include "Experimental.h"
|
|
|
|
#include "TrackPanel.h" // for TrackInfo
|
|
|
|
#ifdef _MSC_VER
|
|
//Disable truncation warnings
|
|
#pragma warning( disable : 4786 )
|
|
#endif
|
|
|
|
#ifdef __WXDEBUG__
|
|
// if we are in a debug build of audacity
|
|
/// Define this to do extended (slow) debuging of TrackListIterator
|
|
// #define DEBUG_TLI
|
|
#endif
|
|
|
|
Track::Track(const std::shared_ptr<DirManager> &projDirManager)
|
|
: vrulerSize(36,0),
|
|
mDirManager(projDirManager)
|
|
{
|
|
mSelected = false;
|
|
mLinked = false;
|
|
|
|
mY = 0;
|
|
mHeight = DefaultHeight;
|
|
mIndex = 0;
|
|
|
|
mMinimized = false;
|
|
|
|
mOffset = 0.0;
|
|
|
|
mChannel = MonoChannel;
|
|
}
|
|
|
|
Track::Track(const Track &orig)
|
|
: vrulerSize( orig.vrulerSize )
|
|
{
|
|
mY = 0;
|
|
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;
|
|
mHeight = orig.mHeight;
|
|
mMinimized = orig.mMinimized;
|
|
mChannel = orig.mChannel;
|
|
}
|
|
|
|
void Track::SetSelected(bool s)
|
|
{
|
|
mSelected = s;
|
|
}
|
|
|
|
void Track::Merge(const Track &orig)
|
|
{
|
|
mSelected = orig.mSelected;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
int Track::GetMinimizedHeight() const
|
|
{
|
|
auto height = TrackInfo::MinimumTrackHeight();
|
|
|
|
if (GetLink()) {
|
|
auto halfHeight = height / 2;
|
|
if (GetLinked())
|
|
return halfHeight;
|
|
else
|
|
return height - halfHeight;
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
int Track::GetIndex() const
|
|
{
|
|
return mIndex;
|
|
}
|
|
|
|
void Track::SetIndex(int index)
|
|
{
|
|
mIndex = index;
|
|
}
|
|
|
|
int Track::GetY() const
|
|
{
|
|
return mY;
|
|
}
|
|
|
|
void Track::SetY(int y)
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList && !pList->mPendingUpdates.empty()) {
|
|
auto orig = pList->FindById( GetId() );
|
|
if (orig && orig != this) {
|
|
// delegate, and rely on the update to copy back
|
|
orig->SetY(y);
|
|
pList->UpdatePendingTracks();
|
|
return;
|
|
}
|
|
}
|
|
|
|
DoSetY(y);
|
|
}
|
|
|
|
void Track::DoSetY(int y)
|
|
{
|
|
mY = y;
|
|
}
|
|
|
|
int Track::GetHeight() const
|
|
{
|
|
if (mMinimized) {
|
|
return GetMinimizedHeight();
|
|
}
|
|
|
|
return mHeight;
|
|
}
|
|
|
|
void Track::SetHeight(int h)
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList && !pList->mPendingUpdates.empty()) {
|
|
auto orig = pList->FindById( GetId() );
|
|
if (orig && orig != this) {
|
|
// delegate, and rely on RecalcPositions to copy back
|
|
orig->SetHeight(h);
|
|
return;
|
|
}
|
|
}
|
|
|
|
DoSetHeight(h);
|
|
|
|
if (pList) {
|
|
pList->RecalcPositions(mNode);
|
|
pList->ResizingEvent(mNode);
|
|
}
|
|
}
|
|
|
|
void Track::DoSetHeight(int h)
|
|
{
|
|
mHeight = h;
|
|
}
|
|
|
|
bool Track::GetMinimized() const
|
|
{
|
|
return mMinimized;
|
|
}
|
|
|
|
void Track::SetMinimized(bool isMinimized)
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList && !pList->mPendingUpdates.empty()) {
|
|
auto orig = pList->FindById( GetId() );
|
|
if (orig && orig != this) {
|
|
// delegate, and rely on RecalcPositions to copy back
|
|
orig->SetMinimized(isMinimized);
|
|
return;
|
|
}
|
|
}
|
|
|
|
DoSetMinimized(isMinimized);
|
|
|
|
if (pList) {
|
|
pList->RecalcPositions(mNode);
|
|
pList->ResizingEvent(mNode);
|
|
}
|
|
}
|
|
|
|
void Track::DoSetMinimized(bool isMinimized)
|
|
{
|
|
mMinimized = isMinimized;
|
|
}
|
|
|
|
void Track::SetLinked(bool l)
|
|
{
|
|
auto pList = mList.lock();
|
|
if (pList && !pList->mPendingUpdates.empty()) {
|
|
auto orig = pList->FindById( GetId() );
|
|
if (orig && orig != this) {
|
|
// delegate, and rely on RecalcPositions to copy back
|
|
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;
|
|
}
|
|
|
|
bool Track::IsSyncLockSelected() const
|
|
{
|
|
#ifdef EXPERIMENTAL_SYNC_LOCK
|
|
AudacityProject *p = GetActiveProject();
|
|
if (!p || !p->IsSyncLocked())
|
|
return false;
|
|
|
|
auto pList = mList.lock();
|
|
SyncLockedTracksIterator git(pList.get());
|
|
Track *t = git.StartWith(const_cast<Track*>(this));
|
|
|
|
if (!t) {
|
|
// Not in a sync-locked group.
|
|
return ((this->GetKind() == Track::Wave) || (this->GetKind() == Track::Label)) && GetSelected();
|
|
}
|
|
|
|
for (; t; t = git.Next()) {
|
|
if (t->GetSelected())
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<Track> Track::FindTrack()
|
|
{
|
|
return Pointer( this );
|
|
}
|
|
|
|
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 );
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// TrackListIterator
|
|
TrackListIterator::TrackListIterator(TrackList * val, TrackNodePointer p)
|
|
: l{ val }
|
|
, cur{ p }
|
|
{
|
|
}
|
|
|
|
TrackListIterator::TrackListIterator(TrackList * val)
|
|
: l{ val }
|
|
, cur{}
|
|
{
|
|
if (l)
|
|
cur = l->getBegin();
|
|
}
|
|
|
|
Track *TrackListIterator::StartWith(Track * val)
|
|
{
|
|
if (val == NULL) {
|
|
return First();
|
|
}
|
|
|
|
if (l == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
if (val->mList.lock() == NULL)
|
|
return nullptr;
|
|
|
|
cur = val->GetNode();
|
|
return cur.first->get();
|
|
}
|
|
|
|
Track *TrackListIterator::First(TrackList * val)
|
|
{
|
|
if (val != NULL) {
|
|
l = val;
|
|
}
|
|
|
|
if (l == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
cur = l->getBegin();
|
|
|
|
if (!l->isNull(cur)) {
|
|
return cur.first->get();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Track *TrackListIterator::Last(bool skiplinked)
|
|
{
|
|
if (l == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
cur = l->getPrev( l->getEnd() );
|
|
if ( l->isNull( cur ) )
|
|
return nullptr;
|
|
|
|
// With skiplinked set, we won't return the second channel of a linked pair
|
|
if (skiplinked) {
|
|
auto prev = l->getPrev( cur );
|
|
if ( !l->isNull( prev ) &&
|
|
!(*cur.first)->GetLinked() &&
|
|
(*cur.first)->GetLink()
|
|
)
|
|
cur = prev;
|
|
}
|
|
|
|
return cur.first->get();
|
|
}
|
|
|
|
Track *TrackListIterator::Next(bool skipLinked)
|
|
{
|
|
#ifdef DEBUG_TLI // if we are debugging this bit
|
|
wxASSERT_MSG((!cur || (*l).Contains((*cur).t)), wxT("cur invalid at start of Next(). List changed since iterator created?")); // check that cur is in the list
|
|
#endif
|
|
|
|
if (!l || l->isNull(cur))
|
|
return nullptr;
|
|
|
|
if (skipLinked &&
|
|
(*cur.first)->GetLinked())
|
|
cur = l->getNext( cur );
|
|
|
|
#ifdef DEBUG_TLI // if we are debugging this bit
|
|
wxASSERT_MSG((!cur || (*l).Contains((*cur).t)), wxT("cur invalid after skipping linked tracks.")); // check that cur is in the list
|
|
#endif
|
|
|
|
if (!l->isNull(cur))
|
|
cur = l->getNext( cur );
|
|
|
|
#ifdef DEBUG_TLI // if we are debugging this bit
|
|
wxASSERT_MSG((!cur || (*l).Contains((*cur).t)), wxT("cur invalid after moving to next track.")); // check that cur is in the list if it is not null
|
|
#endif
|
|
|
|
if (!l->isNull(cur))
|
|
return cur.first->get();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Track *TrackListIterator::Prev(bool skiplinked)
|
|
{
|
|
if (!l || l->isNull(cur))
|
|
return nullptr;
|
|
|
|
cur = l->getPrev( cur );
|
|
if ( l->isNull( cur ) )
|
|
return nullptr;
|
|
|
|
if ( skiplinked ) {
|
|
auto prev = l->getPrev( cur );
|
|
if( !l->isNull( prev ) && (*prev.first)->GetLinked() )
|
|
cur = prev;
|
|
}
|
|
|
|
return cur.first->get();
|
|
}
|
|
|
|
Track *TrackListIterator::operator *() const
|
|
{
|
|
if ( !l || l->isNull( cur ) )
|
|
return nullptr;
|
|
else
|
|
return cur.first->get();
|
|
}
|
|
|
|
Track *TrackListIterator::RemoveCurrent()
|
|
{
|
|
if ( !l || l->isNull( cur ) )
|
|
return nullptr;
|
|
|
|
cur = l->Remove( cur.first->get() );
|
|
|
|
#ifdef DEBUG_TLI // if we are debugging this bit
|
|
wxASSERT_MSG((!cur || (*l).Contains((*cur).t)), wxT("cur invalid after deletion of track.")); // check that cur is in the list
|
|
#endif
|
|
|
|
if ( !l->isNull( cur ) )
|
|
return cur.first->get();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool TrackListIterator::operator == (const TrackListIterator &other) const
|
|
{
|
|
// Order these steps so as not to use operator == on default-constructed
|
|
// std::list::iterator -- that crashes in the MSVC 2013 standard library
|
|
bool isEnd = !l || l->isNull( cur );
|
|
bool otherIsEnd = !other.l || other.l->isNull( other.cur );
|
|
|
|
return (isEnd == otherIsEnd && (isEnd || cur == other.cur));
|
|
}
|
|
|
|
//
|
|
// TrackListCondIterator (base class for iterators that iterate over all tracks
|
|
// that meet a condition)
|
|
//
|
|
|
|
Track *TrackListCondIterator::StartWith(Track *val)
|
|
{
|
|
Track *t = TrackListIterator::StartWith(val);
|
|
|
|
if (t && !this->Condition(t))
|
|
return nullptr;
|
|
|
|
return t;
|
|
}
|
|
|
|
Track *TrackListCondIterator::First(TrackList *val)
|
|
{
|
|
Track *t = TrackListIterator::First(val);
|
|
|
|
while (t && !this->Condition(t)) {
|
|
t = TrackListIterator::Next();
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
Track *TrackListCondIterator::Next(bool skiplinked)
|
|
{
|
|
while (Track *t = TrackListIterator::Next(skiplinked)) {
|
|
if (this->Condition(t)) {
|
|
return t;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
Track *TrackListCondIterator::Prev(bool skiplinked)
|
|
{
|
|
while (Track *t = TrackListIterator::Prev(skiplinked))
|
|
{
|
|
if (this->Condition(t)) {
|
|
return t;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
Track *TrackListCondIterator::Last(bool skiplinked)
|
|
{
|
|
Track *t = TrackListIterator::Last(skiplinked);
|
|
|
|
while (t && !this->Condition(t)) {
|
|
t = TrackListIterator::Prev(skiplinked);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
// TrackListOfKindIterator
|
|
TrackListOfKindIterator::TrackListOfKindIterator(int kind, TrackList * val)
|
|
: TrackListCondIterator(val)
|
|
{
|
|
this->kind = kind;
|
|
}
|
|
|
|
bool TrackListOfKindIterator::Condition(Track *t)
|
|
{
|
|
return kind == Track::All || t->GetKind() == kind;
|
|
}
|
|
|
|
//SelectedTrackListOfKindIterator
|
|
bool SelectedTrackListOfKindIterator::Condition(Track *t)
|
|
{
|
|
return TrackListOfKindIterator::Condition(t) && t->GetSelected();
|
|
}
|
|
|
|
// VisibleTrackIterator
|
|
//
|
|
// Based on TrackListIterator returns only the currently visible tracks.
|
|
//
|
|
VisibleTrackIterator::VisibleTrackIterator(AudacityProject *project)
|
|
: TrackListCondIterator(project->GetTracks())
|
|
{
|
|
mProject = project;
|
|
mPanelRect.SetTop(mProject->mViewInfo.vpos);
|
|
mPanelRect.SetSize(mProject->GetTPTracksUsableArea());
|
|
}
|
|
|
|
bool VisibleTrackIterator::Condition(Track *t)
|
|
{
|
|
wxRect r(0, t->GetY(), 1, t->GetHeight());
|
|
if( r.Intersects(mPanelRect) )
|
|
return true;
|
|
auto partner = t->GetLink();
|
|
if ( partner && t->GetLinked() )
|
|
return Condition( partner );
|
|
return false;
|
|
}
|
|
|
|
// SyncLockedTracksIterator
|
|
//
|
|
// Based on TrackListIterator returns only tracks belonging to the group
|
|
// in which the starting track is a member.
|
|
//
|
|
SyncLockedTracksIterator::SyncLockedTracksIterator(TrackList * val)
|
|
: TrackListIterator(val),
|
|
mInLabelSection(false)
|
|
{
|
|
}
|
|
|
|
namespace {
|
|
inline bool IsSyncLockableNonLabelTrack( const Track *pTrack )
|
|
{
|
|
return nullptr != dynamic_cast< const AudioTrack * >( pTrack );
|
|
}
|
|
}
|
|
|
|
Track *SyncLockedTracksIterator::StartWith(Track * member)
|
|
{
|
|
Track *t = NULL;
|
|
|
|
// A sync-locked group consists of any positive number of wave (or note)
|
|
// tracks followed by any
|
|
// non-negative number of label tracks. Step back through any label tracks,
|
|
// and then through the wave tracks above them.
|
|
|
|
while (member && member->GetKind() == Track::Label) {
|
|
member = l->GetPrev(member);
|
|
}
|
|
|
|
while (member && IsSyncLockableNonLabelTrack(member)) {
|
|
t = member;
|
|
member = l->GetPrev(member);
|
|
}
|
|
|
|
// Make it current (if t is still NULL there are no wave tracks, so we're
|
|
// not in a sync-locked group).
|
|
if (t)
|
|
cur = t->GetNode();
|
|
|
|
mInLabelSection = false;
|
|
|
|
return t;
|
|
}
|
|
|
|
bool SyncLockedTracksIterator::IsGoodNextTrack(const Track *t) const
|
|
{
|
|
if (!t)
|
|
return false;
|
|
|
|
const bool isLabel = ( t->GetKind() == Track::Label );
|
|
const bool isSyncLockable = IsSyncLockableNonLabelTrack( t );
|
|
|
|
if ( !( isLabel || isSyncLockable ) ) {
|
|
return false;
|
|
}
|
|
|
|
if (mInLabelSection && !isLabel) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Track *SyncLockedTracksIterator::Next(bool skiplinked)
|
|
{
|
|
Track *t = TrackListIterator::Next(skiplinked);
|
|
|
|
if (!t)
|
|
return nullptr;
|
|
|
|
if ( ! IsGoodNextTrack(t) ) {
|
|
cur = l->getEnd();
|
|
return nullptr;
|
|
}
|
|
|
|
mInLabelSection = ( t->GetKind() == Track::Label );
|
|
|
|
return t;
|
|
}
|
|
|
|
Track *SyncLockedTracksIterator::Prev(bool skiplinked)
|
|
{
|
|
Track *t = TrackListIterator::Prev(skiplinked);
|
|
|
|
//
|
|
// Ways to end a sync-locked group in reverse
|
|
//
|
|
|
|
// Beginning of tracks
|
|
if (!t)
|
|
return nullptr;
|
|
|
|
const bool isLabel = ( t->GetKind() == Track::Label );
|
|
const bool isSyncLockable = IsSyncLockableNonLabelTrack( t );
|
|
|
|
if ( !( isLabel || isSyncLockable ) ) {
|
|
cur = l->getEnd();
|
|
return nullptr;
|
|
}
|
|
|
|
if ( !mInLabelSection && isLabel ) {
|
|
cur = l->getEnd();
|
|
return nullptr;
|
|
}
|
|
|
|
mInLabelSection = isLabel;
|
|
|
|
return t;
|
|
}
|
|
|
|
Track *SyncLockedTracksIterator::Last(bool skiplinked)
|
|
{
|
|
if ( !l || l->isNull( cur ) )
|
|
return nullptr;
|
|
|
|
Track *t = cur.first->get();
|
|
|
|
while (const auto next = l->GetNext(t, skiplinked)) {
|
|
if ( ! IsGoodNextTrack(next) )
|
|
break;
|
|
t = Next(skiplinked);
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
// 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_PERMUTED, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_RESIZING, wxCommandEvent);
|
|
wxDEFINE_EVENT(EVT_TRACKLIST_DELETION, wxCommandEvent);
|
|
|
|
// same value as in the default constructed TrackId:
|
|
long TrackList::sCounter = -1;
|
|
|
|
TrackList::TrackList()
|
|
: wxEvtHandler()
|
|
{
|
|
}
|
|
|
|
// Factory function
|
|
std::shared_ptr<TrackList> TrackList::Create()
|
|
{
|
|
std::shared_ptr<TrackList> result{ safenew TrackList{} };
|
|
result->mSelf = result;
|
|
return result;
|
|
}
|
|
|
|
TrackList &TrackList::operator= (TrackList &&that)
|
|
{
|
|
if (this != &that) {
|
|
this->Clear();
|
|
Swap(that);
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
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});
|
|
};
|
|
|
|
SwapLOTs( *this, mSelf, that, that.mSelf );
|
|
SwapLOTs( this->mPendingUpdates, mSelf, that.mPendingUpdates, that.mSelf );
|
|
mUpdaters.swap(that.mUpdaters);
|
|
}
|
|
|
|
TrackList::~TrackList()
|
|
{
|
|
Clear(false);
|
|
}
|
|
|
|
void TrackList::RecalcPositions(TrackNodePointer node)
|
|
{
|
|
if ( isNull( node ) )
|
|
return;
|
|
|
|
Track *t;
|
|
int i = 0;
|
|
int y = 0;
|
|
|
|
auto prev = getPrev( node );
|
|
if ( !isNull( prev ) ) {
|
|
t = prev.first->get();
|
|
i = t->GetIndex() + 1;
|
|
y = t->GetY() + t->GetHeight();
|
|
}
|
|
|
|
const auto theEnd = end();
|
|
for (auto n = TrackListIterator{ this, node }; n != theEnd; ++n) {
|
|
t = *n;
|
|
t->SetIndex(i++);
|
|
t->DoSetY(y);
|
|
y += t->GetHeight();
|
|
}
|
|
|
|
UpdatePendingTracks();
|
|
}
|
|
|
|
void TrackList::PermutationEvent()
|
|
{
|
|
auto e = std::make_unique<wxCommandEvent>(EVT_TRACKLIST_PERMUTED);
|
|
// wxWidgets will own the event object
|
|
QueueEvent(e.release());
|
|
}
|
|
|
|
void TrackList::DeletionEvent()
|
|
{
|
|
auto e = std::make_unique<wxCommandEvent>(EVT_TRACKLIST_DELETION);
|
|
// wxWidgets will own the event object
|
|
QueueEvent(e.release());
|
|
}
|
|
|
|
void TrackList::ResizingEvent(TrackNodePointer node)
|
|
{
|
|
auto e = std::make_unique<TrackListEvent>(EVT_TRACKLIST_RESIZING);
|
|
e->mpTrack = *node.first;
|
|
// wxWidgets will own the event object
|
|
QueueEvent(e.release());
|
|
}
|
|
|
|
void TrackList::Permute(const std::vector<TrackNodePointer> &permutation)
|
|
{
|
|
for (const auto iter : permutation) {
|
|
ListOfTracks::value_type track = std::move(*iter.first);
|
|
erase(iter.first);
|
|
Track *pTrack = track.get();
|
|
pTrack->SetOwner(mSelf,
|
|
{ insert(ListOfTracks::end(), std::move(track)), this });
|
|
}
|
|
auto n = getBegin();
|
|
RecalcPositions(n);
|
|
PermutationEvent();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
template<typename TrackKind>
|
|
Track *TrackList::Add(std::unique_ptr<TrackKind> &&t)
|
|
{
|
|
Track *pTrack;
|
|
push_back(ListOfTracks::value_type(pTrack = t.release()));
|
|
|
|
auto n = getPrev( getEnd() );
|
|
|
|
pTrack->SetOwner(mSelf, n);
|
|
pTrack->SetId( TrackId{ ++sCounter } );
|
|
RecalcPositions(n);
|
|
ResizingEvent(n);
|
|
return back().get();
|
|
}
|
|
|
|
// Make instantiations for the linker to find
|
|
template Track *TrackList::Add<TimeTrack>(std::unique_ptr<TimeTrack> &&);
|
|
#if defined(USE_MIDI)
|
|
template Track *TrackList::Add<NoteTrack>(std::unique_ptr<NoteTrack> &&);
|
|
#endif
|
|
template Track *TrackList::Add<WaveTrack>(std::unique_ptr<WaveTrack> &&);
|
|
template Track *TrackList::Add<LabelTrack>(std::unique_ptr<LabelTrack> &&);
|
|
template Track *TrackList::Add<Track>(std::unique_ptr<Track> &&);
|
|
|
|
template<typename TrackKind>
|
|
Track *TrackList::AddToHead(std::unique_ptr<TrackKind> &&t)
|
|
{
|
|
Track *pTrack;
|
|
push_front(ListOfTracks::value_type(pTrack = t.release()));
|
|
auto n = getBegin();
|
|
pTrack->SetOwner(mSelf, n);
|
|
pTrack->SetId( TrackId{ ++sCounter } );
|
|
RecalcPositions(n);
|
|
ResizingEvent(n);
|
|
return front().get();
|
|
}
|
|
|
|
// Make instantiations for the linker to find
|
|
template Track *TrackList::AddToHead<TimeTrack>(std::unique_ptr<TimeTrack> &&);
|
|
|
|
template<typename TrackKind>
|
|
Track *TrackList::Add(std::shared_ptr<TrackKind> &&t)
|
|
{
|
|
push_back(t);
|
|
|
|
auto n = getPrev( getEnd() );
|
|
|
|
t->SetOwner(mSelf, n);
|
|
t->SetId( TrackId{ ++sCounter } );
|
|
RecalcPositions(n);
|
|
ResizingEvent(n);
|
|
return back().get();
|
|
}
|
|
|
|
// Make instantiations for the linker to find
|
|
template Track *TrackList::Add<Track>(std::shared_ptr<Track> &&);
|
|
template Track *TrackList::Add<WaveTrack>(std::shared_ptr<WaveTrack> &&);
|
|
|
|
auto TrackList::Replace(Track * t, ListOfTracks::value_type &&with) ->
|
|
ListOfTracks::value_type
|
|
{
|
|
ListOfTracks::value_type holder;
|
|
if (t && with) {
|
|
auto node = t->GetNode();
|
|
t->SetOwner({}, {});
|
|
|
|
holder = std::move(*node.first);
|
|
|
|
Track *pTrack = with.get();
|
|
*node.first = std::move(with);
|
|
pTrack->SetOwner(mSelf, node);
|
|
pTrack->SetId( t->GetId() );
|
|
RecalcPositions(node);
|
|
|
|
DeletionEvent();
|
|
ResizingEvent(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 = std::move( *node.first );
|
|
|
|
result = getNext( node );
|
|
erase(node.first);
|
|
if ( !isNull( result ) )
|
|
RecalcPositions(result);
|
|
|
|
DeletionEvent();
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
|
|
void TrackList::Select(Track * t, bool selected /* = true */ )
|
|
{
|
|
if (t) {
|
|
const auto node = t->GetNode();
|
|
if ( !isNull( node ) ) {
|
|
t->SetSelected( selected );
|
|
if ( t->GetLinked() ) {
|
|
auto next = getNext( node );
|
|
if ( !isNull( next ) )
|
|
(*next.first)->SetSelected( selected );
|
|
}
|
|
else {
|
|
auto prev = getPrev( node );
|
|
if ( !isNull( prev ) && (*prev.first)->GetLinked() )
|
|
(*prev.first)->SetSelected( selected );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// For mono track height of track
|
|
/// For stereo track combined height of both channels.
|
|
int TrackList::GetGroupHeight(Track * t) const
|
|
{
|
|
int height = t->GetHeight();
|
|
|
|
t = t->GetLink();
|
|
if (t) {
|
|
height += t->GetHeight();
|
|
}
|
|
return height;
|
|
}
|
|
|
|
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 track or pair of
|
|
// tracks in s1 with the track or pair of tracks in s2.
|
|
// The complication is that the tracks are stored in a single
|
|
// linked list, and pairs of tracks are marked only by a flag
|
|
// in one of the tracks.
|
|
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
|
|
Track *link;
|
|
link = (*s1.first)->GetLink();
|
|
bool linked1 = link != nullptr;
|
|
if (linked1 && !(*s1.first)->GetLinked()) {
|
|
s1 = link->GetNode();
|
|
}
|
|
|
|
link = (*s2.first)->GetLink();
|
|
bool linked2 = link != nullptr;
|
|
if (linked2 && !(*s2.first)->GetLinked()) {
|
|
s2 = link->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);
|
|
std::swap(linked1, linked2);
|
|
}
|
|
|
|
// Remove tracks
|
|
ListOfTracks::value_type save11 = std::move(*s1.first), save12{};
|
|
s1.first = erase(s1.first);
|
|
if (linked1) {
|
|
wxASSERT(s1 != s2);
|
|
save12 = std::move(*s1.first), s1.first = erase(s1.first);
|
|
}
|
|
const bool same = (s1 == s2);
|
|
|
|
ListOfTracks::value_type save21 = std::move(*s2.first), save22{};
|
|
s2.first = erase(s2.first);
|
|
if (linked2)
|
|
save22 = std::move(*s2.first), s2.first = erase(s2.first);
|
|
|
|
if (same)
|
|
// We invalidated s1!
|
|
s1 = s2;
|
|
|
|
// Reinsert them
|
|
Track *pTrack;
|
|
if (save22)
|
|
pTrack = save22.get(),
|
|
pTrack->SetOwner(mSelf, s1 = { insert(s1.first, std::move(save22)), this });
|
|
pTrack = save21.get(),
|
|
pTrack->SetOwner(mSelf, s1 = { insert(s1.first, std::move(save21)), this });
|
|
|
|
if (save12)
|
|
pTrack = save12.get(),
|
|
pTrack->SetOwner(mSelf, s2 = { insert(s2.first, std::move(save12)), this });
|
|
pTrack = save11.get(),
|
|
pTrack->SetOwner(mSelf, s2 = { insert(s2.first, std::move(save11)), this });
|
|
|
|
// Now correct the Index in the tracks, and other things
|
|
RecalcPositions(s1);
|
|
PermutationEvent();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
TimeTrack *TrackList::GetTimeTrack()
|
|
{
|
|
auto iter = std::find_if(begin(), end(),
|
|
[] ( Track *t ) { return t->GetKind() == Track::Time; }
|
|
);
|
|
if (iter == end())
|
|
return nullptr;
|
|
else
|
|
return static_cast<TimeTrack*>(*iter);
|
|
}
|
|
|
|
const TimeTrack *TrackList::GetTimeTrack() const
|
|
{
|
|
return const_cast<TrackList*>(this)->GetTimeTrack();
|
|
}
|
|
|
|
unsigned TrackList::GetNumExportChannels(bool selectionOnly) const
|
|
{
|
|
/* counters for tracks panned different places */
|
|
int numLeft = 0;
|
|
int numRight = 0;
|
|
//int numMono = 0;
|
|
/* track iteration kit */
|
|
const Track *tr;
|
|
TrackListConstIterator iter;
|
|
|
|
for (tr = iter.First(this); tr != NULL; tr = iter.Next()) {
|
|
|
|
// Want only unmuted wave tracks.
|
|
auto wt = static_cast<const WaveTrack *>(tr);
|
|
if ((tr->GetKind() != Track::Wave) ||
|
|
wt->GetMute())
|
|
continue;
|
|
|
|
// do we only want selected ones?
|
|
if (selectionOnly && !(tr->GetSelected())) {
|
|
//want selected but this one is not
|
|
continue;
|
|
}
|
|
|
|
// Found a left channel
|
|
if (tr->GetChannel() == Track::LeftChannel) {
|
|
numLeft++;
|
|
}
|
|
|
|
// Found a right channel
|
|
else if (tr->GetChannel() == Track::RightChannel) {
|
|
numRight++;
|
|
}
|
|
|
|
// Found a mono channel, but it may be panned
|
|
else if (tr->GetChannel() == Track::MonoChannel) {
|
|
float pan = ((WaveTrack*)tr)->GetPan();
|
|
|
|
// Figure out what kind of channel it should be
|
|
if (pan == -1.0) { // panned hard left
|
|
numLeft++;
|
|
}
|
|
else if (pan == 1.0) { // panned hard right
|
|
numRight++;
|
|
}
|
|
else if (pan == 0) { // panned dead center
|
|
// numMono++;
|
|
}
|
|
else { // panned somewhere else
|
|
numLeft++;
|
|
numRight++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if there is stereo content, report 2, else report 1
|
|
if (numRight > 0 || numLeft > 0) {
|
|
return 2;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
namespace {
|
|
template<typename Array>
|
|
Array GetWaveTracks(TrackListIterator p, const TrackListIterator end,
|
|
bool selectionOnly, bool includeMuted)
|
|
{
|
|
Array waveTrackArray;
|
|
|
|
for (; p != end; ++p) {
|
|
const auto &track = *p;
|
|
auto wt = static_cast<const WaveTrack *>(&*track);
|
|
if (track->GetKind() == Track::Wave &&
|
|
(includeMuted || !wt->GetMute()) &&
|
|
(track->GetSelected() || !selectionOnly)) {
|
|
waveTrackArray.push_back( Track::Pointer< WaveTrack >( track ) );
|
|
}
|
|
}
|
|
|
|
return waveTrackArray;
|
|
}
|
|
}
|
|
|
|
WaveTrackArray TrackList::GetWaveTrackArray(bool selectionOnly, bool includeMuted)
|
|
{
|
|
return GetWaveTracks<WaveTrackArray>(begin(), end(), selectionOnly, includeMuted);
|
|
}
|
|
|
|
WaveTrackConstArray TrackList::GetWaveTrackConstArray(bool selectionOnly, bool includeMuted) const
|
|
{
|
|
auto list = const_cast<TrackList*>(this);
|
|
return GetWaveTracks<WaveTrackConstArray>(
|
|
list->begin(), list->end(), selectionOnly, includeMuted);
|
|
}
|
|
|
|
#if defined(USE_MIDI)
|
|
NoteTrackConstArray TrackList::GetNoteTrackConstArray(bool selectionOnly) const
|
|
{
|
|
NoteTrackConstArray noteTrackArray;
|
|
|
|
for(const auto &track : *this) {
|
|
if (track->GetKind() == Track::Note &&
|
|
(track->GetSelected() || !selectionOnly)) {
|
|
noteTrackArray.push_back( Track::Pointer<const NoteTrack>(track) );
|
|
}
|
|
}
|
|
|
|
return noteTrackArray;
|
|
}
|
|
#endif
|
|
|
|
int TrackList::GetHeight() const
|
|
{
|
|
int height = 0;
|
|
|
|
if (!empty()) {
|
|
auto track = getPrev( getEnd() ).first->get();
|
|
height = track->GetY() + track->GetHeight();
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
namespace {
|
|
// Abstract the common pattern of the following three member functions
|
|
double doubleMin(double a, double b) { return std::min(a, b); }
|
|
double doubleMax(double a, double b) { return std::max(a, b); }
|
|
inline double Accumulate
|
|
(const TrackList &list,
|
|
double (Track::*memfn)() const,
|
|
double (*combine)(double, double))
|
|
{
|
|
// Default the answer to zero for empty list
|
|
if (list.empty()) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Otherwise accumulate minimum or maximum of track values
|
|
auto iter = list.begin();
|
|
double acc = (**iter++.*memfn)();
|
|
return std::accumulate(iter, list.end(), acc,
|
|
[=](double acc, const Track *pTrack) {
|
|
return combine(acc, (*pTrack.*memfn)());
|
|
});
|
|
}
|
|
}
|
|
|
|
double TrackList::GetMinOffset() const
|
|
{
|
|
return Accumulate(*this, &Track::GetOffset, doubleMin);
|
|
}
|
|
|
|
double TrackList::GetStartTime() const
|
|
{
|
|
return Accumulate(*this, &Track::GetStartTime, doubleMin);
|
|
}
|
|
|
|
double TrackList::GetEndTime() const
|
|
{
|
|
return Accumulate(*this, &Track::GetEndTime, doubleMax);
|
|
}
|
|
|
|
std::shared_ptr<Track>
|
|
TrackList::RegisterPendingChangedTrack( Updater updater, Track *src )
|
|
{
|
|
std::shared_ptr<Track> pTrack;
|
|
if (src)
|
|
// convert from unique_ptr to shared_ptr
|
|
pTrack.reset( src->Duplicate().release() );
|
|
|
|
if (pTrack) {
|
|
mUpdaters.push_back( updater );
|
|
mPendingUpdates.push_back( pTrack );
|
|
auto n = mPendingUpdates.end();
|
|
--n;
|
|
pTrack->SetOwner(mSelf, {n, &mPendingUpdates});
|
|
}
|
|
|
|
return pTrack;
|
|
}
|
|
|
|
void TrackList::RegisterPendingNewTrack( const std::shared_ptr<Track> &pTrack )
|
|
{
|
|
auto copy = pTrack;
|
|
Add<Track>( std::move( copy ) );
|
|
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->DoSetY(src->GetY());
|
|
pendingTrack->DoSetHeight(src->GetHeight());
|
|
pendingTrack->DoSetMinimized(src->GetMinimized());
|
|
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();
|
|
|
|
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 );
|
|
}
|
|
else
|
|
++it;
|
|
}
|
|
|
|
if (!empty())
|
|
RecalcPositions(getBegin());
|
|
}
|
|
|
|
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) {
|
|
auto src = FindById( pendingTrack->GetId() );
|
|
if (src)
|
|
this->Replace(src, std::move(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(std::move(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( mSelf, {iter, this} );
|
|
pendingTrack->SetId( TrackId{ ++sCounter } );
|
|
if (!inserted) {
|
|
first = iter;
|
|
inserted = true;
|
|
}
|
|
}
|
|
}
|
|
if (inserted) {
|
|
RecalcPositions({first, this});
|
|
result = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::shared_ptr<Track> TrackList::FindPendingChangedTrack(TrackId id) const
|
|
{
|
|
// Linear search. Tracks in a project are usually very few.
|
|
auto it = std::find_if( mPendingUpdates.begin(), mPendingUpdates.end(),
|
|
[=](const ListOfTracks::value_type &ptr){ return ptr->GetId() == id; } );
|
|
if (it == mPendingUpdates.end())
|
|
return {};
|
|
return *it;
|
|
}
|
|
|
|
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 "AudioIO.h"
|
|
TransportTracks GetAllPlaybackTracks(const TrackList &trackList, bool selectedOnly, bool useMidi)
|
|
{
|
|
TransportTracks result;
|
|
result.playbackTracks = trackList.GetWaveTrackConstArray(selectedOnly);
|
|
#ifdef EXPERIMENTAL_MIDI_OUT
|
|
if (useMidi)
|
|
result.midiTracks = trackList.GetNoteTrackConstArray(selectedOnly);
|
|
#else
|
|
WXUNUSED(useMidi);
|
|
#endif
|
|
return result;
|
|
}
|