mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-15 04:27:57 -04:00
Finally remove Song's dependency to MainWindow by moving the window title update which is triggered due to a changed project file name from the class Song into MainWindow. Implementation details: Add a new signal projectFileNameChanged to Song and connect the new slot method MainWindow::onProjectFileNameChanged to it. Update the window title whenever the slot is triggered. Add a new private method Song::setProjectFileName which sets the project file name and emits the signal (only if the file name really changes). Call setProjectFileName everywhere where m_fileName was manipulated directly. This is done to ensure that the signal can be potentially emitted in all relevant situations. Remove the calls to gui->mainWindow from Song::createNewProjectFromTemplate. These changes finally remove the include to "MainWindow.h". The include for "ConfigManager.h" was previously done implicitly through the include of "MainWindow.h" and therefore had to be added explicitly after the latter is removed.
1369 lines
28 KiB
C++
1369 lines
28 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 <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 "Pattern.h"
|
|
#include "PianoRoll.h"
|
|
#include "ProjectJournal.h"
|
|
#include "ProjectNotes.h"
|
|
#include "SongEditor.h"
|
|
#include "TimeLineWidget.h"
|
|
#include "PeakController.h"
|
|
|
|
|
|
tick_t MidiTime::s_ticksPerTact = DefaultTicksPerTact;
|
|
|
|
|
|
|
|
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_oldTicksPerTact( DefaultTicksPerTact ),
|
|
m_masterVolumeModel( 100, 0, 200, this, tr( "Master volume" ) ),
|
|
m_masterPitchModel( 0, -12, 12, this, tr( "Master pitch" ) ),
|
|
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_loadingProject( false ),
|
|
m_isCancelled( false ),
|
|
m_playMode( Mode_None ),
|
|
m_length( 0 ),
|
|
m_patternToPlay( NULL ),
|
|
m_loopPattern( false ),
|
|
m_elapsedTicks( 0 ),
|
|
m_elapsedTacts( 0 )
|
|
{
|
|
for(int i = 0; i < Mode_Count; ++i) m_elapsedMilliSeconds[i] = 0;
|
|
connect( &m_tempoModel, SIGNAL( dataChanged() ),
|
|
this, SLOT( setTempo() ) );
|
|
connect( &m_tempoModel, SIGNAL( dataUnchanged() ),
|
|
this, SLOT( setTempo() ) );
|
|
connect( &m_timeSigModel, SIGNAL( dataChanged() ),
|
|
this, SLOT( setTimeSignature() ) );
|
|
|
|
|
|
connect( Engine::mixer(), SIGNAL( sampleRateChanged() ), this,
|
|
SLOT( updateFramesPerTick() ) );
|
|
|
|
connect( &m_masterVolumeModel, SIGNAL( dataChanged() ),
|
|
this, SLOT( masterVolumeChanged() ) );
|
|
/* 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()
|
|
{
|
|
MidiTime::setTicksPerTact( ticksPerTact() );
|
|
emit timeSignatureChanged( m_oldTicksPerTact, ticksPerTact() );
|
|
emit dataChanged();
|
|
m_oldTicksPerTact = ticksPerTact();
|
|
|
|
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()
|
|
{
|
|
// if not playing, nothing to do
|
|
if( m_playing == false )
|
|
{
|
|
return;
|
|
}
|
|
|
|
TrackList trackList;
|
|
int tcoNum = -1; // track content object number
|
|
|
|
// determine the list of tracks to play and the track content object
|
|
// (TCO) number
|
|
switch( m_playMode )
|
|
{
|
|
case Mode_PlaySong:
|
|
trackList = tracks();
|
|
// at song-start we have to reset the LFOs
|
|
if( m_playPos[Mode_PlaySong] == 0 )
|
|
{
|
|
EnvelopeAndLfoParameters::instances()->reset();
|
|
}
|
|
break;
|
|
|
|
case Mode_PlayBB:
|
|
if( Engine::getBBTrackContainer()->numOfBBs() > 0 )
|
|
{
|
|
tcoNum = Engine::getBBTrackContainer()->
|
|
currentBB();
|
|
trackList.push_back( BBTrack::findBBTrack(
|
|
tcoNum ) );
|
|
}
|
|
break;
|
|
|
|
case Mode_PlayPattern:
|
|
if( m_patternToPlay != NULL )
|
|
{
|
|
tcoNum = m_patternToPlay->getTrack()->
|
|
getTCONum( m_patternToPlay );
|
|
trackList.push_back(
|
|
m_patternToPlay->getTrack() );
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
|
|
}
|
|
|
|
// if we have no tracks to play, nothing to do
|
|
if( trackList.empty() == true )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// check for looping-mode and act if necessary
|
|
TimeLineWidget * tl = m_playPos[m_playMode].m_timeLine;
|
|
bool checkLoop =
|
|
tl != NULL && m_exporting == false && tl->loopPointsEnabled();
|
|
|
|
if( checkLoop )
|
|
{
|
|
// if looping-mode is enabled and we are outside of the looping
|
|
// range, go to the beginning of the range
|
|
if( m_playPos[m_playMode] < tl->loopBegin() ||
|
|
m_playPos[m_playMode] >= tl->loopEnd() )
|
|
{
|
|
setToTime(tl->loopBegin());
|
|
m_playPos[m_playMode].setTicks(
|
|
tl->loopBegin().getTicks() );
|
|
emit updateSampleTracks();
|
|
}
|
|
}
|
|
|
|
f_cnt_t framesPlayed = 0;
|
|
const float framesPerTick = Engine::framesPerTick();
|
|
|
|
while( framesPlayed < Engine::mixer()->framesPerPeriod() )
|
|
{
|
|
m_vstSyncController.update();
|
|
|
|
float currentFrame = m_playPos[m_playMode].currentFrame();
|
|
// did we play a tick?
|
|
if( currentFrame >= framesPerTick )
|
|
{
|
|
int ticks = m_playPos[m_playMode].getTicks() +
|
|
( int )( currentFrame / framesPerTick );
|
|
|
|
m_vstSyncController.setAbsolutePosition( ticks );
|
|
|
|
// did we play a whole tact?
|
|
if( ticks >= MidiTime::ticksPerTact() )
|
|
{
|
|
// per default we just continue playing even if
|
|
// there's no more stuff to play
|
|
// (song-play-mode)
|
|
int maxTact = m_playPos[m_playMode].getTact()
|
|
+ 2;
|
|
|
|
// then decide whether to go over to next tact
|
|
// or to loop back to first tact
|
|
if( m_playMode == Mode_PlayBB )
|
|
{
|
|
maxTact = Engine::getBBTrackContainer()
|
|
->lengthOfCurrentBB();
|
|
}
|
|
else if( m_playMode == Mode_PlayPattern &&
|
|
m_loopPattern == true &&
|
|
tl != NULL &&
|
|
tl->loopPointsEnabled() == false )
|
|
{
|
|
maxTact = m_patternToPlay->length()
|
|
.getTact();
|
|
}
|
|
|
|
// end of played object reached?
|
|
if( m_playPos[m_playMode].getTact() + 1
|
|
>= maxTact )
|
|
{
|
|
// then start from beginning and keep
|
|
// offset
|
|
ticks %= ( maxTact * MidiTime::ticksPerTact() );
|
|
|
|
// wrap milli second counter
|
|
setToTimeByTicks(ticks);
|
|
|
|
m_vstSyncController.setAbsolutePosition( ticks );
|
|
}
|
|
}
|
|
m_playPos[m_playMode].setTicks( ticks );
|
|
|
|
if( checkLoop )
|
|
{
|
|
m_vstSyncController.startCycle(
|
|
tl->loopBegin().getTicks(), tl->loopEnd().getTicks() );
|
|
|
|
// if looping-mode is enabled and we have got
|
|
// past the looping range, return to the
|
|
// beginning of the range
|
|
if( m_playPos[m_playMode] >= tl->loopEnd() )
|
|
{
|
|
m_playPos[m_playMode].setTicks( tl->loopBegin().getTicks() );
|
|
setToTime(tl->loopBegin());
|
|
}
|
|
else if( m_playPos[m_playMode] == tl->loopEnd() - 1 )
|
|
{
|
|
emit updateSampleTracks();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_vstSyncController.stopCycle();
|
|
}
|
|
|
|
currentFrame = fmodf( currentFrame, framesPerTick );
|
|
m_playPos[m_playMode].setCurrentFrame( currentFrame );
|
|
}
|
|
|
|
f_cnt_t framesToPlay =
|
|
Engine::mixer()->framesPerPeriod() - framesPlayed;
|
|
|
|
f_cnt_t framesLeft = ( f_cnt_t )framesPerTick -
|
|
( f_cnt_t )currentFrame;
|
|
// skip last frame fraction
|
|
if( framesLeft == 0 )
|
|
{
|
|
++framesPlayed;
|
|
m_playPos[m_playMode].setCurrentFrame( currentFrame
|
|
+ 1.0f );
|
|
continue;
|
|
}
|
|
// do we have samples left in this tick but these are less
|
|
// than samples we have to play?
|
|
if( framesLeft < framesToPlay )
|
|
{
|
|
// then set framesToPlay to remaining samples, the
|
|
// rest will be played in next loop
|
|
framesToPlay = framesLeft;
|
|
}
|
|
|
|
if( ( f_cnt_t ) currentFrame == 0 )
|
|
{
|
|
processAutomations(trackList, m_playPos[m_playMode], framesToPlay);
|
|
|
|
// loop through all tracks and play them
|
|
for( int i = 0; i < trackList.size(); ++i )
|
|
{
|
|
trackList[i]->play( m_playPos[m_playMode],
|
|
framesToPlay,
|
|
framesPlayed, tcoNum );
|
|
}
|
|
}
|
|
|
|
// update frame-counters
|
|
framesPlayed += framesToPlay;
|
|
m_playPos[m_playMode].setCurrentFrame( framesToPlay +
|
|
currentFrame );
|
|
m_elapsedMilliSeconds[m_playMode] += MidiTime::ticksToMilliseconds(framesToPlay / framesPerTick, getTempo());
|
|
m_elapsedTacts = m_playPos[Mode_PlaySong].getTact();
|
|
m_elapsedTicks = ( m_playPos[Mode_PlaySong].getTicks() % ticksPerTact() ) / 48;
|
|
}
|
|
}
|
|
|
|
|
|
void Song::processAutomations(const TrackList &tracklist, MidiTime 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);
|
|
MidiTime 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;
|
|
}
|
|
}
|
|
|
|
// Apply values
|
|
for (auto it = values.begin(); it != values.end(); it++)
|
|
{
|
|
if (! recordedModels.contains(it.key()))
|
|
{
|
|
it.key()->setAutomatedValue(it.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Song::setModified(bool value)
|
|
{
|
|
if( !m_loadingProject )
|
|
{
|
|
if (m_modified != value)
|
|
{
|
|
m_modified = value;
|
|
emit modified();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::pair<MidiTime, MidiTime> Song::getExportEndpoints() const
|
|
{
|
|
if ( m_renderBetweenMarkers )
|
|
{
|
|
return std::pair<MidiTime, MidiTime>(
|
|
m_playPos[Mode_PlaySong].m_timeLine->loopBegin(),
|
|
m_playPos[Mode_PlaySong].m_timeLine->loopEnd()
|
|
);
|
|
}
|
|
else if ( m_exportLoop )
|
|
{
|
|
return std::pair<MidiTime, MidiTime>( MidiTime(0, 0), MidiTime(m_length, 0) );
|
|
}
|
|
else
|
|
{
|
|
// if not exporting as a loop, we leave one bar of padding at the end of the song to accomodate reverb, etc.
|
|
return std::pair<MidiTime, MidiTime>( MidiTime(0, 0), MidiTime(m_length+1, 0) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
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( TrackList::const_iterator it = tracks().begin();
|
|
it != tracks().end(); ++it )
|
|
{
|
|
if( Engine::getSong()->isExporting() &&
|
|
( *it )->isMuted() )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const tact_t cur = ( *it )->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[m_playMode] += MidiTime::ticksToMilliseconds( ticks - ticksFromPlayMode, getTempo() );
|
|
m_playPos[playMode].setTicks( ticks );
|
|
m_playPos[playMode].setCurrentFrame( 0.0f );
|
|
|
|
// 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;
|
|
}
|
|
|
|
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() );
|
|
|
|
// remove all note-play-handles that are active
|
|
Engine::mixer()->clear();
|
|
|
|
m_playMode = Mode_None;
|
|
|
|
emit stopped();
|
|
emit playbackStateChanged();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::startExport()
|
|
{
|
|
stop();
|
|
if(m_renderBetweenMarkers)
|
|
{
|
|
m_playPos[Mode_PlaySong].setTicks( m_playPos[Mode_PlaySong].m_timeLine->loopBegin().getTicks() );
|
|
}
|
|
else
|
|
{
|
|
m_playPos[Mode_PlaySong].setTicks( 0 );
|
|
}
|
|
|
|
playSong();
|
|
|
|
m_exporting = true;
|
|
|
|
m_vstSyncController.setPlaybackState( true );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::stopExport()
|
|
{
|
|
stop();
|
|
m_exporting = false;
|
|
m_exportLoop = false;
|
|
|
|
m_vstSyncController.setPlaybackState( m_playing );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::insertBar()
|
|
{
|
|
m_tracksMutex.lockForRead();
|
|
for( TrackList::const_iterator it = tracks().begin();
|
|
it != tracks().end(); ++it )
|
|
{
|
|
( *it )->insertTact( 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 )->removeTact( 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(MidiTime time, int tcoNum) const
|
|
{
|
|
return TrackContainer::automatedValuesFromTracks(TrackList(tracks()) << m_globalAutomationTrack, 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();
|
|
|
|
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 );
|
|
|
|
InstrumentTrackView::cleanupWindowCache();
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 );
|
|
// if file could not be opened, head-node is null and we create
|
|
// new project
|
|
if( dataFile.head().isNull() )
|
|
{
|
|
if( m_loadOnLaunch )
|
|
{
|
|
createNewProject();
|
|
}
|
|
setProjectFileName(m_oldFileName);
|
|
return;
|
|
}
|
|
|
|
m_oldFileName = m_fileName;
|
|
|
|
clearProject();
|
|
|
|
clearErrors();
|
|
|
|
DataFile::LocaleHelper localeHelper( DataFile::LocaleHelper::ModeLoad );
|
|
|
|
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();
|
|
|
|
// 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
|
|
{
|
|
QTextStream(stderr) << Engine::getSong()->errorSummary() << endl;
|
|
}
|
|
}
|
|
|
|
m_loadingProject = false;
|
|
setModified(false);
|
|
m_loadOnLaunch = false;
|
|
}
|
|
|
|
|
|
// only save current song as _filename and do nothing else
|
|
bool Song::saveProjectFile( const QString & filename )
|
|
{
|
|
DataFile::LocaleHelper localeHelper( DataFile::LocaleHelper::ModeSave );
|
|
|
|
DataFile dataFile( DataFile::SongProject );
|
|
|
|
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() );
|
|
|
|
return dataFile.writeFile( filename );
|
|
}
|
|
|
|
|
|
|
|
// Save the current song
|
|
bool Song::guiSaveProject()
|
|
{
|
|
DataFile dataFile( DataFile::SongProject );
|
|
QString fileNameWithExtension = dataFile.nameWithExtension( m_fileName );
|
|
setProjectFileName(fileNameWithExtension);
|
|
|
|
bool const saveResult = saveProjectFile( m_fileName );
|
|
|
|
if( saveResult )
|
|
{
|
|
setModified(false);
|
|
}
|
|
|
|
return saveResult;
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save the current song with the given filename
|
|
bool Song::guiSaveProjectAs( const QString & _file_name )
|
|
{
|
|
QString o = m_oldFileName;
|
|
m_oldFileName = m_fileName;
|
|
setProjectFileName(_file_name);
|
|
|
|
if(!guiSaveProject())
|
|
{
|
|
// Saving failed. Restore old filenames.
|
|
setProjectFileName(m_oldFileName);
|
|
m_oldFileName = o;
|
|
|
|
return false;
|
|
}
|
|
|
|
m_oldFileName = m_fileName;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
|
|
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 );
|
|
Q_ASSERT( c != NULL );
|
|
|
|
addController( c );
|
|
|
|
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 )
|
|
{
|
|
m_errors.append( error );
|
|
}
|
|
|
|
|
|
|
|
bool Song::hasErrors()
|
|
{
|
|
return ( m_errors.length() > 0 );
|
|
}
|
|
|
|
|
|
|
|
QString Song::errorSummary()
|
|
{
|
|
QString errors = m_errors.join("\n") + '\n';
|
|
|
|
errors.prepend( "\n\n" );
|
|
errors.prepend( tr( "The following errors occured while loading: " ) );
|
|
|
|
return errors;
|
|
}
|