You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
miditones/miditones.c

1387 lines
53 KiB

/*********************************************************************************************
*
* MIDITONES: Convert a MIDI file into a simple bytestream of notes
*
*
* MIDITONES converts a MIDI music file into a much simplified stream of commands, so that
* the music can easily be played on a small microcontroller-based synthesizer that has
* only simple tone generators. This is on github at www.github.com/LenShustek/miditones.
*
* Volume ("velocity") and instrument information in the MIDI file can either be
* discarded or kept. All the tracks are processed and merged into a single time-ordered
* stream of "note on", "note off", "change instrument" and "delay" commands.
*
* This was written for the "Playtune" series of Arduino and Teensy microcontroller
* synthesizers. See the separate documentation for the various Playtune.players at
* www.github.com/LenShustek/arduino-playtune
* www.github.com/LenShustek/ATtiny-playtune
* www.github.com/LenShustek/Playtune_poll
* www.github.com/LenShustek/Playtune_samp
* MIDITONES may also prove useful for other simple music synthesizers..
*
* The output can be either a C-language source code fragment that initializes an
* array with the command bytestream, or a binary file with the bytestream itself.
*
* MIDITONES is written in standard ANSI C and is meant to be executed from the
* command line. There is no GUI interface.
*
* The MIDI file format is complicated, and this has not been tested on all of its
* variations. In particular we have tested only format type "1", which seems
* to be what most of them are. Let me know if you find MIDI files that it
* won't digest and I'll see if I can fix it.
*
* There is a companion program in the same repository called Miditones_scroll
* that can convert the bytestream generated by MIDITONES into a piano-player
* like listing for debugging or annotation. See the documentation in the
* beginning of its source code.
*
*
* ***** The MIDITONES command line *****
*
* To convert a MIDI file called "chopin.mid" into a command bytestream, execute
*
* miditones chopin
*
* It will create a file in the same directory called "chopin.c" which contains
* the C-language statement to intiialize an array called "score" with the bytestream.
*
*
* The general form for command line execution is this:
*
* miditones <options> <basefilename>
*
* The <basefilename> is the base name, without an extension, for the input and
* output files. It can contain directory path information, or not.
*
* The input file is <basefilename>.mid The output filename(s)
* are the base file name with .c, .bin, and/or .log extensions.
*
*
* The following commonly-used command-line options can be specified:
*
* -v Add velocity (volume) information to the output bytestream
*
* -i Add instrument change commands to the output bytestream
*
* -pt Translate notes in the MIDI percussion track to note numbers 128..255
* and assign them to a tone generator as usual.
*
* -d Generate a self-describing file header that says which optional bytestream
* fields are present. This is highly recommended if you are using later
* Playtune players that can check the header to know what data to expect.
*
* -b Generate a binary file with the name <basefilename>.bin, instead of a
* C-language source file with the name <basefilename>.c.
*
* -tn Generate the bytestream so that at most "n" tone generators are used.
* The default is 6 tone generators, and the maximum is 16. The program
* will report how many notes had to be discarded because there weren't
* enough tone generators.
*
*
* The best combination of options to use with the later Playtune music players is:
* -v -i -pt -d
*
*
* The following are lesser-used command-line options:
*
* -p Only parse the MIDI file, and don't generate an output file.
* Tracks are processed sequentially instead of being merged into chronological order.
* This is mostly useful for debugging MIDI file parsing problems.
*
* -lp Log input file parsing information to the <basefilename>.log file
*
* -lg Log output bytestream generation information to the <basefilename>.log file
*
* -nx Put about "x" items on each line of the C file output
*
* -sn Use bytestream generation strategy "n".
* Two strategies are currently implemented:
* 1:favor track 1 notes instead of all tracks equally
* 2:try to keep each track to its own tone generator
*
* -cn Only process the channel numbers whose bits are on in the number "n".
* For example, -c3 means "only process channels 0 and 1"
*
* -kn Change the musical key of the output by n chromatic notes.
* -k-12 goes one octave down, -k12 goes one octave up, etc.
*
* -pi Ignore notes in the MIDI percussion track 9 (also called 10 by some)
*
* -dp Generate IDE-dependent C code to define PROGMEM
*
* -h Give command-line help.
*
*
* ***** The score bytestream *****
*
* The generated bytestream is a series of commands that turn notes on and off,
* maybe change instruments, and begin delays until the next note change.
* Here are the details, with numbers shown in hexadecimal.
*
* If the high-order bit of the byte is 1, then it is one of the following commands:
*
* 9t nn [vv]
* Start playing note nn on tone generator t. Generators are numbered
* starting with 0. The note numbers are the MIDI numbers for the chromatic
* scale, with decimal 60 being Middle C, and decimal 69 being Middle A (440 Hz).
* If the -v option was given, a second byte is added to indicate note volume.
*
* 8t Stop playing the note on tone generator t.
*
* Ct ii Change tone generator t to play instrument ii from now on. This will only
* be generated if the -i option was given.
*
* F0 End of score; stop playing.
*
* E0 End of score; start playing again from the beginning.
* (Shown for completeness; MIDITONES won't generate this.)
*
* If the high-order bit of the byte is 0, it is a command to delay for a while until
* the next note change. The other 7 bits and the 8 bits of the following byte are
* interpreted as a 15-bit big-endian integer that is the number of milliseconds to
* wait before processing the next command. For example,
*
* 07 D0
*
* would cause a delay of 0x07d0 = 2000 decimal millisconds, or 2 seconds. Any tones
* that were playing before the delay command will continue to play.
*
* If the -d option is specified, the bytestream begins with a little header that tells
* what optional information will be in the data. This makes the file more self-describing,
* and allows music players to adapt to different kinds of files. The later Playtune
* players do that. The header looks like this:
*
* 'Pt' Two ascii characters that signal the presence of the header
* nn The length (in one byte) of the entire header, 6..255
* ff1 A byte of flag bits, three of which are currently defined:
* 80 velocity information is present
* 40 instrument change information is present
* 20 translated percussion notes are present
* ff2 Another byte of flags, currently undefined
* tt The number (in one byte) of tone generators actually used in this music.
*
* Any subsequent header bytes covered by the count, if present, are currently undefined
* and should be ignored by players.
*
* Len Shustek, 4 Feb 2011 and later
*
*----------------------------------------------------------------------------------------
* The MIT License (MIT)
* Copyright (c) 2011,2013,2015,2016, Len Shustek
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*********************************************************************************************/
// formatted with: indent miditones.c -br -brf -brs -ce -npsl -nut -i3 -l100 -lc100
/*
* Change log
* 19 January 2011, L.Shustek, V1.0
* -Initial release.
* 26 February 2011, L. Shustek, V1.1
* -Expand the documentation generated in the output file.
* -End the binary output file with an "end of score" command.
* -Fix bug: Some "stop note" commands were generated too early.
* 04 March 2011, L. Shustek, V1.2
* -Minor error message rewording.
* 13 June 2011, L. Shustek, V1.3
* -Add -s2 strategy to try to keep each track on its own tone generator
* for when there are separate speakers. This obviously works only when
* each track is monophonic. (Suggested by Michal Pustejovsky)
* 20 November 2011, L. Shustek, V1.4
* -Add -cn option to mask which channels (tracks) to process
* -Add -kn option to change key
* Both of these are in support of music-playing on my Tesla Coil.
* 05 December 2011, L. Shustek, V1.5
* -Fix command line parsing error for option -s1
* -Display the commandline in the C file output
* -Change to decimal instead of hex for note numbers in the C file output
* 06 August 2013, L. Shustek, V1.6
* -Changed to allow compilation and execution in 64-bit environments
* by using C99 standard intN_t and uintN_t types for MIDI structures,
* and formatting specifications like "PRId32" instead of "ld".
* 04 April 2015, L. Shustek, V1.7
* -Made friendlier to other compilers: import source of strlcpy and strlcat,
* fixed various type mismatches that the LCC compiler didn't fret about.
* Generate "const" for data initialization for compatibility with Arduino IDE v1.6.x.
* 23 January 2016, D. Blackketter, V1.8
* -Fix warnings and errors building on Mac OS X via "gcc miditones.c"
* 25 January 2016, D. Blackketter, Paul Stoffregen, V1.9
* -Merge in velocity output option from Arduino/Teensy Audio Library
* 26 June 2016, L. Shustek, V1.10
* -Fix overflow problem in calculating long delays. (Thanks go to Tiago Rocha.)
* In the process I discover and work around an LCC 32-bit compiler bug.
* 14 August 2016: L. Shustek, V1.11
* -Fix our interpretation of MIDI "running status": it applies only to MIDI events
* (8x through Ex), not, as we thought, also to Sysex (Fx) or Meta (FF) events.
* -Improve parsing of text events for the log.
* -Change log file note and patch numbers, etc., to decimal.
* -Document a summary of the MIDI file format so I don't have to keep looking it up.
* -Add -pi and -pt options to ignore or translate the MIDI percussion track 9.
* -Remove string.h for more portability; add strlength().
* -Add -i option for recording instrument types.in the bytestream.
* -Add -d option for generating a file description header.
* -Add -dp option to make generating the PROGMEM definition optional
* -Add -n option to specify number of items per output line
* -Do better error checking on options
* -Reformat option help
* 26 September 2016, Scott Allen, V1.12
* - Fix spelling and minor formatting errors
* - Fix -p option parsing and handling, which broke when -pi and -pt were added
* - Fix handling of the -nx option to count more accurately
* - Give a proper error message for missing base name
* - Include the header and terminator in the score byte count
*/
#define VERSION "1.12"
/*--------------------------------------------------------------------------------------------
A CONCISE SUMMARY OF MIDI FILE FORMAT
L. Shustek, 16 July 2016.
Gleaned from http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html
Notation:
<xxx> is 1-4 bytes of 7-bit data, concatenated into one 7- to 28-bit number. The high bit of the last byte is 0.
lower case letters are hex digits. If preceeded by 0, only low 7 bits are used.
"xx" are ascii text characters
{xxx}... means indefinite repeat of xxx
A MIDI file is: header_chunk {track_chunk}...
header_chunk
"MThd" 00000006 ffff nnnn dddd
track_chunk
"MTrk" llllllll {<deltatime> track_event}...
"running status" track_event
0x to 7x: assume a missing 8n to En event code which is the same as the last MIDI-event track_event
MIDI-event track_event
8n 0kk 0vv note off, channel n, note kk, velocity vv
9n 0kk 0vv note on, channel n, note kk, velocity vv
An 0kk 0vv key pressure, channel n, note kk, pressure vv
Bn 0cc 0vv control value change, channel n, controller cc, new value vv
Cn 0pp program patch (instrument) change, channel n, new program pp
Dn 0vv channel pressure, channel n, pressure vv
En 0ll 0mm pitch wheel change, value llmm
Note that channel 9 (called 10 by some programs) is used for percussion, particularly notes 35 to 81.
Sysex event track_event
F0 0ii {0dd}... F7 system-dependent data for manufacture ii. See www.gweep.net/~prefect/eng/reference/protocol/midispec.html
F2 0ll 0mm song position pointer
F3 0ss song select
F6 tune request
F7 end of system-dependent data
F8 timing clock sync
FA start playing
FB continue playing
FC stop playing
FE active sensing (hearbeat)
Meta event track_event
FF 00 02 ssss specify sequence number
FF 01 <len> "xx"... arbitrary text
FF 02 <len> "xx"... copyright notice
FF 03 <len> "xx"... sequence or track name
FF 04 <len> "xx"... instrument name
FF 05 <len> "xx"... lyric to be sung
FF 06 <len> "xx"... name of marked point in the score
FF 07 <len> "xx"... description of cue point in the score
FF 20 01 0c default channel for subsequent events without a channel is c
FF 2F 00 end of track
FF 51 03 tttttt set tempo in microseconds per quarter-note
FF 54 05 hhmmssfrff set SMPTE time to start the track
FF 58 04 nnddccbb set time signature
FF 59 02 sfmi set key signature
FF 7F <len> data sequencer-specific data
--------------------------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>
#include <time.h>
#include <inttypes.h>
/*********** MIDI file header formats *****************/
struct midi_header {
int8_t MThd[4];
uint32_t header_size;
uint16_t format_type;
uint16_t number_of_tracks;
uint16_t time_division;
};
struct track_header {
int8_t MTrk[4];
uint32_t track_size;
};
/*********** Global variables ******************/
#define MAX_TONEGENS 16 /* max tone generators: tones we can play simultaneously */
#define DEFAULT_TONEGENS 6 /* default number of tone generators */
#define MAX_TRACKS 24 /* max number of MIDI tracks we will process */
#define PERCUSSION_TRACK 9 /* the track MIDI uses for percussion sounds */
bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput, define_progmem,
velocityoutput, instrumentoutput, percussion_ignore, percussion_translate, do_header;
FILE *infile, *outfile, *logfile;
uint8_t *buffer, *hdrptr;
unsigned long buflen;
int num_tracks;
int tracks_done = 0;
int outfile_maxitems = 26;
int outfile_itemcount = 0;
int num_tonegens = DEFAULT_TONEGENS;
int num_tonegens_used = 0;
int instrument_changes = 0;
int note_on_commands = 0;
unsigned channel_mask = 0xffff; // bit mask of channels to process
int keyshift = 0; // optional chromatic note shift for output file
long int outfile_bytecount = 0;
unsigned int ticks_per_beat = 240;
unsigned long timenow = 0;
unsigned long tempo; /* current tempo in usec/qnote */
struct tonegen_status { /* current status of a tone generator */
bool playing; /* is it playing? */
int track; /* if so, which track is the note from? */
int note; /* what note is playing? */
int instrument; /* what instrument? */
} tonegen[MAX_TONEGENS] = {
{
0}
};
struct track_status { /* current processing point of a MIDI track */
uint8_t *trkptr; /* ptr to the next note change */
uint8_t *trkend; /* ptr past the end of the track */
unsigned long time; /* what time we're at in the score */
unsigned long tempo; /* the tempo last set, in usec per qnote */
unsigned int preferred_tonegen; /* for strategy2, try to use this generator */
unsigned char cmd; /* CMD_xxxx next to do */
unsigned char note; /* for which note */
unsigned char chan; /* from which channel it was */
unsigned char velocity;
unsigned char last_event; /* the last event, for MIDI's "running status" */
bool tonegens[MAX_TONEGENS]; /* which tone generators our notes are playing on */
} track[MAX_TRACKS] = {
{
0}
};
int midi_chan_instrument[16] = {
0
}; /* which instrument is currently being played on each channel */
/* output bytestream commands, which are also stored in track_status.cmd */
#define CMD_PLAYNOTE 0x90 /* play a note: low nibble is generator #, note is next byte */
#define CMD_STOPNOTE 0x80 /* stop a note: low nibble is generator # */
#define CMD_INSTRUMENT 0xc0 /* change instrument; low nibble is generator #, instrument is next byte */
#define CMD_RESTART 0xe0 /* restart the score from the beginning */
#define CMD_STOP 0xf0 /* stop playing */
/* if CMD < 0x80, then the other 7 bits and the next byte are a 15-bit number of msec to delay */
/* these other commands stored in the track_status.com */
#define CMD_TEMPO 0xFE /* tempo in usec per quarter note ("beat") */
#define CMD_TRACKDONE 0xFF /* no more data left in this track */
struct file_hdr_t { /* what the optional file header looks like */
char id1; // 'P'
char id2; // 't'
unsigned char hdr_length; // length of whole file header
unsigned char f1; // flag byte 1
unsigned char f2; // flag byte 2
unsigned char num_tgens; // how many tone generators are used by this score
} file_header = {
'P', 't', sizeof (struct file_hdr_t), 0, 0, MAX_TONEGENS};
#define HDR_F1_VOLUME_PRESENT 0x80
#define HDR_F1_INSTRUMENTS_PRESENT 0x40
#define HDR_F1_PERCUSSION_PRESENT 0x20
long int file_header_num_tgens_position;
/************** command-line processing *******************/
void SayUsage (char *programName) {
static char *usage[] = {
"Convert MIDI files to an Arduino PLAYTUNE bytestream",
"",
"Use: miditones <options> <basefilename>",
" input file will be <basefilename>.mid",
" output file will be <basefilename>.bin or .c",
" log file will be <basefilename>.log",
"",
"Commonly-used options:",
" -v include velocity data",
" -i include instrument change commands",
" -pt translate notes in the percussion track to notes 129 to 255",
" -d include a self-describing file header",
" -b generate binary file output instead of C source text",
" -tn use at most n tone generators (default is 6, max is 16)",
"",
" The best options for later Playtune music players are: -v -i -pt -d",
"",
"Lesser-used command-line options:",
" -p parse only, don't generate bytestream",
" -lp log input parsing",
" -lg log output generation",
" -nx put about x items on each line of the C file output",
" -s1 strategy 1: favor track 1",
" -s2 strategy 2: try to assign tracks to specific tone generators",
" -cn mask for which tracks to process, e.g. -c3 for only 0 and 1",
" -kn key shift in chromatic notes, positive or negative",
" -pi ignore notes in the percussion track (9)",
" -dp define PROGMEM in output C code",
NULL
};
int i = 0;
while (usage[i] != NULL)
fprintf (stderr, "%s\n", usage[i++]);
}
int HandleOptions (int argc, char *argv[]) {
/* returns the index of the first argument that is not an option; i.e.
does not start with a dash or a slash*/
int i, nch, firstnonoption = 0;
/* --- The following skeleton comes from C:\lcc\lib\wizard\textmode.tpl. */
for (i = 1; i < argc; i++) {
if (argv[i][0] == '/' || argv[i][0] == '-') {
switch (toupper (argv[i][1])) {
case 'H':
case '?':
SayUsage (argv[0]);
exit (1);
case 'L':
if (toupper (argv[i][2]) == 'G')
loggen = true;
else if (toupper (argv[i][2]) == 'P')
logparse = true;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break;
case 'P':
if (argv[i][2] == '\0') {
parseonly = true;
break;
}
else if (toupper (argv[i][2]) == 'I')
percussion_ignore = true;
else if (toupper (argv[i][2]) == 'T')
percussion_translate = true;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break;
case 'B':
binaryoutput = true;
if (argv[i][2] != '\0')
goto opterror;
break;
case 'V':
velocityoutput = true;
if (argv[i][2] != '\0')
goto opterror;
break;
case 'I':
instrumentoutput = true;
if (argv[i][2] != '\0')
goto opterror;
break;
case 'S':
if (argv[i][2] == '1')
strategy1 = true;
else if (argv[i][2] == '2')
strategy2 = true;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break;
case 'T':
if (sscanf (&argv[i][2], "%d%n", &num_tonegens, &nch) != 1
|| num_tonegens < 1 || num_tonegens > MAX_TONEGENS)
goto opterror;
printf ("Using %d tone generators.\n", num_tonegens);
if (argv[i][2 + nch] != '\0')
goto opterror;
break;
case 'N':
if (sscanf (&argv[i][2], "%d%n", &outfile_maxitems, &nch) != 1 || outfile_maxitems < 1)
goto opterror;
if (argv[i][2 + nch] != '\0')
goto opterror;
break;
case 'C':
if (sscanf (&argv[i][2], "%d%n", &channel_mask, &nch) != 1 || channel_mask > 0xffff)
goto opterror;
printf ("Channel (track) mask is %04X.\n", channel_mask);
if (argv[i][2 + nch] != '\0')
goto opterror;
break;
case 'K':
if (sscanf (&argv[i][2], "%d%n", &keyshift, &nch) != 1 || keyshift < -100
|| keyshift > 100)
goto opterror;
printf ("Using keyshift %d.\n", keyshift);
if (argv[i][2 + nch] != '\0')
goto opterror;
break;
case 'D':
if (argv[i][2] == '\0') {
do_header = true;
break;
}
if (toupper (argv[i][2]) == 'P')
define_progmem = true;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break;
/* add more option switches here */
opterror:
default:
fprintf (stderr, "\n*** unknown option: %s\n\n", argv[i]);
SayUsage (argv[0]);
exit (4);
}
} else {
firstnonoption = i;
break;
}
}
return firstnonoption;
}
void print_command_line (int argc, char *argv[]) {
int i;
fprintf (outfile, "// command line: ");
for (i = 0; i < argc; i++)
fprintf (outfile, "%s ", argv[i]);
fprintf (outfile, "\n");
}
/**************** utility routines **********************/
/* portable string length */
int strlength (const char *str) {
int i;
for (i = 0; str[i] != '\0'; ++i);
return i;
}
/* safe string copy */
size_t miditones_strlcpy (char *dst, const char *src, size_t siz) {
char *d = dst;
const char *s = src;
size_t n = siz;
/* Copy as many bytes as will fit */
if (n != 0) {
while (--n != 0) {
if ((*d++ = *s++) == '\0')
break;
}
}
/* Not enough room in dst, add NUL and traverse rest of src */
if (n == 0) {
if (siz != 0)
*d = '\0'; /* NUL-terminate dst */
while (*s++);
}
return (s - src - 1); /* count does not include NUL */
}
/* safe string concatenation */
size_t miditones_strlcat (char *dst, const char *src, size_t siz) {
char *d = dst;
const char *s = src;
size_t n = siz;
size_t dlen;
/* Find the end of dst and adjust bytes left but don't go past end */
while (n-- != 0 && *d != '\0')
d++;
dlen = d - dst;
n = siz - dlen;
if (n == 0)
return (dlen + strlength (s));
while (*s != '\0') {
if (n != 1) {
*d++ = *s;
n--;
}
s++;
}
*d = '\0';
return (dlen + (s - src)); /* count does not include NUL */
}
/* match a constant character sequence */
int charcmp (const char *buf, const char *match) {
int len, i;
len = strlength (match);
for (i = 0; i < len; ++i)
if (buf[i] != match[i])
return 0;
return 1;
}
/* announce a fatal MIDI file format error */
void midi_error (char *msg, unsigned char *bufptr) {
unsigned char *ptr;
fprintf (stderr, "---> MIDI file error at position %04X (%d): %s\n",
(uint16_t) (bufptr - buffer), (uint16_t) (bufptr - buffer), msg);
/* print some bytes surrounding the error */
ptr = bufptr - 16;
if (ptr < buffer)
ptr = buffer;
for (; ptr <= bufptr + 16 && ptr < buffer + buflen; ++ptr)
fprintf (stderr, ptr == bufptr ? " [%02X] " : "%02X ", *ptr);
fprintf (stderr, "\n");
exit (8);
}
/* check that we have a specified number of bytes left in the buffer */
void chk_bufdata (unsigned char *ptr, unsigned long int len) {
if ((unsigned) (ptr + len - buffer) > buflen)
midi_error ("data missing", ptr);
}
/* fetch big-endian numbers */
uint16_t rev_short (uint16_t val) {
return ((val & 0xff) << 8) | ((val >> 8) & 0xff);
}
uint32_t rev_long (uint32_t val) {
return (((rev_short ((uint16_t) val) & 0xffff) << 16) |
(rev_short ((uint16_t) (val >> 16)) & 0xffff));
}
/* account for new items in the non-binary output file
and generate a newline every so often. */
void outfile_items (int n) {
outfile_bytecount += n;
outfile_itemcount += n;
if (!binaryoutput && outfile_itemcount >= outfile_maxitems) {
fprintf (outfile, "\n");
outfile_itemcount = 0;
}
}
/************** process the MIDI file header *****************/
void process_header (void) {
struct midi_header *hdr;
unsigned int time_division;
chk_bufdata (hdrptr, sizeof (struct midi_header));
hdr = (struct midi_header *) hdrptr;
if (!charcmp ((char *) hdr->MThd, "MThd"))
midi_error ("Missing 'MThd'", hdrptr);
num_tracks = rev_short (hdr->number_of_tracks);
time_division = rev_short (hdr->time_division);
if (time_division < 0x8000)
ticks_per_beat = time_division;
else
ticks_per_beat = ((time_division >> 8) & 0x7f) /* SMTE frames/sec */ *(time_division & 0xff); /* ticks/SMTE frame */
if (logparse) {
fprintf (logfile, "Header size %" PRId32 "\n", rev_long (hdr->header_size));
fprintf (logfile, "Format type %d\n", rev_short (hdr->format_type));
fprintf (logfile, "Number of tracks %d\n", num_tracks);
fprintf (logfile, "Time division %04X\n", time_division);
fprintf (logfile, "Ticks/beat = %d\n", ticks_per_beat);
}
hdrptr += rev_long (hdr->header_size) + 8; /* point past header to track header, presumably. */
return;
}
/**************** Process a MIDI track header *******************/
void start_track (int tracknum) {
struct track_header *hdr;
unsigned long tracklen;
chk_bufdata (hdrptr, sizeof (struct track_header));
hdr = (struct track_header *) hdrptr;
if (!charcmp ((char *) (hdr->MTrk), "MTrk"))
midi_error ("Missing 'MTrk'", hdrptr);
tracklen = rev_long (hdr->track_size);
if (logparse)
fprintf (logfile, "\nTrack %d length %ld\n", tracknum, tracklen);
hdrptr += sizeof (struct track_header); /* point past header */
chk_bufdata (hdrptr, tracklen);
track[tracknum].trkptr = hdrptr;
hdrptr += tracklen; /* point to the start of the next track */
track[tracknum].trkend = hdrptr; /* the point past the end of the track */
}
/* Get a MIDI-style variable-length integer */
unsigned long get_varlen (uint8_t ** ptr) {
/* Get a 1-4 byte variable-length value and adjust the pointer past it.
These are a succession of 7-bit values with a MSB bit of zero marking the end */
unsigned long val;
int i, byte;
val = 0;
for (i = 0; i < 4; ++i) {
byte = *(*ptr)++;
val = (val << 7) | (byte & 0x7f);
if (!(byte & 0x80))
return val;
}
return val;
}
/*************** Process the MIDI track data ***************************/
/* Skip in the track for the next "note on", "note off" or "set tempo" command,
then record that information in the track status block and return. */
void find_note (int tracknum) {
unsigned long int delta_time;
int event, chan;
int i;
int note, velocity, controller, pressure, pitchbend, instrument;
int meta_cmd, meta_length;
unsigned long int sysex_length;
struct track_status *t;
char *tag;
/* process events */
t = &track[tracknum]; /* our track status structure */
while (t->trkptr < t->trkend) {
delta_time = get_varlen (&t->trkptr);
if (logparse) {
fprintf (logfile, "trk %d ", tracknum);
if (delta_time) {
fprintf (logfile, "delta time %4ld, ", delta_time);
} else {
fprintf (logfile, " ");
}
}
t->time += delta_time;
if (*t->trkptr < 0x80)
event = t->last_event; /* using "running status": same event as before */
else { /* otherwise get new "status" (event type) */
event = *t->trkptr++;
}
if (event == 0xff) { /* meta-event */
meta_cmd = *t->trkptr++;
meta_length = *t->trkptr++;
switch (meta_cmd) {
case 0x00:
if (logparse)
fprintf (logfile, "sequence number %d\n", rev_short (*(unsigned short *) t->trkptr));
break;
case 0x01:
tag = "description";
goto show_text;
case 0x02:
tag = "copyright";
goto show_text;
case 0x03:
tag = "track name";
if (tracknum == 0 && !parseonly && !binaryoutput) {
/* Incredibly, MIDI has no standard for recording the name of the piece!
Track 0's "trackname" is often used for that so we output it to the C file as documentation. */
fprintf (outfile, "// ");
for (i = 0; i < meta_length; ++i) {
int ch = t->trkptr[i];
fprintf (outfile, "%c", isprint (ch) ? ch : '?');
}
fprintf (outfile, "\n");
}
goto show_text;
case 0x04:
tag = "instrument name";
goto show_text;
case 0x05:
tag = "lyric";
goto show_text;
case 0x06:
tag = "marked point";
goto show_text;
case 0x07:
tag = "cue point";
show_text:
if (logparse) {
fprintf (logfile, "meta cmd %02X, length %d, %s: \"", meta_cmd, meta_length, tag);
for (i = 0; i < meta_length; ++i) {
int ch = t->trkptr[i];
fprintf (logfile, "%c", isprint (ch) ? ch : '?');
}
fprintf (logfile, "\"\n");
}
break;
case 0x20:
if (logparse)
fprintf (logfile, "channel prefix %d\n", *t->trkptr);
break;
case 0x2f:
if (logparse)
fprintf (logfile, "end of track\n");
break;
case 0x51: /* tempo: 3 byte big-endian integer! */
t->cmd = CMD_TEMPO;
t->tempo = rev_long (*(unsigned long *) (t->trkptr - 1)) & 0xffffffL;
if (logparse)
fprintf (logfile, "set tempo %ld usec/qnote\n", t->tempo);
t->trkptr += meta_length;
return;
case 0x54:
if (logparse)
fprintf (logfile, "SMPTE offset %08" PRIx32 "\n",
rev_long (*(unsigned long *) t->trkptr));
break;
case 0x58:
if (logparse)
fprintf (logfile, "time signature %08" PRIx32 "\n",
rev_long (*(unsigned long *) t->trkptr));
break;
case 0x59:
if (logparse)
fprintf (logfile, "key signature %04X\n", rev_short (*(unsigned short *) t->trkptr));
break;
case 0x7f:
tag = "sequencer data";
goto show_hex;
default: /* unknown meta command */
tag = "???";
show_hex:
if (logparse) {
fprintf (logfile, "meta cmd %02X, length %d, %s: ", meta_cmd, meta_length, tag);
for (i = 0; i < meta_length; ++i)
fprintf (logfile, "%02X ", t->trkptr[i]);
fprintf (logfile, "\n");
}
break;
}
t->trkptr += meta_length;
}
else if (event < 0x80)
midi_error ("Unknown MIDI event type", t->trkptr);
else {
if (event < 0xf0)
t->last_event = event; // remember "running status" if not meta or sysex event
chan = event & 0xf;
t->chan = chan;
switch (event >> 4) {
case 0x8:
t->note = *t->trkptr++;
velocity = *t->trkptr++;
note_off:
if (logparse)
fprintf (logfile, "note %d off, chan %d, velocity %d\n", t->note, chan, velocity);
if ((1 << chan) & channel_mask) { /* if we're processing this channel */
t->cmd = CMD_STOPNOTE;
return; /* stop processing and return */
}
break; // else keep looking
case 0x9:
t->note = *t->trkptr++;
velocity = *t->trkptr++;
if (velocity == 0) /* some scores use note-on with zero velocity for off! */
goto note_off;
t->velocity = velocity;
if (logparse)
fprintf (logfile, "note %d on, chan %d, velocity %d\n", t->note, chan, velocity);
if ((1 << chan) & channel_mask) { /* if we're processing this channel */
t->cmd = CMD_PLAYNOTE;
return; /* stop processing and return */
}
break; // else keep looking
case 0xa:
note = *t->trkptr++;
velocity = *t->trkptr++;
if (logparse)
fprintf (logfile, "after-touch %d, %d\n", note, velocity);
break;
case 0xb:
controller = *t->trkptr++;
velocity = *t->trkptr++;
if (logparse)
fprintf (logfile, "control change %d, %d\n", controller, velocity);
break;
case 0xc:
instrument = *t->trkptr++;
midi_chan_instrument[chan] = instrument; // record new instrument for this channel
if (logparse)
fprintf (logfile, "program patch %d\n", instrument);
break;
case 0xd:
pressure = *t->trkptr++;
if (logparse)
fprintf (logfile, "channel after-touch %d\n", pressure);
break;
case 0xe:
pitchbend = *t->trkptr++ | (*t->trkptr++ << 7);
if (logparse)
fprintf (logfile, "pitch wheel change %d\n", pitchbend);
break;
case 0xf:
sysex_length = get_varlen (&t->trkptr);
if (logparse)
fprintf (logfile, "SysEx event %d, %ld bytes\n", event, sysex_length);
t->trkptr += sysex_length;
break;
default:
midi_error ("Unknown MIDI command", t->trkptr);
}
}
}
t->cmd = CMD_TRACKDONE; /* no more notes to process */
++tracks_done;
}
/********************* main ****************************/
int main (int argc, char *argv[]) {
int argno;
char *filebasename;
#define MAXPATH 120
char filename[MAXPATH];
int tracknum;
int earliest_tracknum;
unsigned long earliest_time;
int notes_skipped = 0;
printf ("MIDITONES V%s, (C) 2011-2016 Len Shustek\n", VERSION);
if (argc == 1) { /* no arguments */
SayUsage (argv[0]);
return 1;
}
/* process options */
argno = HandleOptions (argc, argv);
if (argno == 0) {
fprintf (stderr, "\n*** No basefilename given\n\n");
SayUsage (argv[0]);
exit (4);
}
filebasename = argv[argno];
/* Open the log file */
if (logparse || loggen) {
miditones_strlcpy (filename, filebasename, MAXPATH);
miditones_strlcat (filename, ".log", MAXPATH);
logfile = fopen (filename, "w");
if (!logfile) {
fprintf (stderr, "Unable to open log file %s\n", filename);
return 1;
}
fprintf (logfile, "MIDITONES V%s log file\n", VERSION);
}
/* Open the input file */
miditones_strlcpy (filename, filebasename, MAXPATH);
miditones_strlcat (filename, ".mid", MAXPATH);
infile = fopen (filename, "rb");
if (!infile) {
fprintf (stderr, "Unable to open input file %s\n", filename);
return 1;
}
/* Read the whole input file into memory */
fseek (infile, 0, SEEK_END); /* find file size */
buflen = ftell (infile);
fseek (infile, 0, SEEK_SET);
buffer = (unsigned char *) malloc (buflen + 1);
if (!buffer) {
fprintf (stderr, "Unable to allocate %ld bytes for the file\n", buflen);
return 1;
}
fread (buffer, buflen, 1, infile);
fclose (infile);
if (logparse)
fprintf (logfile, "Processing %s, %ld bytes\n", filename, buflen);
/* Create the output file */
if (!parseonly) {
miditones_strlcpy (filename, filebasename, MAXPATH);
if (binaryoutput) {
miditones_strlcat (filename, ".bin", MAXPATH);
outfile = fopen (filename, "wb");
} else {
miditones_strlcat (filename, ".c", MAXPATH);
outfile = fopen (filename, "w");
}
if (!outfile) {
fprintf (stderr, "Unable to open output file %s\n", filename);
return 1;
}
file_header.f1 = (velocityoutput ? HDR_F1_VOLUME_PRESENT : 0)
| (instrumentoutput ? HDR_F1_INSTRUMENTS_PRESENT : 0)
| (percussion_translate ? HDR_F1_PERCUSSION_PRESENT : 0);
file_header.num_tgens = num_tonegens;
if (!binaryoutput) { /* create header of C file that initializes score data */
time_t rawtime;
time (&rawtime);
fprintf (outfile, "// Playtune bytestream for file \"%s.mid\" ", filebasename);
fprintf (outfile, "created by MIDITONES V%s on %s", VERSION,
asctime (localtime (&rawtime)));
print_command_line (argc, argv);
if (channel_mask != 0xffff)
fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask);
if (keyshift != 0)
fprintf (outfile, "// Keyshift was %d chromatic notes\n", keyshift);
if (define_progmem) {
fprintf (outfile, "#ifdef __AVR__\n");
fprintf (outfile, "#include <avr/pgmspace.h>\n");
fprintf (outfile, "#else\n");
fprintf (outfile, "#define PROGMEM\n");
fprintf (outfile, "#endif\n");
}
fprintf (outfile, "const unsigned char PROGMEM score [] = {\n");
if (do_header) { // write the C initialization for the file header
fprintf (outfile, "'P','t', 6, 0x%02X, 0x%02X, ", file_header.f1, file_header.f2);
fflush (outfile);
file_header_num_tgens_position = ftell (outfile); // remember where the number of tone generators is
fprintf (outfile, "%2d, // (Playtune file header)\n", file_header.num_tgens);
outfile_bytecount += 6;
}
} else if (do_header) { // write the binary file header
for (int i = 0; i < sizeof (file_header); ++i)
fputc (((unsigned char *) &file_header)[i], outfile);
file_header_num_tgens_position = (char *) &file_header.num_tgens - (char *) &file_header;
outfile_bytecount += sizeof (file_header);
}
}
/* process the MIDI file header */
hdrptr = buffer; /* pointer to file and track headers */
process_header ();
printf (" Processing %d tracks.\n", num_tracks);
if (num_tracks > MAX_TRACKS)
midi_error ("Too many tracks", buffer);
/* initialize processing of all the tracks */
for (tracknum = 0; tracknum < num_tracks; ++tracknum) {
start_track (tracknum); /* process the track header */
find_note (tracknum); /* position to the first note on/off */
/* if we are in "parse only" mode, do the whole track,
so we do them one at a time instead of time-synchronized. */
if (parseonly)
while (track[tracknum].cmd != CMD_TRACKDONE)
find_note (tracknum);
}
/* Continue processing all tracks, in an order based on the simulated time.
This is not unlike multiway merging used for tape sorting algoritms in the 50's! */
tracknum = 0;
if (!parseonly) {
do { /* while there are still track notes to process */
struct track_status *trk;
struct tonegen_status *tg;
int tgnum;
int count_tracks;
unsigned long delta_time, delta_msec;
/* Find the track with the earliest event time,
and output a delay command if time has advanced.
A potential improvement: If there are multiple tracks with the same time,
first do the ones with STOPNOTE as the next command, if any. That would
help avoid running out of tone generators. In practice, though, most MIDI
files do all the STOPNOTEs first anyway, so it won't have much effect.
*/
earliest_time = 0x7fffffff;
/* Usually we start with the track after the one we did last time (tracknum),
so that if we run out of tone generators, we have been fair to all the tracks.
The alternate "strategy1" says we always start with track 0, which means
that we favor early tracks over later ones when there aren't enough tone generators.
*/
count_tracks = num_tracks;
if (strategy1)
tracknum = num_tracks; /* beyond the end, so we start with track 0 */
do {
if (++tracknum >= num_tracks)
tracknum = 0;
trk = &track[tracknum];
if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) {
earliest_time = trk->time;
earliest_tracknum = tracknum;
}
}
while (--count_tracks);
tracknum = earliest_tracknum; /* the track we picked */
trk = &track[tracknum];
if (loggen)
fprintf (logfile, "Earliest time is trk %d, time %ld\n", tracknum, earliest_time);
if (earliest_time < timenow)
midi_error ("INTERNAL: time went backwards", trk->trkptr);
/* If time has advanced, output a "delay" command */
delta_time = earliest_time - timenow;
if (delta_time) {
/* Convert ticks to milliseconds based on the current tempo */
unsigned long long temp;
temp = ((unsigned long long) delta_time * tempo) / ticks_per_beat;
delta_msec = temp / 1000; // get around LCC compiler bug
if (loggen)
fprintf (logfile, "->Delay %ld msec (%ld ticks)\n", delta_msec, delta_time);
if (delta_msec > 0x7fff)
midi_error ("INTERNAL: time delta too big", trk->trkptr);
/* output a 15-bit delay in big-endian format */
if (binaryoutput) {
putc ((unsigned char) (delta_msec >> 8), outfile);
putc ((unsigned char) (delta_msec & 0xff), outfile);
outfile_bytecount += 2;
} else {
fprintf (outfile, "%ld,%ld, ", delta_msec >> 8, delta_msec & 0xff);
outfile_items (2);
}
}
timenow = earliest_time;
/* If this track event is "set tempo", just change the global tempo.
That affects how we generate "delay" commands. */
if (trk->cmd == CMD_TEMPO) {
tempo = trk->tempo;
if (loggen)
fprintf (logfile, "Tempo changed to %ld usec/qnote\n", tempo);
find_note (tracknum);
}
/* If this track event is "stop note", process it and all subsequent "stop notes" for this track
that are happening at the same time. Doing so frees up as many tone generators as possible. */
else if (trk->cmd == CMD_STOPNOTE)
do {
// stop a note
if (!percussion_ignore || trk->chan != PERCUSSION_TRACK) /* if we didn't ignore it as percussion */
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { /* find which generator is playing it */
tg = &tonegen[tgnum];
if (tg->playing && tg->track == tracknum && tg->note == trk->note) {
if (loggen)
fprintf (logfile,
"->Stop note %d, generator %d, track %d\n",
tg->note, tgnum, tracknum);
if (binaryoutput) {
putc (CMD_STOPNOTE | tgnum, outfile);
outfile_bytecount += 1;
} else {
fprintf (outfile, "0x%02X, ", CMD_STOPNOTE | tgnum);
outfile_items (1);
}
tg->playing = false;
trk->tonegens[tgnum] = false;
}
}
find_note (tracknum); // use up the note
}
while (trk->cmd == CMD_STOPNOTE && trk->time == timenow);
/* If this track event is "start note", process only it.
Don't do more than one, so we allow other tracks their chance at grabbing tone generators. */
else if (trk->cmd == CMD_PLAYNOTE) {
if (!percussion_ignore || trk->chan != PERCUSSION_TRACK) { /* ignore percussion track notes if asked to */
bool foundgen = false;
/* maybe try to use the same tone generator that this track used last time */
if (strategy2) {
tg = &tonegen[trk->preferred_tonegen];
if (!tg->playing) {
tgnum = trk->preferred_tonegen;
foundgen = true;
}
}
/* if not, then try for a free tone generator that had been playing the same instrument we need */
if (!foundgen)
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
tg = &tonegen[tgnum];
if (!tg->playing && tg->instrument == midi_chan_instrument[trk->chan]) {
foundgen = true;
break;
}
}
/* if not, then try for any free tone generator */
if (!foundgen)
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
tg = &tonegen[tgnum];
if (!tg->playing) {
foundgen = true;
break;
}
}
if (foundgen) {
int shifted_note;
if (tgnum + 1 > num_tonegens_used)
num_tonegens_used = tgnum + 1;
tg->playing = true;
tg->track = tracknum;
tg->note = trk->note;
trk->tonegens[tgnum] = true;
trk->preferred_tonegen = tgnum;
++note_on_commands;
if (tg->instrument != midi_chan_instrument[trk->chan]) { /* new instrument for this generator */
tg->instrument = midi_chan_instrument[trk->chan];
++instrument_changes;
if (loggen)
fprintf (logfile,
"gen %d changed to instrument %d\n", tgnum, tg->instrument);
if (instrumentoutput) { /* output a "change instrument" command */
if (binaryoutput) {
putc (CMD_INSTRUMENT | tgnum, outfile);
putc (tg->instrument, outfile);
} else {
fprintf (outfile, "0x%02X,%d, ", CMD_INSTRUMENT | tgnum, tg->instrument);
outfile_items (2);
}
}
}
if (loggen)
fprintf (logfile,
"->Start note %d, generator %d, instrument %d, track %d\n",
trk->note, tgnum, tg->instrument, tracknum);
if (percussion_translate && trk->chan == PERCUSSION_TRACK) { /* if requested, */
shifted_note = trk->note + 128; // shift percussion notes up to 128..255
} else { /* shift notes as requested */
shifted_note = trk->note + keyshift;
if (shifted_note < 0)
shifted_note = 0;
if (shifted_note > 127)
shifted_note = 127;
}
if (binaryoutput) {
putc (CMD_PLAYNOTE | tgnum, outfile);
putc (shifted_note, outfile);
outfile_bytecount += 2;
if (velocityoutput) {
putc (trk->velocity, outfile);
outfile_bytecount++;
}
} else {
if (velocityoutput == 0) {
fprintf (outfile, "0x%02X,%d, ", CMD_PLAYNOTE | tgnum, shifted_note);
outfile_items (2);
} else {
fprintf (outfile, "0x%02X,%d,%d, ",
CMD_PLAYNOTE | tgnum, shifted_note, trk->velocity);
outfile_items (3);
}
}
} else {
if (loggen)
fprintf (logfile,
"----> No free generator, skipping note %d, track %d\n",
trk->note, tracknum);
++notes_skipped;
}
}
find_note (tracknum); // use up the note
}
} /* !parseonly do */
while (tracks_done < num_tracks);
// generate the end-of-score command and some commentary
outfile_bytecount++;
if (binaryoutput)
putc (CMD_STOP, outfile);
else {
fprintf (outfile,
"0x%02x};\n// This score contains %ld bytes, and %d tone generator%s used.\n",
CMD_STOP, outfile_bytecount, num_tonegens_used,
num_tonegens_used == 1 ? " is" : "s are");
if (notes_skipped)
fprintf (outfile, "// %d notes had to be skipped.\n", notes_skipped);
}
printf (" %s %d tone generators were used.\n",
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
if (notes_skipped)
printf
(" %d notes were skipped because there weren't enough tone generators.\n",
notes_skipped);
printf (" %ld bytes of score data were generated.\n", outfile_bytecount);
if (loggen)
fprintf (logfile, "%d note-on commands, %d instrument changes.\n",
note_on_commands, instrument_changes);
if (do_header) { // rewrite the file header with the actual number of tone generators used
if (fseek (outfile, file_header_num_tgens_position, SEEK_SET) != 0)
fprintf (stderr, "Can't seek to number of tone generators in the header\n");
else {
if (binaryoutput)
putc (num_tonegens_used, outfile);
else
fprintf (outfile, "%2d", num_tonegens_used);
}
}
fclose (outfile);
} /* if (!parseonly) */
if (loggen || logparse)
fclose (logfile);
printf (" Done.\n");
return 0;
}