/* ScummVM - Scumm Interpreter * Copyright (C) 2001 Ludvig Strigeus * 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 "scumm/scumm.h" #include "sound/midiparser.h" #include "scumm/saveload.h" #include "common/util.h" #include "common/engine.h" #include "imuse_internal.h" //////////////////////////////////////// // // Helper functions // //////////////////////////////////////// static uint read_word(byte *a) { return (a[0] << 8) + a[1]; } ////////////////////////////////////////////////// // // IMuse Player implementation // ////////////////////////////////////////////////// Player::Player() : _midi (0), _parser (0), _parts (0), _active (false), _scanning (false), _id (0), _priority (0), _volume (0), _pan (0), _transpose (0), _detune (0), _vol_eff (0), _track_index (0), _loop_to_beat (0), _loop_from_beat (0), _loop_counter (0), _loop_to_tick (0), _loop_from_tick (0), _speed (128), _isMT32 (false), _isGM (false), _se (0), _vol_chan (0) { } Player::~Player() { if (_parser) { delete _parser; _parser = 0; } } bool Player::startSound (int sound, MidiDriver *midi) { void *mdhd; int i; // Not sure what the old code was doing, // but we'll go ahead and do a similar check. mdhd = _se->findStartOfSound (sound); if (!mdhd) { warning ("Player::startSound(): Couldn't find start of sound %d!", sound); return false; } /* mdhd = _se->findTag(sound, MDHD_TAG, 0); if (mdhd == NULL) { mdhd = _se->findTag(sound, MDPG_TAG, 0); if (mdhd == NULL) { warning("P::startSound failed: Couldn't find %s", MDHD_TAG); return false; } } */ _isMT32 = _se->isMT32(sound); _isGM = _se->isGM(sound); _parts = NULL; _active = true; _midi = midi; _id = sound; _priority = 0x80; _volume = 0x7F; _vol_chan = 0xFFFF; _vol_eff = (_se->get_channel_volume(0xFFFF) << 7) >> 7; _pan = 0; _transpose = 0; _detune = 0; for (i = 0; i < ARRAYSIZE(_parameterFaders); ++i) _parameterFaders[i].init(); hook_clear(); if (start_seq_sound(sound) != 0) { _active = false; _midi = NULL; return false; } return true; } bool Player::isFadingOut() { int i; for (i = 0; i < ARRAYSIZE(_parameterFaders); ++i) { if (_parameterFaders[i].param == ParameterFader::pfVolume && _parameterFaders[i].end == 0) { return true; } } return false; } void Player::clear() { if (_parser) _parser->unloadMusic(); uninit_parts(); _se->ImFireAllTriggers (_id); _active = false; _midi = NULL; } void Player::hook_clear() { memset(&_hook, 0, sizeof(_hook)); } int Player::start_seq_sound (int sound, bool reset_vars) { byte *ptr; if (reset_vars) { _loop_to_beat = 1; _loop_from_beat = 1; _track_index = 0; _loop_counter = 0; _loop_to_tick = 0; _loop_from_tick = 0; } ptr = _se->findStartOfSound (sound); if (ptr == NULL) return -1; if (_parser) delete _parser; if (!memcmp (ptr, "FORM", 4)) _parser = MidiParser::createParser_XMIDI(); else _parser = MidiParser::createParser_SMF(); _parser->setMidiDriver (this); _parser->property (MidiParser::mpSmartJump, 1); _parser->loadMusic (ptr, 0); _parser->setTrack (_track_index); setSpeed (reset_vars ? 128 : _speed); return 0; } void Player::uninit_parts() { if (_parts && _parts->_player != this) error("asd"); while (_parts) _parts->uninit(); // In case another player is waiting to allocate parts if (_midi) _se->reallocateMidiChannels (_midi); } void Player::setSpeed(byte speed) { _speed = speed; if (_parser) _parser->setTimerRate (((_midi->getBaseTempo() * speed) >> 7) * _se->_tempoFactor / 100); } void Player::send (uint32 b) { byte cmd = (byte) (b & 0xF0); byte chan = (byte) (b & 0x0F); byte param1 = (byte) ((b >> 8) & 0xFF); byte param2 = (byte) ((b >> 16) & 0xFF); Part *part; switch (cmd >> 4) { case 0x8: // Key Off if (!_scanning) key_off (chan, param1); else clear_active_note (chan, param1); break; case 0x9: // Key On if (!_scanning) key_on (chan, param1, param2); else set_active_note (chan, param1); break; case 0xB: // Control Change part = (param1 == 123 ? getActivePart (chan) : getPart (chan)); if (!part) break; switch (param1) { case 1: // Modulation Wheel part->set_modwheel (param2); break; case 7: // Volume part->setVolume (param2); break; case 10: // Pan Position part->set_pan (param2 - 0x40); break; case 16: // Pitchbend Factor (non-standard) part->set_pitchbend_factor (param2); break; case 17: // GP Slider 2 part->set_detune (param2 - 0x40); break; case 18: // GP Slider 3 part->set_pri (param2 - 0x40); _se->reallocateMidiChannels (_midi); break; case 64: // Sustain Pedal part->set_pedal (param2 != 0); break; case 91: // Effects Level part->set_effect_level (param2); break; case 93: // Chorus Level part->set_chorus(param2); break; case 123: // All Notes Off part->silence(); break; default: warning("Player::send(): Invalid control change %d", param1); } break; case 0xC: // Program Change part = getPart (chan); if (part) { if (_isGM) { if (param1 < 128) part->set_program (param1); } else { if (param1 < 32) part->load_global_instrument (param1); } } break; case 0xE: // Pitch Bend part = getPart (chan); if (part) part->set_pitchbend (((param2 << 7) | param1) - 0x2000); break; case 0xA: // Aftertouch case 0xD: // Channel Pressure case 0xF: // Sequence Controls break; default: if (!_scanning) { warning ("Player::send(): Invalid command %d", cmd); clear(); } } return; } void Player::sysEx (byte *p, uint16 len) { byte code; byte a; uint b; byte buf[128]; Part *part; // Check SysEx manufacturer. // Roland is 0x41 a = *p++; --len; if (a != IMUSE_SYSEX_ID) { if (a == ROLAND_SYSEX_ID) { // Roland custom instrument definition. part = getPart (p[0] & 0x0F); if (part) { part->_instrument.roland (p - 1); if (part->clearToTransmit()) part->_instrument.send (part->_mc); } } else { warning ("Unknown SysEx manufacturer 0x%02X", (int) a); } return; } --len; // Too big? if (len >= sizeof(buf) * 2) return; #ifdef IMUSE_DEBUG for (a = 0; a < len + 1 && a < 19; ++a) { sprintf ((char *)&buf[a*3], " %02X", p[a]); } // next for if (a < len + 1) { buf[a*3] = buf[a*3+1] = buf[a*3+2] = '.'; ++a; } // end if buf[a*3] = '\0'; debug (0, "[%02d] SysEx:%s", _id, buf); #endif switch (code = *p++) { case 0: if (g_scumm->_gameId != GID_SAMNMAX) { // There are 17 bytes of useful information beyond // what we've read so far. All we know about them is // as follows: // BYTE 00: Channel # // BYTE 02: BIT 01 (0x01): Part on? (1 = yes) // BYTE 05: Volume (upper 4 bits) [guessing] // BYTE 06: Volume (lower 4 bits) [guessing] // BYTE 09: BIT 04 (0x08): Percussion? (1 = yes) // BYTE 15: Program (upper 4 bits) // BYTE 16: Program (lower 4 bits) part = getPart (p[0] & 0x0F); if (part) { part->set_onoff (p[2] & 0x01); part->setVolume ((p[5] & 0x0F) << 4 | (p[6] & 0x0F)); part->_percussion = _isGM ? ((p[9] & 0x08) > 0) : false; if (part->_percussion) { if (part->_mc) { part->off(); _se->reallocateMidiChannels (_midi); } } else { // Even in cases where a program does not seem to be specified, // i.e. bytes 15 and 16 are 0, we send a program change because // 0 is a valid program number. MI2 tests show that in such // cases, a regular program change message always seems to follow // anyway. if (_isGM) part->_instrument.program ((p[15] & 0x0F) << 4 | (p[16] & 0x0F), _isMT32); part->sendAll(); } } } else { // Sam & Max: Trigger Event // Triggers are set by doCommand (ImSetTrigger). // When a SysEx marker is encountered whose sound // ID and marker ID match what was set by ImSetTrigger, // something magical is supposed to happen.... for (a = 0; a < 16; ++a) { if (_se->_snm_triggers [a].sound == _id && _se->_snm_triggers [a].id == *p) { _se->_snm_triggers [a].sound = _se->_snm_triggers [a].id = 0; _se->doCommand (_se->_snm_triggers [a].command [0], _se->_snm_triggers [a].command [1], _se->_snm_triggers [a].command [2], _se->_snm_triggers [a].command [3], 0, 0, 0, 0); break; } } } // end if break; case 1: // This SysEx is used in Sam & Max for maybe_jump. if (_scanning) break; maybe_jump (p[0], p[1] - 1, (read_word (p + 2) - 1) * 4 + p[4], ((p[5] * TICKS_PER_BEAT) >> 2) + p[6]); break; case 2: // Start of song. Ignore for now. break; case 16: // Adlib instrument definition (Part) a = *p++ & 0x0F; ++p; // Skip hardware type part = getPart(a); if (part) { if (len == 63) { decode_sysex_bytes(p, buf, len - 3); part->set_instrument((byte *) buf); } else { // SPK tracks have len == 49 here, and are not supported part->set_program (254); // Must be invalid, but not 255 (which is reserved) } } break; case 17: // Adlib instrument definition (Global) p += 2; // Skip hardware type and... whatever came right before it a = *p++; decode_sysex_bytes(p, buf, len - 4); _se->setGlobalAdlibInstrument (a, buf); break; case 33: // Parameter adjust a = *p++ & 0x0F; ++p; // Skip hardware type decode_sysex_bytes(p, buf, len - 3); part = getPart(a); if (part) part->set_param(read_word(buf), read_word(buf + 2)); break; case 48: // Hook - jump if (_scanning) break; decode_sysex_bytes(p + 1, buf, len - 2); maybe_jump (buf[0], read_word (buf + 1), read_word (buf + 3), read_word (buf + 5)); break; case 49: // Hook - global transpose decode_sysex_bytes(p + 1, buf, len - 2); maybe_set_transpose(buf); break; case 50: // Hook - part on/off buf[0] = *p++ & 0x0F; decode_sysex_bytes(p, buf + 1, len - 2); maybe_part_onoff(buf); break; case 51: // Hook - set volume buf[0] = *p++ & 0x0F; decode_sysex_bytes(p, buf + 1, len - 2); maybe_set_volume(buf); break; case 52: // Hook - set program buf[0] = *p++ & 0x0F; decode_sysex_bytes(p, buf + 1, len - 2); maybe_set_program(buf); break; case 53: // Hook - set transpose buf[0] = *p++ & 0x0F; decode_sysex_bytes(p, buf + 1, len - 2); maybe_set_transpose_part(buf); break; case 64: // Marker p++; len -= 2; while (len--) { _se->handle_marker(_id, *p++); } break; case 80: // Loop decode_sysex_bytes(p + 1, buf, len - 2); setLoop(read_word(buf), read_word(buf + 2), read_word(buf + 4), read_word(buf + 6), read_word(buf + 8) ); break; case 81: // End loop clearLoop(); break; case 96: // Set instrument part = getPart(p[0] & 0x0F); b = (p[1] & 0x0F) << 12 | (p[2] & 0x0F) << 8 | (p[4] & 0x0F) << 4 | (p[4] & 0x0F); if (part) part->set_instrument(b); break; default: warning ("Unknown SysEx command %d", (int) code); } } void Player::decode_sysex_bytes(byte *src, byte *dst, int len) { while (len >= 0) { *dst++ = (src[0] << 4) | (src[1] & 0xF); src += 2; len -= 2; } } void Player::maybe_jump (byte cmd, uint track, uint beat, uint tick) { // Is this the hook I'm waiting for? if (cmd && _hook._jump[0] != cmd) return; // Reset hook? if (cmd != 0 && cmd < 0x80) { _hook._jump[0] = _hook._jump[1]; _hook._jump[1] = 0; } jump (track, beat, tick); } void Player::maybe_set_transpose(byte *data) { byte cmd; cmd = data[0]; // Is this the hook I'm waiting for? if (cmd && _hook._transpose != cmd) return; // Reset hook? if (cmd != 0 && cmd < 0x80) _hook._transpose = 0; setTranspose(data[1], (int8)data[2]); } void Player::maybe_part_onoff(byte *data) { byte cmd, *p; uint chan; Part *part; cmd = data[1]; chan = data[0]; p = &_hook._part_onoff[chan]; // Is this the hook I'm waiting for? if (cmd && *p != cmd) return; if (cmd != 0 && cmd < 0x80) *p = 0; part = getPart(chan); if (part) part->set_onoff(data[2] != 0); } void Player::maybe_set_volume(byte *data) { byte cmd; byte *p; uint chan; Part *part; cmd = data[1]; chan = data[0]; p = &_hook._part_volume[chan]; // Is this the hook I'm waiting for? if (cmd && *p != cmd) return; // Reset hook? if (cmd != 0 && cmd < 0x80) *p = 0; part = getPart(chan); if (part) part->setVolume(data[2]); } void Player::maybe_set_program(byte *data) { byte cmd; byte *p; uint chan; Part *part; cmd = data[1]; chan = data[0]; // Is this the hook I'm waiting for? p = &_hook._part_program[chan]; if (cmd && *p != cmd) return; if (cmd != 0 && cmd < 0x80) *p = 0; part = getPart(chan); if (part) part->set_program(data[2]); } void Player::maybe_set_transpose_part(byte *data) { byte cmd; byte *p; uint chan; cmd = data[1]; chan = data[0]; // Is this the hook I'm waiting for? p = &_hook._part_transpose[chan]; if (cmd && *p != cmd) return; // Reset hook? if (cmd != 0 && cmd < 0x80) *p = 0; part_set_transpose(chan, data[2], (int8)data[3]); } int Player::setTranspose(byte relative, int b) { Part *part; if (b > 24 || b < -24 || relative > 1) return -1; if (relative) b = transpose_clamp(_transpose + b, -7, 7); _transpose = b; for (part = _parts; part; part = part->_next) { part->set_transpose(part->_transpose); } return 0; } void Player::clear_active_notes() { memset(_se->_active_notes, 0, sizeof(_se->_active_notes)); } void Player::clear_active_note(int chan, byte note) { _se->_active_notes[note] &= ~(1 << chan); } void Player::set_active_note(int chan, byte note) { _se->_active_notes[note] |= (1 << chan); } void Player::part_set_transpose(uint8 chan, byte relative, int8 b) { Part *part; if (b > 24 || b < -24) return; part = getPart(chan); if (!part) return; if (relative) b = transpose_clamp(b + part->_transpose, -7, 7); part->set_transpose(b); } void Player::key_on(uint8 chan, byte note, uint8 velocity) { Part *part; part = getPart(chan); if (!part || !part->_on) return; part->key_on(note, velocity); } void Player::key_off(uint8 chan, byte note) { Part *part; for (part = _parts; part; part = part->_next) { if (part->_chan == (byte)chan && part->_on) part->key_off(note); } } bool Player::jump(uint track, uint beat, uint tick) { if (!_parser) return false; _track_index = track; _parser->setTrack (track); if (!_parser->jumpToTick ((beat - 1) * TICKS_PER_BEAT + tick)) return false; turn_off_pedals(); return true; } bool Player::setLoop(uint count, uint tobeat, uint totick, uint frombeat, uint fromtick) { if (tobeat + 1 >= frombeat) return false; if (tobeat == 0) tobeat = 1; _loop_counter = 0; // Because of possible interrupts _loop_to_beat = tobeat; _loop_to_tick = totick; _loop_from_beat = frombeat; _loop_from_tick = fromtick; _loop_counter = count; return true; } void Player::clearLoop() { _loop_counter = 0; } void Player::turn_off_pedals() { Part *part; for (part = _parts; part; part = part->_next) { if (part->_pedal) part->set_pedal(false); } } Part *Player::getActivePart (uint8 chan) { Part *part = _parts; while (part) { if (part->_chan == chan) return part; part = part->_next; } return 0; } Part *Player::getPart(uint8 chan) { Part *part = getActivePart (chan); if (part) return part; part = _se->allocate_part (_priority, _midi); if (!part) { warning("no parts available"); return NULL; } // Insert part into front of parts list part->_prev = NULL; part->_next = _parts; if (_parts) _parts->_prev = part; _parts = part; part->_chan = chan; part->setup(this); return part; } uint Player::update_actives() { Part *part; uint16 *active; int count = 0; clear_active_notes(); active = _se->_active_notes; for (part = _parts; part; part = part->_next) { if (part->_mc) count += part->update_actives(active); } return count; } void Player::setPriority (int pri) { Part *part; _priority = pri; for (part = _parts; part; part = part->_next) { part->set_pri(part->_pri); } _se->reallocateMidiChannels (_midi); } void Player::setPan (int pan) { Part *part; _pan = pan; for (part = _parts; part; part = part->_next) { part->set_pan(part->_pan); } } void Player::setDetune (int detune) { Part *part; _detune = detune; for (part = _parts; part; part = part->_next) { part->set_detune(part->_detune); } } int Player::scan(uint totrack, uint tobeat, uint totick) { if (!_active || !_parser) return -1; if (tobeat == 0) tobeat++; turn_off_parts(); clear_active_notes(); _scanning = true; _parser->setTrack (totrack); if (!_parser->jumpToTick ((tobeat - 1) * TICKS_PER_BEAT + totick, true)) { _scanning = false; return -1; } _scanning = false; _se->reallocateMidiChannels (_midi); play_active_notes(); if (_track_index != totrack) { _track_index = totrack; _loop_counter = 0; } return 0; } void Player::turn_off_parts() { Part *part; for (part = _parts; part; part = part->_next) part->off(); _se->reallocateMidiChannels (_midi); } void Player::play_active_notes() { int i, j; uint mask; for (i = 0; i != 128; i++) { mask = _se->_active_notes[i]; for (j = 0; j != 16; j++, mask >>= 1) { if (mask & 1) { key_on(j, i, 80); } } } } int Player::setVolume(byte vol) { Part *part; if (vol > 127) return -1; _volume = vol; _vol_eff = _se->get_channel_volume(_vol_chan) * (vol + 1) >> 7; for (part = _parts; part; part = part->_next) { part->setVolume(part->_vol); } return 0; } int Player::getParam(int param, byte chan) { switch (param) { case 0: return (byte)_priority; case 1: return (byte)_volume; case 2: return (byte)_pan; case 3: return (byte)_transpose; case 4: return (byte)_detune; case 5: return _speed; case 6: return _track_index; case 7: return getBeatIndex(); case 8: return (_parser ? _parser->getTick() % TICKS_PER_BEAT : 0); // _tick_index; case 9: return _loop_counter; case 10: return _loop_to_beat; case 11: return _loop_to_tick; case 12: return _loop_from_beat; case 13: return _loop_from_tick; case 14: case 15: case 16: case 17: return query_part_param(param, chan); case 18: case 19: case 20: case 21: case 22: case 23: return _hook.query_param(param, chan); default: return -1; } } int Player::query_part_param(int param, byte chan) { Part *part; part = _parts; while (part) { if (part->_chan == chan) { switch (param) { case 14: return part->_on; case 15: return part->_vol; case 16: return (int) part->_instrument; case 17: return part->_transpose; default: return -1; } } part = part->_next; } return 129; } void Player::onTimer() { // First handle any parameter transitions // that are occuring. transitionParameters(); // Since the volume parameter can cause // the player to be deactivated, check // to make sure we're still active. if (!_active || !_parser) return; uint32 target_tick = _parser->getTick(); uint beat_index = target_tick / TICKS_PER_BEAT + 1; uint tick_index = target_tick % TICKS_PER_BEAT; if (_loop_counter && (beat_index > _loop_from_beat || (beat_index == _loop_from_beat && tick_index >= _loop_from_tick))) { _loop_counter--; jump (_track_index, _loop_to_beat, _loop_to_tick); } _parser->onTimer(); } // "time" is referenced as hundredths of a second. // IS THAT CORRECT?? // We convert it to microseconds before prceeding int Player::addParameterFader (int param, int target, int time) { int start; switch (param) { case ParameterFader::pfVolume: // HACK: If volume is set to 0 with 0 time, // set it so immediately but DON'T clear // the player. This fixes a problem with // music being cleared inappropriately // in S&M when playing with the Dinosaur. if (!target && !time) { setVolume (0); return 0; } // Volume fades are handled differently. start = _volume; break; case ParameterFader::pfTranspose: // FIXME: Is this transpose? And what's the scale? // It's set to fade to -2400 in the tunnel of love. warning ("parameterTransition(3) outside Tunnel of Love?"); start = _transpose; target /= 200; break; case ParameterFader::pfSpeed: // FIXME: Is the speed from 0-100? // Right now I convert it to 0-128. start = _speed; target = target * 128 / 100; break; case 127: // FIXME: This MIGHT fade ALL supported parameters, // but I'm not sure. return 0; default: warning ("Player::addParameterFader(): Unknown parameter %d", param); return 0; // Should be -1, but we'll let the script think it worked. } ParameterFader *ptr = &_parameterFaders[0]; ParameterFader *best = 0; int i; for (i = ARRAYSIZE(_parameterFaders); i; --i, ++ptr) { if (ptr->param == param) { best = ptr; start = ptr->end; break; } else if (!ptr->param) { best = ptr; } } if (best) { best->param = param; best->start = start; best->end = target; if (!time) best->total_time = 1; else best->total_time = (uint32) time * 10000; best->current_time = 0; } else { warning ("IMuse Player %d: Out of parameter faders", _id); return -1; } return 0; } void Player::transitionParameters() { uint32 advance = _midi->getBaseTempo(); int value; ParameterFader *ptr = &_parameterFaders[0]; int i; for (i = ARRAYSIZE(_parameterFaders); i; --i, ++ptr) { if (!ptr->param) continue; ptr->current_time += advance; if (ptr->current_time > ptr->total_time) ptr->current_time = ptr->total_time; value = (int32) ptr->start + (int32) (ptr->end - ptr->start) * (int32) ptr->current_time / (int32) ptr->total_time; switch (ptr->param) { case ParameterFader::pfVolume: // Volume. if (!value && !ptr->end) { clear(); return; } setVolume ((byte) value); break; case ParameterFader::pfSpeed: // Speed. setSpeed ((byte) value); break; case ParameterFader::pfTranspose: // FIXME: Is this really transpose? setTranspose (0, value); break; } if (ptr->current_time >= ptr->total_time) ptr->param = 0; } } uint Player::getBeatIndex() { return (_parser ? (_parser->getTick() / TICKS_PER_BEAT + 1) : 0); } void Player::removePart (Part *part) { // Unlink if (part->_next) part->_next->_prev = part->_prev; if (part->_prev) part->_prev->_next = part->_next; else _parts = part->_next; part->_next = part->_prev = 0; } void Player::fixAfterLoad() { _midi = _se->getBestMidiDriver (_id); if (!_midi) { clear(); } else { start_seq_sound (_id, false); setSpeed (_speed); if (_parser) _parser->jumpToTick (_music_tick); // start_seq_sound already switched tracks _isMT32 = _se->isMT32 (_id); _isGM = _se->isGM (_id); } } uint32 Player::getBaseTempo() { return (_midi ? _midi->getBaseTempo() : 0); } void Player::metaEvent (byte type, byte *msg, uint16 len) { if (type == 0x2F) { _parser->unloadMusic(); clear(); } } //////////////////////////////////////// // // Player save/load functions // //////////////////////////////////////// enum { TYPE_PART = 1, TYPE_PLAYER = 2 }; int Player::save_or_load(Serializer *ser) { static const SaveLoadEntry playerEntries[] = { MKREF(Player, _parts, TYPE_PART, VER_V8), MKLINE(Player, _active, sleByte, VER_V8), MKLINE(Player, _id, sleUint16, VER_V8), MKLINE(Player, _priority, sleByte, VER_V8), MKLINE(Player, _volume, sleByte, VER_V8), MKLINE(Player, _pan, sleInt8, VER_V8), MKLINE(Player, _transpose, sleByte, VER_V8), MKLINE(Player, _detune, sleInt8, VER_V8), MKLINE(Player, _vol_chan, sleUint16, VER_V8), MKLINE(Player, _vol_eff, sleByte, VER_V8), MKLINE(Player, _speed, sleByte, VER_V8), MK_OBSOLETE(Player, _song_index, sleUint16, VER_V8, VER_V19), MKLINE(Player, _track_index, sleUint16, VER_V8), MK_OBSOLETE(Player, _timer_counter, sleUint16, VER_V8, VER_V17), MKLINE(Player, _loop_to_beat, sleUint16, VER_V8), MKLINE(Player, _loop_from_beat, sleUint16, VER_V8), MKLINE(Player, _loop_counter, sleUint16, VER_V8), MKLINE(Player, _loop_to_tick, sleUint16, VER_V8), MKLINE(Player, _loop_from_tick, sleUint16, VER_V8), MK_OBSOLETE(Player, _tempo, sleUint32, VER_V8, VER_V19), MK_OBSOLETE(Player, _cur_pos, sleUint32, VER_V8, VER_V17), MK_OBSOLETE(Player, _next_pos, sleUint32, VER_V8, VER_V17), MK_OBSOLETE(Player, _song_offset, sleUint32, VER_V8, VER_V17), MK_OBSOLETE(Player, _tick_index, sleUint16, VER_V8, VER_V17), MK_OBSOLETE(Player, _beat_index, sleUint16, VER_V8, VER_V17), MK_OBSOLETE(Player, _ticks_per_beat, sleUint16, VER_V8, VER_V17), MKLINE(Player, _music_tick, sleUint32, VER_V19), MKLINE(Player, _hook._jump[0], sleByte, VER_V8), MKLINE(Player, _hook._transpose, sleByte, VER_V8), MKARRAY(Player, _hook._part_onoff[0], sleByte, 16, VER_V8), MKARRAY(Player, _hook._part_volume[0], sleByte, 16, VER_V8), MKARRAY(Player, _hook._part_program[0], sleByte, 16, VER_V8), MKARRAY(Player, _hook._part_transpose[0], sleByte, 16, VER_V8), MKEND() }; const SaveLoadEntry parameterFaderEntries[] = { MKLINE(ParameterFader, param, sleInt16, VER_V17), MKLINE(ParameterFader, start, sleInt16, VER_V17), MKLINE(ParameterFader, end, sleInt16, VER_V17), MKLINE(ParameterFader, total_time, sleUint32, VER_V17), MKLINE(ParameterFader, current_time, sleUint32, VER_V17), MKEND() }; if (!ser->isSaving() && _parser) { delete _parser; _parser = 0; } _music_tick = _parser ? _parser->getTick() : 0; ser->saveLoadEntries (this, playerEntries); ser->saveLoadArrayOf (_parameterFaders, ARRAYSIZE(_parameterFaders), sizeof(ParameterFader), parameterFaderEntries); return 0; }