diff --git a/README.txt b/README.txt index f5f29dd..48ffd8c 100644 --- a/README.txt +++ b/README.txt @@ -1,171 +1,241 @@ -/********************************************************************************************* -* -* 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 -* www.github.com/LenShustek/Playtune_synth -* 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 -* -* The 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 .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 .bin, instead of a -* C-language source file with the name .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 .log file -* -* -lg Log output bytestream generation information to the .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". In addition to decimal, -* "n" can be also specified in hex using a 0x prefix or octal with a 0 prefix. -* -* -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 -* -* -r Terminate the output file with a "restart" command instead of a "stop" command. -* -* -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, replacing any previous note. -* Generators are numbered starting with 0. The note numbers are the MIDI -* numbers for the chromatic scale, with 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. -* -* 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 -* \ No newline at end of file + + MIDITONES: Convert a MIDI file into a simple bytestream of notes + + + MIDITONES compiles a MIDI music file into a much simplified compact time-ordered 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. + + MIDITONES was written for the "Playtune" series of Arduino and Teensy + microcontroller software synthesizers: + + www.github.com/LenShustek/arduino-playtune + This original version of Playtune, first released in 2011, uses a separate hardware timer + for each note to generate a square wave on an output pin. All the pins are then combined + with a simple resistor network connected to a speaker and/or amplifier. It can only play + as many simutaneous notes as there are timers. There is no volume modulation. + + www.github.com/LenShustek/Playtune_poll + This second vesion uses only one hardware timer that interrupts periodically at a fast + rate, and toggles square waves onto any number of digital output pins. It also implements + primitive volume modulation by changing the duty cycle of the square wave. The number of + simultaneous notes is limited only by the number of output pins and the speed of the processor. + + www.github.com/LenShustek/Playtune_samp + The third version also uses only one hardware timer interrupting frequently, but + uses the hardware digital-to-analog converter on high-performance microntrollers like + the Teensy to generate an analog wave that is the sum of stored samples of sounds for + many different instruments. The samples are scaled to the right frequency and volume, + and any number of instrument samples can be used and mapped to MIDI patches. The sound + quality is much better, although not in league with real synthesizers. It currently + only supports Teensy boards. + + www.github.com/LenShustek/Playtune_synth + The fourth version is an audio object for the PJRC Audio Library. + https://www.pjrc.com/teensy/td_libs_Audio.html + It allows up to 16 simultaneous sound generators that are internally mixed, at + the appropriate volume, to produce one monophonic audio stream. + Sounds are created from sampled one-cycle waveforms for any number of instruments, + each with its own attack-hold-decay-sustain-release envelope. Percussion sounds + (from MIDI channel 10) are generated from longer sampled waveforms of a complete + instrument strike. Each generator's volume is independently adjusted according to + the MIDI velocity of the note being played before all channels are mixed. + + www.github.com/LenShustek/ATtiny-playtune + This is a much simplified version that fits, with a small song, into an ATtiny + processor with only 4K of flash memory. It also using polling with only one timer, + and avoids multiplication or division at runtime for speed. It was written + for the Evil Mad Scientist menorah kit: + https://www.evilmadscientist.com/2009/new-led-hanukkah-menorah-kit/ + https://forum.evilmadscientist.com/discussion/263/making-the-menorah-play-music + (Imagine what you can do with the $1 8-pin ATtiny85 with a whopping 8K!) + + MIDITONES may also prove useful for other simple music synthesizers. There are + various forks of the code, and the Playtune players, on Githib. + + *** THE PROGRAM + + MIDITONES is written in standard ANSI C and is meant to be executed from the + command line. There is no GUI interface. + + 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. + + 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 near the + top of its source code. + + +*** THE 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 + + The 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 .mid, and the output filename(s) + are the base file name with .c, .h, .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 .bin, instead of a + C-language source file with the name .c. + + -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 + 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: + + -c=n Only process the channel numbers whose bits are on in the number "n". + For example, -c3 means "only process channels 0 and 1". In addition to decimal, + "n" can be also specified in hex using a 0x prefix. + + -dp Generate Arduino IDE-dependent C code that uses PROGMEM for the bytestream. + + -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. + + -lp Log input file parsing information to the .log file + + -lg Log output bytestream generation information to the .log file + + -n=x Put about "x" items on each line of the C file output + + -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. + + -pi Ignore notes in the MIDI percussion track 9 (also called 10 by some) + + -r Terminate the output file with a "restart" command instead of a "stop" command. + + -sn Use bytestream generation strategy "n". Two are currently implemented: + 1:favor track 1 notes instead of all tracks equally + 2:try to keep each track to its own tone generator + + -h Give command-line help. + + -delaymin=x Don't generate delays less than x milliseconds long, to reduce the number + of "delay" commands and thus make the bytestream smaller, at the expense of + moving notes slightly. The deficits are accumulated and eventually used, + so that there is no loss of synchronization in the long term. + The default is 0, which means timing is exact to the millisecond. + + -releasetime=x Stop each note x milliseconds before it is supposed to end. This results + in better sound separation between notes. It might also allow more notes to + be played with fewer tone generators, since there could be fewer + simultaneous notes playing. + + -notemin=x The releasetime notwithstanding, don't let the resulting note be reduced + to smaller than x milliseconds. Making releasetime very large and notemin + small results in staccato sounds. + + -attacktime=x The high-volume attack phase of a note lasts x milliseconds, after which + the lower-volume sustain phase begins, unless the release time makes it + too short. (Only valid with -v.) + + -attacknotemax=x Notes larger than x milliseconds won't have the attack/sustain profile + applied. That allows sustained organ-like pedaling. + + -sustainlevel=p The volume level during the sustain phase is p percent of the starting + note volume. (Only valid with -v.) + + -scorename Use as the name of the score in the generated C code + instead of "score", and name the file .h instead of + .c. That allows multiple scores to be directly #included + into an Arduino .ino file without modification. + + Note that for backwards compatibility and easier batch-file processing, the equal sign + for specifying an option's numeric value may be omitted. Also, numeric values may be + specified in hex as 0xhhhh. + +*** THE SCORE BYTESTREAM + + The generated bytestream is a series of commands to turn notes on and off, + change instruments, and request a delay until the next event time. + 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, + where the two characters are a hex representation of one byte: + + 9t nn [vv] + Start playing note nn on tone generator t, replacing any previous note. + Generators are numbered starting with 0. The note numbers are the MIDI + numbers for the chromatic scale, with decimal 69 being Middle A (440 Hz). + If the -v option was given, the third byte specifies the 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, but start playing again from the beginning. This is + generated by the -r option. + + 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 volume 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, 2011 to 2019; see the change log. diff --git a/miditones.c b/miditones.c index 2dd74cd..5f45234 100644 --- a/miditones.c +++ b/miditones.c @@ -1,178 +1,250 @@ /********************************************************************************************* -* -* 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 -* www.github.com/LenShustek/Playtune_synth -* 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 -* -* The 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 .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 .bin, instead of a -* C-language source file with the name .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 .log file -* -* -lg Log output bytestream generation information to the .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". In addition to decimal, -* "n" can be also specified in hex using a 0x prefix or octal with a 0 prefix. -* -* -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 -* -* -r Terminate the output file with a "restart" command instead of a "stop" command. -* -* -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, replacing any previous note. -* Generators are numbered starting with 0. The note numbers are the MIDI -* numbers for the chromatic scale, with 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. -* -* 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 -* + + MIDITONES: Convert a MIDI file into a simple bytestream of notes + + + MIDITONES compiles a MIDI music file into a much simplified compact time-ordered 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. + + MIDITONES was written for the "Playtune" series of Arduino and Teensy + microcontroller software synthesizers: + + www.github.com/LenShustek/arduino-playtune + This original version of Playtune, first released in 2011, uses a separate hardware timer + for each note to generate a square wave on an output pin. All the pins are then combined + with a simple resistor network connected to a speaker and/or amplifier. It can only play + as many simutaneous notes as there are timers. There is no volume modulation. + + www.github.com/LenShustek/Playtune_poll + This second vesion uses only one hardware timer that interrupts periodically at a fast + rate, and toggles square waves onto any number of digital output pins. It also implements + primitive volume modulation by changing the duty cycle of the square wave. The number of + simultaneous notes is limited only by the number of output pins and the speed of the processor. + + www.github.com/LenShustek/Playtune_samp + The third version also uses only one hardware timer interrupting frequently, but + uses the hardware digital-to-analog converter on high-performance microntrollers like + the Teensy to generate an analog wave that is the sum of stored samples of sounds for + many different instruments. The samples are scaled to the right frequency and volume, + and any number of instrument samples can be used and mapped to MIDI patches. The sound + quality is much better, although not in league with real synthesizers. It currently + only supports Teensy boards. + + www.github.com/LenShustek/Playtune_synth + The fourth version is an audio object for the PJRC Audio Library. + https://www.pjrc.com/teensy/td_libs_Audio.html + It allows up to 16 simultaneous sound generators that are internally mixed, at + the appropriate volume, to produce one monophonic audio stream. + Sounds are created from sampled one-cycle waveforms for any number of instruments, + each with its own attack-hold-decay-sustain-release envelope. Percussion sounds + (from MIDI channel 10) are generated from longer sampled waveforms of a complete + instrument strike. Each generator's volume is independently adjusted according to + the MIDI velocity of the note being played before all channels are mixed. + + www.github.com/LenShustek/ATtiny-playtune + This is a much simplified version that fits, with a small song, into an ATtiny + processor with only 4K of flash memory. It also using polling with only one timer, + and avoids multiplication or division at runtime for speed. It was written + for the Evil Mad Scientist menorah kit: + https://www.evilmadscientist.com/2009/new-led-hanukkah-menorah-kit/ + https://forum.evilmadscientist.com/discussion/263/making-the-menorah-play-music + (Imagine what you can do with the $1 8-pin ATtiny85 with a whopping 8K!) + + MIDITONES may also prove useful for other simple music synthesizers. There are + various forks of the code, and the Playtune players, on Githib. + + *** THE PROGRAM + + MIDITONES is written in standard ANSI C and is meant to be executed from the + command line. There is no GUI interface. + + 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. + + 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 near the + top of its source code. + + +*** THE 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 + + The 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 .mid, and the output filename(s) + are the base file name with .c, .h, .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 .bin, instead of a + C-language source file with the name .c. + + -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 + 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: + + -c=n Only process the channel numbers whose bits are on in the number "n". + For example, -c3 means "only process channels 0 and 1". In addition to decimal, + "n" can be also specified in hex using a 0x prefix. + + -dp Generate Arduino IDE-dependent C code that uses PROGMEM for the bytestream. + + -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. + + -lp Log input file parsing information to the .log file + + -lg Log output bytestream generation information to the .log file + + -n=x Put about "x" items on each line of the C file output + + -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. + + -pi Ignore notes in the MIDI percussion track 9 (also called 10 by some) + + -r Terminate the output file with a "restart" command instead of a "stop" command. + + -sn Use bytestream generation strategy "n". Two are currently implemented: + 1:favor track 1 notes instead of all tracks equally + 2:try to keep each track to its own tone generator + + -h Give command-line help. + + -delaymin=x Don't generate delays less than x milliseconds long, to reduce the number + of "delay" commands and thus make the bytestream smaller, at the expense of + moving notes slightly. The deficits are accumulated and eventually used, + so that there is no loss of synchronization in the long term. + The default is 0, which means timing is exact to the millisecond. + + -releasetime=x Stop each note x milliseconds before it is supposed to end. This results + in better sound separation between notes. It might also allow more notes to + be played with fewer tone generators, since there could be fewer + simultaneous notes playing. + + -notemin=x The releasetime notwithstanding, don't let the resulting note be reduced + to smaller than x milliseconds. Making releasetime very large and notemin + small results in staccato sounds. + + -attacktime=x The high-volume attack phase of a note lasts x milliseconds, after which + the lower-volume sustain phase begins, unless the release time makes it + too short. (Only valid with -v.) + + -attacknotemax=x Notes larger than x milliseconds won't have the attack/sustain profile + applied. That allows sustained organ-like pedaling. + + -sustainlevel=p The volume level during the sustain phase is p percent of the starting + note volume. (Only valid with -v.) + + -scorename Use as the name of the score in the generated C code + instead of "score", and name the file .h instead of + .c. That allows multiple scores to be directly #included + into an Arduino .ino file without modification. + + Note that for backwards compatibility and easier batch-file processing, the equal sign + for specifying an option's numeric value may be omitted. Also, numeric values may be + specified in hex as 0xhhhh. + +*** THE SCORE BYTESTREAM + + The generated bytestream is a series of commands to turn notes on and off, + change instruments, and request a delay until the next event time. + 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, + where the two characters are a hex representation of one byte: + + 9t nn [vv] + Start playing note nn on tone generator t, replacing any previous note. + Generators are numbered starting with 0. The note numbers are the MIDI + numbers for the chromatic scale, with decimal 69 being Middle A (440 Hz). + If the -v option was given, the third byte specifies the 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, but start playing again from the beginning. This is + generated by the -r option. + + 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 volume 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, 2011 to 2019; see the change log. + *---------------------------------------------------------------------------------------- * The MIT License (MIT) -* Copyright (c) 2011,2013,2015,2016, Len Shustek +* Copyright (c) 2011,2013,2015,2016,2019 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 @@ -193,85 +265,117 @@ *********************************************************************************************/ // formatted with: Astyle -style=lisp -indent=spaces=3 -mode=c -/* -* 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 -* 30 September 2016, Scott Allen, V1.13 -* - Allow -c channel numbers to be specified as hex or octal -* - Add -r to end the file with "repeat" instead of "score end" -* 30 September 2016, Len Shustek, V1.14 -* - Prevent unnecessary "note off" commands from being generated by delaying -* them until we see if a "note on" is generated before the next wait. -* Thanks to Scott Allen for inspiring me to do this. In the best case we've -* seen, this makes the bytestream 21% smaller! -* 13 November 2017, Earle Philhower, V1.15 -* - Allow META fields to be larger than 127 bytes. -* 2 January 2018, Kodest, V1.16 -* - Don't generate zero-length delays -* 13 September 2018, Paul Stoffregen, V1.17 - - Fix compile errors on Linux with gcc run in default mode -* 1 January 2019, Len Shustek, V1.18 - - Fix the bug found by Chris van Marle (thanks!) that caused delays not to be - generated until the tempo was set. (The default is 500,000 usec/beat, not 0.) - - Abandon LCC and compile under Microsoft Visual Studio 2017. - - Reformat to condense the source code, so you see more protein and less - syntactic sugar on each screen. +/* 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 + 30 September 2016, Scott Allen, V1.13 + -Allow -c channel numbers to be specified as hex or octal + -Add -r to end the file with "repeat" instead of "score end" + 30 September 2016, Len Shustek, V1.14 + -Prevent unnecessary "note off" commands from being generated by delaying + them until we see if a "note on" is generated before the next wait. + Thanks to Scott Allen for inspiring me to do this. In the best case we've + seen, this makes the bytestream 21% smaller! + 13 November 2017, Earle Philhower, V1.15 + -Allow META fields to be larger than 127 bytes. + 2 January 2018, Kodest, V1 + -Don't generate zero-length delays + 13 September 2018, Paul Stoffregen, V1.17 + -Fix compile errors on Linux with gcc run in default mode + 1 January 2019, Len Shustek, V1.18 + -Fix the bug found by Chris van Marle (thanks!) that caused delays not to be + generated until the tempo was set. (The default is 500,000 usec/beat, not 0.) + -Abandon LCC and compile under Microsoft Visual Studio 2017. + -Reformat to condense the source code, so you see more protein and less + 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. + 4 January 2019, Len Shustek, V2.00 + -Major revision: completely rewrite score processing to allow out-of-order event + queuing. That lets us implement "release" time that ends notes early, and + "sustain" time at reduced volume, if we are encoding volume. You can sometimes + take advantage of release time to play more notes with the same number of tone + generators. It also can improve the sounds for repeated notes, although it + might be at the expense of increased bytestream size. + -Change the treatment of tracks and channels to more faithfully reproduce the + synthesizer model: each channel is an instrument, and can play multiple notes, + but only one at each frequency. It doesn't matter which tracks they come from. + -Simplify and generalize option parsing, and rename some of the newer ones. + -Add -scorename so multiple scores can be directly #included into .ino files + without manually editing the names. + +future version ideas + + -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 + channel-by-channel basis, by using a .cfg file which has + commands like these: + options + track 1 // melody + options -attacktime=100 -sustainlevel=50% -releasetime=10000 -notemin=200 + channel 8 // organ + options -attacktime=1000 -sustainlevel=80% -releasetime=100 -notemin=200 */ -#define VERSION "1.18" +#define VERSION "2.00" /*-------------------------------------------------------------------------------------------- @@ -279,6 +383,7 @@ A CONCISE SUMMARY OF MIDI FILE FORMAT L. Shustek, 16 July 2016. Gleaned from http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html +but also check out http://midi.teragonaudio.com/tech/miditech.htm Notation: 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. @@ -291,9 +396,16 @@ a MIDI file is: a header_chunk is: "MThd" 00000006 ffff nnnn dddd + 00000006 is the number of bytes in the rest of the header + ffff is the format type (we have only seen type 1) + nnnn is the number of tracks + dddd is the number of ticks per beat (ie, per quarter note) + (it is often 480 or 240) a track_chunk is: "MTrk" llllllll { track_event}... + llllllll is the length of the track data, in bytes + is the number of ticks to delay before the track_event 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 @@ -331,14 +443,90 @@ a meta event track_event is: FF 06 "xx"... name of marked point in the score FF 07 "xx"... description of cue point in the score FF 20 01 0c default channel for subsequent events without a channel is c + FF 21 01 pp MIDI port is pp FF 2F 00 end of track - FF 51 03 tttttt set tempo in microseconds per quarter-note + FF 51 03 tttttt set tempo in microseconds per quarter-note, for all tracks 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 data sequencer-specific data ---------------------------------------------------------------------------------------------*/ +Note that "set tempo" events are supposed to occur in only one track (generally the first), +which may or may not also contain MIDI note events. +See https://stackoverflow.com/questions/1080297/how-does-midi-tempo-message-apply-to-other-tracks + --------------------------------------------------------------------------------------------*/ + +/*--------------- processing outline ----------------------------------- +Lots of details are omitted. Note that MIDI track parsing is based +on counting "ticks", but our queueing is based on real-time seconds. +The number of ticks per second changes with the tempo. + +noteinfo + time (of start or end), track, channel, note, instrument, volume +track status + time in ticks, cmd, chan, note, volume +tgen status + playing? stopnote_pending? + noteinfo +channel status + instrument, {note_playing?, noteinfo}[slots] +queue entry: + {PLAY|STOP}, noteinfo + + process track data + forall trks: find next note + do // whole song + earliest_track time in ticks = min(trk->time) + accumuulate running absolute time (for queuing) based on the current tempo + if CMD_TEMPO, + set global tempo + find next note + else if CMD_PLAYNOTE + find a !note_playing[] channel slot to use + queue CMD_PLAYNOTE at time, noteinfo + find next note + else if CMD_STOPNOTE + find the note's slot among the channel's notes_playing + compute Sustain and Release times based on ADSR profile and note length + if(Sustain) queue CMD_PLAYNOTE at Sustain time, noteinfo with adjusted volume + queue CMD_STOPNOTE at Release time (or now), noteinfo + remove from channel's notes_playing + find next note + while not all CMD_TRACKDONE + +find next note + do forever + t->time += varlen + if "note off", CMD_STOPNOTE, return + if "note on", CMD_PLAYNOTE, return + if "tempo", CMD_TEMPO, return + if "program patch", change channel's instrument + else log a comment about the MIDI event + if end of track, CMD_TRACKDONE, return + +queue command + if queue has no room, output queue entries + insertion-sort the new item into the queue based on the time + +output queue entries at the oldest time + static output time, time deficit + if time has advanced + output DELAY + for all entries at the same oldest time + if STOP + find tgen matching channel and note + tgen: stopnote pending, not playing + else PLAY + find a tgen not playing (best: same, good: same instrument, ok: any free) + tgen "not stopnote pending", "playing" + if instrument change, output "set instrument" + output PLAY tgen + remove from queue + for all tgen + if stopnote pending + output STOP tgen + tgen: stopnote not pending +-----------------------------------------------------------------------------*/ #include #include @@ -346,7 +534,8 @@ a meta event track_event is: #include #include #include - +typedef unsigned char byte; +typedef uint32_t timestamp; // see note about this in the queuing routines /*********** MIDI file header formats *****************/ @@ -361,18 +550,20 @@ 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 */ -#define DEFAULT_TEMPO 500000L /* the MIDI-specified default tempo in usec/beat */ +#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 +#define NUM_CHANNELS 16 // MIDI-specified number of channels +#define MAX_CHANNELNOTES 16 // max number of notes playing simultaneously on a channel +#define DEFAULT_TEMPO 500000L // the MIDI-specified default tempo in usec/beat +#define DEFAULT_BEATTIME 240 // the MIDI-specified default ticks per beat bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput, define_progmem, - velocityoutput, instrumentoutput, percussion_ignore, percussion_translate, do_header, - gen_restart; + volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header, + gen_restart, scorename; FILE *infile, *outfile, *logfile; uint8_t *buffer, *hdrptr; unsigned long buflen; @@ -384,240 +575,213 @@ 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 +int notes_skipped = 0; +int events_delayed = 0; +int stopnotes_without_playnotes = 0; +int playnotes_without_stopnotes = 0; +int sustainphases_skipped = 0; +int sustainphases_done = 0; +int consecutive_delays = 0; +bool last_output_was_delay = false; +int noteinfo_overflow = 0, noteinfo_notfound = 0; +unsigned channel_mask = 0xffff; // bit mask of channels to process +int keyshift = 0; // optional chromatic note shift for output file +unsigned long delaymin_usec = 0; // events this close get merged together to save bytestream space +unsigned long releasetime_usec = 0; // release time in usec for silence at the end of notes +unsigned long notemin_usec = 250; // minimum note time in usec after the release is deducted +unsigned long attacktime_usec = 0; // the high volume attack phase lasts this time, if not 0 (only for -v) +unsigned long attacknotemax_usec = ULONG_MAX; // the longest note to which the attack/sustain profile is used (only for -v) +int sustainlevel_pct = 50; // the percent of attack volume for the sustain phase (only for -v) 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? */ - bool stopnote_pending; /* do we need to stop this generator before the next wait? */ - 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; /* the current volume */ - 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 */ +unsigned int ticks_per_beat = DEFAULT_BEATTIME; -/* output bytestream commands, which are also stored in track_status.cmd */ +unsigned long timenow_ticks = 0; // the current processing time in ticks +timestamp timenow_usec = 0; // the current processing time in usec +unsigned long timenow_usec_updated = 0; // when, in ticks, we last updated timenow_usec using the current tempo +timestamp output_usec = 0; // the time we last output, in usec +unsigned int output_deficit_usec = 0; // the leftover usec < 1000 still to be used for a "delay" +unsigned long tempo; // current global tempo in usec/beat + +int tempo_changes = 0; // how many times we changed the global tempo +long int delays_saved = 0; // how many delays were saved because of non-zero merge time + +/* 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 */ +/* the following other commands are 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 }; - +struct file_hdr_t { // what our optional file header looks like + char id1; // 'P' + char id2; // 't' + byte hdr_length; // length of whole file header + byte f1; // flag byte 1 #define HDR_F1_VOLUME_PRESENT 0x80 #define HDR_F1_INSTRUMENTS_PRESENT 0x40 #define HDR_F1_PERCUSSION_PRESENT 0x20 - + byte f2; // flag byte 2 + byte num_tgens; // how many tone generators are used by this score +} file_header = { + 'P', 't', sizeof (struct file_hdr_t), 0, 0, MAX_TONEGENS }; long int file_header_num_tgens_position; /************** command-line processing *******************/ -void SayUsage (char *programName) { +void check_option(bool condition, char *msg) { + if (!condition) { + fprintf(stderr, "*** %s\n", msg); + exit(8); } } + +bool opt_key(const char* arg, const char* keyword) { + do { // check for a keyword option and nothing after it + if (tolower(*arg++) != *keyword++) return false; } + while (*keyword); + return *arg == '\0'; } + +bool opt_int(const char* arg, const char* keyword, int *pval, int min, int max) { + do { // check for a "keyword=integer" option and nothing after it + if (tolower(*arg++) != *keyword++) + return false; } + while (*keyword); + if (*arg == '=') ++arg; // = is optional, actually + int num, nch; + if (sscanf(arg, *arg == '0' && *(arg + 1) == 'x' ? "%x%n" : "%d%n", &num, &nch) != 1) return false; + if (num < min || num > max || arg[nch] != '\0') return false; + *pval = num; + return true; } + +bool opt_str(const char* arg, const char* keyword, const char** str) { + do { // check for a "keyword=string" option + if (tolower(*arg++) != *keyword++) return false; } + while (*keyword); + *str = arg; // ptr to "string" part, which could be null + return true; } + +void SayUsage(char *programName) { static char *usage[] = { "Convert MIDI files to an Arduino PLAYTUNE bytestream", "", "Use: miditones ", " input file will be .mid", - " output file will be .bin or .c", + " output file will be .bin or .c or .h", " log file will be .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)", + " -v include volume 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 a binary file output instead of C source code", + " -t=n 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", - " -r terminate output file with \"restart\" instead of \"stop\" command", + " -c=n mask for which tracks to process, e.g. -c3 for only 0 and 1", + " -dp define PROGMEM in output C code", + " -k=n key shift in chromatic notes, positive or negative", + " -lp log input parsing", + " -lg log output generation", + " -n=n put about n items on each line of the C file output", + " -p parse only, don't generate bytestream", + " -pi ignore notes in the percussion track, 9", + " -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", + " -delaymin=x minimum delay is x msec, to save bytestream space", + " -attacktime=x the high volume attack phase lasts x msec", + " -attacknotemax=x notes over x msec don't use the attack/sustain profile", + " -sustainlevel=p the sustain level is p percent of maximum volume", + " -releasetime=x release each note x msec before it ends", + " -notemin=x don't let release shorten the note to less than x msec", + " -scorename use as the score name in a .h file", NULL }; - int i = 0; - while (usage[i] != NULL) - fprintf (stderr, "%s\n", usage[i++]); } - + for (int i=0; usage[i] != NULL; ++i) + 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++) { + /* 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, firstnonoption = 0; 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], "%i%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; - case 'R': - gen_restart = true; - if (argv[i][2] != '\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); } } + int tempint; + char *arg = argv[i] + 1; + if (opt_key(arg, "h") || opt_key(arg, "?")) { + SayUsage(argv[0]); exit(1); } + else if (opt_key(arg, "b")) binaryoutput = true; + else if (opt_int(arg, "c", &channel_mask, 1, 0xffff)) + printf("Channel (track) mask is %04X\n", channel_mask); + else if (opt_key(arg, "d")) do_header = true; + else if (opt_key(arg, "dp")) define_progmem = true; + else if (opt_key(arg, "lg")) loggen = true; + else if (opt_key(arg, "i")) instrumentoutput = true; + else if (opt_int(arg, "k", &keyshift, -100, 100)) + printf("Using keyshift %d\n", keyshift); + else if (opt_key(arg, "lp")) logparse = true; + else if (opt_int(arg, "n", &outfile_maxitems, 1, INT_MAX)); + else if (opt_key(arg, "p")) parseonly = true; + 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_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; + else if (opt_int(arg, "attacktime", &tempint, 0, INT_MAX)) { + attacktime_usec = tempint * 1000; + check_option(volume_output, "-attacktime only works with -v"); } + else if (opt_int(arg, "attacknotemax", &tempint, 0, INT_MAX)) { + attacknotemax_usec = tempint * 1000; + check_option(volume_output, "-attacknotemax only works with -v"); } + else if (opt_int(arg, "sustainlevel", &sustainlevel_pct, 1, 100)) + check_option(volume_output, "-sustainlevel only works with -v"); + else if (opt_key(arg, "s1")) strategy1 = true; + else if (opt_key(arg, "s2")) strategy2 = true; + else if (opt_key(arg, "scorename")) scorename = true; + else if (opt_int(arg, "t", &num_tonegens, 1, MAX_TONEGENS)) + printf("Using %d tone generators\n", num_tonegens); + else if (opt_key(arg, "v")) volume_output = true; + /* add more option switches here */ + else { + fprintf(stderr, "\n*** bad 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"); } +void print_command_line (FILE *file, int argc, char *argv[]) { + fprintf (file, "// command line: "); + for (int i = 0; i < argc; i++) fprintf (file, "%s ", argv[i]); + fprintf (file, "\n"); } /**************** utility routines **********************/ +void assert(bool condition, char *msg) { + if (!condition) { + fprintf(stderr, "*** internal assertion error: %s\n", msg); + if (logfile) fprintf(logfile, "*** internal assertion error: %s\n", msg); + exit(8); } } + +/* announce a fatal MIDI file format error */ +void midi_error(char *msg, byte *bufptr) { + fprintf(stderr, "---> MIDI file error at position %04X (%d): %s\n", + (uint16_t)(bufptr - buffer), (uint16_t)(bufptr - buffer), msg); + byte *ptr = bufptr - 16; // print some bytes surrounding the error + 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); } + /* portable string length */ int strlength (const char *str) { int i; @@ -629,33 +793,26 @@ 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) { + if (n != 0) { /* Copy as many bytes as will fit */ 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++); } + if ((*d++ = *s++) == '\0') break; } } + if (n == 0) { /* Not enough room in dst, add NUL and traverse rest of src */ + 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++; + while (n-- != 0 && *d != '\0') d++; dlen = d - dst; n = siz - dlen; - if (n == 0) - return (dlen + strlength (s)); + if (n == 0) return (dlen + strlength (s)); while (*s != '\0') { if (n != 1) { *d++ = *s; @@ -666,38 +823,19 @@ size_t miditones_strlcat (char *dst, const char *src, size_t siz) { } /* 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; + 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) { +void chk_bufdata (byte *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); } @@ -707,7 +845,6 @@ 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) { outfile_bytecount += n; outfile_itemcount += n; @@ -715,9 +852,319 @@ void outfile_items (int n) { fprintf (outfile, "\n"); outfile_itemcount = 0; } } +//******* structures for recording track, channel, and tone generator status + +// Note that the tempo can change as notes are played, maybe many times. +// In order to keep track of how long notes are to play, we have to incrementally +// accumulate the duration of all playing notes every time the tempo changes, and +// then one final time when the "stop note" event occurs. + +struct noteinfo { // everything we might care about as a note plays + timestamp time_usec; // when it starts or stops, in absolute usec since song start + int track, channel, note, instrument, volume; // all the nitty-gritty about it +}; + +struct tonegen_status { // current status of a tone generator + bool playing; // is it playing? + bool stopnote_pending; // are we due to issue a stop note command? + struct noteinfo note; // if so, the details of the note being played +} tonegen[MAX_TONEGENS] = { 0 }; + +struct track_status { // current status of a MIDI track + uint8_t *trkptr; // ptr to the next event we care about + uint8_t *trkend; // ptr just past the end of the track + unsigned long time; // what time we're at in the score, in ticks + unsigned long tempo; // the last tempo set on this track + int preferred_tonegen; // for strategy2: try to use this generator + byte cmd; // next CMD_xxxx event coming up + byte chan, note, volume; // if it is CMD_PLAYNOTE or CMD_STOPNOTE, the note info + byte last_event; // the last event, for MIDI's "running status" +} track[MAX_TRACKS] = { 0 }; + +struct channel_status { // current status of a channel + int instrument; // which instrument this channel currently plays + bool note_playing[MAX_CHANNELNOTES]; // slots for notes that are playing on this channel + struct noteinfo notes_playing[MAX_CHANNELNOTES]; // information about them +} channel[NUM_CHANNELS] = { 0 }; + +char *describe(struct noteinfo *np) { // create a description of a note + // WARNING: returns a pointer to a static string, so only call once per line, in a printf! + static char notedescription[100]; + sprintf(notedescription, "at %lu.%03lu msec, note %d (0x%02X) track %d channel %d volume %d instrument %d", + np->time_usec / 1000, np->time_usec % 1000, np->note, np->note, + np->track, np->channel, np->volume, np->instrument); + return notedescription; } + +/************** output reorder queue routines ****************** + +We queue commands to be issued at arbitrary times and sort them in time order. We flush +them out, allocating tone generators and creating the output bytestream, only as needed +to get more space in the queue. As we do that, we generate the "delay" commands required. + +All the queuing must be done with a microsecond time base, not ticks, because the tempo, +which controls the duration of ticks, may (and does!) change while notes are being played. + +We currently define "timestamps" by typedef as uint32_t, which is enough for songs as long +as 71 minutes. If longer songs are required, change it to uint64_t. + +We assume that "unsigned long" variables are big enough to track the total number of ticks +in a song. If they are 32 bits, the typical 480 ticks/beat and 500,000 usec/tick imply +960 ticks/second, for a song length of 51 days. Even if the song plays much faster, it's +still plenty long. +*/ + +#define QUEUE_SIZE 100 // maximum number of note play/stop commands we queue + +struct queue_entry { // the format of each queue entry + byte cmd; // CMD_PLAY or CMD_STOP + struct noteinfo note; // info about the note, including the action time +} queue[QUEUE_SIZE]; + +int queue_numitems = 0; +int queue_oldest_ndx = 0, queue_newest_ndx = 0; +int debugcount = 0; + +void show_queue(void) { // for debugging: dump the whole event queue + FILE *fid = logfile; + if (!logfile) fid = stdout; + fprintf(fid, "***at output time %lu.%03lu queue has %d items; oldest at %d, newest at %d\n", + output_usec / 1000, output_usec % 1000, queue_numitems, queue_oldest_ndx, queue_newest_ndx); + int ndx = queue_oldest_ndx; + if (queue_numitems > 0) while (1) { + struct queue_entry *q = &queue[ndx]; + fprintf(fid, "%2d: %s %s\n", ndx, q->cmd == CMD_PLAYNOTE ? "PLAY" : "STOP", describe(&q->note)); + if (ndx == queue_newest_ndx) break; + if (++ndx >= QUEUE_SIZE) ndx = 0; } } + +void show_tonegens(void) { // for debugging: dump tone generator status + FILE *fid = logfile; + if (!logfile) fid = stdout; + fprintf(fid, "*** tone generator status at output time %lu.%03lu\n", output_usec / 1000, output_usec % 1000); + for (int tgnum = 0; tgnum < num_tonegens; ++tgnum) { + struct tonegen_status *tg = &tonegen[tgnum]; + if (tg->playing) + fprintf(fid, "#%d: playing note %d (%02X), instrument %d, track %d channel %d%s\n", + tgnum, tg->note.note, tg->note.note, tg->note.instrument, tg->note.track, tg->note.channel, + tg->stopnote_pending ? ", stopnote pending\n" : ""); + else fprintf(fid, "#%d: idle\n", tgnum); } } + +// find an idle tone generator we can use +int find_idle_tgen(struct noteinfo *np) { // returns -1 if there isn't one + struct tonegen_status *tg; + int tgnum; + bool foundgen = false; + for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { // first, is this note already playing on this channel? + tg = &tonegen[tgnum]; + if (tg->playing + && tg->note.note == np->note && tg->note.channel == np->channel) { + // this must be the start of the sustain phase of a playing note + ++playnotes_without_stopnotes; + if (loggen) fprintf(logfile, " *** playnote without stopnote, tgen %d, %s\n", + tgnum, describe(np)); + foundgen = true; + break; } } + if (!foundgen && strategy2) { // try to use the same tone generator that this track used last time + struct track_status *trk = &track[np->track]; + tg = &tonegen[trk->preferred_tonegen]; + if (!tg->playing) { + tgnum = trk->preferred_tonegen; + foundgen = true; } } + if (!foundgen) // if not, then try for a free tone generator that had been playing the same instrument we need + for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { + tg = &tonegen[tgnum]; + if (!tg->playing && tg->note.instrument == np->instrument) { + foundgen = true; + break; } } + if (!foundgen) // if not, then try for any free tone generator + for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { + tg = &tonegen[tgnum]; + if (!tg->playing) { + foundgen = true; + break; } } + if (foundgen) return tgnum; + return -1; } + +void remove_queue_entry(int ndx) { // remove the oldest queue entry + struct queue_entry *q = &queue[ndx]; + + if (q->cmd == CMD_STOPNOTE) { + // find the tone generator playing this note, and record a pending stop + int tgnum; + for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { + struct tonegen_status *tg = &tonegen[tgnum]; + if (tg->playing + && tg->note.note == q->note.note + && tg->note.channel == q->note.channel) { // found the note + tg->stopnote_pending = true; // "stop note needed unless another start note follows" + tg->playing = false; // free the tg to be reallocated, but note the stop time in case + tg->note.time_usec = q->note.time_usec; // the tg doesn't get used and we generate it + if (loggen) fprintf(logfile, " pending stop tgen %d %s\n", tgnum, describe(&q->note)); + break; } } + if (tgnum >= num_tonegens) { + // If we exited the loop without finding the generator playing this note, presumably it never started + // because there weren't any free tone generators. Is there some assertion we can use to verify that? + ++stopnotes_without_playnotes; + if (loggen) fprintf(logfile, " *** stopnote without playnote, %s\n", describe(&q->note)); } } + + else { // CMD_PLAYNOTE + assert(q->cmd == CMD_PLAYNOTE, "bad cmd in remove_queue_entry"); + int tgnum = find_idle_tgen(&q->note); + struct tonegen_status *tg = &tonegen[tgnum]; + if (tgnum >= 0) { // we found a tone generator we can use + if (tgnum + 1 > num_tonegens_used) num_tonegens_used = tgnum + 1; + if (tg->note.instrument != q->note.instrument) { // it's a new instrument for this generator + tg->note.instrument = q->note.instrument; + ++instrument_changes; + if (loggen) fprintf(logfile, " tgen %d changed to instrument %d\n", tgnum, tg->note.instrument); + if (instrumentoutput) { // output a "change instrument" command + if (binaryoutput) { + putc(CMD_INSTRUMENT | tgnum, outfile); + putc(tg->note.instrument, outfile); + outfile_bytecount += 2; } + else { + 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; + tg->stopnote_pending = false; // don't bother to issue "stop note" + tg->note = q->note; // structure copy of note info + track[tg->note.track].preferred_tonegen = tgnum; + ++note_on_commands; + last_output_was_delay = false; + if (binaryoutput) { + putc(CMD_PLAYNOTE | tgnum, outfile); + putc(tg->note.note, outfile); + outfile_bytecount += 2; + if (volume_output) { + putc(tg->note.volume, outfile); + outfile_bytecount +=1; } } + else { + if (volume_output == 0) { + 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); + outfile_items(3); } } } + else { + if (loggen) fprintf(logfile, " *** at %lu.%03lu msec no free generator; skipping %s\n", + output_usec / 1000, output_usec % 1000, describe(&q->note)); + ++notes_skipped; } } } + +void generate_delay(unsigned long delta_msec) { // output a delay command + if (delta_msec > 0) { + assert(delta_msec <= 0x7fff, "time delta too big"); + if (last_output_was_delay) { + ++consecutive_delays; + if (loggen) fprintf(logfile, " *** this is a consecutive delay, of %d msec\n", delta_msec); } + last_output_was_delay = true; + if (binaryoutput) { // output a 15-bit delay in big-endian format + putc((byte)(delta_msec >> 8), outfile); + putc((byte)(delta_msec & 0xff), outfile); + outfile_bytecount += 2; } + else { + 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 +void pull_queue(void) { + if (loggen) fprintf(logfile, " <-pull from queue at %lu.%03lu msec\n", output_usec / 1000, output_usec % 1000); + timestamp oldtime = queue[queue_oldest_ndx].note.time_usec; // the oldest time + assert(oldtime >= output_usec, "oldest queue entry goes backward in pull_queue"); + unsigned long delta_usec = (oldtime - output_usec) + output_deficit_usec; + unsigned long delta_msec = delta_usec / 1000; + output_deficit_usec = delta_usec % 1000; + if (delta_usec > (unsigned long)delaymin_usec) { // if time has advanced beyond the merge threshold, output a delay + if (delta_msec > 0) { + generate_delay(delta_msec); + if (loggen) fprintf(logfile, " at %lu.%03lu msec, delay for %ld msec to %lu.%03lu msec; deficit is %lu usec\n", + output_usec / 1000, output_usec % 1000, delta_msec, + oldtime / 1000, oldtime % 1000, output_deficit_usec); } + else if (loggen) fprintf(logfile, " at %lu.%03lu msec, a delay of only %lu usec was skipped, and the deficit is now %lu usec\n", + output_usec / 1000, output_usec % 1000, oldtime-output_usec, output_deficit_usec); + output_usec = oldtime; } + else if (delta_msec > 0) ++delays_saved; + + do { // output and remove all entries at the same (oldest) time in the queue + // or which are only delaymin newer + remove_queue_entry(queue_oldest_ndx); + if (++queue_oldest_ndx >= QUEUE_SIZE) queue_oldest_ndx = 0; + --queue_numitems; } + while (queue_numitems > 0 && queue[queue_oldest_ndx].note.time_usec <= oldtime + (timestamp)delaymin_usec); + + // do any "stop notes" still needed to be generated? + for (int tgnum = 0; tgnum < num_tonegens; ++tgnum) { + struct tonegen_status *tg = &tonegen[tgnum]; + if (tg->stopnote_pending) { // got one + last_output_was_delay = false; + if (binaryoutput) { + putc(CMD_STOPNOTE | tgnum, outfile); + outfile_bytecount += 1; } + else { + fprintf(outfile, "0x%02X, ", CMD_STOPNOTE | tgnum); + outfile_items(1); } + if (loggen) fprintf(logfile, " stop tgen %d %s\n", tgnum, describe(&tg->note)); + tg->stopnote_pending = false; + tg->playing = false; } } } + +void flush_queue(void) { // empty the queue + while (queue_numitems > 0) pull_queue(); } + +// queue a "note on" or "note off" command +void queue_cmd(byte cmd, struct noteinfo *np) { + if (loggen) fprintf(logfile, " queue %s %s\n", + cmd == CMD_PLAYNOTE ? "PLAY" : cmd == CMD_STOPNOTE ? "STOP" : "????", + describe(np)); + if (queue_numitems == QUEUE_SIZE) pull_queue(); + assert(queue_numitems < QUEUE_SIZE, "no room in queue"); + timestamp horizon = output_usec + output_deficit_usec; + if (np->time_usec < horizon) { // don't allow revisionist history + if (loggen) fprintf(logfile, " event delayed by %lu usec because queue is too small\n", + horizon - np->time_usec); + np->time_usec = horizon; + ++events_delayed; } + int ndx; + if (queue_numitems == 0) { // queue is empty; restart it + ndx = queue_oldest_ndx = queue_newest_ndx = queue_numitems = 0; } + else { // find a place to insert the new entry in time order + // this is a stable incremental insertion sort + ndx = queue_newest_ndx; // start with newest, since we are most often newer + while (queue[ndx].note.time_usec > np->time_usec) { // search backwards for something as new or older + if (ndx == queue_oldest_ndx) { // none: we are oldest; add to the start + if (--queue_oldest_ndx < 0) queue_oldest_ndx = QUEUE_SIZE - 1; + ndx = queue_oldest_ndx; + goto insert; } + if (--ndx < 0) ndx = QUEUE_SIZE - 1; } + // we are to insert the new item after "ndx", so shift all later entries down, if any + int from_ndx, to_ndx; + if (++queue_newest_ndx >= QUEUE_SIZE) queue_newest_ndx = 0; + to_ndx = queue_newest_ndx; + while (1) { + if ((from_ndx = to_ndx - 1) < 0) from_ndx = QUEUE_SIZE - 1; + if (from_ndx == ndx) break; + queue[to_ndx] = queue[from_ndx]; // structure copy + to_ndx = from_ndx; } + if (++ndx >= QUEUE_SIZE) ndx = 0; } +insert: // store the item at ndx + ++queue_numitems; + queue[ndx].cmd = cmd; // fille in the queue entry + queue[ndx].note = *np; // structure copy of the note +} + +void show_queue_cmd(timestamp time_usec, byte cmd, int note) { + printf("debug queue %s note %02X at %6ld\n", cmd == CMD_PLAYNOTE ? "PLAY" : "STOP", note, time_usec); + struct noteinfo notedata; + notedata.time_usec = time_usec; + notedata.track = 0; + notedata.channel = 0; + notedata.note = note; + notedata.instrument = 1; + notedata.volume = 100; + queue_cmd(cmd, ¬edata); + show_queue(); } + /************** process the MIDI file header *****************/ -void process_header (void) { +void process_file_header (void) { struct midi_header *hdr; unsigned int time_division; @@ -740,10 +1187,7 @@ void process_header (void) { 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) { +void process_track_header (int tracknum) { struct track_header *hdr; unsigned long tracklen; @@ -752,8 +1196,7 @@ void start_track (int tracknum) { 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); + 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; @@ -761,233 +1204,326 @@ void start_track (int tracknum) { 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) { +unsigned long get_varlen (uint8_t ** ptr) { // get a MIDI-style integer /* 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)) + unsigned long val = 0; + for (int i = 0; i < 4; ++i) { + byte b = *(*ptr)++; + val = (val << 7) | (b & 0x7f); + if (!(b & 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. */ +// Skip in the track for the next "note on", "note off" or "set tempo" command and return. -void find_note (int tracknum) { - unsigned long int delta_time; +void find_next_note (int tracknum) { + unsigned long int delta_ticks; 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 */ + struct track_status *t = &track[tracknum]; // our track status structure while (t->trkptr < t->trkend) { - - delta_time = get_varlen (&t->trkptr); + delta_ticks = get_varlen (&t->trkptr); + t->time += delta_ticks; if (logparse) { - fprintf (logfile, "trk %d ", tracknum); - if (delta_time) { - fprintf (logfile, "delta time %4ld, ", delta_time); } + fprintf(logfile, "# trk %d ", tracknum); + if (loggen) fprintf(logfile, "at ticks+%lu=%lu: ", delta_ticks, t->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 */ + if (delta_ticks > 0) fprintf(logfile, "ticks+%-5lu%7lu ", delta_ticks, t->time); + else fprintf(logfile, " "); } } + + if (*t->trkptr < 0x80) event = t->last_event; // using "running status": same event as before + else event = *t->trkptr++; // otherwise get new "status" (event type) */ + + if (event == 0xff) { // meta-event meta_cmd = *t->trkptr++; meta_length = get_varlen (&t->trkptr); switch (meta_cmd) { case 0x00: - if (logparse) - fprintf (logfile, "sequence number %d\n", rev_short (*(unsigned short *) t->trkptr)); + if (logparse) fprintf (logfile, "sequence number %d\n", rev_short (*(unsigned short *) t->trkptr)); break; case 0x01: - tag = "description"; - goto show_text; + tag = "description"; goto show_text; case 0x02: - tag = "copyright"; - goto show_text; + 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) { + for (int 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; + tag = "instrument name"; goto show_text; case 0x05: - tag = "lyric"; - goto show_text; + tag = "lyric"; goto show_text; case 0x06: - tag = "marked point"; - goto show_text; + tag = "marked point"; goto show_text; case 0x07: - tag = "cue point"; + tag = "cue point"; goto show_text; + case 0x08: + tag = "program name"; goto show_text; + case 0x09: + tag = "device (port) name"; show_text: if (logparse) { fprintf (logfile, "meta cmd %02X, length %d, %s: \"", meta_cmd, meta_length, tag); - for (i = 0; i < meta_length; ++i) { + for (int 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); + if (logparse) fprintf (logfile, "channel prefix %d\n", *t->trkptr); + break; + case 0x21: + if (logparse) fprintf(logfile, "MIDI port %d\n", *t->trkptr); break; case 0x2f: - if (logparse) - fprintf (logfile, "end of track\n"); + if (logparse) fprintf (logfile, "end of track\n"); break; - case 0x51: /* tempo: 3 byte big-endian integer! */ + case 0x51: // tempo: 3 byte big-endian integer, not a varlen 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->tempo = rev_long (*(uint32_t *) (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)); + if (logparse) fprintf (logfile, "SMPTE offset %08" PRIx32 "\n", + rev_long (*(uint32_t *) t->trkptr)); break; case 0x58: - if (logparse) - fprintf (logfile, "time signature %08" PRIx32 "\n", - rev_long (*(unsigned long *) t->trkptr)); + if (logparse) fprintf (logfile, "time signature %08" PRIx32 "\n", + rev_long (*(uint32_t *) t->trkptr)); break; case 0x59: - if (logparse) - fprintf (logfile, "key signature %04X\n", rev_short (*(unsigned short *) t->trkptr)); + if (logparse) fprintf (logfile, "key signature %04X\n", rev_short (*(unsigned short *) t->trkptr)); break; case 0x7f: - tag = "sequencer data"; - goto show_hex; + 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) + for (int 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 < 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; + t->chan = chan = event & 0xf; switch (event >> 4) { - case 0x8: + case 0x8: // note off 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->volume = *t->trkptr++; +note_off: if (logparse) fprintf(logfile, "note %d (0x%02X) off, channel %d, volume %d\n", t->note, t->note, chan, t->volume); + if ((1 << chan) & channel_mask // we're processing this channel + && (!percussion_ignore || chan != PERCUSSION_TRACK)) { // and not ignoring percussion + if (!instrumentoutput) t->chan = 0; // if no insruments, force all notes to channel 0 + t->cmd = CMD_STOPNOTE; /* stop processing and return */ + return; } + break; + case 0x9: // note on t->note = *t->trkptr++; - velocity = *t->trkptr++; - if (velocity == 0) /* some scores use note-on with zero velocity for off! */ + t->volume = *t->trkptr++; + if (t->volume == 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: + if (logparse) fprintf(logfile, "note %d (0x%02X) on, channel %d, volume %d\n", t->note, t->note, chan, t->volume); + if ((1 << chan) & channel_mask // we're processing this channel + && (!percussion_ignore || chan != PERCUSSION_TRACK)) { // and not ignoring percussion + if (!instrumentoutput) t->chan = 0; // if no insruments, force all notes to channel 0 + t->cmd = CMD_PLAYNOTE; /* stop processing and return */ + return; } + break; + case 0xa: // key pressure note = *t->trkptr++; velocity = *t->trkptr++; - if (logparse) - fprintf (logfile, "after-touch %d, %d\n", note, velocity); + if (logparse) fprintf (logfile, "channel %d: note %d (0x%02X) has key pressure %d\n", chan, note, note, velocity); break; - case 0xb: + case 0xb: // control value change controller = *t->trkptr++; velocity = *t->trkptr++; - if (logparse) - fprintf (logfile, "control change %d, %d\n", controller, velocity); + if (logparse) fprintf (logfile, "channel %d: change control value of controller %d to %d\n", chan, controller, velocity); break; - case 0xc: + case 0xc: // program patch, ie which instrument instrument = *t->trkptr++; - midi_chan_instrument[chan] = instrument; // record new instrument for this channel - if (logparse) - fprintf (logfile, "program patch %d\n", instrument); + channel[chan].instrument = instrument; // record new instrument for this channel + if (logparse) fprintf (logfile, "channel %d: program patch to instrument %d\n", chan, instrument); break; - case 0xd: + case 0xd: // channel pressure pressure = *t->trkptr++; - if (logparse) - fprintf (logfile, "channel after-touch %d\n", pressure); + if (logparse) fprintf (logfile, "channel %d: after-touch pressure is %d\n", chan, pressure); break; - case 0xe: + case 0xe: // pitch wheel change pitchbend = *t->trkptr++ | (*t->trkptr++ << 7); - if (logparse) - fprintf (logfile, "pitch wheel change %d\n", pitchbend); + if (logparse) fprintf (logfile, "pitch wheel change to %d\n", pitchbend); break; - case 0xf: + case 0xf: // sysex event sysex_length = get_varlen (&t->trkptr); - if (logparse) - fprintf (logfile, "SysEx event %d, %ld bytes\n", event, sysex_length); + if (logparse) fprintf (logfile, "SysEx event %d with %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 */ + t->cmd = CMD_TRACKDONE; //no more events to process on this track ++tracks_done; } +void show_noteinfo_slots(int channum) { + struct channel_status *cp = &channel[channum]; + if (loggen) { + fprintf(logfile, "notes playing for channel %d:\n", channum); + for (int ndx = 0; ndx < MAX_CHANNELNOTES; ++ndx) + if (cp->note_playing[ndx]) { + struct noteinfo *np = &cp->notes_playing[ndx]; + fprintf(logfile, " %2d: %s\n", ndx, describe(np)); } } } + +void process_track_data(void) { + unsigned long last_earliest_time = 0; + + do { // while there are still track notes to process + + /* Find the track with the earliest event time, and process it's event. + + 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. + + 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. */ + + struct track_status *trk; + int count_tracks = num_tracks; + unsigned long earliest_time = 0x7fffffff; // in ticks, of course + int tracknum = 0; + int earliest_tracknum; + 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]; + assert(earliest_time >= timenow_ticks, "time went backwards in process_track_data"); + timenow_ticks = earliest_time; // we make it the global time + timenow_usec += (uint64_t)(timenow_ticks - timenow_usec_updated) * tempo / ticks_per_beat; + timenow_usec_updated = timenow_ticks; // usec version is updated based on the current tempo + if (loggen) { + if (earliest_time != last_earliest_time) { + fprintf(logfile, "->process trk %d at time %lu.%03lu msec (%lu ticks)\n", + tracknum, timenow_usec / 1000, timenow_usec % 1000, timenow_ticks); + last_earliest_time = earliest_time; } } + struct channel_status *cp = &channel[trk->chan]; // the channel info, if play or stop + + if (trk->cmd == CMD_TEMPO) { // change the global tempo, which affects future usec computations + if (tempo != trk->tempo) { + ++tempo_changes; + tempo = trk->tempo; } + if (loggen) fprintf(logfile, " tempo set to %ld usec/qnote\n", tempo); + find_next_note(tracknum); } + + else { // should be PLAYNOTE or STOPNOTE + if (percussion_translate && trk->chan == PERCUSSION_TRACK) + trk->note += 128; // maybe move percussion notes up to 128..255 + else { // shift notes as requested + trk->note += keyshift; + if (trk->note < 0) trk->note = 0; + if (trk->note > 127) trk->note = 127; } + + if (trk->cmd == CMD_STOPNOTE) { + int ndx; // find the noteinfo for this note -- which better be playing -- in the channel status + for (ndx = 0; ndx < MAX_CHANNELNOTES; ++ndx) { + if (cp->note_playing[ndx] + && cp->notes_playing[ndx].note == trk->note + && cp->notes_playing[ndx].track == tracknum) + break; } + if (ndx >= MAX_CHANNELNOTES) { + ++noteinfo_notfound; // presumably the array overflowed on input + if (loggen) fprintf(logfile, " *** noteinfo slot not found to stop track %d note %d (%02X) channel %d\n", + tracknum, trk->note, trk->note, trk->chan); } + else { + // Analyze the sustain and release parameters. We might generate another "note on" + // command with reduced volume, and/or move the stopnote command earlier than now. + struct noteinfo *np = &cp->notes_playing[ndx]; + unsigned long duration_usec = timenow_usec - np->time_usec; // it has the start time in it + unsigned long truncation; + if (duration_usec <= notemin_usec) truncation = 0; + else if (duration_usec < releasetime_usec + notemin_usec) truncation = duration_usec - notemin_usec; + else truncation = releasetime_usec; + if (attacktime_usec > 0 && duration_usec < attacknotemax_usec) { + if (duration_usec - truncation > attacktime_usec) { // do a sustain phase + if ((np->volume = np->volume * sustainlevel_pct / 100) <= 0) np->volume = 1; + np->time_usec += attacktime_usec; // adjust time to be when sustain phase starts + queue_cmd(CMD_PLAYNOTE, np); + ++sustainphases_done; } + else ++sustainphases_skipped; } + np->time_usec = timenow_usec - truncation; // adjust time to be when the note stops + queue_cmd(CMD_STOPNOTE, np); + cp->note_playing[ndx] = false; } + find_next_note(tracknum); } + + else if (trk->cmd == CMD_PLAYNOTE) { // Process only one "start note", so other tracks get a chance at tone generators + int ndx; // find an unused noteinfo slot to use + for (ndx = 0; ndx < MAX_CHANNELNOTES; ++ndx) { + if (!cp->note_playing[ndx]) break; } + if (ndx >= MAX_CHANNELNOTES) { + ++noteinfo_overflow; // too many simultaneous notes + if (loggen) fprintf(logfile, " *** no noteinfo slot to queue track %d note %d (%02X) channel %d\n", + tracknum, trk->note, trk->note, trk->chan); + show_noteinfo_slots(tracknum); } + else { + cp->note_playing[ndx] = true; // assign it to us + struct noteinfo *pn = &cp->notes_playing[ndx]; + pn->time_usec = timenow_usec; // fill it in + pn->track = tracknum; + pn->channel = trk->chan; + pn->note = trk->note; + pn->instrument = cp->instrument; + pn->volume = trk->volume; + queue_cmd(CMD_PLAYNOTE, pn); } + find_next_note(tracknum); } // use up the note + + else assert(false, "bad cmd in process_track_data"); } } + + while (tracks_done < num_tracks); + + // empty the output queue and generate the end-of-score command + flush_queue(); + if (loggen) { + fprintf(logfile, "ending timenow_usec: %lu.%03lu\n", timenow_usec / 1000, timenow_usec % 1000); + fprintf(logfile, "ending output_usec: %lu.%03lu\n", output_usec / 1000, output_usec % 1000); } + assert(timenow_usec >= output_usec, "time deficit at end of song"); + generate_delay((timenow_usec - output_usec) / 1000); + if (binaryoutput) { + 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, "\n"); } } -/* generate "stop note" commands for any channels that have them pending */ - -void gen_stopnotes(void) { - struct tonegen_status *tg; - int tgnum; - for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { - tg = &tonegen[tgnum]; - if (tg->stopnote_pending) { - if (binaryoutput) { - putc (CMD_STOPNOTE | tgnum, outfile); - outfile_bytecount += 1; } - else { - fprintf (outfile, "0x%02X, ", CMD_STOPNOTE | tgnum); - outfile_items (1); } - tg->stopnote_pending = false; } } } /********************* main ****************************/ @@ -996,38 +1532,35 @@ int main (int argc, char *argv[]) { 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 */ + printf ("MIDITONES V%s, (C) 2011-2019 Len Shustek\n", VERSION); + if (argc == 1) { // no arguments SayUsage (argv[0]); return 1; } - /* process options */ - - argno = HandleOptions (argc, argv); + argno = HandleOptions (argc, argv); // process options 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) { + if (logparse || loggen) { // open the log file 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 */ - + fprintf (logfile, "MIDITONES V%s log file\n", VERSION); + print_command_line(logfile, argc, argv); } + if (loggen) { + fprintf(logfile, "\nThere are %d independent time-ordered streams in this log file:\n", logparse ? 3 : 2); + if (logparse) fprintf(logfile, " - the parsed MIDI events, marked with #\n"); + fprintf(logfile, " - the MIDI play/stop events being queued, announced with ->\n" + " - the generated bytestream commands pulled from the queue, announced with <-\n\n"); } + + // open the input file miditones_strlcpy (filename, filebasename, MAXPATH); miditones_strlcat (filename, ".mid", MAXPATH); infile = fopen (filename, "rb"); @@ -1035,34 +1568,30 @@ int main (int argc, char *argv[]) { 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 */ + // Read the whole input file into memory + fseek (infile, 0, SEEK_END); // find its size buflen = ftell (infile); fseek (infile, 0, SEEK_SET); - buffer = (unsigned char *) malloc (buflen + 1); + buffer = (byte *) 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 (logparse) fprintf (logfile, "Processing %s, %ld bytes\n", filename, buflen); - if (!parseonly) { + if (!parseonly) { // create the output file miditones_strlcpy (filename, filebasename, MAXPATH); if (binaryoutput) { miditones_strlcat (filename, ".bin", MAXPATH); outfile = fopen (filename, "wb"); } else { - miditones_strlcat (filename, ".c", MAXPATH); + miditones_strlcat (filename, scorename ? ".h" : ".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) + file_header.f1 = (volume_output ? HDR_F1_VOLUME_PRESENT : 0) | (instrumentoutput ? HDR_F1_INSTRUMENTS_PRESENT : 0) | (percussion_translate ? HDR_F1_PERCUSSION_PRESENT : 0); file_header.num_tgens = num_tonegens; @@ -1072,7 +1601,7 @@ int main (int argc, char *argv[]) { 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); + print_command_line (outfile, argc, argv); if (channel_mask != 0xffff) fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask); if (keyshift != 0) @@ -1083,7 +1612,8 @@ int main (int argc, char *argv[]) { fprintf (outfile, "#else\n"); fprintf (outfile, "#define PROGMEM\n"); fprintf (outfile, "#endif\n"); } - fprintf (outfile, "const unsigned char PROGMEM score [] = {\n"); + 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); fflush (outfile); @@ -1093,252 +1623,87 @@ int main (int argc, char *argv[]) { else if (do_header) { // write the binary file header int i; for (i = 0; i < sizeof (file_header); ++i) - fputc (((unsigned char *) &file_header)[i], outfile); + fputc (((byte *) &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 (); + // process the MIDI file header + hdrptr = buffer; // point to the file and track headers + process_file_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 */ + if (num_tracks > MAX_TRACKS) midi_error ("Too many tracks", buffer); + // initialize for processing of all the tracks tempo = DEFAULT_TEMPO; - for (tracknum = 0; tracknum < num_tracks; ++tracknum) { + for (int tracknum = 0; tracknum < num_tracks; ++tracknum) { track[tracknum].tempo = DEFAULT_TEMPO; - start_track (tracknum); /* process the track header */ - find_note (tracknum); /* position to the first note on/off */ + process_track_header (tracknum); + find_next_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); } + find_next_note (tracknum); } +#if 0 + // TEMP test queuing routines + show_queue_cmd(12, CMD_PLAYNOTE, 100); + show_queue_cmd(12, CMD_PLAYNOTE, 101); + show_queue_cmd(12, CMD_PLAYNOTE, 103); + show_queue_cmd(20, CMD_STOPNOTE, 101); + show_queue_cmd(15, CMD_STOPNOTE, 103); + show_queue_cmd(21, CMD_PLAYNOTE, 104); + show_queue_cmd(20, CMD_STOPNOTE, 100); + show_queue_cmd(20, CMD_STOPNOTE, 104); + show_queue_cmd(22, CMD_PLAYNOTE, 109); + flush_queue(); +#endif + if (!parseonly) { - /* 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! */ + process_track_data(); // do all the tracks interleaved, like a 1950's multiway merge - 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 */ - delta_msec = ((unsigned long long) delta_time * tempo) / ticks_per_beat / 1000; - if (delta_msec) { // if time delay didn't round down to zero msec - gen_stopnotes(); /* first check if any tone generators have "stop note" commands pending */ - 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); - tg->stopnote_pending = true; /* must stop the current note if another doesn't start first */ - 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; - tg->stopnote_pending = false; - 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 - gen_stopnotes(); /* flush out any pending "stop note" commands */ - outfile_bytecount++; - if (binaryoutput) - putc (gen_restart ? CMD_RESTART : CMD_STOP, outfile); - else { - fprintf (outfile, - "0x%02x};\n// This score contains %ld bytes, and %d tone generator%s used.\n", - gen_restart ? CMD_RESTART : CMD_STOP, outfile_bytecount, num_tonegens_used, - num_tonegens_used == 1 ? " is" : "s are"); + // generate the ending commentary + if (!binaryoutput) { + fprintf(outfile, "\n// This score contains %ld bytes, and %d tone generator%s used.\n", + 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); + 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"); + printf(" %d notes were skipped because there weren't enough tone generators.\n", + notes_skipped); + if (consecutive_delays) + printf(" %d consecutive delays could be eliminated\n", consecutive_delays); + if (events_delayed) + printf(" %d \"stop note\" commands were delayed because the %d-element output queue is too small\n", + events_delayed, QUEUE_SIZE); + if (noteinfo_overflow + noteinfo_notfound > 0) + printf(" %d notes couldn't be recorded in the track status, so then %d notes couldn't be found\n" + " (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); + 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)); + if (loggen) { + fprintf(logfile, "%d note-on commands, %d instrument changes.\n", + note_on_commands, instrument_changes); + fprintf(logfile, "%d stop-notes without start-notes, %d start-notes without stop-notes\n", + stopnotes_without_playnotes, playnotes_without_stopnotes); + if (attacktime_usec > 0) fprintf(logfile, "%d sustain phases done, and %d skipped because the notes were too short\n", + sustainphases_done, sustainphases_skipped); } + if (0 && 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); + putc(num_tonegens_used, outfile); else - fprintf (outfile, "%2d", num_tonegens_used); } } - fclose (outfile); } /* if (!parseonly) */ + fprintf(outfile, "%2d", num_tonegens_used); } } + fclose(outfile); } if (loggen || logparse) fclose (logfile); diff --git a/miditones.exe b/miditones.exe index 0255333..6ae421a 100644 Binary files a/miditones.exe and b/miditones.exe differ