Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions src/frontend/qt_sdl/AudioLowPass.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
Copyright 2016-2026 melonDS team

This file is part of melonDS.

melonDS is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.

melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/.
*/

#ifndef AUDIOLOWPASS_H
#define AUDIOLOWPASS_H

#include <algorithm>
#include <cmath>
#include <cstdint>

// fourth-order Butterworth low-pass: two cascaded RBJ biquads, stereo with
// independent state per channel. the cutoff is smoothed rather than jumped,
// since stepping the coefficients would click. wide open leaves the output alone.
class AudioLowPass
{
public:
static constexpr double kSmoothingTau = 0.05; // cutoff smoother, seconds
static constexpr double kBypassThreshold = 0.995; // fraction of wide-open
// section Q's for a fourth-order Butterworth cascade
static constexpr double kSectionQ[2] = {0.54119610014619698, 1.3065629648763766};
static constexpr double kMinCutoff = 20.0;

void Init(double sampleRate)
{
SampleRate = sampleRate;
WideOpen = std::max(0.45 * sampleRate, kMinCutoff);
for (int s = 0; s < 2; s++)
{
Stages[s].z1[0] = Stages[s].z1[1] = 0.0;
Stages[s].z2[0] = Stages[s].z2[1] = 0.0;
}
SetCutoffNow(WideOpen);
}

double WideOpenCutoff() const { return WideOpen; }
double Cutoff() const { return CurCutoff; }
bool Bypassed() const { return CurCutoff >= (WideOpen * kBypassThreshold); }

// advance the smoothed cutoff by one block, then filter in place.
// the coefficients are interpolated across the block rather than replaced
// in one go: a transposed direct-form biquad's state encodes its past under
// the coefficients that produced it, so a step leaves the two inconsistent
// and the filter rings, once per block.
void Process(int16_t* samples, int numFrames, double targetHz, double blockSeconds)
{
if (numFrames < 1) return;

double from[2][5], to[2][5];
BeginBlock(targetHz, blockSeconds, from, to);
bool bypass = Bypassed();

for (int i = 0; i < numFrames; i++)
{
StepCoefficients(from, to, (double)(i + 1) / numFrames);
for (int ch = 0; ch < 2; ch++)
{
// runs even when bypassed: the state must stay in step with
// the signal, or re-engaging would click
double y = ProcessSample(samples[(i*2)+ch], ch);
if (!bypass) samples[(i*2)+ch] = Saturate(y);
}
}

EndBlock(to);
}

// advance the cutoff and the filter state over silence, writing nothing.
// used while muted, so unmuting neither steps the coefficients nor dumps
// whatever was still ringing in the biquads into a buffer meant to be quiet.
void ProcessMuted(int numFrames, double targetHz, double blockSeconds)
{
if (numFrames < 1) return;

double from[2][5], to[2][5];
BeginBlock(targetHz, blockSeconds, from, to);

for (int i = 0; i < numFrames; i++)
{
StepCoefficients(from, to, (double)(i + 1) / numFrames);
for (int ch = 0; ch < 2; ch++)
ProcessSample(0.0, ch);
}

EndBlock(to);
}

void Smooth(double targetHz, double blockSeconds)
{
targetHz = std::clamp(targetHz, kMinCutoff, WideOpen);
double a = 1.0 - std::exp(-blockSeconds / kSmoothingTau);
SetCutoffNow(CurCutoff + ((targetHz - CurCutoff) * a));
}

void SetCutoffNow(double cutoffHz)
{
CurCutoff = std::clamp(cutoffHz, kMinCutoff, WideOpen);
for (int s = 0; s < 2; s++)
Stages[s].Design(CurCutoff, SampleRate, kSectionQ[s]);
}

double ProcessSample(double x, int ch)
{
double y = x;
for (int s = 0; s < 2; s++)
y = Stages[s].Run(y, ch);
return y;
}

private:
static int16_t Saturate(double y)
{
long v = std::lround(y);
if (v > 32767) v = 32767;
if (v < -32768) v = -32768;
return (int16_t)v;
}

// coefficients as they stand, then as designed for this block's cutoff
void BeginBlock(double targetHz, double blockSeconds, double from[2][5], double to[2][5])
{
for (int s = 0; s < 2; s++) Stages[s].Snapshot(from[s]);
Smooth(targetHz, blockSeconds);
for (int s = 0; s < 2; s++) Stages[s].Snapshot(to[s]);
}

void StepCoefficients(const double from[2][5], const double to[2][5], double t)
{
for (int s = 0; s < 2; s++) Stages[s].Lerp(from[s], to[s], t);
}

// land exactly on the designed set, so rounding in the interpolation
// cannot accumulate across blocks
void EndBlock(const double to[2][5])
{
for (int s = 0; s < 2; s++) Stages[s].Restore(to[s]);
}

struct Biquad
{
double b0 = 1.0, b1 = 0.0, b2 = 0.0, a1 = 0.0, a2 = 0.0;
double z1[2] = {0.0, 0.0};
double z2[2] = {0.0, 0.0};

void Snapshot(double out[5]) const
{
out[0] = b0; out[1] = b1; out[2] = b2; out[3] = a1; out[4] = a2;
}

void Restore(const double in[5])
{
b0 = in[0]; b1 = in[1]; b2 = in[2]; a1 = in[3]; a2 = in[4];
}

void Lerp(const double from[5], const double to[5], double t)
{
b0 = from[0] + ((to[0] - from[0]) * t);
b1 = from[1] + ((to[1] - from[1]) * t);
b2 = from[2] + ((to[2] - from[2]) * t);
a1 = from[3] + ((to[3] - from[3]) * t);
a2 = from[4] + ((to[4] - from[4]) * t);
}

void Design(double cutoffHz, double sampleRate, double q)
{
double w0 = 2.0 * M_PI * (cutoffHz / sampleRate);
double cw = std::cos(w0);
double alpha = std::sin(w0) / (2.0 * q);
double a0 = 1.0 + alpha;

b0 = ((1.0 - cw) * 0.5) / a0;
b1 = (1.0 - cw) / a0;
b2 = b0;
a1 = (-2.0 * cw) / a0;
a2 = (1.0 - alpha) / a0;
}

// transposed direct form II
double Run(double x, int ch)
{
double y = (b0 * x) + z1[ch];
z1[ch] = (b1 * x) - (a1 * y) + z2[ch];
z2[ch] = (b2 * x) - (a2 * y);
return y;
}
};

double SampleRate = 48000.0;
double WideOpen = 21600.0;
double CurCutoff = 21600.0;
Biquad Stages[2];
};

#endif // AUDIOLOWPASS_H
32 changes: 32 additions & 0 deletions src/frontend/qt_sdl/AudioSettingsDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ AudioSettingsDialog::AudioSettingsDialog(QWidget* parent) : QDialog(parent), ui(
oldBitDepth = cfg.GetInt("Audio.BitDepth");
oldVolume = instcfg.GetInt("Audio.Volume");
oldDSiSync = instcfg.GetBool("Audio.DSiVolumeSync");
oldTimeStretch = cfg.GetBool("SpeedUpTimeStretch");
oldLowPassEnable = cfg.GetBool("SpeedUpLowPassEnable");
oldLowPass = cfg.GetInt("SpeedUpLowPass");

volume = oldVolume;
dsiSync = oldDSiSync;
Expand All @@ -69,6 +72,13 @@ AudioSettingsDialog::AudioSettingsDialog(QWidget* parent) : QDialog(parent), ui(

ui->chkSyncDSiVolume->setChecked(oldDSiSync);

ui->cbSpeedUpTimeStretch->setChecked(oldTimeStretch);
ui->cbSpeedUpLowPass->setChecked(oldLowPassEnable);
state = ui->spinSpeedUpLowPass->blockSignals(true);
ui->spinSpeedUpLowPass->setValue(oldLowPass);
ui->spinSpeedUpLowPass->blockSignals(state);
ui->spinSpeedUpLowPass->setEnabled(oldLowPassEnable);

// Setup volume slider accordingly
if (emuActive && emuInstance->getNDS()->ConsoleType == 1)
{
Expand Down Expand Up @@ -182,6 +192,9 @@ void AudioSettingsDialog::on_AudioSettingsDialog_rejected()
cfg.SetInt("Audio.BitDepth", oldBitDepth);
instcfg.SetInt("Audio.Volume", oldVolume);
instcfg.SetBool("Audio.DSiVolumeSync", oldDSiSync);
cfg.SetBool("SpeedUpTimeStretch", oldTimeStretch);
cfg.SetBool("SpeedUpLowPassEnable", oldLowPassEnable);
cfg.SetInt("SpeedUpLowPass", oldLowPass);

emit updateAudioVolume(oldVolume, oldDSiSync);
emit updateAudioSettings();
Expand Down Expand Up @@ -227,6 +240,25 @@ void AudioSettingsDialog::on_slVolume_valueChanged(int val)
emit updateAudioVolume(val, dsiSync);
}

void AudioSettingsDialog::on_cbSpeedUpTimeStretch_clicked(bool checked)
{
emuInstance->getGlobalConfig().SetBool("SpeedUpTimeStretch", checked);
emit updateAudioSettings();
}

void AudioSettingsDialog::on_cbSpeedUpLowPass_clicked(bool checked)
{
ui->spinSpeedUpLowPass->setEnabled(checked);
emuInstance->getGlobalConfig().SetBool("SpeedUpLowPassEnable", checked);
emit updateAudioSettings();
}

void AudioSettingsDialog::on_spinSpeedUpLowPass_valueChanged(int val)
{
emuInstance->getGlobalConfig().SetInt("SpeedUpLowPass", val);
emit updateAudioSettings();
}

void AudioSettingsDialog::on_chkSyncDSiVolume_clicked(bool checked)
{
dsiSync = checked;
Expand Down
6 changes: 6 additions & 0 deletions src/frontend/qt_sdl/AudioSettingsDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ private slots:
void on_cbBitDepth_currentIndexChanged(int idx);
void on_slVolume_valueChanged(int val);
void on_chkSyncDSiVolume_clicked(bool checked);
void on_cbSpeedUpTimeStretch_clicked(bool checked);
void on_cbSpeedUpLowPass_clicked(bool checked);
void on_spinSpeedUpLowPass_valueChanged(int val);
void onChangeMicMode(int mode);
void on_btnMicWavBrowse_clicked();

Expand All @@ -80,6 +83,9 @@ private slots:
int oldBitDepth;
int oldVolume;
bool oldDSiSync;
bool oldTimeStretch;
bool oldLowPassEnable;
int oldLowPass;
QButtonGroup* grpMicMode;

int volume;
Expand Down
51 changes: 51 additions & 0 deletions src/frontend/qt_sdl/AudioSettingsDialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,57 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Fast-forward and slow-mo</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="cbSpeedUpTimeStretch">
<property name="toolTip">
<string>Time-stretch fast-forward and slow-mo audio so it keeps its pitch instead of being chopped up.</string>
</property>
<property name="text">
<string>Time-stretch fast-forward/slow-mo</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="cbSpeedUpLowPass">
<property name="toolTip">
<string>Roll the treble off fast-forward audio, which turns harsh when sped up.</string>
</property>
<property name="text">
<string>Low-pass fast-forward</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinSpeedUpLowPass">
<property name="toolTip">
<string>Cutoff for fast-forward audio only, divided by the emulation speed. Use the highest value that still sounds good sped up; 48000 Hz is the same as unchecking the box.</string>
</property>
<property name="suffix">
<string> Hz</string>
</property>
<property name="minimum">
<number>1000</number>
</property>
<property name="maximum">
<number>48000</number>
</property>
<property name="singleStep">
<number>500</number>
</property>
<property name="value">
<number>48000</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
Expand Down
Loading