ref: a0e667de8678b045f8f08a7d87ba86239e33fa6b
dir: /src/mus2mid.c/
// // Copyright(C) 1993-1996 Id Software, Inc. // Copyright(C) 2005-2014 Simon Howard // Copyright(C) 2006 Ben Ryves 2006 // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // mus2mid.c - Ben Ryves 2006 - http://benryves.com - benryves@benryves.com // Use to convert a MUS file into a single track, type 0 MIDI file. #include <stdio.h> #include "doomtype.h" #include "i_swap.h" #include "memio.h" #include "mus2mid.h" #define NUM_CHANNELS 16 #define MIDI_PERCUSSION_CHAN 9 #define MUS_PERCUSSION_CHAN 15 // MUS event codes typedef enum { mus_releasekey = 0x00, mus_presskey = 0x10, mus_pitchwheel = 0x20, mus_systemevent = 0x30, mus_changecontroller = 0x40, mus_scoreend = 0x60 } musevent; // MIDI event codes typedef enum { midi_releasekey = 0x80, midi_presskey = 0x90, midi_aftertouchkey = 0xA0, midi_changecontroller = 0xB0, midi_changepatch = 0xC0, midi_aftertouchchannel = 0xD0, midi_pitchwheel = 0xE0 } midievent; // Structure to hold MUS file header typedef PACKED_STRUCT ( { byte id[4]; unsigned short scorelength; unsigned short scorestart; unsigned short primarychannels; unsigned short secondarychannels; unsigned short instrumentcount; }) musheader; // Standard MIDI type 0 header + track header static const byte midiheader[] = { 'M', 'T', 'h', 'd', // Main header 0x00, 0x00, 0x00, 0x06, // Header size 0x00, 0x00, // MIDI type (0) 0x00, 0x01, // Number of tracks 0x00, 0x46, // Resolution 'M', 'T', 'r', 'k', // Start of track 0x00, 0x00, 0x00, 0x00 // Placeholder for track length }; // Cached channel velocities static byte channelvelocities[] = { 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127 }; // Timestamps between sequences of MUS events static unsigned int queuedtime = 0; // Counter for the length of the track static unsigned int tracksize; static const byte controller_map[] = { 0x00, 0x20, 0x01, 0x07, 0x0A, 0x0B, 0x5B, 0x5D, 0x40, 0x43, 0x78, 0x7B, 0x7E, 0x7F, 0x79 }; static int channel_map[NUM_CHANNELS]; // Write timestamp to a MIDI file. static boolean WriteTime(unsigned int time, MEMFILE *midioutput) { unsigned int buffer = time & 0x7F; byte writeval; while ((time >>= 7) != 0) { buffer <<= 8; buffer |= ((time & 0x7F) | 0x80); } for (;;) { writeval = (byte)(buffer & 0xFF); if (mem_fwrite(&writeval, 1, 1, midioutput) != 1) { return true; } ++tracksize; if ((buffer & 0x80) != 0) { buffer >>= 8; } else { queuedtime = 0; return false; } } } // Write the end of track marker static boolean WriteEndTrack(MEMFILE *midioutput) { byte endtrack[] = {0xFF, 0x2F, 0x00}; if (WriteTime(queuedtime, midioutput)) { return true; } if (mem_fwrite(endtrack, 1, 3, midioutput) != 3) { return true; } tracksize += 3; return false; } // Write a key press event static boolean WritePressKey(byte channel, byte key, byte velocity, MEMFILE *midioutput) { byte working = midi_presskey | channel; if (WriteTime(queuedtime, midioutput)) { return true; } if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = key & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = velocity & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } tracksize += 3; return false; } // Write a key release event static boolean WriteReleaseKey(byte channel, byte key, MEMFILE *midioutput) { byte working = midi_releasekey | channel; if (WriteTime(queuedtime, midioutput)) { return true; } if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = key & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = 0; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } tracksize += 3; return false; } // Write a pitch wheel/bend event static boolean WritePitchWheel(byte channel, short wheel, MEMFILE *midioutput) { byte working = midi_pitchwheel | channel; if (WriteTime(queuedtime, midioutput)) { return true; } if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = wheel & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = (wheel >> 7) & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } tracksize += 3; return false; } // Write a patch change event static boolean WriteChangePatch(byte channel, byte patch, MEMFILE *midioutput) { byte working = midi_changepatch | channel; if (WriteTime(queuedtime, midioutput)) { return true; } if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = patch & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } tracksize += 2; return false; } // Write a valued controller change event static boolean WriteChangeController_Valued(byte channel, byte control, byte value, MEMFILE *midioutput) { byte working = midi_changecontroller | channel; if (WriteTime(queuedtime, midioutput)) { return true; } if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } working = control & 0x7F; if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } // Quirk in vanilla DOOM? MUS controller values should be // 7-bit, not 8-bit. working = value;// & 0x7F; // Fix on said quirk to stop MIDI players from complaining that // the value is out of range: if (working & 0x80) { working = 0x7F; } if (mem_fwrite(&working, 1, 1, midioutput) != 1) { return true; } tracksize += 3; return false; } // Write a valueless controller change event static boolean WriteChangeController_Valueless(byte channel, byte control, MEMFILE *midioutput) { return WriteChangeController_Valued(channel, control, 0, midioutput); } // Allocate a free MIDI channel. static int AllocateMIDIChannel(void) { int result; int max; int i; // Find the current highest-allocated channel. max = -1; for (i=0; i<NUM_CHANNELS; ++i) { if (channel_map[i] > max) { max = channel_map[i]; } } // max is now equal to the highest-allocated MIDI channel. We can // now allocate the next available channel. This also works if // no channels are currently allocated (max=-1) result = max + 1; // Don't allocate the MIDI percussion channel! if (result == MIDI_PERCUSSION_CHAN) { ++result; } return result; } // Given a MUS channel number, get the MIDI channel number to use // in the outputted file. static int GetMIDIChannel(int mus_channel, MEMFILE *midioutput) { // Find the MIDI channel to use for this MUS channel. // MUS channel 15 is the percusssion channel. if (mus_channel == MUS_PERCUSSION_CHAN) { return MIDI_PERCUSSION_CHAN; } else { // If a MIDI channel hasn't been allocated for this MUS channel // yet, allocate the next free MIDI channel. if (channel_map[mus_channel] == -1) { channel_map[mus_channel] = AllocateMIDIChannel(); // First time using the channel, send an "all notes off" // event. This fixes "The D_DDTBLU disease" described here: // https://www.doomworld.com/vb/source-ports/66802-the WriteChangeController_Valueless(channel_map[mus_channel], 0x7b, midioutput); } return channel_map[mus_channel]; } } static boolean ReadMusHeader(MEMFILE *file, musheader *header) { boolean result; result = mem_fread(&header->id, sizeof(byte), 4, file) == 4 && mem_fread(&header->scorelength, sizeof(short), 1, file) == 1 && mem_fread(&header->scorestart, sizeof(short), 1, file) == 1 && mem_fread(&header->primarychannels, sizeof(short), 1, file) == 1 && mem_fread(&header->secondarychannels, sizeof(short), 1, file) == 1 && mem_fread(&header->instrumentcount, sizeof(short), 1, file) == 1; if (result) { header->scorelength = SHORT(header->scorelength); header->scorestart = SHORT(header->scorestart); header->primarychannels = SHORT(header->primarychannels); header->secondarychannels = SHORT(header->secondarychannels); header->instrumentcount = SHORT(header->instrumentcount); } return result; } // Read a MUS file from a stream (musinput) and output a MIDI file to // a stream (midioutput). // // Returns 0 on success or 1 on failure. boolean mus2mid(MEMFILE *musinput, MEMFILE *midioutput) { // Header for the MUS file musheader musfileheader; // Descriptor for the current MUS event byte eventdescriptor; int channel; // Channel number musevent event; // Bunch of vars read from MUS lump byte key; byte controllernumber; byte controllervalue; // Buffer used for MIDI track size record byte tracksizebuffer[4]; // Flag for when the score end marker is hit. int hitscoreend = 0; // Temp working byte byte working; // Used in building up time delays unsigned int timedelay; // Initialise channel map to mark all channels as unused. for (channel=0; channel<NUM_CHANNELS; ++channel) { channel_map[channel] = -1; } // Grab the header if (!ReadMusHeader(musinput, &musfileheader)) { return true; } #ifdef CHECK_MUS_HEADER // Check MUS header if (musfileheader.id[0] != 'M' || musfileheader.id[1] != 'U' || musfileheader.id[2] != 'S' || musfileheader.id[3] != 0x1A) { return true; } #endif // Seek to where the data is held if (mem_fseek(musinput, (long)musfileheader.scorestart, MEM_SEEK_SET) != 0) { return true; } // So, we can assume the MUS file is faintly legit. Let's start // writing MIDI data... mem_fwrite(midiheader, 1, sizeof(midiheader), midioutput); tracksize = 0; // Now, process the MUS file: while (!hitscoreend) { // Handle a block of events: while (!hitscoreend) { // Fetch channel number and event code: if (mem_fread(&eventdescriptor, 1, 1, musinput) != 1) { return true; } channel = GetMIDIChannel(eventdescriptor & 0x0F, midioutput); event = eventdescriptor & 0x70; switch (event) { case mus_releasekey: if (mem_fread(&key, 1, 1, musinput) != 1) { return true; } if (WriteReleaseKey(channel, key, midioutput)) { return true; } break; case mus_presskey: if (mem_fread(&key, 1, 1, musinput) != 1) { return true; } if (key & 0x80) { if (mem_fread(&channelvelocities[channel], 1, 1, musinput) != 1) { return true; } channelvelocities[channel] &= 0x7F; } if (WritePressKey(channel, key, channelvelocities[channel], midioutput)) { return true; } break; case mus_pitchwheel: if (mem_fread(&key, 1, 1, musinput) != 1) { break; } if (WritePitchWheel(channel, (short)(key * 64), midioutput)) { return true; } break; case mus_systemevent: if (mem_fread(&controllernumber, 1, 1, musinput) != 1) { return true; } if (controllernumber < 10 || controllernumber > 14) { return true; } if (WriteChangeController_Valueless(channel, controller_map[controllernumber], midioutput)) { return true; } break; case mus_changecontroller: if (mem_fread(&controllernumber, 1, 1, musinput) != 1) { return true; } if (mem_fread(&controllervalue, 1, 1, musinput) != 1) { return true; } if (controllernumber == 0) { if (WriteChangePatch(channel, controllervalue, midioutput)) { return true; } } else { if (controllernumber < 1 || controllernumber > 9) { return true; } if (WriteChangeController_Valued(channel, controller_map[controllernumber], controllervalue, midioutput)) { return true; } } break; case mus_scoreend: hitscoreend = 1; break; default: return true; break; } if (eventdescriptor & 0x80) { break; } } // Now we need to read the time code: if (!hitscoreend) { timedelay = 0; for (;;) { if (mem_fread(&working, 1, 1, musinput) != 1) { return true; } timedelay = timedelay * 128 + (working & 0x7F); if ((working & 0x80) == 0) { break; } } queuedtime += timedelay; } } // End of track if (WriteEndTrack(midioutput)) { return true; } // Write the track size into the stream if (mem_fseek(midioutput, 18, MEM_SEEK_SET)) { return true; } tracksizebuffer[0] = (tracksize >> 24) & 0xff; tracksizebuffer[1] = (tracksize >> 16) & 0xff; tracksizebuffer[2] = (tracksize >> 8) & 0xff; tracksizebuffer[3] = tracksize & 0xff; if (mem_fwrite(tracksizebuffer, 1, 4, midioutput) != 4) { return true; } return false; } #ifdef STANDALONE #include "m_misc.h" #include "z_zone.h" int main(int argc, char *argv[]) { MEMFILE *src, *dst; byte *infile; long infile_len; void *outfile; size_t outfile_len; if (argc != 3) { printf("Usage: %s <musfile> <midfile>\n", argv[0]); exit(-1); } Z_Init(); infile_len = M_ReadFile(argv[1], &infile); src = mem_fopen_read(infile, infile_len); dst = mem_fopen_write(); if (mus2mid(src, dst)) { fprintf(stderr, "mus2mid() failed\n"); exit(-1); } // Write result to output file: mem_get_buf(dst, &outfile, &outfile_len); M_WriteFile(argv[2], outfile, outfile_len); return 0; } #endif