Support for alternative tunings and keyboard mappings (#5522)

Co-authored-by: Kevin Zander <veratil@gmail.com>
Co-authored-by: Dominic Clark <mrdomclark@gmail.com>
Co-authored-by: Martin <martin@sigma.he29.net>
This commit is contained in:
Martin Pavelek
2021-09-09 19:49:24 +02:00
committed by GitHub
parent ace502f1a5
commit e07861ced3
42 changed files with 2079 additions and 92 deletions

View File

@@ -29,6 +29,7 @@ set(LMMS_SRCS
core/InstrumentPlayHandle.cpp
core/InstrumentSoundShaping.cpp
core/JournallingObject.cpp
core/Keymap.cpp
core/Ladspa2LMMS.cpp
core/LadspaControl.cpp
core/LadspaManager.cpp
@@ -39,6 +40,7 @@ set(LMMS_SRCS
core/MemoryManager.cpp
core/MeterModel.cpp
core/MicroTimer.cpp
core/Microtuner.cpp
core/Mixer.cpp
core/MixerProfiler.cpp
core/MixerWorkerThread.cpp
@@ -67,6 +69,7 @@ set(LMMS_SRCS
core/SamplePlayHandle.cpp
core/SampleRecordHandle.cpp
core/SampleTCO.cpp
core/Scale.cpp
core/SerializingObject.cpp
core/Song.cpp
core/TempoSyncKnobModel.cpp

View File

@@ -35,6 +35,12 @@ void ComboBoxModel::addItem( QString item, unique_ptr<PixmapLoader> loader )
}
void ComboBoxModel::replaceItem(std::size_t index, QString item, unique_ptr<PixmapLoader> loader)
{
assert(index < m_items.size());
m_items[index] = Item(move(item), move(loader));
emit propertiesChanged();
}
void ComboBoxModel::clear()

149
src/core/Keymap.cpp Normal file
View File

@@ -0,0 +1,149 @@
/*
* Keymap.cpp - implementation of keymap class
*
* Copyright (c) 2020 Martin Pavelek <he29.HS/at/gmail.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 "Keymap.h"
#include <QDomElement>
Keymap::Keymap() :
m_description(tr("empty")),
m_firstKey(0),
m_lastKey(NumKeys - 1),
m_middleKey(DefaultMiddleKey),
m_baseKey(DefaultBaseKey),
m_baseFreq(DefaultBaseFreq)
{
}
Keymap::Keymap(
QString description,
std::vector<int> newMap,
int newFirst,
int newLast,
int newMiddle,
int newBaseKey,
float newBaseFreq
) :
m_description(description),
m_map(std::move(newMap)),
m_firstKey(newFirst),
m_lastKey(newLast),
m_middleKey(newMiddle),
m_baseKey(newBaseKey),
m_baseFreq(newBaseFreq)
{
}
/**
* \brief Return scale degree for a given key, based on current map and first/middle/last notes
* \param MIDI key to be mapped
* \return Scale degree defined by the mapping on success, -1 if key isn't mapped
*/
int Keymap::getDegree(int key) const
{
if (key < m_firstKey || key > m_lastKey) {return -1;}
if (m_map.empty()) {return key;} // exception: empty mapping table means linear (1:1) mapping
const int keyOffset = key - m_middleKey; // -127..127
const int key_rem = keyOffset % static_cast<int>(m_map.size()); // remainder
const int key_mod = key_rem >= 0 ? key_rem : key_rem + m_map.size(); // true modulo
return m_map[key_mod];
}
/**
* \brief Return octave offset for a given key, based on current map and the middle note
* \param MIDI key to be mapped
* \return Octave offset defined by the mapping on success, 0 if key isn't mapped
*/
int Keymap::getOctave(int key) const
{
// The keymap wraparound cannot cause an octave transition if a key isn't mapped or the map is empty → return 0
if (m_map.empty() || getDegree(key) == -1) {return 0;}
const int keyOffset = key - m_middleKey;
if (keyOffset >= 0)
{
return keyOffset / static_cast<int>(m_map.size());
}
else
{
return (keyOffset + 1) / static_cast<int>(m_map.size()) - 1;
}
}
QString Keymap::getDescription() const
{
return m_description;
}
void Keymap::setDescription(QString description)
{
m_description = description;
}
void Keymap::saveSettings(QDomDocument &document, QDomElement &element)
{
element.setAttribute("description", m_description);
element.setAttribute("first_key", m_firstKey);
element.setAttribute("last_key", m_lastKey);
element.setAttribute("middle_key", m_middleKey);
element.setAttribute("base_key", m_baseKey);
element.setAttribute("base_freq", m_baseFreq);
for (int i = 0; i < m_map.size(); i++)
{
QDomElement degree = document.createElement("degree");
element.appendChild(degree);
degree.setAttribute("value", m_map[i]);
}
}
void Keymap::loadSettings(const QDomElement &element)
{
m_description = element.attribute("description");
m_firstKey = element.attribute("first_key").toInt();
m_lastKey = element.attribute("last_key").toInt();
m_middleKey = element.attribute("middle_key").toInt();
m_baseKey = element.attribute("base_key").toInt();
m_baseFreq = element.attribute("base_freq").toDouble();
QDomNode node = element.firstChild();
m_map.clear();
for (int i = 0; !node.isNull(); i++)
{
m_map.push_back(node.toElement().attribute("value").toInt());
node = node.nextSibling();
}
}

167
src/core/Microtuner.cpp Normal file
View File

@@ -0,0 +1,167 @@
/*
* Microtuner.cpp - manage tuning and scale information of an instrument
*
* Copyright (c) 2020 Martin Pavelek <he29.HS/at/gmail.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 "Microtuner.h"
#include <vector>
#include <cmath>
#include "ConfigManager.h"
#include "Engine.h"
#include "Keymap.h"
#include "Scale.h"
#include "Song.h"
Microtuner::Microtuner() :
Model(nullptr, tr("Microtuner")),
m_enabledModel(false, this, tr("Microtuner on / off")),
m_scaleModel(this, tr("Selected scale")),
m_keymapModel(this, tr("Selected keyboard mapping")),
m_keyRangeImportModel(true)
{
for (unsigned int i = 0; i < MaxScaleCount; i++)
{
m_scaleModel.addItem(QString::number(i) + ": " + Engine::getSong()->getScale(i)->getDescription());
}
for (unsigned int i = 0; i < MaxKeymapCount; i++)
{
m_keymapModel.addItem(QString::number(i) + ": " + Engine::getSong()->getKeymap(i)->getDescription());
}
connect(Engine::getSong(), SIGNAL(scaleListChanged(int)), this, SLOT(updateScaleList(int)));
connect(Engine::getSong(), SIGNAL(keymapListChanged(int)), this, SLOT(updateKeymapList(int)));
}
/** \brief Return frequency for a given MIDI key, using the active mapping and scale.
* \param key A MIDI key number ranging from 0 to 127.
* \return Frequency in Hz; 0 if key is out of range or not mapped.
*/
float Microtuner::keyToFreq(int key, int userBaseNote) const
{
if (key < 0 || key >= NumKeys) {return 0;}
Song *song = Engine::getSong();
if (!song) {return 0;}
// Get keymap and scale selected at this moment
std::shared_ptr<const Keymap> keymap = song->getKeymap(m_keymapModel.value());
std::shared_ptr<const Scale> scale = song->getScale(m_scaleModel.value());
const std::vector<Interval> &intervals = scale->getIntervals();
// Convert MIDI key to scale degree + octave offset.
// The octaves are primarily driven by the keymap wraparound: octave count is increased or decreased if the key
// goes over or under keymap range. In case the keymap refers to a degree that does not exist in the scale, it is
// assumed the keymap is non-repeating or just really big, so the octaves are also driven by the scale wraparound.
const int keymapDegree = keymap->getDegree(key); // which interval should be used according to the keymap
if (keymapDegree == -1) {return 0;} // key is not mapped, abort
const int keymapOctave = keymap->getOctave(key); // how many times did the keymap repeat
const int octaveDegree = intervals.size() - 1; // index of the interval with octave ratio
if (octaveDegree == 0) { // octave interval is 1/1, i.e. constant base frequency
return keymap->getBaseFreq(); // → return the baseFreq directly
}
const int scaleOctave = keymapDegree / octaveDegree;
// which interval should be used according to the scale and keymap together
const int degree_rem = keymapDegree % octaveDegree;
const int scaleDegree = degree_rem >= 0 ? degree_rem : degree_rem + octaveDegree; // get true modulo
// Compute base note (the "A4 reference") degree and octave
const int baseNote = m_keyRangeImportModel.value() ? keymap->getBaseKey() : userBaseNote;
const int baseKeymapDegree = keymap->getDegree(baseNote);
if (baseKeymapDegree == -1) {return 0;} // base key is not mapped, umm...
const int baseKeymapOctave = keymap->getOctave(baseNote);
const int baseScaleOctave = baseKeymapDegree / octaveDegree;
const int baseDegree_rem = baseKeymapDegree % octaveDegree;
const int baseScaleDegree = baseDegree_rem >= 0 ? baseDegree_rem : baseDegree_rem + octaveDegree;
// Compute frequency of the middle note and return the final frequency
const double octaveRatio = intervals[octaveDegree].getRatio();
const float middleFreq = (keymap->getBaseFreq() / pow(octaveRatio, (baseScaleOctave + baseKeymapOctave)))
/ intervals[baseScaleDegree].getRatio();
return middleFreq * intervals[scaleDegree].getRatio() * pow(octaveRatio, keymapOctave + scaleOctave);
}
/**
* \brief Update scale name displayed in the microtuner scale list.
* \param index Index of the scale to update; update all scales if -1 or out of range.
*/
void Microtuner::updateScaleList(int index)
{
if (index >= 0 && index < MaxScaleCount)
{
m_scaleModel.replaceItem(index,
QString::number(index) + ": " + Engine::getSong()->getScale(index)->getDescription());
}
else
{
for (int i = 0; i < MaxScaleCount; i++)
{
m_scaleModel.replaceItem(i,
QString::number(i) + ": " + Engine::getSong()->getScale(i)->getDescription());
}
}
}
/**
* \brief Update keymap name displayed in the microtuner scale list.
* \param index Index of the keymap to update; update all keymaps if -1 or out of range.
*/
void Microtuner::updateKeymapList(int index)
{
if (index >= 0 && index < MaxKeymapCount)
{
m_keymapModel.replaceItem(index,
QString::number(index) + ": " + Engine::getSong()->getKeymap(index)->getDescription());
}
else
{
for (int i = 0; i < MaxKeymapCount; i++)
{
m_keymapModel.replaceItem(i,
QString::number(i) + ": " + Engine::getSong()->getKeymap(i)->getDescription());
}
}
}
void Microtuner::saveSettings(QDomDocument &document, QDomElement &element)
{
m_enabledModel.saveSettings(document, element, "enabled");
m_scaleModel.saveSettings(document, element, "scale");
m_keymapModel.saveSettings(document, element, "keymap");
m_keyRangeImportModel.saveSettings(document, element, "range_import");
}
void Microtuner::loadSettings(const QDomElement &element)
{
m_enabledModel.loadSettings(element, "enabled");
m_scaleModel.loadSettings(element, "scale");
m_keymapModel.loadSettings(element, "keymap");
m_keyRangeImportModel.loadSettings(element, "range_import");
}

View File

@@ -24,15 +24,15 @@
*/
#include "NotePlayHandle.h"
#include "lmms_constants.h"
#include "BasicFilters.h"
#include "DetuningHelper.h"
#include "InstrumentSoundShaping.h"
#include "InstrumentTrack.h"
#include "Instrument.h"
#include "Mixer.h"
#include "Song.h"
NotePlayHandle::BaseDetuning::BaseDetuning( DetuningHelper *detuning ) :
m_value( detuning ? detuning->automationPattern()->valueAt( 0 ) : 0 )
{
@@ -516,19 +516,38 @@ bool NotePlayHandle::operator==( const NotePlayHandle & _nph ) const
void NotePlayHandle::updateFrequency()
{
int mp = m_instrumentTrack->m_useMasterPitchModel.value() ? Engine::getSong()->masterPitch() : 0;
const float pitch =
( key() -
m_instrumentTrack->baseNoteModel()->value() +
mp +
m_baseDetuning->value() )
/ 12.0f;
m_frequency = BaseFreq * powf( 2.0f, pitch + m_instrumentTrack->pitchModel()->value() / ( 100 * 12.0f ) );
m_unpitchedFrequency = BaseFreq * powf( 2.0f, pitch );
int masterPitch = m_instrumentTrack->m_useMasterPitchModel.value() ? Engine::getSong()->masterPitch() : 0;
int baseNote = m_instrumentTrack->baseNoteModel()->value();
float detune = m_baseDetuning->value();
float instrumentPitch = m_instrumentTrack->pitchModel()->value();
for( NotePlayHandleList::Iterator it = m_subNotes.begin(); it != m_subNotes.end(); ++it )
if (m_instrumentTrack->m_microtuner.enabled())
{
( *it )->updateFrequency();
// custom key mapping and scale: get frequency from the microtuner
const float detuneMaster = detune + masterPitch;
if (m_instrumentTrack->isKeyMapped(key()))
{
const auto frequency = m_instrumentTrack->m_microtuner.keyToFreq(key(), baseNote);
m_frequency = frequency * powf(2.f, (detuneMaster + instrumentPitch / 100) / 12.f);
m_unpitchedFrequency = frequency * powf(2.f, detuneMaster / 12.f);
}
else
{
m_frequency = m_unpitchedFrequency = 0;
}
}
else
{
// default key mapping and 12-TET frequency computation with default 440 Hz base note frequency
const float pitch = (key() - baseNote + masterPitch + detune) / 12.0f;
m_frequency = DefaultBaseFreq * powf(2.0f, pitch + instrumentPitch / (100 * 12.0f));
m_unpitchedFrequency = DefaultBaseFreq * powf(2.0f, pitch);
}
for (auto it : m_subNotes)
{
it->updateFrequency();
}
}

View File

@@ -56,6 +56,7 @@
#include "endian_handling.h"
#include "Engine.h"
#include "GuiApplication.h"
#include "lmms_constants.h"
#include "Mixer.h"
#include "PathUtil.h"
@@ -75,7 +76,7 @@ SampleBuffer::SampleBuffer() :
m_loopEndFrame(0),
m_amplification(1.0f),
m_reversed(false),
m_frequency(BaseFreq),
m_frequency(DefaultBaseFreq),
m_sampleRate(mixerSampleRate())
{
@@ -718,6 +719,9 @@ bool SampleBuffer::play(
// variable for determining if we should currently be playing backwards in a ping-pong loop
bool isBackwards = state->isBackwards();
// The SampleBuffer can play a given sample with increased or decreased pitch. However, only
// samples that contain a tone that matches the default base note frequency of 440 Hz will
// produce the exact requested pitch in [Hz].
const double freqFactor = (double) freq / (double) m_frequency *
m_sampleRate / Engine::mixer()->processingSampleRate();

View File

@@ -27,6 +27,7 @@
#include "BBTrack.h"
#include "Engine.h"
#include "InstrumentTrack.h"
#include "lmms_constants.h"
#include "Mixer.h"
#include "SampleTCO.h"
@@ -110,10 +111,11 @@ void SamplePlayHandle::play( sampleFrame * buffer )
/* stereoVolumeVector v =
{ { m_volumeModel->value() / DefaultVolume,
m_volumeModel->value() / DefaultVolume } };*/
if( ! m_sampleBuffer->play( workingBuffer, &m_state, frames,
BaseFreq ) )
// SamplePlayHandle always plays the sample at its original pitch;
// it is used only for previews, SampleTracks and the metronome.
if (!m_sampleBuffer->play(workingBuffer, &m_state, frames, DefaultBaseFreq))
{
memset( workingBuffer, 0, frames * sizeof( sampleFrame ) );
memset(workingBuffer, 0, frames * sizeof(sampleFrame));
}
}

122
src/core/Scale.cpp Normal file
View File

@@ -0,0 +1,122 @@
/*
* Scale.cpp - implementation of scale class
*
* Copyright (c) 2020 Martin Pavelek <he29.HS/at/gmail.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 "Scale.h"
#include <QDomElement>
Interval::Interval(float cents) :
m_numerator(0),
m_denominator(0),
m_cents(cents)
{
m_ratio = powf(2.f, m_cents / 1200.f);
}
Interval::Interval(uint32_t numerator, uint32_t denominator) :
m_numerator(numerator),
m_denominator(denominator > 0 ? denominator : 1),
m_cents(0)
{
m_ratio = static_cast<float>(m_numerator) / m_denominator;
}
void Interval::saveSettings(QDomDocument &document, QDomElement &element)
{
if (m_denominator > 0)
{
element.setAttribute("num", QString::number(m_numerator));
element.setAttribute("den", QString::number(m_denominator));
}
else
{
element.setAttribute("cents", QString::number(m_cents));
}
}
void Interval::loadSettings(const QDomElement &element)
{
m_numerator = element.attribute("num", "0").toULong();
m_denominator = element.attribute("den", "0").toULong();
m_cents = element.attribute("cents", "0").toDouble();
if (m_denominator) {m_ratio = static_cast<float>(m_numerator) / m_denominator;}
else {m_ratio = powf(2.f, m_cents / 1200.f);}
}
Scale::Scale() :
m_description(tr("empty"))
{
m_intervals.push_back(Interval(1, 1));
}
Scale::Scale(QString description, std::vector<Interval> intervals) :
m_description(description),
m_intervals(std::move(intervals))
{
}
QString Scale::getDescription() const
{
return m_description;
}
void Scale::setDescription(QString description)
{
m_description = description;
}
void Scale::saveSettings(QDomDocument &document, QDomElement &element)
{
element.setAttribute("description", m_description);
for (auto& interval : m_intervals)
{
interval.saveState(document, element);
}
}
void Scale::loadSettings(const QDomElement &element)
{
m_description = element.attribute("description");
QDomNode node = element.firstChild();
m_intervals.clear();
for (int i = 0; !node.isNull(); i++)
{
Interval temp;
temp.restoreState(node.toElement());
m_intervals.push_back(temp);
node = node.nextSibling();
}
}

View File

@@ -116,6 +116,9 @@ Song::Song() :
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>();}
}
@@ -1132,6 +1135,14 @@ void Song::loadProject( const QString & fileName )
{
restoreControllerStates( node.toElement() );
}
else if (node.nodeName() == "scales")
{
restoreScaleStates(node.toElement());
}
else if (node.nodeName() == "keymaps")
{
restoreKeymapStates(node.toElement());
}
else if( gui )
{
if( node.nodeName() == gui->getControllerRackView()->nodeName() )
@@ -1240,6 +1251,9 @@ bool Song::saveProjectFile(const QString & filename, bool withResources)
saveControllerStates( dataFile, dataFile.content() );
saveScaleStates(dataFile, dataFile.content());
saveKeymapStates(dataFile, dataFile.content());
m_savingProject = false;
return dataFile.writeFile(filename, withResources);
@@ -1329,6 +1343,56 @@ void Song::removeAllControllers()
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
@@ -1452,3 +1516,41 @@ QString Song::errorSummary()
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::mixer()->requestChangeInModel();
std::atomic_store(&m_scales[index], newScale);
emit scaleListChanged(index);
Engine::mixer()->doneChangeInModel();
}
void Song::setKeymap(unsigned int index, std::shared_ptr<Keymap> newMap)
{
if (index >= MaxKeymapCount) {index = 0;}
Engine::mixer()->requestChangeInModel();
std::atomic_store(&m_keymaps[index], newMap);
emit keymapListChanged(index);
Engine::mixer()->doneChangeInModel();
}

View File

@@ -78,6 +78,7 @@ SET(LMMS_SRCS
gui/widgets/GroupBox.cpp
gui/widgets/InstrumentFunctionViews.cpp
gui/widgets/InstrumentMidiIOView.cpp
gui/widgets/InstrumentMiscView.cpp
gui/widgets/InstrumentSoundShapingView.cpp
gui/widgets/LeftRightNav.cpp
gui/widgets/Knob.cpp
@@ -89,6 +90,7 @@ SET(LMMS_SRCS
gui/widgets/ControlLayout.cpp
gui/widgets/LinkedModelGroupViews.cpp
gui/widgets/MeterDialog.cpp
gui/widgets/MicrotunerConfig.cpp
gui/widgets/MidiPortMenu.cpp
gui/widgets/NStateButton.cpp
gui/widgets/Oscilloscope.cpp

View File

@@ -36,6 +36,7 @@
#include "FxMixerView.h"
#include "InstrumentTrack.h"
#include "MainWindow.h"
#include "MicrotunerConfig.h"
#include "PianoRoll.h"
#include "ProjectNotes.h"
#include "SongEditor.h"
@@ -144,6 +145,10 @@ GuiApplication::GuiApplication()
m_projectNotes = new ProjectNotes;
connect(m_projectNotes, SIGNAL(destroyed(QObject*)), this, SLOT(childDestroyed(QObject*)));
displayInitProgress(tr("Preparing microtuner"));
m_microtunerConfig = new MicrotunerConfig;
connect(m_microtunerConfig, SIGNAL(destroyed(QObject*)), this, SLOT(childDestroyed(QObject*)));
displayInitProgress(tr("Preparing beat/bassline editor"));
m_bbEditor = new BBEditor(Engine::getBBTrackContainer());
connect(m_bbEditor, SIGNAL(destroyed(QObject*)), this, SLOT(childDestroyed(QObject*)));
@@ -210,6 +215,10 @@ void GuiApplication::childDestroyed(QObject *obj)
{
m_projectNotes = nullptr;
}
else if (obj == m_microtunerConfig)
{
m_microtunerConfig = nullptr;
}
else if (obj == m_controllerRackView)
{
m_controllerRackView = nullptr;

View File

@@ -52,6 +52,7 @@
#include "GuiApplication.h"
#include "ImportFilter.h"
#include "InstrumentTrack.h"
#include "MicrotunerConfig.h"
#include "PianoRoll.h"
#include "PluginBrowser.h"
#include "PluginFactory.h"
@@ -546,6 +547,14 @@ void MainWindow::finalize()
m_toolBar );
project_notes_window->setShortcut( Qt::CTRL + Qt::Key_7 );
ToolButton * microtuner_window = new ToolButton(
embed::getIconPixmap( "microtuner" ),
tr( "Microtuner configuration" ) +
" (Ctrl+8)",
this, SLOT( toggleMicrotunerWin() ),
m_toolBar );
microtuner_window->setShortcut( Qt::CTRL + Qt::Key_8 );
m_toolBarLayout->addWidget( song_editor_window, 1, 1 );
m_toolBarLayout->addWidget( bb_editor_window, 1, 2 );
m_toolBarLayout->addWidget( piano_roll_window, 1, 3 );
@@ -553,6 +562,7 @@ void MainWindow::finalize()
m_toolBarLayout->addWidget( fx_mixer_window, 1, 5 );
m_toolBarLayout->addWidget( controllers_window, 1, 6 );
m_toolBarLayout->addWidget( project_notes_window, 1, 7 );
m_toolBarLayout->addWidget( microtuner_window, 1, 8 );
m_toolBarLayout->setColumnStretch( 100, 1 );
// setup-dialog opened before?
@@ -1112,6 +1122,13 @@ void MainWindow::toggleFxMixerWin()
}
void MainWindow::toggleMicrotunerWin()
{
toggleWindow( gui->getMicrotunerConfig() );
}
void MainWindow::updateViewMenu()
{
m_viewMenu->clear();
@@ -1147,6 +1164,10 @@ void MainWindow::updateViewMenu()
tr( "Project Notes" ) + "\tCtrl+7",
this, SLOT( toggleProjectNotesWin() )
);
m_viewMenu->addAction(embed::getIconPixmap( "microtuner" ),
tr( "Microtuner" ) + "\tCtrl+8",
this, SLOT( toggleMicrotunerWin() )
);
m_viewMenu->addSeparator();

View File

@@ -147,6 +147,9 @@ PianoView::PianoView(QWidget *parent) :
layout->setMargin( 0 );
layout->addSpacing( PIANO_BASE+PW_WHITE_KEY_HEIGHT );
layout->addWidget( m_pianoScroll );
// trigger a redraw if keymap definitions change (different keys may become disabled)
connect(Engine::getSong(), SIGNAL(keymapListChanged(int)), this, SLOT(update()));
}
/*! \brief Map a keyboard key being pressed to a note in our keyboard view
@@ -305,6 +308,10 @@ void PianoView::modelChanged()
connect(m_piano->instrumentTrack()->baseNoteModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_piano->instrumentTrack()->firstKeyModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_piano->instrumentTrack()->lastKeyModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_piano->instrumentTrack()->microtuner()->enabledModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_piano->instrumentTrack()->microtuner()->keymapModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_piano->instrumentTrack()->microtuner()->keyRangeImportModel(), SIGNAL(dataChanged()),
this, SLOT(update()));
}
}
@@ -405,8 +412,7 @@ void PianoView::pianoScrolled(int new_pos)
void PianoView::contextMenuEvent(QContextMenuEvent *me)
{
if (me->pos().y() > PIANO_BASE || m_piano == nullptr ||
// m_piano->instrumentTrack()->microtuner()->keyRangeImport())
false)
m_piano->instrumentTrack()->keyRangeImport())
{
QWidget::contextMenuEvent(me);
return;
@@ -470,8 +476,7 @@ void PianoView::mousePressEvent(QMouseEvent *me)
emit keyPressed(key_num);
}
// else if (!m_piano->instrumentTrack()->microtuner()->keyRangeImport())
else if (true)
else if (!m_piano->instrumentTrack()->keyRangeImport())
{
// upper section, select which marker (base / first / last note) will be moved
m_movedNoteModel = getNearestMarker(key_num);
@@ -853,8 +858,7 @@ void PianoView::paintEvent( QPaintEvent * )
p.setPen( Qt::white );
// Controls for first / last / base key models are shown only if microtuner or its key range import are disabled
// if (m_piano != nullptr && !m_piano->instrumentTrack()->microtuner()->keyRangeImport())
if (m_piano != nullptr && true)
if (m_piano != nullptr && !m_piano->instrumentTrack()->keyRangeImport())
{
// Draw the base note marker and first / last note boundary markers
const int base_key = m_piano->instrumentTrack()->baseNoteModel()->value();
@@ -888,9 +892,7 @@ void PianoView::paintEvent( QPaintEvent * )
}
// draw normal, pressed or disabled key, depending on state and position of current key
if (m_piano &&
cur_key >= m_piano->instrumentTrack()->firstKeyModel()->value() &&
cur_key <= m_piano->instrumentTrack()->lastKeyModel()->value())
if (m_piano && m_piano->instrumentTrack()->isKeyMapped(cur_key))
{
if (m_piano && m_piano->isKeyPressed(cur_key))
{
@@ -924,9 +926,7 @@ void PianoView::paintEvent( QPaintEvent * )
int startKey = m_startKey;
if (startKey > 0 && Piano::isBlackKey(static_cast<Keys>(--startKey)))
{
if (m_piano &&
startKey >= m_piano->instrumentTrack()->firstKeyModel()->value() &&
startKey <= m_piano->instrumentTrack()->lastKeyModel()->value())
if (m_piano && m_piano->instrumentTrack()->isKeyMapped(startKey))
{
if (m_piano && m_piano->isKeyPressed(startKey))
{
@@ -949,9 +949,7 @@ void PianoView::paintEvent( QPaintEvent * )
if (Piano::isBlackKey(cur_key))
{
// draw normal, pressed or disabled key, depending on state and position of current key
if (m_piano &&
cur_key >= m_piano->instrumentTrack()->firstKeyModel()->value() &&
cur_key <= m_piano->instrumentTrack()->lastKeyModel()->value())
if (m_piano && m_piano->instrumentTrack()->isKeyMapped(cur_key))
{
if (m_piano && m_piano->isKeyPressed(cur_key))
{

View File

@@ -449,6 +449,9 @@ PianoRoll::PianoRoll() :
this, SLOT(changeSnapMode()));
m_stepRecorder.initialize();
// trigger a redraw if keymap definitions change (different keys may become disabled)
connect(Engine::getSong(), SIGNAL(keymapListChanged(int)), this, SLOT(update()));
}
@@ -905,6 +908,9 @@ void PianoRoll::setCurrentPattern( Pattern* newPattern )
connect(m_pattern->instrumentTrack()->firstKeyModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_pattern->instrumentTrack()->lastKeyModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_pattern->instrumentTrack()->microtuner()->keymapModel(), SIGNAL(dataChanged()), this, SLOT(update()));
connect(m_pattern->instrumentTrack()->microtuner()->keyRangeImportModel(), SIGNAL(dataChanged()),
this, SLOT(update()));
update();
emit currentPatternChanged();
@@ -3162,8 +3168,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe )
const int key,
const int yb)
{
const bool mapped = m_pattern->instrumentTrack()->firstKeyModel()->value() <= key &&
m_pattern->instrumentTrack()->lastKeyModel()->value() >= key;
const bool mapped = m_pattern->instrumentTrack()->isKeyMapped(key);
const bool pressed = m_pattern->instrumentTrack()->pianoModel()->isKeyPressed(key);
const int keyCode = key % KeysPerOctave;
const int yt = yb - gridCorrection(key);

View File

@@ -211,25 +211,3 @@ void InstrumentMidiIOView::modelChanged()
}
}
InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) :
QWidget( parent )
{
QVBoxLayout* layout = new QVBoxLayout( this );
layout->setMargin( 5 );
m_pitchGroupBox = new GroupBox( tr ( "MASTER PITCH" ) );
layout->addWidget( m_pitchGroupBox );
QHBoxLayout* masterPitchLayout = new QHBoxLayout( m_pitchGroupBox );
masterPitchLayout->setContentsMargins( 8, 18, 8, 8 );
QLabel *tlabel = new QLabel(tr( "Enables the use of master pitch" ) );
tlabel->setFont( pointSize<8>( tlabel->font() ) );
m_pitchGroupBox->setModel( &it->m_useMasterPitchModel );
masterPitchLayout->addWidget( tlabel );
layout->addStretch();
}
InstrumentMiscView::~InstrumentMiscView()
{
}

View File

@@ -0,0 +1,86 @@
/*
* InstrumentMiscView.cpp - Miscellaneous instrument settings
*
* Copyright (c) 2005-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
* Copyright (c) 2020 Martin Pavelek <he29.HS/at/gmail.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 "InstrumentMiscView.h"
#include <QLabel>
#include <QLayout>
#include "ComboBox.h"
#include "GroupBox.h"
#include "gui_templates.h"
#include "InstrumentTrack.h"
#include "LedCheckbox.h"
InstrumentMiscView::InstrumentMiscView(InstrumentTrack *it, QWidget *parent) :
QWidget(parent)
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setMargin(5);
// Master pitch toggle
m_pitchGroupBox = new GroupBox(tr("MASTER PITCH"));
m_pitchGroupBox->setModel(&it->m_useMasterPitchModel);
layout->addWidget(m_pitchGroupBox);
QHBoxLayout *masterPitchLayout = new QHBoxLayout(m_pitchGroupBox);
masterPitchLayout->setContentsMargins(8, 18, 8, 8);
QLabel *tlabel = new QLabel(tr("Enables the use of master pitch"));
tlabel->setFont(pointSize<8>(tlabel->font()));
masterPitchLayout->addWidget(tlabel);
// Microtuner settings
m_microtunerGroupBox = new GroupBox(tr("MICROTUNER"));
m_microtunerGroupBox->setModel(it->m_microtuner.enabledModel());
layout->addWidget(m_microtunerGroupBox);
QVBoxLayout *microtunerLayout = new QVBoxLayout(m_microtunerGroupBox);
microtunerLayout->setContentsMargins(8, 18, 8, 8);
QLabel *scaleLabel = new QLabel(tr("Active scale:"));
microtunerLayout->addWidget(scaleLabel);
m_scaleCombo = new ComboBox();
m_scaleCombo->setModel(it->m_microtuner.scaleModel());
microtunerLayout->addWidget(m_scaleCombo);
QLabel *keymapLabel = new QLabel(tr("Active keymap:"));
microtunerLayout->addWidget(keymapLabel);
m_keymapCombo = new ComboBox();
m_keymapCombo->setModel(it->m_microtuner.keymapModel());
microtunerLayout->addWidget(m_keymapCombo);
m_rangeImportCheckbox = new LedCheckBox(tr("Import note ranges from keymap"), this);
m_rangeImportCheckbox->setModel(it->m_microtuner.keyRangeImportModel());
m_rangeImportCheckbox->setToolTip(tr("When enabled, the first, last and base notes of this instrument will be overwritten with values specified by the selected keymap."));
m_rangeImportCheckbox->setCheckable(true);
microtunerLayout->addWidget(m_rangeImportCheckbox);
// Fill remaining space
layout->addStretch();
}

View File

@@ -0,0 +1,647 @@
/*
* MicrotunerConfig.cpp - configuration widget for scales and keymaps
*
* Copyright (c) 2020 Martin Pavelek <he29.HS/at/gmail.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 "MicrotunerConfig.h"
#include <QFile>
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QRegExp>
#include <QTextStream>
#include "ComboBox.h"
#include "embed.h"
#include "Engine.h"
#include "FileDialog.h"
#include "GuiApplication.h"
#include "Knob.h"
#include "LcdSpinBox.h"
#include "lmms_constants.h"
#include "lmmsversion.h"
#include "MainWindow.h"
#include "Song.h"
MicrotunerConfig::MicrotunerConfig() :
QWidget(),
m_scaleComboModel(nullptr, tr("Selected scale")),
m_keymapComboModel(nullptr, tr("Selected keymap")),
m_firstKeyModel(0, 0, NumKeys - 1, nullptr, tr("First key")),
m_lastKeyModel(NumKeys - 1, 0, NumKeys - 1, nullptr, tr("Last key")),
m_middleKeyModel(DefaultMiddleKey, 0, NumKeys - 1, nullptr, tr("Middle key")),
m_baseKeyModel(DefaultBaseKey, 0, NumKeys - 1, nullptr, tr("Base key")),
m_baseFreqModel(DefaultBaseFreq, 0.1f, 9999.999f, 0.001f, nullptr, tr("Base note frequency"))
{
setWindowIcon(embed::getIconPixmap("microtuner"));
setWindowTitle(tr("Microtuner"));
// Organize into 2 main columns: scales and keymaps
QGridLayout *microtunerLayout = new QGridLayout();
microtunerLayout->setSpacing(2);
// ----------------------------------
// Scale sub-column
//
QLabel *scaleLabel = new QLabel(tr("Scale:"));
microtunerLayout->addWidget(scaleLabel, 0, 0, 1, 2, Qt::AlignBottom);
for (unsigned int i = 0; i < MaxScaleCount; i++)
{
m_scaleComboModel.addItem(QString::number(i) + ": " + Engine::getSong()->getScale(i)->getDescription());
}
ComboBox *scaleCombo = new ComboBox();
scaleCombo->setModel(&m_scaleComboModel);
microtunerLayout->addWidget(scaleCombo, 1, 0, 1, 2);
connect(&m_scaleComboModel, &ComboBoxModel::dataChanged, [=] {updateScaleForm();});
m_scaleNameEdit = new QLineEdit("12-TET");
m_scaleNameEdit->setToolTip(tr("Scale description. Cannot start with \"!\" and cannot contain a newline character."));
microtunerLayout->addWidget(m_scaleNameEdit, 2, 0, 1, 2);
QPushButton *loadScaleButton = new QPushButton(tr("Load"));
QPushButton *saveScaleButton = new QPushButton(tr("Save"));
microtunerLayout->addWidget(loadScaleButton, 3, 0, 1, 1);
microtunerLayout->addWidget(saveScaleButton, 3, 1, 1, 1);
connect(loadScaleButton, &QPushButton::clicked, [=] {loadScaleFromFile();});
connect(saveScaleButton, &QPushButton::clicked, [=] {saveScaleToFile();});
m_scaleTextEdit = new QPlainTextEdit();
m_scaleTextEdit->setPlainText("100.0\n200.0\n300.0\n400.0\n500.0\n600.0\n700.0\n800.0\n900.0\n1000.0\n1100.0\n1200.0");
m_scaleTextEdit->setToolTip(tr("Enter intervals on separate lines. Numbers containing a decimal point are treated as cents.\nOther inputs are treated as integer ratios and must be in the form of \'a/b\' or \'a\'.\nUnity (0.0 cents or ratio 1/1) is always present as a hidden first value; do not enter it manually."));
microtunerLayout->addWidget(m_scaleTextEdit, 4, 0, 2, 2);
QPushButton *applyScaleButton = new QPushButton(tr("Apply scale"));
microtunerLayout->addWidget(applyScaleButton, 6, 0, 1, 2);
connect(applyScaleButton, &QPushButton::clicked, [=] {applyScale();});
// ----------------------------------
// Mapping sub-column
//
QLabel *keymapLabel = new QLabel(tr("Keymap:"));
microtunerLayout->addWidget(keymapLabel, 0, 2, 1, 2, Qt::AlignBottom);
for (unsigned int i = 0; i < MaxKeymapCount; i++)
{
m_keymapComboModel.addItem(QString::number(i) + ": " + Engine::getSong()->getKeymap(i)->getDescription());
}
ComboBox *keymapCombo = new ComboBox();
keymapCombo->setModel(&m_keymapComboModel);
microtunerLayout->addWidget(keymapCombo, 1, 2, 1, 2);
connect(&m_keymapComboModel, &ComboBoxModel::dataChanged, [=] {updateKeymapForm();});
m_keymapNameEdit = new QLineEdit("default");
m_keymapNameEdit->setToolTip(tr("Keymap description. Cannot start with \"!\" and cannot contain a newline character."));
microtunerLayout->addWidget(m_keymapNameEdit, 2, 2, 1, 2);
QPushButton *loadKeymapButton = new QPushButton(tr("Load"));
QPushButton *saveKeymapButton = new QPushButton(tr("Save"));
microtunerLayout->addWidget(loadKeymapButton, 3, 2, 1, 1);
microtunerLayout->addWidget(saveKeymapButton, 3, 3, 1, 1);
connect(loadKeymapButton, &QPushButton::clicked, [=] {loadKeymapFromFile();});
connect(saveKeymapButton, &QPushButton::clicked, [=] {saveKeymapToFile();});
m_keymapTextEdit = new QPlainTextEdit();
m_keymapTextEdit->setPlainText("0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11");
m_keymapTextEdit->setToolTip(tr("Enter key mappings on separate lines. Each line assigns a scale degree to a MIDI key,\nstarting with the middle key and continuing in sequence.\nThe pattern repeats for keys outside of the explicit keymap range.\nMultiple keys can be mapped to the same scale degree.\nEnter \'x\' if you wish to leave the key disabled / not mapped."));
microtunerLayout->addWidget(m_keymapTextEdit, 4, 2, 1, 2);
// Mapping ranges
QGridLayout *keymapRangeLayout = new QGridLayout();
microtunerLayout->addLayout(keymapRangeLayout, 5, 2, 1, 2, Qt::AlignCenter | Qt::AlignTop);
LcdSpinBox *firstKeySpin = new LcdSpinBox(3, nullptr, tr("First key"));
firstKeySpin->setLabel(tr("FIRST"));
firstKeySpin->setToolTip(tr("First MIDI key that will be mapped"));
firstKeySpin->setModel(&m_firstKeyModel);
keymapRangeLayout->addWidget(firstKeySpin, 0, 0);
LcdSpinBox *lastKeySpin = new LcdSpinBox(3, nullptr, tr("Last key"));
lastKeySpin->setLabel(tr("LAST"));
lastKeySpin->setToolTip(tr("Last MIDI key that will be mapped"));
lastKeySpin->setModel(&m_lastKeyModel);
keymapRangeLayout->addWidget(lastKeySpin, 0, 1);
LcdSpinBox *middleKeySpin = new LcdSpinBox(3, nullptr, tr("Middle key"));
middleKeySpin->setLabel(tr("MIDDLE"));
middleKeySpin->setToolTip(tr("First line in the keymap refers to this MIDI key"));
middleKeySpin->setModel(&m_middleKeyModel);
keymapRangeLayout->addWidget(middleKeySpin, 0, 2);
LcdSpinBox *baseKeySpin = new LcdSpinBox(3, nullptr, tr("Base key"));
baseKeySpin->setLabel(tr("BASE N."));
baseKeySpin->setToolTip(tr("Base note frequency will be assigned to this MIDI key"));
baseKeySpin->setModel(&m_baseKeyModel);
keymapRangeLayout->addWidget(baseKeySpin, 1, 0);
LcdFloatSpinBox *baseFreqSpin = new LcdFloatSpinBox(4, 3, tr("Base note frequency"));
baseFreqSpin->setLabel(tr("BASE NOTE FREQ"));
baseFreqSpin->setModel(&m_baseFreqModel);
baseFreqSpin->setToolTip(tr("Base note frequency"));
keymapRangeLayout->addWidget(baseFreqSpin, 1, 1, 1, 2);
QPushButton *applyKeymapButton = new QPushButton(tr("Apply keymap"));
microtunerLayout->addWidget(applyKeymapButton, 6, 2, 1, 2);
connect(applyKeymapButton, &QPushButton::clicked, [=] {applyKeymap();});
updateScaleForm();
updateKeymapForm();
connect(Engine::getSong(), SIGNAL(scaleListChanged(int)), this, SLOT(updateScaleList(int)));
connect(Engine::getSong(), SIGNAL(scaleListChanged(int)), this, SLOT(updateScaleForm()));
connect(Engine::getSong(), SIGNAL(keymapListChanged(int)), this, SLOT(updateKeymapList(int)));
connect(Engine::getSong(), SIGNAL(keymapListChanged(int)), this, SLOT(updateKeymapForm()));
microtunerLayout->setRowStretch(4, 10);
this->setLayout(microtunerLayout);
// Add to the main window and setup fixed size etc.
QMdiSubWindow *subWin = gui->mainWindow()->addWindowedWidget(this);
subWin->setAttribute(Qt::WA_DeleteOnClose, false);
subWin->setMinimumWidth(300);
subWin->setMinimumHeight(300);
subWin->setMaximumWidth(500);
subWin->setMaximumHeight(700);
subWin->hide();
// No maximize button
Qt::WindowFlags flags = subWin->windowFlags();
flags &= ~Qt::WindowMaximizeButtonHint;
subWin->setWindowFlags(flags);
}
/**
* \brief Update list of available scales.
* \param index Index of the scale to update; update all scales if -1 or out of range.
*/
void MicrotunerConfig::updateScaleList(int index)
{
if (index >= 0 && index < MaxScaleCount)
{
m_scaleComboModel.replaceItem(index,
QString::number(index) + ": " + Engine::getSong()->getScale(index)->getDescription());
}
else
{
for (int i = 0; i < MaxScaleCount; i++)
{
m_scaleComboModel.replaceItem(i,
QString::number(i) + ": " + Engine::getSong()->getScale(i)->getDescription());
}
}
}
/**
* \brief Update list of available keymaps.
* \param index Index of the keymap to update; update all keymaps if -1 or out of range.
*/
void MicrotunerConfig::updateKeymapList(int index)
{
if (index >= 0 && index < MaxKeymapCount)
{
m_keymapComboModel.replaceItem(index,
QString::number(index) + ": " + Engine::getSong()->getKeymap(index)->getDescription());
}
else
{
for (int i = 0; i < MaxKeymapCount; i++)
{
m_keymapComboModel.replaceItem(i,
QString::number(i) + ": " + Engine::getSong()->getKeymap(i)->getDescription());
}
}
}
/**
* \brief Fill all the scale-related values based on currently selected scale
*/
void MicrotunerConfig::updateScaleForm()
{
Song *song = Engine::getSong();
if (song == nullptr) {return;}
auto newScale = song->getScale(m_scaleComboModel.value());
m_scaleNameEdit->setText(newScale->getDescription());
// fill in the intervals
m_scaleTextEdit->setPlainText("");
const std::vector<Interval> &intervals = newScale->getIntervals();
for (std::size_t i = 1; i < intervals.size(); i++)
{
m_scaleTextEdit->appendPlainText(intervals[i].getString());
}
// scroll back to the top
QTextCursor tmp = m_scaleTextEdit->textCursor();
tmp.movePosition(QTextCursor::Start);
m_scaleTextEdit->setTextCursor(tmp);
}
/**
* \brief Fill all the keymap-related values based on currently selected keymap
*/
void MicrotunerConfig::updateKeymapForm()
{
Song *song = Engine::getSong();
if (song == nullptr) {return;}
auto newMap = song->getKeymap(m_keymapComboModel.value());
m_keymapNameEdit->setText(newMap->getDescription());
m_keymapTextEdit->setPlainText("");
const std::vector<int> &map = newMap->getMap();
for (std::size_t i = 0; i < map.size(); i++)
{
if (map[i] >= 0) {m_keymapTextEdit->appendPlainText(QString::number(map[i]));}
else {m_keymapTextEdit->appendPlainText("x");}
}
QTextCursor tmp = m_keymapTextEdit->textCursor();
tmp.movePosition(QTextCursor::Start);
m_keymapTextEdit->setTextCursor(tmp);
m_firstKeyModel.setValue(newMap->getFirstKey());
m_lastKeyModel.setValue(newMap->getLastKey());
m_middleKeyModel.setValue(newMap->getMiddleKey());
m_baseKeyModel.setValue(newMap->getBaseKey());
m_baseFreqModel.setValue(newMap->getBaseFreq());
}
/**
* \brief Validate the scale name and entered interval definitions
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::validateScaleForm()
{
auto fail = [=](QString message) {QMessageBox::critical(this, tr("Scale parsing error"), message);};
// check name
QString name = m_scaleNameEdit->text();
if (name.length() > 0 && name[0] == '!') {fail(tr("Scale name cannot start with an exclamation mark")); return false;}
if (name.contains('\n')) {fail(tr("Scale name cannot contain a new-line character")); return false;}
// check intervals
QStringList input = m_scaleTextEdit->toPlainText().split('\n', QString::SkipEmptyParts);
for (auto &line: input)
{
if (line.isEmpty()) {continue;}
if (line[0] == '!') {continue;} // comment
QString firstSection = line.section(QRegExp("\\s+|/"), 0, 0, QString::SectionSkipEmpty);
if (firstSection.contains('.')) // cent mode
{
bool ok = true;
firstSection.toFloat(&ok);
if (!ok) {fail(tr("Interval defined in cents cannot be converted to a number")); return false;}
}
else // ratio mode
{
bool ok = true;
int num = 1, den = 1;
num = firstSection.toInt(&ok);
if (!ok) {fail(tr("Numerator of an interval defined as a ratio cannot be converted to a number")); return false;}
if (line.contains('/'))
{
den = line.split('/').at(1).section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty).toInt(&ok);
}
if (!ok) {fail(tr("Denominator of an interval defined as a ratio cannot be converted to a number")); return false;}
if (num * den < 0) {fail(tr("Interval defined as a ratio cannot be negative")); return false;}
}
}
return true;
}
/**
* \brief Validate the entered key mapping and other values
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::validateKeymapForm()
{
auto fail = [=](QString message) {QMessageBox::critical(this, tr("Keymap parsing error"), message);};
// check name
QString name = m_keymapNameEdit->text();
if (name.length() > 0 && name[0] == '!') {fail(tr("Keymap name cannot start with an exclamation mark")); return false;}
if (name.contains('\n')) {fail(tr("Keymap name cannot contain a new-line character")); return false;}
// check key mappings
QStringList input = m_keymapTextEdit->toPlainText().split('\n', QString::SkipEmptyParts);
for (auto &line: input)
{
if (line.isEmpty()) {continue;}
if (line[0] == '!') {continue;} // comment
QString firstSection = line.section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty);
if (firstSection == "x") {continue;} // not mapped
// otherwise must contain a number
bool ok = true;
int deg = 0;
deg = firstSection.toInt(&ok);
if (!ok) {fail(tr("Scale degree cannot be converted to a whole number")); return false;}
if (deg < 0) {fail(tr("Scale degree cannot be negative")); return false;}
}
return true;
}
/**
* \brief Parse and apply the entered scale definition
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::applyScale()
{
if (!validateScaleForm()) {return false;};
std::vector<Interval> newIntervals;
newIntervals.push_back(Interval(1, 1));
QStringList input = m_scaleTextEdit->toPlainText().split('\n', QString::SkipEmptyParts);
for (auto &line: input)
{
if (line.isEmpty()) {continue;}
if (line[0] == '!') {continue;} // comment
QString firstSection = line.section(QRegExp("\\s+|/"), 0, 0, QString::SectionSkipEmpty);
if (firstSection.contains('.')) // cent mode
{
newIntervals.push_back(Interval(firstSection.toFloat()));
}
else // ratio mode
{
int num = 1, den = 1;
num = firstSection.toInt();
if (line.contains('/'))
{
den = line.split('/').at(1).section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty).toInt();
}
newIntervals.push_back(Interval(num, den));
}
}
Song *song = Engine::getSong();
if (song == nullptr) {return false;}
auto newScale = std::make_shared<Scale>(m_scaleNameEdit->text(), std::move(newIntervals));
song->setScale(m_scaleComboModel.value(), newScale);
return true;
}
/**
* \brief Parse and apply the entered keymap definition
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::applyKeymap()
{
if (!validateKeymapForm()) {return false;}
std::vector<int> newMap;
QStringList input = m_keymapTextEdit->toPlainText().split('\n', QString::SkipEmptyParts);
for (auto &line: input)
{
if (line.isEmpty()) {continue;}
if (line[0] == '!') {continue;} // comment
QString firstSection = line.section(QRegExp("\\s+"), 0, 0, QString::SectionSkipEmpty);
if (firstSection == "x")
{
newMap.push_back(-1); // not mapped
continue;
}
newMap.push_back(firstSection.toInt());
}
Song *song = Engine::getSong();
if (song == nullptr) {return false;}
auto newKeymap = std::make_shared<Keymap>(
m_keymapNameEdit->text(),
std::move(newMap),
m_firstKeyModel.value(),
m_lastKeyModel.value(),
m_middleKeyModel.value(),
m_baseKeyModel.value(),
m_baseFreqModel.value()
);
song->setKeymap(m_keymapComboModel.value(), newKeymap);
if (newKeymap->getDegree(newKeymap->getBaseKey()) == -1) {
QMessageBox::warning(this, tr("Invalid keymap"), tr("Base key is not mapped to any scale degree. No sound will be produced as there is no way to assign reference frequency to any note."));}
return true;
}
/**
* \brief Parse an .scl file and apply the loaded scale if it is valid
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::loadScaleFromFile()
{
QString fileName = FileDialog::getOpenFileName(this, tr("Open scale"), "", tr("Scala scale definition (*.scl)"));
if (fileName == "") {return false;}
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::critical(this, tr("Scale load failure"), tr("Unable to open selected file."));
return false;
}
QTextStream stream(&file);
int i = -2, limit = 0;
m_scaleNameEdit->setText("");
m_scaleTextEdit->clear();
while (!stream.atEnd() && i < limit)
{
QString line = stream.readLine();
if (line != "" && line[0] == '!') {continue;} // comment
switch(i) {
case -2: m_scaleNameEdit->setText(line); break; // first non-comment line = name or description
case -1: limit = line.toInt(); break; // second non-comment line = degree count
default: m_scaleTextEdit->appendPlainText(line); break; // all other lines = interval definitions
}
i++;
}
return applyScale();
}
/**
* \brief Parse a .kbm file and apply the loaded keymap if it is valid
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::loadKeymapFromFile()
{
QString fileName = FileDialog::getOpenFileName(this, tr("Open keymap"), "", tr("Scala keymap definition (*.kbm)"));
if (fileName == "") {return false;}
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::critical(this, tr("Keymap load failure"), tr("Unable to open selected file."));
return false;
}
QTextStream stream(&file);
int i = -7, limit = 0;
m_keymapNameEdit->setText(QFileInfo(fileName).baseName()); // .kbm does not store description, use file name
m_keymapTextEdit->clear();
while (!stream.atEnd() && i < limit)
{
QString line = stream.readLine();
if (line != "" && line[0] == '!')
{
if (line.length() > 1 && line[1] == '!' && i == -7) // LMMS extension: double "!" occuring before any
{ // value is loaded marks a description field.
m_keymapNameEdit->setText(line.mid(2));
}
continue;
}
switch(i) {
case -7: limit = line.toInt(); break; // first non-comment line = keymap size
case -6: m_firstKeyModel.setValue(line.toInt()); break; // second non-comment line = first key
case -5: m_lastKeyModel.setValue(line.toInt()); break; // third non-comment line = last key
case -4: m_middleKeyModel.setValue(line.toInt()); break; // fourth non-comment line = middle key
case -3: m_baseKeyModel.setValue(line.toInt()); break; // fifth non-comment line = base key
case -2: m_baseFreqModel.setValue(line.toDouble()); break; // sixth non-comment line = base freq
case -1: break; // ignored // seventh non-comment line = octave degree
default: m_keymapTextEdit->appendPlainText(line); break; // all other lines = mapping definitions
}
i++;
}
return applyKeymap();
}
/**
* \brief Save currently entered scale definition as .scl file
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::saveScaleToFile()
{
if (!applyScale()) {return false;}
QString fileName = FileDialog::getSaveFileName(this, tr("Save scale"), "", tr("Scala scale definition (*.scl)"));
if (fileName == "") {return false;}
if (QFileInfo(fileName).suffix() != "scl") {fileName = fileName + ".scl";}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly))
{
QMessageBox::critical(this, tr("Scale save failure"), tr("Unable to open selected file for writing."));
return false;
}
Song *song = Engine::getSong();
if (song == nullptr) {return false;}
QTextStream stream(&file);
stream << "! " << QFileInfo(fileName).fileName() << "\n";
stream << "! Exported from LMMS " LMMS_VERSION "\n";
stream << "!\n";
stream << "! Scale description:\n";
stream << m_scaleNameEdit->text() << "\n";
stream << "!\n";
stream << "! Number of degrees:\n";
stream << song->getScale(m_scaleComboModel.value())->getIntervals().size() - 1 << "\n";
stream << "!\n";
stream << "! Intervals:\n";
stream << m_scaleTextEdit->toPlainText() << "\n";
return true;
}
/**
* \brief Save currently entered keymap definition as .kbm file
* \return true if input is valid, false if problems were detected
*/
bool MicrotunerConfig::saveKeymapToFile()
{
if (!applyKeymap()) {return false;}
QString fileName = FileDialog::getSaveFileName(this, tr("Save keymap"), "", tr("Scala keymap definition (*.kbm)"));
if (fileName == "") {return false;}
if (QFileInfo(fileName).suffix() != "kbm") {fileName = fileName + ".kbm";}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly))
{
QMessageBox::critical(this, tr("Keymap save failure"), tr("Unable to open selected file for writing."));
return false;
}
Song *song = Engine::getSong();
if (song == nullptr) {return false;}
QTextStream stream(&file);
stream << "! " << QFileInfo(fileName).fileName() << "\n";
stream << "! Exported from LMMS " LMMS_VERSION "\n";
stream << "!\n";
stream << "! Keymap description:\n";
stream << "!!" << m_keymapNameEdit->text() << "\n";
stream << "!\n";
stream << "! Keymap size:\n";
stream << song->getKeymap(m_keymapComboModel.value())->getMap().size() << "\n";
stream << "!\n";
stream << "! First key:\n";
stream << m_firstKeyModel.value() << "\n";
stream << "! Last key:\n";
stream << m_lastKeyModel.value() << "\n";
stream << "! Middle key:\n";
stream << m_middleKeyModel.value() << "\n";
stream << "! Base key:\n";
stream << m_baseKeyModel.value() << "\n";
stream << "! Base frequency:\n";
stream << m_baseFreqModel.value() << "\n";
stream << "! Octave degree (always using the last scale degree):\n";
stream << "0\n";
stream << "!\n";
stream << "! Key mappings:\n";
stream << m_keymapTextEdit->toPlainText() << "\n";
return true;
}
void MicrotunerConfig::saveSettings(QDomDocument &document, QDomElement &element)
{
MainWindow::saveWidgetState(this, element);
}
void MicrotunerConfig::loadSettings(const QDomElement &element)
{
MainWindow::restoreWidgetState(this, element);
}
void MicrotunerConfig::closeEvent(QCloseEvent *ce)
{
if (parentWidget()) {parentWidget()->hide();}
else {hide();}
ce->ignore();
}

View File

@@ -58,6 +58,7 @@
#include "Instrument.h"
#include "InstrumentFunctionViews.h"
#include "InstrumentMidiIOView.h"
#include "InstrumentMiscView.h"
#include "Knob.h"
#include "LcdSpinBox.h"
#include "LedCheckbox.h"
@@ -109,8 +110,8 @@ InstrumentTrack::InstrumentTrack( TrackContainer* tc ) :
m_soundShaping( this ),
m_arpeggio( this ),
m_noteStacking( this ),
m_piano(this)
// m_microtuner(this)
m_piano(this),
m_microtuner()
{
m_pitchModel.setCenterValue( 0 );
m_panningModel.setCenterValue( DefaultPanning );
@@ -151,22 +152,92 @@ InstrumentTrack::InstrumentTrack( TrackContainer* tc ) :
}
bool InstrumentTrack::keyRangeImport() const
{
return m_microtuner.enabled() && m_microtuner.keyRangeImport();
}
/** \brief Check if there is a valid mapping for the given key and it is within defined of range.
*/
bool InstrumentTrack::isKeyMapped(int key) const
{
if (key < firstKey() || key > lastKey()) {return false;}
if (!m_microtuner.enabled()) {return true;}
Song *song = Engine::getSong();
if (!song) {return false;}
return song->getKeymap(m_microtuner.currentKeymap())->getDegree(key) != -1;
}
/** \brief Return first mapped key, based on currently selected keymap or user selection.
* \return Number ranging from 0 to NumKeys -1
*/
int InstrumentTrack::firstKey() const
{
if (keyRangeImport())
{
return Engine::getSong()->getKeymap(m_microtuner.currentKeymap())->getFirstKey();
}
else
{
return m_firstKeyModel.value();
}
}
/** \brief Return last mapped key, based on currently selected keymap or user selection.
* \return Number ranging from 0 to NumKeys -1
*/
int InstrumentTrack::lastKey() const
{
if (keyRangeImport())
{
return Engine::getSong()->getKeymap(m_microtuner.currentKeymap())->getLastKey();
}
else
{
return m_lastKeyModel.value();
}
}
/** \brief Return base key number, based on currently selected keymap or user selection.
* \return Number ranging from 0 to NumKeys -1
*/
int InstrumentTrack::baseNote() const
{
int mp = m_useMasterPitchModel.value() ? Engine::getSong()->masterPitch() : 0;
return m_baseNoteModel.value() - mp;
if (keyRangeImport())
{
return Engine::getSong()->getKeymap(m_microtuner.currentKeymap())->getBaseKey() - mp;
}
else
{
return m_baseNoteModel.value() - mp;
}
}
int InstrumentTrack::firstKey() const
/** \brief Return frequency assigned to the base key, based on currently selected keymap.
* \return Frequency in Hz
*/
float InstrumentTrack::baseFreq() const
{
return m_firstKeyModel.value();
if (m_microtuner.enabled())
{
return Engine::getSong()->getKeymap(m_microtuner.currentKeymap())->getBaseFreq();
}
else
{
return DefaultBaseFreq;
}
}
int InstrumentTrack::lastKey() const
{
return m_lastKeyModel.value();
}
InstrumentTrack::~InstrumentTrack()
@@ -790,6 +861,7 @@ void InstrumentTrack::saveTrackSpecificSettings( QDomDocument& doc, QDomElement
m_firstKeyModel.saveSettings(doc, thisElement, "firstkey");
m_lastKeyModel.saveSettings(doc, thisElement, "lastkey");
m_useMasterPitchModel.saveSettings( doc, thisElement, "usemasterpitch");
m_microtuner.saveSettings(doc, thisElement);
// Save MIDI CC stuff
m_midiCCEnable->saveSettings(doc, thisElement, "enablecc");
@@ -856,6 +928,7 @@ void InstrumentTrack::loadTrackSpecificSettings( const QDomElement & thisElement
m_firstKeyModel.loadSettings(thisElement, "firstkey");
m_lastKeyModel.loadSettings(thisElement, "lastkey");
m_useMasterPitchModel.loadSettings( thisElement, "usemasterpitch");
m_microtuner.loadSettings(thisElement);
// clear effect-chain just in case we load an old preset without FX-data
m_audioPort.effects()->clear();
@@ -1669,12 +1742,26 @@ void InstrumentTrackWindow::modelChanged()
m_pitchRangeLabel->hide();
}
if (m_track->instrument() && m_track->instrument()->flags().testFlag(Instrument::IsMidiBased))
{
m_miscView->microtunerGroupBox()->hide();
m_track->m_microtuner.enabledModel()->setValue(false);
}
else
{
m_miscView->microtunerGroupBox()->show();
}
m_ssView->setModel( &m_track->m_soundShaping );
m_noteStackingView->setModel( &m_track->m_noteStacking );
m_arpeggioView->setModel( &m_track->m_arpeggio );
m_midiView->setModel( &m_track->m_midiPort );
m_effectView->setModel( m_track->m_audioPort.effects() );
m_miscView->pitchGroupBox()->setModel(&m_track->m_useMasterPitchModel);
m_miscView->microtunerGroupBox()->setModel(m_track->m_microtuner.enabledModel());
m_miscView->scaleCombo()->setModel(m_track->m_microtuner.scaleModel());
m_miscView->keymapCombo()->setModel(m_track->m_microtuner.keymapModel());
m_miscView->rangeImportCheckbox()->setModel(m_track->m_microtuner.keyRangeImportModel());
updateName();
}