/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "audio/midiparser.h" #include "audio/mididrv.h" #include "audio/miles.h" #include "common/textconsole.h" #include "common/util.h" /** * The XMIDI version of MidiParser. * * Much of this code is adapted from the XMIDI implementation from the exult * project. */ class MidiParser_XMIDI : public MidiParser { protected: static const uint8 MAXIMUM_TRACK_BRANCHES = 128; struct Loop { byte *pos; byte repeat; }; Loop _loop[4]; int _loopCount; /** * The sequence branches defined for each track. These point to * positions in the MIDI data. */ byte *_trackBranches[MAXIMUM_TRACKS][MAXIMUM_TRACK_BRANCHES]; XMidiCallbackProc _callbackProc; void *_callbackData; // We need to support XMIDI TIMB for 7th guest, which uses // Miles Audio drivers. The MT32 driver needs to get the TIMB chunk, so that it // can install all required timbres before the song starts playing. // This contains a pointer to _driver if it supports the required // interface; otherwise it is null. Audio::MidiDriver_Miles_Xmidi_Timbres *_newTimbreListDriver; byte *_tracksTimbreList[120]; ///< Timbre-List for each track. uint32 _tracksTimbreListSize[120]; ///< Size of the Timbre-List for each track. protected: uint32 readVLQ2(byte * &data); /** * Platform independent LE uint32 read-and-advance. * This helper function reads Little Endian 32-bit numbers * from a memory pointer, at the same time advancing * the pointer. */ uint32 read4low(byte *&data); void parseNextEvent(EventInfo &info) override; void resetTracking() override { MidiParser::resetTracking(); _loopCount = -1; } void onTrackStart(uint8 track) override; public: MidiParser_XMIDI(XMidiCallbackProc proc, void *data, int8 source = -1) : MidiParser(source), _callbackProc(proc), _callbackData(data), _newTimbreListDriver(nullptr), _loopCount(-1) { memset(_loop, 0, sizeof(_loop)); memset(_trackBranches, 0, sizeof(_trackBranches)); memset(_tracksTimbreList, 0, sizeof(_tracksTimbreList)); memset(_tracksTimbreListSize, 0, sizeof(_tracksTimbreListSize)); } ~MidiParser_XMIDI() { stopPlaying(); } void setMidiDriver(MidiDriver_BASE *driver) override; bool loadMusic(byte *data, uint32 size) override; bool hasJumpIndex(uint8 index) override; bool jumpToIndex(uint8 index, bool stopNotes) override; int32 determineDataSize(Common::SeekableReadStream *stream) override; }; // This is a special XMIDI variable length quantity uint32 MidiParser_XMIDI::readVLQ2(byte * &pos) { uint32 value = 0; while (!(pos[0] & 0x80)) { value += *pos++; } return value; } uint32 MidiParser_XMIDI::read4low(byte *&data) { uint32 val = READ_LE_UINT32(data); data += 4; return val; } bool MidiParser_XMIDI::hasJumpIndex(uint8 index) { if (_activeTrack >= _numTracks) return false; return index < MAXIMUM_TRACK_BRANCHES && _trackBranches[_activeTrack][index] != nullptr; } bool MidiParser_XMIDI::jumpToIndex(uint8 index, bool stopNotes) { if (_activeTrack >= _numTracks || _pause) return false; if (index >= MAXIMUM_TRACK_BRANCHES || _trackBranches[_activeTrack][index] == nullptr) { warning("MidiParser-XMIDI: jumpToIndex called with invalid sequence branch index %x", index); return false; } // Prevent concurrent execution of multiple jumps assert(!_jumpingToTick); _jumpingToTick = true; if (stopNotes) { if (!_smartJump || !_position._playPos) { allNotesOff(); } else { hangAllActiveNotes(); } } resetTracking(); _position._playPos = _trackBranches[_activeTrack][index]; parseNextEvent(_nextEvent); _jumpingToTick = false; return true; } void MidiParser_XMIDI::parseNextEvent(EventInfo &info) { info.start = _position._playPos; info.delta = readVLQ2(_position._playPos); info.loop = false; // Process the next event. info.event = *(_position._playPos++); switch (info.event >> 4) { case 0x9: // Note On info.basic.param1 = *(_position._playPos++); info.basic.param2 = *(_position._playPos++); info.length = readVLQ(_position._playPos); if (info.length == 0) { // Notes with length 0 are played with a very short duration by the AIL driver. // However, the MidiParser will treat notes with length 0 as "active notes"; i.e. // they will only get turned off when a corresponding Note Off event is encountered. // Because XMIDI does not contain Note Off events, this will cause the note to hang. // Set length to 1 to prevent this from happening. info.length = 1; } if (info.basic.param2 == 0) { info.event = info.channel() | 0x80; info.length = 0; } break; case 0xC: case 0xD: info.basic.param1 = *(_position._playPos++); info.basic.param2 = 0; break; case 0x8: case 0xA: case 0xE: info.basic.param1 = *(_position._playPos++); info.basic.param2 = *(_position._playPos++); break; case 0xB: info.basic.param1 = *(_position._playPos++); info.basic.param2 = *(_position._playPos++); // This isn't a full XMIDI implementation, but it should // hopefully be "good enough" for most things. switch (info.basic.param1) { // Simplified XMIDI looping. case 0x74: { // XMIDI_CONTROLLER_FOR_LOOP byte *pos = _position._playPos; if (_loopCount < ARRAYSIZE(_loop) - 1) _loopCount++; else warning("XMIDI: Exceeding maximum loop count %d", ARRAYSIZE(_loop)); _loop[_loopCount].pos = pos; _loop[_loopCount].repeat = info.basic.param2; break; } case 0x75: // XMIDI_CONTROLLER_NEXT_BREAK if (_loopCount >= 0) { if (info.basic.param2 < 64) { // End the current loop. _loopCount--; } else { // Repeat 0 means "loop forever". if (_loop[_loopCount].repeat) { if (--_loop[_loopCount].repeat == 0) { _loopCount--; } else { _position._playPos = _loop[_loopCount].pos; info.loop = true; } } else { _position._playPos = _loop[_loopCount].pos; info.loop = true; } } } break; case 0x77: // XMIDI_CONTROLLER_CALLBACK_TRIG if (_callbackProc) _callbackProc(info.basic.param2, _callbackData); break; case 0x78: // XMIDI_CONTROLLER_SEQ_BRANCH_INDEX // This controller marks a branch point. It is converted // to an entry in the RBRN header by the XMIDI conversion // tool. For playback it is unnecessary. break; case 0x6e: // XMIDI_CONTROLLER_CHAN_LOCK case 0x6f: // XMIDI_CONTROLLER_CHAN_LOCK_PROT case 0x70: // XMIDI_CONTROLLER_VOICE_PROT case 0x71: // XMIDI_CONTROLLER_TIMBRE_PROT case 0x72: // XMIDI_CONTROLLER_BANK_CHANGE // These controllers are handled in the Miles drivers break; case 0x73: // XMIDI_CONTROLLER_IND_CTRL_PREFIX case 0x76: // XMIDI_CONTROLLER_CLEAR_BB_COUNT default: if (info.basic.param1 >= 0x73 && info.basic.param1 <= 0x76) { warning("Unsupported XMIDI controller %d (0x%2x)", info.basic.param1, info.basic.param1); } break; } // Should we really keep passing the XMIDI controller events to // the MIDI driver, or should we turn them into some kind of // NOP events? (Dummy meta events, perhaps?) Ah well, it has // worked so far, so it shouldn't cause any damage... break; case 0xF: // Meta or SysEx event switch (info.event & 0x0F) { case 0x2: // Song Position Pointer info.basic.param1 = *(_position._playPos++); info.basic.param2 = *(_position._playPos++); break; case 0x3: // Song Select info.basic.param1 = *(_position._playPos++); info.basic.param2 = 0; break; case 0x6: case 0x8: case 0xA: case 0xB: case 0xC: case 0xE: info.basic.param1 = info.basic.param2 = 0; break; case 0x0: // SysEx info.length = readVLQ(_position._playPos); info.ext.data = _position._playPos; _position._playPos += info.length; break; case 0xF: // META event info.ext.type = *(_position._playPos++); info.length = readVLQ(_position._playPos); info.ext.data = _position._playPos; _position._playPos += info.length; if (info.ext.type == 0x51 && info.length == 3) { // Tempo event. We want to make these constant 500,000. info.ext.data[0] = 0x07; info.ext.data[1] = 0xA1; info.ext.data[2] = 0x20; } break; default: warning("MidiParser_XMIDI::parseNextEvent: Unsupported event code %x", info.event); break; } break; default: break; } } void MidiParser_XMIDI::setMidiDriver(MidiDriver_BASE *driver) { MidiParser::setMidiDriver(driver); _newTimbreListDriver = dynamic_cast(driver); } bool MidiParser_XMIDI::loadMusic(byte *data, uint32 size) { uint32 i = 0; byte *start; uint32 len; uint32 chunkLen; char buf[32]; _loopCount = -1; unloadMusic(); byte *pos = data; if (!memcmp(pos, "FORM", 4)) { pos += 4; // Read length of len = read4high(pos); start = pos; // XDIRless XMIDI, we can handle them here. if (!memcmp(pos, "XMID", 4)) { warning("XMIDI doesn't have XDIR"); pos += 4; _numTracks = 1; } else if (memcmp(pos, "XDIR", 4)) { // Not an XMIDI that we recognize warning("Expected 'XDIR' but found '%c%c%c%c'", pos[0], pos[1], pos[2], pos[3]); return false; } else { // Seems Valid pos += 4; _numTracks = 0; for (i = 4; i < len; i++) { // Read 4 bytes of type memcpy(buf, pos, 4); pos += 4; // Read length of chunk chunkLen = read4high(pos); // Add eight bytes i += 8; if (memcmp(buf, "INFO", 4) == 0) { // Must be at least 2 bytes long if (chunkLen < 2) { warning("Invalid chunk length %d for 'INFO' block", (int)chunkLen); return false; } _numTracks = (byte)read2low(pos); if (chunkLen > 2) { warning("Chunk length %d is greater than 2", (int)chunkLen); //pos += chunkLen - 2; } break; } // Must align pos += (chunkLen + 1) & ~1; i += (chunkLen + 1) & ~1; } // Didn't get to fill the header if (_numTracks == 0) { warning("Didn't find a valid track count"); return false; } // Ok now to start part 2 // Goto the right place pos = start + ((len + 1) & ~1); if (memcmp(pos, "CAT ", 4)) { // Not an XMID warning("Expected 'CAT ' but found '%c%c%c%c'", pos[0], pos[1], pos[2], pos[3]); return false; } pos += 4; // Now read length of this track len = read4high(pos); if (memcmp(pos, "XMID", 4)) { // Not an XMID warning("Expected 'XMID' but found '%c%c%c%c'", pos[0], pos[1], pos[2], pos[3]); return false; } pos += 4; } // Ok it's an XMIDI. // We're going to identify and store the location for each track. if (_numTracks > ARRAYSIZE(_tracks)) { warning("Can only handle %d tracks but was handed %d", (int)ARRAYSIZE(_tracks), (int)_numTracks); return false; } int tracksRead = 0; uint32 branchOffsets[128]; memset(branchOffsets, 0, sizeof(branchOffsets)); memset(_trackBranches, 0, sizeof(_trackBranches)); memset(_tracksTimbreList, 0, sizeof(_tracksTimbreList)); memset(_tracksTimbreListSize, 0, sizeof(_tracksTimbreListSize)); while (tracksRead < _numTracks) { if (!memcmp(pos, "FORM", 4)) { // Skip this plus the 4 bytes after it. pos += 8; } else if (!memcmp(pos, "XMID", 4)) { // Skip this. pos += 4; } else if (!memcmp(pos, "TIMB", 4)) { // Custom timbres // chunk data is as follows: // UINT16LE timbre count (amount of custom timbres used by this track) // BYTE patchId // BYTE bankId // * timbre count pos += 4; len = read4high(pos); _tracksTimbreList[tracksRead] = pos; // Skip the length bytes _tracksTimbreListSize[tracksRead] = len; pos += (len + 1) & ~1; } else if (!memcmp(pos, "EVNT", 4)) { // Ahh! What we're looking for at last. _tracks[tracksRead] = pos + 8; // Skip the EVNT and length bytes pos += 4; len = read4high(pos); pos += (len + 1) & ~1; // Calculate branch index positions using the track position we just found for (int j = 0; j < MAXIMUM_TRACK_BRANCHES; ++j) { if (branchOffsets[j] != 0) { byte *branchPos = _tracks[tracksRead] + branchOffsets[j]; if (branchPos >= pos) { warning("Invalid sequence branch position (after track end)"); branchPos = _tracks[tracksRead]; } _trackBranches[tracksRead][j] = branchPos; } } // Clear the branch offsets for the next track memset(branchOffsets, 0, sizeof(branchOffsets)); ++tracksRead; } else if (!memcmp(pos, "RBRN", 4)) { // optional branch point offsets pos += 4; len = read4high(pos); uint16 numBranches = (len - 2) / 6; uint16 numBranches2 = read2low(pos); if (numBranches != numBranches2) { warning("Number of sequence branch definitions %d does not match RBRN block length %d", numBranches2, len); numBranches = 0; } for (int j = 0; j < numBranches; ++j) { uint16 index = read2low(pos); if (index >= MAXIMUM_TRACK_BRANCHES) { warning("Invalid sequence branch index value %x", index); pos += 4; continue; } // This is the offset from the start of the track branchOffsets[index] = read4low(pos); } } else { warning("Hit invalid block '%c%c%c%c' while scanning for track locations", pos[0], pos[1], pos[2], pos[3]); return false; } } // If we got this far, we successfully established // the locations for each of our tracks. // Note that we assume the original data passed in // will persist beyond this call, i.e. we do NOT // copy the data to our own buffer. Take warning.... _ppqn = 60; resetTracking(); setTempo(500000); // Start playback of the first track. setTrack(0); return true; } return false; } int32 MidiParser_XMIDI::determineDataSize(Common::SeekableReadStream *stream) { int32 length = 0; byte buf[4]; Common::fill(buf, buf + 4, 0); // Read FourCC. stream->read(buf, 4); if (!memcmp(buf, "FORM", 4)) { // Optional XDIR header. // Skip over the header. uint32 headerLength = stream->readUint32BE(); stream->seek(headerLength, SEEK_CUR); // Read next FourCC. Common::fill(buf, buf + 4, 0); stream->read(buf, 4); // Add header length to total length. length += 8; length += headerLength; } if (!memcmp(buf, "CAT ", 4)) { // CAT chunk. uint32 catLength = stream->readUint32BE(); // Add catalog chunk length to total length. length += 8; length += catLength; } else { // XMIDI files must have a CAT chunk. warning("Expected FORM or CAT but found '%c%c%c%c' instead", buf[0], buf[1], buf[2], buf[3]); return -1; } return length; } void MidiParser_XMIDI::onTrackStart(uint8 track) { // Load custom timbres if (_newTimbreListDriver && _tracksTimbreListSize[track] > 0) _newTimbreListDriver->processXMIDITimbreChunk(_tracksTimbreList[track], _tracksTimbreListSize[track]); } void MidiParser::defaultXMidiCallback(byte eventData, void *data) { warning("MidiParser: defaultXMidiCallback(%d)", eventData); } MidiParser *MidiParser::createParser_XMIDI(XMidiCallbackProc proc, void *data, int source) { return new MidiParser_XMIDI(proc, data, source); }