mtx tuning file format specifications

and how to build an mtx loader program with examples for Max/MSP

 

© Victor Cerullo 2003-2010 (last updated 10 Nov 2010)

 

 

Tuning Interchange Documents

 

The increasing popularity of software synthesizers and software samplers over the past decade generated a growing interest for standardized microtuning systems based on human-readable text files containing the device microtuning data (later on referred as Tuning Interchange Documents, or TIDocs, in this document). Besides those file formats based on MIDI Tuning Standard sysex messages that can be embedded in standard binary MIDI files (.mid), some noticeable TIDoc-based solutions were also adopted to add microtonal support for commercial software synthesizers that have involved, among others, the Scala format (.scl) proposed by Manuel Op de Coul, and the AnaMark format (.tun) proposed by Mark Henning. This article focuses on the syntax requirements of the Microtuner TIDoc format (.mtx), the native file format of Max Magic Microtuner, a microtonal software developed in 2003 as a companion application for Max/MSP.

 

Contents

 

 

 

The Microtuner TIDoc format (.mtx)

 

The mtx is a frequency-based TIDoc format based on three main concepts:

 

I) human readability;

II) easy programming of the loading and conversion routines;

III) no need for a different keyboard mapping format: scales and keyboard mappings use the same format.

The syntax specifications can be summed up as follows:

 

1. the end of line identifier can be a carriage return character (CR = ASCII 13), a line feed character (LF = ASCII 10) or a combination of the two, depending on the platform that generated the file (e.g. Windows uses CR+LF, Unix and OS X use LF only, some older systems like the Mac OS use CR only): the receiving device must correctly interpret any of them; empty lines are allowed anywhere in the file and they should be ignored by the receiving device;

 

2. comment lines containing remarks start with a double slash character ("//"); all comment lines should be placed in the header part of the file (this is not mandatory);

 

3. the first and only line starting with "@" followed by a MIDI note number (integer 0-127) is interpreted as the starting MIDI note number all the frequencies listed in the file will be referred to (60 = middle C);

 

4. a special line following the starting MIDI note number line represents the scale expansion mode: this line can contain either the word ":absolute" or the word ":intervals";

 

4.1 in order to define an n-tone octave-based scale, where frequencies are doubled in each octave, the n tones (frequencies) of at least one octave (any octave of the scale) must be listed after the ":absolute" statement; as a particular case, a whole keyboard mapping can be defined with the ":absolute" mode (the starting MIDI note number line in this case would be "@0");

 

4.2 in order to define a non-octave-based scale based on intervals between frequencies, the receiving device has to determine n different frequency ratios between the adjacent tones of a scale, so in this case n+1 frequencies must be listed below an ":intervals" statement. The receiving device will then calculate and use the ratios to expand the scale all over the full MIDI note range; as a particular case, the ":intervals" mode is also useful for octave-based scales based on an equal division of the octave, where the frequency ratio between two adjacent tones is a constant (like in the twelve-tone Equal Tempered scale);

 

5. zero frequency values are allowed in the frequency list in order for the corresponding MIDI notes to be left unmapped in case of the ":absolute" mode, but their presence in the list is inconsistent in the case of the ":intervals" mode;

 

6. the scale expansion algorithm of the receiving device will have to properly manage out-of-range values according to the device limits.

 

 

Example 1: twelve-tone Equal Tempered scale (expansion mode = absolute)

 

// 12 tone Equal Tempered scale, absolute mode

@60

:absolute

261.62558

277.182617

293.664764

311.126984

329.627563

349.228241

369.994415

391.995422

415.304688

440

466.163757

493.883301

 

 

Example 2: twelve-tone Equal Tempered scale (expansion mode = intervals)

 

// 12 tone Equal Tempered scale, intervals mode

//

// A4 (MIDI note #69) = ref. frequency = f1

// f2 = f1 * pow(2, 1/12)

@69

:intervals

440.

466.16376151809

 

 

Building an mtx file loader program

 

A simple mtx loader program is made up of two separate routines built around a globally declared array containing the 128 keymap frequencies; these routines are:

 

-          the file parser

-          the scale expansion algorithm

 

The file parser is no more no less than a typical text file parser that has to comply with the mapping rules of the mtx file format previously described. A robust parser should keep into account all possible "worst cases" consisting of situations you may have to face when reading a corrupted file or a file that has not been created correctly: such a complete approach is out of scope in this documentation and here I will describe a basic solution which is simple to explain and implement, and effective at the same time. The scale expansion algorithm can also be coded in different ways, but the version you will find here does not require any significant modifications to be adapted to a specific context or language so it can be considered as the one you should normally use in your projects.

 

 

Main variables

 

The main variables and their meaning are as follows:

 

double FTable[128] : frequency table (keymap)

bool intervals : absolute = 0, intervals = 1 (or short used as boolean)

int key : MIDI scale start key (0-127)

int period : number of valid frequencies read from the file

 

 

File parser

 

Inside the parser routine the main variables (intervals, key and period) are initialized and the tuning text file is read to determine their scale definition values; the frequencies read from the file are immediately stored in the corresponding elements of the frequency table (the global FTable array), according to the MIDI scale start key.

 

Variable declaration

 

double FTable[128]; // Frequency table (keymap)

 

int key; // Scale start MIDI note

int period; // Number of tones the scale is composed of

bool intervals; // "Consider Intervals" flag (you may need to use short instead of bool with some compilers)

 

const int MaxLineSize = 1000;

char filename[256];

FILE *in_file;

short i;

char line[MaxLineSize];  // Single line read from file

char c;

short pos, keyline, freqline;

 

File processing

 

Given a valid filename stored in the string named "filename", a simple mtx file parser can be obtained with the following code (for a more robust approach, see the Max/MSP examples below):

 

in_file = fopen(filename, "r");

if (in_file != NULL) {

  printf("Parsing file %s", filename);

  for (i=0; i < 128; ++i) FTable[i]=0;

  intervals = 1;

  period = 0;

  key = 0;

  keyline = 0;

  freqline = 0;

  do {

    pos = 0;

    do {

      c = '\0';

      c = fgetc(in_file);

      if ((c=='\0')||(c=='\r')||(c=='\n')||(c==EOF)) break;

      if ((pos==0)&&(keyline!=1)&&(isdigit(c)||(c=='.'))) freqline = 1;

      if ((pos==0)&&(c=='@')&&(keyline==0)) keyline = 1; else line[pos++] = c;

    } while ((c != EOF) && (pos < MaxLineSize));

    line[pos] = '\0';

    if (strcmp(line,":absolute")==0) intervals=0;

    if (keyline==1) {

      key = atoi(line);

      printf("mtx_loader: scale starts at MIDI key %d", key);

      keyline = 99;

    }

    if (freqline==1) {

      FTable[key+period] = atof(line);

      ++period;

      freqline = 0;

    }

  } while (c != EOF);

  fclose(in_file);

  printf("mtx_loader: number of tones in the scale = %d", period);

  if (period > 0) mtx_loader_expand(x, key,period,intervals);

}

else {

  printf("Error: unable to open file %s", &filename);

}

 

Scale expansion algorithm

 

This is the routine where, based on the main variables values (intervals, key and period), the scale is expanded so that all the empty elements in the FTable array are assigned a frequency value. The code, in the form of a function, is the following:

 

void mtx_loader_expand(int key, int period, bool intervals)

{

  double FRatio[128];

  short i, count;

  short UpperLimit;

  if (intervals==0) {

    // case of ConsiderIntervals checkbox not marked

    for (i=key-1; i >= 0; --i) FTable[i]=FTable[i+period]/2;

    for (i=key+period; i < 128; ++i) FTable[i]=FTable[i-period]*2;

  }

  else {

    // case of ConsiderIntervals checkbox marked

    if (key+period-1 > 126) UpperLimit=126; else UpperLimit=key+period-1;

    for (i=key; i<=UpperLimit; ++i) {

    if (FTable[i]!=0) FRatio[i-key]=FTable[i+1]/(FTable[i]); else {

     if (FTable[i+1]==0) FRatio[i-key]=1; else FRatio[i-key]=999999999;

    }}

    if (period>1) {

      count=0;

      for (i=key+period; i<=127; ++i) {

        FTable[i]=FTable[i-1]*FRatio[count];

        ++count;

        if (count>period-2) count=0;

      }

      count=period-2;

      for (i=1; i<=key; ++i) {

        FTable[key-i]=FTable[key-i+1]/FRatio[count];

        --count;

        if (count<0) count=period-2;

      }

    }

    else {

      // do nothing (less than two tones in the scale)

    }}

}

 

 

Example 1: building an mtx loader external object for Max/MSP

 

Here is an example where the parser and the scale expansion algorithm have been put together to build an mtx loader external object for Max/MSP. This is the original c++ code of the multiplatform version (Windows and OS X Universal Binary) of the "mtx_loader" object released in 2007 (mtx_loader version 1.4, compiled with Apple Xcode for OS X and Cygwin gcc for Windows, using the Max/MSP SDK).

 

 

Mtx_loader version 1.4 for Max/MSP

 

Download this Universal Binary external object for OS X, help patch included

Download this external object for Windows, help patch included

Download the source code as plain text (for Apple Xcode and Cygwin gcc)

 

 

// mtx_loader.c - (C) Victor Cerullo 2007

// version 1.4 - April 2007

//

// multiplatform code compatible with Apple Xcode and Cygwin gcc for Windows

// (same functionalities as mtx_loader version 1.3, with improved file parser)

 

#include <ext.h>

#include <ext_path.h>

#include <math.h>

 

typedef struct mtx_loader

{

    t_object p_ob;

    long p_value0;   // MIDI note value - received from left inlet

    void *p_outlet;  // frequency float outlet

    void *p_out1;    // list of MIDI cents (for sending to a coll object)

    void *p_out2;    // list of frequencies (for sending to a coll object)

    void *p_out3;    // rightmost outlet bangs in case of successful scale expansion

} t_mtx_loader;

 

double FTable[128];  // Frequency table (keymap)

t_symbol *ps_nothing;

void *mtx_loader_class;

void mtx_loader_expand(t_mtx_loader *x, short key, short period, short intervals);

void mtx_loader_read(t_mtx_loader *x, t_symbol *s);

void mtx_loader_doread(t_mtx_loader *x, t_symbol *s, short argc, t_atom *argv);

void mtx_loader_reset(t_mtx_loader *x);

void mtx_loader_dump(t_mtx_loader *x);

void mtx_loader_loadbang(t_mtx_loader *x);

void mtx_loader_sendlist(t_mtx_loader *x);

void mtx_loader_int(t_mtx_loader *x, long n);

void mtx_loader_list(t_mtx_loader *x, t_symbol *s, short ac, t_atom *av);

void mtx_loader_retune(t_mtx_loader *x, t_symbol *s, short ac, t_atom *av);

void mtx_loader_cent(t_mtx_loader *x, t_symbol *s, short ac, t_atom *av);

void mtx_loader_assist(t_mtx_loader *x, void *b, long m, long a, char *s);

void *mtx_loader_new(long n);

 

void main(void)

{

    setup((t_messlist **)&mtx_loader_class, (method)mtx_loader_new, 0L, (short)sizeof(t_mtx_loader), 0L, A_DEFLONG, 0);

    addint((method)mtx_loader_int);

    addmess((method)mtx_loader_read, "read", A_DEFSYM,0);   // open, read and process tuning file

    addmess((method)mtx_loader_reset, "reset", A_DEFSYM,0); // reset to 12-tET keymap

    addmess((method)mtx_loader_dump, "dump", A_DEFSYM,0);   // bang and dump frequency keymap

    addmess((method)mtx_loader_list,"list", A_GIMME,0);     // process list input (MIDI note n, frequency)

    addmess((method)mtx_loader_retune,"retune", A_GIMME,0); // retune keymap to a given reference frequency (MIDI note n, frequency)

    addmess((method)mtx_loader_cent,"cent", A_GIMME,0);     // transpose keymap (+/- cent)

    addmess((method)mtx_loader_assist, "assist", A_CANT, 0);

    addmess((method)mtx_loader_loadbang, "loadbang", A_CANT, 0);

    ps_nothing = gensym("");

}

 

void mtx_loader_expand(t_mtx_loader *x, short key, short period, short intervals)

{

    double FRatio[128];

    short i, count;

    short UpperLimit;

    if (intervals==0) {

        // case of ConsiderIntervals checkbox not marked

        for (i=key-1; i >= 0; --i) FTable[i]=FTable[i+period]/2;

        for (i=key+period; i < 128; ++i) FTable[i]=FTable[i-period]*2;

        mtx_loader_dump(x);

    }

    else {

        // case of ConsiderIntervals checkbox marked

        if (key+period-1 > 126) UpperLimit=126; else UpperLimit=key+period-1;

        for (i=key; i<=UpperLimit; ++i) {

            if (FTable[i]!=0) FRatio[i-key]=FTable[i+1]/(FTable[i]); else {

                if (FTable[i+1]==0) FRatio[i-key]=1; else FRatio[i-key]=999999999;

        }}

        if (period>1) {

            count=0;

            for (i=key+period; i<=127; ++i) {

                FTable[i]=FTable[i-1]*FRatio[count];

                ++count;

                if (count>period-2) count=0;

            }

            count=period-2;

            for (i=1; i<=key; ++i) {

                FTable[key-i]=FTable[key-i+1]/FRatio[count];

                --count;

                if (count<0) count=period-2;

            }

            mtx_loader_dump(x);

        }

        else {

            post("mtx_loader error: INTERVALS mode requires at least two tones");

    }}

}

 

void mtx_loader_read(t_mtx_loader *x, t_symbol *s) {

    defer(x,(method)mtx_loader_doread,s,0,0);

}

 

void mtx_loader_doread(t_mtx_loader *x, t_symbol *s, short argc, t_atom *argv)

{

    char filename[256], fname[256];

    filename[0] = 0;

    fname[0] = 0;

    t_filehandle f_fh;

    long size, bytecount;

    bytecount = 1;

    Byte data[16];

    short i, path, NoGoodFile;

    long outtype;

    const int MaxLineSize = 1024;

    char line[MaxLineSize];  // Single line read from file

    char c;

    short pos, keyline, freqline;

    int key;             // Scale start MIDI note

    int period;          // Number of tones the scale is composed of

    short intervals;     // "Consider Intervals" flag

    if (s==ps_nothing) {

        open_promptset("Select a Microtuner tuning file (.mtx)");

        if (open_dialog(filename, &path, &outtype, 0L, 0)) {

            post("mtx_loader warning: no file selected");

            return;

        }

        } else {

        strcpy(fname, s->s_name);

        strcpy(filename, s->s_name);

        if (locatefile_extended(fname, &path, &outtype, 0L, 0)) {

            post("mtx_loader error: file not found");

            return;

        }

    }

    if (!path_opensysfile(filename,path,&f_fh,READ_PERM)) {

        post("mtx_loader: parsing file %s", filename);

        intervals = 1;

        period = 0;

        key = 0;

        keyline = 0;

        freqline = 0;

        sysfile_geteof(f_fh,&size);

        NoGoodFile = 0;

        do {

            pos = 0;

            do {

                c = '\0';

                if (sysfile_read(f_fh,&bytecount,&data)) {

                    c = EOF;

                    break;

                }

                --size;

                c = (char)data[0];

                if ((c=='\0')||(c=='\r')||(c=='\n')||(c==EOF)||(size==0)) break;

                if ((pos==0)&&(keyline!=1)&&(isdigit(c)||(c=='.'))) freqline = 1;

                if ((pos==0)&&(c=='@')&&(keyline==0)) keyline = 1; else line[pos++] = c;

            } while ((c != EOF) && (size > 0) && (pos < MaxLineSize));

            if (pos < MaxLineSize) {

                line[pos] = '\0';

                if (strcmp(line,":absolute")==0) intervals=0;

                if (keyline==1) {

                    key = atoi(line);

                    if ((key>=0) && (key<128)){

                        post("mtx_loader: scale starts at MIDI key %d", key);

                        for (i=0; i < 128; ++i) FTable[i]=0;

                      keyline = 99;

                    } else NoGoodFile = 1;

                }

                if ((keyline==99)&&(freqline==1)) {

                    if ((key+period) < 128) {

                        FTable[key+period] = atof(line);

                        ++period;

                        freqline = 0;

                    } else NoGoodFile = 1;

                }

            } else NoGoodFile = 1;

        } while ((c != EOF) && (size > 0) && (!NoGoodFile));

        sysfile_close(f_fh);

        if ((keyline!=99)||(period==0)) NoGoodFile = 1;

        if (!NoGoodFile) {

            post("mtx_loader: number of tones in the scale = %d", period);

            if (!intervals) post("mtx_loader: expansion mode = ABSOLUTE");

            else post("mtx_loader: expansion mode = INTERVALS");

            mtx_loader_expand(x, key,period,intervals);  

        } else post("mtx_loader error: file %s is not a valid mtx tuning file", &filename);

    }  else post("mtx_loader error: unable to open file %s", &filename);

}

 

void mtx_loader_reset(t_mtx_loader *x)

{

    // Initialize 12-tET default scale seed and expand keymap

    short i;

    for (i=0; i < 128; ++i) FTable[i]=0;

    FTable[69]=440;

    FTable[70]=466.16376151809;

    mtx_loader_expand(x, 69,2,1);

}

 

void mtx_loader_dump(t_mtx_loader *x)

{

    outlet_bang (x->p_out3);

    mtx_loader_sendlist(x);

}

 

void mtx_loader_sendlist(t_mtx_loader *x)

{

    t_atom FreqList[2], CentList[2];

    short i;

    double ET;

    for (i=0; i < 128; ++i) {

        SETLONG(CentList, i);

        ET = 440 * pow(2, (((double)i-69)/12));

        SETFLOAT(CentList+1, (100*i+(log(FTable[i])-log(ET))*1200/log(2)));

        SETLONG(FreqList, i);

        SETFLOAT(FreqList+1, FTable[i]);

        outlet_list(x->p_out1, 0L, 2, CentList);

        outlet_list(x->p_out2, 0L, 2, FreqList);

    }

}

 

void mtx_loader_int(t_mtx_loader *x, long n)

{

    float frequency;

    x->p_value0 = n;

    if (n<0) {

        x->p_value0 = 0;

    };

    if (n>127) {

        x->p_value0 = 127;

    };

    frequency = FTable[x->p_value0];

    outlet_float(x->p_outlet, frequency);

}

 

void mtx_loader_list(t_mtx_loader *x, t_symbol *s, short ac, t_atom *av)

{

    short i;

    if (ac==2) {

        if (av->a_type==A_LONG) {

            if ((av->a_w.w_long >= 0)&&(av->a_w.w_long < 128)) {

                i = av->a_w.w_long;

                av++;

                if (av->a_type==A_FLOAT) {

                    if (av->a_w.w_float >= 0) {

                        FTable[i]=(double)av->a_w.w_float;

                        mtx_loader_dump(x);

                    }

                }

                else if (av->a_type==A_LONG) {

                    if (av->a_w.w_long >= 0) {

                        FTable[i]=(double)av->a_w.w_long;

                        mtx_loader_dump(x);

                    }

                }

            }

        }

    }

}

 

void mtx_loader_retune(t_mtx_loader *x, t_symbol *s, short ac, t_atom *av)

{

    //retune only works when the frequency of the target MIDI note is > 0

    short i,j;

    double newref;

    if (ac==2) {

        if (av->a_type==A_LONG) {

            if ((av->a_w.w_long >= 0)&&(av->a_w.w_long < 128)) {

                i = av->a_w.w_long;

                av++;

                if (av->a_type==A_FLOAT) {

                    newref = (double)av->a_w.w_float;

                    if ((newref > 0)&&(FTable[i] > 0)) {

                        for (j=0; j < 128; ++j) FTable[j]=newref*(FTable[j]/FTable[i]);

                        mtx_loader_dump(x);

                    }

                }

                else if (av->a_type==A_LONG) {

                    newref = (double)av->a_w.w_long;

                    if ((newref > 0)&&(FTable[i] > 0)) {

                        for (j=0; j < 128; ++j) FTable[j]=newref*(FTable[j]/FTable[i]);

                        mtx_loader_dump(x);

                    }

                }

            }

        }

    }

}

 

void mtx_loader_cent(t_mtx_loader *x, t_symbol *s, short ac, t_atom *av)

{

    short i;

    if (ac==1) {

        if (av->a_type==A_LONG) {

            for (i=0; i < 128; ++i) FTable[i]=FTable[i]*pow(2, (double)av->a_w.w_long/1200);

            mtx_loader_dump(x);

        }

        else if (av->a_type==A_FLOAT) {

            for (i=0; i < 128; ++i) FTable[i]=FTable[i]*pow(2, (double)av->a_w.w_float/1200);

            mtx_loader_dump(x);

        }

    }

}

 

void mtx_loader_assist(t_mtx_loader *x, void *b, long m, long a, char *s)

{

    if (m == ASSIST_INLET)

    sprintf(s,"MIDI note in");

    else {

        switch (a) {

            case 0:

            sprintf(s,"frequency out (float)");

            break;

            case 1:

            sprintf(s,"MIDI cent keymap dump (list)");

            break;

            case 2:

            sprintf(s,"frequency keymap dump (list)");

            break;

            case 3:

            sprintf(s,"bang after scale expansion");

            break;

        }

    }

}

 

void mtx_loader_loadbang(t_mtx_loader *x)

{

    mtx_loader_dump(x);

}

 

void *mtx_loader_new(long n)

{

    t_mtx_loader *x;

    x = (t_mtx_loader *)newobject(mtx_loader_class);

    x->p_out3 = outlet_new(x,0);

    x->p_out2 = listout(x);

    x->p_out1 = listout(x);

    x->p_outlet = floatout(x);

    x->p_value0    = 0;

    mtx_loader_reset(x); // always start in 12-tET mode

    return(x);

}

 

 

Example 2: the Microtuner external object for Max/MSP

 

A more sophisticated approach based on the same overall design described for the mtx_loader object has been used to build the Microtuner external object for Max/MSP. Microtuner is an enhanced mtx file loader that contains a frequency table which is twice the size of a MIDI keymap (i.e. an array of 256 frequencies instead of only 128), in order to make scale-relative pitch bend and modulation easier to manage in a microtonal synthesizer architecture (so that the upper and lower limits for the pitch bend wheel, for instance, will be actual tones of the microtonal scale).

 

 

 

Microtuner version 1.1 for Max/MSP

 

Download this Universal Binary external object for OS X, help patch included

Download this external object for Windows, help patch included

Download the source code as plain text (for Apple Xcode and Cygwin gcc)

 

A relatively powerful feature of the Microtuner object is its response to the "et" message, which can be used to self-configure an n-tone equal tempered scale of a specified width automatically; it requires three parameters: number of tones (max. 128), scale width and MIDI note (0..127) to be used as base frequency. The scale width is a float parameter that must be greater than 1 (unison interval) with a maximum of 10 (ten times the base frequency).

 

Examples:

 

  • et 12 2. 60 = 12-tone equal tempered octave (same as using the "reset" message)

 

  • et 19 2. 60 = 19-tone equal tempered octave with a base frequency equal to MIDI note 60 (central C)

 

  • et 13 3. 60 = 13-tone equal tempered tritave (Bohlen-Pierce temperament)

 

 

 

Example 3: how to build a microtonal synthesizer with Max/MSP

 

The Microtuner external object can be used to build a microtonal synthesizer with Max/MSP quite easily. In this example I will refer to the Max/MSP tutorial patch "MIDI Synthesizer" (tutorial no. 19 of the MSP documentation) and use it as the basis for building a microtonal version of the same. The main patch contains the controller of a two-voice FM synth:

 

 

Mtx FM microtonal synthesizer for Max/MSP

 

Download this tutorial synthesizer patch for Max/MSP with all related subpatches

 

Note that since the frequency table is defined as a global array inside the Microtuner object, messages like "read", "reset" and "et" only need to be sent to one of the instances of the oscillator subpatch, i.e. the one you will consider as the "main" oscillator. The "range" message instead needs to be sent to all the oscillators, because its argument is individually stored in each instance of the Microtuner object. In this architecture the Microtuner object is placed inside the oscillator subpatch, as shown below:

 

 

For clarity, pitch bend rescaling has been segregated as a separate subpatch called "CleverBend": this is where the scale-relative multiplicative bending factor is determined and sent to a signal outlet:

 

 

 

Additional information and downloads

 

Download the Mtx archive (more than 3,400 tuning files converted from the Scala archive)

 

Max Magic Microtuner: http://groups.yahoo.com/group/16tone

Max/MSP: http://www.cycling74.com