summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
author Aaron Giles <aaron@aarongiles.com>2021-05-27 18:26:31 -0700
committer Aaron Giles <aaron@aarongiles.com>2021-05-27 18:26:31 -0700
commit5b2448c0847481bf6e72cdab0fe5707736f3208c (patch)
treeadd39c567c10b2d17c2e2180745cf1668e15f86c
parentd724ac7c8a3e9d15741ed37991ba37359ed24175 (diff)
midiin: Add support for providing a .mid file as input
-rw-r--r--src/devices/imagedev/midiin.cpp378
-rw-r--r--src/devices/imagedev/midiin.h109
2 files changed, 467 insertions, 20 deletions
diff --git a/src/devices/imagedev/midiin.cpp b/src/devices/imagedev/midiin.cpp
index 62c7cba52a1..54a9f1c2297 100644
--- a/src/devices/imagedev/midiin.cpp
+++ b/src/devices/imagedev/midiin.cpp
@@ -1,5 +1,5 @@
// license:BSD-3-Clause
-// copyright-holders:R. Belmont
+// copyright-holders:R. Belmont,Aaron Giles
/*********************************************************************
midiin.c
@@ -64,23 +64,57 @@ void midiin_device::device_reset()
void midiin_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr)
{
- if (!id) {
- uint8_t buf[8192*4];
- int bytesRead;
+ if (id == 0)
+ {
+ // if there's a sequence playing, that takes priority
+ midi_event *event = m_sequence.current_event();
+ if (event != nullptr)
+ {
+ attotime curtime = timer.expire();
+ if (curtime < m_sequence_start)
+ {
+ // we could get called before we're supposed to start; show a countdown
+ attotime delta = m_sequence_start - curtime;
+ popmessage("Waiting to start MIDI playback... %d", delta.seconds());
+ m_timer->adjust(std::min(delta, attotime(1, 0)));
+ }
+ else
+ {
+ // update the playback time
+ curtime -= m_sequence_start;
+ popmessage("Playing MIDI file: %d:%02d / %d:%02d", curtime.seconds() / 60, curtime.seconds() % 60, m_sequence.duration().seconds() / 60, m_sequence.duration().seconds() % 60);
- if (!m_midi) {
- return;
- }
+ // if it's time to process the current event, do it and advance
+ if (curtime >= event->time())
+ {
+ for (auto &curbyte : event->data())
+ xmit_char(curbyte);
+ event = m_sequence.advance_event();
+ }
- while (m_midi->poll())
+ // if there are more events, set a timer to trigger them
+ // (minimum duration 1 sec so that our playback time doesn't skip)
+ if (event != nullptr)
+ m_timer->adjust(std::min(event->time() - curtime, attotime(1, 0)));
+ else
+ popmessage("End of MIDI file");
+ }
+ }
+ else if (m_midi)
{
- bytesRead = m_midi->read(buf);
+ uint8_t buf[8192*4];
+ int bytesRead;
- if (bytesRead > 0)
+ while (m_midi->poll())
{
- for (int i = 0; i < bytesRead; i++)
+ bytesRead = m_midi->read(buf);
+
+ if (bytesRead > 0)
{
- xmit_char(buf[i]);
+ for (int i = 0; i < bytesRead; i++)
+ {
+ xmit_char(buf[i]);
+ }
}
}
}
@@ -93,17 +127,34 @@ void midiin_device::device_timer(emu_timer &timer, device_timer_id id, int param
image_init_result midiin_device::call_load()
{
- m_midi = machine().osd().create_midi_device();
-
- if (!m_midi->open_input(filename()))
+ // attempt to load if it's a real file
+ m_err = load_image_by_path(OPEN_FLAG_READ, filename());
+ if (m_err == IMAGE_ERROR_SUCCESS)
{
- m_midi.reset();
+ // if the parsing succeeds, schedule the start to happen at least
+ // 10 seconds after starting to allow the keyboards to initialize
+ // TODO: this should perhaps be a driver-configurable parameter?
+ if (m_sequence.parse(reinterpret_cast<u8 *>(ptr()), length()))
+ {
+ m_sequence_start = std::max(machine().time(), attotime(10, 0));
+ m_timer->adjust(attotime::zero);
+ return image_init_result::PASS;
+ }
return image_init_result::FAIL;
}
+ else
+ {
+ m_midi = machine().osd().create_midi_device();
+
+ if (!m_midi->open_input(filename()))
+ {
+ m_midi.reset();
+ return image_init_result::FAIL;
+ }
- m_timer->adjust(attotime::from_hz(1500), 0, attotime::from_hz(1500));
- m_timer->enable(true);
- return image_init_result::PASS;
+ m_timer->adjust(attotime::from_hz(1500), 0, attotime::from_hz(1500));
+ return image_init_result::PASS;
+ }
}
/*-------------------------------------------------
@@ -117,6 +168,7 @@ void midiin_device::call_unload()
m_midi->close();
}
m_midi.reset();
+ m_sequence.clear();
m_timer->enable(false);
}
@@ -165,3 +217,291 @@ void midiin_device::xmit_char(uint8_t data)
}
}
}
+
+
+//-------------------------------------------------
+// fourcc_le - helper to compute the little-endian
+// version of a fourcc value from a string
+//-------------------------------------------------
+
+static constexpr u32 fourcc_le(char const *string)
+{
+ return string[0] | (string[1] << 8) | (string[2] << 16) | (string[3] << 24);
+}
+
+
+//-------------------------------------------------
+// midi_parser - constructor
+//-------------------------------------------------
+
+midiin_device::midi_parser::midi_parser(u8 const *data, u32 length, u32 offset) :
+ m_data(data),
+ m_length(length),
+ m_offset(offset)
+{
+}
+
+
+//-------------------------------------------------
+// subset - construct a midi_parser that
+// represents a subset of the buffer, and advance
+// our offset past it
+//-------------------------------------------------
+
+midiin_device::midi_parser midiin_device::midi_parser::subset(u32 length)
+{
+ check_bounds(length);
+ midi_parser result(m_data + m_offset, length, 0);
+ m_offset += length;
+ return result;
+}
+
+
+//-------------------------------------------------
+// rewind - back up by the given number of bytes
+//-------------------------------------------------
+
+midiin_device::midi_parser &midiin_device::midi_parser::rewind(u32 count)
+{
+ count = std::min(count, m_offset);
+ m_offset -= count;
+ return *this;
+}
+
+
+//-------------------------------------------------
+// variable - return the MIDI standard "variable"
+// value
+//-------------------------------------------------
+
+u32 midiin_device::midi_parser::variable()
+{
+ u32 result = 0;
+ for (int which = 0; which < 4; which++)
+ {
+ u8 curbyte = byte();
+ result = (result << 7) | (curbyte & 0x7f);
+ if ((curbyte & 0x80) == 0)
+ return result;
+ }
+ throw error("Invalid variable length field");
+}
+
+
+//-------------------------------------------------
+// check_bounds - check to see if we have at least
+// 'length' bytes left to consume; if not,
+// throw an error
+//-------------------------------------------------
+
+void midiin_device::midi_parser::check_bounds(u32 length)
+{
+ if (m_offset + length > m_length)
+ throw error("Out of bounds error");
+}
+
+
+//-------------------------------------------------
+// event_at - return a reference to the sequence
+// event at the given tick, or create a new one
+// if one doesn't yet exist
+//-------------------------------------------------
+
+midiin_device::midi_event &midiin_device::midi_sequence::event_at(u32 tick)
+{
+ for (auto it = m_list.begin(); it != m_list.end(); ++it)
+ {
+ if (it->tick() == tick)
+ return *it;
+ if (it->tick() > tick)
+ return *m_list.emplace(it, tick);
+ }
+ m_list.emplace_back(tick);
+ return m_list.back();
+}
+
+
+//-------------------------------------------------
+// parse - parse a MIDI sequence from a buffer
+//-------------------------------------------------
+
+bool midiin_device::midi_sequence::parse(u8 const *data, u32 length)
+{
+ // start with an empty list of events
+ m_list.clear();
+
+ // by default parse the whole data
+ midi_parser buffer(data, length, 0);
+
+ // catch errors to make parsing easier
+ try
+ {
+ // if not a RIFF-encoed MIDI, just parse as-is
+ if (buffer.dword_le() != fourcc_le("RIFF"))
+ parse_midi_data(buffer.reset());
+ else
+ {
+ // check the RIFF type and size
+ u32 riffsize = buffer.dword_le();
+ u32 rifftype = buffer.dword_le();
+ if (rifftype != fourcc_le("RMID"))
+ throw midi_parser::error("Input RIFF file is not of type RMID");
+
+ // loop over RIFF chunks
+ midi_parser riffdata = buffer.subset(riffsize - 4);
+ while (!riffdata.eob())
+ {
+ u32 chunktype = riffdata.dword_le();
+ u32 chunksize = riffdata.dword_le();
+ midi_parser chunk = riffdata.subset(chunksize);
+ if (chunktype == fourcc_le("data"))
+ {
+ parse_midi_data(chunk);
+ break;
+ }
+ }
+ }
+ m_iterator = m_list.begin();
+ return true;
+ }
+ catch (midi_parser::error &)
+ {
+ m_list.clear();
+ m_iterator = m_list.begin();
+ return false;
+ }
+}
+
+
+//-------------------------------------------------
+// parse_midi_data - parse the core MIDI format
+// into tracks
+//-------------------------------------------------
+
+void midiin_device::midi_sequence::parse_midi_data(midi_parser &buffer)
+{
+ // scan for syntactic correctness, and to find global state
+ u32 headertype = buffer.dword_le();
+ if (headertype != fourcc_le("MThd"))
+ throw midi_parser::error("Input file is not a MIDI file.");
+ if (buffer.dword_be() != 0x00000006)
+ throw midi_parser::error("Invalid MIDI file header.");
+
+ // parse format info
+ int format = buffer.word_be();
+ if (format > 2)
+ throw midi_parser::error("Invalid MIDI file header.");
+
+ // parse track count
+ u16 tracks = buffer.word_be();
+ if (format == 0 && tracks != 1)
+ throw midi_parser::error("MIDI format 0 expects exactly one track.");
+
+ // parse time divisor
+ u16 timediv = buffer.word_be();
+ if ((timediv & 0x8000) != 0)
+ throw midi_parser::error("SMPTE timecode time division not supported.");
+ if (timediv == 0)
+ throw midi_parser::error("Invalid time divisor of 0.");
+
+ // iterate over tracks
+ u32 curtick = 0;
+ for (u16 index = 0; index < tracks; index++)
+ {
+ // verify header
+ if (buffer.dword_le() != fourcc_le("MTrk"))
+ throw midi_parser::error("Invalid MIDI track header.");
+ u32 chunksize = buffer.dword_be();
+
+ // parse the track data
+ midi_parser trackdata = buffer.subset(chunksize);
+ u32 numticks = parse_track_data(trackdata, curtick);
+ if (format == 2)
+ curtick += numticks;
+ }
+
+ // now go through the event list and compute times
+ u32 lasttick = 0;
+ attotime ticktime = attotime::from_usec(1000000) / timediv;
+ attotime curtime;
+ for (auto &event : m_list)
+ {
+ // update the elapsed time
+ u32 curtick = event.tick();
+ curtime += ticktime * (curtick - lasttick);
+ lasttick = curtick;
+
+ // determine if we have a new tempo here before replacing the time
+ if (!event.time().is_zero())
+ ticktime = event.time() / timediv;
+ event.set_time(curtime);
+ }
+}
+
+
+//-------------------------------------------------
+// parse_track_data - parse data from a track and
+// add it to the buffer
+//-------------------------------------------------
+
+u32 midiin_device::midi_sequence::parse_track_data(midi_parser &buffer, u32 start_tick)
+{
+ u32 curtick = start_tick;
+ u8 last_type = 0;
+ while (!buffer.eob())
+ {
+ // parse the time delta
+ curtick += buffer.variable();
+ midi_event &event = event_at(curtick);
+
+ // handle running status
+ u8 type = buffer.byte();
+ if (BIT(type, 7) != 0)
+ last_type = type;
+ else
+ {
+ type = last_type;
+ buffer.rewind(1);
+ }
+
+ // determine the event class
+ uint8_t eclass = type >> 4;
+ if (eclass != 15)
+ {
+ // simple events: all but program change and aftertouch have a second parameter
+ // TODO: should we respect the channel for these? or maybe the drivers should
+ // configure us with the number of channels they support?
+ event.append(type & 0xf0);
+ event.append(buffer.byte());
+ if (eclass != 12 && eclass != 13)
+ event.append(buffer.byte());
+ }
+ else if (type != 0xff)
+ {
+ // handle non-meta events
+ midi_parser eventdata = buffer.subset(buffer.variable());
+ while (!eventdata.eob())
+ event.append(eventdata.byte());
+ }
+ else
+ {
+ // handle meta-events
+ u8 type = buffer.byte();
+ midi_parser eventdata = buffer.subset(buffer.variable());
+
+ // end of data?
+ if (type == 0x2f)
+ break;
+
+ // only care about tempo events; set the "time" to the new tick
+ // value; we will sweep this later and compute actual times
+ if (type == 0x51)
+ {
+ u32 usec_per_quarter = eventdata.triple_be();
+ if (usec_per_quarter != 0)
+ event.set_time(attotime::from_usec(usec_per_quarter));
+ }
+ }
+ }
+ return curtick;
+}
diff --git a/src/devices/imagedev/midiin.h b/src/devices/imagedev/midiin.h
index ef8e9e315b0..28a0826fc0b 100644
--- a/src/devices/imagedev/midiin.h
+++ b/src/devices/imagedev/midiin.h
@@ -1,5 +1,5 @@
// license:BSD-3-Clause
-// copyright-holders:R. Belmont
+// copyright-holders:R. Belmont,Aaron Giles
/*********************************************************************
midiin.h
@@ -66,6 +66,113 @@ private:
uint8_t m_xmitring[XMIT_RING_SIZE];
int m_xmit_read, m_xmit_write;
bool m_tx_busy;
+
+ // internal helper class for parsing
+ class midi_parser
+ {
+ public:
+ // minimal error class
+ class error
+ {
+ public:
+ error(char const *description) : m_description(description) { }
+ char const *description() const { return m_description; }
+ private:
+ char const *m_description;
+ };
+
+ // construction
+ midi_parser(u8 const *data, u32 length, u32 offset);
+
+ // end of buffer?
+ bool eob() const { return (m_offset >= m_length); }
+
+ // create a subset
+ midi_parser subset(u32 length);
+
+ // change position within buffer
+ midi_parser &rewind(u32 count);
+ midi_parser &reset() { return rewind(m_offset); }
+
+ // read data of various sizes and endiannesses
+ u8 byte() { check_bounds(1); return m_data[m_offset++]; }
+ u16 word_be() { u16 result = byte() << 8; return result | byte(); }
+ u32 triple_be() { u32 result = word_be() << 8; return result | byte(); }
+ u32 dword_be() { u32 result = word_be() << 16; return result | word_be(); }
+ u32 dword_le() { return swapendian_int32(dword_be()); }
+
+ // special variable reader for MIDI
+ u32 variable();
+
+ private:
+ // internal helper
+ void check_bounds(u32 length);
+
+ // internal state
+ u8 const *m_data;
+ u32 m_length;
+ u32 m_offset;
+ };
+
+ // internal helper class reperesenting an event at a given
+ // time containing MIDI data
+ class midi_event
+ {
+ public:
+ // constructor
+ midi_event(u32 tick) :
+ m_tick(tick) { }
+
+ // simple getters
+ u32 tick() const { return m_tick; }
+ attotime const &time() const { return m_time; }
+ std::vector<u8> const &data() const { return m_data; }
+
+ // simple setters
+ void set_time(attotime const &time) { m_time = time; }
+
+ // append data to the buffer
+ midi_event &append(u8 byte) { m_data.push_back(byte); return *this; }
+
+ private:
+ // internal state
+ u32 m_tick;
+ attotime m_time;
+ std::vector<u8> m_data;
+ };
+
+ // internal helper class representing a MIDI sequence
+ class midi_sequence
+ {
+ public:
+ // constructor
+ midi_sequence() : m_iterator(m_list.begin()) { }
+
+ // clear the sequence
+ void clear() { m_list.clear(); }
+
+ // parse a new sequence
+ bool parse(u8 const *data, u32 length);
+
+ // rewind to the start of time
+ void rewind(attotime const &basetime);
+ midi_event *current_event() const { return (m_iterator == m_list.end()) ? nullptr : &(*m_iterator); }
+ midi_event *advance_event() { ++m_iterator; return current_event(); }
+ attotime const &duration() { return m_list.back().time(); }
+
+ private:
+ // internal helpers
+ midi_event &event_at(u32 tick);
+ u32 parse_track_data(midi_parser &buffer, u32 start_tick);
+ void parse_midi_data(midi_parser &buffer);
+
+ // internal state
+ std::list<midi_event> m_list;
+ std::list<midi_event>::iterator m_iterator;
+ };
+
+ midi_sequence m_sequence;
+ attotime m_sequence_start;
};
// device type definition