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

@@ -81,5 +81,168 @@
<projectnotes width="640" x="700" y="10" maximized="0" height="400" visible="0" minimized="0"></projectnotes>
<timeline lp1pos="192" lp0pos="0" lpstate="0"/>
<controllers/>
<scales>
<scale description="12-tone EDO (default)">
<interval num="1" den="1"/>
<interval cents="100"/>
<interval cents="200"/>
<interval cents="300"/>
<interval cents="400"/>
<interval cents="500"/>
<interval cents="600"/>
<interval cents="700"/>
<interval cents="800"/>
<interval cents="900"/>
<interval cents="1000"/>
<interval cents="1100"/>
<interval num="2" den="1"/>
</scale>
<scale description="24-tone EDO">
<interval num="1" den="1"/>
<interval cents="50"/>
<interval cents="100"/>
<interval cents="150"/>
<interval cents="200"/>
<interval cents="250"/>
<interval cents="300"/>
<interval cents="350"/>
<interval cents="400"/>
<interval cents="450"/>
<interval cents="500"/>
<interval cents="550"/>
<interval cents="600"/>
<interval cents="650"/>
<interval cents="700"/>
<interval cents="750"/>
<interval cents="800"/>
<interval cents="850"/>
<interval cents="900"/>
<interval cents="950"/>
<interval cents="1000"/>
<interval cents="1050"/>
<interval cents="1100"/>
<interval cents="1150"/>
<interval num="2" den="1"/>
</scale>
<scale description="7-tone JI C-major (Ptolemaic)">
<interval num="1" den="1"/>
<interval num="9" den="8"/>
<interval num="5" den="4"/>
<interval num="4" den="3"/>
<interval num="3" den="2"/>
<interval num="5" den="3"/>
<interval num="15" den="8"/>
<interval num="2" den="1"/>
</scale>
<scale description="12-tone JI (3-limit / Pythagorean)">
<interval num="1" den="1"/>
<interval num="256" den="243"/>
<interval num="9" den="8"/>
<interval num="32" den="27"/>
<interval num="81" den="64"/>
<interval num="4" den="3"/>
<interval num="729" den="512"/>
<interval num="3" den="2"/>
<interval num="128" den="81"/>
<interval num="27" den="16"/>
<interval num="16" den="9"/>
<interval num="243" den="128"/>
<interval num="2" den="1"/>
</scale>
<scale description="5-tone A-minor (3-limit / Pythagorean)">
<interval num="1" den="1"/>
<interval num="32" den="27"/>
<interval num="4" den="3"/>
<interval num="3" den="2"/>
<interval num="16" den="9"/>
<interval num="2" den="1"/>
</scale>
</scales>
<keymaps>
<keymap middle_key="0" description="all keys 1:1 (default)" base_freq="440" last_key="127" first_key="0" base_key="69">
</keymap>
<keymap middle_key="60" description="white keys to 7 degrees" base_freq="440" last_key="127" first_key="0" base_key="69">
<degree value="0"/>
<degree value="-1"/>
<degree value="1"/>
<degree value="-1"/>
<degree value="2"/>
<degree value="3"/>
<degree value="-1"/>
<degree value="4"/>
<degree value="-1"/>
<degree value="5"/>
<degree value="-1"/>
<degree value="6"/>
</keymap>
<keymap middle_key="60" description="white keys to 12 degrees" base_freq="440" last_key="127" first_key="0" base_key="69">
<degree value="0"/>
<degree value="-1"/>
<degree value="2"/>
<degree value="-1"/>
<degree value="4"/>
<degree value="5"/>
<degree value="-1"/>
<degree value="7"/>
<degree value="-1"/>
<degree value="9"/>
<degree value="-1"/>
<degree value="11"/>
</keymap>
<keymap middle_key="60" description="white keys to 24 degrees" base_freq="440" last_key="127" first_key="0" base_key="69">
<degree value="0"/>
<degree value="-1"/>
<degree value="2"/>
<degree value="-1"/>
<degree value="4"/>
<degree value="5"/>
<degree value="-1"/>
<degree value="7"/>
<degree value="-1"/>
<degree value="9"/>
<degree value="-1"/>
<degree value="11"/>
<degree value="12"/>
<degree value="-1"/>
<degree value="14"/>
<degree value="-1"/>
<degree value="16"/>
<degree value="17"/>
<degree value="-1"/>
<degree value="19"/>
<degree value="-1"/>
<degree value="21"/>
<degree value="-1"/>
<degree value="23"/>
</keymap>
<keymap middle_key="60" description="black keys to 5 degrees" base_freq="440" last_key="127" first_key="0" base_key="61">
<degree value="-1"/>
<degree value="0"/>
<degree value="-1"/>
<degree value="1"/>
<degree value="-1"/>
<degree value="-1"/>
<degree value="2"/>
<degree value="-1"/>
<degree value="3"/>
<degree value="-1"/>
<degree value="4"/>
<degree value="-1"/>
</keymap>
<keymap middle_key="60" description="black keys to 12 degrees" base_freq="466.165" last_key="127" first_key="0" base_key="70">
<degree value="-1"/>
<degree value="1"/>
<degree value="-1"/>
<degree value="3"/>
<degree value="-1"/>
<degree value="-1"/>
<degree value="6"/>
<degree value="-1"/>
<degree value="8"/>
<degree value="-1"/>
<degree value="10"/>
<degree value="-1"/>
</keymap>
</keymaps>
</song>
</lmms-project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -25,6 +25,7 @@
#ifndef COMBOBOX_MODEL_H
#define COMBOBOX_MODEL_H
#include <cassert>
#include <memory>
#include <utility>
#include <vector>
@@ -52,6 +53,8 @@ public:
void addItem( QString item, std::unique_ptr<PixmapLoader> loader = nullptr );
void replaceItem(std::size_t index, QString item, std::unique_ptr<PixmapLoader> loader = nullptr);
void clear();
int findText( const QString& txt ) const;

View File

@@ -37,6 +37,7 @@ class BBEditor;
class ControllerRackView;
class FxMixerView;
class MainWindow;
class MicrotunerConfig;
class PianoRollWindow;
class ProjectNotes;
class SongEditorWindow;
@@ -59,6 +60,7 @@ public:
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:
BBEditor* m_bbEditor;
PianoRollWindow* m_pianoRoll;
ProjectNotes* m_projectNotes;
MicrotunerConfig* m_microtunerConfig;
ControllerRackView* m_controllerRackView;
QLabel* m_loadingProgressLabel;
};

View File

@@ -65,22 +65,4 @@ private:
} ;
class InstrumentMiscView : public QWidget
{
Q_OBJECT
public:
InstrumentMiscView( InstrumentTrack *it, QWidget* parent );
~InstrumentMiscView();
GroupBox * pitchGroupBox()
{
return m_pitchGroupBox;
}
private:
GroupBox * m_pitchGroupBox;
};
#endif

View File

@@ -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 <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.
*
*/
#ifndef INSTRUMENT_MISC_VIEW_H
#define INSTRUMENT_MISC_VIEW_H
#include <QWidget>
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

View File

@@ -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 @@ public:
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 @@ private:
Piano m_piano;
Microtuner m_microtuner;
std::unique_ptr<BoolModel> m_midiCCEnable;
std::unique_ptr<FloatModel> m_midiCCModel[MidiControllerCount];

79
include/Keymap.h Normal file
View File

@@ -0,0 +1,79 @@
/*
* Keymap.h - holds information about a key mapping
*
* 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.
*
*/
#ifndef KEYMAP_H
#define KEYMAP_H
#include <vector>
#include <QObject>
#include <QString>
#include "Note.h"
#include "SerializingObject.h"
class Keymap : public QObject, public SerializingObject
{
Q_OBJECT
public:
Keymap();
Keymap(
QString description,
std::vector<int> 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<int> &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<int> 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

View File

@@ -152,6 +152,7 @@ public slots:
void toggleBBEditorWin( bool forceShow = false );
void toggleSongEditorWin();
void toggleProjectNotesWin();
void toggleMicrotunerWin();
void toggleFxMixerWin();
void togglePianoRollWin();
void toggleControllerRack();

73
include/Microtuner.h Normal file
View File

@@ -0,0 +1,73 @@
/*
* Microtuner.h - 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.
*
*/
#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

View File

@@ -0,0 +1,93 @@
/*
* MicrotunerConfig.h - 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.
*
*/
#ifndef MICROTUNER_CONFIG_H
#define MICROTUNER_CONFIG_H
#include <QCloseEvent>
#include <QLineEdit>
#include <QMainWindow>
#include <QPlainTextEdit>
#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

View File

@@ -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"

87
include/Scale.h Normal file
View File

@@ -0,0 +1,87 @@
/*
* Scale.h - holds information about a scale and its intervals
*
* 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.
*
*/
#ifndef SCALE_H
#define SCALE_H
#include <cmath>
#include <cstdint>
#include <vector>
#include <QObject>
#include <QString>
#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<Interval> intervals);
QString getDescription() const;
void setDescription(QString description);
const std::vector<Interval> &getIntervals() const {return m_intervals;}
void setIntervals(std::vector<Interval> 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<Interval> m_intervals; //!< a series of ratios that define the scale
};
#endif

View File

@@ -25,6 +25,7 @@
#ifndef SONG_H
#define SONG_H
#include <memory>
#include <utility>
#include <QtCore/QSharedMemory>
@@ -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 @@ public:
bool isSavingProject() const;
std::shared_ptr<const Scale> getScale(unsigned int index) const;
std::shared_ptr<const Keymap> getKeymap(unsigned int index) const;
void setScale(unsigned int index, std::shared_ptr<Scale> newScale);
void setKeymap(unsigned int index, std::shared_ptr<Keymap> newMap);
public slots:
void playSong();
void record();
@@ -416,6 +425,12 @@ private:
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:
TimePos m_exportSongEnd;
TimePos m_exportEffectiveLength;
std::shared_ptr<Scale> m_scales[MaxScaleCount];
std::shared_ptr<Keymap> m_keymaps[MaxKeymapCount];
AutomatedValueMap m_oldAutomatedValues;
friend class LmmsCore;
@@ -495,6 +513,8 @@ signals:
void stopped();
void modified();
void projectFileNameChanged();
void scaleListChanged(int index);
void keymapListChanged(int index);
} ;

View File

@@ -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

View File

@@ -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<int>( floorf( ( m_sampleBuffer.endFrame() - m_sampleBuffer.startFrame() ) * freq_factor ) );

View File

@@ -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 );

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();
}