ref: 725c9b8ab73131f2d8a527e90ebc2938b6093948
parent: d2efe0521e570178ffabac2379a12f5dc443aa0f
author: Roman Fomin <[email protected]>
date: Wed Dec 14 18:45:28 EST 2022
win midi: Support SysEx, proper device reset and other updates (#1558) * win midi: Support SysEx, proper device reset and other updates * Make music_win_module, clean up i_sdlmusic.c * Add support of SysEx messages. * Correctly reset MIDI devices with SysEx messages (by ceski). * Implement a "capital tone fallback" emulation (by ceski). * Fix looping timing, various fixes (by ceski). * Add full support of EMIDI, loop points (Final Fantasy and RPG Maker) (by ceski). * Fix Linux build * Fix Makefile * Add new config variables * Add missed entries to music_win_devices[] (for config compatibility?) * Remove unused function It was added for the initial version of i_winmusc.c * Add missed `const` in i_winmusic.c * Delete file immediately after use. * Add `const` and header guards to midifallback.*
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -65,7 +65,8 @@
i_timer.c i_timer.h
i_video.c i_video.h
i_videohr.c i_videohr.h
- i_winmusic.c i_winmusic.h
+ i_winmusic.c
+ midifallback.c midifallback.h
midifile.c midifile.h
mus2mid.c mus2mid.h
m_bbox.c m_bbox.h
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -81,7 +81,8 @@
i_timer.c i_timer.h \
i_video.c i_video.h \
i_videohr.c i_videohr.h \
-i_winmusic.c i_winmusic.h \
+i_winmusic.c \
+midifallback.c midifallback.h \
midifile.c midifile.h \
mus2mid.c mus2mid.h \
m_bbox.c m_bbox.h \
--- a/src/i_oplmusic.c
+++ b/src/i_oplmusic.c
@@ -1197,7 +1197,7 @@
switch (controller)
{
- case MIDI_CONTROLLER_MAIN_VOLUME:
+ case MIDI_CONTROLLER_VOLUME_MSB:
SetChannelVolume(channel, param, true);
break;
--- a/src/i_sdlmusic.c
+++ b/src/i_sdlmusic.c
@@ -25,8 +25,6 @@
#include "SDL.h"
#include "SDL_mixer.h"
-#include "i_winmusic.h"
-
#include "config.h"
#include "doomtype.h"
#include "memio.h"
@@ -138,8 +136,6 @@
static boolean sdl_was_initialized = false;
-static boolean win_midi_stream_opened = false;
-
static boolean musicpaused = false;
static int current_music_volume;
@@ -161,13 +157,6 @@
{
if (music_initialized)
{
-#if defined(_WIN32)
- if (win_midi_stream_opened)
- {
- I_WIN_ShutdownMusic();
- win_midi_stream_opened = false;
- }
-#endif
Mix_HaltMusic();
music_initialized = false;
@@ -284,15 +273,6 @@
Mix_SetMusicCMD(snd_musiccmd);
}
-#if defined(_WIN32)
- // Don't enable it for GUS or Fluidsynth, since they handle their own volume
- // just fine.
- if (snd_musicdevice != SNDDEVICE_GUS && !fluidsynth_sf_is_set)
- {
- win_midi_stream_opened = I_WIN_InitMusic();
- }
-#endif
-
return music_initialized;
}
@@ -314,9 +294,6 @@
vol = (current_music_volume * MIX_MAX_VOLUME) / 127;
}
-#if defined(_WIN32)
- I_WIN_SetMusicVolume(vol);
-#endif
Mix_VolumeMusic(vol);
}
@@ -341,7 +318,7 @@
return;
}
- if (handle == NULL && !win_midi_stream_opened)
+ if (handle == NULL)
{
return;
}
@@ -355,16 +332,7 @@
loops = 1;
}
-#if defined(_WIN32)
- if (win_midi_stream_opened)
- {
- I_WIN_PlaySong(looping);
- }
- else
-#endif
- {
- Mix_PlayMusic((Mix_Music *) handle, loops);
- }
+ Mix_PlayMusic((Mix_Music *) handle, loops);
}
static void I_SDL_PauseSong(void)
@@ -374,18 +342,9 @@
return;
}
-#if defined(_WIN32)
- if (win_midi_stream_opened)
- {
- I_WIN_PauseSong();
- }
- else
-#endif
- {
- musicpaused = true;
+ musicpaused = true;
- UpdateMusicVolume();
- }
+ UpdateMusicVolume();
}
static void I_SDL_ResumeSong(void)
@@ -395,18 +354,9 @@
return;
}
-#if defined(_WIN32)
- if (win_midi_stream_opened)
- {
- I_WIN_ResumeSong();
- }
- else
-#endif
- {
- musicpaused = false;
+ musicpaused = false;
- UpdateMusicVolume();
- }
+ UpdateMusicVolume();
}
static void I_SDL_StopSong(void)
@@ -416,16 +366,7 @@
return;
}
-#if defined(_WIN32)
- if (win_midi_stream_opened)
- {
- I_WIN_StopSong();
- }
- else
-#endif
- {
- Mix_HaltMusic();
- }
+ Mix_HaltMusic();
}
static void I_SDL_UnRegisterSong(void *handle)
@@ -437,19 +378,10 @@
return;
}
-#if defined(_WIN32)
- if (win_midi_stream_opened)
+ if (handle != NULL)
{
- I_WIN_UnRegisterSong();
+ Mix_FreeMusic(music);
}
- else
-#endif
- {
- if (handle != NULL)
- {
- Mix_FreeMusic(music);
- }
- }
}
// Determine whether memory block is a .mid file
@@ -515,40 +447,21 @@
// by now, but Mix_SetMusicCMD() only works with Mix_LoadMUS(), so
// we have to generate a temporary file.
-#if defined(_WIN32)
- // If we do not have an external music command defined, play
- // music with the Windows native MIDI.
- if (win_midi_stream_opened)
+ music = Mix_LoadMUS(filename);
+ if (music == NULL)
{
- if (I_WIN_RegisterSong(filename))
- {
- music = (void *) 1;
- }
- else
- {
- music = NULL;
- fprintf(stderr, "Error loading midi: Failed to register song.\n");
- }
+ // Failed to load
+ fprintf(stderr, "Error loading midi: %s\n", Mix_GetError());
}
- else
-#endif
- {
- music = Mix_LoadMUS(filename);
- if (music == NULL)
- {
- // Failed to load
- fprintf(stderr, "Error loading midi: %s\n", Mix_GetError());
- }
- // Remove the temporary MIDI file; however, when using an external
- // MIDI program we can't delete the file. Otherwise, the program
- // won't find the file to play. This means we leave a mess on
- // disk :(
+ // Remove the temporary MIDI file; however, when using an external
+ // MIDI program we can't delete the file. Otherwise, the program
+ // won't find the file to play. This means we leave a mess on
+ // disk :(
- if (strlen(snd_musiccmd) == 0)
- {
- M_remove(filename);
- }
+ if (strlen(snd_musiccmd) == 0)
+ {
+ M_remove(filename);
}
free(filename);
--- a/src/i_sound.c
+++ b/src/i_sound.c
@@ -91,6 +91,9 @@
static music_module_t *music_modules[] =
{
+#ifdef _WIN32
+ &music_win_module,
+#endif
#ifndef DISABLE_SDL2MIXER
&music_sdl_module,
#endif // DISABLE_SDL2MIXER
@@ -162,6 +165,18 @@
music_modules[i]->sound_devices,
music_modules[i]->num_sound_devices))
{
+ #ifdef _WIN32
+ // Skip the native Windows MIDI module if using Timidity or
+ // FluidSynth.
+
+ if ((strcmp(timidity_cfg_path, "")
+ || strcmp(fluidsynth_sf_path, ""))
+ && music_modules[i] == &music_win_module)
+ {
+ continue;
+ }
+ #endif
+
// Initialize the module
if (music_modules[i]->Init())
@@ -492,6 +507,8 @@
M_BindIntVariable("gus_ram_kb", &gus_ram_kb);
#ifdef _WIN32
M_BindStringVariable("winmm_midi_device", &winmm_midi_device);
+ M_BindIntVariable("winmm_reset_type", &winmm_reset_type);
+ M_BindIntVariable("winmm_reset_delay", &winmm_reset_delay);
M_BindIntVariable("winmm_reverb_level", &winmm_reverb_level);
M_BindIntVariable("winmm_chorus_level", &winmm_chorus_level);
#endif
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -258,6 +258,7 @@
extern music_module_t music_sdl_module;
extern music_module_t music_opl_module;
extern music_module_t music_pack_module;
+extern music_module_t music_win_module;
// For OPL module:
@@ -269,13 +270,11 @@
extern char *fluidsynth_sf_path;
extern char *timidity_cfg_path;
#ifdef _WIN32
+extern char *winmm_midi_device;
+extern int winmm_reset_type;
+extern int winmm_reset_delay;
extern int winmm_reverb_level;
extern int winmm_chorus_level;
-#endif
-
-
-#ifdef _WIN32
-extern char *winmm_midi_device;
#endif
#endif
--- a/src/i_winmusic.c
+++ b/src/i_winmusic.c
@@ -1,5 +1,6 @@
//
-// Copyright(C) 2021 Roman Fomin
+// Copyright(C) 2021-2022 Roman Fomin
+// Copyright(C) 2022 ceski
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
@@ -19,33 +20,78 @@
#include <windows.h>
#include <mmsystem.h>
-#include <math.h>
+#include <mmreg.h>
#include <stdio.h>
#include <stdlib.h>
+#include <math.h>
#include "doomtype.h"
+#include "i_sound.h"
+#include "i_system.h"
#include "m_misc.h"
+#include "memio.h"
+#include "mus2mid.h"
#include "midifile.h"
-#include "i_sound.h"
-#include "i_winmusic.h"
+#include "midifallback.h"
+char *winmm_midi_device = NULL;
+int winmm_reverb_level = -1;
+int winmm_chorus_level = -1;
-#define BETWEEN(l,u,x) (((l)>(x))?(l):((x)>(u))?(u):(x))
+enum
+{
+ RESET_TYPE_NONE,
+ RESET_TYPE_GS,
+ RESET_TYPE_GM,
+ RESET_TYPE_GM2,
+ RESET_TYPE_XG,
+};
-#define REVERB_MIN 0
-#define REVERB_MAX 127
-#define CHORUS_MIN 0
-#define CHORUS_MAX 127
+int winmm_reset_type = RESET_TYPE_GS;
+int winmm_reset_delay = 0;
-char *winmm_midi_device = NULL;
-int winmm_reverb_level = 40;
-int winmm_chorus_level = 0;
+static const byte gs_reset[] = {
+ 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7
+};
+static const byte gm_system_on[] = {
+ 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7
+};
+
+static const byte gm2_system_on[] = {
+ 0xF0, 0x7E, 0x7F, 0x09, 0x03, 0xF7
+};
+
+static const byte xg_system_on[] = {
+ 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7
+};
+
+static const byte ff_loopStart[] = {'l', 'o', 'o', 'p', 'S', 't', 'a', 'r', 't'};
+static const byte ff_loopEnd[] = {'l', 'o', 'o', 'p', 'E', 'n', 'd'};
+
+static boolean use_fallback;
+
+#define DEFAULT_VOLUME 100
+static int channel_volume[MIDI_CHANNELS_PER_TRACK];
+static float volume_factor = 0.0f;
+static boolean update_volume = false;
+
+static DWORD timediv;
+static DWORD tempo;
+
+static UINT MidiDevice;
static HMIDISTRM hMidiStream;
+static MIDIHDR MidiStreamHdr;
static HANDLE hBufferReturnEvent;
static HANDLE hExitEvent;
static HANDLE hPlayerThread;
+// MS GS Wavetable Synth Device ID.
+static int ms_gs_synth = MIDI_MAPPER;
+
+// EMIDI device for track designation.
+static int emidi_device;
+
// This is a reduced Windows MIDIEVENT structure for MEVT_F_SHORT
// type of events.
@@ -58,49 +104,56 @@
typedef struct
{
- native_event_t *native_events;
- int num_events;
- int position;
+ midi_track_iter_t *iter;
+ unsigned int elapsed_time;
+ unsigned int saved_elapsed_time;
+ boolean end_of_track;
+ boolean saved_end_of_track;
+ unsigned int emidi_device_flags;
+ boolean emidi_designated;
+ boolean emidi_program;
+ boolean emidi_volume;
+ int emidi_loop_count;
+} win_midi_track_t;
+
+typedef struct
+{
+ win_midi_track_t *tracks;
+ unsigned int elapsed_time;
+ unsigned int saved_elapsed_time;
+ unsigned int num_tracks;
boolean looping;
+ boolean ff_loop;
+ boolean ff_restart;
+ boolean rpg_loop;
} win_midi_song_t;
static win_midi_song_t song;
+#define BUFFER_INITIAL_SIZE 1024
+
typedef struct
{
- midi_track_iter_t *iter;
- int absolute_time;
-} win_midi_track_t;
+ byte *data;
+ unsigned int size;
+ unsigned int position;
+} buffer_t;
-static float volume_factor = 1.0;
+static buffer_t buffer;
-// Save the last volume for each MIDI channel.
-
-static int channel_volume[MIDI_CHANNELS_PER_TRACK];
-
-// Macros for use with the Windows MIDIEVENT dwEvent field.
-
-#define MIDIEVENT_CHANNEL(x) (x & 0x0000000F)
-#define MIDIEVENT_TYPE(x) (x & 0x000000F0)
-#define MIDIEVENT_DATA1(x) ((x & 0x0000FF00) >> 8)
-#define MIDIEVENT_VOLUME(x) ((x & 0x007F0000) >> 16)
-
// Maximum of 4 events in the buffer for faster volume updates.
#define STREAM_MAX_EVENTS 4
-typedef struct
-{
- native_event_t events[STREAM_MAX_EVENTS];
- int num_events;
- MIDIHDR MidiStreamHdr;
-} buffer_t;
+#define MAKE_EVT(a, b, c, d) ((DWORD)((a) | ((b) << 8) | ((c) << 16) | ((d) << 24)))
-static buffer_t buffer;
+#define PADDED_SIZE(x) (((x) + sizeof(DWORD) - 1) & ~(sizeof(DWORD) - 1))
+static boolean initial_playback = false;
+
// Message box for midiStream errors.
-static void MidiErrorMessageBox(DWORD dwError)
+static void MidiError(const char *prefix, DWORD dwError)
{
char szErrorBuf[MAXERRORLENGTH];
MMRESULT mmr;
@@ -108,300 +161,991 @@
mmr = midiOutGetErrorText(dwError, (LPSTR) szErrorBuf, MAXERRORLENGTH);
if (mmr == MMSYSERR_NOERROR)
{
- MessageBox(NULL, szErrorBuf, "midiStream Error", MB_ICONEXCLAMATION);
+ char *msg = M_StringJoin(prefix, ": ", szErrorBuf, NULL);
+ MessageBox(NULL, msg, "midiStream Error", MB_ICONEXCLAMATION);
+ free(msg);
}
else
{
- fprintf(stderr, "Unknown midiStream error.\n");
+ fprintf(stderr, "%s: Unknown midiStream error.\n", prefix);
}
}
-// Fill the buffer with MIDI events, adjusting the volume as needed.
+// midiStream callback.
-static void FillBuffer(void)
+static void CALLBACK MidiStreamProc(HMIDIOUT hMidi, UINT uMsg,
+ DWORD_PTR dwInstance, DWORD_PTR dwParam1,
+ DWORD_PTR dwParam2)
{
- int i;
-
- for (i = 0; i < STREAM_MAX_EVENTS; ++i)
+ if (uMsg == MOM_DONE)
{
- native_event_t *event = &buffer.events[i];
+ SetEvent(hBufferReturnEvent);
+ }
+}
- if (song.position >= song.num_events)
+static void AllocateBuffer(const unsigned int size)
+{
+ MIDIHDR *hdr = &MidiStreamHdr;
+ MMRESULT mmr;
+
+ if (buffer.data)
+ {
+ mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+ if (mmr != MMSYSERR_NOERROR)
{
- if (song.looping)
- {
- song.position = 0;
- }
- else
- {
- break;
- }
+ MidiError("midiOutUnprepareHeader", mmr);
}
+ }
- *event = song.native_events[song.position];
+ buffer.size = PADDED_SIZE(size);
+ buffer.data = I_Realloc(buffer.data, buffer.size);
- if (MIDIEVENT_TYPE(event->dwEvent) == MIDI_EVENT_CONTROLLER &&
- MIDIEVENT_DATA1(event->dwEvent) == MIDI_CONTROLLER_MAIN_VOLUME)
- {
- int volume = MIDIEVENT_VOLUME(event->dwEvent);
+ hdr->lpData = (LPSTR)buffer.data;
+ hdr->dwBytesRecorded = 0;
+ hdr->dwBufferLength = buffer.size;
+ mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+ if (mmr != MMSYSERR_NOERROR)
+ {
+ MidiError("midiOutPrepareHeader", mmr);
+ }
+}
- channel_volume[MIDIEVENT_CHANNEL(event->dwEvent)] = volume;
+static void WriteBufferPad(void)
+{
+ unsigned int padding = PADDED_SIZE(buffer.position);
+ memset(buffer.data + buffer.position, 0, padding - buffer.position);
+ buffer.position = padding;
+}
- volume *= volume_factor;
+static void WriteBuffer(const byte *ptr, unsigned int size)
+{
+ if (buffer.position + size >= buffer.size)
+ {
+ AllocateBuffer(size + buffer.size * 2);
+ }
- event->dwEvent = (event->dwEvent & 0xFF00FFFF) |
- ((volume & 0x7F) << 16);
- }
+ memcpy(buffer.data + buffer.position, ptr, size);
+ buffer.position += size;
+}
- song.position++;
+static void StreamOut(void)
+{
+ MIDIHDR *hdr = &MidiStreamHdr;
+ MMRESULT mmr;
+
+ hdr->lpData = (LPSTR)buffer.data;
+ hdr->dwBytesRecorded = buffer.position;
+
+ mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR));
+ if (mmr != MMSYSERR_NOERROR)
+ {
+ MidiError("midiStreamOut", mmr);
}
+}
- buffer.num_events = i;
+static void SendShortMsg(int time, int status, int channel, int param1, int param2)
+{
+ native_event_t native_event;
+ native_event.dwDeltaTime = time;
+ native_event.dwStreamID = 0;
+ native_event.dwEvent = MAKE_EVT(status | channel, param1, param2, MEVT_SHORTMSG);
+ WriteBuffer((byte *)&native_event, sizeof(native_event_t));
}
-// Queue MIDI events.
+static void SendLongMsg(int time, const byte *ptr, int length)
+{
+ native_event_t native_event;
+ native_event.dwDeltaTime = time;
+ native_event.dwStreamID = 0;
+ native_event.dwEvent = MAKE_EVT(length, 0, 0, MEVT_LONGMSG);
+ WriteBuffer((byte *)&native_event, sizeof(native_event_t));
+ WriteBuffer(ptr, length);
+ WriteBufferPad();
+}
-static void StreamOut(void)
+static void SendNOPMsg(int time)
{
- MIDIHDR *hdr = &buffer.MidiStreamHdr;
- MMRESULT mmr;
+ native_event_t native_event;
+ native_event.dwDeltaTime = time;
+ native_event.dwStreamID = 0;
+ native_event.dwEvent = MAKE_EVT(0, 0, 0, MEVT_NOP);
+ WriteBuffer((byte *)&native_event, sizeof(native_event_t));
+}
- int num_events = buffer.num_events;
+static void SendDelayMsg(int time_ms)
+{
+ // Convert ms to ticks (see "Standard MIDI Files 1.0" page 14).
+ int time_ticks = (float)time_ms * 1000 * timediv / tempo + 0.5f;
+ SendNOPMsg(time_ticks);
+}
- if (num_events == 0)
+static void UpdateTempo(int time, midi_event_t *event)
+{
+ native_event_t native_event;
+
+ tempo = MAKE_EVT(event->data.meta.data[2], event->data.meta.data[1],
+ event->data.meta.data[0], 0);
+
+ native_event.dwDeltaTime = time;
+ native_event.dwStreamID = 0;
+ native_event.dwEvent = MAKE_EVT(tempo, 0, 0, MEVT_TEMPO);
+ WriteBuffer((byte *)&native_event, sizeof(native_event_t));
+}
+
+static void SendVolumeMsg(int time, int channel, int volume)
+{
+ int scaled_volume = volume * volume_factor + 0.5f;
+ SendShortMsg(time, MIDI_EVENT_CONTROLLER, channel,
+ MIDI_CONTROLLER_VOLUME_MSB, scaled_volume);
+ channel_volume[channel] = volume;
+}
+
+static void UpdateVolume(void)
+{
+ int i;
+
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
{
- return;
+ SendVolumeMsg(0, i, channel_volume[i]);
}
+}
- hdr->lpData = (LPSTR)buffer.events;
- hdr->dwBytesRecorded = num_events * sizeof(native_event_t);
+static void ResetVolume(void)
+{
+ int i;
- mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR));
- if (mmr != MMSYSERR_NOERROR)
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
{
- MidiErrorMessageBox(mmr);
+ SendVolumeMsg(0, i, DEFAULT_VOLUME);
}
}
-// midiStream callback.
+static void ResetReverb(int reset_type)
+{
+ int i;
+ int reverb = winmm_reverb_level;
-static void CALLBACK MidiStreamProc(HMIDIIN hMidi, UINT uMsg,
- DWORD_PTR dwInstance, DWORD_PTR dwParam1,
- DWORD_PTR dwParam2)
+ if (reverb == -1 && reset_type == RESET_TYPE_NONE)
+ {
+ // No reverb specified and no SysEx reset selected. Use GM default.
+ reverb = 40;
+ }
+
+ if (reverb > -1)
+ {
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+ {
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_REVERB, reverb);
+ }
+ }
+}
+
+static void ResetChorus(int reset_type)
{
- if (uMsg == MOM_DONE)
+ int i;
+ int chorus = winmm_chorus_level;
+
+ if (chorus == -1 && reset_type == RESET_TYPE_NONE)
{
- SetEvent(hBufferReturnEvent);
+ // No chorus specified and no SysEx reset selected. Use GM default.
+ chorus = 0;
}
+
+ if (chorus > -1)
+ {
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+ {
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_CHORUS, chorus);
+ }
+ }
}
-// The Windows API documentation states: "Applications should not call any
-// multimedia functions from inside the callback function, as doing so can
-// cause a deadlock." We use thread to avoid possible deadlocks.
+static void ResetControllers(void)
+{
+ int i;
-static DWORD WINAPI PlayerProc(void)
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+ {
+ // Reset commonly used controllers.
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RESET_ALL_CTRLS, 0);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_PAN, 64);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_BANK_SELECT_MSB, 0);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_BANK_SELECT_LSB, 0);
+ SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE, i, 0, 0);
+ }
+}
+
+static void ResetPitchBendSensitivity(void)
{
- HANDLE events[2] = { hBufferReturnEvent, hExitEvent };
+ int i;
- while (1)
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
{
- switch (WaitForMultipleObjects(2, events, FALSE, INFINITE))
+ // Set RPN MSB/LSB to pitch bend sensitivity.
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 0);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 0);
+
+ // Reset pitch bend sensitivity to +/- 2 semitones and 0 cents.
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_MSB, 2);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_LSB, 0);
+
+ // Set RPN MSB/LSB to null value after data entry.
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 127);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 127);
+ }
+}
+
+static void ResetDevice(void)
+{
+ int i;
+ int reset_type;
+
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+ {
+ // Stop sound prior to reset to prevent volume spikes.
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+ SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0);
+ }
+
+ if (MidiDevice == ms_gs_synth)
+ {
+ // MS GS Wavetable Synth lacks instrument fallback in GS mode which can
+ // cause wrong or silent notes (MAYhem19.wad D_DM2TTL). It also responds
+ // to XG System On when it should ignore it.
+ switch (winmm_reset_type)
{
- case WAIT_OBJECT_0:
- FillBuffer();
- StreamOut();
+ case RESET_TYPE_NONE:
+ reset_type = RESET_TYPE_NONE;
break;
- case WAIT_OBJECT_0 + 1:
- return 0;
+ case RESET_TYPE_GS:
+ reset_type = RESET_TYPE_GS;
+ break;
+
+ default:
+ reset_type = RESET_TYPE_GM;
+ break;
}
}
- return 0;
+ else // Unknown device
+ {
+ // Most devices support GS mode. Exceptions are some older hardware and
+ // a few older VSTis. Some devices lack instrument fallback in GS mode.
+ switch (winmm_reset_type)
+ {
+ case RESET_TYPE_NONE:
+ case RESET_TYPE_GM:
+ case RESET_TYPE_GM2:
+ case RESET_TYPE_XG:
+ reset_type = winmm_reset_type;
+ break;
+
+ default:
+ reset_type = RESET_TYPE_GS;
+ break;
+ }
+ }
+
+ // Use instrument fallback in GS mode.
+ MIDI_ResetFallback();
+ use_fallback = (reset_type == RESET_TYPE_GS);
+
+ // Assign EMIDI device for track designation.
+ emidi_device = (reset_type == RESET_TYPE_GS);
+
+ switch (reset_type)
+ {
+ case RESET_TYPE_NONE:
+ ResetControllers();
+ break;
+
+ case RESET_TYPE_GS:
+ SendLongMsg(0, gs_reset, sizeof(gs_reset));
+ break;
+
+ case RESET_TYPE_GM:
+ SendLongMsg(0, gm_system_on, sizeof(gm_system_on));
+ break;
+
+ case RESET_TYPE_GM2:
+ SendLongMsg(0, gm2_system_on, sizeof(gm2_system_on));
+ break;
+
+ case RESET_TYPE_XG:
+ SendLongMsg(0, xg_system_on, sizeof(xg_system_on));
+ break;
+ }
+
+ if (reset_type == RESET_TYPE_NONE || MidiDevice == ms_gs_synth)
+ {
+ // MS GS Wavetable Synth doesn't reset pitch bend sensitivity, even
+ // when sending a GM/GS reset, so do it manually.
+ ResetPitchBendSensitivity();
+ }
+
+ ResetReverb(reset_type);
+ ResetChorus(reset_type);
+
+ // Reset volume (initial playback or on shutdown if no SysEx reset).
+ if (initial_playback || reset_type == RESET_TYPE_NONE)
+ {
+ // Scale by slider on initial playback, max on shutdown.
+ volume_factor = initial_playback ? volume_factor : 1.0f;
+ ResetVolume();
+ }
+
+ // Send delay after reset. This is for hardware devices only (e.g. SC-55).
+ if (winmm_reset_delay > 0)
+ {
+ SendDelayMsg(winmm_reset_delay);
+ }
}
-// Convert a multi-track MIDI file to an array of Windows MIDIEVENT structures.
+static boolean IsSysExReset(const byte *msg, int length)
+{
+ if (length < 5)
+ {
+ return false;
+ }
-static void MIDItoStream(midi_file_t *file)
+ switch (msg[0])
+ {
+ case 0x41: // Roland
+ switch (msg[2])
+ {
+ case 0x42: // GS
+ switch (msg[3])
+ {
+ case 0x12: // DT1
+ if (length == 10 &&
+ msg[4] == 0x00 && // Address MSB
+ msg[5] == 0x00 && // Address
+ msg[6] == 0x7F && // Address LSB
+ ((msg[7] == 0x00 && // Data (MODE-1)
+ msg[8] == 0x01) || // Checksum (MODE-1)
+ (msg[7] == 0x01 && // Data (MODE-2)
+ msg[8] == 0x00))) // Checksum (MODE-2)
+ {
+ // SC-88 System Mode Set
+ // 41 <dev> 42 12 00 00 7F 00 01 F7 (MODE-1)
+ // 41 <dev> 42 12 00 00 7F 01 00 F7 (MODE-2)
+ return true;
+ }
+ else if (length == 10 &&
+ msg[4] == 0x40 && // Address MSB
+ msg[5] == 0x00 && // Address
+ msg[6] == 0x7F && // Address LSB
+ msg[7] == 0x00 && // Data (GS Reset)
+ msg[8] == 0x41) // Checksum
+ {
+ // GS Reset
+ // 41 <dev> 42 12 40 00 7F 00 41 F7
+ return true;
+ }
+ break;
+ }
+ break;
+ }
+ break;
+
+ case 0x43: // Yamaha
+ switch (msg[2])
+ {
+ case 0x2B: // TG300
+ if (length == 9 &&
+ msg[3] == 0x00 && // Start Address b20 - b14
+ msg[4] == 0x00 && // Start Address b13 - b7
+ msg[5] == 0x7F && // Start Address b6 - b0
+ msg[6] == 0x00 && // Data
+ msg[7] == 0x01) // Checksum
+ {
+ // TG300 All Parameter Reset
+ // 43 <dev> 2B 00 00 7F 00 01 F7
+ return true;
+ }
+ break;
+
+ case 0x4C: // XG
+ if (length == 8 &&
+ msg[3] == 0x00 && // Address High
+ msg[4] == 0x00 && // Address Mid
+ (msg[5] == 0x7E || // Address Low (System On)
+ msg[5] == 0x7F) && // Address Low (All Parameter Reset)
+ msg[6] == 0x00) // Data
+ {
+ // XG System On, XG All Parameter Reset
+ // 43 <dev> 4C 00 00 7E 00 F7
+ // 43 <dev> 4C 00 00 7F 00 F7
+ return true;
+ }
+ break;
+ }
+ break;
+
+ case 0x7E: // Universal Non-Real Time
+ switch (msg[2])
+ {
+ case 0x09: // General Midi
+ if (length == 5 &&
+ (msg[3] == 0x01 || // GM System On
+ msg[3] == 0x02 || // GM System Off
+ msg[3] == 0x03)) // GM2 System On
+ {
+ // GM System On/Off, GM2 System On
+ // 7E <dev> 09 01 F7
+ // 7E <dev> 09 02 F7
+ // 7E <dev> 09 03 F7
+ return true;
+ }
+ break;
+ }
+ break;
+ }
+ return false;
+}
+
+static void SendSysExMsg(int time, const byte *data, int length)
{
- int i;
+ native_event_t native_event;
+ boolean is_sysex_reset;
+ const byte event_type = MIDI_EVENT_SYSEX;
- int num_tracks = MIDI_NumTracks(file);
- win_midi_track_t *tracks = malloc(num_tracks * sizeof(win_midi_track_t));
+ is_sysex_reset = IsSysExReset(data, length);
- int current_time = 0;
+ if (is_sysex_reset && MidiDevice == ms_gs_synth)
+ {
+ // Ignore SysEx reset from MIDI file for MS GS Wavetable Synth.
+ SendNOPMsg(time);
+ return;
+ }
- for (i = 0; i < num_tracks; ++i)
+ // Send the SysEx message.
+ native_event.dwDeltaTime = time;
+ native_event.dwStreamID = 0;
+ native_event.dwEvent = MAKE_EVT(length + sizeof(byte), 0, 0, MEVT_LONGMSG);
+ WriteBuffer((byte *)&native_event, sizeof(native_event_t));
+ WriteBuffer(&event_type, sizeof(byte));
+ WriteBuffer(data, length);
+ WriteBufferPad();
+
+ if (is_sysex_reset)
{
- tracks[i].iter = MIDI_IterateTrack(file, i);
- tracks[i].absolute_time = 0;
+ // SysEx reset also resets volume. Take the default channel volumes
+ // and scale them by the user's volume slider.
+ ResetVolume();
+
+ // Disable instrument fallback and give priority to MIDI file. Fallback
+ // assumes GS (SC-55 level) and the MIDI file could be GM, GM2, XG, or
+ // GS (SC-88 or higher). Preserve the composer's intent.
+ MIDI_ResetFallback();
+ use_fallback = false;
+
+ // Use default device for EMIDI.
+ emidi_device = EMIDI_DEVICE_GENERAL_MIDI;
}
+}
- song.native_events = calloc(MIDI_NumEvents(file), sizeof(native_event_t));
+static void SendProgramMsg(int time, int channel, int program,
+ midi_fallback_t *fallback)
+{
+ switch ((int)fallback->type)
+ {
+ case FALLBACK_BANK_MSB:
+ SendShortMsg(time, MIDI_EVENT_CONTROLLER, channel,
+ MIDI_CONTROLLER_BANK_SELECT_MSB, fallback->value);
+ SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE, channel, program, 0);
+ break;
- while (1)
+ case FALLBACK_DRUMS:
+ SendShortMsg(time, MIDI_EVENT_PROGRAM_CHANGE, channel,
+ fallback->value, 0);
+ break;
+
+ default:
+ SendShortMsg(time, MIDI_EVENT_PROGRAM_CHANGE, channel, program, 0);
+ break;
+ }
+}
+
+static void SetLoopPoint(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < song.num_tracks; ++i)
{
- midi_event_t *event;
- DWORD data = 0;
- int min_time = INT_MAX;
- int idx = -1;
+ MIDI_SetLoopPoint(song.tracks[i].iter);
+ song.tracks[i].saved_end_of_track = song.tracks[i].end_of_track;
+ song.tracks[i].saved_elapsed_time = song.tracks[i].elapsed_time;
+ }
+ song.saved_elapsed_time = song.elapsed_time;
+}
- // Look for an event with a minimal delta time.
- for (i = 0; i < num_tracks; ++i)
- {
- int time = 0;
+static void CheckFFLoop(midi_event_t *event)
+{
+ if (event->data.meta.length == sizeof(ff_loopStart) &&
+ !memcmp(event->data.meta.data, ff_loopStart, sizeof(ff_loopStart)))
+ {
+ SetLoopPoint();
+ song.ff_loop = true;
+ }
+ else if (song.ff_loop && event->data.meta.length == sizeof(ff_loopEnd) &&
+ !memcmp(event->data.meta.data, ff_loopEnd, sizeof(ff_loopEnd)))
+ {
+ song.ff_restart = true;
+ }
+}
- if (tracks[i].iter == NULL)
+static boolean AddToBuffer(unsigned int delta_time, midi_event_t *event,
+ win_midi_track_t *track)
+{
+ unsigned int i;
+ unsigned int flag;
+ int count;
+ midi_fallback_t fallback = {FALLBACK_NONE, 0};
+
+ if (use_fallback)
+ {
+ MIDI_CheckFallback(event, &fallback);
+ }
+
+ switch ((int)event->event_type)
+ {
+ case MIDI_EVENT_SYSEX:
+ SendSysExMsg(delta_time, event->data.sysex.data,
+ event->data.sysex.length);
+ return false;
+
+ case MIDI_EVENT_META:
+ switch (event->data.meta.type)
{
- continue;
+ case MIDI_META_END_OF_TRACK:
+ track->end_of_track = true;
+ SendNOPMsg(delta_time);
+ break;
+
+ case MIDI_META_SET_TEMPO:
+ UpdateTempo(delta_time, event);
+ break;
+
+ case MIDI_META_MARKER:
+ CheckFFLoop(event);
+ SendNOPMsg(delta_time);
+ break;
+
+ default:
+ SendNOPMsg(delta_time);
+ break;
}
+ return true;
+ }
- time = tracks[i].absolute_time + MIDI_GetDeltaTime(tracks[i].iter);
+ if (track->emidi_designated && (emidi_device & ~track->emidi_device_flags))
+ {
+ // Send NOP if this device has been excluded from this track.
+ SendNOPMsg(delta_time);
+ return true;
+ }
- if (time < min_time)
+ switch ((int)event->event_type)
+ {
+ case MIDI_EVENT_CONTROLLER:
+ switch (event->data.channel.param1)
{
- min_time = time;
- idx = i;
- }
- }
+ case MIDI_CONTROLLER_VOLUME_MSB:
+ if (track->emidi_volume)
+ {
+ SendNOPMsg(delta_time);
+ }
+ else
+ {
+ SendVolumeMsg(delta_time, event->data.channel.channel,
+ event->data.channel.param2);
+ }
+ break;
- // No more MIDI events left, end the loop.
- if (idx == -1)
- {
- break;
- }
+ case MIDI_CONTROLLER_VOLUME_LSB:
+ SendNOPMsg(delta_time);
+ break;
+
+ case MIDI_CONTROLLER_BANK_SELECT_LSB:
+ if (fallback.type == FALLBACK_BANK_LSB)
+ {
+ SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
+ event->data.channel.channel,
+ MIDI_CONTROLLER_BANK_SELECT_LSB,
+ fallback.value);
+ }
+ else
+ {
+ SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
+ event->data.channel.channel,
+ MIDI_CONTROLLER_BANK_SELECT_LSB,
+ event->data.channel.param2);
+ }
+ break;
+
+ case EMIDI_CONTROLLER_TRACK_DESIGNATION:
+ if (track->elapsed_time < timediv)
+ {
+ flag = event->data.channel.param2;
+
+ if (flag == EMIDI_DEVICE_ALL)
+ {
+ track->emidi_device_flags = UINT_MAX;
+ track->emidi_designated = true;
+ }
+ else if (flag <= EMIDI_DEVICE_ULTRASOUND)
+ {
+ track->emidi_device_flags |= 1 << flag;
+ track->emidi_designated = true;
+ }
+ }
+ SendNOPMsg(delta_time);
+ break;
- tracks[idx].absolute_time = min_time;
+ case EMIDI_CONTROLLER_TRACK_EXCLUSION:
+ if (song.rpg_loop)
+ {
+ SetLoopPoint();
+ }
+ else if (track->elapsed_time < timediv)
+ {
+ flag = event->data.channel.param2;
- if (!MIDI_GetNextEvent(tracks[idx].iter, &event))
- {
- MIDI_FreeIterator(tracks[idx].iter);
- tracks[idx].iter = NULL;
- continue;
- }
+ if (!track->emidi_designated)
+ {
+ track->emidi_device_flags = UINT_MAX;
+ track->emidi_designated = true;
+ }
- switch ((int)event->event_type)
- {
- case MIDI_EVENT_META:
- if (event->data.meta.type == MIDI_META_SET_TEMPO)
- {
- data = event->data.meta.data[2] |
- (event->data.meta.data[1] << 8) |
- (event->data.meta.data[0] << 16) |
- (MEVT_TEMPO << 24);
- }
- break;
+ if (flag <= EMIDI_DEVICE_ULTRASOUND)
+ {
+ track->emidi_device_flags &= ~(1 << flag);
+ }
+ }
+ SendNOPMsg(delta_time);
+ break;
- case MIDI_EVENT_NOTE_OFF:
- case MIDI_EVENT_NOTE_ON:
- case MIDI_EVENT_AFTERTOUCH:
- case MIDI_EVENT_CONTROLLER:
- case MIDI_EVENT_PITCH_BEND:
- data = event->event_type |
- event->data.channel.channel |
- (event->data.channel.param1 << 8) |
- (event->data.channel.param2 << 16) |
- (MEVT_SHORTMSG << 24);
- break;
+ case EMIDI_CONTROLLER_PROGRAM_CHANGE:
+ if (track->emidi_program || track->elapsed_time < timediv)
+ {
+ track->emidi_program = true;
+ SendProgramMsg(delta_time, event->data.channel.channel,
+ event->data.channel.param2, &fallback);
+ }
+ else
+ {
+ SendNOPMsg(delta_time);
+ }
+ break;
- case MIDI_EVENT_PROGRAM_CHANGE:
- case MIDI_EVENT_CHAN_AFTERTOUCH:
- data = event->event_type |
- event->data.channel.channel |
- (event->data.channel.param1 << 8) |
- (0 << 16) |
- (MEVT_SHORTMSG << 24);
- break;
- }
+ case EMIDI_CONTROLLER_VOLUME:
+ if (track->emidi_volume || track->elapsed_time < timediv)
+ {
+ track->emidi_volume = true;
+ SendVolumeMsg(delta_time, event->data.channel.channel,
+ event->data.channel.param2);
+ }
+ else
+ {
+ SendNOPMsg(delta_time);
+ }
+ break;
+
+ case EMIDI_CONTROLLER_LOOP_BEGIN:
+ count = event->data.channel.param2;
+ count = (count == 0) ? (-1) : count;
+ track->emidi_loop_count = count;
+ MIDI_SetLoopPoint(track->iter);
+ SendNOPMsg(delta_time);
+ break;
+
+ case EMIDI_CONTROLLER_LOOP_END:
+ if (event->data.channel.param2 == EMIDI_LOOP_FLAG)
+ {
+ if (track->emidi_loop_count != 0)
+ {
+ MIDI_RestartAtLoopPoint(track->iter);
+ }
- if (data)
- {
- native_event_t *native_event = &song.native_events[song.num_events];
+ if (track->emidi_loop_count > 0)
+ {
+ track->emidi_loop_count--;
+ }
+ }
+ SendNOPMsg(delta_time);
+ break;
- native_event->dwDeltaTime = min_time - current_time;
- native_event->dwStreamID = 0;
- native_event->dwEvent = data;
+ case EMIDI_CONTROLLER_GLOBAL_LOOP_BEGIN:
+ count = event->data.channel.param2;
+ count = (count == 0) ? (-1) : count;
+ for (i = 0; i < song.num_tracks; ++i)
+ {
+ song.tracks[i].emidi_loop_count = count;
+ MIDI_SetLoopPoint(song.tracks[i].iter);
+ }
+ SendNOPMsg(delta_time);
+ break;
- song.num_events++;
- current_time = min_time;
- }
- }
+ case EMIDI_CONTROLLER_GLOBAL_LOOP_END:
+ if (event->data.channel.param2 == EMIDI_LOOP_FLAG)
+ {
+ for (i = 0; i < song.num_tracks; ++i)
+ {
+ if (song.tracks[i].emidi_loop_count != 0)
+ {
+ MIDI_RestartAtLoopPoint(song.tracks[i].iter);
+ }
+
+ if (song.tracks[i].emidi_loop_count > 0)
+ {
+ song.tracks[i].emidi_loop_count--;
+ }
+ }
+ }
+ SendNOPMsg(delta_time);
+ break;
- if (tracks)
+ default:
+ SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
+ event->data.channel.channel,
+ event->data.channel.param1,
+ event->data.channel.param2);
+ break;
+ }
+ break;
+
+ case MIDI_EVENT_NOTE_OFF:
+ case MIDI_EVENT_NOTE_ON:
+ case MIDI_EVENT_AFTERTOUCH:
+ case MIDI_EVENT_PITCH_BEND:
+ SendShortMsg(delta_time, event->event_type,
+ event->data.channel.channel,
+ event->data.channel.param1,
+ event->data.channel.param2);
+ break;
+
+ case MIDI_EVENT_PROGRAM_CHANGE:
+ if (track->emidi_program)
+ {
+ SendNOPMsg(delta_time);
+ }
+ else
+ {
+ SendProgramMsg(delta_time, event->data.channel.channel,
+ event->data.channel.param1, &fallback);
+ }
+ break;
+
+ case MIDI_EVENT_CHAN_AFTERTOUCH:
+ SendShortMsg(delta_time, MIDI_EVENT_CHAN_AFTERTOUCH,
+ event->data.channel.channel,
+ event->data.channel.param1, 0);
+ break;
+
+ default:
+ SendNOPMsg(delta_time);
+ break;
+ }
+
+ return true;
+}
+
+static void RestartLoop(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < song.num_tracks; ++i)
{
- free(tracks);
+ MIDI_RestartAtLoopPoint(song.tracks[i].iter);
+ song.tracks[i].end_of_track = song.tracks[i].saved_end_of_track;
+ song.tracks[i].elapsed_time = song.tracks[i].saved_elapsed_time;
}
+ song.elapsed_time = song.saved_elapsed_time;
}
-static void UpdateVolume(void)
+static void RestartTracks(void)
{
- int i;
+ unsigned int i;
- // Send MIDI controller events to adjust the volume.
- for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+ for (i = 0; i < song.num_tracks; ++i)
{
- DWORD msg = 0;
+ MIDI_RestartIterator(song.tracks[i].iter);
+ song.tracks[i].elapsed_time = 0;
+ song.tracks[i].end_of_track = false;
+ song.tracks[i].emidi_device_flags = 0;
+ song.tracks[i].emidi_designated = false;
+ song.tracks[i].emidi_program = false;
+ song.tracks[i].emidi_volume = false;
+ song.tracks[i].emidi_loop_count = 0;
+ }
+ song.elapsed_time = 0;
+}
- int value = channel_volume[i] * volume_factor;
+static boolean IsRPGLoop(void)
+{
+ unsigned int i;
+ unsigned int num_rpg_events = 0;
+ unsigned int num_emidi_events = 0;
+ midi_event_t *event = NULL;
- msg = MIDI_EVENT_CONTROLLER | i | (MIDI_CONTROLLER_MAIN_VOLUME << 8) |
- (value << 16);
+ for (i = 0; i < song.num_tracks; ++i)
+ {
+ while (MIDI_GetNextEvent(song.tracks[i].iter, &event))
+ {
+ if (event->event_type == MIDI_EVENT_CONTROLLER)
+ {
+ switch (event->data.channel.param1)
+ {
+ case EMIDI_CONTROLLER_TRACK_EXCLUSION:
+ num_rpg_events++;
+ break;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ case EMIDI_CONTROLLER_TRACK_DESIGNATION:
+ case EMIDI_CONTROLLER_PROGRAM_CHANGE:
+ case EMIDI_CONTROLLER_VOLUME:
+ case EMIDI_CONTROLLER_LOOP_BEGIN:
+ case EMIDI_CONTROLLER_LOOP_END:
+ case EMIDI_CONTROLLER_GLOBAL_LOOP_BEGIN:
+ case EMIDI_CONTROLLER_GLOBAL_LOOP_END:
+ num_emidi_events++;
+ break;
+ }
+ }
+ }
+
+ MIDI_RestartIterator(song.tracks[i].iter);
}
+
+ return (num_rpg_events == 1 && num_emidi_events == 0);
}
-void ResetDevice(void)
+static void FillBuffer(void)
{
- for (int i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+ unsigned int i;
+ int num_events;
+
+ buffer.position = 0;
+
+ if (initial_playback)
{
- DWORD msg = 0;
+ ResetDevice();
+ StreamOut();
+ song.rpg_loop = IsRPGLoop();
+ initial_playback = false;
+ return;
+ }
- // RPN sequence to adjust pitch bend range (RPN value 0x0000)
- msg = MIDI_EVENT_CONTROLLER | i | 0x65 << 8 | 0x00 << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x64 << 8 | 0x00 << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ if (update_volume)
+ {
+ update_volume = false;
+ UpdateVolume();
+ StreamOut();
+ return;
+ }
- // reset pitch bend range to central tuning +/- 2 semitones and 0 cents
- msg = MIDI_EVENT_CONTROLLER | i | 0x06 << 8 | 0x02 << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x26 << 8 | 0x00 << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ for (num_events = 0; num_events < STREAM_MAX_EVENTS; )
+ {
+ midi_event_t *event = NULL;
+ win_midi_track_t *track = NULL;
+ unsigned int min_time = UINT_MAX;
+ unsigned int delta_time;
- // end of RPN sequence
- msg = MIDI_EVENT_CONTROLLER | i | 0x64 << 8 | 0x7F << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x65 << 8 | 0x7F << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ // Find next event across all tracks.
+ for (i = 0; i < song.num_tracks; ++i)
+ {
+ if (!song.tracks[i].end_of_track)
+ {
+ unsigned int time = song.tracks[i].elapsed_time +
+ MIDI_GetDeltaTime(song.tracks[i].iter);
+ if (time < min_time)
+ {
+ min_time = time;
+ track = &song.tracks[i];
+ }
+ }
+ }
- // reset all controllers
- msg = MIDI_EVENT_CONTROLLER | i | 0x79 << 8 | 0x00 << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ // No more events. Restart or stop song.
+ if (track == NULL)
+ {
+ if (song.elapsed_time)
+ {
+ if (song.ff_restart || song.rpg_loop)
+ {
+ song.ff_restart = false;
+ RestartLoop();
+ continue;
+ }
+ else if (song.looping)
+ {
+ RestartTracks();
+ continue;
+ }
+ }
+ break;
+ }
- // reset pan to 64 (center)
- msg = MIDI_EVENT_CONTROLLER | i | 0x0A << 8 | 0x40 << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ track->elapsed_time = min_time;
+ delta_time = min_time - song.elapsed_time;
+ song.elapsed_time = min_time;
- // reset reverb and other effect controllers
- msg = MIDI_EVENT_CONTROLLER | i | 0x5B << 8 | winmm_reverb_level << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x5C << 8 | 0x00 << 16; // tremolo
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x5D << 8 | winmm_chorus_level << 16;
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x5E << 8 | 0x00 << 16; // detune
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
- msg = MIDI_EVENT_CONTROLLER | i | 0x5F << 8 | 0x00 << 16; // phaser
- midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+ if (!MIDI_GetNextEvent(track->iter, &event))
+ {
+ track->end_of_track = true;
+ continue;
+ }
+
+ // Restart FF loop after sending all events that share same timediv.
+ if (song.ff_restart && MIDI_GetDeltaTime(track->iter) > 0)
+ {
+ song.ff_restart = false;
+ RestartLoop();
+ continue;
+ }
+
+ if (!AddToBuffer(delta_time, event, track))
+ {
+ StreamOut();
+ return;
+ }
+
+ num_events++;
}
+
+ if (num_events)
+ {
+ StreamOut();
+ }
}
-boolean I_WIN_InitMusic(void)
+// The Windows API documentation states: "Applications should not call any
+// multimedia functions from inside the callback function, as doing so can
+// cause a deadlock." We use thread to avoid possible deadlocks.
+
+static DWORD WINAPI PlayerProc(void)
{
- UINT MidiDevice;
+ HANDLE events[2] = { hBufferReturnEvent, hExitEvent };
+
+ while (1)
+ {
+ switch (WaitForMultipleObjects(2, events, FALSE, INFINITE))
+ {
+ case WAIT_OBJECT_0:
+ FillBuffer();
+ break;
+
+ case WAIT_OBJECT_0 + 1:
+ return 0;
+ }
+ }
+ return 0;
+}
+
+static boolean I_WIN_InitMusic(void)
+{
int all_devices;
int i;
- MIDIHDR *hdr = &buffer.MidiStreamHdr;
MIDIOUTCAPS mcaps;
MMRESULT mmr;
@@ -441,73 +1185,72 @@
}
}
+ // Is this device MS GS Synth?
+ if (mcaps.wMid == MM_MICROSOFT &&
+ mcaps.wPid == MM_MSFT_GENERIC_MIDISYNTH &&
+ mcaps.wTechnology == MOD_SWSYNTH)
+ {
+ ms_gs_synth = MidiDevice;
+ }
+
mmr = midiStreamOpen(&hMidiStream, &MidiDevice, (DWORD)1,
(DWORD_PTR)MidiStreamProc, (DWORD_PTR)NULL,
CALLBACK_FUNCTION);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamOpen", mmr);
return false;
}
- hdr->lpData = (LPSTR)buffer.events;
- hdr->dwBytesRecorded = 0;
- hdr->dwBufferLength = STREAM_MAX_EVENTS * sizeof(native_event_t);
- hdr->dwFlags = 0;
- hdr->dwOffset = 0;
+ AllocateBuffer(BUFFER_INITIAL_SIZE);
- mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
- if (mmr != MMSYSERR_NOERROR)
- {
- MidiErrorMessageBox(mmr);
- return false;
- }
-
hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
- winmm_reverb_level = BETWEEN(REVERB_MIN, REVERB_MAX, winmm_reverb_level);
- winmm_chorus_level = BETWEEN(CHORUS_MIN, CHORUS_MAX, winmm_chorus_level);
- ResetDevice();
+ MIDI_InitFallback();
return true;
}
-void I_WIN_SetMusicVolume(int volume)
+static void I_WIN_SetMusicVolume(int volume)
{
- volume_factor = sqrt((float)volume / 120);
+ static int last_volume = -1;
- UpdateVolume();
+ if (last_volume == volume)
+ {
+ // Ignore holding key down in volume menu.
+ return;
+ }
+
+ last_volume = volume;
+
+ volume_factor = sqrtf((float)volume / 120);
+
+ update_volume = true;
}
-void I_WIN_StopSong(void)
+static void I_WIN_StopSong(void)
{
MMRESULT mmr;
- if (hPlayerThread)
+ if (!hPlayerThread)
{
- SetEvent(hExitEvent);
- WaitForSingleObject(hPlayerThread, INFINITE);
-
- CloseHandle(hPlayerThread);
- hPlayerThread = NULL;
+ return;
}
- ResetDevice();
+ SetEvent(hExitEvent);
+ WaitForSingleObject(hPlayerThread, INFINITE);
+ CloseHandle(hPlayerThread);
+ hPlayerThread = NULL;
mmr = midiStreamStop(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamStop", mmr);
}
- mmr = midiOutReset((HMIDIOUT)hMidiStream);
- if (mmr != MMSYSERR_NOERROR)
- {
- MidiErrorMessageBox(mmr);
- }
}
-void I_WIN_PlaySong(boolean looping)
+static void I_WIN_PlaySong(void *handle, boolean looping)
{
MMRESULT mmr;
@@ -517,16 +1260,18 @@
0, 0, 0);
SetThreadPriority(hPlayerThread, THREAD_PRIORITY_TIME_CRITICAL);
+ initial_playback = true;
+
+ SetEvent(hBufferReturnEvent);
+
mmr = midiStreamRestart(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamRestart", mmr);
}
-
- UpdateVolume();
}
-void I_WIN_PauseSong(void)
+static void I_WIN_PauseSong(void)
{
MMRESULT mmr;
@@ -533,11 +1278,11 @@
mmr = midiStreamPause(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamPause", mmr);
}
}
-void I_WIN_ResumeSong(void)
+static void I_WIN_ResumeSong(void)
{
MMRESULT mmr;
@@ -544,101 +1289,224 @@
mmr = midiStreamRestart(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamRestart", mmr);
}
}
-boolean I_WIN_RegisterSong(char *filename)
+// Determine whether memory block is a .mid file
+
+static boolean IsMid(byte *mem, int len)
{
- int i;
+ return len > 4 && !memcmp(mem, "MThd", 4);
+}
+
+static boolean ConvertMus(byte *musdata, int len, const char *filename)
+{
+ MEMFILE *instream;
+ MEMFILE *outstream;
+ void *outbuf;
+ size_t outbuf_len;
+ int result;
+
+ instream = mem_fopen_read(musdata, len);
+ outstream = mem_fopen_write();
+
+ result = mus2mid(instream, outstream);
+
+ if (result == 0)
+ {
+ mem_get_buf(outstream, &outbuf, &outbuf_len);
+
+ M_WriteFile(filename, outbuf, outbuf_len);
+ }
+
+ mem_fclose(instream);
+ mem_fclose(outstream);
+
+ return result;
+}
+
+static void *I_WIN_RegisterSong(void *data, int len)
+{
+ unsigned int i;
+ char *filename;
midi_file_t *file;
- MIDIPROPTIMEDIV timediv;
- MIDIPROPTEMPO tempo;
+
+ MIDIPROPTIMEDIV prop_timediv;
+ MIDIPROPTEMPO prop_tempo;
MMRESULT mmr;
+ // MUS files begin with "MUS"
+ // Reject anything which doesnt have this signature
+
+ filename = M_TempFile("doom.mid");
+
+ if (IsMid(data, len))
+ {
+ M_WriteFile(filename, data, len);
+ }
+ else
+ {
+ // Assume a MUS file and try to convert
+
+ ConvertMus(data, len, filename);
+ }
+
file = MIDI_LoadFile(filename);
+ M_remove(filename);
+ free(filename);
+
if (file == NULL)
{
fprintf(stderr, "I_WIN_RegisterSong: Failed to load MID.\n");
- return false;
+ return NULL;
}
- // Initialize channels volume.
- for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
- {
- channel_volume[i] = 100;
- }
-
- timediv.cbStruct = sizeof(MIDIPROPTIMEDIV);
- timediv.dwTimeDiv = MIDI_GetFileTimeDivision(file);
- mmr = midiStreamProperty(hMidiStream, (LPBYTE)&timediv,
+ prop_timediv.cbStruct = sizeof(MIDIPROPTIMEDIV);
+ prop_timediv.dwTimeDiv = MIDI_GetFileTimeDivision(file);
+ mmr = midiStreamProperty(hMidiStream, (LPBYTE)&prop_timediv,
MIDIPROP_SET | MIDIPROP_TIMEDIV);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
- return false;
+ MidiError("midiStreamProperty", mmr);
+ return NULL;
}
+ timediv = prop_timediv.dwTimeDiv;
// Set initial tempo.
- tempo.cbStruct = sizeof(MIDIPROPTIMEDIV);
- tempo.dwTempo = 500000; // 120 bmp
- mmr = midiStreamProperty(hMidiStream, (LPBYTE)&tempo,
+ prop_tempo.cbStruct = sizeof(MIDIPROPTIMEDIV);
+ prop_tempo.dwTempo = 500000; // 120 BPM
+ mmr = midiStreamProperty(hMidiStream, (LPBYTE)&prop_tempo,
MIDIPROP_SET | MIDIPROP_TEMPO);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
- return false;
+ MidiError("midiStreamProperty", mmr);
+ return NULL;
}
+ tempo = prop_tempo.dwTempo;
- MIDItoStream(file);
+ song.num_tracks = MIDI_NumTracks(file);
+ song.tracks = calloc(song.num_tracks, sizeof(win_midi_track_t));
+ for (i = 0; i < song.num_tracks; ++i)
+ {
+ song.tracks[i].iter = MIDI_IterateTrack(file, i);
+ }
- MIDI_FreeFile(file);
-
ResetEvent(hBufferReturnEvent);
ResetEvent(hExitEvent);
- FillBuffer();
- StreamOut();
-
- return true;
+ return file;
}
-void I_WIN_UnRegisterSong(void)
+static void I_WIN_UnRegisterSong(void *handle)
{
- if (song.native_events)
+ if (song.tracks)
{
- free(song.native_events);
- song.native_events = NULL;
+ int i;
+ for (i = 0; i < song.num_tracks; ++i)
+ {
+ MIDI_FreeIterator(song.tracks[i].iter);
+ song.tracks[i].iter = NULL;
+ }
+ free(song.tracks);
+ song.tracks = NULL;
}
- song.num_events = 0;
- song.position = 0;
+ if (handle)
+ {
+ MIDI_FreeFile(handle);
+ }
+ song.elapsed_time = 0;
+ song.saved_elapsed_time = 0;
+ song.num_tracks = 0;
+ song.looping = false;
+ song.ff_loop = false;
+ song.ff_restart = false;
+ song.rpg_loop = false;
}
-void I_WIN_ShutdownMusic(void)
+static void I_WIN_ShutdownMusic(void)
{
- MIDIHDR *hdr = &buffer.MidiStreamHdr;
MMRESULT mmr;
+ if (!hMidiStream)
+ {
+ return;
+ }
+
I_WIN_StopSong();
- I_WIN_UnRegisterSong();
+ I_WIN_UnRegisterSong(NULL);
- mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+ // Reset device at shutdown.
+ buffer.position = 0;
+ ResetDevice();
+ StreamOut();
+ mmr = midiStreamRestart(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamRestart", mmr);
}
+ WaitForSingleObject(hBufferReturnEvent, INFINITE);
+ mmr = midiStreamStop(hMidiStream);
+ if (mmr != MMSYSERR_NOERROR)
+ {
+ MidiError("midiStreamStop", mmr);
+ }
+ if (buffer.data)
+ {
+ mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, &MidiStreamHdr,
+ sizeof(MIDIHDR));
+ if (mmr != MMSYSERR_NOERROR)
+ {
+ MidiError("midiOutUnprepareHeader", mmr);
+ }
+ free(buffer.data);
+ buffer.data = NULL;
+ buffer.size = 0;
+ buffer.position = 0;
+ }
+
mmr = midiStreamClose(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
- MidiErrorMessageBox(mmr);
+ MidiError("midiStreamClose", mmr);
}
-
hMidiStream = NULL;
CloseHandle(hBufferReturnEvent);
CloseHandle(hExitEvent);
}
+
+static boolean I_WIN_MusicIsPlaying(void)
+{
+ return (song.num_tracks > 0);
+}
+
+static snddevice_t music_win_devices[] =
+{
+ SNDDEVICE_PAS,
+ SNDDEVICE_WAVEBLASTER,
+ SNDDEVICE_SOUNDCANVAS,
+ SNDDEVICE_GENMIDI,
+ SNDDEVICE_AWE32,
+};
+
+music_module_t music_win_module =
+{
+ music_win_devices,
+ arrlen(music_win_devices),
+ I_WIN_InitMusic,
+ I_WIN_ShutdownMusic,
+ I_WIN_SetMusicVolume,
+ I_WIN_PauseSong,
+ I_WIN_ResumeSong,
+ I_WIN_RegisterSong,
+ I_WIN_UnRegisterSong,
+ I_WIN_PlaySong,
+ I_WIN_StopSong,
+ I_WIN_MusicIsPlaying,
+ NULL, // Poll
+};
#endif
--- a/src/i_winmusic.h
+++ /dev/null
@@ -1,37 +1,0 @@
-//
-// Copyright(C) 2021 Roman Fomin
-//
-// 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.
-//
-// DESCRIPTION:
-// Windows native MIDI
-
-#ifndef __I_WINMUSIC__
-#define __I_WINMUSIC__
-
-#ifdef _WIN32
-
-#include "doomtype.h"
-
-boolean I_WIN_InitMusic(void);
-void I_WIN_PlaySong(boolean looping);
-void I_WIN_PauseSong(void);
-void I_WIN_ResumeSong(void);
-void I_WIN_StopSong(void);
-void I_WIN_SetMusicVolume(int volume);
-boolean I_WIN_RegisterSong(char* filename);
-void I_WIN_UnRegisterSong(void);
-void I_WIN_ShutdownMusic(void);
-
-
-#endif // _WIN32
-
-#endif // __I_WINMUSIC__
--- a/src/m_config.c
+++ b/src/m_config.c
@@ -973,13 +973,26 @@
CONFIG_VARIABLE_STRING(winmm_midi_device),
//!
- // Reverb level for native Windows MIDI, default 40, range 0-127.
+ // Reset device type for native Windows MIDI, default 1. Valid values are
+ // 0 (None), 1 (GS Mode), 2 (GM Mode), 3 (GM2 Mode), 4 (XG Mode).
//
+ CONFIG_VARIABLE_INT(winmm_reset_type),
+
+ //!
+ // Reset device delay for native Windows MIDI, default 0, median value 100 ms.
+ //
+
+ CONFIG_VARIABLE_INT(winmm_reset_delay),
+
+ //!
+ // Reverb level for native Windows MIDI, default -1, range 0-127.
+ //
+
CONFIG_VARIABLE_INT(winmm_reverb_level),
//!
- // Chorus level for native Windows MIDI, default 0, range 0-127.
+ // Chorus level for native Windows MIDI, default -1, range 0-127.
//
CONFIG_VARIABLE_INT(winmm_chorus_level),
--- /dev/null
+++ b/src/midifallback.c
@@ -1,0 +1,366 @@
+//
+// Copyright(C) 2022 ceski
+//
+// 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.
+//
+// DESCRIPTION:
+// MIDI instrument fallback support
+//
+
+#ifdef _WIN32
+
+#include "doomtype.h"
+#include "midifile.h"
+#include "midifallback.h"
+
+static const byte drums_table[128] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x18, 0x19, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28,
+ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30,
+ 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F
+};
+
+static byte variation[128][128];
+static byte bank_msb[MIDI_CHANNELS_PER_TRACK];
+static byte drum_map[MIDI_CHANNELS_PER_TRACK];
+static boolean selected[MIDI_CHANNELS_PER_TRACK];
+
+static void UpdateDrumMap(const byte *msg, unsigned int length)
+{
+ byte idx;
+ byte checksum;
+
+ // GS allows drums on any channel using SysEx messages.
+ // The message format is F0 followed by:
+ //
+ // 41 10 42 12 40 <ch> 15 <map> <sum> F7
+ //
+ // <ch> is [11-19, 10, 1A-1F] for channels 1-16. Note the position of 10.
+ // <map> is 00-02 for off (normal part), drum map 1, or drum map 2.
+ // <sum> is checksum.
+
+ if (length == 10 &&
+ msg[0] == 0x41 && // Roland
+ msg[1] == 0x10 && // Device ID
+ msg[2] == 0x42 && // GS
+ msg[3] == 0x12 && // DT1
+ msg[4] == 0x40 && // Address MSB
+ msg[6] == 0x15 && // Address LSB
+ msg[9] == 0xF7) // SysEx EOX
+ {
+ checksum = 128 - ((int)msg[4] + msg[5] + msg[6] + msg[7]) % 128;
+
+ if (msg[8] != checksum)
+ {
+ return;
+ }
+
+ if (msg[5] == 0x10) // Channel 10
+ {
+ idx = 9;
+ }
+ else if (msg[5] < 0x1A) // Channels 1-9
+ {
+ idx = (msg[5] & 0x0F) - 1;
+ }
+ else // Channels 11-16
+ {
+ idx = msg[5] & 0x0F;
+ }
+
+ drum_map[idx] = msg[7];
+ }
+}
+
+static boolean GetProgramFallback(byte idx, byte program,
+ midi_fallback_t *fallback)
+{
+ if (drum_map[idx] == 0) // Normal channel
+ {
+ if (bank_msb[idx] == 0 || variation[bank_msb[idx]][program])
+ {
+ // Found a capital or variation for this bank select MSB.
+ selected[idx] = true;
+ return false;
+ }
+
+ fallback->type = FALLBACK_BANK_MSB;
+
+ if (!selected[idx] || bank_msb[idx] > 63)
+ {
+ // Fall to capital when no instrument has (successfully)
+ // selected this variation or if the variation is above 63.
+ fallback->value = 0;
+ return true;
+ }
+
+ // A previous instrument used this variation but it's not
+ // valid for the current instrument. Fall to the next valid
+ // "sub-capital" (next variation that is a multiple of 8).
+ fallback->value = (bank_msb[idx] / 8) * 8;
+ while (fallback->value > 0)
+ {
+ if (variation[fallback->value][program])
+ {
+ break;
+ }
+ fallback->value -= 8;
+ }
+ return true;
+ }
+ else // Drums channel
+ {
+ if (program != drums_table[program])
+ {
+ // Use drum set from drums fallback table.
+ // Drums 0-63 and 127: same as original SC-55 (1.00 - 1.21).
+ // Drums 64-126: standard drum set (0).
+ fallback->type = FALLBACK_DRUMS;
+ fallback->value = drums_table[program];
+ selected[idx] = true;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void MIDI_CheckFallback(const midi_event_t *event, midi_fallback_t *fallback)
+{
+ byte idx;
+ byte program;
+
+ switch ((int)event->event_type)
+ {
+ case MIDI_EVENT_SYSEX:
+ UpdateDrumMap(event->data.sysex.data, event->data.sysex.length);
+ break;
+
+ case MIDI_EVENT_CONTROLLER:
+ idx = event->data.channel.channel;
+ switch (event->data.channel.param1)
+ {
+ case MIDI_CONTROLLER_BANK_SELECT_MSB:
+ bank_msb[idx] = event->data.channel.param2;
+ selected[idx] = false;
+ break;
+
+ case MIDI_CONTROLLER_BANK_SELECT_LSB:
+ selected[idx] = false;
+ if (event->data.channel.param2 > 0)
+ {
+ // Bank select LSB > 0 not supported. This also
+ // preserves user's current SC-XX map.
+ fallback->type = FALLBACK_BANK_LSB;
+ fallback->value = 0;
+ return;
+ }
+ break;
+
+ case EMIDI_CONTROLLER_PROGRAM_CHANGE:
+ program = event->data.channel.param2;
+ if (GetProgramFallback(idx, program, fallback))
+ {
+ return;
+ }
+ break;
+ }
+ break;
+
+ case MIDI_EVENT_PROGRAM_CHANGE:
+ idx = event->data.channel.channel;
+ program = event->data.channel.param1;
+ if (GetProgramFallback(idx, program, fallback))
+ {
+ return;
+ }
+ break;
+ }
+
+ fallback->type = FALLBACK_NONE;
+ fallback->value = 0;
+}
+
+void MIDI_ResetFallback(void)
+{
+ int i;
+
+ for (i = 0; i < MIDI_CHANNELS_PER_TRACK; i++)
+ {
+ bank_msb[i] = 0;
+ drum_map[i] = 0;
+ selected[i] = false;
+ }
+
+ // Channel 10 (index 9) is set to drum map 1 by default.
+ drum_map[9] = 1;
+}
+
+void MIDI_InitFallback(void)
+{
+ byte program;
+
+ MIDI_ResetFallback();
+
+ // Capital
+ for (program = 0; program < 128; program++)
+ {
+ variation[0][program] = 1;
+ }
+
+ // Variation #1
+ variation[1][38] = 1;
+ variation[1][57] = 1;
+ variation[1][60] = 1;
+ variation[1][80] = 1;
+ variation[1][81] = 1;
+ variation[1][98] = 1;
+ variation[1][102] = 1;
+ variation[1][104] = 1;
+ variation[1][120] = 1;
+ variation[1][121] = 1;
+ variation[1][122] = 1;
+ variation[1][123] = 1;
+ variation[1][124] = 1;
+ variation[1][125] = 1;
+ variation[1][126] = 1;
+ variation[1][127] = 1;
+
+ // Variation #2
+ variation[2][102] = 1;
+ variation[2][120] = 1;
+ variation[2][122] = 1;
+ variation[2][123] = 1;
+ variation[2][124] = 1;
+ variation[2][125] = 1;
+ variation[2][126] = 1;
+ variation[2][127] = 1;
+
+ // Variation #3
+ variation[3][122] = 1;
+ variation[3][123] = 1;
+ variation[3][124] = 1;
+ variation[3][125] = 1;
+ variation[3][126] = 1;
+ variation[3][127] = 1;
+
+ // Variation #4
+ variation[4][122] = 1;
+ variation[4][124] = 1;
+ variation[4][125] = 1;
+ variation[4][126] = 1;
+
+ // Variation #5
+ variation[5][122] = 1;
+ variation[5][124] = 1;
+ variation[5][125] = 1;
+ variation[5][126] = 1;
+
+ // Variation #6
+ variation[6][125] = 1;
+
+ // Variation #7
+ variation[7][125] = 1;
+
+ // Variation #8
+ variation[8][0] = 1;
+ variation[8][1] = 1;
+ variation[8][2] = 1;
+ variation[8][3] = 1;
+ variation[8][4] = 1;
+ variation[8][5] = 1;
+ variation[8][6] = 1;
+ variation[8][11] = 1;
+ variation[8][12] = 1;
+ variation[8][14] = 1;
+ variation[8][16] = 1;
+ variation[8][17] = 1;
+ variation[8][19] = 1;
+ variation[8][21] = 1;
+ variation[8][24] = 1;
+ variation[8][25] = 1;
+ variation[8][26] = 1;
+ variation[8][27] = 1;
+ variation[8][28] = 1;
+ variation[8][30] = 1;
+ variation[8][31] = 1;
+ variation[8][38] = 1;
+ variation[8][39] = 1;
+ variation[8][40] = 1;
+ variation[8][48] = 1;
+ variation[8][50] = 1;
+ variation[8][61] = 1;
+ variation[8][62] = 1;
+ variation[8][63] = 1;
+ variation[8][80] = 1;
+ variation[8][81] = 1;
+ variation[8][107] = 1;
+ variation[8][115] = 1;
+ variation[8][116] = 1;
+ variation[8][117] = 1;
+ variation[8][118] = 1;
+ variation[8][125] = 1;
+
+ // Variation #9
+ variation[9][14] = 1;
+ variation[9][118] = 1;
+ variation[9][125] = 1;
+
+ // Variation #16
+ variation[16][0] = 1;
+ variation[16][4] = 1;
+ variation[16][5] = 1;
+ variation[16][6] = 1;
+ variation[16][16] = 1;
+ variation[16][19] = 1;
+ variation[16][24] = 1;
+ variation[16][25] = 1;
+ variation[16][28] = 1;
+ variation[16][39] = 1;
+ variation[16][62] = 1;
+ variation[16][63] = 1;
+
+ // Variation #24
+ variation[24][4] = 1;
+ variation[24][6] = 1;
+
+ // Variation #32
+ variation[32][16] = 1;
+ variation[32][17] = 1;
+ variation[32][24] = 1;
+ variation[32][52] = 1;
+
+ // CM-64 Map (PCM)
+ for (program = 0; program < 64; program++)
+ {
+ variation[126][program] = 1;
+ }
+
+ // CM-64 Map (LA)
+ for (program = 0; program < 128; program++)
+ {
+ variation[127][program] = 1;
+ }
+}
+
+#endif
--- /dev/null
+++ b/src/midifallback.h
@@ -1,0 +1,46 @@
+//
+// Copyright(C) 2022 ceski
+//
+// 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.
+//
+// DESCRIPTION:
+// MIDI instrument fallback support
+//
+
+#ifdef _WIN32
+
+#ifndef MIDIFALLBACK_H
+#define MIDIFALLBACK_H
+
+#include "doomtype.h"
+#include "midifile.h"
+
+typedef enum midi_fallback_type_t
+{
+ FALLBACK_NONE,
+ FALLBACK_BANK_MSB,
+ FALLBACK_BANK_LSB,
+ FALLBACK_DRUMS,
+} midi_fallback_type_t;
+
+typedef struct midi_fallback_t
+{
+ midi_fallback_type_t type;
+ byte value;
+} midi_fallback_t;
+
+void MIDI_CheckFallback(const midi_event_t *event, midi_fallback_t *fallback);
+void MIDI_ResetFallback(void);
+void MIDI_InitFallback(void);
+
+#endif // MIDIFALLBACK_H
+
+#endif // _WIN32
--- a/src/midifile.c
+++ b/src/midifile.c
@@ -70,6 +70,7 @@
{
midi_track_t *track;
unsigned int position;
+ unsigned int loop_point;
};
struct midi_file_s
@@ -638,21 +639,6 @@
return file->num_tracks;
}
-// Get the number of events in a MIDI file.
-
-unsigned int MIDI_NumEvents(midi_file_t *file)
-{
- int i;
- unsigned int num_events = 0;
-
- for (i = 0; i < file->num_tracks; ++i)
- {
- num_events += file->tracks[i].num_events;
- }
-
- return num_events;
-}
-
// Start iterating over the events in a track.
midi_track_iter_t *MIDI_IterateTrack(midi_file_t *file, unsigned int track)
@@ -664,6 +650,7 @@
iter = malloc(sizeof(*iter));
iter->track = &file->tracks[track];
iter->position = 0;
+ iter->loop_point = 0;
return iter;
}
@@ -728,6 +715,17 @@
void MIDI_RestartIterator(midi_track_iter_t *iter)
{
iter->position = 0;
+ iter->loop_point = 0;
+}
+
+void MIDI_SetLoopPoint(midi_track_iter_t *iter)
+{
+ iter->loop_point = iter->position;
+}
+
+void MIDI_RestartAtLoopPoint(midi_track_iter_t *iter)
+{
+ iter->position = iter->loop_point;
}
#ifdef TEST
--- a/src/midifile.h
+++ b/src/midifile.h
@@ -27,54 +27,94 @@
{
MIDI_EVENT_NOTE_OFF = 0x80,
MIDI_EVENT_NOTE_ON = 0x90,
- MIDI_EVENT_AFTERTOUCH = 0xa0,
- MIDI_EVENT_CONTROLLER = 0xb0,
- MIDI_EVENT_PROGRAM_CHANGE = 0xc0,
- MIDI_EVENT_CHAN_AFTERTOUCH = 0xd0,
- MIDI_EVENT_PITCH_BEND = 0xe0,
+ MIDI_EVENT_AFTERTOUCH = 0xA0,
+ MIDI_EVENT_CONTROLLER = 0xB0,
+ MIDI_EVENT_PROGRAM_CHANGE = 0xC0,
+ MIDI_EVENT_CHAN_AFTERTOUCH = 0xD0,
+ MIDI_EVENT_PITCH_BEND = 0xE0,
- MIDI_EVENT_SYSEX = 0xf0,
- MIDI_EVENT_SYSEX_SPLIT = 0xf7,
- MIDI_EVENT_META = 0xff,
+ MIDI_EVENT_SYSEX = 0xF0,
+ MIDI_EVENT_SYSEX_SPLIT = 0xF7,
+ MIDI_EVENT_META = 0xFF,
} midi_event_type_t;
typedef enum
{
- MIDI_CONTROLLER_BANK_SELECT = 0x0,
- MIDI_CONTROLLER_MODULATION = 0x1,
- MIDI_CONTROLLER_BREATH_CONTROL = 0x2,
- MIDI_CONTROLLER_FOOT_CONTROL = 0x3,
- MIDI_CONTROLLER_PORTAMENTO = 0x4,
- MIDI_CONTROLLER_DATA_ENTRY = 0x5,
+ MIDI_CONTROLLER_BANK_SELECT_MSB = 0x00,
+ MIDI_CONTROLLER_MODULATION = 0x01,
+ MIDI_CONTROLLER_BREATH_CONTROL = 0x02,
+ MIDI_CONTROLLER_FOOT_CONTROL = 0x04,
+ MIDI_CONTROLLER_PORTAMENTO = 0x05,
+ MIDI_CONTROLLER_DATA_ENTRY_MSB = 0x06,
+ MIDI_CONTROLLER_VOLUME_MSB = 0x07,
+ MIDI_CONTROLLER_PAN = 0x0A,
- MIDI_CONTROLLER_MAIN_VOLUME = 0x7,
- MIDI_CONTROLLER_PAN = 0xa,
+ MIDI_CONTROLLER_BANK_SELECT_LSB = 0x20,
+ MIDI_CONTROLLER_DATA_ENTRY_LSB = 0x26,
+ MIDI_CONTROLLER_VOLUME_LSB = 0X27,
- MIDI_CONTROLLER_ALL_NOTES_OFF = 0x7b,
+ MIDI_CONTROLLER_REVERB = 0x5B,
+ MIDI_CONTROLLER_CHORUS = 0x5D,
+
+ MIDI_CONTROLLER_RPN_LSB = 0x64,
+ MIDI_CONTROLLER_RPN_MSB = 0x65,
+
+ MIDI_CONTROLLER_ALL_SOUND_OFF = 0x78,
+ MIDI_CONTROLLER_RESET_ALL_CTRLS = 0x79,
+ MIDI_CONTROLLER_ALL_NOTES_OFF = 0x7B,
} midi_controller_t;
typedef enum
{
- MIDI_META_SEQUENCE_NUMBER = 0x0,
+ MIDI_META_SEQUENCE_NUMBER = 0x00,
- MIDI_META_TEXT = 0x1,
- MIDI_META_COPYRIGHT = 0x2,
- MIDI_META_TRACK_NAME = 0x3,
- MIDI_META_INSTR_NAME = 0x4,
- MIDI_META_LYRICS = 0x5,
- MIDI_META_MARKER = 0x6,
- MIDI_META_CUE_POINT = 0x7,
+ MIDI_META_TEXT = 0x01,
+ MIDI_META_COPYRIGHT = 0x02,
+ MIDI_META_TRACK_NAME = 0x03,
+ MIDI_META_INSTR_NAME = 0x04,
+ MIDI_META_LYRICS = 0x05,
+ MIDI_META_MARKER = 0x06,
+ MIDI_META_CUE_POINT = 0x07,
MIDI_META_CHANNEL_PREFIX = 0x20,
- MIDI_META_END_OF_TRACK = 0x2f,
+ MIDI_META_END_OF_TRACK = 0x2F,
MIDI_META_SET_TEMPO = 0x51,
MIDI_META_SMPTE_OFFSET = 0x54,
MIDI_META_TIME_SIGNATURE = 0x58,
MIDI_META_KEY_SIGNATURE = 0x59,
- MIDI_META_SEQUENCER_SPECIFIC = 0x7f,
+ MIDI_META_SEQUENCER_SPECIFIC = 0x7F,
} midi_meta_event_type_t;
+#define EMIDI_LOOP_FLAG 0x7F
+
+typedef enum
+{
+ EMIDI_DEVICE_GENERAL_MIDI = 0x00,
+ EMIDI_DEVICE_SOUND_CANVAS = 0x01,
+ EMIDI_DEVICE_AWE32 = 0x02,
+ EMIDI_DEVICE_WAVE_BLASTER = 0x03,
+ EMIDI_DEVICE_SOUND_BLASTER = 0x04,
+ EMIDI_DEVICE_PRO_AUDIO = 0x05,
+ EMIDI_DEVICE_SOUND_MAN_16 = 0x06,
+ EMIDI_DEVICE_ADLIB = 0x07,
+ EMIDI_DEVICE_SOUNDSCAPE = 0x08,
+ EMIDI_DEVICE_ULTRASOUND = 0x09,
+ EMIDI_DEVICE_ALL = 0x7F,
+} emidi_device_t;
+
+typedef enum
+{
+ EMIDI_CONTROLLER_TRACK_DESIGNATION = 0x6E,
+ EMIDI_CONTROLLER_TRACK_EXCLUSION = 0x6F,
+ EMIDI_CONTROLLER_PROGRAM_CHANGE = 0x70,
+ EMIDI_CONTROLLER_VOLUME = 0x71,
+ EMIDI_CONTROLLER_LOOP_BEGIN = 0x74,
+ EMIDI_CONTROLLER_LOOP_END = 0x75,
+ EMIDI_CONTROLLER_GLOBAL_LOOP_BEGIN = 0x76,
+ EMIDI_CONTROLLER_GLOBAL_LOOP_END = 0x77,
+} emidi_controller_t;
+
typedef struct
{
// Meta event type:
@@ -145,10 +185,6 @@
unsigned int MIDI_NumTracks(midi_file_t *file);
-// Get the number of events in a MIDI file.
-
-unsigned int MIDI_NumEvents(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);
@@ -168,6 +204,14 @@
// Reset an iterator to the beginning of a track.
void MIDI_RestartIterator(midi_track_iter_t *iter);
+
+// Set loop point to current position.
+
+void MIDI_SetLoopPoint(midi_track_iter_t *iter);
+
+// Set position to saved loop point.
+
+void MIDI_RestartAtLoopPoint(midi_track_iter_t *iter);
#endif /* #ifndef MIDIFILE_H */