mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-14 21:59:17 +00:00
572 lines
16 KiB
C++
572 lines
16 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 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* sound functionality
|
|
*/
|
|
|
|
#include "tinsel/sound.h"
|
|
|
|
#include "tinsel/adpcm.h"
|
|
#include "tinsel/dw.h"
|
|
#include "tinsel/config.h"
|
|
#include "tinsel/music.h"
|
|
#include "tinsel/strres.h"
|
|
#include "tinsel/tinsel.h"
|
|
#include "tinsel/sysvar.h"
|
|
#include "tinsel/background.h"
|
|
|
|
#include "common/endian.h"
|
|
#include "common/memstream.h"
|
|
#include "common/system.h"
|
|
|
|
#include "audio/mixer.h"
|
|
#include "audio/decoders/adpcm.h"
|
|
#include "audio/decoders/flac.h"
|
|
#include "audio/decoders/mp3.h"
|
|
#include "audio/decoders/raw.h"
|
|
#include "audio/decoders/vorbis.h"
|
|
#include "audio/decoders/xa.h"
|
|
|
|
|
|
#include "gui/message.h"
|
|
|
|
namespace Tinsel {
|
|
|
|
extern LANGUAGE g_sampleLanguage;
|
|
|
|
//--------------------------- General data ----------------------------------
|
|
|
|
SoundManager::SoundManager(TinselEngine *vm) :
|
|
//_vm(vm), // TODO: Enable this once global _vm var is gone
|
|
_sampleIndex(0), _sampleIndexLen(0),
|
|
_soundMode(kVOCMode) {
|
|
|
|
for (int i = 0; i < kNumChannels; i++)
|
|
_channels[i].sampleNum = _channels[i].subSample = -1;
|
|
}
|
|
|
|
SoundManager::~SoundManager() {
|
|
free(_sampleIndex);
|
|
}
|
|
|
|
/**
|
|
* Plays the specified sample through the sound driver.
|
|
* @param id Identifier of sample to be played
|
|
* @param type type of sound (voice or sfx)
|
|
* @param handle sound handle
|
|
*/
|
|
// playSample for DiscWorld 1
|
|
bool SoundManager::playSample(int id, Audio::Mixer::SoundType type, Audio::SoundHandle *handle) {
|
|
// Floppy version has no sample file.
|
|
if (!_vm->isV1CD())
|
|
return false;
|
|
|
|
// no sample driver?
|
|
if (!_vm->_mixer->isReady())
|
|
return false;
|
|
|
|
Channel &curChan = _channels[kChannelTinsel1];
|
|
|
|
// stop any currently playing sample
|
|
_vm->_mixer->stopHandle(curChan.handle);
|
|
|
|
// make sure id is in range
|
|
assert(id > 0 && id < _sampleIndexLen);
|
|
|
|
curChan.sampleNum = id;
|
|
curChan.subSample = 0;
|
|
|
|
// get file offset for this sample
|
|
uint32 dwSampleIndex = _sampleIndex[id];
|
|
|
|
// move to correct position in the sample file
|
|
_sampleStream.seek(dwSampleIndex);
|
|
if (_sampleStream.eos() || _sampleStream.err() || (uint32)_sampleStream.pos() != dwSampleIndex)
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
|
|
// read the length of the sample
|
|
uint32 sampleLen = _sampleStream.readUint32LE();
|
|
if (_sampleStream.eos() || _sampleStream.err())
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
|
|
if (TinselV1PSX) {
|
|
// Read the stream and create a XA ADPCM audio stream
|
|
Audio::AudioStream *xaStream = Audio::makeXAStream(_sampleStream.readStream(sampleLen), 44100);
|
|
|
|
// FIXME: Should set this in a different place ;)
|
|
_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, _vm->_config->_soundVolume);
|
|
//_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic);
|
|
_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, _vm->_config->_voiceVolume);
|
|
|
|
// Play the audio stream
|
|
_vm->_mixer->playStream(type, &curChan.handle, xaStream);
|
|
} else {
|
|
// allocate a buffer
|
|
byte *sampleBuf = (byte *)malloc(sampleLen);
|
|
assert(sampleBuf);
|
|
|
|
// read all of the sample
|
|
if (_sampleStream.read(sampleBuf, sampleLen) != sampleLen)
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
|
|
// FIXME: Should set this in a different place ;)
|
|
_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, _vm->_config->_soundVolume);
|
|
//_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic);
|
|
_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, _vm->_config->_voiceVolume);
|
|
|
|
Audio::AudioStream *sampleStream = 0;
|
|
|
|
// play it
|
|
switch (_soundMode) {
|
|
case kMP3Mode:
|
|
#ifdef USE_MAD
|
|
{
|
|
Common::MemoryReadStream *compressedStream =
|
|
new Common::MemoryReadStream(sampleBuf, sampleLen, DisposeAfterUse::YES);
|
|
sampleStream = Audio::makeMP3Stream(compressedStream, DisposeAfterUse::YES);
|
|
}
|
|
#endif
|
|
break;
|
|
case kVorbisMode:
|
|
#ifdef USE_VORBIS
|
|
{
|
|
Common::MemoryReadStream *compressedStream =
|
|
new Common::MemoryReadStream(sampleBuf, sampleLen, DisposeAfterUse::YES);
|
|
sampleStream = Audio::makeVorbisStream(compressedStream, DisposeAfterUse::YES);
|
|
}
|
|
#endif
|
|
break;
|
|
case kFLACMode:
|
|
#ifdef USE_FLAC
|
|
{
|
|
Common::MemoryReadStream *compressedStream =
|
|
new Common::MemoryReadStream(sampleBuf, sampleLen, DisposeAfterUse::YES);
|
|
sampleStream = Audio::makeFLACStream(compressedStream, DisposeAfterUse::YES);
|
|
}
|
|
#endif
|
|
break;
|
|
default:
|
|
sampleStream = Audio::makeRawStream(sampleBuf, sampleLen, 22050, Audio::FLAG_UNSIGNED);
|
|
break;
|
|
}
|
|
if (sampleStream) {
|
|
_vm->_mixer->playStream(type, &curChan.handle, sampleStream);
|
|
}
|
|
}
|
|
|
|
if (handle)
|
|
*handle = curChan.handle;
|
|
|
|
return true;
|
|
}
|
|
|
|
void SoundManager::playDW1MacMusic(Common::File &s, uint32 length) {
|
|
// TODO: It's a bad idea to load the music track in a buffer.
|
|
// We should use a SubReadStream instead, and keep midi.dat open.
|
|
// However, the track lengths aren't that big (about 1-4MB),
|
|
// so this shouldn't be a major issue.
|
|
byte *soundData = (byte *)malloc(length);
|
|
assert(soundData);
|
|
|
|
// read all of the sample
|
|
if (s.read(soundData, length) != length)
|
|
error(FILE_IS_CORRUPT, MIDI_FILE);
|
|
|
|
Common::SeekableReadStream *memStream = new Common::MemoryReadStream(soundData, length);
|
|
|
|
Audio::SoundHandle *handle = &_channels[kChannelDW1MacMusic].handle;
|
|
|
|
// Stop any previously playing music track
|
|
_vm->_mixer->stopHandle(*handle);
|
|
|
|
// TODO: Compression support (MP3/OGG/FLAC) for midi.dat in DW1 Mac
|
|
Audio::RewindableAudioStream *musicStream = Audio::makeRawStream(memStream, 22050, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
|
|
|
|
if (musicStream)
|
|
_vm->_mixer->playStream(Audio::Mixer::kMusicSoundType, handle, Audio::makeLoopingAudioStream(musicStream, 0));
|
|
}
|
|
|
|
// playSample for DiscWorld 2
|
|
bool SoundManager::playSample(int id, int sub, bool bLooped, int x, int y, int priority,
|
|
Audio::Mixer::SoundType type, Audio::SoundHandle *handle) {
|
|
|
|
// no sample driver?
|
|
if (!_vm->_mixer->isReady())
|
|
return false;
|
|
|
|
Channel *curChan;
|
|
|
|
uint8 sndVol = 255;
|
|
|
|
// Sample on screen?
|
|
if (!offscreenChecks(x, y))
|
|
return false;
|
|
|
|
// If that sample is already playing, stop it
|
|
stopSpecSample(id, sub);
|
|
|
|
if (type == Audio::Mixer::kSpeechSoundType) {
|
|
curChan = &_channels[kChannelTalk];
|
|
} else if (type == Audio::Mixer::kSFXSoundType) {
|
|
uint32 oldestTime = g_system->getMillis();
|
|
int oldestChan = kChannelSFX;
|
|
|
|
int chan;
|
|
for (chan = kChannelSFX; chan < kNumChannels; chan++) {
|
|
if (!_vm->_mixer->isSoundHandleActive(_channels[chan].handle))
|
|
break;
|
|
|
|
if ((_channels[chan].lastStart < oldestTime) &&
|
|
(_channels[chan].priority <= priority)) {
|
|
|
|
oldestTime = _channels[chan].lastStart;
|
|
oldestChan = chan;
|
|
}
|
|
}
|
|
|
|
if (chan == kNumChannels) {
|
|
if (_channels[oldestChan].priority > priority) {
|
|
warning("playSample: No free channel");
|
|
return false;
|
|
}
|
|
|
|
chan = oldestChan;
|
|
}
|
|
|
|
if (_vm->_pcmMusic->isDimmed() && SysVar(SYS_SceneFxDimFactor))
|
|
sndVol = 255 - 255/SysVar(SYS_SceneFxDimFactor);
|
|
|
|
curChan = &_channels[chan];
|
|
} else {
|
|
warning("playSample: Unknown SoundType");
|
|
return false;
|
|
}
|
|
|
|
// stop any currently playing sample
|
|
_vm->_mixer->stopHandle(curChan->handle);
|
|
|
|
// make sure id is in range
|
|
assert(id > 0 && id < _sampleIndexLen);
|
|
|
|
// get file offset for this sample
|
|
uint32 dwSampleIndex = _sampleIndex[id];
|
|
|
|
if (dwSampleIndex == 0) {
|
|
warning("Tinsel2 playSample, non-existent sample %d", id);
|
|
return false;
|
|
}
|
|
|
|
// move to correct position in the sample file
|
|
_sampleStream.seek(dwSampleIndex);
|
|
if (_sampleStream.eos() || _sampleStream.err() || (uint32)_sampleStream.pos() != dwSampleIndex)
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
|
|
// read the length of the sample
|
|
uint32 sampleLen = _sampleStream.readUint32LE();
|
|
if (_sampleStream.eos() || _sampleStream.err())
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
|
|
if (sampleLen & 0x80000000) {
|
|
// Has sub samples
|
|
|
|
int32 numSubs = sampleLen & ~0x80000000;
|
|
|
|
assert(sub >= 0 && sub < numSubs);
|
|
|
|
// Skipping
|
|
for (int32 i = 0; i < sub; i++) {
|
|
sampleLen = _sampleStream.readUint32LE();
|
|
_sampleStream.skip(sampleLen);
|
|
if (_sampleStream.eos() || _sampleStream.err())
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
}
|
|
sampleLen = _sampleStream.readUint32LE();
|
|
if (_sampleStream.eos() || _sampleStream.err())
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
}
|
|
|
|
debugC(DEBUG_DETAILED, kTinselDebugSound, "Playing sound %d.%d, %d bytes at %d (pan %d)", id, sub, sampleLen,
|
|
_sampleStream.pos(), getPan(x));
|
|
|
|
// allocate a buffer
|
|
byte *sampleBuf = (byte *) malloc(sampleLen);
|
|
assert(sampleBuf);
|
|
|
|
// read all of the sample
|
|
if (_sampleStream.read(sampleBuf, sampleLen) != sampleLen)
|
|
error(FILE_IS_CORRUPT, _vm->getSampleFile(g_sampleLanguage));
|
|
|
|
Common::MemoryReadStream *compressedStream =
|
|
new Common::MemoryReadStream(sampleBuf, sampleLen, DisposeAfterUse::YES);
|
|
Audio::AudioStream *sampleStream = 0;
|
|
|
|
switch (_soundMode) {
|
|
case kMP3Mode:
|
|
#ifdef USE_MAD
|
|
sampleStream = Audio::makeMP3Stream(compressedStream, DisposeAfterUse::YES);
|
|
#endif
|
|
break;
|
|
case kVorbisMode:
|
|
#ifdef USE_VORBIS
|
|
sampleStream = Audio::makeVorbisStream(compressedStream, DisposeAfterUse::YES);
|
|
#endif
|
|
break;
|
|
case kFLACMode:
|
|
#ifdef USE_FLAC
|
|
sampleStream = Audio::makeFLACStream(compressedStream, DisposeAfterUse::YES);
|
|
#endif
|
|
break;
|
|
default:
|
|
sampleStream = new Tinsel6_ADPCMStream(compressedStream, DisposeAfterUse::YES, sampleLen, 22050, 1, 24);
|
|
break;
|
|
}
|
|
|
|
// FIXME: Should set this in a different place ;)
|
|
_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, _vm->_config->_soundVolume);
|
|
//_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundVolumeMusic);
|
|
_vm->_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, _vm->_config->_voiceVolume);
|
|
|
|
curChan->sampleNum = id;
|
|
curChan->subSample = sub;
|
|
curChan->looped = bLooped;
|
|
curChan->x = x;
|
|
curChan->y = y;
|
|
curChan->priority = priority;
|
|
curChan->lastStart = g_system->getMillis();
|
|
// /---Compression----\ Milis BytesPerSecond
|
|
// not needed and won't work when using MP3/OGG/FLAC anyway
|
|
//curChan->timeDuration = (((sampleLen * 64) / 25) * 1000) / (22050 * 2);
|
|
|
|
// Play it
|
|
_vm->_mixer->playStream(type, &curChan->handle, sampleStream);
|
|
|
|
_vm->_mixer->setChannelVolume(curChan->handle, sndVol);
|
|
_vm->_mixer->setChannelBalance(curChan->handle, getPan(x));
|
|
|
|
if (handle)
|
|
*handle = curChan->handle;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns FALSE if sample doesn't need playing
|
|
*/
|
|
bool SoundManager::offscreenChecks(int x, int &y) {
|
|
// No action if no x specification
|
|
if (x == -1)
|
|
return true;
|
|
|
|
// convert x to offset from screen center
|
|
x -= PlayfieldGetCenterX(FIELD_WORLD);
|
|
|
|
if (x < -SCREEN_WIDTH || x > SCREEN_WIDTH) {
|
|
// A long way offscreen, ignore it
|
|
return false;
|
|
} else if (x < -SCREEN_WIDTH/2 || x > SCREEN_WIDTH/2) {
|
|
// Off-screen, attennuate it
|
|
|
|
y = (y > 0) ? (y / 2) : 50;
|
|
|
|
return true;
|
|
} else
|
|
return true;
|
|
}
|
|
|
|
int8 SoundManager::getPan(int x) {
|
|
if (x == -1)
|
|
return 0;
|
|
|
|
x -= PlayfieldGetCenterX(FIELD_WORLD);
|
|
|
|
if (x == 0)
|
|
return 0;
|
|
|
|
if (x < 0) {
|
|
if (x < (-SCREEN_WIDTH / 2))
|
|
return -127;
|
|
|
|
x = (-x * 127) / (SCREEN_WIDTH / 2);
|
|
|
|
return 0 - x;
|
|
}
|
|
|
|
if (x > (SCREEN_WIDTH / 2))
|
|
return 127;
|
|
|
|
x = (x * 127) / (SCREEN_WIDTH / 2);
|
|
|
|
return x;
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE if there is a sample for the specified sample identifier.
|
|
* @param id Identifier of sample to be checked
|
|
*/
|
|
bool SoundManager::sampleExists(int id) {
|
|
if (_vm->_mixer->isReady()) {
|
|
// make sure id is in range
|
|
if (id > 0 && id < _sampleIndexLen) {
|
|
// check for a sample index
|
|
if (_sampleIndex[id])
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// no sample driver or no sample
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns true if a sample is currently playing.
|
|
*/
|
|
bool SoundManager::sampleIsPlaying() {
|
|
if (!TinselV2)
|
|
return _vm->_mixer->isSoundHandleActive(_channels[kChannelTinsel1].handle);
|
|
|
|
for (int i = 0; i < kNumChannels; i++)
|
|
if (_vm->_mixer->isSoundHandleActive(_channels[i].handle))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Stops any currently playing sample.
|
|
*/
|
|
void SoundManager::stopAllSamples() {
|
|
if (!TinselV2) {
|
|
_vm->_mixer->stopHandle(_channels[kChannelTinsel1].handle);
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < kNumChannels; i++)
|
|
_vm->_mixer->stopHandle(_channels[i].handle);
|
|
}
|
|
|
|
void SoundManager::stopSpecSample(int id, int sub) {
|
|
debugC(DEBUG_DETAILED, kTinselDebugSound, "stopSpecSample(%d, %d)", id, sub);
|
|
|
|
if (!TinselV2) {
|
|
if (_channels[kChannelTinsel1].sampleNum == id)
|
|
_vm->_mixer->stopHandle(_channels[kChannelTinsel1].handle);
|
|
return;
|
|
}
|
|
|
|
for (int i = kChannelTalk; i < kNumChannels; i++) {
|
|
if ((_channels[i].sampleNum == id) && (_channels[i].subSample == sub))
|
|
_vm->_mixer->stopHandle(_channels[i].handle);
|
|
}
|
|
}
|
|
|
|
void SoundManager::setSFXVolumes(uint8 volume) {
|
|
if (!TinselV2)
|
|
return;
|
|
|
|
for (int i = kChannelSFX; i < kNumChannels; i++)
|
|
_vm->_mixer->setChannelVolume(_channels[i].handle, volume);
|
|
}
|
|
|
|
void SoundManager::showSoundError(const char *errorMsg, const char *soundFile) {
|
|
Common::String msg;
|
|
msg = Common::String::format(errorMsg, soundFile);
|
|
GUI::MessageDialog dialog(msg, "OK");
|
|
dialog.runModal();
|
|
|
|
error("%s", msg.c_str());
|
|
}
|
|
|
|
/**
|
|
* Opens and inits all sound sample files.
|
|
*/
|
|
void SoundManager::openSampleFiles() {
|
|
// V1 Floppy and V0 demo versions have no sample files
|
|
if (TinselV0 || (TinselV1 && !_vm->isV1CD()))
|
|
return;
|
|
|
|
TinselFile f;
|
|
|
|
if (_sampleIndex)
|
|
// already allocated
|
|
return;
|
|
|
|
// Open sample index (*.idx) in binary mode
|
|
if (f.open(_vm->getSampleIndex(g_sampleLanguage))) {
|
|
uint32 fileSize = f.size();
|
|
_sampleIndex = (uint32 *)malloc(fileSize);
|
|
if (_sampleIndex == NULL) {
|
|
showSoundError(NO_MEM, _vm->getSampleIndex(g_sampleLanguage));
|
|
return;
|
|
}
|
|
|
|
_sampleIndexLen = fileSize / 4; // total sample of indices (DWORDs)
|
|
|
|
// Load data
|
|
for (int i = 0; i < _sampleIndexLen; ++i) {
|
|
_sampleIndex[i] = f.readUint32LE();
|
|
if (f.err()) {
|
|
showSoundError(FILE_READ_ERROR, _vm->getSampleIndex(g_sampleLanguage));
|
|
}
|
|
}
|
|
|
|
f.close();
|
|
|
|
// Detect format of soundfile by looking at 1st sample-index
|
|
switch (TO_BE_32(_sampleIndex[0])) {
|
|
case MKTAG('M','P','3',' '):
|
|
debugC(DEBUG_DETAILED, kTinselDebugSound, "Detected MP3 sound-data");
|
|
_soundMode = kMP3Mode;
|
|
break;
|
|
case MKTAG('O','G','G',' '):
|
|
debugC(DEBUG_DETAILED, kTinselDebugSound, "Detected OGG sound-data");
|
|
_soundMode = kVorbisMode;
|
|
break;
|
|
case MKTAG('F','L','A','C'):
|
|
debugC(DEBUG_DETAILED, kTinselDebugSound, "Detected FLAC sound-data");
|
|
_soundMode = kFLACMode;
|
|
break;
|
|
default:
|
|
debugC(DEBUG_DETAILED, kTinselDebugSound, "Detected original sound-data");
|
|
break;
|
|
}
|
|
|
|
// Normally the 1st sample index points to nothing at all. We use it to
|
|
// determine if the game's sample files have been compressed, thus restore
|
|
// it here
|
|
_sampleIndex[0] = 0;
|
|
} else {
|
|
showSoundError(FILE_READ_ERROR, _vm->getSampleIndex(g_sampleLanguage));
|
|
}
|
|
|
|
// Open sample file (*.smp) in binary mode
|
|
if (!_sampleStream.open(_vm->getSampleFile(g_sampleLanguage))) {
|
|
showSoundError(FILE_READ_ERROR, _vm->getSampleFile(g_sampleLanguage));
|
|
}
|
|
}
|
|
|
|
void SoundManager::closeSampleStream() {
|
|
_sampleStream.close();
|
|
free(_sampleIndex);
|
|
_sampleIndex = 0;
|
|
_sampleIndexLen = 0;
|
|
}
|
|
|
|
} // End of namespace Tinsel
|