|
|
@ -46,6 +46,15 @@ |
|
|
|
(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 |
|
|
|
|
|
|
|
The fifth version is for the Teensy 3.1/3.2, and uses the four Periodic Interval |
|
|
|
|
|
|
|
Timers in the Cortex M4 processor to support up to 4 simultaneous notes. |
|
|
|
|
|
|
|
It uses less CPU time than the polling version, but is limited to 4 notes at a time. |
|
|
|
|
|
|
|
(This was written to experiment with multi-channel multi-Tesla Coil music playing, |
|
|
|
|
|
|
|
where I use Flexible Timer Module FTM0 for generating precise one-shot pulses. |
|
|
|
|
|
|
|
But I ultimately switched to the polling version to play more simultaneous notes.) |
|
|
|
|
|
|
|
www.github.com/LenShustek/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 |
|
|
@ -157,6 +166,9 @@ |
|
|
|
|
|
|
|
|
|
|
|
-h Give command-line help. |
|
|
|
-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. |
|
|
|
|
|
|
|
|
|
|
|
-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, |
|
|
@ -213,7 +225,7 @@ |
|
|
|
|
|
|
|
|
|
|
|
F0 End of score; stop playing. |
|
|
|
F0 End of score; stop playing. |
|
|
|
|
|
|
|
|
|
|
|
E0 End of score, but start playing again from the beginning. This is
|
|
|
|
E0 End of score, but start playing again from the beginning. This is |
|
|
|
generated by the -r option. |
|
|
|
generated by the -r option. |
|
|
|
|
|
|
|
|
|
|
|
If the high-order bit of the byte is 0, it is a command to delay for a while until |
|
|
|
If the high-order bit of the byte is 0, it is a command to delay for a while until |
|
|
@ -243,11 +255,11 @@ |
|
|
|
Any subsequent header bytes covered by the count, if present, are currently undefined |
|
|
|
Any subsequent header bytes covered by the count, if present, are currently undefined |
|
|
|
and should be ignored by players. |
|
|
|
and should be ignored by players. |
|
|
|
|
|
|
|
|
|
|
|
Len Shustek, 2011 to 2019; see the change log. |
|
|
|
Len Shustek, 2011 to 2021; see the change log. |
|
|
|
|
|
|
|
|
|
|
|
*---------------------------------------------------------------------------------------- |
|
|
|
*---------------------------------------------------------------------------------------- |
|
|
|
* The MIT License (MIT) |
|
|
|
* The MIT License (MIT) |
|
|
|
* Copyright (c) 2011,2013,2015,2016,2019 Len Shustek |
|
|
|
* Copyright (c) 2011,2013,2015,2016,2019,2021 Len Shustek |
|
|
|
* |
|
|
|
* |
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|
|
|
* of this software and associated documentation files (the "Software"), to deal |
|
|
|
* of this software and associated documentation files (the "Software"), to deal |
|
|
@ -367,14 +379,17 @@ |
|
|
|
-Let user supply full filename to MIDI file on command line to be friendlier |
|
|
|
-Let user supply full filename to MIDI file on command line to be friendlier |
|
|
|
to users using shell autocompletion. If a .mid or .MID file is provided, |
|
|
|
to users using shell autocompletion. If a .mid or .MID file is provided, |
|
|
|
the extension will be dropped to generate the base filename. |
|
|
|
the extension will be dropped to generate the base filename. |
|
|
|
|
|
|
|
22 April 2021, Len Shustek, V2.2 |
|
|
|
|
|
|
|
-Add -showskipped to log the places where notes had to be discarded because |
|
|
|
|
|
|
|
there aren't enough tone generators. |
|
|
|
|
|
|
|
|
|
|
|
future version ideas |
|
|
|
future version ideas |
|
|
|
|
|
|
|
|
|
|
|
-Perhaps elide "note off/note on" event sequences for the same note that |
|
|
|
-Perhaps elide "note off/note on" event sequences for the same note that |
|
|
|
become adjacent because of -delaymin. Does that happen much, or at all? |
|
|
|
become adjacent because of -delaymin. Does that happen much, or at all? |
|
|
|
|
|
|
|
|
|
|
|
-Allow the flexibility to specify note timing on a track-by-track or
|
|
|
|
-Allow the flexibility to specify note timing on a track-by-track or |
|
|
|
channel-by-channel basis, by using a <basefile>.cfg file which has
|
|
|
|
channel-by-channel basis, by using a <basefile>.cfg file which has |
|
|
|
commands like these: |
|
|
|
commands like these: |
|
|
|
options <global options> |
|
|
|
options <global options> |
|
|
|
track 1 // melody
|
|
|
|
track 1 // melody
|
|
|
@ -382,7 +397,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.1" |
|
|
|
#define VERSION "2.2" |
|
|
|
|
|
|
|
|
|
|
|
/*--------------------------------------------------------------------------------------------
|
|
|
|
/*--------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
@ -465,7 +480,7 @@ See https://stackoverflow.com/questions/1080297/how-does-midi-tempo-message-appl |
|
|
|
|
|
|
|
|
|
|
|
/*--------------- 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. |
|
|
|
|
|
|
|
|
|
|
|
noteinfo |
|
|
|
noteinfo |
|
|
@ -518,7 +533,7 @@ queue command |
|
|
|
output queue entries at the oldest time |
|
|
|
output queue entries at the oldest time |
|
|
|
static output time, time deficit |
|
|
|
static output time, time deficit |
|
|
|
if time has advanced |
|
|
|
if time has advanced |
|
|
|
output DELAY
|
|
|
|
output DELAY |
|
|
|
for all entries at the same oldest time |
|
|
|
for all entries at the same oldest time |
|
|
|
if STOP |
|
|
|
if STOP |
|
|
|
find tgen matching channel and note |
|
|
|
find tgen matching channel and note |
|
|
@ -571,7 +586,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; |
|
|
|
gen_restart, scorename, showskipped; |
|
|
|
FILE *infile, *outfile, *logfile; |
|
|
|
FILE *infile, *outfile, *logfile; |
|
|
|
uint8_t *buffer, *hdrptr; |
|
|
|
uint8_t *buffer, *hdrptr; |
|
|
|
unsigned long buflen; |
|
|
|
unsigned long buflen; |
|
|
@ -703,6 +718,7 @@ void SayUsage(char *programName) { |
|
|
|
" -r terminate output file with \"restart\" instead of \"stop\" command", |
|
|
|
" -r terminate output file with \"restart\" instead of \"stop\" command", |
|
|
|
" -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", |
|
|
|
" -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", |
|
|
@ -752,6 +768,7 @@ int HandleOptions (int argc, char *argv[]) { |
|
|
|
else if (opt_key(arg, "s1")) strategy1 = true; |
|
|
|
else if (opt_key(arg, "s1")) strategy1 = true; |
|
|
|
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_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; |
|
|
@ -898,8 +915,11 @@ struct channel_status { // current status of a channel |
|
|
|
char *describe(struct noteinfo *np) { // create a description of a note
|
|
|
|
char *describe(struct noteinfo *np) { // create a description of a note
|
|
|
|
// WARNING: returns a pointer to a static string, so only call once per line, in a printf!
|
|
|
|
// WARNING: returns a pointer to a static string, so only call once per line, in a printf!
|
|
|
|
static char notedescription[100]; |
|
|
|
static char notedescription[100]; |
|
|
|
sprintf(notedescription, "at %lu.%03lu msec, note %d (0x%02X) track %d channel %d volume %d instrument %d", |
|
|
|
int secs = np->time_usec / 1000000; |
|
|
|
np->time_usec / 1000, np->time_usec % 1000, np->note, np->note, |
|
|
|
sprintf(notedescription, "at %3lu.%06lu sec (%d:%02d), note %d (0x%02X) track %d channel %d volume %d instrument %d", |
|
|
|
|
|
|
|
secs, np->time_usec % 1000000, |
|
|
|
|
|
|
|
secs/60, secs % 60, |
|
|
|
|
|
|
|
np->note, np->note, |
|
|
|
np->track, np->channel, np->volume, np->instrument); |
|
|
|
np->track, np->channel, np->volume, np->instrument); |
|
|
|
return notedescription; } |
|
|
|
return notedescription; } |
|
|
|
|
|
|
|
|
|
|
@ -1001,7 +1021,7 @@ void remove_queue_entry(int ndx) { // remove the oldest queue entry |
|
|
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { |
|
|
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { |
|
|
|
struct tonegen_status *tg = &tonegen[tgnum]; |
|
|
|
struct tonegen_status *tg = &tonegen[tgnum]; |
|
|
|
if (tg->playing |
|
|
|
if (tg->playing |
|
|
|
&& tg->note.note == q->note.note
|
|
|
|
&& tg->note.note == q->note.note |
|
|
|
&& tg->note.channel == q->note.channel) { // found the note
|
|
|
|
&& tg->note.channel == q->note.channel) { // found the note
|
|
|
|
tg->stopnote_pending = true; // "stop note needed unless another start note follows"
|
|
|
|
tg->stopnote_pending = true; // "stop note needed unless another start note follows"
|
|
|
|
tg->playing = false; // free the tg to be reallocated, but note the stop time in case
|
|
|
|
tg->playing = false; // free the tg to be reallocated, but note the stop time in case
|
|
|
@ -1056,7 +1076,8 @@ void remove_queue_entry(int ndx) { // remove the oldest queue entry |
|
|
|
else { |
|
|
|
else { |
|
|
|
if (loggen) fprintf(logfile, " *** at %lu.%03lu msec no free generator; skipping %s\n", |
|
|
|
if (loggen) fprintf(logfile, " *** at %lu.%03lu msec no free generator; skipping %s\n", |
|
|
|
output_usec / 1000, output_usec % 1000, describe(&q->note)); |
|
|
|
output_usec / 1000, output_usec % 1000, describe(&q->note)); |
|
|
|
++notes_skipped; } } } |
|
|
|
if (showskipped) printf(" *** no free generator %s\n", |
|
|
|
|
|
|
|
describe(&q->note)); ++notes_skipped; } } } |
|
|
|
|
|
|
|
|
|
|
|
void generate_delay(unsigned long delta_msec) { // output a delay command
|
|
|
|
void generate_delay(unsigned long delta_msec) { // output a delay command
|
|
|
|
if (delta_msec > 0) { |
|
|
|
if (delta_msec > 0) { |
|
|
@ -1557,10 +1578,9 @@ int main (int argc, char *argv[]) { |
|
|
|
// strip off trailing .mid or .MID extension if provided by user
|
|
|
|
// strip off trailing .mid or .MID extension if provided by user
|
|
|
|
basenamelen = strlength(filebasename); |
|
|
|
basenamelen = strlength(filebasename); |
|
|
|
if (basenamelen > 4 && |
|
|
|
if (basenamelen > 4 && |
|
|
|
(charcmp (filebasename + basenamelen - 4, ".mid") || |
|
|
|
(charcmp (filebasename + basenamelen - 4, ".mid") || |
|
|
|
charcmp (filebasename + basenamelen - 4, ".MID"))) { |
|
|
|
charcmp (filebasename + basenamelen - 4, ".MID"))) { |
|
|
|
filebasename[basenamelen - 4] = 0; |
|
|
|
filebasename[basenamelen - 4] = 0; } |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (logparse || loggen) { // open the log file
|
|
|
|
if (logparse || loggen) { // open the log file
|
|
|
|
miditones_strlcpy (filename, filebasename, MAXPATH); |
|
|
|
miditones_strlcpy (filename, filebasename, MAXPATH); |
|
|
|