fix bug that eliminated duplicate notes unintentionally; add -noduplicates option

master
Len Shustek 3 years ago
parent 3a0d14ce12
commit 5d5acb2e84
  1. 39
      README.txt
  2. 155
      miditones.c
  3. BIN
      miditones.exe
  4. 36
      miditones_scroll.c
  5. BIN
      miditones_scroll.exe

@ -44,7 +44,7 @@
(from MIDI channel 10) are generated from longer sampled waveforms of a complete (from MIDI channel 10) are generated from longer sampled waveforms of a complete
instrument strike. Each generator's volume is independently adjusted according to instrument strike. Each generator's volume is independently adjusted according to
the MIDI velocity of the note being played before all channels are mixed. the MIDI velocity of the note being played before all channels are mixed.
www.github.com/LenShustek/Playtune_Teensy www.github.com/LenShustek/Playtune_Teensy
The fifth version is for the Teensy 3.1/3.2, and uses the four Periodic Interval 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. 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, (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. 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.) 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 www.github.com/LenShustek/ATtiny-playtune
This is a much simplified version that fits, with a small song, into an ATtiny 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!) (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 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 *** THE PROGRAM
@ -136,8 +135,8 @@
The following are lesser-used command-line options: The following are lesser-used command-line options:
-c=n Only process the channel numbers whose bits are on in the number "n". -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, For example, -c3 means "only process channels 0 and 1". In addition to
"n" can be also specified in hex using a 0x prefix. 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. -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 -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. -p Only parse the MIDI file, and don't generate an output file.
Tracks are processed sequentially instead of being merged into chronological order. Tracks are processed sequentially instead of being merged into chronological
This is mostly useful for debugging MIDI file parsing problems. 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. -r Terminate the output file with a "restart" command instead of a "stop" command.
@ -164,14 +163,18 @@
-h Give command-line help. -h Give command-line help.
-showskipped Display information to the console each note that had to be skipped -showskipped Display information to the console about each note that had to be
because there weren't enough tone generators. 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 -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 of "delay" commands and thus make the bytestream smaller, at the expense of
moving notes slightly. The deficits are accumulated and eventually used, moving notes slightly. The deficits are accumulated and eventually used,
so that there is no loss of synchronization in the long term. 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 -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 in better sound separation between notes. It might also allow more notes to
@ -187,10 +190,10 @@
too short. (Only valid with -v.) too short. (Only valid with -v.)
-attacknotemax=x Notes larger than x milliseconds won't have the attack/sustain profile -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 -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 <basefilename> as the name of the score in the generated C code -scorename Use <basefilename> as the name of the score in the generated C code
instead of "score", and name the file <basefilename>.h instead of instead of "score", and name the file <basefilename>.h instead of
@ -203,7 +206,7 @@
*** THE SCORE BYTESTREAM *** 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. change instruments, and request a delay until the next event time.
Here are the details, with numbers shown in hexadecimal. Here are the details, with numbers shown in hexadecimal.
@ -218,8 +221,8 @@
8t Stop playing the note on tone generator t. 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 Ct ii Change tone generator t to play instrument ii from now on. This will
be generated if the -i option was given. only be generated if the -i option was given.
F0 End of score; stop playing. F0 End of score; stop playing.
@ -250,7 +253,7 @@
ff2 Another byte of flags, currently undefined ff2 Another byte of flags, currently undefined
tt The number (in one byte) of tone generators actually used in this music. 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. and should be ignored by players.
Len Shustek, 2011 to 2021; see the change log. Len Shustek, 2011 to 2021; see the change log.

@ -46,7 +46,7 @@
(from MIDI channel 10) are generated from longer sampled waveforms of a complete (from MIDI channel 10) are generated from longer sampled waveforms of a complete
instrument strike. Each generator's volume is independently adjusted according to instrument strike. Each generator's volume is independently adjusted according to
the MIDI velocity of the note being played before all channels are mixed. the MIDI velocity of the note being played before all channels are mixed.
www.github.com/LenShustek/Playtune_Teensy www.github.com/LenShustek/Playtune_Teensy
The fifth version is for the Teensy 3.1/3.2, and uses the four Periodic Interval 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. 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, (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. 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.) 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 www.github.com/LenShustek/ATtiny-playtune
This is a much simplified version that fits, with a small song, into an ATtiny 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!) (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 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 *** THE PROGRAM
@ -138,8 +137,8 @@
The following are lesser-used command-line options: The following are lesser-used command-line options:
-c=n Only process the channel numbers whose bits are on in the number "n". -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, For example, -c3 means "only process channels 0 and 1". In addition to
"n" can be also specified in hex using a 0x prefix. 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. -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 -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. -p Only parse the MIDI file, and don't generate an output file.
Tracks are processed sequentially instead of being merged into chronological order. Tracks are processed sequentially instead of being merged into chronological
This is mostly useful for debugging MIDI file parsing problems. 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. -r Terminate the output file with a "restart" command instead of a "stop" command.
@ -166,14 +165,18 @@
-h Give command-line help. -h Give command-line help.
-showskipped Display information to the console each note that had to be skipped -showskipped Display information to the console about each note that had to be
because there weren't enough tone generators. 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 -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 of "delay" commands and thus make the bytestream smaller, at the expense of
moving notes slightly. The deficits are accumulated and eventually used, moving notes slightly. The deficits are accumulated and eventually used,
so that there is no loss of synchronization in the long term. 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 -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 in better sound separation between notes. It might also allow more notes to
@ -189,10 +192,10 @@
too short. (Only valid with -v.) too short. (Only valid with -v.)
-attacknotemax=x Notes larger than x milliseconds won't have the attack/sustain profile -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 -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 <basefilename> as the name of the score in the generated C code -scorename Use <basefilename> as the name of the score in the generated C code
instead of "score", and name the file <basefilename>.h instead of instead of "score", and name the file <basefilename>.h instead of
@ -205,7 +208,7 @@
*** THE SCORE BYTESTREAM *** 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. change instruments, and request a delay until the next event time.
Here are the details, with numbers shown in hexadecimal. Here are the details, with numbers shown in hexadecimal.
@ -220,8 +223,8 @@
8t Stop playing the note on tone generator t. 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 Ct ii Change tone generator t to play instrument ii from now on. This will
be generated if the -i option was given. only be generated if the -i option was given.
F0 End of score; stop playing. F0 End of score; stop playing.
@ -252,7 +255,7 @@
ff2 Another byte of flags, currently undefined ff2 Another byte of flags, currently undefined
tt The number (in one byte) of tone generators actually used in this music. 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. and should be ignored by players.
Len Shustek, 2011 to 2021; see the change log. Len Shustek, 2011 to 2021; see the change log.
@ -382,6 +385,15 @@
22 April 2021, Len Shustek, V2.2 22 April 2021, Len Shustek, V2.2
-Add -showskipped to log the places where notes had to be discarded because -Add -showskipped to log the places where notes had to be discarded because
there aren't enough tone generators. 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 future version ideas
@ -397,7 +409,7 @@ future version ideas
channel 8 // organ channel 8 // organ
options -attacktime=1000 -sustainlevel=80% -releasetime=100 -notemin=200 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 but also check out http://midi.teragonaudio.com/tech/miditech.htm
Notation: Notation:
<xxx> 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. <xxx> 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. lower case letters are hex digits. If preceeded by 0, only low 7 bits are used.
"xx" are ascii text characters "xx" are ascii text characters
{xxx}... means indefinite repeat of xxx {xxx}... means indefinite repeat of xxx
@ -457,28 +469,31 @@ a Sysex event track_event is:
a meta event track_event is: a meta event track_event is:
FF 00 02 ssss specify sequence number FF 00 02 ssss specify sequence number
FF 01 <len> "xx"... arbitrary text FF 01 <len> "xx"... arbitrary description text
FF 02 <len> "xx"... copyright notice FF 02 <len> "xx"... copyright notice
FF 03 <len> "xx"... sequence or track name FF 03 <len> "xx"... sequence or track name
FF 04 <len> "xx"... instrument name FF 04 <len> "xx"... instrument name
FF 05 <len> "xx"... lyric to be sung FF 05 <len> "xx"... lyric to be sung
FF 06 <len> "xx"... name of marked point in the score FF 06 <len> "xx"... name of marked point in the score
FF 07 <len> "xx"... description of cue point in the score FF 07 <len> "xx"... description of cue point in the score
FF 08 <len> "xx"... program name
FF 09 <len> "xx"... device (port) name
FF 20 01 0c default channel for subsequent events without a channel is c FF 20 01 0c default channel for subsequent events without a channel is c
FF 21 01 pp MIDI port is pp FF 21 01 pp MIDI port is pp
FF 2F 00 end of track 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 54 05 hhmmssfrff set SMPTE time to start the track
FF 58 04 nnddccbb set time signature FF 58 04 nnddccbb set time signature
FF 59 02 sfmi set key signature FF 59 02 sfmi set key signature
FF 7F <len> data sequencer-specific data FF 7F <len> data sequencer-specific data
Note that "set tempo" events are supposed to occur in only one track (generally the first), 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 See https://stackoverflow.com/questions/1080297/how-does-midi-tempo-message-apply-to-other-tracks
--------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------*/
/*--------------- processing outline ----------------------------------- /*--------------- processing outline -----------------------------------
Lots of details are omitted. Note that MIDI track parsing is based Lots of details are omitted. Note that MIDI track parsing is based
on counting "ticks", but our queueing is based on real-time seconds. on counting "ticks", but our queueing is based on real-time seconds.
The number of ticks per second changes with the tempo. 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, bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput, define_progmem,
volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header, volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header,
gen_restart, scorename, showskipped; gen_restart, scorename, showskipped, noduplicates;
FILE *infile, *outfile, *logfile; FILE *infile, *outfile, *logfile;
uint8_t *buffer, *hdrptr; uint8_t *buffer, *hdrptr;
unsigned long buflen; unsigned long buflen;
@ -719,6 +734,7 @@ void SayUsage(char *programName) {
" -s1 strategy 1: favor track 1", " -s1 strategy 1: favor track 1",
" -s2 strategy 2: try to assign tracks to specific tone generators", " -s2 strategy 2: try to assign tracks to specific tone generators",
" -showskipped display information about each note that had to be skipped", " -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", " -delaymin=x minimum delay is x msec, to save bytestream space",
" -attacktime=x the high volume attack phase lasts x msec", " -attacktime=x the high volume attack phase lasts x msec",
" -attacknotemax=x notes over x msec don't use the attack/sustain profile", " -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[]) { int HandleOptions (int argc, char *argv[]) {
/* returns the index of the first argument that is not an option, /* returns the index of the first argument that is not an option,
i.e. does not start with a dash or a slash */ 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] == '-') { if (argv[i][0] == '/' || argv[i][0] == '-') {
int tempint; int tempint;
char *arg = argv[i] + 1; 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, "s2")) strategy2 = true;
else if (opt_key(arg, "scorename")) scorename = true; else if (opt_key(arg, "scorename")) scorename = true;
else if (opt_key(arg, "showskipped")) showskipped = 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)) else if (opt_int(arg, "t", &num_tonegens, 1, MAX_TONEGENS))
printf("Using %d tone generators\n", num_tonegens); printf("Using %d tone generators\n", num_tonegens);
else if (opt_key(arg, "v")) volume_output = true; 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 //******* 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 // 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 // accumulate the duration of all playing notes every time the tempo changes, and
// then one final time when the "stop note" event occurs. // 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 int track, channel, note, instrument, volume; // all the nitty-gritty about it
}; };
struct tonegen_status { // current status of a tone generator struct tonegen_status { // current status of a tone generator
bool playing; // is it playing? bool playing; // is it playing?
bool stopnote_pending; // are we due to issue a stop note command? 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 *trkptr; // ptr to the next event we care about
uint8_t *trkend; // ptr just past the end of the track 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 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 int preferred_tonegen; // for strategy2: try to use this generator
byte cmd; // next CMD_xxxx event coming up byte cmd; // next CMD_xxxx event coming up
byte chan, note, volume; // if it is CMD_PLAYNOTE or CMD_STOPNOTE, the note info 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 struct queue_entry { // the format of each queue entry
byte cmd; // CMD_PLAY or CMD_STOP 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 struct noteinfo note; // info about the note, including the action time
} queue[QUEUE_SIZE]; } queue[QUEUE_SIZE];
@ -960,7 +984,7 @@ void show_queue(void) { // for debugging: dump the whole event queue
int ndx = queue_oldest_ndx; int ndx = queue_oldest_ndx;
if (queue_numitems > 0) while (1) { if (queue_numitems > 0) while (1) {
struct queue_entry *q = &queue[ndx]; 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_newest_ndx) break;
if (++ndx >= QUEUE_SIZE) ndx = 0; } } 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? for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { // first, is this note already playing on this channel?
tg = &tonegen[tgnum]; tg = &tonegen[tgnum];
if (tg->playing 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 // this must be the start of the sustain phase of a playing note
++playnotes_without_stopnotes; ++playnotes_without_stopnotes;
if (loggen) fprintf(logfile, " *** playnote without stopnote, tgen %d, %s\n", 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 void remove_queue_entry(int ndx) { // remove the oldest queue entry
struct queue_entry *q = &queue[ndx]; struct queue_entry *q = &queue[ndx];
if (q->delete) return; // if marked for deletion, just ignore it
if (q->cmd == CMD_STOPNOTE) { 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 // find the tone generator playing this note, and record a pending stop
int tgnum; int tgnum;
for (tgnum = 0; tgnum < num_tonegens; ++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 else { // CMD_PLAYNOTE
assert(q->cmd == CMD_PLAYNOTE, "bad cmd in remove_queue_entry"); 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); int tgnum = find_idle_tgen(&q->note);
struct tonegen_status *tg = &tonegen[tgnum]; struct tonegen_status *tg = &tonegen[tgnum];
if (tgnum >= 0) { // we found a tone generator we can use if (tgnum >= 0) { // we found a tone generator we can use
@ -1120,7 +1147,7 @@ void pull_queue(void) {
--queue_numitems; } --queue_numitems; }
while (queue_numitems > 0 && queue[queue_oldest_ndx].note.time_usec <= oldtime + (timestamp)delaymin_usec); 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) { for (int tgnum = 0; tgnum < num_tonegens; ++tgnum) {
struct tonegen_status *tg = &tonegen[tgnum]; struct tonegen_status *tg = &tonegen[tgnum];
if (tg->stopnote_pending) { // got one if (tg->stopnote_pending) { // got one
@ -1138,12 +1165,57 @@ void pull_queue(void) {
void flush_queue(void) { // empty the queue void flush_queue(void) { // empty the queue
while (queue_numitems > 0) pull_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 // queue a "note on" or "note off" command
void queue_cmd(byte cmd, struct noteinfo *np) { void queue_cmd(byte cmd, struct noteinfo *np) {
if (loggen) fprintf(logfile, " queue %s %s\n", if (loggen) fprintf(logfile, " queue %s %s\n",
cmd == CMD_PLAYNOTE ? "PLAY" : cmd == CMD_STOPNOTE ? "STOP" : "????", cmd == CMD_PLAYNOTE ? "PLAY" : cmd == CMD_STOPNOTE ? "STOP" : "????",
describe(np)); 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"); assert(queue_numitems < QUEUE_SIZE, "no room in queue");
timestamp horizon = output_usec + output_deficit_usec; timestamp horizon = output_usec + output_deficit_usec;
if (np->time_usec < horizon) { // don't allow revisionist history 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; } if (++ndx >= QUEUE_SIZE) ndx = 0; }
insert: // store the item at ndx insert: // store the item at ndx
++queue_numitems; ++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 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) { 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); 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); 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 if ((1 << chan) & channel_mask // we're processing this channel
&& (!percussion_ignore || chan != PERCUSSION_TRACK)) { // and not ignoring percussion && (!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 */ t->cmd = CMD_STOPNOTE; /* stop processing and return */
return; } return; }
break; 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 (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 if ((1 << chan) & channel_mask // we're processing this channel
&& (!percussion_ignore || chan != PERCUSSION_TRACK)) { // and not ignoring percussion && (!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 */ t->cmd = CMD_PLAYNOTE; /* stop processing and return */
return; } return; }
break; break;
@ -1492,7 +1565,7 @@ void process_track_data(void) {
++noteinfo_notfound; // presumably the array overflowed on input ++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", 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); } 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" // 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. // command with reduced volume, and/or move the stopnote command earlier than now.
struct noteinfo *np = &cp->notes_playing[ndx]; struct noteinfo *np = &cp->notes_playing[ndx];
@ -1563,7 +1636,7 @@ int main (int argc, char *argv[]) {
#define MAXPATH 120 #define MAXPATH 120
char filename[MAXPATH]; 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 if (argc == 1) { // no arguments
SayUsage (argv[0]); SayUsage (argv[0]);
return 1; } return 1; }
@ -1700,11 +1773,11 @@ int main (int argc, char *argv[]) {
// generate the ending commentary // generate the ending commentary
if (!binaryoutput) { if (!binaryoutput) {
fprintf(outfile, "\n// This score contains %ld bytes, and %d tone generator%s used.\n", fprintf(outfile, "\n// This %ld byte score contains %d notes and uses %d tone generator%s\n",
outfile_bytecount, num_tonegens_used, outfile_bytecount, note_on_commands, num_tonegens_used,
num_tonegens_used == 1 ? " is" : "s are"); num_tonegens_used == 1 ? "" : "s");
if (notes_skipped) 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", printf(" %s %d tone generators were used.\n",
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used); num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
if (notes_skipped) if (notes_skipped)

Binary file not shown.

@ -43,12 +43,15 @@
* *
* -x Show notes in hex instead of as octave-noteletter-sharp * -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 * For source code to this and related programs, see
* www.github.com/LenShustek/miditones * www.github.com/LenShustek/miditones
* www.github.com/LenShustek/arduino-playtune * www.github.com/LenShustek/arduino-playtune
* www.github.com/LenShustek/Playtune_poll * www.github.com/LenShustek/Playtune_poll
* www.github.com/LenShustek/Playtune_poll * www.github.com/LenShustek/Playtune_poll
* www.github.com/LenShustek/Playtune_synth * www.github.com/LenShustek/Playtune_synth
* www.github.com/LenShustek/Playtune_Teensy
* www.github.com/LenShustek/ATtiny-playtune * www.github.com/LenShustek/ATtiny-playtune
* *
*---------------------------------------------------------------------------------------- *----------------------------------------------------------------------------------------
@ -112,9 +115,12 @@
* 23 April 2021, L. Shustek, V1.9 * 23 April 2021, L. Shustek, V1.9
* - show a summary at the end of what instruments were used and how many times * - show a summary at the end of what instruments were used and how many times
* - show the lowest and highest volume used * - 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 <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -153,6 +159,8 @@ bool codeoutput = false;
bool expect_volume = false; bool expect_volume = false;
bool ignore_volume = false; bool ignore_volume = false;
bool showhex = false; bool showhex = false;
bool showbytestream = true;
bool got_instruments = false;
unsigned max_vol = 0, min_vol = 255; unsigned max_vol = 0, min_vol = 255;
struct file_hdr_t { /* what the optional file header looks like */ 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", " -vi expects and ignores volume information",
" -c creates an annotated C source file as <basefile>.c", " -c creates an annotated C source file as <basefile>.c",
" -x show notes in hex instead of octave/note", " -x show notes in hex instead of octave/note",
" -n don't show the bytestream data",
"" }; "" };
int i = 0; int i = 0;
while (usage[i][0] != '\0') while (usage[i][0] != '\0')
@ -260,6 +269,9 @@ int HandleOptions (int argc, char *argv[]) {
case 'X': case 'X':
showhex = true; showhex = true;
break; break;
case 'N':
showbytestream = false;
break;
case 'T': case 'T':
if (sscanf (&argv[i][2], "%d", &num_tonegens) != 1 || num_tonegens < 1 if (sscanf (&argv[i][2], "%d", &num_tonegens) != 1 || num_tonegens < 1
|| num_tonegens > MAX_TONEGENS) || num_tonegens > MAX_TONEGENS)
@ -383,12 +395,13 @@ void print_status (void) {
else else
fprintf (outfile, " v%-3d", gen_volume[gen]); } fprintf (outfile, " v%-3d", gen_volume[gen]); }
// display the hex commands that created these changes // 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; warning = false;
if (codeoutput) if (codeoutput)
fprintf (outfile, "*/ "); // end comment fprintf (outfile, "*/ "); // end comment
for (; lastbufptr <= bufptr; ++lastbufptr) if (showbytestream) for (; lastbufptr <= bufptr; ++lastbufptr)
fprintf (outfile, codeoutput ? "0x%02X," : "%02X ", *lastbufptr); fprintf (outfile, codeoutput ? "0x%02X," : "%02X ", *lastbufptr);
fprintf (outfile, "\n"); fprintf (outfile, "\n");
lastbufptr = bufptr + 1; } lastbufptr = bufptr + 1; }
@ -413,6 +426,7 @@ int main (int argc, char *argv[]) {
return 1; } return 1; }
argno = HandleOptions (argc, argv); /* process options */ argno = HandleOptions (argc, argv); /* process options */
if (codeoutput) showbytestream = true;
filebasename = argv[argno]; filebasename = argv[argno];
strlcpy (filename, filebasename, MAXPATH); // Open the input file strlcpy (filename, filebasename, MAXPATH); // Open the input file
@ -487,7 +501,9 @@ int main (int argc, char *argv[]) {
fprintf (outfile, " time "); fprintf (outfile, " time ");
for (unsigned i = 0; i < num_tonegens; ++i) for (unsigned i = 0; i < num_tonegens; ++i)
fprintf (outfile, expect_volume && !ignore_volume ? " gen%-5d" : " gen%-2d", 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) for (gen = 0; gen < num_tonegens; ++gen)
gen_note[gen] = SILENT; gen_note[gen] = SILENT;
@ -533,6 +549,7 @@ int main (int argc, char *argv[]) {
gen_note[gen] = SILENT; gen_note[gen] = SILENT;
gen_did_stopnote[gen] = true; } gen_did_stopnote[gen] = true; }
else if (cmd == 0xc0) { /* change instrument */ else if (cmd == 0xc0) { /* change instrument */
got_instruments = true;
gen_instrument[gen] = *++bufptr & 0x7f; gen_instrument[gen] = *++bufptr & 0x7f;
gen_instrument_changed[gen] = true; } gen_instrument_changed[gen] = true; }
else { 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 stopnote commands were unnecessary.\n", stopnotes_before_startnote);
fprintf (infofile, "%u consecutive delays could have been merged.\n", consecutive_delays); 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"); if (stopnotes_before_startnote + consecutive_delays > 0) fprintf (infofile, "(Those locations are marked with \"!\")\n");
fprintf(infofile, "instruments used:\n"); if (got_instruments) {
for (int i = 0; i < 128; ++i) fprintf(infofile, "instruments used:\n");
if (instrument_count[i]) { for (int i = 0; i < 128; ++i)
fprintf(infofile, " %s (%3d, 0x%02X) %7d\n", instrumentname[i], i, i, instrument_count[i]); } if (instrument_count[i]) {
fprintf(infofile, " %s (%3d, 0x%02X) %7d\n", instrumentname[i], i, i, instrument_count[i]); } }
if (expect_volume) if (expect_volume)
fprintf(infofile, "volume ranged from %d to %d\n", min_vol, max_vol); fprintf(infofile, "volume ranged from %d to %d\n", min_vol, max_vol);
printf ("Done.\n"); } printf ("Done.\n"); }

Binary file not shown.
Loading…
Cancel
Save