mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-16 14:50:17 +00:00
820 lines
24 KiB
C++
820 lines
24 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
// SNDDecoder based on snd2wav by Abraham Macias Paredes
|
|
// https://github.com/System25/drxtract/blob/master/snd2wav
|
|
// License: GNU GPL v2 (see COPYING file for details)
|
|
|
|
#include "common/file.h"
|
|
#include "common/macresman.h"
|
|
#include "common/substream.h"
|
|
|
|
#include "audio/decoders/wave.h"
|
|
#include "audio/decoders/raw.h"
|
|
#include "audio/softsynth/pcspk.h"
|
|
#include "audio/decoders/aiff.h"
|
|
|
|
#include "director/director.h"
|
|
#include "director/movie.h"
|
|
#include "director/castmember.h"
|
|
#include "director/sound.h"
|
|
#include "director/util.h"
|
|
#include "director/window.h"
|
|
|
|
namespace Director {
|
|
|
|
DirectorSound::DirectorSound(Window *window) : _window(window) {
|
|
uint numChannels = 2;
|
|
if (g_director->getVersion() >= 300) {
|
|
numChannels = 4;
|
|
}
|
|
|
|
for (uint i = 0; i < numChannels; i++) {
|
|
_channels.push_back(SoundChannel());
|
|
}
|
|
|
|
_mixer = g_system->getMixer();
|
|
|
|
_speaker = new Audio::PCSpeaker();
|
|
_mixer->playStream(Audio::Mixer::kSFXSoundType,
|
|
&_pcSpeakerHandle, _speaker, -1, 50, 0, DisposeAfterUse::NO, true);
|
|
|
|
_enable = true;
|
|
}
|
|
|
|
DirectorSound::~DirectorSound() {
|
|
this->stopSound();
|
|
unloadSampleSounds();
|
|
delete _speaker;
|
|
}
|
|
|
|
SoundChannel *DirectorSound::getChannel(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return nullptr;
|
|
return &_channels[soundChannel - 1];
|
|
}
|
|
|
|
void DirectorSound::playFile(Common::String filename, uint8 soundChannel) {
|
|
if (debugChannelSet(-1, kDebugFast))
|
|
return;
|
|
|
|
AudioFileDecoder af(filename);
|
|
Audio::AudioStream *sound = af.getAudioStream(false, false, DisposeAfterUse::YES);
|
|
|
|
cancelFade(soundChannel);
|
|
stopSound(soundChannel);
|
|
_mixer->playStream(Audio::Mixer::kSFXSoundType, &_channels[soundChannel - 1].handle, sound, -1, getChannelVolume(soundChannel));
|
|
|
|
// Set the last played sound so that cast member 0 in the sound channel doesn't stop this file.
|
|
setLastPlayedSound(soundChannel, SoundID(), false);
|
|
}
|
|
|
|
void DirectorSound::playMCI(Audio::AudioStream &stream, uint32 from, uint32 to) {
|
|
Audio::SeekableAudioStream *seekStream = dynamic_cast<Audio::SeekableAudioStream *>(&stream);
|
|
Audio::SubSeekableAudioStream *subSeekStream = new Audio::SubSeekableAudioStream(seekStream, Audio::Timestamp(from, seekStream->getRate()), Audio::Timestamp(to, seekStream->getRate()));
|
|
|
|
// TODO: make sound enable settings work on this one
|
|
_mixer->stopHandle(_scriptSound);
|
|
_mixer->playStream(Audio::Mixer::kSFXSoundType, &_scriptSound, subSeekStream);
|
|
}
|
|
|
|
uint8 DirectorSound::getChannelVolume(uint8 soundChannel) {
|
|
return _enable ? _channels[soundChannel - 1].volume : 0;
|
|
}
|
|
|
|
void DirectorSound::playStream(Audio::AudioStream &stream, uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
cancelFade(soundChannel);
|
|
_mixer->stopHandle(_channels[soundChannel - 1].handle);
|
|
_mixer->playStream(Audio::Mixer::kSFXSoundType, &_channels[soundChannel - 1].handle, &stream, -1, getChannelVolume(soundChannel));
|
|
}
|
|
|
|
void DirectorSound::playSound(SoundID soundID, uint8 soundChannel, bool forPuppet) {
|
|
switch (soundID.type) {
|
|
case kSoundCast:
|
|
playCastMember(CastMemberID(soundID.u.cast.member, soundID.u.cast.castLib), soundChannel, forPuppet);
|
|
break;
|
|
case kSoundExternal:
|
|
playExternalSound(soundID.u.external.menu, soundID.u.external.submenu, soundChannel);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void DirectorSound::playCastMember(CastMemberID memberID, uint8 soundChannel, bool forPuppet) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
if (memberID.member == 0) {
|
|
// Normally cast member 0 stops the sound.
|
|
// But there are some sounds where it doesn't. Those are:
|
|
// 1. playFile
|
|
// 2. FPlay
|
|
// 3. non-puppet looping sounds
|
|
// 4. maybe more?
|
|
if (shouldStopOnZero(soundChannel)) {
|
|
stopSound(soundChannel);
|
|
} else {
|
|
// Don't stop the currently playing sound, just set the last played sound to 0.
|
|
setLastPlayedSound(soundChannel, SoundID(), false);
|
|
}
|
|
} else {
|
|
CastMember *soundCast = _window->getCurrentMovie()->getCastMember(memberID);
|
|
if (soundCast) {
|
|
if (soundCast->_type != kCastSound) {
|
|
warning("DirectorSound::playCastMember: attempted to play a non-SoundCastMember %s", memberID.asString().c_str());
|
|
} else {
|
|
bool looping = ((SoundCastMember *)soundCast)->_looping;
|
|
bool stopOnZero = true;
|
|
|
|
if (!forPuppet && isLastPlayedSound(soundChannel, memberID)) {
|
|
// We just played this sound.
|
|
// If the sound is not marked "looping", we should not play it again.
|
|
if (!looping)
|
|
return;
|
|
|
|
// If the sound is not finished yet, we need to wait more before playing it again.
|
|
if (isChannelActive(soundChannel))
|
|
return;
|
|
|
|
// We know that this is a non-puppet, looping sound.
|
|
// We don't want to stop it if this channel's cast member changes to 0.
|
|
stopOnZero = false;
|
|
}
|
|
|
|
AudioDecoder *ad = ((SoundCastMember *)soundCast)->_audio;
|
|
if (!ad) {
|
|
warning("DirectorSound::playCastMember: no audio data attached to %s", memberID.asString().c_str());
|
|
return;
|
|
}
|
|
|
|
Audio::AudioStream *as;
|
|
as = ad->getAudioStream(looping, forPuppet);
|
|
|
|
if (!as) {
|
|
warning("DirectorSound::playCastMember: audio data failed to load from cast");
|
|
return;
|
|
}
|
|
playStream(*as, soundChannel);
|
|
setLastPlayedSound(soundChannel, memberID, stopOnZero);
|
|
}
|
|
} else {
|
|
warning("DirectorSound::playCastMember: couldn't find %s", memberID.asString().c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void DirectorSound::setSoundEnabled(bool enabled) {
|
|
if (_enable == enabled)
|
|
return;
|
|
if (!enabled)
|
|
stopSound();
|
|
_enable = enabled;
|
|
}
|
|
|
|
void SNDDecoder::loadExternalSoundStream(Common::SeekableReadStreamEndian &stream) {
|
|
_size = stream.readUint32BE();
|
|
|
|
uint16 sampleRateFlag = stream.readUint16();
|
|
/*uint16 unk2 = */ stream.readUint16();
|
|
|
|
_data = (byte *)malloc(_size);
|
|
stream.read(_data, _size);
|
|
|
|
switch (sampleRateFlag) {
|
|
case 1:
|
|
_rate = 22254;
|
|
break;
|
|
case 2:
|
|
_rate = 11127;
|
|
break;
|
|
case 3:
|
|
_rate = 7300;
|
|
break;
|
|
case 4:
|
|
_rate = 5500;
|
|
break;
|
|
default:
|
|
warning("DirectorSound::loadExternalSoundStream: Can't handle sampleRateFlag %d, using default one", sampleRateFlag);
|
|
_rate = 5500;
|
|
break;
|
|
}
|
|
|
|
// this may related to the unk2 flag
|
|
// TODO: figure out how to read audio flags
|
|
_flags = Audio::FLAG_UNSIGNED;
|
|
_channels = 1;
|
|
}
|
|
|
|
void DirectorSound::registerFade(uint8 soundChannel, bool fadeIn, int ticks) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
// sound enable is not working on fade sounds, so we just return directly when sounds are not enabling
|
|
if (!_enable)
|
|
return;
|
|
|
|
cancelFade(soundChannel);
|
|
|
|
int startVol = fadeIn ? 0 : _channels[soundChannel - 1].volume;
|
|
int targetVol = fadeIn ? _channels[soundChannel - 1].volume : 0;
|
|
|
|
_channels[soundChannel - 1].fade = new FadeParams(startVol, targetVol, ticks, _window->getVM()->getMacTicks(), fadeIn);
|
|
_mixer->setChannelVolume(_channels[soundChannel - 1].handle, startVol);
|
|
}
|
|
|
|
bool DirectorSound::fadeChannel(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel) || !isChannelActive(soundChannel))
|
|
return false;
|
|
|
|
FadeParams *fade = _channels[soundChannel - 1].fade;
|
|
if (!fade)
|
|
return false;
|
|
|
|
fade->lapsedTicks = _window->getVM()->getMacTicks() - fade->startTicks;
|
|
if (fade->lapsedTicks > fade->totalTicks) {
|
|
cancelFade(soundChannel);
|
|
return false;
|
|
}
|
|
|
|
int fadeVol;
|
|
if (fade->fadeIn) {
|
|
fadeVol = MIN(fade->lapsedTicks * ((float)fade->targetVol / fade->totalTicks), (float)Audio::Mixer::kMaxChannelVolume);
|
|
} else {
|
|
fadeVol = MAX((fade->totalTicks - fade->lapsedTicks) * ((float)fade->startVol / fade->totalTicks), (float)0);
|
|
}
|
|
|
|
_mixer->setChannelVolume(_channels[soundChannel - 1].handle, fadeVol);
|
|
return true;
|
|
}
|
|
|
|
void DirectorSound::cancelFade(uint8 soundChannel) {
|
|
// NOTE: It is assumed that soundChannel has already been validated, which is
|
|
// why this method is private.
|
|
|
|
if (_channels[soundChannel - 1].fade) {
|
|
_mixer->setChannelVolume(_channels[soundChannel - 1].handle, _channels[soundChannel - 1].fade->targetVol);
|
|
|
|
delete _channels[soundChannel - 1].fade;
|
|
_channels[soundChannel - 1].fade = nullptr;
|
|
}
|
|
}
|
|
|
|
bool DirectorSound::isChannelActive(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return false;
|
|
return _mixer->isSoundHandleActive(_channels[soundChannel - 1].handle);
|
|
}
|
|
|
|
bool DirectorSound::isChannelValid(uint8 soundChannel) {
|
|
if (soundChannel == 0 || soundChannel > _channels.size()) {
|
|
warning("Invalid sound channel %d", soundChannel);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DirectorSound::loadSampleSounds(uint type) {
|
|
if (type < kMinSampledMenu || type > kMaxSampledMenu) {
|
|
warning("DirectorSound::loadSampleSounds: Invalid menu number %d", type);
|
|
return;
|
|
}
|
|
|
|
if (!_sampleSounds[type - kMinSampledMenu].empty())
|
|
return;
|
|
|
|
// trying to load external sample sounds
|
|
// lazy loading
|
|
uint32 tag = MKTAG('C', 'S', 'N', 'D');
|
|
uint id = 0xFF;
|
|
Archive *archive = nullptr;
|
|
|
|
for (Common::HashMap<Common::String, Archive *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo>::iterator it = g_director->_openResFiles.begin(); it != g_director->_openResFiles.end(); ++it) {
|
|
Common::Array<uint16> idList = it->_value->getResourceIDList(tag);
|
|
for (uint j = 0; j < idList.size(); j++) {
|
|
if ((idList[j] & 0xFF) == type) {
|
|
id = idList[j];
|
|
archive = it->_value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (id == 0xFF) {
|
|
warning("Score::loadSampleSounds: can not find CSND resource with id %d", type);
|
|
return;
|
|
}
|
|
|
|
Common::SeekableReadStreamEndian *csndData = archive->getResource(tag, id);
|
|
|
|
/*uint32 flag = */ csndData->readUint32();
|
|
|
|
// the flag should be 0x604E
|
|
// i'm not sure what's that mean, but it occurs in those csnd files
|
|
|
|
// contains how many csnd data
|
|
uint16 num = csndData->readUint16();
|
|
|
|
// read the offset first;
|
|
Common::Array<uint32> offset(num);
|
|
for (uint i = 0; i < num; i++)
|
|
offset[i] = csndData->readUint32();
|
|
|
|
for (uint i = 0; i < num; i++) {
|
|
csndData->seek(offset[i]);
|
|
|
|
SNDDecoder *ad = new SNDDecoder();
|
|
ad->loadExternalSoundStream(*csndData);
|
|
_sampleSounds[type - kMinSampledMenu].push_back(ad);
|
|
}
|
|
|
|
delete csndData;
|
|
}
|
|
|
|
void DirectorSound::unloadSampleSounds() {
|
|
for (uint i = 0; i < kNumSampledMenus; i++) {
|
|
for (uint j = 0; j < _sampleSounds[i].size(); j++) {
|
|
delete _sampleSounds[i][j];
|
|
}
|
|
_sampleSounds[i].clear();
|
|
}
|
|
}
|
|
|
|
void DirectorSound::playExternalSound(uint16 menu, uint16 submenu, uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
SoundID soundId(kSoundExternal, menu, submenu);
|
|
if (isChannelActive(soundChannel) && isLastPlayedSound(soundChannel, soundId))
|
|
return;
|
|
|
|
if (menu < kMinSampledMenu || menu > kMaxSampledMenu) {
|
|
warning("DirectorSound::playExternalSound: Invalid menu number %d", menu);
|
|
return;
|
|
}
|
|
|
|
Common::Array<AudioDecoder *> &menuSounds = _sampleSounds[menu - kMinSampledMenu];
|
|
if (menuSounds.empty())
|
|
loadSampleSounds(menu);
|
|
|
|
if (1 <= submenu && submenu <= menuSounds.size()) {
|
|
playStream(*(menuSounds[submenu - 1]->getAudioStream()), soundChannel);
|
|
setLastPlayedSound(soundChannel, soundId);
|
|
} else {
|
|
warning("DirectorSound::playExternalSound: Could not find sound %d %d", menu, submenu);
|
|
}
|
|
}
|
|
|
|
void DirectorSound::changingMovie() {
|
|
for (uint i = 1; i < _channels.size(); i++) {
|
|
_channels[i - 1].movieChanged = true;
|
|
if (isChannelPuppet(i)) {
|
|
setPuppetSound(SoundID(), i); // disable puppet sound
|
|
} else if (isChannelActive(i)) {
|
|
// Don't stop this sound until there's a new, non-zero sound in this channel.
|
|
_channels[i - 1].stopOnZero = false;
|
|
|
|
// If this is a looping sound, make it loop automatically until that happens.
|
|
const SoundID &lastPlayedSound = _channels[i - 1].lastPlayedSound;
|
|
if (lastPlayedSound.type == kSoundCast) {
|
|
CastMemberID memberID(lastPlayedSound.u.cast.member, lastPlayedSound.u.cast.castLib);
|
|
CastMember *soundCast = _window->getCurrentMovie()->getCastMember(memberID);
|
|
if (soundCast && soundCast->_type == kCastSound && static_cast<SoundCastMember *>(soundCast)->_looping) {
|
|
_mixer->loopChannel(_channels[i - 1].handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
unloadSampleSounds(); // TODO: we can possibly keep this between movies
|
|
}
|
|
|
|
void DirectorSound::setLastPlayedSound(uint8 soundChannel, SoundID soundId, bool stopOnZero) {
|
|
_channels[soundChannel - 1].lastPlayedSound = soundId;
|
|
_channels[soundChannel - 1].stopOnZero = stopOnZero;
|
|
_channels[soundChannel - 1].movieChanged = false;
|
|
}
|
|
|
|
bool DirectorSound::isLastPlayedSound(uint8 soundChannel, const SoundID &soundId) {
|
|
return !_channels[soundChannel - 1].movieChanged && _channels[soundChannel - 1].lastPlayedSound == soundId;
|
|
}
|
|
|
|
bool DirectorSound::shouldStopOnZero(uint8 soundChannel) {
|
|
return _channels[soundChannel - 1].stopOnZero;
|
|
}
|
|
|
|
void DirectorSound::stopSound(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
cancelFade(soundChannel);
|
|
_mixer->stopHandle(_channels[soundChannel - 1].handle);
|
|
setLastPlayedSound(soundChannel, SoundID());
|
|
return;
|
|
}
|
|
|
|
void DirectorSound::stopSound() {
|
|
for (uint i = 0; i < _channels.size(); i++) {
|
|
cancelFade(i + 1);
|
|
|
|
_mixer->stopHandle(_channels[i].handle);
|
|
setLastPlayedSound(i + 1, SoundID());
|
|
}
|
|
|
|
_mixer->stopHandle(_scriptSound);
|
|
_mixer->stopHandle(_pcSpeakerHandle);
|
|
}
|
|
|
|
void DirectorSound::systemBeep() {
|
|
_speaker->play(Audio::PCSpeaker::kWaveFormSquare, 500, 150);
|
|
}
|
|
|
|
bool DirectorSound::isChannelPuppet(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return false;
|
|
|
|
// cast member ID 0 means "not a puppet"
|
|
if (_channels[soundChannel - 1].puppet.type == kSoundCast && _channels[soundChannel - 1].puppet.u.cast.member == 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void DirectorSound::setPuppetSound(SoundID soundId, uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
_channels[soundChannel - 1].newPuppet = true;
|
|
_channels[soundChannel - 1].puppet = soundId;
|
|
}
|
|
|
|
void DirectorSound::playPuppetSound(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return;
|
|
|
|
// only play if the puppet was just set
|
|
if (!_channels[soundChannel - 1].newPuppet)
|
|
return;
|
|
|
|
_channels[soundChannel - 1].newPuppet = false;
|
|
playSound(_channels[soundChannel - 1].puppet, soundChannel, true);
|
|
}
|
|
|
|
void DirectorSound::playFPlaySound() {
|
|
if (_fplayQueue.empty())
|
|
return;
|
|
// only when the previous sound is finished, shall we play next one
|
|
if (isChannelActive(1))
|
|
return;
|
|
|
|
Common::String sndName = _fplayQueue.pop();
|
|
if (sndName.equalsIgnoreCase("stop")) {
|
|
stopSound(1);
|
|
_currentSoundName = "";
|
|
|
|
if (_fplayQueue.empty())
|
|
return;
|
|
else
|
|
sndName = _fplayQueue.pop();
|
|
}
|
|
|
|
uint32 tag = MKTAG('s', 'n', 'd', ' ');
|
|
uint id = 0xFFFF;
|
|
Archive *archive = nullptr;
|
|
|
|
// iterate opened ResFiles
|
|
for (Common::HashMap<Common::String, Archive *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo>::iterator it = g_director->_openResFiles.begin(); it != g_director->_openResFiles.end(); ++it) {
|
|
id = it->_value->findResourceID(tag, sndName, true);
|
|
if (id != 0xFFFF) {
|
|
archive = it->_value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (id == 0xFFFF) {
|
|
warning("DirectorSound:playFPlaySound: can not find sound %s", sndName.c_str());
|
|
return;
|
|
}
|
|
|
|
Common::SeekableReadStreamEndian *sndData = archive->getResource(tag, id);
|
|
if (sndData != nullptr) {
|
|
SNDDecoder *ad = new SNDDecoder();
|
|
ad->loadStream(*sndData);
|
|
delete sndData;
|
|
|
|
Audio::AudioStream *as;
|
|
bool looping = false;
|
|
|
|
if (!_fplayQueue.empty() && _fplayQueue.front().equalsIgnoreCase("continuous")) {
|
|
_fplayQueue.pop();
|
|
looping = true;
|
|
}
|
|
|
|
// FPlay is controlled by Lingo, not the score, like a puppet,
|
|
// so we'll get the puppet version of the stream.
|
|
as = ad->getAudioStream(looping, true);
|
|
|
|
if (!as) {
|
|
warning("DirectorSound:playFPlaySound: failed to get audio stream");
|
|
return;
|
|
}
|
|
|
|
// update current playing sound
|
|
_currentSoundName = sndName;
|
|
|
|
playStream(*as, 1);
|
|
delete ad;
|
|
}
|
|
|
|
// Set the last played sound so that cast member 0 in the sound channel doesn't stop this file.
|
|
setLastPlayedSound(1, SoundID(), false);
|
|
}
|
|
|
|
void DirectorSound::playFPlaySound(const Common::Array<Common::String> &fplayList) {
|
|
for (uint i = 0; i < fplayList.size(); i++)
|
|
_fplayQueue.push(fplayList[i]);
|
|
|
|
// stop the previous sound, because new one is comming
|
|
if (isChannelActive(1))
|
|
stopSound(1);
|
|
|
|
playFPlaySound();
|
|
}
|
|
|
|
void DirectorSound::setSoundLevelInternal(uint8 soundChannel, uint8 soundLevel) {
|
|
// we have 8 level of sounds, and in ScummVM, we have range 0 to 255, thus 1 level represent 32
|
|
_channels[soundChannel - 1].volume = soundLevel * 32;
|
|
if (_enable && isChannelActive(soundChannel))
|
|
_mixer->setChannelVolume(_channels[soundChannel - 1].handle, _channels[soundChannel - 1].volume);
|
|
}
|
|
|
|
// -1 represent all the sound channel
|
|
void DirectorSound::setSouldLevel(int channel, uint8 soundLevel) {
|
|
if (soundLevel >= 8) {
|
|
warning("DirectorSound::setSoundLevel: soundLevel %d out of bounds", soundLevel);
|
|
return;
|
|
}
|
|
|
|
if (channel != -1) {
|
|
if (!isChannelValid(channel))
|
|
return;
|
|
setSoundLevelInternal(channel, soundLevel);
|
|
} else {
|
|
for (uint i = 0; i < _channels.size(); i++)
|
|
setSoundLevelInternal(i + 1, soundLevel);
|
|
}
|
|
}
|
|
|
|
uint8 DirectorSound::getSoundLevel(uint8 soundChannel) {
|
|
if (!isChannelValid(soundChannel))
|
|
return 0;
|
|
return _channels[soundChannel - 1].volume / 32;
|
|
}
|
|
|
|
SNDDecoder::SNDDecoder()
|
|
: AudioDecoder() {
|
|
_data = nullptr;
|
|
_channels = 0;
|
|
_size = 0;
|
|
_rate = 0;
|
|
_flags = 0;
|
|
_loopStart = _loopEnd = 0;
|
|
}
|
|
|
|
SNDDecoder::~SNDDecoder() {
|
|
if (_data) {
|
|
free(_data);
|
|
}
|
|
}
|
|
|
|
bool SNDDecoder::loadStream(Common::SeekableReadStreamEndian &stream) {
|
|
if (_data) {
|
|
free(_data);
|
|
_data = nullptr;
|
|
}
|
|
|
|
if (debugChannelSet(5, kDebugLoading)) {
|
|
debugC(5, kDebugLoading, "snd header:");
|
|
stream.hexdump(0x4e);
|
|
}
|
|
|
|
uint16 format = stream.readUint16();
|
|
if (format == 1) {
|
|
uint16 dataTypeCount = stream.readUint16();
|
|
for (uint16 i = 0; i < dataTypeCount; i++) {
|
|
uint16 dataType = stream.readUint16();
|
|
if (dataType == 5) {
|
|
// Sampled sound data
|
|
uint32 options = stream.readUint32();
|
|
_channels = (options & 0x80) ? 1 : 2;
|
|
if (!processCommands(stream))
|
|
return false;
|
|
} else {
|
|
warning("SNDDecoder: Unsupported data type: %d", dataType);
|
|
return false;
|
|
}
|
|
}
|
|
} else if (format == 2) {
|
|
_channels = 1;
|
|
/*uint16 refCount =*/stream.readUint16();
|
|
if (!processCommands(stream))
|
|
return false;
|
|
} else {
|
|
warning("SNDDecoder: Bad format: %d", format);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SNDDecoder::processCommands(Common::SeekableReadStreamEndian &stream) {
|
|
uint16 cmdCount = stream.readUint16();
|
|
for (uint16 i = 0; i < cmdCount; i++) {
|
|
uint16 cmd = stream.readUint16();
|
|
if (cmd == 0x8050 || cmd == 0x8051) {
|
|
if (!processBufferCommand(stream))
|
|
return false;
|
|
} else {
|
|
warning("SNDDecoder: Unsupported command: %d", cmd);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SNDDecoder::processBufferCommand(Common::SeekableReadStreamEndian &stream) {
|
|
if (_data) {
|
|
warning("SNDDecoder: Already read data");
|
|
return false;
|
|
}
|
|
|
|
/*uint16 unk1 =*/stream.readUint16();
|
|
int32 offset = stream.readUint32();
|
|
if (offset != stream.pos()) {
|
|
warning("SNDDecoder: Bad sound header offset. Expected: %d, read: %d", (int)stream.pos(), offset);
|
|
return false;
|
|
}
|
|
/*uint32 dataPtr =*/stream.readUint32();
|
|
uint32 param = stream.readUint32();
|
|
_rate = stream.readUint16();
|
|
/*uint16 rateExt =*/stream.readUint16();
|
|
_loopStart = stream.readUint32();
|
|
_loopEnd = stream.readUint32();
|
|
byte encoding = stream.readByte();
|
|
byte baseFrequency = stream.readByte();
|
|
if (baseFrequency != 0x3c) {
|
|
warning("SNDDecoder: Unsupported base frequency: %d", baseFrequency);
|
|
return false;
|
|
}
|
|
uint32 frameCount = 0;
|
|
uint16 bits = 8;
|
|
if (encoding == 0x00) {
|
|
// Standard sound header
|
|
frameCount = param / _channels;
|
|
} else if (encoding == 0xff) {
|
|
// Extended sound header
|
|
_channels = param;
|
|
frameCount = stream.readUint32();
|
|
for (uint32 i = 0; i < 0x0a; i++) {
|
|
// aiff sample rate
|
|
stream.readByte();
|
|
}
|
|
/*uint32 markerChunk =*/stream.readUint32();
|
|
/*uint32 instrumentsChunk =*/stream.readUint32();
|
|
/*uint32 aesRecording =*/stream.readUint32();
|
|
bits = stream.readUint16();
|
|
|
|
// future use
|
|
stream.readUint16();
|
|
stream.readUint32();
|
|
stream.readUint32();
|
|
stream.readUint32();
|
|
} else if (encoding == 0xfe) {
|
|
// Compressed sound header
|
|
warning("SNDDecoder: Compressed sound header not supported");
|
|
return false;
|
|
} else {
|
|
warning("SNDDecoder: Bad encoding: %d", encoding);
|
|
return false;
|
|
}
|
|
|
|
_flags = 0;
|
|
_flags |= (_channels == 2) ? Audio::FLAG_STEREO : 0;
|
|
_flags |= (bits == 16) ? Audio::FLAG_16BITS : 0;
|
|
_flags |= (bits == 8) ? Audio::FLAG_UNSIGNED : 0;
|
|
_size = frameCount * _channels * (bits == 16 ? 2 : 1);
|
|
|
|
_data = (byte *)malloc(_size);
|
|
assert(_data);
|
|
stream.read(_data, _size);
|
|
|
|
return true;
|
|
}
|
|
|
|
Audio::AudioStream *SNDDecoder::getAudioStream(bool looping, bool forPuppet, DisposeAfterUse::Flag disposeAfterUse) {
|
|
if (!_data)
|
|
return nullptr;
|
|
byte *buffer = (byte *)malloc(_size);
|
|
memcpy(buffer, _data, _size);
|
|
|
|
Audio::SeekableAudioStream *stream = Audio::makeRawStream(buffer, _size, _rate, _flags, disposeAfterUse);
|
|
|
|
if (looping) {
|
|
if (hasLoopBounds()) {
|
|
// If this is for a puppet, return an automatically looping stream.
|
|
// Otherwise, the sound will be looped by the score.
|
|
if (forPuppet)
|
|
return new Audio::SubLoopingAudioStream(stream, 0, Audio::Timestamp(0, _loopStart, _rate), Audio::Timestamp(0, _loopEnd, _rate));
|
|
else
|
|
return new Audio::SubSeekableAudioStream(stream, Audio::Timestamp(0, _loopStart, _rate), Audio::Timestamp(0, _loopEnd, _rate));
|
|
} else {
|
|
// Not sure if looping sounds can appear without loop bounds.
|
|
// Let's just log a warning and loop the entire sound...
|
|
warning("SNDDecoder::getAudioStream: Looping sound has no loop bounds");
|
|
if (forPuppet)
|
|
return new Audio::LoopingAudioStream(stream, 0);
|
|
else
|
|
return stream;
|
|
}
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
bool SNDDecoder::hasLoopBounds() {
|
|
return _loopStart != 0 || _loopEnd != 0;
|
|
}
|
|
|
|
AudioFileDecoder::AudioFileDecoder(Common::String &path)
|
|
: AudioDecoder() {
|
|
_path = path;
|
|
_macresman = nullptr;
|
|
}
|
|
|
|
AudioFileDecoder::~AudioFileDecoder() {
|
|
delete _macresman;
|
|
}
|
|
|
|
Audio::AudioStream *AudioFileDecoder::getAudioStream(bool looping, bool forPuppet, DisposeAfterUse::Flag disposeAfterUse) {
|
|
if (_path.empty())
|
|
return nullptr;
|
|
|
|
_macresman = new Common::MacResManager();
|
|
_macresman->open(Common::Path(pathMakeRelative(_path), g_director->_dirSeparator));
|
|
Common::SeekableReadStream *file = _macresman->getDataFork();
|
|
|
|
if (file == nullptr) {
|
|
warning("Failed to open %s", _path.c_str());
|
|
delete file;
|
|
return nullptr;
|
|
}
|
|
uint32 magic1 = file->readUint32BE();
|
|
file->readUint32BE();
|
|
uint32 magic2 = file->readUint32BE();
|
|
file->seek(0);
|
|
|
|
Audio::RewindableAudioStream *stream = nullptr;
|
|
if (magic1 == MKTAG('R', 'I', 'F', 'F') &&
|
|
magic2 == MKTAG('W', 'A', 'V', 'E')) {
|
|
stream = Audio::makeWAVStream(file, disposeAfterUse);
|
|
} else if (magic1 == MKTAG('F', 'O', 'R', 'M') &&
|
|
(magic2 == MKTAG('A', 'I', 'F', 'F') || magic2 == MKTAG('A', 'I', 'F', 'C'))) {
|
|
stream = Audio::makeAIFFStream(file, disposeAfterUse);
|
|
} else {
|
|
warning("Unknown file type for %s", _path.c_str());
|
|
delete file;
|
|
}
|
|
|
|
if (stream) {
|
|
if (looping && forPuppet) {
|
|
// If this is for a puppet, return an automatically looping stream.
|
|
// Otherwise, the sound will be looped by the score
|
|
return new Audio::LoopingAudioStream(stream, 0);
|
|
}
|
|
return stream;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
} // End of namespace Director
|