/* 22-23 September 2023, by Ruud Harmsen. pilaintv.c Pila intervallos musical, pro essayar struer scalas in intonation juste. Pile up musical intervals, trying to build scales in just intonation. Vide https://rudhar.com/musica/hwedwortehun/ia08.htm pro plus explicationes. See https://rudhar.com/musica/hwedwortehun/ia08.htm for more explanations (in Interlingua). 24 September 2023: Specify sought scale as command line arguments: SearchSpecific mode. 25 September 2023: In SearchSpecific mode, when no scales found whatsoever, automatically retry several times, with parameters MAXFACT and MAXLCM set higher each time. 26 and 27 September 2023: Added an optional report of chords that can be made with each scale that was found on specification. 25 June - 5 July 2025: Optional check to reject wolf fifths and wolf fourths. */ static char Legend[] = { "\ Column VI which later appears vertically along with other data, first \n\ appears horizontally, to facilitate text searches. \n\ \n\ Explanation of the columns: \n\ \n\ I - The sequence number of the degree in the scale. \n\ \n\ II - The degree’s pitch relative to the tone in the first degree. \n\ Rounded to the nearest 50 cents multiple, so reduced to a \n\ 24 quarter notes per octave scale. \n\ \n\ III - The degree’s pitch relative to the tone in the first degree, \n\ as it actually is, accurate to a thousandth of a cent, \n\ calculated from the scale degree’s interval integers. \n\ \n\ IV - The degree’s pitch relative to the tone in the first degree. \n\ Rounded to the nearest multiple of 22.642 cents, so reduced to \n\ a division of the octave into 53 equal parts. \n\ \n\ V - The number of such parts that make the distance. \n\ \n\ VI - The integer from a series of minimal integers, such that they \n\ make the tone ratios for the scale’s degrees. \n\ \n\ VI - The smallest integers that make the frequency ratio of the \n\ scale degree’s tone. \n\ \n\ VII - That ratio. \n\ \n\ VIII - A visual separator. \n\ \n\ IX - The scale degree’s pitch relative to that of the previous \n\ degree, expressed as a ratio of integers. \n\ \n\ X - That interval in cents. \n\ \n\ XI - The sequence numbers of the degrees in the scale, once again \n\ for easy reference. \n\ \n\ I II III IV V VI VII VIII IX X XI \n\ \n\ "}; #include #include #include #include #include #include "../../rhmusicfuncs/rhmusicfuncs.h" #define NUMINTV (int)(sizeof intervals / sizeof intervals[0]) #define NUMCHST (int)(sizeof ChordDegrees / sizeof ChordDegrees[0]) #define NUMDGRE 14+1 #define MAXFACT 90 #define MAXLCM 125 #define CENTSMARGIN 25.0 static long int attempt; /* Counters for various reasons to reject */ static long int cnt_factor; /* Numerator factor too high */ static long int cnt_ovrsht; /* Note found beyond octave */ static long int cnt_lcmthi; /* Least common multiple too high */ static long int cnt_nowant; /* Not according to specs */ static long int cnt_wolf54; /* Wolf fifth or fourth */ static long int cnt_validf; /* Valid scale */ static const struct IntvalRatio intervals[] = { { 9, 8}, {10, 9}, {16,15}, {25,24}, {11,10}, {12,11}, {25,22}, { 6, 5}, {32,27}, {17,16}, {18,17}, { 7, 6}, { 8, 7}, {19,18}, {20,19}, {45,38}, }; static struct workScale_s { /* This variable is long long (64 bits), so even when experimentally allowing much too high LCMs (least common multiples), overflow is unlikely too occur. Even if not allowing them, you must calculate them in order to know they’re too high. */ LLI RatioSequence; struct IntvalRatio cumulated; double WantedCents; } wS[NUMDGRE]; static int SearchSpecific; struct ScaleOptions Opt; static int NumDegreesForChords; static int ChordDegrees[5]; static void recurs (int grado); static void ReportThisScale (int maxgrad); static int FindRatioSequence (int maxgrad); static void DecodeSearchSpecific (int argc, char **argv); static void ReportChords (int maxgrad); static LLI GetRatioSequence (void *Scale, int index); int main (int argc, char **argv) { int maxAttempt; GetScaleCmdlineArgs(&argc, &argv, &Opt, 0); if (argc > 1) DecodeSearchSpecific(argc, argv); maxAttempt = SearchSpecific ? 5 : 1; fwrite(Legend, (sizeof Legend) - 1, 1, stdout); for (attempt = 1; attempt <= maxAttempt && cnt_validf == 0; attempt++) { cnt_factor = cnt_ovrsht = cnt_lcmthi = cnt_nowant = cnt_wolf54 = cnt_validf = 0; wS[0].cumulated.numer = wS[0].cumulated.denom = 1; recurs(1); } if (attempt > maxAttempt) attempt--; printf("%8ld: numerator exceeds %ld\n", cnt_factor, MAXFACT * attempt); printf("%8ld: ratio overshoots 2.0\n", cnt_ovrsht); printf("%8ld: least common multiple exceeds %ld\n", cnt_lcmthi, MAXLCM * attempt); printf("%8ld: not according to specs\n", cnt_nowant); printf("%8ld: rejected for wolf 5th or 4th\n", cnt_wolf54); printf("%8ld: valid scales\n", cnt_validf); return 0; } static void recurs (int grado) { int ivl; if (grado >= NUMDGRE) return; for (ivl = 0; ivl < NUMINTV; ivl++) { /* Cumulate intervals by multiplying the previous cumulated interval with the one of this scale step. */ wS[grado ].cumulated.numer = wS[grado-1].cumulated.numer * intervals[ivl].numer; wS[grado ].cumulated.denom = wS[grado-1].cumulated.denom * intervals[ivl].denom; /* Reduce the fraction to its essential factors */ long int thisGcd = gcd(wS[grado].cumulated.numer, wS[grado].cumulated.denom); wS[grado].cumulated.numer /= thisGcd; wS[grado].cumulated.denom /= thisGcd; /* This criterion added 24 September 2023 */ if (SearchSpecific && fabs(rats2cents(wS[grado].cumulated.numer, wS[grado].cumulated.denom) - wS[grado].WantedCents) > CENTSMARGIN) { cnt_nowant++; continue; } if (wS[grado].cumulated.numer > MAXFACT * attempt) { cnt_factor++; continue; } if ((double)wS[grado].cumulated.numer > wS[grado].cumulated.denom * 2.0) { cnt_ovrsht++; continue; } if (wS[grado].cumulated.numer == wS[grado].cumulated.denom * 2) { if (FindRatioSequence(grado)) cnt_lcmthi++; else if (Opt.CheckWolf && ScaleContainsWolf(wS, NUMDGRE, &Opt, GetRatioSequence)) { cnt_wolf54++; } else { cnt_validf++; ReportThisScale(grado); if (NumDegreesForChords > 0) ReportChords(grado); } continue; } recurs(grado + 1); } } static void ReportThisScale (int maxgrad) { int grado, prev; struct IntvalRatio relat; long thisGcd; double centsabs, centsrel; /* First print column V horizontally, which later appears vertically along with other data. */ for (grado = 0; grado <=maxgrad; grado++) printf("%3lld ", wS[grado].RatioSequence); printf("\n"); for (grado = 0; grado <=maxgrad; grado++) { prev = grado - 1; int snap24, snap53; centsabs = rats2cents(wS[grado].cumulated.numer, wS[grado].cumulated.denom); snap24 = (int)(centsabs / 1200 * 24 + 0.5); snap53 = (int)(centsabs / 1200 * 53 + 0.5); printf("%2d %5.0f %8.3f %8.3f %2d/53 ", grado + 1, snap24 * 1200 / 24.0, centsabs, snap53 * 1200 / 53.0, snap53); printf("%3lld %3ld:%-3ld -- ", wS[grado].RatioSequence, wS[grado].cumulated.numer, wS[grado].cumulated.denom); if (grado == 0) printf("%-16s", ""); else { relat.numer = wS[grado].cumulated.numer * wS[prev].cumulated.denom; relat.denom = wS[grado].cumulated.denom * wS[prev].cumulated.numer; thisGcd = gcd(relat.numer, relat.denom); relat.numer /= thisGcd; relat.denom /= thisGcd; centsrel = rats2cents(relat.numer, relat.denom); printf("%3ld:%-3ld %7.3f ", relat.numer, relat.denom, centsrel); } printf("%2d\n", grado + 1); } printf("\n"); } static int FindRatioSequence (int maxgrad) { int grado; /* Least common multiple */ LLI lcm = wS[0].cumulated.denom; LLI numerGcd = wS[0].cumulated.numer; LLI thisGcd; for (grado = 1; grado < maxgrad; grado++) { numerGcd = gcd(wS[grado].cumulated.numer, numerGcd); thisGcd = gcd(wS[grado].cumulated.denom, lcm); lcm = lcm * wS[grado].cumulated.denom / thisGcd; } if (lcm > MAXLCM * attempt) return 1; for (grado = 0; grado <= maxgrad; grado++) wS[grado].RatioSequence = wS[grado].cumulated.numer * lcm / wS[grado].cumulated.denom / numerGcd; return 0; } /* This function added 24 September 2023 */ static void DecodeSearchSpecific (int argc, char **argv) { int absolute = 0; argc--, argv++; if (strcmp(argv[0], "-a") == 0) { absolute = 1; argc--, argv++; } /* Initialise the wanted cents of the sought scale with standard equidistant values for a normal major scale. */ wS[0].WantedCents = 0; /* C */ wS[1].WantedCents = 200; /* D */ wS[2].WantedCents = 400; /* E */ wS[3].WantedCents = 500; /* F */ wS[4].WantedCents = 700; /* G */ wS[5].WantedCents = 900; /* A */ wS[6].WantedCents = 1100; /* B */ wS[7].WantedCents = 1200; /* C */ while (argc > 0 && strcmp(argv[0], "-c") != 0) { int index, letter; if (absolute || !isalpha(argv[0][0])) index = atoi(argv[0]); else { letter = tolower(argv[0][0]); if (letter < 'c') letter += 7; index = letter - 'c'; } argc--, argv++; if (argc > 0 && index >= 0 && index < NUMDGRE && strcmp(argv[0], "-c") != 0) { double wanted = atof(argv[0]); if (absolute) wS[index].WantedCents = wanted; else wS[index].WantedCents += wanted; argc--, argv++; SearchSpecific = 1; } } if (argc > 0 && strcmp(argv[0], "-c") == 0) { /* Specify a chord to be evaluated with found scales. Example: ./pilaintv 0 0 -c 1 3 5 7 to see seventh chords based on C major. */ int index; argc--, argv++; for ( ; argc > 0; argc--, argv++) { index = atoi(argv[0]); if (index >= 1 && index < 2 * NUMDGRE) { ChordDegrees[NumDegreesForChords] = index; NumDegreesForChords++; if (NumDegreesForChords >= NUMCHST) break; } } } } static void ReportChords (int maxgrad) { int i, chorddeg, index, octav; long thisGcd; for (chorddeg = 0; chorddeg < maxgrad; chorddeg++) { index = (ChordDegrees[0] - 1 + chorddeg) % maxgrad; octav = (ChordDegrees[0] - 1 + chorddeg) / maxgrad + 1; thisGcd = wS[index].RatioSequence * octav; for (i = 1; i < NumDegreesForChords && i < NUMCHST; i++) { index = (ChordDegrees[i] - 1 + chorddeg) % maxgrad; octav = (ChordDegrees[i] - 1 + chorddeg) / maxgrad + 1; thisGcd = gcd(wS[index].RatioSequence * octav, thisGcd); } for (i = 0; i < NumDegreesForChords && i < NUMCHST; i++) { index = (ChordDegrees[i] - 1 + chorddeg) % maxgrad; octav = (ChordDegrees[i] - 1 + chorddeg) / maxgrad + 1; printf("%3lld-%-2d ", wS[index].RatioSequence * octav / thisGcd, index + 1); } printf("\n"); } printf("\n"); } static LLI GetRatioSequence (void *Scale, int index) { struct workScale_s *p = Scale; return p[index].RatioSequence; }