shithub: choc

Download patch

ref: ca23c95db8053b82fb817241e5de83a881f931e5
parent: 076adeb0aa24e9bdc677435ddb6e444db58d5436
author: Simon Howard <fraggle@gmail.com>
date: Sun Aug 30 17:34:12 EDT 2009

Initial/basic MIDI track playback.

Subversion-branch: /branches/opl-branch
Subversion-revision: 1643

--- a/src/i_oplmusic.c
+++ b/src/i_oplmusic.c
@@ -42,7 +42,7 @@
 #include "opl.h"
 #include "midifile.h"
 
-#define TEST
+//#define TEST
 
 #define MAXMIDLENGTH (96 * 1024)
 #define GENMIDI_NUM_INSTRS  128
@@ -80,6 +80,37 @@
     genmidi_voice_t opl3_voice;
 } PACKEDATTR genmidi_instr_t;
 
+// Data associated with a channel of a track that is currently playing.
+
+typedef struct
+{
+    // The instrument currently used for this track.
+
+    genmidi_instr_t *instrument;
+
+    // Volume level
+
+    int volume;
+} opl_channel_data_t;
+
+// Data associated with a track that is currently playing.
+
+typedef struct
+{
+    // Data for each channel.
+
+    opl_channel_data_t channels[MIDI_CHANNELS_PER_TRACK];
+
+    // Track iterator used to read new events.
+
+    midi_track_iter_t *iter;
+
+    // Tempo control variables
+
+    unsigned int ticks_per_beat;
+    unsigned int us_per_beat;
+} opl_track_data_t;
+
 typedef struct opl_voice_s opl_voice_t;
 
 struct opl_voice_s
@@ -93,6 +124,15 @@
     // Currently-loaded instrument data
     genmidi_instr_t *current_instr;
 
+    // The channel currently using this voice.
+    opl_channel_data_t *channel;
+
+    // The note that this voice is playing.
+    unsigned int note;
+
+    // The frequency value being used.
+    unsigned int freq;
+
     // Next in freelist
     opl_voice_t *next;
 };
@@ -104,6 +144,22 @@
     { 0x03, 0x04, 0x05, 0x0b, 0x0c, 0x0d, 0x13, 0x14, 0x15 }
 };
 
+// Frequency values to use for each note.
+
+static const unsigned int note_frequencies[] = {
+
+    // These frequencies are only used for the first seven
+    // MIDI note values:
+
+    0x158, 0x16d, 0x183, 0x19a, 0x1b2, 0x1cc, 0x1e7, 
+
+    // These frequencies are used repeatedly, cycling around
+    // for each octave:
+
+    0x204, 0x223, 0x244, 0x266, 0x28b, 0x2b1,
+    0x2da, 0x306, 0x334, 0x365, 0x398, 0x3cf,
+};
+
 static boolean music_initialised = false;
 
 //static boolean musicpaused = false;
@@ -119,6 +175,10 @@
 static opl_voice_t voices[OPL_NUM_VOICES];
 static opl_voice_t *voice_free_list;
 
+// Track data for playing tracks:
+
+static opl_track_data_t *tracks;
+
 // In the initialisation stage, register writes are spaced by reading
 // from the register port (0).  After initialisation, spacing is
 // peformed by reading from the data port instead.  I have no idea
@@ -310,6 +370,9 @@
 {
     opl_voice_t **rover;
 
+    voice->channel = NULL;
+    voice->note = 0;
+
     // Search to the end of the freelist (This is how Doom behaves!)
 
     rover = &voice_free_list;
@@ -320,6 +383,7 @@
     }
 
     *rover = voice;
+    voice->next = NULL;
 }
 
 // Load data to the specified operator
@@ -498,13 +562,273 @@
     current_music_volume = volume;
 }
 
+static opl_voice_t *FindVoiceForNote(opl_channel_data_t *channel, int note)
+{
+    unsigned int i;
+
+    for (i=0; i<OPL_NUM_VOICES; ++i)
+    {
+        if (voices[i].channel == channel && voices[i].note == note)
+        {
+            return &voices[i];
+        }
+    }
+
+    return NULL;
+}
+
+static void NoteOffEvent(opl_track_data_t *track, midi_event_t *event)
+{
+    opl_voice_t *voice;
+    opl_channel_data_t *channel;
+
+    printf("note off: channel %i, %i, %i\n",
+           event->data.channel.channel,
+           event->data.channel.param1,
+           event->data.channel.param2);
+
+    channel = &track->channels[event->data.channel.channel];
+
+    // Find the voice being used to play the note.
+
+    voice = FindVoiceForNote(channel, event->data.channel.param1);
+
+    if (voice == NULL)
+    {
+        return;
+    }
+
+    // Note off.
+
+    WriteRegister(OPL_REGS_FREQ_2 + voice->index, voice->freq >> 8);
+
+    // Finished with this voice now.
+
+    ReleaseVoice(voice);
+}
+
+// Given a MIDI note number, get the corresponding OPL
+// frequency value to use.
+
+static unsigned int FrequencyForNote(unsigned int note)
+{
+    unsigned int octave;
+    unsigned int key_num;
+
+    // The first seven frequencies in the frequencies array are used
+    // only for the first seven MIDI notes.  After this, the frequency
+    // value loops around the same twelve notes, increasing the
+    // octave.
+
+    if (note < 7)
+    {
+        return note_frequencies[note];
+    }
+    else
+    {
+        octave = (note - 7) / 12;
+        key_num = (note - 7) % 12;
+
+        return note_frequencies[key_num + 7] | (octave << 10);
+    }
+}
+
+static void NoteOnEvent(opl_track_data_t *track, midi_event_t *event)
+{
+    opl_voice_t *voice;
+    opl_channel_data_t *channel;
+
+    printf("note on: channel %i, %i, %i\n",
+           event->data.channel.channel,
+           event->data.channel.param1,
+           event->data.channel.param2);
+
+    // The channel.
+
+    channel = &track->channels[event->data.channel.channel];
+
+    // Find a voice to use for this new note.
+
+    voice = GetFreeVoice();
+
+    if (voice == NULL)
+    {
+        return;
+    }
+
+    // Program the voice with the instrument data:
+
+    SetVoiceInstrument(voice, &channel->instrument->opl2_voice);
+
+    // TODO: Set the volume level.
+
+    WriteRegister(OPL_REGS_LEVEL + voice->op2, 0);
+
+    // Play the note.
+
+    voice->channel = channel;
+    voice->note = event->data.channel.param1;
+
+    // Write the frequency value to turn the note on.
+
+    voice->freq = FrequencyForNote(voice->note);
+
+    WriteRegister(OPL_REGS_FREQ_1 + voice->index, voice->freq & 0xff);
+    WriteRegister(OPL_REGS_FREQ_2 + voice->index, (voice->freq >> 8) | 0x20);
+}
+
+static void ProgramChangeEvent(opl_track_data_t *track, midi_event_t *event)
+{
+    int channel;
+    int instrument;
+
+    // Set the instrument used on this channel.
+
+    channel = event->data.channel.channel;
+    instrument = event->data.channel.param1;
+    track->channels[channel].instrument = &main_instrs[instrument];
+
+    // TODO: Look through existing voices that are turned on on this
+    // channel, and change the instrument.
+}
+
+static void ControllerEvent(opl_track_data_t *track, midi_event_t *event)
+{
+    printf("change controller: channel %i, %i, %i\n",
+           event->data.channel.channel,
+           event->data.channel.param1,
+           event->data.channel.param2);
+
+    // TODO: Volume, pan.
+}
+
+// Process a MIDI event from a track.
+
+static void ProcessEvent(opl_track_data_t *track, midi_event_t *event)
+{
+    switch (event->event_type)
+    {
+        case MIDI_EVENT_NOTE_OFF:
+            NoteOffEvent(track, event);
+            break;
+
+        case MIDI_EVENT_NOTE_ON:
+            NoteOnEvent(track, event);
+            break;
+
+        case MIDI_EVENT_CONTROLLER:
+            ControllerEvent(track, event);
+            break;
+
+        case MIDI_EVENT_PROGRAM_CHANGE:
+            ProgramChangeEvent(track, event);
+            break;
+
+        default:
+            fprintf(stderr, "Unknown MIDI event type %i\n", event->event_type);
+            break;
+    }
+}
+
+static void ScheduleTrack(opl_track_data_t *track);
+
+// Callback function invoked when another event needs to be read from
+// a track.
+
+static void TrackTimerCallback(void *arg)
+{
+    opl_track_data_t *track = arg;
+    midi_event_t *event;
+
+    // Get the next event and process it.
+
+    if (!MIDI_GetNextEvent(track->iter, &event))
+    {
+        return;
+    }
+
+    ProcessEvent(track, event);
+
+    // Reschedule the callback for the next event in the track.
+
+    ScheduleTrack(track);
+}
+
+static void ScheduleTrack(opl_track_data_t *track)
+{
+    unsigned int nticks;
+    unsigned int us;
+    static int total = 0;
+
+    // Get the number of microseconds until the next event.
+
+    nticks = MIDI_GetDeltaTime(track->iter);
+    us = (nticks * track->us_per_beat) / track->ticks_per_beat;
+    total += us;
+
+    // Set a timer to be invoked when the next event is
+    // ready to play.
+
+    OPL_SetCallback(us / 1000, TrackTimerCallback, track);
+}
+
+// Initialise a channel.
+
+static void InitChannel(opl_track_data_t *track, opl_channel_data_t *channel)
+{
+    // TODO: Work out sensible defaults?
+
+    channel->instrument = &main_instrs[0];
+    channel->volume = 127;
+}
+
+// Start a MIDI track playing:
+
+static void StartTrack(midi_file_t *file, unsigned int track_num)
+{
+    opl_track_data_t *track;
+    unsigned int i;
+
+    track = &tracks[track_num];
+    track->iter = MIDI_IterateTrack(file, track_num);
+    track->ticks_per_beat = MIDI_GetFileTimeDivision(file);
+
+    // Default is 120 bpm.
+    // TODO: this is wrong
+
+    track->us_per_beat = 500 * 1000 * 200;
+
+    for (i=0; i<MIDI_CHANNELS_PER_TRACK; ++i)
+    {
+        InitChannel(track, &track->channels[i]);
+    }
+
+    // Schedule the first event.
+
+    ScheduleTrack(track);
+}
+
 // Start playing a mid
 
 static void I_OPL_PlaySong(void *handle, int looping)
 {
-    if (!music_initialised)
+    midi_file_t *file;
+    unsigned int i;
+
+    if (!music_initialised || handle == NULL)
     {
         return;
+    }
+
+    file = handle;
+
+    // Allocate track data.
+
+    tracks = malloc(MIDI_NumTracks(file) * sizeof(opl_track_data_t));
+
+    for (i=0; i<MIDI_NumTracks(file); ++i)
+    {
+        StartTrack(file, i);
     }
 }
 
--- a/src/midifile.c
+++ b/src/midifile.c
@@ -26,6 +26,7 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <assert.h>
 
 #include "doomdef.h"
 #include "doomtype.h"
@@ -62,6 +63,12 @@
     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;
@@ -624,6 +631,68 @@
     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;
+}
+
+// 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;
 }
 
 #ifdef TEST
--- a/src/midifile.h
+++ b/src/midifile.h
@@ -27,7 +27,10 @@
 #define MIDIFILE_H
 
 typedef struct midi_file_s midi_file_t;
+typedef struct midi_track_iter_s midi_track_iter_t;
 
+#define MIDI_CHANNELS_PER_TRACK 16
+
 typedef enum
 {
     MIDI_EVENT_NOTE_OFF        = 0x80,
@@ -129,8 +132,33 @@
     } data;
 } midi_event_t;
 
+// Load a MIDI file.
+
 midi_file_t *MIDI_LoadFile(char *filename);
+
+// Free a MIDI file.
+
 void MIDI_FreeFile(midi_file_t *file);
+
+// Get the time division value from the MIDI header.
+
+unsigned int MIDI_GetFileTimeDivision(midi_file_t *file);
+
+// Get the number of tracks in a MIDI file.
+
+unsigned int MIDI_NumTracks(midi_file_t *file);
+
+// Start iterating over the events in a track.
+
+midi_track_iter_t *MIDI_IterateTrack(midi_file_t *file, unsigned int track_num);
+
+// Get the time until the next MIDI event in a track.
+
+unsigned int MIDI_GetDeltaTime(midi_track_iter_t *iter);
+
+// Get a pointer to the next MIDI event.
+
+int MIDI_GetNextEvent(midi_track_iter_t *iter, midi_event_t **event);
 
 #endif /* #ifndef MIDIFILE_H */