├── .gitignore ├── tests ├── data │ ├── kbm-fix-012023 │ │ ├── exact4.scl │ │ ├── ed4-4.scl │ │ ├── empty-c-100.kbm │ │ ├── threenote-c-100.kbm │ │ ├── fournote-c-100.kbm │ │ ├── threenote-c-100-from-1.kbm │ │ ├── sixnote-c-100.kbm │ │ ├── eightnote-c-100.kbm │ │ └── twelve-c-100.kbm │ ├── carlos-alpha.scl │ ├── 6-exact.scl │ ├── 12-intune-nodesc.scl │ ├── 12-ET-P5.scl │ ├── 12-intune.scl │ ├── 12-shuffled.scl │ ├── 12-intune-dosle.scl │ ├── bad │ │ ├── missingnote.scl │ │ ├── blanknote.scl │ │ ├── badnote.scl │ │ ├── empty-bad.kbm │ │ ├── extraline.scl │ │ ├── empty-extra.kbm │ │ ├── missing-note.kbm │ │ ├── blank-line.kbm │ │ ├── garbage-key.kbm │ │ ├── extraline-long.kbm │ │ └── bad-rast.ascl │ ├── mapping-allkeys-from-59-a440.kbm │ ├── mapping-whitekeys-from-59-a440.kbm │ ├── ED3-17.scl │ ├── ED4-17.scl │ ├── empty-note69.kbm │ ├── empty-note61.kbm │ ├── piano.kbm │ ├── empty-note69-dosle.kbm │ ├── 128.kbm │ ├── marvel12.scl │ ├── 31edo_meantone.kbm │ ├── kbm-wrapping-7822 │ │ ├── 31edo2-subset-57.kbm │ │ ├── 31edo2-subset.kbm │ │ └── 31edo2.scl │ ├── 31edo.scl │ ├── 31edo_dos_lineends.scl │ ├── liwung-tbn.kbm │ ├── zeus22.scl │ ├── rast.kbm │ ├── maqamat.kbm │ ├── mapping-n60-250.kbm │ ├── liwung-tbn.ascl │ ├── mapping-note54-to-259-6.kbm │ ├── mapping-note53-to-430-408.kbm │ ├── mapping-n60-fifths.kbm │ ├── mapping-whitekeysalt-from-59-a440.kbm │ ├── mapping-a440-constant.kbm │ ├── mapping-whitekeys-a440.kbm │ ├── shuffle-a440-constant.kbm │ ├── 31-edo.kbm │ ├── mapping-whitekeys-c261.kbm │ ├── mapping-whitekeys-from-48-a440.kbm │ ├── mapping-a442-7-to-12.kbm │ ├── 31-edo.ascl │ ├── rast.ascl │ ├── maqamat.ascl │ └── rast6.ascl ├── symbolcheck1.cpp ├── symbolcheck2.cpp └── alltests.cpp ├── .clang-format ├── .github └── workflows │ ├── code-checks.yml │ └── build-pr.yml ├── scripts └── release-notes.sh ├── LICENSE.md ├── commands ├── parsecheck.cpp └── showmapping.cpp ├── CMakeLists.txt ├── README.md └── include ├── Tunings.h └── TuningsImpl.h /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .DS_Store 3 | .idea/ 4 | cmake-*/ 5 | ignore/ 6 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/exact4.scl: -------------------------------------------------------------------------------- 1 | ! Exacct 4s 2 | Exact 4 3 | 4 4 | 5/4 5 | 3/2 6 | 7/4 7 | 2/1 8 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/ed4-4.scl: -------------------------------------------------------------------------------- 1 | ! Exacct 4s 2 | All the n/1 up to 5 3 | 4 4 | 2/1 5 | 3/1 6 | 4/1 7 | 5/1 8 | -------------------------------------------------------------------------------- /tests/data/carlos-alpha.scl: -------------------------------------------------------------------------------- 1 | ! Wendy Carlos Alpha Scale (a single even temp of 78 cents) 2 | Carlos Alpha 78 cent scale 3 | 1 4 | 78.0 5 | -------------------------------------------------------------------------------- /tests/data/6-exact.scl: -------------------------------------------------------------------------------- 1 | ! 6 exact 2 | ! 3 | HD2 06-12 - Harmonic division of 2: Harmonics 06-12 4 | 6 5 | ! 6 | 7/6 7 | 4/3 8 | 3/2 9 | 5/3 10 | 11/6 11 | 2/1 12 | -------------------------------------------------------------------------------- /tests/data/12-intune-nodesc.scl: -------------------------------------------------------------------------------- 1 | ! 12 TET with a blank description 2 | ! 3 | 4 | 12 5 | ! 6 | 100.0 7 | 200.0 8 | 300.0 9 | 400.0 10 | 500.0 11 | 600.0 12 | 700.0 13 | 800.0 14 | 900.0 15 | 1000.0 16 | 1100.0 17 | 2/1 18 | -------------------------------------------------------------------------------- /tests/data/12-ET-P5.scl: -------------------------------------------------------------------------------- 1 | ! C:\Users\green\Music\VST\Scales\12-22_Dorian.scl 2 | ! 3 | 22-edo, mode 3 1 1 3 1 3 1 3 1 1 3 1 4 | 12 5 | ! 6 | 100.0 7 | 200.0 8 | 300.0 9 | 400.0 10 | 500.0 11 | 600.0 12 | 3/2 13 | 800.0 14 | 900.0 15 | 1000.0 16 | 1100.0 17 | 2/1 18 | -------------------------------------------------------------------------------- /tests/data/12-intune.scl: -------------------------------------------------------------------------------- 1 | ! C:\Users\green\Music\VST\Scales\12-22_Dorian.scl 2 | ! 3 | 22-edo, mode 3 1 1 3 1 3 1 3 1 1 3 1 4 | 12 5 | ! 6 | 100.0 7 | 200.0 8 | 300.0 9 | 400.0 10 | 500.0 11 | 600.0 12 | 700.0 13 | 800.0 14 | 900.0 15 | 1000.0 16 | 1100.0 17 | 2/1 18 | -------------------------------------------------------------------------------- /tests/data/12-shuffled.scl: -------------------------------------------------------------------------------- 1 | ! C:\Users\green\Music\VST\Scales\12-22_Dorian.scl 2 | ! 3 | 22-edo, mode 3 1 1 3 1 3 1 3 1 1 3 1 4 | 12 5 | ! 6 | 200.0 7 | 100.0 8 | 300.0 9 | 500.0 10 | 400.0 11 | 600.0 12 | 700.0 13 | 800.0 14 | 1000.0 15 | 900.0 16 | 1100.0 17 | 2/1 18 | -------------------------------------------------------------------------------- /tests/symbolcheck1.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Make sure we don't have duplicated symbols by compiling a pair of objects 3 | ** and linking them 4 | */ 5 | 6 | #include "Tunings.h" 7 | 8 | double symbolcheck1() 9 | { 10 | auto k = Tunings::tuneNoteTo(60, 100); 11 | return k.tuningFrequency; 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/12-intune-dosle.scl: -------------------------------------------------------------------------------- 1 | ! C:\Users\green\Music\VST\Scales\12-22_Dorian.scl 2 | ! 3 | 22-edo, mode 3 1 1 3 1 3 1 3 1 1 3 1 4 | 12 5 | ! 6 | 100.0 7 | 200.0 8 | 300.0 9 | 400.0 10 | 500.0 11 | 600.0 12 | 700.0 13 | 800.0 14 | 900.0 15 | 1000.0 16 | 1100.0 17 | 2/1 18 | -------------------------------------------------------------------------------- /tests/data/bad/missingnote.scl: -------------------------------------------------------------------------------- 1 | ! D:\Scala Batch\ED3-17.scl 2 | ! 3 | ED3-17 - Equal division of harmonic 3 into 17 parts 4 | 17 5 | ! 6 | 111.87971 7 | 223.75941 8 | 335.63912 9 | 447.51882 10 | 559.39853 11 | 671.27824 12 | 783.15794 13 | 895.03765 14 | 1118.79706 15 | 1230.67677 16 | 1342.55647 17 | 1454.43618 18 | 1566.31588 19 | 1678.19559 20 | 1790.07529 21 | 3/1 22 | -------------------------------------------------------------------------------- /tests/data/mapping-allkeys-from-59-a440.kbm: -------------------------------------------------------------------------------- 1 | ! 12EDO_WhiteKeys.kbm 2 | ! Size of map: 3 | 12 4 | ! MIDI range: 5 | 0 6 | 127 7 | ! Middle note: 8 | 59 9 | ! Reference note (MIDI number and frequency in Hz): 10 | 69 11 | 440 12 | ! Scale degree to consider as formal octave: 13 | 12 14 | ! Mapping: 15 | 0 16 | 1 17 | 2 18 | 3 19 | 4 20 | 5 21 | 6 22 | 7 23 | 8 24 | 9 25 | 10 26 | 11 27 | 28 | -------------------------------------------------------------------------------- /tests/data/mapping-whitekeys-from-59-a440.kbm: -------------------------------------------------------------------------------- 1 | ! 12EDO_WhiteKeys.kbm 2 | ! Size of map: 3 | 12 4 | ! MIDI range: 5 | 0 6 | 127 7 | ! Middle note: 8 | 59 9 | ! Reference note (MIDI number and frequency in Hz): 10 | 69 11 | 440 12 | ! Scale degree to consider as formal octave: 13 | 12 14 | ! Mapping: 15 | 0 16 | x 17 | 2 18 | x 19 | 4 20 | 5 21 | x 22 | 7 23 | x 24 | 9 25 | x 26 | 11 27 | 28 | -------------------------------------------------------------------------------- /tests/data/bad/blanknote.scl: -------------------------------------------------------------------------------- 1 | ! D:\Scala Batch\ED3-17.scl 2 | ! 3 | ED3-17 - Equal division of harmonic 3 into 17 parts 4 | 17 5 | ! 6 | 111.87971 7 | 223.75941 8 | 335.63912 9 | 447.51882 10 | 559.39853 11 | 671.27824 12 | 13 | 895.03765 14 | 1006.91735 15 | 1118.79706 16 | 1230.67677 17 | 1342.55647 18 | 1454.43618 19 | 1566.31588 20 | 1678.19559 21 | 1790.07529 22 | 3/1 23 | -------------------------------------------------------------------------------- /tests/data/ED3-17.scl: -------------------------------------------------------------------------------- 1 | ! D:\Scala Batch\ED3-17.scl 2 | ! 3 | ED3-17 - Equal division of harmonic 3 into 17 parts 4 | 17 5 | ! 6 | 111.87971 7 | 223.75941 8 | 335.63912 9 | 447.51882 10 | 559.39853 11 | 671.27824 12 | 783.15794 13 | 895.03765 14 | 1006.91735 15 | 1118.79706 16 | 1230.67677 17 | 1342.55647 18 | 1454.43618 19 | 1566.31588 20 | 1678.19559 21 | 1790.07529 22 | 3/1 23 | -------------------------------------------------------------------------------- /tests/data/ED4-17.scl: -------------------------------------------------------------------------------- 1 | ! D:\Scala Batch\ED4-17.scl 2 | ! 3 | ED4-17 - Equal division of harmonic 4 into 17 parts 4 | 17 5 | ! 6 | 141.17647 7 | 282.35294 8 | 423.52941 9 | 564.70588 10 | 705.88235 11 | 847.05882 12 | 988.23529 13 | 1129.41176 14 | 1270.58824 15 | 1411.76471 16 | 1552.94118 17 | 1694.11765 18 | 1835.29412 19 | 1976.47059 20 | 2117.64706 21 | 2258.82353 22 | 4/1 23 | -------------------------------------------------------------------------------- /tests/data/bad/badnote.scl: -------------------------------------------------------------------------------- 1 | ! D:\Scala Batch\ED3-17.scl 2 | ! 3 | ED3-17 - Equal division of harmonic 3 into 17 parts 4 | 17 5 | ! 6 | 111.87971 7 | 223.75941 8 | 335.63912 9 | 447.51882 10 | 559.39853 11 | 671.27824 12 | What is this 13 | 895.03765 14 | 1006.91735 15 | 1118.79706 16 | 1230.67677 17 | 1342.55647 18 | 1454.43618 19 | 1566.31588 20 | 1678.19559 21 | 1790.07529 22 | 3/1 23 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | --- 5 | Language: Cpp 6 | BasedOnStyle: LLVM 7 | IndentWidth: 4 8 | AlignAfterOpenBracket: Align 9 | BreakBeforeBraces: Allman 10 | ColumnLimit: 100 11 | SortIncludes: false 12 | --- 13 | Language: ObjC 14 | BasedOnStyle: LLVM 15 | IndentWidth: 4 16 | AlignAfterOpenBracket: Align 17 | BreakBeforeBraces: Allman 18 | ColumnLimit: 100 19 | SortIncludes: false 20 | --- 21 | 22 | -------------------------------------------------------------------------------- /tests/data/bad/empty-bad.kbm: -------------------------------------------------------------------------------- 1 | ! 61-277-61 Concert C#, Db.kbm 2 | ! 3 | ! Size of map: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 61 11 | ! Reference note for which frequency is given: 12 | 61 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 280.0 15 | ! Scale degree to consider as formal octave: 16 | -------------------------------------------------------------------------------- /tests/data/empty-note69.kbm: -------------------------------------------------------------------------------- 1 | ! 61-277-61 Concert C#, Db.kbm 2 | ! 3 | ! Size of map: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 452 15 | ! Scale degree to consider as formal octave: 16 | 0 17 | ! Mapping. 18 | -------------------------------------------------------------------------------- /tests/data/bad/extraline.scl: -------------------------------------------------------------------------------- 1 | ! D:\Scala Batch\ED3-17.scl 2 | ! 3 | ED3-17 - Equal division of harmonic 3 into 17 parts 4 | 17 5 | ! 6 | 111.87971 7 | 223.75941 8 | 335.63912 9 | 447.51882 10 | 559.39853 11 | 671.27824 12 | 783.15794 13 | 895.03765 14 | 1006.91735 15 | 1118.79706 16 | 1230.67677 17 | 1342.55647 18 | 1454.43618 19 | 1566.31588 20 | 1678.19559 21 | 1790.07529 22 | 3/1 23 | 24 | All this extra garbage after 17 notes is ignored by my parser 25 | -------------------------------------------------------------------------------- /tests/data/empty-note61.kbm: -------------------------------------------------------------------------------- 1 | ! 61-277-61 Concert C#, Db.kbm 2 | ! 3 | ! Size of map: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 61 11 | ! Reference note for which frequency is given: 12 | 61 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 280.0 15 | ! Scale degree to consider as formal octave: 16 | 0 17 | ! Mapping. 18 | -------------------------------------------------------------------------------- /tests/data/piano.kbm: -------------------------------------------------------------------------------- 1 | ! piano.kbm 2 | ! Key-for-key mapping for 88-key piano with A4=440 3 | ! Size: 4 | 0 5 | ! First MIDI note number to retune: 6 | 21 7 | ! Last MIDI note number to retune: 8 | 108 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 21 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave: 16 | 88 17 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/empty-c-100.kbm: -------------------------------------------------------------------------------- 1 | ! 61-277-61 Concert C#, Db.kbm 2 | ! 3 | ! Size of map: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 0 17 | ! Mapping. 18 | -------------------------------------------------------------------------------- /tests/data/bad/empty-extra.kbm: -------------------------------------------------------------------------------- 1 | ! 61-277-61 Concert C#, Db.kbm 2 | ! 3 | ! Size of map: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 61 11 | ! Reference note for which frequency is given: 12 | 61 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 280.0 15 | ! Scale degree to consider as formal octave: 16 | 0 17 | ! Mapping. 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/data/empty-note69-dosle.kbm: -------------------------------------------------------------------------------- 1 | ! 61-277-61 Concert C#, Db.kbm 2 | ! 3 | ! Size of map: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 452 15 | ! Scale degree to consider as formal octave: 16 | 0 17 | ! Mapping. 18 | -------------------------------------------------------------------------------- /tests/data/128.kbm: -------------------------------------------------------------------------------- 1 | ! 128.kbm 2 | ! Key-for-key mapping with Middle C on standard frequency. 3 | ! Size: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 0 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 261.6256 15 | ! Scale degree to consider as formal octave: 16 | 127 17 | 18 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/threenote-c-100.kbm: -------------------------------------------------------------------------------- 1 | ! Map a four note scale exactly 2 | ! 3 | ! Size of map: 4 | 3 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 4 17 | ! Mapping. 18 | 0 19 | 1 20 | 3 21 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/fournote-c-100.kbm: -------------------------------------------------------------------------------- 1 | ! Map a four note scale exactly 2 | ! 3 | ! Size of map: 4 | 4 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 4 17 | ! Mapping. 18 | 0 19 | 1 20 | 2 21 | 3 22 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/threenote-c-100-from-1.kbm: -------------------------------------------------------------------------------- 1 | ! Map a four note scale exactly 2 | ! 3 | ! Size of map: 4 | 3 5 | ! First MIDI note number to retune: 6 | 1 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 59 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 4 17 | ! Mapping. 18 | 0 19 | 1 20 | 3 21 | -------------------------------------------------------------------------------- /tests/data/marvel12.scl: -------------------------------------------------------------------------------- 1 | ! marvel12.scl 2 | Marvel[12] hobbit in 197-tET 3 | 12 4 | ! 5 | 115.73604 6 | 201.01523 7 | 316.75127 8 | 383.75635 9 | 499.49239 10 | 584.77157 11 | 700.50761 12 | 816.24365 13 | 931.97970 14 | 968.52792 15 | 1084.26396 16 | 2/1 17 | ! 18 | ! ! premarvel12.scl 19 | ! ! 20 | ! Premarvel[12] hobbit 5-limit transversal = diadie2 = pump9 21 | ! 12 22 | ! ! 23 | ! 16/15 24 | ! 9/8 25 | ! 6/5 26 | ! 5/4 27 | ! 4/3 28 | ! 45/32 29 | ! 3/2 30 | ! 8/5 31 | ! 128/75 32 | ! 225/128 33 | ! 15/8 34 | ! 2/1 35 | -------------------------------------------------------------------------------- /.github/workflows/code-checks.yml: -------------------------------------------------------------------------------- 1 | name: Format Check 2 | on: [pull_request] 3 | jobs: 4 | formatting-check: 5 | name: Clang Format Check 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | path: [ 'tests', 'include' ] 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v3 13 | 14 | - name: Run clang-format style check 15 | uses: surge-synthesizer/sst-githubactions/clang-format-check@main 16 | with: 17 | path: ${{ matrix.path }} 18 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/sixnote-c-100.kbm: -------------------------------------------------------------------------------- 1 | ! Map a four note scale exactly 2 | ! 3 | ! Size of map: 4 | 6 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 4 17 | ! Mapping. 18 | 0 19 | 1 20 | 3 21 | 4 22 | 5 23 | 7 24 | -------------------------------------------------------------------------------- /tests/symbolcheck2.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ** Make sure we don't have duplicated symbols by compiling a pair of objects 3 | ** and linking them 4 | */ 5 | 6 | #include 7 | #include 8 | #include "Tunings.h" 9 | 10 | double symbolcheck2() 11 | { 12 | auto k = Tunings::tuneNoteTo(60, 200); 13 | return k.tuningFrequency; 14 | } 15 | 16 | int main(int argc, char **argv) 17 | { 18 | extern double symbolcheck1(); 19 | std::cout << "100 and 200 are " << symbolcheck1() << " and " << symbolcheck2() << std::endl; 20 | } 21 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/eightnote-c-100.kbm: -------------------------------------------------------------------------------- 1 | ! Map a four note scale exactly 2 | ! 3 | ! Size of map: 4 | 8 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 4 17 | ! Mapping. 18 | 0 19 | 1 20 | 2 21 | 3 22 | 4 23 | 5 24 | 6 25 | 7 26 | -------------------------------------------------------------------------------- /scripts/release-notes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat <<- EOH 4 | # Automated build of surge-synth-team tuning library executables 5 | 6 | Look, the tuning library is a C++ library which you use inside other synths. 7 | Your most likely usage of it is as a developer implemeenting tuning. But we 8 | do make a binary of our command line exe and test exe available at the back 9 | of our pipeline. If that's something you'd like to download, you can do so here! 10 | 11 | EOH 12 | date 13 | echo "" 14 | echo "Most recent commits:" 15 | echo "" 16 | git log --pretty=oneline | head -5 17 | -------------------------------------------------------------------------------- /tests/data/kbm-fix-012023/twelve-c-100.kbm: -------------------------------------------------------------------------------- 1 | ! Map a four note scale exactly 2 | ! 3 | ! Size of map: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 100 15 | ! Scale degree to consider as formal octave: 16 | 4 17 | ! Mapping. 18 | 0 19 | 1 20 | 2 21 | 3 22 | 4 23 | 5 24 | 6 25 | 7 26 | 8 27 | 9 28 | 10 29 | 11 30 | 31 | -------------------------------------------------------------------------------- /tests/data/31edo_meantone.kbm: -------------------------------------------------------------------------------- 1 | ! 31edo_meantone.kbm 2 | ! 3 | ! Size of map: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 69 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.000000 15 | ! Scale degree to consider as formal octave: 16 | 31 17 | ! Mapping. 18 | 0 19 | 3 20 | 5 21 | 8 22 | 10 23 | 13 24 | 16 25 | 18 26 | 21 27 | 23 28 | 26 29 | 29 30 | -------------------------------------------------------------------------------- /tests/data/kbm-wrapping-7822/31edo2-subset-57.kbm: -------------------------------------------------------------------------------- 1 | ! foo.kbm 2 | ! 3 | ! Size of map: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 57 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 400 15 | ! Scale degree to consider as formal octave: 16 | 31 17 | ! Mapping. 18 | 0 19 | 3 20 | 5 21 | 8 22 | 10 23 | 13 24 | 16 25 | 18 26 | 21 27 | 23 28 | 26 29 | 28 30 | -------------------------------------------------------------------------------- /tests/data/kbm-wrapping-7822/31edo2-subset.kbm: -------------------------------------------------------------------------------- 1 | ! foo.kbm 2 | ! 3 | ! Size of map: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry in the mapping is mapped to: 10 | 69 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 400 15 | ! Scale degree to consider as formal octave: 16 | 31 17 | ! Mapping. 18 | 0 19 | 3 20 | 5 21 | 8 22 | 10 23 | 13 24 | 16 25 | 18 26 | 21 27 | 23 28 | 26 29 | 28 30 | -------------------------------------------------------------------------------- /tests/data/kbm-wrapping-7822/31edo2.scl: -------------------------------------------------------------------------------- 1 | ! foo.scl 2 | ! 3 | 4 | 31 5 | ! 6 | 38.70968 7 | 77.41935 8 | 116.12903 9 | 154.83871 10 | 193.54839 11 | 232.25806 12 | 270.96774 13 | 309.67742 14 | 348.38710 15 | 387.09677 16 | 425.80645 17 | 464.51613 18 | 503.22581 19 | 541.93548 20 | 580.64516 21 | 619.35484 22 | 658.06452 23 | 696.77419 24 | 735.48387 25 | 774.19355 26 | 812.90323 27 | 851.61290 28 | 890.32258 29 | 929.03226 30 | 967.74194 31 | 1006.45161 32 | 1045.16129 33 | 1083.87097 34 | 1122.58065 35 | 1161.29032 36 | 2/1 37 | -------------------------------------------------------------------------------- /tests/data/31edo.scl: -------------------------------------------------------------------------------- 1 | ! C:\Users\green\Documents\Music_Production\Scales\31edo.scl 2 | ! 3 | 31 equal divisions of octave 4 | 31 5 | ! 6 | 38.70968 7 | 77.41935 8 | 116.12903 9 | 154.83871 10 | 193.54839 11 | 232.25806 12 | 270.96774 13 | 309.67742 14 | 348.38710 15 | 387.09677 16 | 425.80645 17 | 464.51613 18 | 503.22581 19 | 541.93548 20 | 580.64516 21 | 619.35484 22 | 658.06452 23 | 696.77419 24 | 735.48387 25 | 774.19355 26 | 812.90323 27 | 851.61290 28 | 890.32258 29 | 929.03226 30 | 967.74194 31 | 1006.45161 32 | 1045.16129 33 | 1083.87097 34 | 1122.58065 35 | 1161.29032 36 | 2/1 37 | -------------------------------------------------------------------------------- /tests/data/31edo_dos_lineends.scl: -------------------------------------------------------------------------------- 1 | ! C:\Users\green\Documents\Music_Production\Scales\31edo.scl 2 | ! 3 | 31 equal divisions of octave 4 | 31 5 | ! 6 | 38.70968 7 | 77.41935 8 | 116.12903 9 | 154.83871 10 | 193.54839 11 | 232.25806 12 | 270.96774 13 | 309.67742 14 | 348.38710 15 | 387.09677 16 | 425.80645 17 | 464.51613 18 | 503.22581 19 | 541.93548 20 | 580.64516 21 | 619.35484 22 | 658.06452 23 | 696.77419 24 | 735.48387 25 | 774.19355 26 | 812.90323 27 | 851.61290 28 | 890.32258 29 | 929.03226 30 | 967.74194 31 | 1006.45161 32 | 1045.16129 33 | 1083.87097 34 | 1122.58065 35 | 1161.29032 36 | 2/1 37 | -------------------------------------------------------------------------------- /tests/data/liwung-tbn.kbm: -------------------------------------------------------------------------------- 1 | ! Size of map. The pattern repeats every so many keys: 2 | 5 3 | ! First MIDI note number to retune: 4 | 0 5 | ! Last MIDI note number to retune: 6 | 127 7 | ! Middle note where the first entry of the mapping is mapped to: 8 | 61 9 | ! Reference note for which frequency is given: 10 | 61 11 | ! Frequency to tune the above note to (floating point e.g. 440.0): 12 | 295.0290 13 | ! Scale degree to consider as formal octave (determines difference in pitch 14 | ! between adjacent mapping patterns): 15 | 5 16 | ! Mapping. 17 | ! The numbers represent scale degrees mapped to keys. The first entry is for 18 | ! the given middle note, the next for subsequent higher keys. 19 | 0 20 | 1 21 | 2 22 | 3 23 | 4 24 | -------------------------------------------------------------------------------- /tests/data/zeus22.scl: -------------------------------------------------------------------------------- 1 | ! zeus22.scl 2 | Zeus[22] hobbit (121/120&176/175) in POTE tuning 3 | 22 4 | ! 5 | 47.21796 6 | 109.87010 7 | 157.08806 8 | 230.88883 9 | 266.95816 10 | 314.17612 11 | 387.97689 12 | 424.04622 13 | 497.84699 14 | 545.06495 15 | 592.28291 16 | 654.93505 17 | 702.15301 18 | 775.95378 19 | 812.02311 20 | 885.82388 21 | 933.04184 22 | 969.11117 23 | 1042.91194 24 | 1090.12990 25 | 1152.78204 26 | 2/1 27 | ! 28 | !! prezeus22.scl 29 | ! Zeus[22] transversal 30 | ! 22 31 | !! 32 | ! 33/32 33 | ! 16/15 34 | ! 11/10 35 | ! 8/7 36 | ! 64/55 37 | ! 77/64 38 | ! 5/4 39 | ! 14/11 40 | ! 4/3 41 | ! 11/8 42 | ! 45/32 43 | ! 16/11 44 | ! 3/2 45 | ! 11/7 46 | ! 8/5 47 | ! 5/3 48 | ! 55/32 49 | ! 7/4 50 | ! 11/6 51 | ! 15/8 52 | ! 64/33 53 | ! 2/1 54 | -------------------------------------------------------------------------------- /tests/data/rast.kbm: -------------------------------------------------------------------------------- 1 | ! Size of map. The pattern repeats every so many keys: 2 | 12 3 | ! First MIDI note number to retune: 4 | 0 5 | ! Last MIDI note number to retune: 6 | 127 7 | ! Middle note where the first entry of the mapping is mapped to: 8 | 60 9 | ! Reference note for which frequency is given: 10 | 60 11 | ! Frequency to tune the above note to (floating point e.g. 440.0): 12 | 261.6256 13 | ! Scale degree to consider as formal octave (determines difference in pitch 14 | ! between adjacent mapping patterns): 15 | 12 16 | ! Mapping. 17 | ! The numbers represent scale degrees mapped to keys. The first entry is for 18 | ! the given middle note, the next for subsequent higher keys. 19 | 0 20 | 1 21 | 2 22 | 3 23 | 4 24 | 5 25 | 6 26 | 7 27 | 8 28 | 9 29 | 10 30 | 11 31 | -------------------------------------------------------------------------------- /tests/data/maqamat.kbm: -------------------------------------------------------------------------------- 1 | ! Size of map. The pattern repeats every so many keys: 2 | 16 3 | ! First MIDI note number to retune: 4 | 0 5 | ! Last MIDI note number to retune: 6 | 127 7 | ! Middle note where the first entry of the mapping is mapped to: 8 | 60 9 | ! Reference note for which frequency is given: 10 | 60 11 | ! Frequency to tune the above note to (floating point e.g. 440.0): 12 | 261.6256 13 | ! Scale degree to consider as formal octave (determines difference in pitch 14 | ! between adjacent mapping patterns): 15 | 16 16 | ! Mapping. 17 | ! The numbers represent scale degrees mapped to keys. The first entry is for 18 | ! the given middle note, the next for subsequent higher keys. 19 | 0 20 | 1 21 | 2 22 | 3 23 | 4 24 | 5 25 | 6 26 | 7 27 | 8 28 | 9 29 | 10 30 | 11 31 | 12 32 | 13 33 | 14 34 | 15 35 | -------------------------------------------------------------------------------- /tests/data/mapping-n60-250.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 250.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 0 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | -------------------------------------------------------------------------------- /tests/data/liwung-tbn.ascl: -------------------------------------------------------------------------------- 1 | ! Liwung-TBN.ascl 2 | ! 3 | The pelog Liwung scale has five notes taken from the seven-tone Pelog scale. This version is based on the tuning of a to-be-named gamelan tuned in a Javanese style. This scale is referred to as Pelog Da = Galimer with the pitch order: S-G-U-L-T. If using a MIDI piano controller, we recommend setting "Black Keys Only" in the Track Tuning MIDI Controller Layout. 4 | 5 5 | ! 6 | 140.00000 cents ! Galimer 7 | 559.00000 cents ! Bungur 8 | 704.00000 cents ! Loloran 9 | 814.00000 cents ! Tugu 10 | 2/1 ! Singgul 11 | ! S=Singgul, G=Galimer, B=Bungur, L=Loloran, and T=Tugu 12 | ! @ABL REFERENCE_PITCH 3 0 295.029 13 | ! @ABL NOTE_RANGE_BY_INDEX -9 0 14 | ! @ABL NOTE_NAMES "S" "G" "U" "L" "T" 15 | ! @ABL LINK https://www.ableton.com/learn-more/tuning-systems/liwung-tbn -------------------------------------------------------------------------------- /tests/data/mapping-note54-to-259-6.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 54 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 259.6 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 0 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | -------------------------------------------------------------------------------- /tests/data/mapping-note53-to-430-408.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 0 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 53 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 430.408 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 0 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | -------------------------------------------------------------------------------- /tests/data/mapping-n60-fifths.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 2 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 250.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 7 24 | -------------------------------------------------------------------------------- /tests/data/mapping-whitekeysalt-from-59-a440.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | 12 4 | ! First MIDI note number to retune: 5 | 0 6 | ! Last MIDI note number to retune: 7 | 127 8 | ! Middle note where the first entry of the mapping is mapped to: 9 | 59 10 | ! Reference note for which frequency is given: 11 | 69 12 | ! Frequency to tune the above note to (floating point e.g. 440.0): 13 | 440.0 14 | ! Scale degree to consider as formal octave (determines difference in pitch 15 | ! between adjacent mapping patterns): 16 | 12 17 | ! Mapping. 18 | ! The numbers represent scale degrees mapped to keys. The first entry is for 19 | ! the given middle note, the next for subsequent higher keys. 20 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 21 | 0 22 | x 23 | 1 24 | x 25 | 2 26 | 3 27 | x 28 | 4 29 | x 30 | 5 31 | x 32 | 6 33 | -------------------------------------------------------------------------------- /tests/data/bad/missing-note.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 1 24 | 2 25 | 3 26 | 4 27 | 5 28 | 6 29 | 8 30 | 9 31 | 10 32 | 11 33 | -------------------------------------------------------------------------------- /tests/data/bad/blank-line.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 1 24 | 2 25 | 3 26 | 4 27 | 28 | 6 29 | 7 30 | 8 31 | 9 32 | 10 33 | 11 34 | -------------------------------------------------------------------------------- /tests/data/mapping-a440-constant.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 1 24 | 2 25 | 3 26 | 4 27 | 5 28 | 6 29 | 7 30 | 8 31 | 9 32 | 10 33 | 11 -------------------------------------------------------------------------------- /tests/data/bad/garbage-key.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 1 24 | 2 25 | 3 26 | garbage 27 | 5 28 | 6 29 | 7 30 | 8 31 | 9 32 | 10 33 | 11 34 | -------------------------------------------------------------------------------- /tests/data/mapping-whitekeys-a440.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 7 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | x 24 | 1 25 | x 26 | 2 27 | 3 28 | x 29 | 4 30 | x 31 | 5 32 | x 33 | 6 34 | -------------------------------------------------------------------------------- /tests/data/shuffle-a440-constant.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 2 24 | 1 25 | 3 26 | 4 27 | 6 28 | 5 29 | 7 30 | 8 31 | 9 32 | 11 33 | 10 34 | -------------------------------------------------------------------------------- /tests/data/31-edo.kbm: -------------------------------------------------------------------------------- 1 | ! Size of map. The pattern repeats every so many keys: 2 | 31 3 | ! First MIDI note number to retune: 4 | 0 5 | ! Last MIDI note number to retune: 6 | 127 7 | ! Middle note where the first entry of the mapping is mapped to: 8 | 60 9 | ! Reference note for which frequency is given: 10 | 83 11 | ! Frequency to tune the above note to (floating point e.g. 440.0): 12 | 440.0000 13 | ! Scale degree to consider as formal octave (determines difference in pitch 14 | ! between adjacent mapping patterns): 15 | 31 16 | ! Mapping. 17 | ! The numbers represent scale degrees mapped to keys. The first entry is for 18 | ! the given middle note, the next for subsequent higher keys. 19 | 0 20 | 1 21 | 2 22 | 3 23 | 4 24 | 5 25 | 6 26 | 7 27 | 8 28 | 9 29 | 10 30 | 11 31 | 12 32 | 13 33 | 14 34 | 15 35 | 16 36 | 17 37 | 18 38 | 19 39 | 20 40 | 21 41 | 22 42 | 23 43 | 24 44 | 25 45 | 26 46 | 27 47 | 28 48 | 29 49 | 30 50 | -------------------------------------------------------------------------------- /tests/data/mapping-whitekeys-c261.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 60 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 261.625565280 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | x 24 | 1 25 | x 26 | 2 27 | 3 28 | x 29 | 4 30 | x 31 | 5 32 | x 33 | 6 34 | -------------------------------------------------------------------------------- /tests/data/mapping-whitekeys-from-48-a440.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 48 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 7 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | x 24 | 1 25 | x 26 | 2 27 | 3 28 | x 29 | 4 30 | x 31 | 5 32 | x 33 | 6 34 | -------------------------------------------------------------------------------- /tests/data/bad/extraline-long.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to: 10 | 60 11 | ! Reference note for which frequency is given: 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 440.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 12 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 1 24 | 2 25 | 3 26 | 4 27 | 5 28 | 6 29 | 7 30 | 8 31 | 9 32 | 10 33 | 11 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/data/mapping-a442-7-to-12.kbm: -------------------------------------------------------------------------------- 1 | ! Template for a keyboard mapping 2 | ! 3 | ! Size of map. The pattern repeats every so many keys: 4 | 12 5 | ! First MIDI note number to retune: 6 | 0 7 | ! Last MIDI note number to retune: 8 | 127 9 | ! Middle note where the first entry of the mapping is mapped to. Tune on b 10 | 59 11 | ! Reference note for which frequency is given. g# is 442 here 12 | 69 13 | ! Frequency to tune the above note to (floating point e.g. 440.0): 14 | 442.0 15 | ! Scale degree to consider as formal octave (determines difference in pitch 16 | ! between adjacent mapping patterns): 17 | 7 18 | ! Mapping. 19 | ! The numbers represent scale degrees mapped to keys. The first entry is for 20 | ! the given middle note, the next for subsequent higher keys. 21 | ! For an unmapped key, put in an "x". At the end, unmapped keys may be left out. 22 | 0 23 | 1 24 | x 25 | 2 26 | x 27 | 3 28 | 4 29 | x 30 | 5 31 | x 32 | 6 33 | x 34 | -------------------------------------------------------------------------------- /.github/workflows/build-pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | on: 3 | pull_request: 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | build_plugin: 11 | name: Test - ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | include: 16 | - os: windows-latest 17 | - os: macos-latest 18 | - os: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install Locales on Linux 26 | if: runner.os == 'Linux' 27 | run: | 28 | sudo locale-gen es_ES 29 | sudo locale-gen fr_FR 30 | sudo locale-gen ja_JP 31 | sudo locale-gen zh_CN 32 | sudo update-locale 33 | 34 | - name: Build pull request version 35 | run: | 36 | cmake -S . -B ./build -DCMAKE_BUILD_TYPE=Release 37 | cmake --build ./build --config Debug --target run-all-tests 38 | 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020, Paul Walker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | 10 | -------------------------------------------------------------------------------- /commands/parsecheck.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Tunings.h" 4 | #include "TuningsImpl.h" 5 | 6 | int main(int argc, char **argv) 7 | { 8 | for (int i = 1; i < argc; ++i) 9 | { 10 | std::cout << std::setw(50) << argv[i]; 11 | try 12 | { 13 | if (strstr(argv[i], ".scl")) 14 | { 15 | Tunings::readSCLFile(argv[i]); 16 | std::cout << " PASSED"; 17 | } 18 | else if (strstr(argv[i], ".kbm")) 19 | { 20 | Tunings::readKBMFile(argv[i]); 21 | std::cout << " PASSED"; 22 | } 23 | else if (strstr(argv[i], ".ascl")) 24 | { 25 | Tunings::readASCLFile(argv[i]); 26 | std::cout << " PASSED"; 27 | } 28 | else 29 | { 30 | std::cout << " SKIPPED"; 31 | } 32 | } 33 | catch (Tunings::TuningError &t) 34 | { 35 | std::cout << " FAILED : " << t.what(); 36 | } 37 | std::cout << std::endl; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/data/31-edo.ascl: -------------------------------------------------------------------------------- 1 | ! 31-EDO.ascl 2 | ! 3 | 31 equal divisions of an octave. 31 proportionally equal and equal sounding semitone intervals per octave. 4 | ! 5 | ! default tuning: degree 23 (890.3225806 cents) 440 Hz, or degree 0 = 263.0921203 Hz 6 | ! 7 | 31 8 | ! 9 | 38.70967742 ! >C 10 | 77.41935484 ! C♯ 11 | 116.1290323 ! D♭ 12 | 154.8387097 ! D 15 | 270.9677419 ! D♯ 16 | 309.6774194 ! E♭ 17 | 348.3870968 ! F 23 | 580.6451613 ! F♯ 24 | 619.3548387 ! G♭ 25 | 658.0645161 ! G 28 | 774.1935484 ! G♯ 29 | 812.9032258 ! A♭ 30 | 851.6129032 ! A 33 | 967.7419355 ! A♯ 34 | 1006.451613 ! B♭ 35 | 1045.16129 ! C" "C♯" "D♭" "D" "D♯" "E♭" "F" "F♯" "G♭" "G" "G♯" "A♭" "A" "A♯" "B♭" " 43 | COMMAND $ 44 | ) 45 | else() 46 | add_custom_target(run-all-tests 47 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 48 | COMMAND $ 49 | COMMAND $ 50 | COMMAND LANG=es_ES $ 51 | COMMAND LANG=fr_FR $ 52 | COMMAND LANG=zh_CN $ 53 | COMMAND LANG= $ 54 | ) 55 | endif() 56 | add_dependencies(run-all-tests tuning-library-tests tuning-library-symbolcheck) 57 | endif() 58 | -------------------------------------------------------------------------------- /commands/showmapping.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Tunings.h" 4 | 5 | using namespace Tunings; 6 | 7 | int main(int argc, char **argv) 8 | { 9 | if (argc == 1) 10 | { 11 | std::cout 12 | << "Usage: " << argv[0] << " scl-file/ascl-file [kbm-file]\n\n" 13 | << "Will show the frequency mapping across the midi keyboard for the scl/kbm combo" 14 | << std::endl; 15 | exit(1); 16 | } 17 | 18 | try 19 | { 20 | auto t = Tuning(); 21 | if (strstr(argv[1], ".ascl")) 22 | { 23 | auto s = readASCLFile(argv[1]); 24 | t = Tuning(s); 25 | } 26 | else 27 | { 28 | auto s = readSCLFile(argv[1]); 29 | KeyboardMapping k; 30 | 31 | if (argc == 3) 32 | { 33 | k = readKBMFile(argv[2]); 34 | } 35 | 36 | t = Tuning(s, k); 37 | } 38 | 39 | std::cout << std::setw(4) << "Note," << std::setw(18) << "Freq (Hz)," << std::setw(18) 40 | << "ScaledFrq," << std::setw(18) << "logScaled," << std::setw(6) << "Pos," 41 | << " Name" << std::endl; 42 | 43 | for (int i = 0; i < 128; ++i) 44 | { 45 | if (t.isMidiNoteMapped(i)) 46 | { 47 | std::cout << std::setw(4) << i << ", " << std::setw(16) << std::setprecision(10) 48 | << std::fixed << t.frequencyForMidiNote(i) << ", " << std::setw(16) 49 | << std::setprecision(10) << std::fixed 50 | << t.frequencyForMidiNoteScaledByMidi0(i) << ", " << std::setw(16) 51 | << std::setprecision(10) << std::fixed 52 | << t.logScaledFrequencyForMidiNote(i) << ", " << std::setw(4) 53 | << t.scalePositionForMidiNote(i) << ", " 54 | << (t.notationMapping.count 55 | ? t.noteNameForScalePosition(t.scalePositionForMidiNote(i)) 56 | : "N/A") 57 | << std::endl; 58 | } 59 | else 60 | { 61 | std::cout << std::setw(4) << i << " [unmapped]" << std::endl; 62 | } 63 | } 64 | } 65 | catch (const Tunings::TuningError &e) 66 | { 67 | std::cout << "Tuning threw an exception: " << e.what() << std::endl; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/data/maqamat.ascl: -------------------------------------------------------------------------------- 1 | ! All Maqamat 24EDO.ascl 2 | ! 3 | This pitch set enables 24-EDO versions of nearly all possible maqamat from a given tonic key (in this case C, but transposable to any pitch), including those of the Rast family as well as Bayati/Saba/Kurd/Hijaz and maqamat of those families. D1/2♭, E1/2♭, A1/2♭, and B1/2♭ are added to the standard 12 pitches. Provided by Sami Abu Shumays. Visit the website to learn more. 4 | ! 5 | ! Jins modulations possible within this pitch set: 6 | ! Rast C: C D E1/2♭ F G 7 | ! Nahawand C: C D E♭ F G 8 | ! Nikriz C: C D E♭ F♯ G 9 | ! Sikah E1/2♭: (D♯) E1/2♭ F G A♭ 10 | ! Nahawand G: G A B♭ C 11 | ! Upper Rast G: G A B1/2♭ C 12 | ! Hijaz G: G A♭ B C D E♭ 13 | ! Bayati G: G A1/2♭ B♭ C D E♭ 14 | ! Saba G: G A1/2♭ B♭ C♭ D E♭ 15 | ! Kurd G: G A♭ B♭ C D E♭ 16 | ! Hijazkar G: E♭ F♯ G A♭ B♮ 17 | ! Saba Dalanshin A: A B1/2♭ C D♭ E♮ F 18 | ! Jiharkah C: B1/2♭ C D E♮ 19 | ! Hijaz C: C D♭ E F (G) 20 | ! Bayati C: C D1/2♭ E♭ F (G) 21 | ! Hijaz F: F G♭ A♮ B♭ 22 | ! (Maqam) Saba C: C D1/2♭ E♭ F♭(E♮) G A♭ B♭ C♭ D♮ E♭ 23 | ! Kurd D: C D♭ E♭ F (G) 24 | ! Nahawand F: (E♮) F G A♭ B♭ C 25 | ! Nikriz F: F G A♭ B♮ C 26 | ! Rast F: D♮ E1/2♭ F G A1/2♭ B♭ C 27 | ! Sikah A1/2♭: (G♯) A1/2♭ B♭ C D♭ 28 | ! Ajam F: F G A B♭ C 29 | ! (Maqam) Zanjaran C: C D♭ E F G A B♭ C 30 | ! Hijazkar C: A♭ B♮ C D♭ E♮ 31 | ! Hijaz Murassaa C: C D♭ E F G♭ 32 | ! Sikah Baladi C: A1/2♭ B1/2♭ C D1/2♭ E1/2♭ 33 | ! 34 | ! References: 35 | ! Inside Arabic Music, Chapter 11 (description of Tuning System); Ch 14-16 (descriptions of Ajnas); Ch 24 (Sayr diagrams) 36 | ! http://maqamworld.com/en/index.php * Maqam World Website, including Maqam and Jins Scales 37 | ! https://www.youtube.com/user/abushumays * "Maqam Lessons" YouTube series, teaching the melodic vocabulary of the ajnas and maqamat 38 | ! 39 | 16 40 | ! 41 | 100. ! D♭/C♯ for Saba Dalanshin A 42 | 150. ! D1/2♭ for Bayati/Saba C 43 | 200. ! D 44 | 300. ! E♭/D♯ 45 | 350. ! E1/2♭ for Rast C and Rast F 46 | 400. ! E♮ 47 | 500. ! F 48 | 600. ! F♯ 49 | 700. ! G 50 | 800. ! A♭ 51 | 850. ! A1/2♭ for Bayati G/Saba G and Rast F 52 | 900. ! A 53 | 1000. ! B♭ 54 | 1050. ! B1/2♭ for Rast C 55 | 1100. ! B♮/C♭ 56 | 1200. 57 | ! 58 | ! @ABL NOTE_NAMES C D♭/C♯ D1/2♭ D E♭/D♯ E1/2♭ E♮ F F♯ G A♭ A1/2♭ A B♭ B1/2♭ B♮/C♭ 59 | ! @ABL REFERENCE_PITCH 3 0 261.6256 60 | ! @ABL NOTE_RANGE_BY_INDEX -1 4 7 3 61 | ! @ABL SOURCE Inside Arabic Music, Chapter 11 (description of Tuning System); Ch 14-16 (descriptions of Ajnas); Ch 24 (Sayr diagrams) 62 | ! @ABL LINK https://www.ableton.com/learn-more/tuning-systems/all-maqamat-24edo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Surge Synth Team Tuning Library 2 | 3 | In [Surge](https://surge-synthesizer.github.io), we added microtuning 4 | and spent a lot of time making sure our 5 | SCL/KBM implementation was properly calibrated and available for C++ 6 | programs. We then added that same implementation to [dexed](https://asb2m10.github.io/dexed/) 7 | through a copy. 8 | 9 | But we realized we could make the functions available as standalone C++ header 10 | only library and get three benefits. 11 | 12 | 1. Share more code between Surge and our Dexed fork 13 | 2. Make the code available to other soft synths where we or others may add microtuning like our tuning workbench synth 14 | 3. Have a set of standalone command line utilities and well documented tests 15 | 16 | So we took the code and re-factored it here under an MIT license. 17 | 18 | Although Surge and Dexed are GPL3, the copyright holders and authors of the original 19 | Surge microtuning implementation (and only that implementation) were happy to relicense. 20 | 21 | ## Using the library in your C++ project 22 | 23 | The C++ library is a standalone header only C++-11 library. There are a variety of ways 24 | to use it in your project but our approach is generally: 25 | 26 | 1. Make this github repo a submodule of your project 27 | 2. Add the "include/" directory to your compiler include path 28 | 3. `#include "Tunings.h"` 29 | 30 | If you use cmake you can also do 31 | 32 | ```cmake 33 | add_subdirectory(this/library EXCLUDE_FROM_ALL) 34 | ``` 35 | 36 | and then add the target `tuning-library` as a library target to your project. 37 | 38 | The code is organized such that Tunings.h is the API and TuningsImpl.h is the header with more 39 | involved implementation bodies. Tunings.h includes TuningsImpl.h automatically. 40 | 41 | ## Building the command line tools and test suite 42 | 43 | ```shell 44 | cmake -Bbuild 45 | cmake --build build --target run-all-tests 46 | cmake --build build --target showmapping 47 | cmake --build build --target parsecheck 48 | ``` 49 | 50 | ## Using the showmapping command 51 | 52 | `showmapping` takes one or two arguments. It either takes an .scl file, in which 53 | case it dumps the frequency table for that .scl file with midi note 60 being the 54 | scale start tuned to 261hz, or it takes an .scl and .kbm file, in which case it 55 | prints the entire internal tuning table for the combination. 56 | 57 | ## Using the parsecheck command 58 | 59 | `parsecheck` takes a list of files and tries to parse them showing errors if it 60 | fails. `parsecheck test/data/*scl` will test that all those SCL parse. 61 | 62 | ## Related Projects 63 | 64 | Github user @chinenual has ported this library to `go` here: https://github.com/chinenual/go-scala/ 65 | 66 | Python bindings for this library are available 67 | [here](https://github.com/surge-synthesizer/tuning-library-python). 68 | 69 | ## Bugs, Problems, etc 70 | 71 | If you find bugs, please open a github issue and we will fix it right away! 72 | 73 | If you have a non-bug problem, you can do the same or you can hop on the slack as 74 | detailed at https://surge-synth-team.org/ 75 | 76 | If you would like to expand our test cases, we are always thrilled for you to do 77 | so. Drop in a pull request. 78 | 79 | If you choose to use the software in your synth, you can go right ahead of course. 80 | That's the point of the MIT license! But if you want to let us know, again pop open 81 | a github or drop in our slack. Always glad to hear from you. 82 | 83 | Enjoy! 84 | -------------------------------------------------------------------------------- /tests/data/rast6.ascl: -------------------------------------------------------------------------------- 1 | ! Rast 6 - modulations + variants.ascl 2 | ! 3 | ! Short description: 4 | A version of Maqam Rast with a wide range of choices of pitch variants. Because of the inclusion of versions of E♭, E1/2♭, E♮, A♭, A1/2♭, A♮, B♭, B1/2♭, and B♮, all of the common modulations within the Maqam Rast family are possible. This version of Maqam Rast was developed by Sami Abu Shumays. Visit the website to learn more. 5 | ! 6 | ! Long description: This represents a version of Maqam Rast with a wide range of choices of pitch variants. Four potential choices for an E1/2♭ (the 3rd scale degree of Rast) are given, ranging from a relatively low version typical of later-20th-century Egypt, to a relatively high version typical of mid-20th century Syria, and two choices in between. Two possible choices for B1/2♭ (the 7th scale degree of Rast) are included. It is typical in maqam Rast to use a B1/2♭ relatively higher than the E1/2♭. Two choices of B♭ are given, because of the preference for a very low B♭ in Jins Nahawand on G, lower than the Pythagorean B♭ used for Bayati and Saba G. In addition, because of the inclusion of versions of E♭, E1/2♭, E♮, A♭, A1/2♭, A♮, B♭, B1/2♭, and B♮, all of the common modulations within the Maqam Rast family are possible. This version of Maqam Rast was developed by Sami Abu Shumays. Visit the website to learn more. 7 | ! 8 | ! Jins modulations possible within this pitch set: 9 | ! Rast C: C D E1/2♭ (pick one) F G 10 | ! Nahawand C: C D E♭ F G 11 | ! Nikriz C: C D E♭ F♯ G 12 | ! Sikah E1/2♭: (D♯) E1/2♭ F G 13 | ! Nahawand G: G A B♭ (lower) C 14 | ! Upper Rast G: G A B1/2♭ (pick one relatively higher than the E1/2♭ choice) C 15 | ! Hijaz G: G A♭ B♮ C D E♭ 16 | ! Bayati G: G A1/2♭ B♭ C D E♭ 17 | ! Saba G: G A1/2♭ B♭ C♭ D E♭ 18 | ! Hijazkar G: E♭ F♯ G A♭ B♮ 19 | ! Saba Dalanshin A: A B1/2♭ C D♭ E♮ F 20 | ! Jiharkah C: B1/2♭ C D E♮ F 21 | ! 22 | ! References: 23 | ! Inside Arabic Music, Chapter 11 (description of Tuning System); Ch 14-16 (descriptions of Ajnas); Ch 24 (Sayr diagrams) 24 | ! http://maqamworld.com/en/index.php * Maqam World Website, including Maqam and Jins Scales 25 | ! https://www.youtube.com/user/abushumays * "Maqam Lessons" YouTube series, teaching the melodic vocabulary of the ajnas and maqamat 26 | ! 27 | 20 28 | ! 29 | 128. ! D♭ for Saba Dalanshin A 30 | 204. ! D Pythagorean 31 | 291. ! E♭ super low for Nahawand C, could also pass for D♯ in a pinch to tonicize E1/2♭ 32 | 342. ! E1/2♭-a- On the low end, Egyptian 1960s - reference composition "YamSaharni" by Sayyed Mekkawi for Umm Kulthum 33 | 347. ! E1/2♭-b- On the medium-low side 34 | 351. ! E1/2♭-c- On the medium side, Egyptian 1940s - reference composition "Ghannili Shwayya" by Zakaria Ahmad for Umm Kulthum 35 | 356. ! E1/2♭-d- On the high-ish side, for "Ya Maal ish-Shaam" and "Ya Shadi il-alhan" 36 | 382. ! E suitable for Jiharkah C and Hijaz C (Saba Dalanshin A) 37 | 498. ! F Pythagorean 38 | 590. ! F♯ for Nikriz C 39 | 702. ! G Pythagorean 40 | 825. ! A♭ for Hijaz G 41 | 840. ! A1/2♭ For Bayati G/Saba G 42 | 911. ! A higher than Pythagorean 43 | 990. ! B♭-a- Lower than Pythagorean, great for Nahawand G 44 | 996. ! B♭-b- Pythagorean 45 | 1056. ! B1/2♭-a- relatively higher than the E1/2♭ 46 | 1061. ! B1/2♭-b- relatively higher than the E1/2♭ 47 | 1092. ! B♮/C♭ lowish for Hijaz G as well as C♭ for Saba G 48 | 1200. 49 | ! 50 | ! @ABL NOTE_NAMES "C " "D♭ " "D " "E♭ " "E1/2♭-a- " "E1/2♭-b- " "E1/2♭-c- " "E1/2♭-d- " "E " "F " "F♯ " "G " "A♭ " "A1/2♭ " "A " "B♭-a- " "B♭-b- " "B1/2♭-a- " "B1/2♭-b- " "B♮/C♭ " 51 | ! @ABL REFERENCE_PITCH 4 0 261.6256 52 | ! @ABL NOTE_RANGE_BY_INDEX 0 0 6 7 53 | ! @ABL SOURCE Inside Arabic Music, Chapter 11 (description of Tuning System); Ch 14-16 (descriptions of Ajnas); Ch 24 (Sayr diagrams of Maqam Rast and Maqam Suznak) 54 | ! @ABL LINK https://www.ableton.com/learn-more/tuning-systems/rast-6 -------------------------------------------------------------------------------- /include/Tunings.h: -------------------------------------------------------------------------------- 1 | // -*-c++-*- 2 | 3 | /** 4 | * Tunings.h 5 | * Copyright Paul Walker, 2019-2020 6 | * Released under the MIT License. See LICENSE.md 7 | * 8 | * Tunings.h contains the public API required to determine full keyboard frequency maps 9 | * for a scala SCL and KBM file in standalone, tested, open licensed C++ header only library. 10 | * 11 | * An example of using the API is 12 | * 13 | * ``` 14 | * auto s = Tunings::readSCLFile( "./my-scale.scl" ); 15 | * auto k = Tunings::readKBMFile( "./my-mapping.kbm" ); 16 | * 17 | * Tunings::Tuning t( s, k ); 18 | * 19 | * std::cout << "The frequency of C4 and A4 are " 20 | * << t.frequencyForMidiNote( 60 ) << " and " 21 | * << t.frequencyForMidiNote( 69 ) << std::endl; 22 | * ``` 23 | * 24 | * The API provides several other points, such as access to the structure of the SCL and KBM, 25 | * the ability to create several prototype SCL and KBM files wthout SCL or KBM content, 26 | * a frequency measure which is normalized by the frequency of standard tuning midi note 0 27 | * and the logarithmic frequency scale, with a doubling per frequency doubling. 28 | * 29 | * Documentation is in the class header below; tests are in `tests/all_tests.cpp` and 30 | * a variety of command line tools accompany the header. 31 | */ 32 | 33 | #ifndef __INCLUDE_TUNINGS_H 34 | #define __INCLUDE_TUNINGS_H 35 | 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | static_assert(__cplusplus >= 202002L, "Surge team libraries have moved to C++ 20"); 43 | 44 | namespace Tunings 45 | { 46 | static constexpr double MIDI_0_FREQ = 8.17579891564371; // or 440.0 * pow( 2.0, - ( 69.0/12.0 ) ) 47 | 48 | /** 49 | * A Tone is a single entry in an SCL file. It is expressed either in cents or in 50 | * a ratio, as described in the SCL documentation. 51 | * 52 | * In most normal use, you will not use this class, and it will be internal to a Scale 53 | */ 54 | struct Tone 55 | { 56 | typedef enum Type 57 | { 58 | kToneCents, // An SCL representation like "133.0" 59 | kToneRatio // An SCL representation like "3/7" 60 | } Type; 61 | 62 | Type type; 63 | double cents; 64 | int64_t ratio_d, ratio_n; 65 | std::string stringRep; 66 | double floatValue; // cents / 1200 + 1. 67 | 68 | int lineno; // which line of the SCL does this tone appear on? 69 | 70 | Tone() : type(kToneRatio), cents(0), ratio_d(1), ratio_n(1), stringRep("1/1"), floatValue(1.0) 71 | { 72 | } 73 | }; 74 | 75 | /** 76 | * Given an SCL string like "100.231" or "3/7" set up a Tone 77 | */ 78 | inline Tone toneFromString(const std::string &t, int lineno = -1); 79 | 80 | /** 81 | * The Scale is the representation of the SCL file. It contains several key 82 | * features. Most importantly it has a count and a vector of Tones. 83 | * 84 | * In most normal use, you will simply pass around instances of this class 85 | * to a Tunings::Tuning instance, but in some cases you may want to create 86 | * or inspect this class yourself. Especially if you are displaying this 87 | * class to your end users, you may want to use the rawText or count methods. 88 | */ 89 | struct Scale 90 | { 91 | std::string name; // The name in the SCL file. Informational only 92 | std::string description; // The description in the SCL file. Informational only 93 | std::string rawText; // The raw text of the SCL file used to create this Scale 94 | int count; // The number of tones 95 | std::vector tones; // The tones 96 | std::vector comments; // The comments 97 | 98 | Scale() : name("empty scale"), description(""), rawText(""), count(0) {} 99 | }; 100 | 101 | /** 102 | * The KeyboardMapping class represents a KBM file. In most cases, the salient 103 | * features are the tuningConstantNote and tuningFrequency, which allow you to 104 | * pick a fixed note in the midi keyboard when retuning. The KBM file can also 105 | * remap individual keys to individual points in a scale, which kere is done with the 106 | * keys vector. 107 | * 108 | * Just as with Scale, the rawText member contains the text of the KBM file used. 109 | */ 110 | struct KeyboardMapping 111 | { 112 | int count; 113 | int firstMidi, lastMidi; 114 | int middleNote; 115 | int tuningConstantNote; 116 | double tuningFrequency, tuningPitch; // pitch = frequency / MIDI_0_FREQ 117 | int tuningOctave; // octave of the tuning reference, only used in Tuning::midiNoteForNoteName() 118 | int octaveDegrees; 119 | std::vector keys; // rather than an 'x' we use a '-1' for skipped keys 120 | 121 | std::string rawText; 122 | std::string name; 123 | 124 | KeyboardMapping(); 125 | }; 126 | 127 | /** 128 | * The NotationMapping class represents the list of note names corresponding 129 | * to the scale tones. 130 | */ 131 | struct NotationMapping 132 | { 133 | int count; 134 | std::vector names; // organized in the same order as Scale::tones 135 | 136 | NotationMapping() : count(0) {} 137 | }; 138 | 139 | /** 140 | * The AbletonScale class represents Ableton's ASCL extension to the SCL file. 141 | * 142 | * @see https://help.ableton.com/hc/en-us/articles/10998372840220-ASCL-Specification 143 | */ 144 | struct AbletonScale 145 | { 146 | Scale scale; 147 | int referencePitchOctave; 148 | int referencePitchIndex; 149 | double referencePitchFreq; 150 | KeyboardMapping keyboardMapping; 151 | NotationMapping notationMapping; 152 | std::string source; 153 | std::string link; 154 | 155 | std::vector rawTexts; 156 | 157 | AbletonScale() 158 | : referencePitchOctave(3), referencePitchIndex(0), 159 | referencePitchFreq(MIDI_0_FREQ * (1 << 5)) 160 | { 161 | } 162 | 163 | friend AbletonScale readASCLStream(std::istream &inf); 164 | 165 | private: 166 | int midiNoteForScalePosition(int scalePosition); 167 | int scalePositionForFrequency(double freq); 168 | double frequencyForScalePosition(int scalePosition); 169 | double centsForScalePosition(int scalePosition); 170 | }; 171 | 172 | /** 173 | * In some failure states, the tuning library will throw an exception of 174 | * type TuningError with a descriptive message. 175 | */ 176 | class TuningError : public std::exception 177 | { 178 | public: 179 | TuningError(std::string m) : whatv(m) {} 180 | virtual const char *what() const noexcept override { return whatv.c_str(); } 181 | 182 | private: 183 | std::string whatv; 184 | }; 185 | 186 | /** 187 | * readSCLStream returns a Scale from the SCL input stream 188 | */ 189 | Scale readSCLStream(std::istream &inf); 190 | 191 | /** 192 | * readSCLFile returns a Scale from the SCL File in fname 193 | */ 194 | Scale readSCLFile(std::string fname); 195 | 196 | /** 197 | * parseSCLData returns a scale from the SCL file contents in memory 198 | */ 199 | Scale parseSCLData(const std::string &sclContents); 200 | 201 | /** 202 | * evenTemperament12NoteScale provides a utility scale which is 203 | * the "standard tuning" scale 204 | */ 205 | Scale evenTemperament12NoteScale(); 206 | 207 | /** 208 | * evenDivisionOfSpanByM provides a scale referd to as "ED2-17" or 209 | * "ED3-24" by dividing the Span into M points. eventDivisionOfSpanByM(2,12) 210 | * should be the evenTemperament12NoteScale 211 | */ 212 | Scale evenDivisionOfSpanByM(int Span, int M); 213 | 214 | /** 215 | * evenDivisionOfCentsByM provides a scale which divides Cents into M 216 | * steps. It is less frequently used than evenDivisionOfSpanByM for obvious 217 | * reasons. If you want the last cents label labeled differently than the cents 218 | * argument, pass in the associated optional label 219 | */ 220 | Scale evenDivisionOfCentsByM(float Cents, int M, const std::string &lastLabel = ""); 221 | 222 | /** 223 | * readKBMStream returns a KeyboardMapping from a KBM input stream 224 | */ 225 | KeyboardMapping readKBMStream(std::istream &inf); 226 | 227 | /** 228 | * readKBMFile returns a KeyboardMapping from a KBM file name 229 | */ 230 | KeyboardMapping readKBMFile(std::string fname); 231 | 232 | /** 233 | * parseKBMData returns a KeyboardMapping from a KBM data in memory 234 | */ 235 | KeyboardMapping parseKBMData(const std::string &kbmContents); 236 | 237 | /** 238 | * tuneA69To creates a KeyboardMapping which keeps the midi note 69 (A4) set 239 | * to a constant frequency, given 240 | */ 241 | KeyboardMapping tuneA69To(double freq); 242 | 243 | /** 244 | * tuneNoteTo creates a KeyboardMapping which keeps the midi note given is set 245 | * to a constant frequency, given 246 | */ 247 | KeyboardMapping tuneNoteTo(int midiNote, double freq); 248 | 249 | /** 250 | * startScaleOnAndTuneNoteTo generates a KBM where scaleStart is the note 0 251 | * of the scale, where midiNote is the tuned note, and where feq is the frequency 252 | */ 253 | KeyboardMapping startScaleOnAndTuneNoteTo(int scaleStart, int midiNote, double freq); 254 | 255 | /** 256 | * readASCLStream returns an AbletonScale from the ASCL input stream 257 | */ 258 | AbletonScale readASCLStream(std::istream &inf); 259 | 260 | /** 261 | * readASCLFile returns an AbletonScale from the ASCL file in fname 262 | */ 263 | AbletonScale readASCLFile(std::string fname); 264 | 265 | /** 266 | * parseASCLData returns an AbletonScale from the ASCL file contents in memory 267 | */ 268 | AbletonScale parseASCLData(const std::string &asclContents); 269 | 270 | /** 271 | * The Tuning class is the primary place where you will interact with this library. 272 | * It is constructed for a scale and mapping and then gives you the ability to 273 | * determine frequencies across and beyond the midi keyboard. Since modulation 274 | * can force key number well outside the [0,127] range in some of our synths we 275 | * support a midi note range from -256 to + 256 spanning more than the entire frequency 276 | * space reasonable. 277 | * 278 | * To use this class, you construct a fresh instance every time you want to use a 279 | * different Scale and Keyboard. If you want to tune to a different scale or mapping, 280 | * just construct a new instance. 281 | */ 282 | class Tuning 283 | { 284 | public: 285 | // The number of notes we pre-compute 286 | constexpr static int N = 512; 287 | 288 | // Construct a tuning with even temperament and standard mapping 289 | Tuning(); 290 | 291 | /** 292 | * Construct a tuning for a particular scale, mapping, or for both. 293 | */ 294 | Tuning(const Scale &s); 295 | Tuning(const KeyboardMapping &k); 296 | Tuning(const AbletonScale &as); 297 | Tuning(const Scale &s, const KeyboardMapping &k, bool allowTuningCenterOnUnmapped = false); 298 | 299 | /* 300 | * Skipped notes can either have nonsense values or interpolated values. 301 | * The old API made the bad choice to have nonsense values which we retain 302 | * for compatability, but this method will return a new tuning with correctly 303 | * interpolated skipped notes. 304 | */ 305 | Tuning withSkippedNotesInterpolated() const; 306 | 307 | /** 308 | * These three related functions provide you the information you 309 | * need to use this tuning. 310 | * 311 | * frequencyForMidiNote returns the Frequency in HZ for a given midi 312 | * note. In standard tuning, FrequencyForMidiNote(69) will be 440 313 | * and frequencyForMidiNote(60) will be 261.62 - the standard frequencies 314 | * for A and middle C. 315 | * 316 | * frequencyForMidiNoteScaledByMidi0 returns the frequency but with the 317 | * standard frequency of midi note 0 divided out. So in standard tuning 318 | * frequencyForMidiNoteScaledByMidi0(0) = 1 and frequencyForMidiNoteScaledByMidi0(60) = 32 319 | * 320 | * Finally logScaledFrequencyForMidiNote returns the log base 2 of the scaled frequency. 321 | * So logScaledFrequencyForMidiNote(0) = 0 and logScaledFrequencyForMidiNote(60) = 5. 322 | * 323 | * Both the frequency measures have the feature of doubling when frequency doubles 324 | * (or when a standard octave is spanned), whereas the log one increase by 1 per frequency 325 | * double. 326 | * 327 | * Depending on your internal pitch model, one of these three methods should allow you 328 | * to calibrate your oscillators to the appropriate frequency based on the midi note 329 | * at hand. 330 | * 331 | * The scalePositionForMidiNote returns the space in the logical scale. Note 0 is the root. 332 | * It has a maxiumum value of count-1. Note that SCL files omit the root internally and so 333 | * this logical scale position is off by 1 from the index in the tones array of the Scale data. 334 | */ 335 | double frequencyForMidiNote(int mn) const; 336 | double frequencyForMidiNoteScaledByMidi0(int mn) const; 337 | double logScaledFrequencyForMidiNote(int mn) const; 338 | double retuningFromEqualInCentsForMidiNote(int mn) const; 339 | double retuningFromEqualInSemitonesForMidiNote(int mn) const; 340 | 341 | int scalePositionForMidiNote(int mn) const; 342 | bool isMidiNoteMapped(int mn) const; 343 | 344 | int midiNoteForNoteName(std::string noteName, int octave) const; 345 | std::string noteNameForScalePosition(int scalePosition) const; 346 | 347 | // For convenience, the scale and mapping used to construct this are kept as public copies 348 | Scale scale; 349 | KeyboardMapping keyboardMapping; 350 | NotationMapping notationMapping; 351 | 352 | private: 353 | std::array ptable, lptable; 354 | std::array scalepositiontable; 355 | bool allowTuningCenterOnUnmapped{false}; 356 | }; 357 | 358 | } // namespace Tunings 359 | 360 | #include "TuningsImpl.h" 361 | 362 | #endif 363 | -------------------------------------------------------------------------------- /include/TuningsImpl.h: -------------------------------------------------------------------------------- 1 | // -*-c++-*- 2 | /** 3 | * TuningsImpl.h 4 | * Copyright 2019-2020 Paul Walker 5 | * Released under the MIT License. See LICENSE.md 6 | * 7 | * This contains the nasty nitty gritty implementation of the api in Tunings.h. You probably 8 | * don't need to read it unless you have found and are fixing a bug, are curious, or want 9 | * to add a feature to the API. For usages of this library, the documentation in Tunings.h and 10 | * the usages in tests/all_tests.cpp should provide you more than enough guidance. 11 | */ 12 | 13 | #ifndef __INCLUDE_TUNINGS_IMPL_H 14 | #define __INCLUDE_TUNINGS_IMPL_H 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace Tunings 30 | { 31 | // Thank you to: https://gist.github.com/josephwb/df09e3a71679461fc104 32 | inline std::istream &getlineEndingIndependent(std::istream &is, std::string &t) 33 | { 34 | t.clear(); 35 | 36 | std::istream::sentry se(is, true); 37 | if (!se) 38 | return is; 39 | 40 | std::streambuf *sb = is.rdbuf(); 41 | 42 | for (;;) 43 | { 44 | int c = sb->sbumpc(); 45 | switch (c) 46 | { 47 | case '\n': 48 | return is; 49 | case '\r': 50 | if (sb->sgetc() == '\n') 51 | { 52 | sb->sbumpc(); 53 | } 54 | return is; 55 | case EOF: 56 | is.setstate(std::ios::eofbit); 57 | if (t.empty()) 58 | { 59 | is.setstate(std::ios::badbit); 60 | } 61 | return is; 62 | default: 63 | t += (char)c; 64 | } 65 | } 66 | } 67 | 68 | inline double locale_atof(const char *s) 69 | { 70 | double result = 0; 71 | std::istringstream istr(s); 72 | istr.imbue(std::locale("C")); 73 | istr >> result; 74 | return result; 75 | } 76 | 77 | inline unsigned positive_mod(int v, unsigned m) 78 | { 79 | int mod = v % (int)m; 80 | if (mod < 0) 81 | mod += m; 82 | return mod; 83 | } 84 | 85 | inline Tone toneFromString(const std::string &fullLine, int lineno) 86 | { 87 | Tone t; 88 | t.stringRep = fullLine; 89 | t.lineno = lineno; 90 | 91 | // Allow end-of-line comments, e.g. "555/524 ! c# 138.75 Hz" 92 | std::string line = fullLine.substr(0, fullLine.find("!", 0)); 93 | 94 | if (line.find('.') != std::string::npos) 95 | { 96 | t.type = Tone::kToneCents; 97 | t.cents = locale_atof(line.c_str()); 98 | } 99 | else 100 | { 101 | t.type = Tone::kToneRatio; 102 | auto slashPos = line.find('/'); 103 | if (slashPos == std::string::npos) 104 | { 105 | t.ratio_n = atoll(line.c_str()); 106 | t.ratio_d = 1; 107 | } 108 | else 109 | { 110 | t.ratio_n = atoll(line.substr(0, slashPos).c_str()); 111 | t.ratio_d = atoll(line.substr(slashPos + 1).c_str()); 112 | } 113 | 114 | if (t.ratio_n == 0 || t.ratio_d == 0) 115 | { 116 | std::string s = "Invalid tone in SCL file."; 117 | if (lineno >= 0) 118 | s += "Line " + std::to_string(lineno) + "."; 119 | s += " Line is '" + line + "'."; 120 | throw TuningError(s); 121 | } 122 | // 2^(cents/1200) = n/d 123 | // cents = 1200 * log(n/d) / log(2) 124 | 125 | t.cents = 1200 * log(1.0 * t.ratio_n / t.ratio_d) / log(2.0); 126 | } 127 | t.floatValue = t.cents / 1200.0 + 1.0; 128 | return t; 129 | } 130 | 131 | inline Scale readSCLStream(std::istream &inf) 132 | { 133 | std::string line; 134 | const int read_header = 0, read_count = 1, read_note = 2, trailing = 3; 135 | int state = read_header; 136 | 137 | Scale res; 138 | std::ostringstream rawOSS; 139 | int lineno = 0; 140 | while (getlineEndingIndependent(inf, line)) 141 | { 142 | rawOSS << line << "\n"; 143 | lineno++; 144 | 145 | if ((state == read_note && line.empty()) || line[0] == '!') 146 | { 147 | res.comments.push_back(line); 148 | continue; 149 | } 150 | switch (state) 151 | { 152 | case read_header: 153 | res.description = line; 154 | state = read_count; 155 | break; 156 | case read_count: 157 | res.count = atoi(line.c_str()); 158 | if (res.count < 1) 159 | { 160 | throw TuningError("Invalid SCL note count."); 161 | } 162 | state = read_note; 163 | break; 164 | case read_note: 165 | auto t = toneFromString(line, lineno); 166 | res.tones.push_back(t); 167 | if ((int)res.tones.size() == res.count) 168 | state = trailing; 169 | 170 | break; 171 | } 172 | } 173 | 174 | if (!(state == read_note || state == trailing)) 175 | { 176 | std::ostringstream oss; 177 | oss << "Incomplete SCL content. Only able to read " << lineno 178 | << " lines of data. Found content up to "; 179 | switch (state) 180 | { 181 | case read_header: 182 | oss << "reading header."; 183 | break; 184 | case read_count: 185 | oss << "reading scale count."; 186 | break; 187 | default: 188 | oss << "unknown state."; 189 | break; 190 | } 191 | throw TuningError(oss.str()); 192 | } 193 | 194 | if ((int)res.tones.size() != res.count) 195 | { 196 | std::string s = 197 | "Read fewer notes than count in file. Count = " + std::to_string(res.count) + 198 | " notes. Array size = " + std::to_string(res.tones.size()); 199 | throw TuningError(s); 200 | } 201 | res.rawText = rawOSS.str(); 202 | return res; 203 | } 204 | 205 | inline Scale readSCLFile(std::string fname) 206 | { 207 | std::ifstream inf; 208 | inf.open(fname); 209 | if (!inf.is_open()) 210 | { 211 | std::string s = "Unable to open file '" + fname + "'"; 212 | throw TuningError(s); 213 | } 214 | 215 | auto res = readSCLStream(inf); 216 | res.name = fname; 217 | return res; 218 | } 219 | 220 | inline Scale parseSCLData(const std::string &d) 221 | { 222 | std::istringstream iss(d); 223 | auto res = readSCLStream(iss); 224 | res.name = "Scale from patch"; 225 | return res; 226 | } 227 | 228 | inline Scale evenTemperament12NoteScale() 229 | { 230 | std::string data = R"SCL(! 12 Tone Equal Temperament.scl 231 | ! 232 | 12 Tone Equal Temperament | ED2-12 - Equal division of harmonic 2 into 12 parts 233 | 12 234 | ! 235 | 100.00000 236 | 200.00000 237 | 300.00000 238 | 400.00000 239 | 500.00000 240 | 600.00000 241 | 700.00000 242 | 800.00000 243 | 900.00000 244 | 1000.00000 245 | 1100.00000 246 | 2/1 247 | )SCL"; 248 | return parseSCLData(data); 249 | } 250 | 251 | inline Scale evenDivisionOfSpanByM(int Span, int M) 252 | { 253 | if (Span <= 0) 254 | throw Tunings::TuningError("Span should be a positive number. You entered " + 255 | std::to_string(Span)); 256 | if (M <= 0) 257 | throw Tunings::TuningError( 258 | "You must divide the period into at least one step. You entered " + std::to_string(M)); 259 | 260 | std::ostringstream oss; 261 | oss.imbue(std::locale("C")); 262 | oss << "! Automatically generated ED" << Span << "-" << M << " scale\n"; 263 | oss << "Automatically generated ED" << Span << "-" << M << " scale\n"; 264 | oss << M << "\n"; 265 | oss << "!\n"; 266 | 267 | double topCents = 1200.0 * log(1.0 * Span) / log(2.0); 268 | double dCents = topCents / M; 269 | for (int i = 1; i < M; ++i) 270 | oss << std::fixed << dCents * i << "\n"; 271 | oss << Span << "/1\n"; 272 | 273 | return parseSCLData(oss.str()); 274 | } 275 | 276 | inline Scale evenDivisionOfCentsByM(float Cents, int M, const std::string &lastLabel) 277 | { 278 | if (Cents <= 0) 279 | throw Tunings::TuningError("Cents should be a positive number. You entered " + 280 | std::to_string(Cents)); 281 | if (M <= 0) 282 | throw Tunings::TuningError( 283 | "You must divide the period into at least one step. You entered " + std::to_string(M)); 284 | 285 | std::ostringstream oss; 286 | oss.imbue(std::locale("C")); 287 | oss << "! Automatically generated Even Division of " << Cents << " ct into " << M << " scale\n"; 288 | oss << "Automatically generated Even Division of " << Cents << " ct into " << M << " scale\n"; 289 | oss << M << "\n"; 290 | oss << "!\n"; 291 | 292 | double topCents = Cents; 293 | double dCents = topCents / M; 294 | for (int i = 1; i < M; ++i) 295 | oss << std::fixed << dCents * i << "\n"; 296 | if (lastLabel.empty()) 297 | oss << Cents << "\n"; 298 | else 299 | oss << lastLabel << "\n"; 300 | 301 | return parseSCLData(oss.str()); 302 | } 303 | 304 | inline KeyboardMapping readKBMStream(std::istream &inf) 305 | { 306 | std::string line; 307 | 308 | KeyboardMapping res; 309 | std::ostringstream rawOSS; 310 | res.keys.clear(); 311 | 312 | enum parsePosition 313 | { 314 | map_size = 0, 315 | first_midi, 316 | last_midi, 317 | middle, 318 | reference, 319 | freq, 320 | degree, 321 | keys, 322 | trailing 323 | }; 324 | parsePosition state = map_size; 325 | 326 | int lineno = 0; 327 | while (getlineEndingIndependent(inf, line)) 328 | { 329 | rawOSS << line << "\n"; 330 | lineno++; 331 | if (line[0] == '!') 332 | { 333 | continue; 334 | } 335 | 336 | if (line == "x") 337 | line = "-1"; 338 | else if (state != trailing) 339 | { 340 | const char *lc = line.c_str(); 341 | bool validLine = line.length() > 0; 342 | char badChar = '\0'; 343 | while (validLine && *lc != '\0') 344 | { 345 | if (!(*lc == ' ' || std::isdigit(*lc) || *lc == '.' || *lc == (char)13 || 346 | *lc == '\n')) 347 | { 348 | validLine = false; 349 | badChar = *lc; 350 | } 351 | lc++; 352 | } 353 | if (!validLine) 354 | { 355 | throw TuningError("Invalid line " + std::to_string(lineno) + ". line='" + line + 356 | "'. Bad character is '" + badChar + "/" + 357 | std::to_string((int)badChar) + "'"); 358 | } 359 | } 360 | 361 | int i = std::atoi(line.c_str()); 362 | double v = locale_atof(line.c_str()); 363 | 364 | switch (state) 365 | { 366 | case map_size: 367 | res.count = i; 368 | break; 369 | case first_midi: 370 | res.firstMidi = i; 371 | break; 372 | case last_midi: 373 | res.lastMidi = i; 374 | break; 375 | case middle: 376 | res.middleNote = i; 377 | break; 378 | case reference: 379 | res.tuningConstantNote = i; 380 | break; 381 | case freq: 382 | res.tuningFrequency = v; 383 | res.tuningPitch = res.tuningFrequency / MIDI_0_FREQ; 384 | break; 385 | case degree: 386 | res.octaveDegrees = i; 387 | break; 388 | case keys: 389 | res.keys.push_back(i); 390 | if ((int)res.keys.size() == res.count) 391 | state = trailing; 392 | break; 393 | case trailing: 394 | break; 395 | } 396 | if (!(state == keys || state == trailing)) 397 | state = (parsePosition)(state + 1); 398 | if (state == keys && res.count == 0) 399 | state = trailing; 400 | } 401 | 402 | if (!(state == keys || state == trailing)) 403 | { 404 | std::ostringstream oss; 405 | oss << "Incomplete KBM stream. Only able to read " << lineno << " lines. Read up to "; 406 | switch (state) 407 | { 408 | case map_size: 409 | oss << "map size."; 410 | break; 411 | case first_midi: 412 | oss << "first midi note."; 413 | break; 414 | case last_midi: 415 | oss << "last midi note."; 416 | break; 417 | case middle: 418 | oss << "scale zero note."; 419 | break; 420 | case reference: 421 | oss << "scale reference note."; 422 | break; 423 | case freq: 424 | oss << "scale reference frequency."; 425 | break; 426 | case degree: 427 | oss << "scale degree."; 428 | break; 429 | default: 430 | oss << "unknown state"; 431 | break; 432 | } 433 | throw TuningError(oss.str()); 434 | } 435 | 436 | if ((int)res.keys.size() != res.count) 437 | { 438 | throw TuningError("Different number of keys than mapping file indicates. Count is " + 439 | std::to_string(res.count) + " and we parsed " + 440 | std::to_string(res.keys.size()) + " keys."); 441 | } 442 | 443 | res.rawText = rawOSS.str(); 444 | return res; 445 | } 446 | 447 | inline KeyboardMapping readKBMFile(std::string fname) 448 | { 449 | std::ifstream inf; 450 | inf.open(fname); 451 | if (!inf.is_open()) 452 | { 453 | std::string s = "Unable to open file '" + fname + "'"; 454 | throw TuningError(s); 455 | } 456 | 457 | auto res = readKBMStream(inf); 458 | res.name = fname; 459 | return res; 460 | } 461 | 462 | inline KeyboardMapping parseKBMData(const std::string &d) 463 | { 464 | std::istringstream iss(d); 465 | auto res = readKBMStream(iss); 466 | res.name = "Mapping from patch"; 467 | return res; 468 | } 469 | 470 | inline Tuning::Tuning() : Tuning(evenTemperament12NoteScale(), KeyboardMapping()) {} 471 | inline Tuning::Tuning(const Scale &s) : Tuning(s, KeyboardMapping()) {} 472 | inline Tuning::Tuning(const KeyboardMapping &k) : Tuning(evenTemperament12NoteScale(), k) {} 473 | inline Tuning::Tuning(const AbletonScale &as) : Tuning(as.scale, as.keyboardMapping) 474 | { 475 | this->notationMapping = as.notationMapping; 476 | } 477 | 478 | inline Tuning::Tuning(const Scale &s_, const KeyboardMapping &k_, bool allowTuningCenterOnUnmapped_) 479 | : allowTuningCenterOnUnmapped(allowTuningCenterOnUnmapped_) 480 | { 481 | // Shadow on purpose to make sure we use the modified version from rotation - use for dev 482 | // int *scale{0}, keyboardMapping{0}; 483 | this->scale = s_; 484 | this->keyboardMapping = k_; 485 | 486 | Scale s = s_; 487 | KeyboardMapping k = k_; 488 | int oSP; 489 | if (s.count <= 0) 490 | throw TuningError("Unable to tune to a scale with no notes. Your scale provided " + 491 | std::to_string(s.count) + " notes."); 492 | 493 | int useMiddleNote{k.middleNote}; 494 | if (k.count > 0) 495 | { 496 | // Is the KBM not spanning the tuning note 497 | auto mapStart = useMiddleNote; 498 | auto mapEnd = useMiddleNote + k.count; 499 | while (mapStart > k.tuningConstantNote) 500 | { 501 | useMiddleNote -= k.count; 502 | mapStart = useMiddleNote; 503 | mapEnd = useMiddleNote + k.count; 504 | // throw std::logic_error("Blah"); 505 | } 506 | while (mapEnd < k.tuningConstantNote) 507 | { 508 | useMiddleNote += k.count; 509 | mapStart = useMiddleNote; 510 | mapEnd = useMiddleNote + k.count; 511 | // throw std::logic_error("Blah"); 512 | } 513 | } 514 | 515 | int kbmRotations{1}; 516 | for (const auto &kv : k.keys) 517 | { 518 | kbmRotations = std::max(kbmRotations, (int)std::ceil(1.0 * kv / s.count)); 519 | } 520 | 521 | if (kbmRotations > 1) 522 | { 523 | // This means the KBM has mapped note 5 in a 4 note scale or some such 524 | // which implies an 'unwrap' operation. So what we are going to do is 525 | // create a new scale which is extended then update the kbm octave position 526 | // accordingly. 527 | Scale newS = s; 528 | newS.count = s.count * kbmRotations; 529 | auto backCents = s.tones.back().cents; 530 | auto pushOff = backCents; 531 | for (int i = 1; i < kbmRotations; ++i) 532 | { 533 | for (const auto &t : s.tones) 534 | { 535 | Tunings::Tone tCopy = t; 536 | tCopy.type = Tone::kToneCents; 537 | tCopy.cents += pushOff; 538 | tCopy.floatValue = tCopy.cents / 1200.0 + 1; 539 | 540 | newS.tones.push_back(tCopy); 541 | } 542 | pushOff += backCents; 543 | } 544 | s = newS; 545 | k.octaveDegrees *= kbmRotations; 546 | if (k.octaveDegrees == 0) 547 | k.octaveDegrees = s.count; 548 | } 549 | 550 | // From the KBM Spec: When not all scale degrees need to be mapped, the size of the map can be 551 | // smaller than the size of the scale. 552 | if (k.octaveDegrees > s.count) 553 | { 554 | throw TuningError("Unable to apply mapping of size " + std::to_string(k.octaveDegrees) + 555 | " to smaller scale of size " + std::to_string(s.count)); 556 | } 557 | 558 | int posPitch0 = 256 + k.tuningConstantNote; 559 | int posScale0 = 256 + useMiddleNote; 560 | 561 | double pitchMod = log(k.tuningPitch) / log(2) - 1; 562 | 563 | int scalePositionOfTuningNote = k.tuningConstantNote - useMiddleNote; 564 | if (k.count > 0) 565 | { 566 | while (scalePositionOfTuningNote >= k.count) 567 | { 568 | scalePositionOfTuningNote -= k.count; 569 | } 570 | while (scalePositionOfTuningNote < 0) 571 | { 572 | scalePositionOfTuningNote += k.count; 573 | } 574 | oSP = scalePositionOfTuningNote; 575 | scalePositionOfTuningNote = k.keys[scalePositionOfTuningNote]; 576 | if (scalePositionOfTuningNote == -1 && !allowTuningCenterOnUnmapped) 577 | { 578 | std::string s = "Keyboard mapping is tuning an unmapped key. "; 579 | s += "Your tuning mapping is mapping key " + std::to_string(k.tuningConstantNote) + 580 | " as " + "the tuning constant note, but that is scale note " + 581 | std::to_string(oSP) + " given your scale root of " + std::to_string(k.middleNote) + 582 | " which your mapping does not assign. Please set your tuning constant " 583 | "note to a mapped key."; 584 | throw TuningError(s); 585 | } 586 | } 587 | double tuningCenterPitchOffset; 588 | if (scalePositionOfTuningNote == 0) 589 | tuningCenterPitchOffset = 0; 590 | else 591 | { 592 | if (scalePositionOfTuningNote == -1 && allowTuningCenterOnUnmapped) 593 | { 594 | int low, high; 595 | bool octave_up = false; 596 | bool octave_down = false; 597 | float pitch_high; 598 | float pitch_low; 599 | // find next closest mapped note 600 | for (int i = oSP - 1; i != oSP; i = (i - 1) % k.count) 601 | { 602 | if (k.keys[i] != -1) 603 | { 604 | low = k.keys[i]; 605 | break; 606 | } 607 | 608 | if (i > oSP) 609 | { 610 | octave_down = true; 611 | } 612 | } 613 | for (int i = oSP + 1; i != oSP; i = (i + 1) % k.count) 614 | { 615 | if (k.keys[i] != -1) 616 | { 617 | high = k.keys[i]; 618 | break; 619 | } 620 | 621 | if (i < oSP) 622 | { 623 | octave_up = true; 624 | } 625 | } 626 | 627 | // determine high and low pitches 628 | double dt = s.tones[s.count - 1].cents; 629 | pitch_low = 630 | octave_down ? s.tones[low - 1].cents - dt : s.tones[low - 1].floatValue - 1.0; 631 | pitch_high = 632 | octave_up ? s.tones[high - 1].cents + dt : s.tones[high - 1].floatValue - 1.0; 633 | tuningCenterPitchOffset = (pitch_high + pitch_low) / 2.f; 634 | } 635 | else 636 | { 637 | double tshift = 0; 638 | double dt = s.tones[s.count - 1].floatValue - 1.0; 639 | while (scalePositionOfTuningNote < 0) 640 | { 641 | scalePositionOfTuningNote += s.count; 642 | tshift += dt; 643 | } 644 | while (scalePositionOfTuningNote > s.count) 645 | { 646 | scalePositionOfTuningNote -= s.count; 647 | tshift -= dt; 648 | } 649 | 650 | if (scalePositionOfTuningNote == 0) 651 | tuningCenterPitchOffset = -tshift; 652 | else 653 | tuningCenterPitchOffset = 654 | s.tones[scalePositionOfTuningNote - 1].floatValue - 1.0 - tshift; 655 | } 656 | } 657 | 658 | double pitches[N]; 659 | 660 | for (int i = 0; i < N; ++i) 661 | { 662 | // TODO: ScaleCenter and PitchCenter are now two different notes. 663 | int distanceFromPitch0 = i - posPitch0; 664 | int distanceFromScale0 = i - posScale0; 665 | 666 | if (distanceFromPitch0 == 0) 667 | { 668 | pitches[i] = 1; 669 | lptable[i] = pitches[i] + pitchMod; 670 | ptable[i] = pow(2.0, lptable[i]); 671 | 672 | if (k.count > 0) 673 | { 674 | int mappingKey = distanceFromScale0 % k.count; 675 | if (mappingKey < 0) 676 | mappingKey += k.count; 677 | 678 | int cm = k.keys[mappingKey]; 679 | if (!allowTuningCenterOnUnmapped && cm < 0) 680 | { 681 | std::string s = "Keyboard mapping is tuning an unmapped key. "; 682 | s += "Your tuning mapping is mapping key " + std::to_string(posPitch0 - 256) + 683 | " as " + "the tuning constant note, but that is scale note " + 684 | std::to_string(mappingKey) + " given your scale root of " + 685 | std::to_string(k.middleNote) + 686 | " which your mapping does not assign. Please set your tuning constant " 687 | "note to a mapped key."; 688 | throw TuningError(s); 689 | } 690 | } 691 | scalepositiontable[i] = scalePositionOfTuningNote % s.count; 692 | #if DEBUG_SCALES 693 | std::cout << "PITCH: i=" << i << " n=" << i - 256 << " p=" << pitches[i] 694 | << " lp=" << lptable[i] << " tp=" << ptable[i] 695 | << " fr=" << ptable[i] * 8.175798915 << std::endl; 696 | #endif 697 | } 698 | else 699 | { 700 | /* 701 | We used to have this which assumed 1-12 702 | Now we have our note number, our distance from the 703 | center note, and the key remapping 704 | int rounds = (distanceFromScale0-1) / s.count; 705 | int thisRound = (distanceFromScale0-1) % s.count; 706 | */ 707 | 708 | int rounds; 709 | int thisRound; 710 | int disable = false; 711 | if (k.count == 0) 712 | { 713 | rounds = (distanceFromScale0 - 1) / s.count; 714 | thisRound = (distanceFromScale0 - 1) % s.count; 715 | } 716 | else 717 | { 718 | /* 719 | ** Now we have this situation. We are at note i so we 720 | ** are m away from the center note which is distanceFromScale0 721 | ** 722 | ** If we mod that by the mapping size we know which note we are on 723 | */ 724 | int mappingKey = distanceFromScale0 % k.count; 725 | int rotations = 0; 726 | if (mappingKey < 0) 727 | { 728 | mappingKey += k.count; 729 | } 730 | // Now have we gone off the end 731 | int dt = distanceFromScale0; 732 | if (dt > 0) 733 | { 734 | while (dt >= k.count) 735 | { 736 | dt -= k.count; 737 | rotations++; 738 | } 739 | } 740 | else 741 | { 742 | while (dt < 0) 743 | { 744 | dt += k.count; 745 | rotations--; 746 | } 747 | } 748 | 749 | int cm = k.keys[mappingKey]; 750 | 751 | int push = 0; 752 | if (cm < 0) 753 | { 754 | disable = true; 755 | } 756 | else 757 | { 758 | if (cm > s.count) 759 | { 760 | throw TuningError(std::string( 761 | "Mapping KBM note longer than scale; key=" + std::to_string(cm) + 762 | " scale count=" + std::to_string(s.count))); 763 | } 764 | push = mappingKey - cm; 765 | } 766 | 767 | if (k.octaveDegrees > 0 && k.octaveDegrees != k.count) 768 | { 769 | rounds = rotations; 770 | thisRound = cm - 1; 771 | if (thisRound < 0) 772 | { 773 | thisRound = k.octaveDegrees - 1; 774 | rounds--; 775 | } 776 | } 777 | else 778 | { 779 | rounds = (distanceFromScale0 - push - 1) / s.count; 780 | thisRound = (distanceFromScale0 - push - 1) % s.count; 781 | } 782 | 783 | #ifdef DEBUG_SCALES 784 | if (i > 256 + 53 && i < 265 + 85) 785 | std::cout << "MAPPING n=" << i - 256 << " pushes ds0=" << distanceFromScale0 786 | << " cmc=" << k.count << " tr=" << thisRound << " r=" << rounds 787 | << " mk=" << mappingKey << " cm=" << cm << " push=" << push 788 | << " dis=" << disable << " mk-p-1=" << mappingKey - push - 1 789 | << " rotations=" << rotations << " od=" << k.octaveDegrees 790 | << std::endl; 791 | #endif 792 | } 793 | 794 | if (thisRound < 0) 795 | { 796 | thisRound += s.count; 797 | rounds -= 1; 798 | } 799 | 800 | if (disable) 801 | { 802 | pitches[i] = 0; 803 | scalepositiontable[i] = -1; 804 | } 805 | else 806 | { 807 | pitches[i] = s.tones[thisRound].floatValue + 808 | rounds * (s.tones[s.count - 1].floatValue - 1.0) - 809 | tuningCenterPitchOffset; 810 | scalepositiontable[i] = (thisRound + 1) % s.count; 811 | } 812 | 813 | lptable[i] = pitches[i] + pitchMod; 814 | ptable[i] = pow(2.0, pitches[i] + pitchMod); 815 | 816 | #if DEBUG_SCALES 817 | if (i > 296 && i < 340) 818 | std::cout << "PITCH: i=" << i << " n=" << i - 256 << " ds0=" << distanceFromScale0 819 | << " dp0=" << distanceFromPitch0 << " r=" << rounds << " t=" << thisRound 820 | << " p=" << pitches[i] << " t=" << s.tones[thisRound].floatValue 821 | << " c=" << s.tones[thisRound].cents << " dis=" << disable 822 | << " tp=" << ptable[i] << " fr=" << ptable[i] * 8.175798915 << " tcpo=" 823 | << tuningCenterPitchOffset 824 | 825 | //<< " l2p=" << log(otp)/log(2.0) 826 | //<< " l2p-p=" << log(otp)/log(2.0) - pitches[i] - rounds - 3 827 | << std::endl; 828 | #endif 829 | } 830 | } 831 | 832 | /* 833 | * Finally we may have constructed an invalid tuning 834 | */ 835 | } 836 | 837 | inline double Tuning::frequencyForMidiNote(int mn) const 838 | { 839 | auto mni = std::min(std::max(0, mn + 256), N - 1); 840 | return ptable[mni] * MIDI_0_FREQ; 841 | } 842 | 843 | inline double Tuning::frequencyForMidiNoteScaledByMidi0(int mn) const 844 | { 845 | auto mni = std::min(std::max(0, mn + 256), N - 1); 846 | return ptable[mni]; 847 | } 848 | 849 | inline double Tuning::logScaledFrequencyForMidiNote(int mn) const 850 | { 851 | auto mni = std::min(std::max(0, mn + 256), N - 1); 852 | return lptable[mni]; 853 | } 854 | 855 | inline double Tuning::retuningFromEqualInCentsForMidiNote(int mn) const 856 | { 857 | return retuningFromEqualInSemitonesForMidiNote(mn) * 100.0; 858 | } 859 | inline double Tuning::retuningFromEqualInSemitonesForMidiNote(int mn) const 860 | { 861 | return logScaledFrequencyForMidiNote(mn) * 12 - mn; 862 | } 863 | 864 | inline int Tuning::scalePositionForMidiNote(int mn) const 865 | { 866 | auto mni = std::min(std::max(0, mn + 256), N - 1); 867 | return scalepositiontable[mni]; 868 | } 869 | 870 | inline bool Tuning::isMidiNoteMapped(int mn) const 871 | { 872 | auto mni = std::min(std::max(0, mn + 256), N - 1); 873 | return scalepositiontable[mni] >= 0; 874 | } 875 | 876 | inline int Tuning::midiNoteForNoteName(std::string noteName, int octave) const 877 | { 878 | const auto it = std::find(notationMapping.names.begin(), notationMapping.names.end(), noteName); 879 | if (it == notationMapping.names.end()) 880 | { 881 | std::string s = "Invalid note name '" + noteName + "'"; 882 | throw TuningError(s); 883 | } 884 | int scalePosition = positive_mod(it - notationMapping.names.begin() + 1, notationMapping.count); 885 | return std::min( 886 | std::max(0, scalePosition + keyboardMapping.middleNote + 887 | keyboardMapping.octaveDegrees * (octave - keyboardMapping.tuningOctave)), 888 | N - 1); 889 | } 890 | 891 | inline std::string Tuning::noteNameForScalePosition(int scalePosition) const 892 | { 893 | if (notationMapping.count == 0) 894 | { 895 | std::string s = "No note names found in the tuning."; 896 | throw TuningError(s); 897 | } 898 | return notationMapping.names.at(positive_mod(scalePosition - 1, notationMapping.count)); 899 | } 900 | 901 | inline Tuning Tuning::withSkippedNotesInterpolated() const 902 | { 903 | Tuning res = *this; 904 | for (int i = 1; i < N - 1; ++i) 905 | { 906 | if (scalepositiontable[i] < 0) 907 | { 908 | int nxt = i + 1; 909 | int prv = i - 1; 910 | while (prv >= 0 && scalepositiontable[prv] < 0) 911 | prv--; 912 | while (nxt < N && scalepositiontable[nxt] < 0) 913 | nxt++; 914 | float dist = (float)(nxt - prv); 915 | float frac = (float)(i - prv) / dist; 916 | res.lptable[i] = (1.0 - frac) * lptable[prv] + frac * lptable[nxt]; 917 | res.ptable[i] = pow(2.0, res.lptable[i]); 918 | } 919 | } 920 | return res; 921 | } 922 | 923 | inline KeyboardMapping::KeyboardMapping() 924 | : count(0), firstMidi(0), lastMidi(127), middleNote(60), tuningConstantNote(60), 925 | tuningFrequency(MIDI_0_FREQ * 32.0), tuningPitch(32.0), tuningOctave(4), octaveDegrees(0), 926 | rawText(""), name("") 927 | { 928 | std::ostringstream oss; 929 | oss.imbue(std::locale("C")); 930 | oss << "! Default KBM file\n"; 931 | oss << count << "\n" 932 | << firstMidi << "\n" 933 | << lastMidi << "\n" 934 | << middleNote << "\n" 935 | << tuningConstantNote << "\n" 936 | << tuningFrequency << "\n" 937 | << octaveDegrees << "\n"; 938 | rawText = oss.str(); 939 | } 940 | 941 | inline KeyboardMapping tuneA69To(double freq) { return tuneNoteTo(69, freq); } 942 | 943 | inline KeyboardMapping tuneNoteTo(int midiNote, double freq) 944 | { 945 | return startScaleOnAndTuneNoteTo(60, midiNote, freq); 946 | } 947 | 948 | inline KeyboardMapping startScaleOnAndTuneNoteTo(int scaleStart, int midiNote, double freq) 949 | { 950 | std::ostringstream oss; 951 | oss.imbue(std::locale("C")); 952 | oss << "! Automatically generated mapping, tuning note " << midiNote << " to " << freq 953 | << " Hz\n" 954 | << "!\n" 955 | << "! Size of map\n" 956 | << 0 << "\n" 957 | << "! First and last MIDI notes to map - map the entire keyboard\n" 958 | << 0 << "\n" 959 | << 127 << "\n" 960 | << "! Middle note where the first entry in the scale is mapped.\n" 961 | << scaleStart << "\n" 962 | << "! Reference note where frequency is fixed\n" 963 | << midiNote << "\n" 964 | << "! Frequency for MIDI note " << midiNote << "\n" 965 | << freq << "\n" 966 | << "! Scale degree for formal octave. This is an empty mapping, so:\n" 967 | << 0 << "\n" 968 | << "! Mapping. This is an empty mapping so list no keys\n"; 969 | 970 | return parseKBMData(oss.str()); 971 | } 972 | 973 | inline AbletonScale readASCLStream(std::istream &inf) 974 | { 975 | AbletonScale as; 976 | 977 | /** 978 | * Reverse engineering Ableton Tuning web app to understand ASCL to KBM export. 979 | * 980 | * @see https://tuning.ableton.com/squigadooServer/squigadoo/modular-playground 981 | * 982 | * Start tracing at `scalaKbmFile`. 983 | */ 984 | 985 | // Read the scale and create default KBM parameters 986 | as.scale = readSCLStream(inf); 987 | as.keyboardMapping.count = as.scale.count; 988 | as.keyboardMapping.firstMidi = 0; 989 | as.keyboardMapping.lastMidi = 127; 990 | as.keyboardMapping.middleNote = as.midiNoteForScalePosition(0); 991 | as.keyboardMapping.tuningConstantNote = as.midiNoteForScalePosition(0); 992 | as.keyboardMapping.octaveDegrees = as.keyboardMapping.count; 993 | as.keyboardMapping.keys = std::vector(as.keyboardMapping.count); 994 | std::iota(as.keyboardMapping.keys.begin(), as.keyboardMapping.keys.end(), 0); 995 | 996 | // Parse the scale comments to detect @ABL extensions 997 | for (const auto &comment : as.scale.comments) 998 | { 999 | std::smatch command; 1000 | if (!std::regex_match(comment, command, std::regex("!\\s+@ABL\\s+(.*?)\\s+(.*?)$"))) 1001 | continue; 1002 | as.rawTexts.push_back(command[0]); 1003 | if (command[1] == "NOTE_NAMES") 1004 | { 1005 | std::string rawText = command[2]; 1006 | std::smatch note_names; 1007 | std::regex note_name_regex("\\s*(?:\"(.*?)\\s*\"|(\\S+))\\s*"); 1008 | std::string::const_iterator search_start(rawText.cbegin()); 1009 | while (std::regex_search(search_start, rawText.cend(), note_names, note_name_regex)) 1010 | { 1011 | as.notationMapping.names.push_back(std::string(note_names[1]) + 1012 | std::string(note_names[2])); 1013 | search_start = note_names.suffix().first; 1014 | } 1015 | 1016 | // Move first note to last to correspond to scale.tones 1017 | std::rotate(as.notationMapping.names.begin(), as.notationMapping.names.begin() + 1, 1018 | as.notationMapping.names.end()); 1019 | 1020 | as.notationMapping.count = as.notationMapping.names.size(); 1021 | if (as.notationMapping.count != as.scale.count) 1022 | { 1023 | std::string s = "Invalid NOTE_NAMES entry '" + rawText + "': Expecting " + 1024 | std::to_string(as.scale.count) + " entries but received " + 1025 | std::to_string(as.notationMapping.count); 1026 | throw TuningError(s); 1027 | } 1028 | } 1029 | else if (command[1] == "REFERENCE_PITCH") 1030 | { 1031 | std::string rp = command[2]; 1032 | std::smatch reference_pitch; 1033 | if (std::regex_match(rp, reference_pitch, 1034 | std::regex("\\s*(\\d+)\\s*(\\d+)\\s*([\\d.]+)\\s*$"))) 1035 | { 1036 | as.referencePitchOctave = std::stoi(reference_pitch[1]); 1037 | as.referencePitchIndex = std::stoi(reference_pitch[2]); 1038 | as.referencePitchFreq = locale_atof(reference_pitch.str(3).c_str()); 1039 | as.keyboardMapping.tuningFrequency = as.referencePitchFreq; 1040 | as.keyboardMapping.tuningPitch = as.keyboardMapping.tuningFrequency / MIDI_0_FREQ; 1041 | as.keyboardMapping.tuningConstantNote = 1042 | as.midiNoteForScalePosition(as.referencePitchIndex); 1043 | as.keyboardMapping.tuningOctave = as.referencePitchOctave; 1044 | as.keyboardMapping.middleNote = as.midiNoteForScalePosition(0); 1045 | } 1046 | else 1047 | { 1048 | std::string s = "Invalid REFERENCE_PITCH entry '" + rp + "'"; 1049 | throw TuningError(s); 1050 | } 1051 | } 1052 | else if (command[1] == "NOTE_RANGE_BY_FREQUENCY") 1053 | { 1054 | // TODO 1055 | } 1056 | else if (command[1] == "NOTE_RANGE_BY_INDEX") 1057 | { 1058 | // TODO 1059 | } 1060 | else if (command[1] == "SOURCE") 1061 | { 1062 | as.source = command[2]; 1063 | } 1064 | else if (command[1] == "LINK") 1065 | { 1066 | as.link = command[2]; 1067 | } 1068 | else 1069 | { 1070 | std::string s = "Unhandled Ableton command '" + std::string(command[1]) + "'"; 1071 | throw TuningError(s); 1072 | } 1073 | } 1074 | 1075 | return as; 1076 | } 1077 | 1078 | inline int AbletonScale::midiNoteForScalePosition(int scalePosition) 1079 | { 1080 | auto middleFreq = MIDI_0_FREQ * (1 << 5); 1081 | auto middleIndex = scalePositionForFrequency(middleFreq); 1082 | return std::max(0, std::min(60 + (scalePosition - middleIndex), 127)); 1083 | } 1084 | 1085 | inline int AbletonScale::scalePositionForFrequency(double freq) 1086 | { 1087 | auto n = 0; 1088 | auto r = frequencyForScalePosition(n); 1089 | auto o = freq - r; 1090 | auto i = o > 0 ? 1 : -1; 1091 | auto s = std::abs(o); 1092 | auto a = n; 1093 | auto l = false; 1094 | if (s <= std::numeric_limits::epsilon()) 1095 | return n; 1096 | while (!l) 1097 | { 1098 | n += i; 1099 | r = frequencyForScalePosition(n); 1100 | o = std::abs(freq - r); 1101 | if (o < s) 1102 | { 1103 | s = o; 1104 | a = n; 1105 | } 1106 | if (i > 0) 1107 | l = r > freq; 1108 | else 1109 | l = r < freq; 1110 | } 1111 | return a; 1112 | } 1113 | 1114 | inline double AbletonScale::frequencyForScalePosition(int scalePosition) 1115 | { 1116 | return referencePitchFreq * pow(2, (centsForScalePosition(scalePosition) - 1117 | centsForScalePosition(referencePitchIndex)) / 1118 | 1200); 1119 | } 1120 | 1121 | inline double AbletonScale::centsForScalePosition(int scalePosition) 1122 | { 1123 | auto n = scale.tones.size(); 1124 | auto t = scale.tones[positive_mod(scalePosition, n)]; 1125 | return t.cents + floor(1.0 * scalePosition / n) * scale.tones.back().cents; 1126 | } 1127 | 1128 | inline AbletonScale readASCLFile(std::string fname) 1129 | { 1130 | std::ifstream inf; 1131 | inf.open(fname); 1132 | if (!inf.is_open()) 1133 | { 1134 | std::string s = "Unable to open file '" + fname + "'"; 1135 | throw TuningError(s); 1136 | } 1137 | 1138 | auto res = readASCLStream(inf); 1139 | res.scale.name = fname; 1140 | return res; 1141 | } 1142 | 1143 | inline AbletonScale parseASCLData(const std::string &d) 1144 | { 1145 | std::istringstream iss(d); 1146 | auto res = readASCLStream(iss); 1147 | res.scale.name = "AbletonScale from patch"; 1148 | return res; 1149 | } 1150 | 1151 | } // namespace Tunings 1152 | #endif 1153 | -------------------------------------------------------------------------------- /tests/alltests.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_RUNNER 2 | #include "catch2.hpp" 3 | 4 | #include "Tunings.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* 11 | ** ToDo 12 | ** tuning with non-contiguous kbm 13 | ** tuning with non-monotonic kbm 14 | ** few known tunings across the whole spectrun 15 | */ 16 | 17 | std::string testFile(std::string fn) { return std::string("tests/data/") + fn; } 18 | 19 | std::vector testSCLs() 20 | { 21 | std::vector res = {{"12-intune.scl", "12-shuffled.scl", "31edo.scl", "6-exact.scl", 22 | "marvel12.scl", "zeus22.scl", "ED4-17.scl", "ED3-17.scl", 23 | "31edo_dos_lineends.scl"}}; 24 | return res; 25 | } 26 | 27 | std::vector testKBMs() 28 | { 29 | std::vector res = {{"empty-note61.kbm", "empty-note69.kbm", 30 | "mapping-a440-constant.kbm", "mapping-a442-7-to-12.kbm", 31 | "mapping-whitekeys-a440.kbm", "mapping-whitekeys-c261.kbm", 32 | "shuffle-a440-constant.kbm"}}; 33 | return res; 34 | } 35 | 36 | TEST_CASE("Loading tuning files") 37 | { 38 | SECTION("Load a 12 tone standard tuning") 39 | { 40 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 41 | REQUIRE(s.count == 12); 42 | // FIXME - write a lot more here obviously 43 | } 44 | 45 | SECTION("Load a 12 tone standard tuning with no description") 46 | { 47 | auto s = Tunings::readSCLFile(testFile("12-intune-nodesc.scl")); 48 | REQUIRE(s.count == 12); 49 | // FIXME - write a lot more here obviously 50 | } 51 | 52 | SECTION("KBM File from text") 53 | { 54 | std::ostringstream oss; 55 | oss << "! A scale file\n" 56 | << "! with zero size\n" 57 | << "0\n" 58 | << "! spanning the keybaord\n" 59 | << "0\n" 60 | << "127\n" 61 | << "! With C60 as constant and A as 452\n" 62 | << "60\n69\n452\n" 63 | << "! and an octave might as well be zero\n" 64 | << "0\n"; 65 | 66 | REQUIRE_NOTHROW(Tunings::parseKBMData(oss.str())); 67 | } 68 | 69 | SECTION("Comments read properly") 70 | { 71 | auto s = Tunings::readSCLFile(testFile("rast.ascl")); 72 | REQUIRE(s.comments.size() == 24); 73 | } 74 | } 75 | 76 | TEST_CASE("Identity Tuning Tests") 77 | { 78 | SECTION("12-intune tunes properly") 79 | { 80 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 81 | REQUIRE(s.count == 12); 82 | Tunings::Tuning t(s); 83 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(1e-10)); 84 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(60) == 32.0); 85 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5.0); 86 | } 87 | 88 | SECTION("12-intune doubles properly") 89 | { 90 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 91 | Tunings::Tuning t(s); 92 | for (int i = 0; i < 12; ++i) 93 | { 94 | int note = -12 * 4 + i; 95 | auto sc = t.frequencyForMidiNoteScaledByMidi0(note); 96 | auto lc = t.logScaledFrequencyForMidiNote(note); 97 | while (note < 200) 98 | { 99 | note += 12; 100 | auto nlc = t.logScaledFrequencyForMidiNote(note); 101 | auto nsc = t.frequencyForMidiNoteScaledByMidi0(note); 102 | REQUIRE(nsc == Approx(sc * 2).margin(1e-8)); 103 | REQUIRE(nlc == Approx(lc + 1).margin(1e-8)); 104 | sc = nsc; 105 | lc = nlc; 106 | } 107 | } 108 | } 109 | 110 | SECTION("Scaling is constant") 111 | { 112 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 113 | Tunings::Tuning t(s); 114 | auto f60 = t.frequencyForMidiNote(60); 115 | auto fs60 = t.frequencyForMidiNoteScaledByMidi0(60); 116 | for (int i = -200; i < 200; ++i) 117 | { 118 | auto f = t.frequencyForMidiNote(i); 119 | auto fs = t.frequencyForMidiNoteScaledByMidi0(i); 120 | REQUIRE(f / fs == f60 / fs60); 121 | } 122 | } 123 | } 124 | 125 | TEST_CASE("Simple Keyboard Remapping Tunes A69") 126 | { 127 | SECTION("A440") 128 | { 129 | auto k = Tunings::tuneA69To(440.0); 130 | Tunings::Tuning t(k); 131 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(1e-10)); 132 | REQUIRE(t.frequencyForMidiNote(60) == Approx(261.625565301).margin(1e-10)); 133 | } 134 | 135 | SECTION("A432") 136 | { 137 | auto k = Tunings::tuneA69To(432.0); 138 | Tunings::Tuning t(k); 139 | REQUIRE(t.frequencyForMidiNote(69) == Approx(432.0).margin(1e-10)); 140 | REQUIRE(t.frequencyForMidiNote(60) == Approx(261.625565301 * 432 / 440).margin(1e-10)); 141 | } 142 | 143 | SECTION("Random As Scale Consistently") 144 | { 145 | Tunings::Tuning ut; 146 | 147 | for (int i = 0; i < 100; ++i) 148 | { 149 | auto fr = 400 + 80.0 * rand() / RAND_MAX; 150 | 151 | auto k = Tunings::tuneA69To(fr); 152 | Tunings::Tuning t(k); 153 | REQUIRE(t.frequencyForMidiNote(69) == Approx(fr).margin(1e-10)); 154 | REQUIRE(t.frequencyForMidiNote(60) == Approx(261.625565301 * fr / 440).margin(1e-10)); 155 | 156 | double ldiff = 157 | t.logScaledFrequencyForMidiNote(69) - ut.logScaledFrequencyForMidiNote(69); 158 | double ratio = t.frequencyForMidiNote(69) / ut.frequencyForMidiNote(69); 159 | 160 | for (int j = -200; j < 200; ++j) 161 | { 162 | REQUIRE(t.logScaledFrequencyForMidiNote(j) - ut.logScaledFrequencyForMidiNote(j) == 163 | Approx(ldiff).margin(1e-8)); 164 | REQUIRE(t.frequencyForMidiNote(69) / ut.frequencyForMidiNote(69) == 165 | Approx(ratio).margin(1e-8)); 166 | } 167 | } 168 | } 169 | } 170 | 171 | TEST_CASE("Internal Constraints between Measures") 172 | { 173 | SECTION("Test All Constraints SCL only") 174 | { 175 | for (auto f : testSCLs()) 176 | { 177 | INFO("Testing Constraints with " << f); 178 | auto s = Tunings::readSCLFile(testFile(f)); 179 | Tunings::Tuning t(s); 180 | 181 | for (int i = 0; i < 127; ++i) 182 | { 183 | REQUIRE(t.frequencyForMidiNote(i) == 184 | t.frequencyForMidiNoteScaledByMidi0(i) * Tunings::MIDI_0_FREQ); 185 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(i) == 186 | pow(2.0, t.logScaledFrequencyForMidiNote(i))); 187 | } 188 | } 189 | } 190 | 191 | SECTION("Test All Constraints KBM only") 192 | { 193 | for (auto f : testKBMs()) 194 | { 195 | INFO("Testing Constraints with " << f); 196 | auto k = Tunings::readKBMFile(testFile(f)); 197 | Tunings::Tuning t(k); 198 | 199 | for (int i = 0; i < 127; ++i) 200 | { 201 | REQUIRE(t.frequencyForMidiNote(i) == 202 | t.frequencyForMidiNoteScaledByMidi0(i) * Tunings::MIDI_0_FREQ); 203 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(i) == 204 | pow(2.0, t.logScaledFrequencyForMidiNote(i))); 205 | } 206 | } 207 | } 208 | 209 | SECTION("Test All Constraints SCL and KBM") 210 | { 211 | for (auto fs : testSCLs()) 212 | for (auto fk : testKBMs()) 213 | { 214 | INFO("Testing Constraints with " << fs << " " << fk); 215 | auto s = Tunings::readSCLFile(testFile(fs)); 216 | auto k = Tunings::readKBMFile(testFile(fk)); 217 | 218 | if (k.octaveDegrees > s.count) 219 | continue; // don't test this verion; trap it below as an error case 220 | 221 | Tunings::Tuning t(s, k); 222 | 223 | for (int i = 0; i < 127; ++i) 224 | { 225 | REQUIRE(t.frequencyForMidiNote(i) == 226 | t.frequencyForMidiNoteScaledByMidi0(i) * Tunings::MIDI_0_FREQ); 227 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(i) == 228 | pow(2.0, t.logScaledFrequencyForMidiNote(i))); 229 | } 230 | } 231 | } 232 | 233 | SECTION("Test All Constraints SCL and KBM in Spain") 234 | { 235 | bool spainAvailable = true; 236 | try 237 | { 238 | std::locale("es_ES"); 239 | } 240 | catch (std::exception &e) 241 | { 242 | INFO("es_ES locale not availale on this machine; skpping test"); 243 | spainAvailable = false; 244 | } 245 | 246 | if (spainAvailable) 247 | { 248 | auto priorLocale = std::locale("").name(); 249 | 250 | std::locale::global(std::locale("es_ES")); 251 | for (auto fs : testSCLs()) 252 | for (auto fk : testKBMs()) 253 | { 254 | INFO("Testing Constraints with " << fs << " " << fk); 255 | auto s = Tunings::readSCLFile(testFile(fs)); 256 | auto k = Tunings::readKBMFile(testFile(fk)); 257 | 258 | if (k.octaveDegrees > s.count) 259 | continue; // don't test this verion; trap it below as an error case 260 | 261 | Tunings::Tuning t(s, k); 262 | 263 | for (int i = 0; i < 127; ++i) 264 | { 265 | REQUIRE(t.frequencyForMidiNote(i) == 266 | t.frequencyForMidiNoteScaledByMidi0(i) * Tunings::MIDI_0_FREQ); 267 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(i) == 268 | pow(2.0, t.logScaledFrequencyForMidiNote(i))); 269 | } 270 | } 271 | std::locale::global(std::locale(priorLocale)); 272 | } 273 | } 274 | } 275 | 276 | TEST_CASE("Several Sample Scales") 277 | { 278 | SECTION("Non Monotonic 12 note") 279 | { 280 | auto s = Tunings::readSCLFile(testFile("12-shuffled.scl")); 281 | Tunings::Tuning t(s); 282 | REQUIRE(s.count == 12); 283 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5); 284 | 285 | std::vector order = {{0, 2, 1, 3, 5, 4, 6, 7, 8, 10, 9, 11, 12}}; 286 | auto l60 = t.logScaledFrequencyForMidiNote(60); 287 | for (size_t i = 0; i < order.size(); ++i) 288 | { 289 | auto li = t.logScaledFrequencyForMidiNote(60 + i); 290 | auto oi = order[i]; 291 | REQUIRE(li - l60 == Approx(oi / 12.0).margin(1e-6)); 292 | } 293 | } 294 | 295 | SECTION("31 edo") 296 | { 297 | auto s = Tunings::readSCLFile(testFile("31edo.scl")); 298 | Tunings::Tuning t(s); 299 | REQUIRE(s.count == 31); 300 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5); 301 | 302 | auto prev = t.logScaledFrequencyForMidiNote(60); 303 | for (int i = 1; i < 31; ++i) 304 | { 305 | auto curr = t.logScaledFrequencyForMidiNote(60 + i); 306 | REQUIRE(curr - prev == Approx(1.0 / 31.0).margin(1e-6)); 307 | prev = curr; 308 | } 309 | } 310 | 311 | SECTION("ED3-17") 312 | { 313 | auto s = Tunings::readSCLFile(testFile("ED3-17.scl")); 314 | Tunings::Tuning t(s); 315 | REQUIRE(s.count == 17); 316 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5); 317 | 318 | auto prev = t.logScaledFrequencyForMidiNote(60); 319 | for (int i = 1; i < 40; ++i) 320 | { 321 | auto curr = t.logScaledFrequencyForMidiNote(60 + i); 322 | REQUIRE(pow(2.0, 17 * (curr - prev)) == Approx(3.0).margin(1e-6)); 323 | prev = curr; 324 | } 325 | } 326 | 327 | SECTION("ED4-17") 328 | { 329 | auto s = Tunings::readSCLFile(testFile("ED4-17.scl")); 330 | Tunings::Tuning t(s); 331 | REQUIRE(s.count == 17); 332 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5); 333 | 334 | auto prev = t.logScaledFrequencyForMidiNote(60); 335 | for (int i = 1; i < 40; ++i) 336 | { 337 | auto curr = t.logScaledFrequencyForMidiNote(60 + i); 338 | REQUIRE(pow(2.0, 17 * (curr - prev)) == Approx(4.0).margin(1e-6)); 339 | prev = curr; 340 | } 341 | } 342 | 343 | SECTION("6 exact") 344 | { 345 | auto s = Tunings::readSCLFile(testFile("6-exact.scl")); 346 | Tunings::Tuning t(s); 347 | REQUIRE(s.count == 6); 348 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5); 349 | 350 | std::vector knownValues = {{0, 0.22239, 0.41504, 0.58496, 0.73697, 0.87447, 1.0}}; 351 | 352 | for (size_t i = 0; i < knownValues.size(); ++i) 353 | REQUIRE(t.logScaledFrequencyForMidiNote(60 + i) == 354 | Approx(t.logScaledFrequencyForMidiNote(60) + knownValues[i]).margin(1e-5)); 355 | } 356 | 357 | SECTION("Carlos Alpha (one step scale)") 358 | { 359 | auto s = Tunings::readSCLFile(testFile("carlos-alpha.scl")); 360 | Tunings::Tuning t(s); 361 | REQUIRE(s.count == 1); 362 | REQUIRE(t.logScaledFrequencyForMidiNote(60) == 5); 363 | auto diff = pow(2.0, 78.0 / 1200.0); 364 | for (int i = 30; i < 80; ++i) 365 | { 366 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(i) * diff == 367 | Approx(t.frequencyForMidiNoteScaledByMidi0(i + 1)).margin(1e-5)); 368 | } 369 | } 370 | } 371 | 372 | TEST_CASE("Remapping frequency with non-12-length scales") 373 | { 374 | SECTION("6 exact") 375 | { 376 | auto s = Tunings::readSCLFile(testFile("6-exact.scl")); 377 | Tunings::Tuning t(s); 378 | 379 | for (int i = 0; i < 100; ++i) 380 | { 381 | int mn = rand() % 40 + 40; 382 | double freq = 150 + 300.0 * rand() / RAND_MAX; 383 | INFO("Setting " << mn << " to " << freq); 384 | auto k = Tunings::tuneNoteTo(mn, freq); 385 | Tunings::Tuning mapped(s, k); 386 | 387 | REQUIRE(mapped.frequencyForMidiNote(mn) == Approx(freq).margin(1e-6)); 388 | 389 | // This scale is monotonic so test monotonicity still 390 | for (int i = 1; i < 127; ++i) 391 | { 392 | INFO("About to test at " << i); 393 | if (mapped.frequencyForMidiNote(i) > 1) 394 | REQUIRE(mapped.frequencyForMidiNote(i) > mapped.frequencyForMidiNote(i - 1)); 395 | } 396 | 397 | double n60ldiff = 398 | t.logScaledFrequencyForMidiNote(60) - mapped.logScaledFrequencyForMidiNote(60); 399 | for (int j = 0; j < 128; ++j) 400 | { 401 | REQUIRE(t.logScaledFrequencyForMidiNote(j) - 402 | mapped.logScaledFrequencyForMidiNote(j) == 403 | Approx(n60ldiff).margin(1e-6)); 404 | } 405 | } 406 | } 407 | 408 | SECTION("31 edo") 409 | { 410 | auto s = Tunings::readSCLFile(testFile("31edo.scl")); 411 | Tunings::Tuning t(s); 412 | 413 | for (int i = 0; i < 100; ++i) 414 | { 415 | int mn = rand() % 20 + 50; 416 | double freq = 150 + 300.0 * rand() / RAND_MAX; 417 | INFO("Setting " << mn << " to " << freq); 418 | auto k = Tunings::tuneNoteTo(mn, freq); 419 | Tunings::Tuning mapped(s, k); 420 | 421 | REQUIRE(mapped.frequencyForMidiNote(mn) == Approx(freq).margin(1e-6)); 422 | 423 | // This scale is monotonic so test monotonicity still 424 | for (int i = 1; i < 127; ++i) 425 | { 426 | INFO("About to test at " << i); 427 | if (mapped.frequencyForMidiNote(i) > 1) 428 | REQUIRE(mapped.frequencyForMidiNote(i) > mapped.frequencyForMidiNote(i - 1)); 429 | } 430 | 431 | double n60ldiff = 432 | t.logScaledFrequencyForMidiNote(60) - mapped.logScaledFrequencyForMidiNote(60); 433 | for (int j = 0; j < 128; ++j) 434 | { 435 | REQUIRE(t.logScaledFrequencyForMidiNote(j) - 436 | mapped.logScaledFrequencyForMidiNote(j) == 437 | Approx(n60ldiff).margin(1e-6)); 438 | } 439 | } 440 | } 441 | 442 | SECTION("ED4-17") 443 | { 444 | auto s = Tunings::readSCLFile(testFile("ED4-17.scl")); 445 | Tunings::Tuning t(s); 446 | 447 | for (int i = 0; i < 100; ++i) 448 | { 449 | int mn = rand() % 40 + 40; 450 | double freq = 150 + 300.0 * rand() / RAND_MAX; 451 | INFO("Setting " << mn << " to " << freq); 452 | auto k = Tunings::tuneNoteTo(mn, freq); 453 | Tunings::Tuning mapped(s, k); 454 | 455 | REQUIRE(mapped.frequencyForMidiNote(mn) == Approx(freq).margin(1e-6)); 456 | 457 | // This scale is monotonic so test monotonicity still 458 | for (int i = 1; i < 127; ++i) 459 | { 460 | INFO("About to test at " << i); 461 | if (mapped.frequencyForMidiNote(i) > 1) 462 | REQUIRE(mapped.frequencyForMidiNote(i) > mapped.frequencyForMidiNote(i - 1)); 463 | } 464 | 465 | double n60ldiff = 466 | t.logScaledFrequencyForMidiNote(60) - mapped.logScaledFrequencyForMidiNote(60); 467 | for (int j = 0; j < 128; ++j) 468 | { 469 | REQUIRE(t.logScaledFrequencyForMidiNote(j) - 470 | mapped.logScaledFrequencyForMidiNote(j) == 471 | Approx(n60ldiff).margin(1e-6)); 472 | } 473 | } 474 | } 475 | 476 | SECTION("ED3-17") 477 | { 478 | auto s = Tunings::readSCLFile(testFile("ED3-17.scl")); 479 | Tunings::Tuning t(s); 480 | 481 | for (int i = 0; i < 100; ++i) 482 | { 483 | int mn = rand() % 40 + 40; 484 | double freq = 150 + 300.0 * rand() / RAND_MAX; 485 | INFO("Setting " << mn << " to " << freq); 486 | auto k = Tunings::tuneNoteTo(mn, freq); 487 | Tunings::Tuning mapped(s, k); 488 | 489 | REQUIRE(mapped.frequencyForMidiNote(mn) == Approx(freq).margin(1e-6)); 490 | 491 | // This scale is monotonic so test monotonicity still 492 | for (int i = 1; i < 127; ++i) 493 | { 494 | INFO("About to test at " << i); 495 | if (mapped.frequencyForMidiNote(i) > 1) 496 | REQUIRE(mapped.frequencyForMidiNote(i) > mapped.frequencyForMidiNote(i - 1)); 497 | } 498 | 499 | double n60ldiff = 500 | t.logScaledFrequencyForMidiNote(60) - mapped.logScaledFrequencyForMidiNote(60); 501 | for (int j = 0; j < 128; ++j) 502 | { 503 | REQUIRE(t.logScaledFrequencyForMidiNote(j) - 504 | mapped.logScaledFrequencyForMidiNote(j) == 505 | Approx(n60ldiff).margin(1e-6)); 506 | } 507 | } 508 | } 509 | } 510 | 511 | TEST_CASE("KBMs with Gaps") 512 | { 513 | SECTION("12 Intune with Gap") 514 | { 515 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 516 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-c261.kbm")); 517 | Tunings::Tuning t(s); 518 | Tunings::Tuning tm(s, k); 519 | 520 | REQUIRE(s.count == 12); 521 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(1e-6)); 522 | 523 | // That KBM maps the white keys to the chromatic start so 524 | std::vector> maps = {{60, 60}, {61, 62}, {62, 64}, {63, 65}, 525 | {64, 67}, {65, 69}, {66, 71}}; 526 | for (auto p : maps) 527 | { 528 | REQUIRE(t.logScaledFrequencyForMidiNote(p.first) == 529 | Approx(tm.logScaledFrequencyForMidiNote(p.second)).margin(1e-5)); 530 | } 531 | } 532 | } 533 | 534 | TEST_CASE("Scala KBMs from Issue 42") 535 | { 536 | SECTION("Piano.kbm") 537 | { 538 | auto k = Tunings::readKBMFile(testFile("piano.kbm")); 539 | REQUIRE(k.count == 0); 540 | } 541 | 542 | SECTION("128.kbm") 543 | { 544 | auto k = Tunings::readKBMFile(testFile("128.kbm")); 545 | REQUIRE(k.count == 0); 546 | } 547 | } 548 | 549 | TEST_CASE("KBM ReOrdering") 550 | { 551 | SECTION("Non Monotonic KBM note") 552 | { 553 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 554 | auto k = Tunings::readKBMFile(testFile("shuffle-a440-constant.kbm")); 555 | Tunings::Tuning t(s, k); 556 | 557 | REQUIRE(s.count == 12); 558 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(1e-6)); 559 | 560 | std::vector order = {{0, 2, 1, 3, 4, 6, 5, 7, 8, 9, 11, 10, 12}}; 561 | auto l60 = t.logScaledFrequencyForMidiNote(60); 562 | for (size_t i = 0; i < order.size(); ++i) 563 | { 564 | INFO("Testing note " << i); 565 | auto li = t.logScaledFrequencyForMidiNote(60 + i); 566 | auto oi = order[i]; 567 | REQUIRE(li - l60 == Approx(oi / 12.0).margin(1e-6)); 568 | } 569 | } 570 | } 571 | 572 | TEST_CASE("Exceptions and Bad Files") 573 | { 574 | SECTION("Read Non-present files") 575 | { 576 | REQUIRE_THROWS_AS(Tunings::readSCLFile("blahlfdsfds"), Tunings::TuningError); 577 | REQUIRE_THROWS_AS(Tunings::readKBMFile("blahlfdsfds"), Tunings::TuningError); 578 | 579 | // Lets make sure what is reasonable 580 | try 581 | { 582 | Tunings::readSCLFile("MISSING"); 583 | } 584 | catch (const Tunings::TuningError &e) 585 | { 586 | REQUIRE(std::string(e.what()) == "Unable to open file 'MISSING'"); 587 | } 588 | 589 | try 590 | { 591 | Tunings::readKBMFile("MISSING"); 592 | } 593 | catch (const Tunings::TuningError &e) 594 | { 595 | REQUIRE(std::string(e.what()) == "Unable to open file 'MISSING'"); 596 | } 597 | } 598 | 599 | SECTION("Mappings bigger than Scales Throw") 600 | { 601 | bool testedAtLeastOne = false; 602 | for (auto fs : testSCLs()) 603 | for (auto fk : testKBMs()) 604 | { 605 | INFO("Looking for mis-sized pairs " << fs << " " << fk); 606 | auto s = Tunings::readSCLFile(testFile(fs)); 607 | auto k = Tunings::readKBMFile(testFile(fk)); 608 | 609 | if (k.octaveDegrees <= s.count) 610 | continue; 611 | 612 | testedAtLeastOne = true; 613 | REQUIRE_THROWS_AS(Tunings::Tuning(s, k), Tunings::TuningError); 614 | } 615 | REQUIRE(testedAtLeastOne); 616 | } 617 | 618 | SECTION("Bad SCL") 619 | { 620 | // Trailing data is OK 621 | REQUIRE_NOTHROW(Tunings::readSCLFile(testFile("bad/extraline.scl"))); 622 | 623 | REQUIRE_THROWS_AS(Tunings::readSCLFile(testFile("bad/badnote.scl")), Tunings::TuningError); 624 | REQUIRE_THROWS_AS(Tunings::readSCLFile(testFile("bad/blanknote.scl")), 625 | Tunings::TuningError); 626 | REQUIRE_THROWS_AS(Tunings::readSCLFile(testFile("bad/missingnote.scl")), 627 | Tunings::TuningError); 628 | } 629 | 630 | SECTION("Bad KBM") 631 | { 632 | REQUIRE_THROWS_AS(Tunings::readKBMFile(testFile("bad/blank-line.kbm")), 633 | Tunings::TuningError); 634 | REQUIRE_THROWS_AS(Tunings::readKBMFile(testFile("bad/empty-bad.kbm")), 635 | Tunings::TuningError); 636 | REQUIRE_THROWS_AS(Tunings::readKBMFile(testFile("bad/garbage-key.kbm")), 637 | Tunings::TuningError); 638 | REQUIRE_NOTHROW(Tunings::readKBMFile(testFile("bad/empty-extra.kbm"))); 639 | REQUIRE_NOTHROW(Tunings::readKBMFile(testFile("bad/extraline-long.kbm"))); 640 | REQUIRE_THROWS_AS(Tunings::readKBMFile(testFile("bad/missing-note.kbm")), 641 | Tunings::TuningError); 642 | } 643 | 644 | SECTION("Bad SCL Data") 645 | { 646 | REQUIRE_THROWS_AS(Tunings::parseSCLData(""), Tunings::TuningError); 647 | 648 | try 649 | { 650 | Tunings::parseSCLData(""); 651 | REQUIRE(false); 652 | } 653 | catch (const Tunings::TuningError &e) 654 | { 655 | auto s = std::string(e.what()); 656 | INFO(s); 657 | REQUIRE(s.find("Incomplete SCL") != std::string::npos); 658 | REQUIRE(s.find("Only able to read 0 lines") != std::string::npos); 659 | } 660 | } 661 | 662 | SECTION("Bad KBM Data") 663 | { 664 | REQUIRE_THROWS_AS(Tunings::parseKBMData(""), Tunings::TuningError); 665 | 666 | try 667 | { 668 | Tunings::parseKBMData(""); 669 | REQUIRE(false); 670 | } 671 | catch (const Tunings::TuningError &e) 672 | { 673 | auto s = std::string(e.what()); 674 | INFO(s); 675 | REQUIRE(s.find("Incomplete KBM") != std::string::npos); 676 | REQUIRE(s.find("Only able to read 0 lines") != std::string::npos); 677 | } 678 | } 679 | } 680 | 681 | TEST_CASE("Built in Generators") 682 | { 683 | SECTION("ED2") 684 | { 685 | auto s = Tunings::evenDivisionOfSpanByM(2, 12); 686 | REQUIRE(s.count == 12); 687 | REQUIRE(s.rawText.size() > 1); 688 | Tunings::Tuning ut; 689 | Tunings::Tuning t(s); 690 | for (int i = 0; i < 128; ++i) 691 | REQUIRE(t.logScaledFrequencyForMidiNote(i) == ut.logScaledFrequencyForMidiNote(i)); 692 | } 693 | 694 | SECTION("ED3-17") 695 | { 696 | auto s = Tunings::evenDivisionOfSpanByM(3, 17); 697 | auto sf = Tunings::readSCLFile(testFile("ED3-17.scl")); 698 | 699 | Tunings::Tuning ut(sf); 700 | Tunings::Tuning t(s); 701 | for (int i = 0; i < 128; ++i) 702 | REQUIRE(t.logScaledFrequencyForMidiNote(i) == 703 | Approx(ut.logScaledFrequencyForMidiNote(i)).margin(1e-6)); 704 | } 705 | 706 | SECTION("ED4-17") 707 | { 708 | auto s = Tunings::evenDivisionOfSpanByM(4, 17); 709 | auto sf = Tunings::readSCLFile(testFile("ED4-17.scl")); 710 | 711 | Tunings::Tuning ut(sf); 712 | Tunings::Tuning t(s); 713 | for (int i = 0; i < 128; ++i) 714 | REQUIRE(t.logScaledFrequencyForMidiNote(i) == 715 | Approx(ut.logScaledFrequencyForMidiNote(i)).margin(1e-6)); 716 | } 717 | 718 | SECTION("Constraints on random EDN-M") 719 | { 720 | for (int i = 0; i < 100; ++i) 721 | { 722 | int Span = rand() % 7 + 2; 723 | int M = rand() % 50 + 3; 724 | INFO("Constructing " << i << " scale ED " << Span << " - " << M); 725 | 726 | auto s = Tunings::evenDivisionOfSpanByM(Span, M); 727 | 728 | REQUIRE(s.count == M); 729 | REQUIRE(s.rawText.size() > 1); 730 | 731 | Tunings::Tuning t(s); 732 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(60) * Span == 733 | Approx(t.frequencyForMidiNoteScaledByMidi0(60 + M)).margin(1e-7)); 734 | 735 | auto d0 = t.logScaledFrequencyForMidiNote(1) - t.logScaledFrequencyForMidiNote(0); 736 | for (auto i = 1; i < 128; ++i) 737 | { 738 | auto d = 739 | t.logScaledFrequencyForMidiNote(i) - t.logScaledFrequencyForMidiNote(i - 1); 740 | REQUIRE(d == Approx(d0).margin(1e-7)); 741 | } 742 | } 743 | } 744 | 745 | SECTION("EDMN Errors") 746 | { 747 | REQUIRE_THROWS_AS(Tunings::evenDivisionOfSpanByM(0, 12), Tunings::TuningError); 748 | REQUIRE_THROWS_AS(Tunings::evenDivisionOfSpanByM(2, 0), Tunings::TuningError); 749 | REQUIRE_THROWS_AS(Tunings::evenDivisionOfSpanByM(0, 0), Tunings::TuningError); 750 | 751 | REQUIRE_THROWS_AS(Tunings::evenDivisionOfSpanByM(-1, 12), Tunings::TuningError); 752 | REQUIRE_THROWS_AS(Tunings::evenDivisionOfSpanByM(2, -1), Tunings::TuningError); 753 | REQUIRE_THROWS_AS(Tunings::evenDivisionOfSpanByM(-1, -1), Tunings::TuningError); 754 | } 755 | 756 | SECTION("KBM Generator") 757 | { 758 | for (int i = 0; i < 100; ++i) 759 | { 760 | int n = rand() % 60 + 30; 761 | int fr = 1000.0 * rand() / RAND_MAX + 50; 762 | auto k = Tunings::tuneNoteTo(n, fr); 763 | REQUIRE(k.tuningConstantNote == n); 764 | REQUIRE(k.tuningFrequency == fr); 765 | REQUIRE(k.tuningPitch == k.tuningFrequency / Tunings::MIDI_0_FREQ); 766 | REQUIRE(k.rawText.size() > 1); 767 | } 768 | } 769 | } 770 | 771 | TEST_CASE("Dos Line Endings and Blanks") 772 | { 773 | SECTION("SCL") { REQUIRE_NOTHROW(Tunings::readSCLFile(testFile("12-intune-dosle.scl"))); } 774 | 775 | SECTION("Properly read a file with DOS line endings") 776 | { 777 | auto s = Tunings::readSCLFile(testFile("31edo_dos_lineends.scl")); 778 | REQUIRE(s.count == 31); 779 | INFO("If coded with std::getline this will contain a \\r on unixes") 780 | REQUIRE(s.description == "31 equal divisions of octave"); 781 | 782 | // the parsing should ive the same floatvalues independent of crlf status obviously 783 | auto q = Tunings::readSCLFile(testFile("31edo.scl")); 784 | for (int i = 0; i < q.count; ++i) 785 | { 786 | REQUIRE(q.tones[i].floatValue == s.tones[i].floatValue); 787 | } 788 | } 789 | 790 | SECTION("KBM") 791 | { 792 | REQUIRE_NOTHROW(Tunings::readKBMFile(testFile("empty-note69-dosle.kbm"))); 793 | auto k = Tunings::readKBMFile(testFile("empty-note69-dosle.kbm")); 794 | REQUIRE(k.tuningConstantNote == 69); 795 | } 796 | 797 | SECTION("Blank SCL") 798 | { 799 | REQUIRE_THROWS_AS(Tunings::parseSCLData(""), Tunings::TuningError); 800 | 801 | // but what if we do construct a bad one? 802 | Tunings::Scale s; 803 | s.count = 0; 804 | s.tones.clear(); 805 | REQUIRE_THROWS_AS(Tunings::Tuning(s), Tunings::TuningError); 806 | } 807 | } 808 | 809 | TEST_CASE("Tone API") 810 | { 811 | // This is exercised a million times above so just a light test here 812 | SECTION("Valid Tones") 813 | { 814 | auto t1 = Tunings::toneFromString("130.0"); 815 | REQUIRE(t1.type == Tunings::Tone::kToneCents); 816 | REQUIRE(t1.cents == 130.0); 817 | REQUIRE(t1.floatValue == 130.0 / 1200.0 + 1.0); 818 | 819 | auto t2 = Tunings::toneFromString("7/2"); 820 | REQUIRE(t2.type == Tunings::Tone::kToneRatio); 821 | REQUIRE(t2.ratio_d == 2); 822 | REQUIRE(t2.ratio_n == 7); 823 | REQUIRE(t2.floatValue == Approx(log(7.0 / 2.0) / log(2.0) + 1.0).margin(1e-6)); 824 | 825 | auto t3 = Tunings::toneFromString("3"); 826 | REQUIRE(t3.type == Tunings::Tone::kToneRatio); 827 | REQUIRE(t3.ratio_d == 1); 828 | REQUIRE(t3.ratio_n == 3); 829 | REQUIRE(t3.floatValue == Approx(log(3.0 / 1.0) / log(2.0) + 1.0).margin(1e-6)); 830 | 831 | auto t4 = Tunings::toneFromString("555/524 ! c# 138.75 Hz"); 832 | REQUIRE(t4.type == Tunings::Tone::kToneRatio); 833 | REQUIRE(t4.ratio_d == 524); 834 | REQUIRE(t4.ratio_n == 555); 835 | REQUIRE(t4.floatValue == Approx(log(555.0 / 524.0) / log(2.0) + 1.0).margin(1e-6)); 836 | } 837 | 838 | SECTION("Ridiculously Long Fraction Tones") 839 | { 840 | uint64_t top{3}, bottom{2}; 841 | for (auto q = 0; q < 18; ++q) 842 | { 843 | auto frac = std::to_string(top) + "/" + std::to_string(bottom); 844 | INFO("Parsing " << frac << " at " << q); 845 | auto t = Tunings::toneFromString(frac); 846 | REQUIRE(t.type == Tunings::Tone::kToneRatio); 847 | REQUIRE(t.ratio_n == top); 848 | REQUIRE(t.ratio_d == bottom); 849 | top = top * 10; 850 | bottom = bottom * 10; 851 | } 852 | } 853 | 854 | SECTION("Error Tones") 855 | { 856 | REQUIRE_THROWS_AS(Tunings::toneFromString("Not a number"), Tunings::TuningError); 857 | // FIXME - these cases doesn't throw yet 858 | // REQUIRE_THROWS_AS( Tunings::toneFromString( "100.200 with extra stuff" ), 859 | // Tunings::TuningError ); REQUIRE_THROWS_AS( Tunings::toneFromString( "7/4/2" ), 860 | // Tunings::TuningError ); REQUIRE_THROWS_AS( Tunings::toneFromString( "7*2" ), 861 | // Tunings::TuningError ); 862 | } 863 | } 864 | 865 | TEST_CASE("Scale Position") 866 | { 867 | SECTION("Untuned") 868 | { 869 | Tunings::Tuning t; 870 | for (int i = 0; i < 127; ++i) 871 | REQUIRE(t.scalePositionForMidiNote(i) == i % 12); 872 | } 873 | 874 | SECTION("Untuned, Mapped") 875 | { 876 | { 877 | auto k = Tunings::startScaleOnAndTuneNoteTo(60, 69, 440); 878 | Tunings::Tuning t(k); 879 | 880 | for (int i = 0; i < 127; ++i) 881 | { 882 | INFO("Check " << i << " " << t.scalePositionForMidiNote(i)); 883 | REQUIRE(t.scalePositionForMidiNote(i) == i % 12); 884 | } 885 | } 886 | for (int j = 0; j < 100; ++j) 887 | { 888 | int n = rand() % 60 + 30; 889 | auto k = Tunings::startScaleOnAndTuneNoteTo(n, 69, 440); 890 | Tunings::Tuning t(k); 891 | 892 | INFO("Checking scale position with 0 mapped to " << n << " " << n % 12); 893 | for (int i = 0; i < 127; ++i) 894 | { 895 | INFO("Check " << i << " " << t.scalePositionForMidiNote(i) << " with n=" << n); 896 | REQUIRE(t.scalePositionForMidiNote(i) == (i + 12 - n % 12) % 12); 897 | } 898 | } 899 | 900 | { 901 | // Check whitekeys 902 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-c261.kbm")); 903 | Tunings::Tuning t(k); 904 | 905 | // That KBM maps the white keys to the chromatic start so 906 | std::vector> maps = {{0, 0}, {2, 1}, {4, 2}, {5, 3}, 907 | {7, 4}, {9, 5}, {11, 6}}; 908 | for (int i = 0; i < 127; ++i) 909 | { 910 | auto spn = t.scalePositionForMidiNote(i); 911 | int expected = -1; 912 | for (auto p : maps) 913 | if (i % 12 == p.first) 914 | expected = p.second; 915 | INFO("Checking SPN at " << i << " " << expected << " " << spn); 916 | REQUIRE(spn == expected); 917 | } 918 | } 919 | } 920 | 921 | SECTION("Tuned, Unmapped") 922 | { 923 | // Check longer and shorter scales 924 | { 925 | auto s = Tunings::readSCLFile(testFile("zeus22.scl")); 926 | Tunings::Tuning t(s); 927 | 928 | int off = 60; 929 | while (off > 0) 930 | off -= s.count; 931 | 932 | for (int i = 0; i < 127; ++i) 933 | { 934 | INFO("Check " << i << " " << t.scalePositionForMidiNote(i) << off); 935 | REQUIRE(t.scalePositionForMidiNote(i) == (i - off) % s.count); 936 | } 937 | } 938 | 939 | // Check longer and shorter scales 940 | { 941 | auto s = Tunings::readSCLFile(testFile("6-exact.scl")); 942 | Tunings::Tuning t(s); 943 | 944 | int off = 60; 945 | while (off >= 0) 946 | off -= s.count; 947 | 948 | for (int i = 0; i < 127; ++i) 949 | { 950 | INFO("Check " << i << " " << t.scalePositionForMidiNote(i) << off); 951 | REQUIRE(t.scalePositionForMidiNote(i) == (i - off) % s.count); 952 | } 953 | } 954 | } 955 | 956 | SECTION("Tuned, Mapped") 957 | { 958 | // And check some combos 959 | for (int j = 0; j < 100; ++j) 960 | { 961 | int n = rand() % 60 + 30; 962 | 963 | auto s = Tunings::readSCLFile(testFile("zeus22.scl")); 964 | auto k = Tunings::startScaleOnAndTuneNoteTo(n, 69, 440); 965 | Tunings::Tuning t(s, k); 966 | 967 | int off = n; 968 | while (off > 0) 969 | off -= s.count; 970 | 971 | for (int i = 0; i < 127; ++i) 972 | { 973 | INFO("Check " << i << " " << t.scalePositionForMidiNote(i) << " " << off 974 | << " n=" << n); 975 | REQUIRE(t.scalePositionForMidiNote(i) == (i - off) % s.count); 976 | } 977 | } 978 | } 979 | } 980 | 981 | TEST_CASE("Default KBM Constructor has Right Base") 982 | { 983 | SECTION("All Scales with Default KBM") 984 | { 985 | for (auto scl : testSCLs()) 986 | { 987 | INFO("Loading SCL " << scl); 988 | auto s = Tunings::readSCLFile(testFile(scl)); 989 | Tunings::Tuning t(s); 990 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(60) == 32); 991 | } 992 | } 993 | } 994 | 995 | TEST_CASE("Different KBM period from Scale period") 996 | { 997 | SECTION("31Edo with mean tone mapping") 998 | { 999 | /* 1000 | * Even though we have a 31 note octave we have 12 key mapping. 1001 | */ 1002 | auto s = Tunings::readSCLFile(testFile("31edo.scl")); 1003 | auto k = Tunings::readKBMFile(testFile("31edo_meantone.kbm")); 1004 | 1005 | Tunings::Tuning t(s, k); 1006 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0)); 1007 | REQUIRE(t.frequencyForMidiNote(69 + 12) == Approx(880.0)); 1008 | } 1009 | 1010 | SECTION("Perfect 5th UnMapped") 1011 | { 1012 | auto s = Tunings::readSCLFile(testFile("12-ET-P5.scl")); 1013 | Tunings::Tuning t(s); 1014 | for (int i = 60 - 36; i < 127; i += 12) 1015 | { 1016 | INFO("Checking perfect 5th at " << i); 1017 | auto f = t.frequencyForMidiNote(i); 1018 | auto f5 = t.frequencyForMidiNote(i + 7); 1019 | REQUIRE(f5 == Approx(f * 1.5).margin(1e-6)); 1020 | } 1021 | } 1022 | 1023 | SECTION("Perfect 5th 07 mapping") 1024 | { 1025 | auto s = Tunings::readSCLFile(testFile("12-ET-P5.scl")); 1026 | auto k = Tunings::readKBMFile(testFile("mapping-n60-fifths.kbm")); 1027 | Tunings::Tuning t(s, k); 1028 | 1029 | for (int i = 60; i < 70; i += 2) 1030 | { 1031 | INFO("Checking perfect 5th at " << i); 1032 | auto f = t.frequencyForMidiNote(i); 1033 | auto f5 = t.frequencyForMidiNote(i + 1); 1034 | REQUIRE(f5 == Approx(f * 1.5).margin(1e-6)); 1035 | } 1036 | } 1037 | } 1038 | 1039 | TEST_CASE("KBM Constructor RawText") 1040 | { 1041 | SECTION("KBM") 1042 | { 1043 | auto k = Tunings::KeyboardMapping(); 1044 | INFO("Raw text is " << k.rawText); 1045 | auto kparse = Tunings::parseKBMData(k.rawText); 1046 | REQUIRE(k.count == kparse.count); 1047 | REQUIRE(k.firstMidi == kparse.firstMidi); 1048 | REQUIRE(k.lastMidi == kparse.lastMidi); 1049 | REQUIRE(k.middleNote == kparse.middleNote); 1050 | REQUIRE(k.tuningConstantNote == kparse.tuningConstantNote); 1051 | REQUIRE(k.tuningFrequency == Approx(kparse.tuningFrequency)); 1052 | REQUIRE(k.octaveDegrees == kparse.octaveDegrees); 1053 | } 1054 | } 1055 | 1056 | TEST_CASE("Skipped Note API") 1057 | { 1058 | SECTION("Default Tuning skips Nothing") 1059 | { 1060 | auto t = Tunings::Tuning(); 1061 | for (int i = 0; i < 128; ++i) 1062 | REQUIRE(t.isMidiNoteMapped(i)); 1063 | } 1064 | 1065 | SECTION("SCL-only Tuning skips Nothing") 1066 | { 1067 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1068 | auto t = Tunings::Tuning(s); 1069 | for (int i = 0; i < 128; ++i) 1070 | REQUIRE(t.isMidiNoteMapped(i)); 1071 | } 1072 | 1073 | SECTION("KBM-only Tuning absent skips skips Nothing") 1074 | { 1075 | auto k = Tunings::readKBMFile(testFile("empty-note69.kbm")); 1076 | auto t = Tunings::Tuning(k); 1077 | for (int i = 0; i < 128; ++i) 1078 | REQUIRE(t.isMidiNoteMapped(i)); 1079 | } 1080 | 1081 | SECTION("Fully Mapped") 1082 | { 1083 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1084 | auto k = Tunings::readKBMFile(testFile("empty-note69.kbm")); 1085 | auto t = Tunings::Tuning(s, k); 1086 | for (int i = 0; i < 128; ++i) 1087 | REQUIRE(t.isMidiNoteMapped(i)); 1088 | } 1089 | 1090 | SECTION("Gaps in the Maps") 1091 | { 1092 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1093 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-a440.kbm")); 1094 | auto t = Tunings::Tuning(s, k); 1095 | for (int k = 0; k < 128; ++k) 1096 | { 1097 | int i = k % 12; 1098 | bool isOn = i == 0 || i == 2 || i == 4 || i == 5 || i == 7 || i == 9 || i == 11; 1099 | INFO(k << " scpos=" << i << " isOn = " << isOn); 1100 | REQUIRE(t.isMidiNoteMapped(k) == isOn); 1101 | } 1102 | } 1103 | 1104 | SECTION("Gaps in the Maps KBM Only") 1105 | { 1106 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-a440.kbm")); 1107 | auto t = Tunings::Tuning(k); 1108 | for (int k = 0; k < 128; ++k) 1109 | { 1110 | int i = k % 12; 1111 | bool isOn = i == 0 || i == 2 || i == 4 || i == 5 || i == 7 || i == 9 || i == 11; 1112 | INFO(k << " scpos=" << i << " isOn = " << isOn); 1113 | REQUIRE(t.isMidiNoteMapped(k) == isOn); 1114 | } 1115 | } 1116 | 1117 | SECTION("Tuning with Gaps") 1118 | { 1119 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1120 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-a440.kbm")); 1121 | auto t = Tunings::Tuning(s, k); 1122 | for (int k = 2; k < 128; ++k) 1123 | { 1124 | int i = k % 12; 1125 | bool isOn = i == 0 || i == 2 || i == 4 || i == 5 || i == 7 || i == 9 || i == 11; 1126 | int priorOn = (i == 0 || i == 5) ? 1 : 2; 1127 | INFO(k << " scpos=" << i << " isOn = " << isOn << " priorOn " << priorOn); 1128 | 1129 | if (isOn) 1130 | REQUIRE(t.logScaledFrequencyForMidiNote(k) > 1131 | t.logScaledFrequencyForMidiNote(k - priorOn)); 1132 | } 1133 | } 1134 | 1135 | SECTION("Tuning with Gaps and Interpolation") 1136 | { 1137 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1138 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-a440.kbm")); 1139 | auto t = Tunings::Tuning(s, k).withSkippedNotesInterpolated(); 1140 | for (int k = 2; k < 128; ++k) 1141 | { 1142 | // Now we have filled in, all the APIs should be monotonic 1143 | INFO("Testing monotnicity note " << k); 1144 | int i = k % 12; 1145 | bool isOn = i == 0 || i == 2 || i == 4 || i == 5 || i == 7 || i == 9 || i == 11; 1146 | REQUIRE(t.isMidiNoteMapped(k) == isOn); 1147 | 1148 | REQUIRE(t.logScaledFrequencyForMidiNote(k) > t.logScaledFrequencyForMidiNote(k - 1)); 1149 | REQUIRE(t.frequencyForMidiNote(k) > t.frequencyForMidiNote(k - 1)); 1150 | REQUIRE(t.frequencyForMidiNoteScaledByMidi0(k) > 1151 | t.frequencyForMidiNoteScaledByMidi0(k - 1)); 1152 | } 1153 | } 1154 | } 1155 | 1156 | TEST_CASE("Skipped Note and Root") 1157 | { 1158 | SECTION("Tuning from 60 works") 1159 | { 1160 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1161 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-a440.kbm")); 1162 | auto t = Tunings::Tuning(s, k); 1163 | REQUIRE(t.isMidiNoteMapped(60)); 1164 | REQUIRE(t.isMidiNoteMapped(69)); 1165 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(0.01)); 1166 | REQUIRE(t.frequencyForMidiNote(60) == Approx(246.9416506281).margin(0.01)); 1167 | } 1168 | 1169 | SECTION("Tuning from 59 throws") 1170 | { 1171 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1172 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-from-59-a440.kbm")); 1173 | REQUIRE_THROWS(Tunings::Tuning(s, k)); 1174 | } 1175 | 1176 | SECTION("Tuning from 59 no throw") 1177 | { 1178 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1179 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-from-59-a440.kbm")); 1180 | auto t = Tunings::Tuning(s, k, true); 1181 | REQUIRE(t.frequencyForMidiNote(59) == Approx(246.94).margin(0.01)); 1182 | } 1183 | 1184 | SECTION("Tuning from 59 altmapping") 1185 | { 1186 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1187 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeysalt-from-59-a440.kbm")); 1188 | REQUIRE_NOTHROW(Tunings::Tuning(s, k, true)); 1189 | REQUIRE_THROWS(Tunings::Tuning(s, k, false)); 1190 | REQUIRE_THROWS(Tunings::Tuning(s, k)); 1191 | auto t = Tunings::Tuning(s, k, true); 1192 | REQUIRE(t.frequencyForMidiNote(59) == Approx(440.0 * pow(2.f, -(5.5 / 12))).margin(0.01)); 1193 | } 1194 | 1195 | SECTION("Tuning from 48 works") 1196 | { 1197 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1198 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-from-48-a440.kbm")); 1199 | auto t = Tunings::Tuning(s, k); 1200 | REQUIRE(t.isMidiNoteMapped(60)); 1201 | REQUIRE(t.isMidiNoteMapped(69)); 1202 | REQUIRE(t.frequencyForMidiNote(60) == Approx(246.9416506281).margin(0.01)); 1203 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(0.01)); 1204 | } 1205 | 1206 | SECTION("Tuning from 48 works with Interpolation") 1207 | { 1208 | auto s = Tunings::readSCLFile(testFile("12-intune.scl")); 1209 | auto k = Tunings::readKBMFile(testFile("mapping-whitekeys-from-48-a440.kbm")); 1210 | auto t = Tunings::Tuning(s, k); 1211 | t = t.withSkippedNotesInterpolated(); 1212 | REQUIRE(t.isMidiNoteMapped(60)); 1213 | REQUIRE(t.isMidiNoteMapped(69)); 1214 | REQUIRE(t.frequencyForMidiNote(60) == Approx(246.9416506281).margin(0.01)); 1215 | REQUIRE(t.frequencyForMidiNote(69) == Approx(440.0).margin(0.01)); 1216 | } 1217 | } 1218 | 1219 | TEST_CASE("Wrapped KBMs") 1220 | { 1221 | for (const auto &kbm : 1222 | {"empty-c-100.kbm", "fournote-c-100.kbm", "eightnote-c-100.kbm", "twelve-c-100.kbm"}) 1223 | { 1224 | DYNAMIC_SECTION("Testing KBM " << kbm) 1225 | { 1226 | auto scale = Tunings::readSCLFile(testFile("kbm-fix-012023/exact4.scl")); 1227 | auto map = Tunings::readKBMFile(testFile(std::string("kbm-fix-012023/") + kbm)); 1228 | auto tun = Tunings::Tuning(scale, map); 1229 | 1230 | REQUIRE(tun.frequencyForMidiNote(60) == 100.0); 1231 | for (int i = 61; i < 76; ++i) 1232 | { 1233 | auto ni = tun.frequencyForMidiNote(i); 1234 | auto np = tun.frequencyForMidiNote(i - 1); 1235 | auto diff = i <= 64 ? 25 : (i <= 68 ? 50 : (i <= 72 ? 100 : 200)); 1236 | REQUIRE(ni > np); 1237 | REQUIRE(ni - np == Approx(diff).margin(0.001)); 1238 | } 1239 | } 1240 | } 1241 | DYNAMIC_SECTION("Skipper One") 1242 | { 1243 | auto scale = Tunings::readSCLFile(testFile("kbm-fix-012023/exact4.scl")); 1244 | auto map = Tunings::readKBMFile(testFile("kbm-fix-012023/threenote-c-100.kbm")); 1245 | auto tun = Tunings::Tuning(scale, map); 1246 | int note = 60; 1247 | for (const auto &v : {100.0, 125.0, 175., 200., 250., 350., 400.}) 1248 | { 1249 | REQUIRE(tun.frequencyForMidiNote(note) == Approx(v).margin(0.001)); 1250 | note++; 1251 | } 1252 | } 1253 | DYNAMIC_SECTION("Skipper 59") 1254 | { 1255 | auto scale = Tunings::readSCLFile(testFile("kbm-fix-012023/exact4.scl")); 1256 | auto map = Tunings::readKBMFile(testFile("kbm-fix-012023/threenote-c-100-from-1.kbm")); 1257 | auto tun = Tunings::Tuning(scale, map); 1258 | int note = 60; 1259 | for (const auto &v : {100.0, 140., 160., 200., 280., 320.}) 1260 | { 1261 | REQUIRE(tun.frequencyForMidiNote(note) == Approx(v).margin(0.001)); 1262 | note++; 1263 | } 1264 | } 1265 | DYNAMIC_SECTION("Skipper Long") 1266 | { 1267 | auto scale = Tunings::readSCLFile(testFile("kbm-fix-012023/exact4.scl")); 1268 | auto map = Tunings::readKBMFile(testFile("kbm-fix-012023/sixnote-c-100.kbm")); 1269 | auto tun = Tunings::Tuning(scale, map); 1270 | int note = 60; 1271 | for (const auto &v : {100.0, 125.0, 175., 200., 250., 350., 400.}) 1272 | { 1273 | REQUIRE(tun.frequencyForMidiNote(note) == Approx(v).margin(0.001)); 1274 | note++; 1275 | } 1276 | } 1277 | } 1278 | 1279 | TEST_CASE("Retuning API") 1280 | { 1281 | SECTION("12TET is retuned zero") 1282 | { 1283 | auto s = Tunings::evenTemperament12NoteScale(); 1284 | auto t = Tunings::Tuning(s); 1285 | for (int i = 0; i < 128; ++i) 1286 | { 1287 | REQUIRE(t.retuningFromEqualInSemitonesForMidiNote(i) == Approx(0.f).margin(1e-7)); 1288 | REQUIRE(t.retuningFromEqualInCentsForMidiNote(i) == Approx(0.f).margin(1e-7)); 1289 | } 1290 | } 1291 | 1292 | for (int diff = -8; diff < 8; ++diff) 1293 | { 1294 | DYNAMIC_SECTION("12TET with A moved by " << diff << " keys") 1295 | { 1296 | auto s = Tunings::evenTemperament12NoteScale(); 1297 | auto k = Tunings::tuneNoteTo(69 + diff, 440.0); 1298 | auto t = Tunings::Tuning(s, k); 1299 | for (int i = 0; i < 128; ++i) 1300 | { 1301 | // This minus sign is because we are tuning note 69+diff so at -1, we tune 1302 | // note 68 to 440. This means when you press note 69 you want to get 466 1303 | // which is the same as tuning note 69 *up* one semitone. Just a test 1304 | // construction artifact 1305 | REQUIRE(t.retuningFromEqualInSemitonesForMidiNote(i) == Approx(-diff).margin(1e-7)); 1306 | REQUIRE(t.retuningFromEqualInCentsForMidiNote(i) == 1307 | Approx(-diff * 100.f).margin(1e-7)); 1308 | } 1309 | } 1310 | } 1311 | 1312 | SECTION("24 TET centered at zero") 1313 | { 1314 | auto s = Tunings::evenDivisionOfSpanByM(2, 24); 1315 | auto t = Tunings::Tuning(s); 1316 | for (int i = 60 - 24; i <= 60 + 24; ++i) 1317 | { 1318 | auto dist = 60 - i; 1319 | auto rt = dist * 0.5; 1320 | REQUIRE(t.retuningFromEqualInSemitonesForMidiNote(i) == Approx(rt).margin(1e-7)); 1321 | REQUIRE(t.retuningFromEqualInCentsForMidiNote(i) == Approx(rt * 100.0).margin(1e-7)); 1322 | } 1323 | } 1324 | } 1325 | 1326 | TEST_CASE("Surge 7822 non uniform mapping misses scale center") 1327 | { 1328 | SECTION("At Note 57 - no wrapping") 1329 | { 1330 | auto scale = Tunings::readSCLFile(testFile("kbm-wrapping-7822/31edo2.scl")); 1331 | auto map = Tunings::readKBMFile(testFile("kbm-wrapping-7822/31edo2-subset-57.kbm")); 1332 | auto t = Tunings::Tuning(scale, map); 1333 | REQUIRE(t.frequencyForMidiNote(60) == Approx(400.0)); 1334 | REQUIRE(t.frequencyForMidiNote(61) == Approx(418.2936581199)); 1335 | } 1336 | SECTION("At Note 69 - wrapping") 1337 | { 1338 | auto scale = Tunings::readSCLFile(testFile("kbm-wrapping-7822/31edo2.scl")); 1339 | auto map = Tunings::readKBMFile(testFile("kbm-wrapping-7822/31edo2-subset.kbm")); 1340 | auto t = Tunings::Tuning(scale, map); 1341 | REQUIRE(t.frequencyForMidiNote(60) == Approx(400.0)); 1342 | REQUIRE(t.frequencyForMidiNote(61) == Approx(418.2936581199)); 1343 | } 1344 | } 1345 | 1346 | TEST_CASE("Loading Ableton scales") 1347 | { 1348 | SECTION("Good ASCL file") 1349 | { 1350 | auto s = Tunings::readASCLFile(testFile("rast.ascl")); 1351 | REQUIRE(s.scale.count == 12); 1352 | REQUIRE(s.source == "Inside Arabic Music, Chapter 11 (description of Tuning System); Ch " 1353 | "14-16 (descriptions of Ajnas); Ch 24 (Sayr diagrams)"); 1354 | REQUIRE(s.link == "https://www.ableton.com/learn-more/tuning-systems/rast-1"); 1355 | REQUIRE(s.rawTexts.size() == 4); 1356 | REQUIRE(s.rawTexts[0] == "! @ABL NOTE_NAMES C D♭ D \"E♭\" E1/2♭ F F♯ G A♭ A B♭ \"B1/2♭\""); 1357 | REQUIRE(s.notationMapping.count == 12); 1358 | REQUIRE(s.notationMapping.names[11] == "C"); 1359 | REQUIRE(s.keyboardMapping.count == 12); 1360 | REQUIRE(s.keyboardMapping.keys.size() == 12); 1361 | REQUIRE(s.keyboardMapping.tuningFrequency == Approx(261.6256)); 1362 | REQUIRE(s.keyboardMapping.middleNote == 60); 1363 | REQUIRE(s.keyboardMapping.tuningConstantNote == 60); 1364 | 1365 | auto s2 = Tunings::readASCLFile(testFile("rast6.ascl")); 1366 | REQUIRE(s2.notationMapping.count == 20); 1367 | REQUIRE(s2.notationMapping.names[3] == "E1/2♭-a-"); 1368 | } 1369 | 1370 | SECTION("ASCL compared with Ableton-generated KBM") 1371 | { 1372 | std::string files[3] = {"maqamat", "31-edo", "liwung-tbn"}; 1373 | for (std::string file : files) 1374 | { 1375 | auto s = Tunings::readASCLFile(testFile(file + ".ascl")); 1376 | auto k = Tunings::readKBMFile(testFile(file + ".kbm")); 1377 | REQUIRE(s.keyboardMapping.count == k.count); 1378 | REQUIRE(s.keyboardMapping.tuningFrequency == k.tuningFrequency); 1379 | REQUIRE(s.keyboardMapping.middleNote == k.middleNote); 1380 | REQUIRE(s.keyboardMapping.tuningConstantNote == k.tuningConstantNote); 1381 | } 1382 | } 1383 | 1384 | SECTION("Bad ASCL file") 1385 | { 1386 | REQUIRE_THROWS_AS(Tunings::readASCLFile(testFile("bad/bad-rast.ascl")), 1387 | Tunings::TuningError); 1388 | } 1389 | 1390 | SECTION("Tuning read with ASCL") 1391 | { 1392 | auto s = Tunings::readASCLFile(testFile("rast.ascl")); 1393 | Tunings::Tuning t(s); 1394 | REQUIRE(t.frequencyForMidiNote(s.keyboardMapping.tuningConstantNote) == 1395 | Approx(s.referencePitchFreq)); 1396 | REQUIRE(t.scalePositionForMidiNote(s.keyboardMapping.tuningConstantNote) == 1397 | s.referencePitchIndex); 1398 | REQUIRE(t.midiNoteForNoteName("C", 3) == 60); 1399 | REQUIRE(t.midiNoteForNoteName("C", -100) == 0); 1400 | REQUIRE(t.midiNoteForNoteName("C", 100) == 511); 1401 | REQUIRE(t.midiNoteForNoteName("E1/2♭", 3) == 64); 1402 | REQUIRE(t.midiNoteForNoteName("E1/2♭", 2) == 64 - 12); 1403 | REQUIRE_THROWS_AS(t.midiNoteForNoteName("E1/3♭", 3), Tunings::TuningError); 1404 | REQUIRE(t.noteNameForScalePosition(0) == "C"); 1405 | REQUIRE(t.noteNameForScalePosition(4) == "E1/2♭"); 1406 | REQUIRE(t.noteNameForScalePosition(4 + t.scale.count) == "E1/2♭"); 1407 | 1408 | auto s2 = Tunings::readASCLFile(testFile("rast6.ascl")); 1409 | Tunings::Tuning t2(s2); 1410 | REQUIRE(t2.midiNoteForNoteName("C", 4) == 60); 1411 | } 1412 | 1413 | SECTION("Tuning read without ASCL") 1414 | { 1415 | auto s = Tunings::readASCLFile(testFile("31edo.scl")); 1416 | Tunings::Tuning t(s); 1417 | REQUIRE_THROWS_AS(t.midiNoteForNoteName("E1/3♭", 3), Tunings::TuningError); 1418 | REQUIRE_THROWS_AS(t.noteNameForScalePosition(4), Tunings::TuningError); 1419 | } 1420 | } 1421 | 1422 | int main(int argc, char **argv) 1423 | { 1424 | if (getenv("LANG") != nullptr) 1425 | { 1426 | try 1427 | { 1428 | std::locale::global(std::locale(getenv("LANG"))); 1429 | std::cout << "Setting LOCALE to '" << getenv("LANG") << "'. "; 1430 | 1431 | time_t rawtime; 1432 | struct tm *timeinfo; 1433 | char buffer[512]; 1434 | 1435 | time(&rawtime); 1436 | timeinfo = localtime(&rawtime); 1437 | 1438 | // work around a windows g++ warning error 1439 | const char *fmt = "%A %e %B %Y"; 1440 | strftime(buffer, sizeof(buffer), fmt, timeinfo); 1441 | 1442 | std::cout << "Date in this locale: '" << buffer << "'" << std::endl; 1443 | ; 1444 | } 1445 | catch (std::exception &e) 1446 | { 1447 | std::locale::global(std::locale("C")); 1448 | } 1449 | } 1450 | int result = Catch::Session().run(argc, argv); 1451 | return result; 1452 | } 1453 | --------------------------------------------------------------------------------