mirror of
https://github.com/LMMS/lmms.git
synced 2026-01-08 06:28:08 -05:00
1114 lines
33 KiB
C++
1114 lines
33 KiB
C++
/*
|
|
* carla.cpp - Carla for LMMS
|
|
*
|
|
* Copyright (C) 2014-2018 Filipe Coelho <falktx@falktx.com>
|
|
*
|
|
* 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 "Carla.h"
|
|
|
|
#include "AudioEngine.h"
|
|
#include "Engine.h"
|
|
#include "GuiApplication.h"
|
|
#include "InstrumentPlayHandle.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "Knob.h"
|
|
#include "MidiEventToByteSeq.h"
|
|
#include "MainWindow.h"
|
|
#include "FontHelper.h"
|
|
#include "Song.h"
|
|
|
|
#include <QApplication>
|
|
#include <QComboBox>
|
|
#include <QCompleter>
|
|
#include <QFileDialog>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMdiArea>
|
|
#include <QPushButton>
|
|
#include <QScrollArea>
|
|
#include <QSizePolicy>
|
|
#include <QSpacerItem>
|
|
#include <QSplitter>
|
|
#include <QString>
|
|
#include <QStringList>
|
|
#include <QStringListModel>
|
|
#include <QTimerEvent>
|
|
#include <QVBoxLayout>
|
|
|
|
#include <cstring>
|
|
|
|
#include "embed.h"
|
|
|
|
namespace lmms
|
|
{
|
|
|
|
// this doesn't seem to be defined anywhere
|
|
static const double ticksPerBeat = 48.0;
|
|
|
|
/*
|
|
* Current TODO items:
|
|
* - get plugin instance name (to use in external window title)
|
|
* - offline mode change callback
|
|
* - midi output
|
|
*
|
|
* All other items are to be done in Carla itself.
|
|
*/
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
#define handlePtr ((CarlaInstrument*)handle)
|
|
|
|
static uint32_t host_get_buffer_size(NativeHostHandle handle)
|
|
{
|
|
return handlePtr->handleGetBufferSize();
|
|
}
|
|
|
|
static double host_get_sample_rate(NativeHostHandle handle)
|
|
{
|
|
return handlePtr->handleGetSampleRate();
|
|
}
|
|
|
|
static bool host_is_offline(NativeHostHandle handle)
|
|
{
|
|
return handlePtr->handleIsOffline();
|
|
}
|
|
|
|
static const NativeTimeInfo* host_get_time_info(NativeHostHandle handle)
|
|
{
|
|
return handlePtr->handleGetTimeInfo();
|
|
}
|
|
|
|
static bool host_write_midi_event(NativeHostHandle, const NativeMidiEvent*)
|
|
{
|
|
return false; // unsupported?
|
|
}
|
|
|
|
static void host_ui_parameter_changed(NativeHostHandle handle, uint32_t index, float value)
|
|
{
|
|
handlePtr->handleUiParameterChanged(index, value);
|
|
}
|
|
|
|
static void host_ui_custom_data_changed(NativeHostHandle handle, const char* key, const char* value)
|
|
{
|
|
// unused
|
|
}
|
|
|
|
static void host_ui_closed(NativeHostHandle handle)
|
|
{
|
|
handlePtr->handleUiClosed();
|
|
}
|
|
|
|
static intptr_t host_dispatcher(NativeHostHandle handle, NativeHostDispatcherOpcode opcode, int32_t index, intptr_t value, void* ptr, float opt)
|
|
{
|
|
return handlePtr->handleDispatcher(opcode, index, value, ptr, opt);
|
|
}
|
|
|
|
#undef handlePtr
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
static const char* host_ui_open_file(NativeHostHandle, bool isDir, const char* title, const char* filter)
|
|
{
|
|
static QByteArray retStr;
|
|
const QFileDialog::Options options(isDir ? QFileDialog::ShowDirsOnly : 0x0);
|
|
|
|
retStr = QFileDialog::getOpenFileName(QApplication::activeWindow(), title, "", filter, nullptr, options).toUtf8();
|
|
|
|
return retStr.isEmpty() ? nullptr : retStr.constData();
|
|
}
|
|
|
|
static const char* host_ui_save_file(NativeHostHandle, bool isDir, const char* title, const char* filter)
|
|
{
|
|
static QByteArray retStr;
|
|
const QFileDialog::Options options(isDir ? QFileDialog::ShowDirsOnly : 0x0);
|
|
|
|
retStr = QFileDialog::getSaveFileName(QApplication::activeWindow(), title, "", filter, nullptr, options).toUtf8();
|
|
|
|
return retStr.isEmpty() ? nullptr : retStr.constData();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
CarlaInstrument::CarlaInstrument(InstrumentTrack* const instrumentTrack, const Descriptor* const descriptor, const bool isPatchbay)
|
|
: Instrument(instrumentTrack, descriptor, nullptr, Flag::IsSingleStreamed | Flag::IsMidiBased | Flag::IsNotBendable),
|
|
kIsPatchbay(isPatchbay),
|
|
fHandle(nullptr),
|
|
fDescriptor(isPatchbay ? carla_get_native_patchbay_plugin() : carla_get_native_rack_plugin()),
|
|
fMidiEventCount(0),
|
|
m_paramModels()
|
|
{
|
|
fHost.handle = this;
|
|
fHost.uiName = nullptr;
|
|
fHost.uiParentId = 0;
|
|
|
|
// carla/resources contains PyQt scripts required for launch
|
|
QDir path(carla_get_library_folder());
|
|
#if defined(CARLA_OS_LINUX)
|
|
path.cdUp();
|
|
path.cdUp();
|
|
QString resourcesPath = path.absolutePath() + "/share/carla/resources";
|
|
#else
|
|
// parse prefix from dll filename
|
|
QString resourcesPath = path.absolutePath() + "/resources";
|
|
#endif
|
|
fHost.resourceDir = strdup(resourcesPath.toUtf8().constData());
|
|
fHost.get_buffer_size = host_get_buffer_size;
|
|
fHost.get_sample_rate = host_get_sample_rate;
|
|
fHost.is_offline = host_is_offline;
|
|
fHost.get_time_info = host_get_time_info;
|
|
fHost.write_midi_event = host_write_midi_event;
|
|
fHost.ui_parameter_changed = host_ui_parameter_changed;
|
|
fHost.ui_custom_data_changed = host_ui_custom_data_changed;
|
|
fHost.ui_closed = host_ui_closed;
|
|
fHost.ui_open_file = host_ui_open_file;
|
|
fHost.ui_save_file = host_ui_save_file;
|
|
fHost.dispatcher = host_dispatcher;
|
|
|
|
std::memset(&fTimeInfo, 0, sizeof(NativeTimeInfo));
|
|
fTimeInfo.bbt.valid = true; // always valid
|
|
|
|
fHandle = fDescriptor->instantiate(&fHost);
|
|
Q_ASSERT(fHandle != nullptr);
|
|
|
|
if (fHandle != nullptr && fDescriptor->activate != nullptr)
|
|
fDescriptor->activate(fHandle);
|
|
|
|
// we need a play-handle which cares for calling play()
|
|
auto iph = new InstrumentPlayHandle(this, instrumentTrack);
|
|
Engine::audioEngine()->addPlayHandle( iph );
|
|
|
|
#if CARLA_VERSION_HEX >= CARLA_MIN_PARAM_VERSION
|
|
// text filter completion
|
|
m_completerModel = new QStringListModel(this);
|
|
m_paramsCompleter = new QCompleter(m_completerModel, this);
|
|
m_paramsCompleter->setCaseSensitivity(Qt::CaseInsensitive);
|
|
m_paramsCompleter->setCompletionMode(QCompleter::PopupCompletion);
|
|
|
|
// Add static amount of CarlaParamFloatModel's.
|
|
const auto paramCount = fDescriptor->get_parameter_count(fHandle);
|
|
m_paramModels.reserve(paramCount);
|
|
for (auto i = std::size_t{0}; i < paramCount; ++i)
|
|
{
|
|
m_paramModels.push_back(new CarlaParamFloatModel(this));
|
|
connect(m_paramModels[i], &CarlaParamFloatModel::dataChanged,
|
|
this, [this, i]() {paramModelChanged(i);}, Qt::DirectConnection);
|
|
}
|
|
#endif
|
|
|
|
connect(Engine::audioEngine(), SIGNAL(sampleRateChanged()), this, SLOT(sampleRateChanged()));
|
|
}
|
|
|
|
CarlaInstrument::~CarlaInstrument()
|
|
{
|
|
Engine::audioEngine()->removePlayHandlesOfTypes(instrumentTrack(), PlayHandle::Type::NotePlayHandle | PlayHandle::Type::InstrumentPlayHandle);
|
|
|
|
if (fHost.resourceDir != nullptr)
|
|
{
|
|
std::free((char*)fHost.resourceDir);
|
|
fHost.resourceDir = nullptr;
|
|
}
|
|
|
|
if (fHost.uiName != nullptr)
|
|
{
|
|
std::free((char*)fHost.uiName);
|
|
fHost.uiName = nullptr;
|
|
}
|
|
|
|
if (fHandle == nullptr)
|
|
return;
|
|
|
|
if (fDescriptor->deactivate != nullptr)
|
|
fDescriptor->deactivate(fHandle);
|
|
|
|
if (fDescriptor->cleanup != nullptr)
|
|
fDescriptor->cleanup(fHandle);
|
|
|
|
fHandle = nullptr;
|
|
|
|
#if CARLA_VERSION_HEX >= CARLA_MIN_PARAM_VERSION
|
|
clearParamModels();
|
|
#endif
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
uint32_t CarlaInstrument::handleGetBufferSize() const
|
|
{
|
|
return Engine::audioEngine()->framesPerPeriod();
|
|
}
|
|
|
|
double CarlaInstrument::handleGetSampleRate() const
|
|
{
|
|
return Engine::audioEngine()->outputSampleRate();
|
|
}
|
|
|
|
bool CarlaInstrument::handleIsOffline() const
|
|
{
|
|
return false; // TODO
|
|
}
|
|
|
|
const NativeTimeInfo* CarlaInstrument::handleGetTimeInfo() const
|
|
{
|
|
return &fTimeInfo;
|
|
}
|
|
|
|
void CarlaInstrument::handleUiParameterChanged(const uint32_t index, const float value) const
|
|
{
|
|
if (m_paramModels.size() > index)
|
|
{
|
|
m_paramModels[index]->setValue(value);
|
|
}
|
|
}
|
|
|
|
void CarlaInstrument::handleUiClosed()
|
|
{
|
|
emit uiClosed();
|
|
}
|
|
|
|
intptr_t CarlaInstrument::handleDispatcher(const NativeHostDispatcherOpcode opcode, const int32_t index,
|
|
const intptr_t value, void* const ptr, const float opt)
|
|
{
|
|
intptr_t ret = 0;
|
|
|
|
// source/includes/CarlaNative.h
|
|
// NATIVE_HOST_OPCODE_NULL = 0, nothing
|
|
// NATIVE_HOST_OPCODE_UPDATE_PARAMETER = 1, uses index, -1 for all
|
|
// NATIVE_HOST_OPCODE_UPDATE_MIDI_PROGRAM = 2, uses index, -1 for all; may use value for channel
|
|
// NATIVE_HOST_OPCODE_RELOAD_PARAMETERS = 3, nothing
|
|
// NATIVE_HOST_OPCODE_RELOAD_MIDI_PROGRAMS = 4, nothing
|
|
// NATIVE_HOST_OPCODE_RELOAD_ALL = 5, nothing
|
|
// NATIVE_HOST_OPCODE_UI_UNAVAILABLE = 6, nothing
|
|
// NATIVE_HOST_OPCODE_HOST_IDLE = 7, nothing
|
|
// NATIVE_HOST_OPCODE_INTERNAL_PLUGIN = 8, nothing
|
|
// NATIVE_HOST_OPCODE_QUEUE_INLINE_DISPLAY = 9, nothing
|
|
// NATIVE_HOST_OPCODE_UI_TOUCH_PARAMETER = 10 uses index, value as bool
|
|
|
|
switch (opcode)
|
|
{
|
|
case NATIVE_HOST_OPCODE_UI_UNAVAILABLE:
|
|
handleUiClosed();
|
|
break;
|
|
case NATIVE_HOST_OPCODE_HOST_IDLE:
|
|
qApp->processEvents();
|
|
break;
|
|
#if CARLA_VERSION_HEX >= CARLA_MIN_PARAM_VERSION
|
|
case NATIVE_HOST_OPCODE_UI_TOUCH_PARAMETER:
|
|
// param index, value as bool
|
|
// true = mousePress
|
|
// false = mouseRelease
|
|
if (!value) {
|
|
updateParamModel(index);
|
|
}
|
|
break;
|
|
case NATIVE_HOST_OPCODE_RELOAD_ALL:
|
|
refreshParams();
|
|
break;
|
|
case NATIVE_HOST_OPCODE_UPDATE_PARAMETER:
|
|
break;
|
|
case NATIVE_HOST_OPCODE_RELOAD_PARAMETERS:
|
|
refreshParams();
|
|
break;
|
|
case NATIVE_HOST_OPCODE_UPDATE_MIDI_PROGRAM:
|
|
case NATIVE_HOST_OPCODE_RELOAD_MIDI_PROGRAMS:
|
|
case NATIVE_HOST_OPCODE_INTERNAL_PLUGIN:
|
|
case NATIVE_HOST_OPCODE_QUEUE_INLINE_DISPLAY:
|
|
break;
|
|
#endif
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
QString CarlaInstrument::nodeName() const
|
|
{
|
|
return descriptor()->name;
|
|
}
|
|
|
|
void CarlaInstrument::saveSettings(QDomDocument& doc, QDomElement& parent)
|
|
{
|
|
if (fHandle == nullptr || fDescriptor->get_state == nullptr)
|
|
return;
|
|
|
|
char* const state = fDescriptor->get_state(fHandle);
|
|
|
|
if (state == nullptr)
|
|
return;
|
|
|
|
QDomDocument carlaDoc("carla");
|
|
|
|
if (carlaDoc.setContent(QString(state)))
|
|
{
|
|
QDomNode n = doc.importNode(carlaDoc.documentElement(), true);
|
|
parent.appendChild(n);
|
|
}
|
|
|
|
std::free(state);
|
|
|
|
#if CARLA_VERSION_HEX >= CARLA_MIN_PARAM_VERSION
|
|
for (uint32_t index = 0; index < m_paramModels.size(); ++index)
|
|
{
|
|
QString idStr = CARLA_SETTING_PREFIX + QString::number(index);
|
|
m_paramModels[index]->saveSettings(doc, parent, idStr);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void CarlaInstrument::refreshParams(bool init)
|
|
{
|
|
m_paramGroupCount = 0;
|
|
if (fDescriptor->get_parameter_count != nullptr &&
|
|
fDescriptor->get_parameter_info != nullptr &&
|
|
fDescriptor->get_parameter_value != nullptr &&
|
|
fDescriptor->set_parameter_value != nullptr)
|
|
{
|
|
QList<QString> completerData;
|
|
QList<QString> groups; // used to count no. groups.
|
|
|
|
uint32_t paramCount = fDescriptor->get_parameter_count(fHandle);
|
|
for (uint32_t i=0; i < paramCount; ++i)
|
|
{
|
|
const NativeParameter* paramInfo(fDescriptor->get_parameter_info(fHandle, i));
|
|
|
|
m_paramModels[i]->setOutput((paramInfo->hints & NATIVE_PARAMETER_IS_OUTPUT));
|
|
m_paramModels[i]->setEnabled((paramInfo->hints & NATIVE_PARAMETER_IS_ENABLED));
|
|
m_paramModels[i]->setValue(fDescriptor->get_parameter_value(fHandle, i));
|
|
|
|
// Get parameter name
|
|
QString name = "_NO_NAME_";
|
|
if (paramInfo->name != nullptr)
|
|
{
|
|
name = paramInfo->name;
|
|
}
|
|
|
|
if (paramInfo->groupName != nullptr)
|
|
{
|
|
m_paramModels[i]->setGroupName(paramInfo->groupName);
|
|
|
|
if (m_paramModels[i]->enabled() && !groups.contains(paramInfo->groupName))
|
|
{
|
|
groups.push_back(paramInfo->groupName);
|
|
m_paramGroupCount++;
|
|
}
|
|
m_paramModels[i]->setGroupId(groups.indexOf(paramInfo->groupName));
|
|
}
|
|
|
|
completerData.push_back(name);
|
|
|
|
m_paramModels[i]->setDisplayName(name);
|
|
m_paramModels[i]->setRange(paramInfo->ranges.min,
|
|
paramInfo->ranges.max,
|
|
paramInfo->ranges.step);
|
|
|
|
// Load settings into model.
|
|
if (init)
|
|
{
|
|
QString idStr = CARLA_SETTING_PREFIX + QString::number(i);
|
|
m_paramModels[i]->loadSettings(m_settingsElem, idStr);
|
|
}
|
|
}
|
|
// Set completer data
|
|
m_completerModel->setStringList(completerData);
|
|
}
|
|
emit paramsUpdated();
|
|
}
|
|
|
|
void CarlaInstrument::clearParamModels()
|
|
{
|
|
//Delete the models, this also disconnects all connections (automation and controller connections)
|
|
for (uint32_t index = 0; index < m_paramModels.size(); ++index)
|
|
{
|
|
delete m_paramModels[index];
|
|
}
|
|
|
|
//Clear the list
|
|
m_paramModels.clear();
|
|
|
|
m_paramGroupCount = 0;
|
|
}
|
|
|
|
void CarlaInstrument::paramModelChanged(uint32_t index)
|
|
{ // Update Carla param (LMMS -> Carla)
|
|
if (!m_paramModels[index]->isOutput())
|
|
{
|
|
if (fDescriptor->set_parameter_value != nullptr)
|
|
{
|
|
fDescriptor->set_parameter_value(fHandle, index, m_paramModels[index]->value());
|
|
|
|
}
|
|
|
|
// TODO? Shouldn't Carla be doing this?
|
|
if (fDescriptor->ui_set_parameter_value != nullptr)
|
|
{
|
|
fDescriptor->ui_set_parameter_value(fHandle, index, m_paramModels[index]->value());
|
|
}
|
|
}
|
|
}
|
|
|
|
void CarlaInstrument::updateParamModel(uint32_t index)
|
|
{ // Called on param changed (Carla -> LMMS)
|
|
if (fDescriptor->get_parameter_value != nullptr)
|
|
{
|
|
m_paramModels[index]->setValue(
|
|
fDescriptor->get_parameter_value(fHandle, index)
|
|
);
|
|
}
|
|
}
|
|
|
|
void CarlaInstrument::loadSettings(const QDomElement& elem)
|
|
{
|
|
if (fHandle == nullptr || fDescriptor->set_state == nullptr)
|
|
return;
|
|
|
|
QDomDocument carlaDoc("carla");
|
|
carlaDoc.appendChild(carlaDoc.importNode(elem.firstChildElement(), true ));
|
|
|
|
fDescriptor->set_state(fHandle, carlaDoc.toString(0).toUtf8().constData());
|
|
|
|
#if CARLA_VERSION_HEX >= CARLA_MIN_PARAM_VERSION
|
|
// Store to load parameter knobs settings when added.
|
|
m_settingsElem = const_cast<QDomElement&>(elem);
|
|
refreshParams(true);
|
|
#endif
|
|
}
|
|
|
|
void CarlaInstrument::play(SampleFrame* workingBuffer)
|
|
{
|
|
const uint bufsize = Engine::audioEngine()->framesPerPeriod();
|
|
|
|
zeroSampleFrames(workingBuffer, bufsize);
|
|
|
|
if (fHandle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// set time info
|
|
Song * const s = Engine::getSong();
|
|
fTimeInfo.playing = s->isPlaying();
|
|
fTimeInfo.frame = s->getPlayPos(s->playMode()).frames(Engine::framesPerTick());
|
|
fTimeInfo.usecs = s->getMilliseconds()*1000;
|
|
fTimeInfo.bbt.bar = s->getBars() + 1;
|
|
fTimeInfo.bbt.beat = s->getBeat() + 1;
|
|
fTimeInfo.bbt.tick = s->getBeatTicks();
|
|
fTimeInfo.bbt.barStartTick = ticksPerBeat*s->getTimeSigModel().getNumerator()*s->getBars();
|
|
fTimeInfo.bbt.beatsPerBar = s->getTimeSigModel().getNumerator();
|
|
fTimeInfo.bbt.beatType = s->getTimeSigModel().getDenominator();
|
|
fTimeInfo.bbt.ticksPerBeat = ticksPerBeat;
|
|
fTimeInfo.bbt.beatsPerMinute = s->getTempo();
|
|
|
|
#ifndef _MSC_VER
|
|
float buf1[bufsize];
|
|
float buf2[bufsize];
|
|
#else
|
|
float *buf1 = static_cast<float *>(_alloca(bufsize * sizeof(float)));
|
|
float *buf2 = static_cast<float *>(_alloca(bufsize * sizeof(float)));
|
|
#endif
|
|
|
|
float* rBuf[] = { buf1, buf2 };
|
|
std::memset(buf1, 0, sizeof(float)*bufsize);
|
|
std::memset(buf2, 0, sizeof(float)*bufsize);
|
|
|
|
{
|
|
const QMutexLocker ml(&fMutex);
|
|
// TODO FIXME this is just here so it compiles.
|
|
// https://github.com/falkTX/Carla/blob/8bceb9ed173a10b29038f8abb4383710c0e497c1/source/includes/CarlaNative.h
|
|
// FIXME for v3.0, use const for the input buffer
|
|
#if CARLA_VERSION_HEX >= CARLA_VERSION_HEX_3
|
|
fDescriptor->process(fHandle, (const float**)rBuf, rBuf, bufsize, fMidiEvents, fMidiEventCount);
|
|
#else
|
|
fDescriptor->process(fHandle, rBuf, rBuf, bufsize, fMidiEvents, fMidiEventCount);
|
|
#endif
|
|
fMidiEventCount = 0;
|
|
}
|
|
|
|
for (uint i=0; i < bufsize; ++i)
|
|
{
|
|
workingBuffer[i][0] = buf1[i];
|
|
workingBuffer[i][1] = buf2[i];
|
|
}
|
|
}
|
|
|
|
bool CarlaInstrument::handleMidiEvent(const MidiEvent& event, const TimePos&, f_cnt_t offset)
|
|
{
|
|
const QMutexLocker ml(&fMutex);
|
|
|
|
if (fMidiEventCount >= kMaxMidiEvents)
|
|
return false;
|
|
|
|
NativeMidiEvent& nEvent(fMidiEvents[fMidiEventCount++]);
|
|
std::memset(&nEvent, 0, sizeof(NativeMidiEvent));
|
|
|
|
nEvent.port = 0;
|
|
nEvent.time = offset;
|
|
std::size_t written = writeToByteSeq(event, nEvent.data, sizeof(NativeMidiEvent::data));
|
|
if(written) { nEvent.size = written; }
|
|
else { --fMidiEventCount; }
|
|
|
|
return true;
|
|
}
|
|
|
|
gui::PluginView* CarlaInstrument::instantiateView(QWidget* parent)
|
|
{
|
|
// Disable plugin focus per https://bugreports.qt.io/browse/QTBUG-30181
|
|
#ifndef CARLA_OS_MAC
|
|
if (QWidget* const window = parent->window())
|
|
// TODO: Remove cast; Only needed for Qt4
|
|
fHost.uiParentId = (uintptr_t)window->winId();
|
|
else
|
|
#endif
|
|
fHost.uiParentId = 0;
|
|
|
|
std::free((char*)fHost.uiName);
|
|
|
|
// TODO - get plugin instance name
|
|
//fHost.uiName = strdup(parent->windowTitle().toUtf8().constData());
|
|
fHost.uiName = strdup(kIsPatchbay ? "CarlaPatchbay-LMMS" : "CarlaRack-LMMS");
|
|
|
|
return new gui::CarlaInstrumentView(this, parent);
|
|
}
|
|
|
|
void CarlaInstrument::sampleRateChanged()
|
|
{
|
|
fDescriptor->dispatcher(fHandle, NATIVE_PLUGIN_OPCODE_SAMPLE_RATE_CHANGED, 0, 0, nullptr, handleGetSampleRate());
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
namespace gui
|
|
{
|
|
|
|
CarlaInstrumentView::CarlaInstrumentView(CarlaInstrument* const instrument, QWidget* const parent)
|
|
: InstrumentViewFixedSize(instrument, parent),
|
|
fHandle(instrument->fHandle),
|
|
fDescriptor(instrument->fDescriptor),
|
|
fTimerId(fHandle != nullptr && fDescriptor->ui_idle != nullptr ? startTimer(30) : 0),
|
|
m_carlaInstrument(instrument),
|
|
m_parent(parent),
|
|
m_paramsSubWindow(nullptr),
|
|
m_paramsView(nullptr)
|
|
{
|
|
setAutoFillBackground(true);
|
|
|
|
QPalette pal;
|
|
pal.setBrush(backgroundRole(), instrument->kIsPatchbay ? PLUGIN_NAME::getIconPixmap("artwork-patchbay") : PLUGIN_NAME::getIconPixmap("artwork-rack"));
|
|
setPalette(pal);
|
|
|
|
auto l = new QHBoxLayout(this);
|
|
l->setContentsMargins( 20, 180, 10, 10 );
|
|
l->setSpacing(3);
|
|
l->setAlignment(Qt::AlignTop);
|
|
|
|
// Show GUI button
|
|
m_toggleUIButton = new QPushButton( tr( "Show GUI" ), this );
|
|
m_toggleUIButton->setCheckable( true );
|
|
m_toggleUIButton->setChecked( false );
|
|
m_toggleUIButton->setIcon( embed::getIconPixmap( "zoom" ) );
|
|
m_toggleUIButton->setFont(adjustedToPixelSize(m_toggleUIButton->font(), SMALL_FONT_SIZE));
|
|
connect( m_toggleUIButton, SIGNAL( clicked(bool) ), this, SLOT( toggleUI( bool ) ) );
|
|
|
|
m_toggleUIButton->setToolTip(
|
|
tr("Click here to show or hide the graphical user interface (GUI) of Carla."));
|
|
|
|
// Open params sub window button
|
|
m_toggleParamsWindowButton = new QPushButton(tr("Params"), this);
|
|
m_toggleParamsWindowButton->setIcon(embed::getIconPixmap("controller"));
|
|
m_toggleParamsWindowButton->setCheckable(true);
|
|
m_toggleParamsWindowButton->setFont(adjustedToPixelSize(m_toggleParamsWindowButton->font(), SMALL_FONT_SIZE));
|
|
#if CARLA_VERSION_HEX < CARLA_MIN_PARAM_VERSION
|
|
m_toggleParamsWindowButton->setEnabled(false);
|
|
m_toggleParamsWindowButton->setToolTip(tr("Available from Carla version 2.1 and up."));
|
|
#else
|
|
connect(m_toggleParamsWindowButton, SIGNAL(clicked(bool)), this, SLOT(toggleParamsWindow()));
|
|
#endif
|
|
|
|
// Add widgets to layout
|
|
l->addWidget( m_toggleUIButton );
|
|
l->addWidget(m_toggleParamsWindowButton);
|
|
|
|
// Connect signals
|
|
connect(m_toggleUIButton, SIGNAL(clicked(bool)), this, SLOT(toggleUI(bool)));
|
|
connect(instrument, SIGNAL(uiClosed()), this, SLOT(uiClosed()));
|
|
}
|
|
|
|
CarlaInstrumentView::~CarlaInstrumentView()
|
|
{
|
|
if (m_toggleUIButton->isChecked())
|
|
{
|
|
toggleUI(false);
|
|
}
|
|
|
|
#if CARLA_VERSION_HEX >= CARLA_MIN_PARAM_VERSION
|
|
if (m_paramsView)
|
|
{
|
|
delete m_paramsView;
|
|
m_paramsView = nullptr;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void CarlaInstrumentView::toggleUI(bool visible)
|
|
{
|
|
if (fHandle != nullptr && fDescriptor->ui_show != nullptr) {
|
|
// TODO: remove when fixed upstream
|
|
// change working path to location of carla.dll to avoid conflict with lmms
|
|
#if defined(CARLA_OS_WIN32) || defined(CARLA_OS_WIN64)
|
|
if (visible) {
|
|
QString backupDir = QDir::currentPath();
|
|
QDir::setCurrent(carla_get_library_folder());
|
|
fDescriptor->ui_show(fHandle, true);
|
|
QDir::setCurrent(backupDir);
|
|
return;
|
|
}
|
|
#endif
|
|
fDescriptor->ui_show(fHandle, visible);
|
|
}
|
|
}
|
|
|
|
void CarlaInstrumentView::uiClosed()
|
|
{
|
|
m_toggleUIButton->setChecked(false);
|
|
}
|
|
|
|
void CarlaInstrumentView::modelChanged()
|
|
{
|
|
}
|
|
|
|
void CarlaInstrumentView::timerEvent(QTimerEvent* event)
|
|
{
|
|
if (event->timerId() == fTimerId)
|
|
fDescriptor->ui_idle(fHandle);
|
|
|
|
InstrumentView::timerEvent(event);
|
|
}
|
|
|
|
void CarlaInstrumentView::toggleParamsWindow()
|
|
{
|
|
if (!m_paramsSubWindow)
|
|
{
|
|
m_paramsView = new CarlaParamsView(this, m_parent);
|
|
connect(m_paramsSubWindow, SIGNAL(uiClosed()), this, SLOT(paramsUiClosed()));
|
|
}
|
|
else
|
|
{
|
|
if (m_paramsSubWindow->isVisible())
|
|
{
|
|
m_paramsSubWindow->hide();
|
|
}
|
|
else
|
|
{
|
|
m_paramsSubWindow->show();
|
|
}
|
|
}
|
|
}
|
|
|
|
void CarlaInstrumentView::paramsUiClosed()
|
|
{
|
|
m_toggleParamsWindowButton->setChecked(false);
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
|
|
CarlaParamsView::CarlaParamsView(CarlaInstrumentView* const instrumentView, QWidget* const parent)
|
|
: InstrumentView(instrumentView->m_carlaInstrument, parent),
|
|
m_carlaInstrument(instrumentView->m_carlaInstrument),
|
|
m_carlaInstrumentView(instrumentView),
|
|
m_maxColumns(6),
|
|
m_curColumn(0),
|
|
m_curRow(0),
|
|
m_curOutColumn(0),
|
|
m_curOutRow(0)
|
|
{
|
|
auto centralWidget = new QWidget(this);
|
|
auto verticalLayout = new QVBoxLayout(centralWidget);
|
|
|
|
// -- Toolbar
|
|
m_toolBarLayout = new QHBoxLayout();
|
|
|
|
// Toolbar widgets
|
|
QSizePolicy sizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
|
|
sizePolicy.setHorizontalStretch(0);
|
|
sizePolicy.setVerticalStretch(0);
|
|
|
|
// Params filter line edit
|
|
m_paramsFilterLineEdit = new QLineEdit(this);
|
|
m_paramsFilterLineEdit->setPlaceholderText(tr("Search.."));
|
|
m_paramsFilterLineEdit->setCompleter(m_carlaInstrument->m_paramsCompleter);
|
|
|
|
// Clear filter line edit button
|
|
m_clearFilterButton = new QPushButton(tr(""), this);
|
|
m_clearFilterButton->setIcon(embed::getIconPixmap("edit_erase"));
|
|
m_clearFilterButton->setToolTip(tr("Clear filter text"));
|
|
sizePolicy.setHeightForWidth(m_clearFilterButton->sizePolicy().hasHeightForWidth());
|
|
m_clearFilterButton->setSizePolicy(sizePolicy);
|
|
|
|
// Show automated only button
|
|
m_automatedOnlyButton = new QPushButton(tr(""), this);
|
|
m_automatedOnlyButton->setIcon(embed::getIconPixmap("automation"));
|
|
m_automatedOnlyButton->setToolTip(
|
|
tr("Only show knobs with a connection."));
|
|
m_automatedOnlyButton->setCheckable(true);
|
|
sizePolicy.setHeightForWidth(m_automatedOnlyButton->sizePolicy().hasHeightForWidth());
|
|
m_automatedOnlyButton->setSizePolicy(sizePolicy);
|
|
|
|
// Group name combobox
|
|
m_groupFilterCombo = new QComboBox(this);
|
|
m_groupFilterModel = new QStringListModel(this);
|
|
m_groupFilterCombo->setModel(m_groupFilterModel);
|
|
|
|
// Add stuff to toolbar
|
|
m_toolBarLayout->addWidget(m_paramsFilterLineEdit);
|
|
m_toolBarLayout->addWidget(m_clearFilterButton);
|
|
m_toolBarLayout->addWidget(m_automatedOnlyButton);
|
|
m_toolBarLayout->addWidget(m_groupFilterCombo);
|
|
|
|
// -- Input params
|
|
auto inputFrame = new QFrame(this);
|
|
auto inputLayout = new QVBoxLayout(inputFrame);
|
|
auto inputLabel = new QLabel("Input parameters", inputFrame);
|
|
|
|
m_inputScrollArea = new QScrollArea(inputFrame);
|
|
m_inputScrollAreaWidgetContent = new QWidget();
|
|
m_inputScrollAreaLayout = new QGridLayout(m_inputScrollAreaWidgetContent);
|
|
|
|
m_inputScrollAreaWidgetContent->setLayout(m_inputScrollAreaLayout);
|
|
m_inputScrollAreaWidgetContent->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
|
|
|
|
m_inputScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
m_inputScrollArea->setWidget(m_inputScrollAreaWidgetContent);
|
|
m_inputScrollArea->setWidgetResizable(true);
|
|
m_inputScrollArea->setFrameShadow(QFrame::Plain);
|
|
m_inputScrollArea->setFrameShape(QFrame::NoFrame);
|
|
|
|
m_inputScrollAreaLayout->setContentsMargins(3, 3, 3, 3);
|
|
m_inputScrollAreaLayout->setVerticalSpacing(12);
|
|
m_inputScrollAreaLayout->setHorizontalSpacing(6);
|
|
m_inputScrollAreaLayout->setColumnStretch(m_maxColumns, 1);
|
|
|
|
inputLayout->addWidget(inputLabel);
|
|
inputLayout->addWidget(m_inputScrollArea);
|
|
|
|
// -- Output params
|
|
auto outputFrame = new QFrame(this);
|
|
auto outputLayout = new QVBoxLayout(outputFrame);
|
|
auto outputLabel = new QLabel("Output parameters", outputFrame);
|
|
|
|
m_outputScrollArea = new QScrollArea(outputFrame);
|
|
m_outputScrollAreaWidgetContent = new QWidget();
|
|
m_outputScrollAreaLayout = new QGridLayout(m_outputScrollAreaWidgetContent);
|
|
|
|
m_outputScrollAreaWidgetContent->setLayout(m_outputScrollAreaLayout);
|
|
m_outputScrollAreaWidgetContent->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
|
|
|
|
m_outputScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
|
m_outputScrollArea->setWidget(m_outputScrollAreaWidgetContent);
|
|
m_outputScrollArea->setWidgetResizable(true);
|
|
m_outputScrollArea->setFrameShadow(QFrame::Plain);
|
|
m_outputScrollArea->setFrameShape(QFrame::NoFrame);
|
|
|
|
m_outputScrollAreaLayout->setContentsMargins(3, 28, 3, 3);
|
|
m_outputScrollAreaLayout->setVerticalSpacing(12);
|
|
m_outputScrollAreaLayout->setHorizontalSpacing(6);
|
|
m_outputScrollAreaLayout->setColumnStretch(m_maxColumns, 1);
|
|
|
|
outputLayout->addWidget(outputLabel);
|
|
outputLayout->addWidget(m_outputScrollArea);
|
|
|
|
// -- QSplitter
|
|
auto splitter = new QSplitter(Qt::Vertical, this);
|
|
|
|
// -- Add layout and widgets.
|
|
verticalLayout->addLayout(m_toolBarLayout);
|
|
splitter->addWidget(inputFrame);
|
|
splitter->addWidget(outputFrame);
|
|
verticalLayout->addWidget(splitter);
|
|
|
|
// -- Sub window
|
|
auto win = new CarlaParamsSubWindow(getGUI()->mainWindow()->workspace()->viewport(),
|
|
Qt::SubWindow | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
|
|
m_carlaInstrumentView->m_paramsSubWindow = getGUI()->mainWindow()->workspace()->addSubWindow(win);
|
|
m_carlaInstrumentView->m_paramsSubWindow->setSizePolicy(
|
|
QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
|
|
m_carlaInstrumentView->m_paramsSubWindow->setMinimumHeight(200);
|
|
m_carlaInstrumentView->m_paramsSubWindow->setMinimumWidth(200);
|
|
m_carlaInstrumentView->m_paramsSubWindow->resize(600, 400);
|
|
m_carlaInstrumentView->m_paramsSubWindow->setWidget(centralWidget);
|
|
centralWidget->setWindowTitle(m_carlaInstrument->instrumentTrack()->name() + tr(" - Parameters"));
|
|
|
|
// -- Connect signals
|
|
connect(m_carlaInstrumentView->m_paramsSubWindow, SIGNAL(resized()), this, SLOT(windowResized()));
|
|
connect(m_paramsFilterLineEdit, SIGNAL(textChanged(const QString)), this, SLOT(filterKnobs()));
|
|
connect(m_clearFilterButton, SIGNAL(clicked(bool)), this, SLOT(clearFilterText()));
|
|
connect(m_automatedOnlyButton, SIGNAL(toggled(bool)), this, SLOT(filterKnobs()));
|
|
connect(m_groupFilterCombo, SIGNAL(currentTextChanged(const QString)), this, SLOT(filterKnobs()));
|
|
connect(m_carlaInstrument, SIGNAL(paramsUpdated()), this, SLOT(refreshKnobs()));
|
|
|
|
m_carlaInstrumentView->m_paramsSubWindow->show(); // Show the subwindow
|
|
|
|
// Add knobs if there are any already.
|
|
// Call this after show() so the m_inputScrollArea->width() is set properly.
|
|
refreshKnobs(); // Will trigger filterKnobs() due m_groupFilterCombo->setCurrentIndex(0)
|
|
}
|
|
|
|
CarlaParamsView::~CarlaParamsView()
|
|
{
|
|
// Close and delete m_paramsSubWindow
|
|
if (m_carlaInstrumentView->m_paramsSubWindow)
|
|
{
|
|
m_carlaInstrumentView->m_paramsSubWindow->setAttribute(Qt::WA_DeleteOnClose);
|
|
m_carlaInstrumentView->m_paramsSubWindow->close();
|
|
|
|
delete m_carlaInstrumentView->m_paramsSubWindow;
|
|
m_carlaInstrumentView->m_paramsSubWindow = nullptr;
|
|
}
|
|
|
|
m_carlaInstrumentView->m_paramsView = nullptr;
|
|
|
|
// Clear models
|
|
if (!m_carlaInstrument->m_paramModels.empty())
|
|
{
|
|
m_carlaInstrument->clearParamModels();
|
|
}
|
|
}
|
|
|
|
void CarlaParamsView::clearFilterText()
|
|
{
|
|
m_paramsFilterLineEdit->setText("");
|
|
}
|
|
|
|
void CarlaParamsView::filterKnobs()
|
|
{
|
|
clearKnobs(); // Remove all knobs from the layout.
|
|
|
|
if (!m_carlaInstrument->m_paramGroupCount)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Calc how many knobs will fit horizontal in the params window.
|
|
uint16_t maxKnobWidth = m_maxKnobWidthPerGroup[m_groupFilterCombo->currentIndex()];
|
|
maxKnobWidth += m_inputScrollAreaLayout->spacing();
|
|
if (!maxKnobWidth)
|
|
{
|
|
// Prevent possible division by zero.
|
|
return;
|
|
}
|
|
m_maxColumns = m_inputScrollArea->width() / maxKnobWidth;
|
|
|
|
QString text = m_paramsFilterLineEdit->text();
|
|
for (uint32_t i = 0; i < m_knobs.size(); ++i)
|
|
{
|
|
// Don't show disabled (unused) knobs.
|
|
if (!m_carlaInstrument->m_paramModels[i]->enabled())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Filter on automation only
|
|
if (m_automatedOnlyButton->isChecked())
|
|
{
|
|
if (! m_carlaInstrument->m_paramModels[i]->isAutomatedOrControlled())
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Filter on group name
|
|
if (m_groupFilterCombo->currentText() != m_carlaInstrument->m_paramModels[i]->groupName())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Filter on text
|
|
if (text != "")
|
|
{
|
|
if (m_knobs[i]->objectName().contains(text, Qt::CaseInsensitive))
|
|
{
|
|
addKnob(i);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
addKnob(i);
|
|
}
|
|
}
|
|
|
|
// Add spacer so all knobs go to top
|
|
auto verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
|
m_inputScrollAreaLayout->addItem(verticalSpacer, m_curRow+1, 0, 1, 1);
|
|
}
|
|
|
|
void CarlaParamsView::refreshKnobs()
|
|
{
|
|
// Make sure all the knobs are deleted.
|
|
for (uint32_t i = 0; i < m_knobs.size(); ++i)
|
|
{
|
|
delete m_knobs[i]; // Delete knob widgets itself.
|
|
}
|
|
m_knobs.clear(); // Clear the pointer list.
|
|
|
|
// Reset position data.
|
|
m_curColumn = 0;
|
|
m_curRow = 0;
|
|
|
|
m_curOutColumn = 0;
|
|
m_curOutRow = 0;
|
|
|
|
// Clear max knob width per group
|
|
m_maxKnobWidthPerGroup.clear();
|
|
m_maxKnobWidthPerGroup.reserve(m_carlaInstrument->m_paramGroupCount);
|
|
for (uint8_t i = 0; i < m_carlaInstrument->m_paramGroupCount; i++)
|
|
{
|
|
m_maxKnobWidthPerGroup[i] = 0;
|
|
}
|
|
|
|
if (m_carlaInstrument->m_paramModels.empty()) { return; }
|
|
|
|
// Make room in QList m_knobs
|
|
m_knobs.reserve(m_carlaInstrument->m_paramModels.size());
|
|
|
|
QStringList groupNameList;
|
|
groupNameList.reserve(m_carlaInstrument->m_paramGroupCount);
|
|
|
|
for (const auto currentParamModel : m_carlaInstrument->m_paramModels)
|
|
{
|
|
bool enabled = currentParamModel->enabled();
|
|
const QString name = currentParamModel->displayName();
|
|
|
|
auto currentKnob = new Knob(KnobType::Dark28, name, m_inputScrollAreaWidgetContent, Knob::LabelRendering::LegacyFixedFontSize);
|
|
currentKnob->setHintText(name, "");
|
|
currentKnob->setObjectName(name); // this is being used for filtering the knobs.
|
|
|
|
// Set the newly created model to the knob.
|
|
currentKnob->setModel(currentParamModel);
|
|
currentKnob->setEnabled(enabled);
|
|
|
|
m_knobs.push_back(currentKnob);
|
|
|
|
if (enabled)
|
|
{
|
|
// Collect group names
|
|
if (!groupNameList.contains(currentParamModel->groupName()))
|
|
{
|
|
groupNameList.append(currentParamModel->groupName());
|
|
}
|
|
|
|
// Store biggest knob width per group (so we can calc how many knobs we can fit horizontally)
|
|
auto & maxGroupWidth = m_maxKnobWidthPerGroup[currentParamModel->groupId()];
|
|
maxGroupWidth = std::max(maxGroupWidth, static_cast<uint16_t>(currentKnob->width()));
|
|
}
|
|
}
|
|
|
|
// Set new list with group names to the model
|
|
if (!groupNameList.count())
|
|
{
|
|
groupNameList.append("No params");
|
|
}
|
|
m_groupFilterModel->setStringList(groupNameList);
|
|
m_groupFilterCombo->setCurrentIndex(0);
|
|
}
|
|
|
|
|
|
void CarlaParamsView::windowResized()
|
|
{
|
|
filterKnobs();
|
|
}
|
|
|
|
|
|
void CarlaParamsView::addKnob(uint32_t index)
|
|
{
|
|
bool output = m_carlaInstrument->m_paramModels[index]->isOutput();
|
|
if (output)
|
|
{
|
|
m_outputScrollAreaLayout->addWidget(
|
|
m_knobs[index], m_curOutRow, m_curOutColumn, Qt::AlignHCenter | Qt::AlignTop);
|
|
m_knobs[index]->setEnabled(false); // We should not be able to adjust output.
|
|
m_knobs[index]->show();
|
|
if (m_curOutColumn < m_maxColumns - 1)
|
|
{
|
|
m_curOutColumn++;
|
|
}
|
|
else
|
|
{
|
|
m_curOutColumn = 0;
|
|
m_curOutRow++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Add the new knob to layout
|
|
m_inputScrollAreaLayout->addWidget(m_knobs[index], m_curRow, m_curColumn, Qt::AlignHCenter | Qt::AlignTop);
|
|
m_inputScrollAreaLayout->setColumnStretch(m_curColumn, 1);
|
|
|
|
// Chances that we did close() on the widget is big, so show it.
|
|
m_knobs[index]->show();
|
|
|
|
// Keep track of current column and row index.
|
|
if (m_curColumn < m_maxColumns - 1)
|
|
{
|
|
m_curColumn++;
|
|
}
|
|
else
|
|
{
|
|
m_curColumn = 0;
|
|
m_curRow++;
|
|
}
|
|
}
|
|
}
|
|
void CarlaParamsView::clearKnobs()
|
|
{
|
|
// Remove knobs from layout.
|
|
for (uint16_t i = 0; i < m_knobs.size(); ++i)
|
|
{
|
|
m_knobs[i]->close();
|
|
}
|
|
|
|
// Remove spacers
|
|
for (int16_t i=m_inputScrollAreaLayout->count() - 1; i > 0; i--)
|
|
{
|
|
auto item = m_inputScrollAreaLayout->takeAt(i);
|
|
if (item->widget()) {continue;}
|
|
delete item;
|
|
}
|
|
for (int16_t i=m_outputScrollAreaLayout->count() - 1; i > 0; i--)
|
|
{
|
|
auto item = m_outputScrollAreaLayout->takeAt(i);
|
|
if (item->widget()) {continue;}
|
|
delete item;
|
|
}
|
|
|
|
// Reset position data.
|
|
m_curColumn = 0;
|
|
m_curRow = 0;
|
|
|
|
m_curOutColumn = 0;
|
|
m_curOutRow = 0;
|
|
}
|
|
|
|
|
|
} // namespace gui
|
|
|
|
} // namespace lmms
|