diff --git a/include/config.hpp b/include/config.hpp index 7361a40d1..6041286dc 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -1,12 +1,21 @@ #pragma once #include +#include #include "audio/dsp_core.hpp" -#include "renderer.hpp" #include "frontend_settings.hpp" +#include "renderer.hpp" struct AudioDeviceConfig { + // Audio curve to use for volumes between 0-100 + enum class VolumeCurve : int { + Cubic = 0, // Samples are scaled by volume ^ 3 + Linear = 1, // Samples are scaled by volume + }; + float volumeRaw = 1.0f; + VolumeCurve volumeCurve = VolumeCurve::Cubic; + bool muteAudio = false; float getVolume() const { @@ -16,6 +25,9 @@ struct AudioDeviceConfig { return volumeRaw; } + + static VolumeCurve volumeCurveFromString(std::string inString); + static const char* volumeCurveToString(VolumeCurve curve); }; // Remember to initialize every field here to its default value otherwise bad things will happen diff --git a/src/config.cpp b/src/config.cpp index a8c88a68f..fd4d969d4 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,5 +1,7 @@ #include "config.hpp" +#include +#include #include #include #include @@ -105,6 +107,7 @@ void EmulatorConfig::load() { // Our volume ranges from 0.0 (muted) to 2.0 (boosted, using a logarithmic scale). 1.0 is the "default" volume, ie we don't adjust the PCM // samples at all. audioDeviceConfig.volumeRaw = float(std::clamp(toml::find_or(audio, "AudioVolume", 1.0), 0.0, 2.0)); + audioDeviceConfig.volumeCurve = AudioDeviceConfig::volumeCurveFromString(toml::find_or(audio, "VolumeCurve", "cubic")); } } @@ -188,6 +191,7 @@ void EmulatorConfig::save() { data["Audio"]["EnableAACAudio"] = aacEnabled; data["Audio"]["MuteAudio"] = audioDeviceConfig.muteAudio; data["Audio"]["AudioVolume"] = double(audioDeviceConfig.volumeRaw); + data["Audio"]["VolumeCurve"] = std::string(AudioDeviceConfig::volumeCurveToString(audioDeviceConfig.volumeCurve)); data["Audio"]["PrintDSPFirmware"] = printDSPFirmware; data["Battery"]["ChargerPlugged"] = chargerPlugged; @@ -203,3 +207,26 @@ void EmulatorConfig::save() { file << data; file.close(); } + +AudioDeviceConfig::VolumeCurve AudioDeviceConfig::volumeCurveFromString(std::string inString) { + // Transform to lower-case to make the setting case-insensitive + std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); }); + + if (inString == "cubic") { + return VolumeCurve::Cubic; + } else if (inString == "linear") { + return VolumeCurve::Linear; + } + + // Default to cubic curve + return VolumeCurve::Cubic; +} + +const char* AudioDeviceConfig::volumeCurveToString(AudioDeviceConfig::VolumeCurve curve) { + switch (curve) { + case VolumeCurve::Linear: return "linear"; + + case VolumeCurve::Cubic: + default: return "cubic"; + } +} \ No newline at end of file diff --git a/src/core/audio/miniaudio_device.cpp b/src/core/audio/miniaudio_device.cpp index b513450ed..550fb039d 100644 --- a/src/core/audio/miniaudio_device.cpp +++ b/src/core/audio/miniaudio_device.cpp @@ -134,6 +134,12 @@ void MiniAudioDevice::init(Samples& samples, bool safe) { } else { // If our volume is in [0.0, 1.0) then just multiply by the volume. No need to clamp, since there is no danger of our samples wrapping // around due to overflow + + // If we're applying cubic volume curve, raise volume to the power of 3 + if (self->audioSettings.volumeCurve == AudioDeviceConfig::VolumeCurve::Cubic) { + audioVolume = audioVolume * audioVolume * audioVolume; + } + for (usize i = 0; i < samplesWritten; i += 2) { s16 l = s16(float(sample[0]) * audioVolume); s16 r = s16(float(sample[1]) * audioVolume); diff --git a/src/panda_qt/config_window.cpp b/src/panda_qt/config_window.cpp index 60ec3ed26..99b38b24d 100644 --- a/src/panda_qt/config_window.cpp +++ b/src/panda_qt/config_window.cpp @@ -240,6 +240,16 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win connectCheckbox(muteAudio, config.audioDeviceConfig.muteAudio); audioLayout->addRow(muteAudio); + QComboBox* volumeCurveType = new QComboBox; + volumeCurveType->addItem(tr("Cubic")); + volumeCurveType->addItem(tr("Linear")); + volumeCurveType->setCurrentIndex(static_cast(config.audioDeviceConfig.volumeCurve)); + connect(volumeCurveType, &QComboBox::currentIndexChanged, this, [&](int index) { + config.audioDeviceConfig.volumeCurve = static_cast(index); + updateConfig(); + }); + audioLayout->addRow(tr("Volume curve"), volumeCurveType); + QSpinBox* volumeRaw = new QSpinBox(); volumeRaw->setRange(0, 200); volumeRaw->setValue(config.audioDeviceConfig.volumeRaw * 100);