Files
lmms/src/core/Song.cpp
IanCaio fbea78945b Fix bug introduced by #5657 (#5982)
* Fix bug introduced by #5657

	There was a bug introduced by #5657 where reloading a project
and playing it could cause a Segmentation Fault crash. After some
debugging, @DomClark tracked the issue to be likely a use-after-free
being caused by m_oldAutomatedValues not being cleared when the project
was loaded again.
	This commit adds a line to clear the m_oldAutomatedValues map on
Song::clearProject(), which is called from Song::loadProject().
	Now, instead of using a Signal/Slot connection to move the
control of the models back to the controllers, every time the song is
processing the automations, the control of the models that were
processed in the last cycle are moved back to the controller. The same
is done under Song::stop(), so the last cycle models control is moved
back to the controller.
	That removes the need to have a pointer to the controlled model
in the controller object.
	Adds mixer model change request to avoid race condition.

Co-authored-by: Dominic Clark <mrdomclark@gmail.com>
2021-04-21 00:26:29 -05:00

1455 lines
32 KiB
C++

/*
* Song.cpp - root of the model tree
*
* 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.
*
*/
#include "Song.h"
#include <QTextStream>
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QMessageBox>
#include <algorithm>
#include <cmath>
#include <functional>
#include "AutomationTrack.h"
#include "AutomationEditor.h"
#include "BBEditor.h"
#include "BBTrack.h"
#include "BBTrackContainer.h"
#include "ConfigManager.h"
#include "ControllerRackView.h"
#include "ControllerConnection.h"
#include "embed.h"
#include "EnvelopeAndLfoParameters.h"
#include "FxMixer.h"
#include "FxMixerView.h"
#include "GuiApplication.h"
#include "ExportFilter.h"
#include "InstrumentTrack.h"
#include "NotePlayHandle.h"
#include "Pattern.h"
#include "PianoRoll.h"
#include "ProjectJournal.h"
#include "ProjectNotes.h"
#include "SongEditor.h"
#include "TimeLineWidget.h"
#include "PeakController.h"
tick_t TimePos::s_ticksPerBar = DefaultTicksPerBar;
Song::Song() :
TrackContainer(),
m_globalAutomationTrack( dynamic_cast<AutomationTrack *>(
Track::create( Track::HiddenAutomationTrack,
this ) ) ),
m_tempoModel( DefaultTempo, MinTempo, MaxTempo, this, tr( "Tempo" ) ),
m_timeSigModel( this ),
m_oldTicksPerBar( DefaultTicksPerBar ),
m_masterVolumeModel( 100, 0, 200, this, tr( "Master volume" ) ),
m_masterPitchModel( 0, -12, 12, this, tr( "Master pitch" ) ),
m_nLoadingTrack( 0 ),
m_fileName(),
m_oldFileName(),
m_modified( false ),
m_loadOnLaunch( true ),
m_recording( false ),
m_exporting( false ),
m_exportLoop( false ),
m_renderBetweenMarkers( false ),
m_playing( false ),
m_paused( false ),
m_savingProject( false ),
m_loadingProject( false ),
m_isCancelled( false ),
m_playMode( Mode_None ),
m_length( 0 ),
m_patternToPlay( NULL ),
m_loopPattern( false ),
m_elapsedTicks( 0 ),
m_elapsedBars( 0 ),
m_loopRenderCount(1),
m_loopRenderRemaining(1),
m_oldAutomatedValues()
{
for(int i = 0; i < Mode_Count; ++i) m_elapsedMilliSeconds[i] = 0;
connect( &m_tempoModel, SIGNAL( dataChanged() ),
this, SLOT( setTempo() ), Qt::DirectConnection );
connect( &m_tempoModel, SIGNAL( dataUnchanged() ),
this, SLOT( setTempo() ), Qt::DirectConnection );
connect( &m_timeSigModel, SIGNAL( dataChanged() ),
this, SLOT( setTimeSignature() ), Qt::DirectConnection );
connect( Engine::mixer(), SIGNAL( sampleRateChanged() ), this,
SLOT( updateFramesPerTick() ) );
connect( &m_masterVolumeModel, SIGNAL( dataChanged() ),
this, SLOT( masterVolumeChanged() ), Qt::DirectConnection );
/* connect( &m_masterPitchModel, SIGNAL( dataChanged() ),
this, SLOT( masterPitchChanged() ) );*/
qRegisterMetaType<Note>( "Note" );
setType( SongContainer );
}
Song::~Song()
{
m_playing = false;
delete m_globalAutomationTrack;
}
void Song::masterVolumeChanged()
{
Engine::mixer()->setMasterGain( m_masterVolumeModel.value() /
100.0f );
}
void Song::setTempo()
{
Engine::mixer()->requestChangeInModel();
const bpm_t tempo = ( bpm_t ) m_tempoModel.value();
PlayHandleList & playHandles = Engine::mixer()->playHandles();
for( PlayHandleList::Iterator it = playHandles.begin();
it != playHandles.end(); ++it )
{
NotePlayHandle * nph = dynamic_cast<NotePlayHandle *>( *it );
if( nph && !nph->isReleased() )
{
nph->lock();
nph->resize( tempo );
nph->unlock();
}
}
Engine::mixer()->doneChangeInModel();
Engine::updateFramesPerTick();
m_vstSyncController.setTempo( tempo );
emit tempoChanged( tempo );
}
void Song::setTimeSignature()
{
TimePos::setTicksPerBar( ticksPerBar() );
emit timeSignatureChanged( m_oldTicksPerBar, ticksPerBar() );
emit dataChanged();
m_oldTicksPerBar = ticksPerBar();
m_vstSyncController.setTimeSignature(
getTimeSigModel().getNumerator(), getTimeSigModel().getDenominator() );
}
void Song::savePos()
{
TimeLineWidget * tl = m_playPos[m_playMode].m_timeLine;
if( tl != NULL )
{
tl->savePos( m_playPos[m_playMode] );
}
}
void Song::processNextBuffer()
{
m_vstSyncController.setPlaybackJumped(false);
// If nothing is playing, there is nothing to do
if (!m_playing) { return; }
// At the beginning of the song, we have to reset the LFOs
if (m_playMode == Mode_PlaySong && getPlayPos() == 0)
{
EnvelopeAndLfoParameters::instances()->reset();
}
TrackList trackList;
int clipNum = -1; // The number of the clip that will be played
// Determine the list of tracks to play and the clip number
switch (m_playMode)
{
case Mode_PlaySong:
trackList = tracks();
break;
case Mode_PlayBB:
if (Engine::getBBTrackContainer()->numOfBBs() > 0)
{
clipNum = Engine::getBBTrackContainer()->currentBB();
trackList.push_back(BBTrack::findBBTrack(clipNum));
}
break;
case Mode_PlayPattern:
if (m_patternToPlay)
{
clipNum = m_patternToPlay->getTrack()->getTCONum(m_patternToPlay);
trackList.push_back(m_patternToPlay->getTrack());
}
break;
default:
return;
}
// If we have no tracks to play, there is nothing to do
if (trackList.empty()) { return; }
// If the playback position is outside of the range [begin, end), move it to
// begin and inform interested parties.
// Returns true if the playback position was moved, else false.
const auto enforceLoop = [this](const TimePos& begin, const TimePos& end)
{
if (getPlayPos() < begin || getPlayPos() >= end)
{
setToTime(begin);
m_vstSyncController.setPlaybackJumped(true);
emit updateSampleTracks();
return true;
}
return false;
};
const auto timeline = getPlayPos().m_timeLine;
const auto loopEnabled = !m_exporting && timeline && timeline->loopPointsEnabled();
// Ensure playback begins within the loop if it is enabled
if (loopEnabled) { enforceLoop(timeline->loopBegin(), timeline->loopEnd()); }
// Inform VST plugins if the user moved the play head
if (getPlayPos().jumped())
{
m_vstSyncController.setPlaybackJumped(true);
getPlayPos().setJumped(false);
}
const auto framesPerTick = Engine::framesPerTick();
const auto framesPerPeriod = Engine::mixer()->framesPerPeriod();
f_cnt_t frameOffsetInPeriod = 0;
while (frameOffsetInPeriod < framesPerPeriod)
{
auto frameOffsetInTick = getPlayPos().currentFrame();
// If a whole tick has elapsed, update the frame and tick count, and check any loops
if (frameOffsetInTick >= framesPerTick)
{
// Transfer any whole ticks from the frame count to the tick count
const auto elapsedTicks = static_cast<int>(frameOffsetInTick / framesPerTick);
getPlayPos().setTicks(getPlayPos().getTicks() + elapsedTicks);
frameOffsetInTick -= elapsedTicks * framesPerTick;
getPlayPos().setCurrentFrame(frameOffsetInTick);
// If we are playing a BB track, or a pattern with no loop enabled,
// loop back to the beginning when we reach the end
if (m_playMode == Mode_PlayBB)
{
enforceLoop(TimePos{0}, TimePos{Engine::getBBTrackContainer()->lengthOfCurrentBB(), 0});
}
else if (m_playMode == Mode_PlayPattern && m_loopPattern && !loopEnabled)
{
enforceLoop(TimePos{0}, m_patternToPlay->length());
}
// Handle loop points, and inform VST plugins of the loop status
if (loopEnabled || (m_loopRenderRemaining > 1 && getPlayPos() >= timeline->loopBegin()))
{
m_vstSyncController.startCycle(
timeline->loopBegin().getTicks(), timeline->loopEnd().getTicks());
// Loop if necessary, and decrement the remaining loops if we did
if (enforceLoop(timeline->loopBegin(), timeline->loopEnd())
&& m_loopRenderRemaining > 1)
{
m_loopRenderRemaining--;
}
}
else
{
m_vstSyncController.stopCycle();
}
}
const f_cnt_t framesUntilNextPeriod = framesPerPeriod - frameOffsetInPeriod;
const f_cnt_t framesUntilNextTick = static_cast<f_cnt_t>(std::ceil(framesPerTick - frameOffsetInTick));
// We want to proceed to the next buffer or tick, whichever is closer
const auto framesToPlay = std::min(framesUntilNextPeriod, framesUntilNextTick);
if (frameOffsetInPeriod == 0)
{
// First frame of buffer: update VST sync position.
// This must be done after we've corrected the frame/tick count,
// but before actually playing any frames.
m_vstSyncController.setAbsolutePosition(getPlayPos().getTicks()
+ getPlayPos().currentFrame() / static_cast<double>(framesPerTick));
m_vstSyncController.update();
}
if (static_cast<f_cnt_t>(frameOffsetInTick) == 0)
{
// First frame of tick: process automation and play tracks
processAutomations(trackList, getPlayPos(), framesToPlay);
for (const auto track : trackList)
{
track->play(getPlayPos(), framesToPlay, frameOffsetInPeriod, clipNum);
}
}
// Update frame counters
frameOffsetInPeriod += framesToPlay;
frameOffsetInTick += framesToPlay;
getPlayPos().setCurrentFrame(frameOffsetInTick);
m_elapsedMilliSeconds[m_playMode] += TimePos::ticksToMilliseconds(framesToPlay / framesPerTick, getTempo());
m_elapsedBars = m_playPos[Mode_PlaySong].getBar();
m_elapsedTicks = (m_playPos[Mode_PlaySong].getTicks() % ticksPerBar()) / 48;
}
}
void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp_t)
{
AutomatedValueMap values;
QSet<const AutomatableModel*> recordedModels;
TrackContainer* container = this;
int tcoNum = -1;
switch (m_playMode)
{
case Mode_PlaySong:
break;
case Mode_PlayBB:
{
Q_ASSERT(tracklist.size() == 1);
Q_ASSERT(tracklist.at(0)->type() == Track::BBTrack);
auto bbTrack = dynamic_cast<BBTrack*>(tracklist.at(0));
auto bbContainer = Engine::getBBTrackContainer();
container = bbContainer;
tcoNum = bbTrack->index();
}
break;
default:
return;
}
values = container->automatedValuesAt(timeStart, tcoNum);
TrackList tracks = container->tracks();
Track::tcoVector tcos;
for (Track* track : tracks)
{
if (track->type() == Track::AutomationTrack) {
track->getTCOsInRange(tcos, 0, timeStart);
}
}
// Process recording
for (TrackContentObject* tco : tcos)
{
auto p = dynamic_cast<AutomationPattern *>(tco);
TimePos relTime = timeStart - p->startPosition();
if (p->isRecording() && relTime >= 0 && relTime < p->length())
{
const AutomatableModel* recordedModel = p->firstObject();
p->recordValue(relTime, recordedModel->value<float>());
recordedModels << recordedModel;
}
}
// Checks if an automated model stopped being automated by automation patterns
// so we can move the control back to any connected controller again
for (auto it = m_oldAutomatedValues.begin(); it != m_oldAutomatedValues.end(); it++)
{
AutomatableModel * am = it.key();
if (am->controllerConnection() && !values.contains(am))
{
am->setUseControllerValue(true);
}
}
m_oldAutomatedValues = values;
// Apply values
for (auto it = values.begin(); it != values.end(); it++)
{
if (! recordedModels.contains(it.key()))
{
it.key()->setAutomatedValue(it.value());
}
else if (!it.key()->useControllerValue())
{
it.key()->setUseControllerValue(true);
}
}
}
void Song::setModified(bool value)
{
if( !m_loadingProject && m_modified != value)
{
m_modified = value;
emit modified();
}
}
bool Song::isExportDone() const
{
return !isExporting() || m_playPos[m_playMode] >= m_exportSongEnd;
}
int Song::getExportProgress() const
{
TimePos pos = m_playPos[m_playMode];
if (pos >= m_exportSongEnd)
{
return 100;
}
else if (pos <= m_exportSongBegin)
{
return 0;
}
else if (pos >= m_exportLoopEnd)
{
pos = (m_exportLoopBegin-m_exportSongBegin) + (m_exportLoopEnd - m_exportLoopBegin) *
m_loopRenderCount + (pos - m_exportLoopEnd);
}
else if ( pos >= m_exportLoopBegin )
{
pos = (m_exportLoopBegin-m_exportSongBegin) + ((m_exportLoopEnd - m_exportLoopBegin) *
(m_loopRenderCount - m_loopRenderRemaining)) + (pos - m_exportLoopBegin);
}
else
{
pos = (pos - m_exportSongBegin);
}
return (float)pos/(float)m_exportEffectiveLength*100.0f;
}
void Song::playSong()
{
m_recording = false;
if( isStopped() == false )
{
stop();
}
m_playMode = Mode_PlaySong;
m_playing = true;
m_paused = false;
m_vstSyncController.setPlaybackState( true );
savePos();
emit playbackStateChanged();
}
void Song::record()
{
m_recording = true;
// TODO: Implement
}
void Song::playAndRecord()
{
playSong();
m_recording = true;
}
void Song::playBB()
{
if( isStopped() == false )
{
stop();
}
m_playMode = Mode_PlayBB;
m_playing = true;
m_paused = false;
m_vstSyncController.setPlaybackState( true );
savePos();
emit playbackStateChanged();
}
void Song::playPattern( const Pattern* patternToPlay, bool loop )
{
if( isStopped() == false )
{
stop();
}
m_patternToPlay = patternToPlay;
m_loopPattern = loop;
if( m_patternToPlay != NULL )
{
m_playMode = Mode_PlayPattern;
m_playing = true;
m_paused = false;
}
savePos();
emit playbackStateChanged();
}
void Song::updateLength()
{
m_length = 0;
m_tracksMutex.lockForRead();
for (auto track : tracks())
{
if (m_exporting && track->isMuted())
{
continue;
}
const bar_t cur = track->length();
if( cur > m_length )
{
m_length = cur;
}
}
m_tracksMutex.unlock();
emit lengthChanged( m_length );
}
void Song::setPlayPos( tick_t ticks, PlayModes playMode )
{
tick_t ticksFromPlayMode = m_playPos[playMode].getTicks();
m_elapsedTicks += ticksFromPlayMode - ticks;
m_elapsedMilliSeconds[playMode] += TimePos::ticksToMilliseconds( ticks - ticksFromPlayMode, getTempo() );
m_playPos[playMode].setTicks( ticks );
m_playPos[playMode].setCurrentFrame( 0.0f );
m_playPos[playMode].setJumped( true );
// send a signal if playposition changes during playback
if( isPlaying() )
{
emit playbackPositionChanged();
emit updateSampleTracks();
}
}
void Song::togglePause()
{
if( m_paused == true )
{
m_playing = true;
m_paused = false;
}
else
{
m_playing = false;
m_paused = true;
}
m_vstSyncController.setPlaybackState( m_playing );
emit playbackStateChanged();
}
void Song::stop()
{
// do not stop/reset things again if we're stopped already
if( m_playMode == Mode_None )
{
return;
}
// To avoid race conditions with the processing threads
Engine::mixer()->requestChangeInModel();
TimeLineWidget * tl = m_playPos[m_playMode].m_timeLine;
m_paused = false;
m_recording = true;
if( tl )
{
switch( tl->behaviourAtStop() )
{
case TimeLineWidget::BackToZero:
m_playPos[m_playMode].setTicks(0);
m_elapsedMilliSeconds[m_playMode] = 0;
break;
case TimeLineWidget::BackToStart:
if( tl->savedPos() >= 0 )
{
m_playPos[m_playMode].setTicks(tl->savedPos().getTicks());
setToTime(tl->savedPos());
tl->savePos( -1 );
}
break;
case TimeLineWidget::KeepStopPosition:
break;
}
}
else
{
m_playPos[m_playMode].setTicks( 0 );
m_elapsedMilliSeconds[m_playMode] = 0;
}
m_playing = false;
m_elapsedMilliSeconds[Mode_None] = m_elapsedMilliSeconds[m_playMode];
m_playPos[Mode_None].setTicks(m_playPos[m_playMode].getTicks());
m_playPos[m_playMode].setCurrentFrame( 0 );
m_vstSyncController.setPlaybackState( m_exporting );
m_vstSyncController.setAbsolutePosition(
m_playPos[m_playMode].getTicks()
+ m_playPos[m_playMode].currentFrame()
/ (double) Engine::framesPerTick() );
// remove all note-play-handles that are active
Engine::mixer()->clear();
// Moves the control of the models that were processed on the last frame
// back to their controllers.
for (auto it = m_oldAutomatedValues.begin(); it != m_oldAutomatedValues.end(); it++)
{
AutomatableModel * am = it.key();
am->setUseControllerValue(true);
}
m_oldAutomatedValues.clear();
m_playMode = Mode_None;
Engine::mixer()->doneChangeInModel();
emit stopped();
emit playbackStateChanged();
}
void Song::startExport()
{
stop();
m_exporting = true;
updateLength();
if (m_renderBetweenMarkers)
{
m_exportSongBegin = m_exportLoopBegin = m_playPos[Mode_PlaySong].m_timeLine->loopBegin();
m_exportSongEnd = m_exportLoopEnd = m_playPos[Mode_PlaySong].m_timeLine->loopEnd();
m_playPos[Mode_PlaySong].setTicks( m_playPos[Mode_PlaySong].m_timeLine->loopBegin().getTicks() );
}
else
{
m_exportSongEnd = TimePos(m_length, 0);
// Handle potentially ridiculous loop points gracefully.
if (m_loopRenderCount > 1 && m_playPos[Mode_PlaySong].m_timeLine->loopEnd() > m_exportSongEnd)
{
m_exportSongEnd = m_playPos[Mode_PlaySong].m_timeLine->loopEnd();
}
if (!m_exportLoop)
m_exportSongEnd += TimePos(1,0);
m_exportSongBegin = TimePos(0,0);
// FIXME: remove this check once we load timeline in headless mode
if (m_playPos[Mode_PlaySong].m_timeLine)
{
m_exportLoopBegin = m_playPos[Mode_PlaySong].m_timeLine->loopBegin() < m_exportSongEnd &&
m_playPos[Mode_PlaySong].m_timeLine->loopEnd() <= m_exportSongEnd ?
m_playPos[Mode_PlaySong].m_timeLine->loopBegin() : TimePos(0,0);
m_exportLoopEnd = m_playPos[Mode_PlaySong].m_timeLine->loopBegin() < m_exportSongEnd &&
m_playPos[Mode_PlaySong].m_timeLine->loopEnd() <= m_exportSongEnd ?
m_playPos[Mode_PlaySong].m_timeLine->loopEnd() : TimePos(0,0);
}
m_playPos[Mode_PlaySong].setTicks( 0 );
}
m_exportEffectiveLength = (m_exportLoopBegin - m_exportSongBegin) + (m_exportLoopEnd - m_exportLoopBegin)
* m_loopRenderCount + (m_exportSongEnd - m_exportLoopEnd);
m_loopRenderRemaining = m_loopRenderCount;
playSong();
m_vstSyncController.setPlaybackState( true );
}
void Song::stopExport()
{
stop();
m_exporting = false;
m_vstSyncController.setPlaybackState( m_playing );
}
void Song::insertBar()
{
m_tracksMutex.lockForRead();
for( TrackList::const_iterator it = tracks().begin();
it != tracks().end(); ++it )
{
( *it )->insertBar( m_playPos[Mode_PlaySong] );
}
m_tracksMutex.unlock();
}
void Song::removeBar()
{
m_tracksMutex.lockForRead();
for( TrackList::const_iterator it = tracks().begin();
it != tracks().end(); ++it )
{
( *it )->removeBar( m_playPos[Mode_PlaySong] );
}
m_tracksMutex.unlock();
}
void Song::addBBTrack()
{
Track * t = Track::create( Track::BBTrack, this );
Engine::getBBTrackContainer()->setCurrentBB( dynamic_cast<BBTrack *>( t )->index() );
}
void Song::addSampleTrack()
{
( void )Track::create( Track::SampleTrack, this );
}
void Song::addAutomationTrack()
{
( void )Track::create( Track::AutomationTrack, this );
}
bpm_t Song::getTempo()
{
return ( bpm_t )m_tempoModel.value();
}
AutomationPattern * Song::tempoAutomationPattern()
{
return AutomationPattern::globalAutomationPattern( &m_tempoModel );
}
AutomatedValueMap Song::automatedValuesAt(TimePos time, int tcoNum) const
{
return TrackContainer::automatedValuesFromTracks(TrackList{m_globalAutomationTrack} << tracks(), time, tcoNum);
}
void Song::clearProject()
{
Engine::projectJournal()->setJournalling( false );
if( m_playing )
{
stop();
}
for( int i = 0; i < Mode_Count; i++ )
{
setPlayPos( 0, ( PlayModes )i );
}
Engine::mixer()->requestChangeInModel();
if( gui && gui->getBBEditor() )
{
gui->getBBEditor()->trackContainerView()->clearAllTracks();
}
if( gui && gui->songEditor() )
{
gui->songEditor()->m_editor->clearAllTracks();
}
if( gui && gui->fxMixerView() )
{
gui->fxMixerView()->clear();
}
QCoreApplication::sendPostedEvents();
Engine::getBBTrackContainer()->clearAllTracks();
clearAllTracks();
Engine::fxMixer()->clear();
if( gui && gui->automationEditor() )
{
gui->automationEditor()->setCurrentPattern( NULL );
}
if( gui && gui->pianoRoll() )
{
gui->pianoRoll()->reset();
}
m_tempoModel.reset();
m_masterVolumeModel.reset();
m_masterPitchModel.reset();
m_timeSigModel.reset();
// Clear the m_oldAutomatedValues AutomatedValueMap
m_oldAutomatedValues.clear();
AutomationPattern::globalAutomationPattern( &m_tempoModel )->clear();
AutomationPattern::globalAutomationPattern( &m_masterVolumeModel )->
clear();
AutomationPattern::globalAutomationPattern( &m_masterPitchModel )->
clear();
Engine::mixer()->doneChangeInModel();
if( gui && gui->getProjectNotes() )
{
gui->getProjectNotes()->clear();
}
removeAllControllers();
emit dataChanged();
Engine::projectJournal()->clearJournal();
Engine::projectJournal()->setJournalling( true );
}
// create new file
void Song::createNewProject()
{
QString defaultTemplate = ConfigManager::inst()->userTemplateDir()
+ "default.mpt";
if( QFile::exists( defaultTemplate ) )
{
createNewProjectFromTemplate( defaultTemplate );
return;
}
defaultTemplate = ConfigManager::inst()->factoryProjectsDir()
+ "templates/default.mpt";
if( QFile::exists( defaultTemplate ) )
{
createNewProjectFromTemplate( defaultTemplate );
return;
}
m_loadingProject = true;
clearProject();
Engine::projectJournal()->setJournalling( false );
m_oldFileName = "";
setProjectFileName("");
Track * t;
t = Track::create( Track::InstrumentTrack, this );
dynamic_cast<InstrumentTrack * >( t )->loadInstrument(
"tripleoscillator" );
t = Track::create( Track::InstrumentTrack,
Engine::getBBTrackContainer() );
dynamic_cast<InstrumentTrack * >( t )->loadInstrument(
"kicker" );
Track::create( Track::SampleTrack, this );
Track::create( Track::BBTrack, this );
Track::create( Track::AutomationTrack, this );
m_tempoModel.setInitValue( DefaultTempo );
m_timeSigModel.reset();
m_masterVolumeModel.setInitValue( 100 );
m_masterPitchModel.setInitValue( 0 );
QCoreApplication::instance()->processEvents();
m_loadingProject = false;
Engine::getBBTrackContainer()->updateAfterTrackAdd();
Engine::projectJournal()->setJournalling( true );
QCoreApplication::sendPostedEvents();
setModified(false);
m_loadOnLaunch = false;
}
void Song::createNewProjectFromTemplate( const QString & templ )
{
loadProject( templ );
// clear file-name so that user doesn't overwrite template when
// saving...
m_oldFileName = "";
setProjectFileName("");
// update window title
m_loadOnLaunch = false;
}
// load given song
void Song::loadProject( const QString & fileName )
{
QDomNode node;
m_loadingProject = true;
Engine::projectJournal()->setJournalling( false );
m_oldFileName = m_fileName;
setProjectFileName(fileName);
DataFile dataFile( m_fileName );
bool cantLoadProject = false;
// if file could not be opened, head-node is null and we create
// new project
if( dataFile.head().isNull() )
{
cantLoadProject = true;
}
else
{
// We check if plugins contain local paths to prevent malicious code being
// added to project bundles and loaded with "local:" paths
if (dataFile.hasLocalPlugins())
{
cantLoadProject = true;
if (gui)
{
QMessageBox::critical(NULL, tr("Aborting project load"),
tr("Project file contains local paths to plugins, which could be used to "
"run malicious code."));
}
else
{
QTextStream(stderr) << tr("Can't load project: "
"Project file contains local paths to plugins.") << endl;
}
}
}
if (cantLoadProject)
{
if( m_loadOnLaunch )
{
createNewProject();
}
setProjectFileName(m_oldFileName);
return;
}
m_oldFileName = m_fileName;
clearProject();
clearErrors();
Engine::mixer()->requestChangeInModel();
// get the header information from the DOM
m_tempoModel.loadSettings( dataFile.head(), "bpm" );
m_timeSigModel.loadSettings( dataFile.head(), "timesig" );
m_masterVolumeModel.loadSettings( dataFile.head(), "mastervol" );
m_masterPitchModel.loadSettings( dataFile.head(), "masterpitch" );
if( m_playPos[Mode_PlaySong].m_timeLine )
{
// reset loop-point-state
m_playPos[Mode_PlaySong].m_timeLine->toggleLoopPoints( 0 );
}
if( !dataFile.content().firstChildElement( "track" ).isNull() )
{
m_globalAutomationTrack->restoreState( dataFile.content().
firstChildElement( "track" ) );
}
//Backward compatibility for LMMS <= 0.4.15
PeakController::initGetControllerBySetting();
// Load mixer first to be able to set the correct range for FX channels
node = dataFile.content().firstChildElement( Engine::fxMixer()->nodeName() );
if( !node.isNull() )
{
Engine::fxMixer()->restoreState( node.toElement() );
if( gui )
{
// refresh FxMixerView
gui->fxMixerView()->refreshDisplay();
}
}
node = dataFile.content().firstChild();
QDomNodeList tclist=dataFile.content().elementsByTagName("trackcontainer");
m_nLoadingTrack=0;
for( int i=0,n=tclist.count(); i<n; ++i )
{
QDomNode nd=tclist.at(i).firstChild();
while(!nd.isNull())
{
if( nd.isElement() && nd.nodeName() == "track" )
{
++m_nLoadingTrack;
if( nd.toElement().attribute("type").toInt() == Track::BBTrack )
{
n += nd.toElement().elementsByTagName("bbtrack").at(0)
.toElement().firstChildElement().childNodes().count();
}
nd=nd.nextSibling();
}
}
}
while( !node.isNull() && !isCancelled() )
{
if( node.isElement() )
{
if( node.nodeName() == "trackcontainer" )
{
( (JournallingObject *)( this ) )->restoreState( node.toElement() );
}
else if( node.nodeName() == "controllers" )
{
restoreControllerStates( node.toElement() );
}
else if( gui )
{
if( node.nodeName() == gui->getControllerRackView()->nodeName() )
{
gui->getControllerRackView()->restoreState( node.toElement() );
}
else if( node.nodeName() == gui->pianoRoll()->nodeName() )
{
gui->pianoRoll()->restoreState( node.toElement() );
}
else if( node.nodeName() == gui->automationEditor()->m_editor->nodeName() )
{
gui->automationEditor()->m_editor->restoreState( node.toElement() );
}
else if( node.nodeName() == gui->getProjectNotes()->nodeName() )
{
gui->getProjectNotes()->SerializingObject::restoreState( node.toElement() );
}
else if( node.nodeName() == m_playPos[Mode_PlaySong].m_timeLine->nodeName() )
{
m_playPos[Mode_PlaySong].m_timeLine->restoreState( node.toElement() );
}
}
}
node = node.nextSibling();
}
// quirk for fixing projects with broken positions of TCOs inside
// BB-tracks
Engine::getBBTrackContainer()->fixIncorrectPositions();
// Connect controller links to their controllers
// now that everything is loaded
ControllerConnection::finalizeConnections();
// Remove dummy controllers that was added for correct connections
m_controllers.erase(std::remove_if(m_controllers.begin(), m_controllers.end(),
[](Controller* c){return c->type() == Controller::DummyController;}),
m_controllers.end());
// resolve all IDs so that autoModels are automated
AutomationPattern::resolveAllIDs();
Engine::mixer()->doneChangeInModel();
ConfigManager::inst()->addRecentlyOpenedProject( fileName );
Engine::projectJournal()->setJournalling( true );
emit projectLoaded();
if( isCancelled() )
{
m_isCancelled = false;
createNewProject();
return;
}
if ( hasErrors())
{
if ( gui )
{
QMessageBox::warning( NULL, tr("LMMS Error report"), errorSummary(),
QMessageBox::Ok );
}
else
{
#if (QT_VERSION >= QT_VERSION_CHECK(5,15,0))
QTextStream(stderr) << Engine::getSong()->errorSummary() << Qt::endl;
#else
QTextStream(stderr) << Engine::getSong()->errorSummary() << endl;
#endif
}
}
m_loadingProject = false;
setModified(false);
m_loadOnLaunch = false;
}
// only save current song as filename and do nothing else
bool Song::saveProjectFile(const QString & filename, bool withResources)
{
DataFile dataFile( DataFile::SongProject );
m_savingProject = true;
m_tempoModel.saveSettings( dataFile, dataFile.head(), "bpm" );
m_timeSigModel.saveSettings( dataFile, dataFile.head(), "timesig" );
m_masterVolumeModel.saveSettings( dataFile, dataFile.head(), "mastervol" );
m_masterPitchModel.saveSettings( dataFile, dataFile.head(), "masterpitch" );
saveState( dataFile, dataFile.content() );
m_globalAutomationTrack->saveState( dataFile, dataFile.content() );
Engine::fxMixer()->saveState( dataFile, dataFile.content() );
if( gui )
{
gui->getControllerRackView()->saveState( dataFile, dataFile.content() );
gui->pianoRoll()->saveState( dataFile, dataFile.content() );
gui->automationEditor()->m_editor->saveState( dataFile, dataFile.content() );
gui->getProjectNotes()->SerializingObject::saveState( dataFile, dataFile.content() );
m_playPos[Mode_PlaySong].m_timeLine->saveState( dataFile, dataFile.content() );
}
saveControllerStates( dataFile, dataFile.content() );
m_savingProject = false;
return dataFile.writeFile(filename, withResources);
}
// Save the current song
bool Song::guiSaveProject()
{
return guiSaveProjectAs(m_fileName);
}
// Save the current song with the given filename
bool Song::guiSaveProjectAs(const QString & filename)
{
DataFile dataFile(DataFile::SongProject);
QString fileNameWithExtension = dataFile.nameWithExtension(filename);
bool withResources = m_saveOptions.saveAsProjectBundle.value();
bool const saveResult = saveProjectFile(fileNameWithExtension, withResources);
// After saving, restore default save options.
m_saveOptions.setDefaultOptions();
// If we saved a bundle, we keep the project on the original
// file and still keep it as modified
if (saveResult && !withResources)
{
setModified(false);
setProjectFileName(fileNameWithExtension);
}
return saveResult;
}
void Song::saveControllerStates( QDomDocument & doc, QDomElement & element )
{
// save settings of controllers
QDomElement controllersNode = doc.createElement( "controllers" );
element.appendChild( controllersNode );
for( int i = 0; i < m_controllers.size(); ++i )
{
m_controllers[i]->saveState( doc, controllersNode );
}
}
void Song::restoreControllerStates( const QDomElement & element )
{
QDomNode node = element.firstChild();
while( !node.isNull() && !isCancelled() )
{
Controller * c = Controller::create( node.toElement(), this );
if (c) {addController(c);}
else
{
// Fix indices to ensure correct connections
m_controllers.append(Controller::create(
Controller::DummyController, this));
}
node = node.nextSibling();
}
}
void Song::removeAllControllers()
{
while (m_controllers.size() != 0)
{
removeController(m_controllers.at(0));
}
m_controllers.clear();
}
void Song::exportProjectMidi(QString const & exportFileName) const
{
// instantiate midi export plugin
TrackContainer::TrackList const & tracks = this->tracks();
TrackContainer::TrackList const & tracks_BB = Engine::getBBTrackContainer()->tracks();
ExportFilter *exf = dynamic_cast<ExportFilter *> (Plugin::instantiate("midiexport", nullptr, nullptr));
if (exf)
{
exf->tryExport(tracks, tracks_BB, getTempo(), m_masterPitchModel.value(), exportFileName);
}
else
{
qDebug() << "failed to load midi export filter!";
}
}
void Song::updateFramesPerTick()
{
Engine::updateFramesPerTick();
}
void Song::setModified()
{
setModified(true);
}
void Song::setProjectFileName(QString const & projectFileName)
{
if (m_fileName != projectFileName)
{
m_fileName = projectFileName;
emit projectFileNameChanged();
}
}
void Song::addController( Controller * controller )
{
if( controller && !m_controllers.contains( controller ) )
{
m_controllers.append( controller );
emit controllerAdded( controller );
this->setModified();
}
}
void Song::removeController( Controller * controller )
{
int index = m_controllers.indexOf( controller );
if( index != -1 )
{
m_controllers.remove( index );
emit controllerRemoved( controller );
delete controller;
this->setModified();
}
}
void Song::clearErrors()
{
m_errors.clear();
}
void Song::collectError( const QString error )
{
if (!m_errors.contains(error)) { m_errors[error] = 1; }
else { m_errors[ error ]++; }
}
bool Song::hasErrors()
{
return !(m_errors.isEmpty());
}
QString Song::errorSummary()
{
QString errors;
auto i = m_errors.constBegin();
while (i != m_errors.constEnd())
{
errors.append( i.key() );
if( i.value() > 1 )
{
errors.append( tr(" (repeated %1 times)").arg( i.value() ) );
}
errors.append("\n");
++i;
}
errors.prepend( "\n\n" );
errors.prepend( tr( "The following errors occurred while loading: " ) );
return errors;
}
bool Song::isSavingProject() const {
return m_savingProject;
}