From 5b2448c0847481bf6e72cdab0fe5707736f3208c Mon Sep 17 00:00:00 2001 From: Aaron Giles Date: Thu, 27 May 2021 18:26:31 -0700 Subject: midiin: Add support for providing a .mid file as input --- src/devices/imagedev/midiin.cpp | 378 ++++++++++++++++++++++++++++++++++++++-- src/devices/imagedev/midiin.h | 109 +++++++++++- 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(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 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 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 m_list; + std::list::iterator m_iterator; + }; + + midi_sequence m_sequence; + attotime m_sequence_start; }; // device type definition -- cgit v1.2.3