/*
* This file is part of the DXX-Rebirth project .
* It is copyright by its individual contributors, as recorded in the
* project's Git history. See COPYING.txt at the top level for license
* terms and a link to the Git history.
*/
/*
* This code handles HMP files. It can:
* - Open/read/close HMP files
* - Play HMP via Windows MIDI
* - Convert HMP to MIDI for further use
* Based on work of Arne de Bruijn and the JFFEE project
*/
#include
#include
#include
#include
#include "hmp.h"
#include "u_mem.h"
#include "console.h"
#include "timer.h"
#include "serial.h"
#include "dxxsconf.h"
#include "dsx-ns.h"
#include "compiler-make_unique.h"
#include "compiler-range_for.h"
#include "partial_range.h"
namespace dcx {
#define MIDIINT(x) (words_bigendian ? (x) : (SWAPINT(x)))
#define MIDISHORT(x) (words_bigendian ? (x) : (SWAPSHORT(x)))
#ifdef _WIN32
static int midi_volume;
static int channel_volume[16];
static void hmp_stop(hmp_file *hmp);
#endif
// READ/OPEN/CLOSE HMP
hmp_file::~hmp_file()
{
#ifdef _WIN32
hmp_stop(this);
#endif
}
std::unique_ptr hmp_open(const char *filename) {
int data, num_tracks, tempo;
auto fp = PHYSFSX_openReadBuffered(filename);
if (!fp)
return NULL;
std::unique_ptr hmp(new hmp_file{});
char buf[8];
if ((PHYSFS_read(fp, buf, 1, 8) != 8) || (memcmp(buf, "HMIMIDIP", 8)))
{
return NULL;
}
if (PHYSFSX_fseek(fp, 0x30, SEEK_SET))
{
return NULL;
}
if (PHYSFS_read(fp, &num_tracks, 4, 1) != 1)
{
return NULL;
}
if ((num_tracks < 1) || (num_tracks > HMP_TRACKS))
{
return NULL;
}
hmp->num_trks = num_tracks;
if (PHYSFSX_fseek(fp, 0x38, SEEK_SET))
{
return NULL;
}
if (PHYSFS_read(fp, &tempo, 4, 1) != 1)
{
return NULL;
}
hmp->tempo = INTEL_INT(tempo);
if (PHYSFSX_fseek(fp, 0x308, SEEK_SET))
{
return NULL;
}
for (int i = 0; i < num_tracks; i++) {
if ((PHYSFSX_fseek(fp, 4, SEEK_CUR)) || (PHYSFS_read(fp, &data, 4, 1) != 1))
{
return NULL;
}
data -= 12;
hmp->trks[i].len = data;
hmp->trks[i].data = make_unique(data);
/* finally, read track data */
if ((PHYSFSX_fseek(fp, 4, SEEK_CUR)) || (PHYSFS_read(fp, hmp->trks[i].data.get(), data, 1) != 1))
{
return NULL;
}
hmp->trks[i].loop_set = 0;
}
hmp->filesize = PHYSFS_fileLength(fp);
return hmp;
}
#ifdef _WIN32
// PLAY HMP AS MIDI
void hmp_stop(hmp_file *hmp)
{
MIDIHDR *mhdr;
if (!hmp->stop) {
hmp->stop = 1;
//PumpMessages();
midiStreamStop(hmp->hmidi);
while (hmp->bufs_in_mm)
timer_delay(1);
}
while ((mhdr = hmp->evbuf)) {
midiOutUnprepareHeader((HMIDIOUT)hmp->hmidi, mhdr, sizeof(MIDIHDR));
hmp->evbuf = mhdr->lpNext;
d_free(mhdr);
}
if (hmp->hmidi) {
midiStreamClose(hmp->hmidi);
hmp->hmidi = NULL;
}
}
/*
* read a HMI type variable length number
*/
static int get_var_num_hmi(unsigned char *data, int datalen, unsigned long *value) {
unsigned char *p;
unsigned long v = 0;
int shift = 0;
p = data;
while ((datalen > 0) && !(*p & 0x80)) {
v += *(p++) << shift;
shift += 7;
datalen --;
}
if (!datalen)
return 0;
v += (*(p++) & 0x7f) << shift;
if (value) *value = v;
return p - data;
}
/*
* read a MIDI type variable length number
*/
static int get_var_num(unsigned char *data, int datalen,
unsigned long *value) {
unsigned char *orgdata = data;
unsigned long v = 0;
while ((datalen > 0) && (*data & 0x80))
v = (v << 7) + (*(data++) & 0x7f);
if (!datalen)
return 0;
v = (v << 7) + *(data++);
if (value) *value = v;
return data - orgdata;
}
static int get_event(hmp_file *hmp, event *ev) {
static const array cmdlen{{3,3,3,3,2,2,3}};
unsigned long got;
unsigned long mindelta, delta;
int ev_num;
hmp_track *fndtrk = nullptr;
mindelta = INT_MAX;
range_for (auto &rtrk, partial_range(hmp->trks, static_cast(hmp->num_trks)))
{
const auto trk = &rtrk;
if (!trk->left || (hmp->loop_start && hmp->looping && !trk->loop_set))
continue;
if (!(got = get_var_num_hmi(trk->cur, trk->left, &delta)))
return HMP_INVALID_FILE;
if (trk->left > got + 2 && *(trk->cur + got) == 0xff
&& *(trk->cur + got + 1) == 0x2f) {/* end of track */
trk->left = 0;
continue;
}
if (hmp->loop_start && hmp->looping)
if (trk->cur == trk->loop)
delta = trk->loop_start - hmp->loop_start;
delta += trk->cur_time - hmp->cur_time;
if (delta < mindelta) {
mindelta = delta;
fndtrk = trk;
}
}
const auto trk = fndtrk;
if (!trk)
return HMP_EOF;
got = get_var_num_hmi(trk->cur, trk->left, &delta);
if (hmp->loop_start && hmp->looping)
if (trk->cur == trk->loop)
delta = trk->loop_start - hmp->loop_start;
trk->cur_time += delta;
if (!hmp->loop_start && *(trk->cur + got) >> 4 == MIDI_CONTROL_CHANGE && *(trk->cur + got + 1) == HMP_LOOP_START)
{
hmp->loop_start = trk->cur_time;
if ((hmp->filesize == 86949) && (trk->cur_time == 29)) // special ugly HACK HACK HACK for Descent2-version of descent.hmp where loop at this point causes instruments not being reset properly. No track supporting HMP loop I know of meets the requirements for the condition below and even if so - it only disabled the HMP loop feature.
{
hmp->loop_start = 0;
}
}
if (!hmp->loop_end && *(trk->cur + got) >> 4 == MIDI_CONTROL_CHANGE && *(trk->cur + got + 1) == HMP_LOOP_END)
hmp->loop_end = trk->cur_time;
if (hmp->loop_start && !trk->loop_set && trk->cur_time >= hmp->loop_start)
{
trk->loop_start = trk->cur_time;
trk->loop = trk->cur;
trk->len = trk->left;
trk->loop_set = 1;
}
if (hmp->loop_end && trk->cur_time > hmp->loop_end)
return HMP_EOF;
ev->delta = trk->cur_time - hmp->cur_time;
hmp->cur_time = trk->cur_time;
if ((trk->left -= got) < 3)
return HMP_INVALID_FILE;
trk->cur += got;
/*memset(ev, 0, sizeof(*ev));*/ev->datalen = 0;
ev->msg[0] = ev_num = *(trk->cur++);
trk->left--;
if (ev_num < 0x80)
return HMP_INVALID_FILE; /* invalid command */
if (ev_num < 0xf0) {
ev->msg[1] = *(trk->cur++);
trk->left--;
if (cmdlen[((ev_num) >> 4) - 8] == 3) {
ev->msg[2] = *(trk->cur++);
trk->left--;
if (ev->msg[0] >> 4 == MIDI_CONTROL_CHANGE && ev->msg[1] == MIDI_VOLUME)
{
channel_volume[ev->msg[0] & 0xf] = ev->msg[2];
ev->msg[2] = ev->msg[2] * midi_volume / MIDI_VOLUME_SCALE;
}
}
} else if (ev_num == 0xff) {
ev->msg[1] = *(trk->cur++);
trk->left--;
if (!(got = get_var_num(ev->data = trk->cur,
trk->left, (unsigned long *)&ev->datalen)))
return HMP_INVALID_FILE;
trk->cur += ev->datalen;
if (trk->left <= ev->datalen)
return HMP_INVALID_FILE;
trk->left -= ev->datalen;
} else /* sysex -> error */
return HMP_INVALID_FILE;
return 0;
}
static int fill_buffer(hmp_file *hmp) {
MIDIHDR *mhdr = hmp->evbuf;
unsigned int *p = reinterpret_cast(mhdr->lpData + mhdr->dwBytesRecorded);
unsigned int *pend = reinterpret_cast(mhdr->lpData + mhdr->dwBufferLength);
unsigned int i;
event ev{};
while (p + 4 <= pend) {
if (hmp->pending_size) {
i = (p - pend) * 4;
if (i > hmp->pending_size)
i = hmp->pending_size;
*(p++) = hmp->pending_event | i;
*(p++) = 0;
memcpy(reinterpret_cast(p), hmp->pending, i);
hmp->pending_size -= i;
p += (i + 3) / 4;
} else {
if ((i = get_event(hmp, &ev))) {
mhdr->dwBytesRecorded = (reinterpret_cast(p)) - (reinterpret_cast(mhdr->lpData));
return i;
}
if (ev.datalen) {
hmp->pending_size = ev.datalen;
hmp->pending = ev.data;
hmp->pending_event = ev.msg[0] << 24;
} else {
*(p++) = ev.delta;
*(p++) = 0;
*(p++) = (((DWORD)MEVT_SHORTMSG) << 24) |
((DWORD)ev.msg[0]) |
(((DWORD)ev.msg[1]) << 8) |
(((DWORD)ev.msg[2]) << 16);
}
}
}
mhdr->dwBytesRecorded = (reinterpret_cast(p)) - (reinterpret_cast(mhdr->lpData));
return 0;
}
static int setup_buffers(hmp_file *hmp) {
MIDIHDR *buf, *lastbuf;
lastbuf = NULL;
for (int i = 0; i < HMP_BUFFERS; i++) {
if (!(buf = (MIDIHDR *)d_malloc(HMP_BUFSIZE + sizeof(MIDIHDR))))
return HMP_OUT_OF_MEM;
memset(buf, 0, sizeof(MIDIHDR));
buf->lpData = reinterpret_cast(buf) + sizeof(MIDIHDR);
buf->dwBufferLength = HMP_BUFSIZE;
buf->dwUser = reinterpret_cast(hmp);
buf->lpNext = lastbuf;
lastbuf = buf;
}
hmp->evbuf = lastbuf;
return 0;
}
static void reset_tracks(struct hmp_file *hmp)
{
for (int i = 0; i < hmp->num_trks; i++) {
if (hmp->trks[i].loop_set)
hmp->trks[i].cur = hmp->trks[i].loop;
else
hmp->trks[i].cur = hmp->trks[i].data.get();
hmp->trks[i].left = hmp->trks[i].len;
hmp->trks[i].cur_time = 0;
}
hmp->cur_time = 0;
}
static void _stdcall midi_callback(HMIDISTRM, UINT uMsg, DWORD, DWORD_PTR dw1, DWORD)
{
hmp_file *hmp;
int rc;
if (uMsg != MOM_DONE)
return;
const auto mhdr = reinterpret_cast(dw1);
mhdr->dwBytesRecorded = 0;
hmp = reinterpret_cast(mhdr->dwUser);
mhdr->lpNext = hmp->evbuf;
hmp->evbuf = mhdr;
hmp->bufs_in_mm--;
if (!hmp->stop) {
while (fill_buffer(hmp) == HMP_EOF) {
if (hmp->bLoop)
hmp->stop = 0;
else
hmp->stop = 1;
hmp->looping = 1;
reset_tracks(hmp);
}
if ((rc = midiStreamOut(hmp->hmidi, hmp->evbuf,
sizeof(MIDIHDR))) != MMSYSERR_NOERROR) {
/* ??? */
} else {
hmp->evbuf = hmp->evbuf->lpNext;
hmp->bufs_in_mm++;
}
}
}
static void setup_tempo(hmp_file *hmp, unsigned long tempo) {
MIDIHDR *mhdr = hmp->evbuf;
unsigned int *p = reinterpret_cast(mhdr->lpData + mhdr->dwBytesRecorded);
*(p++) = 0;
*(p++) = 0;
*(p++) = (((DWORD)MEVT_TEMPO)<<24) | tempo;
mhdr->dwBytesRecorded += 12;
}
void hmp_setvolume(hmp_file *hmp, int volume)
{
if (hmp)
for (int channel = 0; channel < 16; channel++)
midiOutShortMsg((HMIDIOUT)hmp->hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_VOLUME << 8 | (channel_volume[channel] * volume / MIDI_VOLUME_SCALE) << 16));
midi_volume = volume;
}
int hmp_play(hmp_file *hmp, int bLoop)
{
int rc;
MIDIPROPTIMEDIV mptd;
hmp->bLoop = bLoop;
hmp->loop_start = 0;
hmp->loop_end = 0;
hmp->looping = 0;
hmp->devid = MIDI_MAPPER;
if ((rc = setup_buffers(hmp)))
return rc;
if ((midiStreamOpen(&hmp->hmidi, &hmp->devid,1, static_cast(reinterpret_cast(&midi_callback)), 0, CALLBACK_FUNCTION)) != MMSYSERR_NOERROR)
{
hmp->hmidi = NULL;
return HMP_MM_ERR;
}
mptd.cbStruct = sizeof(mptd);
mptd.dwTimeDiv = hmp->tempo;
if ((midiStreamProperty(hmp->hmidi, reinterpret_cast(&mptd), MIDIPROP_SET|MIDIPROP_TIMEDIV)) != MMSYSERR_NOERROR)
{
return HMP_MM_ERR;
}
reset_tracks(hmp);
setup_tempo(hmp, 0x0f4240);
hmp->stop = 0;
while (hmp->evbuf) {
if ((rc = fill_buffer(hmp))) {
if (rc == HMP_EOF) {
reset_tracks(hmp);
continue;
} else
return rc;
}
if ((rc = midiOutPrepareHeader((HMIDIOUT)hmp->hmidi, hmp->evbuf,
sizeof(MIDIHDR))) != MMSYSERR_NOERROR) {
return HMP_MM_ERR;
}
if ((rc = midiStreamOut(hmp->hmidi, hmp->evbuf,
sizeof(MIDIHDR))) != MMSYSERR_NOERROR) {
return HMP_MM_ERR;
}
hmp->evbuf = hmp->evbuf->lpNext;
hmp->bufs_in_mm++;
}
midiStreamRestart(hmp->hmidi);
return 0;
}
void hmp_pause(hmp_file *hmp)
{
if (hmp)
midiStreamPause(hmp->hmidi);
}
void hmp_resume(hmp_file *hmp)
{
if (hmp)
midiStreamRestart(hmp->hmidi);
}
void hmp_reset()
{
HMIDIOUT hmidi;
MIDIHDR mhdr;
MMRESULT rval;
char GS_Reset[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 };
if ((rval = midiOutOpen(&hmidi, MIDI_MAPPER, 0, 0, 0)) != MMSYSERR_NOERROR)
{
switch (rval)
{
case MIDIERR_NODEVICE:
con_printf(CON_DEBUG, "midiOutOpen Error: no MIDI port was found.");
break;
case MMSYSERR_ALLOCATED:
con_printf(CON_DEBUG, "midiOutOpen Error: specified resource is already allocated.");
break;
case MMSYSERR_BADDEVICEID:
con_printf(CON_DEBUG, "midiOutOpen Error: specified device identifier is out of range.");
break;
case MMSYSERR_INVALPARAM:
con_printf(CON_DEBUG, "midiOutOpen Error: specified pointer or structure is invalid.");
break;
case MMSYSERR_NOMEM:
con_printf(CON_DEBUG, "midiOutOpen Error: unable to allocate or lock memory.");
break;
default:
con_printf(CON_DEBUG, "midiOutOpen Error code %i",rval);
break;
}
return;
}
for (int channel = 0; channel < 16; channel++)
{
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_ALL_SOUNDS_OFF << 8 | 0 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_RESET_ALL_CONTROLLERS << 8 | 0 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_ALL_NOTES_OFF << 8 | 0 << 16));
channel_volume[channel] = 100;
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_PANPOT << 8 | 64 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_REVERB << 8 | 40 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_CHORUS << 8 | 0 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_BANK_SELECT_MSB << 8 | 0 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_BANK_SELECT_LSB << 8 | 0 << 16));
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_PROGRAM_CHANGE << 4 | 0 << 8));
}
mhdr.lpData = GS_Reset;
mhdr.dwBufferLength = sizeof(GS_Reset);
mhdr.dwFlags = 0;
if ((rval = midiOutPrepareHeader(hmidi, &mhdr, sizeof(MIDIHDR))) == MMSYSERR_NOERROR)
{
if ((rval = midiOutLongMsg(hmidi, &mhdr, sizeof(MIDIHDR))) == MMSYSERR_NOERROR)
{
fix64 wait_done = timer_query();
while (!(mhdr.dwFlags & MHDR_DONE))
{
auto timer = timer_update();
if (timer >= wait_done + F1_0)
{
con_printf(CON_DEBUG, "hmp_reset: Timeout waiting for MHDR_DONE");
break;
}
}
}
else
{
switch (rval)
{
case MIDIERR_NOTREADY:
con_printf(CON_DEBUG, "midiOutLongMsg Error: the hardware is busy with other data.");
break;
case MIDIERR_UNPREPARED:
con_printf(CON_DEBUG, "midiOutLongMsg Error: the buffer pointed to by lpMidiOutHdr has not been prepared.");
break;
case MMSYSERR_INVALHANDLE:
con_printf(CON_DEBUG, "midiOutLongMsg Error: the specified device handle is invalid.");
break;
case MMSYSERR_INVALPARAM:
con_printf(CON_DEBUG, "midiOutLongMsg Error: the specified pointer or structure is invalid.");
break;
default:
con_printf(CON_DEBUG, "midiOutLongMsg Error code %i",rval);
break;
}
}
midiOutUnprepareHeader(hmidi, &mhdr, sizeof(MIDIHDR));
timer_delay(F1_0/20);
}
else
{
switch (rval)
{
case MMSYSERR_INVALHANDLE:
con_printf(CON_DEBUG, "midiOutPrepareHeader Error: The specified device handle is invalid.");
break;
case MMSYSERR_INVALPARAM:
con_printf(CON_DEBUG, "midiOutPrepareHeader Error: The specified address is invalid or the given stream buffer is greater than 64K.");
break;
case MMSYSERR_NOMEM:
con_printf(CON_DEBUG, "midiOutPrepareHeader Error: The system is unable to allocate or lock memory.");
break;
default:
con_printf(CON_DEBUG, "midiOutPrepareHeader Error code %i",rval);
break;
}
}
for (int channel = 0; channel < 16; channel++)
midiOutShortMsg(hmidi, (DWORD)(channel | MIDI_CONTROL_CHANGE << 4 | MIDI_VOLUME << 8 | (100 * midi_volume / MIDI_VOLUME_SCALE) << 16));
midiOutClose(hmidi);
}
#endif
// CONVERSION FROM HMP TO MIDI
static void hmptrk2mid(ubyte* data, int size, std::vector &midbuf)
{
ubyte *dptr = data;
ubyte lc1 = 0,last_com = 0;
uint d;
int n1;
while (data < dptr + size)
{
if (data[0] & 0x80) {
ubyte b = (data[0] & 0x7F);
midbuf.emplace_back(b);
}
else {
d = (data[0] & 0x7F);
n1 = 0;
while ((data[n1] & 0x80) == 0) {
n1++;
d += (data[n1] & 0x7F) << (n1 * 7);
}
n1 = 1;
while ((data[n1] & 0x80) == 0) {
n1++;
if (n1 == 4)
throw std::runtime_error("bad HMP");
}
for(int n2 = 0; n2 <= n1; n2++) {
ubyte b = (data[n1 - n2] & 0x7F);
if (n2 != n1)
b |= 0x80;
midbuf.emplace_back(b);
}
data += n1;
}
data++;
if (*data == 0xFF) { //meta?
midbuf.insert(midbuf.end(), data, data + 3 + data[2]);
if (data[1] == 0x2F)
break;
}
else {
lc1=data[0] ;
if ((lc1&0x80) == 0)
throw std::runtime_error("bad HMP");
switch (lc1 & 0xF0) {
case 0x80:
case 0x90:
case 0xA0:
case 0xB0:
case 0xE0:
if (lc1 != last_com)
{
midbuf.emplace_back(lc1);
}
midbuf.insert(midbuf.end(), data + 1, data + 3);
data += 3;
break;
case 0xC0:
case 0xD0:
if (lc1 != last_com)
{
midbuf.emplace_back(lc1);
}
midbuf.emplace_back(data[1]);
data += 2;
break;
default:
throw std::runtime_error("bad HMP");
}
last_com = lc1;
}
}
}
struct be_bytebuffer_t : serial::writer::bytebuffer_t
{
be_bytebuffer_t(pointer u) : bytebuffer_t(u) {}
static uint16_t endian() { return big_endian; }
};
const array magic_header{{'M', 'T', 'h', 'd'}};
const array tempo{{'M','T','r','k',0,0,0,11,0,0xFF,0x51,0x03,0x18,0x80,0x00,0,0xFF,0x2F,0}};
const array track_header{{'M', 'T', 'r', 'k', 0, 0, 0, 0}};
struct midhdr
{
int16_t num_trks;
int16_t time_div;
midhdr(hmp_file *hmp) :
num_trks(hmp->num_trks), time_div(hmp->tempo*1.6)
{
}
};
DEFINE_SERIAL_CONST_UDT_TO_MESSAGE(midhdr, m, (magic_header, static_cast(6), static_cast(1), m.num_trks, m.time_div, tempo));
void hmp2mid(const char *hmp_name, std::vector &midbuf)
{
std::unique_ptr hmp = hmp_open(hmp_name);
if (!hmp)
return;
const midhdr mh(hmp.get());
// write MIDI-header
midbuf.resize(serial::message_type::maximum_size);
be_bytebuffer_t bb(&midbuf[0]);
serial::process_buffer(bb, mh);
// tracks
for (int i = 1; i < hmp->num_trks; i++)
{
midbuf.insert(midbuf.end(), track_header.begin(), track_header.end());
auto size_before = midbuf.size();
auto midtrklenpos = midbuf.size() - 4;
hmptrk2mid(hmp->trks[i].data.get(), hmp->trks[i].len, midbuf);
auto size_after = midbuf.size();
be_bytebuffer_t bbmi(&midbuf[midtrklenpos]);
serial::process_buffer(bbmi, static_cast(size_after - size_before));
}
}
}