diff --git a/NEWS b/NEWS index bb660828c23..0971c1f4138 100644 --- a/NEWS +++ b/NEWS @@ -34,7 +34,7 @@ For a more comprehensive changelog for the latest experimental CVS code, see: Sword2: - Simplified memory/resource management - Simplified sound effects handling - - Added support for compressed speech. + - Added support for compressed speech and music. - Fixed various minor bugs. 0.6.1b (2004-08-03) diff --git a/sword2/driver/d_sound.cpp b/sword2/driver/d_sound.cpp index e4446359ecf..c8bbd03b5f7 100644 --- a/sword2/driver/d_sound.cpp +++ b/sword2/driver/d_sound.cpp @@ -36,171 +36,15 @@ namespace Sword2 { +static AudioStream *makeCLUStream(File *fp, int size); + static File fpMus; static void premix_proc(void *param, int16 *data, uint len) { ((Sound *) param)->streamMusic(data, len); } -// ---------------------------------------------------------------------------- -// Custom AudioStream class to handle Broken Sword II's audio compression. -// ---------------------------------------------------------------------------- - -#define BUFFER_SIZE 4096 - -#define GetCompressedShift(n) ((n) >> 4) -#define GetCompressedSign(n) (((n) >> 3) & 1) -#define GetCompressedAmplitude(n) ((n) & 7) - -class CLUInputStream : public AudioStream { -private: - File *_file; - uint32 _end_pos; - int16 _outbuf[BUFFER_SIZE]; - byte _inbuf[BUFFER_SIZE]; - const int16 *_bufferEnd; - const int16 *_pos; - - uint16 _prev; - - void refill(); - inline bool eosIntern() const; -public: - CLUInputStream(File *file, int duration); - ~CLUInputStream(); - - int readBuffer(int16 *buffer, const int numSamples); - - int16 read(); - bool endOfData() const { return eosIntern(); } - bool isStereo() const { return false; } - int getRate() const { return 22050; } -}; - -CLUInputStream::CLUInputStream(File *file, int size) - : _file(file), _bufferEnd(_outbuf + BUFFER_SIZE) { - - _file->incRef(); - - // Determine the end position. - _end_pos = file->pos() + size; - - // Read in initial data - _prev = _file->readUint16LE(); - refill(); -} - -CLUInputStream::~CLUInputStream() { - _file->decRef(); -} - -inline int16 CLUInputStream::read() { - assert(!eosIntern()); - - int16 sample = *_pos++; - if (_pos >= _bufferEnd) { - refill(); - } - return sample; -} - -inline bool CLUInputStream::eosIntern() const { - return _pos >= _bufferEnd; -} - -int CLUInputStream::readBuffer(int16 *buffer, const int numSamples) { - int samples = 0; - while (samples < numSamples && !eosIntern()) { - const int len = MIN(numSamples - samples, (int) (_bufferEnd - _pos)); - memcpy(buffer, _pos, len * 2); - buffer += len; - _pos += len; - samples += len; - if (_pos >= _bufferEnd) { - refill(); - } - } - return samples; -} - -void CLUInputStream::refill() { - byte *in = _inbuf; - int16 *out = _outbuf; - uint len_left = _file->read(in, MIN((uint32) BUFFER_SIZE, _end_pos - _file->pos())); - - while (len_left > 0) { - uint16 delta = GetCompressedAmplitude(*in) << GetCompressedShift(*in); - uint16 sample; - - if (GetCompressedSign(*in)) - sample = _prev - delta; - else - sample = _prev + delta; - - _prev = sample; - *out++ = sample; - *in++; - len_left--; - } - - _pos = _outbuf; - _bufferEnd = out; -} - -AudioStream *makeCLUStream(File *file, int size) { - assert(size >= 2); - return new CLUInputStream(file, size); -} - -// ---------------------------------------------------------------------------- -// Main sound class -// ---------------------------------------------------------------------------- - -Sound::Sound(Sword2Engine *vm) { - _vm = vm; - _mutex = _vm->_system->createMutex(); - - memset(_fx, 0, sizeof(_fx)); - - _soundOn = true; - - _speechStatus = false; - _speechPaused = false; - _speechMuted = false; - _speechVol = 14; - - _fxPaused = false; - _fxMuted = false; - _fxVol = 14; - - _musicVol = 16; - _musicMuted = false; - - for (int i = 0; i < MAXMUS; i++) - _music[i]._converter = makeRateConverter(_music[i].getRate(), _vm->_mixer->getOutputRate(), _music[i].isStereo(), false); - - _vm->_mixer->setupPremix(premix_proc, this); -} - -Sound::~Sound() { - int i; - - _vm->_mixer->setupPremix(0, 0); - - for (i = 0; i < MAXMUS; i++) - delete _music[i]._converter; - - for (i = 0; i < MAXFX; i++) - stopFxHandle(i); - - if (_mutex) - _vm->_system->deleteMutex(_mutex); -} - -// FIXME: Merge openSoundFile() and getAudioStream() once compressed music has -// been implemented? - -int Sound::openSoundFile(File *fp, const char *base) { +static AudioStream *getAudioStream(File *fp, const char *base, int cd, uint32 id, uint32 *numSamples) { struct { const char *ext; int mode; @@ -217,24 +61,27 @@ int Sound::openSoundFile(File *fp, const char *base) { { "clu", kCLUMode } }; + int soundMode = -1; char filename[20]; - int cd = _vm->_resman->whichCd(); - int i; - for (i = 0; i < ARRAYSIZE(file_types); i++) { + for (int i = 0; i < ARRAYSIZE(file_types); i++) { File f; sprintf(filename, "%s%d.%s", base, cd, file_types[i].ext); - if (f.open(filename)) + if (f.open(filename)) { + soundMode = file_types[i].mode; break; + } sprintf(filename, "%s.%s", base, file_types[i].ext); - if (f.open(filename)) + if (f.open(filename)) { + soundMode = file_types[i].mode; break; + } } - if (i == ARRAYSIZE(file_types)) - return 0; + if (soundMode == 0) + return NULL; // The assumption here is that a sound file is closed when the sound // finishes, and we never play sounds from two different files at the @@ -248,13 +95,7 @@ int Sound::openSoundFile(File *fp, const char *base) { if (!fp->isOpen()) fp->open(filename); - return file_types[i].mode; -} - -AudioStream *Sound::getAudioStream(File *fp, const char *base, uint32 id, uint32 *numSamples) { - int soundMode = openSoundFile(fp, base); - - if (!soundMode) + if (!fp->isOpen()) return NULL; fp->seek((id + 1) * ((soundMode == kCLUMode) ? 8 : 12), SEEK_SET); @@ -298,6 +139,385 @@ AudioStream *Sound::getAudioStream(File *fp, const char *base, uint32 id, uint32 } } +#define BUFFER_SIZE 4096 + +// ---------------------------------------------------------------------------- +// Custom AudioStream class to handle Broken Sword II's audio compression. +// ---------------------------------------------------------------------------- + +#define GetCompressedShift(n) ((n) >> 4) +#define GetCompressedSign(n) (((n) >> 3) & 1) +#define GetCompressedAmplitude(n) ((n) & 7) + +class CLUInputStream : public AudioStream { +private: + File *_file; + bool _firstTime; + uint32 _file_pos; + uint32 _end_pos; + int16 _outbuf[BUFFER_SIZE]; + byte _inbuf[BUFFER_SIZE]; + const int16 *_bufferEnd; + const int16 *_pos; + + uint16 _prev; + + void refill(); + inline bool eosIntern() const; +public: + CLUInputStream(File *file, int size); + ~CLUInputStream(); + + int readBuffer(int16 *buffer, const int numSamples); + + int16 read(); + bool endOfData() const { return eosIntern(); } + bool isStereo() const { return false; } + int getRate() const { return 22050; } +}; + +CLUInputStream::CLUInputStream(File *file, int size) + : _file(file), _firstTime(true), _bufferEnd(_outbuf + BUFFER_SIZE) { + + _file->incRef(); + + // Determine the end position. + _end_pos = file->pos() + size; + + // Read in initial data + _file_pos = _file->pos(); + refill(); +} + +CLUInputStream::~CLUInputStream() { + _file->decRef(); +} + +inline int16 CLUInputStream::read() { + assert(!eosIntern()); + + int16 sample = *_pos++; + if (_pos >= _bufferEnd) { + refill(); + } + return sample; +} + +inline bool CLUInputStream::eosIntern() const { + return _pos >= _bufferEnd; +} + +int CLUInputStream::readBuffer(int16 *buffer, const int numSamples) { + int samples = 0; + while (samples < numSamples && !eosIntern()) { + const int len = MIN(numSamples - samples, (int) (_bufferEnd - _pos)); + memcpy(buffer, _pos, len * 2); + buffer += len; + _pos += len; + samples += len; + if (_pos >= _bufferEnd) { + refill(); + } + } + return samples; +} + +void CLUInputStream::refill() { + byte *in = _inbuf; + int16 *out = _outbuf; + + _file->seek(_file_pos, SEEK_SET); + + uint len_left = _file->read(in, MIN((uint32) BUFFER_SIZE, _end_pos - _file->pos())); + + _file_pos = _file->pos(); + + while (len_left > 0) { + uint16 sample; + + if (_firstTime) { + _firstTime = false; + _prev = READ_LE_UINT16(in); + sample = _prev; + len_left -= 2; + in += 2; + } else { + uint16 delta = GetCompressedAmplitude(*in) << GetCompressedShift(*in); + if (GetCompressedSign(*in)) + sample = _prev - delta; + else + sample = _prev + delta; + + _prev = sample; + len_left--; + in++; + } + + *out++ = sample; + } + + _pos = _outbuf; + _bufferEnd = out; +} + +AudioStream *makeCLUStream(File *file, int size) { + return new CLUInputStream(file, size); +} + +// ---------------------------------------------------------------------------- +// Another custom AudioStream class, to wrap around the various AudioStream +// classes used for music decompression, and to add looping, fading, etc. +// ---------------------------------------------------------------------------- + +// The length of a fade-in/out, in milliseconds. +#define FADE_LENGTH 3000 + +class MusicInputStream : public AudioStream { +private: + int _cd; + uint32 _musicId; + AudioStream *_decoder; + int16 _buffer[BUFFER_SIZE]; + const int16 *_bufferEnd; + const int16 *_pos; + bool _remove; + uint32 _numSamples; + uint32 _samplesLeft; + bool _looping; + int32 _fading; + int32 _fadeSamples; + bool _paused; + + void refill(); + inline bool eosIntern() const; +public: + MusicInputStream(int cd, uint32 musicId, bool looping); + ~MusicInputStream(); + + int readBuffer(int16 *buffer, const int numSamples); + + bool endOfData() const { return eosIntern(); } + bool isStereo() const { return _decoder->isStereo(); } + int getRate() const { return _decoder->getRate(); } + + void fadeUp(); + void fadeDown(); + + bool isReady() { return _decoder != NULL; } + int32 isFading() { return _fading; } + + bool readyToRemove(); + int32 getTimeRemaining(); +}; + +MusicInputStream::MusicInputStream(int cd, uint32 musicId, bool looping) + : _cd(cd), _musicId(musicId), _bufferEnd(_buffer + BUFFER_SIZE), + _remove(false), _looping(looping), _fading(0) { + _decoder = getAudioStream(&fpMus, "music", _cd, _musicId, &_numSamples); + if (_decoder) { + _samplesLeft = _numSamples; + _fadeSamples = (getRate() * FADE_LENGTH) / 1000; + fadeUp(); + + // Read in initial data + refill(); + } +} + +MusicInputStream::~MusicInputStream() { + delete _decoder; +}; + +inline bool MusicInputStream::eosIntern() const { + if (_looping) + return false; + return _pos >= _bufferEnd; +} + +int MusicInputStream::readBuffer(int16 *buffer, const int numSamples) { + if (!_decoder) + return 0; + + int samples = 0; + while (samples < numSamples && !eosIntern()) { + const int len = MIN(numSamples - samples, (int) (_bufferEnd - _pos)); + memcpy(buffer, _pos, len * 2); + buffer += len; + _pos += len; + samples += len; + if (_pos >= _bufferEnd) { + refill(); + } + } + return samples; +} + +void MusicInputStream::refill() { + int16 *buf = _buffer; + uint32 numSamples = 0; + uint32 len_left; + bool endFade = false; + + len_left = BUFFER_SIZE; + + if (_fading > 0 && (uint32) _fading < len_left) + len_left = _fading; + + if (_samplesLeft < len_left) + len_left = _samplesLeft; + + if (!_looping) { + // None-looping music is faded out at the end. If this fade + // out would have started somewhere within the len_left samples + // to read, we only read up to that point. This way, we can + // treat this fade as any other. + + if (!_fading) { + uint32 currentlyAt = _numSamples - _samplesLeft; + uint32 fadeOutAt = _numSamples - _fadeSamples; + uint32 readTo = currentlyAt + len_left; + + if (fadeOutAt == currentlyAt) + fadeDown(); + else if (fadeOutAt > currentlyAt && fadeOutAt <= readTo) { + len_left = fadeOutAt - currentlyAt; + endFade = true; + } + } + } + + int desired = len_left - numSamples; + int len = _decoder->readBuffer(buf, desired); + + // Shouldn't happen, but if it does it could cause an infinite loop. + // Of course there were bugs that caused it to happen several times + // during development. :-) + + if (len < desired) { + warning("Expected %d samples, but got none", desired); + if (!len) + return; + } + + buf += len; + numSamples += len; + len_left -= len; + _samplesLeft -= len; + + int16 *ptr; + + if (_fading > 0) { + // Fade down + for (ptr = _buffer; ptr < buf; ptr++) { + if (--_fading <= 0) { + _looping = false; + _remove = true; + break; + } + *ptr = (*ptr * _fading) / _fadeSamples; + } + } else if (_fading < 0) { + // Fade up + for (ptr = _buffer; ptr < buf; ptr++) { + _fading--; + *ptr = -(*ptr * _fading) / _fadeSamples; + if (_fading <= -_fadeSamples) { + _fading = 0; + break; + } + } + } + + if (endFade) + fadeDown(); + + if (!_samplesLeft) { + if (_looping) { + delete _decoder; + _decoder = getAudioStream(&fpMus, "music", _cd, _musicId, &_numSamples); + _samplesLeft = _numSamples; + } else + _remove = true; + } + + _pos = _buffer; + _bufferEnd = buf; +} + +void MusicInputStream::fadeUp() { + if (_fading > 0) + _fading = -_fading; + else if (_fading == 0) + _fading = -1; +} + +void MusicInputStream::fadeDown() { + if (_fading < 0) + _fading = -_fading; + else if (_fading == 0) + _fading = _fadeSamples; +} + +bool MusicInputStream::readyToRemove() { + return _remove; +} + +int32 MusicInputStream::getTimeRemaining() { + // This is far from exact, but it doesn't have to be. + return (_samplesLeft + BUFFER_SIZE) / getRate(); +} + +// ---------------------------------------------------------------------------- +// Main sound class +// ---------------------------------------------------------------------------- + +Sound::Sound(Sword2Engine *vm) { + _vm = vm; + _mutex = _vm->_system->createMutex(); + + memset(_fx, 0, sizeof(_fx)); + + _soundOn = true; + + _speechPaused = false; + _speechMuted = false; + _speechVol = 14; + + _fxPaused = false; + _fxMuted = false; + _fxVol = 14; + + _musicVol = 16; + _musicPaused = false; + _musicMuted = false; + + for (int i = 0; i < MAXMUS; i++) { + _music[i] = NULL; + _converter[i] = NULL; + } + + _vm->_mixer->setupPremix(premix_proc, this); +} + +Sound::~Sound() { + int i; + + _vm->_mixer->setupPremix(0, 0); + + for (i = 0; i < MAXMUS; i++) { + delete _music[i]; + delete _converter[i]; + } + + for (i = 0; i < MAXFX; i++) + stopFxHandle(i); + + stopSpeech(); + + if (_mutex) + _vm->_system->deleteMutex(_mutex); +} + void Sound::streamMusic(int16 *data, uint len) { Common::StackLock lock(_mutex); @@ -305,16 +525,24 @@ void Sound::streamMusic(int16 *data, uint len) { return; for (int i = 0; i < MAXMUS; i++) { - if (!_music[i]._streaming || _music[i]._paused) - continue; - - st_volume_t volume = _musicMuted ? 0 : _musicVolTable[_musicVol]; - - fpMus.seek(_music[i]._filePos, SEEK_SET); - _music[i]._converter->flow(_music[i], data, len, volume, volume); + if (_music[i] && _music[i]->readyToRemove()) { + delete _music[i]; + delete _converter[i]; + _music[i] = NULL; + _converter[i] = NULL; + } } - if (!_music[0]._streaming && !_music[1]._streaming && fpMus.isOpen()) + if (!_musicPaused) { + for (int i = 0; i < MAXMUS; i++) { + if (_music[i]) { + st_volume_t volume = _musicMuted ? 0 : _musicVolTable[_musicVol]; + _converter[i]->flow(*_music[i], data, len, volume, volume); + } + } + } + + if (!_music[0] && !_music[1] && fpMus.isOpen()) fpMus.close(); } @@ -322,8 +550,6 @@ void Sound::streamMusic(int16 *data, uint len) { * This function creates the pan table. */ -// FIXME: Could we use the FLAG_REVERSE_STEREO mixer flag instead? - void Sound::buildPanTable(bool reverse) { int i; @@ -344,117 +570,371 @@ void Sound::buildPanTable(bool reverse) { // MUSIC // ---------------------------------------------------------------------------- -// All music is encoded at 22050 Hz so this means fading takes 3 seconds. -#define FADE_SAMPLES 66150 - int32 Sound::_musicVolTable[17] = { 0, 15, 31, 47, 63, 79, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239, 255 }; -void MusicHandle::fadeDown(void) { - if (_streaming) { - if (_fading < 0) - _fading = -_fading; - else if (_fading == 0) - _fading = FADE_SAMPLES; +/** + * Mutes/Unmutes the music. + * @param mute If mute is false, restore the volume to the last set master + * level. Otherwise the music is muted (volume 0). + */ + +void Sound::muteMusic(bool mute) { + _musicMuted = mute; +} + +/** + * @return the music's mute state, true if mute, false if not mute + */ + +bool Sound::isMusicMute(void) { + return _musicMuted; +} + +/** + * Set the volume of any future as well as playing music. + * @param volume volume, from 0 (silent) to 16 (max) + */ + +void Sound::setMusicVolume(uint8 volume) { + if (volume > 16) + volume = 16; + + _musicVol = volume; +} + +/** + * @return the volume setting for music + */ + +uint8 Sound::getMusicVolume(void) { + return _musicVol; +} + +/** + * Stops the music dead in its tracks. Any music that is currently being + * streamed is paused. + */ + +void Sound::pauseMusic(void) { + Common::StackLock lock(_mutex); + + if (!_soundOn) + return; + + _musicPaused = true; +} + +/** + * Restarts the music from where it was stopped. + */ + +void Sound::unpauseMusic(void) { + Common::StackLock lock(_mutex); + + if (!_soundOn) + return; + + _musicPaused = false; +} + +/** + * Fades out and stops the music. + */ + +void Sound::stopMusic(void) { + Common::StackLock lock(_mutex); + + for (int i = 0; i < MAXMUS; i++) + if (_music[i]) + _music[i]->fadeDown(); +} + +void Sound::waitForLeadOut(void) { + int i = getFxIndex(-1); + + if (i == MAXFX) + return; + + while (_fx[i]._handle.isActive()) { + _vm->_graphics->updateDisplay(); + _vm->_system->delay_msecs(30); } } -void MusicHandle::fadeUp(void) { - if (_streaming) { - if (_fading > 0) - _fading = -_fading; - else if (_fading == 0) - _fading = -1; +/** + * Streams music from a cluster file. + * @param filename the file name of the music cluster file + * @param musicId the id of the music to stream + * @param looping true if the music is to loop back to the start + * @return RD_OK or an error code + */ + +int32 Sound::streamCompMusic(uint32 musicId, bool looping) { + Common::StackLock lock(_mutex); + + int primary = -1; + int secondary = -1; + + // If both music streams are active, one of them will have to go. + + if (_music[0] && _music[1]) { + int32 fade0 = _music[0]->isFading(); + int32 fade1 = _music[1]->isFading(); + + if (!fade0 && !fade1) { + // Neither is fading. This shouldn't happen, so just + // pick one and be done with it. + primary = 0; + } else if (fade0 && !fade1) { + // Stream 0 is fading, so pick that one. + primary = 0; + } else if (!fade0 && fade1) { + // Stream 1 is fading, so pick that one. + primary = 1; + } else { + // Both streams are fading. Pick the one that is + // closest to silent. + if (ABS(fade0) < ABS(fade1)) + primary = 0; + else + primary = 1; + } + + delete _music[primary]; + delete _converter[primary]; + _music[primary] = NULL; + _converter[primary] = NULL; } -} -int32 MusicHandle::play(uint32 musicId, bool looping) { - fpMus.seek((musicId + 1) * 8, SEEK_SET); - _fileStart = fpMus.readUint32LE(); + // Pick the available music stream. If no music is playing it doesn't + // matter which we use. - uint32 len = fpMus.readUint32LE(); + if (_music[0] || _music[1]) { + if (_music[0]) { + primary = 1; + secondary = 0; + } else { + primary = 0; + secondary = 1; + } + } else + primary = 0; - if (!_fileStart || !len) - return RDERR_INVALIDID; + // Don't start streaming if the volume is off. + if (isMusicMute()) + return RD_OK; - _fileEnd = _fileStart + len; - _filePos = _fileStart; - _streaming = true; - _firstTime = true; - _looping = looping; - fadeUp(); + if (secondary != -1) + _music[secondary]->fadeDown(); + + _music[primary] = new MusicInputStream(_vm->_resman->whichCd(), musicId, looping); + + if (!_music[primary]->isReady()) { + delete _music[primary]; + _music[primary] = NULL; + return RDERR_INVALIDFILENAME; + } + + _converter[primary] = makeRateConverter(_music[primary]->getRate(), _vm->_mixer->getOutputRate(), _music[primary]->isStereo(), false); return RD_OK; } -void MusicHandle::stop(void) { - _streaming = false; - _looping = false; - _fading = 0; -} +/** + * @return the time left for the current music, in seconds. + */ -int MusicHandle::readBuffer(int16 *buffer, const int numSamples) { - assert(numSamples > 0); - int samples; +int32 Sound::musicTimeRemaining(void) { + Common::StackLock lock(_mutex); - // Assume the file handle has been correctly positioned already. - - for (samples = 0; samples < numSamples && !endOfData(); samples++) { - int16 out; - if (_firstTime) { - _lastSample = fpMus.readUint16LE(); - _filePos += 2; - _firstTime = false; - out = _lastSample; - } else { - uint8 in = fpMus.readByte(); - uint16 delta = GetCompressedAmplitude(in) << GetCompressedShift(in); - - if (GetCompressedSign(in)) - out = _lastSample - delta; - else - out = _lastSample + delta; - - _filePos++; - _lastSample = out; - - if (_looping) { - if (_filePos >= _fileEnd) { - _firstTime = true; - _filePos = _fileStart; - fpMus.seek(_filePos, SEEK_SET); - } - } else { - // Fade out at the end of the music, unless it already is. - if (_fileEnd - _filePos <= FADE_SAMPLES && _fading <= 0) - fadeDown(); - } - - if (_fading > 0) { - if (--_fading == 0) { - _streaming = false; - _looping = false; - } - out = (out * _fading) / FADE_SAMPLES; - } else if (_fading < 0) { - _fading--; - out = -(out * _fading) / FADE_SAMPLES; - if (_fading <= -FADE_SAMPLES) - _fading = 0; - } - } - - *buffer++ = out; + for (int i = 0; i < MAXMUS; i++) { + if (_music[i] && _music[i]->isFading() <= 0) + return _music[i]->getTimeRemaining(); } - return samples; + return 0; } -bool MusicHandle::endOfData(void) const { - return (!_streaming || _filePos >= _fileEnd); +// ---------------------------------------------------------------------------- +// SPEECH +// ---------------------------------------------------------------------------- + +/** + * Mutes/Unmutes the speech. + * @param mute If mute is false, restore the volume to the last set master + * level. Otherwise the speech is muted (volume 0). + */ + +void Sound::muteSpeech(bool mute) { + _speechMuted = mute; + + if (_soundHandleSpeech.isActive()) { + byte volume = mute ? 0 : 16 * _speechVol; + + _vm->_mixer->setChannelVolume(_soundHandleSpeech, volume); + } } +/** + * @return the speech's mute state, true if mute, false if not mute + */ + +bool Sound::isSpeechMute(void) { + return _speechMuted; +} + +/** + * Set the volume of any future as well as playing speech samples. + * @param volume volume, from 0 (silent) to 14 (max) + */ + +void Sound::setSpeechVolume(uint8 volume) { + if (volume > 14) + volume = 14; + + _speechVol = volume; + + if (_soundHandleSpeech.isActive() && !_speechMuted && _soundHandleSpeech.isActive()) { + _vm->_mixer->setChannelVolume(_soundHandleSpeech, 16 * _speechVol); + } +} + +/** + * @return the volume setting for speech + */ + +uint8 Sound::getSpeechVolume(void) { + return _speechVol; +} + +/** + * Stops the speech dead in its tracks. + */ + +void Sound::pauseSpeech(void) { + _speechPaused = true; + _vm->_mixer->pauseHandle(_soundHandleSpeech, true); +} + +/** + * Restarts the speech from where it was stopped. + */ + +void Sound::unpauseSpeech(void) { + _speechPaused = false; + _vm->_mixer->pauseHandle(_soundHandleSpeech, false); +} + +/** + * Stops the speech from playing. + */ + +int32 Sound::stopSpeech(void) { + if (!_soundOn) + return RD_OK; + + if (_soundHandleSpeech.isActive()) { + _vm->_mixer->stopHandle(_soundHandleSpeech); + return RD_OK; + } + return RDERR_SPEECHNOTPLAYING; +} + +/** + * @return Either RDSE_SAMPLEPLAYING or RDSE_SAMPLEFINISHED + */ + +int32 Sound::getSpeechStatus(void) { + return _soundHandleSpeech.isActive() ? RDSE_SAMPLEPLAYING : RDSE_SAMPLEFINISHED; +} + +/** + * Returns either RDSE_QUIET or RDSE_SPEAKING + */ + +int32 Sound::amISpeaking(void) { + if (!_speechMuted && !_speechPaused && _soundHandleSpeech.isActive()) + return RDSE_SPEAKING; + + return RDSE_QUIET; +} + +/** + * This function loads and decompresses a list of speech from a cluster, but + * does not play it. This is used for cutscene voice-overs, presumably to + * avoid having to read from more than one file on the CD during playback. + * @param speechid the text line id used to reference the speech + * @param buf a pointer to the buffer that will be allocated for the sound + */ + +uint32 Sound::preFetchCompSpeech(uint32 speechid, uint16 **buf) { + File fp; + uint32 numSamples; + + AudioStream *input = getAudioStream(&fp, "speech", _vm->_resman->whichCd(), speechid, &numSamples); + + *buf = NULL; + + // Decompress data into speech buffer. + + uint32 bufferSize = 2 * numSamples; + + *buf = (uint16 *) malloc(bufferSize); + if (!*buf) { + delete input; + fp.close(); + return 0; + } + + uint32 readSamples = input->readBuffer((int16 *) *buf, numSamples); + + fp.close(); + delete input; + + return 2 * readSamples; +} + +/** + * This function loads, decompresses and plays a line of speech. An error + * occurs if speech is already playing. + * @param speechid the text line id used to reference the speech + * @param vol volume, 0 (minimum) to 16 (maximum) + * @param pan panning, -16 (full left) to 16 (full right) + */ + +int32 Sound::playCompSpeech(uint32 speechid, uint8 vol, int8 pan) { + if (_speechMuted) + return RD_OK; + + if (getSpeechStatus() == RDERR_SPEECHPLAYING) + return RDERR_SPEECHPLAYING; + + File *fp = new File; + AudioStream *input = getAudioStream(fp, "speech", _vm->_resman->whichCd(), speechid, NULL); + + // Make the AudioStream object the sole owner of the file so that it + // will die along with the AudioStream when the speech has finished. + fp->decRef(); + + if (!input) + return RDERR_INVALIDID; + + // Modify the volume according to the master volume + + byte volume = _speechMuted ? 0 : vol * _speechVol; + int8 p = _panTable[pan + 16]; + + // Start the speech playing + _vm->_mixer->playInputStream(&_soundHandleSpeech, input, false, volume, p); + return RD_OK; +} + +// ---------------------------------------------------------------------------- +// SOUND EFFECTS +// ---------------------------------------------------------------------------- + /** * Retrieve information about an in-memory WAV file. * @param data The WAV data @@ -504,451 +984,6 @@ bool Sound::getWavInfo(byte *data, WavInfo *wavInfo) { return true; } -/** - * Mutes/Unmutes the music. - * @param mute If mute is false, restore the volume to the last set master - * level. Otherwise the music is muted (volume 0). - */ - -void Sound::muteMusic(bool mute) { - _musicMuted = mute; -} - -/** - * @return the music's mute state, true if mute, false if not mute - */ - -bool Sound::isMusicMute(void) { - return _musicMuted; -} - -/** - * Set the volume of any future as well as playing music. - * @param volume volume, from 0 (silent) to 16 (max) - */ - -void Sound::setMusicVolume(uint8 volume) { - if (volume > 16) - volume = 16; - - _musicVol = volume; -} - -/** - * @return the volume setting for music - */ - -uint8 Sound::getMusicVolume(void) { - return _musicVol; -} - -/** - * Stops the music dead in its tracks. Any music that is currently being - * streamed is paused. - */ - -void Sound::pauseMusic(void) { - Common::StackLock lock(_mutex); - - if (_soundOn) { - for (int i = 0; i < MAXMUS; i++) - _music[i]._paused = true; - } -} - -/** - * Restarts the music from where it was stopped. - */ - -void Sound::unpauseMusic(void) { - Common::StackLock lock(_mutex); - - if (_soundOn) { - for (int i = 0; i < MAXMUS; i++) - _music[i]._paused = false; - } -} - -/** - * Fades out and stops the music. - */ - -void Sound::stopMusic(void) { - Common::StackLock lock(_mutex); - - for (int i = 0; i < MAXMUS; i++) - _music[i].fadeDown(); -} - -/** - * Save/Restore information about current music so that we can restore it - * after the credits. - */ - -void Sound::saveMusicState(void) { - Common::StackLock lock(_mutex); - - int saveStream; - - if (_music[0]._streaming && _music[0]._fading <= 0) { - saveStream = 0; - } else if (_music[1]._streaming && _music[0]._fading <= 0) { - saveStream = 1; - } else { - _music[2]._streaming = false; - savedMusicFilename = NULL; - return; - } - - _music[2]._streaming = true; - _music[2]._fading = 0; - _music[2]._looping = _music[saveStream]._looping; - _music[2]._fileStart = _music[saveStream]._fileStart; - _music[2]._filePos = _music[saveStream]._filePos; - _music[2]._fileEnd = _music[saveStream]._fileEnd; - _music[2]._lastSample = _music[saveStream]._lastSample; - - if (fpMus.isOpen()) - savedMusicFilename = strdup(fpMus.name()); - else - savedMusicFilename = NULL; -} - -void Sound::restoreMusicState(void) { - Common::StackLock lock(_mutex); - - // Fade out any music that happens to be playing - - for (int i = 0; i < MAXMUS; i++) - _music[i].fadeDown(); - - if (!_music[2]._streaming) - return; - - int restoreStream; - - if (!_music[0]._streaming) - restoreStream = 0; - else - restoreStream = 1; - - _music[restoreStream]._streaming = true; - _music[restoreStream]._fading = 0; - _music[restoreStream]._looping = _music[2]._looping; - _music[restoreStream]._fileStart = _music[2]._fileStart; - _music[restoreStream]._filePos = _music[2]._filePos; - _music[restoreStream]._fileEnd = _music[2]._fileEnd; - _music[restoreStream]._lastSample = _music[2]._lastSample; - _music[restoreStream].fadeUp(); - - if (savedMusicFilename) { - if (fpMus.isOpen()) - fpMus.close(); - - fpMus.open(savedMusicFilename); - free(savedMusicFilename); - savedMusicFilename = NULL; - } -} - -void Sound::waitForLeadOut(void) { - int i = getFxIndex(-1); - - if (i == MAXFX) - return; - - while (_fx[i]._handle.isActive()) { - _vm->_graphics->updateDisplay(); - _vm->_system->delay_msecs(30); - } -} - -/** - * Streams music from a cluster file. - * @param filename the file name of the music cluster file - * @param musicId the id of the music to stream - * @param looping true if the music is to loop back to the start - * @return RD_OK or an error code - */ - -int32 Sound::streamCompMusic(uint32 musicId, bool looping) { - Common::StackLock lock(_mutex); - - int32 primaryStream = -1; - int32 secondaryStream = -1; - - // If both music streams are playing, one of them will have to go. - - if (_music[0]._streaming && _music[1]._streaming) { - if (!_music[0]._fading && !_music[1]._fading) { - // None of them are fading. This shouldn't happen, so - // just pick one and be done with it. - primaryStream = 0; - } else if (_music[0]._fading && !_music[1]._fading) { - // Stream 0 is fading, so pick that one. - primaryStream = 0; - } else if (!_music[0]._fading && _music[1]._fading) { - // Stream 1 is fading, so pick that one. - primaryStream = 1; - } else { - // Both streams are fading. Pick the one that is - // closest to silent. - if (ABS(_music[0]._fading) < ABS(_music[1]._fading)) - primaryStream = 0; - else - primaryStream = 1; - } - - _music[primaryStream].stop(); - } - - // Pick the available music stream. If no music is playing it doesn't - // matter which we use, so pick the first one. - - if (_music[0]._streaming || _music[1]._streaming) { - if (_music[0]._streaming) { - primaryStream = 1; - secondaryStream = 0; - } else { - primaryStream = 0; - secondaryStream = 1; - } - } else - primaryStream = 0; - - // Don't start streaming if the volume is off. - if (isMusicMute()) - return RD_OK; - - // Start other music stream fading out - if (secondaryStream != -1) - _music[secondaryStream].fadeDown(); - - switch (openSoundFile(&fpMus, "music")) { - case 0: - return RDERR_INVALIDFILENAME; - case kCLUMode: - break; - default: - warning("Compressed music is not yet supported"); - return RDERR_INVALIDFILENAME; - } - - return _music[primaryStream].play(musicId, looping); -} - -/** - * @return the time left for the current music, in seconds. - */ - -int32 Sound::musicTimeRemaining(void) { - Common::StackLock lock(_mutex); - - for (int i = 0; i < MAXMUS; i++) { - if (_music[i]._streaming && _music[i]._fading <= 0) - return (_music[i]._fileEnd - _music[i]._filePos) / _music[i].getRate(); - } - - return 0; -} - -// ---------------------------------------------------------------------------- -// SPEECH -// ---------------------------------------------------------------------------- - -/** - * Mutes/Unmutes the speech. - * @param mute If mute is false, restore the volume to the last set master - * level. Otherwise the speech is muted (volume 0). - */ - -void Sound::muteSpeech(bool mute) { - _speechMuted = mute; - - if (getSpeechStatus() == RDSE_SAMPLEPLAYING) { - byte volume = mute ? 0 : 16 * _speechVol; - - _vm->_mixer->setChannelVolume(_soundHandleSpeech, volume); - } -} - -/** - * @return the speech's mute state, true if mute, false if not mute - */ - -bool Sound::isSpeechMute(void) { - return _speechMuted; -} - -/** - * Set the volume of any future as well as playing speech samples. - * @param volume volume, from 0 (silent) to 14 (max) - */ - -void Sound::setSpeechVolume(uint8 volume) { - if (volume > 14) - volume = 14; - - _speechVol = volume; - - if (_soundHandleSpeech.isActive() && !_speechMuted && getSpeechStatus() == RDSE_SAMPLEPLAYING) { - _vm->_mixer->setChannelVolume(_soundHandleSpeech, 16 * _speechVol); - } -} - -/** - * @return the volume setting for speech - */ - -uint8 Sound::getSpeechVolume(void) { - return _speechVol; -} - -/** - * Stops the speech dead in its tracks. - */ - -void Sound::pauseSpeech(void) { - if (getSpeechStatus() == RDSE_SAMPLEPLAYING) { - _speechPaused = true; - _vm->_mixer->pauseHandle(_soundHandleSpeech, true); - } -} - -/** - * Restarts the speech from where it was stopped. - */ - -void Sound::unpauseSpeech(void) { - if (_speechPaused) { - _speechPaused = false; - _vm->_mixer->pauseHandle(_soundHandleSpeech, false); - } -} - -/** - * Stops the speech from playing. - */ - -int32 Sound::stopSpeech(void) { - if (!_soundOn) - return RD_OK; - - if (_speechStatus) { - _vm->_mixer->stopHandle(_soundHandleSpeech); - _speechStatus = false; - return RD_OK; - } - return RDERR_SPEECHNOTPLAYING; -} - -/** - * @return Either RDSE_SAMPLEPLAYING or RDSE_SAMPLEFINISHED - */ - -int32 Sound::getSpeechStatus(void) { - if (!_soundOn || !_speechStatus) - return RDSE_SAMPLEFINISHED; - - if (_speechPaused) - return RDSE_SAMPLEPLAYING; - - if (!_soundHandleSpeech.isActive()) { - _speechStatus = false; - return RDSE_SAMPLEFINISHED; - } - return RDSE_SAMPLEPLAYING; -} - -/** - * Returns either RDSE_QUIET or RDSE_SPEAKING - */ - -int32 Sound::amISpeaking(void) { - if (!_speechMuted && !_speechPaused && _soundHandleSpeech.isActive()) - return RDSE_SPEAKING; - - return RDSE_QUIET; -} - -/** - * This function loads and decompresses a list of speech from a cluster, but - * does not play it. This is used for cutscene voice-overs, presumably to - * avoid having to read from more than one file on the CD during playback. - * @param speechid the text line id used to reference the speech - * @param buf a pointer to the buffer that will be allocated for the sound - */ - -uint32 Sound::preFetchCompSpeech(uint32 speechid, uint16 **buf) { - File fp; - uint32 numSamples; - - AudioStream *input = getAudioStream(&fp, "speech", speechid, &numSamples); - - *buf = NULL; - - // Decompress data into speech buffer. - - uint32 bufferSize = 2 * numSamples; - - *buf = (uint16 *) malloc(bufferSize); - if (!*buf) { - delete input; - fp.close(); - return 0; - } - - uint32 readSamples = input->readBuffer((int16 *) *buf, numSamples); - - fp.close(); - delete input; - - return 2 * readSamples; -} - -/** - * This function loads, decompresses and plays a line of speech. An error - * occurs if speech is already playing. - * @param speechid the text line id used to reference the speech - * @param vol volume, 0 (minimum) to 16 (maximum) - * @param pan panning, -16 (full left) to 16 (full right) - */ - -int32 Sound::playCompSpeech(uint32 speechid, uint8 vol, int8 pan) { - if (_speechMuted) - return RD_OK; - - if (getSpeechStatus() == RDERR_SPEECHPLAYING) - return RDERR_SPEECHPLAYING; - - File *fp = new File; - AudioStream *input = getAudioStream(fp, "speech", speechid, NULL); - - // Make the AudioStream object the sole owner of the file so that it - // will die along with the AudioStream when the speech has finished. - fp->decRef(); - - if (!input) - return RDERR_INVALIDID; - - // Modify the volume according to the master volume - - byte volume = _speechMuted ? 0 : vol * _speechVol; - int8 p = _panTable[pan + 16]; - - // Start the speech playing - _speechPaused = true; - - _vm->_mixer->playInputStream(&_soundHandleSpeech, input, false, volume, p); - _speechStatus = true; - - return RD_OK; -} - -// ---------------------------------------------------------------------------- -// SOUND EFFECTS -// ---------------------------------------------------------------------------- - /** * @return the index of the sound effect with the ID passed in. */ diff --git a/sword2/driver/d_sound.h b/sword2/driver/d_sound.h index 0ba542bdfea..eeed5ca77aa 100644 --- a/sword2/driver/d_sound.h +++ b/sword2/driver/d_sound.h @@ -28,6 +28,8 @@ class RateConverter; namespace Sword2 { +class MusicInputStream; + // Max number of sound fx #define MAXFX 16 #define MAXMUS 2 @@ -55,36 +57,6 @@ struct FxHandle { PlayingSoundHandle _handle; }; -class MusicHandle : public AudioStream { -public: - RateConverter *_converter; - bool _firstTime; - bool _streaming; - bool _paused; - bool _looping; - int32 _fading; - int32 _fileStart; - int32 _filePos; - int32 _fileEnd; - uint16 _lastSample; - - bool isStereo(void) const { return false; } - int getRate(void) const { return 22050; } - - void fadeDown(void); - void fadeUp(void); - int32 play(uint32 musicId, bool looping); - void stop(void); - int readBuffer(int16 *buffer, const int numSamples); - bool endOfData(void) const; - // This stream never 'ends' - bool endOfStream(void) const { return false; } - - MusicHandle() : _firstTime(false), _streaming(false), _paused(false), - _looping(false), _fading(0), _fileStart(0), - _filePos(0), _fileEnd(0), _lastSample(0) {} -}; - class Sound { private: Sword2Engine *_vm; @@ -95,13 +67,13 @@ private: bool _soundOn; static int32 _musicVolTable[17]; - MusicHandle _music[MAXMUS + 1]; - char *savedMusicFilename; + MusicInputStream *_music[MAXMUS]; + RateConverter *_converter[MAXMUS]; + bool _musicPaused; bool _musicMuted; uint8 _musicVol; PlayingSoundHandle _soundHandleSpeech; - bool _speechStatus; bool _speechPaused; bool _speechMuted; uint8 _speechVol; @@ -114,9 +86,6 @@ private: int32 getFxIndex(int32 id); void stopFxHandle(int i); - int openSoundFile(File *fp, const char *base); - AudioStream *getAudioStream(File *fp, const char *base, uint32 id, uint32 *numSamples); - public: Sound(Sword2Engine *vm); ~Sound(); @@ -133,8 +102,6 @@ public: void pauseMusic(void); void unpauseMusic(void); void stopMusic(void); - void saveMusicState(void); - void restoreMusicState(void); void waitForLeadOut(void); int32 streamCompMusic(uint32 musicId, bool looping); int32 musicTimeRemaining(void); diff --git a/sword2/function.cpp b/sword2/function.cpp index 4f95b89852c..6c275f97473 100644 --- a/sword2/function.cpp +++ b/sword2/function.cpp @@ -369,6 +369,8 @@ struct CreditsLine { #define CREDITS_LINE_SPACING 20 int32 Logic::fnPlayCredits(int32 *params) { + uint32 loopingMusicId = _vm->_loopingMusicId; + // This function just quits the game if this is the playable demo, ie. // credits are NOT played in the demo any more! @@ -383,11 +385,8 @@ int32 Logic::fnPlayCredits(int32 *params) { _vm->setMouse(0); - _vm->_sound->saveMusicState(); - _vm->_sound->muteFx(true); _vm->_sound->muteSpeech(true); - _vm->_sound->stopMusic(); _vm->_graphics->waitForFade(); _vm->_graphics->fadeDown(); @@ -703,10 +702,16 @@ int32 Logic::fnPlayCredits(int32 *params) { if (_vm->_quit) return IR_CONT; - _vm->_sound->restoreMusicState(); _vm->_sound->muteFx(false); _vm->_sound->muteSpeech(false); + if (loopingMusicId) { + pars[0] = loopingMusicId; + pars[1] = FX_LOOP; + fnPlayMusic(pars); + } else + fnStopMusic(NULL); + _vm->_thisScreen.new_palette = 99; if (!_vm->_mouseStatus || _choosing)