/* 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * $URL$ * $Id$ * */ #include "sound/midiparser.h" #include "sound/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), _centerPitchWheelOnUnload(false), _num_tracks(0), _active_track(255), _abort_parse(0) { memset(_active_notes, 0, sizeof(_active_notes)); _next_event.start = NULL; _next_event.delta = 0; _next_event.event = 0; _next_event.length = 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; } } void MidiParser::sendToDriver(uint32 b) { _driver->send(b); } 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) sendToDriver(0x80 | channel, note, 0); --_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) { sendToDriver(0x80 | ptr->channel, ptr->note, 0); 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 // Check for trailing 0xF7 -- if present, remove it. if (info.ext.data[info.length-1] == 0xF7) _driver->sysEx(info.ext.data, (uint16)info.length-1); else _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 { stopPlaying(); _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); } sendToDriver(info.event, info.basic.param1, info.basic.param2); } 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)) { sendToDriver(0x80 | j, i, 0); } } } // Turn off all hanging notes for (i = 0; i < ARRAYSIZE(_hanging_notes); i++) { if (_hanging_notes[i].time_left) { sendToDriver(0x80 | _hanging_notes[i].channel, _hanging_notes[i].note, 0); _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) { sendToDriver(0xB0 | i, 0x7b, 0); // All notes off } 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; // 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 == _active_track && isPlaying()) 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::stopPlaying() { allNotesOff(); resetTracking(); } 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; 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 (int j = 0; j < 16; ++j) { if (temp_active[i] & (1 << j)) { activeNote(j, i, false); sendToDriver(0x80 | j, i, 0); } } } break; } } } bool MidiParser::jumpToTick(uint32 tick, bool fireEvents, bool stopNotes, bool dontSendNoteOn) { 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) { if (info.ext.data[info.length-1] == 0xF7) _driver->sysEx(info.ext.data, (uint16)info.length-1); else _driver->sysEx(info.ext.data, (uint16)info.length); } else { if (info.command() == 0x9 && dontSendNoteOn) { // Don't send note on; doing so creates a "warble" with some instruments on the MT-32. // Refer to patch #3117577 // TODO: this is currently done by SCI only, but it seems sensible enough to do this // for all engines } else { sendToDriver(info.event, info.basic.param1, info.basic.param2); } } } parseNextEvent(_next_event); } } if (stopNotes) { 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; 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); } } } }