You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
miditones/miditones.c

1821 lines
88 KiB

/*********************************************************************************************
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, &notedata);
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; }