/*
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *
 *  Copyright (C) 2020-2024  The DOSBox Staging Team
 *  Copyright (C) 2002-2021  The DOSBox Team
 *
 *  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 "mixer.h"

#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <optional>
#include <sys/types.h>

#include <SDL.h>
#include <speex/speex_resampler.h>

#include "audio_vector.h"
#include "../capture/capture.h"
#include "channel_names.h"
#include "checks.h"
#include "control.h"
#include "cross.h"
#include "gus.h"
#include "hardware.h"
#include "lpt_dac.h"
#include "mapper.h"
#include "math_utils.h"
#include "mem.h"
#include "midi.h"
#include "pcspeaker.h"
#include "pic.h"
#include "ps1audio.h"
#include "reelmagic/player.h"
#include "ring_buffer.h"
#include "rwqueue.h"
#include "setup.h"
#include "string_utils.h"
#include "tandy_sound.h"
#include "timer.h"
#include "tracy.h"
#include "video.h"

#include "mverb/MVerb.h"
#include "tal-chorus/ChorusEngine.h"

CHECK_NARROWING();

// #define DEBUG_MIXER

constexpr auto DefaultSampleRateHz = 48000;

// Over how many milliseconds will we permit a signal to grow from
// zero up to peak amplitude? (recommended 10 to 20ms)
constexpr auto EnvelopeMaxExpansionOverMs = 15;

// Regardless if the signal needed to be eveloped or not, how long
// should the envelope monitor the initial signal? (recommended > 5s)
constexpr auto EnvelopeExpiresAfterSeconds = 10;

constexpr auto MaxPrebufferMs = 100;

template <class T, size_t ROWS, size_t COLS>
using matrix = std::array<std::array<T, COLS>, ROWS>;

using HighpassFilter = std::array<Iir::Butterworth::HighPass<2>, 2>;

using EmVerb = MVerb<float>;

struct CrossfeedSettings {
	CrossfeedPreset preset = CrossfeedPreset::None;
	float global_strength  = 0.0f;
};

struct ReverbSettings {
	EmVerb mverb = {};

	// MVerb does not have an integrated high-pass filter to shape
	// the low-end response like other reverbs. So we're adding one
	// here. This helps take control over low-frequency build-up,
	// resulting in a more pleasant sound.
	HighpassFilter highpass_filter = {};

	ReverbPreset preset            = ReverbPreset::None;
	float synthesizer_send_level   = 0.0f;
	float digital_audio_send_level = 0.0f;
	float highpass_cutoff_freq_hz  = 1.0f;

	void Setup(const float predelay, const float early_mix, const float size,
	           const float density, const float bandwidth_freq_hz,
	           const float decay, const float dampening_freq_hz,
	           const float synth_level, const float digital_level,
	           const float highpass_freq_hz, const int sample_rate_hz)
	{
		assert(highpass_freq_hz > 0);
		assert(sample_rate_hz > 0);

		synthesizer_send_level   = synth_level;
		digital_audio_send_level = digital_level;
		highpass_cutoff_freq_hz  = highpass_freq_hz;

		mverb.setParameter(EmVerb::PREDELAY, predelay);
		mverb.setParameter(EmVerb::EARLYMIX, early_mix);
		mverb.setParameter(EmVerb::SIZE, size);
		mverb.setParameter(EmVerb::DENSITY, density);
		mverb.setParameter(EmVerb::BANDWIDTHFREQ, bandwidth_freq_hz);
		mverb.setParameter(EmVerb::DECAY, decay);
		mverb.setParameter(EmVerb::DAMPINGFREQ, dampening_freq_hz);

		// Always max gain (no attenuation)
		mverb.setParameter(EmVerb::GAIN, 1.0f);

		// Always 100% wet output signal
		mverb.setParameter(EmVerb::MIX, 1.0f);

		mverb.setSampleRate(static_cast<float>(sample_rate_hz));

		for (auto& f : highpass_filter) {
			f.setup(sample_rate_hz, highpass_freq_hz);
		}
	}
};

struct ChorusSettings {
	ChorusEngine chorus_engine = ChorusEngine(DefaultSampleRateHz);

	ChorusPreset preset            = ChorusPreset::None;
	float synthesizer_send_level   = 0.0f;
	float digital_audio_send_level = 0.0f;

	void Setup(const float synth_level, const float digital_level,
	           const int sample_rate_hz)
	{
		assert(sample_rate_hz > 0);

		synthesizer_send_level   = synth_level;
		digital_audio_send_level = digital_level;

		chorus_engine.setSampleRate(static_cast<float>(sample_rate_hz));

		constexpr auto Chorus1Enabled  = true;
		constexpr auto Chorus2Disabled = false;
		chorus_engine.setEnablesChorus(Chorus1Enabled, Chorus2Disabled);

		// The chorus effect can only operates in 100% wet output mode,
		// so we don't need to configure it for that.
	}
};

// This shows up nicely as 50% and -6.00 dB in the MIXER command's output
constexpr auto Minus6db = 0.501f;

struct MixerSettings {
	RWQueue<AudioFrame> final_output{1};
	RWQueue<int16_t> capture_queue{1};

	std::thread thread = {};

	// The default master gain is -6dB (50% volume) to minimise the change
	// for clipping.
	AudioFrame master_volume = {Minus6db, Minus6db};

	// Output by mix_samples, to be enqueud into the final_output queue
	std::vector<AudioFrame> output_buffer = {};

	// Temporary mixing buffers
	std::vector<AudioFrame> reverb_buffer       = {};
	std::vector<AudioFrame> chorus_buffer       = {};
	std::vector<int16_t> capture_buffer         = {};
	std::vector<AudioFrame> fast_forward_buffer = {};

	std::map<std::string, MixerChannelPtr> channels = {};

	std::map<std::string, MixerChannelSettings> channel_settings_cache = {};

	std::atomic<bool> thread_should_quit = false;

	// Sample rate negotiated with SDL (technically, this is the rate of
	// sample *frames* per second).
	std::atomic<int> sample_rate_hz = 0;

	// Matches SDL AudioSpec.samples type
	int blocksize    = 0;
	int prebuffer_ms = 25;

	SDL_AudioDeviceID sdl_device = 0;

	std::atomic<MixerState> state = MixerState::Uninitialized;

	HighpassFilter highpass_filter = {};
	Compressor compressor          = {};
	bool do_compressor             = false;

	CrossfeedSettings crossfeed = {};
	bool do_crossfeed           = false;

	ReverbSettings reverb = {};
	bool do_reverb        = false;

	ChorusSettings chorus = {};
	bool do_chorus        = false;

	bool is_manually_muted = false;

	std::atomic<bool> fast_forward_mode = false;

	std::recursive_mutex mutex = {};
};

static struct MixerSettings mixer = {};

extern RWQueue<std::unique_ptr<AudioVector>> soundblaster_mixer_queue;

[[maybe_unused]] static const char* to_string(const ResampleMethod m)
{
	switch (m) {
	case ResampleMethod::LerpUpsampleOrResample:
		return "LERP upsample or resample";

	case ResampleMethod::ZeroOrderHoldAndResample:
		return "zero-order-hold and resample";

	case ResampleMethod::Resample: return "Resample";

	default: assertm(false, "Invalid ResampleMethod"); return "";
	}
}

void MixerChannel::SetLineoutMap(const StereoLine map)
{
	output_map = map;
}

StereoLine MixerChannel::GetLineoutMap() const
{
	return output_map;
}

static Section_prop* get_mixer_section()
{
	assert(control);

	auto section = static_cast<Section_prop*>(control->GetSection("mixer"));
	assert(section);

	return section;
}

int MIXER_GetPreBufferMs()
{
	assert(mixer.prebuffer_ms > 0);
	assert(mixer.prebuffer_ms <= MaxPrebufferMs);

	return mixer.prebuffer_ms;
}

int MIXER_GetSampleRate()
{
	const auto sample_rate_hz = mixer.sample_rate_hz.load();
	assert(sample_rate_hz > 0);
	return sample_rate_hz;
}

void MIXER_EnableFastForwardMode()
{
	mixer.fast_forward_mode = true;
}

void MIXER_DisableFastForwardMode()
{
	mixer.fast_forward_mode = false;
}

bool MIXER_FastForwardModeEnabled()
{
	return mixer.fast_forward_mode;
}

// The queues listed here are for audio devices that run on the main thread
// The mixer thread can be waiting on the main thread to produce audio in these queues
// We need to stop them before aquiring a mutex lock to avoid a deadlock
// These are called infrequently when global mixer state is changed (mostly on device init/destroy and in the MIXER command line program)
// Individual channels also have a mutex which can be safely aquired without stopping these queues
void MIXER_LockMixerThread()
{
	if (pc_speaker) {
		pc_speaker->output_queue.Stop();
	}
	if (tandy_dac) {
		tandy_dac->output_queue.Stop();
	}
	if (ps1_dac) {
		ps1_dac->output_queue.Stop();
	}
	if (lpt_dac) {
		lpt_dac->output_queue.Stop();
	}
	if (gus) {
		gus->output_queue.Stop();
	}
	soundblaster_mixer_queue.Stop();
	reel_magic_audio.output_queue.Stop();
	mixer.mutex.lock();
}

void MIXER_UnlockMixerThread()
{
	if (pc_speaker) {
		pc_speaker->output_queue.Start();
	}
	if (tandy_dac) {
		tandy_dac->output_queue.Start();
	}
	if (ps1_dac) {
		ps1_dac->output_queue.Start();
	}
	if (lpt_dac) {
		lpt_dac->output_queue.Start();
	}
	if (gus) {
		gus->output_queue.Start();
	}
	soundblaster_mixer_queue.Start();
	reel_magic_audio.output_queue.Start();
	mixer.mutex.unlock();
}

MixerChannel::MixerChannel(MIXER_Handler _handler, const char* _name,
                           const std::set<ChannelFeature>& _features)
        : sleeper(*this),
          name(_name),
          envelope(_name),
          handler(_handler),
          features(_features)
{
	do_sleep = HasFeature(ChannelFeature::Sleep);
}

bool MixerChannel::HasFeature(const ChannelFeature feature) const
{
	return features.find(feature) != features.end();
}

std::set<ChannelFeature> MixerChannel::GetFeatures() const
{
	return features;
}

bool StereoLine::operator==(const StereoLine other) const
{
	return left == other.left && right == other.right;
}

static void set_global_crossfeed(const MixerChannelPtr& channel)
{
	assert(channel);

	const auto apply_crossfeed = (channel->GetName() == ChannelName::Opl &&
	                              channel->HasFeature(ChannelFeature::Stereo)) ||
	                             (channel->GetName() == ChannelName::Cms);

	if (!mixer.do_crossfeed || !apply_crossfeed) {
		channel->SetCrossfeedStrength(0.0f);
	} else {
		channel->SetCrossfeedStrength(mixer.crossfeed.global_strength);
	}
}

static void set_global_reverb(const MixerChannelPtr& channel)
{
	assert(channel);

	if (!mixer.do_reverb || !channel->HasFeature(ChannelFeature::ReverbSend)) {
		channel->SetReverbLevel(0.0f);

	} else if (channel->HasFeature(ChannelFeature::Synthesizer)) {
		channel->SetReverbLevel(mixer.reverb.synthesizer_send_level);

	} else if (channel->HasFeature(ChannelFeature::DigitalAudio)) {
		channel->SetReverbLevel(mixer.reverb.digital_audio_send_level);
	}
}

static void set_global_chorus(const MixerChannelPtr& channel)
{
	assert(channel);

	if (!mixer.do_chorus || !channel->HasFeature(ChannelFeature::ChorusSend)) {
		channel->SetChorusLevel(0.0f);

	} else if (channel->HasFeature(ChannelFeature::Synthesizer)) {
		channel->SetChorusLevel(mixer.chorus.synthesizer_send_level);

	} else if (channel->HasFeature(ChannelFeature::DigitalAudio)) {
		channel->SetChorusLevel(mixer.chorus.digital_audio_send_level);
	}
}

static CrossfeedPreset crossfeed_pref_to_preset(const std::string& pref)
{
	const auto pref_has_bool = parse_bool_setting(pref);
	if (pref_has_bool) {
		return *pref_has_bool ? DefaultCrossfeedPreset
		                      : CrossfeedPreset::None;
	}
	if (pref == "light") {
		return CrossfeedPreset::Light;
	}
	if (pref == "normal") {
		return CrossfeedPreset::Normal;
	}
	if (pref == "strong") {
		return CrossfeedPreset::Strong;
	}

	// the conf system programmatically guarantees only the above prefs are
	// used
	LOG_WARNING("MIXER: Invalid 'crossfeed' setting: '%s', using 'off'",
	            pref.c_str());
	return CrossfeedPreset::None;
}

static const char* to_string(const CrossfeedPreset preset)
{
	switch (preset) {
	case CrossfeedPreset::None: return "off";
	case CrossfeedPreset::Light: return "light";
	case CrossfeedPreset::Normal: return "normal";
	case CrossfeedPreset::Strong: return "strong";
	default: assertm(false, "Invalid CrossfeedPreset"); return "";
	}
}

static void sync_crossfeed_setting(const CrossfeedPreset preset)
{
	const auto string_prop = get_mixer_section()->GetStringProp("crossfeed");
	string_prop->SetValue(to_string(preset));
}

CrossfeedPreset MIXER_GetCrossfeedPreset()
{
	return mixer.crossfeed.preset;
}

void MIXER_SetCrossfeedPreset(const CrossfeedPreset new_preset)
{
	MIXER_LockMixerThread();

	auto& c = mixer.crossfeed; // short-hand reference

	// Unchanged?
	if (new_preset == c.preset) {
		return;
	}
	// Set new
	assert(c.preset != new_preset);
	c.preset = new_preset;

	switch (c.preset) {
	case CrossfeedPreset::None: break;
	case CrossfeedPreset::Light: c.global_strength = 0.20f; break;
	case CrossfeedPreset::Normal: c.global_strength = 0.40f; break;
	case CrossfeedPreset::Strong: c.global_strength = 0.60f; break;
	}

	// Configure the channels
	mixer.do_crossfeed = (c.preset != CrossfeedPreset::None);

	// Configure the channels
	for (const auto& [_, channel] : mixer.channels) {
		set_global_crossfeed(channel);
	}

	sync_crossfeed_setting(c.preset);

	if (mixer.do_crossfeed) {
		LOG_MSG("MIXER: Crossfeed enabled ('%s' preset)",
		        to_string(c.preset));
	} else {
		LOG_MSG("MIXER: Crossfeed disabled");
	}

	MIXER_UnlockMixerThread();
}

static ReverbPreset reverb_pref_to_preset(const std::string& pref)
{
	const auto pref_has_bool = parse_bool_setting(pref);
	if (pref_has_bool) {
		return *pref_has_bool ? DefaultReverbPreset : ReverbPreset::None;
	}
	if (pref == "tiny") {
		return ReverbPreset::Tiny;
	}
	if (pref == "small") {
		return ReverbPreset::Small;
	}
	if (pref == "medium") {
		return ReverbPreset::Medium;
	}
	if (pref == "large") {
		return ReverbPreset::Large;
	}
	if (pref == "huge") {
		return ReverbPreset::Huge;
	}

	// the conf system programmatically guarantees only the above prefs are
	// used
	LOG_WARNING("MIXER: Invalid 'reverb' setting: '%s', using 'off'",
	            pref.c_str());
	return ReverbPreset::None;
}

static const char* to_string(const ReverbPreset preset)
{
	switch (preset) {
	case ReverbPreset::None: return "off";
	case ReverbPreset::Tiny: return "tiny";
	case ReverbPreset::Small: return "small";
	case ReverbPreset::Medium: return "medium";
	case ReverbPreset::Large: return "large";
	case ReverbPreset::Huge: return "huge";
	default: assertm(false, "Invalid ReverbPreset"); return "";
	}
}

static void sync_reverb_setting(const ReverbPreset preset)
{
	const auto string_prop = get_mixer_section()->GetStringProp("reverb");
	string_prop->SetValue(to_string(preset));
}

ReverbPreset MIXER_GetReverbPreset()
{
	return mixer.reverb.preset;
}

void MIXER_SetReverbPreset(const ReverbPreset new_preset)
{
	MIXER_LockMixerThread();

	auto& r = mixer.reverb; // short-hand reference

	r.preset = new_preset;

	const auto rate_hz = mixer.sample_rate_hz.load();

	// clang-format off
	switch (r.preset) { //             PREDLY EARLY  SIZE   DENSITY BW_FREQ DECAY  DAMP_LV SYN_LV DIG_LV HIPASS_HZ
	case ReverbPreset::Tiny:   r.Setup(0.00f, 1.00f, 0.05f, 0.50f,  0.50f,  0.00f, 1.00f,  0.65f, 0.65f, 200.0f, rate_hz); break;
	case ReverbPreset::Small:  r.Setup(0.00f, 1.00f, 0.17f, 0.42f,  0.50f,  0.50f, 0.70f,  0.40f, 0.08f, 200.0f, rate_hz); break;
	case ReverbPreset::Medium: r.Setup(0.00f, 0.75f, 0.50f, 0.50f,  0.95f,  0.42f, 0.21f,  0.54f, 0.07f, 170.0f, rate_hz); break;
	case ReverbPreset::Large:  r.Setup(0.00f, 0.75f, 0.75f, 0.50f,  0.95f,  0.52f, 0.21f,  0.70f, 0.05f, 140.0f, rate_hz); break;
	case ReverbPreset::Huge:   r.Setup(0.00f, 0.75f, 0.75f, 0.50f,  0.95f,  0.52f, 0.21f,  0.85f, 0.05f, 140.0f, rate_hz); break;
	case ReverbPreset::None:   break;
	}
	// clang-format on

	// Configure the channels
	mixer.do_reverb = (r.preset != ReverbPreset::None);

	for (const auto& [_, channel] : mixer.channels) {
		set_global_reverb(channel);
	}

	sync_reverb_setting(r.preset);

	if (mixer.do_reverb) {
		LOG_MSG("MIXER: Reverb enabled ('%s' preset)", to_string(r.preset));
	} else {
		LOG_MSG("MIXER: Reverb disabled");
	}

	MIXER_UnlockMixerThread();
}

static ChorusPreset chorus_pref_to_preset(const std::string& pref)
{
	const auto pref_has_bool = parse_bool_setting(pref);
	if (pref_has_bool) {
		return *pref_has_bool ? DefaultChorusPreset : ChorusPreset::None;
	}
	if (pref == "light") {
		return ChorusPreset::Light;
	}
	if (pref == "normal") {
		return ChorusPreset::Normal;
	}
	if (pref == "strong") {
		return ChorusPreset::Strong;
	}

	// the conf system programmatically guarantees only the above prefs are
	// used
	LOG_WARNING("MIXER: Invalid 'chorus' setting: '%s', using ' off'",
	            pref.c_str());
	return ChorusPreset::None;
}

static const char* to_string(const ChorusPreset preset)
{
	switch (preset) {
	case ChorusPreset::None: return "off";
	case ChorusPreset::Light: return "light";
	case ChorusPreset::Normal: return "normal";
	case ChorusPreset::Strong: return "strong";
	default: assertm(false, "Invalid ChorusPreset"); return "";
	}
}

static void sync_chorus_setting(const ChorusPreset preset)
{
	const auto string_prop = get_mixer_section()->GetStringProp("chorus");
	string_prop->SetValue(to_string(preset));
}

ChorusPreset MIXER_GetChorusPreset()
{
	return mixer.chorus.preset;
}

void MIXER_SetChorusPreset(const ChorusPreset new_preset)
{
	MIXER_LockMixerThread();

	auto& c = mixer.chorus; // short-hand reference

	// Unchanged?
	if (new_preset == c.preset) {
		return;
	}
	// Set new
	assert(c.preset != new_preset);
	c.preset = new_preset;

	const auto rate_hz = mixer.sample_rate_hz.load();

	// clang-format off
	switch (c.preset) { //             SYN_LV  DIG_LV
	case ChorusPreset::Light:  c.Setup(0.33f,  0.00f, rate_hz); break;
	case ChorusPreset::Normal: c.Setup(0.54f,  0.00f, rate_hz); break;
	case ChorusPreset::Strong: c.Setup(0.75f,  0.00f, rate_hz); break;
	case ChorusPreset::None:   break;
	}
	// clang-format on

	// Configure the channels
	mixer.do_chorus = (c.preset != ChorusPreset::None);

	for (const auto& [_, channel] : mixer.channels) {
		set_global_chorus(channel);
	}

	sync_chorus_setting(c.preset);

	if (mixer.do_chorus) {
		LOG_MSG("MIXER: Chorus enabled ('%s' preset)", to_string(c.preset));
	} else {
		LOG_MSG("MIXER: Chorus disabled");
	}

	MIXER_UnlockMixerThread();
}

static void init_compressor(const bool compressor_enabled)
{
	MIXER_LockMixerThread();
	mixer.do_compressor = compressor_enabled;
	if (!mixer.do_compressor) {
		MIXER_UnlockMixerThread();
		LOG_MSG("MIXER: Master compressor disabled");
		return;
	}

	const auto _0dbfs_sample_value = Max16BitSampleValue;
	const auto threshold_db        = -6.0f;
	const auto ratio               = 3.0f;
	const auto attack_time_ms      = 0.01f;
	const auto release_time_ms     = 5000.0f;
	const auto rms_window_ms       = 10.0;

	mixer.compressor.Configure(mixer.sample_rate_hz,
	                           _0dbfs_sample_value,
	                           threshold_db,
	                           ratio,
	                           attack_time_ms,
	                           release_time_ms,
	                           rms_window_ms);

	MIXER_UnlockMixerThread();
	LOG_MSG("MIXER: Master compressor enabled");
}

void MIXER_DeregisterChannel(MixerChannelPtr& channel_to_remove)
{
	if (!channel_to_remove) {
		return;
	}

	MIXER_LockMixerThread();

	auto it = mixer.channels.cbegin();
	while (it != mixer.channels.cend()) {
		const auto [name, channel] = *it;

		if (channel.get() == channel_to_remove.get()) {
			// Save channel settings to a cache so we can
			// restore them if the channel gets recreated later.
			// This is necessary to persist channel settings when
			// changing the `sbtype` , for example.
			mixer.channel_settings_cache[name] = channel->GetSettings();

			it = mixer.channels.erase(it);
			break;
		}
		++it;
	}

	MIXER_UnlockMixerThread();
}

MixerChannelPtr MIXER_AddChannel(MIXER_Handler handler,
                                 const int sample_rate_hz, const char* name,
                                 const std::set<ChannelFeature>& features)
{
	// We allow 0 for the UseMixerRate special value
	assert(sample_rate_hz >= 0);

	auto chan = std::make_shared<MixerChannel>(handler, name, features);
	chan->SetSampleRate(sample_rate_hz);
	chan->SetAppVolume({1.0f, 1.0f});

	const auto chan_rate_hz = chan->GetSampleRate();
	if (chan_rate_hz == mixer.sample_rate_hz) {
		LOG_MSG("%s: Operating at %d Hz without resampling", name, chan_rate_hz);
	} else {
		LOG_MSG("%s: Operating at %d Hz and %s to the output rate",
		        name,
		        chan_rate_hz,
		        chan_rate_hz > mixer.sample_rate_hz ? "downsampling"
		                                            : "upsampling");
	}

	// Try to restore saved channel settings from the cache first.
	if (mixer.channel_settings_cache.find(name) !=
	    mixer.channel_settings_cache.end()) {
		chan->SetSettings(mixer.channel_settings_cache[name]);
	} else {
		// If no saved settings exist, set the defaults.
		chan->Enable(false);

		chan->SetUserVolume({1.0f, 1.0f});

		// We're only dealing with stereo channels internally, so we
		// need to set the "stereo" line-out even for mono content.
		chan->SetChannelMap(StereoMap);

		set_global_crossfeed(chan);
		set_global_reverb(chan);
		set_global_chorus(chan);
	}

	MIXER_LockMixerThread();
	mixer.channels[name] = chan; // replace the old, if it exists
	MIXER_UnlockMixerThread();

	return chan;
}

MixerChannelPtr MIXER_FindChannel(const char* name)
{
	auto it = mixer.channels.find(name);

	if (it == mixer.channels.end()) {
		// Deprecated alias SPKR to PCSPEAKER
		if (std::string_view(name) == "SPKR") {
			LOG_WARNING(
			        "MIXER: 'SPKR' is deprecated due to inconsistent "
			        "naming, please use 'PCSPEAKER' instead");
			it = mixer.channels.find(ChannelName::PcSpeaker);

			// Deprecated alias FM to OPL
		} else if (std::string_view(name) == "FM") {
			LOG_WARNING(
			        "MIXER: 'FM' is deprecated due to inconsistent "
			        "naming, please use '%s' instead",
			        ChannelName::Opl);
			it = mixer.channels.find(ChannelName::Opl);
		}
	}

	const auto chan = (it != mixer.channels.end()) ? it->second : nullptr;

	return chan;
}

std::map<std::string, MixerChannelPtr>& MIXER_GetChannels()
{
	return mixer.channels;
}

void MixerChannel::Set0dbScalar(const float scalar)
{
	// Realistically we expect some channels might need a fixed boost
	// to get to 0dB, but others might need a range mapping, like from
	// a unity float [-1.0f, +1.0f] to  16-bit int [-32k,+32k] range.
	assert(scalar >= 0.0f && scalar <= static_cast<int16_t>(Max16BitSampleValue));

	db0_volume_gain = scalar;

	UpdateCombinedVolume();
}

void MixerChannel::UpdateCombinedVolume()
{
	std::lock_guard lock(mutex);
	combined_volume_gain = user_volume_gain * app_volume_gain * db0_volume_gain;
}

const AudioFrame MixerChannel::GetUserVolume() const
{
	return user_volume_gain;
}

void MixerChannel::SetUserVolume(const AudioFrame volume)
{
	// Allow unconstrained user-defined values
	user_volume_gain = volume;
	UpdateCombinedVolume();
}

const AudioFrame MixerChannel::GetAppVolume() const
{
	return app_volume_gain;
}

void MixerChannel::SetAppVolume(const AudioFrame volume)
{
	// Constrain application-defined volume between 0% and 100%
	auto clamp_to_unity = [](const float vol) {
		constexpr auto MinUnityVolume = 0.0f;
		constexpr auto MaxUnityVolume = 1.0f;
		return clamp(vol, MinUnityVolume, MaxUnityVolume);
	};
	app_volume_gain = {clamp_to_unity(volume.left),
	                   clamp_to_unity(volume.right)};
	UpdateCombinedVolume();

#ifdef DEBUG
	LOG_MSG("MIXER: %-7s channel: application requested volume "
	        "{%3.0f%%, %3.0f%%}, and was set to {%3.0f%%, %3.0f%%}",
	        name,
	        static_cast<double>(left),
	        static_cast<double>(right),
	        static_cast<double>(app_volume_gain.left  * 100.0f),
	        static_cast<double>(app_volume_gain.right * 100.0f));
#endif
}

const AudioFrame MIXER_GetMasterVolume()
{
	return mixer.master_volume;
}

void MIXER_SetMasterVolume(const AudioFrame volume)
{
	mixer.master_volume = volume;
}

void MixerChannel::SetChannelMap(const StereoLine map)
{
	assert(map.left == Left || map.left == Right);
	assert(map.right == Left || map.right == Right);
	channel_map = map;

#ifdef DEBUG
	LOG_MSG("MIXER: %-7s channel: application changed audio channel mapping to left=>%s and right=>%s",
	        name,
	        channel_map.left == Left ? "left" : "right",
	        channel_map.right == Left ? "left" : "right");
#endif
}

void MixerChannel::Enable(const bool should_enable)
{
	// Is the channel already in the desired state?
	if (is_enabled == should_enable) {
		return;
	}

	// Lock the channel before changing states
	std::lock_guard lock(mutex);

	// Prepare the channel to go dormant
	if (!should_enable) {
		// Clear the current counters and sample values to
		// start clean if/when this channel is re-enabled.
		// Samples can be buffered into disable channels, so
		// we don't perform this zero'ing in the enable phase.

		frames_needed = 0;
		audio_frames.clear();

		prev_frame = {0.0f, 0.0f};
		next_frame = {0.0f, 0.0f};

		ClearResampler();
	}

	is_enabled = should_enable;
}

// Depending on the resampling method and the channel, mixer and ZoH upsampler
// rates, the following scenarios are possible:
//
// LerpUpsampleOrResample
// ----------------------
//   - Linear interpolation resampling only if:
//         channel_rate_hz < mixer_rate_hz
//
//   - Speex resampling only if:
//         channel_rate_hz > mixer_rate_hz
//
//   - No resampling if:
//         channel_rate_hz == mixer_rate_hz
//
// ZeroOrderHoldAndResample
// ------------------------
//   - Speex resampling only if:
//         channel_rate_hz > zoh_target_freq_hz AND
//         channel_rate_hz != mixer_rate_hz
//
//   - neither ZoH upsampling nor Speex resampling if:
//         channel_rate_hz >= zoh_target_rate_hz AND
//         channel_rate_hz == mixer_rate_hz
//
//   - ZoH upsampling only if:
//         channel_rate_hz < zoh_target_freq_hz AND
//         zoh_target_rate_hz == mixer_rate_hz
//
//   - both ZoH upsampling AND Speex resampling if:
//         channel_rate_hz < zoh_target_rate_hz AND
//         zoh_target_rate_hz != mixer_rate_hz
//
// Resample
// --------
//   - Speex resampling if:
//   	   channel_rate_hz != mixer_rate_hz
//
void MixerChannel::ConfigureResampler()
{
#ifdef DEBUG_MIXER
	LOG_TRACE("%s: Configuring resampler", name.c_str());
#endif

	std::lock_guard lock(mutex);

	const auto channel_rate_hz = sample_rate_hz.load();
	const auto mixer_rate_hz   = mixer.sample_rate_hz.load();

	do_lerp_upsample = false;
	do_zoh_upsample  = false;
	do_resample      = false;

	auto configure_speex_resampler = [&](const int _in_rate_hz) {
		const spx_uint32_t in_rate_hz  = _in_rate_hz;
		const spx_uint32_t out_rate_hz = mixer_rate_hz;

		// Only init the resampler once
		if (!speex_resampler.state) {
			constexpr auto NumChannels     = 2; // always stereo
			constexpr auto ResampleQuality = 5;

			speex_resampler.state = speex_resampler_init(NumChannels,
			                                             in_rate_hz,
			                                             out_rate_hz,
			                                             ResampleQuality,
			                                             nullptr);
		}

		speex_resampler_set_rate(speex_resampler.state, in_rate_hz, out_rate_hz);

		LOG_DEBUG("%s: Speex resampler is on, input rate: %d Hz, output rate: %d Hz",
		          name.c_str(),
		          in_rate_hz,
		          out_rate_hz);
	};

	switch (resample_method) {
	case ResampleMethod::LerpUpsampleOrResample:
		if (channel_rate_hz < mixer_rate_hz) {
			do_lerp_upsample = true;
			InitLerpUpsamplerState();
#ifdef DEBUG_MIXER
			LOG_DEBUG("%s: Linear interpolation resampler is on",
			          name.c_str());
#endif
		} else if (channel_rate_hz > mixer_rate_hz) {
			do_resample = true;
			configure_speex_resampler(channel_rate_hz);

		} else {
			// channel_rate_hz == mixer_rate_hz
			// no resampling is needed
		}
		break;

	case ResampleMethod::ZeroOrderHoldAndResample:
		if (channel_rate_hz < zoh_upsampler.target_rate_hz) {
			do_zoh_upsample = true;
			InitZohUpsamplerState();
#ifdef DEBUG_MIXER
			LOG_DEBUG("%s: Zero-order-hold upsampler is on, target rate: %d Hz ",
			          name.c_str(),
			          zoh_upsampler.target_rate_hz);
#endif
			if (zoh_upsampler.target_rate_hz != mixer_rate_hz) {
				do_resample = true;
				configure_speex_resampler(zoh_upsampler.target_rate_hz);
			}

		} else {
			// channel_rate_hz >= zoh_upsampler.target_rate_hz
			// We cannot ZoH upsample, but we might need to resample
			//
			if (channel_rate_hz != mixer_rate_hz) {
				do_resample = true;
				configure_speex_resampler(channel_rate_hz);
			}
		}
		break;

	case ResampleMethod::Resample:
		if (channel_rate_hz != mixer_rate_hz) {
			do_resample = true;
			configure_speex_resampler(channel_rate_hz);
		}
		break;
	}
}

// Clear the resampler and prime its input queue with zeroes
void MixerChannel::ClearResampler()
{
	if (do_lerp_upsample) {
		InitLerpUpsamplerState();
	}
	if (do_zoh_upsample) {
		InitZohUpsamplerState();
	}
	if (do_resample) {
		assert(speex_resampler.state);
		speex_resampler_reset_mem(speex_resampler.state);
		speex_resampler_skip_zeros(speex_resampler.state);

#ifdef DEBUG_MIXER
		LOG_DEBUG("%s: Speex resampler cleared and primed %d-frame input queue",
		          name.c_str(),
		          speex_resampler_get_input_latency(speex_resampler.state));
#endif
	}
}

void MixerChannel::SetSampleRate(const int new_sample_rate_hz)
{
	// We allow 0 for the UseMixerRate special value
	assert(new_sample_rate_hz >= 0);

	// If the requested rate is zero, then avoid resampling by running the
	// channel at the mixer's rate
	const int target_rate_hz = (new_sample_rate_hz == UseMixerRate)
	                                 ? mixer.sample_rate_hz.load()
	                                 : new_sample_rate_hz;
	assert(target_rate_hz > 0);

	// Nothing to do: the channel is already running at the requested rate
	if (target_rate_hz == sample_rate_hz) {
		return;
	}

#ifdef DEBUG_MIXER
	LOG_DEBUG("%s: Changing rate from %d to %d Hz",
	          name.c_str(),
	          sample_rate_hz,
	          target_rate_hz);
#endif

	std::lock_guard lock(mutex);

	sample_rate_hz = target_rate_hz;

	envelope.Update(sample_rate_hz,
	                peak_amplitude,
	                EnvelopeMaxExpansionOverMs,
	                EnvelopeExpiresAfterSeconds);

	ConfigureResampler();
}

const std::string& MixerChannel::GetName() const
{
	return name;
}

int MixerChannel::GetSampleRate() const
{
	return sample_rate_hz;
}

static float get_mixer_frames_per_tick()
{
	return static_cast<float>(mixer.sample_rate_hz) / 1000.0f;
}

float MixerChannel::GetFramesPerTick() const
{
	const float stretch_factor = static_cast<float>(sample_rate_hz) / static_cast<float>(mixer.sample_rate_hz);
	return get_mixer_frames_per_tick() * stretch_factor;
}

float MixerChannel::GetFramesPerBlock() const
{
	const float stretch_factor = static_cast<float>(sample_rate_hz) / static_cast<float>(mixer.sample_rate_hz);
	return static_cast<float>(mixer.blocksize) * stretch_factor;
}

void MixerChannel::SetPeakAmplitude(const int peak)
{
	peak_amplitude = peak;
	envelope.Update(sample_rate_hz,
	                peak_amplitude,
	                EnvelopeMaxExpansionOverMs,
	                EnvelopeExpiresAfterSeconds);
}

void MixerChannel::Mix(const int frames_requested)
{
	assert(frames_requested >= 0);

	if (!is_enabled) {
		return;
	}

	frames_needed = frames_requested;

	while (frames_needed > audio_frames.size()) {
		std::unique_lock lock(mutex);

		auto stretch_factor = static_cast<float>(sample_rate_hz) /
						static_cast<float>(mixer.sample_rate_hz);

		auto frames_remaining = iceil(
		        static_cast<float>(frames_needed - audio_frames.size()) *
		        stretch_factor);

		// Avoid underflow
		if (frames_remaining <= 0) {
			break;
		}

		lock.unlock();
		handler(frames_remaining);
	}
}

void MixerChannel::AddSilence()
{
	std::lock_guard lock(mutex);

	if (audio_frames.size() < frames_needed) {
		if (prev_frame.left == 0.0f && prev_frame.right == 0.0f) {
			while (audio_frames.size() < frames_needed) {
				audio_frames.push_back({0.0f, 0.0f});
			}

			// Make sure the next samples are zero when they get
			// switched to prev
			next_frame = {0.0f, 0.0f};

		} else {
			bool stereo = last_samples_were_stereo;

			const auto mapped_output_left  = output_map.left;
			const auto mapped_output_right = output_map.right;

			while (audio_frames.size() < frames_needed) {
				// Fade gradually to silence to avoid clicks.
				// Maybe the fade factor f depends on the sample
				// rate.
				constexpr auto f = 4.0f;

				for (auto ch = 0; ch < 2; ++ch) {
					if (prev_frame[ch] > f) {
						next_frame[ch] = prev_frame[ch] - f;
					} else if (prev_frame[ch] < -f) {
						next_frame[ch] = prev_frame[ch] + f;
					} else {
						next_frame[ch] = 0.0f;
					}
				}

				const auto frame_with_gain =
				        (stereo ? prev_frame
				                : AudioFrame{prev_frame.left}) *
				        combined_volume_gain;

				AudioFrame out_frame = {};
				out_frame[mapped_output_left]  = frame_with_gain.left;
				out_frame[mapped_output_right] = frame_with_gain.right;

				audio_frames.push_back(out_frame);
				prev_frame = next_frame;
			}
		}
	}

	last_samples_were_silence = true;
}

static void log_filter_settings(const std::string& channel_name,
                                const std::string& filter_name, const int order,
                                const int cutoff_freq_hz)
{
	assert(order > 0);
	assert(cutoff_freq_hz > 0);

	constexpr auto DbPerOrder = 6;

	LOG_MSG("%s: %s filter enabled (%d dB/oct at %d Hz)",
	        channel_name.c_str(),
	        filter_name.c_str(),
	        order * DbPerOrder,
	        cutoff_freq_hz);
}

void MixerChannel::SetHighPassFilter(const FilterState state)
{
	std::lock_guard lock(mutex);
	filters.highpass.state = state;

	if (filters.highpass.state == FilterState::On) {
		assert(filters.highpass.order > 0);
		assert(filters.highpass.cutoff_freq_hz > 0);

		log_filter_settings(name,
		                    "High-pass",
		                    filters.highpass.order,
		                    filters.highpass.cutoff_freq_hz);
	}
}

void MixerChannel::SetLowPassFilter(const FilterState state)
{
	std::lock_guard lock(mutex);
	filters.lowpass.state = state;

	if (filters.lowpass.state == FilterState::On) {
		assert(filters.lowpass.order > 0);
		assert(filters.lowpass.cutoff_freq_hz > 0);

		log_filter_settings(name,
		                    "Low-pass",
		                    filters.lowpass.order,
		                    filters.lowpass.cutoff_freq_hz);
	}
}

FilterState MixerChannel::GetHighPassFilterState() const
{
	return filters.highpass.state;
}

FilterState MixerChannel::GetLowPassFilterState() const
{
	return filters.lowpass.state;
}

static int clamp_filter_cutoff_freq([[maybe_unused]] const std::string& channel_name,
                                    const int cutoff_freq_hz)
{
	assert(cutoff_freq_hz > 0);

	const auto max_cutoff_freq_hz = mixer.sample_rate_hz / 2 - 1;

	if (cutoff_freq_hz <= max_cutoff_freq_hz) {
		return cutoff_freq_hz;
	} else {
		LOG_DEBUG(
		        "%s: Filter cutoff frequency %d Hz is above half the "
		        "sample rate, clamping to %d Hz",
		        channel_name.c_str(),
		        cutoff_freq_hz,
		        max_cutoff_freq_hz);

		return max_cutoff_freq_hz;
	}
}

void MixerChannel::ConfigureHighPassFilter(const int order, const int _cutoff_freq_hz)
{
	assert(order > 0);
	assert(_cutoff_freq_hz > 0);

	std::lock_guard lock(mutex);

	const auto cutoff_freq_hz = clamp_filter_cutoff_freq(name, _cutoff_freq_hz);

	assert(order > 0 && order <= MaxFilterOrder);
	for (auto& f : filters.highpass.hpf) {
		f.setup(order, mixer.sample_rate_hz, cutoff_freq_hz);
	}

	filters.highpass.order          = order;
	filters.highpass.cutoff_freq_hz = cutoff_freq_hz;
}

void MixerChannel::ConfigureLowPassFilter(const int order, const int _cutoff_freq_hz)
{
	assert(order > 0);
	assert(_cutoff_freq_hz > 0);

	std::lock_guard lock(mutex);

	const auto cutoff_freq_hz = clamp_filter_cutoff_freq(name, _cutoff_freq_hz);

	assert(order > 0 && order <= MaxFilterOrder);
	for (auto& f : filters.lowpass.lpf) {
		f.setup(order, mixer.sample_rate_hz, cutoff_freq_hz);
	}

	filters.lowpass.order          = order;
	filters.lowpass.cutoff_freq_hz = cutoff_freq_hz;
}

// Tries to set custom filter settings from the passed in filter preferences.
// Returns true if the custom filters could be successfully set, false
// otherwise and disables all filters for the channel.
bool MixerChannel::TryParseAndSetCustomFilter(const std::string& filter_prefs)
{
	SetLowPassFilter(FilterState::Off);
	SetHighPassFilter(FilterState::Off);

	if (!(filter_prefs.starts_with("lpf") || filter_prefs.starts_with("hpf"))) {
		return false;
	}

	const auto parts = split_with_empties(filter_prefs, ' ');

	const auto single_filter = (parts.size() == 3);
	const auto dual_filter   = (parts.size() == 6);

	if (!(single_filter || dual_filter)) {
		LOG_WARNING(
		        "%s: Invalid custom filter definition: '%s'. Must be "
		        "specified in \"lpf|hpf ORDER CUTOFF_FREQUENCY\" format",
		        name.c_str(),
		        filter_prefs.c_str());
		return false;
	}

	auto set_filter = [&](const std::string& type_pref,
	                      const std::string& order_pref,
	                      const std::string& cutoff_freq_pref) {
		const auto filter_name = (type_pref == "lpf") ? "low-pass"
		                                              : "high-pass";

		int order;
		if (!sscanf(order_pref.c_str(), "%d", &order) || order < 1 ||
		    order > MaxFilterOrder) {
			LOG_WARNING(
			        "%s: Invalid custom %s filter order: '%s'. "
			        "Must be an integer between 1 and %d.",
			        name.c_str(),
			        filter_name,
			        order_pref.c_str(),
			        MaxFilterOrder);
			return false;
		}

		int cutoff_freq_hz;
		if (!sscanf(cutoff_freq_pref.c_str(), "%d", &cutoff_freq_hz) ||
		    cutoff_freq_hz <= 0) {
			LOG_WARNING(
			        "%s: Invalid custom %s filter cutoff frequency: '%s'. "
			        "Must be a positive number.",
			        name.c_str(),
			        filter_name,
			        cutoff_freq_pref.c_str());
			return false;
		}

		if (type_pref == "lpf") {
			ConfigureLowPassFilter(order, cutoff_freq_hz);
			SetLowPassFilter(FilterState::On);

		} else if (type_pref == "hpf") {
			ConfigureHighPassFilter(order, cutoff_freq_hz);
			SetHighPassFilter(FilterState::On);

		} else {
			LOG_WARNING("%s: Invalid custom filter type: '%s'. Must be either 'hpf' or 'lpf'.",
			            name.c_str(),
			            type_pref.c_str());
			return false;
		}
		return true;
	};

	if (single_filter) {
		auto i = 0;

		const auto filter_type           = parts[i++];
		const auto filter_order          = parts[i++];
		const auto filter_cutoff_freq_hz = parts[i++];

		return set_filter(filter_type, filter_order, filter_cutoff_freq_hz);
	} else {
		auto i = 0;

		const auto filter1_type           = parts[i++];
		const auto filter1_order          = parts[i++];
		const auto filter1_cutoff_freq_hz = parts[i++];

		const auto filter2_type           = parts[i++];
		const auto filter2_order          = parts[i++];
		const auto filter2_cutoff_freq_hz = parts[i++];

		if (filter1_type == filter2_type) {
			LOG_WARNING(
			        "%s: Invalid custom filter definition: '%s'. "
			        "The two filters must be of different types.",
			        name.c_str(),
			        filter_prefs.c_str());
			return false;
		}

		if (!set_filter(filter1_type, filter1_order, filter1_cutoff_freq_hz)) {
			return false;
		}

		return set_filter(filter2_type, filter2_order, filter2_cutoff_freq_hz);
	}
}

void MixerChannel::SetZeroOrderHoldUpsamplerTargetRate(const int target_rate_hz)
{
	assert(target_rate_hz > 0);

	std::lock_guard lock(mutex);

	// TODO make sure that the ZOH target frequency cannot be set after the
	// filter has been configured
	zoh_upsampler.target_rate_hz = target_rate_hz;

#ifdef DEBUG_MIXER
	LOG_DEBUG("%s: Set zero-order-hold upsampler target rate to %d Hz",
	          name.c_str(),
	          target_rate_hz);
#endif

	ConfigureResampler();
}

void MixerChannel::InitZohUpsamplerState()
{
	assert(sample_rate_hz < zoh_upsampler.target_rate_hz);

	zoh_upsampler.step = static_cast<float>(sample_rate_hz) /
	                     static_cast<float>(zoh_upsampler.target_rate_hz);
	assert(zoh_upsampler.step < 1.0f);

	zoh_upsampler.pos  = 0.0f;
}

void MixerChannel::InitLerpUpsamplerState()
{
	assert(sample_rate_hz < mixer.sample_rate_hz);

	lerp_upsampler.step = static_cast<float>(sample_rate_hz) /
	                      static_cast<float>(mixer.sample_rate_hz.load());
	assert(lerp_upsampler.step < 1.0f);

	lerp_upsampler.pos        = 0.0f;
	lerp_upsampler.last_frame = {0.0f, 0.0f};
}

void MixerChannel::SetResampleMethod(const ResampleMethod method)
{
#ifdef DEBUG_MIXER
	LOG_ERR("%s: Set resample method to %s", name.c_str(), to_string(method));
#endif

	std::lock_guard lock(mutex);

	resample_method = method;

	ConfigureResampler();
}

void MixerChannel::SetCrossfeedStrength(const float strength)
{
	std::lock_guard lock(mutex);

	assert(strength >= 0.0f);
	assert(strength <= 1.0f);

	do_crossfeed = (HasFeature(ChannelFeature::Stereo) && strength > 0.0f);

	if (!do_crossfeed) {
#ifdef DEBUG_MIXER
		LOG_DEBUG("%s: Crossfeed is off", name.c_str());
#endif
		crossfeed.strength = 0.0f;
		return;
	}

	crossfeed.strength = strength;

	// map [0, 1] range to [0.5, 0]
	auto p = (1.0f - strength) / 2.0f;

	constexpr auto Center = 0.5f;
	crossfeed.pan_left    = Center - p;
	crossfeed.pan_right   = Center + p;

#ifdef DEBUG_MIXER
	LOG_DEBUG("%s: Crossfeed strength: %.3f",
	          name.c_str(),
	          static_cast<double>(crossfeed.strength));
#endif
}

float MixerChannel::GetCrossfeedStrength() const
{
	return crossfeed.strength;
}

void MixerChannel::SetReverbLevel(const float level)
{
	std::lock_guard lock(mutex);

	constexpr auto LevelMin    = 0.0f;
	constexpr auto LevelMax    = 1.0f;
	constexpr auto LevelMinDb = -40.0f;
	constexpr auto LevelMaxDb = 0.0f;

	assert(level >= LevelMin);
	assert(level <= LevelMax);

	do_reverb_send = (HasFeature(ChannelFeature::ReverbSend) && level > LevelMin);

	if (!do_reverb_send) {
#ifdef DEBUG_MIXER
		LOG_DEBUG("%s: Reverb send is off", name.c_str());
#endif
		reverb.level     = LevelMin;
		reverb.send_gain = LevelMinDb;
		return;
	}

	reverb.level = level;

	const auto level_db = remap(LevelMin, LevelMax, LevelMinDb, LevelMaxDb, level);

	reverb.send_gain = decibel_to_gain(level_db);

#ifdef DEBUG_MIXER
	LOG_DEBUG("%s: Reverb send is on: level: %4.2f, level_db: %6.2f, gain: %4.2f",
	          name.c_str(),
	          level,
	          level_db,
	          reverb.send_gain);
#endif
}

float MixerChannel::GetReverbLevel() const
{
	return reverb.level;
}

void MixerChannel::SetChorusLevel(const float level)
{
	constexpr auto LevelMin    = 0.0f;
	constexpr auto LevelMax    = 1.0f;
	constexpr auto LevelMinDb = -24.0f;
	constexpr auto LevelMaxDb = 0.0f;

	assert(level >= LevelMin);
	assert(level <= LevelMax);

	do_chorus_send = (HasFeature(ChannelFeature::ChorusSend) && level > LevelMin);

	if (!do_chorus_send) {
#ifdef DEBUG_MIXER
		LOG_DEBUG("%s: Chorus send is off", name.c_str());
#endif
		chorus.level     = LevelMin;
		chorus.send_gain = LevelMinDb;
		return;
	}

	chorus.level = level;

	const auto level_db = remap(LevelMin, LevelMax, LevelMinDb, LevelMaxDb, level);

	chorus.send_gain = decibel_to_gain(level_db);

#ifdef DEBUG_MIXER
	LOG_DEBUG("%s: Chorus send is on: level: %4.2f, level_db: %6.2f, gain: %4.2f",
	          name.c_str(),
	          level,
	          level_db,
	          chorus.send_gain);
#endif
}

float MixerChannel::GetChorusLevel() const
{
	return chorus.level;
}

// Floating-point conversion from unsigned 8-bit to signed 16-bit.
// This is only used to populate a lookup table that's 20-fold faster.
static constexpr int16_t u8to16(const int u_val)
{
	assert(u_val >= 0 && u_val <= UINT8_MAX);
	const auto s_val = u_val - 128;
	if (s_val > 0) {
		constexpr auto Scalar = Max16BitSampleValue / 127.0;
		return static_cast<int16_t>(round(s_val * Scalar));
	}
	return static_cast<int16_t>(s_val * 256);
}

// 8-bit to 16-bit lookup tables
int16_t lut_u8to16[UINT8_MAX + 1] = {};

static constexpr int16_t* lut_s8to16 = lut_u8to16 + 128;

static constexpr void fill_8to16_lut()
{
	for (int i = 0; i <= UINT8_MAX; ++i) {
		lut_u8to16[i] = u8to16(i);
	}
}

template <class Type, bool stereo, bool signeddata, bool nativeorder>
AudioFrame MixerChannel::ConvertNextFrame(const Type* data, const int pos)
{
	assert(pos >= 0);

	AudioFrame frame = {};

	const auto left_pos  = pos * 2 + 0;
	const auto right_pos = pos * 2 + 1;

	if (sizeof(Type) == 1) {
		// Integer types
		// unsigned 8-bit
		if (!signeddata) {
			if (stereo) {
				frame.left  = lut_u8to16[static_cast<uint8_t>(data[left_pos])];
				frame.right = lut_u8to16[static_cast<uint8_t>(data[right_pos])];
			} else {
				frame.left = lut_u8to16[static_cast<uint8_t>(data[pos])];
			}
		}
		// signed 8-bit
		else {
			if (stereo) {
				frame.left  = lut_s8to16[static_cast<int8_t>(data[left_pos])];
				frame.right = lut_s8to16[static_cast<int8_t>(data[right_pos])];
			} else {
				frame.left = lut_s8to16[static_cast<int8_t>(data[pos])];
			}
		}
	} else {
		// 16-bit and 32-bit both contain 16-bit data internally
		if (signeddata) {
			if (stereo) {
				if (nativeorder) {
					frame.left  = static_cast<float>(data[left_pos]);
					frame.right = static_cast<float>(data[right_pos]);
				} else {
					auto host_pt0 = reinterpret_cast<const uint8_t* const>(
					        data + left_pos);
					auto host_pt1 = reinterpret_cast<const uint8_t* const>(
					        data + right_pos);

					if (sizeof(Type) == 2) {
						frame.left  = (int16_t)host_readw(host_pt0);
						frame.right = (int16_t)host_readw(host_pt1);
					} else {
						frame.left = static_cast<float>(
						        (int32_t)host_readd(host_pt0));
						frame.right = static_cast<float>(
						        (int32_t)host_readd(host_pt1));
					}
				}
			} else { // mono
				if (nativeorder) {
					frame.left = static_cast<float>(data[pos]);
				} else {
					auto host_pt = reinterpret_cast<const uint8_t* const>(
					        data + pos);

					if (sizeof(Type) == 2) {
						frame.left = (int16_t)host_readw(host_pt);
					} else {
						frame.left = static_cast<float>(
						        (int32_t)host_readd(host_pt));
					}
				}
			}
		} else { // unsigned
			const auto offs = 32768;
			if (stereo) {
				if (nativeorder) {
					frame.left = static_cast<float>(
					        static_cast<int>(data[left_pos]) -
					        offs);
					frame.right = static_cast<float>(
					        static_cast<int>(data[right_pos]) -
					        offs);
				} else {
					auto host_pt0 = reinterpret_cast<const uint8_t* const>(
					        data + left_pos);
					auto host_pt1 = reinterpret_cast<const uint8_t* const>(
					        data + right_pos);

					if (sizeof(Type) == 2) {
						frame.left = static_cast<float>(
						        static_cast<int>(host_readw(
						                host_pt0)) -
						        offs);
						frame.right = static_cast<float>(
						        static_cast<int>(host_readw(
						                host_pt1)) -
						        offs);
					} else {
						frame.left = static_cast<float>(
						        static_cast<int>(host_readd(
						                host_pt0)) -
						        offs);
						frame.right = static_cast<float>(
						        static_cast<int>(host_readd(
						                host_pt1)) -
						        offs);
					}
				}
			} else { // mono
				if (nativeorder) {
					frame.left = static_cast<float>(
					        static_cast<int>(data[pos]) - offs);
				} else {
					auto host_pt = reinterpret_cast<const uint8_t* const>(
					        data + pos);

					if (sizeof(Type) == 2) {
						frame.left = static_cast<float>(
						        static_cast<int>(host_readw(
						                host_pt)) -
						        offs);
					} else {
						frame.left = static_cast<float>(
						        static_cast<int>(host_readd(
						                host_pt)) -
						        offs);
					}
				}
			}
		}
	}
	return frame;
}

// Converts sample stream to floats, performs output channel mappings, removes
// clicks, and optionally performs zero-order-hold-upsampling.
template <class Type, bool stereo, bool signeddata, bool nativeorder>
void MixerChannel::ConvertSamplesAndMaybeZohUpsample(const Type* data,
                                                     const int num_frames)
{
	assert(num_frames >= 0);
	convert_buffer.clear();

	const auto mapped_output_left  = output_map.left;
	const auto mapped_output_right = output_map.right;

	const auto mapped_channel_left  = channel_map.left;
	const auto mapped_channel_right = channel_map.right;

	auto pos             = 0;

	while (pos < num_frames) {
		prev_frame = next_frame;

		if (std::is_same<Type, float>::value) {
			if (stereo) {
				next_frame.left = static_cast<float>(
				        data[pos * 2 + 0]);
				next_frame.right = static_cast<float>(
				        data[pos * 2 + 1]);
			} else {
				next_frame.left = static_cast<float>(data[pos]);
			}
		} else {
			next_frame = ConvertNextFrame<Type, stereo, signeddata, nativeorder>(
			        data, pos);
		}

		AudioFrame frame_with_gain = {};
		if (stereo) {
			frame_with_gain = {prev_frame[mapped_channel_left],
			                   prev_frame[mapped_channel_right]};
		} else {
			frame_with_gain = {prev_frame[mapped_channel_left]};
		}
		frame_with_gain *= combined_volume_gain;

		// Process initial samples through an expanding envelope to
		// prevent severe clicks and pops. Becomes a no-op when done.
		envelope.Process(stereo, frame_with_gain);

		AudioFrame out_frame = {};
		out_frame[mapped_output_left]  += frame_with_gain.left;
		out_frame[mapped_output_right] += frame_with_gain.right;

		convert_buffer.push_back(out_frame);

		if (do_zoh_upsample) {
			zoh_upsampler.pos += zoh_upsampler.step;
			if (zoh_upsampler.pos > 1.0f) {
				zoh_upsampler.pos -= 1.0f;
				++pos;
			}
		} else {
			++pos;
		}
	}
}

static spx_uint32_t estimate_max_out_frames(SpeexResamplerState* resampler_state,
                                            const spx_uint32_t in_frames)
{
	assert(resampler_state);
	assert(in_frames);

	spx_uint32_t ratio_num = 0;
	spx_uint32_t ratio_den = 0;
	speex_resampler_get_ratio(resampler_state, &ratio_num, &ratio_den);
	assert(ratio_num && ratio_den);

	return ceil_udivide(in_frames * ratio_den, ratio_num);
}

AudioFrame MixerChannel::ApplyCrossfeed(const AudioFrame frame) const
{
	// Pan mono sample using -6dB linear pan law in the stereo field
	// pan: 0.0 = left, 0.5 = center, 1.0 = right
	auto pan = [](const float sample, const float pan) -> AudioFrame {
		return {(1.0f - pan) * sample, pan * sample};
	};
	const auto a = pan(frame.left,  crossfeed.pan_left);
	const auto b = pan(frame.right, crossfeed.pan_right);
	return {a.left + b.left, a.right + b.right};
}

// Returns true if configuration succeeded and false otherwise
bool MixerChannel::ConfigureFadeOut(const std::string& prefs)
{
	std::lock_guard lock(mutex);
	return sleeper.ConfigureFadeOut(prefs);
}

// Returns true if configuration succeeded and false otherwise
bool MixerChannel::Sleeper::ConfigureFadeOut(const std::string& prefs)
{
	auto set_wait_and_fade = [&](const int wait_ms, const int fade_ms) {
		fadeout_or_sleep_after_ms = wait_ms;

		fadeout_decrement_per_ms = 1.0f / static_cast<float>(fade_ms);

		LOG_MSG("%s: Fade-out enabled (wait %d ms then fade for %d ms)",
		        channel.GetName().c_str(),
		        wait_ms,
		        fade_ms);
	};
	// Disable fade-out (default)
	if (has_false(prefs)) {
		wants_fadeout = false;
		return true;
	}
	// Enable fade-out with defaults
	if (has_true(prefs)) {
		set_wait_and_fade(DefaultWaitMs, DefaultWaitMs);
		wants_fadeout = true;
		return true;
	}

	// Let the fade-out last between 10 ms and 3 seconds.
	constexpr auto MinFadeMs = 10;
	constexpr auto MaxFadeMs = 3000;

	// Custom setting in 'WAIT FADE' syntax, where both are milliseconds.
	if (auto prefs_vec = split(prefs); prefs_vec.size() == 2) {
		const auto wait_ms = parse_int(prefs_vec[0]);
		const auto fade_ms = parse_int(prefs_vec[1]);
		if (wait_ms && fade_ms) {
			const auto wait_is_valid = (*wait_ms >= MinWaitMs &&
			                            *wait_ms <= MaxWaitMs);

			const auto fade_is_valid = (*fade_ms >= MinFadeMs &&
			                            *fade_ms <= MaxFadeMs);

			if (wait_is_valid && fade_is_valid) {
				set_wait_and_fade(*wait_ms, *fade_ms);
				wants_fadeout = true;
				return true;
			}
		}
	}
	// Otherwise inform the user and disable the fade
	LOG_WARNING(
	        "%s: Invalid custom fade-out definition: '%s'. Must be "
	        "specified in \"WAIT FADE\" format where WAIT is between "
	        "%d and %d (in milliseconds) and FADE is between %d and "
	        "%d (in milliseconds); using 'off'.",
	        channel.GetName().c_str(),
	        prefs.c_str(),
	        MinWaitMs,
	        MaxWaitMs,
	        MinFadeMs,
	        MaxFadeMs);

	wants_fadeout = false;
	return false;
}

void MixerChannel::Sleeper::DecrementFadeLevel(const int awake_for_ms)
{
	assert(awake_for_ms >= 0);
	assert(awake_for_ms >= fadeout_or_sleep_after_ms);
	const auto elapsed_fade_ms = static_cast<float>(
	        awake_for_ms - fadeout_or_sleep_after_ms);

	const auto decrement = fadeout_decrement_per_ms * elapsed_fade_ms;

	constexpr auto MinLevel = 0.0f;
	constexpr auto MaxLevel = 1.0f;
	fadeout_level = std::clamp(MaxLevel - decrement, MinLevel, MaxLevel);
}

MixerChannel::Sleeper::Sleeper(MixerChannel& c, const int sleep_after_ms)
        : channel(c),
          fadeout_or_sleep_after_ms(sleep_after_ms)
{
	assert(sleep_after_ms >= 0);

	// The constructed sleep period is programmatically controlled (so assert)
	assert(fadeout_or_sleep_after_ms >= MinWaitMs);
	assert(fadeout_or_sleep_after_ms <= MaxWaitMs);
}

// Either fades the frame or checks if the channel had any signal output.
AudioFrame MixerChannel::Sleeper::MaybeFadeOrListen(const AudioFrame& frame)
{
	if (wants_fadeout) {
		// When fading, we actively drive down the channel level
		return frame * fadeout_level;
	}
	if (!had_signal) {
		// Otherwise, we simply passively listen for signal
		constexpr auto SilenceThreshold = 1.0f;
		had_signal = fabsf(frame.left)  > SilenceThreshold ||
		             fabsf(frame.right) > SilenceThreshold;
	}
	return frame;
}

void MixerChannel::Sleeper::MaybeSleep()
{
	// A signed integer can a durration of ~24 days in milliseconds, which is
	// surely more than enough.
	const auto awake_for_ms = check_cast<int>(GetTicksSince(woken_at_ms));

	// Not enough time has passed.. .. try to sleep later
	if (awake_for_ms < fadeout_or_sleep_after_ms) {
		return;
	}
	if (wants_fadeout) {
		// The channel is still fading out.. try to sleep later
		if (fadeout_level > 0.0f) {
			DecrementFadeLevel(awake_for_ms);
			return;
		}
	} else if (had_signal) {
		// The channel is still producing a signal.. so stay away
		WakeUp();
		return;
	}
	channel.Enable(false);
	// LOG_INFO("MIXER: %s fell asleep", channel.name.c_str());
}

// Returns true when actually awoken otherwise false if already awake.
bool MixerChannel::Sleeper::WakeUp()
{
	// Always reset for another round of awakeness
	woken_at_ms   = GetTicks();
	fadeout_level = 1.0f;
	had_signal    = false;

	const auto was_sleeping = !channel.is_enabled;
	if (was_sleeping) {
		channel.Enable(true);
		// LOG_INFO("MIXER: %s woke up", channel.name.c_str());
	}
	return was_sleeping;
}

// Audio devices that use the sleep feature need to wake up the channel whenever
// they might prepare new samples for it. Typically this is on IO port
// writes into the card.
bool MixerChannel::WakeUp()
{
	assert(do_sleep);
	std::lock_guard lock(mutex);
	return sleeper.WakeUp();
}

template <class Type, bool stereo, bool signeddata, bool nativeorder>
void MixerChannel::AddSamples(const int num_frames, const Type* data)
{
	assert(num_frames >= 0);

	if (num_frames <= 0) {
		return;
	}

	std::lock_guard lock(mutex);

	last_samples_were_stereo = stereo;

	// All possible resampling scenarios:
	//
	// - No upsampling or resampling
	// - LERP  upsampling only
	// - ZoH   upsampling only
	// - Speex resampling only
	// - ZoH upsampling followed by Speex resampling

	// Zero-order-hold upsampling is performed in
	// ConvertSamplesAndMaybeZohUpsample to reduce the number of temporary
	// buffers and to simplify the code.
	//

	// Assert that we're not attempting to do both LERP and Speex resample
	// We can do one or neither
	assert((do_lerp_upsample && !do_resample) || (!do_lerp_upsample && do_resample) || (!do_lerp_upsample && !do_resample));

	ConvertSamplesAndMaybeZohUpsample<Type, stereo, signeddata, nativeorder>(data, num_frames);

	// Starting index this function will start writing to
	// The audio_frames vector can contain previously converted/resampled audio
	const size_t audio_frames_starting_size = audio_frames.size();

	if (do_lerp_upsample) {
		assert(!do_resample);

		auto& s = lerp_upsampler;

		for (size_t i = 0; i < convert_buffer.size();) {
			const auto curr_frame = convert_buffer[i];

			assert(s.pos >= 0.0f && s.pos <= 1.0f);
			AudioFrame lerped_frame = {};
			lerped_frame.left = lerp(s.last_frame.left,
			                           curr_frame.left,
			                           s.pos);

			lerped_frame.right = lerp(s.last_frame.right,
			                            curr_frame.right,
			                            s.pos);

			audio_frames.push_back(lerped_frame);

			s.pos += s.step;
#if 0
			LOG_DEBUG(
			        "%s: AddSamples last %.1f:%.1f curr %.1f:%.1f"
			        " -> out %.1f:%.1f, pos=%.2f, step=%.2f",
			        name.c_str(),
			        s.last_frame.left,
			        s.last_frame.right,
			        curr_frame.left,
			        curr_frame.right,
			        out_left,
			        out_right,
			        s.pos,
			        s.step);
#endif
			if (s.pos > 1.0f) {
				s.pos -= 1.0f;
				s.last_frame = curr_frame;

				// Move to the next input frame
				i += 1;
			}
		}
	} else if (do_resample) {
		auto in_frames = check_cast<spx_uint32_t>(convert_buffer.size());

		auto out_frames = check_cast<spx_uint32_t>(
		        estimate_max_out_frames(speex_resampler.state, in_frames));

		// Store this as a temporary variable
		// out_frames gets modified by Speex to reflect the actual frames it wrote
		const auto estimated_frames = out_frames;

		audio_frames.resize(audio_frames_starting_size + estimated_frames);

		// These are vectors of AudioFrame which is just 2 packed floats
		const auto input_ptr = reinterpret_cast<const float*>(convert_buffer.data());
		auto output_ptr = reinterpret_cast<float*>(audio_frames.data() + audio_frames_starting_size);

		speex_resampler_process_interleaved_float(speex_resampler.state,
		                                          input_ptr,
		                                          &in_frames,
		                                          output_ptr,
		                                          &out_frames);

		// 'out_frames' now contains the actual number of
		// resampled frames, so ensure the number of output frames
		// is within the logical size.
		assert(out_frames <= estimated_frames);
		audio_frames.resize(audio_frames_starting_size + out_frames); // only shrinks
	} else {
		audio_frames.insert(audio_frames.end(), convert_buffer.begin(), convert_buffer.end());
	}

	// Optionally filter, apply crossfeed
	// Runs in-place over newly added frames
	for (size_t i = audio_frames_starting_size; i < audio_frames.size(); ++i) {
		if (filters.highpass.state == FilterState::On) {
			audio_frames[i] = {filters.highpass.hpf[0].filter(audio_frames[i].left),
			                   filters.highpass.hpf[1].filter(audio_frames[i].right)};
		}
		if (filters.lowpass.state == FilterState::On) {
			audio_frames[i] = {filters.lowpass.lpf[0].filter(audio_frames[i].left),
			                   filters.lowpass.lpf[1].filter(audio_frames[i].right)};
		}

		if (do_crossfeed) {
			audio_frames[i] = ApplyCrossfeed(audio_frames[i]);
		}
	}
}

// TODO Move this into the Sound Blaster code.
// This is only called in the "Direct DAC" (non-DMA) operational mode of the
// Sound Blaster where the DAC is controlled by the CPU.
//
void MixerChannel::AddStretched(const int len, int16_t* data)
{
	assert(len >= 0);

	std::lock_guard lock(mutex);

	// Stretch mono input stream into a tick's worth of frames
	static float frame_counter = 0.0f;
	frame_counter += get_mixer_frames_per_tick();
	int frames_remaining = ifloor(frame_counter);
	frame_counter -= static_cast<float>(frames_remaining);
	assert(frames_remaining >= 0);

	// Used for time-stretching the audio
	float pos = 0;
	float step = static_cast<float>(len) / static_cast<float>(frames_remaining);

	const auto mapped_output_left  = output_map.left;
	const auto mapped_output_right = output_map.right;

	while (frames_remaining--) {
		auto prev_sample = prev_frame.left;
		auto curr_sample = static_cast<float>(*data);
		float out_sample = 0;

		switch (resample_method) {
		case ResampleMethod::LerpUpsampleOrResample:
		case ResampleMethod::Resample:
			out_sample = lerp(prev_sample, curr_sample, pos);
			break;

		case ResampleMethod::ZeroOrderHoldAndResample:
			out_sample = curr_sample;
		}

		auto frame_with_gain = AudioFrame{out_sample} * combined_volume_gain;

		AudioFrame out_frame           = {};
		out_frame[mapped_output_left]  = frame_with_gain.left;
		out_frame[mapped_output_right] = frame_with_gain.right;

		audio_frames.push_back(out_frame);

		// Advance input position
		pos += step;
		if (pos > 1.0f) {
			pos -= 1.0f;
			prev_frame = {curr_sample};
			++data;
		}
	}
}

void MixerChannel::AddSamples_m8(const int num_frames, const uint8_t* data)
{
	AddSamples<uint8_t, false, false, true>(num_frames, data);
}

void MixerChannel::AddSamples_s8(const int num_frames, const uint8_t* data)
{
	AddSamples<uint8_t, true, false, true>(num_frames, data);
}

void MixerChannel::AddSamples_m8s(const int num_frames, const int8_t* data)
{
	AddSamples<int8_t, false, true, true>(num_frames, data);
}

void MixerChannel::AddSamples_s8s(const int num_frames, const int8_t* data)
{
	AddSamples<int8_t, true, true, true>(num_frames, data);
}

void MixerChannel::AddSamples_m16(const int num_frames, const int16_t* data)
{
	AddSamples<int16_t, false, true, true>(num_frames, data);
}

void MixerChannel::AddSamples_s16(const int num_frames, const int16_t* data)
{
	AddSamples<int16_t, true, true, true>(num_frames, data);
}

void MixerChannel::AddSamples_m16u(const int num_frames, const uint16_t* data)
{
	AddSamples<uint16_t, false, false, true>(num_frames, data);
}

void MixerChannel::AddSamples_s16u(const int num_frames, const uint16_t* data)
{
	AddSamples<uint16_t, true, false, true>(num_frames, data);
}

void MixerChannel::AddSamples_mfloat(const int num_frames, const float* data)
{
	AddSamples<float, false, true, true>(num_frames, data);
}

void MixerChannel::AddSamples_sfloat(const int num_frames, const float* data)
{
	AddSamples<float, true, true, true>(num_frames, data);
}

void MixerChannel::AddSamples_m16_nonnative(const int num_frames, const int16_t* data)
{
	AddSamples<int16_t, false, true, false>(num_frames, data);
}

void MixerChannel::AddSamples_s16_nonnative(const int num_frames, const int16_t* data)
{
	AddSamples<int16_t, true, true, false>(num_frames, data);
}

void MixerChannel::AddSamples_m16u_nonnative(const int num_frames, const uint16_t* data)
{
	AddSamples<uint16_t, false, false, false>(num_frames, data);
}

void MixerChannel::AddSamples_s16u_nonnative(const int num_frames, const uint16_t* data)
{
	AddSamples<uint16_t, true, false, false>(num_frames, data);
}

std::string MixerChannel::DescribeLineout() const
{
	if (!HasFeature(ChannelFeature::Stereo)) {
		return MSG_Get("SHELL_CMD_MIXER_CHANNEL_MONO");
	}
	if (output_map == StereoMap) {
		return MSG_Get("SHELL_CMD_MIXER_CHANNEL_STEREO");
	}
	if (output_map == ReverseMap) {
		return MSG_Get("SHELL_CMD_MIXER_CHANNEL_REVERSE");
	}

	// Output_map is programmtically set (not directly assigned from user
	// data), so we can assert.
	assertm(false, "Unknown lineout mode");
	return "unknown";
}

MixerChannelSettings MixerChannel::GetSettings() const
{
	MixerChannelSettings s = {};

	s.is_enabled         = is_enabled;
	s.user_volume        = GetUserVolume();
	s.lineout_map        = GetLineoutMap();
	s.crossfeed_strength = GetCrossfeedStrength();
	s.reverb_level       = GetReverbLevel();
	s.chorus_level       = GetChorusLevel();

	return s;
}

void MixerChannel::SetSettings(const MixerChannelSettings& s)
{
	is_enabled = s.is_enabled;

	SetUserVolume(s.user_volume);
	SetLineoutMap(s.lineout_map);

	if (mixer.do_crossfeed) {
		SetCrossfeedStrength(s.crossfeed_strength);
	}
	if (mixer.do_reverb) {
		SetReverbLevel(s.reverb_level);
	}
	if (mixer.do_chorus) {
		SetChorusLevel(s.chorus_level);
	}
}

MixerChannel::~MixerChannel()
{
	if (speex_resampler.state) {
		speex_resampler_destroy(speex_resampler.state);
		speex_resampler.state = nullptr;
	}
}

// We use floats in the range of 16 bit integers everywhere.
// SDL expects floats to be normalized from 1.0 to -1.0
// It might be better for us to use normalized floats elsewhere in the future.
// For now, that probably breaks some assumptions elsewhere in the mixer.
// So just normalize as a final step before sending the data to SDL.
static float normalize_sample(float sample)
{
	return sample / 32768.0f;
}

// Mix a certain amount of new sample frames
static void mix_samples(const int frames_requested)
{
	assert(frames_requested >= 0);

	mixer.output_buffer.clear();
	mixer.output_buffer.resize(frames_requested);
	mixer.reverb_buffer.clear();
	mixer.reverb_buffer.resize(frames_requested);
	mixer.chorus_buffer.clear();
	mixer.chorus_buffer.resize(frames_requested);

	// Render all channels and accumulate results in the master mixbuffer
	for (const auto& [_, channel] : mixer.channels) {
		channel->Mix(frames_requested);
		std::lock_guard lock(channel->mutex);
		const size_t num_frames = std::min(mixer.output_buffer.size(), channel->audio_frames.size());
		for (size_t i = 0; i < num_frames; ++i) {
			if (channel->do_sleep) {
				mixer.output_buffer[i] += channel->sleeper.MaybeFadeOrListen(channel->audio_frames[i]);
			} else {
				mixer.output_buffer[i] += channel->audio_frames[i];
			}

			if (mixer.do_reverb && channel->do_reverb_send) {
				mixer.reverb_buffer[i] += channel->audio_frames[i] * channel->reverb.send_gain;
			}

			if (mixer.do_chorus && channel->do_chorus_send) {
				mixer.chorus_buffer[i] += channel->audio_frames[i] * channel->chorus.send_gain;
			}
		}
		channel->audio_frames.erase(channel->audio_frames.begin(), channel->audio_frames.begin() + num_frames);
		if (channel->do_sleep) {
			channel->sleeper.MaybeSleep();
		}
	}

	if (mixer.do_reverb) {
		for (size_t i = 0; i < mixer.reverb_buffer.size(); ++i) {
			// High-pass filter the reverb input
			auto& hpf = mixer.reverb.highpass_filter;

			// MVerb operates on two non-interleaved sample streams
			AudioFrame in_frame = mixer.reverb_buffer[i];
			in_frame.left = hpf[0].filter(in_frame.left);
			in_frame.right = hpf[1].filter(in_frame.right);

			float* in_buf[2] = {&in_frame.left, &in_frame.right};

			AudioFrame out_frame = {};
			float* out_buf[2] = {&out_frame.left, &out_frame.right};

			constexpr auto NumFrames = 1;
			mixer.reverb.mverb.process(in_buf, out_buf, NumFrames);

			mixer.output_buffer[i] += out_frame;
		}
	}

	if (mixer.do_chorus) {
		// Apply chorus effect to the chorus aux buffer, then mix the
		// results to the master output
		for (size_t i = 0; i < mixer.chorus_buffer.size(); ++i) {
			auto frame = mixer.chorus_buffer[i];
			mixer.chorus.chorus_engine.process(&frame.left, &frame.right);
			mixer.output_buffer[i] += frame;
		}
	}

	// Apply high-pass filter to the master output
	for (auto& frame : mixer.output_buffer) {
		auto& hpf = mixer.highpass_filter;
		frame = {
			hpf[0].filter(frame.left),
			hpf[1].filter(frame.right)
		};
	}

	// Apply master gain
	for (auto& frame : mixer.output_buffer) {
		frame *= mixer.master_volume;
	}

	if (mixer.do_compressor) {
		// Apply compressor to the master output as the very last step
		for (auto& frame: mixer.output_buffer) {
			frame = mixer.compressor.Process(frame);
		}
	}

	// Capture audio output if requested
	if (CAPTURE_IsCapturingAudio() || CAPTURE_IsCapturingVideo()) {
		mixer.capture_buffer.clear();
		mixer.capture_buffer.reserve(mixer.output_buffer.size() * 2);

		for (const auto frame : mixer.output_buffer) {
			const auto left = static_cast<uint16_t>(
			        clamp_to_int16(static_cast<int>(frame.left)));

			const auto right = static_cast<uint16_t>(
			        clamp_to_int16(static_cast<int>(frame.right)));

			mixer.capture_buffer.push_back(static_cast<int16_t>(host_to_le16(left)));
			mixer.capture_buffer.push_back(static_cast<int16_t>(host_to_le16(right)));
		}

		if (mixer.capture_queue.Size() + mixer.capture_buffer.size() > mixer.capture_queue.MaxCapacity()) {
			// We're producing more audio than the capture is consuming.
			// This usually happens when the main thread is being slowed down by video encoding.
			// Ex: Slow host CPU or using zlib rather than zlib_ng
			// Not ideal as this results in an audible "skip forward"
			// Without this, it's a complete stuttery mess though so it's the lesser of two evils
			mixer.capture_queue.Clear();
		}
		mixer.capture_queue.NonblockingBulkEnqueue(mixer.capture_buffer, mixer.capture_buffer.size());
	}

	// Normalize the final output before sending to SDL
	for (auto& frame: mixer.output_buffer) {
		frame.left = normalize_sample(frame.left);
		frame.right = normalize_sample(frame.right);
	}
}

// Run in the main thread by a PIC Callback
static void capture_callback()
{
	if (!(CAPTURE_IsCapturingAudio() || CAPTURE_IsCapturingVideo())) {
		return;
	}

	static float frame_counter = 0.0f;
	frame_counter += get_mixer_frames_per_tick();
	const int num_frames = ifloor(frame_counter);
	frame_counter -= static_cast<float>(num_frames);
	assert(num_frames > 0);

	const int num_samples = num_frames * 2;

	// We can't block waiting on the mixer thread
	// Some mixer channels block waiting on the main thread and this would deadlock
	std::vector<int16_t> frames = {};
	const int samples_available = check_cast<int>(mixer.capture_queue.Size());
	const int samples_requested = std::min(num_samples, samples_available);
	if (samples_requested > 0) {
		mixer.capture_queue.BulkDequeue(frames, std::min(num_samples, samples_available));
	}

	// Fill with silence if needed
	frames.resize(num_samples);

	CAPTURE_AddAudioData(mixer.sample_rate_hz, num_frames, frames.data());
}

static void SDLCALL mixer_callback([[maybe_unused]] void* userdata,
                                   Uint8* stream, int bytes_requested)
{
	assert(bytes_requested >= 0);

	ZoneScoped;
	memset(stream, 0, static_cast<size_t>(bytes_requested));

	constexpr auto BytesPer32BitSample = 4;
	constexpr auto BytesPerSampleFrame = BytesPer32BitSample * 2; // stereo

	const auto frames_requested = bytes_requested / BytesPerSampleFrame;

	// Mac OSX has been observed to be problematic if we ever block inside SDL's callback
	// This ensures that we do not block waiting for more audio
	// In the queue has run dry, we write what we have available and the rest of the request is silence
	const auto num_frames = std::min(check_cast<int>(mixer.final_output.Size()), frames_requested);
	if (num_frames > 0) {
		std::vector<AudioFrame> audio_frames = {};
		mixer.final_output.BulkDequeue(audio_frames, num_frames);
		memcpy(stream, audio_frames.data(), audio_frames.size() * sizeof(AudioFrame));
	}
}

static void mixer_thread_loop()
{
	double last_mixed = 0.0;
	while (!mixer.thread_should_quit) {
		std::unique_lock lock(mixer.mutex);
		assert(mixer.state != MixerState::Uninitialized);

		// This code is mostly for the fast-forward button (hold Alt + F12)
		const double now         = PIC_AtomicIndex();
		const double actual_time = now - last_mixed;
		const double expected_time = (static_cast<double>(mixer.blocksize) / static_cast<double>(mixer.sample_rate_hz)) * 1000.0;
		last_mixed = now;

		// "Underflow" is not a concern since moving to a threaded mixer
		// If the CPU is running slower than real-time, the audio drivers will naturally slow down the audio
		// Therefore, we can always request at least a blocksize worth of audio
		int frames_requested = mixer.blocksize;

		if (mixer.fast_forward_mode) {
			// Flag is set only by the fast-forward hotkey handler
			// Usually this means the emulation core is running much faster than real-time
			// We must consume more audio to "catch up" but always request at least a blocksize
			frames_requested = std::max(mixer.blocksize, ifloor(actual_time * get_mixer_frames_per_tick()));
		}

		mix_samples(frames_requested);
		assert(mixer.output_buffer.size() == check_cast<size_t>(frames_requested));

		lock.unlock();

		if (mixer.state == MixerState::NoSound) {
			// SDL callback is not running. Mixed sound gets discarded.
			// Sleep for the expected duration to simulate the time it would have taken to playback the audio.
			constexpr double NanosecondsPerMillisecond = 1000000.0;
			const uint64_t nap_time = static_cast<uint64_t>(expected_time * NanosecondsPerMillisecond);
			std::this_thread::sleep_for(std::chrono::nanoseconds(nap_time));
			continue;
		} else if (mixer.state == MixerState::Muted) {
			// SDL callback remains active. Enqueue silence.
			mixer.output_buffer.clear();
			mixer.output_buffer.resize(mixer.blocksize);
			mixer.final_output.BulkEnqueue(mixer.output_buffer, mixer.output_buffer.size());
			continue;
		}

		// Only true if we were in fast-forward mode at the time we calculated frames_requested
		// That variable could have changed by now but we need to always squash down to a blocksize of audio
		const bool audio_needs_squashing = frames_requested > mixer.blocksize;
		auto& to_mix = audio_needs_squashing ? mixer.fast_forward_buffer : mixer.output_buffer;

		if (audio_needs_squashing) {
			// This is "chipmunk mode" meant for fast-forward
			// It's basic sample skipping to compress a large amount of audio into a single blocksize
			assert(frames_requested > mixer.blocksize);

			mixer.fast_forward_buffer.clear();
			mixer.fast_forward_buffer.reserve(mixer.blocksize);
			const float index_add = static_cast<float>(frames_requested) / static_cast<float>(mixer.blocksize);
			float float_index = 0.0f;

			for (int i = 0; i < mixer.blocksize; ++i) {
				const size_t src_index = std::min(check_cast<size_t>(iroundf(float_index)), mixer.output_buffer.size() - 1);
				mixer.fast_forward_buffer.push_back(mixer.output_buffer[src_index]);
				float_index += index_add;
			}
		}

		assert(to_mix.size() == static_cast<size_t>(mixer.blocksize));
		mixer.final_output.BulkEnqueue(to_mix, to_mix.size());
	}
}

static void stop_mixer([[maybe_unused]] Section* sec)
{
	MIXER_CloseAudioDevice();
}

[[maybe_unused]] static const char* to_string(const MixerState s)
{
	switch (s) {
	case MixerState::Uninitialized: return "Uninitialized";
	case MixerState::NoSound: return "No sound";
	case MixerState::On: return "On";
	case MixerState::Muted: return "Mute";
	}
	return "unknown!";
}

static void set_mixer_state(const MixerState new_state)
{
	// Only Muted <-> On state transitions are allowed
	assert(new_state == MixerState::Muted || new_state == MixerState::On);

#ifdef DEBUG_MIXER
	LOG_MSG("MIXER: Changing mixer state from %s to '%s'",
	        to_string(mixer.state),
	        to_string(new_state));
#endif

	if (mixer.state == MixerState::Uninitialized) {
		mixer.final_output.Start();
		if (mixer.sdl_device > 0) {
			// SDL starts out paused so unpause it when we first set the mixer state.
			// We always keep SDL running in the future.
			// When the mixer becomes muted, we just write silence.
			constexpr int Unpause = 0;
			SDL_PauseAudioDevice(mixer.sdl_device, Unpause);
		}
	}

	if (new_state == MixerState::Muted) {
		// Clear out any audio in the queue to avoid a stutter on un-mute
		mixer.final_output.Clear();
	}

	mixer.state = new_state;
}

void MIXER_CloseAudioDevice()
{
	TIMER_DelTickHandler(capture_callback);

	if (mixer.thread.joinable()) {
		mixer.thread_should_quit = true;
		mixer.final_output.Stop();
		mixer.thread.join();
	}

	for (const auto& [_, channel] : mixer.channels) {
		channel->Enable(false);
	}

	if (mixer.sdl_device > 0) {
		SDL_CloseAudioDevice(mixer.sdl_device);
		mixer.sdl_device = 0;
	}
	mixer.state = MixerState::Uninitialized;
}

// Sets `mixer.sample_rate_hz` and `mixer.blocksize` on success
static bool init_sdl_sound(const int requested_sample_rate_hz,
                           const int requested_blocksize_in_frames,
                           const bool allow_negotiate)
{
	SDL_AudioSpec desired  = {};
	SDL_AudioSpec obtained = {};

	constexpr auto NumStereoChannels = 2;

	desired.channels = NumStereoChannels;
	desired.format   = AUDIO_F32SYS;
	desired.freq     = requested_sample_rate_hz;
	desired.samples  = check_cast<uint16_t>(requested_blocksize_in_frames);

	desired.callback = mixer_callback;
	desired.userdata = nullptr;

	int sdl_allow_flags = SDL_AUDIO_ALLOW_FREQUENCY_CHANGE;

	if (allow_negotiate) {
		// Allow negotiating the audio buffer size, hopefully to obtain
		// a block size that achieves stutter-free playback at a low
		// latency.
		sdl_allow_flags |= SDL_AUDIO_ALLOW_SAMPLES_CHANGE;
	}

	// Open the audio device
	constexpr auto SdlError = 0;

	// Null requests the most reasonable default device
	constexpr auto DeviceName = nullptr;
	// Non-zero is the device is to be opened for recording as well
	constexpr auto IsCapture = 0;

	if ((mixer.sdl_device = SDL_OpenAudioDevice(
	             DeviceName, IsCapture, &desired, &obtained, sdl_allow_flags)) ==
	    SdlError) {
		LOG_WARNING("MIXER: Can't open audio device: '%s'; sound output is disabled",
		            SDL_GetError());

		set_section_property_value("mixer", "nosound", "off");
		return false;
	}

	// Opening SDL audio device succeeded
	//
	// An opened audio device starts out paused, and should be enabled for
	// playing by calling `SDL_PauseAudioDevice()` when you are ready for your
	// audio callback function to be called. We do that in
	// `set_mixer_state()`.
	//
	const auto obtained_sample_rate_hz = obtained.freq;
	const auto obtained_blocksize      = obtained.samples;

	mixer.sample_rate_hz = obtained_sample_rate_hz;
	mixer.blocksize      = obtained_blocksize;

	assert(obtained.channels == NumStereoChannels);
	assert(obtained.format == desired.format);

	// Did SDL negotiate a different playback rate?
	if (obtained_sample_rate_hz != requested_sample_rate_hz) {
		LOG_WARNING("MIXER: SDL negotiated the requested sample rate of %d to %d Hz",
		            requested_sample_rate_hz,
		            obtained_sample_rate_hz);

		set_section_property_value("mixer",
		                           "rate",
		                           format_str("%d",
		                                      mixer.sample_rate_hz.load()));
	}

	// Did SDL negotiate a different blocksize?
	if (obtained_blocksize != requested_blocksize_in_frames) {
		LOG_MSG("MIXER: SDL negotiated the requested blocksize of "
		        "%d to %d frames",
		        requested_blocksize_in_frames,
		        obtained_blocksize);

		set_section_property_value("mixer",
		                           "blocksize",
		                           format_str("%d", mixer.blocksize));
	}

	LOG_MSG("MIXER: Initialised stereo %d Hz audio with %d sample frame buffer",
	        mixer.sample_rate_hz.load(),
	        mixer.blocksize);

	return true;
}

static void init_master_highpass_filter()
{
	// The purpose of this filter is two-fold:
	//
	// - Remove any DC offset from the summed master output (any high-pass
	//   filter can achieve this, even a 6dB/oct HPF at 1Hz). Virtually all
	//   synth modules (CMS, OPL, etc.) can introduce DC offset; this
	//   usually isn't a problem on real hardware as most audio interfaces
	//   include a DC-blocking or high-pass filter in the analog output
	//   stages.
	//
	// - Get rid of (or more precisely, attenuate) unnecessary rumble below
	//   20Hz that serves no musical purpose and only eats up headroom.
	//   Issues like this could have gone unnoticed in the 80s/90s due to
	//   much lower quality consumer audio equipment available, plus
	//   most sound cards had weak bass response (on some models the bass
	//   rolloff starts from as high as 100-120Hz), so the presence of
	//   unnecessary ultra low-frequency content never became an issue back
	//   then.
	//
	// Thanks to the float mixbuffer, it is sufficient to peform the
	// high-pass filtering only once at the very end of the processing
	// chain, instead of doing it on every single mixer channel.
	//
	MIXER_LockMixerThread();
	constexpr auto HighpassCutoffFreqHz = 20.0;
	for (auto& f : mixer.highpass_filter) {
		f.setup(mixer.sample_rate_hz, HighpassCutoffFreqHz);
	}
	MIXER_UnlockMixerThread();
}

void MIXER_Init(Section* sec)
{
	Section_prop* secprop = static_cast<Section_prop*>(sec);
	assert(secprop);

	MIXER_LockMixerThread();

	if (mixer.state == MixerState::Uninitialized) {
		// Initialize the 8-bit to 16-bit lookup table
		fill_8to16_lut();

		const auto mixer_state = secprop->Get_bool("nosound")
		                               ? MixerState::NoSound
		                               : MixerState::On;

		auto set_no_sound = [&] {
			assert(mixer.sdl_device == 0);

			LOG_MSG("MIXER: Sound output disabled ('nosound' mode)");

			mixer.state = MixerState::NoSound;
		};

		mixer.sample_rate_hz = secprop->Get_int("rate");
		mixer.blocksize      = secprop->Get_int("blocksize");

		if (mixer_state == MixerState::NoSound) {
			set_no_sound();

		} else {
			if (init_sdl_sound(secprop->Get_int("rate"),
							   secprop->Get_int("blocksize"),
							   secprop->Get_bool("negotiate"))) {

				// This also unpauses the audio device which is
				// opened in paused mode by SDL.
				set_mixer_state(MixerState::On);
			} else {
				set_no_sound();
			}
		}

		const auto requested_prebuffer_ms = secprop->Get_int("prebuffer");
		mixer.prebuffer_ms = clamp(requested_prebuffer_ms, 1, MaxPrebufferMs);

		const auto prebuffer_frames = (mixer.sample_rate_hz *
		                               mixer.prebuffer_ms) /
		                              1000;

		sec->AddDestroyFunction(&stop_mixer);

		mixer.final_output.Resize(mixer.blocksize + prebuffer_frames);

		// One second of audio
		mixer.capture_queue.Resize(mixer.sample_rate_hz * 2);

		mixer.thread = std::thread(mixer_thread_loop);
		set_thread_name(mixer.thread, "dosbox:mixer");

		TIMER_AddTickHandler(capture_callback);
	}

	// Initialise crossfeed
	const auto new_crossfeed_preset = crossfeed_pref_to_preset(
	        secprop->Get_string("crossfeed"));

	if (mixer.crossfeed.preset != new_crossfeed_preset) {
		MIXER_SetCrossfeedPreset(new_crossfeed_preset);
	}

	// Initialise reverb
	const auto new_reverb_preset = reverb_pref_to_preset(
	        secprop->Get_string("reverb"));

	if (mixer.reverb.preset != new_reverb_preset) {
		MIXER_SetReverbPreset(new_reverb_preset);
	}

	// Initialise chorus
	const auto new_chorus_preset = chorus_pref_to_preset(
	        secprop->Get_string("chorus"));

	if (mixer.chorus.preset != new_chorus_preset) {
		MIXER_SetChorusPreset(new_chorus_preset);
	}

	init_master_highpass_filter();

	// Initialise master compressor
	init_compressor(secprop->Get_bool("compressor"));

	MIXER_UnlockMixerThread();
}

void MIXER_Mute()
{
	if (mixer.state == MixerState::On) {
		set_mixer_state(MixerState::Muted);
		MIDI_Mute();
		GFX_NotifyAudioMutedStatus(true);
		LOG_MSG("MIXER: Muted audio output");
	}
}

void MIXER_Unmute()
{
	if (mixer.state == MixerState::Muted) {
		set_mixer_state(MixerState::On);
		MIDI_Unmute();
		GFX_NotifyAudioMutedStatus(false);
		LOG_MSG("MIXER: Unmuted audio output");
	}
}

bool MIXER_IsManuallyMuted()
{
	return mixer.is_manually_muted;
}

// Toggle the mixer on/off when a 'true' bool is passed in.
static void handle_toggle_mute(const bool was_pressed)
{
	// The "pressed" bool argument is used by the Mapper API, which sends a
	// true-bool for key down events and a false-bool for key up events.
	if (!was_pressed) {
		return;
	}

	switch (mixer.state) {
	case MixerState::NoSound:
		LOG_WARNING("MIXER: Mute requested, but sound is disabled ('nosound' mode)");
		break;

	case MixerState::Muted:
		MIXER_Unmute();
		mixer.is_manually_muted = false;
		break;

	case MixerState::On:
		MIXER_Mute();
		mixer.is_manually_muted = true;
		break;

	default: break;
	};
}

static void init_mixer_dosbox_settings(Section_prop& sec_prop)
{
#if defined(WIN32)
	// Longstanding known-good defaults for Windows
	constexpr auto DefaultBlocksize      = 1024;
	constexpr auto DefaultPrebufferMs    = 25;
	constexpr bool DefaultAllowNegotiate = false;

#else
	// Non-Windows platforms tolerate slightly lower latency
	constexpr auto DefaultBlocksize      = 512;
	constexpr auto DefaultPrebufferMs    = 20;
	constexpr bool DefaultAllowNegotiate = true;
#endif

	constexpr auto WhenIdle    = Property::Changeable::WhenIdle;
	constexpr auto OnlyAtStart = Property::Changeable::OnlyAtStart;

	auto bool_prop = sec_prop.Add_bool("nosound", OnlyAtStart, false);
	assert(bool_prop);
	bool_prop->Set_help(
	        "Enable silent mode (disabled by default).\n"
	        "Sound is still emulated in silent mode, but DOSBox outputs no sound to the host.\n"
	        "Capturing the emulated audio output to a WAV file works in silent mode.");

	auto int_prop = sec_prop.Add_int("rate", OnlyAtStart, DefaultSampleRateHz);
	assert(int_prop);
	int_prop->SetMinMax(8000, 96000);
	int_prop->Set_help(
	        "Sample rate of DOSBox's internal audio mixer in Hz (%s by default).\n"
	        "Valid range is 8000 to 96000 Hz. The vast majority of consumer-grade audio\n"
	        "hardware uses a native rate of 48000 Hz. Recommend leaving this as-is unless\n"
	        "you have good reason to change it. The OS will most likely resample non-standard\n"
	        "sample rates to 48000 Hz anyway.");

	int_prop = sec_prop.Add_int("blocksize", OnlyAtStart, DefaultBlocksize);
	int_prop->SetMinMax(64, 8192);
	int_prop->Set_help(
	        "Block size of the host audio device in sample frames (%s by default).\n"
	        "Valid range is 64 to 8192. Should be set to power-of-two values (e.g., 256,\n"
	        "512, 1024, etc.) Larger values might help with sound stuttering but will\n"
	        "introduce more latency. Also see 'negotiate'.");

	int_prop = sec_prop.Add_int("prebuffer", OnlyAtStart, DefaultPrebufferMs);
	int_prop->SetMinMax(0, MaxPrebufferMs);
	int_prop->Set_help(
	        "How many milliseconds of sound to render in advance on top of 'blocksize'\n"
	        "(%s by default). Larger values might help with sound stuttering but will\n"
	        "introduce more latency.");

	bool_prop = sec_prop.Add_bool("negotiate", OnlyAtStart, DefaultAllowNegotiate);
	bool_prop->Set_help(
	        "Negotiate a possibly better 'blocksize' setting (%s by default).\n"
	        "Enable it if you're not getting audio or the sound is stuttering with your\n"
	        "'blocksize' setting. Disable it to force the manually set 'blocksize' value.");

	constexpr auto DefaultOn = true;
	bool_prop = sec_prop.Add_bool("compressor", WhenIdle, DefaultOn);
	bool_prop->Set_help(
	        "Enable the auto-leveling compressor on the master channel to prevent clipping\n"
	        "of the audio output:\n"
	        "  off:  Disable compressor.\n"
	        "  on:   Enable compressor (default).");

	auto string_prop = sec_prop.Add_string("crossfeed", WhenIdle, "off");
	string_prop->Set_help(
	        "Enable crossfeed on the OPL and CMS (Gameblaster) mixer channels. Many games\n"
	        "pan the instruments 100%% left and 100%% right in the stereo field on these audio\n"
	        "devices which is unpleasant to listen to in headphones. With crossfeed enabled,\n"
	        "a portion of the left channel signal is mixed into the right channel and vice\n"
	        "versa, creating a more natural listening experience.\n"
	        "  off:     No crossfeed (default).\n"
	        "  on:      Enable crossfeed (normal preset).\n"
	        "  light:   Light crossfeed (strength 15).\n"
	        "  normal:  Normal crossfeed (strength 40).\n"
	        "  strong:  Strong crossfeed (strength 65).\n"
	        "\n"
	        "Notes:\n"
	        "  - Use the MIXER command to apply crossfeed to other audio channels as well\n"
	        "    and to fine-tune the crossfeed strength per channel.");
	string_prop->Set_values({"off", "on", "light", "normal", "strong"});

	string_prop = sec_prop.Add_string("reverb", WhenIdle, "off");
	string_prop->Set_help(
	        "Enable reverb globally to add a sense of space to the sound:\n"
	        "  off:     No reverb (default).\n"
	        "  on:      Enable reverb (medium preset).\n"
	        "  tiny:    Simulates the sound of a small integrated speaker in a room;\n"
	        "           specifically designed for small-speaker audio systems\n"
	        "           (PC speaker, Tandy, PS/1 Audio, and LPT DAC devices).\n"
	        "  small:   Adds a subtle sense of space; good for games that use a single\n"
	        "           synth channel (typically OPL) for both music and sound effects.\n"
	        "  medium:  Medium room preset that works well with a wide variety of games.\n"
	        "  large:   Large hall preset recommended for games that use separate\n"
	        "           channels for music and digital audio.\n"
	        "  huge:    A stronger variant of the large hall preset; works really well\n"
	        "           in some games with more atmospheric soundtracks.\n"
	        "\n"
	        "Notes:\n"
	        "  - The presets apply a noticeable amount of reverb to the synth mixer channels\n"
	        "    (except for synths with built-in reverb; e.g., the Roland MT-32), and a\n"
	        "    subtle amount to the digital audio channels.\n"
	        "  - Use the MIXER command to fine-tune the reverb levels per channel.");
	string_prop->Set_values(
	        {"off", "on", "tiny", "small", "medium", "large", "huge"});

	string_prop = sec_prop.Add_string("chorus", WhenIdle, "off");
	string_prop->Set_help(
	        "Enable chorus globally to add a sense of stereo movement to the sound:\n"
	        "  off:     No chorus (default).\n"
	        "  on:      Enable chorus (normal preset).\n"
	        "  light:   A light chorus effect (especially suited for synth music that\n"
	        "           features lots of white noise).\n"
	        "  normal:  Normal chorus that works well with a wide variety of games.\n"
	        "  strong:  An obvious and upfront chorus effect.\n"
	        "\n"
	        "Notes:\n"
	        "  - The presets apply the chorus effect to the synth channels only (except\n"
	        "    for synths with built-in chorus; e.g. the Roland MT-32).\n"
	        "  - Use the MIXER command to fine-tune the chorus levels per channel.");
	string_prop->Set_values({"off", "on", "light", "normal", "strong"});

	MAPPER_AddHandler(handle_toggle_mute, SDL_SCANCODE_F8, PRIMARY_MOD, "mute", "Mute");
}

void MIXER_AddConfigSection(const ConfigPtr& conf)
{
	assert(conf);

	constexpr auto ChangeableAtRuntime = true;

	Section_prop* sec = conf->AddSection_prop("mixer",
	                                          &MIXER_Init,
	                                          ChangeableAtRuntime);
	assert(sec);
	init_mixer_dosbox_settings(*sec);
}
