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

Commit

Permalink
Add proper SysEx support to PortMidi (#526)
Browse files Browse the repository at this point in the history
* Add proper SysEx support to PortMidi

* Add additional reset messages for compatibility with MS GS Wavetable Synth

* Update comment for MS GS Wavetable Synth resets

* Stop sound if a shutdown is called directly
  • Loading branch information
ceski-1 authored Sep 2, 2022
1 parent 3d24453 commit af82970
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 76 deletions.
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

0 comments on commit af82970

Please sign in to comment.