From e07861ced3665082d2bd3d241d9864abd5ce88a1 Mon Sep 17 00:00:00 2001 From: Martin Pavelek Date: Thu, 9 Sep 2021 19:49:24 +0200 Subject: [PATCH] Support for alternative tunings and keyboard mappings (#5522) Co-authored-by: Kevin Zander Co-authored-by: Dominic Clark Co-authored-by: Martin --- data/projects/templates/default.mpt | 163 +++++ data/themes/classic/lcd_19green_dot.png | Bin 0 -> 1658 bytes data/themes/classic/lcd_19pink_dot.png | Bin 0 -> 1555 bytes data/themes/classic/lcd_19red_dot.png | Bin 0 -> 1529 bytes data/themes/classic/microtuner.png | Bin 0 -> 6685 bytes data/themes/default/lcd_19green_dot.png | Bin 0 -> 1331 bytes data/themes/default/lcd_19pink_dot.png | Bin 0 -> 1150 bytes data/themes/default/lcd_19red_dot.png | Bin 0 -> 1202 bytes data/themes/default/microtuner.png | Bin 0 -> 2631 bytes include/ComboBoxModel.h | 3 + include/GuiApplication.h | 3 + include/InstrumentMidiIOView.h | 18 - include/InstrumentMiscView.h | 63 ++ include/InstrumentTrack.h | 13 +- include/Keymap.h | 79 +++ include/MainWindow.h | 1 + include/Microtuner.h | 73 ++ include/MicrotunerConfig.h | 93 +++ include/Mixer.h | 5 - include/Scale.h | 87 +++ include/Song.h | 20 + include/lmms_constants.h | 6 +- .../audio_file_processor.cpp | 3 +- plugins/sfxr/sfxr.cpp | 4 +- src/core/CMakeLists.txt | 3 + src/core/ComboBoxModel.cpp | 6 + src/core/Keymap.cpp | 149 ++++ src/core/Microtuner.cpp | 167 +++++ src/core/NotePlayHandle.cpp | 45 +- src/core/SampleBuffer.cpp | 6 +- src/core/SamplePlayHandle.cpp | 8 +- src/core/Scale.cpp | 122 ++++ src/core/Song.cpp | 102 +++ src/gui/CMakeLists.txt | 2 + src/gui/GuiApplication.cpp | 9 + src/gui/MainWindow.cpp | 21 + src/gui/PianoView.cpp | 28 +- src/gui/editors/PianoRoll.cpp | 9 +- src/gui/widgets/InstrumentMidiIOView.cpp | 22 - src/gui/widgets/InstrumentMiscView.cpp | 86 +++ src/gui/widgets/MicrotunerConfig.cpp | 647 ++++++++++++++++++ src/tracks/InstrumentTrack.cpp | 101 ++- 42 files changed, 2077 insertions(+), 90 deletions(-) create mode 100644 data/themes/classic/lcd_19green_dot.png create mode 100644 data/themes/classic/lcd_19pink_dot.png create mode 100644 data/themes/classic/lcd_19red_dot.png create mode 100644 data/themes/classic/microtuner.png create mode 100644 data/themes/default/lcd_19green_dot.png create mode 100644 data/themes/default/lcd_19pink_dot.png create mode 100644 data/themes/default/lcd_19red_dot.png create mode 100644 data/themes/default/microtuner.png create mode 100644 include/InstrumentMiscView.h create mode 100644 include/Keymap.h create mode 100644 include/Microtuner.h create mode 100644 include/MicrotunerConfig.h create mode 100644 include/Scale.h create mode 100644 src/core/Keymap.cpp create mode 100644 src/core/Microtuner.cpp create mode 100644 src/core/Scale.cpp create mode 100644 src/gui/widgets/InstrumentMiscView.cpp create mode 100644 src/gui/widgets/MicrotunerConfig.cpp 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 0000000000000000000000000000000000000000..1459b7d9af335f5bb136ee32e70f06e1e4fdd085 GIT binary patch literal 1658 zcmV-=28H>FP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1Y$lIAD~{qHIE2mwU&I4D@PH`wFvGhh1a>7JRb z+O170nh;drJGb=9Y*ukym}`N0qg zdU<`eeDoc1{TyJ=L^Qw3i>+s#{!H|-@-v~Fl^HLOUVkPMe#zYP$D?Rj+EIdr2E~a`$~~;%Hav=V zyYGR!!^H)}0%%U$tI!}u1UFR*a@v%4zi7e4%E^YFc_HFQ*2Q@60vIRwxM{P=kSNlD z$^vQvF~4o0d);xbx2R*~AZRjSGQx>q28p`rmZz=r$)JnRa0wb)@_(~>9uQb-Fok%``{rcVLIBdGWr<9 z9g8}qc8qptxKExk>(tq%%|6HU1^TSIbhTxxudzHhB#{2m!b%YQ{e}`N-(ET6Cl>^;_ z+|RiE0=4jaN0A$#lZ7XymuwWaeMPth_q!dtDsycuBvJV8b~`q!3(fEPrBpgr(3rXF zoQO)twuJcN7s3xf^QC^0uj&yft4zWQ=Odg)O#GNg5tR9bU7M+U!hda0LyfG1X4?y4 zLk{lM1OA(%+BDc`d!M~KrtCd8NS0J;>}%~lX2o9sf{^VH!T+PoYe2PI_zV*0GfY9< zHw15kYDcH5NsWSKCqdj7tN{zirpE!7AWDRGB9)lxG}Mi*umXrrls&LH4=!-T+Ke)6 zHO<#TNP)(Do4rmu%;?ju6&bn`xxzIZ#;jvmR=yoAmwR8~Co~9~oGOO$k}2@eu$yS> z+a@Kj_a&z6b27JFz0#27gLbs(UT!bznl*15FoEuE%g@(F@k@&hPeBTojB96~cmrwG z!B+N$oKuP{we2>yk0tK0y`o~S4pJj?nXn&V(_&a8IgvuD^O>w7{;-9bgKW!J!?6bW zgZ$HLtqZ3RK3qa+Jx7!diN47vH;q7{!?JTQI@Egi1!tJUI6p_Nryqtx;nBf9dzg6> zvE4hQO3ZbIi6HH8=4x#v;xR&ssLegEOt8vO9`*o2mk(ngE;2v`Z024Q;69VJmKpes ztgptTTs^aFq!QAxg1{p(PmMtdK+^g=dC+1^0Eg`gbnbKZodB?Tg4FY3u=gw<`y?%P zv|~eFQa!Y7pwFl)u$pLq%#=5?U3bQedWI%g1ZEs$F;KyQQFNQ+As#qX6L2Bd4JHoI zY2mQPI#DoJyJW$6?@&!Y6N85!c-%Bby(!rQR7E+8^su#2)6@-A=4GG)hyh1dz#5sL z`^bA}&wjiJ9CncYCUKPoDzEAeXi`Du08Dr3m`y@HiVj@aG0NWvJm*p@Lq#wE000JJ zOGiWi9sn2sN}{jG+5i9m32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^Rf1sW3$4$&Ug z9{>OVxJg7oR4C7_kTD9vKoCXW><$=6Kw@X1mDt#N0V|tSf;Jw-K4tEcCKt&8QdtU$ zkhO@>Bp4LQ?{;~+Z{~|X_8kayR8?ul(8&-NZ^hHBV&%&dW6Y1AI^3J64;7(;G`2MW zt0ZETL=~|(ZCLLQ0Nl@|vp2ryy&caq>G73D`nnSG}q@d=c+TG=kf_f~{wK&(W zRqTff%iWxEH?N3tJLP&@0^nWls7VXF8cz$vp9HPO9$cFy8w^*~_5c6?07*qoM6N<$ Ef}!yhdjJ3c literal 0 HcmV?d00001 diff --git a/data/themes/classic/lcd_19pink_dot.png b/data/themes/classic/lcd_19pink_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..aa167803ae28c73701e6cb0632328a4326b3ae59 GIT binary patch literal 1555 zcmV+u2JHEXP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1YwvgIZW{AU$e0ul&`<%ox>W(TwUG`7!uXR4CQ z;o8``AVhadIPKrRBK?IQRjR3qTI;Q4{A#bA9S@f8-`!KL*Zuz0r>D>M#O?V9!&K0h z`?c+7yu&W<6ZCOJ_VO8ulwV-C zFBm;`lAp1=5c(9+`^umGq3=Y0$1avd?0a`!8<1-0+0<(**AoT~_$k-4hNid#&w90v zX4v8cV+(8!w3(o_PZCW3!^%d4dcA@MsjQrI(e8ft3l? z3e5Z#3tj8>YrRQ^l^0u~^xu(lu=DqtWtNSYdD zWmY2KXyC^YIhV=<43G#fN&#*PGC|q+tc^KJ*H{+5%_uD(LP{$NYI<>yRjWWhS`8X< zDN>V&G*L0>;;ppWq_w8))F{`YRYjYsns#+7R!yv#nprpR+zU#W^wu?X>%F^UQHN{C zY{v-q!J~{iWVE5fMjt*QpIN8OHg($U(-*I@kp8PJP0LnazG*{ByKdQS>$csu2ecL? zYDl!uu;}4$tkILUpID9`)dRRM*)oYx-vj(&3%LScsB8M|D#+AUh9tI$2KAdHv zTKO<{ILn=p6vAMQD%CurLgDbFI|NAWU&xKE^uJz-LrFPdz4dX!SgALbNM~Pn2rz zNQFME3WJojBODRM?j5J(=4_3vAyjmWAsZlR0uY*VL$G79PFWtUkJ5ahYAfpXIk?MC zyJ%=1hzHP*2Bc%_xMC=_VpHv2%*W0Sr*7cX-G;7GSHY2?WkL42ry~)Xa|1fx%A_8Z z8On*Fdnvs3i5){V$2s|yF;@$G5$V~KF;qQhOWG8rc!yAfrL1v8RhDpY8Ec@{;9KCJ zk|i)`^xK_d$8kHdC^A+~Lf9sP;j(hm=NrRjzNL|m9Lo8YfrfzP zeCa8D5LWS9HJ)!7VvmOQCLW8=nCqP7YVFzHH({fOI#+Z)I4Vqa&boI&QS5 z(*eA3)P$O`kxvkG*>)8NiGi$$OYsZvYeQkfNv3&kpJPF2V+RkMYX@jBm|y9MLebOT zzpSv;DAD!c00006VoOIv0QCR`0BAflPtyPZ010qNS#tmY4c7nw4c7reD4Tcy000Mc zNliru5jY%3}Zk0IW$wK~yNut&lwm!cY)|zs*}Dpj|2@O|bS7!O{@CgS}Vr z0J%oG;AwJ&RKcvpQ+W9S3$xYk@a+tY>}6{NqcJYLiI~h9f_@i4yr2_k{-PiBx2c7M zjXf4pYAvX>@I>Wu4)+TH)uTgBl33+(0B*0R7eWt7h{qLf#H~`0Oa=R|pyiY(cvFBO z$eG2eTdJ^>j8ZaBoF(NN9RcWu9&N0J!Q+j!NGiKgV{h!AAVqi~_0|9Y002ovPDHLk FV1hev-pv32 literal 0 HcmV?d00001 diff --git a/data/themes/classic/lcd_19red_dot.png b/data/themes/classic/lcd_19red_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..b9137754a9006f4247e6a872e88cbbf69bea331e GIT binary patch literal 1529 zcmV zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1Y=uI?rb{Ld=71Z-m)FNdep9d!8}_;Q~2^h&K7 z6i5izW@a!+>#sj|`U4l0%&DkX3$u)?Lc#}M_e{mI8;r#^<>JU<5IcxF62m5|WI%9xw8`8*W9-VrLLWG~9Y?`5C)J?Pu`bpT1$2+tV*!p-A}+Z2N}M z(@**{b{C;95q)_6^1;eG(Z6E1XWe_fR@aIFDNE0y-j;HoFt7v47aE4*xE0TSOQ2LN zae%Q2Hap5l6vRM#Q=v(XHm%zZ8VIrQVn@q7fcWXF;v76c21z-EHoJt(Bo(X-uqNo{ z_gLuqjq7hvh382a$pmAB8NZbDQ|Esv?}qk1(k06Eg%f0fxcF!qVI1a8jZvV0_JgTj zd~Zpgbdx_3E8t8o%oPi4xxY+wr4Mb%21%1qc4j96 zjuregM9xyUg8@?INoj){!A@|le0*b$(;dsouWqCk5TRn?peCe&tWp&E@lv6orlKXO zs;O($&}L%A)XdyYj%qGmT;1HgdU&f+OVw)CYpqdRa0pVEhJ~l75z!i)HkfXh-7uoH z&Ru%x+O2!9J$f6E&nQEO4Ig#HXp^T*Z2xJ~)2uUQTeKjhRhBMWzUqqAHng^7m#y2j z@492Rch>0HwqIDIZ_NFkHF{V(jFmgiKUhPz`s`$+;4eHTK*Mp{7=lKL*4(uTsqV}%>ByS53J30 zPZYTXon3e^&9af$b{@0&Y|f4kxRIBer|OIA!uohF2@?&uCYr;ReF(-wT|VtZkdwFO`xg)$kfuWN=0MyeWs6Kn^B z-g%Zyyb~!>#q6ua5g!#Nj#`Mp80HPlQnr-OkOTLw=3O5G2%$4##0_+Uig^&c-!TjH)y?=ek$dNyprg@Wo@aC2$ni zI6)vjAccasf!(Uy=CEZ-t**pv92HsH3#&CO*Q)A-+9mgj4M<~^Si|S)0*&sMiMnO> znGcT)+})GmuYl{whyMu1?-Im7<+6Htw?{))B;VEZu9BMUws%&q*da$ve%16gK8$KK z55%a*9|8m*fX)plZYU$-EIh#(XtV*2v_Jy@)FyI`b1Om62)1B`4d0N6Ai4Mk%aDYx zB{OeWy~0RV2JN{+wh&~aBajF7#BN-}O6ddJ4d?Z1$~;pn<^m-^9q|-wLMNr1E>?HNAAsf;JitS-xIn%F;$gzcOCGB zU4QiCMWw_<>XCZFynrgj+YZFpJ0txKt6`@el;|zQ00006VoOIv0H6S50BD8Nu73ak z010qNS#tmY4c7nw4c7reD4Tcy000McNliru0Io?yK~yNuV-(=# z`pD=W3>D=eDzs#!8BCNQoS%RGFtA*E{1mSI+T*7T fj6yuzC?N&_f^HylVTV0v00000NkvXXu0mjfA`8u^ literal 0 HcmV?d00001 diff --git a/data/themes/classic/microtuner.png b/data/themes/classic/microtuner.png new file mode 100644 index 0000000000000000000000000000000000000000..7880394cbc9530b586126dbe9bf7b0eea541af01 GIT binary patch literal 6685 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 literal 0 HcmV?d00001 diff --git a/data/themes/default/lcd_19green_dot.png b/data/themes/default/lcd_19green_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..2f4a8cf990071842f1970581498955619abfe30a GIT binary patch literal 1331 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 QoB#j-07*qoM6N<$f}z$4d;kCd literal 0 HcmV?d00001 diff --git a/data/themes/default/lcd_19red_dot.png b/data/themes/default/lcd_19red_dot.png new file mode 100644 index 0000000000000000000000000000000000000000..430768f3ee619b39ea8e73b0d6cda1241063d2f1 GIT binary patch literal 1202 zcmV;j1Wo&iP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3vrlI120h5xgPEWr|bu^jQL$_}#p9DLo~(>QY+vkR9rX`Fb8>vg?qy5xSoE_rJDY%grj9}H7P zU0*+KJ^l&1el+kW5cSXY;^T=EPoTG+pMdrB%y@bH;t5pwWvAzNX-~V9AExu%eLgmQ zRbPLLLyW12SGn-2R4+W^^ip4U;#NGl+Hr!%u>_t{`FrgO<@e&fzkSi}%Ew+mOHr;j zvF(dSkDc^;?QVrWjp(E2Pk&5*Z}O9Nd(YT=y=IqUP|C_Pskf=zZ*c68vfj-M#c?a% z{Vok9FvS646Er)@DAb6d_9mf8Nt@PP4;mPv@M1^JJfQfoMRjgGAU#2N6x!^y)mgxT z>j1TSh`f)5?ta_d-=ILxliV}MU$qrMA*=95<$LgSFd}w zvL)(X*o1RQmu#zco08npY$k+(5LD+0(tLLZRW!pyv6T#`z6VxrMk{kmU4i{F8$is< zps}>MbN0?_8SMzBK+AH&@E2Imr@^qEPXoIO#?b&GLLiYtj3JRjjNBju07$I{55O1% QFaQ7m07*qoM6N<$f`@}QGXMYp literal 0 HcmV?d00001 diff --git a/data/themes/default/microtuner.png b/data/themes/default/microtuner.png new file mode 100644 index 0000000000000000000000000000000000000000..c197ba1abc5c7ca985127407a4accf7626c0cebb GIT binary patch literal 2631 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 literal 0 HcmV?d00001 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(); }