diff --git a/README.txt b/README.txt index 7ae092d..4fab864 100644 --- a/README.txt +++ b/README.txt @@ -44,7 +44,7 @@ (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. @@ -52,7 +52,6 @@ (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/Playtune_Teensy www.github.com/LenShustek/ATtiny-playtune This is a much simplified version that fits, with a small song, into an ATtiny @@ -64,7 +63,7 @@ (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. + various forks of this code, and of the Playtune players, on Githib. *** THE PROGRAM @@ -136,8 +135,8 @@ 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. + 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. @@ -151,10 +150,10 @@ -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. + 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) + -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. @@ -164,14 +163,18 @@ -h Give command-line help. - -showskipped Display information to the console each note that had to be skipped - because there weren't enough tone generators. + -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 timing is exact to the millisecond. + 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 @@ -187,10 +190,10 @@ 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. + 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. (Only valid with -v.) + 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 @@ -203,7 +206,7 @@ *** THE SCORE BYTESTREAM - The generated bytestream is a series of commands to turn notes on and off, + 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. @@ -218,8 +221,8 @@ 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. + 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. @@ -250,7 +253,7 @@ 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 + 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. \ No newline at end of file + Len Shustek, 2011 to 2021; see the change log. diff --git a/miditones.c b/miditones.c index 2dc4c22..9844af1 100644 --- a/miditones.c +++ b/miditones.c @@ -46,7 +46,7 @@ (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. @@ -54,7 +54,6 @@ (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/Playtune_Teensy www.github.com/LenShustek/ATtiny-playtune This is a much simplified version that fits, with a small song, into an ATtiny @@ -66,7 +65,7 @@ (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. + various forks of this code, and of the Playtune players, on Githib. *** THE PROGRAM @@ -138,8 +137,8 @@ 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. + 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. @@ -153,10 +152,10 @@ -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. + 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) + -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. @@ -166,14 +165,18 @@ -h Give command-line help. - -showskipped Display information to the console each note that had to be skipped - because there weren't enough tone generators. + -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 timing is exact to the millisecond. + 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 @@ -189,10 +192,10 @@ 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. + 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. (Only valid with -v.) + 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 @@ -205,7 +208,7 @@ *** THE SCORE BYTESTREAM - The generated bytestream is a series of commands to turn notes on and off, + 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. @@ -220,8 +223,8 @@ 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. + 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. @@ -252,7 +255,7 @@ 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 + 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. @@ -382,6 +385,15 @@ 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 @@ -397,7 +409,7 @@ future version ideas channel 8 // organ options -attacktime=1000 -sustainlevel=80% -releasetime=100 -notemin=200 */ -#define VERSION "2.2" +#define VERSION "2.4" /*-------------------------------------------------------------------------------------------- @@ -408,7 +420,7 @@ Gleaned from http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfilefor 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. + 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 @@ -457,28 +469,31 @@ a Sysex event track_event is: a meta event track_event is: FF 00 02 ssss specify sequence number - FF 01 "xx"... arbitrary text + 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 quarter-note, for all tracks + 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. +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. @@ -586,7 +601,7 @@ struct track_header { bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput, define_progmem, volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header, - gen_restart, scorename, showskipped; + gen_restart, scorename, showskipped, noduplicates; FILE *infile, *outfile, *logfile; uint8_t *buffer, *hdrptr; unsigned long buflen; @@ -719,6 +734,7 @@ void SayUsage(char *programName) { " -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", @@ -733,7 +749,8 @@ void SayUsage(char *programName) { 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++) { + int i, firstnonoption = 0; + for (i = 1; i < argc; i++) { if (argv[i][0] == '/' || argv[i][0] == '-') { int tempint; char *arg = argv[i] + 1; @@ -769,6 +786,7 @@ int HandleOptions (int argc, char *argv[]) { 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; @@ -879,7 +897,11 @@ void outfile_items (int n) { //******* structures for recording track, channel, and tone generator status -// Note that the tempo can change as notes are played, maybe many times. +// 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. @@ -889,6 +911,7 @@ struct noteinfo { // everything we might care about as a note 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? @@ -899,7 +922,7 @@ 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 + 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 @@ -945,6 +968,7 @@ still plenty long. 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]; @@ -960,7 +984,7 @@ void show_queue(void) { // for debugging: dump the whole event queue 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)); + 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; } } @@ -984,7 +1008,8 @@ int find_idle_tgen(struct noteinfo *np) { // returns -1 if there isn't one 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.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", @@ -1014,8 +1039,9 @@ int find_idle_tgen(struct noteinfo *np) { // returns -1 if there isn't one 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) { @@ -1036,6 +1062,7 @@ void remove_queue_entry(int ndx) { // remove the oldest queue entry 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 @@ -1120,7 +1147,7 @@ void pull_queue(void) { --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? + // 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 @@ -1138,12 +1165,57 @@ void pull_queue(void) { 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(); + 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 @@ -1175,9 +1247,10 @@ void queue_cmd(byte cmd, struct noteinfo *np) { 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].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); @@ -1364,7 +1437,7 @@ show_hex: 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 + if (!instrumentoutput) t->chan = 0; // if no instruments, force all notes to channel 0 t->cmd = CMD_STOPNOTE; /* stop processing and return */ return; } break; @@ -1376,7 +1449,7 @@ note_off: if (logparse) fprintf(logfile, "note %d (0x%02X) off, channel %d, vo 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 + if (!instrumentoutput) t->chan = 0; // if no instruments, force all notes to channel 0 t->cmd = CMD_PLAYNOTE; /* stop processing and return */ return; } break; @@ -1492,7 +1565,7 @@ void process_track_data(void) { ++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 { + 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]; @@ -1563,7 +1636,7 @@ int main (int argc, char *argv[]) { #define MAXPATH 120 char filename[MAXPATH]; - printf ("MIDITONES V%s, (C) 2011-2020 Len Shustek\n", VERSION); + printf ("MIDITONES V%s, (C) 2011-2021 Len Shustek\n", VERSION); if (argc == 1) { // no arguments SayUsage (argv[0]); return 1; } @@ -1700,11 +1773,11 @@ int main (int argc, char *argv[]) { // 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"); + 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); } + 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) diff --git a/miditones.exe b/miditones.exe index ba15dfc..07e6d9c 100644 Binary files a/miditones.exe and b/miditones.exe differ diff --git a/miditones_scroll.c b/miditones_scroll.c index e92f06e..47fa090 100644 --- a/miditones_scroll.c +++ b/miditones_scroll.c @@ -43,12 +43,15 @@ * * -x Show notes in hex instead of as octave-noteletter-sharp * +* -n Don't show the bytestream data. (Ignored if -c is specified.) +* * For source code to this and related programs, see * www.github.com/LenShustek/miditones * www.github.com/LenShustek/arduino-playtune * www.github.com/LenShustek/Playtune_poll * www.github.com/LenShustek/Playtune_poll * www.github.com/LenShustek/Playtune_synth +* www.github.com/LenShustek/Playtune_Teensy * www.github.com/LenShustek/ATtiny-playtune * *---------------------------------------------------------------------------------------- @@ -112,9 +115,12 @@ * 23 April 2021, L. Shustek, V1.9 * - show a summary at the end of what instruments were used and how many times * - show the lowest and highest volume used +* 5 May 2021, L. Shustek, V1.10 +* - add -n option to not print the bytestream data +* - don't show instrument summary if instrument data wasn't in the bytestream */ -#define VERSION "1.9" +#define VERSION "1.10" #include #include @@ -153,6 +159,8 @@ bool codeoutput = false; bool expect_volume = false; bool ignore_volume = false; bool showhex = false; +bool showbytestream = true; +bool got_instruments = false; unsigned max_vol = 0, min_vol = 255; struct file_hdr_t { /* what the optional file header looks like */ @@ -235,6 +243,7 @@ void SayUsage (char *programName) { " -vi expects and ignores volume information", " -c creates an annotated C source file as .c", " -x show notes in hex instead of octave/note", + " -n don't show the bytestream data", "" }; int i = 0; while (usage[i][0] != '\0') @@ -260,6 +269,9 @@ int HandleOptions (int argc, char *argv[]) { case 'X': showhex = true; break; + case 'N': + showbytestream = false; + break; case 'T': if (sscanf (&argv[i][2], "%d", &num_tonegens) != 1 || num_tonegens < 1 || num_tonegens > MAX_TONEGENS) @@ -383,12 +395,13 @@ void print_status (void) { else fprintf (outfile, " v%-3d", gen_volume[gen]); } // display the hex commands that created these changes - fprintf (outfile, "%3u.%03u %c%04X: ", delay/1000, delay%1000, warning ? '!' : ' ', (unsigned int) (lastbufptr - buffer)); + fprintf (outfile, "%3u.%03u %c", delay/1000, delay%1000, warning ? '!' : ' '); + if (showbytestream) fprintf (outfile, "%04X: ", (unsigned int) (lastbufptr - buffer)); warning = false; if (codeoutput) fprintf (outfile, "*/ "); // end comment - for (; lastbufptr <= bufptr; ++lastbufptr) - fprintf (outfile, codeoutput ? "0x%02X," : "%02X ", *lastbufptr); + if (showbytestream) for (; lastbufptr <= bufptr; ++lastbufptr) + fprintf (outfile, codeoutput ? "0x%02X," : "%02X ", *lastbufptr); fprintf (outfile, "\n"); lastbufptr = bufptr + 1; } @@ -413,6 +426,7 @@ int main (int argc, char *argv[]) { return 1; } argno = HandleOptions (argc, argv); /* process options */ + if (codeoutput) showbytestream = true; filebasename = argv[argno]; strlcpy (filename, filebasename, MAXPATH); // Open the input file @@ -487,7 +501,9 @@ int main (int argc, char *argv[]) { fprintf (outfile, " time "); for (unsigned i = 0; i < num_tonegens; ++i) fprintf (outfile, expect_volume && !ignore_volume ? " gen%-5d" : " gen%-2d", i); - fprintf (outfile, "delay addr bytestream code\n\n"); + fprintf(outfile, "delay"); + if (showbytestream) fprintf(outfile, " addr bytestream code"); + fprintf(outfile, "\n\n"); for (gen = 0; gen < num_tonegens; ++gen) gen_note[gen] = SILENT; @@ -533,6 +549,7 @@ int main (int argc, char *argv[]) { gen_note[gen] = SILENT; gen_did_stopnote[gen] = true; } else if (cmd == 0xc0) { /* change instrument */ + got_instruments = true; gen_instrument[gen] = *++bufptr & 0x7f; gen_instrument_changed[gen] = true; } else { @@ -562,10 +579,11 @@ int main (int argc, char *argv[]) { fprintf (infofile, "%u stopnote commands were unnecessary.\n", stopnotes_before_startnote); fprintf (infofile, "%u consecutive delays could have been merged.\n", consecutive_delays); if (stopnotes_before_startnote + consecutive_delays > 0) fprintf (infofile, "(Those locations are marked with \"!\")\n"); - fprintf(infofile, "instruments used:\n"); - for (int i = 0; i < 128; ++i) - if (instrument_count[i]) { - fprintf(infofile, " %s (%3d, 0x%02X) %7d\n", instrumentname[i], i, i, instrument_count[i]); } + if (got_instruments) { + fprintf(infofile, "instruments used:\n"); + for (int i = 0; i < 128; ++i) + if (instrument_count[i]) { + fprintf(infofile, " %s (%3d, 0x%02X) %7d\n", instrumentname[i], i, i, instrument_count[i]); } } if (expect_volume) fprintf(infofile, "volume ranged from %d to %d\n", min_vol, max_vol); printf ("Done.\n"); } diff --git a/miditones_scroll.exe b/miditones_scroll.exe index 8235666..420279c 100644 Binary files a/miditones_scroll.exe and b/miditones_scroll.exe differ