summaryrefslogtreecommitdiff
path: root/src/midifile.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/midifile.c')
-rw-r--r--src/midifile.c815
1 files changed, 815 insertions, 0 deletions
diff --git a/src/midifile.c b/src/midifile.c
new file mode 100644
index 00000000..bd935ca1
--- /dev/null
+++ b/src/midifile.c
@@ -0,0 +1,815 @@
+// Emacs style mode select -*- C++ -*-
+//-----------------------------------------------------------------------------
+//
+// Copyright(C) 2009 Simon Howard
+//
+// 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.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+// 02111-1307, USA.
+//
+// DESCRIPTION:
+// Reading of MIDI files.
+//
+//-----------------------------------------------------------------------------
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#include "doomdef.h"
+#include "doomtype.h"
+#include "i_swap.h"
+#include "midifile.h"
+
+#define HEADER_CHUNK_ID "MThd"
+#define TRACK_CHUNK_ID "MTrk"
+#define MAX_BUFFER_SIZE 0x10000
+
+typedef struct
+{
+ byte chunk_id[4];
+ unsigned int chunk_size;
+} PACKEDATTR chunk_header_t;
+
+typedef struct
+{
+ chunk_header_t chunk_header;
+ unsigned short format_type;
+ unsigned short num_tracks;
+ unsigned short time_division;
+} PACKEDATTR midi_header_t;
+
+typedef struct
+{
+ // Length in bytes:
+
+ unsigned int data_len;
+
+ // Events in this track:
+
+ midi_event_t *events;
+ int num_events;
+} midi_track_t;
+
+struct midi_track_iter_s
+{
+ midi_track_t *track;
+ unsigned int position;
+};
+
+struct midi_file_s
+{
+ midi_header_t header;
+
+ // All tracks in this file:
+ midi_track_t *tracks;
+ unsigned int num_tracks;
+
+ // Data buffer used to store data read for SysEx or meta events:
+ byte *buffer;
+ unsigned int buffer_size;
+};
+
+// Check the header of a chunk:
+
+static boolean CheckChunkHeader(chunk_header_t *chunk,
+ char *expected_id)
+{
+ boolean result;
+
+ result = (memcmp((char *) chunk->chunk_id, expected_id, 4) == 0);
+
+ if (!result)
+ {
+ fprintf(stderr, "CheckChunkHeader: Expected '%s' chunk header, "
+ "got '%c%c%c%c'\n",
+ expected_id,
+ chunk->chunk_id[0], chunk->chunk_id[1],
+ chunk->chunk_id[2], chunk->chunk_id[3]);
+ }
+
+ return result;
+}
+
+// Read a single byte. Returns false on error.
+
+static boolean ReadByte(byte *result, FILE *stream)
+{
+ int c;
+
+ c = fgetc(stream);
+
+ if (c == EOF)
+ {
+ fprintf(stderr, "ReadByte: Unexpected end of file\n");
+ return false;
+ }
+ else
+ {
+ *result = (byte) c;
+
+ return true;
+ }
+}
+
+// Read a variable-length value.
+
+static boolean ReadVariableLength(unsigned int *result, FILE *stream)
+{
+ int i;
+ byte b;
+
+ *result = 0;
+
+ for (i=0; i<4; ++i)
+ {
+ if (!ReadByte(&b, stream))
+ {
+ fprintf(stderr, "ReadVariableLength: Error while reading "
+ "variable-length value\n");
+ return false;
+ }
+
+ // Insert the bottom seven bits from this byte.
+
+ *result <<= 7;
+ *result |= b & 0x7f;
+
+ // If the top bit is not set, this is the end.
+
+ if ((b & 0x80) == 0)
+ {
+ return true;
+ }
+ }
+
+ fprintf(stderr, "ReadVariableLength: Variable-length value too "
+ "long: maximum of four bytes\n");
+ return false;
+}
+
+// Read a byte sequence into the data buffer.
+
+static void *ReadByteSequence(unsigned int num_bytes, FILE *stream)
+{
+ unsigned int i;
+ byte *result;
+
+ // Allocate a buffer:
+
+ result = malloc(num_bytes);
+
+ if (result == NULL)
+ {
+ fprintf(stderr, "ReadByteSequence: Failed to allocate buffer\n");
+ return NULL;
+ }
+
+ // Read the data:
+
+ for (i=0; i<num_bytes; ++i)
+ {
+ if (!ReadByte(&result[i], stream))
+ {
+ fprintf(stderr, "ReadByteSequence: Error while reading byte %u\n",
+ i);
+ free(result);
+ return NULL;
+ }
+ }
+
+ return result;
+}
+
+// Read a MIDI channel event.
+// two_param indicates that the event type takes two parameters
+// (three byte) otherwise it is single parameter (two byte)
+
+static boolean ReadChannelEvent(midi_event_t *event,
+ byte event_type, boolean two_param,
+ FILE *stream)
+{
+ byte b;
+
+ // Set basics:
+
+ event->event_type = event_type & 0xf0;
+ event->data.channel.channel = event_type & 0x0f;
+
+ // Read parameters:
+
+ if (!ReadByte(&b, stream))
+ {
+ fprintf(stderr, "ReadChannelEvent: Error while reading channel "
+ "event parameters\n");
+ return false;
+ }
+
+ event->data.channel.param1 = b;
+
+ // Second parameter:
+
+ if (two_param)
+ {
+ if (!ReadByte(&b, stream))
+ {
+ fprintf(stderr, "ReadChannelEvent: Error while reading channel "
+ "event parameters\n");
+ return false;
+ }
+
+ event->data.channel.param2 = b;
+ }
+
+ return true;
+}
+
+// Read sysex event:
+
+static boolean ReadSysExEvent(midi_event_t *event, int event_type,
+ FILE *stream)
+{
+ event->event_type = event_type;
+
+ if (!ReadVariableLength(&event->data.sysex.length, stream))
+ {
+ fprintf(stderr, "ReadSysExEvent: Failed to read length of "
+ "SysEx block\n");
+ return false;
+ }
+
+ // Read the byte sequence:
+
+ event->data.sysex.data = ReadByteSequence(event->data.sysex.length, stream);
+
+ if (event->data.sysex.data == NULL)
+ {
+ fprintf(stderr, "ReadSysExEvent: Failed while reading SysEx event\n");
+ return false;
+ }
+
+ return true;
+}
+
+// Read meta event:
+
+static boolean ReadMetaEvent(midi_event_t *event, FILE *stream)
+{
+ byte b;
+
+ event->event_type = MIDI_EVENT_META;
+
+ // Read meta event type:
+
+ if (!ReadByte(&b, stream))
+ {
+ fprintf(stderr, "ReadMetaEvent: Failed to read meta event type\n");
+ return false;
+ }
+
+ event->data.meta.type = b;
+
+ // Read length of meta event data:
+
+ if (!ReadVariableLength(&event->data.meta.length, stream))
+ {
+ fprintf(stderr, "ReadSysExEvent: Failed to read length of "
+ "SysEx block\n");
+ return false;
+ }
+
+ // Read the byte sequence:
+
+ event->data.meta.data = ReadByteSequence(event->data.meta.length, stream);
+
+ if (event->data.meta.data == NULL)
+ {
+ fprintf(stderr, "ReadSysExEvent: Failed while reading SysEx event\n");
+ return false;
+ }
+
+ return true;
+}
+
+static boolean ReadEvent(midi_event_t *event, unsigned int *last_event_type,
+ FILE *stream)
+{
+ byte event_type;
+
+ if (!ReadVariableLength(&event->delta_time, stream))
+ {
+ fprintf(stderr, "ReadEvent: Failed to read event timestamp\n");
+ return false;
+ }
+
+ if (!ReadByte(&event_type, stream))
+ {
+ fprintf(stderr, "ReadEvent: Failed to read event type\n");
+ return false;
+ }
+
+ // All event types have their top bit set. Therefore, if
+ // the top bit is not set, it is because we are using the "same
+ // as previous event type" shortcut to save a byte. Skip back
+ // a byte so that we read this byte again.
+
+ if ((event_type & 0x80) == 0)
+ {
+ event_type = *last_event_type;
+
+ if (fseek(stream, -1, SEEK_CUR) < 0)
+ {
+ fprintf(stderr, "ReadEvent: Unable to seek in stream\n");
+ return false;
+ }
+ }
+ else
+ {
+ *last_event_type = event_type;
+ }
+
+ // Check event type:
+
+ switch (event_type & 0xf0)
+ {
+ // Two parameter channel events:
+
+ case MIDI_EVENT_NOTE_OFF:
+ case MIDI_EVENT_NOTE_ON:
+ case MIDI_EVENT_AFTERTOUCH:
+ case MIDI_EVENT_CONTROLLER:
+ case MIDI_EVENT_PITCH_BEND:
+ return ReadChannelEvent(event, event_type, true, stream);
+
+ // Single parameter channel events:
+
+ case MIDI_EVENT_PROGRAM_CHANGE:
+ case MIDI_EVENT_CHAN_AFTERTOUCH:
+ return ReadChannelEvent(event, event_type, false, stream);
+
+ default:
+ break;
+ }
+
+ // Specific value?
+
+ switch (event_type)
+ {
+ case MIDI_EVENT_SYSEX:
+ case MIDI_EVENT_SYSEX_SPLIT:
+ return ReadSysExEvent(event, event_type, stream);
+
+ case MIDI_EVENT_META:
+ return ReadMetaEvent(event, stream);
+
+ default:
+ break;
+ }
+
+ fprintf(stderr, "ReadEvent: Unknown MIDI event type: 0x%x\n", event_type);
+ return false;
+}
+
+// Free an event:
+
+static void FreeEvent(midi_event_t *event)
+{
+ // Some event types have dynamically allocated buffers assigned
+ // to them that must be freed.
+
+ switch (event->event_type)
+ {
+ case MIDI_EVENT_SYSEX:
+ case MIDI_EVENT_SYSEX_SPLIT:
+ free(event->data.sysex.data);
+ break;
+
+ case MIDI_EVENT_META:
+ free(event->data.meta.data);
+ break;
+
+ default:
+ // Nothing to do.
+ break;
+ }
+}
+
+// Read and check the track chunk header
+
+static boolean ReadTrackHeader(midi_track_t *track, FILE *stream)
+{
+ size_t records_read;
+ chunk_header_t chunk_header;
+
+ records_read = fread(&chunk_header, sizeof(chunk_header_t), 1, stream);
+
+ if (records_read < 1)
+ {
+ return false;
+ }
+
+ if (!CheckChunkHeader(&chunk_header, TRACK_CHUNK_ID))
+ {
+ return false;
+ }
+
+ track->data_len = SDL_SwapBE32(chunk_header.chunk_size);
+
+ return true;
+}
+
+static boolean ReadTrack(midi_track_t *track, FILE *stream)
+{
+ midi_event_t *new_events;
+ midi_event_t *event;
+ unsigned int last_event_type;
+
+ track->num_events = 0;
+ track->events = NULL;
+
+ // Read the header:
+
+ if (!ReadTrackHeader(track, stream))
+ {
+ return false;
+ }
+
+ // Then the events:
+
+ last_event_type = 0;
+
+ for (;;)
+ {
+ // Resize the track slightly larger to hold another event:
+
+ new_events = realloc(track->events,
+ sizeof(midi_event_t) * (track->num_events + 1));
+
+ if (new_events == NULL)
+ {
+ return false;
+ }
+
+ track->events = new_events;
+
+ // Read the next event:
+
+ event = &track->events[track->num_events];
+ if (!ReadEvent(event, &last_event_type, stream))
+ {
+ return false;
+ }
+
+ ++track->num_events;
+
+ // End of track?
+
+ if (event->event_type == MIDI_EVENT_META
+ && event->data.meta.type == MIDI_META_END_OF_TRACK)
+ {
+ break;
+ }
+ }
+
+ return true;
+}
+
+// Free a track:
+
+static void FreeTrack(midi_track_t *track)
+{
+ unsigned int i;
+
+ for (i=0; i<track->num_events; ++i)
+ {
+ FreeEvent(&track->events[i]);
+ }
+
+ free(track->events);
+}
+
+static boolean ReadAllTracks(midi_file_t *file, FILE *stream)
+{
+ unsigned int i;
+
+ // Allocate list of tracks and read each track:
+
+ file->tracks = malloc(sizeof(midi_track_t) * file->num_tracks);
+
+ if (file->tracks == NULL)
+ {
+ return false;
+ }
+
+ memset(file->tracks, 0, sizeof(midi_track_t) * file->num_tracks);
+
+ // Read each track:
+
+ for (i=0; i<file->num_tracks; ++i)
+ {
+ if (!ReadTrack(&file->tracks[i], stream))
+ {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Read and check the header chunk.
+
+static boolean ReadFileHeader(midi_file_t *file, FILE *stream)
+{
+ size_t records_read;
+ unsigned int format_type;
+
+ records_read = fread(&file->header, sizeof(midi_header_t), 1, stream);
+
+ if (records_read < 1)
+ {
+ return false;
+ }
+
+ if (!CheckChunkHeader(&file->header.chunk_header, HEADER_CHUNK_ID)
+ || SDL_SwapBE32(file->header.chunk_header.chunk_size) != 6)
+ {
+ fprintf(stderr, "ReadFileHeader: Invalid MIDI chunk header! "
+ "chunk_size=%i\n",
+ SDL_SwapBE32(file->header.chunk_header.chunk_size));
+ return false;
+ }
+
+ format_type = SDL_SwapBE16(file->header.format_type);
+ file->num_tracks = SDL_SwapBE16(file->header.num_tracks);
+
+ if ((format_type != 0 && format_type != 1)
+ || file->num_tracks < 1)
+ {
+ fprintf(stderr, "ReadFileHeader: Only type 0/1 "
+ "MIDI files supported!\n");
+ return false;
+ }
+
+ return true;
+}
+
+void MIDI_FreeFile(midi_file_t *file)
+{
+ int i;
+
+ if (file->tracks != NULL)
+ {
+ for (i=0; i<file->num_tracks; ++i)
+ {
+ FreeTrack(&file->tracks[i]);
+ }
+
+ free(file->tracks);
+ }
+
+ free(file);
+}
+
+midi_file_t *MIDI_LoadFile(char *filename)
+{
+ midi_file_t *file;
+ FILE *stream;
+
+ file = malloc(sizeof(midi_file_t));
+
+ if (file == NULL)
+ {
+ return NULL;
+ }
+
+ file->tracks = NULL;
+ file->num_tracks = 0;
+ file->buffer = NULL;
+ file->buffer_size = 0;
+
+ // Open file
+
+ stream = fopen(filename, "rb");
+
+ if (stream == NULL)
+ {
+ fprintf(stderr, "MIDI_LoadFile: Failed to open '%s'\n", filename);
+ MIDI_FreeFile(file);
+ return NULL;
+ }
+
+ // Read MIDI file header
+
+ if (!ReadFileHeader(file, stream))
+ {
+ fclose(stream);
+ MIDI_FreeFile(file);
+ return NULL;
+ }
+
+ // Read all tracks:
+
+ if (!ReadAllTracks(file, stream))
+ {
+ fclose(stream);
+ MIDI_FreeFile(file);
+ return NULL;
+ }
+
+ fclose(stream);
+
+ return file;
+}
+
+// Get the number of tracks in a MIDI file.
+
+unsigned int MIDI_NumTracks(midi_file_t *file)
+{
+ return file->num_tracks;
+}
+
+// Start iterating over the events in a track.
+
+midi_track_iter_t *MIDI_IterateTrack(midi_file_t *file, unsigned int track)
+{
+ midi_track_iter_t *iter;
+
+ assert(track < file->num_tracks);
+
+ iter = malloc(sizeof(*iter));
+ iter->track = &file->tracks[track];
+ iter->position = 0;
+
+ return iter;
+}
+
+void MIDI_FreeIterator(midi_track_iter_t *iter)
+{
+ free(iter);
+}
+
+// Get the time until the next MIDI event in a track.
+
+unsigned int MIDI_GetDeltaTime(midi_track_iter_t *iter)
+{
+ if (iter->position < iter->track->num_events)
+ {
+ midi_event_t *next_event;
+
+ next_event = &iter->track->events[iter->position];
+
+ return next_event->delta_time;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+// Get a pointer to the next MIDI event.
+
+int MIDI_GetNextEvent(midi_track_iter_t *iter, midi_event_t **event)
+{
+ if (iter->position < iter->track->num_events)
+ {
+ *event = &iter->track->events[iter->position];
+ ++iter->position;
+
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+}
+
+unsigned int MIDI_GetFileTimeDivision(midi_file_t *file)
+{
+ return file->header.time_division;
+}
+
+void MIDI_RestartIterator(midi_track_iter_t *iter)
+{
+ iter->position = 0;
+}
+
+#ifdef TEST
+
+static char *MIDI_EventTypeToString(midi_event_type_t event_type)
+{
+ switch (event_type)
+ {
+ case MIDI_EVENT_NOTE_OFF:
+ return "MIDI_EVENT_NOTE_OFF";
+ case MIDI_EVENT_NOTE_ON:
+ return "MIDI_EVENT_NOTE_ON";
+ case MIDI_EVENT_AFTERTOUCH:
+ return "MIDI_EVENT_AFTERTOUCH";
+ case MIDI_EVENT_CONTROLLER:
+ return "MIDI_EVENT_CONTROLLER";
+ case MIDI_EVENT_PROGRAM_CHANGE:
+ return "MIDI_EVENT_PROGRAM_CHANGE";
+ case MIDI_EVENT_CHAN_AFTERTOUCH:
+ return "MIDI_EVENT_CHAN_AFTERTOUCH";
+ case MIDI_EVENT_PITCH_BEND:
+ return "MIDI_EVENT_PITCH_BEND";
+ case MIDI_EVENT_SYSEX:
+ return "MIDI_EVENT_SYSEX";
+ case MIDI_EVENT_SYSEX_SPLIT:
+ return "MIDI_EVENT_SYSEX_SPLIT";
+ case MIDI_EVENT_META:
+ return "MIDI_EVENT_META";
+
+ default:
+ return "(unknown)";
+ }
+}
+
+void PrintTrack(midi_track_t *track)
+{
+ midi_event_t *event;
+ unsigned int i;
+
+ for (i=0; i<track->num_events; ++i)
+ {
+ event = &track->events[i];
+
+ if (event->delta_time > 0)
+ {
+ printf("Delay: %i ticks\n", event->delta_time);
+ }
+
+ printf("Event type: %s (%i)\n",
+ MIDI_EventTypeToString(event->event_type),
+ event->event_type);
+
+ switch(event->event_type)
+ {
+ case MIDI_EVENT_NOTE_OFF:
+ case MIDI_EVENT_NOTE_ON:
+ case MIDI_EVENT_AFTERTOUCH:
+ case MIDI_EVENT_CONTROLLER:
+ case MIDI_EVENT_PROGRAM_CHANGE:
+ case MIDI_EVENT_CHAN_AFTERTOUCH:
+ case MIDI_EVENT_PITCH_BEND:
+ printf("\tChannel: %i\n", event->data.channel.channel);
+ printf("\tParameter 1: %i\n", event->data.channel.param1);
+ printf("\tParameter 2: %i\n", event->data.channel.param2);
+ break;
+
+ case MIDI_EVENT_SYSEX:
+ case MIDI_EVENT_SYSEX_SPLIT:
+ printf("\tLength: %i\n", event->data.sysex.length);
+ break;
+
+ case MIDI_EVENT_META:
+ printf("\tMeta type: %i\n", event->data.meta.type);
+ printf("\tLength: %i\n", event->data.meta.length);
+ break;
+ }
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ midi_file_t *file;
+ unsigned int i;
+
+ if (argc < 2)
+ {
+ printf("Usage: %s <filename>\n", argv[0]);
+ exit(1);
+ }
+
+ file = MIDI_LoadFile(argv[1]);
+
+ if (file == NULL)
+ {
+ fprintf(stderr, "Failed to open %s\n", argv[1]);
+ exit(1);
+ }
+
+ for (i=0; i<file->num_tracks; ++i)
+ {
+ printf("\n== Track %i ==\n\n", i);
+
+ PrintTrack(&file->tracks[i]);
+ }
+
+ return 0;
+}
+
+#endif
+