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);
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 (
//
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 :
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
Variable declaration
double
FTable[128]; // Frequency table (keymap)
int
key; // Scale start
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
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:
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