winmidi: Optimize reset messages, change volume control, add instrument fallback support (#834)

* Cosmetic changes.

* Add volume safeguards.

* Use device setting for reverb and chorus by default.

* Clean up volume change behavior.

* Move "all notes off" and "reset all controllers" to reset function.

* Optimize set of reset messages depending on device and reset type.

* Ignore SysEx resets from MIDI files for MS GS Wavetable Synth.

* Check for Roland master volume combined with master tune, etc.

* Check for Yamaha master volume combined with master tune, etc.

* Don't replace master volume message; send a new one.

* Change order of volume check.

* Switch from master volume to channel volume control for better compatibility with MIDI devices.

* Change default `winmm_reset_delay` to 0.

* Change default winmm_reset_delay to 0.

* Remove unused variable.

* Add GS instrument fallback support.

* Cast `event_type` as `int`

* Fix switch/case.

* Fix hot swapping.

* Remove unneeded `return`.

* Add instrument fallback support for all devices in GS mode.

* Assign reset defaults to all devices.

* Fix formatting.
This commit is contained in:
ceski 2022-12-09 02:09:49 -08:00 committed by GitHub
parent 61302e811f
commit 72ddbe8ffc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 706 additions and 164 deletions

View File

@ -53,6 +53,7 @@ set(WOOF_SOURCES
m_snapshot.c m_snapshot.h
m_swap.h
memio.c memio.h
midifallback.c midifallback.h
midifile.c midifile.h
mus2mid.c mus2mid.h
net_client.c net_client.h

View File

@ -19,6 +19,7 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <mmsystem.h>
#include <mmreg.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
@ -30,11 +31,21 @@
#include "memio.h"
#include "mus2mid.h"
#include "midifile.h"
#include "midifallback.h"
int winmm_reset_type = 2;
int winmm_reset_delay = 100;
int winmm_reverb_level = 40;
int winmm_chorus_level = 0;
int winmm_reset_type = -1;
int winmm_reset_delay = 0;
int winmm_reverb_level = -1;
int winmm_chorus_level = -1;
enum
{
RESET_TYPE_NONE,
RESET_TYPE_GS,
RESET_TYPE_GM,
RESET_TYPE_GM2,
RESET_TYPE_XG,
};
static byte gs_reset[] = {
0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7
@ -52,14 +63,12 @@ static byte xg_system_on[] = {
0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7
};
static byte master_volume_msg[] = {
0xF0, 0x7F, 0x7F, 0x04, 0x01, 0x7F, 0x7F, 0xF7
};
static boolean use_fallback;
#define DEFAULT_MASTER_VOLUME 16383
static unsigned int master_volume = DEFAULT_MASTER_VOLUME;
#define DEFAULT_VOLUME 100
static int channel_volume[MIDI_CHANNELS_PER_TRACK];
static int last_volume = -1;
static float volume_factor = 1.0f;
static float volume_factor = 0.0f;
static boolean update_volume = false;
static DWORD timediv;
@ -72,7 +81,7 @@ static HANDLE hBufferReturnEvent;
static HANDLE hExitEvent;
static HANDLE hPlayerThread;
// MS GS Wavetable Syhth id
// MS GS Wavetable Synth Device ID.
static int ms_gs_synth = MIDI_MAPPER;
// This is a reduced Windows MIDIEVENT structure for MEVT_F_SHORT
@ -244,12 +253,11 @@ static void SendNOPMsg(int time)
WriteBuffer((byte *)&native_event, sizeof(native_event_t));
}
static void SendDelayMsg(void)
static void SendDelayMsg(int time_ms)
{
// Convert ms to ticks (see "Standard MIDI Files 1.0" page 14).
int ticks = (float)winmm_reset_delay * 1000 * timediv / tempo + 0.5f;
SendNOPMsg(ticks);
int time_ticks = (float)time_ms * 1000 * timediv / tempo + 0.5f;
SendNOPMsg(time_ticks);
}
static void UpdateTempo(int time)
@ -261,78 +269,209 @@ static void UpdateTempo(int time)
WriteBuffer((byte *)&native_event, sizeof(native_event_t));
}
static void UpdateVolume(int time)
static void SendVolumeMsg(int time, int channel)
{
int volume = master_volume * volume_factor + 0.5f;
master_volume_msg[5] = volume & 0x7F;
master_volume_msg[6] = (volume >> 7) & 0x7F;
SendLongMsg(time, master_volume_msg, sizeof(master_volume_msg));
int volume = channel_volume[channel] * volume_factor + 0.5f;
SendShortMsg(time, MIDI_EVENT_CONTROLLER, channel,
MIDI_CONTROLLER_MAIN_VOLUME, volume);
}
static void ResetDevice(void)
static void UpdateVolume(void)
{
int i;
for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
{
// midiOutReset() sends "all notes off" and "reset all controllers."
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0);
SendVolumeMsg(0, i);
}
}
// Reset pitch bend sensitivity to +/- 2 semitones and 0 cents.
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 0);
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 0);
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_MSB, 2);
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_LSB, 0);
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 127);
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 127);
static void ResetVolume(void)
{
int i;
// Reset channel volume and pan.
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_MAIN_VOLUME, 100);
for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
{
channel_volume[i] = DEFAULT_VOLUME;
SendVolumeMsg(0, i);
}
}
static void ResetReverb(int reset_type)
{
int i;
int reverb = winmm_reverb_level;
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)
{
int i;
int chorus = winmm_chorus_level;
if (chorus == -1 && reset_type == RESET_TYPE_NONE)
{
// 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);
}
}
}
static void ResetControllers(void)
{
int i;
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);
// Reset instrument.
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);
}
if (MidiDevice != ms_gs_synth)
{
// Send SysEx reset message.
switch (winmm_reset_type)
{
case 1: // GS Reset
SendLongMsg(0, gs_reset, sizeof(gs_reset));
break;
case 2: // GM System On
SendLongMsg(0, gm_system_on, sizeof(gm_system_on));
break;
case 3: // GM2 System On
SendLongMsg(0, gm2_system_on, sizeof(gm2_system_on));
break;
case 4: // XG System On
SendLongMsg(0, xg_system_on, sizeof(xg_system_on));
break;
default: // None
break;
}
// Send delay after reset.
if (winmm_reset_delay > 0)
static void ResetPitchBendSensitivity(void)
{
SendDelayMsg();
}
}
int i;
for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
{
// Reset reverb and chorus.
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_REVERB, winmm_reverb_level);
SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_CHORUS, winmm_chorus_level);
// 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 RESET_TYPE_NONE:
reset_type = RESET_TYPE_NONE;
break;
case RESET_TYPE_GS:
reset_type = RESET_TYPE_GS;
break;
default:
reset_type = RESET_TYPE_GM;
break;
}
}
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);
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);
}
}
@ -447,56 +586,18 @@ static boolean IsSysExReset(const byte *msg, int length)
return false;
}
static boolean IsMasterVolume(const byte *msg, int length, unsigned int *volume)
{
// General Midi (7F <dev> 04 01 <lsb> <msb> F7)
if (length == 7 &&
msg[0] == 0x7F && // Universal Real Time
msg[2] == 0x04 && // Device Control
msg[3] == 0x01) // Master Volume
{
*volume = (msg[4] & 0x7F) | ((msg[5] & 0x7F) << 7);
return true;
}
// Roland (41 <dev> 42 12 40 00 04 <vol> <sum> F7)
if (length == 10 &&
msg[0] == 0x41 && // Roland
msg[2] == 0x42 && // GS
msg[3] == 0x12 && // DT1
msg[4] == 0x40 && // Address MSB
msg[5] == 0x00 && // Address
msg[6] == 0x04) // Address LSB
{
*volume = DEFAULT_MASTER_VOLUME * (msg[7] & 0x7F) / 127;
return true;
}
// Yamaha (43 <dev> 4C 00 00 04 <vol> F7)
if (length == 8 &&
msg[0] == 0x43 && // Yamaha
msg[2] == 0x4C && // XG
msg[3] == 0x00 && // Address High
msg[4] == 0x00 && // Address Mid
msg[5] == 0x04) // Address Low
{
*volume = DEFAULT_MASTER_VOLUME * (msg[6] & 0x7F) / 127;
return true;
}
return false;
}
static void SendSysExMsg(int time, const byte *data, int length)
{
native_event_t native_event;
boolean is_sysex_reset;
const byte event_type = MIDI_EVENT_SYSEX;
if (IsMasterVolume(data, length, &master_volume))
is_sysex_reset = IsSysExReset(data, length);
if (is_sysex_reset && MidiDevice == ms_gs_synth)
{
// Found a master volume message in the MIDI file. Take this new
// value and scale it by the user's volume slider.
UpdateVolume(time);
// Ignore SysEx reset from MIDI file for MS GS Wavetable Synth.
SendNOPMsg(time);
return;
}
@ -509,12 +610,17 @@ static void SendSysExMsg(int time, const byte *data, int length)
WriteBuffer(data, length);
WriteBufferPad();
if (IsSysExReset(data, length))
if (is_sysex_reset)
{
// SysEx reset also resets master volume. Take the default master
// volume and scale it by the user's volume slider.
master_volume = DEFAULT_MASTER_VOLUME;
UpdateVolume(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;
}
}
@ -527,11 +633,10 @@ static void FillBuffer(void)
if (initial_playback)
{
initial_playback = false;
master_volume = DEFAULT_MASTER_VOLUME;
ResetDevice();
StreamOut();
initial_playback = false;
return;
}
@ -539,7 +644,7 @@ static void FillBuffer(void)
{
update_volume = false;
UpdateVolume(0);
UpdateVolume();
StreamOut();
return;
}
@ -550,6 +655,7 @@ static void FillBuffer(void)
int min_time = INT_MAX;
int idx = -1;
int delta_time;
midi_fallback_t fallback = {FALLBACK_NONE, 0};
// Look for an event with a minimal delta time.
for (i = 0; i < MIDI_NumTracks(song.file); ++i)
@ -600,6 +706,11 @@ static void FillBuffer(void)
delta_time = min_time - song.current_time;
if (use_fallback)
{
MIDI_CheckFallback(event, &fallback);
}
switch ((int)event->event_type)
{
case MIDI_EVENT_META:
@ -607,7 +718,8 @@ static void FillBuffer(void)
{
case MIDI_META_SET_TEMPO:
tempo = MAKE_EVT(event->data.meta.data[2],
event->data.meta.data[1], event->data.meta.data[0], 0);
event->data.meta.data[1],
event->data.meta.data[0], 0);
UpdateTempo(delta_time);
break;
@ -621,22 +733,63 @@ static void FillBuffer(void)
break;
case MIDI_EVENT_CONTROLLER:
if (event->data.channel.param1 == MIDI_CONTROLLER_MAIN_VOLUME)
{
// Adjust channel volume.
int volume = event->data.channel.param2;
channel_volume[event->data.channel.channel] = volume;
SendVolumeMsg(delta_time, event->data.channel.channel);
break;
}
else if (fallback.type == FALLBACK_BANK_LSB &&
event->data.channel.param1 == MIDI_CONTROLLER_BANK_SELECT_LSB)
{
SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
event->data.channel.channel,
MIDI_CONTROLLER_BANK_SELECT_LSB,
fallback.value);
break;
}
// Fall through.
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);
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 (fallback.type == FALLBACK_BANK_MSB)
{
SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
event->data.channel.channel,
MIDI_CONTROLLER_BANK_SELECT_MSB,
fallback.value);
SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE,
event->data.channel.channel,
event->data.channel.param1, 0);
break;
}
else if (fallback.type == FALLBACK_DRUMS)
{
SendShortMsg(delta_time, MIDI_EVENT_PROGRAM_CHANGE,
event->data.channel.channel,
fallback.value, 0);
break;
}
// Fall through.
case MIDI_EVENT_CHAN_AFTERTOUCH:
SendShortMsg(delta_time, event->event_type, event->data.channel.channel,
SendShortMsg(delta_time, event->event_type,
event->data.channel.channel,
event->data.channel.param1, 0);
break;
case MIDI_EVENT_SYSEX:
SendSysExMsg(delta_time, event->data.sysex.data, event->data.sysex.length);
SendSysExMsg(delta_time, event->data.sysex.data,
event->data.sysex.length);
song.current_time = min_time;
StreamOut();
return;
@ -698,6 +851,8 @@ static boolean I_WIN_InitMusic(int device)
hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
MIDI_InitFallback();
return true;
}
@ -713,32 +868,28 @@ static void I_WIN_SetMusicVolume(int volume)
volume_factor = sqrtf((float)volume / 15);
update_volume = true;
update_volume = (song.file != NULL);
}
static void I_WIN_StopSong(void *handle)
{
MMRESULT mmr;
if (hPlayerThread)
if (!hPlayerThread)
{
return;
}
SetEvent(hExitEvent);
WaitForSingleObject(hPlayerThread, INFINITE);
CloseHandle(hPlayerThread);
hPlayerThread = NULL;
}
mmr = midiStreamStop(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
MidiError("midiStreamStop", mmr);
}
mmr = midiOutReset((HMIDIOUT)hMidiStream);
if (mmr != MMSYSERR_NOERROR)
{
MidiError("midiOutReset", mmr);
}
}
static void I_WIN_PlaySong(void *handle, boolean looping)
@ -753,8 +904,6 @@ static void I_WIN_PlaySong(void *handle, boolean looping)
initial_playback = true;
update_volume = true;
SetEvent(hBufferReturnEvent);
mmr = midiStreamRestart(hMidiStream);
@ -915,28 +1064,37 @@ static void I_WIN_ShutdownMusic(void)
}
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)
{
MidiError("midiStreamClose", mmr);
}
hMidiStream = NULL;
if (buffer.data)
{
free(buffer.data);
buffer.data = NULL;
}
buffer.size = 0;
buffer.position = 0;
CloseHandle(hBufferReturnEvent);
CloseHandle(hExitEvent);
}
#include <mmreg.h>
int I_WIN_DeviceList(const char* devices[], int size)
{
int i;

View File

@ -2325,29 +2325,29 @@ default_t defaults[] = {
{
"winmm_reset_type",
(config_t *) &winmm_reset_type, NULL,
{2}, {0, 4}, number, ss_none, wad_no,
"SysEx reset for native MIDI (0 = None, 1 = GS Reset, 2 = GM System On (Default), 3 = GM2 System On, 4 = XG System On"
{-1}, {-1, 4}, number, ss_none, wad_no,
"SysEx reset for native MIDI (-1 = Default, 0 = None, 1 = GS, 2 = GM, 3 = GM2, 4 = XG)"
},
{
"winmm_reset_delay",
(config_t *) &winmm_reset_delay, NULL,
{100}, {0, 2000}, number, ss_none, wad_no,
{0}, {0, 2000}, number, ss_none, wad_no,
"Delay after reset for native MIDI (milliseconds)"
},
{
"winmm_chorus_level",
(config_t *) &winmm_chorus_level, NULL,
{0}, {0, 127}, number, ss_none, wad_no,
"fine tune default chorus level for native MIDI"
},
{
"winmm_reverb_level",
(config_t *) &winmm_reverb_level, NULL,
{40}, {0, 127}, number, ss_none, wad_no,
"fine tune default reverb level for native MIDI"
{-1}, {-1, 127}, number, ss_none, wad_no,
"Reverb send level for native MIDI (-1 = Default, 0 = Off, 127 = Max)"
},
{
"winmm_chorus_level",
(config_t *) &winmm_chorus_level, NULL,
{-1}, {-1, 127}, number, ss_none, wad_no,
"Chorus send level for native MIDI (-1 = Default, 0 = Off, 127 = Max)"
},
#endif

345
src/midifallback.c Normal file
View File

@ -0,0 +1,345 @@
//
// 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
//
#include <stdlib.h>
#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(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];
}
}
void MIDI_CheckFallback(midi_event_t *event, midi_fallback_t *fallback)
{
byte idx;
unsigned int *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;
}
break;
case MIDI_EVENT_PROGRAM_CHANGE:
idx = event->data.channel.channel;
program = &event->data.channel.param1;
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;
break;
}
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;
}
// 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;
}
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;
}
}
break;
}
fallback->type = FALLBACK_NONE;
fallback->value = 0xFF;
}
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)
{
int 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;
}
}

38
src/midifallback.h Normal file
View File

@ -0,0 +1,38 @@
//
// 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
//
#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(midi_event_t *event, midi_fallback_t *fallback);
void MIDI_ResetFallback(void);
void MIDI_InitFallback(void);