test version for -m

testcode
Len Shustek 6 years ago
parent fd994d37cb
commit dab0d8ea31
  1. 234
      miditones.c
  2. BIN
      miditones.exe

@ -111,6 +111,10 @@
* *
* -pi Ignore notes in the MIDI percussion track 9 (also called 10 by some) * -pi Ignore notes in the MIDI percussion track 9 (also called 10 by some)
* *
* -mx Merge events that are within x milliseconds, to reduce the number of "delay"
* commands and thus make the bytestream smaller. The deficits are accumulated
* so that there is no loss of synchronization in the long term.
*
* -dp Generate IDE-dependent C code to define PROGMEM * -dp Generate IDE-dependent C code to define PROGMEM
* *
* -r Terminate the output file with a "restart" command instead of a "stop" command. * -r Terminate the output file with a "restart" command instead of a "stop" command.
@ -260,7 +264,7 @@
* seen, this makes the bytestream 21% smaller! * seen, this makes the bytestream 21% smaller!
* 13 November 2017, Earle Philhower, V1.15 * 13 November 2017, Earle Philhower, V1.15
* - Allow META fields to be larger than 127 bytes. * - Allow META fields to be larger than 127 bytes.
* 2 January 2018, Kodest, V1.16 * 2 January 2018, Kodest, V1
* - Don't generate zero-length delays * - Don't generate zero-length delays
* 13 September 2018, Paul Stoffregen, V1.17 * 13 September 2018, Paul Stoffregen, V1.17
- Fix compile errors on Linux with gcc run in default mode - Fix compile errors on Linux with gcc run in default mode
@ -270,8 +274,13 @@
- Abandon LCC and compile under Microsoft Visual Studio 2017. - Abandon LCC and compile under Microsoft Visual Studio 2017.
- Reformat to condense the source code, so you see more protein and less - Reformat to condense the source code, so you see more protein and less
syntactic sugar on each screen. syntactic sugar on each screen.
* 4 January 2019, Len Shustek, V1.19
- As suggested by Chris van Marle, add the "-mx" parameter to allow timing to be
flexible in order to avoid small delays and thus save space in the bytestream.
- Don't discard fractions of a millisecond in the delay timing, to avoid gradual
drift of the music. This has been a minor problem since V1.0 in 2011.
*/ */
#define VERSION "1.18" #define VERSION "1.19"
/*-------------------------------------------------------------------------------------------- /*--------------------------------------------------------------------------------------------
@ -291,9 +300,13 @@ a MIDI file is:
a header_chunk is: a header_chunk is:
"MThd" 00000006 ffff nnnn dddd "MThd" 00000006 ffff nnnn dddd
ffff is the format type (we have only seen 1)
nnnn is the number of tracks
dddd is the number of ticks per beat (ie, quarter note)
a track_chunk is: a track_chunk is:
"MTrk" llllllll {<deltatime> track_event}... "MTrk" llllllll {<deltatime> track_event}...
<deltatime> is the number of ticks to delay before this event
a running status track_event is: a running status track_event is:
0x to 7x: assume a missing 8n to En event code which is the same as the last MIDI-event track_event 0x to 7x: assume a missing 8n to En event code which is the same as the last MIDI-event track_event
@ -337,8 +350,43 @@ a meta event track_event is:
FF 58 04 nnddccbb set time signature FF 58 04 nnddccbb set time signature
FF 59 02 sfmi set key signature FF 59 02 sfmi set key signature
FF 7F <len> data sequencer-specific data FF 7F <len> data sequencer-specific data
--------------------------------------------------------------------------------------------*/
--------------------------------------------------------------------------------------------*/
/*--------------- processing outline -------------------------------------------------------
main
forall trks, find_note
do
find trk with earliest_time = min(trk->time)
if timenow-earliest_time > minimum delay
gen_stopnotes
output: delay timenow-earliest_time
if CMD_TEMPO,
set global tempo
find_note
else if CMD_STOPNOTE
do
tgen->stopnote_pending = true
find_note
while CMD_STOPNOTE
else if CMD_PLAYNOTE
find tgen
stopnote_pending = false
output: CMD_PLAYNOTE
find_note
while not all CMD_TRACKDONE
exit
find_note
do forever
t->time = varlen
if "note off", CMD_STOPNOTE, return
if "note on", CMD_PLAYNOTE, return
if "tempo", CMD_TEMPO, return
if end of track, CMD_TRACKDONE, return
gen_stopnotes
forall tgen
if stopnote_pending,
output: CMD_STOPNOTE, stopnote_pending = false
--------------------------------------------------------------------------------------------*/
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -388,8 +436,12 @@ unsigned channel_mask = 0xffff; // bit mask of channels to process
int keyshift = 0; // optional chromatic note shift for output file int keyshift = 0; // optional chromatic note shift for output file
long int outfile_bytecount = 0; long int outfile_bytecount = 0;
unsigned int ticks_per_beat = 240; unsigned int ticks_per_beat = 240;
unsigned long timenow = 0; unsigned long timenow = 0; // time now, in ticks
unsigned long tempo; /* current tempo in usec/qnote */ unsigned long tempo; // current global tempo in usec/beat
unsigned long long songtime_usec = 0;
int tempo_changes = 0; // how many times we changed the global tempo
unsigned closetime_msec = 0; // how close, in msec, events should be before they are merged
long int delays_saved = 0; // how many delays were saved because of non-zero merge time
struct tonegen_status { /* current status of a tone generator */ struct tonegen_status { /* current status of a tone generator */
bool playing; /* is it playing? */ bool playing; /* is it playing? */
@ -404,8 +456,9 @@ struct tonegen_status { /* current status of a tone generator */
struct track_status { /* current processing point of a MIDI track */ struct track_status { /* current processing point of a MIDI track */
uint8_t *trkptr; /* ptr to the next note change */ uint8_t *trkptr; /* ptr to the next note change */
uint8_t *trkend; /* ptr past the end of the track */ uint8_t *trkend; /* ptr past the end of the track */
unsigned long time; /* what time we're at in the score */ unsigned long time; /* what time we're at in the score, in ticks */
unsigned long tempo; /* the tempo last set, in usec per qnote */ unsigned long deficit_usec; /* how much behind we are, in usec */
unsigned long tempo; /* the tempo last set, in usec per beat */
unsigned int preferred_tonegen; /* for strategy2, try to use this generator */ unsigned int preferred_tonegen; /* for strategy2, try to use this generator */
unsigned char cmd; /* CMD_xxxx next to do */ unsigned char cmd; /* CMD_xxxx next to do */
unsigned char note; /* for which note */ unsigned char note; /* for which note */
@ -482,6 +535,7 @@ void SayUsage (char *programName) {
" -cn mask for which tracks to process, e.g. -c3 for only 0 and 1", " -cn mask for which tracks to process, e.g. -c3 for only 0 and 1",
" -kn key shift in chromatic notes, positive or negative", " -kn key shift in chromatic notes, positive or negative",
" -pi ignore notes in the percussion track (9)", " -pi ignore notes in the percussion track (9)",
" -mx merge events that are within x msec, to save bytestream space",
" -dp define PROGMEM in output C code", " -dp define PROGMEM in output C code",
" -r terminate output file with \"restart\" instead of \"stop\" command", " -r terminate output file with \"restart\" instead of \"stop\" command",
NULL }; NULL };
@ -505,97 +559,78 @@ int HandleOptions (int argc, char *argv[]) {
SayUsage (argv[0]); SayUsage (argv[0]);
exit (1); exit (1);
case 'L': case 'L':
if (toupper (argv[i][2]) == 'G') if (toupper (argv[i][2]) == 'G') loggen = true;
loggen = true; else if (toupper (argv[i][2]) == 'P') logparse = true;
else if (toupper (argv[i][2]) == 'P') else goto opterror;
logparse = true; if (argv[i][3] != '\0') goto opterror;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break; break;
case 'P': case 'P':
if (argv[i][2] == '\0') { if (argv[i][2] == '\0') {
parseonly = true; parseonly = true;
break; } break; }
else if (toupper (argv[i][2]) == 'I') else if (toupper (argv[i][2]) == 'I') percussion_ignore = true;
percussion_ignore = true; else if (toupper (argv[i][2]) == 'T') percussion_translate = true;
else if (toupper (argv[i][2]) == 'T') else goto opterror;
percussion_translate = true; if (argv[i][3] != '\0') goto opterror;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break; break;
case 'B': case 'B':
binaryoutput = true; binaryoutput = true;
if (argv[i][2] != '\0') if (argv[i][2] != '\0') goto opterror;
goto opterror;
break; break;
case 'V': case 'V':
velocityoutput = true; velocityoutput = true;
if (argv[i][2] != '\0') if (argv[i][2] != '\0') goto opterror;
goto opterror;
break; break;
case 'I': case 'I':
instrumentoutput = true; instrumentoutput = true;
if (argv[i][2] != '\0') if (argv[i][2] != '\0') goto opterror;
goto opterror;
break; break;
case 'S': case 'S':
if (argv[i][2] == '1') if (argv[i][2] == '1') strategy1 = true;
strategy1 = true; else if (argv[i][2] == '2') strategy2 = true;
else if (argv[i][2] == '2') else goto opterror;
strategy2 = true; if (argv[i][3] != '\0') goto opterror;
else
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break; break;
case 'T': case 'T':
if (sscanf (&argv[i][2], "%d%n", &num_tonegens, &nch) != 1 if (sscanf (&argv[i][2], "%d%n", &num_tonegens, &nch) != 1
|| num_tonegens < 1 || num_tonegens > MAX_TONEGENS) || num_tonegens < 1 || num_tonegens > MAX_TONEGENS)
goto opterror; goto opterror;
printf ("Using %d tone generators.\n", num_tonegens); printf ("Using %d tone generators.\n", num_tonegens);
if (argv[i][2 + nch] != '\0') if (argv[i][2 + nch] != '\0') goto opterror;
goto opterror;
break; break;
case 'N': case 'N':
if (sscanf (&argv[i][2], "%d%n", &outfile_maxitems, &nch) != 1 || outfile_maxitems < 1) if (sscanf (&argv[i][2], "%d%n", &outfile_maxitems, &nch) != 1 || outfile_maxitems < 1)
goto opterror; goto opterror;
if (argv[i][2 + nch] != '\0') if (argv[i][2 + nch] != '\0') goto opterror;
goto opterror; break;
case 'M':
if (sscanf(&argv[i][2], "%d%n", &closetime_msec, &nch) != 1) goto opterror;
if (argv[i][2 + nch] != '\0') goto opterror;
break; break;
case 'C': case 'C':
if (sscanf (&argv[i][2], "%i%n", &channel_mask, &nch) != 1 || channel_mask > 0xffff) if (sscanf (&argv[i][2], "%i%n", &channel_mask, &nch) != 1 || channel_mask > 0xffff)
goto opterror; goto opterror;
printf ("Channel (track) mask is %04X.\n", channel_mask); printf ("Channel (track) mask is %04X.\n", channel_mask);
if (argv[i][2 + nch] != '\0') if (argv[i][2 + nch] != '\0') goto opterror;
goto opterror;
break; break;
case 'K': case 'K':
if (sscanf (&argv[i][2], "%d%n", &keyshift, &nch) != 1 || keyshift < -100 if (sscanf (&argv[i][2], "%d%n", &keyshift, &nch) != 1 || keyshift < -100
|| keyshift > 100) || keyshift > 100)
goto opterror; goto opterror;
printf ("Using keyshift %d.\n", keyshift); printf ("Using keyshift %d.\n", keyshift);
if (argv[i][2 + nch] != '\0') if (argv[i][2 + nch] != '\0') goto opterror;
goto opterror;
break; break;
case 'D': case 'D':
if (argv[i][2] == '\0') { if (argv[i][2] == '\0') {
do_header = true; do_header = true;
break; } break; }
if (toupper (argv[i][2]) == 'P') if (toupper (argv[i][2]) == 'P') define_progmem = true;
define_progmem = true; else goto opterror;
else if (argv[i][3] != '\0') goto opterror;
goto opterror;
if (argv[i][3] != '\0')
goto opterror;
break; break;
case 'R': case 'R':
gen_restart = true; gen_restart = true;
if (argv[i][2] != '\0') if (argv[i][2] != '\0') goto opterror;
goto opterror;
break; break;
/* add more option switches here */ /* add more option switches here */
opterror: opterror:
@ -608,12 +643,12 @@ opterror:
break; } } break; } }
return firstnonoption; } return firstnonoption; }
void print_command_line (int argc, char *argv[]) { void print_command_line (FILE *file, int argc, char *argv[]) {
int i; int i;
fprintf (outfile, "// command line: "); fprintf (file, "// command line: ");
for (i = 0; i < argc; i++) for (i = 0; i < argc; i++)
fprintf (outfile, "%s ", argv[i]); fprintf (file, "%s ", argv[i]);
fprintf (outfile, "\n"); } fprintf (file, "\n"); }
/**************** utility routines **********************/ /**************** utility routines **********************/
@ -632,30 +667,25 @@ size_t miditones_strlcpy (char *dst, const char *src, size_t siz) {
/* Copy as many bytes as will fit */ /* Copy as many bytes as will fit */
if (n != 0) { if (n != 0) {
while (--n != 0) { while (--n != 0) {
if ((*d++ = *s++) == '\0') if ((*d++ = *s++) == '\0') break; } }
break; } }
/* Not enough room in dst, add NUL and traverse rest of src */ /* Not enough room in dst, add NUL and traverse rest of src */
if (n == 0) { if (n == 0) {
if (siz != 0) if (siz != 0) *d = '\0'; /* NUL-terminate dst */
*d = '\0'; /* NUL-terminate dst */ while (*s++) ; }
while (*s++); }
return (s - src - 1); /* count does not include NUL */ return (s - src - 1); /* count does not include NUL */
} }
/* safe string concatenation */ /* safe string concatenation */
size_t miditones_strlcat (char *dst, const char *src, size_t siz) { size_t miditones_strlcat (char *dst, const char *src, size_t siz) {
char *d = dst; char *d = dst;
const char *s = src; const char *s = src;
size_t n = siz; size_t n = siz;
size_t dlen; size_t dlen;
/* Find the end of dst and adjust bytes left but don't go past end */ /* Find the end of dst and adjust bytes left but don't go past end */
while (n-- != 0 && *d != '\0') while (n-- != 0 && *d != '\0') d++;
d++;
dlen = d - dst; dlen = d - dst;
n = siz - dlen; n = siz - dlen;
if (n == 0) if (n == 0) return (dlen + strlength (s));
return (dlen + strlength (s));
while (*s != '\0') { while (*s != '\0') {
if (n != 1) { if (n != 1) {
*d++ = *s; *d++ = *s;
@ -666,7 +696,6 @@ size_t miditones_strlcat (char *dst, const char *src, size_t siz) {
} }
/* match a constant character sequence */ /* match a constant character sequence */
int charcmp (const char *buf, const char *match) { int charcmp (const char *buf, const char *match) {
int len, i; int len, i;
len = strlength (match); len = strlength (match);
@ -1024,7 +1053,8 @@ int main (int argc, char *argv[]) {
if (!logfile) { if (!logfile) {
fprintf (stderr, "Unable to open log file %s\n", filename); fprintf (stderr, "Unable to open log file %s\n", filename);
return 1; } return 1; }
fprintf (logfile, "MIDITONES V%s log file\n", VERSION); } fprintf (logfile, "MIDITONES V%s log file\n", VERSION);
print_command_line(logfile, argc, argv); }
/* Open the input file */ /* Open the input file */
@ -1072,7 +1102,7 @@ int main (int argc, char *argv[]) {
fprintf (outfile, "// Playtune bytestream for file \"%s.mid\" ", filebasename); fprintf (outfile, "// Playtune bytestream for file \"%s.mid\" ", filebasename);
fprintf (outfile, "created by MIDITONES V%s on %s", VERSION, fprintf (outfile, "created by MIDITONES V%s on %s", VERSION,
asctime (localtime (&rawtime))); asctime (localtime (&rawtime)));
print_command_line (argc, argv); print_command_line (outfile, argc, argv);
if (channel_mask != 0xffff) if (channel_mask != 0xffff)
fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask); fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask);
if (keyshift != 0) if (keyshift != 0)
@ -1128,7 +1158,7 @@ int main (int argc, char *argv[]) {
struct tonegen_status *tg; struct tonegen_status *tg;
int tgnum; int tgnum;
int count_tracks; int count_tracks;
unsigned long delta_time, delta_msec; unsigned long delta_ticks, delta_msec;
/* Find the track with the earliest event time, /* Find the track with the earliest event time,
and output a delay command if time has advanced. and output a delay command if time has advanced.
@ -1151,8 +1181,7 @@ int main (int argc, char *argv[]) {
if (strategy1) if (strategy1)
tracknum = num_tracks; /* beyond the end, so we start with track 0 */ tracknum = num_tracks; /* beyond the end, so we start with track 0 */
do { do {
if (++tracknum >= num_tracks) if (++tracknum >= num_tracks) tracknum = 0;
tracknum = 0;
trk = &track[tracknum]; trk = &track[tracknum];
if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) { if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) {
earliest_time = trk->time; earliest_time = trk->time;
@ -1166,18 +1195,25 @@ int main (int argc, char *argv[]) {
if (earliest_time < timenow) if (earliest_time < timenow)
midi_error ("INTERNAL: time went backwards", trk->trkptr); midi_error ("INTERNAL: time went backwards", trk->trkptr);
/* If time has advanced, output a "delay" command */ /* If time has advanced, maybe output a "delay" command */
delta_time = earliest_time - timenow; delta_ticks = earliest_time - timenow;
if (delta_time) { unsigned long delta_usec = ((unsigned long long) delta_ticks * tempo) / ticks_per_beat;
/* Convert ticks to milliseconds based on the current tempo */ songtime_usec += delta_usec;
delta_msec = ((unsigned long long) delta_time * tempo) / ticks_per_beat / 1000; // We round up closetime_ticks, because otherwise the default tempo of 500,000 usec/beat with
if (delta_msec) { // if time delay didn't round down to zero msec // the default 480 ticks/beat results in 1.04166 msec/tick, so -m1 doesn't work as expected.
unsigned long closetime_ticks = (((unsigned long)closetime_msec * 1000 + 500) * ticks_per_beat) / tempo;
unsigned long deficit_ticks = (trk->deficit_usec * ticks_per_beat) / tempo;
// Generate a delay only if it is larger than the specified "merge if closer than x msec" time.
// But we accumulate the deficit for each track so that we eventually catch up.
if (delta_ticks + deficit_ticks > closetime_ticks) { // yes, we should generate a delay
gen_stopnotes(); /* first check if any tone generators have "stop note" commands pending */ gen_stopnotes(); /* first check if any tone generators have "stop note" commands pending */
if (loggen) delta_usec += trk->deficit_usec; // include any previously accumulated deficit
fprintf (logfile, "->Delay %ld msec (%ld ticks)\n", delta_msec, delta_time); delta_msec = delta_usec / 1000; // the integral number of milliseconds
if (delta_msec > 0x7fff) trk->deficit_usec = delta_usec % 1000; // save the fraction of a milliscond as the new deficit
midi_error ("INTERNAL: time delta too big", trk->trkptr); if (delta_msec) { // really do it if the delay didn't round down to zero msec
if (loggen) fprintf (logfile, "->Delay %ld msec (%ld ticks)\n", delta_msec, delta_ticks);
if (delta_msec > 0x7fff) midi_error ("INTERNAL: time delta too big", trk->trkptr);
/* output a 15-bit delay in big-endian format */ /* output a 15-bit delay in big-endian format */
if (binaryoutput) { if (binaryoutput) {
putc ((unsigned char) (delta_msec >> 8), outfile); putc ((unsigned char) (delta_msec >> 8), outfile);
@ -1186,15 +1222,19 @@ int main (int argc, char *argv[]) {
else { else {
fprintf (outfile, "%ld,%ld, ", delta_msec >> 8, delta_msec & 0xff); fprintf (outfile, "%ld,%ld, ", delta_msec >> 8, delta_msec & 0xff);
outfile_items (2); } } } outfile_items (2); } } }
else if (delta_usec) {
trk->deficit_usec += delta_usec; // accumulate time for delays we skip
++delays_saved; }
timenow = earliest_time; timenow = earliest_time;
/* If this track event is "set tempo", just change the global tempo. /* If this track event is "set tempo", just change the global tempo.
That affects how we generate "delay" commands. */ That affects how we generate "delay" commands. */
if (trk->cmd == CMD_TEMPO) { if (trk->cmd == CMD_TEMPO) {
if (tempo != trk->tempo) ++tempo_changes;
tempo = trk->tempo; tempo = trk->tempo;
if (loggen) if (loggen)
fprintf (logfile, "Tempo changed to %ld usec/qnote\n", tempo); fprintf (logfile, "Tempo set to %ld usec/qnote\n", tempo);
find_note (tracknum); } find_note (tracknum); }
/* If this track event is "stop note", process it and all subsequent "stop notes" for this track /* If this track event is "stop note", process it and all subsequent "stop notes" for this track
@ -1277,10 +1317,8 @@ int main (int argc, char *argv[]) {
} }
else { /* shift notes as requested */ else { /* shift notes as requested */
shifted_note = trk->note + keyshift; shifted_note = trk->note + keyshift;
if (shifted_note < 0) if (shifted_note < 0) shifted_note = 0;
shifted_note = 0; if (shifted_note > 127) shifted_note = 127; }
if (shifted_note > 127)
shifted_note = 127; }
if (binaryoutput) { if (binaryoutput) {
putc (CMD_PLAYNOTE | tgnum, outfile); putc (CMD_PLAYNOTE | tgnum, outfile);
putc (shifted_note, outfile); putc (shifted_note, outfile);
@ -1293,13 +1331,11 @@ int main (int argc, char *argv[]) {
fprintf (outfile, "0x%02X,%d, ", CMD_PLAYNOTE | tgnum, shifted_note); fprintf (outfile, "0x%02X,%d, ", CMD_PLAYNOTE | tgnum, shifted_note);
outfile_items (2); } outfile_items (2); }
else { else {
fprintf (outfile, "0x%02X,%d,%d, ", fprintf (outfile, "0x%02X,%d,%d, ", CMD_PLAYNOTE | tgnum, shifted_note, trk->velocity);
CMD_PLAYNOTE | tgnum, shifted_note, trk->velocity);
outfile_items (3); } } } outfile_items (3); } } }
else { else {
if (loggen) if (loggen)
fprintf (logfile, fprintf (logfile, "----> No free generator, skipping note %d, track %d\n",
"----> No free generator, skipping note %d, track %d\n",
trk->note, tracknum); trk->note, tracknum);
++notes_skipped; } } ++notes_skipped; } }
find_note (tracknum); // use up the note find_note (tracknum); // use up the note
@ -1323,10 +1359,14 @@ int main (int argc, char *argv[]) {
printf (" %s %d tone generators were used.\n", printf (" %s %d tone generators were used.\n",
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used); num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
if (notes_skipped) if (notes_skipped)
printf printf (" %d notes were skipped because there weren't enough tone generators.\n",
(" %d notes were skipped because there weren't enough tone generators.\n",
notes_skipped); notes_skipped);
printf (" %ld bytes of score data were generated.\n", outfile_bytecount); printf (" %ld bytes of score data were generated, ", outfile_bytecount);
printf("representing %u.%03u seconds of music with %d tempo changes\n",
(unsigned)(songtime_usec / 1000000), (unsigned)(songtime_usec/1000 % 1000), tempo_changes);
if (closetime_msec)
printf (" %ld delays were removed because events within %u msec were merged\n",
delays_saved, closetime_msec);
if (loggen) if (loggen)
fprintf (logfile, "%d note-on commands, %d instrument changes.\n", fprintf (logfile, "%d note-on commands, %d instrument changes.\n",
note_on_commands, instrument_changes); note_on_commands, instrument_changes);

Binary file not shown.
Loading…
Cancel
Save