From 42ab839dd6c8a1570b232101eb97f4e54de57935 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Wed, 9 Feb 2011 01:09:01 +0000 Subject: AUDIO: Rename sound/ dir to audio/ svn-id: r55850 --- audio/midiparser.cpp | 467 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 audio/midiparser.cpp (limited to 'audio/midiparser.cpp') diff --git a/audio/midiparser.cpp b/audio/midiparser.cpp new file mode 100644 index 0000000000..e01b8a7fc6 --- /dev/null +++ b/audio/midiparser.cpp @@ -0,0 +1,467 @@ +/* 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/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); + } + } + } +} -- cgit v1.2.3