diff --git a/include/AutomatableModel.h b/include/AutomatableModel.h index f7d9b470e80..d53de7f5fd2 100644 --- a/include/AutomatableModel.h +++ b/include/AutomatableModel.h @@ -144,6 +144,7 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject return (std::round(v) != 0); } + float oldValue() const { return m_oldValue; } template inline T value( int frameOffset = 0 ) const diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index fe7625d300f..0658c389cfd 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -26,7 +26,6 @@ #ifndef LMMS_INSTRUMENT_TRACK_H #define LMMS_INSTRUMENT_TRACK_H - #include "AudioBusHandle.h" #include "InstrumentFunctions.h" #include "InstrumentSoundShaping.h" @@ -34,12 +33,12 @@ #include "Midi.h" #include "MidiEventProcessor.h" #include "MidiPort.h" +#include "MixerChannelLcdModel.h" #include "NotePlayHandle.h" #include "Piano.h" #include "Plugin.h" #include "Track.h" - namespace lmms { @@ -262,7 +261,6 @@ protected slots: void updatePitchRange(); void updateMixerChannel(); - private: void processCCEvent(int controller); @@ -296,7 +294,7 @@ protected slots: FloatModel m_pitchModel; IntModel m_pitchRangeModel; - IntModel m_mixerChannelModel; + MixerChannelLcdModel m_mixerChannelModel; BoolModel m_useMasterPitchModel; Instrument * m_instrument; diff --git a/include/Mixer.h b/include/Mixer.h index 65698267cbe..2f766c24735 100644 --- a/include/Mixer.h +++ b/include/Mixer.h @@ -88,9 +88,15 @@ class MixerChannel : public ThreadableJob void incrementDeps(); void processed(); + int useCount() const { return m_useCount; } + + void incrementUseCount() { ++m_useCount; } + void decrementUseCount() { --m_useCount; } + private: void doProcessing() override; int m_channelIndex; + int m_useCount; std::optional m_color; }; @@ -214,6 +220,11 @@ class LMMS_EXPORT Mixer : public Model, public JournallingObject MixerRouteVector m_mixerRoutes; +signals: + void channelCreated(int index); + void channelDeleted(int index); + void channelsSwapped(int fromIndex, int toIndex); + private: // the mixer channels in the mixer. index 0 is always master. std::vector m_mixerChannels; diff --git a/include/MixerChannelLcdModel.h b/include/MixerChannelLcdModel.h new file mode 100644 index 00000000000..4a44d14e409 --- /dev/null +++ b/include/MixerChannelLcdModel.h @@ -0,0 +1,56 @@ +/* + * MixerChannelLcdModel.h + * + * Copyright (c) 2026 saker + * + * 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. + * + */ + +#ifndef LMMS_MIXER_CHANNEL_LCD_MODEL_H +#define LMMS_MIXER_CHANNEL_LCD_MODEL_H + +#include "AutomatableModel.h" + +namespace lmms { +/** + * @brief An @ref IntModel that keeps track of its assigned mixer channel. + * + * This model tracks its assigned mixer channel. It is a subclass of IntModel that adds functionality to handle channel + * creation, deletion, and swapping. Both the value and valid range are automatically updated to reflect these changes. + */ +class MixerChannelLcdModel : public IntModel +{ +public: + MixerChannelLcdModel(Model* parent = nullptr); + ~MixerChannelLcdModel(); + + MixerChannelLcdModel(const MixerChannelLcdModel&) = delete; + MixerChannelLcdModel(MixerChannelLcdModel&&) = delete; + MixerChannelLcdModel& operator=(const MixerChannelLcdModel&) = delete; + MixerChannelLcdModel& operator=(MixerChannelLcdModel&&) = delete; + +private: + void channelsSwapped(int fromIndex, int toIndex); + void channelDeleted(int index); + void channelCreated(int index); +}; + +} // namespace lmms + +#endif // LMMS_MIXER_CHANNEL_LCD_MODEL_H diff --git a/include/MixerView.h b/include/MixerView.h index ac95b3c6846..2f89561ee0f 100644 --- a/include/MixerView.h +++ b/include/MixerView.h @@ -123,8 +123,6 @@ private slots: QWidget* m_racksWidget; Mixer* m_mixer; - void updateMaxChannelSelector(); - friend class MixerChannelView; } ; diff --git a/include/SampleTrack.h b/include/SampleTrack.h index 329d391cdfe..84610f149be 100644 --- a/include/SampleTrack.h +++ b/include/SampleTrack.h @@ -26,6 +26,7 @@ #define LMMS_SAMPLE_TRACK_H #include "AudioBusHandle.h" +#include "MixerChannelLcdModel.h" #include "Track.h" @@ -94,7 +95,7 @@ public slots: private: FloatModel m_volumeModel; FloatModel m_panningModel; - IntModel m_mixerChannelModel; + MixerChannelLcdModel m_mixerChannelModel; AudioBusHandle m_audioBusHandle; bool m_isPlaying; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 036ba0cd671..dae27113c7c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -45,6 +45,7 @@ set(LMMS_SRCS core/MicroTimer.cpp core/Microtuner.cpp core/MixHelpers.cpp + core/MixerChannelLcdModel.cpp core/Model.cpp core/ModelVisitor.cpp core/Note.cpp diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index 27109f4eedf..020f6810a5d 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -70,7 +70,8 @@ MixerChannel::MixerChannel( int idx, Model * _parent ) : m_lock(), m_queued( false ), m_dependenciesMet(0), - m_channelIndex(idx) + m_channelIndex(idx), + m_useCount(0) { m_buffer.allocateInterleavedBuffer(); } @@ -271,6 +272,7 @@ int Mixer::createChannel() m_mixerChannels[index]->m_muteModel.setValue(true); } + emit channelCreated(index); return index; } @@ -331,50 +333,6 @@ void Mixer::deleteChannel( int index ) // channel deletion is performed between mixer rounds Engine::audioEngine()->requestChangeInModel(); - // go through every instrument and adjust for the channel index change - TrackContainer::TrackList tracks; - - auto& songTracks = Engine::getSong()->tracks(); - auto& patternStoreTracks = Engine::patternStore()->tracks(); - tracks.insert(tracks.end(), songTracks.begin(), songTracks.end()); - tracks.insert(tracks.end(), patternStoreTracks.begin(), patternStoreTracks.end()); - - for( Track* t : tracks ) - { - if( t->type() == Track::Type::Instrument ) - { - auto inst = dynamic_cast(t); - int val = inst->mixerChannelModel()->value(0); - if( val == index ) - { - // we are deleting this track's channel send - // send to master - inst->mixerChannelModel()->setValue(0); - } - else if( val > index ) - { - // subtract 1 to make up for the missing channel - inst->mixerChannelModel()->setValue(val-1); - } - } - else if( t->type() == Track::Type::Sample ) - { - auto strk = dynamic_cast(t); - int val = strk->mixerChannelModel()->value(0); - if( val == index ) - { - // we are deleting this track's channel send - // send to master - strk->mixerChannelModel()->setValue(0); - } - else if( val > index ) - { - // subtract 1 to make up for the missing channel - strk->mixerChannelModel()->setValue(val-1); - } - } - } - MixerChannel * ch = m_mixerChannels[index]; // delete all of this channel's sends and receives @@ -414,6 +372,8 @@ void Mixer::deleteChannel( int index ) } } + emit channelDeleted(index); + Engine::audioEngine()->doneChangeInModel(); } @@ -422,10 +382,8 @@ void Mixer::deleteChannel( int index ) void Mixer::moveChannelLeft( int index ) { // can't move master or first channel - if (index <= 1 || static_cast(index) >= m_mixerChannels.size()) - { - return; - } + if (index <= 1 || static_cast(index) >= m_mixerChannels.size()) { return; } + // channels to swap int a = index - 1, b = index; @@ -433,49 +391,14 @@ void Mixer::moveChannelLeft( int index ) if (m_lastSoloed == a) { m_lastSoloed = b; } else if (m_lastSoloed == b) { m_lastSoloed = a; } - // go through every instrument and adjust for the channel index change - const TrackContainer::TrackList& songTrackList = Engine::getSong()->tracks(); - const TrackContainer::TrackList& patternTrackList = Engine::patternStore()->tracks(); - - for (const auto& trackList : {songTrackList, patternTrackList}) - { - for (const auto& track : trackList) - { - if (track->type() == Track::Type::Instrument) - { - auto inst = (InstrumentTrack*)track; - int val = inst->mixerChannelModel()->value(0); - if( val == a ) - { - inst->mixerChannelModel()->setValue(b); - } - else if( val == b ) - { - inst->mixerChannelModel()->setValue(a); - } - } - else if (track->type() == Track::Type::Sample) - { - auto strk = (SampleTrack*)track; - int val = strk->mixerChannelModel()->value(0); - if( val == a ) - { - strk->mixerChannelModel()->setValue(b); - } - else if( val == b ) - { - strk->mixerChannelModel()->setValue(a); - } - } - } - } - // Swap positions in array - qSwap(m_mixerChannels[index], m_mixerChannels[index - 1]); + std::swap(m_mixerChannels[index], m_mixerChannels[index - 1]); // Update m_channelIndex of both channels m_mixerChannels[index]->setIndex(index); m_mixerChannels[index - 1]->setIndex(index - 1); + + emit channelsSwapped(index, index - 1); } @@ -891,41 +814,12 @@ void Mixer::validateChannelName( int index, int oldIndex ) bool Mixer::isChannelInUse(int index) { - // check if the index mixer channel receives audio from any other channel - if (!m_mixerChannels[index]->m_receives.empty()) - { - return true; - } + assert(index >= 0 && index < m_mixerChannels.size()); - // check if the destination mixer channel on any instrument or sample track is the index mixer channel - TrackContainer::TrackList tracks; - - auto& songTracks = Engine::getSong()->tracks(); - auto& patternStoreTracks = Engine::patternStore()->tracks(); - tracks.insert(tracks.end(), songTracks.begin(), songTracks.end()); - tracks.insert(tracks.end(), patternStoreTracks.begin(), patternStoreTracks.end()); - - for (const auto t : tracks) - { - if (t->type() == Track::Type::Instrument) - { - auto inst = dynamic_cast(t); - if (inst->mixerChannelModel()->value() == index) - { - return true; - } - } - else if (t->type() == Track::Type::Sample) - { - auto strack = dynamic_cast(t); - if (strack->mixerChannelModel()->value() == index) - { - return true; - } - } - } + // check if the index mixer channel receives audio from any other channel + if (!m_mixerChannels[index]->m_receives.empty()) { return true; } - return false; + return m_mixerChannels[index]->useCount() > 0; } diff --git a/src/core/MixerChannelLcdModel.cpp b/src/core/MixerChannelLcdModel.cpp new file mode 100644 index 00000000000..ab5fa021d72 --- /dev/null +++ b/src/core/MixerChannelLcdModel.cpp @@ -0,0 +1,78 @@ +/* + * MixerChannelLcdModel.cpp + * + * Copyright (c) 2026 saker + * + * 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 "MixerChannelLcdModel.h" + +#include "Engine.h" +#include "Mixer.h" + +namespace lmms { +MixerChannelLcdModel::MixerChannelLcdModel(Model* parent) + : IntModel(0, 0, 0, parent, tr("Mixer channel")) +{ + const auto mixer = Engine::mixer(); + setRange(0, mixer->numChannels() - 1, 1); + + connect(mixer, &Mixer::channelsSwapped, this, &MixerChannelLcdModel::channelsSwapped); + connect(mixer, &Mixer::channelDeleted, this, &MixerChannelLcdModel::channelDeleted); + connect(mixer, &Mixer::channelCreated, this, &MixerChannelLcdModel::channelCreated); + + connect(this, &MixerChannelLcdModel::dataChanged, mixer, [this, mixer] { + // both channels must exist so we know the channel was actually changed to another + if (oldValue() < 0 || oldValue() >= mixer->numChannels()) { return; } + if (value() < 0 || value() >= mixer->numChannels()) { return; } + + mixer->mixerChannel(oldValue())->decrementUseCount(); + mixer->mixerChannel(value())->incrementUseCount(); + }); + + mixer->mixerChannel(0)->incrementUseCount(); +} + +MixerChannelLcdModel::~MixerChannelLcdModel() +{ + const auto mixer = Engine::mixer(); + if (value() < 0 || value() >= mixer->numChannels()) { return; } + mixer->mixerChannel(value())->decrementUseCount(); +} + +void MixerChannelLcdModel::channelsSwapped(int fromIndex, int toIndex) +{ + if (value() == fromIndex) { setValue(toIndex); } + else if (value() == toIndex) { setValue(fromIndex); } +} + +void MixerChannelLcdModel::channelDeleted(int index) +{ + if (value() == index) { setValue(0); } + else if (value() > index) { setValue(value() - 1); } + setRange(0, maxValue() - 1); +} + +void MixerChannelLcdModel::channelCreated(int index) +{ + setRange(0, maxValue() + 1); +} + +} // namespace lmms diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index dc1626d5a51..747b423fb49 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -190,8 +190,6 @@ int MixerView::addNewChannel() updateMixerChannel(newChannelIndex); - updateMaxChannelSelector(); - return newChannelIndex; } @@ -231,38 +229,8 @@ void MixerView::refreshDisplay() { updateMixerChannel(i); } - - updateMaxChannelSelector(); } - -// update the and max. channel number for every instrument -void MixerView::updateMaxChannelSelector() -{ - const TrackContainer::TrackList& songTracks = Engine::getSong()->tracks(); - const TrackContainer::TrackList& patternStoreTracks = Engine::patternStore()->tracks(); - - for (const auto& trackList : {songTracks, patternStoreTracks}) - { - for (const auto& track : trackList) - { - if (track->type() == Track::Type::Instrument) - { - auto inst = (InstrumentTrack*)track; - inst->mixerChannelModel()->setRange(0, - m_mixerChannelViews.size()-1,1); - } - else if (track->type() == Track::Type::Sample) - { - auto strk = (SampleTrack*)track; - strk->mixerChannelModel()->setRange(0, - m_mixerChannelViews.size()-1,1); - } - } - } -} - - void MixerView::saveSettings(QDomDocument& doc, QDomElement& domElement) { MainWindow::saveWidgetState(this, domElement); @@ -415,8 +383,6 @@ void MixerView::deleteChannel(int index) selLine = m_mixerChannelViews.size() - 1; } setCurrentMixerChannel(selLine); - - updateMaxChannelSelector(); } void MixerView::deleteUnusedChannels() diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 148351822f7..262eb71c235 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -63,7 +63,6 @@ InstrumentTrack::InstrumentTrack(TrackContainer* tc) : m_audioBusHandle(tr("unnamed_track"), true, &m_volumeModel, &m_panningModel, &m_mutedModel), m_pitchModel(0, MinPitchDefault, MaxPitchDefault, 1, this, tr("Pitch")), m_pitchRangeModel(1, 1, 60, this, tr("Pitch range")), - m_mixerChannelModel(0, 0, 0, this, tr("Mixer channel")), m_useMasterPitchModel(true, this, tr("Master pitch")), m_instrument(nullptr), m_soundShaping(this), @@ -79,8 +78,6 @@ InstrumentTrack::InstrumentTrack(TrackContainer* tc) : m_firstKeyModel.setInitValue(0); m_lastKeyModel.setInitValue(NumKeys - 1); - m_mixerChannelModel.setRange( 0, Engine::mixer()->numChannels()-1, 1); - for( int i = 0; i < NumKeys; ++i ) { m_notes[i] = nullptr; @@ -672,9 +669,6 @@ void InstrumentTrack::updateMixerChannel() m_audioBusHandle.setNextMixerChannel(m_mixerChannelModel.value()); } - - - int InstrumentTrack::masterKey( int _midi_key ) const { diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index 4f023141ecd..48db55edc72 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -48,13 +48,11 @@ SampleTrack::SampleTrack(TrackContainer* tc) : Track(Track::Type::Sample, tc), m_volumeModel(DefaultVolume, MinVolume, MaxVolume, 0.1f, this, tr("Volume")), m_panningModel(DefaultPanning, PanningLeft, PanningRight, 0.1f, this, tr("Panning")), - m_mixerChannelModel(0, 0, 0, this, tr("Mixer channel")), m_audioBusHandle(tr("Sample track"), true, &m_volumeModel, &m_panningModel, &m_mutedModel), m_isPlaying(false) { setName(tr("Sample track")); m_panningModel.setCenterValue(DefaultPanning); - m_mixerChannelModel.setRange(0, Engine::mixer()->numChannels()-1, 1); connect(&m_mixerChannelModel, SIGNAL(dataChanged()), this, SLOT(updateMixerChannel())); } @@ -249,5 +247,4 @@ void SampleTrack::updateMixerChannel() m_audioBusHandle.setNextMixerChannel(m_mixerChannelModel.value()); } - } // namespace lmms