Jacob Vosmaer's blog

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.

Tags: music crowbx dx7 yamaha

IndexContact