1328 lines
47 KiB
C++
1328 lines
47 KiB
C++
//
|
|
// DRO2MIDI - Convert DOSBox raw OPL captures (.dro), Rdos (.raw) and id
|
|
// Software (.imf, .wlf) into MIDI files (.mid)
|
|
//
|
|
// Created by malvineous@shikadi.net in June 2007. See README for license.
|
|
// Based on imf2midi v1.0 written by Guenter Nagler in 1996 (gnagler@ihm.tu-graz.ac.at)
|
|
//
|
|
// v1.0 / 2007-06-16 / malvineous@shikadi.net: Original release
|
|
// - imf2midi with .imf reader hacked to read .dro files instead
|
|
//
|
|
// v1.1 / 2007-07-28 / malvineous@shikadi.net: More file formats
|
|
// - Added .imf and .raw support.
|
|
// - Replaced Guenter's OPL -> MIDI frequency conversion algorithm (from a
|
|
// lookup table into a much more accurate formula), consequently was able
|
|
// to simplify pitchbend code (now conversions with pitchbends enabled
|
|
// sound quite good!)
|
|
//
|
|
// v1.2 / 2007-07-28 / malvineous@shikadi.net: Bugfix release
|
|
// - Fixed some file length calculations causing some files to be converted
|
|
// without any notes.
|
|
// - Added portamento-to-note for large (>2 semitone) pitchbends, but it
|
|
// doesn't seem to work when using Timidity.
|
|
//
|
|
// v1.3 / 2007-09-02 / malvineous@shikadi.net: New features
|
|
// - Fixed "tom tom" incorrectly called "bass drum" in output messages.
|
|
// - Fixed multi-note pitchbends by removing portamento-to-note and
|
|
// adjusting standard pitchbend range instead, thanks to a suggestion
|
|
// by Xky (xkyrauh2001@hotmail.com)
|
|
// - Implemented a better method for reading Adlib register -> MIDI patch
|
|
// mapping information (all stored in inst.txt now instead of having a
|
|
// seperate file for each instrument.) Also improved method for mapping
|
|
// instruments to percussion on MIDI channel 10.
|
|
// - Fixed OPL rhythm instrument conversion issue (a MIDI noteon was being
|
|
// generated too often - if the OPL instrument is on and we receive
|
|
// another keyon, it *shouldn't* generate a fresh MIDI keyon.)
|
|
// - Fixed IMF type-1 conversion issue where unsigned numbers were being
|
|
// read as signed, and the conversion was cutting off half way through.
|
|
//
|
|
// v1.4 / 2009-03-28 / malvineous@shikadi.net
|
|
// - Some code cleanup, fixed all the warnings in midiio.cpp.
|
|
// - Fixed a bunch of char/unsigned char issues, hopefully Win32
|
|
// conversions will now be as reliable as under Linux.
|
|
// - Added line numbers to instrument names and mapping file error messages.
|
|
// - Added new instrument mapping entries for rhythm mode instruments
|
|
// (which previously were hard coded.)
|
|
// - Added transpose and mute options to instrument mapping file.
|
|
// - Added -c option to change OPL constant during conversion, thanks to a
|
|
// suggestion from Wraithverge (liam82067@yahoo.com)
|
|
// - Added -v option to disable note volume (helps with Stunts which
|
|
// otherwise comes out with no notes because they're all silent.)
|
|
// - Corrected OPL volume -> MIDI note velocity algorithm, and added hard
|
|
// limit to prevent notes from having a zero velocity (which stops them
|
|
// from being converted, like with Stunts.)
|
|
// - New instrument mappings (to be copied into insts.txt) are printed to
|
|
// stderr, so "dro2midi 2>> insts.txt" will conveniently append them all
|
|
// to the mapping file. Thanks to Wraithverge for the idea.
|
|
// - Replaced getop() and getchannel() lookup functions with GET_OP() and
|
|
// GET_CHANNEL() macro algorithms to calculate the values as required.
|
|
// - Included 128 standard GM instrument mappings extracted from Creative
|
|
// Labs' MIDI player (see gen_test_midi.cpp.)
|
|
//
|
|
// v1.5 / 2010-03-28 / Wraithverge (liam82067 at yahoo dot com): Changes
|
|
// - Added code for several MIDI Controller Events for output MID-files,
|
|
// and they are the following:
|
|
// Reset Controllers [121] -- At the head of the Event list.
|
|
// Balance (or Pan) [10] -- At the head of the Event list.
|
|
// Expression [11] -- At the head of the Event list.
|
|
// All Notes Off [123] -- At the foot of the Event list.
|
|
// Hopefully, other musicians will find these to be needed, as well.
|
|
// - Added detectors for the (at this time) two DRO formats, and a hint
|
|
// to let us know that the DRO v2.0 format is not supported.
|
|
//
|
|
// v1.6 / 2013-11-14 / bsa (http://bsutherland.github.io/JuceOPLVSTi)
|
|
// - Added -s switch to save detected instruments in Creative Sound
|
|
// Blaster Instrument format (.sbi)
|
|
//
|
|
|
|
#define VERSION "1.6"
|
|
#define MAPPING_FILE "inst.txt"
|
|
|
|
#define PATCH_NAME_FILE "patch.txt"
|
|
#define PERC_NAME_FILE "drum.txt"
|
|
#define NUM_MIDI_PATCHES 128 // 128 MIDI instruments
|
|
#define NUM_MIDI_PERC 128 // 46 MIDI percussive notes (channel 10), but 128 possible notes
|
|
#define INSTR_NAMELEN 32 // Maximum length of an instrument name
|
|
|
|
//#define PITCHBEND_RANGE 12.0 // 12 == pitchbends can go up to a full octave
|
|
#define PITCHBEND_RANGE 24.0 // 24 == pitchbends can go up two full octaves
|
|
#define PITCHBEND_ONESEMITONE (8192.0 / PITCHBEND_RANGE)
|
|
const double pitchbend_center = 8192.0;
|
|
|
|
#include "midiio.hpp"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <assert.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
|
|
#define WRITE_BINARY "wb"
|
|
#define READ_TEXT "r"
|
|
|
|
#ifdef _MSC_VER
|
|
// Keep MS VC++ happy
|
|
#define strncasecmp _strnicmp
|
|
#define strcasecmp _stricmp
|
|
#define snprintf _snprintf
|
|
#define log2(x) (log(x) / log(2.))
|
|
inline double round( double d )
|
|
{
|
|
return floor( d + 0.5 );
|
|
}
|
|
#endif
|
|
|
|
bool bRhythm = true; // convert rhythm mode instruments (-r)
|
|
bool bUsePitchBends = true; // use pitch bends to better match MIDI note frequency with the OPL frequency (-p)
|
|
bool bApproximatePitchbends = false; // if pitchbends are disabled, should we approximate them by playing the nearest note when the pitch changes?
|
|
bool bPerfectMatchesOnly = false; // if true, only match perfect instruments
|
|
bool bEnableVolume = true; // enable note velocity based on OPL instrument volume
|
|
bool bWriteSbiInstruments = false; // write detected instruments to .SBI files
|
|
|
|
// Rhythm instruments
|
|
enum RHYTHM_INSTRUMENT {
|
|
NormalInstrument,
|
|
BassDrum,
|
|
SnareDrum,
|
|
TomTom,
|
|
TopCymbal,
|
|
HiHat
|
|
};
|
|
|
|
// MIDI channels to use for these instruments when they are mapped as normal
|
|
// notes. Probably best not to use 1-10 as these are used by the rest of the
|
|
// OPL mapping code. Note these are zero-based, so the GM drum channel is
|
|
// channel 9 in this context.
|
|
#define CHAN_BASSDRUM 10 // Bass drum sits on OPL channel 7
|
|
#define CHAN_SNAREDRUM 11 // OPL channel 8 carrier
|
|
#define CHAN_TOMTOM 12 // OPL channel 9 modulator
|
|
#define CHAN_TOPCYMBAL 13 // OPL channel 9 carrier
|
|
#define CHAN_HIHAT 14 // OPL channel 8 modulator
|
|
|
|
char cPatchName[NUM_MIDI_PATCHES][INSTR_NAMELEN];
|
|
char cPercName[NUM_MIDI_PERC][INSTR_NAMELEN];
|
|
|
|
char* input = 0;
|
|
char* output = 0;
|
|
|
|
FILE* f = 0; // input file
|
|
MidiWrite* write = 0;
|
|
|
|
//int resolution = 384; // 560Hz IMF
|
|
int resolution = 500; // 1000Hz DRO
|
|
int program = 0;
|
|
float tempo = 120.0;
|
|
|
|
// Arrays of [9] refer to OPL channels, arrays of [16] also refer to OPL
|
|
// channels but use fake channels (11-15) for rhythm mode instruments (but
|
|
// only for elements like keyon and instrument mapping that can happen
|
|
// independently of the OPL channel in use. Things like OPL channel pitch
|
|
// which affect both rhythm mode instruments using that channel are not
|
|
// stored separately, i.e. they're in the [9] array.)
|
|
int mapchannel[16];
|
|
int curfreq[9];
|
|
bool keyAlreadyOn[16];
|
|
int lastkey[16]; // last MIDI key pressed on this channel
|
|
int pitchbent[16];
|
|
int transpose[16]; // used for instruments with mapped transpose values
|
|
int drumnote[16]; // note to play on MIDI channel 10 if Adlib channel has a
|
|
// percussive instrument assigned to it
|
|
int lastprog[16]; // last program/patch set on the MIDI channel
|
|
bool mute[16]; // true if the instrument on this channel is currently muted
|
|
|
|
// Statistics
|
|
int iNotesActive = 0;
|
|
int iPitchbendCount = 0;
|
|
int iTotalNotes = 0;
|
|
|
|
typedef struct
|
|
{
|
|
/*unsigned char reg20[2];
|
|
unsigned char reg40[2];
|
|
unsigned char reg60[2];
|
|
unsigned char reg80[2];
|
|
unsigned char regC0;
|
|
unsigned char regE0[2];*/
|
|
|
|
unsigned int reg20[2];
|
|
unsigned int reg40[2];
|
|
unsigned int reg60[2];
|
|
unsigned int reg80[2];
|
|
unsigned int regC0;
|
|
unsigned int regE0[2];
|
|
|
|
// Is this a normal instrument or a mapping for a rhythm instrument?
|
|
RHYTHM_INSTRUMENT eRhythmInstrument;
|
|
|
|
int prog; // MIDI patch (or -1 if a drum)
|
|
int isdrum;
|
|
int note; // note to play if drum
|
|
bool muted; // true if this instrument is muted
|
|
char name[128];
|
|
|
|
signed int iTranspose; // number of semitones to transpose this instrument
|
|
|
|
// The instrument can be redirected to another. This is used when a new
|
|
// instrument is encountered - its info is printed to the screen then it's
|
|
// recorded (pointing to its best match) to prevent it appearing as a new
|
|
// instrument hundreds of times and cluttering the output.
|
|
int redirect; // if >= 0, use ::instr[redirect] instead of this one
|
|
|
|
// When using this struct to cache current channel parms we need to
|
|
// remember the octave, so that when registers 0xA0-0xA8 are changed
|
|
// (to change note frequency) we can still pass the octave to the note
|
|
// conversion function (as the octave is otherwise only available when
|
|
// setting registers 0xB0-0xB8.)
|
|
int iOctave;
|
|
} INSTRUMENT;
|
|
|
|
INSTRUMENT reg[9]; // current registers of channel
|
|
|
|
#define MAXINSTR 2048
|
|
int instrcnt = 0;
|
|
INSTRUMENT instr[MAXINSTR];
|
|
|
|
int iFormat = 0; // input format
|
|
#define FORMAT_IMF 1
|
|
#define FORMAT_DRO 2
|
|
#define FORMAT_RAW 3
|
|
int iSpeed = 0; // clock speed (in Hz)
|
|
int iInitialSpeed = 0; // first iSpeed value written to MIDI header
|
|
|
|
double dbConversionVal;
|
|
// Convert the given OPL F-num and octave values into a fractional MIDI note
|
|
// number (for the use of pitchbends.)
|
|
double freq2key(int freq, int octave)
|
|
{
|
|
int iFNum = freq;
|
|
int iBlock = octave;
|
|
double dbOriginalFreq = dbConversionVal * (double)iFNum * pow(2, (double)(iBlock - 20));
|
|
return 69.0 + 12.0 * log2(dbOriginalFreq / 440.0);
|
|
}
|
|
|
|
void version()
|
|
{
|
|
printf("DRO2MIDI v" VERSION " - Convert raw Adlib captures to General MIDI\n"
|
|
"Written by malvineous@shikadi.net in 2007\n"
|
|
"Heavily based upon IMF2MIDI written by Guenter Nagler in 1996\n"
|
|
"With contributions by Wraithverge (C) 2010, bsa in 2013\n"
|
|
"http://www.shikadi.net/utils/\n"
|
|
"\n"
|
|
);
|
|
return;
|
|
}
|
|
|
|
void usage()
|
|
{
|
|
version();
|
|
fprintf(stderr,
|
|
"Usage: dro2midi [-p [-a]] [-r] [-i] [-c alt|<num>] [-v] input.dro output.mid\n"
|
|
"\n"
|
|
"Where:\n"
|
|
" -p Disable use of MIDI pitch bends\n"
|
|
" -a If pitchbends are disabled, approximate by playing the nearest note\n"
|
|
" -r Don't convert OPL rhythm-mode instruments\n"
|
|
" -i Only use instruments that match perfectly (default is 'close enough is\n"
|
|
" good enough.') Useful when guessing new patches. Instruments that can't\n"
|
|
" be matched use the first entry in " MAPPING_FILE " (piano by default)\n"
|
|
" -c Change the value used to convert OPL notes into MIDI notes from the\n"
|
|
" default 49716. Any non-zero value can be specified. The special word\n"
|
|
" \"alt\" means 50000, the other commonly used value. It is unlikely any\n"
|
|
" other values will need to be used, unless you get an excessive amount\n"
|
|
" of artificial pitchbends in the output MIDI.\n"
|
|
" -v Disable note velocity and play all notes as loudly as possible,\n"
|
|
" instead of trying to match the volume of the OPL note.\n"
|
|
" -s Write detected instruments to .sbi files\n"
|
|
" (Creative Sound Blaster Instrument).\n"
|
|
"\n"
|
|
"Supported input formats:\n"
|
|
" .raw Rdos RAW OPL capture\n"
|
|
" .dro DOSBox RAW OPL capture\n"
|
|
" .imf id Software Music Format (type-0 and type-1 at 560Hz)\n"
|
|
" .wlf id Software Music Format (type-0 and type-1 at 700Hz)\n"
|
|
"\n"
|
|
"Instrument definitions are read in from " MAPPING_FILE ". Instrument names\n"
|
|
"are read in from " PATCH_NAME_FILE " and " PERC_NAME_FILE ". See the README for more details.\n"
|
|
);
|
|
exit(1);
|
|
}
|
|
|
|
/* Channel 1 2 3 4 5 6 7 8 9
|
|
* Operator 1 00 01 02 08 09 0A 10 11 12 } cells
|
|
* Operator 2 03 04 05 0B 0C 0D 13 14 15 }
|
|
*/
|
|
// Converts a cell into a channel
|
|
#define GET_CHANNEL(i) ((((i) / 8) * 3) + (((i) % 8) % 3))
|
|
|
|
// Converts a cell into 0 (for operator 1) or 1 (for operator 2)
|
|
#define GET_OP(i) (((i) % 8) / 3)
|
|
|
|
// Helper functions to read data from files in a non-optimised but platform
|
|
// independent (little/big endian) way.
|
|
inline unsigned char readByte(FILE *f)
|
|
{
|
|
unsigned char c;
|
|
fread(&c, 1, 1, f);
|
|
return c;
|
|
}
|
|
inline unsigned short readUINT16LE(FILE *f)
|
|
{
|
|
unsigned char c[2];
|
|
fread(&c, 1, 2, f);
|
|
return c[0] | (c[1] << 8L);
|
|
}
|
|
inline unsigned long readUINT32LE(FILE *f)
|
|
{
|
|
unsigned char c[4];
|
|
fread(&c, 1, 4, f);
|
|
return c[0] | (c[1] << 8L) | (c[2] << 16L) | (c[3] << 24L);
|
|
}
|
|
|
|
bool loadInstruments(void)
|
|
{
|
|
for (int i = 0; i < NUM_MIDI_PATCHES; i++) sprintf(cPatchName[i], "Patch #%d", i+1);
|
|
for (int i = 0; i < NUM_MIDI_PERC; i++) sprintf(cPercName[i], "Note #%d", i);
|
|
|
|
char line[256];
|
|
|
|
FILE* p = fopen(PATCH_NAME_FILE, "r");
|
|
if (!p) {
|
|
fprintf(stderr, "Warning: Unable to open file listing patch names ("
|
|
PATCH_NAME_FILE ")\nInstrument names will not be available.\n");
|
|
} else {
|
|
while (fgets(line, sizeof(line)-1, p)) {
|
|
int iValue, iLen;
|
|
char *p = strpbrk(line, "\n\r");
|
|
if (p) *p = '\0'; // terminate the string at the newline
|
|
if (sscanf(line, "%d=%n", &iValue, &iLen) == 1) {
|
|
assert(iValue <= NUM_MIDI_PATCHES);
|
|
snprintf(cPatchName[iValue-1], INSTR_NAMELEN, "%s [%d]", &line[iLen], iValue);
|
|
} else if ((line[0] != '#') && (line[0] != '\n')) {
|
|
fprintf(stderr, "Invalid line in " PATCH_NAME_FILE ": %s\n", line);
|
|
}
|
|
}
|
|
fclose(p);
|
|
}
|
|
|
|
p = fopen(PERC_NAME_FILE, "r");
|
|
if (!p) {
|
|
fprintf(stderr, "Warning: Unable to open file listing percussion note "
|
|
"names (" PERC_NAME_FILE ")\nPercussion names will not be available.\n");
|
|
} else {
|
|
while (fgets(line, sizeof(line)-1, p)) {
|
|
int iValue, iLen;
|
|
char *p = strpbrk(line, "\n\r");
|
|
if (p) *p = '\0'; // terminate the string at the newline
|
|
if (sscanf(line, "%d=%n", &iValue, &iLen) == 1) {
|
|
assert(iValue <= NUM_MIDI_PERC);
|
|
snprintf(cPercName[iValue], INSTR_NAMELEN, "%s [%d]", &line[iLen], iValue);
|
|
} else if ((line[0] != '#') && (line[0] != '\n')) {
|
|
fprintf(stderr, "Invalid line in " PERC_NAME_FILE ": %s\n", line);
|
|
}
|
|
}
|
|
fclose(p);
|
|
}
|
|
|
|
FILE* f = fopen(MAPPING_FILE, "r");
|
|
if (!f) {
|
|
fprintf(stderr, "Warning: Unable to open instrument mapping file "
|
|
MAPPING_FILE ", defaulting to a Grand Piano\nfor all instruments.\n");
|
|
return true;
|
|
}
|
|
INSTRUMENT in;
|
|
memset(&in, 0, sizeof(in));
|
|
in.redirect = -1; // none of these should redirect (but later automatic
|
|
// instruments will redirect to these ones)
|
|
|
|
// Loop until we run out of lines in the data file or we hit the maximum
|
|
// number of instruments
|
|
char value[256];
|
|
int iLineNum = 0;
|
|
for (::instrcnt = 0; fgets(line, sizeof(line)-1, f) && (instrcnt < MAXINSTR);) {
|
|
iLineNum++;
|
|
|
|
// Ignore blank lines and comments
|
|
if ((line[0] == '#') || (line[0] == '\r') || (line[0] == '\n')) continue;
|
|
|
|
// Figure out what type of rhythm mode instrument (if any) the first two
|
|
// chars are referring to.
|
|
char cInstType[3];
|
|
int iNumFields = sscanf(line, "%2s ", cInstType);
|
|
if ((cInstType[0] == 'N') && (cInstType[1] == 'O')) in.eRhythmInstrument = NormalInstrument;
|
|
else if ((cInstType[0] == 'B') && (cInstType[1] == 'D')) in.eRhythmInstrument = BassDrum;
|
|
else if ((cInstType[0] == 'S') && (cInstType[1] == 'D')) in.eRhythmInstrument = SnareDrum;
|
|
else if ((cInstType[0] == 'T') && (cInstType[1] == 'T')) in.eRhythmInstrument = TomTom;
|
|
else if ((cInstType[0] == 'T') && (cInstType[1] == 'C')) in.eRhythmInstrument = TopCymbal;
|
|
else if ((cInstType[0] == 'H') && (cInstType[1] == 'H')) in.eRhythmInstrument = HiHat;
|
|
else {
|
|
fprintf(stderr, "Invalid instrument type \"%s\" on line %d:\n\n %s\n",
|
|
cInstType, iLineNum, line);
|
|
return false;
|
|
}
|
|
|
|
switch (in.eRhythmInstrument) {
|
|
case NormalInstrument:
|
|
case BassDrum:
|
|
// Normal instrument or rhythm Bass Drum, read both
|
|
// operators + connection byte
|
|
iNumFields = sscanf(&line[3], "%02X-%02X/%02X-%02X/%02X-%02X/%02X-%02X"
|
|
"/%02X/%02X-%02X: %255c\n",
|
|
&in.reg20[0], &in.reg20[1],
|
|
&in.reg40[0], &in.reg40[1],
|
|
&in.reg60[0], &in.reg60[1],
|
|
&in.reg80[0], &in.reg80[1],
|
|
&in.regC0,
|
|
&in.regE0[0], &in.regE0[1], value);
|
|
if (iNumFields != 12) {
|
|
fprintf(stderr, "Unable to parse line %d: (expected 12 instrument "
|
|
"fields, got %d)\n\n%s\n", iLineNum, iNumFields, line);
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case TomTom:
|
|
case HiHat:
|
|
// This instrument is one operator only, but it does use the connection
|
|
// byte (probably)
|
|
iNumFields = sscanf(&line[3], "%02X/%02X/%02X/%02X/%02X/%02X: %255c\n",
|
|
&in.reg20[0],
|
|
&in.reg40[0],
|
|
&in.reg60[0],
|
|
&in.reg80[0],
|
|
&in.regC0,
|
|
&in.regE0[0], value);
|
|
if (iNumFields != 7) {
|
|
fprintf(stderr, "Unable to parse line %d: (expected 7 instrument "
|
|
"fields, got %d)\n\n%s\n", iLineNum, iNumFields, line);
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case SnareDrum:
|
|
case TopCymbal:
|
|
// This instrument does not uses the connection byte, so read in one byte
|
|
// less. Also read the values into the other operator.
|
|
iNumFields = sscanf(&line[3], "%02X/%02X/%02X/%02X/%02X: %255c\n",
|
|
&in.reg20[1],
|
|
&in.reg40[1],
|
|
&in.reg60[1],
|
|
&in.reg80[1],
|
|
&in.regE0[1], value);
|
|
if (iNumFields != 6) {
|
|
fprintf(stderr, "Unable to parse line %d: (expected 6 instrument "
|
|
"fields, got %d)\n\n%s\n", iLineNum, iNumFields, line);
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Default options
|
|
in.isdrum = 0;
|
|
in.prog = 1;
|
|
in.iTranspose = 0;
|
|
in.muted = false;
|
|
|
|
// If we got this far it's a valid instrument
|
|
int iValue;
|
|
char nextopt[256];
|
|
// We need to manually terminate the %255c in the sscanf() above, so do
|
|
// this by terminating it at the end-of-line char. Hopefully reading past
|
|
// the end of the input string (which causes this) won't crash things...
|
|
for (int i = 0; i < 256; i++) {
|
|
if (value[i] == '\n') value[i] = 0;
|
|
else if (value[i] == '\0') break;
|
|
}
|
|
value[255] = 0;
|
|
|
|
const char *cList = value;
|
|
int iLen;
|
|
while (sscanf(cList, "%s%n", nextopt, &iLen) >= 1) {
|
|
cList += iLen;
|
|
if (nextopt[0] == '#') break; // reached an end of line comment
|
|
if (nextopt[0] == ' ') continue; // skip double spaces
|
|
if (sscanf(nextopt, "patch=%d", &iValue) == 1) {
|
|
// MIDI patch
|
|
in.isdrum = 0;
|
|
in.prog = iValue - 1;
|
|
if ((in.prog < 0) || (in.prog > 127)) {
|
|
fprintf(stderr, "ERROR: Instrument #%d (line %d) was set to "
|
|
"patch=%d, but this value must be between 1 and 128 inclusive.\n",
|
|
instrcnt, iLineNum, in.prog + 1);
|
|
return false;
|
|
}
|
|
} else if (sscanf(nextopt, "drum=%d", &iValue) == 1) {
|
|
// MIDI drum
|
|
in.isdrum = 1;
|
|
in.prog = -1;
|
|
in.note = iValue;
|
|
if ((in.note < 0) || (in.note > 127)) {
|
|
fprintf(stderr, "ERROR: Drum instrument #%d (line %d) was set to "
|
|
"drum=%d, but this value must be between 1 and 128 inclusive.\n",
|
|
instrcnt, iLineNum, in.note);
|
|
return false;
|
|
}
|
|
} else if (sscanf(nextopt, "transpose=%d", &iValue) == 1) {
|
|
// MIDI drum
|
|
in.iTranspose = iValue;
|
|
} else if (strcmp(nextopt, "mute") == 0) {
|
|
// Mute this instrument
|
|
in.muted = true;
|
|
} else {
|
|
fprintf(stderr, "Unknown instrument option on line %d: %s\n",
|
|
iLineNum, nextopt);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
char cInstTypeText[256];
|
|
switch (in.eRhythmInstrument) {
|
|
case NormalInstrument: cInstTypeText[0] = '\0'; break;
|
|
case BassDrum: strcpy(cInstTypeText, "(OPL BD) "); break;
|
|
case TomTom: strcpy(cInstTypeText, "(OPL TT) "); break;
|
|
case HiHat: strcpy(cInstTypeText, "(OPL HH) "); break;
|
|
case SnareDrum: strcpy(cInstTypeText, "(OPL SD) "); break;
|
|
case TopCymbal: strcpy(cInstTypeText, "(OPL TC) "); break;
|
|
}
|
|
sprintf(in.name, "Inst#%03d %s@ line %3d%s: %s", instrcnt,
|
|
cInstTypeText,
|
|
iLineNum,
|
|
(in.isdrum) ? " (perc)" : "",
|
|
(in.isdrum) ? cPercName[in.note] : cPatchName[in.prog]
|
|
);
|
|
if (in.iTranspose) {
|
|
char cTranspose[256];
|
|
sprintf(cTranspose, " @ %+d semitones", in.iTranspose);
|
|
strcat(in.name, cTranspose);
|
|
}
|
|
if (in.muted) strcat(in.name, " {muted}");
|
|
//printf("%s\n", in.name);
|
|
|
|
// Add instrument
|
|
::instr[instrcnt++] = in;
|
|
}
|
|
fclose(f);
|
|
return true;
|
|
}
|
|
|
|
long difference(int a, int b, int importance = 1)
|
|
{
|
|
long diff = a - b;
|
|
if (diff < 0) diff = -diff;
|
|
return diff * importance;
|
|
}
|
|
|
|
long compareinstr(INSTRUMENT& a, INSTRUMENT& b, RHYTHM_INSTRUMENT ri)
|
|
{
|
|
// Note that we're not using b.eRhythmInstrument below as "b" refers to the
|
|
// OPL channel, and this never has an instrument type set. The instrument
|
|
// type we want is passed in as "ri", so we compare against that instead.
|
|
//fprintf(stderr, "%d ", ri);
|
|
switch (ri) {
|
|
case NormalInstrument:
|
|
case BassDrum:
|
|
// Compare the full register set (both operators plus the connection
|
|
// byte) for normal instruments (which occupy a whole channel) and the
|
|
// one rhythm mode instrument which also occupies a whole channel.
|
|
return
|
|
difference(a.eRhythmInstrument, ri, 4) +
|
|
difference(a.reg20[0], b.reg20[0], 2) +
|
|
difference(a.reg20[1], b.reg20[1], 2) +
|
|
difference(a.reg40[0], b.reg40[0], 1) +
|
|
difference(a.reg40[1], b.reg40[1], 1) +
|
|
difference(a.reg60[0], b.reg60[0], 2) +
|
|
difference(a.reg60[1], b.reg60[1], 2) +
|
|
difference(a.reg80[0], b.reg80[0], 2) +
|
|
difference(a.reg80[1], b.reg80[1], 2) +
|
|
difference(a.regC0, b.regC0, 3) +
|
|
difference(a.regE0[0], b.regE0[0], 1) +
|
|
difference(a.regE0[1], b.regE0[1], 1);
|
|
case TomTom:
|
|
case HiHat:
|
|
// These two rhythm instruments only use one operator's settings, the
|
|
// settings of the other operator (should) be ignored. There is also
|
|
// only one Connection byte, but there is no documentation to say whether
|
|
// this applies to these modulator-only rhythm instruments or to the
|
|
// other carrier-only ones (below.) I'm guessing and putting it here.
|
|
return
|
|
difference(a.eRhythmInstrument, ri, 4) +
|
|
difference(a.reg20[0], b.reg20[0], 2) +
|
|
difference(a.reg40[0], b.reg40[0], 1) +
|
|
difference(a.reg60[0], b.reg60[0], 2) +
|
|
difference(a.reg80[0], b.reg80[0], 2) +
|
|
difference(a.regC0, b.regC0, 3) +
|
|
difference(a.regE0[0], b.regE0[0], 1);
|
|
case SnareDrum:
|
|
case TopCymbal:
|
|
// These instruments only use one operator - but the other one compared
|
|
// to the previous set above. They also don't use the single Connection
|
|
// byte (I think.)
|
|
return
|
|
difference(a.eRhythmInstrument, ri, 4) +
|
|
difference(a.reg20[1], b.reg20[1], 2) +
|
|
difference(a.reg40[1], b.reg40[1], 1) +
|
|
difference(a.reg60[1], b.reg60[1], 2) +
|
|
difference(a.reg80[1], b.reg80[1], 2) +
|
|
difference(a.regE0[1], b.regE0[1], 1);
|
|
}
|
|
}
|
|
|
|
void writesbi(const char* filename, int instrno, int chanOPL) {
|
|
char fname[100];
|
|
char title[32];
|
|
snprintf(fname, 100, "%s_%03d.sbi", filename, instrno);
|
|
FILE* f_sbi = fopen(fname, WRITE_BINARY);
|
|
if (!f_sbi) {
|
|
fprintf(stderr, "Could not open instrument file %s for writing.\n", filename);
|
|
} else {
|
|
fwrite("SBI\x1a", sizeof(char), 4, f_sbi);
|
|
memset(title, 0, 32);
|
|
snprintf(title, 32, "dro2midi_%03d", instrno);
|
|
fwrite(title, sizeof(char), 32, f_sbi);
|
|
unsigned char instr[16];
|
|
memset(instr, 0, 16);
|
|
instr[0] = (unsigned char)reg[chanOPL].reg20[0];
|
|
instr[1] = (unsigned char)reg[chanOPL].reg20[1];
|
|
instr[2] = (unsigned char)reg[chanOPL].reg40[0];
|
|
instr[3] = (unsigned char)reg[chanOPL].reg40[1];
|
|
instr[4] = (unsigned char)reg[chanOPL].reg60[0];
|
|
instr[5] = (unsigned char)reg[chanOPL].reg60[1];
|
|
instr[6] = (unsigned char)reg[chanOPL].reg80[0];
|
|
instr[7] = (unsigned char)reg[chanOPL].reg80[1];
|
|
instr[8] = (unsigned char)reg[chanOPL].regE0[0];
|
|
instr[9] = (unsigned char)reg[chanOPL].regE0[1];
|
|
instr[10] = (unsigned char)reg[chanOPL].regC0;
|
|
fwrite(instr, sizeof(char), 16, f_sbi);
|
|
fclose(f_sbi);
|
|
}
|
|
}
|
|
|
|
int findinstr(int chanMIDI)
|
|
{
|
|
assert((chanMIDI < 9) || ((chanMIDI >= CHAN_BASSDRUM) && (chanMIDI <= CHAN_HIHAT)));
|
|
|
|
RHYTHM_INSTRUMENT ri;
|
|
int chanOPL;
|
|
switch (chanMIDI) {
|
|
case CHAN_BASSDRUM: ri = BassDrum; chanOPL = 6; break;
|
|
case CHAN_SNAREDRUM: ri = SnareDrum; chanOPL = 7; break;
|
|
case CHAN_TOMTOM: ri = TomTom; chanOPL = 8; break;
|
|
case CHAN_TOPCYMBAL: ri = TopCymbal; chanOPL = 8; break;
|
|
case CHAN_HIHAT: ri = HiHat; chanOPL = 7; break;
|
|
default: ri = NormalInstrument; chanOPL = chanMIDI; break;
|
|
}
|
|
|
|
int besti = -1;
|
|
long bestdiff = -1;
|
|
for (int i = 0; i < instrcnt; i++) {
|
|
long diff = compareinstr(instr[i], reg[chanOPL], ri);
|
|
if (besti < 0 || diff < bestdiff) {
|
|
bestdiff = diff;
|
|
besti = i;
|
|
if (bestdiff == 0) break;
|
|
}
|
|
}
|
|
|
|
if (besti >= 0) { // could be -1 if no instruments are loaded
|
|
while (instr[besti].redirect >= 0) { // Could have multiple redirects
|
|
// This instrument was an automatically generated one to avoid printing
|
|
// the instrument definition multiple times, so instead of using the auto
|
|
// one, use the one it originally matched against.
|
|
besti = instr[besti].redirect;
|
|
}
|
|
}
|
|
|
|
if (bestdiff != 0) {
|
|
if (::bPerfectMatchesOnly) {
|
|
// User doesn't want "close enough is good enough" instrument guessing
|
|
besti = 0; // use first instrument
|
|
}
|
|
// Couldn't find an exact match, print the details
|
|
switch (ri) {
|
|
case NormalInstrument:
|
|
case BassDrum:
|
|
// Normal instrument or rhythm Bass Drum use both
|
|
// operators + connection byte
|
|
printf("** New instrument in use on channel %d\n** Copy this into "
|
|
MAPPING_FILE " to assign it a MIDI patch:\n", chanOPL);
|
|
fprintf(stderr, "%s %02X-%02X/%02X-%02X/%02X-%02X/%02X-%02X/%02X/"
|
|
"%02X-%02X: patch=?\n",
|
|
((ri == BassDrum) ? "BD" : "NO"),
|
|
reg[chanOPL].reg20[0], reg[chanOPL].reg20[1],
|
|
reg[chanOPL].reg40[0], reg[chanOPL].reg40[1],
|
|
reg[chanOPL].reg60[0], reg[chanOPL].reg60[1],
|
|
reg[chanOPL].reg80[0], reg[chanOPL].reg80[1],
|
|
reg[chanOPL].regC0,
|
|
reg[chanOPL].regE0[0], reg[chanOPL].regE0[1]
|
|
);
|
|
if (::bWriteSbiInstruments) {
|
|
writesbi(output, instrcnt, chanOPL);
|
|
}
|
|
break;
|
|
case TomTom:
|
|
case HiHat:
|
|
// This instrument is one operator only, but it does use the connection
|
|
// byte (probably)
|
|
printf("** New rhythm instrument in use on OPL channel %d modulator\n"
|
|
"** Copy this into " MAPPING_FILE " to assign it a MIDI patch:\n",
|
|
chanOPL);
|
|
fprintf(stderr, "%s %02X/%02X/%02X/%02X/%02X/%02X: "
|
|
"patch=?\n",
|
|
((ri == TomTom) ? "TT" : "HH"),
|
|
reg[chanOPL].reg20[0],
|
|
reg[chanOPL].reg40[0],
|
|
reg[chanOPL].reg60[0],
|
|
reg[chanOPL].reg80[0],
|
|
reg[chanOPL].regC0,
|
|
reg[chanOPL].regE0[0]
|
|
);
|
|
break;
|
|
case SnareDrum:
|
|
case TopCymbal:
|
|
// This instrument does not uses the connection byte, so read in one
|
|
// byte less. Also read the values into the other operator.
|
|
// This instrument is one operator only, but it does use the connection
|
|
// byte (probably)
|
|
printf("** New rhythm instrument in use on OPL channel %d carrier\n"
|
|
"** Copy this into " MAPPING_FILE " to assign it a MIDI patch:\n",
|
|
chanOPL);
|
|
fprintf(stderr, "%s %02X/%02X/%02X/%02X/%02X: "
|
|
"patch=?\n",
|
|
((ri == SnareDrum) ? "SD" : "TC"),
|
|
reg[chanOPL].reg20[1],
|
|
reg[chanOPL].reg40[1],
|
|
reg[chanOPL].reg60[1],
|
|
reg[chanOPL].reg80[1],
|
|
reg[chanOPL].regE0[1]
|
|
);
|
|
break;
|
|
}
|
|
|
|
printf(">> Using similar match: %s\n", instr[besti].name);
|
|
// Save this unknown instrument as a known one, so the same registers don't get printed again
|
|
// reg[channel].prog = instr[besti].prog; // but keep the same patch that we've already assigned to the instrument, so it doesn't drop back to a piano for the rest of the song
|
|
// Maybe ^ isn't necessary if we're redirecting?
|
|
instr[instrcnt] = reg[chanOPL];
|
|
instr[instrcnt].eRhythmInstrument = ri;
|
|
if (besti >= 0) {
|
|
instr[instrcnt].redirect = besti; // Next time this instrument is matched, use the original one instead
|
|
} else {
|
|
instr[instrcnt].redirect = -1; // Will only happen when no instruments are loaded
|
|
}
|
|
instrcnt++;
|
|
}
|
|
return besti;
|
|
}
|
|
|
|
// Function for processing OPL note on and off events, and generating MIDI
|
|
// events in response. This function is also called when the pitch changes
|
|
// while a note is currently being played, causing it to generate MIDI
|
|
// pitchbends (if enabled) instead.
|
|
// Normally chanOPL and chanMIDI will be the same, except for rhythm mode
|
|
// instruments, which uses chanOPL for pitch and instrument patches, but
|
|
// chanMIDI for instrument mapping and note on/off events (since there will be
|
|
// two instrument maps and notes for a single OPL channel.)
|
|
void doNoteOnOff(bool bKeyOn, int chanOPL, int chanMIDI)
|
|
{
|
|
double keyFrac = freq2key(curfreq[chanOPL], reg[chanOPL].iOctave);
|
|
int key = (int)round(keyFrac);
|
|
if ((key > 0) && (bKeyOn)) {
|
|
// This is set to true to forcibly stop a MIDI keyon being generated for
|
|
// this note. This is done when a pitchbend is deemed as having done the
|
|
// job properly.
|
|
bool bKeyonAgain = true;
|
|
|
|
if (keyAlreadyOn[chanMIDI]) {
|
|
// There's already a note playing on this channel, just worry about the pitch of that
|
|
|
|
if (mapchannel[chanMIDI] != gm_drumchannel) {
|
|
// We're using a normal instrument here
|
|
|
|
if (::bUsePitchBends) {
|
|
// It's the same note, but the pitch is off just slightly, use a pitchbend
|
|
//double dbDiff = fabs(keyFrac - key); // should be between -0.9999 and 0.9999
|
|
double dbDiff = keyFrac - (double)(lastkey[chanMIDI] - transpose[chanMIDI]); // hopefully between -PITCHBEND_RANGE and PITCHBEND_RANGE
|
|
|
|
if (dbDiff > PITCHBEND_RANGE) {
|
|
fprintf(stderr, "Warning: This song wanted to pitchbend by %.2f notes, but the maximum is %.1f\n", dbDiff, PITCHBEND_RANGE);
|
|
|
|
// Turn this note off
|
|
write->noteoff(mapchannel[chanMIDI], lastkey[chanMIDI]);
|
|
::iNotesActive--;
|
|
lastkey[chanMIDI] = -1;
|
|
keyAlreadyOn[chanMIDI] = false;
|
|
// leave bKeyonAgain as true, so that a noteon will be played instead
|
|
} else {
|
|
int iNewBend = (int)(pitchbend_center + (PITCHBEND_ONESEMITONE * dbDiff));
|
|
if (iNewBend != pitchbent[chanMIDI]) {
|
|
//printf("pitchbend to %d/%.2lf (center + %d) (%.2lf "
|
|
// "semitones)\n", iNewBend, (double)pitchbend_center*2,
|
|
// (int)(iNewBend - pitchbend_center), (double)dbDiff);
|
|
write->pitchbend(mapchannel[chanMIDI], iNewBend);
|
|
// ::iPitchbendCount++;
|
|
pitchbent[chanMIDI] = iNewBend;
|
|
}
|
|
// This pitchbend has done the job, don't play a noteon
|
|
bKeyonAgain = false;
|
|
}
|
|
} else {
|
|
// We're not using pitchbends, so just switch off the note if it's different (the next one will play below)
|
|
if ((::bApproximatePitchbends) && (key != (lastkey[chanMIDI] - transpose[chanMIDI]))) {
|
|
write->noteoff(mapchannel[chanMIDI], lastkey[chanMIDI]);
|
|
::iNotesActive--;
|
|
lastkey[chanMIDI] = -1;
|
|
keyAlreadyOn[chanMIDI] = false;
|
|
//bKeyonAgain = true;
|
|
} else {
|
|
// Same note, different pitch, just pretend like it's not there
|
|
bKeyonAgain = false;
|
|
}
|
|
}
|
|
} else {
|
|
// This has mapped to a percussive MIDI note, so no pitchbends (or
|
|
// we'll bend all the percussive notes on the MIDI percussion channel.)
|
|
// But we don't want to play the note again, 'cos it's already on, so
|
|
// just ignore the keyon event.
|
|
// We'll also end up here if the instrument parameters are changed
|
|
// while the instrument is sounding, e.g. to change the characteristics
|
|
// of a hihat without sounding a new note. This won't be converted.
|
|
bKeyonAgain = false;
|
|
}
|
|
} // else this is a percussive instrument
|
|
|
|
//} else {
|
|
//if ((!bDontKeyonAgain) && ((!keyAlreadyOn[channel]) || (::bUsePitchBends))) { // If *now* there's no note playing... (or we're using pitchbends, i.e. a portamento has been set up)
|
|
if (bKeyonAgain) { // If *now* there's no note playing... (or we're using pitchbends, i.e. a portamento has been set up)
|
|
// See if we need to update anything
|
|
|
|
// See if the instrument needs to change
|
|
int i = findinstr(chanMIDI);
|
|
if (
|
|
(i >= 0) && (
|
|
(instr[i].prog != lastprog[chanMIDI]) ||
|
|
(
|
|
(instr[i].isdrum) &&
|
|
(drumnote[chanMIDI] != instr[i].note)
|
|
) || (
|
|
// Same instrument mapping, but different mute setting?
|
|
(instr[i].muted != mute[chanMIDI])
|
|
)
|
|
)
|
|
) {
|
|
printf("// Ch%02d <- %s\n", chanMIDI, instr[i].name);
|
|
if (!instr[i].isdrum) {
|
|
// Normal instrument (not MIDI percussion)
|
|
assert(instr[i].prog >= 0);
|
|
|
|
if (mapchannel[chanMIDI] == gm_drumchannel) {
|
|
// This was playing drums, now we're back to normal notes
|
|
|
|
// make sure this sets things back to what they were in the init
|
|
// section in main()
|
|
mapchannel[chanMIDI] = chanMIDI;
|
|
drumnote[chanMIDI] = -1; // NOTE: This drumnote won't be reset if the drum instrument was muted! (As it then wouldn't have been assigned to gm_drumchannel)
|
|
}
|
|
|
|
transpose[chanMIDI] = instr[i].iTranspose;
|
|
write->program(mapchannel[chanMIDI], lastprog[chanMIDI] = instr[i].prog);
|
|
} else {
|
|
// This new instrument is a drum
|
|
assert(instr[i].prog == -1);
|
|
|
|
/*if (instr[i].muted) {
|
|
// This instrument is muted, which means whichever channel we
|
|
// assign it to will become muted. We can't therefore assign it to
|
|
// the drum channel as we normally would, otherwise all the MIDI
|
|
// percussion will become muted. So we assign it to its normal
|
|
// channel instead, like we would with a non-percussion instrument.
|
|
mapchannel[chanMIDI] = chanMIDI;
|
|
} else {
|
|
mapchannel[chanMIDI] = gm_drumchannel;
|
|
}*/
|
|
mapchannel[chanMIDI] = gm_drumchannel;
|
|
drumnote[chanMIDI] = instr[i].note;
|
|
lastprog[chanMIDI] = instr[i].prog;
|
|
// drums don't use transpose values
|
|
}
|
|
mute[chanMIDI] = instr[i].muted;
|
|
}
|
|
|
|
// Play the note
|
|
//if ((::bUsePitchBends) && (!keyAlreadyOn[channel])) {
|
|
if ((::bUsePitchBends) && (mapchannel[chanMIDI] != gm_drumchannel)) { // If pitchbends are enabled and this isn't a percussion instrument
|
|
double dbDiff = keyFrac - key; // should be between -0.9999 and 0.9999
|
|
assert(dbDiff < PITCHBEND_RANGE); // not really necessary...
|
|
|
|
int iNewBend = (int)(pitchbend_center + (PITCHBEND_ONESEMITONE * dbDiff));
|
|
if (iNewBend != pitchbent[chanMIDI]) {
|
|
//printf("new note at pitchbend %d\n", iNewBend);
|
|
write->pitchbend(mapchannel[chanMIDI], iNewBend); // pitchbends are between 0x0000L and 0x2000L
|
|
// ::iPitchbendCount++;
|
|
pitchbent[chanMIDI] = iNewBend;
|
|
}
|
|
}
|
|
|
|
int level;
|
|
if (!mute[chanMIDI]) {
|
|
if (::bEnableVolume) {
|
|
level = reg[chanOPL].reg40[1] & 0x3f;
|
|
if (level > 0x30) level = 0x30; // don't allow fully silent notes
|
|
} else level = 0; // 0 == loudest
|
|
} else {
|
|
level = 0x3f; // silent
|
|
}
|
|
|
|
if (mapchannel[chanMIDI] != gm_drumchannel) {
|
|
// Normal note
|
|
lastkey[chanMIDI] = key + transpose[chanMIDI];
|
|
} else {
|
|
// Percussion
|
|
//write->noteon(gm_drumchannel, drumnote[chanMIDI], (0x3f - level) << 1);
|
|
lastkey[chanMIDI] = drumnote[chanMIDI];
|
|
}
|
|
|
|
write->noteon(mapchannel[chanMIDI], lastkey[chanMIDI], (0x3f - level) << 1);
|
|
//printf("note on chan %d, mute is %s\n", chanMIDI, mute[chanMIDI] ? "true" : "false");
|
|
::iNotesActive++;
|
|
::iTotalNotes++;
|
|
|
|
// If this note went on with a pitchbend active on the channel, count it
|
|
if (pitchbent[chanMIDI] != pitchbend_center) ::iPitchbendCount++;
|
|
|
|
keyAlreadyOn[chanMIDI] = true;
|
|
|
|
} // if (not muted)
|
|
|
|
} else {
|
|
// There's no note currently playing on this channel, so if we've still got
|
|
// one switch it off.
|
|
if (lastkey[chanMIDI] != -1) {
|
|
write->noteoff(mapchannel[chanMIDI], lastkey[chanMIDI]);
|
|
::iNotesActive--;
|
|
lastkey[chanMIDI] = -1;
|
|
keyAlreadyOn[chanMIDI] = false;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int main(int argc, char**argv)
|
|
{
|
|
int c;
|
|
|
|
// Defaults
|
|
::dbConversionVal = 49716.0;
|
|
|
|
argc--; argv++;
|
|
while (argc > 0 && **argv == '-')
|
|
{
|
|
if (strncasecmp(*argv, "-r", 2) == 0) {
|
|
::bRhythm = false;
|
|
printf("Rhythm-mode instruments disabled.\n");
|
|
} else if (strncasecmp(*argv, "-p", 2) == 0) {
|
|
::bUsePitchBends = false;
|
|
printf("Pitchbends disabled.\n");
|
|
} else if (strncasecmp(*argv, "-a", 2) == 0) {
|
|
::bApproximatePitchbends = true;
|
|
} else if (strncasecmp(*argv, "-i", 2) == 0) {
|
|
::bPerfectMatchesOnly = true;
|
|
printf("Only using exact instrument matches - approximations disabled!\n");
|
|
} else if (strncasecmp(*argv, "-v", 2) == 0) {
|
|
::bEnableVolume = false;
|
|
printf("Note velocity disabled, all notes will be played as loud as possible.\n");
|
|
} else if (strncasecmp(*argv, "-s", 2) == 0) {
|
|
::bWriteSbiInstruments = true;
|
|
} else if (strncasecmp(*argv, "-c", 2) == 0) {
|
|
argc--; argv++;
|
|
if (argc == 0) {
|
|
fprintf(stderr, "-c requires a parameter\n");
|
|
usage();
|
|
}
|
|
if (strncasecmp(*argv, "alt", 3) == 0) {
|
|
::dbConversionVal = 50000.0;
|
|
} else {
|
|
// Use the given value
|
|
::dbConversionVal = strtod(*argv, NULL);
|
|
if (::dbConversionVal == 0) {
|
|
fprintf(stderr, "-c requires a non-zero parameter\n");
|
|
usage();
|
|
}
|
|
}
|
|
} else if (strncasecmp(*argv, "--version", 9) == 0) {
|
|
version();
|
|
return 0;
|
|
} else {
|
|
fprintf(stderr, "invalid option %s\n", *argv);
|
|
usage();
|
|
}
|
|
argc--; argv++;
|
|
}
|
|
if (argc < 2) usage();
|
|
|
|
if ((::bUsePitchBends) && (::bApproximatePitchbends)) {
|
|
fprintf(stderr, "ERROR: Pitchbends can only be approximated (-a) if "
|
|
"proper MIDI pitchbends are disabled (-p)\n");
|
|
return 1;
|
|
}
|
|
|
|
input = argv[0];
|
|
output = argv[1];
|
|
if (strcmp(input, output) == 0)
|
|
{
|
|
fprintf(stderr, "cannot convert to same file\n");
|
|
return 1;
|
|
}
|
|
|
|
if (!loadInstruments()) return 1;
|
|
|
|
|
|
f = fopen(input, READ_BINARY);
|
|
if (!f)
|
|
{
|
|
perror(input);
|
|
return 1;
|
|
}
|
|
unsigned long imflen = 0;
|
|
|
|
unsigned char cSig[9];
|
|
fseek(f, 0, SEEK_SET);
|
|
//fgets(cSig, 9, f);
|
|
fread(cSig, 1, 9, f);
|
|
iSpeed = 0;
|
|
if (strncmp((char *)cSig, "DBRAWOPL", 8) == 0) {
|
|
::iFormat = FORMAT_DRO;
|
|
fseek(f, 8, SEEK_SET); // Seek to "version" fields.
|
|
unsigned long version = readUINT32LE(f);
|
|
if (version == 0x10000) {
|
|
printf("Input file is in DOSBox DRO v1.0 format.\n");
|
|
} else if (version == 0x2) {
|
|
printf("Input file is in DOSBox DRO v2.0 format, which is not supported.\n");
|
|
return 2;
|
|
} else {
|
|
printf("Input file is in DOSBox DRO format, but an unknown version!\n");
|
|
return 3;
|
|
}
|
|
::iInitialSpeed = 1000;
|
|
|
|
fseek(f, 16, SEEK_SET); // seek to "length in bytes" field
|
|
imflen = readUINT32LE(f);
|
|
} else if (strncmp((char *)cSig, "RAWADATA", 8) == 0) {
|
|
::iFormat = FORMAT_RAW;
|
|
printf("Input file is in Rdos RAW format.\n");
|
|
|
|
// Read until EOF (0xFFFF is really the end but we'll check that during conversion)
|
|
fseek(f, 0, SEEK_END);
|
|
imflen = ftell(f);
|
|
|
|
fseek(f, 8, SEEK_SET); // seek to "initial clock speed" field
|
|
::iInitialSpeed = 1000;
|
|
int iClockSpeed = readUINT16LE(f);
|
|
if ((iClockSpeed == 0) || (iClockSpeed == 0xFFFF)) {
|
|
::iSpeed = (int)18.2; // default to 18.2Hz...well, 18Hz thanks to rounding
|
|
} else {
|
|
::iSpeed = (int)(1193180.0 / iClockSpeed);
|
|
}
|
|
} else {
|
|
::iFormat = FORMAT_IMF;
|
|
if ((cSig[0] == 0) && (cSig[1] == 0)) {
|
|
printf("Input file appears to be in IMF type-0 format.\n");
|
|
fseek(f, 0, SEEK_END);
|
|
imflen = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
} else {
|
|
printf("Input file appears to be in IMF type-1 format.\n");
|
|
imflen = cSig[0] + (cSig[1] << 8);
|
|
fseek(f, 2, SEEK_SET); // seek to start of actual OPL data
|
|
}
|
|
if (strcasecmp(&input[strlen(input)-3], "imf") == 0) {
|
|
printf("File extension is .imf - using 560Hz speed (rename to .wlf if "
|
|
"this is too slow)\n");
|
|
::iInitialSpeed = 560;
|
|
} else if (strcasecmp(&input[strlen(input)-3], "wlf") == 0) {
|
|
printf("File extension is .wlf - using 700Hz speed (rename to .imf if "
|
|
"this is too fast)\n");
|
|
::iInitialSpeed = 700;
|
|
} else {
|
|
printf("Unknown file extension - must be .imf or .wlf\n");
|
|
return 3;
|
|
}
|
|
}
|
|
printf("Using conversion constant of %.1lf\n", ::dbConversionVal);
|
|
|
|
write = new MidiWrite(output);
|
|
if (!write) {
|
|
fprintf(stderr, "out of memory\n");
|
|
return 1;
|
|
}
|
|
if (!write->getf()) {
|
|
perror(output);
|
|
return 1;
|
|
}
|
|
if (iSpeed == 0) {
|
|
iSpeed = iInitialSpeed;
|
|
}
|
|
resolution = iInitialSpeed / 2;
|
|
write->head(/* version */ 0, /* track count updated later */0, resolution);
|
|
|
|
write->track();
|
|
write->tempo((long)(60000000.0 / tempo));
|
|
write->tact(4,4,24,8);
|
|
|
|
for (c = 0; c < 10; c++) {
|
|
mapchannel[c] = c;
|
|
write->resetctrlrs(mapchannel[c], 0); // Reset All Controllers (Ensures default settings upon every playback).
|
|
write->volume(mapchannel[c], 127);
|
|
write->balance(mapchannel[c], 64); // Usually called 'Pan'.
|
|
write->expression(mapchannel[c], 127); // Similar to 'Volume', but this is primarily used for volume damping.
|
|
}
|
|
|
|
for (c = 0; c <= 8; c++) {
|
|
lastprog[c] = -1;
|
|
reg[c].iOctave = 0;
|
|
}
|
|
|
|
int delay = 0;
|
|
int channel;
|
|
int code, param;
|
|
|
|
for (c = 0; c < 9; c++) {
|
|
curfreq[c] = 0;
|
|
mapchannel[c] = c; // This can get reset when playing a drum and then a normal instrument on a channel - see instrument-change code below
|
|
keyAlreadyOn[c] = false;
|
|
lastkey[c] = -1; // last MIDI key pressed on this channel
|
|
pitchbent[c] = (int)pitchbend_center;
|
|
transpose[c] = 0;
|
|
drumnote[c] = 0; // probably not necessary...
|
|
mute[c] = false;
|
|
|
|
if (::bUsePitchBends) {
|
|
write->control(mapchannel[c], 100, 0); // RPN LSB for "Pitch Bend Sensitivity"
|
|
write->control(mapchannel[c], 101, 0); // RPN MSB for "Pitch Bend Sensitivity"
|
|
write->control(mapchannel[c], 6, (int)PITCHBEND_RANGE); // Data for Pitch Bend Sensitivity (in semitones) - controller 38 can be used for cents in addition
|
|
write->control(mapchannel[c], 100, 0x7F); // RPN LSB for "Finished"
|
|
write->control(mapchannel[c], 101, 0x7F); // RPN MSB for "Finished"
|
|
}
|
|
// write->pitchbend(mapchannel[c], pitchbend_center);
|
|
}
|
|
// Rhythm-mode only channels
|
|
for (c = 10; c < 15; c++) {
|
|
keyAlreadyOn[c] = false;
|
|
mapchannel[c] = c; // as above, this will be changed later but must be
|
|
// eventually set back to this value here (or the instrument will jump
|
|
// channels unexpectedly.)
|
|
pitchbent[c] = (int)pitchbend_center;
|
|
lastkey[c] = -1; // last MIDI key pressed on this channel
|
|
transpose[c] = 0;
|
|
drumnote[c] = 0; // probably not necessary...
|
|
mute[c] = false;
|
|
}
|
|
|
|
int iMinLen; // Minimum length for valid notes to still be present
|
|
switch (::iFormat) {
|
|
case FORMAT_IMF: iMinLen = 4; break;
|
|
case FORMAT_DRO: iMinLen = 2; break;
|
|
case FORMAT_RAW: iMinLen = 2; break;
|
|
}
|
|
|
|
unsigned long iSize = imflen; // sometimes the counter wraps around, need this to stop it from happening
|
|
while ((imflen >= (unsigned long)iMinLen) && (imflen <= iSize)) {
|
|
|
|
// Get the next OPL register and value from the input file
|
|
|
|
switch (::iFormat) {
|
|
case FORMAT_IMF:
|
|
// Write the last iteration's delay (since the delay needs to come *after* the note)
|
|
write->time(delay);
|
|
|
|
code = readByte(f);
|
|
param = readByte(f);
|
|
delay = readUINT16LE(f);
|
|
imflen -= 4;
|
|
break;
|
|
case FORMAT_DRO:
|
|
code = readByte(f);
|
|
imflen--;
|
|
switch (code) {
|
|
case 0x00: // delay (byte)
|
|
delay += 1 + readByte(f);
|
|
imflen--;
|
|
continue;
|
|
case 0x01: // delay (int)
|
|
delay += 1 + readUINT16LE(f);
|
|
imflen -= 2;
|
|
continue;
|
|
case 0x02: // use first OPL chip
|
|
case 0x03: // use second OPL chip
|
|
fprintf(stderr, "Warning: This song uses multiple OPL chips - this isn't yet supported!\n");
|
|
continue;
|
|
case 0x04: // escape
|
|
code = readByte(f);
|
|
imflen--;
|
|
break;
|
|
}
|
|
param = readByte(f);
|
|
imflen--;
|
|
|
|
// Write any delay (as this needs to come *before* the next note)
|
|
write->time(delay);
|
|
delay = 0;
|
|
break;
|
|
case FORMAT_RAW:
|
|
param = readByte(f);
|
|
code = readByte(f);
|
|
imflen -= 2;
|
|
switch (code) {
|
|
case 0x00: // delay
|
|
delay += param;
|
|
continue;
|
|
case 0x02: // control data
|
|
switch (param) {
|
|
case 0x00: {
|
|
if (delay != 0) {
|
|
// See below - we need to write out any delay at the old clock speed before we change it
|
|
write->time((delay * iInitialSpeed / ::iSpeed));
|
|
delay = 0;
|
|
}
|
|
int iClockSpeed = readUINT16LE(f);
|
|
if ((iClockSpeed == 0) || (iClockSpeed == 0xFFFF)) {
|
|
printf("Speed set to invalid value, ignoring speed change.\n");
|
|
} else {
|
|
::iSpeed = (int)round(1193180.0 / iClockSpeed);
|
|
printf("Speed changed to %dHz\n", iSpeed);
|
|
}
|
|
imflen -= 2;
|
|
break;
|
|
}
|
|
case 0x01:
|
|
case 0x02:
|
|
printf("Switching OPL ports is not yet implemented!\n");
|
|
break;
|
|
}
|
|
continue;
|
|
case 0xFF:
|
|
if (param == 0xFF) {
|
|
// End of song
|
|
imflen = 0;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Write any delay (as this needs to come *before* the next note)
|
|
// Since our global clock speed is 1000Hz, we have to multiply this
|
|
// delay accordingly as the delay units are in the current clock speed.
|
|
// This calculation converts them into 1000Hz delay units regardless of
|
|
// the current clock speed.
|
|
if (delay != 0) write->time((delay * iInitialSpeed / ::iSpeed));
|
|
//printf("delay is %d (ticks %d)\n", (delay * iInitialSpeed / ::iSpeed), delay);
|
|
delay = 0;
|
|
break;
|
|
|
|
default: // should never happen
|
|
break;
|
|
|
|
} // switch (::iFormat)
|
|
|
|
// Convert the OPL register and value into a MIDI event
|
|
|
|
if (code >= 0xa0 && code <= 0xa8) { // set freq bits 0-7
|
|
channel = code-0xa0;
|
|
curfreq[channel] = (curfreq[channel] & 0xF00) + (param & 0xff);
|
|
if (keyAlreadyOn[channel]) {
|
|
param = 0x20; // bare noteon for code below
|
|
doNoteOnOff(true, channel, channel);
|
|
}
|
|
continue;
|
|
} else if (code >= 0xB0 && code <= 0xB8) { // set freq bits 8-9 and octave and on/off
|
|
channel = code - 0xb0;
|
|
curfreq[channel] = (curfreq[channel] & 0x0FF) + ((param & 0x03)<<8);
|
|
// save octave so we know what it is if we run 0xA0-0xA8 regs change code
|
|
// next (which doesn't have the octave)
|
|
reg[channel].iOctave = (param >> 2) & 7;
|
|
int keyon = (param >> 5) & 1;
|
|
doNoteOnOff(keyon, channel, channel);
|
|
} else if ((code == 0xBD) && (::bRhythm)) {
|
|
if ((param >> 5) & 1) {
|
|
// Bass Drum
|
|
doNoteOnOff((param >> 4) & 1, channel, CHAN_BASSDRUM);
|
|
doNoteOnOff((param >> 3) & 1, channel, CHAN_SNAREDRUM);
|
|
doNoteOnOff((param >> 2) & 1, channel, CHAN_TOMTOM);
|
|
doNoteOnOff((param >> 1) & 1, channel, CHAN_TOPCYMBAL);
|
|
doNoteOnOff( param & 1, channel, CHAN_HIHAT);
|
|
}
|
|
} else if (code >= 0x20 && code <= 0x35) {
|
|
channel = GET_CHANNEL(code-0x20);
|
|
reg[channel].reg20[GET_OP(code-0x20)] = param;
|
|
} else if (code >= 0x40 && code <= 0x55) {
|
|
channel = GET_CHANNEL(code-0x40);
|
|
reg[channel].reg40[GET_OP(code-0x40)] = param;
|
|
} else if (code >= 0x60 && code <= 0x75) {
|
|
channel = GET_CHANNEL(code-0x60);
|
|
reg[channel].reg60[GET_OP(code-0x60)] = param;
|
|
} else if (code >= 0x80 && code <= 0x95) {
|
|
channel = GET_CHANNEL(code-0x80);
|
|
reg[channel].reg80[GET_OP(code-0x80)] = param;
|
|
} else if (code >= 0xc0 && code <= 0xc8) {
|
|
channel = code-0xc0;
|
|
reg[channel].regC0 = param;
|
|
} else if (code >= 0xe0 && code <= 0xF5) {
|
|
channel = GET_CHANNEL(code-0xe0);
|
|
reg[channel].regE0[GET_OP(code-0xe0)] = param;
|
|
}
|
|
}
|
|
|
|
for (c = 0; c < 10; c++) {
|
|
mapchannel[c] = c;
|
|
write->allnotesoff(mapchannel[c], 0); // All Notes Off (Ensures that even incomplete Notes will be switched-off per each MIDI channel at the end-of-playback).
|
|
}
|
|
|
|
delete write;
|
|
fclose(f);
|
|
|
|
// Display completion message and some stats
|
|
printf("\nConversion complete. Wrote %s\n\n Total pitchbent notes: %d\n"
|
|
" Total notes: %d\n Notes still active at end of song: %d\n\n",
|
|
output, ::iPitchbendCount, ::iTotalNotes, ::iNotesActive);
|
|
|
|
return 0;
|
|
}
|