mirror of
https://github.com/jsgroth/jgenesis.git
synced 2026-01-09 06:01:07 +08:00
[32X/GBA] add additional GBA interpolation options, and improve 32X PWM resampling quality
for GBA, added cubic Hermite option as well as an option to low-pass PSG output when using enhanced interpolation I really do not know what I was thinking when I originally wrote this cubic resampling code
This commit is contained in:
parent
339183c9a5
commit
59ffc61d53
@ -29,6 +29,7 @@ use thiserror::Error;
|
||||
#[derive(Debug, Clone, Copy, Encode, Decode, ConfigDisplay)]
|
||||
pub struct GbaAudioConfig {
|
||||
pub audio_interpolation: GbaAudioInterpolation,
|
||||
pub psg_low_pass: bool,
|
||||
pub pulse_1_enabled: bool,
|
||||
pub pulse_2_enabled: bool,
|
||||
pub wavetable_enabled: bool,
|
||||
@ -41,6 +42,7 @@ impl Default for GbaAudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
audio_interpolation: GbaAudioInterpolation::default(),
|
||||
psg_low_pass: false,
|
||||
pulse_1_enabled: true,
|
||||
pulse_2_enabled: true,
|
||||
wavetable_enabled: true,
|
||||
|
||||
@ -214,9 +214,14 @@ impl AudioResampler {
|
||||
GbaAudioInterpolation::NearestNeighbor => {
|
||||
Self::Basic(Box::new(BasicResampler::new(clock_shift, output_frequency)))
|
||||
}
|
||||
GbaAudioInterpolation::WindowedSinc => Self::Interpolating(Box::new(
|
||||
InterpolatingResampler::new(output_frequency, pcm_frequencies),
|
||||
)),
|
||||
GbaAudioInterpolation::CubicHermite | GbaAudioInterpolation::WindowedSinc => {
|
||||
Self::Interpolating(Box::new(InterpolatingResampler::new(
|
||||
config.audio_interpolation,
|
||||
config.psg_low_pass,
|
||||
output_frequency,
|
||||
pcm_frequencies,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,7 +368,7 @@ impl Apu {
|
||||
self.resampler.push_mixed_sample([sample_l, sample_r]);
|
||||
}
|
||||
}
|
||||
GbaAudioInterpolation::WindowedSinc => {
|
||||
GbaAudioInterpolation::CubicHermite | GbaAudioInterpolation::WindowedSinc => {
|
||||
for _ in 0..pwm_samples_elapsed * psg_ticks {
|
||||
self.psg.tick_1mhz(self.enabled);
|
||||
self.resampler.push_psg(self.psg.sample(self.config.psg_channels_enabled()));
|
||||
@ -750,6 +755,8 @@ impl Apu {
|
||||
self.timer_frequencies[self.pcm_b.timer as usize],
|
||||
],
|
||||
);
|
||||
} else if let AudioResampler::Interpolating(resampler) = &mut self.resampler {
|
||||
resampler.update_psg_low_pass(config.psg_low_pass);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,14 @@
|
||||
|
||||
use crate::apu::PwmClockShift;
|
||||
use bincode::{Decode, Encode};
|
||||
use dsp::design::FilterType;
|
||||
use dsp::iir::SecondOrderIirFilter;
|
||||
use dsp::sinc::{PerformanceSincResampler, QualitySincResampler};
|
||||
use gba_config::GbaAudioInterpolation;
|
||||
use jgenesis_common::audio::CubicResampler;
|
||||
use jgenesis_common::frontend::AudioOutput;
|
||||
use std::array;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
pub struct BasicResampler {
|
||||
@ -45,29 +50,144 @@ impl BasicResampler {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
enum EnhancedResampler {
|
||||
Cubic(CubicResampler<1>),
|
||||
Sinc(QualitySincResampler<1>),
|
||||
}
|
||||
|
||||
impl EnhancedResampler {
|
||||
fn new(
|
||||
interpolation: GbaAudioInterpolation,
|
||||
source_frequency: f64,
|
||||
output_frequency: u64,
|
||||
) -> Self {
|
||||
match interpolation {
|
||||
GbaAudioInterpolation::WindowedSinc => {
|
||||
Self::Sinc(QualitySincResampler::new(source_frequency, output_frequency as f64))
|
||||
}
|
||||
_ => Self::Cubic(CubicResampler::new(source_frequency, output_frequency)),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect(&mut self, sample: f64) {
|
||||
match self {
|
||||
Self::Cubic(resampler) => resampler.collect_sample([sample]),
|
||||
Self::Sinc(resampler) => resampler.collect([sample]),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_source_frequency(&mut self, source_frequency: f64) {
|
||||
match self {
|
||||
Self::Cubic(resampler) => resampler.update_source_frequency(source_frequency),
|
||||
Self::Sinc(resampler) => resampler.update_source_frequency(source_frequency),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_output_frequency(&mut self, output_frequency: u64) {
|
||||
match self {
|
||||
Self::Cubic(resampler) => resampler.update_output_frequency(output_frequency),
|
||||
Self::Sinc(resampler) => resampler.update_output_frequency(output_frequency as f64),
|
||||
}
|
||||
}
|
||||
|
||||
fn output_buffer_pop_front(&mut self) -> Option<[f64; 1]> {
|
||||
match self {
|
||||
Self::Cubic(resampler) => resampler.output_buffer_pop_front(),
|
||||
Self::Sinc(resampler) => resampler.output_buffer_pop_front(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSG source frequency is actually 2 MHz (2^21), but this implementation only runs it at 1 MHz
|
||||
const PSG_SOURCE_FREQUENCY: f64 = 1_048_576.0;
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
struct PsgLowPassFilter {
|
||||
max_pcm_frequency: Option<f64>,
|
||||
psg_low_pass: bool,
|
||||
filters: Option<(SecondOrderIirFilter, SecondOrderIirFilter)>,
|
||||
}
|
||||
|
||||
impl PsgLowPassFilter {
|
||||
fn new(psg_low_pass: bool, pcm_frequencies: [Option<f64>; 2]) -> Self {
|
||||
if !psg_low_pass {
|
||||
return Self { max_pcm_frequency: None, psg_low_pass, filters: None };
|
||||
}
|
||||
|
||||
let max_pcm_frequency = f64_option_max(pcm_frequencies);
|
||||
let filters = max_pcm_frequency.map(|max_pcm_frequency| {
|
||||
let lpf_cutoff = max_pcm_frequency * 0.45;
|
||||
(Self::new_filter(lpf_cutoff), Self::new_filter(lpf_cutoff))
|
||||
});
|
||||
|
||||
Self { max_pcm_frequency, psg_low_pass, filters }
|
||||
}
|
||||
|
||||
fn new_filter(cutoff: f64) -> SecondOrderIirFilter {
|
||||
dsp::design::butterworth(cutoff, PSG_SOURCE_FREQUENCY, FilterType::LowPass)
|
||||
}
|
||||
|
||||
fn filter(&mut self, sample: [f64; 2]) -> [f64; 2] {
|
||||
self.filters
|
||||
.as_mut()
|
||||
.map(|(filter_l, filter_r)| [filter_l.filter(sample[0]), filter_r.filter(sample[1])])
|
||||
.unwrap_or(sample)
|
||||
}
|
||||
|
||||
fn reload(&mut self, psg_low_pass: bool, pcm_frequencies: [Option<f64>; 2]) {
|
||||
if psg_low_pass != self.psg_low_pass
|
||||
|| f64_option_max(pcm_frequencies) != self.max_pcm_frequency
|
||||
{
|
||||
*self = Self::new(psg_low_pass, pcm_frequencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn f64_option_max(values: impl IntoIterator<Item = Option<f64>>) -> Option<f64> {
|
||||
// Less is arbitrary here - if there's ever a NaN frequency, there are bigger problems
|
||||
values.into_iter().flatten().max_by(|&a, &b| a.partial_cmp(&b).unwrap_or(Ordering::Less))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
pub struct InterpolatingResampler {
|
||||
interpolation: GbaAudioInterpolation,
|
||||
psg_low_pass: bool,
|
||||
pcm_frequencies: [Option<f64>; 2],
|
||||
pcm_resamplers: [Option<QualitySincResampler<1>>; 2],
|
||||
pcm_resamplers: [Option<EnhancedResampler>; 2],
|
||||
pcm_samples: [i8; 2],
|
||||
psg_lpf: PsgLowPassFilter,
|
||||
psg_resampler: PerformanceSincResampler<2>,
|
||||
output_frequency: f64,
|
||||
}
|
||||
|
||||
impl InterpolatingResampler {
|
||||
pub fn new(output_frequency: u64, pcm_frequencies: [Option<f64>; 2]) -> Self {
|
||||
pub fn new(
|
||||
interpolation: GbaAudioInterpolation,
|
||||
psg_low_pass: bool,
|
||||
output_frequency: u64,
|
||||
pcm_frequencies: [Option<f64>; 2],
|
||||
) -> Self {
|
||||
Self {
|
||||
interpolation,
|
||||
psg_low_pass,
|
||||
pcm_frequencies,
|
||||
pcm_resamplers: pcm_frequencies.map(|frequency| {
|
||||
frequency
|
||||
.map(|frequency| QualitySincResampler::new(frequency, output_frequency as f64))
|
||||
frequency.map(|frequency| {
|
||||
EnhancedResampler::new(interpolation, frequency, output_frequency)
|
||||
})
|
||||
}),
|
||||
pcm_samples: array::from_fn(|_| 0),
|
||||
psg_lpf: PsgLowPassFilter::new(psg_low_pass, pcm_frequencies),
|
||||
psg_resampler: PerformanceSincResampler::new((1 << 20).into(), output_frequency as f64),
|
||||
output_frequency: output_frequency as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_psg_low_pass(&mut self, psg_low_pass: bool) {
|
||||
self.psg_lpf.reload(psg_low_pass, self.pcm_frequencies);
|
||||
}
|
||||
|
||||
pub fn push_pcm_a(&mut self, sample: i8) {
|
||||
self.push_pcm(0, sample);
|
||||
}
|
||||
@ -80,7 +200,7 @@ impl InterpolatingResampler {
|
||||
self.pcm_samples[i] = sample;
|
||||
|
||||
if let Some(resampler) = &mut self.pcm_resamplers[i] {
|
||||
resampler.collect([f64::from(sample) / 128.0]);
|
||||
resampler.collect(f64::from(sample) / 128.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +213,10 @@ impl InterpolatingResampler {
|
||||
}
|
||||
|
||||
pub fn push_psg(&mut self, sample: (i16, i16)) {
|
||||
self.psg_resampler.collect([f64::from(sample.0) / 512.0, f64::from(sample.1) / 512.0]);
|
||||
let sample =
|
||||
self.psg_lpf.filter([f64::from(sample.0) / 512.0, f64::from(sample.1) / 512.0]);
|
||||
|
||||
self.psg_resampler.collect(sample);
|
||||
}
|
||||
|
||||
pub fn update_pcm_a_frequency(&mut self, frequency: Option<f64>) {
|
||||
@ -112,9 +235,24 @@ impl InterpolatingResampler {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO correct mid-sample position
|
||||
self.pcm_resamplers[i] =
|
||||
frequency.map(|frequency| QualitySincResampler::new(frequency, self.output_frequency));
|
||||
match frequency {
|
||||
Some(frequency) => {
|
||||
self.pcm_resamplers[i]
|
||||
.get_or_insert_with(|| {
|
||||
EnhancedResampler::new(
|
||||
self.interpolation,
|
||||
frequency,
|
||||
self.output_frequency as u64,
|
||||
)
|
||||
})
|
||||
.update_source_frequency(frequency);
|
||||
}
|
||||
None => {
|
||||
self.pcm_resamplers[i] = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.psg_lpf.reload(self.psg_low_pass, self.pcm_frequencies);
|
||||
}
|
||||
|
||||
pub fn update_output_frequency(&mut self, output_frequency: u64) {
|
||||
@ -123,7 +261,7 @@ impl InterpolatingResampler {
|
||||
self.psg_resampler.update_output_frequency(self.output_frequency);
|
||||
|
||||
for resampler in self.pcm_resamplers.iter_mut().flatten() {
|
||||
resampler.update_output_frequency(self.output_frequency);
|
||||
resampler.update_output_frequency(output_frequency);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,11 +281,11 @@ impl InterpolatingResampler {
|
||||
|
||||
let mut pcm_a = self.pcm_resamplers[0]
|
||||
.as_mut()
|
||||
.and_then(QualitySincResampler::output_buffer_pop_front)
|
||||
.and_then(EnhancedResampler::output_buffer_pop_front)
|
||||
.unwrap_or_else(|| [f64::from(self.pcm_samples[0]) / 128.0])[0];
|
||||
let mut pcm_b = self.pcm_resamplers[1]
|
||||
.as_mut()
|
||||
.and_then(QualitySincResampler::output_buffer_pop_front)
|
||||
.and_then(EnhancedResampler::output_buffer_pop_front)
|
||||
.unwrap_or_else(|| [f64::from(self.pcm_samples[1]) / 128.0])[0];
|
||||
|
||||
if pcm_volume_shifts[0] {
|
||||
|
||||
@ -99,7 +99,7 @@ impl PwmAudioFilter {
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
pub struct PwmResampler {
|
||||
filter: PwmAudioFilter,
|
||||
resampler: CubicResampler,
|
||||
resampler: CubicResampler<2>,
|
||||
output: VecDeque<(f64, f64)>,
|
||||
}
|
||||
|
||||
@ -107,14 +107,14 @@ impl PwmResampler {
|
||||
pub fn new(config: &Sega32XEmulatorConfig, output_frequency: u64) -> Self {
|
||||
Self {
|
||||
filter: PwmAudioFilter::new(config, output_frequency),
|
||||
resampler: CubicResampler::new(22000.0),
|
||||
resampler: CubicResampler::new(22000.0, output_frequency),
|
||||
output: VecDeque::with_capacity(48000 / 30),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_sample(&mut self, sample_l: f64, sample_r: f64) {
|
||||
self.resampler.collect_sample(sample_l, sample_r);
|
||||
while let Some((output_l, output_r)) = self.resampler.output_buffer_pop_front() {
|
||||
self.resampler.collect_sample([sample_l, sample_r]);
|
||||
while let Some([output_l, output_r]) = self.resampler.output_buffer_pop_front() {
|
||||
let (output_l, output_r) = self.filter.filter((output_l, output_r));
|
||||
self.output.push_back((output_l, output_r));
|
||||
}
|
||||
|
||||
@ -1,84 +1,72 @@
|
||||
use crate::audio::{
|
||||
DEFAULT_OUTPUT_FREQUENCY, RESAMPLE_SCALING_FACTOR, interpolate_cubic_hermite_6p,
|
||||
};
|
||||
use crate::audio::{RESAMPLE_SCALING_FACTOR, interpolate_cubic_hermite_6p};
|
||||
use bincode::{Decode, Encode};
|
||||
use std::array;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
const BUFFER_LEN: usize = 6;
|
||||
|
||||
#[derive(Debug, Clone, Encode, Decode)]
|
||||
pub struct CubicResampler {
|
||||
pub struct CubicResampler<const CHANNELS: usize> {
|
||||
scaled_source_frequency: u64,
|
||||
output_frequency: u64,
|
||||
cycle_counter_product: u64,
|
||||
scaled_x_counter: u64,
|
||||
input_samples_l: VecDeque<f64>,
|
||||
input_samples_r: VecDeque<f64>,
|
||||
output_samples: VecDeque<(f64, f64)>,
|
||||
input_samples: VecDeque<[f64; CHANNELS]>,
|
||||
output_samples: VecDeque<[f64; CHANNELS]>,
|
||||
}
|
||||
|
||||
impl CubicResampler {
|
||||
impl<const CHANNELS: usize> CubicResampler<CHANNELS> {
|
||||
#[must_use]
|
||||
pub fn new(source_frequency: f64) -> Self {
|
||||
pub fn new(source_frequency: f64, output_frequency: u64) -> Self {
|
||||
let scaled_source_frequency = scale_source_frequency(source_frequency);
|
||||
|
||||
let mut resampler = Self {
|
||||
scaled_source_frequency,
|
||||
output_frequency: DEFAULT_OUTPUT_FREQUENCY,
|
||||
output_frequency,
|
||||
cycle_counter_product: 0,
|
||||
scaled_x_counter: 0,
|
||||
input_samples_l: VecDeque::with_capacity(2 * BUFFER_LEN),
|
||||
input_samples_r: VecDeque::with_capacity(2 * BUFFER_LEN),
|
||||
input_samples: VecDeque::with_capacity(2 * BUFFER_LEN),
|
||||
output_samples: VecDeque::with_capacity(48000 / 60 * 2),
|
||||
};
|
||||
|
||||
resampler.input_samples_l.extend([0.0; BUFFER_LEN]);
|
||||
resampler.input_samples_r.extend([0.0; BUFFER_LEN]);
|
||||
resampler.input_samples.extend([[0.0; CHANNELS]; BUFFER_LEN]);
|
||||
|
||||
resampler
|
||||
}
|
||||
|
||||
pub fn collect_sample(&mut self, sample_l: f64, sample_r: f64) {
|
||||
self.input_samples_l.push_back(sample_l);
|
||||
self.input_samples_r.push_back(sample_r);
|
||||
pub fn collect_sample(&mut self, samples: [f64; CHANNELS]) {
|
||||
self.input_samples.push_back(samples);
|
||||
|
||||
let scaled_output_frequency = self.output_frequency * RESAMPLE_SCALING_FACTOR;
|
||||
self.cycle_counter_product += scaled_output_frequency;
|
||||
while self.cycle_counter_product >= self.scaled_source_frequency {
|
||||
self.cycle_counter_product -= self.scaled_source_frequency;
|
||||
|
||||
// Having fewer than N samples in the buffers _shouldn't_ happen, but don't crash if it does
|
||||
while self.input_samples.len() < BUFFER_LEN {
|
||||
self.input_samples
|
||||
.push_front(self.input_samples.front().copied().unwrap_or([0.0; CHANNELS]));
|
||||
}
|
||||
|
||||
let x = (self.scaled_x_counter as f64) / (scaled_output_frequency as f64);
|
||||
let output: [f64; CHANNELS] = array::from_fn(|channel| {
|
||||
let samples: [f64; 6] = array::from_fn(|i| self.input_samples[i][channel]);
|
||||
interpolate_cubic_hermite_6p(samples, x).clamp(-1.0, 1.0)
|
||||
});
|
||||
|
||||
self.output_samples.push_back(output);
|
||||
|
||||
self.scaled_x_counter += self.scaled_source_frequency;
|
||||
while self.scaled_x_counter >= scaled_output_frequency {
|
||||
self.scaled_x_counter -= scaled_output_frequency;
|
||||
|
||||
self.input_samples_l.pop_front();
|
||||
self.input_samples_r.pop_front();
|
||||
self.input_samples.pop_front();
|
||||
}
|
||||
|
||||
// Having fewer than N samples in the buffers _shouldn't_ happen, but don't crash if it does
|
||||
while self.input_samples_l.len() < BUFFER_LEN {
|
||||
self.input_samples_l.push_back(0.0);
|
||||
}
|
||||
while self.input_samples_r.len() < BUFFER_LEN {
|
||||
self.input_samples_r.push_back(0.0);
|
||||
}
|
||||
|
||||
let x = (self.scaled_x_counter as f64) / (scaled_output_frequency as f64);
|
||||
let output_l =
|
||||
interpolate_cubic_hermite_6p(first_six_samples(&self.input_samples_l), x)
|
||||
.clamp(-1.0, 1.0);
|
||||
let output_r =
|
||||
interpolate_cubic_hermite_6p(first_six_samples(&self.input_samples_r), x)
|
||||
.clamp(-1.0, 1.0);
|
||||
self.output_samples.push_back((output_l, output_r));
|
||||
}
|
||||
|
||||
// Having more than N+1 samples in the buffers here also _shouldn't_ happen, but do something reasonable if it does
|
||||
while self.input_samples_l.len() > BUFFER_LEN + 1 {
|
||||
self.input_samples_l.pop_front();
|
||||
}
|
||||
while self.input_samples_r.len() > BUFFER_LEN + 1 {
|
||||
self.input_samples_r.pop_front();
|
||||
while self.input_samples.len() > BUFFER_LEN + 1 {
|
||||
self.input_samples.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +76,7 @@ impl CubicResampler {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn output_buffer_pop_front(&mut self) -> Option<(f64, f64)> {
|
||||
pub fn output_buffer_pop_front(&mut self) -> Option<[f64; CHANNELS]> {
|
||||
self.output_samples.pop_front()
|
||||
}
|
||||
|
||||
@ -120,10 +108,6 @@ impl CubicResampler {
|
||||
}
|
||||
}
|
||||
|
||||
fn first_six_samples(buffer: &VecDeque<f64>) -> [f64; 6] {
|
||||
[buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]]
|
||||
}
|
||||
|
||||
fn scale_source_frequency(source_frequency: f64) -> u64 {
|
||||
(source_frequency * RESAMPLE_SCALING_FACTOR as f64).round() as u64
|
||||
}
|
||||
|
||||
@ -193,5 +193,6 @@ impl GbaSaveMemory {
|
||||
pub enum GbaAudioInterpolation {
|
||||
#[default]
|
||||
NearestNeighbor,
|
||||
CubicHermite,
|
||||
WindowedSinc,
|
||||
}
|
||||
|
||||
@ -175,26 +175,46 @@ impl App {
|
||||
|
||||
let mut open = true;
|
||||
Window::new("GBA Audio Settings").open(&mut open).resizable(false).show(ctx, |ui| {
|
||||
let rect = ui
|
||||
.group(|ui| {
|
||||
ui.label("Audio interpolation");
|
||||
ui.group(|ui| {
|
||||
let rect = ui
|
||||
.scope(|ui| {
|
||||
ui.label("Audio interpolation");
|
||||
|
||||
ui.radio_value(
|
||||
&mut self.config.game_boy_advance.audio_interpolation,
|
||||
GbaAudioInterpolation::NearestNeighbor,
|
||||
"Nearest neighbor (Native)",
|
||||
);
|
||||
ui.radio_value(
|
||||
&mut self.config.game_boy_advance.audio_interpolation,
|
||||
GbaAudioInterpolation::WindowedSinc,
|
||||
"Windowed sinc (Anti-aliased)",
|
||||
);
|
||||
})
|
||||
.response
|
||||
.interact_rect;
|
||||
if ui.rect_contains_pointer(rect) {
|
||||
self.state.help_text.insert(WINDOW, helptext::AUDIO_INTERPOLATION);
|
||||
}
|
||||
for (value, label) in [
|
||||
(GbaAudioInterpolation::NearestNeighbor, "Nearest neighbor (Native)"),
|
||||
(GbaAudioInterpolation::CubicHermite, "Cubic Hermite"),
|
||||
(GbaAudioInterpolation::WindowedSinc, "Windowed sinc"),
|
||||
] {
|
||||
ui.radio_value(
|
||||
&mut self.config.game_boy_advance.audio_interpolation,
|
||||
value,
|
||||
label,
|
||||
);
|
||||
}
|
||||
})
|
||||
.response
|
||||
.interact_rect;
|
||||
if ui.rect_contains_pointer(rect) {
|
||||
self.state.help_text.insert(WINDOW, helptext::AUDIO_INTERPOLATION);
|
||||
}
|
||||
|
||||
let rect = ui
|
||||
.add_enabled_ui(
|
||||
self.config.game_boy_advance.audio_interpolation
|
||||
!= GbaAudioInterpolation::NearestNeighbor,
|
||||
|ui| {
|
||||
ui.checkbox(
|
||||
&mut self.config.game_boy_advance.psg_low_pass,
|
||||
"Apply low-pass filter to PSG",
|
||||
);
|
||||
},
|
||||
)
|
||||
.response
|
||||
.interact_rect;
|
||||
if ui.rect_contains_pointer(rect) {
|
||||
self.state.help_text.insert(WINDOW, helptext::PSG_LPF);
|
||||
}
|
||||
});
|
||||
|
||||
let rect = ui
|
||||
.group(|ui| {
|
||||
|
||||
@ -46,7 +46,16 @@ pub const AUDIO_INTERPOLATION: HelpText = HelpText {
|
||||
heading: "Audio Interpolation",
|
||||
text: &[
|
||||
"Optionally perform much higher quality audio interpolation than actual hardware does.",
|
||||
"Sinc interpolation significantly reduces audio aliasing and noise, but it can also make audio sound more muffled.",
|
||||
"Sinc interpolation reduces audio aliasing and noise more than cubic interpolation does, but it also usually sounds more muffled.",
|
||||
],
|
||||
};
|
||||
|
||||
pub const PSG_LPF: HelpText = HelpText {
|
||||
heading: "PSG Low-Pass Filter",
|
||||
text: &[
|
||||
"Optionally apply a low-pass filter to PSG audio output when using enhanced interpolation.",
|
||||
"In games that use both the PCM and PSG channels, this makes the PSG channels stand out less.",
|
||||
"The LPF cutoff frequency is set dynamically based on the PCM sample rate.",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ pub struct GameBoyAdvanceAppConfig {
|
||||
pub forced_save_memory_type: Option<GbaSaveMemory>,
|
||||
#[serde(default)]
|
||||
pub audio_interpolation: GbaAudioInterpolation,
|
||||
#[serde(default)]
|
||||
pub psg_low_pass: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
pub pulse_1_enabled: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
|
||||
@ -593,6 +593,7 @@ impl AppConfigExt for AppConfig {
|
||||
forced_save_memory_type: self.game_boy_advance.forced_save_memory_type,
|
||||
audio: GbaAudioConfig {
|
||||
audio_interpolation: self.game_boy_advance.audio_interpolation,
|
||||
psg_low_pass: self.game_boy_advance.psg_low_pass,
|
||||
pulse_1_enabled: self.game_boy_advance.pulse_1_enabled,
|
||||
pulse_2_enabled: self.game_boy_advance.pulse_2_enabled,
|
||||
wavetable_enabled: self.game_boy_advance.wavetable_enabled,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user