mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-14 03:59:35 -04:00
* Undoable add/remove bar * Don't add checkpoints to empty tracks Authored by: Spekular <Spekular@users.noreply.github.com>
1554 lines
35 KiB
C++
1554 lines
35 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 <QMessageBox>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include "AutomationTrack.h"
|
|
#include "AutomationEditor.h"
|
|
#include "ConfigManager.h"
|
|
#include "ControllerRackView.h"
|
|
#include "ControllerConnection.h"
|
|
#include "EnvelopeAndLfoParameters.h"
|
|
#include "Mixer.h"
|
|
#include "MixerView.h"
|
|
#include "GuiApplication.h"
|
|
#include "ExportFilter.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "Keymap.h"
|
|
#include "NotePlayHandle.h"
|
|
#include "MidiClip.h"
|
|
#include "PatternEditor.h"
|
|
#include "PatternStore.h"
|
|
#include "PatternTrack.h"
|
|
#include "PianoRoll.h"
|
|
#include "ProjectJournal.h"
|
|
#include "ProjectNotes.h"
|
|
#include "Scale.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_midiClipToPlay( nullptr ),
|
|
m_loopMidiClip( 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::audioEngine(), 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 );
|
|
|
|
for (int i = 0; i < MaxScaleCount; i++) {m_scales[i] = std::make_shared<Scale>();}
|
|
for (int i = 0; i < MaxKeymapCount; i++) {m_keymaps[i] = std::make_shared<Keymap>();}
|
|
}
|
|
|
|
|
|
|
|
|
|
Song::~Song()
|
|
{
|
|
m_playing = false;
|
|
delete m_globalAutomationTrack;
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::masterVolumeChanged()
|
|
{
|
|
Engine::audioEngine()->setMasterGain( m_masterVolumeModel.value() / 100.0f );
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::setTempo()
|
|
{
|
|
Engine::audioEngine()->requestChangeInModel();
|
|
const bpm_t tempo = ( bpm_t ) m_tempoModel.value();
|
|
PlayHandleList & playHandles = Engine::audioEngine()->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::audioEngine()->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 != nullptr )
|
|
{
|
|
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_PlayPattern:
|
|
if (Engine::patternStore()->numOfPatterns() > 0)
|
|
{
|
|
clipNum = Engine::patternStore()->currentPattern();
|
|
trackList.push_back(PatternTrack::findPatternTrack(clipNum));
|
|
}
|
|
break;
|
|
|
|
case Mode_PlayMidiClip:
|
|
if (m_midiClipToPlay)
|
|
{
|
|
clipNum = m_midiClipToPlay->getTrack()->getClipNum(m_midiClipToPlay);
|
|
trackList.push_back(m_midiClipToPlay->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::audioEngine()->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 pattern track, or a MIDI clip with no loop enabled,
|
|
// loop back to the beginning when we reach the end
|
|
if (m_playMode == Mode_PlayPattern)
|
|
{
|
|
enforceLoop(TimePos{0}, TimePos{Engine::patternStore()->lengthOfCurrentPattern(), 0});
|
|
}
|
|
else if (m_playMode == Mode_PlayMidiClip && m_loopMidiClip && !loopEnabled)
|
|
{
|
|
enforceLoop(TimePos{0}, m_midiClipToPlay->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 clipNum = -1;
|
|
|
|
switch (m_playMode)
|
|
{
|
|
case Mode_PlaySong:
|
|
break;
|
|
case Mode_PlayPattern:
|
|
{
|
|
Q_ASSERT(tracklist.size() == 1);
|
|
Q_ASSERT(tracklist.at(0)->type() == Track::PatternTrack);
|
|
auto patternTrack = dynamic_cast<PatternTrack*>(tracklist.at(0));
|
|
container = Engine::patternStore();
|
|
clipNum = patternTrack->patternIndex();
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
values = container->automatedValuesAt(timeStart, clipNum);
|
|
TrackList tracks = container->tracks();
|
|
|
|
Track::clipVector clips;
|
|
for (Track* track : tracks)
|
|
{
|
|
if (track->type() == Track::AutomationTrack) {
|
|
track->getClipsInRange(clips, 0, timeStart);
|
|
}
|
|
}
|
|
|
|
// Process recording
|
|
for (Clip* clip : clips)
|
|
{
|
|
auto p = dynamic_cast<AutomationClip *>(clip);
|
|
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 clip
|
|
// 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::playPattern()
|
|
{
|
|
if( isStopped() == false )
|
|
{
|
|
stop();
|
|
}
|
|
|
|
m_playMode = Mode_PlayPattern;
|
|
m_playing = true;
|
|
m_paused = false;
|
|
|
|
m_vstSyncController.setPlaybackState( true );
|
|
|
|
savePos();
|
|
|
|
emit playbackStateChanged();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::playMidiClip( const MidiClip* midiClipToPlay, bool loop )
|
|
{
|
|
if( isStopped() == false )
|
|
{
|
|
stop();
|
|
}
|
|
|
|
m_midiClipToPlay = midiClipToPlay;
|
|
m_loopMidiClip = loop;
|
|
|
|
if( m_midiClipToPlay != nullptr )
|
|
{
|
|
m_playMode = Mode_PlayMidiClip;
|
|
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::audioEngine()->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::audioEngine()->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::audioEngine()->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 (Track* track: tracks())
|
|
{
|
|
// FIXME journal batch of tracks instead of each track individually
|
|
if (track->numOfClips() > 0) { track->addJournalCheckPoint(); }
|
|
track->insertBar(m_playPos[Mode_PlaySong]);
|
|
}
|
|
m_tracksMutex.unlock();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::removeBar()
|
|
{
|
|
m_tracksMutex.lockForRead();
|
|
for (Track* track: tracks())
|
|
{
|
|
// FIXME journal batch of tracks instead of each track individually
|
|
if (track->numOfClips() > 0) { track->addJournalCheckPoint(); }
|
|
track->removeBar(m_playPos[Mode_PlaySong]);
|
|
}
|
|
m_tracksMutex.unlock();
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::addPatternTrack()
|
|
{
|
|
Track * t = Track::create(Track::PatternTrack, this);
|
|
Engine::patternStore()->setCurrentPattern(dynamic_cast<PatternTrack*>(t)->patternIndex());
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
|
|
|
|
AutomationClip * Song::tempoAutomationClip()
|
|
{
|
|
return AutomationClip::globalAutomationClip( &m_tempoModel );
|
|
}
|
|
|
|
|
|
AutomatedValueMap Song::automatedValuesAt(TimePos time, int clipNum) const
|
|
{
|
|
return TrackContainer::automatedValuesFromTracks(TrackList{m_globalAutomationTrack} << tracks(), time, clipNum);
|
|
}
|
|
|
|
|
|
|
|
|
|
void Song::clearProject()
|
|
{
|
|
Engine::projectJournal()->setJournalling( false );
|
|
|
|
if( m_playing )
|
|
{
|
|
stop();
|
|
}
|
|
|
|
for( int i = 0; i < Mode_Count; i++ )
|
|
{
|
|
setPlayPos( 0, ( PlayModes )i );
|
|
}
|
|
|
|
|
|
Engine::audioEngine()->requestChangeInModel();
|
|
|
|
if( getGUI() != nullptr && getGUI()->patternEditor() )
|
|
{
|
|
getGUI()->patternEditor()->m_editor->clearAllTracks();
|
|
}
|
|
if( getGUI() != nullptr && getGUI()->songEditor() )
|
|
{
|
|
getGUI()->songEditor()->m_editor->clearAllTracks();
|
|
}
|
|
if( getGUI() != nullptr && getGUI()->mixerView() )
|
|
{
|
|
getGUI()->mixerView()->clear();
|
|
}
|
|
QCoreApplication::sendPostedEvents();
|
|
Engine::patternStore()->clearAllTracks();
|
|
clearAllTracks();
|
|
|
|
Engine::mixer()->clear();
|
|
|
|
if( getGUI() != nullptr && getGUI()->automationEditor() )
|
|
{
|
|
getGUI()->automationEditor()->setCurrentClip( nullptr );
|
|
}
|
|
|
|
if( getGUI() != nullptr && getGUI()->pianoRoll() )
|
|
{
|
|
getGUI()->pianoRoll()->reset();
|
|
}
|
|
|
|
m_tempoModel.reset();
|
|
m_masterVolumeModel.reset();
|
|
m_masterPitchModel.reset();
|
|
m_timeSigModel.reset();
|
|
|
|
// Clear the m_oldAutomatedValues AutomatedValueMap
|
|
m_oldAutomatedValues.clear();
|
|
|
|
AutomationClip::globalAutomationClip( &m_tempoModel )->clear();
|
|
AutomationClip::globalAutomationClip( &m_masterVolumeModel )->
|
|
clear();
|
|
AutomationClip::globalAutomationClip( &m_masterPitchModel )->
|
|
clear();
|
|
|
|
Engine::audioEngine()->doneChangeInModel();
|
|
|
|
if( getGUI() != nullptr && getGUI()->getProjectNotes() )
|
|
{
|
|
getGUI()->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::patternStore());
|
|
dynamic_cast<InstrumentTrack * >( t )->loadInstrument(
|
|
"kicker" );
|
|
Track::create( Track::SampleTrack, this );
|
|
Track::create( Track::PatternTrack, 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::patternStore()->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 (getGUI() != nullptr)
|
|
{
|
|
QMessageBox::critical(nullptr, 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::audioEngine()->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 mixer channels
|
|
node = dataFile.content().firstChildElement( Engine::mixer()->nodeName() );
|
|
if( !node.isNull() )
|
|
{
|
|
Engine::mixer()->restoreState( node.toElement() );
|
|
if( getGUI() != nullptr )
|
|
{
|
|
// refresh MixerView
|
|
getGUI()->mixerView()->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::PatternTrack)
|
|
{
|
|
n += nd.toElement().elementsByTagName("patterntrack").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 (node.nodeName() == "scales")
|
|
{
|
|
restoreScaleStates(node.toElement());
|
|
}
|
|
else if (node.nodeName() == "keymaps")
|
|
{
|
|
restoreKeymapStates(node.toElement());
|
|
}
|
|
else if( getGUI() != nullptr )
|
|
{
|
|
if( node.nodeName() == getGUI()->getControllerRackView()->nodeName() )
|
|
{
|
|
getGUI()->getControllerRackView()->restoreState( node.toElement() );
|
|
}
|
|
else if( node.nodeName() == getGUI()->pianoRoll()->nodeName() )
|
|
{
|
|
getGUI()->pianoRoll()->restoreState( node.toElement() );
|
|
}
|
|
else if( node.nodeName() == getGUI()->automationEditor()->m_editor->nodeName() )
|
|
{
|
|
getGUI()->automationEditor()->m_editor->restoreState( node.toElement() );
|
|
}
|
|
else if( node.nodeName() == getGUI()->getProjectNotes()->nodeName() )
|
|
{
|
|
getGUI()->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 Clips inside pattern tracks
|
|
Engine::patternStore()->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
|
|
AutomationClip::resolveAllIDs();
|
|
|
|
|
|
Engine::audioEngine()->doneChangeInModel();
|
|
|
|
ConfigManager::inst()->addRecentlyOpenedProject( fileName );
|
|
|
|
Engine::projectJournal()->setJournalling( true );
|
|
|
|
emit projectLoaded();
|
|
|
|
if( isCancelled() )
|
|
{
|
|
m_isCancelled = false;
|
|
createNewProject();
|
|
return;
|
|
}
|
|
|
|
if ( hasErrors())
|
|
{
|
|
if ( getGUI() != nullptr )
|
|
{
|
|
QMessageBox::warning( nullptr, 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::mixer()->saveState( dataFile, dataFile.content() );
|
|
if( getGUI() != nullptr )
|
|
{
|
|
getGUI()->getControllerRackView()->saveState( dataFile, dataFile.content() );
|
|
getGUI()->pianoRoll()->saveState( dataFile, dataFile.content() );
|
|
getGUI()->automationEditor()->m_editor->saveState( dataFile, dataFile.content() );
|
|
getGUI()->getProjectNotes()->SerializingObject::saveState( dataFile, dataFile.content() );
|
|
m_playPos[Mode_PlaySong].m_timeLine->saveState( dataFile, dataFile.content() );
|
|
}
|
|
|
|
saveControllerStates( dataFile, dataFile.content() );
|
|
|
|
saveScaleStates(dataFile, dataFile.content());
|
|
saveKeymapStates(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::saveScaleStates(QDomDocument &doc, QDomElement &element)
|
|
{
|
|
QDomElement scalesNode = doc.createElement("scales");
|
|
element.appendChild(scalesNode);
|
|
|
|
for (int i = 0; i < MaxScaleCount; i++)
|
|
{
|
|
m_scales[i]->saveState(doc, scalesNode);
|
|
}
|
|
}
|
|
|
|
|
|
void Song::restoreScaleStates(const QDomElement &element)
|
|
{
|
|
QDomNode node = element.firstChild();
|
|
|
|
for (int i = 0; i < MaxScaleCount && !node.isNull() && !isCancelled(); i++)
|
|
{
|
|
m_scales[i]->restoreState(node.toElement());
|
|
node = node.nextSibling();
|
|
}
|
|
emit scaleListChanged(-1);
|
|
}
|
|
|
|
|
|
void Song::saveKeymapStates(QDomDocument &doc, QDomElement &element)
|
|
{
|
|
QDomElement keymapsNode = doc.createElement("keymaps");
|
|
element.appendChild(keymapsNode);
|
|
|
|
for (int i = 0; i < MaxKeymapCount; i++)
|
|
{
|
|
m_keymaps[i]->saveState(doc, keymapsNode);
|
|
}
|
|
}
|
|
|
|
|
|
void Song::restoreKeymapStates(const QDomElement &element)
|
|
{
|
|
QDomNode node = element.firstChild();
|
|
|
|
for (int i = 0; i < MaxKeymapCount && !node.isNull() && !isCancelled(); i++)
|
|
{
|
|
m_keymaps[i]->restoreState(node.toElement());
|
|
node = node.nextSibling();
|
|
}
|
|
emit keymapListChanged(-1);
|
|
}
|
|
|
|
|
|
void Song::exportProjectMidi(QString const & exportFileName) const
|
|
{
|
|
// instantiate midi export plugin
|
|
TrackContainer::TrackList const & tracks = this->tracks();
|
|
TrackContainer::TrackList const & patternStoreTracks = Engine::patternStore()->tracks();
|
|
|
|
ExportFilter *exf = dynamic_cast<ExportFilter *> (Plugin::instantiate("midiexport", nullptr, nullptr));
|
|
if (exf)
|
|
{
|
|
exf->tryExport(tracks, patternStoreTracks, 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;
|
|
}
|
|
|
|
|
|
std::shared_ptr<const Scale> Song::getScale(unsigned int index) const
|
|
{
|
|
if (index >= MaxScaleCount) {index = 0;}
|
|
|
|
return std::atomic_load(&m_scales[index]);
|
|
}
|
|
|
|
|
|
std::shared_ptr<const Keymap> Song::getKeymap(unsigned int index) const
|
|
{
|
|
if (index >= MaxKeymapCount) {index = 0;}
|
|
|
|
return std::atomic_load(&m_keymaps[index]);
|
|
}
|
|
|
|
|
|
void Song::setScale(unsigned int index, std::shared_ptr<Scale> newScale)
|
|
{
|
|
if (index >= MaxScaleCount) {index = 0;}
|
|
|
|
Engine::audioEngine()->requestChangeInModel();
|
|
std::atomic_store(&m_scales[index], newScale);
|
|
emit scaleListChanged(index);
|
|
Engine::audioEngine()->doneChangeInModel();
|
|
}
|
|
|
|
|
|
void Song::setKeymap(unsigned int index, std::shared_ptr<Keymap> newMap)
|
|
{
|
|
if (index >= MaxKeymapCount) {index = 0;}
|
|
|
|
Engine::audioEngine()->requestChangeInModel();
|
|
std::atomic_store(&m_keymaps[index], newMap);
|
|
emit keymapListChanged(index);
|
|
Engine::audioEngine()->doneChangeInModel();
|
|
}
|