mirror of
https://github.com/libretro/scummvm.git
synced 2024-11-23 17:29:49 +00:00
ULTIMA/NUVIE: Rework music code
This commit adds a MidiParser implementation for Ultima 6's M format and MidiDrivers for AdLib and MT-32. This replaces the old implementation based on AdPlug, which supports AdLib only and does not have ScummVM's support for various OPL emulators and devices. Music for Savage Empire and Martian Dreams has been temporarily disabled, because these games use a different music format and there is no MidiParser yet.
This commit is contained in:
parent
46b44bf344
commit
36001426dc
@ -400,6 +400,7 @@ MidiDriver_ADLIB_Multisource::MidiDriver_ADLIB_Multisource(OPL::Config::OplType
|
||||
_isOpen(false),
|
||||
_accuracyMode(ACCURACY_MODE_SB16_WIN95),
|
||||
_allocationMode(ALLOCATION_MODE_DYNAMIC),
|
||||
_instrumentWriteMode(INSTRUMENT_WRITE_MODE_NOTE_ON),
|
||||
_rhythmModeIgnoreNoteOffs(false),
|
||||
_defaultChannelVolume(0),
|
||||
_noteSelect(NOTE_SELECT_MODE_0),
|
||||
@ -690,8 +691,10 @@ void MidiDriver_ADLIB_Multisource::noteOn(uint8 channel, uint8 note, uint8 veloc
|
||||
activeNote->instrumentId = instrument.instrumentId;
|
||||
activeNote->instrumentDef = instrument.instrumentDef;
|
||||
|
||||
// Write out the instrument definition, volume and panning.
|
||||
writeInstrument(oplChannel, instrument);
|
||||
if (_instrumentWriteMode == INSTRUMENT_WRITE_MODE_NOTE_ON) {
|
||||
// Write out the instrument definition, volume and panning.
|
||||
writeInstrument(oplChannel, instrument);
|
||||
}
|
||||
|
||||
// Calculate and write frequency and block and write key on bit.
|
||||
writeFrequency(oplChannel, instrument.instrumentDef->rhythmType);
|
||||
@ -762,6 +765,41 @@ void MidiDriver_ADLIB_Multisource::controlChange(uint8 channel, uint8 controller
|
||||
void MidiDriver_ADLIB_Multisource::programChange(uint8 channel, uint8 program, uint8 source) {
|
||||
// Just set the MIDI program value; this event does not affect active notes.
|
||||
_controlData[source][channel].program = program;
|
||||
|
||||
if (_instrumentWriteMode == INSTRUMENT_WRITE_MODE_PROGRAM_CHANGE && !(_rhythmMode && channel == MIDI_RHYTHM_CHANNEL)) {
|
||||
InstrumentInfo instrument = determineInstrument(channel, source, 0);
|
||||
|
||||
if (!instrument.instrumentDef || instrument.instrumentDef->isEmpty()) {
|
||||
// Instrument definition contains no data.
|
||||
return;
|
||||
}
|
||||
|
||||
_activeNotesMutex.lock();
|
||||
|
||||
// Determine the OPL channel to use and the active note data to update.
|
||||
uint8 oplChannel = 0xFF;
|
||||
ActiveNote *activeNote = nullptr;
|
||||
// Allocate a melodic OPL channel.
|
||||
oplChannel = allocateOplChannel(channel, source, instrument.instrumentId);
|
||||
if (oplChannel != 0xFF) {
|
||||
activeNote = &_activeNotes[oplChannel];
|
||||
if (activeNote->noteActive) {
|
||||
// Turn off the note currently playing on this OPL channel or
|
||||
// rhythm instrument.
|
||||
writeKeyOff(oplChannel, instrument.instrumentDef->rhythmType);
|
||||
}
|
||||
|
||||
// Update the active note data.
|
||||
activeNote->channel = channel;
|
||||
activeNote->source = source;
|
||||
activeNote->instrumentId = instrument.instrumentId;
|
||||
activeNote->instrumentDef = instrument.instrumentDef;
|
||||
|
||||
writeInstrument(oplChannel, instrument);
|
||||
}
|
||||
|
||||
_activeNotesMutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void MidiDriver_ADLIB_Multisource::channelAftertouch(uint8 channel, uint8 pressure, uint8 source) {
|
||||
|
@ -279,6 +279,26 @@ public:
|
||||
ALLOCATION_MODE_STATIC
|
||||
};
|
||||
|
||||
/**
|
||||
* The available modes for writing the instrument definition to a channel.
|
||||
*/
|
||||
enum InstrumentWriteMode {
|
||||
/**
|
||||
* Will write the instrument definition before each note on event.
|
||||
* Works with both dynamic and static channel allocation modes, but
|
||||
* is less efficient and resets all parameters of the instrument when
|
||||
* a note is played.
|
||||
*/
|
||||
INSTRUMENT_WRITE_MODE_NOTE_ON,
|
||||
/**
|
||||
* Will write the instrument definition after a program change event.
|
||||
* This will only work with a static channel allocation mode. It will
|
||||
* write the instrument only once for many notes and allows parameters
|
||||
* of the instrument to be changed for the following notes.
|
||||
*/
|
||||
INSTRUMENT_WRITE_MODE_PROGRAM_CHANGE
|
||||
};
|
||||
|
||||
/**
|
||||
* The available modes for the OPL note select setting.
|
||||
*/
|
||||
@ -1074,7 +1094,7 @@ protected:
|
||||
* calculated and written. Use type undefined to calculate volume for a
|
||||
* melodic instrument.
|
||||
*/
|
||||
void writeVolume(uint8 oplChannel, uint8 operatorNum, OplInstrumentRhythmType rhythmType = RHYTHM_TYPE_UNDEFINED);
|
||||
virtual void writeVolume(uint8 oplChannel, uint8 operatorNum, OplInstrumentRhythmType rhythmType = RHYTHM_TYPE_UNDEFINED);
|
||||
/**
|
||||
* Calculates the panning for the specified OPL channel or rhythm type
|
||||
* (@see calculatePanning) and writes the new value to the OPL registers.
|
||||
@ -1126,6 +1146,8 @@ protected:
|
||||
AccuracyMode _accuracyMode;
|
||||
// Controls the OPL channel allocation behavior.
|
||||
ChannelAllocationMode _allocationMode;
|
||||
// Controls when the instrument definitions are written.
|
||||
InstrumentWriteMode _instrumentWriteMode;
|
||||
// Controls response to rhythm note off events when rhythm mode is active.
|
||||
bool _rhythmModeIgnoreNoteOffs;
|
||||
|
||||
|
@ -446,7 +446,7 @@ public:
|
||||
virtual void setMidiDriver(MidiDriver_BASE *driver) { _driver = driver; }
|
||||
void setTimerRate(uint32 rate) { _timerRate = rate; }
|
||||
virtual void setTempo(uint32 tempo);
|
||||
void onTimer();
|
||||
virtual void onTimer();
|
||||
|
||||
bool isPlaying() const { return (_position._playPos != 0 && _doParse); }
|
||||
/**
|
||||
|
@ -334,10 +334,14 @@ MODULE_OBJS := \
|
||||
nuvie/script/script_cutscene.o \
|
||||
nuvie/sound/adlib_sfx_manager.o \
|
||||
nuvie/sound/custom_sfx_manager.o \
|
||||
nuvie/sound/mididrv_m_adlib.o \
|
||||
nuvie/sound/mididrv_m_mt32.o \
|
||||
nuvie/sound/midiparser_m.o \
|
||||
nuvie/sound/origin_fx_adib_driver.o \
|
||||
nuvie/sound/pc_speaker_sfx_manager.o \
|
||||
nuvie/sound/song.o \
|
||||
nuvie/sound/song_adplug.o \
|
||||
nuvie/sound/song_filename.o \
|
||||
nuvie/sound/sound_manager.o \
|
||||
nuvie/sound/towns_sfx_manager.o \
|
||||
nuvie/sound/adplug/adplug_player.o \
|
||||
|
@ -234,19 +234,7 @@ void NuvieEngine::syncSoundSettings() {
|
||||
if (!_soundManager)
|
||||
return;
|
||||
|
||||
_soundManager->set_audio_enabled(
|
||||
!ConfMan.hasKey("mute") || !ConfMan.getBool("mute"));
|
||||
_soundManager->set_sfx_enabled(
|
||||
!ConfMan.hasKey("sfx_mute") || !ConfMan.getBool("sfx_mute"));
|
||||
_soundManager->set_music_enabled(
|
||||
!ConfMan.hasKey("music_mute") || !ConfMan.getBool("music_mute"));
|
||||
_soundManager->set_speech_enabled(
|
||||
!ConfMan.hasKey("speech_mute") || !ConfMan.getBool("speech_mute"));
|
||||
|
||||
_soundManager->set_sfx_volume(ConfMan.hasKey("sfx_volume") ?
|
||||
ConfMan.getInt("sfx_volume") : 255);
|
||||
_soundManager->set_music_volume(ConfMan.hasKey("music_volume") ?
|
||||
ConfMan.getInt("music_volume") : 255);
|
||||
_soundManager->syncSoundSettings();
|
||||
}
|
||||
|
||||
bool NuvieEngine::canLoadGameStateCurrently(bool isAutosave) {
|
||||
|
436
engines/ultima/nuvie/sound/mididrv_m_adlib.cpp
Normal file
436
engines/ultima/nuvie/sound/mididrv_m_adlib.cpp
Normal file
@ -0,0 +1,436 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "mididrv_m_adlib.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
const uint16 MidiDriver_M_AdLib::FNUM_VALUES[24] = {
|
||||
0x0, 0x158, 0x182, 0x1B0, 0x1CC, 0x203, 0x241, 0x286,
|
||||
0x0, 0x16A, 0x196, 0x1C7, 0x1E4, 0x21E, 0x25F, 0x2A8,
|
||||
0x0, 0x147, 0x16E, 0x19A, 0x1B5, 0x1E9, 0x224, 0x266
|
||||
};
|
||||
|
||||
MidiDriver_M_AdLib::MidiDriver_M_AdLib() : MidiDriver_ADLIB_Multisource(OPL::Config::kOpl2, 60) {
|
||||
_modulationDepth = MODULATION_DEPTH_LOW;
|
||||
_vibratoDepth = VIBRATO_DEPTH_LOW;
|
||||
_allocationMode = ALLOCATION_MODE_STATIC;
|
||||
_instrumentWriteMode = INSTRUMENT_WRITE_MODE_PROGRAM_CHANGE;
|
||||
|
||||
Common::fill(_slideValues, _slideValues + ARRAYSIZE(_slideValues), 0);
|
||||
Common::fill(_vibratoDepths, _vibratoDepths + ARRAYSIZE(_vibratoDepths), 0);
|
||||
Common::fill(_vibratoFactors, _vibratoFactors + ARRAYSIZE(_vibratoFactors), 0);
|
||||
Common::fill(_vibratoCurrentDepths, _vibratoCurrentDepths + ARRAYSIZE(_vibratoCurrentDepths), 0);
|
||||
Common::fill(_vibratoDirections, _vibratoDirections + ARRAYSIZE(_vibratoDirections), VIBRATO_DIRECTION_RISING);
|
||||
Common::fill(_fadeDirections, _fadeDirections + ARRAYSIZE(_fadeDirections), FADE_DIRECTION_NONE);
|
||||
Common::fill(_fadeStepDelays, _fadeStepDelays + ARRAYSIZE(_fadeStepDelays), 0);
|
||||
Common::fill(_fadeCurrentDelays, _fadeCurrentDelays + ARRAYSIZE(_fadeCurrentDelays), 0);
|
||||
|
||||
_instrumentBank = new OplInstrumentDefinition[16];
|
||||
}
|
||||
|
||||
MidiDriver_M_AdLib::~MidiDriver_M_AdLib() {
|
||||
delete[] _instrumentBank;
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::send(int8 source, uint32 b) {
|
||||
byte command = b & 0xF0;
|
||||
byte channel = b & 0x0F;
|
||||
byte data = (b >> 8) & 0xFF;
|
||||
|
||||
ActiveNote *activeNote;
|
||||
//uint16 channelOffset;
|
||||
//uint16 frequency;
|
||||
switch (command) {
|
||||
case 0x00: // Note off
|
||||
// The original driver always writes both F-num registers with the
|
||||
// supplied note value; it does not check what the active note value
|
||||
// is. The standard noteOff implementation checks if the active note
|
||||
// value matches an active note on the data channel. If the note off
|
||||
// does not match the active note, this could cause a hanging note.
|
||||
// None of the Ultima 6 tracks seem to have this problem however.
|
||||
|
||||
/* DEBUG: Write Ax register
|
||||
// Calculate the frequency.
|
||||
channelOffset = determineChannelRegisterOffset(channel);
|
||||
frequency = calculateFrequency(channel, source, data);
|
||||
|
||||
// Write the low 8 frequency bits.
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMLOW + channelOffset, frequency & 0xFF);
|
||||
*/
|
||||
|
||||
noteOff(channel, data, 0, source);
|
||||
break;
|
||||
|
||||
case 0x10: // Note on
|
||||
// Stop vibrato (if active)
|
||||
_vibratoDirections[channel] = VIBRATO_DIRECTION_RISING;
|
||||
_vibratoCurrentDepths[channel] = 0;
|
||||
|
||||
// The original driver always writes a note off before a note on, even
|
||||
// if there is no note active. The standard noteOn implementation only
|
||||
// writes a note off if a note is active. This causes no audible
|
||||
// difference however.
|
||||
|
||||
/* DEBUG: Write note off
|
||||
_activeNotesMutex.lock();
|
||||
|
||||
// Melodic instrument.
|
||||
activeNote = &_activeNotes[channel];
|
||||
|
||||
// Calculate the frequency.
|
||||
channelOffset = determineChannelRegisterOffset(channel);
|
||||
frequency = calculateFrequency(channel, source, data);
|
||||
activeNote->oplFrequency = frequency;
|
||||
|
||||
// Write the low 8 frequency bits.
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMLOW + channelOffset, frequency & 0xFF);
|
||||
// Write the high 2 frequency bits and block and add the key on bit.
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMHIGH_BLOCK_KEYON + channelOffset, frequency >> 8);
|
||||
|
||||
// Update the active note data.
|
||||
activeNote->noteActive = false;
|
||||
activeNote->noteSustained = false;
|
||||
// Register the current note counter value when turning off a note.
|
||||
activeNote->noteCounterValue = _noteCounter;
|
||||
|
||||
_activeNotesMutex.unlock();
|
||||
*/
|
||||
|
||||
noteOn(channel, data, 0x7F, source);
|
||||
break;
|
||||
|
||||
case 0x20: // Set pitch
|
||||
// If a note is already active on this channel, this will just update
|
||||
// the pitch. Otherwise it is the same as a Note on.
|
||||
|
||||
_activeNotesMutex.lock();
|
||||
|
||||
// Determine the OPL channel to use and the active note data to update.
|
||||
uint8 oplChannel;
|
||||
oplChannel = 0xFF;
|
||||
activeNote = nullptr;
|
||||
// Allocate a melodic OPL channel.
|
||||
oplChannel = allocateOplChannel(channel, source, 0);
|
||||
if (oplChannel != 0xFF)
|
||||
activeNote = &_activeNotes[oplChannel];
|
||||
|
||||
if (activeNote != nullptr) {
|
||||
if (!activeNote->noteActive) {
|
||||
// If there is no note active currently, treat this as a
|
||||
// regular note on.
|
||||
noteOn(channel, data, 0x7F, source);
|
||||
} else {
|
||||
// If there is a note active, only update the frequency.
|
||||
activeNote->note = data;
|
||||
activeNote->oplNote = data;
|
||||
// Calculate and write frequency and block and write key on bit.
|
||||
writeFrequency(oplChannel);
|
||||
}
|
||||
}
|
||||
|
||||
_activeNotesMutex.unlock();
|
||||
|
||||
break;
|
||||
|
||||
case 0x30: // Set level
|
||||
// This directly writes the OPL level register of the carrier operator.
|
||||
// This can also write the key scale level bits.
|
||||
// Note that the control data volume field is used for an OPL level
|
||||
// value, not for a MIDI channel volume value as usual.
|
||||
|
||||
// Stop fade (if active)
|
||||
_fadeDirections[channel] = FADE_DIRECTION_NONE;
|
||||
|
||||
_controlData[source][channel].volume = data;
|
||||
if (_activeNotes[channel].instrumentDef)
|
||||
writeVolume(channel, 1);
|
||||
|
||||
break;
|
||||
|
||||
case 0x40: // Set modulation
|
||||
modulation(channel, data, source);
|
||||
break;
|
||||
|
||||
case 0x50: // Set slide
|
||||
// Start or stop a pitch slide. The slide is processed by onTimer.
|
||||
_slideValues[channel] = (int8)data;
|
||||
break;
|
||||
|
||||
case 0x60: // Set vibrato
|
||||
// Turns vibrato on or off or modifies the parameters. High nibble
|
||||
// is the vibrato depth, low nibble is the vibrato factor. The vibrato
|
||||
// is processed by onTimer.
|
||||
_vibratoDepths[channel] = data >> 4;
|
||||
_vibratoFactors[channel] = data & 0xF;
|
||||
break;
|
||||
|
||||
case 0x70: // Program change
|
||||
programChange(channel, data, source);
|
||||
break;
|
||||
|
||||
case 0x80: // Subcommand
|
||||
uint8 subcommand;
|
||||
subcommand = channel;
|
||||
switch (subcommand) {
|
||||
case 0x1: // Call subroutine
|
||||
case 0x2: // Delay
|
||||
// These are handled by the parser.
|
||||
break;
|
||||
|
||||
case 0x3: // Load instrument
|
||||
// This should be sent to the driver as a meta event.
|
||||
warning("MidiDriver_M_AdLib::send - Received load instrument as command");
|
||||
break;
|
||||
|
||||
case 0x5: // Fade out
|
||||
case 0x6: // Fade in
|
||||
// Starts a volume fade in or out. The high nibble of the data byte
|
||||
// is the channel, the low nibble is the fade delay. The fade is
|
||||
// processed by onTimer.
|
||||
|
||||
channel = data >> 4;
|
||||
_fadeDirections[channel] = (subcommand == 0x5 ? FADE_DIRECTION_FADE_OUT : FADE_DIRECTION_FADE_IN);
|
||||
uint8 delay;
|
||||
delay = (data & 0xF) + 1;
|
||||
_fadeStepDelays[channel] = _fadeCurrentDelays[channel] = delay;
|
||||
break;
|
||||
|
||||
default: // Unknown subcommand
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xE0: // Set loop point
|
||||
case 0xF0: // Return
|
||||
// These are handled by the parser.
|
||||
break;
|
||||
|
||||
default: // Unknown command
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::metaEvent(int8 source, byte type, byte* data, uint16 length) {
|
||||
if (type == 0x3) {
|
||||
// Load instrument
|
||||
// This loads an OPL instrument definition into the bank. The first
|
||||
// byte is the instrument bank number. The next 11 bytes contain the
|
||||
// instrument parameters.
|
||||
|
||||
if (length < 12) {
|
||||
warning("Received a load instrument event with insufficient data length");
|
||||
return;
|
||||
}
|
||||
|
||||
byte instrumentNumber = data[0];
|
||||
assert(instrumentNumber < 16);
|
||||
OplInstrumentDefinition *instrument = &_instrumentBank[instrumentNumber];
|
||||
|
||||
instrument->fourOperator = false;
|
||||
instrument->rhythmType = RHYTHM_TYPE_UNDEFINED;
|
||||
|
||||
instrument->operator0.freqMultMisc = data[1];
|
||||
instrument->operator0.level = data[2];
|
||||
instrument->operator0.decayAttack = data[3];
|
||||
instrument->operator0.releaseSustain = data[4];
|
||||
instrument->operator0.waveformSelect = data[5];
|
||||
|
||||
instrument->operator1.freqMultMisc = data[6];
|
||||
instrument->operator1.level = data[7];
|
||||
instrument->operator1.decayAttack = data[8];
|
||||
instrument->operator1.releaseSustain = data[9];
|
||||
instrument->operator1.waveformSelect = data[10];
|
||||
|
||||
instrument->connectionFeedback0 = data[11];
|
||||
}
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::programChange(uint8 channel, uint8 program, uint8 source) {
|
||||
assert(program < 16);
|
||||
|
||||
// Changing the instrument overwrites the current volume and modulation
|
||||
// settings.
|
||||
_controlData[source][channel].volume = _instrumentBank[program].operator1.level;
|
||||
_controlData[source][channel].modulation = _instrumentBank[program].operator0.level;
|
||||
|
||||
// Note that this will turn off an active note on the channel if there is
|
||||
// one. The original driver does not do this. Changing the instrument while
|
||||
// a note is playing would be strange though; none of the tracks in
|
||||
// Ultima 6 seem to do this.
|
||||
MidiDriver_ADLIB_Multisource::programChange(channel, program, source);
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::modulation(uint8 channel, uint8 modulation, uint8 source) {
|
||||
// This directly writes the OPL level register of the modulator
|
||||
// operator.
|
||||
|
||||
// Note that the control data modulation field is used for an OPL level
|
||||
// value, not for a MIDI channel modulation value as usual.
|
||||
_controlData[source][channel].modulation = modulation;
|
||||
|
||||
uint16 registerOffset = determineOperatorRegisterOffset(channel, 0);
|
||||
writeRegister(OPL_REGISTER_BASE_LEVEL + registerOffset, modulation);
|
||||
}
|
||||
|
||||
uint8 MidiDriver_M_AdLib::allocateOplChannel(uint8 channel, uint8 source, uint8 instrumentId) {
|
||||
// Allocation of M data channels to OPL output channels is simply 1 on 1.
|
||||
return channel;
|
||||
}
|
||||
|
||||
uint16 MidiDriver_M_AdLib::calculateFrequency(uint8 channel, uint8 source, uint8 note) {
|
||||
// An M note value consist of a note lookup value in the low 5 bits and
|
||||
// a block (octave) value in the high 3 bits.
|
||||
uint8 fnumIndex = note & 0x1F;
|
||||
assert(fnumIndex < 24);
|
||||
|
||||
uint16 oplFrequency = FNUM_VALUES[fnumIndex];
|
||||
uint8 block = note >> 5;
|
||||
|
||||
return oplFrequency | (block << 10);
|
||||
}
|
||||
|
||||
uint8 MidiDriver_M_AdLib::calculateUnscaledVolume(uint8 channel, uint8 source, uint8 velocity, OplInstrumentDefinition& instrumentDef, uint8 operatorNum) {
|
||||
// M directy uses OPL level values, so no calculation is necessary.
|
||||
return _controlData[source][channel].volume & OPL_MASK_LEVEL;
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::writeVolume(uint8 oplChannel, uint8 operatorNum, OplInstrumentRhythmType rhythmType) {
|
||||
ActiveNote *activeNote = (rhythmType == RHYTHM_TYPE_UNDEFINED ? &_activeNotes[oplChannel] : &_activeRhythmNotes[rhythmType - 1]);
|
||||
|
||||
// Calculate operator volume.
|
||||
uint16 registerOffset = determineOperatorRegisterOffset(
|
||||
oplChannel, operatorNum, rhythmType, activeNote->instrumentDef->fourOperator);
|
||||
uint8 level = calculateVolume(activeNote->channel, activeNote->source, activeNote->velocity,
|
||||
*activeNote->instrumentDef, operatorNum);
|
||||
|
||||
// Add key scaling level from the last written volume or modulation value
|
||||
// to the calculated level.
|
||||
MidiChannelControlData *controlData = &_controlData[activeNote->source][activeNote->channel];
|
||||
uint8 ksl = (operatorNum == 0 ? controlData->modulation : controlData->volume) & ~OPL_MASK_LEVEL;
|
||||
writeRegister(OPL_REGISTER_BASE_LEVEL + registerOffset, level | ksl);
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::deinitSource(uint8 source) {
|
||||
// Reset effects status.
|
||||
Common::fill(_slideValues, _slideValues + ARRAYSIZE(_slideValues), 0);
|
||||
Common::fill(_vibratoFactors, _vibratoFactors + ARRAYSIZE(_vibratoFactors), 0);
|
||||
Common::fill(_vibratoCurrentDepths, _vibratoCurrentDepths + ARRAYSIZE(_vibratoCurrentDepths), 0);
|
||||
Common::fill(_vibratoDirections, _vibratoDirections + ARRAYSIZE(_vibratoDirections), VIBRATO_DIRECTION_RISING);
|
||||
Common::fill(_fadeDirections, _fadeDirections + ARRAYSIZE(_fadeDirections), FADE_DIRECTION_NONE);
|
||||
|
||||
MidiDriver_ADLIB_Multisource::deinitSource(source);
|
||||
}
|
||||
|
||||
void MidiDriver_M_AdLib::onTimer() {
|
||||
MidiDriver_ADLIB_Multisource::onTimer();
|
||||
|
||||
_activeNotesMutex.lock();
|
||||
|
||||
// Process effects.
|
||||
for (int i = 8; i >= 0; i--) {
|
||||
ActiveNote *activeNote = &_activeNotes[i];
|
||||
|
||||
if (_slideValues[i] != 0) {
|
||||
// Process slide. A slide continually increases or decreases the
|
||||
// note frequency until it is turned off.
|
||||
|
||||
// Increase or decrease the OPL frequency by the slide value.
|
||||
// Note that this can potentially over- or underflow the OPL
|
||||
// frequency, but there is no bounds checking in the original
|
||||
// driver either.
|
||||
activeNote->oplFrequency += _slideValues[i];
|
||||
|
||||
// Write the low 8 frequency bits.
|
||||
uint16 channelOffset = determineChannelRegisterOffset(i);
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMLOW + channelOffset, activeNote->oplFrequency & 0xFF);
|
||||
// Write the high 2 frequency bits and block and add the key on bit.
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMHIGH_BLOCK_KEYON + channelOffset,
|
||||
(activeNote->oplFrequency >> 8) | (activeNote->noteActive ? OPL_MASK_KEYON : 0));
|
||||
} else if (_vibratoFactors[i] > 0 && activeNote->noteActive) {
|
||||
// Process vibrato. Vibrato will alternately increase and decrease
|
||||
// the frequency up to the maximum depth.
|
||||
// The depth is the difference between the minimum and maximum
|
||||
// frequency change, so a positive number, twice the amplitude.
|
||||
// The current depth is converted to the actual frequency offset by
|
||||
// subtracting half the total depth. The offset is then multiplied
|
||||
// by the vibrato factor.
|
||||
// Note that current depth starts at 0, so minimum depth, rather
|
||||
// than at neutral (half depth).
|
||||
|
||||
// Flip vibrato direction if the maximum or minimum depth has been reached.
|
||||
if (_vibratoCurrentDepths[i] >= _vibratoDepths[i]) {
|
||||
_vibratoDirections[i] = VIBRATO_DIRECTION_FALLING;
|
||||
} else if (_vibratoCurrentDepths[i] == 0) {
|
||||
_vibratoDirections[i] = VIBRATO_DIRECTION_RISING;
|
||||
}
|
||||
|
||||
// Update current depth.
|
||||
if (_vibratoDirections[i] == VIBRATO_DIRECTION_FALLING) {
|
||||
_vibratoCurrentDepths[i]--;
|
||||
} else {
|
||||
_vibratoCurrentDepths[i]++;
|
||||
}
|
||||
|
||||
// Convert the depth to an OPL frequency offset.
|
||||
int vibratoOffset = _vibratoCurrentDepths[i] - (_vibratoDepths[i] >> 1);
|
||||
vibratoOffset *= _vibratoFactors[i];
|
||||
|
||||
uint16 frequency = activeNote->oplFrequency + vibratoOffset;
|
||||
|
||||
// Write the low 8 frequency bits.
|
||||
uint16 channelOffset = determineChannelRegisterOffset(i);
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMLOW + channelOffset, frequency & 0xFF);
|
||||
// Write the high 2 frequency bits and block and add the key on bit.
|
||||
writeRegister(OPL_REGISTER_BASE_FNUMHIGH_BLOCK_KEYON + channelOffset,
|
||||
(frequency >> 8) | (activeNote->noteActive ? OPL_MASK_KEYON : 0));
|
||||
}
|
||||
|
||||
if (_fadeDirections[i] != FADE_DIRECTION_NONE && --_fadeCurrentDelays[i] == 0) {
|
||||
// Process fade. A fade will continually increase or decrease the
|
||||
// level (volume) until the maximum or minimum volume is reached.
|
||||
// Then the fade is stopped. A delay determines the speed of the
|
||||
// fade by increasing the number of ticks between each increase or
|
||||
// decrease.
|
||||
|
||||
// Reset delay.
|
||||
_fadeCurrentDelays[i] = _fadeStepDelays[i];
|
||||
|
||||
// Calculate new channel level.
|
||||
int newChannelLevel = _controlData[activeNote->source][i].volume + (_fadeDirections[i] == FADE_DIRECTION_FADE_IN ? -1 : 1);
|
||||
if (newChannelLevel < 0 || newChannelLevel > 0x3F) {
|
||||
// Minimum or maximum level reached. Stop the fade.
|
||||
newChannelLevel = (newChannelLevel < 0) ? 0 : 0x3F;
|
||||
_fadeDirections[i] = FADE_DIRECTION_NONE;
|
||||
}
|
||||
|
||||
// Apply the new volume.
|
||||
_controlData[activeNote->source][i].volume = newChannelLevel;
|
||||
writeVolume(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
_activeNotesMutex.unlock();
|
||||
}
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
102
engines/ultima/nuvie/sound/mididrv_m_adlib.h
Normal file
102
engines/ultima/nuvie/sound/mididrv_m_adlib.h
Normal file
@ -0,0 +1,102 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NUVIE_SOUND_MIDIDRV_M_ADLIB_H
|
||||
#define NUVIE_SOUND_MIDIDRV_M_ADLIB_H
|
||||
|
||||
#include "audio/adlib_ms.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
/**
|
||||
* M driver for AdLib (OPL2).
|
||||
* This driver supports several effects by adjusting OPL frequency and level
|
||||
* values based on timer ticks: slide, vibrato and fade in/out. Only vibrato is
|
||||
* used by the tracks in Ultima 6.
|
||||
*/
|
||||
class MidiDriver_M_AdLib : public MidiDriver_ADLIB_Multisource {
|
||||
protected:
|
||||
// The direction of a volume fade: in (increase) or out (decrease).
|
||||
enum FadeDirection {
|
||||
FADE_DIRECTION_NONE,
|
||||
FADE_DIRECTION_FADE_IN,
|
||||
FADE_DIRECTION_FADE_OUT
|
||||
};
|
||||
|
||||
// The current direction of vibrato pitch change.
|
||||
enum VibratoDirection {
|
||||
VIBRATO_DIRECTION_RISING,
|
||||
VIBRATO_DIRECTION_FALLING
|
||||
};
|
||||
|
||||
// Converts M note values to OPL frequency (F-num) values.
|
||||
static const uint16 FNUM_VALUES[24];
|
||||
|
||||
public:
|
||||
MidiDriver_M_AdLib();
|
||||
~MidiDriver_M_AdLib();
|
||||
|
||||
using MidiDriver_Multisource::send;
|
||||
void send(int8 source, uint32 b) override;
|
||||
void metaEvent(int8 source, byte type, byte *data, uint16 length) override;
|
||||
|
||||
protected:
|
||||
void programChange(uint8 channel, uint8 program, uint8 source) override;
|
||||
void modulation(uint8 channel, uint8 modulation, uint8 source) override;
|
||||
|
||||
uint8 allocateOplChannel(uint8 channel, uint8 source, uint8 instrumentId) override;
|
||||
uint16 calculateFrequency(uint8 channel, uint8 source, uint8 note) override;
|
||||
uint8 calculateUnscaledVolume(uint8 channel, uint8 source, uint8 velocity, OplInstrumentDefinition &instrumentDef, uint8 operatorNum) override;
|
||||
void writeVolume(uint8 oplChannel, uint8 operatorNum, OplInstrumentRhythmType rhythmType = RHYTHM_TYPE_UNDEFINED) override;
|
||||
|
||||
void deinitSource(uint8 source) override;
|
||||
|
||||
void onTimer() override;
|
||||
|
||||
// Number of F-num units each channel will increase/decrease each tick.
|
||||
int8 _slideValues[9];
|
||||
// Maximum number of F-num units the frequency will be changed by vibrato,
|
||||
// before applying the factor, for each channel. This is the difference
|
||||
// between the lowest and highest value (so twice the amplitude).
|
||||
uint8 _vibratoDepths[9];
|
||||
// Multiplication factor for vibrato F-num values for each channel.
|
||||
uint8 _vibratoFactors[9];
|
||||
// The current "progression" of vibrato through the cycle for each channel.
|
||||
// This is before the vibrato factor is applied.
|
||||
uint8 _vibratoCurrentDepths[9];
|
||||
// The current direction in which the vibrato is progressing for each
|
||||
// channel (rising or falling frequency).
|
||||
VibratoDirection _vibratoDirections[9];
|
||||
// The direction of the fade currently active on each channel (in or out).
|
||||
// NONE indicates no fade is active.
|
||||
FadeDirection _fadeDirections[9];
|
||||
// The delay in ticks between each level increase or decrease for each
|
||||
// channel.
|
||||
uint8 _fadeStepDelays[9];
|
||||
// The current fade delay counter value for each channel.
|
||||
uint8 _fadeCurrentDelays[9];
|
||||
};
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
||||
|
||||
#endif
|
205
engines/ultima/nuvie/sound/mididrv_m_mt32.cpp
Normal file
205
engines/ultima/nuvie/sound/mididrv_m_mt32.cpp
Normal file
@ -0,0 +1,205 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "mididrv_m_mt32.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
const uint8 MidiDriver_M_MT32::MIDI_NOTE_MAP[] = {
|
||||
0x00, 0x0C, 0x0E, 0x10, 0x11, 0x13, 0x15, 0x17,
|
||||
0x00, 0x0D, 0x0F, 0x11, 0x12, 0x14, 0x16, 0x18,
|
||||
0x00, 0x0B, 0x0D, 0x0F, 0x10, 0x12, 0x14, 0x16
|
||||
};
|
||||
|
||||
MidiDriver_M_MT32::MidiDriver_M_MT32() : MidiDriver_MT32GM(MT_MT32) {
|
||||
Common::fill(_mInstrumentMidiChannels, _mInstrumentMidiChannels + sizeof(_mInstrumentMidiChannels), 1);
|
||||
Common::fill(_mInstrumentMapping, _mInstrumentMapping + sizeof(_mInstrumentMapping), 0);
|
||||
}
|
||||
|
||||
MidiDriver_M_MT32::~MidiDriver_M_MT32() { }
|
||||
|
||||
int MidiDriver_M_MT32::open(MidiDriver *driver, bool nativeMT32) {
|
||||
int result = MidiDriver_MT32GM::open(driver, nativeMT32);
|
||||
if (result == 0)
|
||||
setInstrumentRemapping(_mInstrumentMapping);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MidiDriver_M_MT32::send(int8 source, uint32 b) {
|
||||
if (!_isOpen) {
|
||||
// During the opening of the driver, some MIDI commands are sent to
|
||||
// initialize the device. These are not M commands so they are sent
|
||||
// straight to the device.
|
||||
MidiDriver_MT32GM::send(source, b);
|
||||
return;
|
||||
}
|
||||
|
||||
byte mCommand = b & 0xF0;
|
||||
if (mCommand >= 0x80) {
|
||||
// These commands are either handled by the parser (call, return,
|
||||
// set loop point, delay) or are not implemented for MT-32
|
||||
// (load instrument, fade). Not all of them have the channel in the
|
||||
// low nibble, so they are filtered out here.
|
||||
return;
|
||||
}
|
||||
byte dataChannel = b & 0x0F;
|
||||
byte data = (b >> 8) & 0xFF;
|
||||
|
||||
MChannelData &mChannelData = _mChannelData[dataChannel];
|
||||
|
||||
// Get the MIDI output channel assigned to this M data channel.
|
||||
int8 outputChannel = source < 0 ? dataChannel : mapSourceChannel(source, dataChannel);
|
||||
if (outputChannel < 0) {
|
||||
warning("MidiDriver_M_MT32::send - Could not map data channel %i to an output channel", dataChannel);
|
||||
return;
|
||||
}
|
||||
|
||||
MidiChannelControlData &controlData = *_controlData[outputChannel];
|
||||
|
||||
byte midiNote;
|
||||
byte mNote;
|
||||
// Convert M to MIDI events
|
||||
switch (mCommand) {
|
||||
case 0x00: // Note off
|
||||
mNote = data & 0x1F;
|
||||
assert(mNote < 24);
|
||||
midiNote = MIDI_NOTE_MAP[mNote] + ((data >> 5) * 12);
|
||||
noteOnOff(outputChannel, MIDI_COMMAND_NOTE_OFF, midiNote, mChannelData.velocity, source, controlData);
|
||||
mChannelData.activeNote = -1;
|
||||
break;
|
||||
case 0x10: // Note on
|
||||
case 0x20: // Set pitch
|
||||
// In the original driver, for Note on events, Note off is explicitly
|
||||
// called first to turn off the previous note. However, the Note off
|
||||
// event is not sent if there is no note active. For Set pitch,
|
||||
// Note off is not explicitly called; Note on is called directly.
|
||||
// However, Note on turns off any active notes first before sending the
|
||||
// Note on event. So despite the different code paths, these events
|
||||
// effectively do the same thing: turn off the currently active note on
|
||||
// the channel, if there is one, then play the new note on the next
|
||||
// tick.
|
||||
|
||||
if (mChannelData.activeNote >= 0) {
|
||||
noteOnOff(outputChannel, MIDI_COMMAND_NOTE_OFF, mChannelData.activeNote, mChannelData.velocity, source, controlData);
|
||||
mChannelData.activeNote = -1;
|
||||
}
|
||||
|
||||
mNote = data & 0x1F;
|
||||
assert(mNote < 24);
|
||||
midiNote = MIDI_NOTE_MAP[mNote] + ((data >> 5) * 12);
|
||||
// The new note is queued for playback on the next timer tick
|
||||
// (see onTimer).
|
||||
if (mChannelData.queuedNote >= 0) {
|
||||
warning("MidiDriver_M_MT32::send - Note on on channel %i while a note is already queued", dataChannel);
|
||||
}
|
||||
mChannelData.queuedNote = midiNote;
|
||||
|
||||
break;
|
||||
case 0x30: // Set level
|
||||
// The OPL level is converted to a MIDI note velocity, which is used
|
||||
// for notes subsequently played on the M channel. The active note is
|
||||
// not affected.
|
||||
mChannelData.velocity = (0x3F - (data & 0x3F)) * 1.5;
|
||||
break;
|
||||
case 0x70: // Program change
|
||||
// When instrument assignments are set on the driver, each M instrument
|
||||
// is assigned to a fixed MIDI output channel. When a program change
|
||||
// event is encountered on an M channel, the MIDI output channel of
|
||||
// that M channel is changed to the MIDI channel assigned to the new M
|
||||
// instrument.
|
||||
uint8 newOutputChannel;
|
||||
assert(data < 16);
|
||||
newOutputChannel = _mInstrumentMidiChannels[data];
|
||||
if (newOutputChannel < 0) {
|
||||
warning("MidiDriver_M_MT32::send - Received program change for unmapped instrument %i", data);
|
||||
break;
|
||||
}
|
||||
if (newOutputChannel != outputChannel && mChannelData.activeNote >= 0) {
|
||||
// Turn off the active note.
|
||||
noteOnOff(outputChannel, MIDI_COMMAND_NOTE_OFF, mChannelData.activeNote, mChannelData.velocity, source, controlData);
|
||||
mChannelData.activeNote = -1;
|
||||
}
|
||||
_channelMap[source][dataChannel] = newOutputChannel;
|
||||
// Because the assignment of instruments to output channels is fixed,
|
||||
// a program change for each channel could be sent once when setting
|
||||
// instrument assignments. However, the original driver sends a program
|
||||
// change every time the instrument on an M channel is changed.
|
||||
programChange(newOutputChannel, data, source, controlData);
|
||||
break;
|
||||
default:
|
||||
// Modulation, slide and vibrato are not implemented for MT-32.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MidiDriver_M_MT32::metaEvent(int8 source, byte type, byte *data, uint16 length) {
|
||||
// Load instrument is ignored for MT-32; instruments are set using
|
||||
// setInstrumentAssignments.
|
||||
}
|
||||
|
||||
void MidiDriver_M_MT32::setInstrumentAssignments(const MInstrumentAssignment *assignments) {
|
||||
// Each M instrument used in the played track (up to 16) should get a MIDI
|
||||
// output channel and a MIDI instrument assigned to it. The MIDI instrument
|
||||
// is set on the output channel and when an M data channel switches to the
|
||||
// corresponding M instrument, the data channel is mapped to that output
|
||||
// channel.
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (assignments[i].midiChannel < 0)
|
||||
break;
|
||||
|
||||
_mInstrumentMidiChannels[i] = assignments[i].midiChannel;
|
||||
_mInstrumentMapping[i] = assignments[i].midiInstrument;
|
||||
}
|
||||
}
|
||||
|
||||
void MidiDriver_M_MT32::stopAllNotes(bool stopSustainedNotes) {
|
||||
MidiDriver_MT32GM::stopAllNotes();
|
||||
|
||||
// Clear active and queued notes.
|
||||
for (int i = 0; i < 9; i++) {
|
||||
_mChannelData[i].activeNote = -1;
|
||||
_mChannelData[i].queuedNote = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void MidiDriver_M_MT32::onTimer() {
|
||||
// Play the queued notes for each M channel.
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (_mChannelData[i].queuedNote >= 0) {
|
||||
int8 outputChannel = mapSourceChannel(0, i);
|
||||
if (outputChannel < 0) {
|
||||
warning("MidiDriver_M_MT32::onTimer - Could not map data channel %i to an output channel", i);
|
||||
continue;
|
||||
}
|
||||
MidiChannelControlData &controlData = *_controlData[outputChannel];
|
||||
noteOnOff(outputChannel, MIDI_COMMAND_NOTE_ON, _mChannelData[i].queuedNote, _mChannelData[i].velocity, 0, controlData);
|
||||
_mChannelData[i].activeNote = _mChannelData[i].queuedNote;
|
||||
_mChannelData[i].queuedNote = -1;
|
||||
}
|
||||
}
|
||||
|
||||
MidiDriver_MT32GM::onTimer();
|
||||
}
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
98
engines/ultima/nuvie/sound/mididrv_m_mt32.h
Normal file
98
engines/ultima/nuvie/sound/mididrv_m_mt32.h
Normal file
@ -0,0 +1,98 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NUVIE_SOUND_MIDIDRV_M_MT32_H
|
||||
#define NUVIE_SOUND_MIDIDRV_M_MT32_H
|
||||
|
||||
#include "audio/mt32gm.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
// An assignment of a MIDI instrument to a MIDI output channel. M data channels
|
||||
// using this instrument will be mapped to the specified MIDI channel.
|
||||
struct MInstrumentAssignment {
|
||||
int8 midiChannel;
|
||||
uint8 midiInstrument;
|
||||
};
|
||||
|
||||
/**
|
||||
* M driver for the Roland MT-32.
|
||||
* The M format is focused on OPL2 and conversion to MIDI is rudimentary. Only
|
||||
* note on/off, channel volume (using note velocity) and program change are
|
||||
* implemented by the original driver.
|
||||
* A mapping of M instruments to MIDI instruments must be set using
|
||||
* setInstrumentAssignments before starting playback of a track.
|
||||
*/
|
||||
class MidiDriver_M_MT32 : public MidiDriver_MT32GM {
|
||||
protected:
|
||||
/**
|
||||
* Playback status information for an M channel.
|
||||
* Note that although this data applies to an M data channel, the values
|
||||
* are MIDI note and velocity values.
|
||||
*/
|
||||
struct MChannelData {
|
||||
// The MIDI note currently played on this channel.
|
||||
int8 activeNote = -1;
|
||||
// The MIDI note velocity currently used on this channel.
|
||||
uint8 velocity = 0;
|
||||
// The MIDI note queued for playback on this channel.
|
||||
int8 queuedNote = -1;
|
||||
};
|
||||
|
||||
// Converts M note values to MIDI notes.
|
||||
static const uint8 MIDI_NOTE_MAP[24];
|
||||
|
||||
public:
|
||||
MidiDriver_M_MT32();
|
||||
~MidiDriver_M_MT32();
|
||||
|
||||
using MidiDriver_MT32GM::open;
|
||||
int open(MidiDriver *driver, bool nativeMT32) override;
|
||||
|
||||
using MidiDriver_MT32GM::send;
|
||||
void send(int8 source, uint32 b) override;
|
||||
void metaEvent(int8 source, byte type, byte *data, uint16 length) override;
|
||||
|
||||
/**
|
||||
* Sets the assignments of the 16 M instruments to the MIDI instruments and
|
||||
* MIDI output channels they should use.
|
||||
*
|
||||
* @param assignments An instrument assignment array of length 16
|
||||
*/
|
||||
void setInstrumentAssignments(const MInstrumentAssignment *assignments);
|
||||
|
||||
void stopAllNotes(bool stopSustainedNotes) override;
|
||||
|
||||
protected:
|
||||
void onTimer() override;
|
||||
|
||||
MChannelData _mChannelData[9];
|
||||
// Mapping of M instrument numbers to MIDI output channels
|
||||
int8 _mInstrumentMidiChannels[16];
|
||||
// Mapping of M instrument numbers to MIDI instrument numbers
|
||||
uint8 _mInstrumentMapping[16];
|
||||
};
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
||||
|
||||
#endif
|
247
engines/ultima/nuvie/sound/midiparser_m.cpp
Normal file
247
engines/ultima/nuvie/sound/midiparser_m.cpp
Normal file
@ -0,0 +1,247 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "midiparser_m.h"
|
||||
|
||||
#include "audio/mididrv.h"
|
||||
#include "audio/midiparser.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
MidiParser_M::MidiParser_M(int8 source) : MidiParser(source) {
|
||||
// M uses a fixed timer frequency of 60 Hz, or 16.667 ms per tick.
|
||||
_psecPerTick = 16667;
|
||||
|
||||
_trackLength = 0;
|
||||
|
||||
_loopPoint = nullptr;
|
||||
_loopStack = new Common::FixedStack<LoopData, 16>();
|
||||
}
|
||||
|
||||
MidiParser_M::~MidiParser_M() {
|
||||
delete _loopStack;
|
||||
}
|
||||
|
||||
bool MidiParser_M::loadMusic(byte* data, uint32 size) {
|
||||
unloadMusic();
|
||||
|
||||
// M uses only 1 track.
|
||||
_tracks[0] = data;
|
||||
_numTracks = 1;
|
||||
_trackLength = size;
|
||||
|
||||
// The global loop defaults to the start of the M data.
|
||||
_loopPoint = data;
|
||||
|
||||
resetTracking();
|
||||
setTrack(0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MidiParser_M::unloadMusic() {
|
||||
MidiParser::unloadMusic();
|
||||
_trackLength = 0;
|
||||
|
||||
_loopPoint = nullptr;
|
||||
_loopStack->clear();
|
||||
}
|
||||
|
||||
// MidiParser::onTimer does some checks based on MIDI note on/off command bytes
|
||||
// which have a different meaning in M, so those checks are removed here.
|
||||
void MidiParser_M::onTimer() {
|
||||
uint32 endTime;
|
||||
uint32 eventTime;
|
||||
|
||||
if (!_position._playPos || !_driver || !_doParse || _pause || !_driver->isReady(_source))
|
||||
return;
|
||||
|
||||
_abortParse = false;
|
||||
endTime = _position._playTime + _timerRate;
|
||||
|
||||
bool loopEvent = false;
|
||||
while (!_abortParse) {
|
||||
EventInfo &info = _nextEvent;
|
||||
|
||||
eventTime = _position._lastEventTime + info.delta * _psecPerTick;
|
||||
if (eventTime > endTime)
|
||||
break;
|
||||
|
||||
if (!info.noop) {
|
||||
// Process the next info.
|
||||
bool ret = processEvent(info);
|
||||
if (!ret)
|
||||
return;
|
||||
}
|
||||
|
||||
loopEvent |= info.loop;
|
||||
|
||||
if (!_abortParse) {
|
||||
_position._lastEventTime = eventTime;
|
||||
_position._lastEventTick += info.delta;
|
||||
parseNextEvent(_nextEvent);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_abortParse) {
|
||||
_position._playTime = endTime;
|
||||
_position._playTick = (_position._playTime - _position._lastEventTime) / _psecPerTick + _position._lastEventTick;
|
||||
if (loopEvent) {
|
||||
// One of the processed events has looped (part of) the MIDI data.
|
||||
// Infinite looping will cause the tracker to overflow eventually.
|
||||
// Reset the tracker positions to prevent this from happening.
|
||||
_position._playTime -= _position._lastEventTime;
|
||||
_position._lastEventTime = 0;
|
||||
_position._playTick -= _position._lastEventTick;
|
||||
_position._lastEventTick = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MidiParser_M::processEvent(const EventInfo& info, bool fireEvents) {
|
||||
if (info.command() == 0x8 && info.channel() == 0x1) {
|
||||
// Call subroutine
|
||||
LoopData loopData { };
|
||||
loopData.returnPos = _position._playPos;
|
||||
loopData.numLoops = info.ext.data[0];
|
||||
uint16 startOffset = READ_LE_UINT16(info.ext.data + 1);
|
||||
assert(startOffset < _trackLength);
|
||||
loopData.startPos = _tracks[0] + startOffset;
|
||||
_loopStack->push(loopData);
|
||||
_position._playPos = loopData.startPos;
|
||||
} else if (info.command() == 0xE) {
|
||||
// Set loop point
|
||||
_loopPoint = _position._playPos;
|
||||
} else if (info.command() == 0xF) {
|
||||
// Return
|
||||
if (_loopStack->empty()) {
|
||||
// Global loop: return to the global loop point
|
||||
_position._playPos = _loopPoint;
|
||||
} else {
|
||||
// Subroutine loop
|
||||
LoopData *loopData = &_loopStack->top();
|
||||
if (loopData->numLoops > 1) {
|
||||
// Return to the start of the subroutine data
|
||||
loopData->numLoops--;
|
||||
_position._playPos = loopData->startPos;
|
||||
} else {
|
||||
// Return to the call position
|
||||
_position._playPos = loopData->returnPos;
|
||||
_loopStack->pop();
|
||||
}
|
||||
}
|
||||
} else if (info.command() == 0x8 && info.channel() == 0x3) {
|
||||
// Load instrument
|
||||
if (fireEvents) {
|
||||
// Send the instrument data as a meta event
|
||||
sendMetaEventToDriver(info.ext.type, info.ext.data, (uint16)info.length);
|
||||
}
|
||||
} else if (fireEvents) {
|
||||
// Other events are handled by the driver
|
||||
sendToDriver(info.event, info.basic.param1, info.basic.param2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MidiParser_M::parseNextEvent(EventInfo &info) {
|
||||
assert(_position._playPos - _tracks[0] < _trackLength);
|
||||
info.start = _position._playPos;
|
||||
info.event = *(_position._playPos++);
|
||||
info.delta = 0;
|
||||
info.basic.param1 = 0;
|
||||
info.basic.param2 = 0;
|
||||
info.noop = false;
|
||||
info.loop = false;
|
||||
|
||||
switch (info.command()) {
|
||||
case 0x0: // Note off
|
||||
case 0x1: // Note on
|
||||
case 0x2: // Set pitch
|
||||
case 0x3: // Set level
|
||||
case 0x4: // Set modulation
|
||||
case 0x5: // Set slide
|
||||
case 0x6: // Set vibrato
|
||||
case 0x7: // Program change
|
||||
// These commands all have 1 data byte.
|
||||
info.basic.param1 = *(_position._playPos++);
|
||||
break;
|
||||
|
||||
case 0x8: // Subcommand
|
||||
switch (info.channel()) {
|
||||
case 0x1: // Call subroutine
|
||||
// This command specifies the number of loops (1 byte) and an
|
||||
// offset in the M data to jump to (2 bytes).
|
||||
info.ext.type = info.channel();
|
||||
info.length = 3;
|
||||
info.ext.data = _position._playPos;
|
||||
_position._playPos += info.length;
|
||||
break;
|
||||
case 0x2: // Delay
|
||||
// This command is used to specify a delta time between the
|
||||
// previous and the next event. It does nothing otherwise.
|
||||
info.delta = *(_position._playPos++);
|
||||
info.noop = true;
|
||||
break;
|
||||
case 0x3: // Load instrument
|
||||
// This command specifies the instrument bank slot in which the
|
||||
// instrument should be loaded (1 byte) plus an OPL instrument
|
||||
// definition (11 bytes).
|
||||
info.ext.type = info.channel();
|
||||
info.length = 12;
|
||||
info.ext.data = _position._playPos;
|
||||
_position._playPos += info.length;
|
||||
break;
|
||||
case 0x5: // Fade out
|
||||
case 0x6: // Fade in
|
||||
// These commands have 1 data byte.
|
||||
info.basic.param1 = *(_position._playPos++);
|
||||
break;
|
||||
default:
|
||||
info.noop = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xE: // Set loop point
|
||||
// This command does not have any data bytes.
|
||||
break;
|
||||
|
||||
case 0xF: // Return
|
||||
// This command does not have any data bytes.
|
||||
info.loop = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
info.noop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MidiParser_M::allNotesOff() {
|
||||
if (_driver) {
|
||||
_driver->stopAllNotes();
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
85
engines/ultima/nuvie/sound/midiparser_m.h
Normal file
85
engines/ultima/nuvie/sound/midiparser_m.h
Normal file
@ -0,0 +1,85 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NUVIE_SOUND_MIDIPARSER_M_H
|
||||
#define NUVIE_SOUND_MIDIPARSER_M_H
|
||||
|
||||
#include "audio/midiparser.h"
|
||||
|
||||
#include "common/stack.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
/**
|
||||
* A parser for the music format M, used by Times Of Lore, Bad Blood and
|
||||
* Ultima 6.
|
||||
* This format is not really a MIDI format; it targets the OPL2 chip. However,
|
||||
* it has several things in common with MIDI: it is a stream of events, it has
|
||||
* note on and note off events and events similar to MIDI controllers, the high
|
||||
* nibble of the first event byte is the command while the low nibble is
|
||||
* usually the channel.
|
||||
* The commands are different. M does not use the status byte / data byte
|
||||
* convention and delta times are specified using a wait command. It uses
|
||||
* channels 0-8, corresponding to the 9 OPL2 channels. OPL rhythm mode is not
|
||||
* used.
|
||||
*/
|
||||
class MidiParser_M : public MidiParser {
|
||||
protected:
|
||||
struct LoopData {
|
||||
byte numLoops;
|
||||
byte *startPos;
|
||||
byte *returnPos;
|
||||
};
|
||||
|
||||
public:
|
||||
MidiParser_M(int8 source = -1);
|
||||
~MidiParser_M();
|
||||
|
||||
bool loadMusic(byte *data, uint32 size) override;
|
||||
void unloadMusic() override;
|
||||
void onTimer() override;
|
||||
|
||||
protected:
|
||||
bool processEvent(const EventInfo &info, bool fireEvents = true) override;
|
||||
void parseNextEvent(EventInfo &info) override;
|
||||
|
||||
void allNotesOff() override;
|
||||
|
||||
uint32 _trackLength;
|
||||
|
||||
// The point in the MIDI data where the global loop (not using the stack)
|
||||
// has started and will return.
|
||||
byte *_loopPoint;
|
||||
|
||||
// A stack of nested loops, similar to a call stack. A call command will
|
||||
// specify an offset where the parser should jump to (startPus), plus a
|
||||
// number of times the data from this offset up to the return command should
|
||||
// be repeated (numLoops). Then the parser resumes with the command after
|
||||
// the call command (returnPos). A maximum depth of 16 levels is supported.
|
||||
Common::FixedStack<LoopData, 16> *_loopStack;
|
||||
};
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
||||
|
||||
#endif
|
@ -33,6 +33,9 @@ public:
|
||||
virtual bool Init(const char *filename) {
|
||||
return false;
|
||||
}
|
||||
virtual bool Init(const char *path, const char *fileId) {
|
||||
return false;
|
||||
}
|
||||
bool Play(bool looping = false) override {
|
||||
return false;
|
||||
}
|
||||
@ -46,8 +49,8 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
void SetName(const char *name) {
|
||||
if (name) m_Filename = name; // SB-X
|
||||
void SetTitle(const char *title) {
|
||||
if (title) m_Title = title; // SB-X
|
||||
}
|
||||
private:
|
||||
|
||||
|
66
engines/ultima/nuvie/sound/song_filename.cpp
Normal file
66
engines/ultima/nuvie/sound/song_filename.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "ultima/nuvie/core/nuvie_defs.h"
|
||||
#include "ultima/nuvie/sound/adplug/emu_opl.h"
|
||||
#include "ultima/nuvie/sound/adplug/u6m.h"
|
||||
#include "ultima/nuvie/sound/song_filename.h"
|
||||
#include "ultima/nuvie/sound/sound_manager.h"
|
||||
#include "ultima/nuvie/nuvie.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
SongFilename::~SongFilename() {
|
||||
}
|
||||
|
||||
bool SongFilename::Init(const char *path, const char *fileId) {
|
||||
return Init(path, fileId, 0);
|
||||
}
|
||||
|
||||
bool SongFilename::Init(const char *filename, const char *fileId, uint16 song_num) {
|
||||
if (filename == NULL)
|
||||
return false;
|
||||
|
||||
m_Filename = filename; // SB-X
|
||||
m_FileId = fileId;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SongFilename::Play(bool looping) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SongFilename::Stop() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SongFilename::SetVolume(uint8 volume) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SongFilename::FadeOut(float seconds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
44
engines/ultima/nuvie/sound/song_filename.h
Normal file
44
engines/ultima/nuvie/sound/song_filename.h
Normal file
@ -0,0 +1,44 @@
|
||||
/* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NUVIE_SOUND_SONG_FILENAME_H
|
||||
#define NUVIE_SOUND_SONG_FILENAME_H
|
||||
|
||||
#include "ultima/nuvie/sound/song.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
|
||||
class SongFilename : public Song {
|
||||
public:
|
||||
~SongFilename() override;
|
||||
bool Init(const char *path, const char *fileId);
|
||||
bool Init(const char *path, const char *fileId, uint16 song_num);
|
||||
bool Play(bool looping = false) override;
|
||||
bool Stop() override;
|
||||
bool SetVolume(uint8 volume) override;
|
||||
bool FadeOut(float seconds) override;
|
||||
};
|
||||
|
||||
} // End of namespace Nuvie
|
||||
} // End of namespace Ultima
|
||||
|
||||
#endif
|
@ -43,9 +43,17 @@ public:
|
||||
string GetName() {
|
||||
return m_Filename;
|
||||
}
|
||||
string GetTitle() {
|
||||
return m_Title;
|
||||
}
|
||||
string GetId() {
|
||||
return m_FileId;
|
||||
}
|
||||
protected:
|
||||
string m_Filename;
|
||||
// static SoundManager *gpSM;
|
||||
string m_Title;
|
||||
string m_FileId;
|
||||
// static SoundManager *gpSM;
|
||||
};
|
||||
|
||||
class SoundCollection {
|
||||
|
@ -24,7 +24,7 @@
|
||||
#include "ultima/nuvie/core/u6_objects.h"
|
||||
#include "ultima/nuvie/sound/sound_manager.h"
|
||||
#include "ultima/nuvie/sound/adplug/emu_opl.h"
|
||||
#include "ultima/nuvie/sound/song_adplug.h"
|
||||
#include "ultima/nuvie/sound/song_filename.h"
|
||||
#include "ultima/nuvie/core/game.h"
|
||||
#include "ultima/nuvie/core/player.h"
|
||||
#include "ultima/nuvie/gui/widgets/map_window.h"
|
||||
@ -34,8 +34,12 @@
|
||||
#include "ultima/nuvie/sound/pc_speaker_sfx_manager.h"
|
||||
#include "ultima/nuvie/sound/towns_sfx_manager.h"
|
||||
#include "ultima/nuvie/sound/custom_sfx_manager.h"
|
||||
#include "ultima/nuvie/files/u6_lzw.h"
|
||||
|
||||
#include "audio/mixer.h"
|
||||
|
||||
#include "common/algorithm.h"
|
||||
#include "common/config-manager.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
@ -55,7 +59,21 @@ static const ObjSfxLookup u6_obj_lookup_tbl[] = {
|
||||
{OBJ_U6_WATER_WHEEL, NUVIE_SFX_WATER_WHEEL}
|
||||
};
|
||||
|
||||
|
||||
// This is the default MT-32 instrument mapping specified in MIDI.DAT
|
||||
const SoundManager::SongMT32InstrumentMapping SoundManager::DEFAULT_MT32_INSTRUMENT_MAPPING[12] = {
|
||||
{'1', "ultima.m", {{1, 25}, {2, 50}, {3, 24}, {4, 27}, {-1, 0}}},
|
||||
{'2', "bootup.m", {{1, 37}, {2, 38}, {-1, 0}}},
|
||||
{'3', "intro.m", {{1, 61}, {2, 60}, {3, 55}, {4, 117}, {5, 117}, {-1, 0}}},
|
||||
{'4', "create.m", {{1, 6}, {2, 1}, {3, 33}, {-1, 0}}},
|
||||
{'5', "forest.m", {{1, 59}, {2, 60}, {-1, 0}}},
|
||||
{'6', "hornpipe.m", {{1, 87}, {2, 60}, {3, 59}, {-1, 0}}},
|
||||
{'7', "engage.m", {{1, 49}, {2, 26}, {3, 18}, {4, 16}, {-1, 0}}},
|
||||
{'8', "stones.m", {{1, 6}, {2, 32}, {-1, 0}}},
|
||||
{'9', "dungeon.m", {{1, 37}, {2, 113}, {3, 55}, {-1, 0}}},
|
||||
{'0', "brit.m", {{1, 12}, {-1, 0}}},
|
||||
{'-', "gargoyle.m", {{1, 38}, {2, 5}, {-1, 0}}},
|
||||
{'=', "end.m", {{1, 38}, {2, 12}, {3, 50}, {4, 94}, {-1, 0}}}
|
||||
};
|
||||
|
||||
bool SoundManager::g_MusicFinished;
|
||||
|
||||
@ -76,12 +94,35 @@ SoundManager::SoundManager(Audio::Mixer *mixer) : _mixer(mixer) {
|
||||
m_SfxManager = NULL;
|
||||
|
||||
opl = NULL;
|
||||
|
||||
_midiDriver = nullptr;
|
||||
_mt32MidiDriver = nullptr;
|
||||
_midiParser = nullptr;
|
||||
_deviceType = MT_NULL;
|
||||
_musicData = nullptr;
|
||||
_mt32InstrumentMapping = nullptr;
|
||||
}
|
||||
|
||||
SoundManager::~SoundManager() {
|
||||
// Stop all mixing
|
||||
_mixer->stopAll();
|
||||
|
||||
musicPause();
|
||||
|
||||
if (_midiDriver) {
|
||||
_midiDriver->setTimerCallback(nullptr, nullptr);
|
||||
_midiDriver->close();
|
||||
}
|
||||
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
if (_midiParser) {
|
||||
delete _midiParser;
|
||||
}
|
||||
if (_midiDriver) {
|
||||
delete _midiDriver;
|
||||
}
|
||||
|
||||
//thanks to wjp for this one
|
||||
while (!m_Songs.empty()) {
|
||||
delete *(m_Songs.begin());
|
||||
@ -110,38 +151,12 @@ bool SoundManager::nuvieStartup(Configuration *config) {
|
||||
Std::string music_cfg_file; //full path and filename to music.cfg
|
||||
Std::string sound_dir;
|
||||
Std::string sfx_style;
|
||||
bool val;
|
||||
|
||||
m_Config = config;
|
||||
|
||||
m_Config->value("config/mute", val, false);
|
||||
audio_enabled = !val;
|
||||
m_Config->value("config/GameType", game_type);
|
||||
m_Config->value("config/audio/stop_music_on_group_change", stop_music_on_group_change, true);
|
||||
|
||||
/* if(audio_enabled == false) // commented out to allow toggling
|
||||
{
|
||||
music_enabled = false;
|
||||
sfx_enabled = false;
|
||||
music_volume = 0;
|
||||
sfx_volume = 0;
|
||||
mixer = NULL;
|
||||
return false;
|
||||
}*/
|
||||
|
||||
m_Config->value("config/music_mute", val, false);
|
||||
music_enabled = !val;
|
||||
m_Config->value("config/sfx_mute", val, false);
|
||||
sfx_enabled = !val;
|
||||
|
||||
int volume;
|
||||
|
||||
m_Config->value("config/music_volume", volume, Audio::Mixer::kMaxChannelVolume);
|
||||
music_volume = clamp(volume, 0, 255);
|
||||
|
||||
m_Config->value("config/sfx_volume", volume, Audio::Mixer::kMaxChannelVolume);
|
||||
sfx_volume = clamp(volume, 0, 255);
|
||||
|
||||
config_key = config_get_game_key(config);
|
||||
config_key.append("/music");
|
||||
config->value(config_key, music_style, "native");
|
||||
@ -154,42 +169,82 @@ bool SoundManager::nuvieStartup(Configuration *config) {
|
||||
config_key.append("/sounddir");
|
||||
config->value(config_key, sound_dir, "");
|
||||
|
||||
if (game_type == NUVIE_GAME_U6) { // FM-Towns speech
|
||||
config->value("config/speech_mute", val, false);
|
||||
speech_enabled = !val;
|
||||
} else {
|
||||
speech_enabled = false;
|
||||
}
|
||||
|
||||
if (!initAudio()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if(music_enabled) // commented out to allow toggling
|
||||
{
|
||||
if (music_style == "native") {
|
||||
if (game_type == NUVIE_GAME_U6)
|
||||
LoadNativeU6Songs(); //FIX need to handle MD & SE music too.
|
||||
} else if (music_style == "custom")
|
||||
LoadCustomSongs(sound_dir);
|
||||
else
|
||||
DEBUG(0, LEVEL_WARNING, "Unknown music style '%s'\n", music_style.c_str());
|
||||
if (music_style == "native") {
|
||||
if (game_type == NUVIE_GAME_U6)
|
||||
LoadNativeU6Songs(); //FIX need to handle MD & SE music too.
|
||||
} else if (music_style == "custom")
|
||||
LoadCustomSongs(sound_dir);
|
||||
else
|
||||
DEBUG(0, LEVEL_WARNING, "Unknown music style '%s'\n", music_style.c_str());
|
||||
|
||||
musicPlayFrom("random");
|
||||
}
|
||||
musicPlayFrom("random");
|
||||
|
||||
// if(sfx_enabled) // commented out to allow toggling
|
||||
{
|
||||
//LoadObjectSamples(sound_dir);
|
||||
//LoadTileSamples(sound_dir);
|
||||
LoadSfxManager(sfx_style);
|
||||
}
|
||||
//LoadObjectSamples(sound_dir);
|
||||
//LoadTileSamples(sound_dir);
|
||||
LoadSfxManager(sfx_style);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundManager::initAudio() {
|
||||
opl = new CEmuopl(_mixer->getOutputRate(), true, true);
|
||||
assert(!_midiDriver);
|
||||
|
||||
int devFlags = MDT_ADLIB | MDT_MIDI | MDT_PREFER_MT32;
|
||||
if (game_type == NUVIE_GAME_U6)
|
||||
// CMS, Tandy and SSI are only supported by Ultima 6 DOS.
|
||||
devFlags |= MDT_CMS | MDT_PCJR | MDT_C64;
|
||||
|
||||
// Check the type of device that the user has configured.
|
||||
MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(devFlags);
|
||||
_deviceType = MidiDriver::getMusicType(dev);
|
||||
|
||||
if (_deviceType == MT_GM && ConfMan.getBool("native_mt32"))
|
||||
_deviceType = MT_MT32;
|
||||
|
||||
switch (_deviceType) {
|
||||
case MT_ADLIB:
|
||||
_midiDriver = new MidiDriver_M_AdLib();
|
||||
break;
|
||||
case MT_MT32:
|
||||
case MT_GM:
|
||||
_midiDriver = _mt32MidiDriver = new MidiDriver_M_MT32();
|
||||
// TODO Parse MIDI.DAT
|
||||
_mt32InstrumentMapping = DEFAULT_MT32_INSTRUMENT_MAPPING;
|
||||
break;
|
||||
case MT_CMS:
|
||||
case MT_PCJR:
|
||||
case MT_C64:
|
||||
default:
|
||||
// TODO Implement these
|
||||
_midiDriver = new MidiDriver_NULL_Multisource();
|
||||
break;
|
||||
}
|
||||
_midiDriver->property(MidiDriver::PROP_USER_VOLUME_SCALING, true);
|
||||
|
||||
// TODO Only Ultima 6 M format is supported.
|
||||
_midiParser = new MidiParser_M(0);
|
||||
_midiParser->property(MidiParser::mpDisableAutoStartPlayback, true);
|
||||
|
||||
// Open the MIDI driver(s).
|
||||
int returnCode = _midiDriver->open();
|
||||
if (returnCode != 0) {
|
||||
warning("SoundManager::initAudio - Failed to open M music driver - error code %d.", returnCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
syncSoundSettings();
|
||||
|
||||
// Connect the driver and the parser.
|
||||
_midiParser->setMidiDriver(_midiDriver);
|
||||
_midiParser->setTimerRate(_midiDriver->getBaseTempo());
|
||||
_midiDriver->setTimerCallback(_midiParser, &_midiParser->timerCallback);
|
||||
|
||||
//opl = new CEmuopl(_mixer->getOutputRate(), true, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -197,46 +252,55 @@ bool SoundManager::LoadNativeU6Songs() {
|
||||
Song *song;
|
||||
|
||||
string filename;
|
||||
string fileId;
|
||||
|
||||
config_get_path(m_Config, "brit.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
fileId = "brit.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
// loadSong(song, filename.c_str());
|
||||
loadSong(song, filename.c_str(), "Rule Britannia");
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Rule Britannia");
|
||||
groupAddSong("random", song);
|
||||
|
||||
config_get_path(m_Config, "forest.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Wanderer (Forest)");
|
||||
fileId = "forest.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Wanderer (Forest)");
|
||||
groupAddSong("random", song);
|
||||
|
||||
config_get_path(m_Config, "stones.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Stones");
|
||||
fileId = "stones.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Stones");
|
||||
groupAddSong("random", song);
|
||||
|
||||
config_get_path(m_Config, "ultima.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Ultima VI Theme");
|
||||
fileId = "ultima.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Ultima VI Theme");
|
||||
groupAddSong("random", song);
|
||||
|
||||
config_get_path(m_Config, "engage.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Engagement and Melee");
|
||||
fileId = "engage.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Engagement and Melee");
|
||||
groupAddSong("combat", song);
|
||||
|
||||
config_get_path(m_Config, "hornpipe.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Captain Johne's Hornpipe");
|
||||
fileId = "hornpipe.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Captain Johne's Hornpipe");
|
||||
groupAddSong("boat", song);
|
||||
|
||||
config_get_path(m_Config, "gargoyle.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Audchar Gargl Zenmur");
|
||||
fileId = "gargoyle.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Audchar Gargl Zenmur");
|
||||
groupAddSong("gargoyle", song);
|
||||
|
||||
config_get_path(m_Config, "dungeon.m", filename);
|
||||
song = new SongAdPlug(_mixer, opl);
|
||||
loadSong(song, filename.c_str(), "Dungeon");
|
||||
fileId = "dungeon.m";
|
||||
config_get_path(m_Config, fileId, filename);
|
||||
song = new SongFilename();
|
||||
loadSong(song, filename.c_str(), fileId.c_str(), "Dungeon");
|
||||
groupAddSong("dungeon", song);
|
||||
|
||||
return true;
|
||||
@ -268,8 +332,10 @@ bool SoundManager::LoadCustomSongs(string sound_dir) {
|
||||
|
||||
song = (Song *)SongExists(token2);
|
||||
if (song == NULL) {
|
||||
// Note: the base class Song does not have an implementation for
|
||||
// Init, so loading custom songs does not work.
|
||||
song = new Song;
|
||||
if (!loadSong(song, filename.c_str()))
|
||||
if (!loadSong(song, filename.c_str(), token2))
|
||||
continue; //error loading song
|
||||
}
|
||||
|
||||
@ -282,8 +348,8 @@ bool SoundManager::LoadCustomSongs(string sound_dir) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundManager::loadSong(Song *song, const char *filename) {
|
||||
if (song->Init(filename)) {
|
||||
bool SoundManager::loadSong(Song *song, const char *filename, const char *fileId) {
|
||||
if (song->Init(filename, fileId)) {
|
||||
m_Songs.push_back(song); //add it to our global list
|
||||
return true;
|
||||
} else {
|
||||
@ -294,9 +360,9 @@ bool SoundManager::loadSong(Song *song, const char *filename) {
|
||||
}
|
||||
|
||||
// (SB-X)
|
||||
bool SoundManager::loadSong(Song *song, const char *filename, const char *title) {
|
||||
if (loadSong(song, filename) == true) {
|
||||
song->SetName(title);
|
||||
bool SoundManager::loadSong(Song *song, const char *filename, const char *fileId, const char *title) {
|
||||
if (loadSong(song, filename, fileId) == true) {
|
||||
song->SetTitle(title);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -480,6 +546,8 @@ bool SoundManager::LoadSfxManager(string sfx_style) {
|
||||
}
|
||||
|
||||
void SoundManager::musicPlayFrom(string group) {
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
if (!music_enabled || !audio_enabled)
|
||||
return;
|
||||
if (m_CurrentGroup != group) {
|
||||
@ -490,25 +558,55 @@ void SoundManager::musicPlayFrom(string group) {
|
||||
}
|
||||
|
||||
void SoundManager::musicPause() {
|
||||
//Mix_PauseMusic();
|
||||
if (m_pCurrentSong != NULL) {
|
||||
m_pCurrentSong->Stop();
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
if (m_pCurrentSong != NULL && _midiParser->isPlaying()) {
|
||||
_midiParser->stopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
/* don't call if audio or music is disabled */
|
||||
void SoundManager::musicPlay() {
|
||||
// Mix_ResumeMusic();
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
// (SB-X) Get a new song if stopped.
|
||||
if (m_pCurrentSong != NULL && _midiParser->isPlaying()) {
|
||||
// Already playing a song.
|
||||
return;
|
||||
}
|
||||
|
||||
// (SB-X) Get a new song if stopped.
|
||||
if (m_pCurrentSong == NULL)
|
||||
m_pCurrentSong = RequestSong(m_CurrentGroup);
|
||||
|
||||
if (m_pCurrentSong != NULL) {
|
||||
m_pCurrentSong->Play();
|
||||
m_pCurrentSong->SetVolume(music_volume);
|
||||
}
|
||||
DEBUG(0, LEVEL_INFORMATIONAL, "assigning new song! '%s'\n", m_pCurrentSong->GetName().c_str());
|
||||
|
||||
// TODO Only Ultima 6 LZW format is supported.
|
||||
uint32 decompressed_filesize;
|
||||
U6Lzw lzw;
|
||||
|
||||
_musicData = lzw.decompress_file(m_pCurrentSong->GetName(), decompressed_filesize);
|
||||
|
||||
bool result = _midiParser->loadMusic(_musicData, decompressed_filesize);
|
||||
if (result) {
|
||||
_midiDriver->deinitSource(0);
|
||||
|
||||
if (_mt32MidiDriver) {
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (!strcmp(m_pCurrentSong->GetId().c_str(), DEFAULT_MT32_INSTRUMENT_MAPPING[i].filename)) {
|
||||
_mt32MidiDriver->setInstrumentAssignments(DEFAULT_MT32_INSTRUMENT_MAPPING[i].instrumentMapping);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = _midiParser->startPlaying();
|
||||
g_MusicFinished = false;
|
||||
}
|
||||
if (!result) {
|
||||
DEBUG(0, LEVEL_ERROR, "play failed!\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SoundManager::musicPlay(const char *filename, uint16 song_num) {
|
||||
@ -518,8 +616,10 @@ void SoundManager::musicPlay(const char *filename, uint16 song_num) {
|
||||
return;
|
||||
|
||||
config_get_path(m_Config, filename, path);
|
||||
SongAdPlug *song = new SongAdPlug(_mixer, opl);
|
||||
song->Init(path.c_str(), song_num);
|
||||
SongFilename *song = new SongFilename();
|
||||
song->Init(path.c_str(), filename, song_num);
|
||||
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
musicStop();
|
||||
m_pCurrentSong = song;
|
||||
@ -529,8 +629,14 @@ void SoundManager::musicPlay(const char *filename, uint16 song_num) {
|
||||
|
||||
// (SB-X) Stop the current song so a new song will play when resumed.
|
||||
void SoundManager::musicStop() {
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
musicPause();
|
||||
m_pCurrentSong = NULL;
|
||||
if (_musicData) {
|
||||
delete _musicData;
|
||||
_musicData = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Std::list < SoundManagerSfx >::iterator SoundManagerSfx_find(Std::list < SoundManagerSfx >::iterator first, Std::list < SoundManagerSfx >::iterator last, const SfxIdType &value) {
|
||||
@ -660,26 +766,13 @@ void SoundManager::update_map_sfx() {
|
||||
|
||||
void SoundManager::update() {
|
||||
if (music_enabled && audio_enabled && g_MusicFinished) {
|
||||
g_MusicFinished = false;
|
||||
if (m_pCurrentSong != NULL) {
|
||||
m_pCurrentSong->Stop();
|
||||
}
|
||||
Common::StackLock lock(_musicMutex);
|
||||
|
||||
if (m_CurrentGroup.length() > 0)
|
||||
m_pCurrentSong = SoundManager::RequestSong(m_CurrentGroup);
|
||||
|
||||
if (m_pCurrentSong) {
|
||||
DEBUG(0, LEVEL_INFORMATIONAL, "assigning new song! '%s'\n", m_pCurrentSong->GetName().c_str());
|
||||
if (!m_pCurrentSong->Play(false)) {
|
||||
DEBUG(0, LEVEL_ERROR, "play failed!\n");
|
||||
}
|
||||
m_pCurrentSong->SetVolume(music_volume);
|
||||
}
|
||||
musicPause();
|
||||
musicPlay();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Sound *SoundManager::SongExists(string name) {
|
||||
Std::list < Sound * >::iterator it;
|
||||
for (it = m_Songs.begin(); it != m_Songs.end(); ++it) {
|
||||
@ -779,6 +872,24 @@ bool SoundManager::playSfx(uint16 sfx_id, bool async) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void SoundManager::syncSoundSettings() {
|
||||
set_audio_enabled(
|
||||
!ConfMan.hasKey("mute") || !ConfMan.getBool("mute"));
|
||||
set_sfx_enabled(
|
||||
!ConfMan.hasKey("sfx_mute") || !ConfMan.getBool("sfx_mute"));
|
||||
// TODO Music is disabled for SE and MD for now
|
||||
set_music_enabled(game_type == NUVIE_GAME_U6 &&
|
||||
(!ConfMan.hasKey("music_mute") || !ConfMan.getBool("music_mute")));
|
||||
set_speech_enabled(game_type == NUVIE_GAME_U6 &&
|
||||
(!ConfMan.hasKey("speech_mute") || !ConfMan.getBool("speech_mute")));
|
||||
|
||||
set_sfx_volume(ConfMan.hasKey("sfx_volume") ? clamp(ConfMan.getInt("sfx_volume"), 0, 255) : 255);
|
||||
set_music_volume(ConfMan.hasKey("music_volume") ? clamp(ConfMan.getInt("music_volume"), 0, 255) : 255);
|
||||
|
||||
if (_midiDriver)
|
||||
_midiDriver->syncSoundSettings();
|
||||
}
|
||||
|
||||
void SoundManager::set_audio_enabled(bool val) {
|
||||
audio_enabled = val;
|
||||
if (audio_enabled && music_enabled)
|
||||
|
@ -30,13 +30,20 @@
|
||||
//-make samples fade in & out according to distance
|
||||
//-try and use original .m files
|
||||
|
||||
#include "mididrv_m_adlib.h"
|
||||
#include "mididrv_m_mt32.h"
|
||||
#include "midiparser_m.h"
|
||||
|
||||
#include "ultima/nuvie/sound/sound.h"
|
||||
#include "ultima/nuvie/sound/song.h"
|
||||
#include "ultima/nuvie/core/nuvie_defs.h"
|
||||
#include "ultima/nuvie/conf/configuration.h"
|
||||
#include "ultima/nuvie/files/nuvie_io_file.h"
|
||||
#include "ultima/nuvie/sound/sfx.h"
|
||||
|
||||
#include "audio/mixer.h"
|
||||
#include "audio/mididrv.h"
|
||||
#include "common/mutex.h"
|
||||
|
||||
namespace Ultima {
|
||||
namespace Nuvie {
|
||||
@ -53,6 +60,15 @@ struct SoundManagerSfx {
|
||||
} ;
|
||||
|
||||
class SoundManager {
|
||||
private:
|
||||
struct SongMT32InstrumentMapping {
|
||||
char midiDatId;
|
||||
const char *filename;
|
||||
MInstrumentAssignment instrumentMapping[16];
|
||||
};
|
||||
|
||||
const static SongMT32InstrumentMapping DEFAULT_MT32_INSTRUMENT_MAPPING[12];
|
||||
|
||||
public:
|
||||
SoundManager(Audio::Mixer *mixer);
|
||||
~SoundManager();
|
||||
@ -73,6 +89,9 @@ public:
|
||||
bool isSoundPLaying(Audio::SoundHandle handle);
|
||||
|
||||
bool playSfx(uint16 sfx_id, bool async = false);
|
||||
|
||||
void syncSoundSettings();
|
||||
|
||||
bool is_audio_enabled() {
|
||||
return audio_enabled;
|
||||
}
|
||||
@ -112,8 +131,8 @@ public:
|
||||
private:
|
||||
bool LoadCustomSongs(string scriptname);
|
||||
bool LoadNativeU6Songs();
|
||||
bool loadSong(Song *song, const char *filename);
|
||||
bool loadSong(Song *song, const char *filename, const char *title);
|
||||
bool loadSong(Song *song, const char *filename, const char *fileId);
|
||||
bool loadSong(Song *song, const char *filename, const char *fileId, const char *title);
|
||||
bool groupAddSong(const char *group, Song *song);
|
||||
|
||||
//bool LoadObjectSamples(string sound_dir);
|
||||
@ -156,6 +175,14 @@ private:
|
||||
|
||||
CEmuopl *opl;
|
||||
|
||||
MidiDriver_Multisource *_midiDriver;
|
||||
MidiDriver_M_MT32 *_mt32MidiDriver;
|
||||
MidiParser_M *_midiParser;
|
||||
MusicType _deviceType;
|
||||
byte *_musicData;
|
||||
const SongMT32InstrumentMapping *_mt32InstrumentMapping;
|
||||
Common::Mutex _musicMutex;
|
||||
|
||||
int game_type; //FIXME there's a nuvie_game_t, but almost everything uses int game_type (or gametype)
|
||||
|
||||
public:
|
||||
|
Loading…
Reference in New Issue
Block a user