Files
scummvm-cursorfix/audio/midiparser.cpp
2026-02-02 04:50:13 +01:00

755 lines
21 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "audio/midiparser.h"
#include "audio/mididrv.h"
#include "common/textconsole.h"
#include "common/util.h"
//////////////////////////////////////////////////
//
// MidiParser implementation
//
//////////////////////////////////////////////////
MidiParser::MidiParser(int8 source) :
_source(source),
_hangingNotesCount(0),
_driver(nullptr),
_timerRate(0x4A0000),
_ppqn(96),
_tempo(500000),
_psecPerTick(5208), // 500000 / 96
_sysExDelay(0),
_autoLoop(false),
_smartJump(false),
_centerPitchWheelOnUnload(false),
_sendSustainOffOnNotesOff(false),
_disableAllNotesOffMidiEvents(false),
_disableAutoStartPlayback(false),
_loopStartPoint(0xFFFFFFFF),
_loopEndPoint(0xFFFFFFFF),
_loopStartPointMs(0xFFFFFFFF),
_loopEndPointMs(0xFFFFFFFF),
_numTracks(0),
_activeTrack(255),
_abortParse(false),
_jumpingToTick(false),
_doParse(true),
_pause(false) {
memset(_activeNotes, 0, sizeof(_activeNotes));
memset(_tracks, 0, sizeof(_tracks));
memset(_numSubtracks, 1, sizeof(_numSubtracks));
for (int i = 0; i < MAXIMUM_SUBTRACKS; i++) {
_nextSubtrackEvents[i].clear();
_nextSubtrackEvents[i].subtrack = i;
}
_nextEvent = &_nextSubtrackEvents[0];
}
void MidiParser::property(int prop, int value) {
switch (prop) {
case mpAutoLoop:
_autoLoop = (value != 0);
break;
case mpSmartJump:
_smartJump = (value != 0);
break;
case mpCenterPitchWheelOnUnload:
_centerPitchWheelOnUnload = (value != 0);
break;
case mpSendSustainOffOnNotesOff:
_sendSustainOffOnNotesOff = (value != 0);
break;
case mpDisableAllNotesOffMidiEvents:
_disableAllNotesOffMidiEvents = (value != 0);
break;
case mpDisableAutoStartPlayback:
_disableAutoStartPlayback = (value != 0);
break;
default:
break;
}
}
void MidiParser::sendToDriver(uint32 b) {
if (_source < 0) {
_driver->send(b);
} else {
_driver->send(_source, b);
}
}
void MidiParser::sendMetaEventToDriver(byte type, const byte *data, uint16 length) {
if (_source < 0) {
_driver->metaEvent(type, data, length);
} else {
_driver->metaEvent(_source, type, data, length);
}
}
void MidiParser::setTempo(uint32 tempo) {
_tempo = tempo;
if (_ppqn)
_psecPerTick = (tempo + (_ppqn >> 2)) / _ppqn;
}
// This is the conventional (i.e. SMF) variable length quantity
uint32 MidiParser::readVLQ(const byte * &data) {
byte str;
uint32 value = 0;
int i;
for (i = 0; i < 4; ++i) {
str = data[0];
++data;
value = (value << 7) | (str & 0x7F);
if (!(str & 0x80))
break;
}
return value;
}
void MidiParser::activeNote(byte channel, byte note, bool active) {
if (note >= 128 || channel >= 16)
return;
if (active)
_activeNotes[note] |= (1 << channel);
else
_activeNotes[note] &= ~(1 << channel);
// See if there are hanging notes that we can cancel
NoteTimer *ptr = _hangingNotes;
int i;
for (i = ARRAYSIZE(_hangingNotes); i; --i, ++ptr) {
if (ptr->channel == channel && ptr->note == note && ptr->timeLeft) {
ptr->timeLeft = 0;
--_hangingNotesCount;
break;
}
}
}
void MidiParser::hangingNote(byte channel, byte note, uint32 timeLeft, bool recycle) {
NoteTimer *best = nullptr;
NoteTimer *ptr = _hangingNotes;
int i;
if (_hangingNotesCount >= ARRAYSIZE(_hangingNotes)) {
warning("MidiParser::hangingNote(): Exceeded polyphony");
return;
}
for (i = ARRAYSIZE(_hangingNotes); i; --i, ++ptr) {
if (ptr->channel == channel && ptr->note == note) {
if (ptr->timeLeft && ptr->timeLeft < timeLeft && recycle)
return;
best = ptr;
if (ptr->timeLeft) {
if (recycle)
sendToDriver(0x80 | channel, note, 0);
--_hangingNotesCount;
}
break;
} else if (!best && ptr->timeLeft == 0) {
best = ptr;
}
}
// Occasionally we might get a zero or negative note
// length, if the note should be turned on and off in
// the same iteration. For now just set it to 1 and
// we'll turn it off in the next cycle.
if (!timeLeft || timeLeft & 0x80000000)
timeLeft = 1;
if (best) {
best->channel = channel;
best->note = note;
best->timeLeft = timeLeft;
++_hangingNotesCount;
} else {
// We checked this up top. We should never get here!
warning("MidiParser::hangingNote(): Internal error");
}
}
void MidiParser::onTimer() {
uint32 endTime;
uint32 eventTime;
uint32 eventTick;
// The SysEx delay can be decreased whenever time passes,
// even if the parser does not parse events.
_sysExDelay -= (_sysExDelay > _timerRate) ? _timerRate : _sysExDelay;
if (!_position.isTracking() || !_driver || !_doParse || _pause || !_driver->isReady(_source))
return;
_abortParse = false;
endTime = _position._playTime + _timerRate;
// Scan our hanging notes for any
// that should be turned off.
if (_hangingNotesCount) {
NoteTimer *ptr = &_hangingNotes[0];
int i;
for (i = ARRAYSIZE(_hangingNotes); i; --i, ++ptr) {
if (ptr->timeLeft) {
if (ptr->timeLeft <= _timerRate) {
sendToDriver(0x80 | ptr->channel, ptr->note, 0);
ptr->timeLeft = 0;
--_hangingNotesCount;
} else {
ptr->timeLeft -= _timerRate;
}
}
}
}
bool loopEvent = false;
while (!_abortParse) {
EventInfo &info = *_nextEvent;
uint8 subtrack = info.subtrack;
eventTick = _position._subtracks[subtrack]._lastEventTick + info.delta;
eventTime = _position._lastEventTime + (eventTick - _position._lastEventTick) * _psecPerTick;
if (_loopStartPoint == 0xFFFFFFFF && _loopStartPointMs != 0xFFFFFFFF && eventTime >= _loopStartPointMs) {
_loopStartPoint = eventTick;
}
if (_loopStartPoint != 0xFFFFFFFF && _loopEndPoint != 0) {
uint32 endTick = (endTime - _position._lastEventTime) / _psecPerTick + _position._lastEventTick;
if ((_loopEndPoint != 0xFFFFFFFF && eventTick > _loopEndPoint && endTick > _loopEndPoint) ||
(eventTime > _loopEndPointMs && endTime > _loopEndPointMs)) {
// Loop
jumpToTick(_loopStartPoint);
_abortParse = true;
break;
}
}
if (eventTime > endTime)
break;
if (!info.noop) {
// Process the next info.
if (info.event < 0x80) {
warning("Bad command or running status %02X", info.event);
_position.stopTracking();
return;
}
if (info.command() == 0x8) {
activeNote(info.channel(), info.basic.param1, false);
} else if (info.command() == 0x9) {
if (info.length > 0)
hangingNote(info.channel(), info.basic.param1, info.length * _psecPerTick - (endTime - eventTime));
else
activeNote(info.channel(), info.basic.param1, true);
}
// Player::metaEvent() in SCUMM will delete the parser object,
// so return immediately if that might have happened.
bool ret = processEvent(info);
if (!ret)
return;
}
loopEvent |= info.loop;
if (!_abortParse) {
_position._playTime = eventTime;
_position._lastEventTime = eventTime;
_position._subtracks[subtrack]._lastEventTime = eventTime;
_position._playTick = eventTick;
_position._lastEventTick = eventTick;
_position._subtracks[subtrack]._lastEventTick = eventTick;
if (_position.isTracking(subtrack)) {
parseNextEvent(_nextSubtrackEvents[subtrack]);
}
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 playtime to overflow
// eventually. Reset the tracker time and tick values to prevent
// this from happening.
rebaseTracking();
}
}
}
void MidiParser::rebaseTracking() {
uint32 earliestLastEventTick = 0xFFFFFFFF;
int earliestLastEventTickSubtrack = -1;
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
if (_position.isTracking(i) && _position._subtracks[i]._lastEventTick < earliestLastEventTick) {
earliestLastEventTick = _position._subtracks[i]._lastEventTick;
earliestLastEventTickSubtrack = i;
}
}
if (earliestLastEventTickSubtrack == -1)
// Shouldn't happen
return;
uint32 earliestLastEventTime = _position._subtracks[earliestLastEventTickSubtrack]._lastEventTime;
// Subtract the same value from all time and tick values to keep
// a common timebase.
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
if (_position.isTracking(i)) {
if (_position._subtracks[i]._lastEventTime >= earliestLastEventTime) {
_position._subtracks[i]._lastEventTime -= earliestLastEventTime;
} else {
// This shouldn't happen; maybe due to rounding?
// Just to be sure there is no underflow...
_position._subtracks[i]._lastEventTime = 0;
}
_position._subtracks[i]._lastEventTick -= earliestLastEventTick;
}
}
if (_position._playTime >= earliestLastEventTime) {
_position._playTime -= earliestLastEventTime;
} else {
_position._playTime = 0;
}
if (_position._lastEventTime >= earliestLastEventTime) {
_position._lastEventTime -= earliestLastEventTime;
} else {
_position._lastEventTime = 0;
}
_position._playTick -= earliestLastEventTick;
_position._lastEventTick -= earliestLastEventTick;
}
void MidiParser::determineNextEvent() {
uint32 lowestNextEventTick = 0xFFFFFFFF;
int lowestNextEventTickSubtrack = -1;
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
if (_position.isTracking(i)) {
uint32 subtrackNextEventTick = _position._subtracks[i]._lastEventTick + _nextSubtrackEvents[i].delta;
if (subtrackNextEventTick < lowestNextEventTick) {
lowestNextEventTick = subtrackNextEventTick;
lowestNextEventTickSubtrack = i;
}
}
}
_nextEvent = &_nextSubtrackEvents[lowestNextEventTickSubtrack >= 0 ? lowestNextEventTickSubtrack : 0];
}
bool MidiParser::processEvent(const EventInfo &info, bool fireEvents) {
if (info.event == 0xF0) {
// SysEx event
// Check for trailing 0xF7 -- if present, remove it.
if (fireEvents) {
if (_sysExDelay > 0)
// Don't process this event if the delay from
// the previous SysEx hasn't passed yet.
return false;
uint16 delay;
if (info.ext.data[info.length-1] == 0xF7)
delay = _driver->sysExNoDelay(info.ext.data, (uint16)info.length-1);
else
delay = _driver->sysExNoDelay(info.ext.data, (uint16)info.length);
// Set the delay in microseconds so the next
// SysEx event will be delayed if necessary.
_sysExDelay = delay * 1000;
}
} else if (info.event == 0xFF) {
// META event
bool sendEventToDriver = true;
if (info.ext.type == 0x2F) {
// End of Track must be processed by us,
// as well as sending it to the output device.
_position.stopTracking(info.subtrack);
if (!_position.isTracking()) {
// All subtracks have finished playing
if (_loopStartPoint != 0xFFFFFFFF && _loopEndPoint == 0) {
jumpToTick(_loopStartPoint);
}
else if (_autoLoop) {
jumpToTick(0);
}
else {
stopPlaying();
if (fireEvents)
sendMetaEventToDriver(info.ext.type, info.ext.data, (uint16)info.length);
}
return false;
}
// Do not send End of Track to driver when there are
// still subtracks playing
sendEventToDriver = false;
} else if (info.ext.type == 0x51) {
if (info.length >= 3) {
setTempo(info.ext.data[0] << 16 | info.ext.data[1] << 8 | info.ext.data[2]);
}
}
if (fireEvents && sendEventToDriver)
sendMetaEventToDriver(info.ext.type, info.ext.data, (uint16)info.length);
} else {
if (fireEvents)
sendToDriver(info.event, info.basic.param1, info.basic.param2);
}
return true;
}
void MidiParser::allNotesOff() {
if (!_driver)
return;
int i, j;
// Turn off all active notes
for (i = 0; i < 128; ++i) {
for (j = 0; j < 16; ++j) {
if (_activeNotes[i] & (1 << j)) {
sendToDriver(0x80 | j, i, 0);
}
}
}
// Turn off all hanging notes
for (i = 0; i < ARRAYSIZE(_hangingNotes); i++) {
if (_hangingNotes[i].timeLeft) {
sendToDriver(0x80 | _hangingNotes[i].channel, _hangingNotes[i].note, 0);
_hangingNotes[i].timeLeft = 0;
}
}
_hangingNotesCount = 0;
if (!_disableAllNotesOffMidiEvents) {
// To be sure, send an "All Note Off" event (but not all MIDI devices
// support this...).
_driver->stopAllNotes(_sendSustainOffOnNotesOff);
}
memset(_activeNotes, 0, sizeof(_activeNotes));
}
void MidiParser::resetTracking() {
_position.clear();
}
void MidiParser::setLoopSection(uint32 startPoint, uint32 endPoint) {
if (endPoint != 0 && endPoint <= startPoint) {
warning("MidiParser::setLoopSection - Attempt to set loop end point before start point");
return;
}
_loopStartPoint = startPoint;
_loopEndPoint = endPoint;
}
void MidiParser::setLoopSectionMicroseconds(uint32 startPoint, uint32 endPoint) {
if (endPoint != 0 && endPoint <= startPoint) {
warning("MidiParser::setLoopSectionMicroseconds - Attempt to set loop end point before start point");
return;
}
_loopStartPointMs = startPoint;
_loopEndPointMs = endPoint;
_loopStartPoint = (startPoint == 0 ? 0 : 0xFFFFFFFF);
_loopEndPoint = (endPoint == 0 ? 0 : 0xFFFFFFFF);
}
void MidiParser::clearLoopSection() {
_loopStartPoint = 0xFFFFFFFF;
_loopEndPoint = 0xFFFFFFFF;
_loopStartPointMs = 0xFFFFFFFF;
_loopEndPointMs = 0xFFFFFFFF;
}
bool MidiParser::setTrack(int track) {
if (track < 0 || track >= _numTracks)
return false;
// We allow restarting the track via setTrack when
// it isn't playing anymore. This allows us to reuse
// a MidiParser when a track has finished and will
// be restarted via setTrack by the client again.
// This isn't exactly how setTrack behaved before though,
// the old MidiParser code did not allow setTrack to be
// used to restart a track, which was already finished.
//
// TODO: Check if any engine has problem with this
// handling, if so we need to find a better way to handle
// track restarts. (KYRA relies on this working)
else if (track == _activeTrack && isPlaying())
return true;
if (_smartJump)
hangAllActiveNotes();
else if (isPlaying())
allNotesOff();
resetTracking();
_pause = false;
memset(_activeNotes, 0, sizeof(_activeNotes));
if (_disableAutoStartPlayback)
_doParse = false;
for (int i = 0; i < MAXIMUM_SUBTRACKS; i++) {
_nextSubtrackEvents[i].clear();
}
_nextEvent = &_nextSubtrackEvents[0];
onTrackStart(track);
_activeTrack = track;
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
_position._subtracks[i]._playPos = _tracks[_activeTrack][i];
parseNextEvent(_nextSubtrackEvents[i]);
}
determineNextEvent();
return true;
}
void MidiParser::stopPlaying() {
if (isPlaying())
allNotesOff();
resetTracking();
_pause = false;
}
bool MidiParser::startPlaying() {
if (_activeTrack >= _numTracks || _pause)
return false;
if (!_position.isTracking()) {
for (int i = 0; i < MAXIMUM_SUBTRACKS; i++) {
_nextSubtrackEvents[i].clear();
}
_nextEvent = &_nextSubtrackEvents[0];
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
_position._subtracks[i]._playPos = _tracks[_activeTrack][i];
parseNextEvent(_nextSubtrackEvents[i]);
}
determineNextEvent();
}
_doParse = true;
return true;
}
void MidiParser::pausePlaying() {
if (isPlaying() && !_pause) {
_pause = true;
allNotesOff();
}
}
void MidiParser::resumePlaying() {
_pause = false;
}
void MidiParser::hangAllActiveNotes() {
// Search for note off events until we have
// accounted for every active note.
uint16 tempActive[128];
memcpy(tempActive, _activeNotes, sizeof (tempActive));
Tracker currentPos(_position);
while (true) {
int i;
for (i = 0; i < 128; ++i)
if (tempActive[i] != 0)
break;
if (i == 128)
break;
if (_position.isTracking(_nextEvent->subtrack))
parseNextEvent(_nextSubtrackEvents[_nextEvent->subtrack]);
determineNextEvent();
uint8 subtrack = _nextEvent->subtrack;
uint32 eventTick = _position._subtracks[subtrack]._lastEventTick + _nextEvent->delta;
if (_nextEvent->command() == 0x8) {
if (tempActive[_nextEvent->basic.param1] & (1 << _nextEvent->channel())) {
hangingNote(_nextEvent->channel(), _nextEvent->basic.param1, (eventTick - currentPos._lastEventTick) * _psecPerTick, false);
tempActive[_nextEvent->basic.param1] &= ~(1 << _nextEvent->channel());
}
} else if (!_position.isTracking() || (_nextEvent->event == 0xFF && _nextEvent->ext.type == 0x2F)) {
_position.stopTracking(subtrack);
if (!_position.isTracking()) {
// warning("MidiParser::hangAllActiveNotes(): Hit End of Track with active notes left");
for (i = 0; i < 128; ++i) {
for (int j = 0; j < 16; ++j) {
if (tempActive[i] & (1 << j)) {
activeNote(j, i, false);
sendToDriver(0x80 | j, i, 0);
}
}
}
break;
}
}
_position._lastEventTick = eventTick;
_position._subtracks[subtrack]._lastEventTick = eventTick;
}
_position = currentPos;
}
bool MidiParser::jumpToTick(uint32 tick, bool fireEvents, bool stopNotes, bool dontSendNoteOn) {
if (_activeTrack >= _numTracks || _pause)
return false;
assert(!_jumpingToTick); // This function is not re-entrant
_jumpingToTick = true;
Tracker currentPos(_position);
EventInfo *currentEvent = _nextEvent;
EventInfo currentSubtrackEvents[MAXIMUM_SUBTRACKS];
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
currentSubtrackEvents[i] = _nextSubtrackEvents[i];
}
resetTracking();
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
_position._subtracks[i]._playPos = _tracks[_activeTrack][i];
parseNextEvent(_nextSubtrackEvents[i]);
}
determineNextEvent();
if (tick > 0) {
while (true) {
EventInfo &info = *_nextEvent;
uint8 subtrack = info.subtrack;
uint32 eventTick = _position._subtracks[subtrack]._lastEventTick + info.delta;
if (eventTick >= tick) {
_position._playTime += (tick - _position._lastEventTick) * _psecPerTick;
_position._playTick = tick;
break;
}
// Some special processing for the fast-forward case
if (info.command() == 0x9 && dontSendNoteOn) {
// Don't send note on; doing so creates a "warble" with
// some instruments on the MT-32. Refer to bug #9262
} else if (info.event == 0xFF && info.ext.type == 0x2F) {
// End of track
_position.stopTracking(info.subtrack);
if (!_position.isTracking()) {
// This means that we failed to find the right tick.
_position = currentPos;
_nextEvent = currentEvent;
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
_nextSubtrackEvents[i] = currentSubtrackEvents[i];
}
_jumpingToTick = false;
return false;
}
} else {
processEvent(info, fireEvents);
}
uint32 eventTime = _position._lastEventTime + (eventTick - _position._lastEventTick) * _psecPerTick;
_position._playTime = eventTime;
_position._lastEventTime = eventTime;
_position._subtracks[subtrack]._lastEventTime = eventTime;
_position._playTick = eventTick;
_position._lastEventTick = eventTick;
_position._subtracks[subtrack]._lastEventTick = eventTick;
if (_position.isTracking(subtrack)) {
parseNextEvent(_nextSubtrackEvents[subtrack]);
}
determineNextEvent();
}
}
if (stopNotes) {
if (!_smartJump || !currentPos.isTracking()) {
allNotesOff();
} else {
Tracker targetPosition(_position);
EventInfo *targetEvent = _nextEvent;
EventInfo targetSubtrackEvents[MAXIMUM_SUBTRACKS];
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
targetSubtrackEvents[i] = _nextSubtrackEvents[i];
}
_position = currentPos;
_nextEvent = currentEvent;
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
_nextSubtrackEvents[i] = currentSubtrackEvents[i];
}
hangAllActiveNotes();
_position = targetPosition;
_nextEvent = targetEvent;
for (int i = 0; i < _numSubtracks[_activeTrack]; i++) {
_nextSubtrackEvents[i] = targetSubtrackEvents[i];
}
}
}
_abortParse = true;
_jumpingToTick = false;
return true;
}
void MidiParser::unloadMusic() {
if (_numTracks == 0)
// No music data loaded
return;
stopPlaying();
_numTracks = 0;
_activeTrack = 255;
_abortParse = true;
memset(_tracks, 0, sizeof(_tracks));
memset(_numSubtracks, 1, sizeof(_numSubtracks));
for (int i = 0; i < MAXIMUM_SUBTRACKS; i++) {
_nextSubtrackEvents[i].clear();
_nextSubtrackEvents[i].subtrack = i;
}
_nextEvent = &_nextSubtrackEvents[0];
clearLoopSection();
if (_centerPitchWheelOnUnload) {
// Center the pitch wheels in preparation for the next piece of
// music. It's not safe to do this from within allNotesOff(),
// and might not even be safe here, so we only do it if the
// client has explicitly asked for it.
if (_driver) {
for (int i = 0; i < 16; ++i) {
sendToDriver(0xE0 | i, 0, 0x40);
}
}
}
}