55a5664363
The noise suppression filter mistakenly operated on the assumption that input audio data would always be in 10ms segments, and would crash if audio data was larger than that size. Because speexdsp operates on fixed audio frame sizes only, we must buffer audio data to fit that frame processing size. This creates a troublesome situation where you must buffer around that specified frame size. The new steps for processing are: 1. Push audio data to input circular buffer. 2. Push number of audio frames and timestamp for that audio packet to an 'info' circular buffer. 3. Check size of input circular buffer, and while it's equal to or above the speexdsp frame size (10ms for minimum latency), pop from the input buffer to a temporary buffer (10ms frames) and process it, then push that temporary buffer to the output circular buffer. 4. Peek at the front of the 'info' circular buffer. 5. If the output circular buffer frame size is equal or larger than next expected number of frames, pop both the info and output buffer, and return the audio data with the expected audio frames/timestamp.
255 lines
6.9 KiB
C
255 lines
6.9 KiB
C
#include <stdint.h>
|
|
#include <inttypes.h>
|
|
|
|
#include <util/circlebuf.h>
|
|
#include <obs-module.h>
|
|
#include <speex/speex_preprocess.h>
|
|
|
|
/* -------------------------------------------------------- */
|
|
|
|
#define do_log(level, format, ...) \
|
|
blog(level, "[noise suppress: '%s'] " format, \
|
|
obs_source_get_name(ng->context), ##__VA_ARGS__)
|
|
|
|
#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__)
|
|
#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__)
|
|
|
|
#ifdef _DEBUG
|
|
#define debug(format, ...) do_log(LOG_DEBUG, format, ##__VA_ARGS__)
|
|
#else
|
|
#define debug(format, ...)
|
|
#endif
|
|
|
|
/* -------------------------------------------------------- */
|
|
|
|
#define S_SUPPRESS_LEVEL "suppress_level"
|
|
|
|
#define MT_ obs_module_text
|
|
#define TEXT_SUPPRESS_LEVEL MT_("NoiseSuppress.SuppressLevel")
|
|
|
|
#define MAX_PREPROC_CHANNELS 2
|
|
|
|
/* -------------------------------------------------------- */
|
|
|
|
struct noise_suppress_data {
|
|
obs_source_t *context;
|
|
int suppress_level;
|
|
|
|
size_t frames;
|
|
size_t channels;
|
|
|
|
struct circlebuf info_buffer;
|
|
struct circlebuf input_buffers[MAX_PREPROC_CHANNELS];
|
|
struct circlebuf output_buffers[MAX_PREPROC_CHANNELS];
|
|
|
|
/* Speex preprocessor state */
|
|
SpeexPreprocessState *states[MAX_PREPROC_CHANNELS];
|
|
|
|
/* 16 bit PCM buffers */
|
|
float *copy_buffers[MAX_PREPROC_CHANNELS];
|
|
spx_int16_t *segment_buffers[MAX_PREPROC_CHANNELS];
|
|
|
|
/* output data */
|
|
struct obs_audio_data output_audio;
|
|
DARRAY(float) output_data;
|
|
};
|
|
|
|
/* -------------------------------------------------------- */
|
|
|
|
#define SUP_MIN -60
|
|
#define SUP_MAX 0
|
|
|
|
static const float c_32_to_16 = (float)INT16_MAX;
|
|
static const float c_16_to_32 = ((float)INT16_MAX + 1.0f);
|
|
|
|
/* -------------------------------------------------------- */
|
|
|
|
static const char *noise_suppress_name(void *unused)
|
|
{
|
|
UNUSED_PARAMETER(unused);
|
|
return obs_module_text("NoiseSuppress");
|
|
}
|
|
|
|
static void noise_suppress_destroy(void *data)
|
|
{
|
|
struct noise_suppress_data *ng = data;
|
|
|
|
for (size_t i = 0; i < ng->channels; i++) {
|
|
speex_preprocess_state_destroy(ng->states[i]);
|
|
circlebuf_free(&ng->input_buffers[i]);
|
|
circlebuf_free(&ng->output_buffers[i]);
|
|
}
|
|
|
|
bfree(ng->segment_buffers[0]);
|
|
bfree(ng->copy_buffers[0]);
|
|
circlebuf_free(&ng->info_buffer);
|
|
da_free(ng->output_data);
|
|
bfree(ng);
|
|
}
|
|
|
|
static inline void alloc_channel(struct noise_suppress_data *ng,
|
|
uint32_t sample_rate, size_t channel, size_t frames)
|
|
{
|
|
ng->states[channel] = speex_preprocess_state_init((int)frames,
|
|
sample_rate);
|
|
|
|
circlebuf_reserve(&ng->input_buffers[channel], frames * sizeof(float));
|
|
circlebuf_reserve(&ng->output_buffers[channel], frames * sizeof(float));
|
|
}
|
|
|
|
static void noise_suppress_update(void *data, obs_data_t *s)
|
|
{
|
|
struct noise_suppress_data *ng = data;
|
|
|
|
uint32_t sample_rate = audio_output_get_sample_rate(obs_get_audio());
|
|
size_t channels = audio_output_get_channels(obs_get_audio());
|
|
size_t frames = (size_t)sample_rate / 100;
|
|
|
|
ng->suppress_level = (int)obs_data_get_int(s, S_SUPPRESS_LEVEL);
|
|
|
|
/* Process 10 millisecond segments to keep latency low */
|
|
ng->frames = frames;
|
|
ng->channels = channels;
|
|
|
|
/* Ignore if already allocated */
|
|
if (ng->states[0])
|
|
return;
|
|
|
|
/* One speex state for each channel (limit 2) */
|
|
ng->copy_buffers[0] = bmalloc(frames * channels * sizeof(float));
|
|
ng->segment_buffers[0] = bmalloc(frames * channels * sizeof(spx_int16_t));
|
|
|
|
if (channels == 2) {
|
|
ng->copy_buffers[1] = ng->copy_buffers[0] + frames;
|
|
ng->segment_buffers[1] = ng->segment_buffers[0] + frames;
|
|
}
|
|
|
|
for (size_t i = 0; i < channels; i++)
|
|
alloc_channel(ng, sample_rate, i, frames);
|
|
}
|
|
|
|
static void *noise_suppress_create(obs_data_t *settings, obs_source_t *filter)
|
|
{
|
|
struct noise_suppress_data *ng =
|
|
bzalloc(sizeof(struct noise_suppress_data));
|
|
|
|
ng->context = filter;
|
|
noise_suppress_update(ng, settings);
|
|
return ng;
|
|
}
|
|
|
|
static inline void process(struct noise_suppress_data *ng)
|
|
{
|
|
/* Pop from input circlebuf */
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
circlebuf_pop_front(&ng->input_buffers[i], ng->copy_buffers[i],
|
|
ng->frames * sizeof(float));
|
|
|
|
/* Set args */
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
speex_preprocess_ctl(ng->states[i],
|
|
SPEEX_PREPROCESS_SET_NOISE_SUPPRESS,
|
|
&ng->suppress_level);
|
|
|
|
/* Convert to 16bit */
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
for (size_t j = 0; j < ng->frames; j++)
|
|
ng->segment_buffers[i][j] = (spx_int16_t)
|
|
(ng->copy_buffers[i][j] * c_32_to_16);
|
|
|
|
/* Execute */
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
speex_preprocess_run(ng->states[i], ng->segment_buffers[i]);
|
|
|
|
/* Convert back to 32bit */
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
for (size_t j = 0; j < ng->frames; j++)
|
|
ng->copy_buffers[i][j] =
|
|
(float)ng->segment_buffers[i][j] / c_16_to_32;
|
|
|
|
/* Push to output circlebuf */
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
circlebuf_push_back(&ng->output_buffers[i], ng->copy_buffers[i],
|
|
ng->frames * sizeof(float));
|
|
}
|
|
|
|
struct ng_audio_info {
|
|
uint32_t frames;
|
|
uint64_t timestamp;
|
|
};
|
|
|
|
static struct obs_audio_data *noise_suppress_filter_audio(void *data,
|
|
struct obs_audio_data *audio)
|
|
{
|
|
struct noise_suppress_data *ng = data;
|
|
struct ng_audio_info info;
|
|
size_t segment_size = ng->frames * sizeof(float);
|
|
size_t out_size;
|
|
|
|
if (!ng->states[0])
|
|
return audio;
|
|
|
|
info.frames = audio->frames;
|
|
info.timestamp = audio->timestamp;
|
|
circlebuf_push_back(&ng->info_buffer, &info, sizeof(info));
|
|
|
|
for (size_t i = 0; i < ng->channels; i++)
|
|
circlebuf_push_back(&ng->input_buffers[i], audio->data[i],
|
|
audio->frames * sizeof(float));
|
|
|
|
while (ng->input_buffers[0].size >= segment_size)
|
|
process(ng);
|
|
|
|
memset(&info, 0, sizeof(info));
|
|
circlebuf_peek_front(&ng->info_buffer, &info, sizeof(info));
|
|
out_size = info.frames * sizeof(float);
|
|
|
|
if (ng->output_buffers[0].size < out_size)
|
|
return NULL;
|
|
|
|
circlebuf_pop_front(&ng->info_buffer, NULL, sizeof(info));
|
|
da_resize(ng->output_data, out_size * ng->channels);
|
|
|
|
for (size_t i = 0; i < ng->channels; i++) {
|
|
ng->output_audio.data[i] =
|
|
(uint8_t*)&ng->output_data.array[i * out_size];
|
|
|
|
circlebuf_pop_front(&ng->output_buffers[i],
|
|
ng->output_audio.data[i],
|
|
out_size);
|
|
}
|
|
|
|
ng->output_audio.frames = info.frames;
|
|
ng->output_audio.timestamp = info.timestamp;
|
|
return &ng->output_audio;
|
|
}
|
|
|
|
static void noise_suppress_defaults(obs_data_t *s)
|
|
{
|
|
obs_data_set_default_int(s, S_SUPPRESS_LEVEL, -30);
|
|
}
|
|
|
|
static obs_properties_t *noise_suppress_properties(void *data)
|
|
{
|
|
obs_properties_t *ppts = obs_properties_create();
|
|
|
|
obs_properties_add_int_slider(ppts, S_SUPPRESS_LEVEL,
|
|
TEXT_SUPPRESS_LEVEL, SUP_MIN, SUP_MAX, 0);
|
|
|
|
UNUSED_PARAMETER(data);
|
|
return ppts;
|
|
}
|
|
|
|
struct obs_source_info noise_suppress_filter = {
|
|
.id = "noise_suppress_filter",
|
|
.type = OBS_SOURCE_TYPE_FILTER,
|
|
.output_flags = OBS_SOURCE_AUDIO,
|
|
.get_name = noise_suppress_name,
|
|
.create = noise_suppress_create,
|
|
.destroy = noise_suppress_destroy,
|
|
.update = noise_suppress_update,
|
|
.filter_audio = noise_suppress_filter_audio,
|
|
.get_defaults = noise_suppress_defaults,
|
|
.get_properties = noise_suppress_properties,
|
|
};
|