Skip to content
This repository has been archived by the owner on Jun 23, 2023. It is now read-only.

Add proper SysEx support to PortMidi #526

Merged
merged 4 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 140 additions & 76 deletions prboom2/src/MUSIC/portmidiplayer.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,63 @@ static const char *pm_name (void)
#endif


static dboolean use_reset_delay;
static unsigned char gs_reset[] = {0xf0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7f, 0x00, 0x41, 0xf7};
static unsigned char gm_system_on[] = {0xf0, 0x7e, 0x7f, 0x09, 0x01, 0xf7};
static unsigned char gm2_system_on[] = {0xf0, 0x7e, 0x7f, 0x09, 0x03, 0xf7};
static unsigned char xg_system_on[] = {0xf0, 0x43, 0x10, 0x4c, 0x00, 0x00, 0x7e, 0x00, 0xf7};
static PmEvent event_buffer[13 * 16];

static void reset_device (unsigned long when)
{
if (!strcasecmp(mus_portmidi_reset_type, "gm"))
Pm_WriteSysEx(pm_stream, when, gm_system_on);
else if (!strcasecmp(mus_portmidi_reset_type, "gm2"))
Pm_WriteSysEx(pm_stream, when, gm2_system_on);
else if (!strcasecmp(mus_portmidi_reset_type, "xg"))
Pm_WriteSysEx(pm_stream, when, xg_system_on);
else // default to "gs"
Pm_WriteSysEx(pm_stream, when, gs_reset);

// additional resets for compatibility with MS GS Wavetable Synth
Pm_Write(pm_stream, event_buffer, 13 * 16);

use_reset_delay = mus_portmidi_reset_delay > 0;
}

static void init_reset_buffer (void)
{
int i;
PmEvent *event = event_buffer;
for (i = 0; i < 16; ++i)
{
// program change to default piano (or drums for ch. 10)
event[0].message = Pm_Message(MIDI_EVENT_PROGRAM_CHANGE | i, 0x00, 0x00);
// reset all controllers
event[1].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x79, 0x00);
// all notes off
event[2].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x7b, 0x00);
// all sound off
event[3].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x78, 0x00);
// reset aftertouch channel pressure to 0
event[4].message = Pm_Message(MIDI_EVENT_CHAN_AFTERTOUCH | i, 0x00, 0x00);
// reset expression to 127 (max)
event[5].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x0b, 0x7f);
// reset pitch bend to 64 (center)
event[6].message = Pm_Message(MIDI_EVENT_PITCH_BEND | i, 0x40, 0x00);
// RPN sequence to adjust pitch bend range (RPN value 0x0000)
event[7].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x65, 0x00);
event[8].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x64, 0x00);
// reset pitch bend range to central tuning +/- 2 semitones and 0 cents
event[9].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x06, 0x02);
event[10].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x26, 0x00);
// end of RPN sequence
event[11].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x64, 0x7f);
event[12].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x65, 0x7f);
event += 13;
}
}

static int pm_init (int samplerate)
{
PmDeviceID outputdevice;
Expand Down Expand Up @@ -175,6 +232,12 @@ static int pm_init (int samplerate)
return 0;
}

// option to block sysex messages from midi file
if (mus_portmidi_filter_sysex)
Pm_SetFilter(pm_stream, PM_FILT_ACTIVE | PM_FILT_SYSEX);

init_reset_buffer();
reset_device(0);
return 1;
}

Expand All @@ -185,7 +248,8 @@ static void pm_shutdown (void)
if (pm_stream)
{
// stop all sound, in case of hanging notes
pm_stop();
if (pm_playing)
pm_stop();

/* ugly deadlock in portmidi win32 implementation:
Expand Down Expand Up @@ -219,13 +283,9 @@ static void pm_shutdown (void)



static PmEvent event_buffer[14 * 16];

static const void *pm_registersong (const void *data, unsigned len)
{
int i;
midimem_t mf;
PmEvent *event = event_buffer;

mf.len = len;
mf.pos = 0;
Expand All @@ -251,34 +311,6 @@ static const void *pm_registersong (const void *data, unsigned len)
//spmc = compute_spmc (MIDI_GetFileTimeDivision (midifile), 500000, 1000);
spmc = MIDI_spmc (midifile, NULL, 1000);

for (i = 0; i < 16; ++i)
{
// RPN sequence to adjust pitch bend range (RPN value 0x0000)
event[0].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x65, 0x00);
event[1].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x64, 0x00);
// reset pitch bend range to central tuning +/- 2 semitones and 0 cents
event[2].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x06, 0x02);
event[3].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x26, 0x00);
// end of RPN sequence
event[4].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x64, 0x7f);
event[5].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x65, 0x7f);
// all notes off
event[6].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x7b, 0x00);
// reset all controllers
event[7].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x79, 0x00);
// reset pan to 64 (center)
event[8].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x0a, 0x40);
// reset reverb to 40 and other effect controllers to 0
event[9].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x5b, 0x28); // reverb
event[10].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x5c, 0x00); // tremolo
event[11].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x5d, 0x00); // chorus
event[12].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x5e, 0x00); // detune
event[13].message = Pm_Message(MIDI_EVENT_CONTROLLER | i, 0x5f, 0x00); // phaser
event += 14;
}

Pm_Write(pm_stream, event_buffer, 14 * 16);

// handle not used
return data;
}
Expand All @@ -302,35 +334,30 @@ extern int mus_extend_volume; // from e6y.h
void I_midiOutSetVolumes (int volume); // from e6y.h
#endif

static int channelvol[16];
static int mastervol;

static void pm_setchvolume (int ch, int v, unsigned long when)
static void set_mastervol (unsigned long when)
{
channelvol[ch] = v;
writeevent (when, MIDI_EVENT_CONTROLLER, ch, 7, channelvol[ch] * pm_volume / 15);
int vol = mastervol * pm_volume / 15;
unsigned char data[] = {0xf0, 0x7f, 0x7f, 0x04, 0x01, vol & 0x7f, vol >> 7, 0xf7};
Pm_WriteSysEx(pm_stream, when, data);
}

static void pm_refreshvolume (void)
static void refresh_mastervol (void)
{
int i;
unsigned long when = Pt_Time ();

for (i = 0; i < 16; i ++)
writeevent (when, MIDI_EVENT_CONTROLLER, i, 7, channelvol[i] * pm_volume / 15);
set_mastervol(when);
}

static void pm_clearchvolume (void)
static void clear_mastervol (void)
{
int i;
for (i = 0; i < 16; i++)
channelvol[i] = 127; // default: max

mastervol = 16383; // default: max, 14-bit
}

static int firsttime = 1;

static void pm_setvolume (int v)
{
static int firsttime = 1;

if (pm_volume == v && !firsttime)
return;
firsttime = 0;
Expand All @@ -348,7 +375,7 @@ static void pm_setvolume (int v)
I_midiOutSetVolumes (pm_volume);
else
#endif
pm_refreshvolume ();
refresh_mastervol();
}


Expand Down Expand Up @@ -388,20 +415,38 @@ static void pm_play (const void *handle, int looping)
pm_playing = 1;
//pm_paused = 0;
pm_delta = 0.0;
pm_clearchvolume ();
pm_refreshvolume ();
clear_mastervol();
if (!firsttime) // set pm_volume first, see pm_setvolume()
{
#ifdef _WIN32
if (!mus_extend_volume)
#endif
refresh_mastervol();
}
trackstart = Pt_Time ();

}

static dboolean is_mastervol (unsigned char *data, int len)
{
unsigned char msg[] = {0xf0, 0x7f, 0x7f, 0x04, 0x01, 0x00, 0x00, 0xf7};
return (len == 8 && !memcmp(data, msg, 5));
}

static dboolean is_sysex_reset (unsigned char *data)
{
return (!memcmp(data, gs_reset, sizeof(gs_reset))
|| !memcmp(data, gm_system_on, sizeof(gm_system_on))
|| !memcmp(data, gm2_system_on, sizeof(gm2_system_on))
|| !memcmp(data, xg_system_on, sizeof(xg_system_on)));
}

static void writesysex (unsigned long when, int etype, unsigned char *data, int len)
{
// sysex code is untested
// it's possible to use an auto-resizing buffer here, but a malformed
// midi file could make it grow arbitrarily large (since it must grow
// until it hits an 0xf7 terminator)
if (len + sysexbufflen > SYSEX_BUFF_SIZE)
if (len + sysexbufflen > SYSEX_BUFF_SIZE - 1)
{
lprintf (LO_WARN, "portmidiplayer: ignoring large or malformed sysex message\n");
sysexbufflen = 0;
Expand All @@ -411,24 +456,51 @@ static void writesysex (unsigned long when, int etype, unsigned char *data, int
sysexbufflen += len;
if (sysexbuff[sysexbufflen - 1] == 0xf7) // terminator
{
memmove(&sysexbuff[1], &sysexbuff[0], sysexbufflen * sizeof(*sysexbuff));
sysexbuff[0] = 0xf0; // start of exclusive (SOX) in front
sysexbufflen++;

#ifdef _WIN32
if (!mus_extend_volume)
#endif
{
if (is_mastervol(sysexbuff, sysexbufflen))
{
// master volume message from midi file, scale by volume slider
mastervol = sysexbuff[6] << 7 | sysexbuff[5]; // back to 14-bit
set_mastervol(when);
sysexbufflen = 0;
return;
}
}

Pm_WriteSysEx (pm_stream, when, sysexbuff);

if (is_sysex_reset(sysexbuff))
{
use_reset_delay = mus_portmidi_reset_delay > 0;

#ifdef _WIN32
if (!mus_extend_volume)
#endif
{
// sysex reset from midi file, reapply master volume
clear_mastervol();
set_mastervol(when);
}
}
sysexbufflen = 0;
}
}

static void pm_stop (void)
{
int i;
unsigned long when = Pt_Time ();
pm_playing = 0;


// songs can be stopped at any time, so reset everything
for (i = 0; i < 16; i++)
{
writeevent (when, MIDI_EVENT_CONTROLLER, i, 123, 0); // all notes off
writeevent (when, MIDI_EVENT_CONTROLLER, i, 121, 0); // reset all parameters
}
reset_device(when);

// abort any partial sysex
sysexbufflen = 0;
}
Expand Down Expand Up @@ -461,6 +533,13 @@ static void pm_render (void *vdest, unsigned bufflen)
double eventdelta;
currevent = events[eventpos];

if (use_reset_delay)
{
// delay after reset, for real devices only (e.g. roland sc-55)
currevent->delta_time += mus_portmidi_reset_delay / spmc;
use_reset_delay = false;
}

// how many samples away event is
eventdelta = currevent->delta_time * spmc;

Expand Down Expand Up @@ -508,26 +587,11 @@ static void pm_render (void *vdest, unsigned bufflen)
return;
}
break; // not interested in most metas
case MIDI_EVENT_CONTROLLER:
if (currevent->data.channel.param1 == 7)
{ // volume event
#ifdef _WIN32
if (!mus_extend_volume)
#endif
{
pm_setchvolume (currevent->data.channel.channel, currevent->data.channel.param2, when);
break;
}
} // fall through
default:
writeevent (when, currevent->event_type, currevent->data.channel.channel, currevent->data.channel.param1, currevent->data.channel.param2);
break;

}
// if the event was a "reset all controllers", we need to additionally re-fix the volume (which itself was reset)
if (currevent->event_type == MIDI_EVENT_CONTROLLER && currevent->data.channel.param1 == 121)
pm_setchvolume (currevent->data.channel.channel, 127, when);

// event processed so advance midiclock
pm_delta += eventdelta;
eventpos++;
Expand Down
3 changes: 3 additions & 0 deletions prboom2/src/SDL/i_sound.c
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,9 @@ int mus_fluidsynth_chorus;
int mus_fluidsynth_reverb;
int mus_fluidsynth_gain; // NSM fine tune fluidsynth output level
int mus_opl_gain; // NSM fine tune OPL output level
const char *mus_portmidi_reset_type; // portmidi reset type
int mus_portmidi_reset_delay; // portmidi delay after reset
int mus_portmidi_filter_sysex; // portmidi block sysex from midi files


static void Exp_ShutdownMusic(void)
Expand Down
3 changes: 3 additions & 0 deletions prboom2/src/i_sound.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ extern int mus_fluidsynth_chorus;
extern int mus_fluidsynth_reverb;
extern int mus_fluidsynth_gain; // NSM fine tune fluidsynth output level
extern int mus_opl_gain; // NSM fine tune OPL output level
extern const char *mus_portmidi_reset_type; // portmidi reset type
extern int mus_portmidi_reset_delay; // portmidi delay after reset
extern int mus_portmidi_filter_sysex; // portmidi block sysex from midi files

// prefered MIDI player
typedef enum
Expand Down
3 changes: 3 additions & 0 deletions prboom2/src/m_misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ default_t defaults[] =
{"mus_fluidsynth_reverb",{&mus_fluidsynth_reverb},{0},0,1,def_bool,ss_none},
{"mus_fluidsynth_gain",{&mus_fluidsynth_gain},{50},0,1000,def_int,ss_none}, // NSM fine tune fluidsynth output level
{"mus_opl_gain",{&mus_opl_gain},{50},0,1000,def_int,ss_none}, // NSM fine tune opl output level
{"mus_portmidi_reset_type",{NULL, &mus_portmidi_reset_type},{0,"gs"},UL,UL,def_str,ss_none}, // portmidi reset type (gs, gm, gm2, xg)
{"mus_portmidi_reset_delay",{&mus_portmidi_reset_delay},{0},0,2000,def_int,ss_none}, // portmidi delay after reset (milliseconds)
{"mus_portmidi_filter_sysex",{&mus_portmidi_filter_sysex},{0},0,1,def_bool,ss_none}, // portmidi block sysex from midi files

{"Video settings",{NULL},{0},UL,UL,def_none,ss_none},
{"videomode",{NULL, &default_videomode},{0,"8bit"},UL,UL,def_str,ss_none},
Expand Down