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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ handle these variables being missing, but the shader may malfunction.)
this to convert the UV coordinates of the pixel being processed to the coordinates of that texel in the source
texture, or otherwise scale UV coordinate distances into texel distances.
* **`prevous_output`** (`texture2d`)—The previous output of the filter (2.5.0)
* **`audio_peak`** (`float`)—The instantaneous maximum audio level (peak) from the selected audio source, normalized to 0.0-1.0.
More reactive to sudden sounds like drums.
* **`audio_magnitude`** (`float`)—The RMS (Root Mean Square) audio level from the selected audio source, normalized to 0.0-1.0.
Smoother representation of sustained audio levels.

### Optional Preprocessing Macros

Expand All @@ -164,6 +168,7 @@ I recommend *.shader* as they do not require `Use Effect File (.effect)` as pixe
| animated_texture.effect | Animates a texture with polar sizing and color options | |
| alpha-gaming-bent-camera.shader | | ![image](https://github.com/exeldro/obs-shaderfilter/assets/5457024/5fb6fec8-fc1b-46eb-96aa-17ce37a7ca20) |
| ascii.shader | a little example of ascii art | ![image](https://github.com/exeldro/obs-shaderfilter/assets/5457024/682ad2d3-d32a-464e-a3af-0791ba0fc829) |
| audio.shader | An example of a shader that reacts to real audio monitoring.
| background_removal.effect | simple implementation of background removal. Optional color space corrections | |
| blink.shader | A shader that fades the opacity of the output in and out over time, with a configurable speed multiplier. Demonstrates the user of the `elapsed_time` parameter. | |
| bloom.shader | simple shaders to add bloom effects | ![image](https://github.com/exeldro/obs-shaderfilter/assets/5457024/567e5dc4-ec20-42fa-a344-2be1e6516b01) |
Expand Down
42 changes: 42 additions & 0 deletions data/examples/audio.shader
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Audio shader example showing the difference between audio_peak and audio_magnitude.
// Left half uses audio_peak (red), right half uses audio_magnitude (blue).
uniform float audio_peak;
uniform float audio_magnitude;

uniform float intensity <
string label = "Audio intensity";
string widget_type = "slider";
float minimum = 0.1;
float maximum = 3.0;
float step = 0.1;
> = 1.0;

float4 mainImage(VertData v_in) : TARGET {
float4 color = image.Sample(textureSampler, v_in.uv);

// Split screen based on UV coordinate
if (v_in.uv.x < 0.5) {
// Left half: audio_peak (instantaneous spikes, more reactive)
// Tint with red to show peak activity.
float peak_strength = audio_peak * intensity;
float3 peak_color = color.rgb + float3(peak_strength, 0, 0);
return float4(peak_color, color.a);
} else {
// Right half: audio_magnitude (RMS/averaged levels, smoother)
// Tint with blue to show magnitude activity.
float mag_strength = audio_magnitude * intensity;
float3 mag_color = color.rgb + float3(0, 0, mag_strength);
return float4(mag_color, color.a);
}
}

/*
EXPLANATION:
- audio_peak: Shows instantaneous maximum levels, very responsive to drums/percussion.
- audio_magnitude: Shows RMS (Root Mean Square) levels, smoother and represents sustained audio.

TYPICAL BEHAVIOR:
- With music containing drums: Left side (peak) will flash more dramatically on beats.
- With sustained tones: Right side (magnitude) will show more consistent levels.
- Peak reacts faster to sudden sounds, magnitude is more stable for smooth effects.
*/
140 changes: 140 additions & 0 deletions obs-shaderfilter.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <math.h>

#include <util/threading.h>
#ifdef _WIN32
Expand Down Expand Up @@ -225,6 +226,8 @@ struct shader_filter_data {
gs_eparam_t *param_transition_time;
gs_eparam_t *param_convert_linear;
gs_eparam_t *param_previous_output;
gs_eparam_t *param_audio_peak;
gs_eparam_t *param_audio_magnitude;

int expand_left;
int expand_right;
Expand All @@ -247,6 +250,13 @@ struct shader_filter_data {
float rand_f;
float rand_instance_f;
float rand_activation_f;
float audio_peak;
float audio_magnitude;

char *audio_source_name;
obs_volmeter_t *volmeter;
float current_audio_peak;
float current_audio_magnitude;

DARRAY(struct effect_param_data) stored_param_list;
};
Expand Down Expand Up @@ -351,6 +361,8 @@ static void shader_filter_clear_params(struct shader_filter_data *filter)
filter->param_loops = NULL;
filter->param_loop_second = NULL;
filter->param_local_time = NULL;
filter->param_audio_peak = NULL;
filter->param_audio_magnitude = NULL;
filter->param_image = NULL;
filter->param_previous_image = NULL;
filter->param_image_a = NULL;
Expand Down Expand Up @@ -612,6 +624,10 @@ static void shader_filter_reload_effect(struct shader_filter_data *filter)
filter->param_loop_second = param;
} else if (strcmp(info.name, "local_time") == 0) {
filter->param_local_time = param;
} else if (strcmp(info.name, "audio_peak") == 0) {
filter->param_audio_peak = param;
} else if (strcmp(info.name, "audio_magnitude") == 0) {
filter->param_audio_magnitude = param;
} else if (strcmp(info.name, "ViewProj") == 0) {
// Nothing.
} else if (strcmp(info.name, "image") == 0) {
Expand Down Expand Up @@ -742,6 +758,11 @@ static void shader_filter_destroy(void *data)
dstr_free(&filter->last_path);
da_free(filter->stored_param_list);

if (filter->volmeter)
obs_volmeter_destroy(filter->volmeter);
if (filter->audio_source_name)
bfree(filter->audio_source_name);

bfree(filter);
}

Expand Down Expand Up @@ -2097,6 +2118,53 @@ static bool shader_filter_convert(obs_properties_t *props, obs_property_t *prope

static const char *shader_filter_texture_file_filter = "Textures (*.bmp *.tga *.png *.jpeg *.jpg *.gif);;";

#define MIN_AUDIO_THRESHOLD -60.0f

static float convert_db_to_linear(float db_value)
{
if (db_value <= MIN_AUDIO_THRESHOLD || db_value > 0.0f)
return 0.0f;

return fmaxf(0.0f, fminf(1.0f, (db_value - MIN_AUDIO_THRESHOLD) / (-MIN_AUDIO_THRESHOLD)));
}

static void shader_filter_audio_callback(void *data, const float magnitude[MAX_AUDIO_CHANNELS],
const float peak[MAX_AUDIO_CHANNELS], const float input_peak[MAX_AUDIO_CHANNELS])
{
UNUSED_PARAMETER(input_peak);
struct shader_filter_data *filter = (struct shader_filter_data *)data;

float max_peak = MIN_AUDIO_THRESHOLD;
for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) {
if (peak[i] > max_peak && peak[i] != 0.0f) {
max_peak = peak[i];
}
}

float max_magnitude = MIN_AUDIO_THRESHOLD;
for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) {
if (magnitude[i] > max_magnitude && magnitude[i] != 0.0f) {
max_magnitude = magnitude[i];
}
}

filter->current_audio_peak = convert_db_to_linear(max_peak);
filter->current_audio_magnitude = convert_db_to_linear(max_magnitude);
}

static bool shader_filter_enum_audio_sources(void *data, obs_source_t *source)
{
obs_property_t *prop = (obs_property_t *)data;
uint32_t flags = obs_source_get_output_flags(source);

if ((flags & OBS_SOURCE_AUDIO) != 0) {
const char *name = obs_source_get_name(source);
obs_property_list_add_string(prop, name, name);
}

return true;
}

static obs_properties_t *shader_filter_properties(void *data)
{
struct shader_filter_data *filter = data;
Expand Down Expand Up @@ -2150,6 +2218,14 @@ static obs_properties_t *shader_filter_properties(void *data)
obs_properties_add_button(props, "reload_effect", obs_module_text("ShaderFilter.ReloadEffect"),
shader_filter_reload_effect_clicked);

if (filter && (filter->param_audio_magnitude || filter->param_audio_peak)) {
obs_property_t *audio_source = obs_properties_add_list(props, "audio_source", "Audio source", OBS_COMBO_TYPE_LIST,
OBS_COMBO_FORMAT_STRING);
obs_property_list_add_string(audio_source, "None", "");

obs_enum_sources(shader_filter_enum_audio_sources, audio_source);
}

DARRAY(obs_property_t *) groups;
da_init(groups);

Expand Down Expand Up @@ -2393,6 +2469,56 @@ static void shader_filter_update(void *data, obs_data_t *settings)
obs_source_update_properties(filter->context);
}

if (filter->param_audio_magnitude || filter->param_audio_peak) {
const char *audio_source_name = obs_data_get_string(settings, "audio_source");
if (!filter->audio_source_name || strcmp(filter->audio_source_name, audio_source_name) != 0) {
obs_source_t *audio_source = strlen(audio_source_name) > 0 ? obs_get_source_by_name(audio_source_name) : NULL;
if (audio_source && ((obs_source_get_output_flags(audio_source) & OBS_SOURCE_AUDIO) == 0)) {
obs_source_release(audio_source);
audio_source = NULL;
}
if (audio_source) {
if (filter->audio_source_name)
bfree(filter->audio_source_name);
filter->audio_source_name = bstrdup(audio_source_name);
}

if (!audio_source) {
audio_source = obs_source_get_ref(obs_filter_get_parent(filter->context));
if (audio_source && ((obs_source_get_output_flags(audio_source) & OBS_SOURCE_AUDIO) == 0)) {
obs_source_release(audio_source);
audio_source = NULL;
}
}
if (audio_source) {
if (!filter->volmeter) {
filter->volmeter = obs_volmeter_create(OBS_FADER_LOG);
obs_volmeter_add_callback(filter->volmeter, shader_filter_audio_callback, filter);
}
obs_volmeter_attach_source(filter->volmeter, audio_source);
obs_source_release(audio_source);
} else {
if (filter->volmeter) {
obs_volmeter_destroy(filter->volmeter);
filter->volmeter = NULL;
}
if (filter->audio_source_name) {
bfree(filter->audio_source_name);
filter->audio_source_name = NULL;
}
}
}
} else {
if (filter->volmeter) {
obs_volmeter_destroy(filter->volmeter);
filter->volmeter = NULL;
}
if (filter->audio_source_name) {
bfree(filter->audio_source_name);
filter->audio_source_name = NULL;
}
}

size_t param_count = filter->stored_param_list.num;
for (size_t param_index = 0; param_index < param_count; param_index++) {
struct effect_param_data *param = (filter->stored_param_list.array + param_index);
Expand Down Expand Up @@ -2664,6 +2790,14 @@ static void shader_filter_tick(void *data, float seconds)
// undecided between this and "rand_float(1);"
filter->rand_f = (float)((double)rand_interval(0, 10000) / (double)10000);

if (filter->volmeter) {
filter->audio_peak = filter->current_audio_peak;
filter->audio_magnitude = filter->current_audio_magnitude;
} else {
filter->audio_peak = 0.0f;
filter->audio_magnitude = 0.0f;
}

filter->output_rendered = false;
filter->input_rendered = false;
}
Expand Down Expand Up @@ -2832,6 +2966,12 @@ void shader_filter_set_effect_params(struct shader_filter_data *filter)
if (filter->param_local_time != NULL) {
gs_effect_set_float(filter->param_local_time, filter->local_time);
}
if (filter->param_audio_peak != NULL) {
gs_effect_set_float(filter->param_audio_peak, filter->audio_peak);
}
if (filter->param_audio_magnitude != NULL) {
gs_effect_set_float(filter->param_audio_magnitude, filter->audio_magnitude);
}
if (filter->param_loops != NULL) {
gs_effect_set_int(filter->param_loops, filter->loops);
}
Expand Down
Loading