diff --git a/audio/cms.cpp b/audio/cms.cpp
new file mode 100644
index 00000000000..64dc2d359f2
--- /dev/null
+++ b/audio/cms.cpp
@@ -0,0 +1,162 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#include "audio/cms.h"
+
+namespace CMS {
+
+CMS *Config::create() {
+ // For now this is fixed to the DOSBox emulator.
+ return new DOSBoxCMS();
+}
+
+bool CMS::_hasInstance = false;
+
+CMS::CMS() {
+ if (_hasInstance)
+ error("There are multiple CMS output instances running.");
+ _hasInstance = true;
+}
+
+CMS::~CMS() {
+ _hasInstance = false;
+}
+
+void CMS::start(TimerCallback *callback, int timerFrequency) {
+ _callback.reset(callback);
+ startCallbacks(timerFrequency);
+}
+
+void CMS::stop() {
+ stopCallbacks();
+ _callback.reset();
+}
+
+EmulatedCMS::EmulatedCMS() :
+ _nextTick(0),
+ _samplesPerTick(0),
+ _baseFreq(0),
+ _handle(new Audio::SoundHandle()) { }
+
+EmulatedCMS::~EmulatedCMS() {
+ // Stop callbacks, just in case. If it's still playing at this
+ // point, there's probably a bigger issue, though. The subclass
+ // needs to call stop() or the pointer can still use be used in
+ // the mixer thread at the same time.
+ stop();
+
+ delete _handle;
+}
+
+int EmulatedCMS::readBuffer(int16 *buffer, const int numSamples) {
+ int len = numSamples / 2;
+ int step;
+
+ do {
+ step = len;
+ if (step > (_nextTick >> FIXP_SHIFT))
+ step = (_nextTick >> FIXP_SHIFT);
+
+ generateSamples(buffer, step);
+
+ _nextTick -= step << FIXP_SHIFT;
+ if (!(_nextTick >> FIXP_SHIFT)) {
+ if (_callback && _callback->isValid())
+ (*_callback)();
+
+ _nextTick += _samplesPerTick;
+ }
+
+ buffer += step * 2;
+ len -= step;
+ } while (len);
+
+ return numSamples;
+}
+
+int EmulatedCMS::getRate() const {
+ return g_system->getMixer()->getOutputRate();
+}
+
+bool EmulatedCMS::endOfData() const {
+ return false;
+}
+
+bool EmulatedCMS::isStereo() const {
+ return true;
+}
+
+void EmulatedCMS::startCallbacks(int timerFrequency) {
+ setCallbackFrequency(timerFrequency);
+ g_system->getMixer()->playStream(Audio::Mixer::kMusicSoundType, _handle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
+}
+
+void EmulatedCMS::stopCallbacks() {
+ g_system->getMixer()->stopHandle(*_handle);
+}
+
+void EmulatedCMS::setCallbackFrequency(int timerFrequency) {
+ _baseFreq = timerFrequency;
+ assert(_baseFreq != 0);
+
+ int d = getRate() / _baseFreq;
+ int r = getRate() % _baseFreq;
+
+ // This is equivalent to (getRate() << FIXP_SHIFT) / BASE_FREQ
+ // but less prone to arithmetic overflow.
+
+ _samplesPerTick = (d << FIXP_SHIFT) + (r << FIXP_SHIFT) / _baseFreq;
+}
+
+DOSBoxCMS::DOSBoxCMS() : _cms(nullptr) { }
+
+DOSBoxCMS::~DOSBoxCMS() {
+ if (_cms)
+ delete _cms;
+}
+
+bool DOSBoxCMS::init() {
+ _cms = new CMSEmulator(getRate());
+ return _cms != nullptr;
+}
+
+void DOSBoxCMS::reset() {
+ _cms->reset();
+}
+
+void DOSBoxCMS::write(int a, int v) {
+ _cms->portWrite(a, v);
+}
+
+void DOSBoxCMS::writeReg(int r, int v) {
+ int address = 0x220;
+ if (r >= 0x100)
+ address += 0x002;
+
+ _cms->portWrite(address + 1, r & 0x1F);
+ _cms->portWrite(address, v);
+}
+
+void DOSBoxCMS::generateSamples(int16 *buffer, int numSamples) {
+ _cms->readBuffer(buffer, numSamples);
+}
+
+} // End of namespace CMS
diff --git a/audio/cms.h b/audio/cms.h
new file mode 100644
index 00000000000..fce144646f4
--- /dev/null
+++ b/audio/cms.h
@@ -0,0 +1,194 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+#ifndef AUDIO_CMS_H
+#define AUDIO_CMS_H
+
+#include "common/func.h"
+#include "common/ptr.h"
+
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+#include "audio/softsynth/cms.h"
+
+namespace CMS {
+
+class CMS;
+
+class Config {
+public:
+ /**
+ * Creates a CMS driver.
+ */
+ static CMS *create();
+};
+
+/**
+ * The type of the CMS timer callback functor.
+ */
+typedef Common::Functor0 TimerCallback;
+
+class CMS {
+private:
+ static bool _hasInstance;
+
+public:
+ // The default number of timer callbacks per second.
+ static const int DEFAULT_CALLBACK_FREQUENCY = 250;
+
+ CMS();
+ virtual ~CMS();
+
+ /**
+ * Initializes the CMS emulator.
+ *
+ * @return true on success, false on failure
+ */
+ virtual bool init() = 0;
+
+ /**
+ * Reinitializes the CMS emulator
+ */
+ virtual void reset() = 0;
+
+ /**
+ * Writes a byte to the given I/O port. CMS responds to 2 sets of 2 ports:
+ * 0x220/0x222 - value for the 1st/2nd chip (channels 0-5/6-B)
+ * 0x221/0x223 - register for the 1st/2nd chip
+ *
+ * @param a port address
+ * @param v value, which will be written
+ */
+ virtual void write(int a, int v) = 0;
+
+ /**
+ * Function to directly write to a specific CMS register. We allow writing
+ * to secondary CMS chip registers by using register values >= 0x100.
+ *
+ * @param r hardware register number to write to
+ * @param v value, which will be written
+ */
+ virtual void writeReg(int r, int v) = 0;
+
+ /**
+ * Start the CMS with callbacks.
+ */
+ void start(TimerCallback *callback, int timerFrequency = DEFAULT_CALLBACK_FREQUENCY);
+
+ /**
+ * Stop the CMS
+ */
+ void stop();
+
+ /**
+ * Change the callback frequency. This must only be called from a
+ * timer proc.
+ */
+ virtual void setCallbackFrequency(int timerFrequency) = 0;
+
+protected:
+ /**
+ * Start the callbacks.
+ */
+ virtual void startCallbacks(int timerFrequency) = 0;
+
+ /**
+ * Stop the callbacks.
+ */
+ virtual void stopCallbacks() = 0;
+
+ /**
+ * The functor for callbacks.
+ */
+ Common::ScopedPtr _callback;
+};
+
+/**
+ * A CMS that represents an emulated CMS.
+ *
+ * This will send callbacks based on the number of samples
+ * decoded in readBuffer().
+ */
+class EmulatedCMS : public CMS, protected Audio::AudioStream {
+protected:
+ static const int FIXP_SHIFT = 16;
+
+public:
+ EmulatedCMS();
+ virtual ~EmulatedCMS();
+
+ // CMS API
+ void setCallbackFrequency(int timerFrequency) override;
+
+ // AudioStream API
+ int readBuffer(int16 *buffer, const int numSamples) override;
+ int getRate() const override;
+ bool endOfData() const override;
+ bool isStereo() const override;
+
+protected:
+ // CMS API
+ void startCallbacks(int timerFrequency) override;
+ void stopCallbacks() override;
+
+ /**
+ * Read up to 'length' samples.
+ *
+ * Data will be in native endianess, 16 bit per sample, signed. buffer will
+ * be filled with interleaved left and right channel samples, starting with
+ * a left sample. The requested number of samples is stereo samples, so if
+ * you request 2 samples, you will get a total of two left channel and two
+ * right channel samples.
+ */
+ virtual void generateSamples(int16 *buffer, int numSamples) = 0;
+
+private:
+ int _baseFreq;
+
+ int _nextTick;
+ int _samplesPerTick;
+
+ Audio::SoundHandle *_handle;
+};
+
+/**
+ * A CMS that represents the DOSBox CMS emulator.
+ */
+class DOSBoxCMS : public EmulatedCMS {
+public:
+ DOSBoxCMS();
+ ~DOSBoxCMS() override;
+
+ bool init() override;
+ void reset() override;
+ void write(int a, int v) override;
+ void writeReg(int r, int v) override;
+
+protected:
+ void generateSamples(int16 *buffer, int numSamples) override;
+
+private:
+ CMSEmulator *_cms;
+};
+
+} // End of namespace CMS
+
+#endif
diff --git a/audio/module.mk b/audio/module.mk
index 8519db4438c..5f670ab18ef 100644
--- a/audio/module.mk
+++ b/audio/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS := \
adlib_ms.o \
audiostream.o \
casio.o \
+ cms.o \
fmopl.o \
mididrv.o \
mididrv_ms.o \
diff --git a/audio/softsynth/cms.cpp b/audio/softsynth/cms.cpp
index 4426b3b8aca..1c390153270 100644
--- a/audio/softsynth/cms.cpp
+++ b/audio/softsynth/cms.cpp
@@ -124,6 +124,10 @@ void CMSEmulator::readBuffer(int16 *buffer, const int numSamples) {
update(1, &buffer[0], numSamples);
}
+void CMSEmulator::reset() {
+ memset(_saa1099, 0, sizeof(SAA1099) * 2);
+}
+
void CMSEmulator::envelope(int chip, int ch) {
SAA1099 *saa = &_saa1099[chip];
if (saa->env_enable[ch]) {
diff --git a/audio/softsynth/cms.h b/audio/softsynth/cms.h
index 7acdd01543b..950c272b6c2 100644
--- a/audio/softsynth/cms.h
+++ b/audio/softsynth/cms.h
@@ -77,6 +77,8 @@ public:
void portWrite(int port, int val);
void readBuffer(int16 *buffer, const int numSamples);
+ void reset();
+
private:
uint32 _sampleRate;