mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-28 04:34:50 +00:00
6dc0191e03
I wasn't able to fully stop ITE's XMIDI music - there would always be a couple of notes hanging even after unloading it. Apparently this music doesn't use the "active notes" mechanism at all, and I guess the ALSA driver doesn't support the "All Note Off" event. I'm still not 100% sure this is the correct fix, but unless someone has any better idea... svn-id: r13910
394 lines
10 KiB
C++
394 lines
10 KiB
C++
/* ScummVM - Scumm Interpreter
|
|
* Copyright (C) 2001-2004 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)) {
|
|
warning("MidiParser::hangingNote(): Exceeded polyphony");
|
|
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!
|
|
warning("MidiParser::hangingNote(): Internal error");
|
|
}
|
|
}
|
|
|
|
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) {
|
|
warning("Bad command or running status %02X", 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, j;
|
|
|
|
// Turn off all active notes
|
|
for (i = 0; i < 128; ++i) {
|
|
for (j = 0; j < 16; ++j) {
|
|
if (_active_notes[i] & (1 << j)) {
|
|
_driver->send (0x80 | j | i << 8);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Turn off all hanging notes
|
|
for (i = 0; i < ARRAYSIZE(_hanging_notes); i++) {
|
|
if (_hanging_notes[i].time_left) {
|
|
_driver->send (0x80 | _hanging_notes[i].channel | _hanging_notes[i].note << 8);
|
|
_hanging_notes[i].time_left = 0;
|
|
}
|
|
}
|
|
_hanging_notes_count = 0;
|
|
|
|
// To be sure, send an "All Note Off" event (but not all MIDI devices support this...)
|
|
for (i = 0; i < 16; ++i)
|
|
_driver->send (0x007BB0 | i);
|
|
memset (_active_notes, 0, sizeof(_active_notes));
|
|
}
|
|
|
|
void MidiParser::resetTracking() {
|
|
_position.clear();
|
|
}
|
|
|
|
bool MidiParser::setTrack (int track) {
|
|
if (track < 0 || track >= _num_tracks)
|
|
return false;
|
|
else if (track == _active_track)
|
|
return true;
|
|
|
|
if (_smartJump)
|
|
hangAllActiveNotes();
|
|
else
|
|
allNotesOff();
|
|
|
|
resetTracking();
|
|
memset (_active_notes, 0, sizeof(_active_notes));
|
|
_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) {
|
|
// warning("MidiParser::hangAllActiveNotes(): Hit End of Track with active notes left");
|
|
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
|
|
_position = currentPos;
|
|
_next_event = currentEvent;
|
|
return false;
|
|
} else {
|
|
if (info.ext.type == 0x51 && info.length >= 3) // Tempo
|
|
setTempo (info.ext.data[0] << 16 | info.ext.data[1] << 8 | info.ext.data[2]);
|
|
if (fireEvents)
|
|
_driver->metaEvent (info.ext.type, info.ext.data, (uint16) info.length);
|
|
}
|
|
} 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;
|
|
}
|