931 lines
23 KiB
C++
931 lines
23 KiB
C++
/* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
// TODO: Original Mac driver allows the interpreter to play notes. Channel
|
|
// is allocated via controller 0x4D. Find out if this is used. This would
|
|
// allow for music and sfx to be played simultaneously.
|
|
|
|
// FIXME: SQ3, LSL2 and HOYLE1 for Amiga don't seem to load any
|
|
// patches, even though patches are present. Later games do load
|
|
// patches, but include disabled patches with a 'd' appended to the
|
|
// filename, e.g. sound.010d. For SQ3, LSL2 and HOYLE1, we should
|
|
// probably disable patch loading. Maybe the original interpreter
|
|
// loads these disabled patches under some specific condition?
|
|
|
|
#include "sci/sound/drivers/mididriver.h"
|
|
#include "sci/sound/drivers/macmixer.h"
|
|
#include "sci/resource/resource.h"
|
|
|
|
#include "common/file.h"
|
|
#include "common/memstream.h"
|
|
#include "common/mutex.h"
|
|
#include "common/system.h"
|
|
#include "common/textconsole.h"
|
|
#include "common/util.h"
|
|
#include "audio/mods/paula.h"
|
|
|
|
namespace Sci {
|
|
|
|
class MidiPlayer_AmigaMac0 : public MidiPlayer {
|
|
public:
|
|
enum {
|
|
kVoices = 4,
|
|
kBaseFreq = 60
|
|
};
|
|
|
|
MidiPlayer_AmigaMac0(SciVersion version, Audio::Mixer *mixer, Common::Mutex &mutex);
|
|
virtual ~MidiPlayer_AmigaMac0();
|
|
|
|
// MidiPlayer
|
|
void close() override;
|
|
void send(uint32 b) override;
|
|
void setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) override;
|
|
uint32 getBaseTempo() override { return (1000000 + kBaseFreq / 2) / kBaseFreq; }
|
|
byte getPlayId() const override { return 0x40; }
|
|
int getPolyphony() const override { return kVoices; }
|
|
bool hasRhythmChannel() const override { return false; }
|
|
void setVolume(byte volume) override;
|
|
int getVolume() override;
|
|
void playSwitch(bool play) override;
|
|
void initTrack(SciSpan<const byte> &trackData) override;
|
|
|
|
protected:
|
|
bool _playSwitch;
|
|
uint _masterVolume;
|
|
|
|
Audio::Mixer *_mixer;
|
|
Audio::SoundHandle _mixerSoundHandle;
|
|
Common::TimerManager::TimerProc _timerProc;
|
|
void *_timerParam;
|
|
bool _isOpen;
|
|
|
|
void freeInstruments();
|
|
void onTimer();
|
|
|
|
struct Instrument {
|
|
Instrument() :
|
|
name(),
|
|
loop(false),
|
|
fixedNote(false),
|
|
seg2Offset(0),
|
|
seg3Offset(0),
|
|
samples(nullptr),
|
|
transpose(0),
|
|
envelope() {}
|
|
|
|
~Instrument() { delete[] samples; }
|
|
|
|
char name[31];
|
|
bool loop;
|
|
bool fixedNote;
|
|
uint32 seg2Offset;
|
|
uint32 seg3Offset;
|
|
const byte *samples;
|
|
int16 transpose;
|
|
|
|
struct Envelope {
|
|
byte skip;
|
|
int8 step;
|
|
byte target;
|
|
} envelope[4];
|
|
};
|
|
|
|
Common::Array<const Instrument *> _instruments;
|
|
|
|
class Voice {
|
|
public:
|
|
Voice(MidiPlayer_AmigaMac0 &driver, byte id) :
|
|
_patch(0),
|
|
_note(-1),
|
|
_velocity(0),
|
|
_pitch(0),
|
|
_instrument(nullptr),
|
|
_loop(false),
|
|
_envState(0),
|
|
_envCntDown(0),
|
|
_envCurVel(0),
|
|
_volume(0),
|
|
_id(id),
|
|
_driver(driver) {}
|
|
|
|
virtual ~Voice() {}
|
|
|
|
virtual void noteOn(int8 note, int8 velocity) = 0;
|
|
virtual void noteOff(int8 note) = 0;
|
|
virtual void setPitchWheel(int16 pitch) {}
|
|
|
|
virtual void stop() = 0;
|
|
virtual void setEnvelopeVolume(byte volume) = 0;
|
|
|
|
void processEnvelope();
|
|
|
|
byte _patch;
|
|
int8 _note;
|
|
byte _velocity;
|
|
uint16 _pitch;
|
|
|
|
const Instrument *_instrument;
|
|
bool _loop;
|
|
|
|
byte _envState;
|
|
byte _envCntDown;
|
|
int8 _envCurVel;
|
|
|
|
byte _volume;
|
|
byte _id;
|
|
|
|
private:
|
|
MidiPlayer_AmigaMac0 &_driver;
|
|
};
|
|
|
|
Common::Array<Voice *> _voices;
|
|
typedef Common::Array<Voice *>::const_iterator VoiceIt;
|
|
|
|
Voice *_channels[MIDI_CHANNELS];
|
|
|
|
Common::Mutex &_mixMutex;
|
|
Common::Mutex _timerMutex;
|
|
};
|
|
|
|
MidiPlayer_AmigaMac0::MidiPlayer_AmigaMac0(SciVersion version, Audio::Mixer *mixer, Common::Mutex &mutex) :
|
|
MidiPlayer(version),
|
|
_playSwitch(true),
|
|
_masterVolume(15),
|
|
_mixer(mixer),
|
|
_mixerSoundHandle(),
|
|
_timerProc(),
|
|
_timerParam(nullptr),
|
|
_isOpen(false),
|
|
_channels(),
|
|
_mixMutex(mutex) {}
|
|
|
|
MidiPlayer_AmigaMac0::~MidiPlayer_AmigaMac0() {
|
|
close();
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::close() {
|
|
if (!_isOpen)
|
|
return;
|
|
|
|
_mixer->stopHandle(_mixerSoundHandle);
|
|
|
|
for (uint ci = 0; ci < ARRAYSIZE(_channels); ++ci)
|
|
_channels[ci] = nullptr;
|
|
|
|
for (VoiceIt v = _voices.begin(); v != _voices.end(); ++v)
|
|
delete *v;
|
|
_voices.clear();
|
|
|
|
freeInstruments();
|
|
|
|
_isOpen = false;
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::setVolume(byte volume) {
|
|
Common::StackLock lock(_mixMutex);
|
|
_masterVolume = CLIP<byte>(volume, 0, 15);
|
|
}
|
|
|
|
int MidiPlayer_AmigaMac0::getVolume() {
|
|
Common::StackLock lock(_mixMutex);
|
|
return _masterVolume;
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::playSwitch(bool play) {
|
|
Common::StackLock lock(_mixMutex);
|
|
_playSwitch = play;
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::initTrack(SciSpan<const byte>& header) {
|
|
if (!_isOpen)
|
|
return;
|
|
|
|
uint8 readPos = 0;
|
|
const uint8 caps = header.getInt8At(readPos++);
|
|
|
|
// We only implement the MIDI functionality here, samples are
|
|
// handled by the generic sample code
|
|
if (caps != 0)
|
|
return;
|
|
|
|
Common::StackLock lock(_mixMutex);
|
|
|
|
uint vi = 0;
|
|
|
|
for (uint i = 0; i < 15; ++i) {
|
|
readPos++;
|
|
const uint8 flags = header.getInt8At(readPos++);
|
|
|
|
if ((flags & getPlayId()) && (vi < kVoices))
|
|
_channels[i] = _voices[vi++];
|
|
else
|
|
_channels[i] = nullptr;
|
|
}
|
|
|
|
_channels[15] = nullptr;
|
|
|
|
for (VoiceIt it = _voices.begin(); it != _voices.end(); ++it) {
|
|
Voice *voice = *it;
|
|
voice->stop();
|
|
voice->_note = -1;
|
|
voice->_envState = 0;
|
|
voice->_pitch = 0x2000;
|
|
}
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::freeInstruments() {
|
|
for (Common::Array<const Instrument *>::iterator it = _instruments.begin(); it != _instruments.end(); ++it)
|
|
delete *it;
|
|
|
|
_instruments.clear();
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::onTimer() {
|
|
_mixMutex.unlock();
|
|
_timerMutex.lock();
|
|
|
|
if (_timerProc)
|
|
(*_timerProc)(_timerParam);
|
|
|
|
_timerMutex.unlock();
|
|
_mixMutex.lock();
|
|
|
|
for (VoiceIt it = _voices.begin(); it != _voices.end(); ++it)
|
|
(*it)->processEnvelope();
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) {
|
|
Common::StackLock lock(_timerMutex);
|
|
_timerProc = timerProc;
|
|
_timerParam = timerParam;
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::send(uint32 b) {
|
|
Common::StackLock lock(_mixMutex);
|
|
|
|
byte command = b & 0xf0;
|
|
byte channel = b & 0xf;
|
|
byte op1 = (b >> 8) & 0xff;
|
|
byte op2 = (b >> 16) & 0xff;
|
|
|
|
Voice *voice = _channels[channel];
|
|
|
|
if (!voice)
|
|
return;
|
|
|
|
switch(command) {
|
|
case 0x80:
|
|
voice->noteOff(op1);
|
|
break;
|
|
case 0x90:
|
|
voice->noteOn(op1, op2);
|
|
break;
|
|
case 0xb0:
|
|
// Not in original driver
|
|
if (op1 == 0x7b && voice->_note != -1 && voice->_envState < 4)
|
|
voice->noteOff(voice->_note);
|
|
break;
|
|
case 0xc0:
|
|
voice->_patch = op1;
|
|
break;
|
|
case 0xe0:
|
|
voice->setPitchWheel((op2 << 7) | op1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void MidiPlayer_AmigaMac0::Voice::processEnvelope() {
|
|
if (_envState == 0 || _envState == 3)
|
|
return;
|
|
|
|
if (_envState == 6) {
|
|
stop();
|
|
_envState = 0;
|
|
return;
|
|
}
|
|
|
|
if (_envCntDown == 0) {
|
|
const uint envIdx = (_envState > 3 ? _envState - 2 : _envState - 1);
|
|
|
|
_envCntDown = _instrument->envelope[envIdx].skip;
|
|
int8 velocity = _envCurVel;
|
|
|
|
if (velocity <= 0) {
|
|
stop();
|
|
_envState = 0;
|
|
return;
|
|
}
|
|
|
|
if (velocity > 63)
|
|
velocity = 63;
|
|
|
|
if (!_driver._playSwitch)
|
|
velocity = 0;
|
|
|
|
setEnvelopeVolume(velocity);
|
|
|
|
const int8 step = _instrument->envelope[envIdx].step;
|
|
if (step < 0) {
|
|
_envCurVel -= step;
|
|
if (_envCurVel > _instrument->envelope[envIdx].target) {
|
|
_envCurVel = _instrument->envelope[envIdx].target;
|
|
++_envState;
|
|
}
|
|
} else {
|
|
_envCurVel -= step;
|
|
if (_envCurVel < _instrument->envelope[envIdx].target) {
|
|
_envCurVel = _instrument->envelope[envIdx].target;
|
|
++_envState;
|
|
}
|
|
}
|
|
}
|
|
|
|
--_envCntDown;
|
|
}
|
|
|
|
class MidiPlayer_Mac0 : public Mixer_Mac<MidiPlayer_Mac0>, public MidiPlayer_AmigaMac0 {
|
|
public:
|
|
MidiPlayer_Mac0(SciVersion version, Audio::Mixer *mixer, Mode mode);
|
|
|
|
// MidiPlayer
|
|
int open(ResourceManager *resMan) override;
|
|
void setVolume(byte volume) override;
|
|
|
|
// MidiDriver
|
|
void close() override;
|
|
|
|
// Mixer_Mac
|
|
static int8 applyChannelVolume(byte volume, byte sample);
|
|
void interrupt() { onTimer(); }
|
|
void onChannelFinished(uint channel);
|
|
|
|
private:
|
|
enum {
|
|
kStepTableSize = 84
|
|
};
|
|
|
|
template <Mode mode>
|
|
void generateSamples(int16 *buf, int len);
|
|
|
|
struct MacInstrument : public Instrument {
|
|
MacInstrument() :
|
|
Instrument(),
|
|
endOffset(0) {}
|
|
|
|
uint32 endOffset;
|
|
};
|
|
|
|
class MacVoice : public Voice {
|
|
public:
|
|
MacVoice(MidiPlayer_Mac0 &driver, byte id) :
|
|
Voice(driver, id),
|
|
_macDriver(driver) {}
|
|
|
|
private:
|
|
void noteOn(int8 note, int8 velocity) override;
|
|
void noteOff(int8 note) override;
|
|
|
|
void stop() override;
|
|
void setEnvelopeVolume(byte volume) override;
|
|
|
|
void calcVoiceStep();
|
|
|
|
MidiPlayer_Mac0 &_macDriver;
|
|
};
|
|
|
|
bool loadInstruments(Common::SeekableReadStream &patch);
|
|
|
|
ufrac_t _stepTable[kStepTableSize];
|
|
};
|
|
|
|
MidiPlayer_Mac0::MidiPlayer_Mac0(SciVersion version, Audio::Mixer *mixer, Mixer_Mac<MidiPlayer_Mac0>::Mode mode) :
|
|
Mixer_Mac<MidiPlayer_Mac0>(mode),
|
|
MidiPlayer_AmigaMac0(version, mixer, _mutex) {
|
|
|
|
for (uint i = 0; i < kStepTableSize; ++i)
|
|
_stepTable[i] = round(0x2000 * pow(2.0, i / 12.0));
|
|
}
|
|
|
|
int MidiPlayer_Mac0::open(ResourceManager *resMan) {
|
|
if (_isOpen)
|
|
return MidiDriver::MERR_ALREADY_OPEN;
|
|
|
|
Resource *patch = g_sci->getResMan()->findResource(ResourceId(kResourceTypePatch, 200), false);
|
|
if (!patch) {
|
|
warning("MidiPlayer_Mac0: Failed to open patch 200");
|
|
return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
|
|
}
|
|
|
|
Common::MemoryReadStream stream(patch->toStream());
|
|
if (!loadInstruments(stream)) {
|
|
freeInstruments();
|
|
return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
|
|
}
|
|
|
|
for (byte vi = 0; vi < kVoices; ++vi)
|
|
_voices.push_back(new MacVoice(*this, vi));
|
|
|
|
startMixer();
|
|
_mixer->playStream(Audio::Mixer::kPlainSoundType, &_mixerSoundHandle, this, -1, _mixer->kMaxChannelVolume, 0, DisposeAfterUse::NO);
|
|
|
|
_isOpen = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void MidiPlayer_Mac0::setVolume(byte volume) {
|
|
MidiPlayer_AmigaMac0::setVolume(volume);
|
|
setMixerVolume(volume / 2 + 1);
|
|
}
|
|
|
|
void MidiPlayer_Mac0::close() {
|
|
MidiPlayer_AmigaMac0::close();
|
|
stopMixer();
|
|
}
|
|
|
|
int8 MidiPlayer_Mac0::applyChannelVolume(byte volume, byte sample) {
|
|
int8 signedSample = sample - 0x80;
|
|
|
|
if (volume == 0)
|
|
return 0;
|
|
|
|
if (volume == 63)
|
|
return signedSample;
|
|
|
|
if (signedSample >= 0)
|
|
return (signedSample * volume + 32) / 64;
|
|
else
|
|
return ~((~signedSample * volume + 32) / 64);
|
|
}
|
|
|
|
void MidiPlayer_Mac0::onChannelFinished(uint channel) {
|
|
_voices[channel]->_envState = 0;
|
|
}
|
|
|
|
void MidiPlayer_Mac0::MacVoice::stop() {
|
|
_macDriver.resetChannel(_id);
|
|
}
|
|
|
|
void MidiPlayer_Mac0::MacVoice::calcVoiceStep() {
|
|
int8 note = _note;
|
|
|
|
if (_instrument->fixedNote)
|
|
note = 72;
|
|
|
|
int16 index = note + _instrument->transpose;
|
|
|
|
index -= 24;
|
|
|
|
while (index < 0)
|
|
index += 12;
|
|
|
|
while (index >= kStepTableSize)
|
|
index -= 12;
|
|
|
|
_macDriver.setChannelStep(_id, _macDriver._stepTable[index]);
|
|
}
|
|
|
|
void MidiPlayer_Mac0::MacVoice::setEnvelopeVolume(byte volume) {
|
|
if (_macDriver._masterVolume == 0 || !_macDriver._playSwitch)
|
|
volume = 0;
|
|
_macDriver.setChannelVolume(_id, volume * _volume >> 6);
|
|
}
|
|
|
|
void MidiPlayer_Mac0::MacVoice::noteOn(int8 note, int8 velocity) {
|
|
if (velocity == 0) {
|
|
noteOff(note);
|
|
return;
|
|
}
|
|
|
|
stop();
|
|
_envState = 0;
|
|
|
|
if (!_macDriver._instruments[_patch]) // Not in original driver
|
|
return;
|
|
|
|
_instrument = _macDriver._instruments[_patch];
|
|
|
|
_velocity = velocity;
|
|
_volume = velocity >> 1;
|
|
_envCurVel = 64;
|
|
_envCntDown = 0;
|
|
_loop = _instrument->loop;
|
|
_note = note;
|
|
|
|
calcVoiceStep();
|
|
|
|
const MacInstrument *ins = static_cast<const MacInstrument *>(_instrument);
|
|
|
|
if (_loop) {
|
|
_envState = 1;
|
|
_macDriver.setChannelData(_id, ins->samples, 0, ins->seg3Offset, ins->seg3Offset - ins->seg2Offset);
|
|
} else {
|
|
_macDriver.setChannelData(_id, ins->samples, 0, ins->endOffset);
|
|
}
|
|
|
|
setEnvelopeVolume(63);
|
|
}
|
|
|
|
void MidiPlayer_Mac0::MacVoice::noteOff(int8 note) {
|
|
if (_note == note) {
|
|
if (_envState != 0) {
|
|
_envState = 4;
|
|
_envCntDown = 0;
|
|
}
|
|
// Original driver doesn't reset note anywhere that I could find,
|
|
// but this seems like a good place to do that
|
|
_note = -1;
|
|
}
|
|
}
|
|
|
|
bool MidiPlayer_Mac0::loadInstruments(Common::SeekableReadStream &patch) {
|
|
char name[33];
|
|
|
|
if (patch.read(name, 8) < 8 || strncmp(name, "X1iUo123", 8) != 0) {
|
|
warning("MidiPlayer_Mac0: Incorrect ID string in patch bank");
|
|
return false;
|
|
}
|
|
|
|
if (patch.read(name, 32) < 32) {
|
|
warning("MidiPlayer_Mac0: Error reading patch bank");
|
|
return false;
|
|
}
|
|
name[32] = 0;
|
|
|
|
debugC(kDebugLevelSound, "Bank: '%s'", name);
|
|
|
|
_instruments.resize(128);
|
|
|
|
for (byte i = 0; i < 128; i++) {
|
|
patch.seek(40 + i * 4);
|
|
uint32 offset = patch.readUint32BE();
|
|
|
|
if (offset == 0) {
|
|
_instruments[i] = 0;
|
|
continue;
|
|
}
|
|
|
|
patch.seek(offset);
|
|
|
|
MacInstrument *instrument = new MacInstrument();
|
|
_instruments[i] = instrument;
|
|
|
|
patch.readUint16BE(); // index
|
|
|
|
const uint16 flags = patch.readUint16BE();
|
|
instrument->loop = flags & 1;
|
|
instrument->fixedNote = !(flags & 2);
|
|
|
|
instrument->seg2Offset = patch.readUint32BE();
|
|
instrument->seg3Offset = patch.readUint32BE();
|
|
instrument->endOffset = patch.readUint32BE();
|
|
|
|
instrument->transpose = patch.readUint16BE();
|
|
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
instrument->envelope[stage].skip = patch.readByte();
|
|
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
instrument->envelope[stage].step = patch.readByte();
|
|
|
|
// In the original, it uses the stage 0 step as the stage 3 target,
|
|
// but we (most likely) don't have to replicate this bug.
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
instrument->envelope[stage].target = patch.readByte();
|
|
|
|
patch.read(instrument->name, 30);
|
|
instrument->name[30] = 0;
|
|
|
|
debugC(kDebugLevelSound, "\tInstrument[%d]: '%s'", i, instrument->name);
|
|
debugC(kDebugLevelSound, "\t\tSegment offsets: %d, %d, %d", instrument->seg2Offset, instrument->seg3Offset, instrument->endOffset);
|
|
debugC(kDebugLevelSound, "\t\tTranspose = %d, Fixed note = %d, Loop = %d", instrument->transpose, instrument->fixedNote, instrument->loop);
|
|
debugC(kDebugLevelSound, "\t\tEnvelope:");
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
debugC(kDebugLevelSound, "\t\t\tStage %d: skip %d, step %d, target %d", stage, instrument->envelope[stage].skip, instrument->envelope[stage].step, instrument->envelope[stage].target);
|
|
|
|
uint32 sampleSize = (instrument->loop ? instrument->seg3Offset : instrument->endOffset) + 1111;
|
|
byte *samples = new byte[sampleSize];
|
|
patch.read(samples, sampleSize);
|
|
instrument->samples = samples;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
class MidiPlayer_Amiga0 : public Audio::Paula, public MidiPlayer_AmigaMac0 {
|
|
public:
|
|
MidiPlayer_Amiga0(SciVersion version, Audio::Mixer *mixer);
|
|
|
|
// MidiPlayer
|
|
int open(ResourceManager *resMan) override;
|
|
|
|
// MidiDriver
|
|
void close() override;
|
|
|
|
// Audio::Paula
|
|
void interrupt() override { onTimer(); }
|
|
|
|
private:
|
|
struct AmigaInstrument : public Instrument {
|
|
AmigaInstrument() :
|
|
Instrument(),
|
|
seg1Size(0),
|
|
seg2Size(0),
|
|
seg3Size(0) {}
|
|
|
|
int16 seg1Size;
|
|
int16 seg2Size;
|
|
int16 seg3Size;
|
|
};
|
|
|
|
class AmigaVoice : public Voice {
|
|
public:
|
|
AmigaVoice(MidiPlayer_Amiga0 &driver, uint id) :
|
|
Voice(driver, id),
|
|
_amigaDriver(driver) {}
|
|
|
|
private:
|
|
void noteOn(int8 note, int8 velocity) override;
|
|
void noteOff(int8 note) override;
|
|
void setPitchWheel(int16 pitch) override;
|
|
|
|
void stop() override;
|
|
void setEnvelopeVolume(byte volume) override;
|
|
|
|
void calcVoiceStep();
|
|
|
|
MidiPlayer_Amiga0 &_amigaDriver;
|
|
};
|
|
|
|
uint _defaultInstrument;
|
|
bool _isEarlyDriver;
|
|
|
|
bool loadInstruments(Common::SeekableReadStream &patch);
|
|
|
|
uint16 _periodTable[333];
|
|
};
|
|
|
|
MidiPlayer_Amiga0::MidiPlayer_Amiga0(SciVersion version, Audio::Mixer *mixer) :
|
|
Audio::Paula(true, mixer->getOutputRate(), mixer->getOutputRate() / kBaseFreq),
|
|
MidiPlayer_AmigaMac0(version, mixer, _mutex),
|
|
_defaultInstrument(0),
|
|
_isEarlyDriver(false) {
|
|
|
|
// These values are close, but not identical to the original
|
|
for (int i = 0; i < ARRAYSIZE(_periodTable); ++i)
|
|
_periodTable[i] = 3579545 / 20000.0 / pow(2.0, (i - 308) / 48.0);
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::AmigaVoice::setEnvelopeVolume(byte volume) {
|
|
// Early games ignore note velocity for envelope-enabled notes
|
|
if (_amigaDriver._isEarlyDriver)
|
|
_amigaDriver.setChannelVolume(_id, volume * _amigaDriver._masterVolume >> 4);
|
|
else
|
|
_amigaDriver.setChannelVolume(_id, (volume * _amigaDriver._masterVolume >> 4) * _volume >> 6);
|
|
}
|
|
|
|
int MidiPlayer_Amiga0::open(ResourceManager *resMan) {
|
|
if (_isOpen)
|
|
return MidiDriver::MERR_ALREADY_OPEN;
|
|
|
|
_isEarlyDriver = g_sci->getGameId() == GID_LSL2 || g_sci->getGameId() == GID_SQ3;
|
|
|
|
Common::File file;
|
|
|
|
if (!file.open("bank.001")) {
|
|
warning("MidiPlayer_Amiga0: Failed to open bank.001");
|
|
return false;
|
|
}
|
|
|
|
if (!loadInstruments(file)) {
|
|
freeInstruments();
|
|
return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
|
|
}
|
|
|
|
for (byte vi = 0; vi < NUM_VOICES; ++vi)
|
|
_voices.push_back(new AmigaVoice(*this, vi));
|
|
|
|
startPaula();
|
|
// Enable reverse stereo to counteract Audio::Paula's reverse stereo
|
|
_mixer->playStream(Audio::Mixer::kPlainSoundType, &_mixerSoundHandle, this, -1, _mixer->kMaxChannelVolume, 0, DisposeAfterUse::NO, false, true);
|
|
_isOpen = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::close() {
|
|
MidiPlayer_AmigaMac0::close();
|
|
clearVoices();
|
|
stopPaula();
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::AmigaVoice::stop() {
|
|
_amigaDriver.clearVoice(_id);
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::AmigaVoice::calcVoiceStep() {
|
|
int8 note = _note;
|
|
|
|
if (_instrument->fixedNote)
|
|
note = 101;
|
|
|
|
int16 index = (note + _instrument->transpose) * 4;
|
|
|
|
if (_pitch >= 0x2000)
|
|
index += (_pitch - 0x2000) / 171;
|
|
else
|
|
index -= (0x2000 - _pitch) / 171;
|
|
|
|
// For very high notes, the original driver reads out of bounds
|
|
// (see e.g. SQ3 intro sequence). We compute the period for
|
|
// these notes. The original hardware would not be able to
|
|
// handle these very low periods, but Audio::Paula doesn't
|
|
// seem to mind.
|
|
|
|
while (index < 96)
|
|
index += 48;
|
|
|
|
index -= 96;
|
|
|
|
while (index >= ARRAYSIZE(_amigaDriver._periodTable))
|
|
index -= 48;
|
|
|
|
_amigaDriver.setChannelPeriod(_id, _amigaDriver._periodTable[index]);
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::AmigaVoice::noteOn(int8 note, int8 velocity) {
|
|
if (velocity == 0) {
|
|
noteOff(note);
|
|
return;
|
|
}
|
|
|
|
_instrument = _amigaDriver._instruments[_patch];
|
|
|
|
// Default to the first instrument in the bank
|
|
if (!_instrument)
|
|
_instrument = _amigaDriver._instruments[_amigaDriver._defaultInstrument];
|
|
|
|
_velocity = velocity;
|
|
_volume = velocity >> 1;
|
|
_loop = _instrument->loop;
|
|
_note = note;
|
|
|
|
stop();
|
|
_envState = 0;
|
|
|
|
calcVoiceStep();
|
|
|
|
const AmigaInstrument *ins = static_cast<const AmigaInstrument *>(_instrument);
|
|
|
|
const int8 *seg1 = (const int8 *)ins->samples;
|
|
const int8 *seg2 = seg1;
|
|
int16 seg1Size = ins->seg1Size;
|
|
seg2 += ins->seg2Offset & 0xfffe;
|
|
int16 seg2Size = ins->seg2Size;
|
|
|
|
if (!_loop) {
|
|
seg1Size = seg1Size + seg2Size + ins->seg3Size;
|
|
seg2 = nullptr;
|
|
seg2Size = 0;
|
|
}
|
|
|
|
if (ins->envelope[0].skip != 0 && _loop) {
|
|
_envCurVel = _volume;
|
|
_envCntDown = 0;
|
|
_envState = 1;
|
|
}
|
|
|
|
_amigaDriver.setChannelData(_id, seg1, seg2, seg1Size * 2, seg2Size * 2);
|
|
if (_amigaDriver._playSwitch)
|
|
_amigaDriver.setChannelVolume(_id, _amigaDriver._masterVolume * _volume >> 4);
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::AmigaVoice::noteOff(int8 note) {
|
|
if (_note == note) {
|
|
if (_envState != 0) {
|
|
_envCurVel = _instrument->envelope[1].target;
|
|
_envState = 4;
|
|
}
|
|
// Original driver doesn't reset note anywhere that I could find,
|
|
// but this seems like a good place to do that
|
|
_note = -1;
|
|
}
|
|
}
|
|
|
|
void MidiPlayer_Amiga0::AmigaVoice::setPitchWheel(int16 pitch) {
|
|
if (_amigaDriver._isEarlyDriver)
|
|
return;
|
|
|
|
_pitch = pitch;
|
|
|
|
if (_note != -1)
|
|
calcVoiceStep();
|
|
}
|
|
|
|
bool MidiPlayer_Amiga0::loadInstruments(Common::SeekableReadStream &patch) {
|
|
char name[31];
|
|
|
|
if (patch.read(name, 8) < 8 || strncmp(name, "X0iUo123", 8) != 0) {
|
|
warning("MidiPlayer_Amiga0: Incorrect ID string in patch bank");
|
|
return false;
|
|
}
|
|
|
|
if (patch.read(name, 30) < 30) {
|
|
warning("MidiPlayer_Amiga0: Error reading patch bank");
|
|
return false;
|
|
}
|
|
name[30] = 0;
|
|
|
|
debugC(kDebugLevelSound, "Bank: '%s'", name);
|
|
|
|
_instruments.resize(128);
|
|
|
|
const uint16 instrumentCount = patch.readUint16BE();
|
|
|
|
if (instrumentCount == 0) {
|
|
warning("MidiPlayer_Amiga0: No instruments found in patch bank");
|
|
return false;
|
|
}
|
|
|
|
for (uint i = 0; i < instrumentCount; ++i) {
|
|
AmigaInstrument *instrument = new AmigaInstrument();
|
|
|
|
const uint16 patchIdx = patch.readUint16BE();
|
|
_instruments[patchIdx] = instrument;
|
|
|
|
if (i == 0)
|
|
_defaultInstrument = patchIdx;
|
|
|
|
patch.read(instrument->name, 30);
|
|
instrument->name[30] = 0;
|
|
const uint16 flags = patch.readUint16BE();
|
|
instrument->loop = flags & 1;
|
|
instrument->fixedNote = !(flags & 2);
|
|
instrument->transpose = patch.readSByte();
|
|
instrument->seg1Size = patch.readSint16BE();
|
|
instrument->seg2Offset = patch.readUint32BE();
|
|
instrument->seg2Size = patch.readSint16BE();
|
|
instrument->seg3Offset = patch.readUint32BE();
|
|
instrument->seg3Size = patch.readSint16BE();
|
|
|
|
// There's some envelope-related bugs here in the original, these were not replicated
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
instrument->envelope[stage].skip = patch.readByte();
|
|
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
instrument->envelope[stage].step = patch.readByte();
|
|
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
instrument->envelope[stage].target = patch.readByte();
|
|
|
|
int32 sampleSize = instrument->seg1Size + instrument->seg2Size + instrument->seg3Size;
|
|
sampleSize <<= 1;
|
|
byte *samples = new byte[sampleSize];
|
|
patch.read(samples, sampleSize);
|
|
instrument->samples = samples;
|
|
|
|
if (patch.eos() || patch.err()) {
|
|
warning("MidiPlayer_Amiga0: Error reading patch bank");
|
|
return false;
|
|
}
|
|
|
|
debugC(kDebugLevelSound, "\tInstrument[%d]: '%s'", patchIdx, instrument->name);
|
|
debugC(kDebugLevelSound, "\t\tSegment 1: offset 0, size %d", instrument->seg1Size * 2);
|
|
debugC(kDebugLevelSound, "\t\tSegment 2: offset %d, size %d", instrument->seg2Offset, instrument->seg2Size * 2);
|
|
debugC(kDebugLevelSound, "\t\tSegment 3: offset %d, size %d", instrument->seg3Offset, instrument->seg3Size * 2);
|
|
debugC(kDebugLevelSound, "\t\tTranspose = %d, Fixed note = %d, Loop = %d", instrument->transpose, instrument->fixedNote, instrument->loop);
|
|
debugC(kDebugLevelSound, "\t\tEnvelope:");
|
|
for (uint stage = 0; stage < ARRAYSIZE(instrument->envelope); ++stage)
|
|
debugC(kDebugLevelSound, "\t\t\tStage %d: skip %d, step %d, target %d", stage, instrument->envelope[stage].skip, instrument->envelope[stage].step, instrument->envelope[stage].target);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
MidiPlayer *MidiPlayer_AmigaMac0_create(SciVersion version, Common::Platform platform) {
|
|
if (platform == Common::kPlatformMacintosh)
|
|
return new MidiPlayer_Mac0(version, g_system->getMixer(), MidiPlayer_Mac0::kModeHq);
|
|
else
|
|
return new MidiPlayer_Amiga0(version, g_system->getMixer());
|
|
}
|
|
|
|
} // End of namespace Sci
|