[Scummvm-cvs-logs] scummvm master -> 4495ae3de00bd808399a5e38cdd58fb5d40fef5d

csnover csnover at users.noreply.github.com
Tue Jun 21 15:23:13 CEST 2016


This automated email contains information about 16 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
cbc3b773aa AUDIO: Make WAV streams seekable
5d3385750d SCI: Split audio sync to its own class
46551fd4b5 SCI32: Rewrite digital audio engine
f02107f682 SCI: Minor cleanup of kDoSound
f939868f3a SCI32: Add kDoSound(play) workaround for LSL6hires
dcc6234d81 SCI32: Add low-pass filter to 8-bit SOL audio
97c11e7d5f SCI32: Fix getTextWidth on consecutive control codes
c4b41f5d7c SCI32: Fix drawText on consecutive control codes
94328f0ec8 SCI32: Make GfxText32::_scaledWidth/Height statics
a613a27b44 SCI32: Implement line drawing (kAddLine/kUpdateLine/kRemoveLine)
ab864ba366 SCI32: Implement kScrollWindow
ceee33ba2c SCI32: Add workaround for kScrollWindowAdd call in Phantasmagoria
52505dc57f SCI32: Implement basic kMessageBox
820e6b8bd3 SCI32: Fixes to GfxText32
a3055d3f49 SCI: Add an explanation about LRU removals when fetching resources
4495ae3de0 SCI32: Remove unused dependencies from GfxFrameout


Commit: cbc3b773aa56ec89060ca542e57597ca5a619f40
    https://github.com/scummvm/scummvm/commit/cbc3b773aa56ec89060ca542e57597ca5a619f40
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-19T14:48:33-05:00

Commit Message:
AUDIO: Make WAV streams seekable

This allows raw PCM in WAVE containers to have duration and be
seekable, and opens the door for ADPCM streams to be seekable later
if necessary.

This change is needed to avoid duplication of RIFF/WAVE container
parsing for SCI engine, which uses raw PCM WAVE files and needs to
be able to determine their lengths.

Changed paths:
    audio/decoders/adpcm.cpp
    audio/decoders/adpcm.h
    audio/decoders/adpcm_intern.h
    audio/decoders/wave.cpp
    audio/decoders/wave.h



diff --git a/audio/decoders/adpcm.cpp b/audio/decoders/adpcm.cpp
index fe5eec5..fd97d5d 100644
--- a/audio/decoders/adpcm.cpp
+++ b/audio/decoders/adpcm.cpp
@@ -433,7 +433,7 @@ int16 Ima_ADPCMStream::decodeIMA(byte code, int channel) {
 	return samp;
 }
 
-RewindableAudioStream *makeADPCMStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, uint32 size, ADPCMType type, int rate, int channels, uint32 blockAlign) {
+SeekableAudioStream *makeADPCMStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, uint32 size, ADPCMType type, int rate, int channels, uint32 blockAlign) {
 	// If size is 0, report the entire size of the stream
 	if (!size)
 		size = stream->size();
diff --git a/audio/decoders/adpcm.h b/audio/decoders/adpcm.h
index 650bc34..353fd3b 100644
--- a/audio/decoders/adpcm.h
+++ b/audio/decoders/adpcm.h
@@ -45,7 +45,7 @@ class SeekableReadStream;
 namespace Audio {
 
 class PacketizedAudioStream;
-class RewindableAudioStream;
+class SeekableAudioStream;
 
 // There are several types of ADPCM encoding, only some are supported here
 // For all the different encodings, refer to:
@@ -74,7 +74,7 @@ enum ADPCMType {
  * @param blockAlign        block alignment ???
  * @return   a new RewindableAudioStream, or NULL, if an error occurred
  */
-RewindableAudioStream *makeADPCMStream(
+SeekableAudioStream *makeADPCMStream(
     Common::SeekableReadStream *stream,
     DisposeAfterUse::Flag disposeAfterUse,
     uint32 size, ADPCMType type,
diff --git a/audio/decoders/adpcm_intern.h b/audio/decoders/adpcm_intern.h
index 92be704..3a2af91 100644
--- a/audio/decoders/adpcm_intern.h
+++ b/audio/decoders/adpcm_intern.h
@@ -39,7 +39,7 @@
 
 namespace Audio {
 
-class ADPCMStream : public RewindableAudioStream {
+class ADPCMStream : public SeekableAudioStream {
 protected:
 	Common::DisposablePtr<Common::SeekableReadStream> _stream;
 	int32 _startpos;
@@ -67,6 +67,8 @@ public:
 	virtual int getRate() const { return _rate; }
 
 	virtual bool rewind();
+	virtual bool seek(const Timestamp &where) { return false; }
+	virtual Timestamp getLength() const { return -1; }
 
 	/**
 	 * This table is used by some ADPCM variants (IMA and OKI) to adjust the
diff --git a/audio/decoders/wave.cpp b/audio/decoders/wave.cpp
index adee749..cdd6412 100644
--- a/audio/decoders/wave.cpp
+++ b/audio/decoders/wave.cpp
@@ -158,7 +158,7 @@ bool loadWAVFromStream(Common::SeekableReadStream &stream, int &size, int &rate,
 	return true;
 }
 
-RewindableAudioStream *makeWAVStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) {
+SeekableAudioStream *makeWAVStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) {
 	int size, rate;
 	byte flags;
 	uint16 type;
diff --git a/audio/decoders/wave.h b/audio/decoders/wave.h
index 6bc9f72..f5b7fb4 100644
--- a/audio/decoders/wave.h
+++ b/audio/decoders/wave.h
@@ -84,7 +84,7 @@ extern bool loadWAVFromStream(
  * @param disposeAfterUse	whether to delete the stream after use
  * @return	a new RewindableAudioStream, or NULL, if an error occurred
  */
-RewindableAudioStream *makeWAVStream(
+SeekableAudioStream *makeWAVStream(
 	Common::SeekableReadStream *stream,
 	DisposeAfterUse::Flag disposeAfterUse);
 


Commit: 5d3385750ddf68f5347bf51f005c86a8e70283e2
    https://github.com/scummvm/scummvm/commit/5d3385750ddf68f5347bf51f005c86a8e70283e2
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-19T14:48:33-05:00

Commit Message:
SCI: Split audio sync to its own class

SCI32 has its own audio handling code, but audio sync code is the
same as SCI16.

Changed paths:
  A engines/sci/sound/sync.cpp
  A engines/sci/sound/sync.h
    engines/sci/engine/ksound.cpp
    engines/sci/module.mk
    engines/sci/sci.cpp
    engines/sci/sci.h
    engines/sci/sound/audio.cpp
    engines/sci/sound/audio.h



diff --git a/engines/sci/engine/ksound.cpp b/engines/sci/engine/ksound.cpp
index 398a623..2b22d68 100644
--- a/engines/sci/engine/ksound.cpp
+++ b/engines/sci/engine/ksound.cpp
@@ -27,6 +27,7 @@
 #include "sci/engine/vm.h"		// for Object
 #include "sci/sound/audio.h"
 #include "sci/sound/soundcmd.h"
+#include "sci/sound/sync.h"
 
 #include "audio/mixer.h"
 #include "common/system.h"
@@ -286,14 +287,12 @@ reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) {
 }
 
 reg_t kDoSync(EngineState *s, int argc, reg_t *argv) {
-	SegManager *segMan = s->_segMan;
 	switch (argv[0].toUint16()) {
 	case kSciAudioSyncStart: {
 		ResourceId id;
 
-		g_sci->_audio->stopSoundSync();
+		g_sci->_sync->stop();
 
-		// Load sound sync resource and lock it
 		if (argc == 3) {
 			id = ResourceId(kResourceTypeSync, argv[2].toUint16());
 		} else if (argc == 7) {
@@ -304,14 +303,14 @@ reg_t kDoSync(EngineState *s, int argc, reg_t *argv) {
 			return s->r_acc;
 		}
 
-		g_sci->_audio->setSoundSync(id, argv[1], segMan);
+		g_sci->_sync->start(id, argv[1]);
 		break;
 	}
 	case kSciAudioSyncNext:
-		g_sci->_audio->doSoundSync(argv[1], segMan);
+		g_sci->_sync->next(argv[1]);
 		break;
 	case kSciAudioSyncStop:
-		g_sci->_audio->stopSoundSync();
+		g_sci->_sync->stop();
 		break;
 	default:
 		error("DoSync: Unhandled subfunction %d", argv[0].toUint16());
diff --git a/engines/sci/module.mk b/engines/sci/module.mk
index a02147e..c35d0b5 100644
--- a/engines/sci/module.mk
+++ b/engines/sci/module.mk
@@ -69,6 +69,7 @@ MODULE_OBJS := \
 	sound/midiparser_sci.o \
 	sound/music.o \
 	sound/soundcmd.o \
+	sound/sync.o \
 	sound/drivers/adlib.o \
 	sound/drivers/amigamac.o \
 	sound/drivers/cms.o \
diff --git a/engines/sci/sci.cpp b/engines/sci/sci.cpp
index e14d12b..8c23d32 100644
--- a/engines/sci/sci.cpp
+++ b/engines/sci/sci.cpp
@@ -43,6 +43,7 @@
 
 #include "sci/sound/audio.h"
 #include "sci/sound/music.h"
+#include "sci/sound/sync.h"
 #include "sci/sound/soundcmd.h"
 #include "sci/graphics/animate.h"
 #include "sci/graphics/cache.h"
@@ -86,6 +87,7 @@ SciEngine::SciEngine(OSystem *syst, const ADGameDescription *desc, SciGameId gam
 	_gfxMacIconBar = 0;
 
 	_audio = 0;
+	_sync = nullptr;
 	_features = 0;
 	_resMan = 0;
 	_gamestate = 0;
@@ -182,6 +184,7 @@ SciEngine::~SciEngine() {
 	delete _gfxScreen;
 
 	delete _audio;
+	delete _sync;
 	delete _soundCmd;
 	delete _kernel;
 	delete _vocabulary;
@@ -267,6 +270,7 @@ Common::Error SciEngine::run() {
 	if (getGameId() == GID_CHRISTMAS1990)
 		_vocabulary = new Vocabulary(_resMan, false);
 	_audio = new AudioPlayer(_resMan);
+	_sync = new Sync(_resMan, segMan);
 	_gamestate = new EngineState(segMan);
 	_eventMan = new EventManager(_resMan->detectFontExtended());
 
@@ -802,6 +806,7 @@ void SciEngine::exitGame() {
 	if (_gamestate->abortScriptProcessing != kAbortLoadGame) {
 		_gamestate->_executionStack.clear();
 		_audio->stopAllAudio();
+		_sync->stop();
 		_soundCmd->clearPlayList();
 	}
 
diff --git a/engines/sci/sci.h b/engines/sci/sci.h
index c49a516..ff41472 100644
--- a/engines/sci/sci.h
+++ b/engines/sci/sci.h
@@ -56,6 +56,7 @@ class SoundCommandParser;
 class EventManager;
 class SegManager;
 class ScriptPatcher;
+class Sync;
 
 class GfxAnimate;
 class GfxCache;
@@ -372,6 +373,7 @@ public:
 #endif
 
 	AudioPlayer *_audio;
+	Sync *_sync;
 	SoundCommandParser *_soundCmd;
 	GameFeatures *_features;
 
diff --git a/engines/sci/sound/audio.cpp b/engines/sci/sound/audio.cpp
index a74bfa2..71b0810 100644
--- a/engines/sci/sound/audio.cpp
+++ b/engines/sci/sound/audio.cpp
@@ -22,7 +22,6 @@
 
 #include "sci/resource.h"
 #include "sci/engine/kernel.h"
-#include "sci/engine/selector.h"
 #include "sci/engine/seg_manager.h"
 #include "sci/sound/audio.h"
 
@@ -45,7 +44,7 @@
 namespace Sci {
 
 AudioPlayer::AudioPlayer(ResourceManager *resMan) : _resMan(resMan), _audioRate(11025),
-		_syncResource(NULL), _syncOffset(0), _audioCdStart(0), _initCD(false) {
+		_audioCdStart(0), _initCD(false) {
 
 	_mixer = g_system->getMixer();
 	_wPlayFlag = false;
@@ -56,7 +55,6 @@ AudioPlayer::~AudioPlayer() {
 }
 
 void AudioPlayer::stopAllAudio() {
-	stopSoundSync();
 	stopAudio();
 	if (_audioCdStart > 0)
 		audioCdStop();
@@ -474,43 +472,6 @@ Audio::RewindableAudioStream *AudioPlayer::getAudioStream(uint32 number, uint32
 	return NULL;
 }
 
-void AudioPlayer::setSoundSync(ResourceId id, reg_t syncObjAddr, SegManager *segMan) {
-	_syncResource = _resMan->findResource(id, 1);
-	_syncOffset = 0;
-
-	if (_syncResource) {
-		writeSelectorValue(segMan, syncObjAddr, SELECTOR(syncCue), 0);
-	} else {
-		warning("setSoundSync: failed to find resource %s", id.toString().c_str());
-		// Notify the scripts to stop sound sync
-		writeSelectorValue(segMan, syncObjAddr, SELECTOR(syncCue), SIGNAL_OFFSET);
-	}
-}
-
-void AudioPlayer::doSoundSync(reg_t syncObjAddr, SegManager *segMan) {
-	if (_syncResource && (_syncOffset < _syncResource->size - 1)) {
-		int16 syncCue = -1;
-		int16 syncTime = (int16)READ_SCI11ENDIAN_UINT16(_syncResource->data + _syncOffset);
-
-		_syncOffset += 2;
-
-		if ((syncTime != -1) && (_syncOffset < _syncResource->size - 1)) {
-			syncCue = (int16)READ_SCI11ENDIAN_UINT16(_syncResource->data + _syncOffset);
-			_syncOffset += 2;
-		}
-
-		writeSelectorValue(segMan, syncObjAddr, SELECTOR(syncTime), syncTime);
-		writeSelectorValue(segMan, syncObjAddr, SELECTOR(syncCue), syncCue);
-	}
-}
-
-void AudioPlayer::stopSoundSync() {
-	if (_syncResource) {
-		_resMan->unlockResource(_syncResource);
-		_syncResource = NULL;
-	}
-}
-
 int AudioPlayer::audioCdPlay(int track, int start, int duration) {
 	if (!_initCD) {
 		// Initialize CD mode if we haven't already
diff --git a/engines/sci/sound/audio.h b/engines/sci/sound/audio.h
index 4a8b265..3d25dca 100644
--- a/engines/sci/sound/audio.h
+++ b/engines/sci/sound/audio.h
@@ -46,12 +46,6 @@ enum AudioCommands {
 	kSciAudioCD = 10 /* Plays SCI1.1 CD audio */
 };
 
-enum AudioSyncCommands {
-	kSciAudioSyncStart = 0,
-	kSciAudioSyncNext = 1,
-	kSciAudioSyncStop = 2
-};
-
 #define AUDIO_VOLUME_MAX 127
 
 class Resource;
@@ -77,10 +71,6 @@ public:
 
 	void handleFanmadeSciAudio(reg_t sciAudioObject, SegManager *segMan);
 
-	void setSoundSync(ResourceId id, reg_t syncObjAddr, SegManager *segMan);
-	void doSoundSync(reg_t syncObjAddr, SegManager *segMan);
-	void stopSoundSync();
-
 	int audioCdPlay(int track, int start, int duration);
 	void audioCdStop();
 	void audioCdUpdate();
@@ -93,8 +83,6 @@ private:
 	uint16 _audioRate;
 	Audio::SoundHandle _audioHandle;
 	Audio::Mixer *_mixer;
-	Resource *_syncResource; /**< Used by kDoSync for speech syncing in CD talkie games */
-	uint _syncOffset;
 	uint32 _audioCdStart;
 	bool _wPlayFlag;
 	bool _initCD;
diff --git a/engines/sci/sound/sync.cpp b/engines/sci/sound/sync.cpp
new file mode 100644
index 0000000..08abad1
--- /dev/null
+++ b/engines/sci/sound/sync.cpp
@@ -0,0 +1,75 @@
+/* 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.
+ *
+ */
+
+#include "sci/engine/kernel.h"
+#include "sci/util.h"
+#include "sync.h"
+
+namespace Sci {
+Sync::Sync(ResourceManager *resMan, SegManager *segMan) :
+	_resMan(resMan),
+	_segMan(segMan),
+	_resource(nullptr),
+	_offset(0) {}
+
+Sync::~Sync() {
+	stop();
+}
+
+void Sync::start(const ResourceId id, const reg_t syncObjAddr) {
+	_resource = _resMan->findResource(id, true);
+	_offset = 0;
+
+	if (_resource) {
+		writeSelectorValue(_segMan, syncObjAddr, SELECTOR(syncCue), 0);
+	} else {
+		warning("Sync::start: failed to find resource %s", id.toString().c_str());
+		// Notify the scripts to stop sound sync
+		writeSelectorValue(_segMan, syncObjAddr, SELECTOR(syncCue), SIGNAL_OFFSET);
+	}
+}
+
+void Sync::next(const reg_t syncObjAddr) {
+	if (_resource && (_offset < _resource->size - 1)) {
+		int16 syncCue = -1;
+		int16 syncTime = (int16)READ_SCI11ENDIAN_UINT16(_resource->data + _offset);
+
+		_offset += 2;
+
+		if ((syncTime != -1) && (_offset < _resource->size - 1)) {
+			syncCue = (int16)READ_SCI11ENDIAN_UINT16(_resource->data + _offset);
+			_offset += 2;
+		}
+
+		writeSelectorValue(_segMan, syncObjAddr, SELECTOR(syncTime), syncTime);
+		writeSelectorValue(_segMan, syncObjAddr, SELECTOR(syncCue), syncCue);
+	}
+}
+
+void Sync::stop() {
+	if (_resource) {
+		_resMan->unlockResource(_resource);
+		_resource = nullptr;
+	}
+}
+
+} // End of namespace Sci
diff --git a/engines/sci/sound/sync.h b/engines/sci/sound/sync.h
new file mode 100644
index 0000000..c80982b
--- /dev/null
+++ b/engines/sci/sound/sync.h
@@ -0,0 +1,57 @@
+/* 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.
+ *
+ */
+
+#ifndef SCI_SOUND_SYNC_H
+#define SCI_SOUND_SYNC_H
+
+#include "sci/engine/selector.h"
+#include "sci/engine/vm_types.h"
+
+namespace Sci {
+
+enum AudioSyncCommands {
+	kSciAudioSyncStart = 0,
+	kSciAudioSyncNext = 1,
+	kSciAudioSyncStop = 2
+};
+
+class Resource;
+class ResourceManager;
+class SegManager;
+
+class Sync {
+	SegManager *_segMan;
+	ResourceManager *_resMan;
+	Resource *_resource;
+	uint _offset;
+
+public:
+	Sync(ResourceManager *resMan, SegManager *segMan);
+	~Sync();
+
+	void start(const ResourceId id, const reg_t syncObjAddr);
+	void next(const reg_t syncObjAddr);
+	void stop();
+};
+
+}
+#endif


Commit: 46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5
    https://github.com/scummvm/scummvm/commit/46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-20T21:02:21-05:00

Commit Message:
SCI32: Rewrite digital audio engine

This provides a complete implementation of kDoAudio through
SCI2.1mid, plus partial implementation of SCI3 features.

Digital audio calls shunted through kDoSound have also been
updated to go through the SCI32 audio mixer, though these shunts
are a bit hacky because the ScummVM implementation of kDoSound
does not currently match how SSCI kDoSound is designed.

It is probably possible in the future to just replace the SCI1.1
audio code (audio.cpp) with the new SCI32 code, since the major
differences seem to be that (1) SCI1.1 only supported one digital
audio playback channel (this is configurable already), (2) it
had extra commands for CD audio playback and queued sample
playback.

Changed paths:
  A engines/sci/sound/audio32.cpp
  A engines/sci/sound/audio32.h
  A engines/sci/sound/decoders/sol.cpp
  A engines/sci/sound/decoders/sol.h
    engines/sci/engine/kernel.h
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/ksound.cpp
    engines/sci/module.mk
    engines/sci/resource.cpp
    engines/sci/resource.h
    engines/sci/resource_audio.cpp
    engines/sci/sci.cpp
    engines/sci/sci.h
    engines/sci/sound/audio.cpp
    engines/sci/sound/music.cpp
    engines/sci/sound/music.h
    engines/sci/sound/soundcmd.cpp
    engines/sci/sound/sync.cpp
    engines/sci/sound/sync.h



diff --git a/engines/sci/engine/kernel.h b/engines/sci/engine/kernel.h
index 62566a7..d88dc75 100644
--- a/engines/sci/engine/kernel.h
+++ b/engines/sci/engine/kernel.h
@@ -420,6 +420,26 @@ reg_t kStubNull(EngineState *s, int argc, reg_t *argv);
 
 #ifdef ENABLE_SCI32
 // SCI2 Kernel Functions
+reg_t kDoAudio32(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioInit(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioWaitForPlay(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioPlay(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioStop(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioPause(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioResume(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioPosition(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioRate(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioVolume(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioGetCapability(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioBitDepth(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioDistort(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioMixing(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioChannels(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioPreload(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioFade(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioHasSignal(EngineState *s, int argc, reg_t *argv);
+reg_t kDoAudioSetLoop(EngineState *s, int argc, reg_t *argv);
+
 reg_t kIsHiRes(EngineState *s, int argc, reg_t *argv);
 reg_t kArray(EngineState *s, int argc, reg_t *argv);
 reg_t kListAt(EngineState *s, int argc, reg_t *argv);
diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index 0ede307..a61213e 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -168,7 +168,7 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	//        signature for SCI21 should be "o"
 	{ SIG_SOUNDSCI21,      9, MAP_CALL(DoSoundStop),               NULL,                   NULL },
 	{ SIG_SOUNDSCI21,     10, MAP_CALL(DoSoundPause),              NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     11, MAP_CALL(DoSoundFade),               NULL,                   kDoSoundFade_workarounds },
+	{ SIG_SOUNDSCI21,     11, MAP_CALL(DoSoundFade),               "oiiii",                kDoSoundFade_workarounds },
 	{ SIG_SOUNDSCI21,     12, MAP_CALL(DoSoundSetHold),            NULL,                   NULL },
 	{ SIG_SOUNDSCI21,     13, MAP_CALL(DoSoundDummy),              NULL,                   NULL },
 	{ SIG_SOUNDSCI21,     14, MAP_CALL(DoSoundSetVolume),          NULL,                   NULL },
@@ -182,6 +182,67 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	SCI_SUBOPENTRY_TERMINATOR
 };
 
+#ifdef ENABLE_SCI32
+// NOTE: In SSCI, some 'unused' kDoAudio subops are actually
+// called indirectly by kDoSound:
+//
+// kDoSoundGetAudioCapability -> kDoAudioGetCapability
+// kDoSoundPlay       -> kDoAudioPlay, kDoAudioStop
+// kDoSoundPause      -> kDoAudioPause, kDoAudioResume
+// kDoSoundFade       -> kDoAudioFade
+// kDoSoundSetVolume  -> kDoAudioVolume
+// kDoSoundSetLoop    -> kDoAudioSetLoop
+// kDoSoundUpdateCues -> kDoAudioPosition
+//
+// In ScummVM, logic inside these kernel functions has been
+// moved to methods of Audio32, and direct calls to Audio32
+// are made from kDoSound instead.
+//
+// Some kDoAudio methods are esoteric and appear to be used
+// only by one or two games:
+//
+// kDoAudioMixing: Phantasmagoria (other games call this
+// function, but only to disable the feature)
+// kDoAudioHasSignal: SQ6 TalkRandCycle
+// kDoAudioPan: Rama RegionSFX::pan method
+//
+// Finally, there is a split in SCI2.1mid audio code.
+// QFG4CD & SQ6 do not have opcodes 18 and 19, but they
+// exist in GK2, KQ7 2.00b, Phantasmagoria 1, PQ:SWAT, and
+// Torin. (It is unknown if they exist in MUMG Deluxe or
+// Shivers 1; they are not used in either of these games.)
+
+//    version,         subId, function-mapping,                    signature,              workarounds
+static const SciKernelMapSubEntry kDoAudio_subops[] = {
+	{ SIG_SCI32,           0, MAP_CALL(DoAudioInit),               "",                     NULL },
+	// SCI2 includes a Sync script that would call
+	// kDoAudioWaitForPlay, but SSCI has no opcode 1 until
+	// SCI2.1early
+	{ SIG_SINCE_SCI21,     1, MAP_CALL(DoAudioWaitForPlay),        "(i)(i)(i)(i)(i)(i)(i)", NULL },
+	{ SIG_SCI32,           2, MAP_CALL(DoAudioPlay),               "(i)(i)(i)(i)(i)(i)(i)", NULL },
+	{ SIG_SCI32,           3, MAP_CALL(DoAudioStop),               "(i)(i)(i)(i)(i)",      NULL },
+	{ SIG_SCI32,           4, MAP_CALL(DoAudioPause),              "(i)(i)(i)(i)(i)",      NULL },
+	{ SIG_SCI32,           5, MAP_CALL(DoAudioResume),             "(i)(i)(i)(i)(i)",      NULL },
+	{ SIG_SCI32,           6, MAP_CALL(DoAudioPosition),           "(i)(i)(i)(i)(i)",      NULL },
+	{ SIG_SCI32,           7, MAP_CALL(DoAudioRate),               "(i)",                  NULL },
+	{ SIG_SCI32,           8, MAP_CALL(DoAudioVolume),             "(i)(i)(i)(i)(i)(i)",   NULL },
+	{ SIG_SCI32,           9, MAP_CALL(DoAudioGetCapability),      "",                     NULL },
+	{ SIG_SCI32,          10, MAP_CALL(DoAudioBitDepth),           "(i)",                  NULL },
+	{ SIG_SCI32,          11, MAP_DUMMY(DoAudioDistort),           "(i)",                  NULL },
+	{ SIG_SCI32,          12, MAP_CALL(DoAudioMixing),             "(i)",                  NULL },
+	{ SIG_SCI32,          13, MAP_CALL(DoAudioChannels),           "(i)",                  NULL },
+	{ SIG_SCI32,          14, MAP_CALL(DoAudioPreload),            "(i)",                  NULL },
+	{ SIG_SINCE_SCI21MID, 15, MAP_CALL(DoAudioFade),               "(iiii)(i)(i)",         NULL },
+	{ SIG_SINCE_SCI21MID, 16, MAP_DUMMY(DoAudioFade36),            "iiiii(iii)(i)",        NULL },
+	{ SIG_SINCE_SCI21MID, 17, MAP_CALL(DoAudioHasSignal),          "",                     NULL },
+	{ SIG_SINCE_SCI21MID, 18, MAP_EMPTY(DoAudioCritical),          "",                     NULL },
+	{ SIG_SINCE_SCI21MID, 19, MAP_CALL(DoAudioSetLoop),            "iii(o)",               NULL },
+	{ SIG_SCI3,           20, MAP_DUMMY(DoAudioPan),               "",                     NULL },
+	{ SIG_SCI3,           21, MAP_DUMMY(DoAudioPanOff),            "",                     NULL },
+	SCI_SUBOPENTRY_TERMINATOR
+};
+#endif
+
 //    version,         subId, function-mapping,                    signature,              workarounds
 static const SciKernelMapSubEntry kGraph_subops[] = {
 	{ SIG_SCI32,           1, MAP_CALL(StubNull),                  "",                     NULL }, // called by gk1 sci32 right at the start
@@ -484,7 +545,10 @@ static SciKernelMapEntry s_kernelMap[] = {
 	{ MAP_CALL(DisposeList),       SIG_EVERYWHERE,           "l",                     NULL,            NULL },
 	{ MAP_CALL(DisposeScript),     SIG_EVERYWHERE,           "i(i*)",                 NULL,            kDisposeScript_workarounds },
 	{ MAP_CALL(DisposeWindow),     SIG_EVERYWHERE,           "i(i)",                  NULL,            NULL },
-	{ MAP_CALL(DoAudio),           SIG_EVERYWHERE,           "i(.*)",                 NULL,            NULL }, // subop
+	{ MAP_CALL(DoAudio),           SCI_VERSION_NONE, SCI_VERSION_2, SIGFOR_ALL, "i(.*)", NULL,         NULL }, // subop
+#ifdef ENABLE_SCI32
+	{ "DoAudio", kDoAudio32,       SIG_SINCE_SCI21, SIGFOR_ALL, "(.*)",          kDoAudio_subops, NULL },
+#endif
 	{ MAP_CALL(DoAvoider),         SIG_EVERYWHERE,           "o(i)",                  NULL,            NULL },
 	{ MAP_CALL(DoBresen),          SIG_EVERYWHERE,           "o",                     NULL,            NULL },
 	{ MAP_CALL(DoSound),           SIG_EVERYWHERE,           "i(.*)",                 kDoSound_subops, NULL },
diff --git a/engines/sci/engine/ksound.cpp b/engines/sci/engine/ksound.cpp
index 2b22d68..301e4c1 100644
--- a/engines/sci/engine/ksound.cpp
+++ b/engines/sci/engine/ksound.cpp
@@ -26,6 +26,9 @@
 #include "sci/engine/kernel.h"
 #include "sci/engine/vm.h"		// for Object
 #include "sci/sound/audio.h"
+#ifdef ENABLE_SCI32
+#include "sci/sound/audio32.h"
+#endif
 #include "sci/sound/soundcmd.h"
 #include "sci/sound/sync.h"
 
@@ -114,7 +117,8 @@ reg_t kDoCdAudio(EngineState *s, int argc, reg_t *argv) {
 }
 
 /**
- * Used for speech playback and digital soundtracks in CD games
+ * Used for speech playback and digital soundtracks in CD games.
+ * This is the SCI16 version; SCI32 is handled separately.
  */
 reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) {
 	// JonesCD uses different functions based on the cdaudio.map file
@@ -185,14 +189,6 @@ reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) {
 		int16 volume = argv[1].toUint16();
 		volume = CLIP<int16>(volume, 0, AUDIO_VOLUME_MAX);
 		debugC(kDebugLevelSound, "kDoAudio: set volume to %d", volume);
-#ifdef ENABLE_SCI32
-		if (getSciVersion() >= SCI_VERSION_2_1_EARLY) {
-			int16 volumePrev = mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType) / 2;
-			volumePrev = CLIP<int16>(volumePrev, 0, AUDIO_VOLUME_MAX);
-			mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, volume * 2);
-			return make_reg(0, volumePrev);
-		} else
-#endif
 		mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, volume * 2);
 		break;
 	}
@@ -233,12 +229,6 @@ reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) {
 		if (getSciVersion() <= SCI_VERSION_1_1) {
 			debugC(kDebugLevelSound, "kDoAudio: CD audio subop");
 			return kDoCdAudio(s, argc - 1, argv + 1);
-#ifdef ENABLE_SCI32
-		} else {
-			// TODO: This isn't CD Audio in SCI32 anymore
-			warning("kDoAudio: Unhandled case 10, %d extra arguments passed", argc - 1);
-			break;
-#endif
 		}
 
 		// 3 new subops in Pharkas CD (including CD demo). kDoAudio in Pharkas sits at seg026:038C
@@ -320,6 +310,155 @@ reg_t kDoSync(EngineState *s, int argc, reg_t *argv) {
 }
 
 #ifdef ENABLE_SCI32
+reg_t kDoAudio32(EngineState *s, int argc, reg_t *argv) {
+	if (!s)
+		return make_reg(0, getSciVersion());
+	error("not supposed to call this");
+}
+
+reg_t kDoAudioInit(EngineState *s, int argc, reg_t *argv) {
+	return make_reg(0, 0);
+}
+
+reg_t kDoAudioWaitForPlay(EngineState *s, int argc, reg_t *argv) {
+	return g_sci->_audio32->kernelPlay(false, argc, argv);
+}
+
+reg_t kDoAudioPlay(EngineState *s, int argc, reg_t *argv) {
+	return g_sci->_audio32->kernelPlay(true, argc, argv);
+}
+
+reg_t kDoAudioStop(EngineState *s, int argc, reg_t *argv) {
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG);
+	return make_reg(0, g_sci->_audio32->stop(channelIndex));
+}
+
+reg_t kDoAudioPause(EngineState *s, int argc, reg_t *argv) {
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG);
+	return make_reg(0, g_sci->_audio32->pause(channelIndex));
+}
+
+reg_t kDoAudioResume(EngineState *s, int argc, reg_t *argv) {
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG);
+	return make_reg(0, g_sci->_audio32->resume(channelIndex));
+}
+
+reg_t kDoAudioPosition(EngineState *s, int argc, reg_t *argv) {
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG);
+	return make_reg(0, g_sci->_audio32->getPosition(channelIndex));
+}
+
+reg_t kDoAudioRate(EngineState *s, int argc, reg_t *argv) {
+	// NOTE: In the original engine this would set the hardware
+	// DSP sampling rate; ScummVM mixer does not need this, so
+	// we only store the value to satisfy engine compatibility.
+
+	if (argc > 0) {
+		const uint16 sampleRate = argv[0].toUint16();
+		if (sampleRate != 0) {
+			g_sci->_audio32->setSampleRate(sampleRate);
+		}
+	}
+
+	return make_reg(0, g_sci->_audio32->getSampleRate());
+}
+
+reg_t kDoAudioVolume(EngineState *s, int argc, reg_t *argv) {
+	const int16 volume = argc > 0 ? argv[0].toSint16() : -1;
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 1, argc > 2 ? argv[2] : NULL_REG);
+
+	if (volume != -1) {
+		g_sci->_audio32->setVolume(channelIndex, volume);
+	}
+
+	return make_reg(0, g_sci->_audio32->getVolume(channelIndex));
+}
+
+reg_t kDoAudioGetCapability(EngineState *s, int argc, reg_t *argv) {
+	return make_reg(0, 1);
+}
+
+reg_t kDoAudioBitDepth(EngineState *s, int argc, reg_t *argv) {
+	// NOTE: In the original engine this would set the hardware
+	// DSP bit depth; ScummVM mixer does not need this, so
+	// we only store the value to satisfy engine compatibility.
+
+	if (argc > 0) {
+		const uint16 bitDepth = argv[0].toUint16();
+		if (bitDepth != 0) {
+			g_sci->_audio32->setBitDepth(bitDepth);
+		}
+	}
+
+	return make_reg(0, g_sci->_audio32->getBitDepth());
+}
+
+reg_t kDoAudioMixing(EngineState *s, int argc, reg_t *argv) {
+	if (argc > 0) {
+		g_sci->_audio32->setAttenuatedMixing(argv[0].toUint16());
+	}
+
+	return make_reg(0, g_sci->_audio32->getAttenuatedMixing());
+}
+
+reg_t kDoAudioChannels(EngineState *s, int argc, reg_t *argv) {
+	// NOTE: In the original engine this would set the hardware
+	// DSP stereo output; ScummVM mixer does not need this, so
+	// we only store the value to satisfy engine compatibility.
+
+	if (argc > 0) {
+		const int16 numChannels = argv[0].toSint16();
+		if (numChannels != 0) {
+			g_sci->_audio32->setNumOutputChannels(numChannels);
+		}
+	}
+
+	return make_reg(0, g_sci->_audio32->getNumOutputChannels());
+}
+
+reg_t kDoAudioPreload(EngineState *s, int argc, reg_t *argv) {
+	// NOTE: In the original engine this would cause audio
+	// data for new channels to be preloaded to memory when
+	// the channel was initialized; we do not need this, so
+	// we only store the value to satisfy engine compatibility.
+
+	if (argc > 0) {
+		g_sci->_audio32->setPreload(argv[0].toUint16());
+	}
+
+	return make_reg(0, g_sci->_audio32->getPreload());
+}
+
+reg_t kDoAudioFade(EngineState *s, int argc, reg_t *argv) {
+	if (argc < 4) {
+		return make_reg(0, 0);
+	}
+
+	// NOTE: Sierra did a nightmarish hack here, temporarily replacing
+	// the argc of the kernel arguments with 2 and then restoring it
+	// after findChannelByArgs was called.
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(2, argv, 0, argc > 5 ? argv[5] : NULL_REG);
+
+	const int16 volume = argv[1].toSint16();
+	const int16 speed = argv[2].toSint16();
+	const int16 steps = argv[3].toSint16();
+	const bool stopAfterFade = argc > 4 ? (bool)argv[4].toUint16() : false;
+
+	return make_reg(0, g_sci->_audio32->fadeChannel(channelIndex, volume, speed, steps, stopAfterFade));
+}
+
+reg_t kDoAudioHasSignal(EngineState *s, int argc, reg_t *argv) {
+	return make_reg(0, g_sci->_audio32->hasSignal());
+}
+
+reg_t kDoAudioSetLoop(EngineState *s, int argc, reg_t *argv) {
+	const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc == 3 ? argv[2] : NULL_REG);
+
+	const bool loop = argv[0].toSint16() != 0 && argv[0].toSint16() != 1;
+
+	g_sci->_audio32->setLoop(channelIndex, loop);
+	return s->r_acc;
+}
 
 reg_t kSetLanguage(EngineState *s, int argc, reg_t *argv) {
 	// This is used by script 90 of MUMG Deluxe from the main menu to toggle
diff --git a/engines/sci/module.mk b/engines/sci/module.mk
index c35d0b5..69ddf00 100644
--- a/engines/sci/module.mk
+++ b/engines/sci/module.mk
@@ -91,6 +91,8 @@ MODULE_OBJS += \
 	graphics/palette32.o \
 	graphics/screen_item32.o \
 	graphics/text32.o \
+	sound/audio32.o \
+	sound/decoders/sol.o \
 	video/robot_decoder.o
 endif
 
diff --git a/engines/sci/resource.cpp b/engines/sci/resource.cpp
index 9f977aa..2a83a57 100644
--- a/engines/sci/resource.cpp
+++ b/engines/sci/resource.cpp
@@ -26,6 +26,9 @@
 #include "common/fs.h"
 #include "common/macresman.h"
 #include "common/textconsole.h"
+#ifdef ENABLE_SCI32
+#include "common/memstream.h"
+#endif
 
 #include "sci/resource.h"
 #include "sci/resource_intern.h"
@@ -221,6 +224,12 @@ void Resource::writeToStream(Common::WriteStream *stream) const {
 	stream->write(data, size);
 }
 
+#ifdef ENABLE_SCI32
+Common::SeekableReadStream *Resource::makeStream() const {
+	return new Common::MemoryReadStream(data, size, DisposeAfterUse::NO);
+}
+#endif
+
 uint32 Resource::getAudioCompressionType() const {
 	return _source->getAudioCompressionType();
 }
@@ -229,7 +238,6 @@ uint32 AudioVolumeResourceSource::getAudioCompressionType() const {
 	return _audioCompressionType;
 }
 
-
 ResourceSource::ResourceSource(ResSourceType type, const Common::String &name, int volNum, const Common::FSNode *resFile)
  : _sourceType(type), _name(name), _volumeNumber(volNum), _resourceFile(resFile) {
 	_scanned = false;
@@ -1445,13 +1453,18 @@ void ResourceManager::readResourcePatchesBase36() {
 		files.clear();
 
 		// audio36 resources start with a @, A, or B
-		// sync36 resources start with a #
+		// sync36 resources start with a #, S, or T
 		if (i == kResourceTypeAudio36) {
 			SearchMan.listMatchingMembers(files, "@???????.???");
 			SearchMan.listMatchingMembers(files, "A???????.???");
 			SearchMan.listMatchingMembers(files, "B???????.???");
-		} else
+		} else {
 			SearchMan.listMatchingMembers(files, "#???????.???");
+#ifdef ENABLE_SCI32
+			SearchMan.listMatchingMembers(files, "S???????.???");
+			SearchMan.listMatchingMembers(files, "T???????.???");
+#endif
+		}
 
 		for (Common::ArchiveMemberList::const_iterator x = files.begin(); x != files.end(); ++x) {
 			name = (*x)->getName();
diff --git a/engines/sci/resource.h b/engines/sci/resource.h
index ef474d9..f70bf48 100644
--- a/engines/sci/resource.h
+++ b/engines/sci/resource.h
@@ -84,7 +84,10 @@ enum ResourceType {
 	kResourceTypePatch,
 	kResourceTypeBitmap,
 	kResourceTypePalette,
-	kResourceTypeCdAudio,
+	kResourceTypeCdAudio = 12,
+#ifdef ENABLE_SCI32
+	kResourceTypeWave = 12,
+#endif
 	kResourceTypeAudio,
 	kResourceTypeSync,
 	kResourceTypeMessage,
@@ -212,6 +215,10 @@ public:
 		return (_type == other._type) && (_number == other._number) && (_tuple == other._tuple);
 	}
 
+	bool operator!=(const ResourceId &other) const {
+		return !operator==(other);
+	}
+
 	bool operator<(const ResourceId &other) const {
 		return (_type < other._type) || ((_type == other._type) && (_number < other._number))
 			    || ((_type == other._type) && (_number == other._number) && (_tuple < other._tuple));
@@ -259,6 +266,10 @@ public:
 	 */
 	void writeToStream(Common::WriteStream *stream) const;
 
+#ifdef ENABLE_SCI32
+	Common::SeekableReadStream *makeStream() const;
+#endif
+
 	const Common::String &getResourceLocation() const;
 
 	// FIXME: This audio specific method is a hack. After all, why should a
diff --git a/engines/sci/resource_audio.cpp b/engines/sci/resource_audio.cpp
index 5717a09..5aeff81 100644
--- a/engines/sci/resource_audio.cpp
+++ b/engines/sci/resource_audio.cpp
@@ -25,7 +25,7 @@
 #include "common/archive.h"
 #include "common/file.h"
 #include "common/textconsole.h"
-
+#include "common/memstream.h"
 #include "sci/resource.h"
 #include "sci/resource_intern.h"
 #include "sci/util.h"
diff --git a/engines/sci/sci.cpp b/engines/sci/sci.cpp
index 8c23d32..6244c43 100644
--- a/engines/sci/sci.cpp
+++ b/engines/sci/sci.cpp
@@ -68,6 +68,7 @@
 #include "sci/graphics/palette32.h"
 #include "sci/graphics/text32.h"
 #include "sci/graphics/frameout.h"
+#include "sci/sound/audio32.h"
 #include "sci/video/robot_decoder.h"
 #endif
 
@@ -88,6 +89,9 @@ SciEngine::SciEngine(OSystem *syst, const ADGameDescription *desc, SciGameId gam
 
 	_audio = 0;
 	_sync = nullptr;
+#ifdef ENABLE_SCI32
+	_audio32 = nullptr;
+#endif
 	_features = 0;
 	_resMan = 0;
 	_gamestate = 0;
@@ -167,6 +171,7 @@ SciEngine::~SciEngine() {
 	delete _robotDecoder;
 	delete _gfxFrameout;
 	delete _gfxRemap32;
+	delete _audio32;
 #endif
 	delete _gfxMenu;
 	delete _gfxControls16;
@@ -269,7 +274,13 @@ Common::Error SciEngine::run() {
 	// Also, XMAS1990 apparently had a parser too. Refer to http://forums.scummvm.org/viewtopic.php?t=9135
 	if (getGameId() == GID_CHRISTMAS1990)
 		_vocabulary = new Vocabulary(_resMan, false);
-	_audio = new AudioPlayer(_resMan);
+
+#ifdef ENABLE_SCI32
+	if (getSciVersion() >= SCI_VERSION_2_1_EARLY) {
+		_audio32 = new Audio32(_resMan);
+	} else
+#endif
+		_audio = new AudioPlayer(_resMan);
 	_sync = new Sync(_resMan, segMan);
 	_gamestate = new EngineState(segMan);
 	_eventMan = new EventManager(_resMan->detectFontExtended());
@@ -805,7 +816,9 @@ void SciEngine::runGame() {
 void SciEngine::exitGame() {
 	if (_gamestate->abortScriptProcessing != kAbortLoadGame) {
 		_gamestate->_executionStack.clear();
-		_audio->stopAllAudio();
+		if (_audio) {
+			_audio->stopAllAudio();
+		}
 		_sync->stop();
 		_soundCmd->clearPlayList();
 	}
diff --git a/engines/sci/sci.h b/engines/sci/sci.h
index ff41472..56ee2f4 100644
--- a/engines/sci/sci.h
+++ b/engines/sci/sci.h
@@ -83,6 +83,7 @@ class GfxTransitions;
 #ifdef ENABLE_SCI32
 class RobotDecoder;
 class GfxFrameout;
+class Audio32;
 #endif
 
 // our engine debug levels
@@ -368,6 +369,7 @@ public:
 	GfxMacIconBar *_gfxMacIconBar; // Mac Icon Bar manager
 
 #ifdef ENABLE_SCI32
+	Audio32 *_audio32;
 	RobotDecoder *_robotDecoder;
 	GfxFrameout *_gfxFrameout; // kFrameout and the like for 32-bit gfx
 #endif
diff --git a/engines/sci/sound/audio.cpp b/engines/sci/sound/audio.cpp
index 71b0810..4fb9a58 100644
--- a/engines/sci/sound/audio.cpp
+++ b/engines/sci/sound/audio.cpp
@@ -253,13 +253,7 @@ static void deDPCM16(byte *soundBuf, Common::SeekableReadStream &audioStream, ui
 
 static void deDPCM8Nibble(byte *soundBuf, int32 &s, byte b) {
 	if (b & 8) {
-#ifdef ENABLE_SCI32
-		// SCI2.1 reverses the order of the table values here
-		if (getSciVersion() >= SCI_VERSION_2_1_EARLY)
-			s -= tableDPCM8[b & 7];
-		else
-#endif
-			s -= tableDPCM8[7 - (b & 7)];
+		s -= tableDPCM8[7 - (b & 7)];
 	} else
 		s += tableDPCM8[b & 7];
 	s = CLIP<int32>(s, 0, 255);
diff --git a/engines/sci/sound/audio32.cpp b/engines/sci/sound/audio32.cpp
new file mode 100644
index 0000000..56b10c5
--- /dev/null
+++ b/engines/sci/sound/audio32.cpp
@@ -0,0 +1,961 @@
+/* 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.
+ *
+ */
+
+#include "sci/sound/audio32.h"
+#include "audio/audiostream.h"      // for SeekableAudioStream
+#include "audio/decoders/raw.h"     // for makeRawStream, RawFlags::FLAG_16BITS
+#include "audio/decoders/wave.h"    // for makeWAVStream
+#include "audio/rate.h"             // for RateConverter, makeRateConverter
+#include "audio/timestamp.h"        // for Timestamp
+#include "common/config-manager.h"  // for ConfMan
+#include "common/endian.h"          // for MKTAG
+#include "common/memstream.h"       // for MemoryReadStream
+#include "common/str.h"             // for String
+#include "common/stream.h"          // for SeekableReadStream
+#include "common/system.h"          // for OSystem, g_system
+#include "common/textconsole.h"     // for warning
+#include "common/types.h"           // for Flag::NO
+#include "engine.h"                 // for Engine, g_engine
+#include "sci/engine/vm_types.h"    // for reg_t, make_reg, NULL_REG
+#include "sci/resource.h"           // for ResourceId, ResourceType::kResour...
+#include "sci/sci.h"                // for SciEngine, g_sci, getSciVersion
+#include "sci/sound/decoders/sol.h" // for makeSOLStream
+
+namespace Sci {
+
+bool detectSolAudio(Common::SeekableReadStream &stream) {
+	const size_t initialPosition = stream.pos();
+
+// TODO: Resource manager for audio resources reads past the
+// header so even though this is the detection algorithm
+// in SSCI, ScummVM can't use it
+#if 0
+	byte header[6];
+	if (stream.read(header, sizeof(header)) != sizeof(header)) {
+		stream.seek(initialPosition);
+		return false;
+	}
+
+	stream.seek(initialPosition);
+
+	if (header[0] != 0x8d || READ_BE_UINT32(header + 2) != MKTAG('S', 'O', 'L', 0)) {
+		return false;
+	}
+
+	return true;
+#else
+	byte header[4];
+	if (stream.read(header, sizeof(header)) != sizeof(header)) {
+		stream.seek(initialPosition);
+		return false;
+	}
+
+	stream.seek(initialPosition);
+
+	if (READ_BE_UINT32(header) != MKTAG('S', 'O', 'L', 0)) {
+		return false;
+	}
+
+	return true;
+#endif
+}
+
+bool detectWaveAudio(Common::SeekableReadStream &stream) {
+	const size_t initialPosition = stream.pos();
+
+	byte blockHeader[8];
+	if (stream.read(blockHeader, sizeof(blockHeader)) != sizeof(blockHeader)) {
+		stream.seek(initialPosition);
+		return false;
+	}
+
+	stream.seek(initialPosition);
+	const uint32 headerType = READ_BE_UINT32(blockHeader);
+
+	if (headerType != MKTAG('R', 'I', 'F', 'F')) {
+		return false;
+	}
+
+	return true;
+}
+
+#pragma mark -
+
+Audio32::Audio32(ResourceManager *resMan) :
+	_resMan(resMan),
+	_mixer(g_system->getMixer()),
+	_handle(),
+	_mutex(),
+
+	_numActiveChannels(0),
+
+	_maxAllowedSampleRate(44100),
+	_maxAllowedBitDepth(16),
+	_maxAllowedOutputChannels(2),
+	_preload(0),
+
+	_robotAudioPaused(false),
+
+	_pausedAtTick(0),
+	_startedAtTick(0),
+
+	_attenuatedMixing(true),
+
+	 _monitoredChannelIndex(-1),
+	 _monitoredBuffer(nullptr),
+	 _monitoredBufferSize(0),
+	 _numMonitoredSamples(0) {
+
+	if (getSciVersion() < SCI_VERSION_3) {
+		_channels.resize(5);
+	} else {
+		_channels.resize(8);
+	}
+
+	_useModifiedAttenuation = false;
+	if (getSciVersion() == SCI_VERSION_2_1_MIDDLE) {
+		switch (g_sci->getGameId()) {
+		case GID_MOTHERGOOSEHIRES:
+		case GID_PQ4:
+		case GID_QFG4:
+		case GID_SQ6:
+			_useModifiedAttenuation = true;
+		default:
+			break;
+		}
+	} else if (getSciVersion() == SCI_VERSION_2_1_EARLY) {
+		switch (g_sci->getGameId()) {
+		// 1.51 uses the non-standard attenuation, but 2.00b
+		// does not, which is strange.
+		case GID_KQ7:
+			_useModifiedAttenuation = true;
+		default:
+			break;
+		}
+	}
+
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
+	_mixer->pauseHandle(_handle, true);
+}
+
+Audio32::~Audio32() {
+	stop(kAllChannels);
+	_mixer->stopHandle(_handle);
+	free(_monitoredBuffer);
+}
+
+#pragma mark -
+#pragma mark AudioStream implementation
+
+int Audio32::writeAudioInternal(Audio::RewindableAudioStream *const sourceStream, Audio::RateConverter *const converter, Audio::st_sample_t *targetBuffer, const int numSamples, const Audio::st_volume_t leftVolume, const Audio::st_volume_t rightVolume, const bool loop) {
+	int samplesToRead = numSamples;
+
+	// The parent rate converter will request N * 2
+	// samples from this `readBuffer` call, because
+	// we tell it that we send stereo output, but
+	// the source stream we're mixing in may be
+	// mono, in which case we need to request half
+	// as many samples from the mono stream and let
+	// the converter double them for stereo output
+	if (!sourceStream->isStereo()) {
+		samplesToRead >>= 1;
+	}
+
+	int samplesWritten = 0;
+
+	do {
+		if (loop && sourceStream->endOfStream()) {
+			sourceStream->rewind();
+		}
+
+		const int loopSamplesWritten = converter->flow(*sourceStream, targetBuffer, samplesToRead, leftVolume, rightVolume);
+
+		if (loopSamplesWritten == 0) {
+			break;
+		}
+
+		samplesToRead -= loopSamplesWritten;
+		samplesWritten += loopSamplesWritten;
+		targetBuffer += loopSamplesWritten << 1;
+	} while (loop && samplesToRead > 0);
+
+	if (!sourceStream->isStereo()) {
+		samplesWritten <<= 1;
+	}
+
+	return samplesWritten;
+}
+
+// In earlier versions of SCI32 engine, audio mixing is
+// split into three different functions.
+//
+// The first function is called from the main game thread in
+// AsyncEventCheck; later versions of SSCI also call it when
+// getting the playback position. This function is
+// responsible for cleaning up finished channels and
+// filling active channel buffers with decompressed audio
+// matching the hardware output audio format so they can
+// just be copied into the main DAC buffer directly later.
+//
+// The second function is called by the audio hardware when
+// the DAC buffer needs to be filled, and by `play` when
+// there is only one active sample (so it can just blow away
+// whatever was already in the DAC buffer). It merges all
+// active channels into the DAC buffer and then updates the
+// offset into the DAC buffer.
+//
+// Finally, a third function is called by the second
+// function, and it actually puts data into the DAC buffer,
+// performing volume, distortion, and balance adjustments.
+//
+// Since we only have one callback from the audio thread,
+// and should be able to do all audio processing in
+// real time, and we have streams, and we do not need to
+// completely fill the audio buffer, the functionality of
+// all these original functions is combined here and
+// simplified.
+int Audio32::readBuffer(Audio::st_sample_t *const buffer, const int numSamples) {
+	Common::StackLock lock(_mutex);
+
+	// The system mixer should not try to get data when
+	// Audio32 is paused
+	assert(_pausedAtTick == 0 && _numActiveChannels > 0);
+
+	// The caller of `readBuffer` is a rate converter,
+	// which reuses (without clearing) an intermediate
+	// buffer, so we need to zero the intermediate buffer
+	// to prevent mixing into audio data from the last
+	// callback.
+	memset(buffer, 0, numSamples * sizeof(Audio::st_sample_t));
+
+	// This emulates the attenuated mixing mode of SSCI
+	// engine, which reduces the volume of the target
+	// buffer when each new channel is mixed in.
+	// Instead of manipulating the content of the target
+	// buffer when mixing (which would either require
+	// modification of RateConverter or an expensive second
+	// pass against the entire target buffer), we just
+	// scale the volume for each channel in advance, with
+	// the earliest (lowest) channel having the highest
+	// amount of attenuation (lowest volume).
+	uint8 attenuationAmount;
+	uint8 attenuationStepAmount;
+	if (_useModifiedAttenuation) {
+		// channel | divisor
+		//       0 | 0  (>> 0)
+		//       1 | 4  (>> 2)
+		//       2 | 8...
+		attenuationAmount = _numActiveChannels * 2;
+		attenuationStepAmount = 2;
+	} else {
+		// channel | divisor
+		//       0 | 2  (>> 1)
+		//       1 | 4  (>> 2)
+		//       2 | 6...
+		if (_monitoredChannelIndex == -1 && _numActiveChannels > 1) {
+			attenuationAmount = _numActiveChannels + 1;
+			attenuationStepAmount = 1;
+		} else {
+			attenuationAmount = 0;
+			attenuationStepAmount = 0;
+		}
+	}
+
+	int maxSamplesWritten = 0;
+
+	for (int16 channelIndex = 0; channelIndex < _numActiveChannels; ++channelIndex) {
+		attenuationAmount -= attenuationStepAmount;
+
+		const AudioChannel &channel = getChannel(channelIndex);
+
+		if (channel.pausedAtTick || (channel.robot && _robotAudioPaused)) {
+			continue;
+		}
+
+		// Channel finished fading and had the
+		// stopChannelOnFade flag set, so no longer exists
+		if (channel.fadeStepsRemaining && processFade(channelIndex)) {
+			--channelIndex;
+			continue;
+		}
+
+		if (channel.robot) {
+			// TODO: Robot audio into output buffer
+			continue;
+		}
+
+		if (channel.vmd) {
+			// TODO: VMD audio into output buffer
+			continue;
+		}
+
+		Audio::st_volume_t leftVolume, rightVolume;
+
+		if (channel.pan == -1 || !isStereo()) {
+			leftVolume = rightVolume = channel.volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume;
+		} else {
+			// TODO: This should match the SCI3 algorithm,
+			// which seems to halve the volume of each
+			// channel when centered; is this intended?
+			leftVolume = channel.volume * (100 - channel.pan) / 100 * Audio::Mixer::kMaxChannelVolume / kMaxVolume;
+			rightVolume = channel.volume * channel.pan / 100 * Audio::Mixer::kMaxChannelVolume / kMaxVolume;
+		}
+
+		if (_monitoredChannelIndex == -1 && _attenuatedMixing) {
+			leftVolume >>= attenuationAmount;
+			rightVolume >>= attenuationAmount;
+		}
+
+		if (channelIndex == _monitoredChannelIndex) {
+			const size_t bufferSize = numSamples * sizeof(Audio::st_sample_t);
+			if (_monitoredBufferSize < bufferSize) {
+				_monitoredBuffer = (Audio::st_sample_t *)realloc(_monitoredBuffer, bufferSize);
+				_monitoredBufferSize = bufferSize;
+			}
+
+			memset(_monitoredBuffer, 0, _monitoredBufferSize);
+
+			_numMonitoredSamples = writeAudioInternal(channel.stream, channel.converter, _monitoredBuffer, numSamples, leftVolume, rightVolume, channel.loop);
+
+			Audio::st_sample_t *sourceBuffer = _monitoredBuffer;
+			Audio::st_sample_t *targetBuffer = buffer;
+			const Audio::st_sample_t *const end = _monitoredBuffer + _numMonitoredSamples;
+			while (sourceBuffer != end) {
+				Audio::clampedAdd(*targetBuffer++, *sourceBuffer++);
+			}
+
+			if (_numMonitoredSamples > maxSamplesWritten) {
+				maxSamplesWritten = _numMonitoredSamples;
+			}
+		} else if (!channel.stream->endOfStream() || channel.loop) {
+			const int channelSamplesWritten = writeAudioInternal(channel.stream, channel.converter, buffer, numSamples, leftVolume, rightVolume, channel.loop);
+			if (channelSamplesWritten > maxSamplesWritten) {
+				maxSamplesWritten = channelSamplesWritten;
+			}
+		}
+	}
+
+	freeUnusedChannels();
+
+	return maxSamplesWritten;
+}
+
+#pragma mark -
+#pragma mark Channel management
+
+int16 Audio32::findChannelByArgs(int argc, const reg_t *argv, const int startIndex, const reg_t soundNode) const {
+	// NOTE: argc/argv are already reduced by one in our engine because
+	// this call is always made from a subop, so no reduction for the
+	// subop is made in this function. SSCI takes extra steps to skip
+	// the subop argument.
+
+	argc -= startIndex;
+	if (argc <= 0) {
+		return kAllChannels;
+	}
+
+	Common::StackLock lock(_mutex);
+
+	if (_numActiveChannels == 0) {
+		return kNoExistingChannel;
+	}
+
+	ResourceId searchId;
+
+	if (argc < 5) {
+		searchId = ResourceId(kResourceTypeAudio, argv[startIndex].toUint16());
+	} else {
+		searchId = ResourceId(
+			kResourceTypeAudio36,
+			argv[startIndex].toUint16(),
+			argv[startIndex + 1].toUint16(),
+			argv[startIndex + 2].toUint16(),
+			argv[startIndex + 3].toUint16(),
+			argv[startIndex + 4].toUint16()
+		);
+	}
+
+	return findChannelById(searchId, soundNode);
+}
+
+int16 Audio32::findChannelById(const ResourceId resourceId, const reg_t soundNode) const {
+	Common::StackLock lock(_mutex);
+
+	if (_numActiveChannels == 0) {
+		return kNoExistingChannel;
+	}
+
+	if (resourceId.getType() == kResourceTypeAudio) {
+		for (int16 i = 0; i < _numActiveChannels; ++i) {
+			const AudioChannel channel = _channels[i];
+			if (
+				channel.id == resourceId &&
+				(soundNode.isNull() || soundNode == channel.soundNode)
+			) {
+				return i;
+			}
+		}
+	} else if (resourceId.getType() == kResourceTypeAudio36) {
+		for (int16 i = 0; i < _numActiveChannels; ++i) {
+			const AudioChannel &candidate = getChannel(i);
+			if (!candidate.robot && candidate.id == resourceId) {
+				return i;
+			}
+		}
+	} else {
+		error("Audio32::findChannelById: Unknown resource type %d", resourceId.getType());
+	}
+
+	return kNoExistingChannel;
+}
+
+void Audio32::freeUnusedChannels() {
+	Common::StackLock lock(_mutex);
+	for (int channelIndex = 0; channelIndex < _numActiveChannels; ++channelIndex) {
+		const AudioChannel &channel = getChannel(channelIndex);
+		if (channel.stream->endOfStream()) {
+			if (channel.loop) {
+				channel.stream->rewind();
+			} else {
+				stop(channelIndex--);
+			}
+		}
+	}
+}
+
+void Audio32::freeChannel(const int16 channelIndex) {
+	// The original engine did this:
+	// 1. Unlock memory-cached resource, if one existed
+	// 2. Close patched audio file descriptor, if one existed
+	// 3. Free decompression memory buffer, if one existed
+	// 4. Clear monitored memory buffer, if one existed
+	Common::StackLock lock(_mutex);
+	AudioChannel &channel = getChannel(channelIndex);
+	_resMan->unlockResource(channel.resource);
+	channel.resource = nullptr;
+	delete channel.stream;
+	channel.stream = nullptr;
+	delete channel.resourceStream;
+	channel.resourceStream = nullptr;
+	delete channel.converter;
+	channel.converter = nullptr;
+
+	if (_monitoredChannelIndex == channelIndex) {
+		_monitoredChannelIndex = -1;
+	}
+}
+
+#pragma mark -
+#pragma mark Script compatibility
+
+void Audio32::setSampleRate(uint16 rate) {
+	if (rate > _maxAllowedSampleRate) {
+		rate = _maxAllowedSampleRate;
+	}
+
+	_globalSampleRate = rate;
+}
+
+void Audio32::setBitDepth(uint8 depth) {
+	if (depth > _maxAllowedBitDepth) {
+		depth = _maxAllowedBitDepth;
+	}
+
+	_globalBitDepth = depth;
+}
+
+void Audio32::setNumOutputChannels(int16 numChannels) {
+	if (numChannels > _maxAllowedOutputChannels) {
+		numChannels = _maxAllowedOutputChannels;
+	}
+
+	_globalNumOutputChannels = numChannels;
+}
+
+#pragma mark -
+#pragma mark Playback
+
+uint16 Audio32::play(int16 channelIndex, const ResourceId resourceId, const bool autoPlay, const bool loop, const int16 volume, const reg_t soundNode, const bool monitor) {
+	Common::StackLock lock(_mutex);
+
+	if (channelIndex != kNoExistingChannel) {
+		AudioChannel &channel = getChannel(channelIndex);
+
+		if (channel.pausedAtTick) {
+			resume(channelIndex);
+			return MIN(65534, 1 + channel.stream->getLength().msecs() * 60 / 1000);
+		}
+
+		warning("Tried to resume channel %s that was not paused", channel.id.toString().c_str());
+		return MIN(65534, 1 + channel.stream->getLength().msecs() * 60 / 1000);
+	}
+
+	if (_numActiveChannels == _channels.size()) {
+		warning("Audio mixer is full when trying to play %s", resourceId.toString().c_str());
+		return 0;
+	}
+
+	// NOTE: SCI engine itself normally searches in this order:
+	//
+	// For Audio36:
+	//
+	// 1. First, request a FD using Audio36 name and use it as the
+	//    source FD for reading the audio resource data.
+	// 2a. If the returned FD is -1, or equals the audio map, or
+	//     equals the audio bundle, try to get the offset of the
+	//     data from the audio map, using the Audio36 name.
+	//
+	//     If the returned offset is -1, this is not a valid resource;
+	//     return 0. Otherwise, set the read offset for the FD to the
+	//     returned offset.
+	// 2b. Otherwise, use the FD as-is (it is a patch file), with zero
+	//     offset, and record it separately so it can be closed later.
+	//
+	// For plain audio:
+	//
+	// 1. First, request an Audio resource from the resource cache. If
+	//    one does not exist, make the same request for a Wave resource.
+	// 2a. If an audio resource was discovered, record its memory ID
+	//     and clear the streaming FD
+	// 2b. Otherwise, request an Audio FD. If one does not exist, make
+	//     the same request for a Wave FD. If neither exist, this is not
+	//     a valid resource; return 0. Otherwise, use the returned FD as
+	//     the streaming ID and set the memory ID to null.
+	//
+	// Once these steps are complete, the audio engine either has a file
+	// descriptor + offset that it can use to read streamed audio, or it
+	// has a memory ID that it can use to read cached audio.
+	//
+	// Here in ScummVM we just ask the resource manager to give us the
+	// resource and we get a seekable stream.
+
+	// TODO: This should be fixed to use streaming, which means
+	// fixing the resource manager to allow streaming, which means
+	// probably rewriting a bunch of the resource manager.
+	Resource *resource = _resMan->findResource(resourceId, true);
+	if (resource == nullptr) {
+		return 0;
+	}
+
+	channelIndex = _numActiveChannels++;
+
+	AudioChannel &channel = getChannel(channelIndex);
+	channel.id = resourceId;
+	channel.resource = resource;
+	channel.loop = loop;
+	channel.robot = false;
+	channel.vmd = false;
+	channel.lastFadeTick = 0;
+	channel.fadeStepsRemaining = 0;
+	channel.soundNode = soundNode;
+	channel.volume = volume < 0 || volume > kMaxVolume ? (int)kMaxVolume : volume;
+	// TODO: SCI3 introduces stereo audio
+	channel.pan = -1;
+
+	if (monitor) {
+		_monitoredChannelIndex = channelIndex;
+	}
+
+	Common::MemoryReadStream headerStream(resource->_header, resource->_headerSize, DisposeAfterUse::NO);
+	Common::SeekableReadStream *dataStream = channel.resourceStream = resource->makeStream();
+
+	if (detectSolAudio(headerStream)) {
+		channel.stream = makeSOLStream(&headerStream, dataStream, DisposeAfterUse::NO);
+	} else if (detectWaveAudio(*dataStream)) {
+		channel.stream = Audio::makeWAVStream(dataStream, DisposeAfterUse::NO);
+	} else {
+		byte flags = Audio::FLAG_LITTLE_ENDIAN;
+		if (_globalBitDepth == 16) {
+			flags |= Audio::FLAG_16BITS;
+		} else {
+			flags |= Audio::FLAG_UNSIGNED;
+		}
+
+		if (_globalNumOutputChannels == 2) {
+			flags |= Audio::FLAG_STEREO;
+		}
+
+		channel.stream = Audio::makeRawStream(dataStream, _globalSampleRate, flags, DisposeAfterUse::NO);
+	}
+
+	channel.converter = Audio::makeRateConverter(channel.stream->getRate(), getRate(), channel.stream->isStereo(), false);
+
+	// NOTE: SCI engine sets up a decompression buffer here for the audio
+	// stream, plus writes information about the sample to the channel to
+	// convert to the correct hardware output format, and allocates the
+	// monitoring buffer to match the bitrate/samplerate/channels of the
+	// original stream. We do not need to do any of these things since we
+	// use audio streams, and allocate and fill the monitoring buffer
+	// when reading audio data from the stream.
+
+	channel.duration = /* round up */ 1 + (channel.stream->getLength().msecs() * 60 / 1000);
+
+	const uint32 now = g_sci->getTickCount();
+	if (!autoPlay) {
+		channel.pausedAtTick = now;
+	}
+	channel.startedAtTick = now;
+
+	if (_numActiveChannels == 1) {
+		_startedAtTick = now;
+		_mixer->pauseHandle(_handle, false);
+	}
+
+	return channel.duration;
+}
+
+bool Audio32::resume(const int16 channelIndex) {
+	if (channelIndex == kNoExistingChannel) {
+		return false;
+	}
+
+	Common::StackLock lock(_mutex);
+	const uint32 now = g_sci->getTickCount();
+
+	if (channelIndex == kAllChannels) {
+		// Global pause in SSCI is an extra layer over
+		// individual channel pauses, so only unpause channels
+		// if there was not a global pause in place
+		if (_pausedAtTick == 0) {
+			return false;
+		}
+
+		for (int i = 0; i < _numActiveChannels; ++i) {
+			AudioChannel &channel = getChannel(i);
+			if (!channel.pausedAtTick) {
+				channel.startedAtTick += now - _pausedAtTick;
+			}
+		}
+
+		_startedAtTick += now - _pausedAtTick;
+		_pausedAtTick = 0;
+		_mixer->pauseHandle(_handle, false);
+		return true;
+	} else if (channelIndex == kRobotChannel) {
+		for (int i = 0; i < _numActiveChannels; ++i) {
+			AudioChannel &channel = getChannel(i);
+			if (channel.robot) {
+				channel.startedAtTick += now - channel.pausedAtTick;
+				channel.pausedAtTick = 0;
+				// TODO: Robot
+				// StartRobot();
+				return true;
+			}
+		}
+	} else {
+		AudioChannel &channel = getChannel(channelIndex);
+		if (channel.pausedAtTick) {
+			channel.startedAtTick += now - channel.pausedAtTick;
+			channel.pausedAtTick = 0;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool Audio32::pause(const int16 channelIndex) {
+	if (channelIndex == kNoExistingChannel) {
+		return false;
+	}
+
+	Common::StackLock lock(_mutex);
+	const uint32 now = g_sci->getTickCount();
+	bool didPause = false;
+
+	if (channelIndex == kAllChannels) {
+		if (_pausedAtTick == 0) {
+			_pausedAtTick = now;
+			_mixer->pauseHandle(_handle, true);
+			didPause = true;
+		}
+	} else if (channelIndex == kRobotChannel) {
+		_robotAudioPaused = true;
+		for (int16 i = 0; i < _numActiveChannels; ++i) {
+			AudioChannel &channel = getChannel(i);
+			if (channel.robot) {
+				channel.pausedAtTick = now;
+			}
+		}
+
+		// NOTE: The actual engine returns false here regardless of whether
+		// or not channels were paused
+	} else {
+		AudioChannel &channel = getChannel(channelIndex);
+
+		if (channel.pausedAtTick == 0) {
+			channel.pausedAtTick = now;
+			didPause = true;
+		}
+	}
+
+	return didPause;
+}
+
+int16 Audio32::stop(const int16 channelIndex) {
+	Common::StackLock lock(_mutex);
+	const int16 oldNumChannels = _numActiveChannels;
+
+	if (channelIndex == kNoExistingChannel || oldNumChannels == 0) {
+		return 0;
+	}
+
+	if (channelIndex == kAllChannels) {
+		for (int i = 0; i < oldNumChannels; ++i) {
+			freeChannel(i);
+		}
+		_numActiveChannels = 0;
+	} else {
+		freeChannel(channelIndex);
+		--_numActiveChannels;
+		for (int i = channelIndex; i < oldNumChannels - 1; ++i) {
+			_channels[i] = _channels[i + 1];
+			if (i + 1 == _monitoredChannelIndex) {
+				_monitoredChannelIndex = i;
+			}
+		}
+	}
+
+	// NOTE: SSCI stops the DSP interrupt and frees the
+	// global decompression buffer here if there are no
+	// more active channels
+	if (_numActiveChannels == 0) {
+		_mixer->pauseHandle(_handle, true);
+	}
+
+	return oldNumChannels;
+}
+
+int16 Audio32::getPosition(const int16 channelIndex) const {
+	Common::StackLock lock(_mutex);
+	if (channelIndex == kNoExistingChannel || _numActiveChannels == 0) {
+		return -1;
+	}
+
+	// NOTE: SSCI treats this as an unsigned short except for
+	// when the value is 65535, then it treats it as signed
+	int position = -1;
+	const uint32 now = g_sci->getTickCount();
+
+	// NOTE: The original engine also queried the audio driver to see whether
+	// it thought that there was audio playback occurring via driver opcode 9
+	if (channelIndex == kAllChannels) {
+		if (_pausedAtTick) {
+			position = _pausedAtTick - _startedAtTick;
+		} else {
+			position = now - _startedAtTick;
+		}
+	} else {
+		const AudioChannel &channel = getChannel(channelIndex);
+
+		if (channel.pausedAtTick) {
+			position = channel.pausedAtTick - channel.startedAtTick;
+		} else if (_pausedAtTick) {
+			position = _pausedAtTick - channel.startedAtTick;
+		} else {
+			position = now - channel.startedAtTick;
+		}
+	}
+
+	return MIN(position, 65534);
+}
+
+void Audio32::setLoop(const int16 channelIndex, const bool loop) {
+	Common::StackLock lock(_mutex);
+
+	if (channelIndex < 0 || channelIndex >= _numActiveChannels) {
+		return;
+	}
+
+	AudioChannel &channel = getChannel(channelIndex);
+	channel.loop = loop;
+}
+
+reg_t Audio32::kernelPlay(const bool autoPlay, const int argc, const reg_t *const argv) {
+	if (argc == 0) {
+		return make_reg(0, _numActiveChannels);
+	}
+
+	const int16 channelIndex = findChannelByArgs(argc, argv, 0, NULL_REG);
+	ResourceId resourceId;
+	bool loop;
+	int16 volume;
+	bool monitor = false;
+	reg_t soundNode;
+
+	if (argc >= 5) {
+		resourceId = ResourceId(kResourceTypeAudio36, argv[0].toUint16(), argv[1].toUint16(), argv[2].toUint16(), argv[3].toUint16(), argv[4].toUint16());
+
+		if (argc < 6 || argv[5].toSint16() == 1) {
+			loop = false;
+		} else {
+			// NOTE: Uses -1 for infinite loop. Presumably the
+			// engine was supposed to allow counter loops at one
+			// point, but ended up only using loop as a boolean.
+			loop = (bool)argv[5].toSint16();
+		}
+
+		if (argc < 7 || argv[6].toSint16() < 0 || argv[6].toSint16() > Audio32::kMaxVolume) {
+			volume = Audio32::kMaxVolume;
+
+			if (argc >= 7) {
+				monitor = true;
+			}
+		} else {
+			volume = argv[6].toSint16();
+		}
+	} else {
+		resourceId = ResourceId(kResourceTypeAudio, argv[0].toUint16());
+
+		if (argc < 2 || argv[1].toSint16() == 1) {
+			loop = false;
+		} else {
+			loop = (bool)argv[1].toSint16();
+		}
+
+		// TODO: SCI3 uses the 0x80 bit as a flag to
+		// indicate "priority channel", but the volume is clamped
+		// in this call to 0x7F so that flag never makes it into
+		// the audio subsystem
+		if (argc < 3 || argv[2].toSint16() < 0 || argv[2].toSint16() > Audio32::kMaxVolume) {
+			volume = Audio32::kMaxVolume;
+
+			if (argc >= 3) {
+				monitor = true;
+			}
+		} else {
+			volume = argv[2].toSint16();
+		}
+
+		soundNode = argc == 4 ? argv[3] : NULL_REG;
+	}
+
+	return make_reg(0, play(channelIndex, resourceId, autoPlay, loop, volume, soundNode, monitor));
+}
+
+#pragma mark -
+#pragma mark Effects
+
+int16 Audio32::getVolume(const int16 channelIndex) const {
+	Common::StackLock lock(_mutex);
+	if (channelIndex < 0 || channelIndex >= _numActiveChannels) {
+		return _mixer->getChannelVolume(_handle) * kMaxVolume / Audio::Mixer::kMaxChannelVolume;
+	}
+
+	return getChannel(channelIndex).volume;
+}
+
+void Audio32::setVolume(const int16 channelIndex, int16 volume) {
+	Common::StackLock lock(_mutex);
+
+	volume = MIN((int16)kMaxVolume, volume);
+	if (channelIndex == kAllChannels) {
+		ConfMan.setInt("sfx_volume", volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume);
+		ConfMan.setInt("speech_volume", volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume);
+		_mixer->setChannelVolume(_handle, volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume);
+		g_engine->syncSoundSettings();
+	} else if (channelIndex != kNoExistingChannel) {
+		getChannel(channelIndex).volume = volume;
+	}
+}
+
+bool Audio32::fadeChannel(const int16 channelIndex, const int16 targetVolume, const int16 speed, const int16 steps, const bool stopAfterFade) {
+	Common::StackLock lock(_mutex);
+
+	if (channelIndex < 0 || channelIndex >= _numActiveChannels) {
+		return false;
+	}
+
+	AudioChannel &channel = getChannel(channelIndex);
+
+	if (channel.id.getType() != kResourceTypeAudio || channel.volume == targetVolume) {
+		return false;
+	}
+
+	if (steps) {
+		channel.fadeVolume = targetVolume;
+		channel.fadeSpeed = speed;
+		channel.fadeStepsRemaining = steps;
+		channel.stopChannelOnFade = stopAfterFade;
+		channel.lastFadeTick = g_sci->getTickCount();
+	} else {
+		setVolume(channelIndex, targetVolume);
+	}
+
+	return true;
+}
+
+bool Audio32::processFade(const int16 channelIndex) {
+	Common::StackLock lock(_mutex);
+	AudioChannel &channel = getChannel(channelIndex);
+
+	uint32 now = g_sci->getTickCount();
+
+	if (channel.lastFadeTick + channel.fadeSpeed <= now) {
+		--channel.fadeStepsRemaining;
+
+		if (!channel.fadeStepsRemaining) {
+			if (channel.stopChannelOnFade) {
+				stop(channelIndex);
+				return true;
+			} else {
+				setVolume(channelIndex, channel.fadeVolume);
+			}
+		} else {
+			int volume = channel.volume - (channel.volume - channel.fadeVolume) / (channel.fadeStepsRemaining + 1);
+
+			if (volume == channel.fadeVolume) {
+				channel.fadeStepsRemaining = 1;
+			}
+
+			setVolume(channelIndex, volume);
+			channel.lastFadeTick = now;
+		}
+	}
+
+	return false;
+}
+
+#pragma mark -
+#pragma mark Signal monitoring
+
+bool Audio32::hasSignal() const {
+	Common::StackLock lock(_mutex);
+
+	if (_monitoredChannelIndex == -1) {
+		return false;
+	}
+
+	const Audio::st_sample_t *buffer = _monitoredBuffer;
+	const Audio::st_sample_t *const end = _monitoredBuffer + _numMonitoredSamples;
+
+	while (buffer != end) {
+		const Audio::st_sample_t sample = *buffer++;
+		if (sample > 1280 || sample < -1280) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+} // End of namespace Sci
diff --git a/engines/sci/sound/audio32.h b/engines/sci/sound/audio32.h
new file mode 100644
index 0000000..59aba66
--- /dev/null
+++ b/engines/sci/sound/audio32.h
@@ -0,0 +1,566 @@
+/* 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.
+ *
+ */
+
+#ifndef SCI_AUDIO32_H
+#define SCI_AUDIO32_H
+#include "audio/audiostream.h"     // for AudioStream, SeekableAudioStream (...
+#include "audio/mixer.h"           // for Mixer, SoundHandle
+#include "audio/rate.h"            // for Audio::st_volume_t, RateConverter
+#include "common/array.h"          // for Array
+#include "common/mutex.h"          // for StackLock, Mutex
+#include "common/scummsys.h"       // for int16, uint8, uint32, uint16
+#include "engines/sci/resource.h"  // for ResourceId
+#include "sci/engine/vm_types.h"   // for reg_t, NULL_REG
+
+namespace Sci {
+
+/**
+ * An audio channel used by the software SCI mixer.
+ */
+struct AudioChannel {
+	/**
+	 * The ID of the resource loaded into this channel.
+	 */
+	ResourceId id;
+
+	/**
+	 * The resource loaded into this channel.
+	 */
+	Resource *resource;
+
+	/**
+	 * Data stream containing the raw audio for the channel.
+	 */
+	Common::SeekableReadStream *resourceStream;
+
+	/**
+	 * The audio stream loaded into this channel.
+	 * `SeekableAudioStream` is used here instead of
+	 * `RewindableAudioStream` because
+	 * `RewindableAudioStream` does not include the
+	 * `getLength` function, which is needed to tell the
+	 * game engine the duration of audio streams.
+	 */
+	Audio::SeekableAudioStream *stream;
+
+	/**
+	 * The converter used to transform and merge the input
+	 * stream into the mixer's output buffer.
+	 */
+	Audio::RateConverter *converter;
+
+	/**
+	 * Duration of the channel, in ticks.
+	 */
+	uint32 duration;
+
+	/**
+	 * The tick when the channel was started.
+	 */
+	uint32 startedAtTick;
+
+	/**
+	 * The tick when the channel was paused.
+	 */
+	uint32 pausedAtTick;
+
+	/**
+	 * Whether or not the audio in this channel should loop
+	 * infinitely.
+	 */
+	bool loop;
+
+	/**
+	 * The time the last fade iteration occurred.
+	 */
+	uint32 lastFadeTick;
+
+	/**
+	 * The target volume of the fade.
+	 */
+	int fadeVolume;
+
+	/**
+	 * The number of ticks that should elapse between
+	 * each change of volume.
+	 */
+	int fadeSpeed;
+
+	/**
+	 * The number of iterations the fade should take to
+	 * complete. If this value is 0, it indicates that the
+	 * channel is not fading.
+	 */
+	int fadeStepsRemaining;
+
+	/**
+	 * Whether or not the channel should be stopped and
+	 * freed when the fade is complete.
+	 */
+	bool stopChannelOnFade;
+
+	/**
+	 * Whether or not this channel contains a Robot
+	 * audio block.
+	 */
+	bool robot;
+
+	/**
+	 * Whether or not this channel contains a VMD audio
+	 * track.
+	 */
+	bool vmd;
+
+	/**
+	 * For digital sound effects, the related VM
+	 * Sound::nodePtr object for the sound.
+	 */
+	reg_t soundNode;
+
+	/**
+	 * The playback volume, from 1 to 127 inclusive.
+	 */
+	int volume;
+
+	/**
+	 * The amount to pan to the right, from 0 to 100.
+	 * 50 is centered, -1 is not panned.
+	 */
+	int pan;
+};
+
+/**
+ * Special audio channel indexes used to select a channel
+ * for digital audio playback.
+ */
+enum AudioChannelIndex {
+	kRobotChannel = -3,
+	kNoExistingChannel = -2,
+	kAllChannels = -1
+};
+
+/**
+ * Audio32 acts as a permanent audio stream into the system
+ * mixer and provides digital audio services for the SCI32
+ * engine, since the system mixer does not support all the
+ * features of SCI.
+ */
+class Audio32 : public Audio::AudioStream {
+public:
+	Audio32(ResourceManager *resMan);
+	~Audio32();
+
+private:
+	ResourceManager *_resMan;
+	Audio::Mixer *_mixer;
+	Audio::SoundHandle _handle;
+	Common::Mutex _mutex;
+
+	enum {
+		/**
+		 * The maximum channel volume.
+		 */
+		kMaxVolume = 127
+	};
+
+#pragma mark -
+#pragma mark AudioStream implementation
+public:
+	int readBuffer(Audio::st_sample_t *const buffer, const int numSamples);
+	bool isStereo() const { return true; }
+	int getRate() const { return _mixer->getOutputRate(); }
+	bool endOfData() const { return _numActiveChannels == 0; }
+	bool endOfStream() const { return false; }
+
+private:
+	/**
+	 * Mixes audio from the given source stream into the
+	 * target buffer using the given rate converter.
+	 */
+	int writeAudioInternal(Audio::RewindableAudioStream *const sourceStream, Audio::RateConverter *const converter, Audio::st_sample_t *targetBuffer, const int numSamples, const Audio::st_volume_t leftVolume, const Audio::st_volume_t rightVolume, const bool loop);
+
+#pragma mark -
+#pragma mark Channel management
+public:
+	/**
+	 * Gets the number of currently active channels.
+	 */
+	inline uint8 getNumActiveChannels() const {
+		Common::StackLock lock(_mutex);
+		return _numActiveChannels;
+	}
+
+	/**
+	 * Finds a channel that is already configured for the
+	 * given audio sample.
+	 *
+	 * @param startIndex The location of the audio resource
+	 * information in the arguments list.
+	 */
+	int16 findChannelByArgs(int argc, const reg_t *argv, const int startIndex, const reg_t soundNode) const;
+
+	/**
+	 * Finds a channel that is already configured for the
+	 * given audio sample.
+	 */
+	int16 findChannelById(const ResourceId resourceId, const reg_t soundNode = NULL_REG) const;
+
+private:
+	/**
+	 * The audio channels.
+	 */
+	Common::Array<AudioChannel> _channels;
+
+	/**
+	 * The number of active audio channels in the mixer.
+	 * Being active is not the same as playing; active
+	 * channels may be paused.
+	 */
+	uint8 _numActiveChannels;
+
+	/**
+	 * Gets the audio channel at the given index.
+	 */
+	inline AudioChannel &getChannel(const int16 channelIndex) {
+		Common::StackLock lock(_mutex);
+		assert(channelIndex >= 0 && channelIndex < _numActiveChannels);
+		return _channels[channelIndex];
+	}
+
+	/**
+	 * Gets the audio channel at the given index.
+	 */
+	inline const AudioChannel &getChannel(const int16 channelIndex) const {
+		Common::StackLock lock(_mutex);
+		assert(channelIndex >= 0 && channelIndex < _numActiveChannels);
+		return _channels[channelIndex];
+	}
+
+	/**
+	 * Frees all non-looping channels that have reached the
+	 * end of their stream.
+	 */
+	void freeUnusedChannels();
+
+	/**
+	 * Frees resources allocated to the given channel.
+	 */
+	void freeChannel(const int16 channelIndex);
+
+#pragma mark -
+#pragma mark Script compatibility
+public:
+	/**
+	 * Gets the (fake) sample rate of the hardware DAC.
+	 * For script compatibility only.
+	 */
+	inline uint16 getSampleRate() const {
+		return _globalSampleRate;
+	}
+
+	/**
+	 * Sets the (fake) sample rate of the hardware DAC.
+	 * For script compatibility only.
+	 */
+	void setSampleRate(uint16 rate);
+
+	/**
+	 * Gets the (fake) bit depth of the hardware DAC.
+	 * For script compatibility only.
+	 */
+	inline uint8 getBitDepth() const {
+		return _globalBitDepth;
+	}
+
+	/**
+	 * Sets the (fake) sample rate of the hardware DAC.
+	 * For script compatibility only.
+	 */
+	void setBitDepth(uint8 depth);
+
+	/**
+	 * Gets the (fake) number of output (speaker) channels
+	 * of the hardware DAC. For script compatibility only.
+	 */
+	inline uint8 getNumOutputChannels() const {
+		return _globalNumOutputChannels;
+	}
+
+	/**
+	 * Sets the (fake) number of output (speaker) channels
+	 * of the hardware DAC. For script compatibility only.
+	 */
+	void setNumOutputChannels(int16 numChannels);
+
+	/**
+	 * Gets the (fake) number of preloaded channels.
+	 * For script compatibility only.
+	 */
+	inline uint8 getPreload() const {
+		return _preload;
+	}
+
+	/**
+	 * Sets the (fake) number of preloaded channels.
+	 * For script compatibility only.
+	 */
+	inline void setPreload(uint8 preload) {
+		_preload = preload;
+	}
+
+private:
+	/**
+	 * The hardware DAC sample rate. Stored only for script
+	 * compatibility.
+	 */
+	uint16 _globalSampleRate;
+
+	/**
+	 * The maximum allowed sample rate of the system mixer.
+	 * Stored only for script compatibility.
+	 */
+	uint16 _maxAllowedSampleRate;
+
+	/**
+	 * The hardware DAC bit depth. Stored only for script
+	 * compatibility.
+	 */
+	uint8 _globalBitDepth;
+
+	/**
+	 * The maximum allowed bit depth of the system mixer.
+	 * Stored only for script compatibility.
+	 */
+	uint8 _maxAllowedBitDepth;
+
+	/**
+	 * The hardware DAC output (speaker) channel
+	 * configuration. Stored only for script compatibility.
+	 */
+	uint8 _globalNumOutputChannels;
+
+	/**
+	 * The maximum allowed number of output (speaker)
+	 * channels of the system mixer. Stored only for script
+	 * compatibility.
+	 */
+	uint8 _maxAllowedOutputChannels;
+
+	/**
+	 * The number of audio channels that should have their
+	 * data preloaded into memory instead of streaming from
+	 * disk.
+	 * 1 = all channels, 2 = 2nd active channel and above,
+	 * etc.
+	 * Stored only for script compatibility.
+	 */
+	uint8 _preload;
+
+#pragma mark -
+#pragma mark Robot
+public:
+
+private:
+	/**
+	 * When true, channels marked as robot audio will not be
+	 * played.
+	 */
+	bool _robotAudioPaused;
+
+#pragma mark -
+#pragma mark Playback
+public:
+	/**
+	 * Starts or resumes playback of an audio channel.
+	 */
+	uint16 play(int16 channelIndex, const ResourceId resourceId, const bool autoPlay, const bool loop, const int16 volume, const reg_t soundNode, const bool monitor);
+
+	/**
+	 * Resumes playback of a paused audio channel, or of
+	 * the entire audio player.
+	 */
+	bool resume(const int16 channelIndex);
+	bool resume(const ResourceId resourceId, const reg_t soundNode = NULL_REG) {
+		Common::StackLock lock(_mutex);
+		return resume(findChannelById(resourceId, soundNode));
+	}
+
+	/**
+	 * Pauses an audio channel, or the entire audio player.
+	 */
+	bool pause(const int16 channelIndex);
+	bool pause(const ResourceId resourceId, const reg_t soundNode = NULL_REG) {
+		Common::StackLock lock(_mutex);
+		return pause(findChannelById(resourceId, soundNode));
+	}
+
+	/**
+	 * Stops and unloads an audio channel, or the entire
+	 * audio player.
+	 */
+	int16 stop(const int16 channelIndex);
+	int16 stop(const ResourceId resourceId, const reg_t soundNode = NULL_REG) {
+		Common::StackLock lock(_mutex);
+		return stop(findChannelById(resourceId, soundNode));
+	}
+
+	/**
+	 * Returns the playback position for the given channel
+	 * number, in ticks.
+	 */
+	int16 getPosition(const int16 channelIndex) const;
+	int16 getPosition(const ResourceId resourceId, const reg_t soundNode = NULL_REG) {
+		Common::StackLock lock(_mutex);
+		return getPosition(findChannelById(resourceId, soundNode));
+	}
+
+	/**
+	 * Sets whether or not the given channel should loop.
+	 */
+	void setLoop(const int16 channelIndex, const bool loop);
+	void setLoop(const ResourceId resourceId, const reg_t soundNode, const bool loop) {
+		Common::StackLock lock(_mutex);
+		setLoop(findChannelById(resourceId, soundNode), loop);
+	}
+
+	reg_t kernelPlay(const bool autoPlay, const int argc, const reg_t *const argv);
+
+private:
+	/**
+	 * The tick when audio was globally paused.
+	 */
+	uint32 _pausedAtTick;
+
+	/**
+	 * The tick when audio was globally started.
+	 */
+	uint32 _startedAtTick;
+
+#pragma mark -
+#pragma mark Effects
+public:
+	/**
+	 * Gets the volume for a given channel. Passing
+	 * `kAllChannels` will get the global volume.
+	 */
+	int16 getVolume(const int16 channelIndex) const;
+	int16 getVolume(const ResourceId resourceId, const reg_t soundNode) const {
+		Common::StackLock lock(_mutex);
+		return getVolume(findChannelById(resourceId, soundNode));
+	}
+
+	/**
+	 * Sets the volume of an audio channel. Passing
+	 * `kAllChannels` will set the global volume.
+	 */
+	void setVolume(const int16 channelIndex, int16 volume);
+	void setVolume(const ResourceId resourceId, const reg_t soundNode, const int16 volume) {
+		Common::StackLock lock(_mutex);
+		setVolume(findChannelById(resourceId, soundNode), volume);
+	}
+
+	/**
+	 * Initiate an immediate fade of the given channel.
+	 */
+	bool fadeChannel(const int16 channelIndex, const int16 targetVolume, const int16 speed, const int16 steps, const bool stopAfterFade);
+	bool fadeChannel(const ResourceId resourceId, const reg_t soundNode, const int16 targetVolume, const int16 speed, const int16 steps, const bool stopAfterFade) {
+		Common::StackLock lock(_mutex);
+		return fadeChannel(findChannelById(resourceId, soundNode), targetVolume, speed, steps, stopAfterFade);
+	}
+
+	/**
+	 * Gets whether attenuated mixing mode is active.
+	 */
+	inline bool getAttenuatedMixing() const {
+		return _attenuatedMixing;
+	}
+
+	/**
+	 * Sets the attenuated mixing mode.
+	 */
+	void setAttenuatedMixing(bool attenuated) {
+		Common::StackLock lock(_mutex);
+		_attenuatedMixing = attenuated;
+	}
+
+private:
+	/**
+	 * If true, audio will be mixed by reducing the target
+	 * buffer by half every time a new channel is mixed in.
+	 * The final channel is not attenuated.
+	 */
+	bool _attenuatedMixing;
+
+	/**
+	 * When true, a modified attenuation algorithm is used
+	 * (`A/4 + B`) instead of standard linear attenuation
+	 * (`A/2 + B/2`).
+	 */
+	bool _useModifiedAttenuation;
+
+	/**
+	 * Processes an audio fade for the given channel.
+	 *
+	 * @returns true if the fade was completed and the
+	 * channel was stopped.
+	 */
+	bool processFade(const int16 channelIndex);
+
+#pragma mark -
+#pragma mark Signal monitoring
+public:
+	/**
+	 * Returns whether the currently monitored audio channel
+	 * contains any signal within the next audio frame.
+	 */
+	bool hasSignal() const;
+
+private:
+	/**
+	 * The index of the channel being monitored for signal,
+	 * or -1 if no channel is monitored. When a channel is
+	 * monitored, it also causes the engine to play only the
+	 * monitored channel.
+	 */
+	int16 _monitoredChannelIndex;
+
+	/**
+	 * The data buffer holding decompressed audio data for
+	 * the channel that will be monitored for an audio
+	 * signal.
+	 */
+	Audio::st_sample_t *_monitoredBuffer;
+
+	/**
+	 * The size of the buffer, in bytes.
+	 */
+	size_t _monitoredBufferSize;
+
+	/**
+	 * The number of valid audio samples in the signal
+	 * monitoring buffer.
+	 */
+	int _numMonitoredSamples;
+};
+
+} // End of namespace Sci
+#endif
diff --git a/engines/sci/sound/decoders/sol.cpp b/engines/sci/sound/decoders/sol.cpp
new file mode 100644
index 0000000..280b24f
--- /dev/null
+++ b/engines/sci/sound/decoders/sol.cpp
@@ -0,0 +1,272 @@
+/* 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.
+ *
+ */
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "common/substream.h"
+#include "common/util.h"
+#include "engines/sci/sci.h"
+#include "engines/sci/sound/decoders/sol.h"
+
+namespace Sci {
+
+// Note that the 16-bit version is also used in coktelvideo.cpp
+static const uint16 tableDPCM16[128] = {
+	0x0000, 0x0008, 0x0010, 0x0020, 0x0030, 0x0040, 0x0050, 0x0060, 0x0070, 0x0080,
+	0x0090, 0x00A0, 0x00B0, 0x00C0, 0x00D0, 0x00E0, 0x00F0, 0x0100, 0x0110, 0x0120,
+	0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0180, 0x0190, 0x01A0, 0x01B0, 0x01C0,
+	0x01D0, 0x01E0, 0x01F0, 0x0200, 0x0208, 0x0210, 0x0218, 0x0220, 0x0228, 0x0230,
+	0x0238, 0x0240, 0x0248, 0x0250, 0x0258, 0x0260, 0x0268, 0x0270, 0x0278, 0x0280,
+	0x0288, 0x0290, 0x0298, 0x02A0, 0x02A8, 0x02B0, 0x02B8, 0x02C0, 0x02C8, 0x02D0,
+	0x02D8, 0x02E0, 0x02E8, 0x02F0, 0x02F8, 0x0300, 0x0308, 0x0310, 0x0318, 0x0320,
+	0x0328, 0x0330, 0x0338, 0x0340, 0x0348, 0x0350, 0x0358, 0x0360, 0x0368, 0x0370,
+	0x0378, 0x0380, 0x0388, 0x0390, 0x0398, 0x03A0, 0x03A8, 0x03B0, 0x03B8, 0x03C0,
+	0x03C8, 0x03D0, 0x03D8, 0x03E0, 0x03E8, 0x03F0, 0x03F8, 0x0400, 0x0440, 0x0480,
+	0x04C0, 0x0500, 0x0540, 0x0580, 0x05C0, 0x0600, 0x0640, 0x0680, 0x06C0, 0x0700,
+	0x0740, 0x0780, 0x07C0, 0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00,
+	0x0F00, 0x1000, 0x1400, 0x1800, 0x1C00, 0x2000, 0x3000, 0x4000
+};
+
+static const byte tableDPCM8[8] = { 0, 1, 2, 3, 6, 10, 15, 21 };
+
+/**
+ * Decompresses 16-bit DPCM compressed audio. Each byte read
+ * outputs one sample into the decompression buffer.
+ */
+static void deDPCM16(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, int16 &sample) {
+	for (uint32 i = 0; i < numBytes; ++i) {
+		const uint8 delta = audioStream.readByte();
+		if (delta & 0x80) {
+			sample -= tableDPCM16[delta & 0x7f];
+		} else {
+			sample += tableDPCM16[delta];
+		}
+		sample = CLIP<int16>(sample, -32768, 32767);
+		*out++ = TO_LE_16(sample);
+	}
+}
+
+/**
+ * Decompresses one half of an 8-bit DPCM compressed audio
+ * byte.
+ */
+static void deDPCM8Nibble(int16 *out, uint8 &sample, uint8 delta) {
+	if (delta & 8) {
+		sample -= tableDPCM8[delta & 7];
+	} else {
+		sample += tableDPCM8[delta & 7];
+	}
+	sample = CLIP<byte>(sample, 0, 255);
+	*out = (sample << 8) ^ 0x8000;
+}
+
+/**
+ * Decompresses 8-bit DPCM compressed audio. Each byte read
+ * outputs two samples into the decompression buffer.
+ */
+static void deDPCM8(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, uint8 &sample) {
+	for (uint32 i = 0; i < numBytes; ++i) {
+		const uint8 delta = audioStream.readByte();
+		deDPCM8Nibble(out++, sample, delta >> 4);
+		deDPCM8Nibble(out++, sample, delta & 0xf);
+	}
+}
+
+# pragma mark -
+
+template<bool STEREO, bool S16BIT>
+SOLStream<STEREO, S16BIT>::SOLStream(Common::SeekableReadStream *stream, const DisposeAfterUse::Flag disposeAfterUse, const int32 dataOffset, const uint16 sampleRate, const int32 rawDataSize) :
+	_stream(stream, disposeAfterUse),
+	_dataOffset(dataOffset),
+	_sampleRate(sampleRate),
+	// SSCI aligns the size of SOL data to 32 bits
+	_rawDataSize(rawDataSize & ~3) {
+		// TODO: This is not valid for stereo SOL files, which
+		// have interleaved L/R compression so need to store the
+		// carried values for each channel separately. See
+		// 60900.aud from Lighthouse for an example stereo file
+		if (S16BIT) {
+			_dpcmCarry16 = 0;
+		} else {
+			_dpcmCarry8 = 0x80;
+		}
+
+		const uint8 compressionRatio = 2;
+		const uint8 numChannels = STEREO ? 2 : 1;
+		const uint8 bytesPerSample = S16BIT ? 2 : 1;
+		_length = Audio::Timestamp((_rawDataSize * compressionRatio * 1000) / (_sampleRate * numChannels * bytesPerSample), 60);
+	}
+
+template <bool STEREO, bool S16BIT>
+bool SOLStream<STEREO, S16BIT>::seek(const Audio::Timestamp &where) override {
+	if (where != 0) {
+		// In order to seek in compressed SOL files, all
+		// previous bytes must be known since it uses
+		// differential compression. Therefore, only seeking
+		// to the beginning is supported now (SSCI does not
+		// offer seeking anyway)
+		return false;
+	}
+
+	if (S16BIT) {
+		_dpcmCarry16 = 0;
+	} else {
+		_dpcmCarry8 = 0x80;
+	}
+
+	return _stream->seek(_dataOffset, SEEK_SET);
+}
+
+template <bool STEREO, bool S16BIT>
+Audio::Timestamp SOLStream<STEREO, S16BIT>::getLength() const override {
+	return _length;
+}
+
+template <bool STEREO, bool S16BIT>
+int SOLStream<STEREO, S16BIT>::readBuffer(int16 *buffer, const int numSamples) override {
+	// Reading an odd number of 8-bit samples will result in a loss of samples
+	// since one byte represents two samples and we do not store the second
+	// nibble in this case; it should never happen in reality
+	assert(S16BIT || (numSamples % 2) == 0);
+
+	const int samplesPerByte = S16BIT ? 1 : 2;
+
+	int32 bytesToRead = numSamples / samplesPerByte;
+	if (_stream->pos() + bytesToRead > _rawDataSize) {
+		bytesToRead = _rawDataSize - _stream->pos();
+	}
+
+	if (S16BIT) {
+		deDPCM16(buffer, *_stream, bytesToRead, _dpcmCarry16);
+	} else {
+		deDPCM8(buffer, *_stream, bytesToRead, _dpcmCarry8);
+	}
+
+	const int samplesRead = bytesToRead * samplesPerByte;
+	return samplesRead;
+}
+
+template <bool STEREO, bool S16BIT>
+bool SOLStream<STEREO, S16BIT>::isStereo() const override {
+	return STEREO;
+}
+
+template <bool STEREO, bool S16BIT>
+int SOLStream<STEREO, S16BIT>::getRate() const override {
+	return _sampleRate;
+}
+
+template <bool STEREO, bool S16BIT>
+bool SOLStream<STEREO, S16BIT>::endOfData() const override {
+	return _stream->eos() || _stream->pos() >= _dataOffset + _rawDataSize;
+}
+
+template <bool STEREO, bool S16BIT>
+bool SOLStream<STEREO, S16BIT>::rewind() override {
+	return seek(0);
+}
+
+Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) {
+
+	// TODO: Might not be necessary? Makes seeking work, but
+	// not sure if audio is ever actually seeked in SSCI.
+	const int32 initialPosition = stream->pos();
+
+	byte header[6];
+	if (stream->read(header, sizeof(header)) != sizeof(header)) {
+		return nullptr;
+	}
+
+	if (header[0] != 0x8d || READ_BE_UINT32(header + 2) != MKTAG('S', 'O', 'L', 0)) {
+		return nullptr;
+	}
+
+	const uint8 headerSize = header[1];
+	const uint16 sampleRate = stream->readUint16LE();
+	const byte flags = stream->readByte();
+	const uint32 dataSize = stream->readUint32LE();
+
+	if (flags & kCompressed) {
+		if (flags & kStereo && flags & k16Bit) {
+			return new SOLStream<true, true>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize);
+		} else if (flags & kStereo) {
+			return new SOLStream<true, false>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize);
+		} else if (flags & k16Bit) {
+			return new SOLStream<false, true>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize);
+		} else {
+			return new SOLStream<false, false>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize);
+		}
+	}
+
+	byte rawFlags = Audio::FLAG_LITTLE_ENDIAN;
+	if (flags & k16Bit) {
+		rawFlags |= Audio::FLAG_16BITS;
+	} else {
+		rawFlags |= Audio::FLAG_UNSIGNED;
+	}
+
+	if (flags & kStereo) {
+		rawFlags |= Audio::FLAG_STEREO;
+	}
+
+	return Audio::makeRawStream(new Common::SeekableSubReadStream(stream, initialPosition + headerSize, initialPosition + headerSize + dataSize, disposeAfterUse), sampleRate, rawFlags, disposeAfterUse);
+}
+
+// TODO: This needs to be removed when resource manager is fixed
+// to not split audio into two parts
+Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *headerStream, Common::SeekableReadStream *dataStream, DisposeAfterUse::Flag disposeAfterUse) {
+
+	if (headerStream->readUint32BE() != MKTAG('S', 'O', 'L', 0)) {
+		return nullptr;
+	}
+
+	const uint16 sampleRate = headerStream->readUint16LE();
+	const byte flags = headerStream->readByte();
+	const int32 dataSize = headerStream->readSint32LE();
+
+	if (flags & kCompressed) {
+		if (flags & kStereo && flags & k16Bit) {
+			return new SOLStream<true, true>(dataStream, disposeAfterUse, 0, sampleRate, dataSize);
+		} else if (flags & kStereo) {
+			return new SOLStream<true, false>(dataStream, disposeAfterUse, 0, sampleRate, dataSize);
+		} else if (flags & k16Bit) {
+			return new SOLStream<false, true>(dataStream, disposeAfterUse, 0, sampleRate, dataSize);
+		} else {
+			return new SOLStream<false, false>(dataStream, disposeAfterUse, 0, sampleRate, dataSize);
+		}
+	}
+
+	byte rawFlags = Audio::FLAG_LITTLE_ENDIAN;
+	if (flags & k16Bit) {
+		rawFlags |= Audio::FLAG_16BITS;
+	} else {
+		rawFlags |= Audio::FLAG_UNSIGNED;
+	}
+
+	if (flags & kStereo) {
+		rawFlags |= Audio::FLAG_STEREO;
+	}
+
+	return Audio::makeRawStream(dataStream, sampleRate, rawFlags, disposeAfterUse);
+}
+
+}
diff --git a/engines/sci/sound/decoders/sol.h b/engines/sci/sound/decoders/sol.h
new file mode 100644
index 0000000..1046d0b
--- /dev/null
+++ b/engines/sci/sound/decoders/sol.h
@@ -0,0 +1,89 @@
+/* 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.
+ *
+ */
+
+#ifndef SCI_SOUND_DECODERS_SOL_H
+#define SCI_SOUND_DECODERS_SOL_H
+#include "audio/audiostream.h"
+#include "common/stream.h"
+
+namespace Sci {
+
+enum SOLFlags {
+	kCompressed = 1,
+	k16Bit      = 4,
+	kStereo     = 16
+};
+
+template <bool STEREO, bool S16BIT>
+class SOLStream : public Audio::SeekableAudioStream {
+private:
+	/**
+	 * Read stream containing possibly-compressed SOL audio.
+	 */
+	Common::DisposablePtr<Common::SeekableReadStream> _stream;
+
+	/**
+	 * Start offset of the audio data in the read stream.
+	 */
+	int32 _dataOffset;
+
+	/**
+	 * Sample rate of audio data.
+	 */
+	uint16 _sampleRate;
+
+	/**
+	 * The raw (possibly-compressed) size of audio data in
+	 * the stream.
+	 */
+	int32 _rawDataSize;
+
+	/**
+	 * The last sample from the previous DPCM decode.
+	 */
+	union {
+		int16 _dpcmCarry16;
+		uint8 _dpcmCarry8;
+	};
+
+	/**
+	 * The calculated length of the stream.
+	 */
+	Audio::Timestamp _length;
+
+	virtual bool seek(const Audio::Timestamp &where) override;
+	virtual Audio::Timestamp getLength() const override;
+	virtual int readBuffer(int16 *buffer, const int numSamples) override;
+	virtual bool isStereo() const override;
+	virtual int getRate() const override;
+	virtual bool endOfData() const override;
+	virtual bool rewind() override;
+
+public:
+	SOLStream(Common::SeekableReadStream *stream, const DisposeAfterUse::Flag disposeAfterUse, const int32 dataOffset, const uint16 sampleRate, const int32 rawDataSize);
+};
+
+Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse);
+
+Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *headerStream, Common::SeekableReadStream *dataStream, DisposeAfterUse::Flag disposeAfterUse);
+}
+#endif
diff --git a/engines/sci/sound/music.cpp b/engines/sci/sound/music.cpp
index 5a11ac3..3f34ecc 100644
--- a/engines/sci/sound/music.cpp
+++ b/engines/sci/sound/music.cpp
@@ -212,6 +212,13 @@ void SciMusic::clearPlayList() {
 void SciMusic::pauseAll(bool pause) {
 	const MusicList::iterator end = _playList.end();
 	for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+#ifdef ENABLE_SCI32
+		// The entire DAC will have been paused by the caller;
+		// do not pause the individual samples too
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY && (*i)->isSample) {
+			continue;
+		}
+#endif
 		soundToggle(*i, pause);
 	}
 }
@@ -472,7 +479,16 @@ void SciMusic::soundPlay(MusicEntry *pSnd) {
 		}
 	}
 
-	if (pSnd->pStreamAud) {
+	if (pSnd->isSample) {
+#ifdef ENABLE_SCI32
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+			g_sci->_audio32->stop(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj);
+
+			g_sci->_audio32->play(kNoExistingChannel, ResourceId(kResourceTypeAudio, pSnd->resourceId), true, pSnd->loop != 0 && pSnd->loop != 1, pSnd->volume, pSnd->soundObj, false);
+
+			return;
+		} else
+#endif
 		if (!_pMixer->isSoundHandleActive(pSnd->hCurrentAud)) {
 			if ((_currentlyPlayingSample) && (_pMixer->isSoundHandleActive(_currentlyPlayingSample->hCurrentAud))) {
 				// Another sample is already playing, we have to stop that one
@@ -550,10 +566,18 @@ void SciMusic::soundStop(MusicEntry *pSnd) {
 	pSnd->status = kSoundStopped;
 	if (_soundVersion <= SCI_VERSION_0_LATE)
 		pSnd->isQueued = false;
-	if (pSnd->pStreamAud) {
-		if (_currentlyPlayingSample == pSnd)
-			_currentlyPlayingSample = NULL;
-		_pMixer->stopHandle(pSnd->hCurrentAud);
+	if (pSnd->isSample) {
+#ifdef ENABLE_SCI32
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+			g_sci->_audio32->stop(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj);
+		} else {
+#endif
+			if (_currentlyPlayingSample == pSnd)
+				_currentlyPlayingSample = NULL;
+			_pMixer->stopHandle(pSnd->hCurrentAud);
+#ifdef ENABLE_SCI32
+		}
+#endif
 	}
 
 	if (pSnd->pMidiParser) {
@@ -572,9 +596,12 @@ void SciMusic::soundStop(MusicEntry *pSnd) {
 
 void SciMusic::soundSetVolume(MusicEntry *pSnd, byte volume) {
 	assert(volume <= MUSIC_VOLUME_MAX);
-	if (pSnd->pStreamAud) {
-		// we simply ignore volume changes for samples, because sierra sci also
-		//  doesn't support volume for samples via kDoSound
+	if (pSnd->isSample) {
+#ifdef ENABLE_SCI32
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+			g_sci->_audio32->setVolume(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj, volume);
+		}
+#endif
 	} else if (pSnd->pMidiParser) {
 		Common::StackLock lock(_mutex);
 		pSnd->pMidiParser->mainThreadBegin();
@@ -614,12 +641,20 @@ void SciMusic::soundKill(MusicEntry *pSnd) {
 
 	_mutex.unlock();
 
-	if (pSnd->pStreamAud) {
-		if (_currentlyPlayingSample == pSnd) {
-			// Forget about this sound, in case it was currently playing
-			_currentlyPlayingSample = NULL;
+	if (pSnd->isSample) {
+#ifdef ENABLE_SCI32
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+			g_sci->_audio32->stop(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj);
+		} else {
+#endif
+			if (_currentlyPlayingSample == pSnd) {
+				// Forget about this sound, in case it was currently playing
+				_currentlyPlayingSample = NULL;
+			}
+			_pMixer->stopHandle(pSnd->hCurrentAud);
+#ifdef ENABLE_SCI32
 		}
-		_pMixer->stopHandle(pSnd->hCurrentAud);
+#endif
 		delete pSnd->pStreamAud;
 		pSnd->pStreamAud = NULL;
 		delete pSnd->pLoopStream;
@@ -685,6 +720,18 @@ void SciMusic::soundResume(MusicEntry *pSnd) {
 }
 
 void SciMusic::soundToggle(MusicEntry *pSnd, bool pause) {
+#ifdef ENABLE_SCI32
+	if (_soundVersion >= SCI_VERSION_2_1_EARLY && pSnd->isSample) {
+		if (pause) {
+			g_sci->_audio32->pause(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj);
+		} else {
+			g_sci->_audio32->resume(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj);
+		}
+
+		return;
+	}
+#endif
+
 	if (pause)
 		soundPause(pSnd);
 	else
@@ -813,6 +860,7 @@ MusicEntry::MusicEntry() {
 	pStreamAud = 0;
 	pLoopStream = 0;
 	pMidiParser = 0;
+	isSample = false;
 
 	for (int i = 0; i < 16; ++i) {
 		_usedChannels[i] = 0xFF;
diff --git a/engines/sci/sound/music.h b/engines/sci/sound/music.h
index 047f63b..3a6de81 100644
--- a/engines/sci/sound/music.h
+++ b/engines/sci/sound/music.h
@@ -31,6 +31,9 @@
 #include "sci/sci.h"
 #include "sci/resource.h"
 #include "sci/sound/drivers/mididriver.h"
+#ifdef ENABLE_SCI32
+#include "sci/sound/audio32.h"
+#endif
 
 namespace Audio {
 class LoopingAudioStream;
@@ -123,6 +126,7 @@ public:
 	Audio::RewindableAudioStream *pStreamAud;
 	Audio::LoopingAudioStream *pLoopStream;
 	Audio::SoundHandle hCurrentAud;
+	bool isSample;
 
 public:
 	MusicEntry();
diff --git a/engines/sci/sound/soundcmd.cpp b/engines/sci/sound/soundcmd.cpp
index efa4735..7d2ab32 100644
--- a/engines/sci/sound/soundcmd.cpp
+++ b/engines/sci/sound/soundcmd.cpp
@@ -23,6 +23,7 @@
 #include "common/config-manager.h"
 #include "audio/audiostream.h"
 #include "audio/mixer.h"
+#include "sci/resource.h"
 #include "sci/sound/audio.h"
 #include "sci/sound/music.h"
 #include "sci/sound/soundcmd.h"
@@ -97,12 +98,21 @@ void SoundCommandParser::initSoundResource(MusicEntry *newSound) {
 		// user wants the digital version.
 		if (_useDigitalSFX || !newSound->soundRes) {
 			int sampleLen;
-			newSound->pStreamAud = _audio->getAudioStream(newSound->resourceId, 65535, &sampleLen);
-			newSound->soundType = Audio::Mixer::kSFXSoundType;
+#ifdef ENABLE_SCI32
+			if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+				newSound->isSample = g_sci->getResMan()->testResource(ResourceId(kResourceTypeAudio, newSound->resourceId));
+			} else {
+#endif
+				newSound->pStreamAud = _audio->getAudioStream(newSound->resourceId, 65535, &sampleLen);
+				newSound->soundType = Audio::Mixer::kSFXSoundType;
+				newSound->isSample = newSound->pStreamAud != nullptr;
+#ifdef ENABLE_SCI32
+			}
+#endif
 		}
 	}
 
-	if (!newSound->pStreamAud && newSound->soundRes)
+	if (!newSound->isSample && newSound->soundRes)
 		_music->soundInitSnd(newSound);
 }
 
@@ -134,7 +144,7 @@ void SoundCommandParser::processInitSound(reg_t obj) {
 
 	_music->pushBackSlot(newSound);
 
-	if (newSound->soundRes || newSound->pStreamAud) {
+	if (newSound->soundRes || newSound->isSample) {
 		// Notify the engine
 		if (_soundVersion <= SCI_VERSION_0_LATE)
 			writeSelectorValue(_segMan, obj, SELECTOR(state), kSoundInitialized);
@@ -314,10 +324,22 @@ reg_t SoundCommandParser::kDoSoundPause(int argc, reg_t *argv, reg_t acc) {
 	}
 
 	reg_t obj = argv[0];
-	uint16 value = argc > 1 ? argv[1].toUint16() : 0;
-	if (!obj.getSegment()) {		// pause the whole playlist
-		_music->pauseAll(value);
-	} else {	// pause a playlist slot
+	const bool shouldPause = argc > 1 ? argv[1].toUint16() : false;
+	if (
+		(_soundVersion < SCI_VERSION_2_1_EARLY && !obj.getSegment()) ||
+		(_soundVersion >= SCI_VERSION_2_1_EARLY && obj.isNull())
+	) {
+		_music->pauseAll(shouldPause);
+#ifdef ENABLE_SCI32
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+			if (shouldPause) {
+				g_sci->_audio32->pause(kAllChannels);
+			} else {
+				g_sci->_audio32->resume(kAllChannels);
+			}
+		}
+#endif
+	} else {
 		MusicEntry *musicSlot = _music->getSlot(obj);
 		if (!musicSlot) {
 			// This happens quite frequently
@@ -325,7 +347,23 @@ reg_t SoundCommandParser::kDoSoundPause(int argc, reg_t *argv, reg_t acc) {
 			return acc;
 		}
 
-		_music->soundToggle(musicSlot, value);
+#ifdef ENABLE_SCI32
+		// NOTE: The original engine also expected a global
+		// "kernel call" flag to be true in order to perform
+		// this action, but the architecture of the ScummVM
+		// implementation is so different that it doesn't
+		// matter here
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
+			const int16 channelIndex = g_sci->_audio32->findChannelById(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj);
+
+			if (shouldPause) {
+				g_sci->_audio32->pause(channelIndex);
+			} else {
+				g_sci->_audio32->resume(channelIndex);
+			}
+		} else
+#endif
+			_music->soundToggle(musicSlot, shouldPause);
 	}
 	return acc;
 }
@@ -355,7 +393,11 @@ reg_t SoundCommandParser::kDoSoundMasterVolume(int argc, reg_t *argv, reg_t acc)
 		int vol = CLIP<int16>(argv[0].toSint16(), 0, MUSIC_MASTERVOLUME_MAX);
 		vol = vol * Audio::Mixer::kMaxMixerVolume / MUSIC_MASTERVOLUME_MAX;
 		ConfMan.setInt("music_volume", vol);
-		ConfMan.setInt("sfx_volume", vol);
+		// In SCI32, digital audio volume is controlled separately by
+		// kDoAudioVolume
+		if (_soundVersion < SCI_VERSION_2_1_EARLY) {
+			ConfMan.setInt("sfx_volume", vol);
+		}
 		g_engine->syncSoundSettings();
 	}
 	return acc;
@@ -378,6 +420,13 @@ reg_t SoundCommandParser::kDoSoundFade(int argc, reg_t *argv, reg_t acc) {
 
 	int volume = musicSlot->volume;
 
+#ifdef ENABLE_SCI32
+	if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
+		g_sci->_audio32->fadeChannel(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, argv[2].toSint16(), argv[3].toSint16(), argv[4].toSint16(), (bool)argv[5].toSint16());
+		return acc;
+	}
+#endif
+
 	// If sound is not playing currently, set signal directly
 	if (musicSlot->status != kSoundPlaying) {
 		debugC(kDebugLevelSound, "kDoSound(fade): %04x:%04x fading requested, but sound is currently not playing", PRINT_REG(obj));
@@ -466,7 +515,18 @@ void SoundCommandParser::processUpdateCues(reg_t obj) {
 		return;
 	}
 
-	if (musicSlot->pStreamAud) {
+	if (musicSlot->isSample) {
+#ifdef ENABLE_SCI32
+		if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+			const int position = g_sci->_audio32->getPosition(g_sci->_audio32->findChannelById(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj));
+
+			if (position == -1) {
+				processStopSound(musicSlot->soundObj, true);
+			}
+
+			return;
+		}
+#endif
 		// Update digital sound effect slots
 		uint currentLoopCounter = 0;
 
@@ -669,6 +729,12 @@ reg_t SoundCommandParser::kDoSoundSetVolume(int argc, reg_t *argv, reg_t acc) {
 
 	value = CLIP<int>(value, 0, MUSIC_VOLUME_MAX);
 
+#ifdef ENABLE_SCI32
+	// SSCI unconditionally sets volume if it is digital audio
+	if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
+		_music->soundSetVolume(musicSlot, value);
+	} else
+#endif
 	if (musicSlot->volume != value) {
 		musicSlot->volume = value;
 		_music->soundSetVolume(musicSlot, value);
@@ -727,6 +793,15 @@ reg_t SoundCommandParser::kDoSoundSetLoop(int argc, reg_t *argv, reg_t acc) {
 		}
 		return acc;
 	}
+
+#ifdef ENABLE_SCI32
+	if (_soundVersion >= SCI_VERSION_2_1_EARLY) {
+		if (value != -1) {
+			value = 1;
+		}
+	}
+#endif
+
 	if (value == -1) {
 		musicSlot->loop = 0xFFFF;
 	} else {
@@ -734,6 +809,13 @@ reg_t SoundCommandParser::kDoSoundSetLoop(int argc, reg_t *argv, reg_t acc) {
 	}
 
 	writeSelectorValue(_segMan, obj, SELECTOR(loop), musicSlot->loop);
+
+#ifdef ENABLE_SCI32
+	if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) {
+		g_sci->_audio32->setLoop(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, value == -1);
+	}
+#endif
+
 	return acc;
 }
 
diff --git a/engines/sci/sound/sync.cpp b/engines/sci/sound/sync.cpp
index 08abad1..4e75dab 100644
--- a/engines/sci/sound/sync.cpp
+++ b/engines/sci/sound/sync.cpp
@@ -25,6 +25,7 @@
 #include "sync.h"
 
 namespace Sci {
+
 Sync::Sync(ResourceManager *resMan, SegManager *segMan) :
 	_resMan(resMan),
 	_segMan(segMan),
diff --git a/engines/sci/sound/sync.h b/engines/sci/sound/sync.h
index c80982b..4b9e2d1 100644
--- a/engines/sci/sound/sync.h
+++ b/engines/sci/sound/sync.h
@@ -38,6 +38,10 @@ class Resource;
 class ResourceManager;
 class SegManager;
 
+/**
+ * Sync class, kDoSync and relevant functions for SCI games.
+ * Provides AV synchronization for animations.
+ */
 class Sync {
 	SegManager *_segMan;
 	ResourceManager *_resMan;
@@ -53,5 +57,5 @@ public:
 	void stop();
 };
 
-}
+} // End of namespace Sci
 #endif


Commit: f02107f68237b515e79057f820804ac77f4366ef
    https://github.com/scummvm/scummvm/commit/f02107f68237b515e79057f820804ac77f4366ef
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-20T21:02:21-05:00

Commit Message:
SCI: Minor cleanup of kDoSound

Replaces unused kernel calls to use kEmpty, and set correct
signatures for SCI32 kernel calls.

Changed paths:
    engines/sci/engine/kernel.h
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/ksound.cpp
    engines/sci/sound/soundcmd.cpp



diff --git a/engines/sci/engine/kernel.h b/engines/sci/engine/kernel.h
index d88dc75..983bd7a 100644
--- a/engines/sci/engine/kernel.h
+++ b/engines/sci/engine/kernel.h
@@ -578,7 +578,6 @@ reg_t kPlayDuck(EngineState *s, int argc, reg_t *argv);
 
 reg_t kDoSoundInit(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundPlay(EngineState *s, int argc, reg_t *argv);
-reg_t kDoSoundRestore(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundDispose(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundMute(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundStop(EngineState *s, int argc, reg_t *argv);
@@ -593,7 +592,6 @@ reg_t kDoSoundUpdateCues(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundSendMidi(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundGlobalReverb(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundSetHold(EngineState *s, int argc, reg_t *argv);
-reg_t kDoSoundDummy(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundGetAudioCapability(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundSuspend(EngineState *s, int argc, reg_t *argv);
 reg_t kDoSoundSetVolume(EngineState *s, int argc, reg_t *argv);
diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index a61213e..d3a1d43 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -102,7 +102,7 @@ struct SciKernelMapSubEntry {
 static const SciKernelMapSubEntry kDoSound_subops[] = {
 	{ SIG_SOUNDSCI0,       0, MAP_CALL(DoSoundInit),               "o",                    NULL },
 	{ SIG_SOUNDSCI0,       1, MAP_CALL(DoSoundPlay),               "o",                    NULL },
-	{ SIG_SOUNDSCI0,       2, MAP_CALL(DoSoundRestore),            "(o)",                  NULL },
+	{ SIG_SOUNDSCI0,       2, MAP_EMPTY(DoSoundRestore),           "(o)",                  NULL },
 	{ SIG_SOUNDSCI0,       3, MAP_CALL(DoSoundDispose),            "o",                    NULL },
 	{ SIG_SOUNDSCI0,       4, MAP_CALL(DoSoundMute),               "(i)",                  NULL },
 	{ SIG_SOUNDSCI0,       5, MAP_CALL(DoSoundStop),               "o",                    NULL },
@@ -115,7 +115,7 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	{ SIG_SOUNDSCI0,      12, MAP_CALL(DoSoundStopAll),            "",                     NULL },
 	{ SIG_SOUNDSCI1EARLY,  0, MAP_CALL(DoSoundMasterVolume),       NULL,                   NULL },
 	{ SIG_SOUNDSCI1EARLY,  1, MAP_CALL(DoSoundMute),               NULL,                   NULL },
-	{ SIG_SOUNDSCI1EARLY,  2, MAP_CALL(DoSoundRestore),            NULL,                   NULL },
+	{ SIG_SOUNDSCI1EARLY,  2, MAP_EMPTY(DoSoundRestore),           NULL,                   NULL },
 	{ SIG_SOUNDSCI1EARLY,  3, MAP_CALL(DoSoundGetPolyphony),       NULL,                   NULL },
 	{ SIG_SOUNDSCI1EARLY,  4, MAP_CALL(DoSoundUpdate),             NULL,                   NULL },
 	{ SIG_SOUNDSCI1EARLY,  5, MAP_CALL(DoSoundInit),               NULL,                   NULL },
@@ -128,11 +128,11 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	{ SIG_SOUNDSCI1EARLY, 12, MAP_CALL(DoSoundSendMidi),           "oiii",                 NULL },
 	{ SIG_SOUNDSCI1EARLY, 13, MAP_CALL(DoSoundGlobalReverb),       "(i)",                  NULL },
 	{ SIG_SOUNDSCI1EARLY, 14, MAP_CALL(DoSoundSetHold),            "oi",                   NULL },
-	{ SIG_SOUNDSCI1EARLY, 15, MAP_CALL(DoSoundDummy),              "",                     NULL },
+	{ SIG_SOUNDSCI1EARLY, 15, MAP_EMPTY(DoSoundDummy),             "",                     NULL },
 	//  ^^ Longbow demo
 	{ SIG_SOUNDSCI1LATE,   0, MAP_CALL(DoSoundMasterVolume),       NULL,                   NULL },
 	{ SIG_SOUNDSCI1LATE,   1, MAP_CALL(DoSoundMute),               NULL,                   NULL },
-	{ SIG_SOUNDSCI1LATE,   2, MAP_CALL(DoSoundRestore),            "",                     NULL },
+	{ SIG_SOUNDSCI1LATE,   2, MAP_EMPTY(DoSoundRestore),           "",                     NULL },
 	{ SIG_SOUNDSCI1LATE,   3, MAP_CALL(DoSoundGetPolyphony),       NULL,                   NULL },
 	{ SIG_SOUNDSCI1LATE,   4, MAP_CALL(DoSoundGetAudioCapability), "",                     NULL },
 	{ SIG_SOUNDSCI1LATE,   5, MAP_CALL(DoSoundSuspend),            "i",                    NULL },
@@ -143,7 +143,7 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	{ SIG_SOUNDSCI1LATE,  10, MAP_CALL(DoSoundPause),              NULL,                   NULL },
 	{ SIG_SOUNDSCI1LATE,  11, MAP_CALL(DoSoundFade),               "oiiii(i)",             kDoSoundFade_workarounds },
 	{ SIG_SOUNDSCI1LATE,  12, MAP_CALL(DoSoundSetHold),            NULL,                   NULL },
-	{ SIG_SOUNDSCI1LATE,  13, MAP_CALL(DoSoundDummy),              NULL,                   NULL },
+	{ SIG_SOUNDSCI1LATE,  13, MAP_EMPTY(DoSoundDummy),             NULL,                   NULL },
 	{ SIG_SOUNDSCI1LATE,  14, MAP_CALL(DoSoundSetVolume),          "oi",                   NULL },
 	{ SIG_SOUNDSCI1LATE,  15, MAP_CALL(DoSoundSetPriority),        "oi",                   NULL },
 	{ SIG_SOUNDSCI1LATE,  16, MAP_CALL(DoSoundSetLoop),            "oi",                   NULL },
@@ -152,32 +152,32 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	{ SIG_SOUNDSCI1LATE,  19, MAP_CALL(DoSoundGlobalReverb),       NULL,                   NULL },
 	{ SIG_SOUNDSCI1LATE,  20, MAP_CALL(DoSoundUpdate),             NULL,                   NULL },
 #ifdef ENABLE_SCI32
-	{ SIG_SOUNDSCI21,      0, MAP_CALL(DoSoundMasterVolume),       NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      1, MAP_CALL(DoSoundMute),               NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      2, MAP_CALL(DoSoundRestore),            NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      3, MAP_CALL(DoSoundGetPolyphony),       NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      4, MAP_CALL(DoSoundGetAudioCapability), NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      5, MAP_CALL(DoSoundSuspend),            NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      6, MAP_CALL(DoSoundInit),               NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      7, MAP_CALL(DoSoundDispose),            NULL,                   NULL },
-	{ SIG_SOUNDSCI21,      8, MAP_CALL(DoSoundPlay),               "o(i)",                 NULL },
+	{ SIG_SOUNDSCI21,      0, MAP_CALL(DoSoundMasterVolume),       "(i)",                  NULL },
+	{ SIG_SOUNDSCI21,      1, MAP_CALL(DoSoundMute),               "(i)",                  NULL },
+	{ SIG_SOUNDSCI21,      2, MAP_EMPTY(DoSoundRestore),           NULL,                   NULL },
+	{ SIG_SOUNDSCI21,      3, MAP_CALL(DoSoundGetPolyphony),       "",                     NULL },
+	{ SIG_SOUNDSCI21,      4, MAP_CALL(DoSoundGetAudioCapability), "",                     NULL },
+	{ SIG_SOUNDSCI21,      5, MAP_CALL(DoSoundSuspend),            "i",                    NULL },
+	{ SIG_SOUNDSCI21,      6, MAP_CALL(DoSoundInit),               "o",                    NULL },
+	{ SIG_SOUNDSCI21,      7, MAP_CALL(DoSoundDispose),            "o",                    NULL },
+	{ SIG_SOUNDSCI21,      8, MAP_CALL(DoSoundPlay),               "o",                    NULL },
 	// ^^ TODO: if this is really the only change between SCI1LATE AND SCI21, we could rename the
 	//     SIG_SOUNDSCI1LATE #define to SIG_SINCE_SOUNDSCI1LATE and make it being SCI1LATE+. Although
 	//     I guess there are many more changes somewhere
 	// TODO: Quest for Glory 4 (SCI2.1) uses the old scheme, we need to detect it accordingly
 	//        signature for SCI21 should be "o"
-	{ SIG_SOUNDSCI21,      9, MAP_CALL(DoSoundStop),               NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     10, MAP_CALL(DoSoundPause),              NULL,                   NULL },
+	{ SIG_SOUNDSCI21,      9, MAP_CALL(DoSoundStop),               "o",                    NULL },
+	{ SIG_SOUNDSCI21,     10, MAP_CALL(DoSoundPause),              "[o0]i",                NULL },
 	{ SIG_SOUNDSCI21,     11, MAP_CALL(DoSoundFade),               "oiiii",                kDoSoundFade_workarounds },
-	{ SIG_SOUNDSCI21,     12, MAP_CALL(DoSoundSetHold),            NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     13, MAP_CALL(DoSoundDummy),              NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     14, MAP_CALL(DoSoundSetVolume),          NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     15, MAP_CALL(DoSoundSetPriority),        NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     16, MAP_CALL(DoSoundSetLoop),            NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     17, MAP_CALL(DoSoundUpdateCues),         NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     18, MAP_CALL(DoSoundSendMidi),           NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     19, MAP_CALL(DoSoundGlobalReverb),       NULL,                   NULL },
-	{ SIG_SOUNDSCI21,     20, MAP_CALL(DoSoundUpdate),             NULL,                   NULL },
+	{ SIG_SOUNDSCI21,     12, MAP_CALL(DoSoundSetHold),            "oi",                   NULL },
+	{ SIG_SOUNDSCI21,     13, MAP_EMPTY(DoSoundDummy),             NULL,                   NULL },
+	{ SIG_SOUNDSCI21,     14, MAP_CALL(DoSoundSetVolume),          "oi",                   NULL },
+	{ SIG_SOUNDSCI21,     15, MAP_CALL(DoSoundSetPriority),        "oi",                   NULL },
+	{ SIG_SOUNDSCI21,     16, MAP_CALL(DoSoundSetLoop),            "oi",                   NULL },
+	{ SIG_SOUNDSCI21,     17, MAP_CALL(DoSoundUpdateCues),         "o",                    NULL },
+	{ SIG_SOUNDSCI21,     18, MAP_CALL(DoSoundSendMidi),           "oiiii",                NULL },
+	{ SIG_SOUNDSCI21,     19, MAP_CALL(DoSoundGlobalReverb),       "(i)",                  NULL },
+	{ SIG_SOUNDSCI21,     20, MAP_CALL(DoSoundUpdate),             "o",                    NULL },
 #endif
 	SCI_SUBOPENTRY_TERMINATOR
 };
diff --git a/engines/sci/engine/ksound.cpp b/engines/sci/engine/ksound.cpp
index 301e4c1..ed53b8d 100644
--- a/engines/sci/engine/ksound.cpp
+++ b/engines/sci/engine/ksound.cpp
@@ -50,7 +50,6 @@ reg_t kDoSound(EngineState *s, int argc, reg_t *argv) {
 
 CREATE_DOSOUND_FORWARD(DoSoundInit)
 CREATE_DOSOUND_FORWARD(DoSoundPlay)
-CREATE_DOSOUND_FORWARD(DoSoundRestore)
 CREATE_DOSOUND_FORWARD(DoSoundDispose)
 CREATE_DOSOUND_FORWARD(DoSoundMute)
 CREATE_DOSOUND_FORWARD(DoSoundStop)
@@ -65,13 +64,41 @@ CREATE_DOSOUND_FORWARD(DoSoundUpdateCues)
 CREATE_DOSOUND_FORWARD(DoSoundSendMidi)
 CREATE_DOSOUND_FORWARD(DoSoundGlobalReverb)
 CREATE_DOSOUND_FORWARD(DoSoundSetHold)
-CREATE_DOSOUND_FORWARD(DoSoundDummy)
 CREATE_DOSOUND_FORWARD(DoSoundGetAudioCapability)
 CREATE_DOSOUND_FORWARD(DoSoundSuspend)
 CREATE_DOSOUND_FORWARD(DoSoundSetVolume)
 CREATE_DOSOUND_FORWARD(DoSoundSetPriority)
 CREATE_DOSOUND_FORWARD(DoSoundSetLoop)
 
+#ifdef ENABLE_SCI32
+reg_t kDoSoundPhantasmagoriaMac(EngineState *s, int argc, reg_t *argv) {
+	// Phantasmagoria Mac (and seemingly no other game (!)) uses this
+	// cutdown version of kDoSound.
+
+	switch (argv[0].toUint16()) {
+	case 0:
+		return g_sci->_soundCmd->kDoSoundMasterVolume(argc - 1, argv + 1, s->r_acc);
+	case 2:
+		return g_sci->_soundCmd->kDoSoundInit(argc - 1, argv + 1, s->r_acc);
+	case 3:
+		return g_sci->_soundCmd->kDoSoundDispose(argc - 1, argv + 1, s->r_acc);
+	case 4:
+		return g_sci->_soundCmd->kDoSoundPlay(argc - 1, argv + 1, s->r_acc);
+	case 5:
+		return g_sci->_soundCmd->kDoSoundStop(argc - 1, argv + 1, s->r_acc);
+	case 8:
+		return g_sci->_soundCmd->kDoSoundSetVolume(argc - 1, argv + 1, s->r_acc);
+	case 9:
+		return g_sci->_soundCmd->kDoSoundSetLoop(argc - 1, argv + 1, s->r_acc);
+	case 10:
+		return g_sci->_soundCmd->kDoSoundUpdateCues(argc - 1, argv + 1, s->r_acc);
+	}
+
+	error("Unknown kDoSound Phantasmagoria Mac subop %d", argv[0].toUint16());
+	return s->r_acc;
+}
+#endif
+
 reg_t kDoCdAudio(EngineState *s, int argc, reg_t *argv) {
 	switch (argv[0].toUint16()) {
 	case kSciAudioPlay: {
@@ -473,33 +500,6 @@ reg_t kSetLanguage(EngineState *s, int argc, reg_t *argv) {
 	return s->r_acc;
 }
 
-reg_t kDoSoundPhantasmagoriaMac(EngineState *s, int argc, reg_t *argv) {
-	// Phantasmagoria Mac (and seemingly no other game (!)) uses this
-	// cutdown version of kDoSound.
-
-	switch (argv[0].toUint16()) {
-	case 0:
-		return g_sci->_soundCmd->kDoSoundMasterVolume(argc - 1, argv + 1, s->r_acc);
-	case 2:
-		return g_sci->_soundCmd->kDoSoundInit(argc - 1, argv + 1, s->r_acc);
-	case 3:
-		return g_sci->_soundCmd->kDoSoundDispose(argc - 1, argv + 1, s->r_acc);
-	case 4:
-		return g_sci->_soundCmd->kDoSoundPlay(argc - 1, argv + 1, s->r_acc);
-	case 5:
-		return g_sci->_soundCmd->kDoSoundStop(argc - 1, argv + 1, s->r_acc);
-	case 8:
-		return g_sci->_soundCmd->kDoSoundSetVolume(argc - 1, argv + 1, s->r_acc);
-	case 9:
-		return g_sci->_soundCmd->kDoSoundSetLoop(argc - 1, argv + 1, s->r_acc);
-	case 10:
-		return g_sci->_soundCmd->kDoSoundUpdateCues(argc - 1, argv + 1, s->r_acc);
-	}
-
-	error("Unknown kDoSound Phantasmagoria Mac subop %d", argv[0].toUint16());
-	return s->r_acc;
-}
-
 #endif
 
 } // End of namespace Sci
diff --git a/engines/sci/sound/soundcmd.cpp b/engines/sci/sound/soundcmd.cpp
index 7d2ab32..c5d8dda 100644
--- a/engines/sci/sound/soundcmd.cpp
+++ b/engines/sci/sound/soundcmd.cpp
@@ -225,17 +225,6 @@ void SoundCommandParser::processPlaySound(reg_t obj, bool playBed) {
 	musicSlot->fadeStep = 0;
 }
 
-reg_t SoundCommandParser::kDoSoundRestore(int argc, reg_t *argv, reg_t acc) {
-	// Called after loading, to restore the playlist
-	// We don't really use or need this
-	return acc;
-}
-
-reg_t SoundCommandParser::kDoSoundDummy(int argc, reg_t *argv, reg_t acc) {
-	warning("cmdDummy invoked");	// not supposed to occur
-	return acc;
-}
-
 reg_t SoundCommandParser::kDoSoundDispose(int argc, reg_t *argv, reg_t acc) {
 	debugC(kDebugLevelSound, "kDoSound(dispose): %04x:%04x", PRINT_REG(argv[0]));
 	processDisposeSound(argv[0]);


Commit: f939868f3a10c0d8a48e602e4ce330982b7fa198
    https://github.com/scummvm/scummvm/commit/f939868f3a10c0d8a48e602e4ce330982b7fa198
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-20T21:52:41-05:00

Commit Message:
SCI32: Add kDoSound(play) workaround for LSL6hires

Changed paths:
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/workarounds.cpp
    engines/sci/engine/workarounds.h



diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index d3a1d43..15a2049 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -160,7 +160,7 @@ static const SciKernelMapSubEntry kDoSound_subops[] = {
 	{ SIG_SOUNDSCI21,      5, MAP_CALL(DoSoundSuspend),            "i",                    NULL },
 	{ SIG_SOUNDSCI21,      6, MAP_CALL(DoSoundInit),               "o",                    NULL },
 	{ SIG_SOUNDSCI21,      7, MAP_CALL(DoSoundDispose),            "o",                    NULL },
-	{ SIG_SOUNDSCI21,      8, MAP_CALL(DoSoundPlay),               "o",                    NULL },
+	{ SIG_SOUNDSCI21,      8, MAP_CALL(DoSoundPlay),               "o",                    kDoSoundPlay_workarounds },
 	// ^^ TODO: if this is really the only change between SCI1LATE AND SCI21, we could rename the
 	//     SIG_SOUNDSCI1LATE #define to SIG_SINCE_SOUNDSCI1LATE and make it being SCI1LATE+. Although
 	//     I guess there are many more changes somewhere
diff --git a/engines/sci/engine/workarounds.cpp b/engines/sci/engine/workarounds.cpp
index 0cb8ff4..d86ad60 100644
--- a/engines/sci/engine/workarounds.cpp
+++ b/engines/sci/engine/workarounds.cpp
@@ -514,6 +514,12 @@ const SciWorkaroundEntry kDisposeScript_workarounds[] = {
 };
 
 //    gameID,           room,script,lvl,          object-name, method-name, local-call-signature, index,                workaround
+const SciWorkaroundEntry kDoSoundPlay_workarounds[] = {
+	{ GID_LSL6HIRES,    -1,  64989,   0,          NULL,          "play",                    NULL,     0, { WORKAROUND_STILLCALL, 0 } }, // always passes an extra null argument
+	SCI_WORKAROUNDENTRY_TERMINATOR
+};
+
+//    gameID,           room,script,lvl,          object-name, method-name, local-call-signature, index,                workaround
 const SciWorkaroundEntry kDoSoundFade_workarounds[] = {
 	{ GID_KQ5,           213,   989,  0,       "globalSound3", "fade",                      NULL,     0, { WORKAROUND_STILLCALL, 0 } }, // english floppy: when bandits leave the secret temple, parameter 4 is an object - bug #5078
 	{ GID_KQ6,           105,   989,  0,        "globalSound", "fade",                      NULL,     0, { WORKAROUND_STILLCALL, 0 } }, // floppy: during intro, parameter 4 is an object
diff --git a/engines/sci/engine/workarounds.h b/engines/sci/engine/workarounds.h
index 8f519a8..680aa47 100644
--- a/engines/sci/engine/workarounds.h
+++ b/engines/sci/engine/workarounds.h
@@ -74,6 +74,7 @@ extern const SciWorkaroundEntry kDeviceInfo_workarounds[];
 extern const SciWorkaroundEntry kDisplay_workarounds[];
 extern const SciWorkaroundEntry kDirLoop_workarounds[];
 extern const SciWorkaroundEntry kDisposeScript_workarounds[];
+extern const SciWorkaroundEntry kDoSoundPlay_workarounds[];
 extern const SciWorkaroundEntry kDoSoundFade_workarounds[];
 extern const SciWorkaroundEntry kFindKey_workarounds[];
 extern const SciWorkaroundEntry kDeleteKey_workarounds[];


Commit: dcc6234d816f7aab8f7ce0f473a7edf626dd03c6
    https://github.com/scummvm/scummvm/commit/dcc6234d816f7aab8f7ce0f473a7edf626dd03c6
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-21T08:14:11-05:00

Commit Message:
SCI32: Add low-pass filter to 8-bit SOL audio

This improves the perceived quality of audio in games that use
8-bit samples for music, like Torin.

Changed paths:
    engines/sci/sound/decoders/sol.cpp



diff --git a/engines/sci/sound/decoders/sol.cpp b/engines/sci/sound/decoders/sol.cpp
index 280b24f..a899cc4 100644
--- a/engines/sci/sound/decoders/sol.cpp
+++ b/engines/sci/sound/decoders/sol.cpp
@@ -70,13 +70,14 @@ static void deDPCM16(int16 *out, Common::ReadStream &audioStream, uint32 numByte
  * byte.
  */
 static void deDPCM8Nibble(int16 *out, uint8 &sample, uint8 delta) {
+	const uint8 lastSample = sample;
 	if (delta & 8) {
 		sample -= tableDPCM8[delta & 7];
 	} else {
 		sample += tableDPCM8[delta & 7];
 	}
 	sample = CLIP<byte>(sample, 0, 255);
-	*out = (sample << 8) ^ 0x8000;
+	*out = ((lastSample + sample) << 7) ^ 0x8000;
 }
 
 /**


Commit: 97c11e7d5fd1f23efc742abd2cb702cb8ce74361
    https://github.com/scummvm/scummvm/commit/97c11e7d5fd1f23efc742abd2cb702cb8ce74361
Author: Willem Jan Palenstijn (wjp at usecode.org)
Date: 2016-06-21T08:14:11-05:00

Commit Message:
SCI32: Fix getTextWidth on consecutive control codes

Changed paths:
    engines/sci/graphics/text32.cpp



diff --git a/engines/sci/graphics/text32.cpp b/engines/sci/graphics/text32.cpp
index d1c223d..62664c1 100644
--- a/engines/sci/graphics/text32.cpp
+++ b/engines/sci/graphics/text32.cpp
@@ -498,7 +498,7 @@ int16 GfxText32::getTextWidth(const uint index, uint length) const {
 					--length;
 
 					fontId = fontId * 10 + currentChar - '0';
-				} while (length > 0 && currentChar >= '0' && currentChar <= '9');
+				} while (length > 0 && *text >= '0' && *text <= '9');
 
 				if (length > 0) {
 					font = _cache->getFont(fontId);
@@ -506,7 +506,11 @@ int16 GfxText32::getTextWidth(const uint index, uint length) const {
 			}
 
 			// Forward through any more unknown control character data
-			while (length > 0 && currentChar != '|') {
+			while (length > 0 && *text != '|') {
+				++text;
+				--length;
+			}
+			if (length > 0) {
 				++text;
 				--length;
 			}
@@ -514,8 +518,10 @@ int16 GfxText32::getTextWidth(const uint index, uint length) const {
 			width += font->getCharWidth(currentChar);
 		}
 
-		currentChar = *text++;
-		--length;
+		if (length > 0) {
+			currentChar = *text++;
+			--length;
+		}
 	}
 
 	return width;


Commit: c4b41f5d7cfd6506c9d541868af71c5288b36c09
    https://github.com/scummvm/scummvm/commit/c4b41f5d7cfd6506c9d541868af71c5288b36c09
Author: Willem Jan Palenstijn (wjp at usecode.org)
Date: 2016-06-21T08:14:11-05:00

Commit Message:
SCI32: Fix drawText on consecutive control codes

Changed paths:
    engines/sci/graphics/text32.cpp



diff --git a/engines/sci/graphics/text32.cpp b/engines/sci/graphics/text32.cpp
index 62664c1..425c7fb 100644
--- a/engines/sci/graphics/text32.cpp
+++ b/engines/sci/graphics/text32.cpp
@@ -311,6 +311,10 @@ void GfxText32::drawText(const uint index, uint length) {
 				++text;
 				--length;
 			}
+			if (length > 0) {
+				++text;
+				--length;
+			}
 		} else {
 			drawChar(currentChar);
 		}


Commit: 94328f0ec8baa96947c5b82c63bdd298ba44981d
    https://github.com/scummvm/scummvm/commit/94328f0ec8baa96947c5b82c63bdd298ba44981d
Author: Willem Jan Palenstijn (wjp at usecode.org)
Date: 2016-06-21T08:14:12-05:00

Commit Message:
SCI32: Make GfxText32::_scaledWidth/Height statics

They were global in SSCI. This way secondary GfxText32 instances
(such as in ScrollWindow) use the correct scaling.

Changed paths:
    engines/sci/graphics/text32.cpp
    engines/sci/graphics/text32.h



diff --git a/engines/sci/graphics/text32.cpp b/engines/sci/graphics/text32.cpp
index 425c7fb..f38a95d 100644
--- a/engines/sci/graphics/text32.cpp
+++ b/engines/sci/graphics/text32.cpp
@@ -39,18 +39,24 @@
 namespace Sci {
 
 int16 GfxText32::_defaultFontId = 0;
+int16 GfxText32::_scaledWidth = 0;
+int16 GfxText32::_scaledHeight = 0;
 
 GfxText32::GfxText32(SegManager *segMan, GfxCache *fonts) :
 	_segMan(segMan),
 	_cache(fonts),
-	_scaledWidth(g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth),
-	_scaledHeight(g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight),
 	// Not a typo, the original engine did not initialise height, only width
 	_width(0),
 	_text(""),
 	_bitmap(NULL_REG) {
 		_fontId = _defaultFontId;
 		_font = _cache->getFont(_defaultFontId);
+
+		if (_scaledWidth == 0) {
+			// initialize the statics
+			_scaledWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
+			_scaledHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
+		}
 	}
 
 reg_t GfxText32::createFontBitmap(int16 width, int16 height, const Common::Rect &rect, const Common::String &text, const uint8 foreColor, const uint8 backColor, const uint8 skipColor, const GuiResourceId fontId, const TextAlign alignment, const int16 borderColor, const bool dimmed, const bool doScaling) {
diff --git a/engines/sci/graphics/text32.h b/engines/sci/graphics/text32.h
index 20adb3d..905e88b 100644
--- a/engines/sci/graphics/text32.h
+++ b/engines/sci/graphics/text32.h
@@ -351,15 +351,15 @@ public:
 
 	/**
 	 * The size of the x-dimension of the coordinate system
-	 * used by the text renderer.
+	 * used by the text renderer. Static since it was global in SSCI.
 	 */
-	int16 _scaledWidth;
+	static int16 _scaledWidth;
 
 	/**
 	 * The size of the y-dimension of the coordinate system
-	 * used by the text renderer.
+	 * used by the text renderer. Static since it was global in SSCI.
 	 */
-	int16 _scaledHeight;
+	static int16 _scaledHeight;
 
 	/**
 	 * The currently active font resource used to write text


Commit: a613a27b44eae68650eb9150ea146dff9befea28
    https://github.com/scummvm/scummvm/commit/a613a27b44eae68650eb9150ea146dff9befea28
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-21T08:14:12-05:00

Commit Message:
SCI32: Implement line drawing (kAddLine/kUpdateLine/kRemoveLine)

This line drawing code lives in a remodelled GfxPaint32 class
that is totally separate from GfxPaint16.

Changed paths:
  R engines/sci/graphics/paint.cpp
  R engines/sci/graphics/paint.h
    engines/sci/console.cpp
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/kgraphics32.cpp
    engines/sci/engine/kpathing.cpp
    engines/sci/graphics/frameout.cpp
    engines/sci/graphics/frameout.h
    engines/sci/graphics/paint16.h
    engines/sci/graphics/paint32.cpp
    engines/sci/graphics/paint32.h
    engines/sci/graphics/screen_item32.cpp
    engines/sci/graphics/screen_item32.h
    engines/sci/graphics/text32.h
    engines/sci/module.mk
    engines/sci/sci.cpp
    engines/sci/sci.h



diff --git a/engines/sci/console.cpp b/engines/sci/console.cpp
index 51fb52b..27ac4fa 100644
--- a/engines/sci/console.cpp
+++ b/engines/sci/console.cpp
@@ -41,9 +41,7 @@
 #include "sci/graphics/cache.h"
 #include "sci/graphics/cursor.h"
 #include "sci/graphics/screen.h"
-#include "sci/graphics/paint.h"
 #include "sci/graphics/paint16.h"
-#include "sci/graphics/paint32.h"
 #include "sci/graphics/palette.h"
 #include "sci/graphics/ports.h"
 #include "sci/graphics/view.h"
@@ -54,6 +52,7 @@
 #include "sci/video/seq_decoder.h"
 #ifdef ENABLE_SCI32
 #include "sci/graphics/frameout.h"
+#include "sci/graphics/paint32.h"
 #include "video/coktel_decoder.h"
 #include "sci/video/robot_decoder.h"
 #endif
@@ -1648,7 +1647,7 @@ bool Console::cmdDrawPic(int argc, const char **argv) {
 #endif
 
 	uint16 resourceId = atoi(argv[1]);
-	_engine->_gfxPaint->kernelDrawPicture(resourceId, 100, false, false, false, 0);
+	_engine->_gfxPaint16->kernelDrawPicture(resourceId, 100, false, false, false, 0);
 	_engine->_gfxScreen->copyToScreen();
 	_engine->sleep(2000);
 
diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index 15a2049..7b5deda 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -800,9 +800,12 @@ static SciKernelMapEntry s_kernelMap[] = {
 	{ MAP_CALL(SetFontRes),        SIG_SCI21EARLY, SIGFOR_ALL, "ii",                  NULL,            NULL },
 	{ MAP_CALL(Font),              SIG_SINCE_SCI21MID, SIGFOR_ALL, "i(.*)",           kFont_subops,    NULL },
 	{ MAP_CALL(Bitmap),            SIG_EVERYWHERE,           "(.*)",                  kBitmap_subops,  NULL },
-	{ MAP_CALL(AddLine),           SIG_EVERYWHERE,           "oiiiiiiiii",            NULL,            NULL },
-	{ MAP_CALL(UpdateLine),        SIG_EVERYWHERE,           "[r0]oiiiiiiiii",        NULL,            NULL },
-	{ MAP_CALL(DeleteLine),        SIG_EVERYWHERE,           "[r0]o",                 NULL,            NULL },
+	{ MAP_CALL(AddLine),           SIG_EVERYWHERE,           "oiiii(iiiii)",          NULL,            NULL },
+	// The first argument is a ScreenItem instance ID that is created by the
+	// engine, not the VM; as a result, in ScummVM, this argument looks like
+	// an integer and not an object, although it is an object reference.
+	{ MAP_CALL(UpdateLine),        SIG_EVERYWHERE,           "ioiiii(iiiii)",         NULL,            NULL },
+	{ MAP_CALL(DeleteLine),        SIG_EVERYWHERE,           "io",                    NULL,            NULL },
 
 	// SCI2.1 Empty Functions
 
diff --git a/engines/sci/engine/kgraphics32.cpp b/engines/sci/engine/kgraphics32.cpp
index b140062..647f9df 100644
--- a/engines/sci/engine/kgraphics32.cpp
+++ b/engines/sci/engine/kgraphics32.cpp
@@ -52,6 +52,7 @@
 #include "sci/graphics/controls32.h"
 #include "sci/graphics/font.h"	// TODO: remove once kBitmap is moved in a separate class
 #include "sci/graphics/frameout.h"
+#include "sci/graphics/paint32.h"
 #include "sci/graphics/palette32.h"
 #include "sci/graphics/text32.h"
 #endif
@@ -700,46 +701,77 @@ reg_t kEditText(EngineState *s, int argc, reg_t *argv) {
 }
 
 reg_t kAddLine(EngineState *s, int argc, reg_t *argv) {
-	return kStubNull(s, argc, argv); // return 0:0 for now, so that follow up calls won't create signature mismatches
-#if 0
-	reg_t plane = argv[0];
-	Common::Point startPoint(argv[1].toUint16(), argv[2].toUint16());
-	Common::Point endPoint(argv[3].toUint16(), argv[4].toUint16());
-	byte priority = (byte)argv[5].toUint16();
-	byte color = (byte)argv[6].toUint16();
-	byte style = (byte)argv[7].toUint16();	// 0: solid, 1: dashed, 2: pattern
-	byte pattern = (byte)argv[8].toUint16();
-	byte thickness = (byte)argv[9].toUint16();
-//	return g_sci->_gfxFrameout->addPlaneLine(plane, startPoint, endPoint, color, priority, 0);
-	return s->r_acc;
-#endif
+	const reg_t plane = argv[0];
+	const Common::Point startPoint(argv[1].toSint16(), argv[2].toSint16());
+	const Common::Point endPoint(argv[3].toSint16(), argv[4].toSint16());
+
+	int16 priority;
+	uint8 color;
+	LineStyle style;
+	uint16 pattern;
+	uint8 thickness;
+
+	if (argc == 10) {
+		priority = argv[5].toSint16();
+		color = (uint8)argv[6].toUint16();
+		style = (LineStyle)argv[7].toSint16();
+		pattern = argv[8].toUint16();
+		thickness = (uint8)argv[9].toUint16();
+	} else {
+		priority = 1000;
+		color = 255;
+		style = kLineStyleSolid;
+		pattern = 0;
+		thickness = 1;
+	}
+
+	return g_sci->_gfxPaint32->kernelAddLine(plane, startPoint, endPoint, priority, color, style, pattern, thickness);
 }
 
 reg_t kUpdateLine(EngineState *s, int argc, reg_t *argv) {
-	return kStub(s, argc, argv);
+	const reg_t screenItemObject = argv[0];
+	const reg_t planeObject = argv[1];
+	const Common::Point startPoint(argv[2].toSint16(), argv[3].toSint16());
+	const Common::Point endPoint(argv[4].toSint16(), argv[5].toSint16());
+
+	int16 priority;
+	uint8 color;
+	LineStyle style;
+	uint16 pattern;
+	uint8 thickness;
+
+	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(planeObject);
+	if (plane == nullptr) {
+		error("kUpdateLine: Plane %04x:%04x not found", PRINT_REG(planeObject));
+	}
+
+	ScreenItem *screenItem = plane->_screenItemList.findByObject(screenItemObject);
+	if (screenItem == nullptr) {
+		error("kUpdateLine: Screen item %04x:%04x not found", PRINT_REG(screenItemObject));
+	}
+
+	if (argc == 11) {
+		priority = argv[6].toSint16();
+		color = (uint8)argv[7].toUint16();
+		style = (LineStyle)argv[8].toSint16();
+		pattern = argv[9].toUint16();
+		thickness = (uint8)argv[10].toUint16();
+	} else {
+		priority = screenItem->_priority;
+		color = screenItem->_celInfo.color;
+		style = kLineStyleSolid;
+		pattern = 0;
+		thickness = 1;
+	}
+
+	g_sci->_gfxPaint32->kernelUpdateLine(screenItem, plane, startPoint, endPoint, priority, color, style, pattern, thickness);
 
-#if 0
-	reg_t hunkId = argv[0];
-	reg_t plane = argv[1];
-	Common::Point startPoint(argv[2].toUint16(), argv[3].toUint16());
-	Common::Point endPoint(argv[4].toUint16(), argv[5].toUint16());
-	// argv[6] is unknown (a number, usually 200)
-	byte color = (byte)argv[7].toUint16();
-	byte priority = (byte)argv[8].toUint16();
-	byte control = (byte)argv[9].toUint16();
-	// argv[10] is unknown (usually a small number, 1 or 2). Thickness, perhaps?
-//	g_sci->_gfxFrameout->updatePlaneLine(plane, hunkId, startPoint, endPoint, color, priority, control);
 	return s->r_acc;
-#endif
 }
+
 reg_t kDeleteLine(EngineState *s, int argc, reg_t *argv) {
-	return kStub(s, argc, argv);
-#if 0
-	reg_t hunkId = argv[0];
-	reg_t plane = argv[1];
-//	g_sci->_gfxFrameout->deletePlaneLine(plane, hunkId);
+	g_sci->_gfxPaint32->kernelDeleteLine(argv[0], argv[1]);
 	return s->r_acc;
-#endif
 }
 
 reg_t kSetScroll(EngineState *s, int argc, reg_t *argv) {
diff --git a/engines/sci/engine/kpathing.cpp b/engines/sci/engine/kpathing.cpp
index 7ac744f..06f1629 100644
--- a/engines/sci/engine/kpathing.cpp
+++ b/engines/sci/engine/kpathing.cpp
@@ -326,7 +326,7 @@ static void draw_line(EngineState *s, Common::Point p1, Common::Point p2, int ty
 	p2.y = CLIP<int16>(p2.y, 0, height - 1);
 
 	assert(type >= 0 && type <= 3);
-	g_sci->_gfxPaint->kernelGraphDrawLine(p1, p2, poly_colors[type], 255, 255);
+	g_sci->_gfxPaint16->kernelGraphDrawLine(p1, p2, poly_colors[type], 255, 255);
 }
 
 static void draw_point(EngineState *s, Common::Point p, int start, int width, int height) {
diff --git a/engines/sci/graphics/frameout.cpp b/engines/sci/graphics/frameout.cpp
index 2d44e38..51a666e 100644
--- a/engines/sci/graphics/frameout.cpp
+++ b/engines/sci/graphics/frameout.cpp
@@ -259,6 +259,22 @@ void GfxFrameout::syncWithScripts(bool addElements) {
 #pragma mark -
 #pragma mark Screen items
 
+void GfxFrameout::deleteScreenItem(ScreenItem *screenItem, Plane *plane) {
+	if (screenItem->_created == 0) {
+		screenItem->_created = 0;
+		screenItem->_updated = 0;
+		screenItem->_deleted = getScreenCount();
+	} else {
+		plane->_screenItemList.erase(screenItem);
+		plane->_screenItemList.pack();
+	}
+}
+
+void GfxFrameout::deleteScreenItem(ScreenItem *screenItem, const reg_t planeObject) {
+	Plane *plane = _planes.findByObject(planeObject);
+	deleteScreenItem(screenItem, plane);
+}
+
 void GfxFrameout::kernelAddScreenItem(const reg_t object) {
 	// The "fred" object is used to test graphics performance;
 	// it is impacted by framerate throttling, so disable the
@@ -335,14 +351,7 @@ void GfxFrameout::kernelDeleteScreenItem(const reg_t object) {
 		return;
 	}
 
-	if (screenItem->_created == 0) {
-		screenItem->_created = 0;
-		screenItem->_updated = 0;
-		screenItem->_deleted = getScreenCount();
-	} else {
-		plane->_screenItemList.erase(screenItem);
-		plane->_screenItemList.pack();
-	}
+	deleteScreenItem(screenItem, plane);
 }
 
 #pragma mark -
diff --git a/engines/sci/graphics/frameout.h b/engines/sci/graphics/frameout.h
index 0f06ee1..4ebb754 100644
--- a/engines/sci/graphics/frameout.h
+++ b/engines/sci/graphics/frameout.h
@@ -197,10 +197,19 @@ private:
 #pragma mark -
 #pragma mark Screen items
 private:
-	void deleteScreenItem(ScreenItem *screenItem, const reg_t plane);
 	void remapMarkRedraw();
 
 public:
+	/**
+	 * Deletes a screen item from the given plane.
+	 */
+	void deleteScreenItem(ScreenItem *screenItem, Plane *plane);
+
+	/**
+	 * Deletes a screen item from the given plane.
+	 */
+	void deleteScreenItem(ScreenItem *screenItem, const reg_t plane);
+
 	void kernelAddScreenItem(const reg_t object);
 	void kernelUpdateScreenItem(const reg_t object);
 	void kernelDeleteScreenItem(const reg_t object);
diff --git a/engines/sci/graphics/paint.cpp b/engines/sci/graphics/paint.cpp
deleted file mode 100644
index 482b81a..0000000
--- a/engines/sci/graphics/paint.cpp
+++ /dev/null
@@ -1,44 +0,0 @@
-/* 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.
- *
- */
-
-#include "graphics/primitives.h"
-
-#include "sci/sci.h"
-#include "sci/engine/state.h"
-#include "sci/engine/selector.h"
-#include "sci/graphics/paint.h"
-
-namespace Sci {
-
-GfxPaint::GfxPaint() {
-}
-
-GfxPaint::~GfxPaint() {
-}
-
-void GfxPaint::kernelDrawPicture(GuiResourceId pictureId, int16 animationNr, bool animationBlackoutFlag, bool mirroredFlag, bool addToFlag, int16 EGApaletteNo) {
-}
-
-void GfxPaint::kernelGraphDrawLine(Common::Point startPoint, Common::Point endPoint, int16 color, int16 priority, int16 control) {
-}
-
-} // End of namespace Sci
diff --git a/engines/sci/graphics/paint.h b/engines/sci/graphics/paint.h
deleted file mode 100644
index b227713..0000000
--- a/engines/sci/graphics/paint.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/* 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.
- *
- */
-
-#ifndef SCI_GRAPHICS_PAINT_H
-#define SCI_GRAPHICS_PAINT_H
-
-namespace Sci {
-
-class GfxPaint {
-public:
-	GfxPaint();
-	virtual ~GfxPaint();
-
-	virtual void kernelDrawPicture(GuiResourceId pictureId, int16 animationNr, bool animationBlackoutFlag, bool mirroredFlag, bool addToFlag, int16 EGApaletteNo);
-	virtual void kernelGraphDrawLine(Common::Point startPoint, Common::Point endPoint, int16 color, int16 priority, int16 control);
-};
-
-} // End of namespace Sci
-
-#endif
diff --git a/engines/sci/graphics/paint16.h b/engines/sci/graphics/paint16.h
index 955cfde..317388b 100644
--- a/engines/sci/graphics/paint16.h
+++ b/engines/sci/graphics/paint16.h
@@ -23,8 +23,6 @@
 #ifndef SCI_GRAPHICS_PAINT16_H
 #define SCI_GRAPHICS_PAINT16_H
 
-#include "sci/graphics/paint.h"
-
 namespace Sci {
 
 class GfxPorts;
@@ -36,7 +34,7 @@ class GfxView;
 /**
  * Paint16 class, handles painting/drawing for SCI16 (SCI0-SCI1.1) games
  */
-class GfxPaint16 : public GfxPaint {
+class GfxPaint16 {
 public:
 	GfxPaint16(ResourceManager *resMan, SegManager *segMan, GfxCache *cache, GfxPorts *ports, GfxCoordAdjuster *coordAdjuster, GfxScreen *screen, GfxPalette *palette, GfxTransitions *transitions, AudioPlayer *audio);
 	~GfxPaint16();
diff --git a/engines/sci/graphics/paint32.cpp b/engines/sci/graphics/paint32.cpp
index a210a46..bfd4648 100644
--- a/engines/sci/graphics/paint32.cpp
+++ b/engines/sci/graphics/paint32.cpp
@@ -20,49 +20,162 @@
  *
  */
 
-#include "sci/sci.h"
-#include "sci/engine/state.h"
-#include "sci/engine/selector.h"
-#include "sci/graphics/coordadjuster.h"
-#include "sci/graphics/cache.h"
+#include "graphics/primitives.h"
+#include "sci/engine/seg_manager.h"
 #include "sci/graphics/paint32.h"
-#include "sci/graphics/font.h"
-#include "sci/graphics/picture.h"
-#include "sci/graphics/view.h"
-#include "sci/graphics/screen.h"
-#include "sci/graphics/palette.h"
+#include "sci/graphics/text32.h"
 
 namespace Sci {
 
-GfxPaint32::GfxPaint32(ResourceManager *resMan, GfxCoordAdjuster *coordAdjuster, GfxScreen *screen, GfxPalette *palette)
-	: _resMan(resMan), _coordAdjuster(coordAdjuster), _screen(screen), _palette(palette) {
+GfxPaint32::GfxPaint32(SegManager *segMan) :
+	_segMan(segMan) {}
+
+reg_t GfxPaint32::kernelAddLine(const reg_t planeObject, const Common::Point &startPoint, const Common::Point &endPoint, const int16 priority, const uint8 color, const LineStyle style, const uint16 pattern, const uint8 thickness) {
+	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(planeObject);
+	if (plane == nullptr) {
+		error("kAddLine: Plane %04x:%04x not found", PRINT_REG(planeObject));
+	}
+
+	Common::Rect gameRect;
+	BitmapResource bitmap = makeLineBitmap(startPoint, endPoint, priority, color, style, pattern, thickness, gameRect);
+
+	CelInfo32 celInfo;
+	celInfo.type = kCelTypeMem;
+	celInfo.bitmap = bitmap.getObject();
+	// SSCI stores the line color on `celInfo`, even though
+	// this is not a `kCelTypeColor`, as a hack so that
+	// `kUpdateLine` can get the originally used color
+	celInfo.color = color;
+
+	ScreenItem *screenItem = new ScreenItem(planeObject, celInfo, Common::Rect(startPoint.x, startPoint.y, startPoint.x + bitmap.getWidth(), startPoint.y + bitmap.getHeight()));
+	screenItem->_priority = priority;
+	screenItem->_fixedPriority = true;
+
+	plane->_screenItemList.add(screenItem);
+
+	return screenItem->_object;
+}
+
+void GfxPaint32::kernelUpdateLine(ScreenItem *screenItem, Plane *plane, const Common::Point &startPoint, const Common::Point &endPoint, const int16 priority, const uint8 color, const LineStyle style, const uint16 pattern, const uint8 thickness) {
+
+	Common::Rect gameRect;
+	BitmapResource bitmap = makeLineBitmap(startPoint, endPoint, priority, color, style, pattern, thickness, gameRect);
+
+	_segMan->freeHunkEntry(screenItem->_celInfo.bitmap);
+	screenItem->_celInfo.bitmap = bitmap.getObject();
+	screenItem->_celInfo.color = color;
+	screenItem->_position = startPoint;
+	screenItem->_priority = priority;
+	screenItem->update();
 }
 
-GfxPaint32::~GfxPaint32() {
+void GfxPaint32::kernelDeleteLine(const reg_t screenItemObject, const reg_t planeObject) {
+	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(planeObject);
+	if (plane == nullptr) {
+		return;
+	}
+
+	ScreenItem *screenItem = plane->_screenItemList.findByObject(screenItemObject);
+	if (screenItem == nullptr) {
+		return;
+	}
+
+	_segMan->freeHunkEntry(screenItem->_celInfo.bitmap);
+	g_sci->_gfxFrameout->deleteScreenItem(screenItem, plane);
 }
 
-void GfxPaint32::fillRect(Common::Rect rect, byte color) {
-	int16 y, x;
-	Common::Rect clipRect = rect;
+void GfxPaint32::plotter(int x, int y, int color, void *data) {
+	LineProperties &properties = *static_cast<LineProperties *>(data);
+	byte *pixels = properties.bitmap->getPixels();
+
+	const uint32 index = properties.bitmap->getWidth() * y + x;
+
+	if (index < properties.bitmap->getDataSize()) {
+		if (properties.solid) {
+			pixels[index] = (uint8)color;
+			return;
+		}
+
+		if (properties.horizontal && x != properties.lastAddress) {
+			properties.lastAddress = x;
+			++properties.patternIndex;
+		} else if (!properties.horizontal && y != properties.lastAddress) {
+			properties.lastAddress = y;
+			++properties.patternIndex;
+		}
 
-	clipRect.clip(_screen->getWidth(), _screen->getHeight());
+		if (properties.pattern[properties.patternIndex]) {
+			pixels[index] = (uint8)color;
+		}
 
-	for (y = clipRect.top; y < clipRect.bottom; y++) {
-		for (x = clipRect.left; x < clipRect.right; x++) {
-			_screen->putPixel(x, y, GFX_SCREEN_MASK_VISUAL, color, 0, 0);
+		if (properties.patternIndex == ARRAYSIZE(properties.pattern)) {
+			properties.patternIndex = 0;
 		}
+	} else {
+		warning("GfxPaint32::plotter: Attempted to write out of bounds (%u >= %u)", index, properties.bitmap->getDataSize());
 	}
 }
 
-void GfxPaint32::kernelDrawPicture(GuiResourceId pictureId, int16 animationNr, bool animationBlackoutFlag, bool mirroredFlag, bool addToFlag, int16 EGApaletteNo) {
-	GfxPicture *picture = new GfxPicture(_resMan, _coordAdjuster, 0, _screen, _palette, pictureId, false);
+BitmapResource GfxPaint32::makeLineBitmap(const Common::Point &startPoint, const Common::Point &endPoint, const int16 priority, const uint8 color, const LineStyle style, uint16 pattern, uint8 thickness, Common::Rect &outRect) {
+	const uint8 skipColor = color != 250 ? 250 : 0;
 
-	picture->draw(animationNr, mirroredFlag, addToFlag, EGApaletteNo);
-	delete picture;
-}
+	// Thickness is expected to be 2n+1
+	thickness = ((MAX((uint8)1, thickness) - 1) | 1);
+	const uint8 halfThickness = thickness >> 1;
+
+	outRect.left = (startPoint.x < endPoint.x ? startPoint.x : endPoint.x) - halfThickness;
+	outRect.top = (startPoint.y < endPoint.y ? startPoint.y : endPoint.y) - halfThickness;
+	outRect.right = (startPoint.x > endPoint.x ? startPoint.x : endPoint.x) + halfThickness + 1;
+	outRect.bottom = (startPoint.y > endPoint.y ? startPoint.y : endPoint.y) + halfThickness + 1;
+
+	BitmapResource bitmap(_segMan, outRect.width(), outRect.height(), skipColor, 0, 0, g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth, g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight, 0, false);
+
+	byte *pixels = bitmap.getPixels();
+	memset(pixels, skipColor, bitmap.getWidth() * bitmap.getHeight());
+
+	LineProperties properties;
+	properties.bitmap = &bitmap;
+
+	switch (style) {
+	case kLineStyleSolid:
+		pattern = 0xFFFF;
+		properties.solid = true;
+		break;
+	case kLineStyleDashed:
+		pattern = 0xFF00;
+		properties.solid = false;
+		break;
+	case kLineStylePattern:
+		properties.solid = pattern == 0xFFFF;
+		break;
+	}
+
+	const Common::Rect drawRect(
+		startPoint.x - outRect.left,
+		startPoint.y - outRect.top,
+		endPoint.x - outRect.left,
+		endPoint.y - outRect.top
+	);
 
-void GfxPaint32::kernelGraphDrawLine(Common::Point startPoint, Common::Point endPoint, int16 color, int16 priority, int16 control) {
-	_screen->drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y, color, priority, control);
+	if (!properties.solid) {
+		for (int i = 0; i < ARRAYSIZE(properties.pattern); ++i) {
+			properties.pattern[i] = (pattern & 0x8000);
+			pattern <<= 1;
+		}
+
+		properties.patternIndex = 0;
+		properties.horizontal = ABS(drawRect.right - drawRect.left) > ABS(drawRect.bottom - drawRect.top);
+		properties.lastAddress = properties.horizontal ? drawRect.left : drawRect.top;
+	}
+
+	if (thickness <= 1) {
+		Graphics::drawLine(drawRect.left, drawRect.top, drawRect.right, drawRect.bottom, color, plotter, &properties);
+	} else {
+		Graphics::drawThickLine2(drawRect.left, drawRect.top, drawRect.right, drawRect.bottom, thickness, color, plotter, &properties);
+	}
+
+	return bitmap;
 }
 
+
 } // End of namespace Sci
diff --git a/engines/sci/graphics/paint32.h b/engines/sci/graphics/paint32.h
index e7a3ec2..6d5a957 100644
--- a/engines/sci/graphics/paint32.h
+++ b/engines/sci/graphics/paint32.h
@@ -23,30 +23,48 @@
 #ifndef SCI_GRAPHICS_PAINT32_H
 #define SCI_GRAPHICS_PAINT32_H
 
-#include "sci/graphics/paint.h"
-
 namespace Sci {
+class BitmapResource;
+class Plane;
+class ScreenItem;
+class SegManager;
 
-class GfxPorts;
+enum LineStyle {
+	kLineStyleSolid,
+	kLineStyleDashed,
+	kLineStylePattern
+};
 
 /**
  * Paint32 class, handles painting/drawing for SCI32 (SCI2+) games
  */
-class GfxPaint32 : public GfxPaint {
+class GfxPaint32 {
 public:
-	GfxPaint32(ResourceManager *resMan, GfxCoordAdjuster *coordAdjuster, GfxScreen *screen, GfxPalette *palette);
-	~GfxPaint32();
+	GfxPaint32(SegManager *segMan);
 
-	void fillRect(Common::Rect rect, byte color);
+private:
+	SegManager *_segMan;
 
-	void kernelDrawPicture(GuiResourceId pictureId, int16 animationNr, bool animationBlackoutFlag, bool mirroredFlag, bool addToFlag, int16 EGApaletteNo);
-	void kernelGraphDrawLine(Common::Point startPoint, Common::Point endPoint, int16 color, int16 priority, int16 control);
+#pragma mark -
+#pragma mark Line drawing
+public:
+	reg_t kernelAddLine(const reg_t planeObject, const Common::Point &startPoint, const Common::Point &endPoint, const int16 priority, const uint8 color, const LineStyle style, const uint16 pattern, const uint8 thickness);
+	void kernelUpdateLine(ScreenItem *screenItem, Plane *plane, const Common::Point &startPoint, const Common::Point &endPoint, const int16 priority, const uint8 color, const LineStyle style, const uint16 pattern, const uint8 thickness);
+	void kernelDeleteLine(const reg_t screenItemObject, const reg_t planeObject);
 
 private:
-	ResourceManager *_resMan;
-	GfxCoordAdjuster *_coordAdjuster;
-	GfxScreen *_screen;
-	GfxPalette *_palette;
+	typedef struct {
+		BitmapResource *bitmap;
+		bool pattern[16];
+		uint8 patternIndex;
+		bool solid;
+		bool horizontal;
+		int lastAddress;
+	} LineProperties;
+
+	static void plotter(int x, int y, int color, void *data);
+
+	BitmapResource makeLineBitmap(const Common::Point &startPoint, const Common::Point &endPoint, const int16 priority, const uint8 color, const LineStyle style, const uint16 pattern, const uint8 thickness, Common::Rect &outRect);
 };
 
 } // End of namespace Sci
diff --git a/engines/sci/graphics/screen_item32.cpp b/engines/sci/graphics/screen_item32.cpp
index fba0fa0..a3728d9 100644
--- a/engines/sci/graphics/screen_item32.cpp
+++ b/engines/sci/graphics/screen_item32.cpp
@@ -515,6 +515,18 @@ void ScreenItem::update(const reg_t object) {
 	_deleted = 0;
 }
 
+void ScreenItem::update() {
+        // TODO: error out if we're not contained in our plane's ScreenItemList
+
+        if (!_created) {
+                _updated = g_sci->_gfxFrameout->getScreenCount();
+        }
+        _deleted = 0;
+
+        delete _celObj;
+        _celObj = nullptr;
+}
+
 // TODO: This code is quite similar to calcRects, so try to deduplicate
 // if possible
 Common::Rect ScreenItem::getNowSeenRect(const Plane &plane) const {
diff --git a/engines/sci/graphics/screen_item32.h b/engines/sci/graphics/screen_item32.h
index 91f54b4..eef2f48 100644
--- a/engines/sci/graphics/screen_item32.h
+++ b/engines/sci/graphics/screen_item32.h
@@ -261,6 +261,12 @@ public:
 	void update(const reg_t object);
 
 	/**
+	 * Updates the properties of the screen item for one not belonging
+	 * to a VM object. Originally GraphicsMgr::UpdateScreenItem.
+	 */
+	void update();
+
+	/**
 	 * Gets the "now seen" rect for the screen item, which
 	 * represents the current size and position of the
 	 * screen item on the screen in script coordinates.
diff --git a/engines/sci/graphics/text32.h b/engines/sci/graphics/text32.h
index 905e88b..c286298 100644
--- a/engines/sci/graphics/text32.h
+++ b/engines/sci/graphics/text32.h
@@ -23,6 +23,7 @@
 #ifndef SCI_GRAPHICS_TEXT32_H
 #define SCI_GRAPHICS_TEXT32_H
 
+#include "sci/engine/state.h"
 #include "sci/graphics/celobj32.h"
 #include "sci/graphics/frameout.h"
 
diff --git a/engines/sci/module.mk b/engines/sci/module.mk
index 69ddf00..ba6c853 100644
--- a/engines/sci/module.mk
+++ b/engines/sci/module.mk
@@ -51,7 +51,6 @@ MODULE_OBJS := \
 	graphics/fontsjis.o \
 	graphics/maciconbar.o \
 	graphics/menu.o \
-	graphics/paint.o \
 	graphics/paint16.o \
 	graphics/palette.o \
 	graphics/picture.o \
diff --git a/engines/sci/sci.cpp b/engines/sci/sci.cpp
index 6244c43..b74dc46 100644
--- a/engines/sci/sci.cpp
+++ b/engines/sci/sci.cpp
@@ -167,6 +167,7 @@ SciEngine::~SciEngine() {
 	// and will be destroyed when _gfxPalette16 is
 	// destroyed
 	delete _gfxControls32;
+	delete _gfxPaint32;
 	delete _gfxText32;
 	delete _robotDecoder;
 	delete _gfxFrameout;
@@ -177,7 +178,7 @@ SciEngine::~SciEngine() {
 	delete _gfxControls16;
 	delete _gfxText16;
 	delete _gfxAnimate;
-	delete _gfxPaint;
+	delete _gfxPaint16;
 	delete _gfxTransitions;
 	delete _gfxCompare;
 	delete _gfxCoordAdjuster;
@@ -675,7 +676,6 @@ void SciEngine::initGraphics() {
 	_gfxCursor = 0;
 	_gfxMacIconBar = 0;
 	_gfxMenu = 0;
-	_gfxPaint = 0;
 	_gfxPaint16 = 0;
 	_gfxPalette16 = 0;
 	_gfxRemap16 = 0;
@@ -718,8 +718,7 @@ void SciEngine::initGraphics() {
 		_gfxCoordAdjuster = new GfxCoordAdjuster32(_gamestate->_segMan);
 		_gfxCursor->init(_gfxCoordAdjuster, _eventMan);
 		_gfxCompare = new GfxCompare(_gamestate->_segMan, _gfxCache, _gfxScreen, _gfxCoordAdjuster);
-		_gfxPaint32 = new GfxPaint32(_resMan, _gfxCoordAdjuster, _gfxScreen, _gfxPalette32);
-		_gfxPaint = _gfxPaint32;
+		_gfxPaint32 = new GfxPaint32(_gamestate->_segMan);
 		_robotDecoder = new RobotDecoder(getPlatform() == Common::kPlatformMacintosh);
 		_gfxFrameout = new GfxFrameout(_gamestate->_segMan, _resMan, _gfxCoordAdjuster, _gfxCache, _gfxScreen, _gfxPalette32, _gfxPaint32);
 		_gfxText32 = new GfxText32(_gamestate->_segMan, _gfxCache);
@@ -734,7 +733,6 @@ void SciEngine::initGraphics() {
 		_gfxCompare = new GfxCompare(_gamestate->_segMan, _gfxCache, _gfxScreen, _gfxCoordAdjuster);
 		_gfxTransitions = new GfxTransitions(_gfxScreen, _gfxPalette16);
 		_gfxPaint16 = new GfxPaint16(_resMan, _gamestate->_segMan, _gfxCache, _gfxPorts, _gfxCoordAdjuster, _gfxScreen, _gfxPalette16, _gfxTransitions, _audio);
-		_gfxPaint = _gfxPaint16;
 		_gfxAnimate = new GfxAnimate(_gamestate, _scriptPatcher, _gfxCache, _gfxPorts, _gfxPaint16, _gfxScreen, _gfxPalette16, _gfxCursor, _gfxTransitions);
 		_gfxText16 = new GfxText16(_gfxCache, _gfxPorts, _gfxPaint16, _gfxScreen);
 		_gfxControls16 = new GfxControls16(_gamestate->_segMan, _gfxPorts, _gfxPaint16, _gfxText16, _gfxScreen);
diff --git a/engines/sci/sci.h b/engines/sci/sci.h
index 56ee2f4..cc26db3 100644
--- a/engines/sci/sci.h
+++ b/engines/sci/sci.h
@@ -67,7 +67,6 @@ class GfxCoordAdjuster;
 class GfxCursor;
 class GfxMacIconBar;
 class GfxMenu;
-class GfxPaint;
 class GfxPaint16;
 class GfxPaint32;
 class GfxPalette;
@@ -358,7 +357,6 @@ public:
 	GfxPalette32 *_gfxPalette32; // Palette for 32-bit gfx
 	GfxRemap *_gfxRemap16;	// Remapping for the QFG4 demo
 	GfxRemap32 *_gfxRemap32; // Remapping for 32-bit gfx
-	GfxPaint *_gfxPaint;
 	GfxPaint16 *_gfxPaint16; // Painting in 16-bit gfx
 	GfxPaint32 *_gfxPaint32; // Painting in 32-bit gfx
 	GfxPorts *_gfxPorts; // Port managment for 16-bit gfx


Commit: ab864ba3666b39586c54467137a2b26d246ba430
    https://github.com/scummvm/scummvm/commit/ab864ba3666b39586c54467137a2b26d246ba430
Author: Willem Jan Palenstijn (wjp at usecode.org)
Date: 2016-06-21T08:14:12-05:00

Commit Message:
SCI32: Implement kScrollWindow

These should be all the actually used subfunctions.

Co-authored-by: Colin Snover <github.com at zetafleet.com>

Changed paths:
    engines/sci/engine/gc.cpp
    engines/sci/engine/kernel.h
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/kgraphics32.cpp
    engines/sci/graphics/controls32.cpp
    engines/sci/graphics/controls32.h
    engines/sci/graphics/screen_item32.cpp
    engines/sci/graphics/text32.cpp
    engines/sci/graphics/text32.h
    engines/sci/sci.cpp



diff --git a/engines/sci/engine/gc.cpp b/engines/sci/engine/gc.cpp
index 70c8c52..b229490 100644
--- a/engines/sci/engine/gc.cpp
+++ b/engines/sci/engine/gc.cpp
@@ -24,6 +24,10 @@
 #include "common/array.h"
 #include "sci/graphics/ports.h"
 
+#ifdef ENABLE_SCI32
+#include "sci/graphics/controls32.h"
+#endif
+
 namespace Sci {
 
 //#define GC_DEBUG_CODE
@@ -150,6 +154,12 @@ AddrSet *findAllActiveReferences(EngineState *s) {
 		}
 	}
 
+#ifdef ENABLE_SCI32
+	// Init: ScrollWindows
+	if (g_sci->_gfxControls32)
+		wm.pushArray(g_sci->_gfxControls32->listObjectReferences());
+#endif
+
 	debugC(kDebugLevelGC, "[GC] -- Finished explicitly loaded scripts, done with root set");
 
 	processWorkList(s->_segMan, wm, heap);
diff --git a/engines/sci/engine/kernel.h b/engines/sci/engine/kernel.h
index 983bd7a..6604387 100644
--- a/engines/sci/engine/kernel.h
+++ b/engines/sci/engine/kernel.h
@@ -467,7 +467,16 @@ reg_t kStringTrnExclude(EngineState *s, int argc, reg_t *argv);
 
 reg_t kScrollWindowCreate(EngineState *s, int argc, reg_t *argv);
 reg_t kScrollWindowAdd(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowPageUp(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowPageDown(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowUpArrow(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowDownArrow(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowHome(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowEnd(EngineState *s, int argc, reg_t *argv);
 reg_t kScrollWindowWhere(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowGo(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowModify(EngineState *s, int argc, reg_t *argv);
+reg_t kScrollWindowHide(EngineState *s, int argc, reg_t *argv);
 reg_t kScrollWindowShow(EngineState *s, int argc, reg_t *argv);
 reg_t kScrollWindowDestroy(EngineState *s, int argc, reg_t *argv);
 
diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index 7b5deda..82184c2 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -468,25 +468,30 @@ static const SciKernelMapSubEntry kString_subops[] = {
 //    version,         subId, function-mapping,                    signature,              workarounds
 static const SciKernelMapSubEntry kScrollWindow_subops[] = {
 	{ SIG_SCI32,           0, MAP_CALL(ScrollWindowCreate),        "oi",                   NULL },
-	{ SIG_SCI32,           1, MAP_CALL(ScrollWindowAdd),           "o.ii.(.)",             NULL },
-	{ SIG_SCI32,           2, MAP_DUMMY(ScrollWindowClear),        "o",                    NULL },
-	{ SIG_SCI32,           3, MAP_DUMMY(ScrollWindowPageUp),       "o",                    NULL },
-	{ SIG_SCI32,           4, MAP_DUMMY(ScrollWindowPageDown),     "o",                    NULL },
-	{ SIG_SCI32,           5, MAP_DUMMY(ScrollWindowUpArrow),      "o",                    NULL },
-	{ SIG_SCI32,           6, MAP_DUMMY(ScrollWindowDownArrow),    "o",                    NULL },
-	{ SIG_SCI32,           7, MAP_DUMMY(ScrollWindowHome),         "o",                    NULL },
-	{ SIG_SCI32,           8, MAP_DUMMY(ScrollWindowEnd),          "o",                    NULL },
-	{ SIG_SCI32,           9, MAP_DUMMY(ScrollWindowResize),       "o.",                   NULL },
-	{ SIG_SCI32,          10, MAP_CALL(ScrollWindowWhere),         "oi",                   NULL },
-	{ SIG_SCI32,          11, MAP_DUMMY(ScrollWindowGo),           "o..",                  NULL },
-	{ SIG_SCI32,          12, MAP_DUMMY(ScrollWindowInsert),       "o.....",               NULL },
-	{ SIG_SCI32,          13, MAP_DUMMY(ScrollWindowDelete),       "o.",                   NULL },
-	{ SIG_SCI32,          14, MAP_DUMMY(ScrollWindowModify),       "o.....(.)",            NULL },
-	{ SIG_SCI32,          15, MAP_DUMMY(ScrollWindowHide),         "o",                    NULL },
-	{ SIG_SCI32,          16, MAP_CALL(ScrollWindowShow),          "o",                    NULL },
-	{ SIG_SCI32,          17, MAP_CALL(ScrollWindowDestroy),       "o",                    NULL },
-	{ SIG_SCI32,          18, MAP_DUMMY(ScrollWindowText),         "o",                    NULL },
-	{ SIG_SCI32,          19, MAP_DUMMY(ScrollWindowReconstruct),  "o.",                   NULL },
+	{ SIG_SCI32,           1, MAP_CALL(ScrollWindowAdd),           "iriii(i)",             NULL },
+	{ SIG_SCI32,           2, MAP_DUMMY(ScrollWindowClear),        "i",                    NULL },
+	{ SIG_SCI32,           3, MAP_CALL(ScrollWindowPageUp),        "i",                    NULL },
+	{ SIG_SCI32,           4, MAP_CALL(ScrollWindowPageDown),      "i",                    NULL },
+	{ SIG_SCI32,           5, MAP_CALL(ScrollWindowUpArrow),       "i",                    NULL },
+	{ SIG_SCI32,           6, MAP_CALL(ScrollWindowDownArrow),     "i",                    NULL },
+	{ SIG_SCI32,           7, MAP_CALL(ScrollWindowHome),          "i",                    NULL },
+	{ SIG_SCI32,           8, MAP_CALL(ScrollWindowEnd),           "i",                    NULL },
+	{ SIG_SCI32,           9, MAP_DUMMY(ScrollWindowResize),       "i.",                   NULL },
+	{ SIG_SCI32,          10, MAP_CALL(ScrollWindowWhere),         "ii",                   NULL },
+	{ SIG_SCI32,          11, MAP_CALL(ScrollWindowGo),            "i..",                  NULL },
+	{ SIG_SCI32,          12, MAP_DUMMY(ScrollWindowInsert),       "i.....",               NULL },
+	{ SIG_SCI32,          13, MAP_DUMMY(ScrollWindowDelete),       "i.",                   NULL },
+	{ SIG_SCI32,          14, MAP_CALL(ScrollWindowModify),        "iiriii(i)",            NULL },
+	{ SIG_SCI32,          15, MAP_CALL(ScrollWindowHide),          "i",                    NULL },
+	{ SIG_SCI32,          16, MAP_CALL(ScrollWindowShow),          "i",                    NULL },
+	{ SIG_SCI32,          17, MAP_CALL(ScrollWindowDestroy),       "i",                    NULL },
+	// LSL6hires uses kScrollWindowText and kScrollWindowReconstruct to try to save
+	// and restore the content of the game's subtitle window, but this feature did not
+	// use the normal save/load functionality of the engine and was actually broken
+	// (all text formatting was missing on restore). Since there is no real reason to
+	// save the subtitle scrollback anyway, we just ignore calls to these two functions.
+	{ SIG_SCI32,          18, MAP_EMPTY(ScrollWindowText),         "i",                    NULL },
+	{ SIG_SCI32,          19, MAP_EMPTY(ScrollWindowReconstruct),  "i.",                   NULL },
 	SCI_SUBOPENTRY_TERMINATOR
 };
 
diff --git a/engines/sci/engine/kgraphics32.cpp b/engines/sci/engine/kgraphics32.cpp
index 647f9df..f8ec96c 100644
--- a/engines/sci/engine/kgraphics32.cpp
+++ b/engines/sci/engine/kgraphics32.cpp
@@ -360,140 +360,138 @@ reg_t kScrollWindow(EngineState *s, int argc, reg_t *argv) {
 }
 
 reg_t kScrollWindowCreate(EngineState *s, int argc, reg_t *argv) {
-	debug("kScrollWindowCreate");
-	kStub(s, argc, argv);
-	return argv[0];
+	const reg_t object = argv[0];
+	const uint16 maxNumEntries = argv[1].toUint16();
+
+	SegManager *segMan = s->_segMan;
+	const int16 borderColor = readSelectorValue(segMan, object, SELECTOR(borderColor));
+	const TextAlign alignment = (TextAlign)readSelectorValue(segMan, object, SELECTOR(mode));
+	const GuiResourceId fontId = (GuiResourceId)readSelectorValue(segMan, object, SELECTOR(font));
+	const int16 backColor = readSelectorValue(segMan, object, SELECTOR(back));
+	const int16 foreColor = readSelectorValue(segMan, object, SELECTOR(fore));
+	const reg_t plane = readSelector(segMan, object, SELECTOR(plane));
+
+	Common::Rect rect;
+	rect.left = readSelectorValue(segMan, object, SELECTOR(nsLeft));
+	rect.top = readSelectorValue(segMan, object, SELECTOR(nsTop));
+	rect.right = readSelectorValue(segMan, object, SELECTOR(nsRight)) + 1;
+	rect.bottom = readSelectorValue(segMan, object, SELECTOR(nsBottom)) + 1;
+	const Common::Point position(rect.left, rect.top);
+
+	return g_sci->_gfxControls32->makeScrollWindow(rect, position, plane, foreColor, backColor, fontId, alignment, borderColor, maxNumEntries);
 }
 
 reg_t kScrollWindowAdd(EngineState *s, int argc, reg_t *argv) {
-	debug("kScrollWindowAdd");
-	return kStubNull(s, argc, argv);
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	const Common::String text = s->_segMan->getString(argv[1]);
+	const GuiResourceId fontId = argv[2].toSint16();
+	const int16 color = argv[3].toSint16();
+	const TextAlign alignment = (TextAlign)argv[4].toSint16();
+	const bool scrollTo = argc > 5 ? (bool)argv[5].toUint16() : true;
+
+	return scrollWindow->add(text, fontId, color, alignment, scrollTo);
 }
 
 reg_t kScrollWindowWhere(EngineState *s, int argc, reg_t *argv) {
-	debug("kScrollWindowWhere");
-	return kStubNull(s, argc, argv);
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	const uint16 where = (argv[1].toUint16() * scrollWindow->where()).toInt();
+
+	return make_reg(0, where);
+}
+
+reg_t kScrollWindowGo(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	const Ratio scrollTop(argv[1].toSint16(), argv[2].toSint16());
+	scrollWindow->go(scrollTop);
+
+	return s->r_acc;
+}
+
+reg_t kScrollWindowModify(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	const reg_t entryId = argv[1];
+	const Common::String newText = s->_segMan->getString(argv[2]);
+	const GuiResourceId fontId = argv[3].toSint16();
+	const int16 color = argv[4].toSint16();
+	const TextAlign alignment = (TextAlign)argv[5].toSint16();
+	const bool scrollTo = argc > 6 ? (bool)argv[6].toUint16() : true;
+
+	return scrollWindow->modify(entryId, newText, fontId, color, alignment, scrollTo);
+}
+
+reg_t kScrollWindowHide(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->hide();
+
+	return s->r_acc;
 }
 
 reg_t kScrollWindowShow(EngineState *s, int argc, reg_t *argv) {
-	debug("kScrollWindowShow");
-	return kStubNull(s, argc, argv);
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->show();
+
+	return s->r_acc;
 }
 
-reg_t kScrollWindowDestroy(EngineState *s, int argc, reg_t *argv) {
-	debug("kScrollWindowDestroy");
-	return kStubNull(s, argc, argv);
+reg_t kScrollWindowPageUp(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->pageUp();
+
+	return s->r_acc;
 }
 
-#if 0
-reg_t kScrollWindow(EngineState *s, int argc, reg_t *argv) {
-	// Used by SQ6 and LSL6 hires for the text area in the bottom of the
-	// screen. The relevant scripts also exist in Phantasmagoria 1, but they're
-	// unused. This is always called by scripts 64906 (ScrollerWindow) and
-	// 64907 (ScrollableWindow).
-
-	reg_t kWindow = argv[1];
-	uint16 op = argv[0].toUint16();
-	switch (op) {
-	case 0:	// Init
-		// TODO: Init reads the nsLeft, nsTop, nsRight, nsBottom,
-		//       borderColor, fore, back, mode, font, plane selectors
-		//       from the window in argv[1].
-		g_sci->_gfxFrameout->initScrollText(argv[2].toUint16());	// maxItems
-		g_sci->_gfxFrameout->clearScrollTexts();
-		return argv[1];	// kWindow
-	case 1: // Show message, called by ScrollableWindow::addString
-	case 14: // Modify message, called by ScrollableWindow::modifyString
-		// TODO: The parameters in Modify are shifted by one: the first
-		//       argument is the handle of the text to modify. The others
-		//       are as Add.
-		{
-		Common::String text = s->_segMan->getString(argv[2]);
-		uint16 x = 0;
-		uint16 y = 0;
-		// TODO: argv[3] is font
-		// TODO: argv[4] is color
-		// TODO: argv[5] is alignment (0 = left, 1 = center, 2 = right)
-		//       font,color,alignment may also be -1. (Maybe same as previous?)
-		// TODO: argv[6] is an optional bool, defaulting to true if not present.
-		//       If true, the old contents are scrolled out of view.
-		// TODO: Return a handle of the inserted text. (Used for modify/insert)
-		//       This handle looks like it should also be usable by kString.
-		g_sci->_gfxFrameout->addScrollTextEntry(text, kWindow, x, y, (op == 14));
-		}
-		break;
-	case 2: // Clear, called by ScrollableWindow::erase
-		g_sci->_gfxFrameout->clearScrollTexts();
-		break;
-	case 3: // Page up, called by ScrollableWindow::scrollTo
-		// TODO
-		kStub(s, argc, argv);
-		break;
-	case 4: // Page down, called by ScrollableWindow::scrollTo
-		// TODO
-		kStub(s, argc, argv);
-		break;
-	case 5: // Up arrow, called by ScrollableWindow::scrollTo
-		g_sci->_gfxFrameout->prevScrollText();
-		break;
-	case 6: // Down arrow, called by ScrollableWindow::scrollTo
-		g_sci->_gfxFrameout->nextScrollText();
-		break;
-	case 7: // Home, called by ScrollableWindow::scrollTo
-		g_sci->_gfxFrameout->firstScrollText();
-		break;
-	case 8: // End, called by ScrollableWindow::scrollTo
-		g_sci->_gfxFrameout->lastScrollText();
-		break;
-	case 9: // Resize, called by ScrollableWindow::resize and ScrollerWindow::resize
-		// TODO: This reads the nsLeft, nsTop, nsRight, nsBottom
-		//       selectors from the SCI object passed in argv[2].
-		kStub(s, argc, argv);
-		break;
-	case 10: // Where, called by ScrollableWindow::where
-		// TODO:
-		// Gives the current relative scroll location as a fraction
-		// with argv[2] as the denominator. (Return value is the numerator.)
-		// Silenced the warnings because of the high amount of console spam
-		//kStub(s, argc, argv);
-		break;
-	case 11: // Go, called by ScrollableWindow::scrollTo
-		// TODO:
-		// Two arguments provide a fraction: argv[2] is num., argv[3] is denom.
-		// Scrolls to the relative location given by the fraction.
-		kStub(s, argc, argv);
-		break;
-	case 12: // Insert, called by ScrollableWindow::insertString
-		// 5 extra parameters here:
-		// handle of insert location (new string takes that position).
-		// text, font, color, alignment
-		// TODO
-		kStub(s, argc, argv);
-		break;
-	// case 13 (Delete) is handled below
-	// case 14 (Modify) is handled above
-	case 15: // Hide, called by ScrollableWindow::hide
-		g_sci->_gfxFrameout->toggleScrollText(false);
-		break;
-	case 16: // Show, called by ScrollableWindow::show
-		g_sci->_gfxFrameout->toggleScrollText(true);
-		break;
-	case 17: // Destroy, called by ScrollableWindow::dispose
-		g_sci->_gfxFrameout->clearScrollTexts();
-		break;
-	case 13: // Delete, unused
-	case 18: // Text, unused
-	case 19: // Reconstruct, unused
-		error("kScrollWindow: Unused subop %d invoked", op);
-		break;
-	default:
-		error("kScrollWindow: unknown subop %d", op);
-		break;
-	}
+reg_t kScrollWindowPageDown(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->pageDown();
+
+	return s->r_acc;
+}
+
+reg_t kScrollWindowUpArrow(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->upArrow();
+
+	return s->r_acc;
+}
+
+reg_t kScrollWindowDownArrow(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->downArrow();
+
+	return s->r_acc;
+}
+
+reg_t kScrollWindowHome(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->home();
+
+	return s->r_acc;
+}
+
+reg_t kScrollWindowEnd(EngineState *s, int argc, reg_t *argv) {
+	ScrollWindow *scrollWindow = g_sci->_gfxControls32->getScrollWindow(argv[0]);
+
+	scrollWindow->end();
+
+	return s->r_acc;
+}
+
+reg_t kScrollWindowDestroy(EngineState *s, int argc, reg_t *argv) {
+	g_sci->_gfxControls32->destroyScrollWindow(argv[0]);
 
 	return s->r_acc;
 }
-#endif
 
 reg_t kFont(EngineState *s, int argc, reg_t *argv) {
 	if (!s)
diff --git a/engines/sci/graphics/controls32.cpp b/engines/sci/graphics/controls32.cpp
index faf1d7d..0401415 100644
--- a/engines/sci/graphics/controls32.cpp
+++ b/engines/sci/graphics/controls32.cpp
@@ -41,7 +41,31 @@ GfxControls32::GfxControls32(SegManager *segMan, GfxCache *cache, GfxText32 *tex
 	_gfxCache(cache),
 	_gfxText32(text),
 	_overwriteMode(false),
-	_nextCursorFlashTick(0) {}
+	_nextCursorFlashTick(0),
+	// SSCI used a memory handle for a ScrollWindow object
+	// as ID. We use a simple numeric handle instead.
+	_nextScrollWindowId(10000) {}
+
+GfxControls32::~GfxControls32() {
+	ScrollWindowMap::iterator it;
+	for (it = _scrollWindows.begin(); it != _scrollWindows.end(); ++it)
+		delete it->_value;
+}
+
+#pragma mark -
+#pragma mark Garbage collection
+
+Common::Array<reg_t> GfxControls32::listObjectReferences() {
+	Common::Array<reg_t> ret;
+	ScrollWindowMap::const_iterator it;
+	for (it = _scrollWindows.begin(); it != _scrollWindows.end(); ++it)
+		ret.push_back(it->_value->getBitmap());
+
+	return ret;
+}
+
+#pragma mark -
+#pragma mark Text input control
 
 reg_t GfxControls32::kernelEditText(const reg_t controlObject) {
 	SegManager *segMan = _segMan;
@@ -350,4 +374,437 @@ void GfxControls32::flashCursor(TextEditor &editor) {
 		_nextCursorFlashTick = g_sci->getTickCount() + 30;
 	}
 }
+
+#pragma mark -
+#pragma mark Scrollable window control
+
+ScrollWindow::ScrollWindow(SegManager *segMan, const Common::Rect &gameRect, const Common::Point &position, const reg_t plane, const uint8 defaultForeColor, const uint8 defaultBackColor, const GuiResourceId defaultFontId, const TextAlign defaultAlignment, const int16 defaultBorderColor, const uint16 maxNumEntries) :
+	_gfxText32(segMan, g_sci->_gfxCache),
+	_maxNumEntries(maxNumEntries),
+	_firstVisibleChar(0),
+	_topVisibleLine(0),
+	_lastVisibleChar(0),
+	_bottomVisibleLine(0),
+	_numLines(0),
+	_numVisibleLines(0),
+	_plane(plane),
+	_foreColor(defaultForeColor),
+	_backColor(defaultBackColor),
+	_borderColor(defaultBorderColor),
+	_fontId(defaultFontId),
+	_alignment(defaultAlignment),
+	_visible(false),
+	_position(position),
+	_screenItem(nullptr),
+	_nextEntryId(1) {
+
+	_entries.reserve(maxNumEntries);
+
+	_gfxText32.setFont(_fontId);
+	_pointSize = _gfxText32._font->getHeight();
+
+	const uint16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
+	const uint16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
+
+	Common::Rect bitmapRect(gameRect);
+	mulinc(bitmapRect, Ratio(_gfxText32._scaledWidth, scriptWidth), Ratio(_gfxText32._scaledHeight, scriptHeight));
+
+	_textRect.left = 2;
+	_textRect.top = 2;
+	_textRect.right = bitmapRect.width() - 2;
+	_textRect.bottom = bitmapRect.height() - 2;
+
+	uint8 skipColor = 0;
+	while (skipColor == _foreColor || skipColor == _backColor) {
+		skipColor++;
+	}
+
+	assert(bitmapRect.width() > 0 && bitmapRect.height() > 0);
+	_bitmap = _gfxText32.createFontBitmap(bitmapRect.width(), bitmapRect.height(), _textRect, "", _foreColor, _backColor, skipColor, _fontId, _alignment, _borderColor, false, false);
+
+	debugC(1, kDebugLevelGraphics, "New ScrollWindow: textRect size: %d x %d, bitmap: %04x:%04x", _textRect.width(), _textRect.height(), PRINT_REG(_bitmap));
+}
+
+ScrollWindow::~ScrollWindow() {
+	// _gfxText32._bitmap will get GCed once ScrollWindow is gone.
+	// _screenItem will be deleted by GfxFrameout
+}
+
+Ratio ScrollWindow::where() const {
+	return Ratio(_topVisibleLine, MAX(_numLines, 1));
+}
+
+void ScrollWindow::show() {
+	if (_visible) {
+		return;
+	}
+
+	if (_screenItem == nullptr) {
+		CelInfo32 celInfo;
+		celInfo.type = kCelTypeMem;
+		celInfo.bitmap = _bitmap;
+
+		_screenItem = new ScreenItem(_plane, celInfo, _position, ScaleInfo());
+	}
+
+	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(_plane);
+	plane->_screenItemList.add(_screenItem);
+
+	_visible = true;
+}
+
+void ScrollWindow::hide() {
+	if (!_visible) {
+		return;
+	}
+
+	g_sci->_gfxFrameout->deleteScreenItem(_screenItem, _plane);
+	_screenItem = nullptr;
+	g_sci->_gfxFrameout->frameOut(true);
+
+	_visible = false;
+}
+
+reg_t ScrollWindow::add(const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment, const bool scrollTo) {
+	if (_entries.size() == _maxNumEntries) {
+		ScrollWindowEntry removedEntry = _entries.remove_at(0);
+		_text.erase(0, removedEntry.text.size());
+		// `_firstVisibleChar` will be reset shortly if
+		// `scrollTo` is true, so there is no reason to
+		// update it
+		if (!scrollTo) {
+			_firstVisibleChar -= removedEntry.text.size();
+		}
+	}
+
+	_entries.push_back(ScrollWindowEntry());
+	ScrollWindowEntry &entry = _entries.back();
+
+	// NOTE: In SSCI the line ID was a memory handle for the
+	// string of this line. We use a numeric ID instead.
+	entry.id = make_reg(0, _nextEntryId++);
+
+	if (_nextEntryId > _maxNumEntries) {
+		_nextEntryId = 1;
+	}
+
+	// NOTE: In SSCI this was updated after _text was
+	// updated, which meant there was an extra unnecessary
+	// subtraction operation (subtracting `entry.text` size)
+	if (scrollTo) {
+		_firstVisibleChar = _text.size();
+	}
+
+	fillEntry(entry, text, fontId, foreColor, alignment);
+	_text += entry.text;
+
+	computeLineIndices();
+	update(true);
+
+	return entry.id;
+}
+
+void ScrollWindow::fillEntry(ScrollWindowEntry &entry, const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment) {
+	entry.alignment = alignment;
+	entry.foreColor = foreColor;
+	entry.fontId = fontId;
+
+	Common::String formattedText;
+
+	// NB: There are inconsistencies here.
+	// If there is a multi-line entry with non-default properties, and it
+	// is only partially displayed, it may not be displayed right, since the
+	// property directives are only added to the first line.
+	// (Verified by trying this in SSCI SQ6 with a custom ScrollWindowAdd call.)
+	//
+	// The converse is also a potential issue (but unverified), where lines
+	// with properties -1 can inherit properties from the previously rendered
+	// line instead of the defaults.
+
+	// NOTE: SSCI added "|s<lineIndex>|" here, but |s| is
+	// not a valid control code, so it just always ended up
+	// getting skipped
+	if (entry.fontId != -1) {
+		formattedText += Common::String::format("|f%d|", entry.fontId);
+	}
+	if (entry.foreColor != -1) {
+		formattedText += Common::String::format("|c%d|", entry.foreColor);
+	}
+	if (entry.alignment != -1) {
+		formattedText += Common::String::format("|a%d|", entry.alignment);
+	}
+	formattedText += text;
+	entry.text = formattedText;
+}
+
+reg_t ScrollWindow::modify(const reg_t id, const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment, const bool scrollTo) {
+
+	EntriesList::iterator it = _entries.begin();
+	uint firstCharLocation = 0;
+	for ( ; it != _entries.end(); ++it) {
+		if (it->id == id) {
+			break;
+		}
+		firstCharLocation += it->text.size();
+	}
+
+	if (it == _entries.end()) {
+		return make_reg(0, 0);
+	}
+
+	ScrollWindowEntry &entry = *it;
+
+	uint oldTextLength = entry.text.size();
+
+	fillEntry(entry, text, fontId, foreColor, alignment);
+	_text.replace(firstCharLocation, oldTextLength, entry.text);
+
+	if (scrollTo) {
+		_firstVisibleChar = firstCharLocation;
+	}
+
+	computeLineIndices();
+	update(true);
+
+	return entry.id;
+}
+
+void ScrollWindow::upArrow() {
+	if (_topVisibleLine == 0) {
+		return;
+	}
+
+	_topVisibleLine--;
+	_bottomVisibleLine--;
+
+	if (_bottomVisibleLine - _topVisibleLine + 1 < _numVisibleLines) {
+		_bottomVisibleLine = _numLines - 1;
+	}
+
+	_firstVisibleChar = _startsOfLines[_topVisibleLine];
+	_lastVisibleChar = _startsOfLines[_bottomVisibleLine + 1] - 1;
+
+	_visibleText = Common::String(_text.c_str() + _firstVisibleChar, _text.c_str() + _lastVisibleChar + 1);
+
+	Common::String lineText(_text.c_str() + _startsOfLines[_topVisibleLine], _text.c_str() + _startsOfLines[_topVisibleLine + 1] - 1);
+
+	debugC(3, kDebugLevelGraphics, "ScrollWindow::upArrow: top: %d, bottom: %d, num: %d, numvis: %d, lineText: %s", _topVisibleLine, _bottomVisibleLine, _numLines, _numVisibleLines, lineText.c_str());
+
+	_gfxText32.scrollLine(lineText, _numVisibleLines, _foreColor, _alignment, _fontId, kScrollUp);
+
+	if (_visible) {
+		assert(_screenItem);
+
+		_screenItem->update();
+		g_sci->_gfxFrameout->frameOut(true);
+	}
+}
+
+void ScrollWindow::downArrow() {
+	if (_topVisibleLine + 1 >= _numLines) {
+		return;
+	}
+
+	_topVisibleLine++;
+	_bottomVisibleLine++;
+
+	if (_bottomVisibleLine + 1 >= _numLines) {
+		_bottomVisibleLine = _numLines - 1;
+	}
+
+	_firstVisibleChar = _startsOfLines[_topVisibleLine];
+	_lastVisibleChar = _startsOfLines[_bottomVisibleLine + 1] - 1;
+
+	_visibleText = Common::String(_text.c_str() + _firstVisibleChar, _text.c_str() + _lastVisibleChar + 1);
+
+	Common::String lineText;
+	if (_bottomVisibleLine - _topVisibleLine + 1 == _numVisibleLines) {
+		lineText = Common::String(_text.c_str() + _startsOfLines[_bottomVisibleLine], _text.c_str() + _startsOfLines[_bottomVisibleLine + 1] - 1);
+	} else {
+		// scroll in empty string
+	}
+
+	debugC(3, kDebugLevelGraphics, "ScrollWindow::downArrow: top: %d, bottom: %d, num: %d, numvis: %d, lineText: %s", _topVisibleLine, _bottomVisibleLine, _numLines, _numVisibleLines, lineText.c_str());
+
+
+	_gfxText32.scrollLine(lineText, _numVisibleLines, _foreColor, _alignment, _fontId, kScrollDown);
+
+	if (_visible) {
+		assert(_screenItem);
+
+		_screenItem->update();
+		g_sci->_gfxFrameout->frameOut(true);
+	}
+}
+
+void ScrollWindow::go(const Ratio location) {
+	const int line = (location * _numLines).toInt();
+	if (line < 0 || line > _numLines) {
+		error("Index is Out of Range in ScrollWindow");
+	}
+
+	_firstVisibleChar = _startsOfLines[line];
+	update(true);
+
+	// HACK:
+	// It usually isn't possible to set _topVisibleLine >= _numLines, and so
+	// update() doesn't. However, in this case we should set _topVisibleLine
+	// past the end. This is clearly visible in Phantasmagoria when dragging
+	// the slider in the About dialog to the very end. The slider ends up lower
+	// than where it can be moved by scrolling down with the arrows.
+	if (location.isOne()) {
+		_topVisibleLine = _numLines;
+	}
+}
+
+void ScrollWindow::home() {
+	if (_firstVisibleChar == 0) {
+		return;
+	}
+
+	_firstVisibleChar = 0;
+	update(true);
+}
+
+void ScrollWindow::end() {
+	if (_bottomVisibleLine + 1 >= _numLines) {
+		return;
+	}
+
+	int line = _numLines - _numVisibleLines;
+	if (line < 0) {
+		line = 0;
+	}
+	_firstVisibleChar = _startsOfLines[line];
+	update(true);
+}
+
+void ScrollWindow::pageUp() {
+	if (_topVisibleLine == 0) {
+		return;
+	}
+
+	_topVisibleLine -= _numVisibleLines;
+	if (_topVisibleLine < 0) {
+		_topVisibleLine = 0;
+	}
+
+	_firstVisibleChar = _startsOfLines[_topVisibleLine];
+	update(true);
+}
+
+void ScrollWindow::pageDown() {
+	if (_topVisibleLine + 1 >= _numLines) {
+		return;
+	}
+
+	_topVisibleLine += _numVisibleLines;
+	if (_topVisibleLine + 1 >= _numLines) {
+		_topVisibleLine = _numLines - 1;
+	}
+
+	_firstVisibleChar = _startsOfLines[_topVisibleLine];
+	update(true);
+}
+
+void ScrollWindow::computeLineIndices() {
+	_gfxText32.setFont(_fontId);
+	// NOTE: Unlike SSCI, foreColor and alignment are not
+	// set since these properties do not affect the width of
+	// lines
+
+	if (_gfxText32._font->getHeight() != _pointSize) {
+		error("Illegal font size font = %d pointSize = %d, should be %d.", _fontId, _gfxText32._font->getHeight(), _pointSize);
+	}
+
+	Common::Rect lineRect(0, 0, _textRect.width(), _pointSize + 3);
+
+	_startsOfLines.clear();
+
+	// NOTE: The original engine had a 1000-line limit; we
+	// do not enforce any limit
+	for (uint charIndex = 0; charIndex < _text.size(); ) {
+		_startsOfLines.push_back(charIndex);
+		charIndex += _gfxText32.getTextCount(_text, charIndex, lineRect, false);
+	}
+
+	_numLines = _startsOfLines.size();
+
+	_startsOfLines.push_back(_text.size());
+
+	_lastVisibleChar = _gfxText32.getTextCount(_text, 0, _fontId, _textRect, false) - 1;
+
+	_bottomVisibleLine = 0;
+	while (
+		_bottomVisibleLine < _numLines - 1 &&
+		_startsOfLines[_bottomVisibleLine + 1] < _lastVisibleChar
+	) {
+		++_bottomVisibleLine;
+	}
+
+	_numVisibleLines = _bottomVisibleLine + 1;
+}
+
+void ScrollWindow::update(const bool doFrameOut) {
+	_topVisibleLine = 0;
+	while (
+		_topVisibleLine < _numLines - 1 &&
+		_firstVisibleChar >= _startsOfLines[_topVisibleLine + 1]
+	) {
+		++_topVisibleLine;
+	}
+
+	_bottomVisibleLine = _topVisibleLine + _numVisibleLines - 1;
+	if (_bottomVisibleLine >= _numLines) {
+		_bottomVisibleLine = _numLines - 1;
+	}
+
+	_firstVisibleChar = _startsOfLines[_topVisibleLine];
+
+	if (_bottomVisibleLine >= 0) {
+		_lastVisibleChar = _startsOfLines[_bottomVisibleLine + 1] - 1;
+	} else {
+		_lastVisibleChar = -1;
+	}
+
+	_visibleText = Common::String(_text.c_str() + _firstVisibleChar, _text.c_str() + _lastVisibleChar + 1);
+
+	_gfxText32.erase(_textRect, false);
+	_gfxText32.drawTextBox(_visibleText);
+
+	if (_visible) {
+		assert(_screenItem);
+
+		_screenItem->update();
+		if (doFrameOut) {
+			g_sci->_gfxFrameout->frameOut(true);
+		}
+	}
+}
+
+reg_t GfxControls32::makeScrollWindow(const Common::Rect &gameRect, const Common::Point &position, const reg_t planeObj, const uint8 defaultForeColor, const uint8 defaultBackColor, const GuiResourceId defaultFontId, const TextAlign defaultAlignment, const int16 defaultBorderColor, const uint16 maxNumEntries) {
+
+	ScrollWindow *scrollWindow = new ScrollWindow(_segMan, gameRect, position, planeObj, defaultForeColor, defaultBackColor, defaultFontId, defaultAlignment, defaultBorderColor, maxNumEntries);
+
+	const uint16 id = _nextScrollWindowId++;
+	_scrollWindows[id] = scrollWindow;
+	return make_reg(0, id);
+}
+
+ScrollWindow *GfxControls32::getScrollWindow(const reg_t id) {
+	ScrollWindowMap::iterator it;
+	it = _scrollWindows.find(id.toUint16());
+	if (it == _scrollWindows.end())
+		error("Invalid ScrollWindow ID");
+
+	return it->_value;
+}
+
+void GfxControls32::destroyScrollWindow(const reg_t id) {
+	ScrollWindow *scrollWindow = getScrollWindow(id);
+	scrollWindow->hide();
+	_scrollWindows.erase(id.getOffset());
+	delete scrollWindow;
+}
+
 } // End of namespace Sci
diff --git a/engines/sci/graphics/controls32.h b/engines/sci/graphics/controls32.h
index 1bb7679..62ab90d 100644
--- a/engines/sci/graphics/controls32.h
+++ b/engines/sci/graphics/controls32.h
@@ -23,12 +23,15 @@
 #ifndef SCI_GRAPHICS_CONTROLS32_H
 #define SCI_GRAPHICS_CONTROLS32_H
 
+#include "sci/graphics/text32.h"
+
 namespace Sci {
 
 class GfxCache;
 class GfxScreen;
 class GfxText32;
 
+
 struct TextEditor {
 	/**
 	 * The bitmap where the editor is rendered.
@@ -100,24 +103,388 @@ struct TextEditor {
 };
 
 /**
+ * A single block of text written to a ScrollWindow.
+ */
+struct ScrollWindowEntry {
+	/**
+	 * ID of the line. In SSCI this was actually a memory
+	 * handle for the string of this line. We use a simple
+	 * numeric ID instead.
+	 */
+	reg_t id;
+
+	/**
+	 * The alignment to use when rendering this line of
+	 * text. If -1, the default alignment from the
+	 * corresponding ScrollWindow will be used.
+	 */
+	TextAlign alignment;
+
+	/**
+	 * The color to use to render this line of text. If -1,
+	 * the default foreground color from the corresponding
+	 * ScrollWindow will be used.
+	 */
+	int16 foreColor;
+
+	/**
+	 * The font to use to render this line of text. If -1,
+	 * the default font from the corresponding ScrollWindow
+	 * will be used.
+	 */
+	GuiResourceId fontId;
+
+	/**
+	 * The text.
+	 */
+	Common::String text;
+};
+
+class ScreenItem;
+
+/**
+ * A scrollable text window.
+ */
+class ScrollWindow {
+public:
+	ScrollWindow(SegManager *segMan, const Common::Rect &gameRect, const Common::Point &position, const reg_t planeObj, const uint8 defaultForeColor, const uint8 defaultBackColor, const GuiResourceId defaultFontId, const TextAlign defaultAlignment, const int16 defaultBorderColor, const uint16 maxNumEntries);
+	~ScrollWindow();
+
+	/**
+	 * Adds a new text entry to the window. If `fontId`,
+	 * `foreColor`, or `alignment` are `-1`, the
+	 * ScrollWindow's default values will be used.
+	 */
+	reg_t add(const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment, const bool scrollTo);
+
+	/**
+	 * Modifies an existing text entry with the given ID. If
+	 * `fontId`, `foreColor`, or `alignment` are `-1`, the
+	 * ScrollWindow's default values will be used.
+	 */
+	reg_t modify(const reg_t id, const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment, const bool scrollTo);
+
+	/**
+	 * Shows the ScrollWindow if it is not already visible.
+	 */
+	void show();
+
+	/**
+	 * Hides the ScrollWindow if it is currently visible.
+	 */
+	void hide();
+
+	/**
+	 * Gets the number of lines that the content of a
+	 * ScrollWindow is scrolled upward, as a ratio of the
+	 * total number of lines of content.
+	 */
+	Ratio where() const;
+
+	/**
+	 * Scrolls the window to a specific location.
+	 */
+	void go(const Ratio location);
+
+	/**
+	 * Scrolls the window to the top.
+	 */
+	void home();
+
+	/**
+	 * Scrolls the window to the bottom.
+	 */
+	void end();
+
+	/**
+	 * Scrolls the window up one line.
+	 */
+	void upArrow();
+
+	/**
+	 * Scrolls the window down one line.
+	 */
+	void downArrow();
+
+	/**
+	 * Scrolls the window up by one page.
+	 */
+	void pageUp();
+
+	/**
+	 * Scrolls the window down by one page.
+	 */
+	void pageDown();
+
+	/**
+	 * Gets a reference to the in-memory bitmap that
+	 * is used to render the text in the ScrollWindow.
+	 */
+	const reg_t getBitmap() const { return _bitmap; }
+
+private:
+	typedef Common::Array<ScrollWindowEntry> EntriesList;
+
+	/**
+	 * A convenience function that fills a
+	 * ScrollWindowEntry's properties.
+	 */
+	void fillEntry(ScrollWindowEntry &entry, const Common::String &text, const GuiResourceId fontId, const int16 foreColor, const TextAlign alignment);
+
+	/**
+	 * Rescans the entire text of the ScrollWindow when an
+	 * entry is added or modified, calculating the character
+	 * offsets of all line endings, the total number of
+	 * lines of text, the height of the viewport (in lines
+	 * of text), the last character visible in the viewport
+	 * (assuming the viewport is scrolled to the top), and
+	 * the line index of the bottommost visible line
+	 * (assuming the viewport is scrolled to the top).
+	 */
+	void computeLineIndices();
+
+	/**
+	 * Calculates which text is visible within the
+	 * ScrollWindow's viewport and renders the text to the
+	 * internal bitmap.
+	 *
+	 * If `doFrameOut` is true, the screen will be refreshed
+	 * immediately instead of waiting for the next call to
+	 * `kFrameOut`.
+	 */
+	void update(const bool doFrameOut);
+
+	/**
+	 * The text renderer.
+	 */
+	GfxText32 _gfxText32;
+
+	/**
+	 * The individual text entries added to the
+	 * ScrollWindow.
+	 */
+	EntriesList _entries;
+
+	/**
+	 * The maximum number of entries allowed. Once this
+	 * limit is reached, the oldest entry will be removed
+	 * when a new entry is added.
+	 */
+	uint _maxNumEntries;
+
+	/**
+	 * A mapping from a line index to the line's character
+	 * offset in `_text`.
+	 */
+	Common::Array<int> _startsOfLines;
+
+	/**
+	 * All text added to the window.
+	 */
+	Common::String _text;
+
+	/**
+	 * Text that is within the viewport of the ScrollWindow.
+	 */
+	Common::String _visibleText;
+
+	/**
+	 * The offset of the first visible character in `_text`.
+	 */
+	int _firstVisibleChar;
+
+	/**
+	 * The index of the line that is at the top of the
+	 * viewport.
+	 */
+	int _topVisibleLine;
+
+	/**
+	 * The index of the last visible character in `_text`,
+	 * or -1 if there is no text.
+	 */
+	int _lastVisibleChar;
+
+	/**
+	 * The index of the line that is at the bottom of the
+	 * viewport, or -1 if there is no text.
+	 */
+	int _bottomVisibleLine;
+
+	/**
+	 * The total number of lines in the backbuffer. This
+	 * number may be higher than the total number of entries
+	 * if an entry contains newlines.
+	 */
+	int _numLines;
+
+	/**
+	 * The number of lines that are currently visible in the
+	 * text area of the window.
+	 */
+	int _numVisibleLines;
+
+	/**
+	 * The plane in which the ScrollWindow should be
+	 * rendered.
+	 */
+	reg_t _plane;
+
+	/**
+	 * The default text color.
+	 */
+	uint8 _foreColor;
+
+	/**
+	 * The default background color of the text bitmap.
+	 */
+	uint8 _backColor;
+
+	/**
+	 * The default border color of the text bitmap. If -1,
+	 * the viewport will have no border.
+	 */
+	int16 _borderColor;
+
+	/**
+	 * The default font used for rendering text into the
+	 * ScrollWindow.
+	 */
+	GuiResourceId _fontId;
+
+	/**
+	 * The default text alignment used for rendering text
+	 * into the ScrollWindow.
+	 */
+	TextAlign _alignment;
+
+	/**
+	 * The visibility of the ScrollWindow.
+	 */
+	bool _visible;
+
+	/**
+	 * The dimensions of the text box inside the font
+	 * bitmap, in text-system coordinates.
+	 */
+	Common::Rect _textRect;
+
+	/**
+	 * The top-left corner of the ScrollWindow's screen
+	 * item, in game script coordinates, relative to the
+	 * parent plane.
+	 */
+	Common::Point _position;
+
+	/**
+	 * The height of the default font in screen pixels. All
+	 * fonts rendered into the ScrollWindow must have this
+	 * same height.
+	 */
+	uint8 _pointSize;
+
+	/**
+	 * The bitmap used to render text.
+	 */
+	reg_t _bitmap;
+
+	/**
+	 * A monotonically increasing ID used to identify
+	 * text entries added to the ScrollWindow.
+	 */
+	uint16 _nextEntryId;
+
+	/**
+	 * The ScrollWindow's screen item.
+	 */
+	ScreenItem *_screenItem;
+};
+
+/**
  * Controls class, handles drawing of controls in SCI32 (SCI2, SCI2.1, SCI3) games
  */
 class GfxControls32 {
 public:
 	GfxControls32(SegManager *segMan, GfxCache *cache, GfxText32 *text);
-
-	reg_t kernelEditText(const reg_t controlObject);
+	~GfxControls32();
 
 private:
 	SegManager *_segMan;
 	GfxCache *_gfxCache;
 	GfxText32 *_gfxText32;
 
+#pragma mark -
+#pragma mark Garbage collection
+public:
+	Common::Array<reg_t> listObjectReferences();
+
+#pragma mark -
+#pragma mark Text input control
+public:
+	reg_t kernelEditText(const reg_t controlObject);
+
+private:
+	/**
+	 * If true, typing will overwrite text that already
+	 * exists at the text cursor's current position.
+	 */
 	bool _overwriteMode;
+
+	/**
+	 * The tick at which the text cursor should be toggled
+	 * by `flashCursor`.
+	 */
 	uint32 _nextCursorFlashTick;
+
+	/**
+	 * Draws the text cursor for the given editor.
+	 */
 	void drawCursor(TextEditor &editor);
+
+	/**
+	 * Erases the text cursor for the given editor.
+	 */
 	void eraseCursor(TextEditor &editor);
+
+	/**
+	 * Toggles the text cursor for the given editor to be
+	 * either drawn or erased.
+	 */
 	void flashCursor(TextEditor &editor);
+
+#pragma mark -
+#pragma mark Scrollable window control
+public:
+	/**
+	 * Creates a new scrollable window and returns the ID
+	 * for the new window, which is used by game scripts to
+	 * interact with scrollable windows.
+	 */
+	reg_t makeScrollWindow(const Common::Rect &gameRect, const Common::Point &position, const reg_t plane, const uint8 defaultForeColor, const uint8 defaultBackColor, const GuiResourceId defaultFontId, const TextAlign defaultAlignment, const int16 defaultBorderColor, const uint16 maxNumEntries);
+
+	/**
+	 * Gets a registered ScrollWindow instance by ID.
+	 */
+	ScrollWindow *getScrollWindow(const reg_t id);
+
+	/**
+	 * Destroys the scroll window with the given ID.
+	 */
+	void destroyScrollWindow(const reg_t id);
+
+private:
+	typedef Common::HashMap<uint16, ScrollWindow *> ScrollWindowMap;
+
+	/**
+	 * Monotonically increasing ID used to identify
+	 * ScrollWindow instances.
+	 */
+	uint16 _nextScrollWindowId;
+
+	/**
+	 * A lookup table for registered ScrollWindow instances.
+	 */
+	ScrollWindowMap _scrollWindows;
 };
 
 } // End of namespace Sci
diff --git a/engines/sci/graphics/screen_item32.cpp b/engines/sci/graphics/screen_item32.cpp
index a3728d9..c0b3240 100644
--- a/engines/sci/graphics/screen_item32.cpp
+++ b/engines/sci/graphics/screen_item32.cpp
@@ -516,15 +516,22 @@ void ScreenItem::update(const reg_t object) {
 }
 
 void ScreenItem::update() {
-        // TODO: error out if we're not contained in our plane's ScreenItemList
+	Plane *plane = g_sci->_gfxFrameout->getPlanes().findByObject(_plane);
+	if (plane == nullptr) {
+		error("ScreenItem::update: Invalid plane %04x:%04x", PRINT_REG(_plane));
+	}
+
+	if (plane->_screenItemList.findByObject(_object) == nullptr) {
+		error("ScreenItem::update: %04x:%04x not in plane %04x:%04x", PRINT_REG(_object), PRINT_REG(_plane));
+	}
 
-        if (!_created) {
-                _updated = g_sci->_gfxFrameout->getScreenCount();
-        }
-        _deleted = 0;
+	if (!_created) {
+		_updated = g_sci->_gfxFrameout->getScreenCount();
+	}
+	_deleted = 0;
 
-        delete _celObj;
-        _celObj = nullptr;
+	delete _celObj;
+	_celObj = nullptr;
 }
 
 // TODO: This code is quite similar to calcRects, so try to deduplicate
diff --git a/engines/sci/graphics/text32.cpp b/engines/sci/graphics/text32.cpp
index f38a95d..d3b5bf5 100644
--- a/engines/sci/graphics/text32.cpp
+++ b/engines/sci/graphics/text32.cpp
@@ -237,8 +237,10 @@ void GfxText32::drawTextBox() {
 	int16 textRectWidth = _textRect.width();
 	_drawPosition.y = _textRect.top;
 	uint charIndex = 0;
-	if (getLongest(&charIndex, textRectWidth) == 0) {
-		error("DrawTextBox GetLongest=0");
+	if (g_sci->getGameId() != GID_PHANTASMAGORIA) {
+		if (getLongest(&charIndex, textRectWidth) == 0) {
+			error("DrawTextBox GetLongest=0");
+		}
 	}
 
 	charIndex = 0;
@@ -651,5 +653,69 @@ int16 GfxText32::getTextCount(const Common::String &text, const uint index, cons
 	return getTextCount(text, index, textRect, doScaling);
 }
 
+void GfxText32::scrollLine(const Common::String &lineText, int numLines, uint8 color, TextAlign align, GuiResourceId fontId, ScrollDirection dir) {
+	BitmapResource bmr(_bitmap);
+	byte *pixels = bmr.getPixels();
+
+	int h = _font->getHeight();
+
+	if (dir == kScrollUp) {
+		// Scroll existing text down
+		for (int i = 0; i < (numLines - 1) * h; ++i) {
+			int y = _textRect.top + numLines * h - i - 1;
+			memcpy(pixels + y * _width + _textRect.left,
+			       pixels + (y - h) * _width + _textRect.left,
+			       _textRect.width());
+		}
+	} else {
+		// Scroll existing text up
+		for (int i = 0; i < (numLines - 1) * h; ++i) {
+			int y = _textRect.top + i;
+			memcpy(pixels + y * _width + _textRect.left,
+			       pixels + (y + h) * _width + _textRect.left,
+			       _textRect.width());
+		}
+	}
+
+	Common::Rect lineRect = _textRect;
+
+	if (dir == kScrollUp) {
+		lineRect.bottom = lineRect.top + h;
+	} else {
+		// It is unclear to me what the purpose of this bottom++ is.
+		// It does not seem to be the usual inc/exc issue.
+		lineRect.top += (numLines - 1) * h;
+		lineRect.bottom++;
+	}
+
+	erase(lineRect, false);
+
+	_drawPosition.x = _textRect.left;
+	_drawPosition.y = _textRect.top;
+	if (dir == kScrollDown) {
+		_drawPosition.y += (numLines - 1) * h;
+	}
+
+	_foreColor = color;
+	_alignment = align;
+	//int fc = _foreColor;
+
+	setFont(fontId);
+
+	_text = lineText;
+	int16 textWidth = getTextWidth(0, lineText.size());
+
+	if (_alignment == kTextAlignCenter) {
+		_drawPosition.x += (_textRect.width() - textWidth) / 2;
+	} else if (_alignment == kTextAlignRight) {
+		_drawPosition.x += _textRect.width() - textWidth;
+	}
+
+	//_foreColor = fc;
+	//setFont(fontId);
+
+	drawText(0, lineText.size());
+}
+
 
 } // End of namespace Sci
diff --git a/engines/sci/graphics/text32.h b/engines/sci/graphics/text32.h
index c286298..88cf149 100644
--- a/engines/sci/graphics/text32.h
+++ b/engines/sci/graphics/text32.h
@@ -30,9 +30,15 @@
 namespace Sci {
 
 enum TextAlign {
-	kTextAlignLeft   = 0,
-	kTextAlignCenter = 1,
-	kTextAlignRight  = 2
+	kTextAlignDefault = -1,
+	kTextAlignLeft    = 0,
+	kTextAlignCenter  = 1,
+	kTextAlignRight   = 2
+};
+
+enum ScrollDirection {
+	kScrollUp,
+	kScrollDown
 };
 
 enum BitmapFlags {
@@ -457,6 +463,13 @@ public:
 	 * `textRect` using the given font.
 	 */
 	int16 getTextCount(const Common::String &text, const uint index, const GuiResourceId fontId, const Common::Rect &textRect, const bool doScaling);
+
+	/**
+	 * Scroll up/down one line. `numLines` is the number of the lines in the
+	 * textarea, and `textLine` contains the text to draw as the newly
+	 * visible line. Originally FontMgr::DrawOneLine and FontMgr::UpOneLine.
+	 */
+	void scrollLine(const Common::String &textLine, int numLines, uint8 color, TextAlign align, GuiResourceId fontId, ScrollDirection dir);
 };
 
 } // End of namespace Sci
diff --git a/engines/sci/sci.cpp b/engines/sci/sci.cpp
index b74dc46..c0f8f21 100644
--- a/engines/sci/sci.cpp
+++ b/engines/sci/sci.cpp
@@ -49,7 +49,6 @@
 #include "sci/graphics/cache.h"
 #include "sci/graphics/compare.h"
 #include "sci/graphics/controls16.h"
-#include "sci/graphics/controls32.h"
 #include "sci/graphics/coordadjuster.h"
 #include "sci/graphics/cursor.h"
 #include "sci/graphics/maciconbar.h"
@@ -65,6 +64,7 @@
 #include "sci/graphics/transitions.h"
 
 #ifdef ENABLE_SCI32
+#include "sci/graphics/controls32.h"
 #include "sci/graphics/palette32.h"
 #include "sci/graphics/text32.h"
 #include "sci/graphics/frameout.h"


Commit: ceee33ba2c8e880be27a23915228fa11af41515c
    https://github.com/scummvm/scummvm/commit/ceee33ba2c8e880be27a23915228fa11af41515c
Author: Willem Jan Palenstijn (wjp at usecode.org)
Date: 2016-06-21T08:14:12-05:00

Commit Message:
SCI32: Add workaround for kScrollWindowAdd call in Phantasmagoria

Changed paths:
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/workarounds.cpp
    engines/sci/engine/workarounds.h



diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index 82184c2..03e56ea 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -468,7 +468,7 @@ static const SciKernelMapSubEntry kString_subops[] = {
 //    version,         subId, function-mapping,                    signature,              workarounds
 static const SciKernelMapSubEntry kScrollWindow_subops[] = {
 	{ SIG_SCI32,           0, MAP_CALL(ScrollWindowCreate),        "oi",                   NULL },
-	{ SIG_SCI32,           1, MAP_CALL(ScrollWindowAdd),           "iriii(i)",             NULL },
+	{ SIG_SCI32,           1, MAP_CALL(ScrollWindowAdd),           "iriii(i)",             kScrollWindowAdd_workarounds },
 	{ SIG_SCI32,           2, MAP_DUMMY(ScrollWindowClear),        "i",                    NULL },
 	{ SIG_SCI32,           3, MAP_CALL(ScrollWindowPageUp),        "i",                    NULL },
 	{ SIG_SCI32,           4, MAP_CALL(ScrollWindowPageDown),      "i",                    NULL },
diff --git a/engines/sci/engine/workarounds.cpp b/engines/sci/engine/workarounds.cpp
index d86ad60..3a6cf0c 100644
--- a/engines/sci/engine/workarounds.cpp
+++ b/engines/sci/engine/workarounds.cpp
@@ -751,6 +751,12 @@ const SciWorkaroundEntry kUnLoad_workarounds[] = {
 	SCI_WORKAROUNDENTRY_TERMINATOR
 };
 
+//    gameID,           room,script,lvl,          object-name, method-name,  local-call-signature, index,                workaround
+const SciWorkaroundEntry kScrollWindowAdd_workarounds[] = {
+	{ GID_PHANTASMAGORIA, 45, 64907,  0,   "ScrollableWindow", "addString",                  NULL,     0, { WORKAROUND_STILLCALL, 0 } }, // ScrollWindow interface passes the last two parameters twice
+};
+
+
 SciWorkaroundSolution trackOriginAndFindWorkaround(int index, const SciWorkaroundEntry *workaroundList, SciTrackOriginReply *trackOrigin) {
 	// HACK for SCI3: Temporarily ignore this
 	if (getSciVersion() == SCI_VERSION_3) {
diff --git a/engines/sci/engine/workarounds.h b/engines/sci/engine/workarounds.h
index 680aa47..248d37f 100644
--- a/engines/sci/engine/workarounds.h
+++ b/engines/sci/engine/workarounds.h
@@ -99,6 +99,7 @@ extern const SciWorkaroundEntry kStrAt_workarounds[];
 extern const SciWorkaroundEntry kStrCpy_workarounds[];
 extern const SciWorkaroundEntry kStrLen_workarounds[];
 extern const SciWorkaroundEntry kUnLoad_workarounds[];
+extern const SciWorkaroundEntry kScrollWindowAdd_workarounds[];
 
 extern SciWorkaroundSolution trackOriginAndFindWorkaround(int index, const SciWorkaroundEntry *workaroundList, SciTrackOriginReply *trackOrigin);
 


Commit: 52505dc57f7cf244e7bc318746add0ce002b6e60
    https://github.com/scummvm/scummvm/commit/52505dc57f7cf244e7bc318746add0ce002b6e60
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-21T08:14:12-05:00

Commit Message:
SCI32: Implement basic kMessageBox

This kernel call seems only to be used by KQ7 1.51 (which was
Windows-only) to send warnings to the user.

It was easy enough to do a basic implementation in the ScummVM
GUI rather than just make it an empty call, so now it is a thing.

Changed paths:
    engines/sci/engine/kernel.h
    engines/sci/engine/kernel_tables.h
    engines/sci/engine/kgraphics32.cpp
    engines/sci/graphics/controls32.cpp
    engines/sci/graphics/controls32.h



diff --git a/engines/sci/engine/kernel.h b/engines/sci/engine/kernel.h
index 6604387..b269e97 100644
--- a/engines/sci/engine/kernel.h
+++ b/engines/sci/engine/kernel.h
@@ -566,6 +566,7 @@ reg_t kMoveToFront(EngineState *s, int argc, reg_t *argv);
 reg_t kMoveToEnd(EngineState *s, int argc, reg_t *argv);
 reg_t kGetWindowsOption(EngineState *s, int argc, reg_t *argv);
 reg_t kWinHelp(EngineState *s, int argc, reg_t *argv);
+reg_t kMessageBox(EngineState *s, int argc, reg_t *argv);
 reg_t kGetConfig(EngineState *s, int argc, reg_t *argv);
 reg_t kGetSierraProfileInt(EngineState *s, int argc, reg_t *argv);
 reg_t kCelInfo(EngineState *s, int argc, reg_t *argv);
diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h
index 03e56ea..19bee35 100644
--- a/engines/sci/engine/kernel_tables.h
+++ b/engines/sci/engine/kernel_tables.h
@@ -728,6 +728,8 @@ static SciKernelMapEntry s_kernelMap[] = {
 	{ MAP_CALL(ListEachElementDo), SIG_EVERYWHERE,           "li(.*)",                NULL,            NULL },
 	{ MAP_CALL(ListFirstTrue),     SIG_EVERYWHERE,           "li(.*)",                NULL,            NULL },
 	{ MAP_CALL(ListIndexOf),       SIG_EVERYWHERE,           "l[o0]",                 NULL,            NULL },
+	// kMessageBox is used only by KQ7 1.51
+	{ MAP_CALL(MessageBox),        SIG_SCI32, SIGFOR_ALL,    "rri",                   NULL,            NULL },
 	{ "OnMe", kIsOnMe,             SIG_EVERYWHERE,           "iioi",                  NULL,            NULL },
 	// Purge is used by the memory manager in SSCI to ensure that X number of bytes (the so called "unmovable
 	// memory") are available when the current room changes. This is similar to the SCI0-SCI1.1 FlushResources
diff --git a/engines/sci/engine/kgraphics32.cpp b/engines/sci/engine/kgraphics32.cpp
index f8ec96c..4083414 100644
--- a/engines/sci/engine/kgraphics32.cpp
+++ b/engines/sci/engine/kgraphics32.cpp
@@ -254,6 +254,10 @@ reg_t kWinHelp(EngineState *s, int argc, reg_t *argv) {
 	return s->r_acc;
 }
 
+reg_t kMessageBox(EngineState *s, int argc, reg_t *argv) {
+	return g_sci->_gfxControls32->kernelMessageBox(s->_segMan->getString(argv[0]), s->_segMan->getString(argv[1]), argv[2].toUint16());
+}
+
 /**
  * Causes an immediate plane transition with an optional transition
  * effect
diff --git a/engines/sci/graphics/controls32.cpp b/engines/sci/graphics/controls32.cpp
index 0401415..61dfbed 100644
--- a/engines/sci/graphics/controls32.cpp
+++ b/engines/sci/graphics/controls32.cpp
@@ -21,7 +21,8 @@
  */
 
 #include "common/system.h"
-
+#include "common/translation.h"
+#include "gui/message.h"
 #include "sci/sci.h"
 #include "sci/console.h"
 #include "sci/event.h"
@@ -807,4 +808,37 @@ void GfxControls32::destroyScrollWindow(const reg_t id) {
 	delete scrollWindow;
 }
 
+#pragma mark -
+#pragma mark Message box
+
+int16 GfxControls32::showMessageBox(const Common::String &message, const char *const okLabel, const char *const altLabel, const int16 okValue, const int16 altValue) {
+	GUI::MessageDialog dialog(message, okLabel, altLabel);
+	return (dialog.runModal() == GUI::kMessageOK) ? okValue : altValue;
+}
+
+reg_t GfxControls32::kernelMessageBox(const Common::String &message, const Common::String &title, const uint16 style) {
+	if (g_engine) {
+		g_engine->pauseEngine(true);
+	}
+
+	int16 result;
+
+	switch (style & 0xF) {
+	case kMessageBoxOK:
+		result = showMessageBox(message, _("OK"), NULL, 1, 1);
+	break;
+	case kMessageBoxYesNo:
+		result = showMessageBox(message, _("Yes"), _("No"), 6, 7);
+	break;
+	default:
+		error("Unsupported MessageBox style 0x%x", style & 0xF);
+	}
+
+	if (g_engine) {
+		g_engine->pauseEngine(false);
+	}
+
+	return make_reg(0, result);
+}
+
 } // End of namespace Sci
diff --git a/engines/sci/graphics/controls32.h b/engines/sci/graphics/controls32.h
index 62ab90d..460b0b5 100644
--- a/engines/sci/graphics/controls32.h
+++ b/engines/sci/graphics/controls32.h
@@ -31,6 +31,10 @@ class GfxCache;
 class GfxScreen;
 class GfxText32;
 
+enum MessageBoxStyle {
+	kMessageBoxOK               = 0x0,
+	kMessageBoxYesNo            = 0x4
+};
 
 struct TextEditor {
 	/**
@@ -485,6 +489,21 @@ private:
 	 * A lookup table for registered ScrollWindow instances.
 	 */
 	ScrollWindowMap _scrollWindows;
+
+#pragma mark -
+#pragma mark Message box
+public:
+	/**
+	 * Displays an OS-level message dialog.
+	 */
+	reg_t kernelMessageBox(const Common::String &message, const Common::String &title, const uint16 style);
+
+private:
+	/**
+	 * Convenience function for creating and showing a
+	 * message box.
+	 */
+	int16 showMessageBox(const Common::String &message, const char *const okLabel, const char *const altLabel, const int16 okValue, const int16 altValue);
 };
 
 } // End of namespace Sci


Commit: 820e6b8bd3783f78605f477a6f3f1548ffd8db98
    https://github.com/scummvm/scummvm/commit/820e6b8bd3783f78605f477a6f3f1548ffd8db98
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-21T08:17:28-05:00

Commit Message:
SCI32: Fixes to GfxText32

1. Inline borderSize constant in createFontBitmap for consistency
2. Fix "DrawTextBox GetLongest=0" warning to only appear in games
   that actually had this check (SQ6 and MGDX)
3. "Fix" implementation of getTextSize for SCI2.1early, which
   gave lines with word wrap disabled a height of 0
4. Add inlining hints to some methods in BitmapResource that were
   missing them for some reason

Changed paths:
    engines/sci/graphics/text32.cpp
    engines/sci/graphics/text32.h



diff --git a/engines/sci/graphics/text32.cpp b/engines/sci/graphics/text32.cpp
index d3b5bf5..993c5c9 100644
--- a/engines/sci/graphics/text32.cpp
+++ b/engines/sci/graphics/text32.cpp
@@ -121,7 +121,6 @@ reg_t GfxText32::createFontBitmap(const CelInfo32 &celInfo, const Common::Rect &
 	int16 scriptWidth = g_sci->_gfxFrameout->getCurrentBuffer().scriptWidth;
 	int16 scriptHeight = g_sci->_gfxFrameout->getCurrentBuffer().scriptHeight;
 
-	int borderSize = 1;
 	mulinc(_textRect, Ratio(_scaledWidth, scriptWidth), Ratio(_scaledHeight, scriptHeight));
 
 	CelObjView view(celInfo.resourceId, celInfo.loopNo, celInfo.celNo);
@@ -159,7 +158,7 @@ reg_t GfxText32::createFontBitmap(const CelInfo32 &celInfo, const Common::Rect &
 			error("TODO: Implement transparent text");
 		} else {
 			if (borderColor != -1) {
-				drawFrame(bitmapRect, borderSize, _borderColor, false);
+				drawFrame(bitmapRect, 1, _borderColor, false);
 			}
 
 			drawTextBox();
@@ -237,7 +236,8 @@ void GfxText32::drawTextBox() {
 	int16 textRectWidth = _textRect.width();
 	_drawPosition.y = _textRect.top;
 	uint charIndex = 0;
-	if (g_sci->getGameId() != GID_PHANTASMAGORIA) {
+
+	if (g_sci->getGameId() == GID_SQ6 || g_sci->getGameId() == GID_MOTHERGOOSEHIRES) {
 		if (getLongest(&charIndex, textRectWidth) == 0) {
 			error("DrawTextBox GetLongest=0");
 		}
@@ -591,11 +591,16 @@ Common::Rect GfxText32::getTextSize(const Common::String &text, int16 maxWidth,
 		}
 	} else {
 		result.right = getTextWidth(0, 10000);
-		// NOTE: In the original engine code, the bottom was not decremented
-		// by 1, which means that the rect was actually a pixel taller than
-		// the height of the font. This was not the case in the other branch,
-		// which decremented the bottom by 1 at the end of the loop.
-		result.bottom = _font->getHeight() + 1;
+
+		if (getSciVersion() < SCI_VERSION_2_1_MIDDLE) {
+			result.bottom = 0;
+		} else {
+			// NOTE: In the original engine code, the bottom was not decremented
+			// by 1, which means that the rect was actually a pixel taller than
+			// the height of the font. This was not the case in the other branch,
+			// which decremented the bottom by 1 at the end of the loop.
+			result.bottom = _font->getHeight() + 1;
+		}
 	}
 
 	if (doScaling) {
diff --git a/engines/sci/graphics/text32.h b/engines/sci/graphics/text32.h
index 88cf149..9892740 100644
--- a/engines/sci/graphics/text32.h
+++ b/engines/sci/graphics/text32.h
@@ -133,7 +133,7 @@ public:
 		setScaledHeight(scaledHeight);
 	}
 
-	reg_t getObject() const {
+	inline reg_t getObject() const {
 		return _object;
 	}
 
@@ -180,7 +180,7 @@ public:
 		return READ_SCI11ENDIAN_UINT32(_bitmap + 20);
 	}
 
-	void setHunkPaletteOffset(uint32 hunkPaletteOffset) {
+	inline void setHunkPaletteOffset(uint32 hunkPaletteOffset) {
 		if (hunkPaletteOffset) {
 			hunkPaletteOffset += getBitmapHeaderSize();
 		}


Commit: a3055d3f491d77f5581b887179e204e5ce858f70
    https://github.com/scummvm/scummvm/commit/a3055d3f491d77f5581b887179e204e5ce858f70
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-21T08:17:28-05:00

Commit Message:
SCI: Add an explanation about LRU removals when fetching resources

Several times I have run into this code and had to take a minute
to remind myself that the call to remove from the LRU on find is
not wrong, so it seemed to deserve a comment.

Changed paths:
    engines/sci/resource.cpp



diff --git a/engines/sci/resource.cpp b/engines/sci/resource.cpp
index 2a83a57..3e50fc1 100644
--- a/engines/sci/resource.cpp
+++ b/engines/sci/resource.cpp
@@ -1051,7 +1051,13 @@ Resource *ResourceManager::findResource(ResourceId id, bool lock) {
 	if (retval->_status == kResStatusNoMalloc)
 		loadResource(retval);
 	else if (retval->_status == kResStatusEnqueued)
+		// The resource is removed from its current position
+		// in the LRU list because it has been requested
+		// again. Below, it will either be locked, or it
+		// will be added back to the LRU list at the 'most
+		// recent' position.
 		removeFromLRU(retval);
+
 	// Unless an error occurred, the resource is now either
 	// locked or allocated, but never queued or freed.
 


Commit: 4495ae3de00bd808399a5e38cdd58fb5d40fef5d
    https://github.com/scummvm/scummvm/commit/4495ae3de00bd808399a5e38cdd58fb5d40fef5d
Author: Colin Snover (github.com at zetafleet.com)
Date: 2016-06-21T08:17:28-05:00

Commit Message:
SCI32: Remove unused dependencies from GfxFrameout

Changed paths:
    engines/sci/graphics/frameout.cpp
    engines/sci/graphics/frameout.h
    engines/sci/sci.cpp



diff --git a/engines/sci/graphics/frameout.cpp b/engines/sci/graphics/frameout.cpp
index 51a666e..ccc56c0 100644
--- a/engines/sci/graphics/frameout.cpp
+++ b/engines/sci/graphics/frameout.cpp
@@ -69,14 +69,12 @@ static int16 unknownCDefaults[2][16] = {
 	/* SCI2.1mid+ */   { 0,  2,  2,  3,  3,  4,  4,  5,  5,  6,  6,  0,  0,   7,   7, 0 }
 };
 
-GfxFrameout::GfxFrameout(SegManager *segMan, ResourceManager *resMan, GfxCoordAdjuster *coordAdjuster, GfxCache *cache, GfxScreen *screen, GfxPalette32 *palette, GfxPaint32 *paint32) :
+GfxFrameout::GfxFrameout(SegManager *segMan, ResourceManager *resMan, GfxCoordAdjuster *coordAdjuster, GfxScreen *screen, GfxPalette32 *palette) :
 	_isHiRes(false),
-	_cache(cache),
 	_palette(palette),
 	_resMan(resMan),
 	_screen(screen),
 	_segMan(segMan),
-	_paint32(paint32),
 	_benchmarkingFinished(false),
 	_throttleFrameOut(true),
 	_showStyles(nullptr),
diff --git a/engines/sci/graphics/frameout.h b/engines/sci/graphics/frameout.h
index 4ebb754..53df5d9 100644
--- a/engines/sci/graphics/frameout.h
+++ b/engines/sci/graphics/frameout.h
@@ -149,10 +149,7 @@ struct ShowStyleEntry {
 typedef Common::Array<DrawList> ScreenItemListList;
 typedef Common::Array<RectList> EraseListList;
 
-class GfxCache;
 class GfxCoordAdjuster32;
-class GfxPaint32;
-class GfxPalette;
 class GfxScreen;
 
 /**
@@ -162,16 +159,14 @@ class GfxScreen;
 class GfxFrameout {
 private:
 	bool _isHiRes;
-	GfxCache *_cache;
 	GfxCoordAdjuster32 *_coordAdjuster;
 	GfxPalette32 *_palette;
 	ResourceManager *_resMan;
 	GfxScreen *_screen;
 	SegManager *_segMan;
-	GfxPaint32 *_paint32;
 
 public:
-	GfxFrameout(SegManager *segMan, ResourceManager *resMan, GfxCoordAdjuster *coordAdjuster, GfxCache *cache, GfxScreen *screen, GfxPalette32 *palette, GfxPaint32 *paint32);
+	GfxFrameout(SegManager *segMan, ResourceManager *resMan, GfxCoordAdjuster *coordAdjuster, GfxScreen *screen, GfxPalette32 *palette);
 	~GfxFrameout();
 
 	void clear();
diff --git a/engines/sci/sci.cpp b/engines/sci/sci.cpp
index c0f8f21..e650b4b 100644
--- a/engines/sci/sci.cpp
+++ b/engines/sci/sci.cpp
@@ -720,7 +720,7 @@ void SciEngine::initGraphics() {
 		_gfxCompare = new GfxCompare(_gamestate->_segMan, _gfxCache, _gfxScreen, _gfxCoordAdjuster);
 		_gfxPaint32 = new GfxPaint32(_gamestate->_segMan);
 		_robotDecoder = new RobotDecoder(getPlatform() == Common::kPlatformMacintosh);
-		_gfxFrameout = new GfxFrameout(_gamestate->_segMan, _resMan, _gfxCoordAdjuster, _gfxCache, _gfxScreen, _gfxPalette32, _gfxPaint32);
+		_gfxFrameout = new GfxFrameout(_gamestate->_segMan, _resMan, _gfxCoordAdjuster, _gfxScreen, _gfxPalette32);
 		_gfxText32 = new GfxText32(_gamestate->_segMan, _gfxCache);
 		_gfxControls32 = new GfxControls32(_gamestate->_segMan, _gfxCache, _gfxText32);
 		_gfxFrameout->run();






More information about the Scummvm-git-logs mailing list