diff --git a/audio/ext/ssnes_dsp.h b/audio/ext/ssnes_dsp.h new file mode 100644 index 0000000000..d36e502454 --- /dev/null +++ b/audio/ext/ssnes_dsp.h @@ -0,0 +1,97 @@ +///// +// API header for external SSNES DSP plugins. +// +// + +#ifndef __SSNES_DSP_PLUGIN_H +#define __SSNES_DSP_PLUGIN_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _WIN32 +#ifdef SSNES_DLL_IMPORT +#define SSNES_API_EXPORT __declspec(dllimport) +#else +#define SSNES_API_EXPORT __declspec(dllexport) +#endif +#define SSNES_API_CALLTYPE __cdecl +#else +#define SSNES_API_EXPORT +#define SSNES_API_CALLTYPE +#endif + +#define SSNES_FALSE 0 +#define SSNES_TRUE 1 + +typedef struct ssnes_dsp_info +{ + // Input sample rate that the DSP plugin receives. This is generally ~32kHz. + // Some small variance is allowed due to syncing behavior. + float input_rate; + // SSNES requests that the DSP plugin resamples the + // input to a certain frequency. + // + // However, the plugin might ignore this + // using the resample field in ssnes_dsp_output_t (see below). + float output_rate; +} ssnes_dsp_info_t; + +typedef struct ssnes_dsp_output +{ + // The DSP plugin has to provide the buffering for the output samples. + // This is for performance reasons to avoid redundant copying of data. + // The samples are laid out in interleaving order: LRLRLRLR + // The range of the samples are [-1.0, 1.0]. + // This range cannot be exceeded without horrible audio glitches. + const float *samples; + // Frames which the DSP plugin outputted for the current process. + // One frame is here defined as a combined sample of + // left and right channels. + // (I.e. 44.1kHz, 16bit stereo will have + // 88.2k samples/sec and 44.1k frames/sec.) + unsigned frames; + + // If true, the DSP plugin did not resample the input audio, + // and requests resampling to the proper frequency to be + // performed outside the plugin. + // If false, + // it is assumed that the output has the same sample rate as the input. + int should_resample; +} ssnes_dsp_output_t; + +typedef struct ssnes_dsp_input +{ + // Input data for the DSP. The samples are interleaved in order: LRLRLRLR + const float *samples; + // Number of frames for input data. + // One frame is here defined as a combined sample of + // left and right channels. + // (I.e. 44.1kHz, 16bit stereo will have + // 88.2k samples/sec and 44.1k frames/sec.) + unsigned frames; +} ssnes_dsp_input_t; + +typedef struct ssnes_dsp_plugin +{ + // Creates a handle of the plugin. Returns NULL if failed. + void* (*init)(const ssnes_dsp_info_t *info); + + // Processes input data. + // The plugin is allowed to return variable sizes for output data. + void (*process)(void *data, ssnes_dsp_output_t *output, + const ssnes_dsp_input_t *input); + + // Frees the handle. + void (*free)(void *data); +} ssnes_dsp_plugin_t; + +SSNES_API_EXPORT const ssnes_dsp_plugin_t* SSNES_API_CALLTYPE + ssnes_dsp_plugin_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/driver.c b/driver.c index da5a75c7b5..47d9f4bc1f 100644 --- a/driver.c +++ b/driver.c @@ -146,6 +146,60 @@ void uninit_drivers(void) uninit_audio(); } +static void init_dsp_plugin(void) +{ + if (!(*g_settings.audio.dsp_plugin)) + return; + + g_extern.audio_data.dsp_lib = dylib_load(g_settings.audio.dsp_plugin); + if (!g_extern.audio_data.dsp_lib) + { + SSNES_ERR("Failed to open DSP plugin: \"%s\" ...\n", g_settings.audio.dsp_plugin); + return; + } + + const ssnes_dsp_plugin_t* (*SSNES_API_CALLTYPE plugin_init)(void) = dylib_proc(g_extern.audio_data.dsp_lib, "ssnes_dsp_plugin_init"); + if (!plugin_init) + { + SSNES_ERR("Failed to find symbol \"ssnes_dsp_plugin_init\" in DSP plugin.\n"); + dylib_close(g_extern.audio_data.dsp_lib); + g_extern.audio_data.dsp_lib = NULL; + return; + } + + g_extern.audio_data.dsp_plugin = plugin_init(); + if (!g_extern.audio_data.dsp_plugin) + { + SSNES_ERR("Failed to get a valid DSP plugin.\n"); + dylib_close(g_extern.audio_data.dsp_lib); + g_extern.audio_data.dsp_lib = NULL; + return; + } + + ssnes_dsp_info_t info = { + .input_rate = g_settings.audio.in_rate, + .output_rate = g_settings.audio.out_rate + }; + g_extern.audio_data.dsp_handle = g_extern.audio_data.dsp_plugin->init(&info); + if (!g_extern.audio_data.dsp_handle) + { + SSNES_ERR("Failed to init DSP plugin.\n"); + dylib_close(g_extern.audio_data.dsp_lib); + g_extern.audio_data.dsp_plugin = NULL; + g_extern.audio_data.dsp_lib = NULL; + return; + } +} + +static void deinit_dsp_plugin(void) +{ + if (g_extern.audio_data.dsp_lib && g_extern.audio_data.dsp_plugin) + { + g_extern.audio_data.dsp_plugin->free(g_extern.audio_data.dsp_handle); + dylib_close(g_extern.audio_data.dsp_lib); + } +} + #define AUDIO_CHUNK_SIZE_BLOCKING 64 #define AUDIO_CHUNK_SIZE_NONBLOCKING 2048 // So we don't get complete line-noise when fast-forwarding audio. #define AUDIO_MAX_RATIO 16 @@ -196,6 +250,8 @@ void init_audio(void) g_extern.audio_data.data_ptr = 0; assert((g_extern.audio_data.outsamples = malloc(max_bufsamples * sizeof(float) * AUDIO_MAX_RATIO))); assert((g_extern.audio_data.conv_outsamples = malloc(max_bufsamples * sizeof(int16_t) * AUDIO_MAX_RATIO))); + + init_dsp_plugin(); } void uninit_audio(void) @@ -221,6 +277,8 @@ void uninit_audio(void) free(g_extern.audio_data.data); g_extern.audio_data.data = NULL; free(g_extern.audio_data.outsamples); g_extern.audio_data.outsamples = NULL; free(g_extern.audio_data.conv_outsamples); g_extern.audio_data.conv_outsamples = NULL; + + deinit_dsp_plugin(); } #ifdef HAVE_DYLIB diff --git a/general.h b/general.h index 9c6ec9fc79..acdaabc77f 100644 --- a/general.h +++ b/general.h @@ -30,6 +30,7 @@ #include "netplay.h" #include "dynamic.h" #include "cheats.h" +#include "audio/ext/ssnes_dsp.h" #ifdef HAVE_CONFIG_H #include "config.h" @@ -103,6 +104,8 @@ struct settings unsigned latency; bool sync; int src_quality; + + char dsp_plugin[256]; } audio; struct @@ -191,6 +194,10 @@ struct global float *outsamples; int16_t *conv_outsamples; + + dylib_t dsp_lib; + const ssnes_dsp_plugin_t *dsp_plugin; + void *dsp_handle; } audio_data; struct diff --git a/settings.c b/settings.c index d6a49eade1..8570fb5b3f 100644 --- a/settings.c +++ b/settings.c @@ -367,6 +367,7 @@ static void parse_config_file(void) CONFIG_GET_STRING(video.driver, "video_driver"); CONFIG_GET_STRING(audio.driver, "audio_driver"); + CONFIG_GET_STRING(audio.dsp_plugin, "audio_dsp_plugin"); CONFIG_GET_STRING(input.driver, "input_driver"); CONFIG_GET_STRING(libsnes, "libsnes_path"); diff --git a/ssnes.c b/ssnes.c index 89b1fe185f..16b386cf59 100644 --- a/ssnes.c +++ b/ssnes.c @@ -152,58 +152,85 @@ static void audio_sample(uint16_t left, uint16_t right) } #endif + const float *output_data = NULL; + unsigned output_frames = 0; + g_extern.audio_data.data[g_extern.audio_data.data_ptr++] = (float)(int16_t)left/0x8000; g_extern.audio_data.data[g_extern.audio_data.data_ptr++] = (float)(int16_t)right/0x8000; - if (g_extern.audio_data.data_ptr >= g_extern.audio_data.chunk_size) - { + if (g_extern.audio_data.data_ptr < g_extern.audio_data.chunk_size) + return; - if (g_extern.frame_is_reverse) // Disable fucked up audio when rewinding... - memset(g_extern.audio_data.data, 0, g_extern.audio_data.chunk_size * sizeof(float)); + ssnes_dsp_input_t dsp_input = { + .samples = g_extern.audio_data.data, + .frames = g_extern.audio_data.data_ptr / 2 + }; + + ssnes_dsp_output_t dsp_output = { + .should_resample = SSNES_TRUE + }; + + if (g_extern.audio_data.dsp_plugin) + g_extern.audio_data.dsp_plugin->process(g_extern.audio_data.dsp_handle, &dsp_output, &dsp_input); + + if (g_extern.frame_is_reverse) // Disable fucked up audio when rewinding... + memset(g_extern.audio_data.data, 0, g_extern.audio_data.chunk_size * sizeof(float)); #ifdef HAVE_SRC - SRC_DATA src_data = { + SRC_DATA src_data = { #else - struct hermite_data src_data = { + struct hermite_data src_data = { #endif - .data_in = g_extern.audio_data.data, - .data_out = g_extern.audio_data.outsamples, - .input_frames = g_extern.audio_data.chunk_size / 2, - .output_frames = g_extern.audio_data.chunk_size * 8, - .end_of_input = 0, - .src_ratio = (double)g_settings.audio.out_rate / (double)g_settings.audio.in_rate, - }; + .data_in = dsp_output.samples ? dsp_output.samples : g_extern.audio_data.data, + .data_out = g_extern.audio_data.outsamples, + .input_frames = dsp_output.samples ? dsp_output.frames : (g_extern.audio_data.chunk_size / 2), + .output_frames = g_extern.audio_data.chunk_size * 8, + .end_of_input = 0, + .src_ratio = (double)g_settings.audio.out_rate / (double)g_settings.audio.in_rate, + }; + if (dsp_output.should_resample) + { #ifdef HAVE_SRC src_process(g_extern.audio_data.source, &src_data); #else hermite_process(g_extern.audio_data.source, &src_data); #endif - if (g_extern.audio_data.use_float) - { - if (driver.audio->write(driver.audio_data, g_extern.audio_data.outsamples, src_data.output_frames_gen * sizeof(float) * 2) < 0) - { - fprintf(stderr, "SSNES [ERROR]: Audio backend failed to write. Will continue without sound.\n"); - g_extern.audio_active = false; - } - } - else - { - for (unsigned i = 0; i < src_data.output_frames_gen * 2; i++) - { - int32_t val = g_extern.audio_data.outsamples[i] * 0x8000; - g_extern.audio_data.conv_outsamples[i] = (val > 0x7FFF) ? 0x7FFF : (val < -0x8000 ? -0x8000 : (int16_t)val); - } - if (driver.audio->write(driver.audio_data, g_extern.audio_data.conv_outsamples, src_data.output_frames_gen * sizeof(int16_t) * 2) < 0) - { - fprintf(stderr, "SSNES [ERROR]: Audio backend failed to write. Will continue without sound.\n"); - g_extern.audio_active = false; - } - } - - g_extern.audio_data.data_ptr = 0; + output_data = g_extern.audio_data.outsamples; + output_frames = src_data.output_frames_gen; } + else + { + output_data = dsp_output.samples; + output_frames = dsp_output.frames; + } + + + if (g_extern.audio_data.use_float) + { + if (driver.audio->write(driver.audio_data, g_extern.audio_data.outsamples, src_data.output_frames_gen * sizeof(float) * 2) < 0) + { + fprintf(stderr, "SSNES [ERROR]: Audio backend failed to write. Will continue without sound.\n"); + g_extern.audio_active = false; + } + } + else + { + for (unsigned i = 0; i < src_data.output_frames_gen * 2; i++) + { + int32_t val = output_data[i] * 0x8000; + g_extern.audio_data.conv_outsamples[i] = (val > 0x7FFF) ? 0x7FFF : (val < -0x8000 ? -0x8000 : (int16_t)val); + } + + if (driver.audio->write(driver.audio_data, g_extern.audio_data.conv_outsamples, output_frames * sizeof(int16_t) * 2) < 0) + { + fprintf(stderr, "SSNES [ERROR]: Audio backend failed to write. Will continue without sound.\n"); + g_extern.audio_active = false; + } + } + + g_extern.audio_data.data_ptr = 0; } static void input_poll(void) diff --git a/ssnes.cfg b/ssnes.cfg index 5f4f251eed..17c6de4162 100644 --- a/ssnes.cfg +++ b/ssnes.cfg @@ -105,6 +105,9 @@ # Override the default audio device the audio_driver uses. This is driver dependant. E.g. ALSA wants a PCM device, OSS wants a path (e.g. /dev/dsp), Jack wants portnames (e.g. system:playback1,system:playback_2), and so on ... # audio_device = +# External DSP plugin that processes audio before it's sent to the driver. +# audio_dsp_plugin = + # Will sync (block) on audio. Recommended. # audio_sync = true