|
|
|
@ -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 <basefilename> as the name of the score in the generated C code |
|
|
|
|
instead of "score", and name the file <basefilename>.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: |
|
|
|
|
<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. |
|
|
|
|
"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 <len> "xx"... arbitrary text |
|
|
|
|
FF 01 <len> "xx"... arbitrary description text |
|
|
|
|
FF 02 <len> "xx"... copyright notice |
|
|
|
|
FF 03 <len> "xx"... sequence or track name |
|
|
|
|
FF 04 <len> "xx"... instrument name |
|
|
|
|
FF 05 <len> "xx"... lyric to be sung |
|
|
|
|
FF 06 <len> "xx"... name of marked 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 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 <len> 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) |
|
|
|
|