From 6173b5c71c47c7a5f646720ccae8dd82552e96d6 Mon Sep 17 00:00:00 2001 From: jdgleaver Date: Wed, 6 Apr 2022 13:36:22 +0100 Subject: [PATCH] Add optional 'fake' stereo sound effect --- src/drivers/libretro/libretro.c | 226 ++++++++++++++++++- src/drivers/libretro/libretro_core_options.h | 47 +++- 2 files changed, 267 insertions(+), 6 deletions(-) diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index febf8e6..c057484 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -804,6 +804,181 @@ static void palette_switch_set_index(uint32_t palette_index) * Palette switching END * ======================================== */ +/* ======================================== + * Stereo Filter START + * ======================================== */ + +enum stereo_filter_type +{ + STEREO_FILTER_NULL = 0, + STEREO_FILTER_DELAY +}; +static enum stereo_filter_type current_stereo_filter = STEREO_FILTER_NULL; + +#define STEREO_FILTER_DELAY_MS_DEFAULT 15.0f; +typedef struct +{ + int32_t *samples; + size_t samples_size; + size_t samples_pos; + size_t delay_count; +} stereo_filter_delay_t; +static stereo_filter_delay_t stereo_filter_delay; +static float stereo_filter_delay_ms = STEREO_FILTER_DELAY_MS_DEFAULT; + +static void stereo_filter_apply_null(int32_t *sound_buffer, size_t size) +{ + size_t i; + /* Each element of sound_buffer is a 16 bit mono sample + * stored in a 32 bit value. We convert this to stereo + * by copying the mono sample to both the high and low + * 16 bit regions of the value and casting sound_buffer + * to int16_t when uploading to the frontend */ + for (i = 0; i < size; i++) + sound_buffer[i] = (sound_buffer[i] << 16) | + (sound_buffer[i] & 0xFFFF); +} + +static void stereo_filter_apply_delay(int32_t *sound_buffer, size_t size) +{ + size_t delay_capacity = stereo_filter_delay.samples_size - + stereo_filter_delay.samples_pos; + size_t i; + + /* Copy current samples into the delay buffer + * (resizing if required) */ + if (delay_capacity < size) + { + int32_t *tmp_buffer = NULL; + size_t tmp_buffer_size; + + tmp_buffer_size = stereo_filter_delay.samples_size + (size - delay_capacity); + tmp_buffer_size = (tmp_buffer_size << 1) - (tmp_buffer_size >> 1); + tmp_buffer = (int32_t *)malloc(tmp_buffer_size * sizeof(int32_t)); + + memcpy(tmp_buffer, stereo_filter_delay.samples, + stereo_filter_delay.samples_pos * sizeof(int32_t)); + + free(stereo_filter_delay.samples); + + stereo_filter_delay.samples = tmp_buffer; + stereo_filter_delay.samples_size = tmp_buffer_size; + } + + for (i = 0; i < size; i++) + stereo_filter_delay.samples[i + + stereo_filter_delay.samples_pos] = sound_buffer[i]; + + stereo_filter_delay.samples_pos += size; + + /* If we have enough samples in the delay + * buffer, mix them into the output */ + if (stereo_filter_delay.samples_pos > + stereo_filter_delay.delay_count) + { + size_t delay_index = 0; + size_t samples_to_mix = stereo_filter_delay.samples_pos - + stereo_filter_delay.delay_count; + samples_to_mix = (samples_to_mix > size) ? + size : samples_to_mix; + + /* Perform 'null' filtering for any samples for + * which a delay buffer entry is unavailable */ + if (size > samples_to_mix) + for (i = 0; i < size - samples_to_mix; i++) + sound_buffer[i] = (sound_buffer[i] << 16) | + (sound_buffer[i] & 0xFFFF); + + /* Each element of sound_buffer is a 16 bit mono sample + * stored in a 32 bit value. We convert this to stereo + * by copying the mono sample to the high (left channel) + * 16 bit region and the delayed sample to the low + * (right channel) region, casting sound_buffer + * to int16_t when uploading to the frontend */ + for (i = size - samples_to_mix; i < size; i++) + sound_buffer[i] = (sound_buffer[i] << 16) | + (stereo_filter_delay.samples[delay_index++] & 0xFFFF); + + /* Remove the mixed samples from the delay buffer */ + memmove(stereo_filter_delay.samples, + stereo_filter_delay.samples + samples_to_mix, + (stereo_filter_delay.samples_pos - samples_to_mix) * + sizeof(int32_t)); + stereo_filter_delay.samples_pos -= samples_to_mix; + } + /* Otherwise apply the regular 'null' filter */ + else + for (i = 0; i < size; i++) + sound_buffer[i] = (sound_buffer[i] << 16) | + (sound_buffer[i] & 0xFFFF); +} + +static void (*stereo_filter_apply)(int32_t *sound_buffer, size_t size) = stereo_filter_apply_null; + +static void stereo_filter_deinit_delay(void) +{ + if (stereo_filter_delay.samples) + free(stereo_filter_delay.samples); + + stereo_filter_delay.samples = NULL; + stereo_filter_delay.samples_size = 0; + stereo_filter_delay.samples_pos = 0; + stereo_filter_delay.delay_count = 0; +} + +static void stereo_filter_init_delay(void) +{ + size_t initial_samples_size; + + /* Convert delay (ms) to number of samples */ + stereo_filter_delay.delay_count = (size_t)( + (stereo_filter_delay_ms / 1000.0f) * + (float)sndsamplerate); + + /* Preallocate delay_count + worst case expected + * samples per frame to minimise reallocation of + * the samples buffer during runtime */ + initial_samples_size = stereo_filter_delay.delay_count + + (size_t)((float)sndsamplerate / NES_PAL_FPS) + 1; + + stereo_filter_delay.samples = (int32_t *)malloc( + initial_samples_size * sizeof(int32_t)); + stereo_filter_delay.samples_size = initial_samples_size; + stereo_filter_delay.samples_pos = 0; + + /* Assign function pointer */ + stereo_filter_apply = stereo_filter_apply_delay; +} + +static void stereo_filter_deinit(void) +{ + /* Clean up */ + stereo_filter_deinit_delay(); + /* Assign default function pointer */ + stereo_filter_apply = stereo_filter_apply_null; +} + +static void stereo_filter_init(void) +{ + stereo_filter_deinit(); + + /* Use a case statement to simplify matters + * if more filter types are added in the + * future... */ + switch (current_stereo_filter) + { + case STEREO_FILTER_DELAY: + stereo_filter_init_delay(); + break; + default: + break; + } +} + +/* ======================================== + * Stereo Filter END + * ======================================== */ + #ifdef HAVE_NTSC_FILTER /* ntsc */ #include "nes_ntsc.h" @@ -1137,6 +1312,7 @@ static bool update_option_visibility(void) "fceumm_sndvolume", "fceumm_sndquality", "fceumm_sndlowpass", + "fceumm_sndstereodelay", "fceumm_swapduty", "fceumm_apu_1", "fceumm_apu_2", @@ -1516,6 +1692,7 @@ void retro_deinit (void) NTSCFilter_Cleanup(); #endif palette_switch_deinit(); + stereo_filter_deinit(); } void retro_reset(void) @@ -1537,6 +1714,7 @@ static void check_variables(bool startup) struct retro_variable var = {0}; char key[256]; int i, enable_apu; + bool stereo_filter_updated = false; /* 1 = Performs only geometry update: e.g. overscans */ /* 2 = Performs video/geometry update when needed and timing changes: e.g. region and filter change */ @@ -1831,6 +2009,45 @@ static void check_variables(bool startup) FCEUI_SetLowPass(lowpass); } + var.key = "fceumm_sndstereodelay"; + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) + { + enum stereo_filter_type filter_type = STEREO_FILTER_NULL; + float filter_delay_ms = STEREO_FILTER_DELAY_MS_DEFAULT; + + if (strcmp(var.value, "disabled") && + (strlen(var.value) > 1)) + { + char value_str[3]; + + value_str[0] = var.value[0]; + value_str[1] = var.value[1]; + value_str[2] = '\0'; + + filter_type = STEREO_FILTER_DELAY; + filter_delay_ms = (float)atoi(var.value); + + filter_delay_ms = (filter_delay_ms < 1.0f) ? + 1.0f : filter_delay_ms; + filter_delay_ms = (filter_delay_ms > 32.0f) ? + 32.0f : filter_delay_ms; + } + + if ((filter_type != current_stereo_filter) || + ((filter_type == STEREO_FILTER_DELAY) && + (filter_delay_ms != stereo_filter_delay_ms))) + { + current_stereo_filter = filter_type; + stereo_filter_delay_ms = filter_delay_ms; + stereo_filter_updated = true; + } + } + + if ((stereo_filter_updated || + (audio_video_updated == 2)) && !startup) + stereo_filter_init(); + var.key = "fceumm_sndvolume"; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) @@ -2365,12 +2582,10 @@ void retro_run(void) FCEUD_UpdateInput(); FCEUI_Emulate(&gfx, &sound, &ssize, 0); - for (i = 0; i < ssize; i++) - sound[i] = (sound[i] << 16) | (sound[i] & 0xffff); - - audio_batch_cb((const int16_t*)sound, ssize); - retro_run_blit(gfx); + + stereo_filter_apply(sound, ssize); + audio_batch_cb((const int16_t*)sound, ssize); } size_t retro_serialize_size(void) @@ -3025,6 +3240,7 @@ bool retro_load_game(const struct retro_game_info *info) ResetPalette(); FCEUD_SoundToggle(); check_variables(true); + stereo_filter_init(); PowerNES(); FCEUI_DisableFourScore(1); diff --git a/src/drivers/libretro/libretro_core_options.h b/src/drivers/libretro/libretro_core_options.h index 1bc1368..488b474 100644 --- a/src/drivers/libretro/libretro_core_options.h +++ b/src/drivers/libretro/libretro_core_options.h @@ -21,7 +21,7 @@ extern "C" { ******************************** */ -#define MAX_CORE_OPTIONS 36 +#define MAX_CORE_OPTIONS 37 /* RETRO_LANGUAGE_ENGLISH */ @@ -268,6 +268,51 @@ struct retro_core_option_v2_definition option_defs[] = { }, "disabled", }, + { + "fceumm_sndstereodelay", + "Stereo Sound Effect", + NULL, + "Enable a fake stereo sound effect by delaying the right audio channel when upmixing the mono signal output from the NES.", + NULL, + "audio", + { + { "disabled", NULL }, + { "01_ms_delay", "1ms Delay" }, + { "02_ms_delay", "2ms Delay" }, + { "03_ms_delay", "3ms Delay" }, + { "04_ms_delay", "4ms Delay" }, + { "05_ms_delay", "5ms Delay" }, + { "06_ms_delay", "6ms Delay" }, + { "07_ms_delay", "7ms Delay" }, + { "08_ms_delay", "8ms Delay" }, + { "09_ms_delay", "9ms Delay" }, + { "10_ms_delay", "10ms Delay" }, + { "11_ms_delay", "11ms Delay" }, + { "12_ms_delay", "12ms Delay" }, + { "13_ms_delay", "13ms Delay" }, + { "14_ms_delay", "14ms Delay" }, + { "15_ms_delay", "15ms Delay (Default)" }, + { "16_ms_delay", "16ms Delay" }, + { "17_ms_delay", "17ms Delay" }, + { "18_ms_delay", "18ms Delay" }, + { "19_ms_delay", "19ms Delay" }, + { "20_ms_delay", "20ms Delay" }, + { "21_ms_delay", "21ms Delay" }, + { "22_ms_delay", "22ms Delay" }, + { "23_ms_delay", "23ms Delay" }, + { "24_ms_delay", "24ms Delay" }, + { "25_ms_delay", "25ms Delay" }, + { "26_ms_delay", "26ms Delay" }, + { "27_ms_delay", "27ms Delay" }, + { "28_ms_delay", "28ms Delay" }, + { "29_ms_delay", "29ms Delay" }, + { "30_ms_delay", "30ms Delay" }, + { "31_ms_delay", "31ms Delay" }, + { "32_ms_delay", "32ms Delay" }, + { NULL, NULL }, + }, + "disabled", + }, { "fceumm_swapduty", "Swap Audio Duty Cycles",