mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-13 19:52:12 -04:00
* Update ringbuffer submodule to fix includes
* Remove cyclic includes
* Remove Qt include prefixes
* Include C++ versions of C headers
E.g.: assert.h -> cassert
* Move CLIP_BORDER_WIDTH into ClipView
This allows to remove includes to TrackView.h in ClipView cpp files.
* Elliminate useless includes
This improves the include structure by elliminating includes that are
not used. Most of this was done by using `include-what-you-use` with
`CMAKE_C_INCLUDE_WHAT_YOU_USE` and `CMAKE_CXX_INCLUDE_WHAT_YOU_USE`
set to (broken down here):
```
include-what-you-use;
-Xiwyu;--mapping_file=/usr/share/include-what-you-use/qt5_11.imp;
-Xiwyu;--keep=*/xmmintrin.h;
-Xiwyu;--keep=*/lmmsconfig.h;
-Xiwyu;--keep=*/weak_libjack.h;
-Xiwyu;--keep=*/sys/*;
-Xiwyu;--keep=*/debug.h;
-Xiwyu;--keep=*/SDL/*;
-Xiwyu;--keep=*/alsa/*;
-Xiwyu;--keep=*/FL/x.h;
-Xiwyu;--keep=*/MidiApple.h;
-Xiwyu;--keep=*/MidiWinMM.h;
-Xiwyu;--keep=*/AudioSoundIo.h
```
* Fixup: Remove empty #if-#ifdef pairs
* Remove LMMS_HAVE_STD(LIB|INT)_H
659 lines
16 KiB
C++
659 lines
16 KiB
C++
/*
|
|
* Track.cpp - implementation of Track class
|
|
*
|
|
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public
|
|
* License along with this program (see COPYING); if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*
|
|
*/
|
|
|
|
/** \file Track.cpp
|
|
* \brief Implementation of Track class
|
|
*/
|
|
|
|
|
|
#include "Track.h"
|
|
|
|
#include <QDomElement>
|
|
#include <QVariant>
|
|
|
|
#include "AutomationClip.h"
|
|
#include "AutomationTrack.h"
|
|
#include "ConfigManager.h"
|
|
#include "Engine.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "PatternStore.h"
|
|
#include "PatternTrack.h"
|
|
#include "SampleTrack.h"
|
|
#include "Song.h"
|
|
|
|
|
|
/*! \brief Create a new (empty) track object
|
|
*
|
|
* The track object is the whole track, linking its contents, its
|
|
* automation, name, type, and so forth.
|
|
*
|
|
* \param type The type of track (Song Editor or Pattern Editor)
|
|
* \param tc The track Container object to encapsulate in this track.
|
|
*
|
|
* \todo check the definitions of all the properties - are they OK?
|
|
*/
|
|
Track::Track( TrackTypes type, TrackContainer * tc ) :
|
|
Model( tc ), /*!< The track Model */
|
|
m_trackContainer( tc ), /*!< The track container object */
|
|
m_type( type ), /*!< The track type */
|
|
m_name(), /*!< The track's name */
|
|
m_mutedModel( false, this, tr( "Mute" ) ), /*!< For controlling track muting */
|
|
m_soloModel( false, this, tr( "Solo" ) ), /*!< For controlling track soloing */
|
|
m_simpleSerializingMode( false ),
|
|
m_clips(), /*!< The clips (segments) */
|
|
m_color( 0, 0, 0 ),
|
|
m_hasColor( false )
|
|
{
|
|
m_trackContainer->addTrack( this );
|
|
m_height = -1;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Destroy this track
|
|
*
|
|
* Delete the clips and remove this track from the track container.
|
|
*/
|
|
Track::~Track()
|
|
{
|
|
lock();
|
|
emit destroyedTrack();
|
|
|
|
while( !m_clips.isEmpty() )
|
|
{
|
|
delete m_clips.last();
|
|
}
|
|
|
|
m_trackContainer->removeTrack( this );
|
|
unlock();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Create a track based on the given track type and container.
|
|
*
|
|
* \param tt The type of track to create
|
|
* \param tc The track container to attach to
|
|
*/
|
|
Track * Track::create( TrackTypes tt, TrackContainer * tc )
|
|
{
|
|
Engine::audioEngine()->requestChangeInModel();
|
|
|
|
Track * t = nullptr;
|
|
|
|
switch( tt )
|
|
{
|
|
case InstrumentTrack: t = new ::InstrumentTrack( tc ); break;
|
|
case PatternTrack: t = new ::PatternTrack( tc ); break;
|
|
case SampleTrack: t = new ::SampleTrack( tc ); break;
|
|
// case EVENT_TRACK:
|
|
// case VIDEO_TRACK:
|
|
case AutomationTrack: t = new ::AutomationTrack( tc ); break;
|
|
case HiddenAutomationTrack:
|
|
t = new ::AutomationTrack( tc, true ); break;
|
|
default: break;
|
|
}
|
|
|
|
if (tc == Engine::patternStore() && t)
|
|
{
|
|
t->createClipsForPattern(Engine::patternStore()->numOfPatterns() - 1);
|
|
}
|
|
|
|
tc->updateAfterTrackAdd();
|
|
|
|
Engine::audioEngine()->doneChangeInModel();
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Create a track inside TrackContainer from track type in a QDomElement and restore state from XML
|
|
*
|
|
* \param element The QDomElement containing the type of track to create
|
|
* \param tc The track container to attach to
|
|
*/
|
|
Track * Track::create( const QDomElement & element, TrackContainer * tc )
|
|
{
|
|
Engine::audioEngine()->requestChangeInModel();
|
|
|
|
Track * t = create(
|
|
static_cast<TrackTypes>( element.attribute( "type" ).toInt() ),
|
|
tc );
|
|
if( t != nullptr )
|
|
{
|
|
t->restoreState( element );
|
|
}
|
|
|
|
Engine::audioEngine()->doneChangeInModel();
|
|
|
|
return t;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Clone a track from this track
|
|
*
|
|
*/
|
|
Track* Track::clone()
|
|
{
|
|
// Save track to temporary XML and load it to create a new identical track
|
|
QDomDocument doc;
|
|
QDomElement parent = doc.createElement("clonedtrack");
|
|
saveState(doc, parent);
|
|
Track* t = create(parent.firstChild().toElement(), m_trackContainer);
|
|
|
|
AutomationClip::resolveAllIDs();
|
|
return t;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*! \brief Save this track's settings to file
|
|
*
|
|
* We save the track type and its muted state and solo state, then append the track-
|
|
* specific settings. Then we iterate through the clips
|
|
* and save all their states in turn.
|
|
*
|
|
* \param doc The QDomDocument to use to save
|
|
* \param element The The QDomElement to save into
|
|
* \todo Does this accurately describe the parameters? I think not!?
|
|
* \todo Save the track height
|
|
*/
|
|
void Track::saveSettings( QDomDocument & doc, QDomElement & element )
|
|
{
|
|
if( !m_simpleSerializingMode )
|
|
{
|
|
element.setTagName( "track" );
|
|
}
|
|
element.setAttribute( "type", type() );
|
|
element.setAttribute( "name", name() );
|
|
m_mutedModel.saveSettings( doc, element, "muted" );
|
|
m_soloModel.saveSettings( doc, element, "solo" );
|
|
// Save the mutedBeforeSolo value so we can recover the muted state if any solo was active (issue 5562)
|
|
element.setAttribute( "mutedBeforeSolo", int(m_mutedBeforeSolo) );
|
|
|
|
if( m_height >= MINIMAL_TRACK_HEIGHT )
|
|
{
|
|
element.setAttribute( "trackheight", m_height );
|
|
}
|
|
|
|
if( m_hasColor )
|
|
{
|
|
element.setAttribute( "color", m_color.name() );
|
|
}
|
|
|
|
QDomElement tsDe = doc.createElement( nodeName() );
|
|
// let actual track (InstrumentTrack, PatternTrack, SampleTrack etc.) save its settings
|
|
element.appendChild( tsDe );
|
|
saveTrackSpecificSettings( doc, tsDe );
|
|
|
|
if( m_simpleSerializingMode )
|
|
{
|
|
m_simpleSerializingMode = false;
|
|
return;
|
|
}
|
|
|
|
// now save settings of all Clip's
|
|
for( clipVector::const_iterator it = m_clips.begin();
|
|
it != m_clips.end(); ++it )
|
|
{
|
|
( *it )->saveState( doc, element );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Load the settings from a file
|
|
*
|
|
* We load the track's type and muted state and solo state, then clear out our
|
|
* current Clip.
|
|
*
|
|
* Then we step through the QDomElement's children and load the
|
|
* track-specific settings and clip states from it
|
|
* one at a time.
|
|
*
|
|
* \param element the QDomElement to load track settings from
|
|
* \todo Load the track height.
|
|
*/
|
|
void Track::loadSettings( const QDomElement & element )
|
|
{
|
|
if( element.attribute( "type" ).toInt() != type() )
|
|
{
|
|
qWarning( "Current track-type does not match track-type of "
|
|
"settings-node!\n" );
|
|
}
|
|
|
|
setName( element.hasAttribute( "name" ) ? element.attribute( "name" ) :
|
|
element.firstChild().toElement().attribute( "name" ) );
|
|
|
|
m_mutedModel.loadSettings( element, "muted" );
|
|
m_soloModel.loadSettings( element, "solo" );
|
|
// Get the mutedBeforeSolo value so we can recover the muted state if any solo was active.
|
|
// Older project files that didn't have this attribute will set the value to false (issue 5562)
|
|
m_mutedBeforeSolo = QVariant( element.attribute( "mutedBeforeSolo", "0" ) ).toBool();
|
|
|
|
if( element.hasAttribute( "color" ) )
|
|
{
|
|
QColor newColor = QColor(element.attribute("color"));
|
|
setColor(newColor);
|
|
}
|
|
else
|
|
{
|
|
resetColor();
|
|
}
|
|
|
|
if( m_simpleSerializingMode )
|
|
{
|
|
QDomNode node = element.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
if( node.isElement() && node.nodeName() == nodeName() )
|
|
{
|
|
loadTrackSpecificSettings( node.toElement() );
|
|
break;
|
|
}
|
|
node = node.nextSibling();
|
|
}
|
|
m_simpleSerializingMode = false;
|
|
return;
|
|
}
|
|
|
|
while( !m_clips.empty() )
|
|
{
|
|
delete m_clips.front();
|
|
// m_clips.erase( m_clips.begin() );
|
|
}
|
|
|
|
QDomNode node = element.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
if( node.isElement() )
|
|
{
|
|
if( node.nodeName() == nodeName() )
|
|
{
|
|
loadTrackSpecificSettings( node.toElement() );
|
|
}
|
|
else if( node.nodeName() != "muted"
|
|
&& node.nodeName() != "solo"
|
|
&& !node.toElement().attribute( "metadata" ).toInt() )
|
|
{
|
|
Clip * clip = createClip(
|
|
TimePos( 0 ) );
|
|
clip->restoreState( node.toElement() );
|
|
}
|
|
}
|
|
node = node.nextSibling();
|
|
}
|
|
|
|
int storedHeight = element.attribute( "trackheight" ).toInt();
|
|
if( storedHeight >= MINIMAL_TRACK_HEIGHT )
|
|
{
|
|
m_height = storedHeight;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Add another Clip into this track
|
|
*
|
|
* \param clip The Clip to attach to this track.
|
|
*/
|
|
Clip * Track::addClip( Clip * clip )
|
|
{
|
|
m_clips.push_back( clip );
|
|
|
|
emit clipAdded( clip );
|
|
|
|
return clip; // just for convenience
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Remove a given Clip from this track
|
|
*
|
|
* \param clip The Clip to remove from this track.
|
|
*/
|
|
void Track::removeClip( Clip * clip )
|
|
{
|
|
clipVector::iterator it = std::find( m_clips.begin(), m_clips.end(), clip );
|
|
if( it != m_clips.end() )
|
|
{
|
|
m_clips.erase( it );
|
|
if( Engine::getSong() )
|
|
{
|
|
Engine::getSong()->updateLength();
|
|
Engine::getSong()->setModified();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*! \brief Remove all Clips from this track */
|
|
void Track::deleteClips()
|
|
{
|
|
while( ! m_clips.isEmpty() )
|
|
{
|
|
delete m_clips.first();
|
|
}
|
|
}
|
|
|
|
|
|
/*! \brief Return the number of clips we contain
|
|
*
|
|
* \return the number of clips we currently contain.
|
|
*/
|
|
int Track::numOfClips()
|
|
{
|
|
return m_clips.size();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Get a Clip by number
|
|
*
|
|
* If the Clip number is less than our Clip array size then fetch that
|
|
* numbered object from the array. Otherwise we warn the user that
|
|
* we've somehow requested a Clip that is too large, and create a new
|
|
* Clip for them.
|
|
* \param clipNum The number of the Clip to fetch.
|
|
* \return the given Clip or a new one if out of range.
|
|
* \todo reject Clip numbers less than zero.
|
|
* \todo if we create a Clip here, should we somehow attach it to the
|
|
* track?
|
|
*/
|
|
Clip * Track::getClip( int clipNum )
|
|
{
|
|
if( clipNum < m_clips.size() )
|
|
{
|
|
return m_clips[clipNum];
|
|
}
|
|
printf( "called Track::getClip( %d ), "
|
|
"but Clip %d doesn't exist\n", clipNum, clipNum );
|
|
return createClip( clipNum * TimePos::ticksPerBar() );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Determine the given Clip's number in our array.
|
|
*
|
|
* \param clip The Clip to search for.
|
|
* \return its number in our array.
|
|
*/
|
|
int Track::getClipNum( const Clip * clip )
|
|
{
|
|
// for( int i = 0; i < getTrackContentWidget()->numOfClips(); ++i )
|
|
clipVector::iterator it = std::find( m_clips.begin(), m_clips.end(), clip );
|
|
if( it != m_clips.end() )
|
|
{
|
|
/* if( getClip( i ) == _clip )
|
|
{
|
|
return i;
|
|
}*/
|
|
return it - m_clips.begin();
|
|
}
|
|
qWarning( "Track::getClipNum(...) -> _clip not found!\n" );
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Retrieve a list of clips that fall within a period.
|
|
*
|
|
* Here we're interested in a range of clips that intersect
|
|
* the given time period.
|
|
*
|
|
* We return the Clips we find in order by time, earliest Clips first.
|
|
*
|
|
* \param clipV The list to contain the found clips.
|
|
* \param start The MIDI start time of the range.
|
|
* \param end The MIDI endi time of the range.
|
|
*/
|
|
void Track::getClipsInRange( clipVector & clipV, const TimePos & start,
|
|
const TimePos & end )
|
|
{
|
|
for( Clip* clip : m_clips )
|
|
{
|
|
int s = clip->startPosition();
|
|
int e = clip->endPosition();
|
|
if( ( s <= end ) && ( e >= start ) )
|
|
{
|
|
// Clip is within given range
|
|
// Insert sorted by Clip's position
|
|
clipV.insert(std::upper_bound(clipV.begin(), clipV.end(), clip, Clip::comparePosition),
|
|
clip);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Swap the position of two clips.
|
|
*
|
|
* First, we arrange to swap the positions of the two Clips in the
|
|
* clips list. Then we swap their start times as well.
|
|
*
|
|
* \param clipNum1 The first Clip to swap.
|
|
* \param clipNum2 The second Clip to swap.
|
|
*/
|
|
void Track::swapPositionOfClips( int clipNum1, int clipNum2 )
|
|
{
|
|
qSwap( m_clips[clipNum1], m_clips[clipNum2] );
|
|
|
|
const TimePos pos = m_clips[clipNum1]->startPosition();
|
|
|
|
m_clips[clipNum1]->movePosition( m_clips[clipNum2]->startPosition() );
|
|
m_clips[clipNum2]->movePosition( pos );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Track::createClipsForPattern(int pattern)
|
|
{
|
|
while( numOfClips() < pattern + 1 )
|
|
{
|
|
TimePos position = TimePos( numOfClips(), 0 );
|
|
Clip * clip = createClip( position );
|
|
clip->changeLength( TimePos( 1, 0 ) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Move all the clips after a certain time later by one bar.
|
|
*
|
|
* \param pos The time at which we want to insert the bar.
|
|
* \todo if we stepped through this list last to first, and the list was
|
|
* in ascending order by Clip time, once we hit a Clip that was earlier
|
|
* than the insert time, we could fall out of the loop early.
|
|
*/
|
|
void Track::insertBar( const TimePos & pos )
|
|
{
|
|
// we'll increase the position of every Clip, positioned behind pos, by
|
|
// one bar
|
|
for( clipVector::iterator it = m_clips.begin();
|
|
it != m_clips.end(); ++it )
|
|
{
|
|
if( ( *it )->startPosition() >= pos )
|
|
{
|
|
( *it )->movePosition( (*it)->startPosition() +
|
|
TimePos::ticksPerBar() );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Move all the clips after a certain time earlier by one bar.
|
|
*
|
|
* \param pos The time at which we want to remove the bar.
|
|
*/
|
|
void Track::removeBar( const TimePos & pos )
|
|
{
|
|
// we'll decrease the position of every Clip, positioned behind pos, by
|
|
// one bar
|
|
for( clipVector::iterator it = m_clips.begin(); it != m_clips.end(); ++it )
|
|
{
|
|
if( ( *it )->startPosition() >= pos )
|
|
{
|
|
(*it)->movePosition((*it)->startPosition() - TimePos::ticksPerBar());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Return the length of the entire track in bars
|
|
*
|
|
* We step through our list of Clips and determine their end position,
|
|
* keeping track of the latest time found in ticks. Then we return
|
|
* that in bars by dividing by the number of ticks per bar.
|
|
*/
|
|
bar_t Track::length() const
|
|
{
|
|
// find last end-position
|
|
tick_t last = 0;
|
|
for( clipVector::const_iterator it = m_clips.begin(); it != m_clips.end(); ++it )
|
|
{
|
|
if( Engine::getSong()->isExporting() &&
|
|
( *it )->isMuted() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const tick_t cur = ( *it )->endPosition();
|
|
if( cur > last )
|
|
{
|
|
last = cur;
|
|
}
|
|
}
|
|
|
|
return last / TimePos::ticksPerBar();
|
|
}
|
|
|
|
|
|
|
|
/*! \brief Invert the track's solo state.
|
|
*
|
|
* We have to go through all the tracks determining if any other track
|
|
* is already soloed. Then we have to save the mute state of all tracks,
|
|
* and set our mute state to on and all the others to off.
|
|
*/
|
|
void Track::toggleSolo()
|
|
{
|
|
const TrackContainer::TrackList & tl = m_trackContainer->tracks();
|
|
|
|
bool soloBefore = false;
|
|
for( TrackContainer::TrackList::const_iterator it = tl.begin();
|
|
it != tl.end(); ++it )
|
|
{
|
|
if( *it != this )
|
|
{
|
|
if( ( *it )->m_soloModel.value() )
|
|
{
|
|
soloBefore = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool solo = m_soloModel.value();
|
|
// Should we use the new behavior of solo or the older/legacy one?
|
|
const bool soloLegacyBehavior = ConfigManager::inst()->value("app", "sololegacybehavior", "0").toInt();
|
|
|
|
for( TrackContainer::TrackList::const_iterator it = tl.begin();
|
|
it != tl.end(); ++it )
|
|
{
|
|
if( solo )
|
|
{
|
|
// save mute-state in case no track was solo before
|
|
if( !soloBefore )
|
|
{
|
|
( *it )->m_mutedBeforeSolo = ( *it )->isMuted();
|
|
}
|
|
// Don't mute AutomationTracks (keep their original state) unless we are on the sololegacybehavior mode
|
|
if( *it == this )
|
|
{
|
|
( *it )->setMuted( false );
|
|
}
|
|
else if( soloLegacyBehavior || ( *it )->type() != AutomationTrack )
|
|
{
|
|
( *it )->setMuted( true );
|
|
}
|
|
if( *it != this )
|
|
{
|
|
( *it )->m_soloModel.setValue( false );
|
|
}
|
|
}
|
|
else if( !soloBefore )
|
|
{
|
|
// Unless we are on the sololegacybehavior mode, only restores the
|
|
// mute state if the track isn't an Automation Track
|
|
if( soloLegacyBehavior || ( *it )->type() != AutomationTrack )
|
|
{
|
|
( *it )->setMuted( ( *it )->m_mutedBeforeSolo );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Track::setColor(const QColor& c)
|
|
{
|
|
m_hasColor = true;
|
|
m_color = c;
|
|
emit colorChanged();
|
|
}
|
|
|
|
void Track::resetColor()
|
|
{
|
|
m_hasColor = false;
|
|
emit colorChanged();
|
|
}
|
|
|
|
|
|
BoolModel *Track::getMutedModel()
|
|
{
|
|
return &m_mutedModel;
|
|
}
|
|
|