|
|
|
|
|
|
|
/*********************************************************************************************
|
|
|
|
|
|
|
|
MIDITONES: Convert a MIDI file into a simple bytestream of notes
|
|
|
|
|
|
|
|
|
|
|
|
MIDITONES compiles a MIDI music file into a much simplified compact time-ordered stream of
|
|
|
|
commands, so that the music can easily be played on a small microcontroller-based synthesizer
|
|
|
|
that has only simple tone generators. This is on github at www.github.com/LenShustek/miditones.
|
|
|
|
|
|
|
|
Volume ("velocity") and instrument information in the MIDI file can either be
|
|
|
|
discarded or kept. All the tracks are processed and merged into a single time-ordered
|
|
|
|
stream of "note on", "note off", "change instrument" and "delay" commands.
|
|
|
|
|
|
|
|
MIDITONES was written for the "Playtune" series of Arduino and Teensy
|
|
|
|
microcontroller software synthesizers:
|
|
|
|
|
|
|
|
www.github.com/LenShustek/arduino-playtune
|
|
|
|
This original version of Playtune, first released in 2011, uses a separate hardware timer
|
|
|
|
for each note to generate a square wave on an output pin. All the pins are then combined
|
|
|
|
with a simple resistor network connected to a speaker and/or amplifier. It can only play
|
|
|
|
as many simutaneous notes as there are timers. There is no volume modulation.
|
|
|
|
|
|
|
|
www.github.com/LenShustek/Playtune_poll
|
|
|
|
This second vesion uses only one hardware timer that interrupts periodically at a fast
|
|
|
|
rate, and toggles square waves onto any number of digital output pins. It also implements
|
|
|
|
primitive volume modulation by changing the duty cycle of the square wave. The number of
|
|
|
|
simultaneous notes is limited only by the number of output pins and the speed of the processor.
|
|
|
|
|
|
|
|
www.github.com/LenShustek/Playtune_samp
|
|
|
|
The third version also uses only one hardware timer interrupting frequently, but
|
|
|
|
uses the hardware digital-to-analog converter on high-performance microntrollers like
|
|
|
|
the Teensy to generate an analog wave that is the sum of stored samples of sounds for
|
|
|
|
many different instruments. The samples are scaled to the right frequency and volume,
|
|
|
|
and any number of instrument samples can be used and mapped to MIDI patches. The sound
|
|
|
|
quality is much better, although not in league with real synthesizers. It currently
|
|
|
|
only supports Teensy boards.
|
|
|
|
|
|
|
|
www.github.com/LenShustek/Playtune_synth
|
|
|
|
The fourth version is an audio object for the PJRC Audio Library.
|
|
|
|
https://www.pjrc.com/teensy/td_libs_Audio.html
|
|
|
|
It allows up to 16 simultaneous sound generators that are internally mixed, at
|
|
|
|
the appropriate volume, to produce one monophonic audio stream.
|
|
|
|
Sounds are created from sampled one-cycle waveforms for any number of instruments,
|
|
|
|
each with its own attack-hold-decay-sustain-release envelope. Percussion sounds
|
|
|
|
(from MIDI channel 10) are generated from longer sampled waveforms of a complete
|
|
|
|
instrument strike. Each generator's volume is independently adjusted according to
|
|
|
|
the MIDI velocity of the note being played before all channels are mixed.
|
|
|
|
|
|
|
|
www.github.com/LenShustek/Playtune_Teensy
|
|
|
|
The fifth version is for the Teensy 3.1/3.2, and uses the four Periodic Interval
|
|
|
|
Timers in the Cortex M4 processor to support up to 4 simultaneous notes.
|
|
|
|
It uses less CPU time than the polling version, but is limited to 4 notes at a time.
|
|
|
|
(This was written to experiment with multi-channel multi-Tesla Coil music playing,
|
|
|
|
where I use Flexible Timer Module FTM0 for generating precise one-shot pulses.
|
|
|
|
But I ultimately switched to the polling version to play more simultaneous notes.)
|
|
|
|
|
|
|
|
www.github.com/LenShustek/ATtiny-playtune
|
|
|
|
This is a much simplified version that fits, with a small song, into an ATtiny
|
|
|
|
processor with only 4K of flash memory. It also using polling with only one timer,
|
|
|
|
and avoids multiplication or division at runtime for speed. It was written
|
|
|
|
for the Evil Mad Scientist menorah kit:
|
|
|
|
https://www.evilmadscientist.com/2009/new-led-hanukkah-menorah-kit/
|
|
|
|
https://forum.evilmadscientist.com/discussion/263/making-the-menorah-play-music
|
|
|
|
(Imagine what you can do with the $1 8-pin ATtiny85 with a whopping 8K!)
|
|
|
|
|
|
|
|
MIDITONES may also prove useful for other simple music synthesizers. There are
|
|
|
|
various forks of this code, and of the Playtune players, on Githib.
|
|
|
|
|
|
|
|
*** THE PROGRAM
|
|
|
|
|
|
|
|
MIDITONES is written in standard ANSI C and is meant to be executed from the
|
|
|
|
command line. There is no GUI interface.
|
|
|
|
|
|
|
|
The output can be either a C-language source code fragment that initializes an
|
|
|
|
array with the command bytestream, or a binary file with the bytestream itself.
|
|
|
|
|
|
|
|
The MIDI file format is complicated, and this has not been tested on all of its
|
|
|
|
variations. In particular we have tested only format type "1", which seems
|
|
|
|
to be what most of them are. Let me know if you find MIDI files that it
|
|
|
|
won't digest and I'll see if I can fix it.
|
|
|
|
|
|
|
|
There is a companion program in the same repository called Miditones_scroll
|
|
|
|
that can convert the bytestream generated by MIDITONES into a piano-player
|
|
|
|
like listing for debugging or annotation. See the documentation near the
|
|
|
|
top of its source code.
|
|
|
|
|
|
|
|
|
|
|
|
*** THE COMMAND LINE
|
|
|
|
|
|
|
|
To convert a MIDI file called "chopin.mid" into a command bytestream, execute
|
|
|
|
|
|
|
|
miditones chopin
|
|
|
|
|
|
|
|
It will create a file in the same directory called "chopin.c" which contains
|
|
|
|
the C-language statement to intiialize an array called "score" with the bytestream.
|
|
|
|
|
|
|
|
|
|
|
|
The general form for command line execution is this:
|
|
|
|
|
|
|
|
miditones <options> <basefilename>
|
|
|
|
|
|
|
|
The <basefilename> is the base name, without an extension, for the input and
|
|
|
|
output files. It can contain directory path information, or not.
|
|
|
|
|
|
|
|
If the user specifies the full .mid filename, the .mid or .MID extension
|
|
|
|
will be dropped and the remaining name will be used as <basefilename>.
|
|
|
|
|
|
|
|
The input file is <basefilename>.mid, and the output filename(s)
|
|
|
|
are the base file name with .c, .h, .bin, and/or .log extensions.
|
|
|
|
|
|
|
|
|
|
|
|
The following commonly-used command-line options can be specified:
|
|
|
|
|
|
|
|
-v Add velocity (volume) information to the output bytestream
|
|
|
|
|
|
|
|
-i Add instrument change commands to the output bytestream
|
|
|
|
|
|
|
|
-pt Translate notes in the MIDI percussion track to note numbers 128..255
|
|
|
|
and assign them to a tone generator as usual.
|
|
|
|
|
|
|
|
-d Generate a self-describing file header that says which optional bytestream
|
|
|
|
fields are present. This is highly recommended if you are using later
|
|
|
|
Playtune players that can check the header to know what data to expect.
|
|
|
|
|
|
|
|
-b Generate a binary file with the name <basefilename>.bin, instead of a
|
|
|
|
C-language source file with the name <basefilename>.c.
|
|
|
|
|
|
|
|
-t=n Generate the bytestream so that at most "n" tone generators are used.
|
|
|
|
The default is 6 tone generators, and the maximum is 16. The program
|
|
|
|
will report how many notes had to be discarded because there weren't
|
|
|
|
enough tone generators.
|
|
|
|
|
|
|
|
The best combination of options to use with the later Playtune music players is:
|
|
|
|
-v -i -pt -d
|
|
|
|
|
|
|
|
The following are lesser-used command-line options:
|
|
|
|
|
|
|
|
-c=n Only process the channel numbers whose bits are on in the number "n".
|
|
|
|
For example, -c3 means "only process channels 0 and 1". In addition to
|
|
|
|
decimal, "n" can be also specified in hex using a 0x prefix.
|
|
|
|
|
|
|
|
-dp Generate Arduino IDE-dependent C code that uses PROGMEM for the bytestream.
|
|
|
|
|
|
|
|
-k=n Change the musical key of the output by n chromatic notes.
|
|
|
|
-k=-12 goes one octave down, -k=12 goes one octave up, etc.
|
|
|
|
|
|
|
|
-lp Log input file parsing information to the <basefilename>.log file
|
|
|
|
|
|
|
|
-lg Log output bytestream generation information to the <basefilename>.log file
|
|
|
|
|
|
|
|
-n=x Put about "x" items on each line of the C file output
|
|
|
|
|
|
|
|
-p Only parse the MIDI file, and don't generate an output file.
|
|
|
|
Tracks are processed sequentially instead of being merged into chronological
|
|
|
|
order. This is mostly useful for debugging MIDI file parsing problems.
|
|
|
|
|
|
|
|
-pi Ignore notes in the MIDI percussion track 9 (also called 10 in some documents)
|
|
|
|
|
|
|
|
-r Terminate the output file with a "restart" command instead of a "stop" command.
|
|
|
|
|
|
|
|
-sn Use bytestream generation strategy "n". Two are currently implemented:
|
|
|
|
1:favor track 1 notes instead of all tracks equally
|
|
|
|
2:try to keep each track to its own tone generator
|
|
|
|
|
|
|
|
-h Give command-line help.
|
|
|
|
|
|
|
|
-showskipped Display information to the console about each note that had to be
|
|
|
|
skipped because there weren't enough tone generators.
|
|
|
|
|
|
|
|
-noduplicates Remove identical notes played on identical instruments for the same time
|
|
|
|
that come from different tracks. This can reduce the number of tone
|
|
|
|
generators needed, and make the file smaller.
|
|
|
|
|
|
|
|
-delaymin=x Don't generate delays less than x milliseconds long, to reduce the number
|
|
|
|
of "delay" commands and thus make the bytestream smaller, at the expense of
|
|
|
|
moving notes slightly. The deficits are accumulated and eventually used,
|
|
|
|
so that there is no loss of synchronization in the long term.
|
|
|
|
The default is 0, which means note timing is exact to the millisecond.
|
|
|
|
|
|
|
|
-releasetime=x Stop each note x milliseconds before it is supposed to end. This results
|
|
|
|
in better sound separation between notes. It might also allow more notes to
|
|
|
|
be played with fewer tone generators, since there could be fewer
|
|
|
|
simultaneous notes playing.
|
|
|
|
|
|
|
|
-notemin=x The releasetime notwithstanding, don't let the resulting note be reduced
|
|
|
|
to smaller than x milliseconds. Making releasetime very large and notemin
|
|
|
|
small results in staccato sounds.
|
|
|
|
|
|
|
|
-attacktime=x The high-volume attack phase of a note lasts x milliseconds, after which
|
|
|
|
the lower-volume sustain phase begins, unless the release time makes it
|
|
|
|
too short. (Only valid with -v.)
|
|
|
|
|
|
|
|
-attacknotemax=x Notes larger than x milliseconds won't have the attack/sustain profile
|
|
|
|
applied. That allows sustained organ-like pedaling. (Only valid with -v.)
|
|
|
|
|
|
|
|
-sustainlevel=p The volume level during the sustain phase is p percent of the starting
|
|
|
|
note volume. The default is 50. (Only valid with -v.)
|
|
|
|
|
|
|
|
-scorename Use <basefilename> as the name of the score in the generated C code
|
|
|
|
instead of "score", and name the file <basefilename>.h instead of
|
|
|
|
<basefilenam>.c. That allows multiple scores to be directly #included
|
|
|
|
into an Arduino .ino file without modification.
|
|
|
|
|
|
|
|
Note that for backwards compatibility and easier batch-file processing, the equal sign
|
|
|
|
for specifying an option's numeric value may be omitted. Also, numeric values may be
|
|
|
|
specified in hex as 0xhhhh.
|
|
|
|
|
|
|
|
*** THE SCORE BYTESTREAM
|
|
|
|
|
|
|
|
The generated bytestream is a series of commands that turn notes on and off,
|
|
|
|
change instruments, and request a delay until the next event time.
|
|
|
|
Here are the details, with numbers shown in hexadecimal.
|
|
|
|
|
|
|
|
If the high-order bit of the byte is 1, then it is one of the following commands,
|
|
|
|
where the two characters are a hex representation of one byte:
|
|
|
|
|
|
|
|
9t nn [vv]
|
|
|
|
Start playing note nn on tone generator t, replacing any previous note.
|
|
|
|
Generators are numbered starting with 0. The note numbers are the MIDI
|
|
|
|
numbers for the chromatic scale, with decimal 69 being Middle A (440 Hz).
|
|
|
|
If the -v option was given, the third byte specifies the note volume.
|
|
|
|
|
|
|
|
8t Stop playing the note on tone generator t.
|
|
|
|
|
|
|
|
Ct ii Change tone generator t to play instrument ii from now on. This will
|
|
|
|
only be generated if the -i option was given.
|
|
|
|
|
|
|
|
F0 End of score; stop playing.
|
|
|
|
|
|
|
|
E0 End of score, but start playing again from the beginning. This is
|
|
|
|
generated by the -r option.
|
|
|
|
|
|
|
|
If the high-order bit of the byte is 0, it is a command to delay for a while until
|
|
|
|
the next note change. The other 7 bits and the 8 bits of the following byte are
|
|
|
|
interpreted as a 15-bit big-endian integer that is the number of milliseconds to
|
|
|
|
wait before processing the next command. For example,
|
|
|
|
|
|
|
|
07 D0
|
|
|
|
|
|
|
|
would cause a delay of 0x07d0 = 2000 decimal millisconds, or 2 seconds. Any tones
|
|
|
|
that were playing before the delay command will continue to play.
|
|
|
|
|
|
|
|
If the -d option is specified, the bytestream begins with a little header that tells
|
|
|
|
what optional information will be in the data. This makes the file more self-describing,
|
|
|
|
and allows music players to adapt to different kinds of files. The later Playtune
|
|
|
|
players do that. The header looks like this:
|
|
|
|
|
|
|
|
'Pt' Two ascii characters that signal the presence of the header
|
|
|
|
nn The length (in one byte) of the entire header, 6..255
|
|
|
|
ff1 A byte of flag bits, three of which are currently defined:
|
|
|
|
80 volume information is present
|
|
|
|
40 instrument change information is present
|
|
|
|
20 translated percussion notes are present
|
|
|
|
ff2 Another byte of flags, currently undefined
|
|
|
|
tt The number (in one byte) of tone generators actually used in this music.
|
|
|
|
|
|
|
|
Any subsequent header bytes included in the length are currently undefined
|
|
|
|
and should be ignored by players.
|
|
|
|
|
|
|
|
Len Shustek, 2011 to 2021; see the change log.
|
|
|
|
|
|
|
|
*----------------------------------------------------------------------------------------
|
|
|
|
* The MIT License (MIT)
|
|
|
|
* Copyright (c) 2011,2013,2015,2016,2019,2021 Len Shustek
|
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
|
|
* IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
*********************************************************************************************/
|
|
|
|
// formatted with: Astyle -style=lisp -indent=spaces=3 -mode=c
|
|
|
|
|
|
|
|
/* Change log
|
|
|
|
|
|
|
|
19 January 2011, L.Shustek, V1.0
|
|
|
|
-Initial release.
|
|
|
|
26 February 2011, L. Shustek, V1.1
|
|
|
|
-Expand the documentation generated in the output file.
|
|
|
|
-End the binary output file with an "end of score" command.
|
|
|
|
-Fix bug: Some "stop note" commands were generated too early.
|
|
|
|
04 March 2011, L. Shustek, V1.2
|
|
|
|
-Minor error message rewording.
|
|
|
|
13 June 2011, L. Shustek, V1.3
|
|
|
|
-Add -s2 strategy to try to keep each track on its own tone generator
|
|
|
|
for when there are separate speakers. This obviously works only when
|
|
|
|
each track is monophonic. (Suggested by Michal Pustejovsky)
|
|
|
|
20 November 2011, L. Shustek, V1.4
|
|
|
|
-Add -cn option to mask which channels (tracks) to process
|
|
|
|
-Add -kn option to change key
|
|
|
|
Both of these are in support of music-playing on my Tesla Coil.
|
|
|
|
05 December 2011, L. Shustek, V1.5
|
|
|
|
-Fix command line parsing error for option -s1
|
|
|
|
-Display the commandline in the C file output
|
|
|
|
-Change to decimal instead of hex for note numbers in the C file output
|
|
|
|
06 August 2013, L. Shustek, V1.6
|
|
|
|
-Changed to allow compilation and execution in 64-bit environments
|
|
|
|
by using C99 standard intN_t and uintN_t types for MIDI structures,
|
|
|
|
and formatting specifications like "PRId32" instead of "ld".
|
|
|
|
04 April 2015, L. Shustek, V1.7
|
|
|
|
-Made friendlier to other compilers: import source of strlcpy and strlcat,
|
|
|
|
fixed various type mismatches that the LCC compiler didn't fret about.
|
|
|
|
Generate "const" for data initialization for compatibility with Arduino IDE v1.6.x.
|
|
|
|
23 January 2016, D. Blackketter, V1.8
|
|
|
|
-Fix warnings and errors building on Mac OS X via "gcc miditones.c"
|
|
|
|
25 January 2016, D. Blackketter, Paul Stoffregen, V1.9
|
|
|
|
-Merge in velocity output option from Arduino/Teensy Audio Library
|
|
|
|
26 June 2016, L. Shustek, V1.10
|
|
|
|
-Fix overflow problem in calculating long delays. (Thanks go to Tiago Rocha.)
|
|
|
|
In the process I discover and work around an LCC 32-bit compiler bug.
|
|
|
|
14 August 2016: L. Shustek, V1.11
|
|
|
|
-Fix our interpretation of MIDI "running status": it applies only to MIDI events
|
|
|
|
(8x through Ex), not, as we thought, also to Sysex (Fx) or Meta (FF) events.
|
|
|
|
-Improve parsing of text events for the log.
|
|
|
|
-Change log file note and patch numbers, etc., to decimal.
|
|
|
|
-Document a summary of the MIDI file format so I don't have to keep looking it up.
|
|
|
|
-Add -pi and -pt options to ignore or translate the MIDI percussion track 9.
|
|
|
|
-Remove string.h for more portability; add strlength().
|
|
|
|
-Add -i option for recording instrument types in the bytestream.
|
|
|
|
-Add -d option for generating a file description header.
|
|
|
|
-Add -dp option to make generating the PROGMEM definition optional
|
|
|
|
-Add -n option to specify number of items per output line
|
|
|
|
-Do better error checking on options
|
|
|
|
-Reformat option help
|
|
|
|
26 September 2016, Scott Allen, V1.12
|
|
|
|
-Fix spelling and minor formatting errors
|
|
|
|
-Fix -p option parsing and handling, which broke when -pi and -pt were added
|
|
|
|
-Fix handling of the -nx option to count more accurately
|
|
|
|
-Give a proper error message for missing base name
|
|
|
|
-Include the header and terminator in the score byte count
|
|
|
|
30 September 2016, Scott Allen, V1.13
|
|
|
|
-Allow -c channel numbers to be specified as hex or octal
|
|
|
|
-Add -r to end the file with "repeat" instead of "score end"
|
|
|
|
30 September 2016, Len Shustek, V1.14
|
|
|
|
-Prevent unnecessary "note off" commands from being generated by delaying
|
|
|
|
them until we see if a "note on" is generated before the next wait.
|
|
|
|
Thanks to Scott Allen for inspiring me to do this. In the best case we've
|
|
|
|
seen, this makes the bytestream 21% smaller!
|
|
|
|
13 November 2017, Earle Philhower, V1.15
|
|
|
|
-Allow META fields to be larger than 127 bytes.
|
|
|
|
2 January 2018, Kodest, V1
|
|
|
|
-Don't generate zero-length delays
|
|
|
|
13 September 2018, Paul Stoffregen, V1.17
|
|
|
|
-Fix compile errors on Linux with gcc run in default mode
|
|
|
|
1 January 2019, Len Shustek, V1.18
|
|
|
|
-Fix the bug found by Chris van Marle (thanks!) that caused delays not to be
|
|
|
|
generated until the tempo was set. (The default is 500,000 usec/beat, not 0.)
|
|
|
|
-Abandon LCC and compile under Microsoft Visual Studio 2017.
|
|
|
|
-Reformat to condense the source code, so you see more protein and less
|
|
|
|
syntactic sugar on each screen.
|
|
|
|
4 January 2019, Len Shustek, V1.19
|
|
|
|
-As suggested by Chris van Marle, add the "-mx" parameter to allow timing to be
|
|
|
|
flexible in order to avoid small delays and thus save space in the bytestream.
|
|
|
|
-Don't discard fractions of a millisecond in the delay timing, to avoid gradual
|
|
|
|
drift of the music. This has been a minor problem since V1.0 in 2011.
|
|
|
|
4 January 2019, Len Shustek, V2.0
|
|
|
|
-Major revision: completely rewrite score processing to allow out-of-order event
|
|
|
|
queuing. That lets us implement "release" time that ends notes early, and
|
|
|
|
"sustain" time at reduced volume, if we are encoding volume. You can sometimes
|
|
|
|
take advantage of release time to play more notes with the same number of tone
|
|
|
|
generators. It also can improve the sounds for repeated notes, although it
|
|
|
|
might be at the expense of increased bytestream size.
|
|
|
|
-Change the treatment of tracks and channels to more faithfully reproduce the
|
|
|
|
synthesizer model: each channel is an instrument, and can play multiple notes,
|
|
|
|
but only one at each frequency. It doesn't matter which tracks they come from.
|
|
|
|
-Simplify and generalize option parsing, and rename some of the newer ones.
|
|
|
|
-Add -scorename so multiple scores can be directly #included into .ino files
|
|
|
|
without manually editing the names.
|
|
|
|
17 October 2020, Ben Combee, V2.1
|
|
|
|
-Let user supply full filename to MIDI file on command line to be friendlier
|
|
|
|
to users using shell autocompletion. If a .mid or .MID file is provided,
|
|
|
|
the extension will be dropped to generate the base filename.
|
|
|
|
22 April 2021, Len Shustek, V2.2
|
|
|
|
-Add -showskipped to log the places where notes had to be discarded because
|
|
|
|
there aren't enough tone generators.
|
|
|
|
25 April 2021, Len Shustek, V2.3
|
|
|
|
-Report how many notes were generated
|
|
|
|
5 May 2021, Len Shustek, V2.4
|
|
|
|
-Fix bug: when finding an idle tone generator, makes sure that the track and
|
|
|
|
instrument of the currently playing note matches before deciding it's a sustain,
|
|
|
|
otherwise multiple identical notes on identical instruments will disappear.
|
|
|
|
(Thanks to Jonathan Oakley for providing an example of the problem.)
|
|
|
|
-But sometimes, to reduce the number of tone generators, it's helpful to eliminate
|
|
|
|
identical notes. So add a -noduplicates option to do just that.
|
|
|
|
|
|
|
|
future version ideas
|
|
|
|
|
|
|
|
-Perhaps elide "note off/note on" event sequences for the same note that
|
|
|
|
become adjacent because of -delaymin. Does that happen much, or at all?
|
|
|
|
|
|
|
|
-Allow the flexibility to specify note timing on a track-by-track or
|
|
|
|
channel-by-channel basis, by using a <basefile>.cfg file which has
|
|
|
|
commands like these:
|
|
|
|
options <global options>
|
|
|
|
track 1 // melody
|
|
|
|
options -attacktime=100 -sustainlevel=50% -releasetime=10000 -notemin=200
|
|
|
|
channel 8 // organ
|
|
|
|
options -attacktime=1000 -sustainlevel=80% -releasetime=100 -notemin=200
|
|
|
|
*/
|
|
|
|
#define VERSION "2.4"
|
|
|
|
|
|
|
|
/*--------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
A CONCISE SUMMARY OF MIDI FILE FORMAT
|
|
|
|
|
|
|
|
L. Shustek, 16 July 2016.
|
|
|
|
Gleaned from http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html
|
|
|
|
but also check out http://midi.teragonaudio.com/tech/miditech.htm
|
|
|
|
|
|
|
|
Notation:
|
|
|
|
<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
|
|
|
|
|
|
|
|
a MIDI file is:
|
|
|
|
header_chunk {track_chunk}...
|
|
|
|
|
|
|
|
a header_chunk is:
|
|
|
|
"MThd" 00000006 ffff nnnn dddd
|
|
|
|
00000006 is the number of bytes in the rest of the header
|
|
|
|
ffff is the format type (we have only seen type 1)
|
|
|
|
nnnn is the number of tracks
|
|
|
|
dddd is the number of ticks per beat (ie, per quarter note)
|
|
|
|
(it is often 480 or 240)
|
|
|
|
|
|
|
|
a track_chunk is:
|
|
|
|
"MTrk" llllllll {<deltatime> track_event}...
|
|
|
|
llllllll is the length of the track data, in bytes
|
|
|
|
<deltatime> is the number of ticks to delay before the track_event
|
|
|
|
|
|
|
|
a running status track_event is:
|
|
|
|
0x to 7x: assume a missing 8n to En event code which is the same as the last MIDI-event track_event
|
|
|
|
|
|
|
|
a MIDI-event track_event is:
|
|
|
|
8n 0kk 0vv note off, channel n, note kk, velocity vv
|
|
|
|
9n 0kk 0vv note on, channel n, note kk, velocity vv
|
|
|
|
An 0kk 0vv key pressure, channel n, note kk, pressure vv
|
|
|
|
Bn 0cc 0vv control value change, channel n, controller cc, new value vv
|
|
|
|
Cn 0pp program patch (instrument) change, channel n, new program pp
|
|
|
|
Dn 0vv channel pressure, channel n, pressure vv
|
|
|
|
En 0ll 0mm pitch wheel change, value llmm
|
|
|
|
|
|
|
|
Note that channel 9 (called 10 by some programs) is used for percussion, particularly notes 35 to 81.
|
|
|
|
|
|
|
|
a Sysex event track_event is:
|
|
|
|
F0 0ii {0dd}... F7 system-dependent data for manufacture ii. See www.gweep.net/~prefect/eng/reference/protocol/midispec.html
|
|
|
|
F2 0ll 0mm song position pointer
|
|
|
|
F3 0ss song select
|
|
|
|
F6 tune request
|
|
|
|
F7 end of system-dependent data
|
|
|
|
F8 timing clock sync
|
|
|
|
FA start playing
|
|
|
|
FB continue playing
|
|
|
|
FC stop playing
|
|
|
|
FE active sensing (hearbeat)
|
|
|
|
|
|
|
|
a meta event track_event is:
|
|
|
|
FF 00 02 ssss specify sequence number
|
|
|
|
FF 01 <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 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. That isn't always true, however,
|
|
|
|
See https://stackoverflow.com/questions/1080297/how-does-midi-tempo-message-apply-to-other-tracks
|
|
|
|
--------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
/*--------------- processing outline -----------------------------------
|
|
|
|
|
|
|
|
Lots of details are omitted. Note that MIDI track parsing is based
|
|
|
|
on counting "ticks", but our queueing is based on real-time seconds.
|
|
|
|
The number of ticks per second changes with the tempo.
|
|
|
|
|
|
|
|
noteinfo
|
|
|
|
time (of start or end), track, channel, note, instrument, volume
|
|
|
|
track status
|
|
|
|
time in ticks, cmd, chan, note, volume
|
|
|
|
tgen status
|
|
|
|
playing? stopnote_pending?
|
|
|
|
noteinfo
|
|
|
|
channel status
|
|
|
|
instrument, {note_playing?, noteinfo}[slots]
|
|
|
|
queue entry:
|
|
|
|
{PLAY|STOP}, noteinfo
|
|
|
|
|
|
|
|
process track data
|
|
|
|
forall trks: find next note
|
|
|
|
do // whole song
|
|
|
|
earliest_track time in ticks = min(trk->time)
|
|
|
|
accumuulate running absolute time (for queuing) based on the current tempo
|
|
|
|
if CMD_TEMPO,
|
|
|
|
set global tempo
|
|
|
|
find next note
|
|
|
|
else if CMD_PLAYNOTE
|
|
|
|
find a !note_playing[] channel slot to use
|
|
|
|
queue CMD_PLAYNOTE at time, noteinfo
|
|
|
|
find next note
|
|
|
|
else if CMD_STOPNOTE
|
|
|
|
find the note's slot among the channel's notes_playing
|
|
|
|
compute Sustain and Release times based on ADSR profile and note length
|
|
|
|
if(Sustain) queue CMD_PLAYNOTE at Sustain time, noteinfo with adjusted volume
|
|
|
|
queue CMD_STOPNOTE at Release time (or now), noteinfo
|
|
|
|
remove from channel's notes_playing
|
|
|
|
find next note
|
|
|
|
while not all CMD_TRACKDONE
|
|
|
|
|
|
|
|
find next note
|
|
|
|
do forever
|
|
|
|
t->time += varlen
|
|
|
|
if "note off", CMD_STOPNOTE, return
|
|
|
|
if "note on", CMD_PLAYNOTE, return
|
|
|
|
if "tempo", CMD_TEMPO, return
|
|
|
|
if "program patch", change channel's instrument
|
|
|
|
else log a comment about the MIDI event
|
|
|
|
if end of track, CMD_TRACKDONE, return
|
|
|
|
|
|
|
|
queue command
|
|
|
|
if queue has no room, output queue entries
|
|
|
|
insertion-sort the new item into the queue based on the time
|
|
|
|
|
|
|
|
output queue entries at the oldest time
|
|
|
|
static output time, time deficit
|
|
|
|
if time has advanced
|
|
|
|
output DELAY
|
|
|
|
for all entries at the same oldest time
|
|
|
|
if STOP
|
|
|
|
find tgen matching channel and note
|
|
|
|
tgen: stopnote pending, not playing
|
|
|
|
else PLAY
|
|
|
|
find a tgen not playing (best: same, good: same instrument, ok: any free)
|
|
|
|
tgen "not stopnote pending", "playing"
|
|
|
|
if instrument change, output "set instrument"
|
|
|
|
output PLAY tgen
|
|
|
|
remove from queue
|
|
|
|
for all tgen
|
|
|
|
if stopnote pending
|
|
|
|
output STOP tgen
|
|
|
|
tgen: stopnote not pending
|
|
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <ctype.h>
|
|
|
|
#include <stdbool.h>
|
|
|
|
#include <time.h>
|
|
|
|
#include <inttypes.h>
|
|
|
|
#include <limits.h>
|
|
|
|
typedef unsigned char byte;
|
|
|
|
typedef uint32_t timestamp; // see note about this in the queuing routines
|
|
|
|
|
|
|
|
/*********** MIDI file header formats *****************/
|
|
|
|
|
|
|
|
struct midi_header {
|
|
|
|
int8_t MThd[4];
|
|
|
|
uint32_t header_size;
|
|
|
|
uint16_t format_type;
|
|
|
|
uint16_t number_of_tracks;
|
|
|
|
uint16_t time_division; };
|
|
|
|
|
|
|
|
struct track_header {
|
|
|
|
int8_t MTrk[4];
|
|
|
|
uint32_t track_size; };
|
|
|
|
|
|
|
|
/*********** Global variables ******************/
|
|
|
|
|
|
|
|
#define MAX_TONEGENS 16 // max tone generators: tones we can play simultaneously
|
|
|
|
#define DEFAULT_TONEGENS 6 // default number of tone generators
|
|
|
|
#define MAX_TRACKS 24 // max number of MIDI tracks we will process
|
|
|
|
#define PERCUSSION_TRACK 9 // the track MIDI uses for percussion sounds
|
|
|
|
#define NUM_CHANNELS 16 // MIDI-specified number of channels
|
|
|
|
#define MAX_CHANNELNOTES 16 // max number of notes playing simultaneously on a channel
|
|
|
|
#define DEFAULT_TEMPO 500000L // the MIDI-specified default tempo in usec/beat
|
|
|
|
#define DEFAULT_BEATTIME 240 // the MIDI-specified default ticks per beat
|
|
|
|
|
|
|
|
bool loggen, logparse, parseonly, strategy1, strategy2, binaryoutput, define_progmem,
|
|
|
|
volume_output, instrumentoutput, percussion_ignore, percussion_translate, do_header,
|
|
|
|
gen_restart, scorename, showskipped, noduplicates;
|
|
|
|
FILE *infile, *outfile, *logfile;
|
|
|
|
uint8_t *buffer, *hdrptr;
|
|
|
|
unsigned long buflen;
|
|
|
|
int num_tracks;
|
|
|
|
int tracks_done = 0;
|
|
|
|
int outfile_maxitems = 26;
|
|
|
|
int outfile_itemcount = 0;
|
|
|
|
int num_tonegens = DEFAULT_TONEGENS;
|
|
|
|
int num_tonegens_used = 0;
|
|
|
|
int instrument_changes = 0;
|
|
|
|
int note_on_commands = 0;
|
|
|
|
int notes_skipped = 0;
|
|
|
|
int events_delayed = 0;
|
|
|
|
int stopnotes_without_playnotes = 0;
|
|
|
|
int playnotes_without_stopnotes = 0;
|
|
|
|
int sustainphases_skipped = 0;
|
|
|
|
int sustainphases_done = 0;
|
|
|
|
int consecutive_delays = 0;
|
|
|
|
bool last_output_was_delay = false;
|
|
|
|
int noteinfo_overflow = 0, noteinfo_notfound = 0;
|
|
|
|
unsigned channel_mask = 0xffff; // bit mask of channels to process
|
|
|
|
int keyshift = 0; // optional chromatic note shift for output file
|
|
|
|
unsigned long delaymin_usec = 0; // events this close get merged together to save bytestream space
|
|
|
|
unsigned long releasetime_usec = 0; // release time in usec for silence at the end of notes
|
|
|
|
unsigned long notemin_usec = 250; // minimum note time in usec after the release is deducted
|
|
|
|
unsigned long attacktime_usec = 0; // the high volume attack phase lasts this time, if not 0 (only for -v)
|
|
|
|
unsigned long attacknotemax_usec = ULONG_MAX; // the longest note to which the attack/sustain profile is used (only for -v)
|
|
|
|
int sustainlevel_pct = 50; // the percent of attack volume for the sustain phase (only for -v)
|
|
|
|
long int outfile_bytecount = 0;
|
|
|
|
unsigned int ticks_per_beat = DEFAULT_BEATTIME;
|
|
|
|
|
|
|
|
unsigned long timenow_ticks = 0; // the current processing time in ticks
|
|
|
|
timestamp timenow_usec = 0; // the current processing time in usec
|
|
|
|
unsigned long timenow_usec_updated = 0; // when, in ticks, we last updated timenow_usec using the current tempo
|
|
|
|
|
|
|
|
timestamp output_usec = 0; // the time we last output, in usec
|
|
|
|
unsigned int output_deficit_usec = 0; // the leftover usec < 1000 still to be used for a "delay"
|
|
|
|
unsigned long tempo; // current global tempo in usec/beat
|
|
|
|
|
|
|
|
int tempo_changes = 0; // how many times we changed the global tempo
|
|
|
|
long int delays_saved = 0; // how many delays were saved because of non-zero merge time
|
|
|
|
|
|
|
|
/* output bytestream commands, which are also stored in track_status.cmd */
|
|
|
|
#define CMD_PLAYNOTE 0x90 /* play a note: low nibble is generator #, note is next byte */
|
|
|
|
#define CMD_STOPNOTE 0x80 /* stop a note: low nibble is generator # */
|
|
|
|
#define CMD_INSTRUMENT 0xc0 /* change instrument; low nibble is generator #, instrument is next byte */
|
|
|
|
#define CMD_RESTART 0xe0 /* restart the score from the beginning */
|
|
|
|
#define CMD_STOP 0xf0 /* stop playing */
|
|
|
|
/* the following other commands are stored in the track_status.com */
|
|
|
|
#define CMD_TEMPO 0xFE /* tempo in usec per quarter note ("beat") */
|
|
|
|
#define CMD_TRACKDONE 0xFF /* no more data left in this track */
|
|
|
|
|
|
|
|
|
|
|
|
struct file_hdr_t { // what our optional file header looks like
|
|
|
|
char id1; // 'P'
|
|
|
|
char id2; // 't'
|
|
|
|
byte hdr_length; // length of whole file header
|
|
|
|
byte f1; // flag byte 1
|
|
|
|
#define HDR_F1_VOLUME_PRESENT 0x80
|
|
|
|
#define HDR_F1_INSTRUMENTS_PRESENT 0x40
|
|
|
|
#define HDR_F1_PERCUSSION_PRESENT 0x20
|
|
|
|
byte f2; // flag byte 2
|
|
|
|
byte num_tgens; // how many tone generators are used by this score
|
|
|
|
} file_header = {
|
|
|
|
'P', 't', sizeof (struct file_hdr_t), 0, 0, MAX_TONEGENS };
|
|
|
|
|
|
|
|
long int file_header_num_tgens_position;
|
|
|
|
|
|
|
|
/************** command-line processing *******************/
|
|
|
|
|
|
|
|
void check_option(bool condition, char *msg) {
|
|
|
|
if (!condition) {
|
|
|
|
fprintf(stderr, "*** %s\n", msg);
|
|
|
|
exit(8); } }
|
|
|
|
|
|
|
|
bool opt_key(const char* arg, const char* keyword) {
|
|
|
|
do { // check for a keyword option and nothing after it
|
|
|
|
if (tolower(*arg++) != *keyword++) return false; }
|
|
|
|
while (*keyword);
|
|
|
|
return *arg == '\0'; }
|
|
|
|
|
|
|
|
bool opt_int(const char* arg, const char* keyword, int *pval, int min, int max) {
|
|
|
|
do { // check for a "keyword=integer" option and nothing after it
|
|
|
|
if (tolower(*arg++) != *keyword++)
|
|
|
|
return false; }
|
|
|
|
while (*keyword);
|
|
|
|
if (*arg == '=') ++arg; // = is optional, actually
|
|
|
|
int num, nch;
|
|
|
|
if (sscanf(arg, *arg == '0' && *(arg + 1) == 'x' ? "%x%n" : "%d%n", &num, &nch) != 1) return false;
|
|
|
|
if (num < min || num > max || arg[nch] != '\0') return false;
|
|
|
|
*pval = num;
|
|
|
|
return true; }
|
|
|
|
|
|
|
|
bool opt_str(const char* arg, const char* keyword, const char** str) {
|
|
|
|
do { // check for a "keyword=string" option
|
|
|
|
if (tolower(*arg++) != *keyword++) return false; }
|
|
|
|
while (*keyword);
|
|
|
|
*str = arg; // ptr to "string" part, which could be null
|
|
|
|
return true; }
|
|
|
|
|
|
|
|
void SayUsage(char *programName) {
|
|
|
|
static char *usage[] = {
|
|
|
|
"Convert MIDI files to an Arduino PLAYTUNE bytestream",
|
|
|
|
"",
|
|
|
|
"Use: miditones <options> <basefilename>",
|
|
|
|
" input file will be <basefilename>.mid",
|
|
|
|
" output file will be <basefilename>.bin or .c or .h",
|
|
|
|
" log file will be <basefilename>.log",
|
|
|
|
"",
|
|
|
|
"Commonly-used options:",
|
|
|
|
" -v include volume data",
|
|
|
|
" -i include instrument change commands",
|
|
|
|
" -pt translate notes in the percussion track to notes 129 to 255",
|
|
|
|
" -d include a self-describing file header",
|
|
|
|
" -b generate a binary file output instead of C source code",
|
|
|
|
" -t=n use at most n tone generators (default is 6, max is 16)",
|
|
|
|
"",
|
|
|
|
" The best options for later Playtune music players are: -v -i -pt -d",
|
|
|
|
"",
|
|
|
|
"Lesser-used command-line options:",
|
|
|
|
" -c=n mask for which tracks to process, e.g. -c3 for only 0 and 1",
|
|
|
|
" -dp define PROGMEM in output C code",
|
|
|
|
" -k=n key shift in chromatic notes, positive or negative",
|
|
|
|
" -lp log input parsing",
|
|
|
|
" -lg log output generation",
|
|
|
|
" -n=n put about n items on each line of the C file output",
|
|
|
|
" -p parse only, don't generate bytestream",
|
|
|
|
" -pi ignore notes in the percussion track, 9",
|
|
|
|
" -r terminate output file with \"restart\" instead of \"stop\" command",
|
|
|
|
" -s1 strategy 1: favor track 1",
|
|
|
|
" -s2 strategy 2: try to assign tracks to specific tone generators",
|
|
|
|
" -showskipped display information about each note that had to be skipped",
|
|
|
|
" -noduplicates remove identical notes playing on different channels",
|
|
|
|
" -delaymin=x minimum delay is x msec, to save bytestream space",
|
|
|
|
" -attacktime=x the high volume attack phase lasts x msec",
|
|
|
|
" -attacknotemax=x notes over x msec don't use the attack/sustain profile",
|
|
|
|
" -sustainlevel=p the sustain level is p percent of maximum volume",
|
|
|
|
" -releasetime=x release each note x msec before it ends",
|
|
|
|
" -notemin=x don't let release shorten the note to less than x msec",
|
|
|
|
" -scorename use <basefilename> as the score name in a .h file",
|
|
|
|
NULL };
|
|
|
|
for (int i=0; usage[i] != NULL; ++i)
|
|
|
|
fprintf(stderr, "%s\n", usage[i]); }
|
|
|
|
|
|
|
|
int HandleOptions (int argc, char *argv[]) {
|
|
|
|
/* returns the index of the first argument that is not an option,
|
|
|
|
i.e. does not start with a dash or a slash */
|
|
|
|
int i, firstnonoption = 0;
|
|
|
|
for (i = 1; i < argc; i++) {
|
|
|
|
if (argv[i][0] == '/' || argv[i][0] == '-') {
|
|
|
|
int tempint;
|
|
|
|
char *arg = argv[i] + 1;
|
|
|
|
if (opt_key(arg, "h") || opt_key(arg, "?")) {
|
|
|
|
SayUsage(argv[0]); exit(1); }
|
|
|
|
else if (opt_key(arg, "b")) binaryoutput = true;
|
|
|
|
else if (opt_int(arg, "c", &channel_mask, 1, 0xffff))
|
|
|
|
printf("Channel (track) mask is %04X\n", channel_mask);
|
|
|
|
else if (opt_key(arg, "d")) do_header = true;
|
|
|
|
else if (opt_key(arg, "dp")) define_progmem = true;
|
|
|
|
else if (opt_key(arg, "lg")) loggen = true;
|
|
|
|
else if (opt_key(arg, "i")) instrumentoutput = true;
|
|
|
|
else if (opt_int(arg, "k", &keyshift, -100, 100))
|
|
|
|
printf("Using keyshift %d\n", keyshift);
|
|
|
|
else if (opt_key(arg, "lp")) logparse = true;
|
|
|
|
else if (opt_int(arg, "n", &outfile_maxitems, 1, INT_MAX));
|
|
|
|
else if (opt_key(arg, "p")) parseonly = true;
|
|
|
|
else if (opt_key(arg, "pi")) percussion_ignore = true;
|
|
|
|
else if (opt_key(arg, "pt")) percussion_translate = true;
|
|
|
|
else if (opt_key(arg, "r")) gen_restart = true;
|
|
|
|
else if (opt_int(arg, "delaymin", &tempint, 1, 1000)) delaymin_usec = tempint * 1000;
|
|
|
|
else if (opt_int(arg, "releasetime", &tempint, 0, INT_MAX)) releasetime_usec = tempint * 1000;
|
|
|
|
else if (opt_int(arg, "notemin", &tempint, 0, INT_MAX)) notemin_usec = tempint * 1000;
|
|
|
|
else if (opt_int(arg, "attacktime", &tempint, 0, INT_MAX)) {
|
|
|
|
attacktime_usec = tempint * 1000;
|
|
|
|
check_option(volume_output, "-attacktime only works with -v"); }
|
|
|
|
else if (opt_int(arg, "attacknotemax", &tempint, 0, INT_MAX)) {
|
|
|
|
attacknotemax_usec = tempint * 1000;
|
|
|
|
check_option(volume_output, "-attacknotemax only works with -v"); }
|
|
|
|
else if (opt_int(arg, "sustainlevel", &sustainlevel_pct, 1, 100))
|
|
|
|
check_option(volume_output, "-sustainlevel only works with -v");
|
|
|
|
else if (opt_key(arg, "s1")) strategy1 = true;
|
|
|
|
else if (opt_key(arg, "s2")) strategy2 = true;
|
|
|
|
else if (opt_key(arg, "scorename")) scorename = true;
|
|
|
|
else if (opt_key(arg, "showskipped")) showskipped = true;
|
|
|
|
else if (opt_key(arg, "noduplicates")) noduplicates = true;
|
|
|
|
else if (opt_int(arg, "t", &num_tonegens, 1, MAX_TONEGENS))
|
|
|
|
printf("Using %d tone generators\n", num_tonegens);
|
|
|
|
else if (opt_key(arg, "v")) volume_output = true;
|
|
|
|
/* add more option switches here */
|
|
|
|
else {
|
|
|
|
fprintf(stderr, "\n*** bad option: %s\n\n", argv[i]);
|
|
|
|
SayUsage(argv[0]);
|
|
|
|
exit(4); } }
|
|
|
|
else {
|
|
|
|
firstnonoption = i;
|
|
|
|
break; } }
|
|
|
|
return firstnonoption; }
|
|
|
|
|
|
|
|
void print_command_line (FILE *file, int argc, char *argv[]) {
|
|
|
|
fprintf (file, "// command line: ");
|
|
|
|
for (int i = 0; i < argc; i++) fprintf (file, "%s ", argv[i]);
|
|
|
|
fprintf (file, "\n"); }
|
|
|
|
|
|
|
|
|
|
|
|
/**************** utility routines **********************/
|
|
|
|
|
|
|
|
void assert(bool condition, char *msg) {
|
|
|
|
if (!condition) {
|
|
|
|
fprintf(stderr, "*** internal assertion error: %s\n", msg);
|
|
|
|
if (logfile) fprintf(logfile, "*** internal assertion error: %s\n", msg);
|
|
|
|
exit(8); } }
|
|
|
|
|
|
|
|
/* announce a fatal MIDI file format error */
|
|
|
|
void midi_error(char *msg, byte *bufptr) {
|
|
|
|
fprintf(stderr, "---> MIDI file error at position %04X (%d): %s\n",
|
|
|
|
(uint16_t)(bufptr - buffer), (uint16_t)(bufptr - buffer), msg);
|
|
|
|
byte *ptr = bufptr - 16; // print some bytes surrounding the error
|
|
|
|
if (ptr < buffer) ptr = buffer;
|
|
|
|
for (; ptr <= bufptr + 16 && ptr < buffer + buflen; ++ptr)
|
|
|
|
fprintf(stderr, ptr == bufptr ? " [%02X] " : "%02X ", *ptr);
|
|
|
|
fprintf(stderr, "\n");
|
|
|
|
exit(8); }
|
|
|
|
|
|
|
|
/* portable string length */
|
|
|
|
int strlength (const char *str) {
|
|
|
|
int i;
|
|
|
|
for (i = 0; str[i] != '\0'; ++i);
|
|
|
|
return i; }
|
|
|
|
|
|
|
|
/* safe string copy */
|
|
|
|
size_t miditones_strlcpy (char *dst, const char *src, size_t siz) {
|
|
|
|
char *d = dst;
|
|
|
|
const char *s = src;
|
|
|
|
size_t n = siz;
|
|
|
|
if (n != 0) { /* Copy as many bytes as will fit */
|
|
|
|
while (--n != 0) {
|
|
|
|
if ((*d++ = *s++) == '\0') break; } }
|
|
|
|
if (n == 0) { /* Not enough room in dst, add NUL and traverse rest of src */
|
|
|
|
if (siz != 0) *d = '\0'; /* NUL-terminate dst */
|
|
|
|
while (*s++) ; }
|
|
|
|
return (s - src - 1); /* count does not include NUL */
|
|
|
|
}
|
|
|
|
|
|
|
|
/* safe string concatenation */
|
|
|
|
size_t miditones_strlcat (char *dst, const char *src, size_t siz) {
|
|
|
|
char *d = dst;
|
|
|
|
const char *s = src;
|
|
|
|
size_t n = siz;
|
|
|
|
size_t dlen;
|
|
|
|
/* Find the end of dst and adjust bytes left but don't go past end */
|
|
|
|
while (n-- != 0 && *d != '\0') d++;
|
|
|
|
dlen = d - dst;
|
|
|
|
n = siz - dlen;
|
|
|
|
if (n == 0) return (dlen + strlength (s));
|
|
|
|
while (*s != '\0') {
|
|
|
|
if (n != 1) {
|
|
|
|
*d++ = *s;
|
|
|
|
n--; }
|
|
|
|
s++; }
|
|
|
|
*d = '\0';
|
|
|
|
return (dlen + (s - src)); /* count does not include NUL */
|
|
|
|
}
|
|
|
|
|
|
|
|
/* match a constant character sequence */
|
|
|
|
int charcmp (const char *buf, const char *match) {
|
|
|
|
int len, i;
|
|
|
|
len = strlength (match);
|
|
|
|
for (i = 0; i < len; ++i)
|
|
|
|
if (buf[i] != match[i]) return 0;
|
|
|
|
return 1; }
|
|
|
|
|
|
|
|
/* check that we have a specified number of bytes left in the buffer */
|
|
|
|
void chk_bufdata (byte *ptr, unsigned long int len) {
|
|
|
|
if ((unsigned) (ptr + len - buffer) > buflen)
|
|
|
|
midi_error ("data missing", ptr); }
|
|
|
|
|
|
|
|
/* fetch big-endian numbers */
|
|
|
|
uint16_t rev_short (uint16_t val) {
|
|
|
|
return ((val & 0xff) << 8) | ((val >> 8) & 0xff); }
|
|
|
|
|
|
|
|
uint32_t rev_long (uint32_t val) {
|
|
|
|
return (((rev_short ((uint16_t) val) & 0xffff) << 16) |
|
|
|
|
(rev_short ((uint16_t) (val >> 16)) & 0xffff)); }
|
|
|
|
|
|
|
|
/* account for new items in the non-binary output file
|
|
|
|
and generate a newline every so often. */
|
|
|
|
void outfile_items (int n) {
|
|
|
|
outfile_bytecount += n;
|
|
|
|
outfile_itemcount += n;
|
|
|
|
if (!binaryoutput && outfile_itemcount >= outfile_maxitems) {
|
|
|
|
fprintf (outfile, "\n");
|
|
|
|
outfile_itemcount = 0; } }
|
|
|
|
|
|
|
|
//******* structures for recording track, channel, and tone generator status
|
|
|
|
|
|
|
|
// Note that the tempo can change while notes are being played, maybe many times.
|
|
|
|
// Although the score timing is specified in the MIDI file in ticks, we queue and
|
|
|
|
// issue note events based on microseconds since the start of the song. We convert
|
|
|
|
// ticks to microseconds, using the current tempo, as the MIDI events from the
|
|
|
|
// various tracks are processsed in tick order.
|
|
|
|
// In order to keep track of how long notes are to play, we have to incrementally
|
|
|
|
// accumulate the duration of all playing notes every time the tempo changes, and
|
|
|
|
// then one final time when the "stop note" event occurs.
|
|
|
|
|
|
|
|
struct noteinfo { // everything we might care about as a note plays
|
|
|
|
timestamp time_usec; // when it starts or stops, in absolute usec since song start
|
|
|
|
int track, channel, note, instrument, volume; // all the nitty-gritty about it
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct tonegen_status { // current status of a tone generator
|
|
|
|
bool playing; // is it playing?
|
|
|
|
bool stopnote_pending; // are we due to issue a stop note command?
|
|
|
|
struct noteinfo note; // if so, the details of the note being played
|
|
|
|
} tonegen[MAX_TONEGENS] = { 0 };
|
|
|
|
|
|
|
|
struct track_status { // current status of a MIDI track
|
|
|
|
uint8_t *trkptr; // ptr to the next event we care about
|
|
|
|
uint8_t *trkend; // ptr just past the end of the track
|
|
|
|
unsigned long time; // what time we're at in the score, in ticks
|
|
|
|
unsigned long tempo; // the last tempo set by this track
|
|
|
|
int preferred_tonegen; // for strategy2: try to use this generator
|
|
|
|
byte cmd; // next CMD_xxxx event coming up
|
|
|
|
byte chan, note, volume; // if it is CMD_PLAYNOTE or CMD_STOPNOTE, the note info
|
|
|
|
byte last_event; // the last event, for MIDI's "running status"
|
|
|
|
} track[MAX_TRACKS] = { 0 };
|
|
|
|
|
|
|
|
struct channel_status { // current status of a channel
|
|
|
|
int instrument; // which instrument this channel currently plays
|
|
|
|
bool note_playing[MAX_CHANNELNOTES]; // slots for notes that are playing on this channel
|
|
|
|
struct noteinfo notes_playing[MAX_CHANNELNOTES]; // information about them
|
|
|
|
} channel[NUM_CHANNELS] = { 0 };
|
|
|
|
|
|
|
|
char *describe(struct noteinfo *np) { // create a description of a note
|
|
|
|
// WARNING: returns a pointer to a static string, so only call once per line, in a printf!
|
|
|
|
static char notedescription[100];
|
|
|
|
int secs = np->time_usec / 1000000;
|
|
|
|
sprintf(notedescription, "at %3lu.%06lu sec (%d:%02d), note %d (0x%02X) track %d channel %d volume %d instrument %d",
|
|
|
|
secs, np->time_usec % 1000000,
|
|
|
|
secs/60, secs % 60,
|
|
|
|
np->note, np->note,
|
|
|
|
np->track, np->channel, np->volume, np->instrument);
|
|
|
|
return notedescription; }
|
|
|
|
|
|
|
|
/************** output reorder queue routines ******************
|
|
|
|
|
|
|
|
We queue commands to be issued at arbitrary times and sort them in time order. We flush
|
|
|
|
them out, allocating tone generators and creating the output bytestream, only as needed
|
|
|
|
to get more space in the queue. As we do that, we generate the "delay" commands required.
|
|
|
|
|
|
|
|
All the queuing must be done with a microsecond time base, not ticks, because the tempo,
|
|
|
|
which controls the duration of ticks, may (and does!) change while notes are being played.
|
|
|
|
|
|
|
|
We currently define "timestamps" by typedef as uint32_t, which is enough for songs as long
|
|
|
|
as 71 minutes. If longer songs are required, change it to uint64_t.
|
|
|
|
|
|
|
|
We assume that "unsigned long" variables are big enough to track the total number of ticks
|
|
|
|
in a song. If they are 32 bits, the typical 480 ticks/beat and 500,000 usec/tick imply
|
|
|
|
960 ticks/second, for a song length of 51 days. Even if the song plays much faster, it's
|
|
|
|
still plenty long.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#define QUEUE_SIZE 100 // maximum number of note play/stop commands we queue
|
|
|
|
|
|
|
|
struct queue_entry { // the format of each queue entry
|
|
|
|
byte cmd; // CMD_PLAY or CMD_STOP
|
|
|
|
byte delete; // delete this command due to "-noduplicates"?
|
|
|
|
struct noteinfo note; // info about the note, including the action time
|
|
|
|
} queue[QUEUE_SIZE];
|
|
|
|
|
|
|
|
int queue_numitems = 0;
|
|
|
|
int queue_oldest_ndx = 0, queue_newest_ndx = 0;
|
|
|
|
int debugcount = 0;
|
|
|
|
|
|
|
|
void show_queue(void) { // for debugging: dump the whole event queue
|
|
|
|
FILE *fid = logfile;
|
|
|
|
if (!logfile) fid = stdout;
|
|
|
|
fprintf(fid, "***at output time %lu.%03lu queue has %d items; oldest at %d, newest at %d\n",
|
|
|
|
output_usec / 1000, output_usec % 1000, queue_numitems, queue_oldest_ndx, queue_newest_ndx);
|
|
|
|
int ndx = queue_oldest_ndx;
|
|
|
|
if (queue_numitems > 0) while (1) {
|
|
|
|
struct queue_entry *q = &queue[ndx];
|
|
|
|
fprintf(fid, "%2d: %s %s %s\n", ndx, q->cmd == CMD_PLAYNOTE ? "PLAY" : "STOP", q->delete ? "deleted" : "", describe(&q->note));
|
|
|
|
if (ndx == queue_newest_ndx) break;
|
|
|
|
if (++ndx >= QUEUE_SIZE) ndx = 0; } }
|
|
|
|
|
|
|
|
void show_tonegens(void) { // for debugging: dump tone generator status
|
|
|
|
FILE *fid = logfile;
|
|
|
|
if (!logfile) fid = stdout;
|
|
|
|
fprintf(fid, "*** tone generator status at output time %lu.%03lu\n", output_usec / 1000, output_usec % 1000);
|
|
|
|
for (int tgnum = 0; tgnum < num_tonegens; ++tgnum) {
|
|
|
|
struct tonegen_status *tg = &tonegen[tgnum];
|
|
|
|
if (tg->playing)
|
|
|
|
fprintf(fid, "#%d: playing note %d (%02X), instrument %d, track %d channel %d%s\n",
|
|
|
|
tgnum, tg->note.note, tg->note.note, tg->note.instrument, tg->note.track, tg->note.channel,
|
|
|
|
tg->stopnote_pending ? ", stopnote pending\n" : "");
|
|
|
|
else fprintf(fid, "#%d: idle\n", tgnum); } }
|
|
|
|
|
|
|
|
// find an idle tone generator we can use
|
|
|
|
int find_idle_tgen(struct noteinfo *np) { // returns -1 if there isn't one
|
|
|
|
struct tonegen_status *tg;
|
|
|
|
int tgnum;
|
|
|
|
bool foundgen = false;
|
|
|
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) { // first, is this note already playing on this channel?
|
|
|
|
tg = &tonegen[tgnum];
|
|
|
|
if (tg->playing
|
|
|
|
&& tg->note.note == np->note && tg->note.channel == np->channel
|
|
|
|
&& tg->note.track == np->track && tg->note.instrument == np->instrument) {
|
|
|
|
// this must be the start of the sustain phase of a playing note
|
|
|
|
++playnotes_without_stopnotes;
|
|
|
|
if (loggen) fprintf(logfile, " *** playnote without stopnote, tgen %d, %s\n",
|
|
|
|
tgnum, describe(np));
|
|
|
|
foundgen = true;
|
|
|
|
break; } }
|
|
|
|
if (!foundgen && strategy2) { // try to use the same tone generator that this track used last time
|
|
|
|
struct track_status *trk = &track[np->track];
|
|
|
|
tg = &tonegen[trk->preferred_tonegen];
|
|
|
|
if (!tg->playing) {
|
|
|
|
tgnum = trk->preferred_tonegen;
|
|
|
|
foundgen = true; } }
|
|
|
|
if (!foundgen) // if not, then try for a free tone generator that had been playing the same instrument we need
|
|
|
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
|
|
|
|
tg = &tonegen[tgnum];
|
|
|
|
if (!tg->playing && tg->note.instrument == np->instrument) {
|
|
|
|
foundgen = true;
|
|
|
|
break; } }
|
|
|
|
if (!foundgen) // if not, then try for any free tone generator
|
|
|
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
|
|
|
|
tg = &tonegen[tgnum];
|
|
|
|
if (!tg->playing) {
|
|
|
|
foundgen = true;
|
|
|
|
break; } }
|
|
|
|
if (foundgen) return tgnum;
|
|
|
|
return -1; }
|
|
|
|
|
|
|
|
void remove_queue_entry(int ndx) { // remove the oldest queue entry
|
|
|
|
struct queue_entry *q = &queue[ndx];
|
|
|
|
if (q->delete) return; // if marked for deletion, just ignore it
|
|
|
|
if (q->cmd == CMD_STOPNOTE) {
|
|
|
|
if (loggen) fprintf(logfile, " dequeue stopnote for %s\n", describe(&q->note));
|
|
|
|
// find the tone generator playing this note, and record a pending stop
|
|
|
|
int tgnum;
|
|
|
|
for (tgnum = 0; tgnum < num_tonegens; ++tgnum) {
|
|
|
|
struct tonegen_status *tg = &tonegen[tgnum];
|
|
|
|
if (tg->playing
|
|
|
|
&& tg->note.note == q->note.note
|
|
|
|
&& tg->note.channel == q->note.channel) { // found the note
|
|
|
|
tg->stopnote_pending = true; // "stop note needed unless another start note follows"
|
|
|
|
tg->playing = false; // free the tg to be reallocated, but note the stop time in case
|
|
|
|
tg->note.time_usec = q->note.time_usec; // the tg doesn't get used and we generate it
|
|
|
|
if (loggen) fprintf(logfile, " pending stop tgen %d %s\n", tgnum, describe(&q->note));
|
|
|
|
break; } }
|
|
|
|
if (tgnum >= num_tonegens) {
|
|
|
|
// If we exited the loop without finding the generator playing this note, presumably it never started
|
|
|
|
// because there weren't any free tone generators. Is there some assertion we can use to verify that?
|
|
|
|
++stopnotes_without_playnotes;
|
|
|
|
if (loggen) fprintf(logfile, " *** stopnote without playnote, %s\n", describe(&q->note)); } }
|
|
|
|
|
|
|
|
else { // CMD_PLAYNOTE
|
|
|
|
assert(q->cmd == CMD_PLAYNOTE, "bad cmd in remove_queue_entry");
|
|
|
|
if (loggen) fprintf(logfile, " dequeue playnote for %s\n", describe(&q->note));
|
|
|
|
int tgnum = find_idle_tgen(&q->note);
|
|
|
|
struct tonegen_status *tg = &tonegen[tgnum];
|
|
|
|
if (tgnum >= 0) { // we found a tone generator we can use
|
|
|
|
if (tgnum + 1 > num_tonegens_used) num_tonegens_used = tgnum + 1;
|
|
|
|
if (tg->note.instrument != q->note.instrument) { // it's a new instrument for this generator
|
|
|
|
tg->note.instrument = q->note.instrument;
|
|
|
|
++instrument_changes;
|
|
|
|
if (loggen) fprintf(logfile, " tgen %d changed to instrument %d\n", tgnum, tg->note.instrument);
|
|
|
|
if (instrumentoutput) { // output a "change instrument" command
|
|
|
|
if (binaryoutput) {
|
|
|
|
putc(CMD_INSTRUMENT | tgnum, outfile);
|
|
|
|
putc(tg->note.instrument, outfile);
|
|
|
|
outfile_bytecount += 2; }
|
|
|
|
else {
|
|
|
|
fprintf(outfile, "0x%02X,%d, ", CMD_INSTRUMENT | tgnum, tg->note.instrument);
|
|
|
|
outfile_items(2); } } }
|
|
|
|
if (loggen) fprintf(logfile, " play tgen %d %s\n", tgnum, describe(&q->note));
|
|
|
|
tg->playing = true;
|
|
|
|
tg->stopnote_pending = false; // don't bother to issue "stop note"
|
|
|
|
tg->note = q->note; // structure copy of note info
|
|
|
|
track[tg->note.track].preferred_tonegen = tgnum;
|
|
|
|
++note_on_commands;
|
|
|
|
last_output_was_delay = false;
|
|
|
|
if (binaryoutput) {
|
|
|
|
putc(CMD_PLAYNOTE | tgnum, outfile);
|
|
|
|
putc(tg->note.note, outfile);
|
|
|
|
outfile_bytecount += 2;
|
|
|
|
if (volume_output) {
|
|
|
|
putc(tg->note.volume, outfile);
|
|
|
|
outfile_bytecount +=1; } }
|
|
|
|
else {
|
|
|
|
if (volume_output == 0) {
|
|
|
|
fprintf(outfile, "0x%02X,%d, ", CMD_PLAYNOTE | tgnum, tg->note.note);
|
|
|
|
outfile_items(2); }
|
|
|
|
else {
|
|
|
|
fprintf(outfile, "0x%02X,%d,%d, ", CMD_PLAYNOTE | tgnum, tg->note.note, tg->note.volume);
|
|
|
|
outfile_items(3); } } }
|
|
|
|
else {
|
|
|
|
if (loggen) fprintf(logfile, " *** at %lu.%03lu msec no free generator; skipping %s\n",
|
|
|
|
output_usec / 1000, output_usec % 1000, describe(&q->note));
|
|
|
|
if (showskipped) printf(" *** no free generator %s\n",
|
|
|
|
describe(&q->note)); ++notes_skipped; } } }
|
|
|
|
|
|
|
|
void generate_delay(unsigned long delta_msec) { // output a delay command
|
|
|
|
if (delta_msec > 0) {
|
|
|
|
assert(delta_msec <= 0x7fff, "time delta too big");
|
|
|
|
if (last_output_was_delay) {
|
|
|
|
++consecutive_delays;
|
|
|
|
if (loggen) fprintf(logfile, " *** this is a consecutive delay, of %d msec\n", delta_msec); }
|
|
|
|
last_output_was_delay = true;
|
|
|
|
if (binaryoutput) { // output a 15-bit delay in big-endian format
|
|
|
|
putc((byte)(delta_msec >> 8), outfile);
|
|
|
|
putc((byte)(delta_msec & 0xff), outfile);
|
|
|
|
outfile_bytecount += 2; }
|
|
|
|
else {
|
|
|
|
fprintf(outfile, "%ld,%ld, ", delta_msec >> 8, delta_msec & 0xff);
|
|
|
|
outfile_items(2); } } }
|
|
|
|
|
|
|
|
// output all queue elements which are at the oldest time or at most "delaymin" later
|
|
|
|
void pull_queue(void) {
|
|
|
|
if (loggen) fprintf(logfile, " <-pull from queue at %lu.%03lu msec\n", output_usec / 1000, output_usec % 1000);
|
|
|
|
timestamp oldtime = queue[queue_oldest_ndx].note.time_usec; // the oldest time
|
|
|
|
assert(oldtime >= output_usec, "oldest queue entry goes backward in pull_queue");
|
|
|
|
unsigned long delta_usec = (oldtime - output_usec) + output_deficit_usec;
|
|
|
|
unsigned long delta_msec = delta_usec / 1000;
|
|
|
|
output_deficit_usec = delta_usec % 1000;
|
|
|
|
if (delta_usec > (unsigned long)delaymin_usec) { // if time has advanced beyond the merge threshold, output a delay
|
|
|
|
if (delta_msec > 0) {
|
|
|
|
generate_delay(delta_msec);
|
|
|
|
if (loggen) fprintf(logfile, " at %lu.%03lu msec, delay for %ld msec to %lu.%03lu msec; deficit is %lu usec\n",
|
|
|
|
output_usec / 1000, output_usec % 1000, delta_msec,
|
|
|
|
oldtime / 1000, oldtime % 1000, output_deficit_usec); }
|
|
|
|
else if (loggen) fprintf(logfile, " at %lu.%03lu msec, a delay of only %lu usec was skipped, and the deficit is now %lu usec\n",
|
|
|
|
output_usec / 1000, output_usec % 1000, oldtime-output_usec, output_deficit_usec);
|
|
|
|
output_usec = oldtime; }
|
|
|
|
else if (delta_msec > 0) ++delays_saved;
|
|
|
|
|
|
|
|
do { // output and remove all entries at the same (oldest) time in the queue
|
|
|
|
// or which are only delaymin newer
|
|
|
|
remove_queue_entry(queue_oldest_ndx);
|
|
|
|
if (++queue_oldest_ndx >= QUEUE_SIZE) queue_oldest_ndx = 0;
|
|
|
|
--queue_numitems; }
|
|
|
|
while (queue_numitems > 0 && queue[queue_oldest_ndx].note.time_usec <= oldtime + (timestamp)delaymin_usec);
|
|
|
|
|
|
|
|
// do any "stop notes" still need to be generated?
|
|
|
|
for (int tgnum = 0; tgnum < num_tonegens; ++tgnum) {
|
|
|
|
struct tonegen_status *tg = &tonegen[tgnum];
|
|
|
|
if (tg->stopnote_pending) { // got one
|
|
|
|
last_output_was_delay = false;
|
|
|
|
if (binaryoutput) {
|
|
|
|
putc(CMD_STOPNOTE | tgnum, outfile);
|
|
|
|
outfile_bytecount += 1; }
|
|
|
|
else {
|
|
|
|
fprintf(outfile, "0x%02X, ", CMD_STOPNOTE | tgnum);
|
|
|
|
outfile_items(1); }
|
|
|
|
if (loggen) fprintf(logfile, " stop tgen %d %s\n", tgnum, describe(&tg->note));
|
|
|
|
tg->stopnote_pending = false;
|
|
|
|
tg->playing = false; } } }
|
|
|
|
|
|
|
|
void flush_queue(void) { // empty the queue
|
|
|
|
while (queue_numitems > 0) pull_queue(); }
|
|
|
|
|
|
|
|
// find the queue entry for the playnote matching the stopnote in queue[search_ndx]
|
|
|
|
int queue_find_playnote(int search_ndx) {
|
|
|
|
if (loggen) fprintf(logfile, " queue_find_playnote(%d), %s: ", search_ndx, describe(&queue[search_ndx].note));
|
|
|
|
for (int ndx = queue_newest_ndx;;) {
|
|
|
|
if (ndx != search_ndx // don't look at the entry we're trying to match
|
|
|
|
&& !queue[ndx].delete // don't re-find queue entries already deleted
|
|
|
|
&& queue[ndx].cmd == CMD_PLAYNOTE // must match playnote
|
|
|
|
&& queue[ndx].note.channel == queue[search_ndx].note.channel //also channel, track, note, and instrument
|
|
|
|
&& queue[ndx].note.track == queue[search_ndx].note.track
|
|
|
|
&& queue[ndx].note.note == queue[search_ndx].note.note
|
|
|
|
&& queue[ndx].note.instrument == queue[search_ndx].note.instrument ) {
|
|
|
|
if (loggen) fprintf(logfile, "found ndx %d\n", ndx);
|
|
|
|
return ndx; }
|
|
|
|
if (ndx == queue_oldest_ndx) break;
|
|
|
|
if (--ndx < 0) ndx = QUEUE_SIZE - 1; }
|
|
|
|
if (loggen) fprintf(logfile, "not found\n");
|
|
|
|
return -1; }
|
|
|
|
|
|
|
|
// find a queue entry for another stopnote matching the stopnote in queue[search_ndx]
|
|
|
|
int queue_find_stopnote(int search_ndx) {
|
|
|
|
if (loggen) fprintf(logfile, " queue_find_stopnote(%d), %s: ", search_ndx, describe(&queue[search_ndx].note));
|
|
|
|
for (int ndx = queue_newest_ndx;;) {
|
|
|
|
if (ndx != search_ndx // don't look at the entry we're trying to match
|
|
|
|
&& !queue[ndx].delete // don't re-find queue entries already deleted
|
|
|
|
&& queue[ndx].cmd == CMD_STOPNOTE // must match stopnote
|
|
|
|
&& queue[ndx].note.time_usec == queue[search_ndx].note.time_usec // also time, note, and instrument
|
|
|
|
&& queue[ndx].note.note == queue[search_ndx].note.note
|
|
|
|
&& queue[ndx].note.instrument == queue[search_ndx].note.instrument) {
|
|
|
|
if (loggen) fprintf(logfile, "found ndx %d\n", ndx);
|
|
|
|
return ndx; }
|
|
|
|
if (ndx == queue_oldest_ndx) break;
|
|
|
|
if (--ndx < 0) ndx = QUEUE_SIZE - 1; }
|
|
|
|
if (loggen) fprintf(logfile, "not found\n");
|
|
|
|
return -1; }
|
|
|
|
|
|
|
|
// For -noduplicates, mark for deletion any note start/stop identical to the CMD_STOPNOTE we just queued
|
|
|
|
void remove_queue_duplicates(int stop_ndx) {
|
|
|
|
int play_ndx, dup_play_ndx, dup_stop_ndx;
|
|
|
|
if ((play_ndx = queue_find_playnote(stop_ndx)) >= 0 // find its matching playnote
|
|
|
|
&& (dup_stop_ndx = queue_find_stopnote(stop_ndx)) >= 0 // find another stopnote for the same note at the same time
|
|
|
|
&& (dup_play_ndx = queue_find_playnote(dup_stop_ndx)) >= 0 // find its matching playnote
|
|
|
|
&& queue[dup_play_ndx].note.time_usec == queue[play_ndx].note.time_usec) { // if it starts at the same time, delete it
|
|
|
|
if (loggen) fprintf(logfile, " remove duplicate, ndxs %d, %d: %s\n", dup_play_ndx, dup_stop_ndx, describe(&queue[dup_play_ndx].note));
|
|
|
|
queue[dup_play_ndx].delete = queue[dup_stop_ndx].delete = true; } }
|
|
|
|
|
|
|
|
// queue a "note on" or "note off" command
|
|
|
|
void queue_cmd(byte cmd, struct noteinfo *np) {
|
|
|
|
if (loggen) fprintf(logfile, " queue %s %s\n",
|
|
|
|
cmd == CMD_PLAYNOTE ? "PLAY" : cmd == CMD_STOPNOTE ? "STOP" : "????",
|
|
|
|
describe(np));
|
|
|
|
if (queue_numitems >= QUEUE_SIZE) pull_queue();
|
|
|
|
assert(queue_numitems < QUEUE_SIZE, "no room in queue");
|
|
|
|
timestamp horizon = output_usec + output_deficit_usec;
|
|
|
|
if (np->time_usec < horizon) { // don't allow revisionist history
|
|
|
|
if (loggen) fprintf(logfile, " event delayed by %lu usec because queue is too small\n",
|
|
|
|
horizon - np->time_usec);
|
|
|
|
np->time_usec = horizon;
|
|
|
|
++events_delayed; }
|
|
|
|
int ndx;
|
|
|
|
if (queue_numitems == 0) { // queue is empty; restart it
|
|
|
|
ndx = queue_oldest_ndx = queue_newest_ndx = queue_numitems = 0; }
|
|
|
|
else { // find a place to insert the new entry in time order
|
|
|
|
// this is a stable incremental insertion sort
|
|
|
|
ndx = queue_newest_ndx; // start with newest, since we are most often newer
|
|
|
|
while (queue[ndx].note.time_usec > np->time_usec) { // search backwards for something as new or older
|
|
|
|
if (ndx == queue_oldest_ndx) { // none: we are oldest; add to the start
|
|
|
|
if (--queue_oldest_ndx < 0) queue_oldest_ndx = QUEUE_SIZE - 1;
|
|
|
|
ndx = queue_oldest_ndx;
|
|
|
|
goto insert; }
|
|
|
|
if (--ndx < 0) ndx = QUEUE_SIZE - 1; }
|
|
|
|
// we are to insert the new item after "ndx", so shift all later entries down, if any
|
|
|
|
int from_ndx, to_ndx;
|
|
|
|
if (++queue_newest_ndx >= QUEUE_SIZE) queue_newest_ndx = 0;
|
|
|
|
to_ndx = queue_newest_ndx;
|
|
|
|
while (1) {
|
|
|
|
if ((from_ndx = to_ndx - 1) < 0) from_ndx = QUEUE_SIZE - 1;
|
|
|
|
if (from_ndx == ndx) break;
|
|
|
|
queue[to_ndx] = queue[from_ndx]; // structure copy
|
|
|
|
to_ndx = from_ndx; }
|
|
|
|
if (++ndx >= QUEUE_SIZE) ndx = 0; }
|
|
|
|
insert: // store the item at ndx
|
|
|
|
++queue_numitems;
|
|
|
|
queue[ndx].cmd = cmd; // fill in the queue entry
|
|
|
|
queue[ndx].delete = false;
|
|
|
|
queue[ndx].note = *np; // structure copy of the note
|
|
|
|
if (noduplicates && cmd == CMD_STOPNOTE) remove_queue_duplicates(ndx); }
|
|
|
|
|
|
|
|
void show_queue_cmd(timestamp time_usec, byte cmd, int note) {
|
|
|
|
printf("debug queue %s note %02X at %6ld\n", cmd == CMD_PLAYNOTE ? "PLAY" : "STOP", note, time_usec);
|
|
|
|
struct noteinfo notedata;
|
|
|
|
notedata.time_usec = time_usec;
|
|
|
|
notedata.track = 0;
|
|
|
|
notedata.channel = 0;
|
|
|
|
notedata.note = note;
|
|
|
|
notedata.instrument = 1;
|
|
|
|
notedata.volume = 100;
|
|
|
|
queue_cmd(cmd, ¬edata);
|
|
|
|
show_queue(); }
|
|
|
|
|
|
|
|
/************** process the MIDI file header *****************/
|
|
|
|
|
|
|
|
void process_file_header (void) {
|
|
|
|
struct midi_header *hdr;
|
|
|
|
unsigned int time_division;
|
|
|
|
|
|
|
|
chk_bufdata (hdrptr, sizeof (struct midi_header));
|
|
|
|
hdr = (struct midi_header *) hdrptr;
|
|
|
|
if (!charcmp ((char *) hdr->MThd, "MThd"))
|
|
|
|
midi_error ("Missing 'MThd'", hdrptr);
|
|
|
|
num_tracks = rev_short (hdr->number_of_tracks);
|
|
|
|
time_division = rev_short (hdr->time_division);
|
|
|
|
if (time_division < 0x8000)
|
|
|
|
ticks_per_beat = time_division;
|
|
|
|
else
|
|
|
|
ticks_per_beat = ((time_division >> 8) & 0x7f) /* SMTE frames/sec */ *(time_division & 0xff); /* ticks/SMTE frame */
|
|
|
|
if (logparse) {
|
|
|
|
fprintf (logfile, "Header size %" PRId32 "\n", rev_long (hdr->header_size));
|
|
|
|
fprintf (logfile, "Format type %d\n", rev_short (hdr->format_type));
|
|
|
|
fprintf (logfile, "Number of tracks %d\n", num_tracks);
|
|
|
|
fprintf (logfile, "Time division %04X\n", time_division);
|
|
|
|
fprintf (logfile, "Ticks/beat = %d\n", ticks_per_beat); }
|
|
|
|
hdrptr += rev_long (hdr->header_size) + 8; /* point past header to track header, presumably. */
|
|
|
|
return; }
|
|
|
|
|
|
|
|
void process_track_header (int tracknum) {
|
|
|
|
struct track_header *hdr;
|
|
|
|
unsigned long tracklen;
|
|
|
|
|
|
|
|
chk_bufdata (hdrptr, sizeof (struct track_header));
|
|
|
|
hdr = (struct track_header *) hdrptr;
|
|
|
|
if (!charcmp ((char *) (hdr->MTrk), "MTrk"))
|
|
|
|
midi_error ("Missing 'MTrk'", hdrptr);
|
|
|
|
tracklen = rev_long (hdr->track_size);
|
|
|
|
if (logparse) fprintf (logfile, "\nTrack %d length %ld\n", tracknum, tracklen);
|
|
|
|
hdrptr += sizeof (struct track_header); /* point past header */
|
|
|
|
chk_bufdata (hdrptr, tracklen);
|
|
|
|
track[tracknum].trkptr = hdrptr;
|
|
|
|
hdrptr += tracklen; /* point to the start of the next track */
|
|
|
|
track[tracknum].trkend = hdrptr; /* the point past the end of the track */
|
|
|
|
}
|
|
|
|
|
|
|
|
unsigned long get_varlen (uint8_t ** ptr) { // get a MIDI-style integer
|
|
|
|
/* Get a 1-4 byte variable-length value and adjust the pointer past it.
|
|
|
|
These are a succession of 7-bit values with a MSB bit of zero marking the end */
|
|
|
|
unsigned long val = 0;
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
byte b = *(*ptr)++;
|
|
|
|
val = (val << 7) | (b & 0x7f);
|
|
|
|
if (!(b & 0x80))
|
|
|
|
return val; }
|
|
|
|
return val; }
|
|
|
|
|
|
|
|
/*************** Process the MIDI track data ***************************/
|
|
|
|
|
|
|
|
// Skip in the track for the next "note on", "note off" or "set tempo" command and return.
|
|
|
|
|
|
|
|
void find_next_note (int tracknum) {
|
|
|
|
unsigned long int delta_ticks;
|
|
|
|
int event, chan;
|
|
|
|
int note, velocity, controller, pressure, pitchbend, instrument;
|
|
|
|
int meta_cmd, meta_length;
|
|
|
|
unsigned long int sysex_length;
|
|
|
|
char *tag;
|
|
|
|
|
|
|
|
struct track_status *t = &track[tracknum]; // our track status structure
|
|
|
|
while (t->trkptr < t->trkend) {
|
|
|
|
delta_ticks = get_varlen (&t->trkptr);
|
|
|
|
t->time += delta_ticks;
|
|
|
|
if (logparse) {
|
|
|
|
fprintf(logfile, "# trk %d ", tracknum);
|
|
|
|
if (loggen) fprintf(logfile, "at ticks+%lu=%lu: ", delta_ticks, t->time);
|
|
|
|
else {
|
|
|
|
if (delta_ticks > 0) fprintf(logfile, "ticks+%-5lu%7lu ", delta_ticks, t->time);
|
|
|
|
else fprintf(logfile, " "); } }
|
|
|
|
|
|
|
|
if (*t->trkptr < 0x80) event = t->last_event; // using "running status": same event as before
|
|
|
|
else event = *t->trkptr++; // otherwise get new "status" (event type) */
|
|
|
|
|
|
|
|
if (event == 0xff) { // meta-event
|
|
|
|
meta_cmd = *t->trkptr++;
|
|
|
|
meta_length = get_varlen (&t->trkptr);
|
|
|
|
switch (meta_cmd) {
|
|
|
|
case 0x00:
|
|
|
|
if (logparse) fprintf (logfile, "sequence number %d\n", rev_short (*(unsigned short *) t->trkptr));
|
|
|
|
break;
|
|
|
|
case 0x01:
|
|
|
|
tag = "description"; goto show_text;
|
|
|
|
case 0x02:
|
|
|
|
tag = "copyright"; goto show_text;
|
|
|
|
case 0x03:
|
|
|
|
tag = "track name";
|
|
|
|
if (tracknum == 0 && !parseonly && !binaryoutput) {
|
|
|
|
/* Incredibly, MIDI has no standard for recording the name of the piece!
|
|
|
|
Track 0's "trackname" is often used for that so we output it to the C file as documentation. */
|
|
|
|
fprintf (outfile, "// ");
|
|
|
|
for (int i = 0; i < meta_length; ++i) {
|
|
|
|
int ch = t->trkptr[i];
|
|
|
|
fprintf (outfile, "%c", isprint (ch) ? ch : '?'); }
|
|
|
|
fprintf (outfile, "\n"); }
|
|
|
|
goto show_text;
|
|
|
|
case 0x04:
|
|
|
|
tag = "instrument name"; goto show_text;
|
|
|
|
case 0x05:
|
|
|
|
tag = "lyric"; goto show_text;
|
|
|
|
case 0x06:
|
|
|
|
tag = "marked point"; goto show_text;
|
|
|
|
case 0x07:
|
|
|
|
tag = "cue point"; goto show_text;
|
|
|
|
case 0x08:
|
|
|
|
tag = "program name"; goto show_text;
|
|
|
|
case 0x09:
|
|
|
|
tag = "device (port) name";
|
|
|
|
show_text:
|
|
|
|
if (logparse) {
|
|
|
|
fprintf (logfile, "meta cmd %02X, length %d, %s: \"", meta_cmd, meta_length, tag);
|
|
|
|
for (int i = 0; i < meta_length; ++i) {
|
|
|
|
int ch = t->trkptr[i];
|
|
|
|
fprintf (logfile, "%c", isprint (ch) ? ch : '?'); }
|
|
|
|
fprintf (logfile, "\"\n"); }
|
|
|
|
break;
|
|
|
|
case 0x20:
|
|
|
|
if (logparse) fprintf (logfile, "channel prefix %d\n", *t->trkptr);
|
|
|
|
break;
|
|
|
|
case 0x21:
|
|
|
|
if (logparse) fprintf(logfile, "MIDI port %d\n", *t->trkptr);
|
|
|
|
break;
|
|
|
|
case 0x2f:
|
|
|
|
if (logparse) fprintf (logfile, "end of track\n");
|
|
|
|
break;
|
|
|
|
case 0x51: // tempo: 3 byte big-endian integer, not a varlen integer!
|
|
|
|
t->cmd = CMD_TEMPO;
|
|
|
|
t->tempo = rev_long (*(uint32_t *) (t->trkptr - 1)) & 0xffffffL;
|
|
|
|
if (logparse) fprintf (logfile, "set tempo %ld usec/qnote\n", t->tempo);
|
|
|
|
t->trkptr += meta_length;
|
|
|
|
return;
|
|
|
|
case 0x54:
|
|
|
|
if (logparse) fprintf (logfile, "SMPTE offset %08" PRIx32 "\n",
|
|
|
|
rev_long (*(uint32_t *) t->trkptr));
|
|
|
|
break;
|
|
|
|
case 0x58:
|
|
|
|
if (logparse) fprintf (logfile, "time signature %08" PRIx32 "\n",
|
|
|
|
rev_long (*(uint32_t *) t->trkptr));
|
|
|
|
break;
|
|
|
|
case 0x59:
|
|
|
|
if (logparse) fprintf (logfile, "key signature %04X\n", rev_short (*(unsigned short *) t->trkptr));
|
|
|
|
break;
|
|
|
|
case 0x7f:
|
|
|
|
tag = "sequencer data"; goto show_hex;
|
|
|
|
default: /* unknown meta command */
|
|
|
|
tag = "???";
|
|
|
|
show_hex:
|
|
|
|
if (logparse) {
|
|
|
|
fprintf (logfile, "meta cmd %02X, length %d, %s: ", meta_cmd, meta_length, tag);
|
|
|
|
for (int i = 0; i < meta_length; ++i)
|
|
|
|
fprintf (logfile, "%02X ", t->trkptr[i]);
|
|
|
|
fprintf (logfile, "\n"); }
|
|
|
|
break; }
|
|
|
|
t->trkptr += meta_length; }
|
|
|
|
|
|
|
|
else if (event < 0x80) midi_error ("Unknown MIDI event type", t->trkptr);
|
|
|
|
|
|
|
|
else {
|
|
|
|
if (event < 0xf0)
|
|
|
|
t->last_event = event; // remember "running status" if not meta or sysex event
|
|
|
|
t->chan = chan = event & 0xf;
|
|
|
|
switch (event >> 4) {
|
|
|
|
case 0x8: // note off
|
|
|
|
t->note = *t->trkptr++;
|
|
|
|
t->volume = *t->trkptr++;
|
|
|
|
note_off: if (logparse) fprintf(logfile, "note %d (0x%02X) off, channel %d, volume %d\n", t->note, t->note, chan, t->volume);
|
|
|
|
if ((1 << chan) & channel_mask // we're processing this channel
|
|
|
|
&& (!percussion_ignore || chan != PERCUSSION_TRACK)) { // and not ignoring percussion
|
|
|
|
if (!instrumentoutput) t->chan = 0; // if no instruments, force all notes to channel 0
|
|
|
|
t->cmd = CMD_STOPNOTE; /* stop processing and return */
|
|
|
|
return; }
|
|
|
|
break;
|
|
|
|
case 0x9: // note on
|
|
|
|
t->note = *t->trkptr++;
|
|
|
|
t->volume = *t->trkptr++;
|
|
|
|
if (t->volume == 0) // some scores use note-on with zero velocity for off!
|
|
|
|
goto note_off;
|
|
|
|
if (logparse) fprintf(logfile, "note %d (0x%02X) on, channel %d, volume %d\n", t->note, t->note, chan, t->volume);
|
|
|
|
if ((1 << chan) & channel_mask // we're processing this channel
|
|
|
|
&& (!percussion_ignore || chan != PERCUSSION_TRACK)) { // and not ignoring percussion
|
|
|
|
if (!instrumentoutput) t->chan = 0; // if no instruments, force all notes to channel 0
|
|
|
|
t->cmd = CMD_PLAYNOTE; /* stop processing and return */
|
|
|
|
return; }
|
|
|
|
break;
|
|
|
|
case 0xa: // key pressure
|
|
|
|
note = *t->trkptr++;
|
|
|
|
velocity = *t->trkptr++;
|
|
|
|
if (logparse) fprintf (logfile, "channel %d: note %d (0x%02X) has key pressure %d\n", chan, note, note, velocity);
|
|
|
|
break;
|
|
|
|
case 0xb: // control value change
|
|
|
|
controller = *t->trkptr++;
|
|
|
|
velocity = *t->trkptr++;
|
|
|
|
if (logparse) fprintf (logfile, "channel %d: change control value of controller %d to %d\n", chan, controller, velocity);
|
|
|
|
break;
|
|
|
|
case 0xc: // program patch, ie which instrument
|
|
|
|
instrument = *t->trkptr++;
|
|
|
|
channel[chan].instrument = instrument; // record new instrument for this channel
|
|
|
|
if (logparse) fprintf (logfile, "channel %d: program patch to instrument %d\n", chan, instrument);
|
|
|
|
break;
|
|
|
|
case 0xd: // channel pressure
|
|
|
|
pressure = *t->trkptr++;
|
|
|
|
if (logparse) fprintf (logfile, "channel %d: after-touch pressure is %d\n", chan, pressure);
|
|
|
|
break;
|
|
|
|
case 0xe: // pitch wheel change
|
|
|
|
pitchbend = *t->trkptr++ | (*t->trkptr++ << 7);
|
|
|
|
if (logparse) fprintf (logfile, "pitch wheel change to %d\n", pitchbend);
|
|
|
|
break;
|
|
|
|
case 0xf: // sysex event
|
|
|
|
sysex_length = get_varlen (&t->trkptr);
|
|
|
|
if (logparse) fprintf (logfile, "SysEx event %d with %ld bytes\n", event, sysex_length);
|
|
|
|
t->trkptr += sysex_length;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
midi_error ("Unknown MIDI command", t->trkptr); } } }
|
|
|
|
t->cmd = CMD_TRACKDONE; //no more events to process on this track
|
|
|
|
++tracks_done; }
|
|
|
|
|
|
|
|
void show_noteinfo_slots(int channum) {
|
|
|
|
struct channel_status *cp = &channel[channum];
|
|
|
|
if (loggen) {
|
|
|
|
fprintf(logfile, "notes playing for channel %d:\n", channum);
|
|
|
|
for (int ndx = 0; ndx < MAX_CHANNELNOTES; ++ndx)
|
|
|
|
if (cp->note_playing[ndx]) {
|
|
|
|
struct noteinfo *np = &cp->notes_playing[ndx];
|
|
|
|
fprintf(logfile, " %2d: %s\n", ndx, describe(np)); } } }
|
|
|
|
|
|
|
|
void process_track_data(void) {
|
|
|
|
unsigned long last_earliest_time = 0;
|
|
|
|
|
|
|
|
do { // while there are still track notes to process
|
|
|
|
|
|
|
|
/* Find the track with the earliest event time, and process it's event.
|
|
|
|
|
|
|
|
A potential improvement: If there are multiple tracks with the same time,
|
|
|
|
first do the ones with STOPNOTE as the next command, if any. That would
|
|
|
|
help avoid running out of tone generators. In practice, though, most MIDI
|
|
|
|
files do all the STOPNOTEs first anyway, so it won't have much effect.
|
|
|
|
|
|
|
|
Usually we start with the track after the one we did last time (tracknum),
|
|
|
|
so that if we run out of tone generators, we have been fair to all the tracks.
|
|
|
|
The alternate "strategy1" says we always start with track 0, which means
|
|
|
|
that we favor early tracks over later ones when there aren't enough tone generators. */
|
|
|
|
|
|
|
|
struct track_status *trk;
|
|
|
|
int count_tracks = num_tracks;
|
|
|
|
unsigned long earliest_time = 0x7fffffff; // in ticks, of course
|
|
|
|
int tracknum = 0;
|
|
|
|
int earliest_tracknum;
|
|
|
|
if (strategy1)
|
|
|
|
tracknum = num_tracks; /* beyond the end, so we start with track 0 */
|
|
|
|
do {
|
|
|
|
if (++tracknum >= num_tracks) tracknum = 0;
|
|
|
|
trk = &track[tracknum];
|
|
|
|
if (trk->cmd != CMD_TRACKDONE && trk->time < earliest_time) {
|
|
|
|
earliest_time = trk->time;
|
|
|
|
earliest_tracknum = tracknum; } }
|
|
|
|
while (--count_tracks);
|
|
|
|
tracknum = earliest_tracknum; /* the track we picked */
|
|
|
|
trk = &track[tracknum];
|
|
|
|
assert(earliest_time >= timenow_ticks, "time went backwards in process_track_data");
|
|
|
|
timenow_ticks = earliest_time; // we make it the global time
|
|
|
|
timenow_usec += (uint64_t)(timenow_ticks - timenow_usec_updated) * tempo / ticks_per_beat;
|
|
|
|
timenow_usec_updated = timenow_ticks; // usec version is updated based on the current tempo
|
|
|
|
if (loggen) {
|
|
|
|
if (earliest_time != last_earliest_time) {
|
|
|
|
fprintf(logfile, "->process trk %d at time %lu.%03lu msec (%lu ticks)\n",
|
|
|
|
tracknum, timenow_usec / 1000, timenow_usec % 1000, timenow_ticks);
|
|
|
|
last_earliest_time = earliest_time; } }
|
|
|
|
struct channel_status *cp = &channel[trk->chan]; // the channel info, if play or stop
|
|
|
|
|
|
|
|
if (trk->cmd == CMD_TEMPO) { // change the global tempo, which affects future usec computations
|
|
|
|
if (tempo != trk->tempo) {
|
|
|
|
++tempo_changes;
|
|
|
|
tempo = trk->tempo; }
|
|
|
|
if (loggen) fprintf(logfile, " tempo set to %ld usec/qnote\n", tempo);
|
|
|
|
find_next_note(tracknum); }
|
|
|
|
|
|
|
|
else { // should be PLAYNOTE or STOPNOTE
|
|
|
|
if (percussion_translate && trk->chan == PERCUSSION_TRACK)
|
|
|
|
trk->note += 128; // maybe move percussion notes up to 128..255
|
|
|
|
else { // shift notes as requested
|
|
|
|
trk->note += keyshift;
|
|
|
|
if (trk->note < 0) trk->note = 0;
|
|
|
|
if (trk->note > 127) trk->note = 127; }
|
|
|
|
|
|
|
|
if (trk->cmd == CMD_STOPNOTE) {
|
|
|
|
int ndx; // find the noteinfo for this note -- which better be playing -- in the channel status
|
|
|
|
for (ndx = 0; ndx < MAX_CHANNELNOTES; ++ndx) {
|
|
|
|
if (cp->note_playing[ndx]
|
|
|
|
&& cp->notes_playing[ndx].note == trk->note
|
|
|
|
&& cp->notes_playing[ndx].track == tracknum)
|
|
|
|
break; }
|
|
|
|
if (ndx >= MAX_CHANNELNOTES) {
|
|
|
|
++noteinfo_notfound; // presumably the array overflowed on input
|
|
|
|
if (loggen) fprintf(logfile, " *** noteinfo slot not found to stop track %d note %d (%02X) channel %d\n",
|
|
|
|
tracknum, trk->note, trk->note, trk->chan); }
|
|
|
|
else { // found the playing note for this stopnote
|
|
|
|
// Analyze the sustain and release parameters. We might generate another "note on"
|
|
|
|
// command with reduced volume, and/or move the stopnote command earlier than now.
|
|
|
|
struct noteinfo *np = &cp->notes_playing[ndx];
|
|
|
|
unsigned long duration_usec = timenow_usec - np->time_usec; // it has the start time in it
|
|
|
|
unsigned long truncation;
|
|
|
|
if (duration_usec <= notemin_usec) truncation = 0;
|
|
|
|
else if (duration_usec < releasetime_usec + notemin_usec) truncation = duration_usec - notemin_usec;
|
|
|
|
else truncation = releasetime_usec;
|
|
|
|
if (attacktime_usec > 0 && duration_usec < attacknotemax_usec) {
|
|
|
|
if (duration_usec - truncation > attacktime_usec) { // do a sustain phase
|
|
|
|
if ((np->volume = np->volume * sustainlevel_pct / 100) <= 0) np->volume = 1;
|
|
|
|
np->time_usec += attacktime_usec; // adjust time to be when sustain phase starts
|
|
|
|
queue_cmd(CMD_PLAYNOTE, np);
|
|
|
|
++sustainphases_done; }
|
|
|
|
else ++sustainphases_skipped; }
|
|
|
|
np->time_usec = timenow_usec - truncation; // adjust time to be when the note stops
|
|
|
|
queue_cmd(CMD_STOPNOTE, np);
|
|
|
|
cp->note_playing[ndx] = false; }
|
|
|
|
find_next_note(tracknum); }
|
|
|
|
|
|
|
|
else if (trk->cmd == CMD_PLAYNOTE) { // Process only one "start note", so other tracks get a chance at tone generators
|
|
|
|
int ndx; // find an unused noteinfo slot to use
|
|
|
|
for (ndx = 0; ndx < MAX_CHANNELNOTES; ++ndx) {
|
|
|
|
if (!cp->note_playing[ndx]) break; }
|
|
|
|
if (ndx >= MAX_CHANNELNOTES) {
|
|
|
|
++noteinfo_overflow; // too many simultaneous notes
|
|
|
|
if (loggen) fprintf(logfile, " *** no noteinfo slot to queue track %d note %d (%02X) channel %d\n",
|
|
|
|
tracknum, trk->note, trk->note, trk->chan);
|
|
|
|
show_noteinfo_slots(tracknum); }
|
|
|
|
else {
|
|
|
|
cp->note_playing[ndx] = true; // assign it to us
|
|
|
|
struct noteinfo *pn = &cp->notes_playing[ndx];
|
|
|
|
pn->time_usec = timenow_usec; // fill it in
|
|
|
|
pn->track = tracknum;
|
|
|
|
pn->channel = trk->chan;
|
|
|
|
pn->note = trk->note;
|
|
|
|
pn->instrument = cp->instrument;
|
|
|
|
pn->volume = trk->volume;
|
|
|
|
queue_cmd(CMD_PLAYNOTE, pn); }
|
|
|
|
find_next_note(tracknum); } // use up the note
|
|
|
|
|
|
|
|
else assert(false, "bad cmd in process_track_data"); } }
|
|
|
|
|
|
|
|
while (tracks_done < num_tracks);
|
|
|
|
|
|
|
|
// empty the output queue and generate the end-of-score command
|
|
|
|
flush_queue();
|
|
|
|
if (loggen) {
|
|
|
|
fprintf(logfile, "ending timenow_usec: %lu.%03lu\n", timenow_usec / 1000, timenow_usec % 1000);
|
|
|
|
fprintf(logfile, "ending output_usec: %lu.%03lu\n", output_usec / 1000, output_usec % 1000); }
|
|
|
|
assert(timenow_usec >= output_usec, "time deficit at end of song");
|
|
|
|
generate_delay((timenow_usec - output_usec) / 1000);
|
|
|
|
if (binaryoutput) {
|
|
|
|
putc(gen_restart ? CMD_RESTART : CMD_STOP, outfile);
|
|
|
|
outfile_bytecount +=1; }
|
|
|
|
else {
|
|
|
|
fprintf(outfile, "0x%02X};", gen_restart ? CMD_RESTART : CMD_STOP);
|
|
|
|
outfile_items(1);
|
|
|
|
fprintf(outfile, "\n"); } }
|
|
|
|
|
|
|
|
|
|
|
|
/********************* main ****************************/
|
|
|
|
|
|
|
|
int main (int argc, char *argv[]) {
|
|
|
|
int argno;
|
|
|
|
char *filebasename;
|
|
|
|
int basenamelen;
|
|
|
|
#define MAXPATH 120
|
|
|
|
char filename[MAXPATH];
|
|
|
|
|
|
|
|
printf ("MIDITONES V%s, (C) 2011-2021 Len Shustek\n", VERSION);
|
|
|
|
if (argc == 1) { // no arguments
|
|
|
|
SayUsage (argv[0]);
|
|
|
|
return 1; }
|
|
|
|
|
|
|
|
argno = HandleOptions (argc, argv); // process options
|
|
|
|
if (argno == 0) {
|
|
|
|
fprintf (stderr, "\n*** No basefilename given\n\n");
|
|
|
|
SayUsage (argv[0]);
|
|
|
|
exit (4); }
|
|
|
|
filebasename = argv[argno];
|
|
|
|
|
|
|
|
// strip off trailing .mid or .MID extension if provided by user
|
|
|
|
basenamelen = strlength(filebasename);
|
|
|
|
if (basenamelen > 4 &&
|
|
|
|
(charcmp (filebasename + basenamelen - 4, ".mid") ||
|
|
|
|
charcmp (filebasename + basenamelen - 4, ".MID"))) {
|
|
|
|
filebasename[basenamelen - 4] = 0; }
|
|
|
|
|
|
|
|
if (logparse || loggen) { // open the log file
|
|
|
|
miditones_strlcpy (filename, filebasename, MAXPATH);
|
|
|
|
miditones_strlcat (filename, ".log", MAXPATH);
|
|
|
|
logfile = fopen (filename, "w");
|
|
|
|
if (!logfile) {
|
|
|
|
fprintf (stderr, "Unable to open log file %s\n", filename);
|
|
|
|
return 1; }
|
|
|
|
fprintf (logfile, "MIDITONES V%s log file\n", VERSION);
|
|
|
|
print_command_line(logfile, argc, argv); }
|
|
|
|
if (loggen) {
|
|
|
|
fprintf(logfile, "\nThere are %d independent time-ordered streams in this log file:\n", logparse ? 3 : 2);
|
|
|
|
if (logparse) fprintf(logfile, " - the parsed MIDI events, marked with #\n");
|
|
|
|
fprintf(logfile, " - the MIDI play/stop events being queued, announced with ->\n"
|
|
|
|
" - the generated bytestream commands pulled from the queue, announced with <-\n\n"); }
|
|
|
|
|
|
|
|
// open the input file
|
|
|
|
miditones_strlcpy (filename, filebasename, MAXPATH);
|
|
|
|
miditones_strlcat (filename, ".mid", MAXPATH);
|
|
|
|
infile = fopen (filename, "rb");
|
|
|
|
if (!infile) {
|
|
|
|
fprintf (stderr, "Unable to open input file %s\n", filename);
|
|
|
|
return 1; }
|
|
|
|
|
|
|
|
// Read the whole input file into memory
|
|
|
|
fseek (infile, 0, SEEK_END); // find its size
|
|
|
|
buflen = ftell (infile);
|
|
|
|
fseek (infile, 0, SEEK_SET);
|
|
|
|
buffer = (byte *) malloc (buflen + 1);
|
|
|
|
if (!buffer) {
|
|
|
|
fprintf (stderr, "Unable to allocate %ld bytes for the file\n", buflen);
|
|
|
|
return 1; }
|
|
|
|
fread (buffer, buflen, 1, infile);
|
|
|
|
fclose (infile);
|
|
|
|
if (logparse) fprintf (logfile, "Processing %s, %ld bytes\n", filename, buflen);
|
|
|
|
|
|
|
|
if (!parseonly) { // create the output file
|
|
|
|
miditones_strlcpy (filename, filebasename, MAXPATH);
|
|
|
|
if (binaryoutput) {
|
|
|
|
miditones_strlcat (filename, ".bin", MAXPATH);
|
|
|
|
outfile = fopen (filename, "wb"); }
|
|
|
|
else {
|
|
|
|
miditones_strlcat (filename, scorename ? ".h" : ".c", MAXPATH);
|
|
|
|
outfile = fopen (filename, "w"); }
|
|
|
|
if (!outfile) {
|
|
|
|
fprintf (stderr, "Unable to open output file %s\n", filename);
|
|
|
|
return 1; }
|
|
|
|
file_header.f1 = (volume_output ? HDR_F1_VOLUME_PRESENT : 0)
|
|
|
|
| (instrumentoutput ? HDR_F1_INSTRUMENTS_PRESENT : 0)
|
|
|
|
| (percussion_translate ? HDR_F1_PERCUSSION_PRESENT : 0);
|
|
|
|
file_header.num_tgens = num_tonegens;
|
|
|
|
if (!binaryoutput) { /* create header of C file that initializes score data */
|
|
|
|
time_t rawtime;
|
|
|
|
time (&rawtime);
|
|
|
|
fprintf (outfile, "// Playtune bytestream for file \"%s.mid\" ", filebasename);
|
|
|
|
fprintf (outfile, "created by MIDITONES V%s on %s", VERSION,
|
|
|
|
asctime (localtime (&rawtime)));
|
|
|
|
print_command_line (outfile, argc, argv);
|
|
|
|
if (channel_mask != 0xffff)
|
|
|
|
fprintf (outfile, "// Only the masked channels were processed: %04X\n", channel_mask);
|
|
|
|
if (keyshift != 0)
|
|
|
|
fprintf (outfile, "// Keyshift was %d chromatic notes\n", keyshift);
|
|
|
|
if (define_progmem) {
|
|
|
|
fprintf (outfile, "#ifdef __AVR__\n");
|
|
|
|
fprintf (outfile, "#include <avr/pgmspace.h>\n");
|
|
|
|
fprintf (outfile, "#else\n");
|
|
|
|
fprintf (outfile, "#define PROGMEM\n");
|
|
|
|
fprintf (outfile, "#endif\n"); }
|
|
|
|
fprintf (outfile, "const unsigned char PROGMEM %s [] = {\n",
|
|
|
|
scorename ? filebasename : "score");
|
|
|
|
if (do_header) { // write the C initialization for the file header
|
|
|
|
fprintf (outfile, "'P','t', 6, 0x%02X, 0x%02X, ", file_header.f1, file_header.f2);
|
|
|
|
fflush (outfile);
|
|
|
|
file_header_num_tgens_position = ftell (outfile); // remember where the number of tone generators is
|
|
|
|
fprintf (outfile, "%2d, // (Playtune file header)\n", file_header.num_tgens);
|
|
|
|
outfile_bytecount += 6; } }
|
|
|
|
else if (do_header) { // write the binary file header
|
|
|
|
int i;
|
|
|
|
for (i = 0; i < sizeof (file_header); ++i)
|
|
|
|
fputc (((byte *) &file_header)[i], outfile);
|
|
|
|
file_header_num_tgens_position = (char *) &file_header.num_tgens - (char *) &file_header;
|
|
|
|
outfile_bytecount += sizeof (file_header); } }
|
|
|
|
|
|
|
|
// process the MIDI file header
|
|
|
|
hdrptr = buffer; // point to the file and track headers
|
|
|
|
process_file_header ();
|
|
|
|
printf (" Processing %d tracks.\n", num_tracks);
|
|
|
|
if (num_tracks > MAX_TRACKS) midi_error ("Too many tracks", buffer);
|
|
|
|
|
|
|
|
// initialize for processing of all the tracks
|
|
|
|
tempo = DEFAULT_TEMPO;
|
|
|
|
for (int tracknum = 0; tracknum < num_tracks; ++tracknum) {
|
|
|
|
track[tracknum].tempo = DEFAULT_TEMPO;
|
|
|
|
process_track_header (tracknum);
|
|
|
|
find_next_note (tracknum); /* position to the first note on/off */
|
|
|
|
/* if we are in "parse only" mode, do the whole track,
|
|
|
|
so we do them one at a time instead of time-synchronized. */
|
|
|
|
if (parseonly)
|
|
|
|
while (track[tracknum].cmd != CMD_TRACKDONE)
|
|
|
|
find_next_note (tracknum); }
|
|
|
|
#if 0
|
|
|
|
// TEMP test queuing routines
|
|
|
|
show_queue_cmd(12, CMD_PLAYNOTE, 100);
|
|
|
|
show_queue_cmd(12, CMD_PLAYNOTE, 101);
|
|
|
|
show_queue_cmd(12, CMD_PLAYNOTE, 103);
|
|
|
|
show_queue_cmd(20, CMD_STOPNOTE, 101);
|
|
|
|
show_queue_cmd(15, CMD_STOPNOTE, 103);
|
|
|
|
show_queue_cmd(21, CMD_PLAYNOTE, 104);
|
|
|
|
show_queue_cmd(20, CMD_STOPNOTE, 100);
|
|
|
|
show_queue_cmd(20, CMD_STOPNOTE, 104);
|
|
|
|
show_queue_cmd(22, CMD_PLAYNOTE, 109);
|
|
|
|
flush_queue();
|
|
|
|
#endif
|
|
|
|
if (!parseonly) {
|
|
|
|
|
|
|
|
process_track_data(); // do all the tracks interleaved, like a 1950's multiway merge
|
|
|
|
|
|
|
|
// generate the ending commentary
|
|
|
|
if (!binaryoutput) {
|
|
|
|
fprintf(outfile, "\n// This %ld byte score contains %d notes and uses %d tone generator%s\n",
|
|
|
|
outfile_bytecount, note_on_commands, num_tonegens_used,
|
|
|
|
num_tonegens_used == 1 ? "" : "s");
|
|
|
|
if (notes_skipped)
|
|
|
|
fprintf(outfile, "// %d notes had to be skipped\n", notes_skipped); }
|
|
|
|
printf(" %s %d tone generators were used.\n",
|
|
|
|
num_tonegens_used < num_tonegens ? "Only" : "All", num_tonegens_used);
|
|
|
|
if (notes_skipped)
|
|
|
|
printf(" %d notes were skipped because there weren't enough tone generators.\n",
|
|
|
|
notes_skipped);
|
|
|
|
if (consecutive_delays)
|
|
|
|
printf(" %d consecutive delays could be eliminated\n", consecutive_delays);
|
|
|
|
if (events_delayed)
|
|
|
|
printf(" %d \"stop note\" commands were delayed because the %d-element output queue is too small\n",
|
|
|
|
events_delayed, QUEUE_SIZE);
|
|
|
|
if (noteinfo_overflow + noteinfo_notfound > 0)
|
|
|
|
printf(" %d notes couldn't be recorded in the track status, so then %d notes couldn't be found\n"
|
|
|
|
" (Consider recompiling with MAX_TRACKNOTES bigger than %d, to allow more simultaneous notes.)\n",
|
|
|
|
noteinfo_overflow, noteinfo_notfound, MAX_CHANNELNOTES);
|
|
|
|
printf(" %ld bytes of score data were generated, ", outfile_bytecount);
|
|
|
|
printf("representing %u.%03u seconds of music with %d tempo changes\n",
|
|
|
|
(unsigned)(timenow_usec / 1000000), (unsigned)(timenow_usec / 1000 % 1000), tempo_changes);
|
|
|
|
if (delaymin_usec)
|
|
|
|
printf(" %ld delays were removed because the minimum delay of %u msec caused events to be merged\n",
|
|
|
|
delays_saved, (unsigned)(delaymin_usec / 1000));
|
|
|
|
if (loggen) {
|
|
|
|
fprintf(logfile, "%d note-on commands, %d instrument changes.\n",
|
|
|
|
note_on_commands, instrument_changes);
|
|
|
|
fprintf(logfile, "%d stop-notes without start-notes, %d start-notes without stop-notes\n",
|
|
|
|
stopnotes_without_playnotes, playnotes_without_stopnotes);
|
|
|
|
if (attacktime_usec > 0) fprintf(logfile, "%d sustain phases done, and %d skipped because the notes were too short\n",
|
|
|
|
sustainphases_done, sustainphases_skipped); }
|
|
|
|
if (0 && do_header) { // rewrite the file header with the actual number of tone generators used
|
|
|
|
if (fseek(outfile, file_header_num_tgens_position, SEEK_SET) != 0)
|
|
|
|
fprintf(stderr, "Can't seek to number of tone generators in the header\n");
|
|
|
|
else {
|
|
|
|
if (binaryoutput)
|
|
|
|
putc(num_tonegens_used, outfile);
|
|
|
|
else
|
|
|
|
fprintf(outfile, "%2d", num_tonegens_used); } }
|
|
|
|
fclose(outfile); }
|
|
|
|
|
|
|
|
if (loggen || logparse)
|
|
|
|
fclose (logfile);
|
|
|
|
printf (" Done.\n");
|
|
|
|
return 0; }
|