scummvm/sound/midiparser.cpp
Jamieson Christian 4ac4d76718 Fix for Bug [766426]: V5 Games: Adlib SFX not looped
Modified Smart Jump logic to deal with active notes
whose Note On and Note Off events BOTH occur OUTSIDE
the range of the jump. While this is not a thorough
way to deal with Note On events that occur outside
jump points, it at least deals with the issue of
long, unchanging Adlib SFX used by some earlier
SCUMM games.

svn-id: r8892
2003-07-10 04:34:44 +00:00

382 lines
9.7 KiB
C++

/* ScummVM - Scumm Interpreter
* Copyright (C) 2001-2003 The ScummVM project
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* $Header$
*
*/
#include "stdafx.h"
#include "midiparser.h"
#include "mididrv.h"
#include "common/util.h"
//////////////////////////////////////////////////
//
// MidiParser implementation
//
//////////////////////////////////////////////////
MidiParser::MidiParser() :
_hanging_notes_count (0),
_driver (0),
_timer_rate (0x4A0000),
_ppqn (96),
_tempo (500000),
_psec_per_tick (5208), // 500000 / 96
_autoLoop (false),
_smartJump (false),
_num_tracks (0),
_active_track (255),
_abort_parse (0)
{
memset (_active_notes, 0, sizeof(_active_notes));
}
void MidiParser::property (int prop, int value) {
switch (prop) {
case mpAutoLoop:
_autoLoop = (value != 0);
case mpSmartJump:
_smartJump = (value != 0);
}
}
void MidiParser::setTempo (uint32 tempo) {
_tempo = tempo;
if (_ppqn)
_psec_per_tick = (tempo + (_ppqn >> 2)) / _ppqn;
}
// This is the conventional (i.e. SMF) variable length quantity
uint32 MidiParser::readVLQ (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)
_active_notes[note] |= (1 << channel);
else
_active_notes[note] &= ~(1 << channel);
// See if there are hanging notes that we can cancel
NoteTimer *ptr = _hanging_notes;
int i;
for (i = ARRAYSIZE(_hanging_notes); i; --i, ++ptr) {
if (ptr->channel == channel && ptr->note == note && ptr->time_left) {
ptr->time_left = 0;
--_hanging_notes_count;
break;
}
}
}
void MidiParser::hangingNote (byte channel, byte note, uint32 time_left, bool recycle) {
NoteTimer *best = 0;
NoteTimer *ptr = _hanging_notes;
int i;
if (_hanging_notes_count >= ARRAYSIZE(_hanging_notes)) {
printf ("WARNING! MidiParser::hangingNote(): Exceeded polyphony!\n");
return;
}
for (i = ARRAYSIZE(_hanging_notes); i; --i, ++ptr) {
if (ptr->channel == channel && ptr->note == note) {
if (ptr->time_left && ptr->time_left < time_left && recycle)
return;
best = ptr;
if (ptr->time_left) {
if (recycle) _driver->send (0x80 | channel | note << 8);
--_hanging_notes_count;
}
break;
} else if (!best && ptr->time_left == 0) {
best = ptr;
}
}
// Occassionally 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 (!time_left || time_left & 0x80000000)
time_left = 1;
if (best) {
best->channel = channel;
best->note = note;
best->time_left = time_left;
++_hanging_notes_count;
} else {
// We checked this up top. We should never get here!
printf ("WARNING! MidiParser::hangingNote(): Internal error!\n");
}
}
void MidiParser::onTimer() {
uint32 end_time;
uint32 event_time;
if (!_position._play_pos || !_driver)
return;
_abort_parse = false;
end_time = _position._play_time + _timer_rate;
// Scan our hanging notes for any
// that should be turned off.
if (_hanging_notes_count) {
NoteTimer *ptr = &_hanging_notes[0];
int i;
for (i = ARRAYSIZE(_hanging_notes); i; --i, ++ptr) {
if (ptr->time_left) {
if (ptr->time_left <= _timer_rate) {
_driver->send (0x80 | ptr->channel | ptr->note << 8);
ptr->time_left = 0;
--_hanging_notes_count;
} else {
ptr->time_left -= _timer_rate;
}
}
}
}
while (!_abort_parse) {
EventInfo &info = _next_event;
event_time = _position._last_event_time + info.delta * _psec_per_tick;
if (event_time > end_time)
break;
// Process the next info.
_position._last_event_tick += info.delta;
if (info.event < 0x80) {
printf ("ERROR! Bad command or running status %02X\n", info.event);
_position._play_pos = 0;
return;
}
if (info.event == 0xF0) {
// SysEx event
_driver->sysEx (info.ext.data, (uint16) info.length);
} else if (info.event == 0xFF) {
// META event
if (info.ext.type == 0x2F) {
// End of Track must be processed by us,
// as well as sending it to the output device.
if (_autoLoop) {
jumpToTick (0);
parseNextEvent (_next_event);
} else {
allNotesOff();
resetTracking();
_driver->metaEvent (info.ext.type, info.ext.data, (uint16) info.length);
}
return;
} 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]);
}
}
_driver->metaEvent (info.ext.type, info.ext.data, (uint16) info.length);
} else {
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 * _psec_per_tick - (end_time - event_time));
else
activeNote (info.channel(), info.basic.param1, true);
}
_driver->send (info.event | info.basic.param1 << 8 | info.basic.param2 << 16);
}
if (!_abort_parse) {
_position._last_event_time = event_time;
parseNextEvent (_next_event);
}
}
if (!_abort_parse) {
_position._play_time = end_time;
_position._play_tick = (_position._play_time - _position._last_event_time) / _psec_per_tick + _position._last_event_tick;
}
}
void MidiParser::allNotesOff() {
if (!_driver)
return;
int i;
for (i = 0; i < 16; ++i)
_driver->send (0x007BB0 | i);
for (i = 0; i < ARRAYSIZE(_hanging_notes); ++i)
_hanging_notes[i].time_left = 0;
_hanging_notes_count = 0;
memset (_active_notes, 0, sizeof(_active_notes));
}
void MidiParser::resetTracking() {
_position.clear();
}
bool MidiParser::setTrack (int track) {
if (track >= _num_tracks)
return false;
else if (track == _active_track)
return true;
if (_smartJump)
hangAllActiveNotes();
else
allNotesOff();
resetTracking();
_active_track = track;
_position._play_pos = _tracks[track];
parseNextEvent (_next_event);
return true;
}
void MidiParser::hangAllActiveNotes() {
// Search for note off events until we have
// accounted for every active note.
uint16 temp_active [128];
memcpy (temp_active, _active_notes, sizeof (temp_active));
uint32 advance_tick = _position._last_event_tick;
while (true) {
int i, j;
for (i = 0; i < 128; ++i)
if (temp_active[i] != 0) break;
if (i == 128) break;
parseNextEvent (_next_event);
advance_tick += _next_event.delta;
if (_next_event.command() == 0x8) {
if (temp_active[_next_event.basic.param1] & (1 << _next_event.channel())) {
hangingNote (_next_event.channel(), _next_event.basic.param1, (advance_tick - _position._last_event_tick) * _psec_per_tick, false);
temp_active[_next_event.basic.param1] &= ~ (1 << _next_event.channel());
}
} else if (_next_event.event == 0xFF && _next_event.ext.type == 0x2F) {
// printf ("MidiParser::hangAllActiveNotes(): Hit End of Track with active notes left!\n");
for (i = 0; i < 128; ++i) {
for (j = 0; j < 16; ++j) {
if (temp_active[i] & (1 << j)) {
activeNote (j, i, false);
_driver->send (0x80 | j | i << 8);
}
}
}
break;
}
}
}
bool MidiParser::jumpToTick (uint32 tick, bool fireEvents) {
if (_active_track >= _num_tracks)
return false;
Tracker currentPos (_position);
EventInfo currentEvent (_next_event);
resetTracking();
_position._play_pos = _tracks[_active_track];
parseNextEvent (_next_event);
if (tick > 0) {
while (true) {
EventInfo &info = _next_event;
if (_position._last_event_tick + info.delta >= tick) {
_position._play_time += (tick - _position._last_event_tick) * _psec_per_tick;
_position._play_tick = tick;
break;
}
_position._last_event_tick += info.delta;
_position._last_event_time += info.delta * _psec_per_tick;
_position._play_tick = _position._last_event_tick;
_position._play_time = _position._last_event_time;
if (info.event == 0xFF) {
if (info.ext.type == 0x2F) { // End of track
if (_autoLoop) {
_position._play_pos = _tracks[_active_track];
parseNextEvent (_next_event);
} else {
_position = currentPos;
_next_event = currentEvent;
return false;
}
break;
} else if (info.ext.type == 0x51) { // Tempo
if (info.length >= 3) {
setTempo (info.ext.data[0] << 16 | info.ext.data[1] << 8 | info.ext.data[2]);
}
}
} else if (fireEvents) {
if (info.event == 0xF0)
_driver->sysEx (info.ext.data, (uint16) info.length);
else
_driver->send (info.event | info.basic.param1 << 8 | info.basic.param2 << 16);
}
parseNextEvent (_next_event);
}
}
if (!_smartJump || !currentPos._play_pos) {
allNotesOff();
} else {
EventInfo targetEvent (_next_event);
Tracker targetPosition (_position);
_position = currentPos;
_next_event = currentEvent;
hangAllActiveNotes();
_next_event = targetEvent;
_position = targetPosition;
}
_abort_parse = true;
return true;
}
void MidiParser::unloadMusic() {
resetTracking();
allNotesOff();
_num_tracks = 0;
_active_track = 255;
_abort_parse = true;
}