diff options
author | 2021-05-27 18:26:31 -0700 | |
---|---|---|
committer | 2021-05-27 18:26:31 -0700 | |
commit | 5b2448c0847481bf6e72cdab0fe5707736f3208c (patch) | |
tree | add39c567c10b2d17c2e2180745cf1668e15f86c /src/devices/imagedev/midiin.cpp | |
parent | d724ac7c8a3e9d15741ed37991ba37359ed24175 (diff) |
midiin: Add support for providing a .mid file as input
Diffstat (limited to 'src/devices/imagedev/midiin.cpp')
-rw-r--r-- | src/devices/imagedev/midiin.cpp | 378 |
1 files changed, 359 insertions, 19 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; +} |