/********************************************************************************************* 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/Playtune_Teensy The fifth version is for the Teensy 3.1/3.2, and uses the four Periodic Interval Timers in the Cortex M4 processor to support up to 4 simultaneous notes. It uses less CPU time than the polling version, but is limited to 4 notes at a time. (This was written to experiment with multi-channel multi-Tesla Coil music playing, where I use Flexible Timer Module FTM0 for generating precise one-shot pulses. But I ultimately switched to the polling version to play more simultaneous notes.) 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 this code, and of the Playtune players, on Githib. *** THE PROGRAM MIDITONES is written in standard ANSI C and is meant to be executed from the 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. If the user specifies the full .mid filename, the .mid or .MID extension will be dropped and the remaining name will be used as . 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 in some documents) -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. -showskipped Display information to the console about each note that had to be skipped because there weren't enough tone generators. -noduplicates Remove identical notes played on identical instruments for the same time that come from different tracks. This can reduce the number of tone generators needed, and make the file smaller. -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 note 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. (Only valid with -v.) -sustainlevel=p The volume level during the sustain phase is p percent of the starting note volume. The default is 50. (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 that 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 included in the length are currently undefined and should be ignored by players. Len Shustek, 2011 to 2021; see the change log. *---------------------------------------------------------------------------------------- * The MIT License (MIT) * Copyright (c) 2011,2013,2015,2016,2019,2021 Len Shustek * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *********************************************************************************************/ // formatted with: 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 -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.0 -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. 17 October 2020, Ben Combee, V2.1 -Let user supply full filename to MIDI file on command line to be friendlier to users using shell autocompletion. If a .mid or .MID file is provided, the extension will be dropped to generate the base filename. 22 April 2021, Len Shustek, V2.2 -Add -showskipped to log the places where notes had to be discarded because there aren't enough tone generators. 25 April 2021, Len Shustek, V2.3 -Report how many notes were generated 5 May 2021, Len Shustek, V2.4 -Fix bug: when finding an idle tone generator, makes sure that the track and instrument of the currently playing note matches before deciding it's a sustain, otherwise multiple identical notes on identical instruments will disappear. (Thanks to Jonathan Oakley for providing an example of the problem.) -But sometimes, to reduce the number of tone generators, it's helpful to eliminate identical notes. So add a -noduplicates option to do just that. 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 "2.4" /*-------------------------------------------------------------------------------------------- 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. lower case letters are hex digits. If preceeded by 0, only low 7 bits are used. "xx" are ascii text characters {xxx}... means indefinite repeat of xxx a MIDI file is: header_chunk {track_chunk}... 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 a MIDI-event track_event is: 8n 0kk 0vv note off, channel n, note kk, velocity vv 9n 0kk 0vv note on, channel n, note kk, velocity vv An 0kk 0vv key pressure, channel n, note kk, pressure vv Bn 0cc 0vv control value change, channel n, controller cc, new value vv Cn 0pp program patch (instrument) change, channel n, new program pp Dn 0vv channel pressure, channel n, pressure vv En 0ll 0mm pitch wheel change, value llmm Note that channel 9 (called 10 by some programs) is used for percussion, particularly notes 35 to 81. a Sysex event track_event is: F0 0ii {0dd}... F7 system-dependent data for manufacture ii. See www.gweep.net/~prefect/eng/reference/protocol/midispec.html F2 0ll 0mm song position pointer F3 0ss song select F6 tune request F7 end of system-dependent data F8 timing clock sync FA start playing FB continue playing FC stop playing FE active sensing (hearbeat) a meta event track_event is: FF 00 02 ssss specify sequence number FF 01 "xx"... arbitrary description text FF 02 "xx"... copyright notice FF 03 "xx"... sequence or track name FF 04 "xx"... instrument name FF 05 "xx"... lyric to be sung FF 06 "xx"... name of marked point in the score FF 07 "xx"... description of cue point in the score FF 08 "xx"... program name FF 09 "xx"... device (port) name 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 beat (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. That isn't always true, however, 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 #include #include #include #include #include typedef unsigned char byte; typedef uint32_t timestamp; // see note about this in the queuing routines /*********** MIDI file header formats *****************/ struct midi_header { int8_t MThd[4]; uint32_t header_size; uint16_t format_type; uint16_t number_of_tracks; uint16_t time_division; }; struct track_header { int8_t MTrk[4]; uint32_t track_size; }; /*********** Global variables ******************/ #define MAX_TONEGENS 16 // max tone generators: tones we can play simultaneously #define DEFAULT_TONEGENS 6 // default number of tone generators #define MAX_TRACKS 24 // max number of MIDI tracks we will process #define PERCUSSION_TRACK 9 // the track MIDI uses for percussion sounds #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, volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header, gen_restart, scorename, showskipped, noduplicates; FILE *infile, *outfile, *logfile; uint8_t *buffer, *hdrptr; unsigned long buflen; int num_tracks; int tracks_done = 0; int outfile_maxitems = 26; int outfile_itemcount = 0; int num_tonegens = DEFAULT_TONEGENS; int num_tonegens_used = 0; int instrument_changes = 0; int note_on_commands = 0; 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 = DEFAULT_BEATTIME; 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 */ /* 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 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 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 or .h", " log file will be .log", "", "Commonly-used options:", " -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:", " -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", " -showskipped display information about each note that had to be skipped", " -noduplicates remove identical notes playing on different channels", " -delaymin=x minimum delay is x msec, to save bytestream space", " -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 }; 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, firstnonoption = 0; for (i = 1; i < argc; i++) { if (argv[i][0] == '/' || argv[i][0] == '-') { 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_key(arg, "showskipped")) showskipped = true; else if (opt_key(arg, "noduplicates")) noduplicates = 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 (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; for (i = 0; str[i] != '\0'; ++i); return i; } /* safe string copy */ size_t miditones_strlcpy (char *dst, const char *src, size_t siz) { char *d = dst; const char *s = src; size_t n = siz; if (n != 0) { /* Copy as many bytes as will fit */ while (--n != 0) { 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++; dlen = d - dst; n = siz - dlen; if (n == 0) return (dlen + strlength (s)); while (*s != '\0') { if (n != 1) { *d++ = *s; n--; } s++; } *d = '\0'; return (dlen + (s - src)); /* count does not include NUL */ } /* match a constant character sequence */ int charcmp (const char *buf, const char *match) { int len, i; len = strlength (match); for (i = 0; i < len; ++i) if (buf[i] != match[i]) return 0; return 1; } /* check that we have a specified number of bytes left in the buffer */ 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); } uint32_t rev_long (uint32_t val) { return (((rev_short ((uint16_t) val) & 0xffff) << 16) | (rev_short ((uint16_t) (val >> 16)) & 0xffff)); } /* account for new items in the non-binary output file and generate a newline every so often. */ void outfile_items (int n) { outfile_bytecount += n; outfile_itemcount += n; if (!binaryoutput && outfile_itemcount >= outfile_maxitems) { fprintf (outfile, "\n"); outfile_itemcount = 0; } } //******* structures for recording track, channel, and tone generator status // Note that the tempo can change while notes are being played, maybe many times. // Although the score timing is specified in the MIDI file in ticks, we queue and // issue note events based on microseconds since the start of the song. We convert // ticks to microseconds, using the current tempo, as the MIDI events from the // various tracks are processsed in tick order. // 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 by 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]; int secs = np->time_usec / 1000000; sprintf(notedescription, "at %3lu.%06lu sec (%d:%02d), note %d (0x%02X) track %d channel %d volume %d instrument %d", secs, np->time_usec % 1000000, secs/60, secs % 60, 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 byte delete; // delete this command due to "-noduplicates"? 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 %s\n", ndx, q->cmd == CMD_PLAYNOTE ? "PLAY" : "STOP", q->delete ? "deleted" : "", 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 && tg->note.track == np->track && tg->note.instrument == np->instrument) { // 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->delete) return; // if marked for deletion, just ignore it if (q->cmd == CMD_STOPNOTE) { if (loggen) fprintf(logfile, " dequeue stopnote for %s\n", describe(&q->note)); // 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"); if (loggen) fprintf(logfile, " dequeue playnote for %s\n", describe(&q->note)); 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)); if (showskipped) printf(" *** no free generator %s\n", 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 need 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(); } // find the queue entry for the playnote matching the stopnote in queue[search_ndx] int queue_find_playnote(int search_ndx) { if (loggen) fprintf(logfile, " queue_find_playnote(%d), %s: ", search_ndx, describe(&queue[search_ndx].note)); for (int ndx = queue_newest_ndx;;) { if (ndx != search_ndx // don't look at the entry we're trying to match && !queue[ndx].delete // don't re-find queue entries already deleted && queue[ndx].cmd == CMD_PLAYNOTE // must match playnote && queue[ndx].note.channel == queue[search_ndx].note.channel //also channel, track, note, and instrument && queue[ndx].note.track == queue[search_ndx].note.track && queue[ndx].note.note == queue[search_ndx].note.note && queue[ndx].note.instrument == queue[search_ndx].note.instrument ) { if (loggen) fprintf(logfile, "found ndx %d\n", ndx); return ndx; } if (ndx == queue_oldest_ndx) break; if (--ndx < 0) ndx = QUEUE_SIZE - 1; } if (loggen) fprintf(logfile, "not found\n"); return -1; } // find a queue entry for another stopnote matching the stopnote in queue[search_ndx] int queue_find_stopnote(int search_ndx) { if (loggen) fprintf(logfile, " queue_find_stopnote(%d), %s: ", search_ndx, describe(&queue[search_ndx].note)); for (int ndx = queue_newest_ndx;;) { if (ndx != search_ndx // don't look at the entry we're trying to match && !queue[ndx].delete // don't re-find queue entries already deleted && queue[ndx].cmd == CMD_STOPNOTE // must match stopnote && queue[ndx].note.time_usec == queue[search_ndx].note.time_usec // also time, note, and instrument && queue[ndx].note.note == queue[search_ndx].note.note && queue[ndx].note.instrument == queue[search_ndx].note.instrument) { if (loggen) fprintf(logfile, "found ndx %d\n", ndx); return ndx; } if (ndx == queue_oldest_ndx) break; if (--ndx < 0) ndx = QUEUE_SIZE - 1; } if (loggen) fprintf(logfile, "not found\n"); return -1; } // For -noduplicates, mark for deletion any note start/stop identical to the CMD_STOPNOTE we just queued void remove_queue_duplicates(int stop_ndx) { int play_ndx, dup_play_ndx, dup_stop_ndx; if ((play_ndx = queue_find_playnote(stop_ndx)) >= 0 // find its matching playnote && (dup_stop_ndx = queue_find_stopnote(stop_ndx)) >= 0 // find another stopnote for the same note at the same time && (dup_play_ndx = queue_find_playnote(dup_stop_ndx)) >= 0 // find its matching playnote && queue[dup_play_ndx].note.time_usec == queue[play_ndx].note.time_usec) { // if it starts at the same time, delete it if (loggen) fprintf(logfile, " remove duplicate, ndxs %d, %d: %s\n", dup_play_ndx, dup_stop_ndx, describe(&queue[dup_play_ndx].note)); queue[dup_play_ndx].delete = queue[dup_stop_ndx].delete = true; } } // 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; // fill in the queue entry queue[ndx].delete = false; queue[ndx].note = *np; // structure copy of the note if (noduplicates && cmd == CMD_STOPNOTE) remove_queue_duplicates(ndx); } 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_file_header (void) { struct midi_header *hdr; unsigned int time_division; chk_bufdata (hdrptr, sizeof (struct midi_header)); hdr = (struct midi_header *) hdrptr; if (!charcmp ((char *) hdr->MThd, "MThd")) midi_error ("Missing 'MThd'", hdrptr); num_tracks = rev_short (hdr->number_of_tracks); time_division = rev_short (hdr->time_division); if (time_division < 0x8000) ticks_per_beat = time_division; else ticks_per_beat = ((time_division >> 8) & 0x7f) /* SMTE frames/sec */ *(time_division & 0xff); /* ticks/SMTE frame */ if (logparse) { fprintf (logfile, "Header size %" PRId32 "\n", rev_long (hdr->header_size)); fprintf (logfile, "Format type %d\n", rev_short (hdr->format_type)); fprintf (logfile, "Number of tracks %d\n", num_tracks); fprintf (logfile, "Time division %04X\n", time_division); fprintf (logfile, "Ticks/beat = %d\n", ticks_per_beat); } hdrptr += rev_long (hdr->header_size) + 8; /* point past header to track header, presumably. */ return; } void process_track_header (int tracknum) { struct track_header *hdr; unsigned long tracklen; chk_bufdata (hdrptr, sizeof (struct track_header)); hdr = (struct track_header *) hdrptr; if (!charcmp ((char *) (hdr->MTrk), "MTrk")) midi_error ("Missing 'MTrk'", hdrptr); tracklen = rev_long (hdr->track_size); if (logparse) fprintf (logfile, "\nTrack %d length %ld\n", tracknum, tracklen); hdrptr += sizeof (struct track_header); /* point past header */ chk_bufdata (hdrptr, tracklen); track[tracknum].trkptr = hdrptr; hdrptr += tracklen; /* point to the start of the next track */ track[tracknum].trkend = hdrptr; /* the point past the end of the track */ } 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 = 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 and return. void find_next_note (int tracknum) { unsigned long int delta_ticks; int event, chan; int note, velocity, controller, pressure, pitchbend, instrument; int meta_cmd, meta_length; unsigned long int sysex_length; char *tag; struct track_status *t = &track[tracknum]; // our track status structure while (t->trkptr < t->trkend) { delta_ticks = get_varlen (&t->trkptr); t->time += delta_ticks; if (logparse) { fprintf(logfile, "# trk %d ", tracknum); if (loggen) fprintf(logfile, "at ticks+%lu=%lu: ", delta_ticks, t->time); else { 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)); break; case 0x01: tag = "description"; goto show_text; case 0x02: tag = "copyright"; goto show_text; case 0x03: tag = "track name"; if (tracknum == 0 && !parseonly && !binaryoutput) { /* Incredibly, MIDI has no standard for recording the name of the piece! Track 0's "trackname" is often used for that so we output it to the C file as documentation. */ fprintf (outfile, "// "); for (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; case 0x05: tag = "lyric"; goto show_text; case 0x06: tag = "marked point"; goto show_text; case 0x07: 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 (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); break; case 0x21: if (logparse) fprintf(logfile, "MIDI port %d\n", *t->trkptr); break; case 0x2f: if (logparse) fprintf (logfile, "end of track\n"); break; case 0x51: // tempo: 3 byte big-endian integer, not a varlen integer! t->cmd = CMD_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 (*(uint32_t *) t->trkptr)); break; case 0x58: 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)); break; case 0x7f: tag = "sequencer data"; goto show_hex; default: /* unknown meta command */ tag = "???"; show_hex: if (logparse) { fprintf (logfile, "meta cmd %02X, length %d, %s: ", meta_cmd, meta_length, tag); for (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 < 0xf0) t->last_event = event; // remember "running status" if not meta or sysex event t->chan = chan = event & 0xf; switch (event >> 4) { case 0x8: // note off t->note = *t->trkptr++; 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 instruments, force all notes to channel 0 t->cmd = CMD_STOPNOTE; /* stop processing and return */ return; } break; case 0x9: // note on t->note = *t->trkptr++; t->volume = *t->trkptr++; if (t->volume == 0) // some scores use note-on with zero velocity for off! goto note_off; 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 instruments, 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, "channel %d: note %d (0x%02X) has key pressure %d\n", chan, note, note, velocity); break; case 0xb: // control value change controller = *t->trkptr++; velocity = *t->trkptr++; if (logparse) fprintf (logfile, "channel %d: change control value of controller %d to %d\n", chan, controller, velocity); break; case 0xc: // program patch, ie which instrument instrument = *t->trkptr++; 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: // channel pressure pressure = *t->trkptr++; if (logparse) fprintf (logfile, "channel %d: after-touch pressure is %d\n", chan, pressure); break; case 0xe: // pitch wheel change pitchbend = *t->trkptr++ | (*t->trkptr++ << 7); if (logparse) fprintf (logfile, "pitch wheel change to %d\n", pitchbend); break; case 0xf: // sysex event sysex_length = get_varlen (&t->trkptr); 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 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 { // found the playing note for this stopnote // 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"); } } /********************* main ****************************/ int main (int argc, char *argv[]) { int argno; char *filebasename; int basenamelen; #define MAXPATH 120 char filename[MAXPATH]; printf ("MIDITONES V%s, (C) 2011-2021 Len Shustek\n", VERSION); if (argc == 1) { // no arguments SayUsage (argv[0]); return 1; } 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]; // strip off trailing .mid or .MID extension if provided by user basenamelen = strlength(filebasename); if (basenamelen > 4 && (charcmp (filebasename + basenamelen - 4, ".mid") || charcmp (filebasename + basenamelen - 4, ".MID"))) { filebasename[basenamelen - 4] = 0; } 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); 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"); if (!infile) { fprintf (stderr, "Unable to open input file %s\n", filename); return 1; } // Read the whole input file into memory fseek (infile, 0, SEEK_END); // find its size buflen = ftell (infile); fseek (infile, 0, SEEK_SET); 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); 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, 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 = (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; if (!binaryoutput) { /* create header of C file that initializes score data */ time_t rawtime; time (&rawtime); fprintf (outfile, "// Playtune bytestream for file \"%s.mid\" ", filebasename); fprintf (outfile, "created by MIDITONES V%s on %s", VERSION, asctime (localtime (&rawtime))); print_command_line (outfile, argc, argv); if (channel_mask != 0xffff) fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask); if (keyshift != 0) fprintf (outfile, "// Keyshift was %d chromatic notes\n", keyshift); if (define_progmem) { fprintf (outfile, "#ifdef __AVR__\n"); fprintf (outfile, "#include \n"); fprintf (outfile, "#else\n"); fprintf (outfile, "#define PROGMEM\n"); fprintf (outfile, "#endif\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); file_header_num_tgens_position = ftell (outfile); // remember where the number of tone generators is fprintf (outfile, "%2d, // (Playtune file header)\n", file_header.num_tgens); outfile_bytecount += 6; } } else if (do_header) { // write the binary file header int i; for (i = 0; i < sizeof (file_header); ++i) 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; // 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 for processing of all the tracks tempo = DEFAULT_TEMPO; for (int tracknum = 0; tracknum < num_tracks; ++tracknum) { track[tracknum].tempo = DEFAULT_TEMPO; 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_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) { process_track_data(); // do all the tracks interleaved, like a 1950's multiway merge // generate the ending commentary if (!binaryoutput) { fprintf(outfile, "\n// This %ld byte score contains %d notes and uses %d tone generator%s\n", outfile_bytecount, note_on_commands, num_tonegens_used, num_tonegens_used == 1 ? "" : "s"); if (notes_skipped) fprintf(outfile, "// %d notes had to be skipped\n", notes_skipped); } printf(" %s %d tone generators were used.\n", num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used); if (notes_skipped) printf(" %d notes were skipped because there weren't enough tone generators.\n", notes_skipped); 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); else fprintf(outfile, "%2d", num_tonegens_used); } } fclose(outfile); } if (loggen || logparse) fclose (logfile); printf (" Done.\n"); return 0; }