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