diff --git a/data/projects/templates/default.mpt b/data/projects/templates/default.mpt
index 677726c64e5..daa2084a4d8 100644
--- a/data/projects/templates/default.mpt
+++ b/data/projects/templates/default.mpt
@@ -81,5 +81,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/themes/classic/lcd_19green_dot.png b/data/themes/classic/lcd_19green_dot.png
new file mode 100644
index 00000000000..1459b7d9af3
Binary files /dev/null and b/data/themes/classic/lcd_19green_dot.png differ
diff --git a/data/themes/classic/lcd_19pink_dot.png b/data/themes/classic/lcd_19pink_dot.png
new file mode 100644
index 00000000000..aa167803ae2
Binary files /dev/null and b/data/themes/classic/lcd_19pink_dot.png differ
diff --git a/data/themes/classic/lcd_19red_dot.png b/data/themes/classic/lcd_19red_dot.png
new file mode 100644
index 00000000000..b9137754a90
Binary files /dev/null and b/data/themes/classic/lcd_19red_dot.png differ
diff --git a/data/themes/classic/microtuner.png b/data/themes/classic/microtuner.png
new file mode 100644
index 00000000000..7880394cbc9
Binary files /dev/null and b/data/themes/classic/microtuner.png differ
diff --git a/data/themes/default/lcd_19green_dot.png b/data/themes/default/lcd_19green_dot.png
new file mode 100644
index 00000000000..2f4a8cf9900
Binary files /dev/null and b/data/themes/default/lcd_19green_dot.png differ
diff --git a/data/themes/default/lcd_19pink_dot.png b/data/themes/default/lcd_19pink_dot.png
new file mode 100644
index 00000000000..5b625360a31
Binary files /dev/null and b/data/themes/default/lcd_19pink_dot.png differ
diff --git a/data/themes/default/lcd_19red_dot.png b/data/themes/default/lcd_19red_dot.png
new file mode 100644
index 00000000000..430768f3ee6
Binary files /dev/null and b/data/themes/default/lcd_19red_dot.png differ
diff --git a/data/themes/default/microtuner.png b/data/themes/default/microtuner.png
new file mode 100644
index 00000000000..c197ba1abc5
Binary files /dev/null and b/data/themes/default/microtuner.png differ
diff --git a/include/ComboBoxModel.h b/include/ComboBoxModel.h
index 82c01e69e94..24fa1055d1d 100644
--- a/include/ComboBoxModel.h
+++ b/include/ComboBoxModel.h
@@ -25,6 +25,7 @@
#ifndef COMBOBOX_MODEL_H
#define COMBOBOX_MODEL_H
+#include
#include
#include
#include
@@ -52,6 +53,8 @@ class LMMS_EXPORT ComboBoxModel : public IntModel
void addItem( QString item, std::unique_ptr loader = nullptr );
+ void replaceItem(std::size_t index, QString item, std::unique_ptr loader = nullptr);
+
void clear();
int findText( const QString& txt ) const;
diff --git a/include/GuiApplication.h b/include/GuiApplication.h
index 825c258372e..9adeb886bd4 100644
--- a/include/GuiApplication.h
+++ b/include/GuiApplication.h
@@ -37,6 +37,7 @@ class BBEditor;
class ControllerRackView;
class FxMixerView;
class MainWindow;
+class MicrotunerConfig;
class PianoRollWindow;
class ProjectNotes;
class SongEditorWindow;
@@ -59,6 +60,7 @@ class LMMS_EXPORT GuiApplication : public QObject
BBEditor* getBBEditor() { return m_bbEditor; }
PianoRollWindow* pianoRoll() { return m_pianoRoll; }
ProjectNotes* getProjectNotes() { return m_projectNotes; }
+ MicrotunerConfig* getMicrotunerConfig() { return m_microtunerConfig; }
AutomationEditorWindow* automationEditor() { return m_automationEditor; }
ControllerRackView* getControllerRackView() { return m_controllerRackView; }
@@ -78,6 +80,7 @@ private slots:
BBEditor* m_bbEditor;
PianoRollWindow* m_pianoRoll;
ProjectNotes* m_projectNotes;
+ MicrotunerConfig* m_microtunerConfig;
ControllerRackView* m_controllerRackView;
QLabel* m_loadingProgressLabel;
};
diff --git a/include/InstrumentMidiIOView.h b/include/InstrumentMidiIOView.h
index e63b4842575..9b1e5adfdb2 100644
--- a/include/InstrumentMidiIOView.h
+++ b/include/InstrumentMidiIOView.h
@@ -65,22 +65,4 @@ class InstrumentMidiIOView : public QWidget, public ModelView
} ;
-class InstrumentMiscView : public QWidget
-{
- Q_OBJECT
-public:
- InstrumentMiscView( InstrumentTrack *it, QWidget* parent );
- ~InstrumentMiscView();
-
- GroupBox * pitchGroupBox()
- {
- return m_pitchGroupBox;
- }
-
-private:
-
- GroupBox * m_pitchGroupBox;
-
-};
-
#endif
diff --git a/include/InstrumentMiscView.h b/include/InstrumentMiscView.h
new file mode 100644
index 00000000000..6024436e8b5
--- /dev/null
+++ b/include/InstrumentMiscView.h
@@ -0,0 +1,63 @@
+/*
+ * InstrumentMiscView.h - widget in instrument-track-window for setting up
+ * miscellaneous options not covered by other tabs
+ *
+ * Copyright (c) 2005-2014 Tobias Doerffel
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * This file is part of LMMS - https://lmms.io
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program (see COPYING); if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef INSTRUMENT_MISC_VIEW_H
+#define INSTRUMENT_MISC_VIEW_H
+
+#include
+
+
+class ComboBox;
+class GroupBox;
+class InstrumentTrack;
+class LedCheckBox;
+
+
+class InstrumentMiscView : public QWidget
+{
+ Q_OBJECT
+public:
+ InstrumentMiscView(InstrumentTrack *it, QWidget *parent);
+
+ GroupBox *pitchGroupBox() {return m_pitchGroupBox;}
+ GroupBox *microtunerGroupBox() {return m_microtunerGroupBox;}
+
+ ComboBox *scaleCombo() {return m_scaleCombo;}
+ ComboBox *keymapCombo() {return m_keymapCombo;}
+
+ LedCheckBox *rangeImportCheckbox() {return m_rangeImportCheckbox;}
+
+private:
+ GroupBox *m_pitchGroupBox;
+ GroupBox *m_microtunerGroupBox;
+
+ ComboBox *m_scaleCombo;
+ ComboBox *m_keymapCombo;
+
+ LedCheckBox *m_rangeImportCheckbox;
+};
+
+#endif
diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h
index e437fcb15d1..f94c057e1f8 100644
--- a/include/InstrumentTrack.h
+++ b/include/InstrumentTrack.h
@@ -30,6 +30,7 @@
#include "GroupBox.h"
#include "InstrumentFunctions.h"
#include "InstrumentSoundShaping.h"
+#include "Microtuner.h"
#include "Midi.h"
#include "MidiCCRackView.h"
#include "MidiEventProcessor.h"
@@ -184,15 +185,23 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor
return &m_lastKeyModel;
}
- int baseNote() const;
+ bool keyRangeImport() const;
+ bool isKeyMapped(int key) const;
int firstKey() const;
int lastKey() const;
+ int baseNote() const;
+ float baseFreq() const;
Piano *pianoModel()
{
return &m_piano;
}
+ Microtuner *microtuner()
+ {
+ return &m_microtuner;
+ }
+
bool isArpeggioEnabled() const
{
return m_arpeggio.m_arpEnabledModel.value();
@@ -305,6 +314,8 @@ protected slots:
Piano m_piano;
+ Microtuner m_microtuner;
+
std::unique_ptr m_midiCCEnable;
std::unique_ptr m_midiCCModel[MidiControllerCount];
diff --git a/include/Keymap.h b/include/Keymap.h
new file mode 100644
index 00000000000..69286034374
--- /dev/null
+++ b/include/Keymap.h
@@ -0,0 +1,79 @@
+/*
+ * Keymap.h - holds information about a key mapping
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * This file is part of LMMS - https://lmms.io
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program (see COPYING); if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef KEYMAP_H
+#define KEYMAP_H
+
+#include
+#include
+#include
+
+#include "Note.h"
+#include "SerializingObject.h"
+
+class Keymap : public QObject, public SerializingObject
+{
+ Q_OBJECT
+public:
+ Keymap();
+ Keymap(
+ QString description,
+ std::vector newMap,
+ int newFirst,
+ int newLast,
+ int newMiddle,
+ int newBaseKey,
+ float newBaseFreq
+ );
+
+ QString getDescription() const;
+ void setDescription(QString description);
+
+ int getMiddleKey() const {return m_middleKey;}
+ int getFirstKey() const {return m_firstKey;}
+ int getLastKey() const {return m_lastKey;}
+ int getBaseKey() const {return m_baseKey;}
+ float getBaseFreq() const {return m_baseFreq;}
+
+ std::size_t getSize() const {return m_map.size();}
+ int getDegree(int key) const;
+ int getOctave(int key) const;
+ const std::vector &getMap() const {return m_map;}
+
+ void saveSettings(QDomDocument &doc, QDomElement &element) override;
+ void loadSettings(const QDomElement &element) override;
+ inline QString nodeName() const override {return "keymap";}
+
+private:
+ QString m_description; //!< name or description of the keymap
+
+ std::vector m_map; //!< key to scale degree mapping
+ int m_firstKey; //!< first key that will be mapped
+ int m_lastKey; //!< last key that will be mapped
+ int m_middleKey; //!< first line of the map refers to this key
+ int m_baseKey; //!< key which is assigned the reference "base note"
+ float m_baseFreq; //!< frequency of the base note (usually A4 @440 Hz)
+};
+
+#endif
diff --git a/include/MainWindow.h b/include/MainWindow.h
index a179e651e49..688e4fc6053 100644
--- a/include/MainWindow.h
+++ b/include/MainWindow.h
@@ -152,6 +152,7 @@ public slots:
void toggleBBEditorWin( bool forceShow = false );
void toggleSongEditorWin();
void toggleProjectNotesWin();
+ void toggleMicrotunerWin();
void toggleFxMixerWin();
void togglePianoRollWin();
void toggleControllerRack();
diff --git a/include/Microtuner.h b/include/Microtuner.h
new file mode 100644
index 00000000000..93998369c03
--- /dev/null
+++ b/include/Microtuner.h
@@ -0,0 +1,73 @@
+/*
+ * Microtuner.h - manage tuning and scale information of an instrument
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * This file is part of LMMS - https://lmms.io
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program (see COPYING); if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef MICROTUNER_H
+#define MICROTUNER_H
+
+#include "AutomatableModel.h"
+#include "ComboBoxModel.h"
+#include "JournallingObject.h"
+#include "lmms_constants.h"
+#include "Note.h"
+
+class LMMS_EXPORT Microtuner : public Model, public JournallingObject
+{
+ Q_OBJECT
+public:
+ explicit Microtuner();
+
+ bool enabled() const {return m_enabledModel.value();}
+ bool keyRangeImport() const {return enabled() && m_keyRangeImportModel.value();}
+ int currentScale() const {return m_scaleModel.value();}
+ int currentKeymap() const {return m_keymapModel.value();}
+
+ BoolModel *enabledModel() {return &m_enabledModel;}
+ ComboBoxModel *scaleModel() {return &m_scaleModel;}
+ ComboBoxModel *keymapModel() {return &m_keymapModel;}
+ BoolModel *keyRangeImportModel() {return &m_keyRangeImportModel;}
+
+ int firstKey() const;
+ int lastKey() const;
+ int baseKey() const;
+ float baseFreq() const;
+
+ float keyToFreq(int key, int userBaseNote) const;
+
+ QString nodeName() const override {return "microtuner";}
+ void saveSettings(QDomDocument & document, QDomElement &element) override;
+ void loadSettings(const QDomElement &element) override;
+
+protected slots:
+ void updateScaleList(int index);
+ void updateKeymapList(int index);
+
+private:
+ BoolModel m_enabledModel; //!< Enable microtuner (otherwise using 12-TET @440 Hz)
+ ComboBoxModel m_scaleModel;
+ ComboBoxModel m_keymapModel;
+ BoolModel m_keyRangeImportModel;
+
+};
+
+#endif
diff --git a/include/MicrotunerConfig.h b/include/MicrotunerConfig.h
new file mode 100644
index 00000000000..00783af4bc3
--- /dev/null
+++ b/include/MicrotunerConfig.h
@@ -0,0 +1,93 @@
+/*
+ * MicrotunerConfig.h - configuration widget for scales and keymaps
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * This file is part of LMMS - https://lmms.io
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program (see COPYING); if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef MICROTUNER_CONFIG_H
+#define MICROTUNER_CONFIG_H
+
+#include
+#include
+#include
+#include
+
+#include "AutomatableModel.h"
+#include "ComboBoxModel.h"
+#include "LcdFloatSpinBox.h"
+#include "LcdSpinBox.h"
+#include "SerializingObject.h"
+
+class LMMS_EXPORT MicrotunerConfig : public QWidget, public SerializingObject
+{
+ Q_OBJECT
+public:
+ MicrotunerConfig();
+
+ void saveSettings(QDomDocument &document, QDomElement &element) override;
+ void loadSettings(const QDomElement &element) override;
+
+ inline QString nodeName() const override
+ {
+ return "MicrotunerConfig";
+ }
+ QSize sizeHint() const override {return QSize(300, 400);}
+
+public slots:
+ void updateScaleList(int index);
+ void updateKeymapList(int index);
+ void updateScaleForm();
+ void updateKeymapForm();
+
+protected:
+ void closeEvent(QCloseEvent *ce) override;
+
+private slots:
+ bool loadScaleFromFile();
+ bool loadKeymapFromFile();
+ bool saveScaleToFile();
+ bool saveKeymapToFile();
+
+private:
+ bool validateScaleForm();
+ bool validateKeymapForm();
+
+ bool applyScale();
+ bool applyKeymap();
+
+ ComboBoxModel m_scaleComboModel; //!< ID of scale currently selected for editing
+ ComboBoxModel m_keymapComboModel; //!< ID of keymap currently selected for editing
+
+ QLineEdit *m_scaleNameEdit; //!< edit field for the scale name or description
+ QLineEdit *m_keymapNameEdit; //!< edit field for the keymap name or description
+
+ QPlainTextEdit *m_scaleTextEdit; //!< text editor field for interval definitions
+ QPlainTextEdit *m_keymapTextEdit; //!< text editor field for key mappings
+
+ IntModel m_firstKeyModel; //!< model for spinbox of currently edited first key
+ IntModel m_lastKeyModel; //!< model for spinbox of currently edited last key
+ IntModel m_middleKeyModel; //!< model for spinbox of currently edited middle key
+
+ IntModel m_baseKeyModel; //!< model for spinbox of currently edited base key
+ FloatModel m_baseFreqModel; //!< model for spinbox of currently edited base note frequency
+};
+
+#endif
diff --git a/include/Mixer.h b/include/Mixer.h
index b656114df64..ad712a416f6 100644
--- a/include/Mixer.h
+++ b/include/Mixer.h
@@ -55,11 +55,6 @@ const int BYTES_PER_SURROUND_FRAME = sizeof( surroundSampleFrame );
const float OUTPUT_SAMPLE_MULTIPLIER = 32767.0f;
-const float BaseFreq = 440.0f;
-const Keys BaseKey = Key_A;
-const Octaves BaseOctave = DefaultOctave;
-
-
#include "PlayHandle.h"
diff --git a/include/Scale.h b/include/Scale.h
new file mode 100644
index 00000000000..5d371502205
--- /dev/null
+++ b/include/Scale.h
@@ -0,0 +1,87 @@
+/*
+ * Scale.h - holds information about a scale and its intervals
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * This file is part of LMMS - https://lmms.io
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program (see COPYING); if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef SCALE_H
+#define SCALE_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include "SerializingObject.h"
+
+class Interval : public SerializingObject
+{
+public:
+ Interval() : m_numerator(1), m_denominator(1), m_cents(0), m_ratio(1) {};
+ explicit Interval(float cents);
+ Interval(uint32_t numerator, uint32_t denominator);
+
+ float getRatio() const {return m_ratio;}
+
+ QString getString() const
+ {
+ if (m_denominator) {return QString::number(m_numerator) + "/" + QString::number(m_denominator);}
+ else {return QString().sprintf("%.4f", m_cents);}
+ }
+
+ void saveSettings(QDomDocument &doc, QDomElement &element) override;
+ void loadSettings(const QDomElement &element) override;
+ inline QString nodeName() const override {return "interval";}
+
+private:
+ // Scala specifies that numerators and denominators should go at least up to 2147483647 → use uint32_t.
+ uint32_t m_numerator; //!< numerator of the interval fraction
+ uint32_t m_denominator; //!< denominator of the interval fraction
+ float m_cents; //!< interval defined in cents (used when denominator is set to zero)
+ float m_ratio; //!< precomputed output value for better performance
+};
+
+
+class Scale : public QObject, public SerializingObject
+{
+ Q_OBJECT
+public:
+ Scale();
+ Scale(QString description, std::vector intervals);
+
+ QString getDescription() const;
+ void setDescription(QString description);
+
+ const std::vector &getIntervals() const {return m_intervals;}
+ void setIntervals(std::vector input) {m_intervals = std::move(input);}
+
+ void saveSettings(QDomDocument &doc, QDomElement &element) override;
+ void loadSettings(const QDomElement &element) override;
+ inline QString nodeName() const override {return "scale";}
+
+private:
+ QString m_description; //!< name or description of the scale
+ std::vector m_intervals; //!< a series of ratios that define the scale
+
+};
+
+#endif
diff --git a/include/Song.h b/include/Song.h
index f9eff1fe29a..a297a488ff8 100644
--- a/include/Song.h
+++ b/include/Song.h
@@ -25,6 +25,7 @@
#ifndef SONG_H
#define SONG_H
+#include
#include
#include
@@ -34,8 +35,11 @@
#include "TrackContainer.h"
#include "Controller.h"
+#include "Keymap.h"
+#include "lmms_constants.h"
#include "MeterModel.h"
#include "Mixer.h"
+#include "Scale.h"
#include "VstSyncController.h"
@@ -350,6 +354,11 @@ class LMMS_EXPORT Song : public TrackContainer
bool isSavingProject() const;
+ std::shared_ptr getScale(unsigned int index) const;
+ std::shared_ptr getKeymap(unsigned int index) const;
+ void setScale(unsigned int index, std::shared_ptr newScale);
+ void setKeymap(unsigned int index, std::shared_ptr newMap);
+
public slots:
void playSong();
void record();
@@ -416,6 +425,12 @@ private slots:
void removeAllControllers();
+ void saveScaleStates(QDomDocument &doc, QDomElement &element);
+ void restoreScaleStates(const QDomElement &element);
+
+ void saveKeymapStates(QDomDocument &doc, QDomElement &element);
+ void restoreKeymapStates(const QDomElement &element);
+
void processAutomations(const TrackList& tracks, TimePos timeStart, fpp_t frames);
void setModified(bool value);
@@ -475,6 +490,9 @@ private slots:
TimePos m_exportSongEnd;
TimePos m_exportEffectiveLength;
+ std::shared_ptr m_scales[MaxScaleCount];
+ std::shared_ptr m_keymaps[MaxKeymapCount];
+
AutomatedValueMap m_oldAutomatedValues;
friend class LmmsCore;
@@ -495,6 +513,8 @@ private slots:
void stopped();
void modified();
void projectFileNameChanged();
+ void scaleListChanged(int index);
+ void keymapListChanged(int index);
} ;
diff --git a/include/lmms_constants.h b/include/lmms_constants.h
index ae6d3d277b1..9a9e550fb32 100644
--- a/include/lmms_constants.h
+++ b/include/lmms_constants.h
@@ -49,9 +49,13 @@ const float F_PI_SQR = (float) LD_PI_SQR;
const float F_E = (float) LD_E;
const float F_E_R = (float) LD_E_R;
+// Microtuner
+const unsigned int MaxScaleCount = 10; //!< number of scales per project
+const unsigned int MaxKeymapCount = 10; //!< number of keyboard mappings per project
+
// Frequency ranges (in Hz).
// Arbitrary low limit for logarithmic frequency scale; >1 Hz.
-const int LOWEST_LOG_FREQ = 10;
+const int LOWEST_LOG_FREQ = 5;
// Full range is defined by LOWEST_LOG_FREQ and current sample rate.
enum FREQUENCY_RANGES
diff --git a/plugins/audio_file_processor/audio_file_processor.cpp b/plugins/audio_file_processor/audio_file_processor.cpp
index 97b2759b2dd..b5276f51d6b 100644
--- a/plugins/audio_file_processor/audio_file_processor.cpp
+++ b/plugins/audio_file_processor/audio_file_processor.cpp
@@ -303,7 +303,8 @@ QString audioFileProcessor::nodeName( void ) const
int audioFileProcessor::getBeatLen( NotePlayHandle * _n ) const
{
- const float freq_factor = BaseFreq / _n->frequency() *
+ const auto baseFreq = instrumentTrack()->baseFreq();
+ const float freq_factor = baseFreq / _n->frequency() *
Engine::mixer()->processingSampleRate() / Engine::mixer()->baseSampleRate();
return static_cast( floorf( ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * freq_factor ) );
diff --git a/plugins/sfxr/sfxr.cpp b/plugins/sfxr/sfxr.cpp
index 1cd58eef405..ef801e0f5bf 100644
--- a/plugins/sfxr/sfxr.cpp
+++ b/plugins/sfxr/sfxr.cpp
@@ -43,6 +43,7 @@ float frnd(float range)
#include "Engine.h"
#include "InstrumentTrack.h"
#include "Knob.h"
+#include "lmms_constants.h"
#include "NotePlayHandle.h"
#include "PixmapButton.h"
#include "ToolTip.h"
@@ -469,7 +470,8 @@ void sfxrInstrument::playNote( NotePlayHandle * _n, sampleFrame * _working_buffe
return;
}
- int32_t pitchedFrameNum = (_n->frequency()/BaseFreq)*frameNum;
+ const auto baseFreq = instrumentTrack()->baseFreq();
+ int32_t pitchedFrameNum = (_n->frequency() / baseFreq) * frameNum;
pitchedFrameNum /= ( currentSampleRate / 44100 );
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index c211d48a7c2..c4807ebcf01 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -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
diff --git a/src/core/ComboBoxModel.cpp b/src/core/ComboBoxModel.cpp
index 7fa905abe42..5694ec5d38c 100644
--- a/src/core/ComboBoxModel.cpp
+++ b/src/core/ComboBoxModel.cpp
@@ -35,6 +35,12 @@ void ComboBoxModel::addItem( QString item, unique_ptr loader )
}
+void ComboBoxModel::replaceItem(std::size_t index, QString item, unique_ptr loader)
+{
+ assert(index < m_items.size());
+ m_items[index] = Item(move(item), move(loader));
+ emit propertiesChanged();
+}
void ComboBoxModel::clear()
diff --git a/src/core/Keymap.cpp b/src/core/Keymap.cpp
new file mode 100644
index 00000000000..325a6d6b6d8
--- /dev/null
+++ b/src/core/Keymap.cpp
@@ -0,0 +1,149 @@
+/*
+ * Keymap.cpp - implementation of keymap class
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * 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
+
+
+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 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(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(m_map.size());
+ }
+ else
+ {
+ return (keyOffset + 1) / static_cast(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();
+ }
+}
diff --git a/src/core/Microtuner.cpp b/src/core/Microtuner.cpp
new file mode 100644
index 00000000000..cbd4f16d30a
--- /dev/null
+++ b/src/core/Microtuner.cpp
@@ -0,0 +1,167 @@
+/*
+ * Microtuner.cpp - manage tuning and scale information of an instrument
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * 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
+#include
+
+#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 keymap = song->getKeymap(m_keymapModel.value());
+ std::shared_ptr scale = song->getScale(m_scaleModel.value());
+ const std::vector &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");
+}
diff --git a/src/core/NotePlayHandle.cpp b/src/core/NotePlayHandle.cpp
index 0caf39b6a77..dd2e23eceac 100644
--- a/src/core/NotePlayHandle.cpp
+++ b/src/core/NotePlayHandle.cpp
@@ -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())
+ {
+ // 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();
+ it->updateFrequency();
}
}
diff --git a/src/core/SampleBuffer.cpp b/src/core/SampleBuffer.cpp
index d4d3aedcc60..13e9df9483c 100644
--- a/src/core/SampleBuffer.cpp
+++ b/src/core/SampleBuffer.cpp
@@ -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();
diff --git a/src/core/SamplePlayHandle.cpp b/src/core/SamplePlayHandle.cpp
index 018629357d4..2b19d56fc34 100644
--- a/src/core/SamplePlayHandle.cpp
+++ b/src/core/SamplePlayHandle.cpp
@@ -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));
}
}
diff --git a/src/core/Scale.cpp b/src/core/Scale.cpp
new file mode 100644
index 00000000000..c71d8607596
--- /dev/null
+++ b/src/core/Scale.cpp
@@ -0,0 +1,122 @@
+/*
+ * Scale.cpp - implementation of scale class
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * 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
+
+
+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(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(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 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();
+ }
+}
diff --git a/src/core/Song.cpp b/src/core/Song.cpp
index 2800f829b17..64773e654b4 100644
--- a/src/core/Song.cpp
+++ b/src/core/Song.cpp
@@ -116,6 +116,9 @@ Song::Song() :
qRegisterMetaType( "Note" );
setType( SongContainer );
+
+ for (int i = 0; i < MaxScaleCount; i++) {m_scales[i] = std::make_shared();}
+ for (int i = 0; i < MaxKeymapCount; i++) {m_keymaps[i] = std::make_shared();}
}
@@ -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 Song::getScale(unsigned int index) const
+{
+ if (index >= MaxScaleCount) {index = 0;}
+
+ return std::atomic_load(&m_scales[index]);
+}
+
+
+std::shared_ptr 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 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 newMap)
+{
+ if (index >= MaxKeymapCount) {index = 0;}
+
+ Engine::mixer()->requestChangeInModel();
+ std::atomic_store(&m_keymaps[index], newMap);
+ emit keymapListChanged(index);
+ Engine::mixer()->doneChangeInModel();
+}
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index d5556d03a77..a04f4bea46c 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -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
diff --git a/src/gui/GuiApplication.cpp b/src/gui/GuiApplication.cpp
index 3effe20afde..a3f39e796fe 100644
--- a/src/gui/GuiApplication.cpp
+++ b/src/gui/GuiApplication.cpp
@@ -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;
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 659a66652f9..73c7065e2b4 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -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();
diff --git a/src/gui/PianoView.cpp b/src/gui/PianoView.cpp
index 64da4895cf7..8771e0588d7 100644
--- a/src/gui/PianoView.cpp
+++ b/src/gui/PianoView.cpp
@@ -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(--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))
{
diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp
index 920504819a6..e08c8508033 100644
--- a/src/gui/editors/PianoRoll.cpp
+++ b/src/gui/editors/PianoRoll.cpp
@@ -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);
diff --git a/src/gui/widgets/InstrumentMidiIOView.cpp b/src/gui/widgets/InstrumentMidiIOView.cpp
index db8ae709bd1..63f9a9532aa 100644
--- a/src/gui/widgets/InstrumentMidiIOView.cpp
+++ b/src/gui/widgets/InstrumentMidiIOView.cpp
@@ -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()
-{
-
-}
diff --git a/src/gui/widgets/InstrumentMiscView.cpp b/src/gui/widgets/InstrumentMiscView.cpp
new file mode 100644
index 00000000000..81f77a88212
--- /dev/null
+++ b/src/gui/widgets/InstrumentMiscView.cpp
@@ -0,0 +1,86 @@
+/*
+ * InstrumentMiscView.cpp - Miscellaneous instrument settings
+ *
+ * Copyright (c) 2005-2014 Tobias Doerffel
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * 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
+#include
+
+#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();
+}
diff --git a/src/gui/widgets/MicrotunerConfig.cpp b/src/gui/widgets/MicrotunerConfig.cpp
new file mode 100644
index 00000000000..519956ed1cd
--- /dev/null
+++ b/src/gui/widgets/MicrotunerConfig.cpp
@@ -0,0 +1,647 @@
+/*
+ * MicrotunerConfig.cpp - configuration widget for scales and keymaps
+ *
+ * Copyright (c) 2020 Martin Pavelek
+ *
+ * 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
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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 &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 &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 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(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 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(
+ 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();
+}
diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp
index 3f55222b5cf..cb6f39da133 100644
--- a/src/tracks/InstrumentTrack.cpp
+++ b/src/tracks/InstrumentTrack.cpp
@@ -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,24 +152,94 @@ InstrumentTrack::InstrumentTrack( TrackContainer* tc ) :
}
-int InstrumentTrack::baseNote() const
+
+bool InstrumentTrack::keyRangeImport() const
{
- int mp = m_useMasterPitchModel.value() ? Engine::getSong()->masterPitch() : 0;
+ 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 m_baseNoteModel.value() - mp;
+ 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
{
- return m_firstKeyModel.value();
+ 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
{
- return m_lastKeyModel.value();
+ 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;
+
+ if (keyRangeImport())
+ {
+ return Engine::getSong()->getKeymap(m_microtuner.currentKeymap())->getBaseKey() - mp;
+ }
+ else
+ {
+ return m_baseNoteModel.value() - mp;
+ }
+}
+
+
+/** \brief Return frequency assigned to the base key, based on currently selected keymap.
+ * \return Frequency in Hz
+ */
+float InstrumentTrack::baseFreq() const
+{
+ if (m_microtuner.enabled())
+ {
+ return Engine::getSong()->getKeymap(m_microtuner.currentKeymap())->getBaseFreq();
+ }
+ else
+ {
+ return DefaultBaseFreq;
+ }
+}
+
+
+
InstrumentTrack::~InstrumentTrack()
{
// De-assign midi device
@@ -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();
}