/* 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 "midiparser_m.h" #include "audio/mididrv.h" #include "audio/midiparser.h" namespace Ultima { namespace Nuvie { MidiParser_M::MidiParser_M(int8 source) : MidiParser(source) { // M uses a fixed timer frequency of 60 Hz, or 16.667 ms per tick. _psecPerTick = 16667; _trackLength = 0; _loopPoint = nullptr; _loopStack = new Common::FixedStack(); } MidiParser_M::~MidiParser_M() { delete _loopStack; } bool MidiParser_M::loadMusic(const byte* data, uint32 size) { unloadMusic(); // M uses only 1 track. _tracks[0][0] = data; _numTracks = 1; _numSubtracks[0] = 1; _trackLength = size; // The global loop defaults to the start of the M data. _loopPoint = data; resetTracking(); setTrack(0); return true; } void MidiParser_M::unloadMusic() { MidiParser::unloadMusic(); _trackLength = 0; _loopPoint = nullptr; _loopStack->clear(); } // MidiParser::onTimer does some checks based on MIDI note on/off command bytes // which have a different meaning in M, so those checks are removed here. void MidiParser_M::onTimer() { uint32 endTime; uint32 eventTime; uint32 eventTick; if (!_position.isTracking() || !_driver || !_doParse || _pause || !_driver->isReady(_source)) return; _abortParse = false; endTime = _position._playTime + _timerRate; bool loopEvent = false; while (!_abortParse) { EventInfo &info = *_nextEvent; eventTick = _position._subtracks[0]._lastEventTick + info.delta; eventTime = _position._lastEventTime + (eventTick - _position._lastEventTick) * _psecPerTick; if (eventTime > endTime) break; if (!info.noop) { // Process the next info. bool ret = processEvent(info); if (!ret) return; } loopEvent |= info.loop; if (!_abortParse) { _position._playTime = eventTime; _position._lastEventTime = eventTime; _position._subtracks[0]._lastEventTime = eventTime; _position._playTick = eventTick; _position._lastEventTick = eventTick; _position._subtracks[0]._lastEventTick = eventTick; if (_position.isTracking(0)) { parseNextEvent(_nextSubtrackEvents[0]); } determineNextEvent(); } } if (!_abortParse) { _position._playTime = endTime; _position._playTick = (endTime - _position._lastEventTime) / _psecPerTick + _position._lastEventTick; if (loopEvent) { // One of the processed events has looped (part of) the MIDI data. // Infinite looping will cause the tracker to overflow eventually. // Reset the tracker positions to prevent this from happening. rebaseTracking(); } } } bool MidiParser_M::processEvent(const EventInfo& info, bool fireEvents) { const byte *playPos = _position._subtracks[0]._playPos; if (info.command() == 0x8 && info.channel() == 0x1) { // Call subroutine LoopData loopData { }; loopData.returnPos = playPos; loopData.numLoops = info.ext.data[0]; uint16 startOffset = READ_LE_UINT16(info.ext.data + 1); assert(startOffset < _trackLength); loopData.startPos = _tracks[0][0] + startOffset; _loopStack->push(loopData); playPos = loopData.startPos; } else if (info.command() == 0xE) { // Set loop point _loopPoint = playPos; } else if (info.command() == 0xF) { // Return if (_loopStack->empty()) { // Global loop: return to the global loop point playPos = _loopPoint; } else { // Subroutine loop LoopData *loopData = &_loopStack->top(); if (loopData->numLoops > 1) { // Return to the start of the subroutine data loopData->numLoops--; playPos = loopData->startPos; } else { // Return to the call position playPos = loopData->returnPos; _loopStack->pop(); } } } else if (info.command() == 0x8 && info.channel() == 0x3) { // Load instrument if (fireEvents) { // Send the instrument data as a meta event sendMetaEventToDriver(info.ext.type, info.ext.data, (uint16)info.length); } } else if (fireEvents) { // Other events are handled by the driver sendToDriver(info.event, info.basic.param1, info.basic.param2); } _position._subtracks[0]._playPos = playPos; return true; } void MidiParser_M::parseNextEvent(EventInfo &info) { const byte *playPos = _position._subtracks[0]._playPos; assert(playPos >= _tracks[0][0]); assert(playPos - _tracks[0][0] < (int)_trackLength); info.start = playPos; info.event = *(playPos++); info.delta = 0; info.basic.param1 = 0; info.basic.param2 = 0; info.noop = false; info.loop = false; switch (info.command()) { case 0x0: // Note off case 0x1: // Note on case 0x2: // Set pitch case 0x3: // Set level case 0x4: // Set modulation case 0x5: // Set slide case 0x6: // Set vibrato case 0x7: // Program change // These commands all have 1 data byte. info.basic.param1 = *(playPos++); break; case 0x8: // Subcommand switch (info.channel()) { case 0x1: // Call subroutine // This command specifies the number of loops (1 byte) and an // offset in the M data to jump to (2 bytes). info.ext.type = info.channel(); info.length = 3; info.ext.data = playPos; playPos += info.length; break; case 0x2: // Delay // This command is used to specify a delta time between the // previous and the next event. It does nothing otherwise. info.delta = *(playPos++); info.noop = true; break; case 0x3: // Load instrument // This command specifies the instrument bank slot in which the // instrument should be loaded (1 byte) plus an OPL instrument // definition (11 bytes). info.ext.type = info.channel(); info.length = 12; info.ext.data = playPos; playPos += info.length; break; case 0x5: // Fade out case 0x6: // Fade in // These commands have 1 data byte. info.basic.param1 = *(playPos++); break; default: info.noop = true; break; } break; case 0xE: // Set loop point // This command does not have any data bytes. break; case 0xF: // Return // This command does not have any data bytes. info.loop = true; break; default: info.noop = true; break; } _position._subtracks[0]._playPos = playPos; } void MidiParser_M::allNotesOff() { if (_driver) { _driver->stopAllNotes(); } } } // End of namespace Nuvie } // End of namespace Ultima