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 <iostream>
17 #include <iomanip>
18 #include <fstream>
19 #include <cstdlib>
20 #include <math.h>
21 #include <sstream>
22 #include <cctype>
23 
24 namespace Tunings
25 {
26     // Thank you to: https://gist.github.com/josephwb/df09e3a71679461fc104
getlineEndingIndependent(std::istream & is,std::string & t)27     inline std::istream& getlineEndingIndependent(std::istream &is,
28                                                 std::string &t)
29     {
30         t.clear();
31 
32         std::istream::sentry se(is, true);
33         std::streambuf *sb = is.rdbuf();
34 
35         for (;;) {
36             int c = sb->sbumpc();
37             switch (c) {
38                 case '\n':
39                     return is;
40                 case '\r':
41                     if (sb->sgetc() == '\n') {
42                         sb->sbumpc();
43                     }
44                     return is;
45                 case EOF:
46                     is.setstate(std::ios::eofbit);
47                     if (t.empty()) {
48                         is.setstate(std::ios::badbit);
49                     }
50                     return is;
51                 default:
52                     t += (char)c;
53             }
54         }
55     }
56 
57 
locale_atof(const char * s)58     inline double locale_atof(const char* s)
59     {
60         double result = 0;
61         std::istringstream istr(s);
62         istr.imbue(std::locale("C"));
63         istr >> result;
64         return result;
65     }
66 
toneFromString(const std::string & line,int lineno)67     inline Tone toneFromString(const std::string &line, int lineno)
68     {
69         Tone t;
70         t.stringRep = line;
71         if (line.find(".") != std::string::npos)
72         {
73             t.type = Tone::kToneCents;
74             t.cents = locale_atof(line.c_str());
75         }
76         else
77         {
78             t.type = Tone::kToneRatio;
79             auto slashPos = line.find("/");
80             if (slashPos == std::string::npos)
81             {
82                 t.ratio_n = atoi(line.c_str());
83                 t.ratio_d = 1;
84             }
85             else
86             {
87                 t.ratio_n = atoi(line.substr(0, slashPos).c_str());
88                 t.ratio_d = atoi(line.substr(slashPos + 1).c_str());
89             }
90 
91             if( t.ratio_n == 0 || t.ratio_d == 0 )
92             {
93                 std::string s = "Invalid tone in SCL file.";
94                 if( lineno >= 0 )
95                     s += "Line " + std::to_string(lineno) + ".";
96                 s += " Line is '" + line + "'.";
97                 throw TuningError(s);
98             }
99             // 2^(cents/1200) = n/d
100             // cents = 1200 * log(n/d) / log(2)
101 
102             t.cents = 1200 * log(1.0 * t.ratio_n/t.ratio_d) / log(2.0);
103         }
104         t.floatValue = t.cents / 1200.0 + 1.0;
105         return t;
106     }
107 
readSCLStream(std::istream & inf)108     inline Scale readSCLStream(std::istream &inf)
109     {
110         std::string line;
111         const int read_header = 0, read_count = 1, read_note = 2, trailing = 3;
112         int state = read_header;
113 
114         Scale res;
115         std::ostringstream rawOSS;
116         int lineno = 0;
117         while (getlineEndingIndependent(inf, line))
118         {
119             rawOSS << line << "\n";
120             lineno ++;
121 
122             if ((state == read_note && line.empty()) || line[0] == '!')
123             {
124                 continue;
125             }
126             switch (state)
127             {
128             case read_header:
129                 res.description = line;
130                 state = read_count;
131                 break;
132             case read_count:
133                 res.count = atoi(line.c_str());
134                 if (res.count < 1)
135                 {
136                     throw TuningError( "Invalid SCL note count." );
137                 }
138                 state = read_note;
139                 break;
140             case read_note:
141                 auto t = toneFromString(line, lineno);
142                 res.tones.push_back(t);
143                 if( (int)res.tones.size() == res.count )
144                     state = trailing;
145 
146                 break;
147             }
148         }
149 
150         if( ! ( state == read_note || state == trailing ) )
151         {
152             throw TuningError( "Incomplete SCL file. Found no notes section in the file." );
153         }
154 
155         if( (int)res.tones.size() != res.count )
156         {
157             std::string s = "Read fewer notes than count in file. Count = " + std::to_string( res.count )
158                 + " notes. Array size = " + std::to_string( res.tones.size() );
159             throw TuningError(s);
160 
161         }
162         res.rawText = rawOSS.str();
163         return res;
164     }
165 
readSCLFile(std::string fname)166     inline Scale readSCLFile(std::string fname)
167     {
168         std::ifstream inf;
169         inf.open(fname);
170         if (!inf.is_open())
171         {
172             std::string s = "Unable to open file '" + fname + "'";
173             throw TuningError(s);
174         }
175 
176         auto res = readSCLStream(inf);
177         res.name = fname;
178         return res;
179     }
180 
parseSCLData(const std::string & d)181     inline Scale parseSCLData(const std::string &d)
182     {
183         std::istringstream iss(d);
184         auto res = readSCLStream(iss);
185         res.name = "Scale from patch";
186         return res;
187     }
188 
evenTemperament12NoteScale()189     inline Scale evenTemperament12NoteScale()
190     {
191         std::string data = R"SCL(! 12 Tone Equal Temperament.scl
192 !
193 12 Tone Equal Temperament | ED2-12 - Equal division of harmonic 2 into 12 parts
194  12
195 !
196  100.00000
197  200.00000
198  300.00000
199  400.00000
200  500.00000
201  600.00000
202  700.00000
203  800.00000
204  900.00000
205  1000.00000
206  1100.00000
207  2/1
208 )SCL";
209         return parseSCLData(data);
210     }
211 
evenDivisionOfSpanByM(int Span,int M)212     inline Scale evenDivisionOfSpanByM( int Span, int M )
213     {
214         if( Span <= 0 )
215             throw Tunings::TuningError( "Span should be a positive number. You entered " + std::to_string( Span ) );
216         if( M <= 0 )
217             throw Tunings::TuningError( "You must divide the period into at least one step. You entered " + std::to_string( M ) );
218 
219         std::ostringstream oss;
220         oss.imbue( std::locale( "C" ) );
221         oss << "! Automatically generated ED" << Span << "-" << M << " scale\n";
222         oss << "Automatically generated ED" << Span << "-" << M << " scale\n";
223         oss << M << "\n";
224         oss << "!\n";
225 
226 
227         double topCents = 1200.0 * log(1.0 * Span) / log(2.0);
228         double dCents = topCents / M;
229         for( int i=1; i<M; ++i )
230             oss << std::fixed << dCents * i << "\n";
231         oss << Span << "/1\n";
232 
233         return parseSCLData( oss.str() );
234     }
235 
readKBMStream(std::istream & inf)236     inline KeyboardMapping readKBMStream(std::istream &inf)
237     {
238         std::string line;
239 
240         KeyboardMapping res;
241         std::ostringstream rawOSS;
242         res.keys.clear();
243 
244         enum parsePosition {
245             map_size = 0,
246             first_midi,
247             last_midi,
248             middle,
249             reference,
250             freq,
251             degree,
252             keys,
253             trailing
254         };
255         parsePosition state = map_size;
256 
257         int lineno  = 0;
258         while (getlineEndingIndependent(inf, line))
259         {
260             rawOSS << line << "\n";
261             lineno ++;
262             if (line[0] == '!')
263             {
264                 continue;
265             }
266 
267             if( line == "x" ) line = "-1";
268             else if( state != trailing )
269             {
270                 const char* lc = line.c_str();
271                 bool validLine = line.length() > 0;
272                 char badChar = '\0';
273                 while( validLine && *lc != '\0' )
274                 {
275                     if( ! ( *lc == ' ' || std::isdigit( *lc ) || *lc == '.'  || *lc == (char)13 || *lc == '\n' ) )
276                     {
277                         validLine = false;
278                         badChar = *lc;
279                     }
280                     lc ++;
281                 }
282                 if( ! validLine )
283                 {
284                     throw TuningError( "Invalid line " + std::to_string( lineno ) + ". line='" + line + "'. Bad character is '" +
285                                        badChar + "/" + std::to_string( (int)badChar ) + "'" );
286                 }
287             }
288 
289             int i = std::atoi(line.c_str());
290             double v = locale_atof(line.c_str());
291 
292             switch (state)
293             {
294             case map_size:
295                 res.count = i;
296                 break;
297             case first_midi:
298                 res.firstMidi = i;
299                 break;
300             case last_midi:
301                 res.lastMidi = i;
302                 break;
303             case middle:
304                 res.middleNote = i;
305                 break;
306             case reference:
307                 res.tuningConstantNote = i;
308                 break;
309             case freq:
310                 res.tuningFrequency = v;
311                 res.tuningPitch = res.tuningFrequency / 8.17579891564371;
312                 break;
313             case degree:
314                 res.octaveDegrees = i;
315                 break;
316             case keys:
317                 res.keys.push_back(i);
318                 if( (int)res.keys.size() == res.count ) state = trailing;
319                 break;
320             case trailing:
321                 break;
322             }
323             if( ! ( state == keys || state == trailing ) ) state = (parsePosition)(state + 1);
324             if( state == keys && res.count == 0 ) state = trailing;
325 
326         }
327 
328         if( ! ( state == keys || state == trailing ) )
329         {
330             throw TuningError( "Incomplete KBM file. Unable to get to keys section of file." );
331         }
332 
333         if( (int)res.keys.size() != res.count )
334         {
335             throw TuningError( "Different number of keys than mapping file indicates. Count is "
336                                + std::to_string( res.count ) + " and we parsed " + std::to_string( res.keys.size() ) + " keys." );
337         }
338 
339         res.rawText = rawOSS.str();
340         return res;
341     }
342 
readKBMFile(std::string fname)343     inline KeyboardMapping readKBMFile(std::string fname)
344     {
345         std::ifstream inf;
346         inf.open(fname);
347         if (!inf.is_open())
348         {
349             std::string s = "Unable to open file '" + fname + "'";
350             throw TuningError(s);
351         }
352 
353         auto res = readKBMStream(inf);
354         res.name = fname;
355         return res;
356     }
357 
parseKBMData(const std::string & d)358     inline KeyboardMapping parseKBMData(const std::string &d)
359     {
360         std::istringstream iss(d);
361         auto res = readKBMStream(iss);
362         res.name = "Mapping from patch";
363         return res;
364     }
365 
Tuning()366     inline Tuning::Tuning() : Tuning( evenTemperament12NoteScale(), KeyboardMapping() ) { }
Tuning(const Scale & s)367     inline Tuning::Tuning(const Scale &s ) : Tuning( s, KeyboardMapping() ) {}
Tuning(const KeyboardMapping & k)368     inline Tuning::Tuning(const KeyboardMapping &k ) : Tuning( evenTemperament12NoteScale(), k ) {}
369 
Tuning(const Scale & s,const KeyboardMapping & k)370     inline Tuning::Tuning(const Scale& s, const KeyboardMapping &k)
371     {
372         scale = s;
373         keyboardMapping = k;
374 
375         if( s.count <= 0 )
376             throw TuningError( "Unable to tune to a scale with no notes. Your scale provided " + std::to_string( s.count ) + " notes." );
377 
378 
379         double pitches[N];
380 
381         int posPitch0 = 256 + k.tuningConstantNote;
382         int posScale0 = 256 + k.middleNote;
383 
384         double pitchMod = log(k.tuningPitch)/log(2) - 1;
385 
386         int scalePositionOfTuningNote = k.tuningConstantNote - k.middleNote;
387         if( k.count > 0 )
388             scalePositionOfTuningNote = k.keys[scalePositionOfTuningNote];
389 
390         double tuningCenterPitchOffset;
391         if( scalePositionOfTuningNote == 0 )
392             tuningCenterPitchOffset = 0;
393         else
394         {
395             double tshift = 0;
396             double dt = s.tones[s.count -1].floatValue - 1.0;
397             while( scalePositionOfTuningNote < 0 )
398             {
399                 scalePositionOfTuningNote += s.count;
400                 tshift += dt;
401             }
402             while( scalePositionOfTuningNote > s.count )
403             {
404                 scalePositionOfTuningNote -= s.count;
405                 tshift -= dt;
406             }
407 
408             if( scalePositionOfTuningNote == 0 )
409                 tuningCenterPitchOffset = -tshift;
410             else
411                 tuningCenterPitchOffset = s.tones[scalePositionOfTuningNote-1].floatValue - 1.0 - tshift;
412         }
413 
414         for (int i=0; i<N; ++i)
415         {
416             // TODO: ScaleCenter and PitchCenter are now two different notes.
417             int distanceFromPitch0 = i - posPitch0;
418             int distanceFromScale0 = i - posScale0;
419 
420             if( distanceFromPitch0 == 0 )
421             {
422                 pitches[i] = 1;
423                 lptable[i] = pitches[i] + pitchMod;
424                 ptable[i] = pow( 2.0, lptable[i] );
425                 scalepositiontable[i] = scalePositionOfTuningNote % s.count;
426 #if DEBUG_SCALES
427                 std::cout << "PITCH: i=" << i << " n=" << i - 256
428                           << " p=" << pitches[i]
429                           << " lp=" << lptable[i]
430                           << " tp=" << ptable[i]
431                           << " fr=" << ptable[i] * 8.175798915
432                           << std::endl;
433 #endif
434             }
435             else
436             {
437                 /*
438                   We used to have this which assumed 1-12
439                   Now we have our note number, our distance from the
440                   center note, and the key remapping
441                   int rounds = (distanceFromScale0-1) / s.count;
442                   int thisRound = (distanceFromScale0-1) % s.count;
443                 */
444 
445                 int rounds;
446                 int thisRound;
447                 int disable = false;
448                 if( ( k.count == 0 ) )
449                 {
450                     rounds = (distanceFromScale0-1) / s.count;
451                     thisRound = (distanceFromScale0-1) % s.count;
452                 }
453                 else
454                 {
455                     /*
456                     ** Now we have this situation. We are at note i so we
457                     ** are m away from the center note which is distanceFromScale0
458                     **
459                     ** If we mod that by the mapping size we know which note we are on
460                     */
461                     int mappingKey = distanceFromScale0 % k.count;
462                     if( mappingKey < 0 )
463                         mappingKey += k.count;
464                     // Now have we gone off the end
465                     int rotations = 0;
466                     int dt = distanceFromScale0;
467                     if( dt > 0 )
468                     {
469                         while( dt >= k.count )
470                         {
471                             dt -= k.count;
472                             rotations ++;
473                         }
474                     }
475                     else
476                     {
477                         while( dt < 0 )
478                         {
479                             dt += k.count;
480                             rotations --;
481                         }
482                     }
483 
484                     int cm = k.keys[mappingKey];
485                     int push = 0;
486                     if( cm < 0 )
487                     {
488                         disable = true;
489                     }
490                     else
491                     {
492                         push = mappingKey - cm;
493                     }
494 
495                     if( k.octaveDegrees > 0 && k.octaveDegrees != k.count )
496                     {
497                         rounds = rotations;
498                         thisRound = cm-1;
499                         if( thisRound < 0 ) { thisRound = k.octaveDegrees - 1; rounds--; }
500                     }
501                     else
502                     {
503                         rounds = (distanceFromScale0 - push - 1) / s.count;
504                         thisRound = (distanceFromScale0 - push - 1) % s.count;
505                     }
506 
507 #ifdef DEBUG_SCALES
508                     if( i > 256+53 && i < 265+85 )
509                         std::cout << "MAPPING n=" << i - 256 << " pushes ds0=" << distanceFromScale0 << " cmc=" << k.count << " tr=" << thisRound << " r=" << rounds << " mk=" << mappingKey << " cm=" << cm << " push=" << push << " dis=" << disable << " mk-p-1=" << mappingKey - push - 1 << " rotations=" << rotations << " od=" << k.octaveDegrees << std::endl;
510 #endif
511 
512 
513                 }
514 
515                 if( thisRound < 0 )
516                 {
517                     thisRound += s.count;
518                     rounds -= 1;
519                 }
520 
521                 if( disable )
522                 {
523                     pitches[i] = 0;
524                     scalepositiontable[i] = -1;
525                 }
526                 else
527                 {
528                     pitches[i] = s.tones[thisRound].floatValue + rounds * (s.tones[s.count - 1].floatValue - 1.0) - tuningCenterPitchOffset;
529                     scalepositiontable[i] = ( thisRound + 1 ) % s.count;
530                 }
531 
532                 lptable[i] = pitches[i] + pitchMod;
533                 ptable[i] = pow( 2.0, pitches[i] + pitchMod );
534 
535 #if DEBUG_SCALES
536                 if( i > 296 && i < 340 )
537                     std::cout << "PITCH: i=" << i << " n=" << i - 256
538                               << " ds0=" << distanceFromScale0
539                               << " dp0=" << distanceFromPitch0
540                               << " r=" << rounds << " t=" << thisRound
541                               << " p=" << pitches[i]
542                               << " t=" << s.tones[thisRound].floatValue << " " << s.tones[thisRound ].cents
543                               << " dis=" << disable
544                               << " tp=" << ptable[i]
545                               << " fr=" << ptable[i] * 8.175798915
546                               << " tcpo=" << tuningCenterPitchOffset
547 
548                         //<< " l2p=" << log(otp)/log(2.0)
549                         //<< " l2p-p=" << log(otp)/log(2.0) - pitches[i] - rounds - 3
550                               << std::endl;
551 #endif
552             }
553         }
554     }
555 
frequencyForMidiNote(int mn)556     inline double Tuning::frequencyForMidiNote( int mn ) const {
557         auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
558         return ptable[ mni ] * MIDI_0_FREQ;
559     }
560 
frequencyForMidiNoteScaledByMidi0(int mn)561     inline double Tuning::frequencyForMidiNoteScaledByMidi0( int mn ) const {
562         auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
563         return ptable[ mni ];
564     }
565 
logScaledFrequencyForMidiNote(int mn)566     inline double Tuning::logScaledFrequencyForMidiNote( int mn ) const {
567         auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
568         return lptable[ mni ];
569     }
570 
scalePositionForMidiNote(int mn)571     inline int Tuning::scalePositionForMidiNote( int mn ) const {
572         auto mni = std::min( std::max( 0, mn + 256 ), N-1 );
573         return scalepositiontable[ mni ];
574     }
575 
KeyboardMapping()576     inline KeyboardMapping::KeyboardMapping()
577         : count(0),
578           firstMidi(0),
579           lastMidi(127),
580           middleNote(60),
581           tuningConstantNote(60),
582           tuningFrequency(MIDI_0_FREQ * 32.0),
583           tuningPitch(32.0),
584           octaveDegrees(0),
585           rawText( "" ),
586           name( "" )
587     {
588         std::ostringstream oss;
589         oss.imbue( std::locale( "C" ) );
590         oss << "! Default KBM file\n";
591         oss << count << "\n"
592             << firstMidi << "\n"
593             << lastMidi << "\n"
594             << middleNote << "\n"
595             << tuningConstantNote << "\n"
596             << tuningFrequency << "\n"
597             << octaveDegrees << "\n";
598         rawText = oss.str();
599     }
600 
tuneA69To(double freq)601     inline KeyboardMapping tuneA69To(double freq)
602     {
603         return tuneNoteTo( 69, freq );
604     }
605 
tuneNoteTo(int midiNote,double freq)606     inline KeyboardMapping tuneNoteTo( int midiNote, double freq )
607     {
608         return startScaleOnAndTuneNoteTo( 60, midiNote, freq );
609     }
610 
startScaleOnAndTuneNoteTo(int scaleStart,int midiNote,double freq)611     inline KeyboardMapping startScaleOnAndTuneNoteTo( int scaleStart, int midiNote, double freq )
612     {
613         std::ostringstream oss;
614         oss.imbue( std::locale( "C" ) );
615         oss << "! Automatically generated mapping, tuning note " << midiNote << " to " << freq << " Hz\n"
616             << "!\n"
617             << "! Size of map\n"
618             << 0 << "\n"
619             << "! First and last MIDI notes to map - map the entire keyboard\n"
620             << 0 << "\n" << 127 << "\n"
621             << "! Middle note where the first entry in the scale is mapped.\n"
622             << scaleStart << "\n"
623             << "! Reference note where frequency is fixed\n"
624             << midiNote << "\n"
625             << "! Frequency for MIDI note " << midiNote << "\n"
626             << freq << "\n"
627             << "! Scale degree for formal octave. This is am empty mapping, so:\n"
628             << 0 << "\n"
629             << "! Mapping. This is an empty mapping so list no keys\n";
630 
631         return parseKBMData( oss.str() );
632     }
633 
634 }
635 #endif
636