Logo Search packages:      
Sourcecode: scummvm version File versions  Download package

midiparser.cpp

/* 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 "audio/midiparser.h"
#include "audio/mididrv.h"
#include "common/textconsole.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),
_sendSustainOffOnNotesOff(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;
      case mpSendSustainOffOnNotesOff:
            _sendSustainOffOnNotesOff = (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
            if (_sendSustainOffOnNotesOff)
                  sendToDriver(0xB0 | i, 0x40, 0); // Also send a sustain off event (bug #3116608)
      }

      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 {
                              // The note on sending code is used by the SCUMM engine. Other engine using this code
                              // (such as SCI) have issues with this, as all the notes sent can be heard when a song
                              // is fast-forwarded.   Thus, if the engine requests it, don't send note on events.
                              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
                              } 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);
                  }
            }
      }
}

Generated by  Doxygen 1.6.0   Back to index