add assembler file format output; other minor changes

master
Len Shustek 3 weeks ago
parent 5d5acb2e84
commit 6b41ad2c16
  1. 14
      README.txt
  2. 117
      miditones.c
  3. BIN
      miditones.exe

@ -64,7 +64,7 @@
MIDITONES may also prove useful for other simple music synthesizers. There are
various forks of this code, and of the Playtune players, on Githib.
*** THE PROGRAM
MIDITONES is written in standard ANSI C and is meant to be executed from the
@ -104,8 +104,8 @@
If the user specifies the full .mid filename, the .mid or .MID extension
will be dropped and the remaining name will be used as <basefilename>.
The input file is <basefilename>.mid, and the output filename(s)
are the base file name with .c, .h, .bin, and/or .log extensions.
The input file is <basefilename>.mid, and the output filename(s) are
the base file name with .c, .h, .bin, .asm, .inc, and/or .log extensions.
The following commonly-used command-line options can be specified:
@ -122,7 +122,8 @@
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.
C-language source file with the name <basefilename>.c. This is useful
to create a file that can be input to Miditones_scroll.
-t=n 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
@ -140,6 +141,9 @@
-dp Generate Arduino IDE-dependent C code that uses PROGMEM for the bytestream.
-asm1802 Generate assembler code for the Cosmac Elf 1802 microprocessor
-asm6502 Generate assembler code for the MOS technology 6502 microprocessor
-k=n Change the musical key of the output by n chromatic notes.
-k=-12 goes one octave down, -k=12 goes one octave up, etc.
@ -256,4 +260,4 @@
Any subsequent header bytes included in the length are currently undefined
and should be ignored by players.
Len Shustek, 2011 to 2021; see the change log.
Len Shustek, 2011 to 2025; see the change log.

@ -66,6 +66,7 @@
MIDITONES may also prove useful for other simple music synthesizers. There are
various forks of this code, and of the Playtune players, on Githib.
*** THE PROGRAM
@ -106,8 +107,8 @@
If the user specifies the full .mid filename, the .mid or .MID extension
will be dropped and the remaining name will be used as <basefilename>.
The input file is <basefilename>.mid, and the output filename(s)
are the base file name with .c, .h, .bin, and/or .log extensions.
The input file is <basefilename>.mid, and the output filename(s) are
the base file name with .c, .h, .bin, .asm, .inc, and/or .log extensions.
The following commonly-used command-line options can be specified:
@ -124,7 +125,8 @@
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.
C-language source file with the name <basefilename>.c. This is useful
to create a file that can be input to Miditones_scroll.
-t=n 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
@ -142,6 +144,9 @@
-dp Generate Arduino IDE-dependent C code that uses PROGMEM for the bytestream.
-asm1802 Generate assembler code for the Cosmac Elf 1802 microprocessor
-asm6502 Generate assembler code for the MOS technology 6502 microprocessor
-k=n Change the musical key of the output by n chromatic notes.
-k=-12 goes one octave down, -k=12 goes one octave up, etc.
@ -258,11 +263,11 @@
Any subsequent header bytes included in the length are currently undefined
and should be ignored by players.
Len Shustek, 2011 to 2021; see the change log.
Len Shustek, 2011 to 2025; see the change log.
*----------------------------------------------------------------------------------------
* The MIT License (MIT)
* Copyright (c) 2011,2013,2015,2016,2019,2021 Len Shustek
* Copyright (c) 2011,2013,2015,2016,2019,2021,2025 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
@ -394,10 +399,13 @@
(Thanks to Jonathan Oakley for providing an example of the problem.)
-But sometimes, to reduce the number of tone generators, it's helpful to eliminate
identical notes. So add a -noduplicates option to do just that.
10 April 2025, Len Shustek, V2.5
-Put the length of the song as a comment at the end of the generated source code.
-Add -asm1802 and -asm6502 options, adopting changes from GitHub user fourstix.
future version ideas
-Perhaps elide "note off/note on" event sequences for the same note that
-Perhaps elide "note off/note on" event sequences for the same note that
become adjacent because of -delaymin. Does that happen much, or at all?
-Allow the flexibility to specify note timing on a track-by-track or
@ -409,7 +417,7 @@ future version ideas
channel 8 // organ
options -attacktime=1000 -sustainlevel=80% -releasetime=100 -notemin=200
*/
#define VERSION "2.4"
#define VERSION "2.5"
/*--------------------------------------------------------------------------------------------
@ -601,7 +609,10 @@ struct track_header {
bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput, define_progmem,
volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header,
gen_restart, scorename, showskipped, noduplicates;
gen_restart, scorename, showskipped, noduplicates, asm_output;
char *comment = "//"; // default start of comment
char *hex = "0x"; // default start of hex constant
char *byteop = ""; // what assembler psuedo-op indicates a byte constant
FILE *infile, *outfile, *logfile;
uint8_t *buffer, *hdrptr;
unsigned long buflen;
@ -708,7 +719,7 @@ void SayUsage(char *programName) {
"",
"Use: miditones <options> <basefilename>",
" input file will be <basefilename>.mid",
" output file will be <basefilename>.bin or .c or .h",
" output file will be <basefilename>.bin, .c, .h, .asm, or .inc",
" log file will be <basefilename>.log",
"",
"Commonly-used options:",
@ -733,6 +744,8 @@ void SayUsage(char *programName) {
" -r terminate output file with \"restart\" instead of \"stop\" command",
" -s1 strategy 1: favor track 1",
" -s2 strategy 2: try to assign tracks to specific tone generators",
" -asm1802 generate assembler code for the 1802 microprocessor",
" -asm6502 generate assembler code for the 6502 microprocessor",
" -showskipped display information about each note that had to be skipped",
" -noduplicates remove identical notes playing on different channels",
" -delaymin=x minimum delay is x msec, to save bytestream space",
@ -771,6 +784,10 @@ int HandleOptions (int argc, char *argv[]) {
else if (opt_key(arg, "pi")) percussion_ignore = true;
else if (opt_key(arg, "pt")) percussion_translate = true;
else if (opt_key(arg, "r")) gen_restart = true;
else if (opt_key(arg, "asm1802")) {
asm_output = true; comment = ";"; byteop = " db "; hex = "$"; outfile_maxitems = 16; }
else if (opt_key(arg, "asm6502")) {
asm_output = true; comment = ";"; byteop = " .byte "; hex = "$"; outfile_maxitems = 16; }
else if (opt_int(arg, "delaymin", &tempint, 1, 1000)) delaymin_usec = tempint * 1000;
else if (opt_int(arg, "releasetime", &tempint, 0, INT_MAX)) releasetime_usec = tempint * 1000;
else if (opt_int(arg, "notemin", &tempint, 0, INT_MAX)) notemin_usec = tempint * 1000;
@ -801,7 +818,7 @@ int HandleOptions (int argc, char *argv[]) {
return firstnonoption; }
void print_command_line (FILE *file, int argc, char *argv[]) {
fprintf (file, "// command line: ");
fprintf (file, "%s command line: ", comment);
for (int i = 0; i < argc; i++) fprintf (file, "%s ", argv[i]);
fprintf (file, "\n"); }
@ -888,12 +905,15 @@ uint32_t rev_long (uint32_t val) {
/* account for new items in the non-binary output file
and generate a newline every so often. */
void outfile_items (int n) {
void outfile_items(int n) {
outfile_bytecount += n;
outfile_itemcount += n;
if (!binaryoutput && outfile_itemcount >= outfile_maxitems) {
fprintf (outfile, "\n");
outfile_itemcount = 0; } }
if (!binaryoutput) {
if (outfile_itemcount >= outfile_maxitems) {
if (asm_output) fprintf(outfile, "\n%s", byteop);
else fprintf(outfile, ",\n");
outfile_itemcount = 0; }
else fprintf(outfile, ", "); } }
//******* structures for recording track, channel, and tone generator status
@ -1076,8 +1096,13 @@ void remove_queue_entry(int ndx) { // remove the oldest queue entry
putc(CMD_INSTRUMENT | tgnum, outfile);
putc(tg->note.instrument, outfile);
outfile_bytecount += 2; }
else if (asm_output) {
fprintf(outfile, "%s%02X", hex, CMD_INSTRUMENT | tgnum);
outfile_items(1);
fprintf(outfile, "%3d", tg->note.instrument);
outfile_items(1); }
else {
fprintf(outfile, "0x%02X,%d, ", CMD_INSTRUMENT | tgnum, tg->note.instrument);
fprintf(outfile, "0x%02X,%d", CMD_INSTRUMENT | tgnum, tg->note.instrument);
outfile_items(2); } } }
if (loggen) fprintf(logfile, " play tgen %d %s\n", tgnum, describe(&q->note));
tg->playing = true;
@ -1093,12 +1118,20 @@ void remove_queue_entry(int ndx) { // remove the oldest queue entry
if (volume_output) {
putc(tg->note.volume, outfile);
outfile_bytecount +=1; } }
else if (asm_output) {
fprintf(outfile, "%s%02X", hex, CMD_PLAYNOTE | tgnum);
outfile_items(1);
fprintf(outfile, "%3d", tg->note.note);
outfile_items(1);
if (volume_output) {
fprintf(outfile, "%3d", tg->note.volume);
outfile_items(1); } }
else {
if (volume_output == 0) {
fprintf(outfile, "0x%02X,%d, ", CMD_PLAYNOTE | tgnum, tg->note.note);
fprintf(outfile, "0x%02X,%d", CMD_PLAYNOTE | tgnum, tg->note.note);
outfile_items(2); }
else {
fprintf(outfile, "0x%02X,%d,%d, ", CMD_PLAYNOTE | tgnum, tg->note.note, tg->note.volume);
fprintf(outfile, "0x%02X,%d,%d", CMD_PLAYNOTE | tgnum, tg->note.note, tg->note.volume);
outfile_items(3); } } }
else {
if (loggen) fprintf(logfile, " *** at %lu.%03lu msec no free generator; skipping %s\n",
@ -1117,8 +1150,13 @@ void generate_delay(unsigned long delta_msec) { // output a delay command
putc((byte)(delta_msec >> 8), outfile);
putc((byte)(delta_msec & 0xff), outfile);
outfile_bytecount += 2; }
else if (asm_output) {
fprintf(outfile, "%s%02X", hex, (byte)(delta_msec >> 8));
outfile_items(1);
fprintf(outfile, "%s%02X", hex, (byte)(delta_msec & 0xff));
outfile_items(1); }
else {
fprintf(outfile, "%ld,%ld, ", delta_msec >> 8, delta_msec & 0xff);
fprintf(outfile, "%ld,%ld", delta_msec >> 8, delta_msec & 0xff);
outfile_items(2); } } }
// output all queue elements which are at the oldest time or at most "delaymin" later
@ -1156,7 +1194,7 @@ void pull_queue(void) {
putc(CMD_STOPNOTE | tgnum, outfile);
outfile_bytecount += 1; }
else {
fprintf(outfile, "0x%02X, ", CMD_STOPNOTE | tgnum);
fprintf(outfile, "%s%02X", hex, CMD_STOPNOTE | tgnum);
outfile_items(1); }
if (loggen) fprintf(logfile, " stop tgen %d %s\n", tgnum, describe(&tg->note));
tg->stopnote_pending = false;
@ -1356,7 +1394,7 @@ void find_next_note (int tracknum) {
tag = "copyright"; goto show_text;
case 0x03:
tag = "track name";
if (tracknum == 0 && !parseonly && !binaryoutput) {
if (tracknum == 0 && !parseonly && !binaryoutput && !asm_output) {
/* 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, "// ");
@ -1622,8 +1660,9 @@ void process_track_data(void) {
putc(gen_restart ? CMD_RESTART : CMD_STOP, outfile);
outfile_bytecount +=1; }
else {
fprintf(outfile, "0x%02X};", gen_restart ? CMD_RESTART : CMD_STOP);
outfile_items(1);
fprintf(outfile, "%s%02X", hex, gen_restart ? CMD_RESTART : CMD_STOP);
++outfile_bytecount;
if (!asm_output) fprintf(outfile, "};");
fprintf(outfile, "\n"); } }
@ -1647,7 +1686,7 @@ int main (int argc, char *argv[]) {
SayUsage (argv[0]);
exit (4); }
filebasename = argv[argno];
// strip off trailing .mid or .MID extension if provided by user
basenamelen = strlength(filebasename);
if (basenamelen > 4 &&
@ -1696,7 +1735,7 @@ int main (int argc, char *argv[]) {
miditones_strlcat (filename, ".bin", MAXPATH);
outfile = fopen (filename, "wb"); }
else {
miditones_strlcat (filename, scorename ? ".h" : ".c", MAXPATH);
miditones_strlcat (filename, asm_output ? (scorename ? ".inc" : ".asm") : (scorename ? ".h" : ".c"), MAXPATH);
outfile = fopen (filename, "w"); }
if (!outfile) {
fprintf (stderr, "Unable to open output file %s\n", filename);
@ -1708,27 +1747,29 @@ int main (int argc, char *argv[]) {
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, "%s Playtune bytestream for file \"%s.mid\" ", comment, filebasename);
fprintf (outfile, "created by MIDITONES V%s on %s", VERSION,
asctime (localtime (&rawtime)));
print_command_line (outfile, argc, argv);
if (channel_mask != 0xffff)
fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask);
fprintf (outfile, "%s Only the masked channels were processed: %04X\n", comment, channel_mask);
if (keyshift != 0)
fprintf (outfile, "// Keyshift was %d chromatic notes\n", keyshift);
fprintf (outfile, "%s Keyshift was %d chromatic notes\n", comment, 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 %s [] = {\n",
if (asm_output) // create the assembler label for the score
fprintf(outfile, "\n%s:\n%s", filebasename, byteop);
else fprintf (outfile, "const unsigned char PROGMEM %s [] = {\n",
scorename ? filebasename : "score");
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);
if (do_header) { // write the initialization for the file header
fprintf (outfile, "'P','t', 6, %s%02X, %s%02X, ", hex, file_header.f1, hex, 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);
fprintf (outfile, "%2d%s %s (Playtune file header)\n%s", file_header.num_tgens, asm_output ? "" : ",", comment, byteop);
outfile_bytecount += 6; } }
else if (do_header) { // write the binary file header
int i;
@ -1773,11 +1814,14 @@ int main (int argc, char *argv[]) {
// generate the ending commentary
if (!binaryoutput) {
fprintf(outfile, "\n// This %ld byte score contains %d notes and uses %d tone generator%s\n",
outfile_bytecount, note_on_commands, num_tonegens_used,
fprintf(outfile, "\n%s This %ld byte score contains %d notes and uses %d tone generator%s.\n",
comment, outfile_bytecount, note_on_commands, num_tonegens_used,
num_tonegens_used == 1 ? "" : "s");
fprintf(outfile, "%s It encodes %u.%03u seconds of music with %d tempo change%s.\n",
comment, (unsigned)(timenow_usec / 1000000), (unsigned)(timenow_usec / 1000 % 1000),
tempo_changes, tempo_changes == 1 ? "" : "s");
if (notes_skipped)
fprintf(outfile, "// %d notes had to be skipped\n", notes_skipped); }
fprintf(outfile, "%s %d notes had to be skipped.\n", comment, notes_skipped); }
printf(" %s %d tone generators were used.\n",
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
if (notes_skipped)
@ -1793,8 +1837,9 @@ int main (int argc, char *argv[]) {
" (Consider recompiling with MAX_TRACKNOTES bigger than %d, to allow more simultaneous notes.)\n",
noteinfo_overflow, noteinfo_notfound, MAX_CHANNELNOTES);
printf(" %ld bytes of score data were generated, ", outfile_bytecount);
printf("representing %u.%03u seconds of music with %d tempo changes\n",
(unsigned)(timenow_usec / 1000000), (unsigned)(timenow_usec / 1000 % 1000), tempo_changes);
printf("encoding %u.%03u seconds of music with %d tempo change%s.\n",
(unsigned)(timenow_usec / 1000000), (unsigned)(timenow_usec / 1000 % 1000),
tempo_changes, tempo_changes == 1 ? "" : "s");
if (delaymin_usec)
printf(" %ld delays were removed because the minimum delay of %u msec caused events to be merged\n",
delays_saved, (unsigned)(delaymin_usec / 1000));

Binary file not shown.
Loading…
Cancel
Save