Adding 31-tet tuning to the DX7 and the CrowBX
2023-11-28
Most modern Western music uses the 12-tone equal temperament tuning system. My curiosity about different tunings was recently rekindled by two things. (1) I read in interviews that Aphex Twin likes using microtunings. (2) I was playing my Prophet 10, which needs quite some time to warm up and get in tune after you turn it on, and I liked how it sounded while still out of tune.
The Prophet 10 has a nice selection of built in alternate tuning tables so once it was warmed up it allowed me to try different ones. I decided I'm drawn to equal temperament tunings because they should save me the hassle of defining a new tuning table for each piece of music I write (assuming I end up using alternate tunings sometimes). The equal temperament offerings in the Prophet 10 are 12-tet (i.e. standard tuning), 19-tet, 24-tet and 31-tet. I liked 31-tet the best.
I don't see myself writing entire pieces in an alternate tuning. But I wonder if it will work to combine 12-tet monophonic parts, if necessary with pitch bend adjustments per note, with some 31-tet polyphonic parts. It would be nice if I have more synths with 31-tet tuning.
I use a DX7 and I already knew it has a "microtuning" feature. It does not include 31-tet among its built-in tables but it lets you upload custom tables. It seems that Scala is a popular tool for defining alternate tunings but it felt better to me to create the tables myself.
Here is a C program that generates 31-tet tables for the DX7.
/* 31tet.c */
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char progname[] = "31tet";
char msg[266 + 6 + 2];
void fatal(char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
exit(1);
}
void fail_usage(void) {
fatal("Usage: %s [-d DEVICE_NUMBER] [-t TRANSPOSE_SEMITONES] [-s "
"MICRO_TUNING_SLOT]\n",
progname);
}
int main(int argc, char **argv) {
int data, i, message_size = sizeof(msg) - 6 - 2;
int ch, transpose = 0, device_number = 0, slot = 0;
uint8_t checksum;
enum { lowest = 938, notes = 31, octave = 1024, max = 10794 };
while ((ch = getopt(argc, argv, "d:t:s:")) != -1) {
switch (ch) {
case 'd':
device_number = atoi(optarg);
if (device_number >> 4)
fatal("error: invalid device number: %d\n", device_number);
break;
case 't':
transpose = atoi(optarg);
break;
case 's':
slot = atoi(optarg);
if (slot < 0 || slot > 1)
fatal("error: invalid slot number: %d\n", slot);
break;
default:
fail_usage();
break;
}
}
if (optind != argc)
fail_usage();
snprintf(msg, 6 + 1, "\xf0\x43%c\x7e%c%c", device_number, message_size >> 7,
message_size & 0x7f);
snprintf(msg + 6, 10 + 1, "LM MCRYM%c", slot);
for (i = 0; i < 128; i++) {
data = (i / notes) * octave + (i % notes) * (octave / notes) + lowest +
(((float)transpose / 12.0) * (float)octave);
if (data > max)
data = max;
msg[16 + 2 * i] = data >> 7;
msg[16 + 2 * i + 1] = data & 0x7f;
}
checksum = 0;
for (i = 6; i < sizeof(msg) - 2; i++)
checksum += msg[i];
msg[sizeof(msg) - 2] = (0x80 - checksum) & 0x7f;
msg[sizeof(msg) - 1] = 0xf7;
fwrite(msg, 1, sizeof(msg), stdout);
return 0;
}
Writing the DX7 31-tet generator was straightforward once I figured out that the synth splits the octave in 1024 equal steps. Because 1024 does not divide by 31 I "resynchronize" every octave: the first 30 notes are 33 steps apart and the 31rd note is 34 steps above the 30th. Repeat for each octave. You can only fit 4 octaves of 31-tet notes into the 128-note MIDI keyboard and all my controller keyboards are only 61 notes wide, which is just under 2 octaves. To make this playable I added a transpose function to the table generator.
The CrowBX used to have a different octave scale factor for each voice because each of the 8 pitch CV outputs has a slightly different scale value, probably due to resistor and opamp tolerances. But looking at the number I decided it's not worth making the distinction: the octave values ranged from 8380 to 8383. I think the error introduced by assuming the octave is 8381.5 across all values is less than the natural instability of the analog circuits that respond to the pitch CV.
I refactored the firmware to use a singe scale across all 8 voices. The pitch CV DAC has 16-bit accuracy so for 128 voices I need 128*2=256
bytes to store a tuning table. This is no problem on the microcontroller I'm using (AtMega328P). If I need to squeeze space at some point I can probably calculate them using fewer bytes of instructions because the tables are so regular. In the end I made a generator program gen_scale.c
that gets called by make
to create scale.h
which contains the actual tables. I don't store scale.h
itself in Git because it can be generated quickly at build time.
The changes for the CrowBX are in this commit series.