diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b71d35..3d1d89d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,18 +89,10 @@ set(OBS_AUDIO_WAVE_SRC ${AW_SRC_DIR}/audiowave-themes.cpp ${AW_THM_DIR}/theme-line.cpp ${AW_THM_DIR}/theme-star.cpp - ${AW_THM_DIR}/theme-hex.cpp - ${AW_THM_DIR}/theme-abstract.cpp - ${AW_THM_DIR}/theme-doughnut.cpp - ${AW_THM_DIR}/theme-cosmic.cpp ${AW_THM_DIR}/theme-square.cpp ${AW_THM_DIR}/theme-circle.cpp - ${AW_THM_DIR}/theme-fluid.cpp - ${AW_THM_DIR}/theme-fluidblob.cpp - ${AW_THM_DIR}/theme-musicmagic.cpp - ${AW_THM_DIR}/theme-magicsquare.cpp - ${AW_THM_DIR}/theme-cartoonframe.cpp - ${AW_THM_DIR}/theme-lightning.cpp + ${AW_THM_DIR}/theme-hex.cpp + ${AW_THM_DIR}/theme-stacked-columns.cpp ${AW_THM_DIR}/theme-rounded-bars.cpp ) diff --git a/buildspec.json b/buildspec.json index 2653476..2906b8d 100644 --- a/buildspec.json +++ b/buildspec.json @@ -38,7 +38,7 @@ }, "name": "audio-wave", "displayName": "Create audio waves out of any audio source", - "version": "1.3.0", + "version": "2.0.0", "author": "MMLTech", "website": "https://ko-fi.com/mmltech", "email": "contact@obscountdown.com" diff --git a/src/audio-wave.cpp b/src/audio-wave.cpp index 81c04da..bd2c12d 100644 --- a/src/audio-wave.cpp +++ b/src/audio-wave.cpp @@ -18,10 +18,16 @@ static const char *kSourceName = "Audio Wave"; static const char *SETTING_AUDIO_SOURCE = "audio_source"; static const char *SETTING_WIDTH = "width"; static const char *SETTING_HEIGHT = "height"; -static const char *SETTING_INSET = "inset_ratio"; // NEW: global inset -static const char *SETTING_AMPLITUDE = "amplitude"; -static const char *SETTING_FRAME_DENSITY = "frame_density"; -static const char *SETTING_CURVE = "curve_power"; +static const char *SETTING_INSET = "inset_ratio"; +static const char *SETTING_COLOR = "color"; +static const char *SETTING_GRADIENT_ENABLED = "gradient_enabled"; +static const char *SETTING_GRADIENT_COLOR1 = "gradient_color1"; +static const char *SETTING_GRADIENT_COLOR2 = "gradient_color2"; +static const char *SETTING_GRADIENT_COLOR3 = "gradient_color3"; +static const char *SETTING_REACT_DB = "react_db"; +static const char *SETTING_PEAK_DB = "peak_db"; +static const char *SETTING_ATTACK_MS = "attack_ms"; +static const char *SETTING_RELEASE_MS = "release_ms"; static const char *SETTING_THEME = AW_SETTING_THEME; static const char *PROP_THEME_GROUP = "theme_group"; static struct obs_source_info audio_wave_source_info; @@ -90,18 +96,96 @@ void audio_wave_set_solid_color(gs_eparam_t *param, uint32_t color) gs_effect_set_vec4(param, &c); } -float audio_wave_apply_curve(const audio_wave_source *s, float v) +static uint32_t parse_hex_color_0xRRGGBB(const char *s, bool *ok) { - if (v < 0.0f) - v = 0.0f; - if (v > 1.0f) - v = 1.0f; + *ok = false; + if (!s) return 0; + while (*s == ' ' || *s == '\t' || *s == ',') ++s; + if (*s == '#') ++s; + if (!*s) return 0; + + unsigned v = 0; + int digits = 0; + for (; *s; ++s) { + char c = *s; + if (c == ' ' || c == '\t' || c == ',') + break; + unsigned d = 0; + if (c >= '0' && c <= '9') d = (unsigned)(c - '0'); + else if (c >= 'a' && c <= 'f') d = 10u + (unsigned)(c - 'a'); + else if (c >= 'A' && c <= 'F') d = 10u + (unsigned)(c - 'A'); + else return 0; + v = (v << 4) | d; + ++digits; + if (digits > 8) return 0; + } + + if (digits == 6) { + *ok = true; + return (uint32_t)v; + } + if (digits == 8) { + *ok = true; + return (uint32_t)(v & 0xFFFFFFu); + } + return 0; +} + +static inline uint32_t lerp_color(uint32_t a, uint32_t b, float t) +{ + const uint8_t ar = (uint8_t)(a & 0xFF); + const uint8_t ag = (uint8_t)((a >> 8) & 0xFF); + const uint8_t ab = (uint8_t)((a >> 16) & 0xFF); + + const uint8_t br = (uint8_t)(b & 0xFF); + const uint8_t bg = (uint8_t)((b >> 8) & 0xFF); + const uint8_t bb = (uint8_t)((b >> 16) & 0xFF); + + const float r = ar + (br - ar) * t; + const float g = ag + (bg - ag) * t; + const float bl = ab + (bb - ab) * t; + + return ((uint32_t)std::lround(bl) << 16) | ((uint32_t)std::lround(g) << 8) | (uint32_t)std::lround(r); +} - float p = s ? s->curve_power : 1.0f; - if (p <= 0.0f) - p = 1.0f; +static void build_gradient_lut(audio_wave_source *s, uint32_t c1, uint32_t c2, uint32_t c3, bool enabled) +{ + if (!s) + return; + + if (!enabled) { + s->gradient_enabled = false; + for (size_t i = 0; i < s->gradient_lut.size(); ++i) + s->gradient_lut[i] = s->color; + return; + } + + if ((c1 & 0xFFFFFFu) == 0) + c1 = s->color; + if ((c2 & 0xFFFFFFu) == 0) + c2 = s->color; + if ((c3 & 0xFFFFFFu) == 0) + c3 = s->color; + + for (int i = 0; i < 256; ++i) { + const float t = (float)i / 255.0f; + if (t <= 0.5f) { + const float u = t / 0.5f; + s->gradient_lut[(size_t)i] = lerp_color(c1, c2, u); + } else { + const float u = (t - 0.5f) / 0.5f; + s->gradient_lut[(size_t)i] = lerp_color(c2, c3, u); + } + } + + s->gradient_enabled = true; +} - return powf(v, p); +float audio_wave_apply_curve(const audio_wave_source * /*s*/, float v) +{ + if (v < 0.0f) v = 0.0f; + if (v > 1.0f) v = 1.0f; + return v; } void audio_wave_build_wave(audio_wave_source *s) @@ -113,6 +197,7 @@ void audio_wave_build_wave(audio_wave_source *s) const size_t frames = s->num_samples; if (!frames || s->samples_left.empty()) { + s->wave_raw.clear(); s->wave.clear(); return; } @@ -120,15 +205,69 @@ void audio_wave_build_wave(audio_wave_source *s) const auto &L = s->samples_left; const auto &R = s->samples_right; - s->wave.resize(frames); + s->wave_raw.resize(frames); for (size_t i = 0; i < frames; ++i) { const float l = (i < L.size()) ? L[i] : 0.0f; const float r = (i < R.size()) ? R[i] : l; - float m = s->gain * 0.5f * (std::fabs(l) + std::fabs(r)); - if (m > 1.0f) - m = 1.0f; - s->wave[i] = m; + const float lin = 0.5f * (std::fabs(l) + std::fabs(r)); + + float db = -120.0f; + if (lin > 0.000001f) + db = 20.0f * log10f(lin); + + const float react = s->react_db; + const float peak = std::max(s->peak_db, react + 0.1f); + float n = (db - react) / (peak - react); + if (n < 0.0f) n = 0.0f; + if (n > 1.0f) n = 1.0f; + + s->wave_raw[i] = n; + } +} + +static void audio_wave_smooth_wave(audio_wave_source *s) +{ + if (!s) + return; + if (s->wave_raw.empty()) { + s->wave.clear(); + s->last_wave_ts_ns = 0; + return; + } + + const uint64_t now = os_gettime_ns(); + float dt = 1.0f / 60.0f; + if (s->last_wave_ts_ns != 0 && now > s->last_wave_ts_ns) { + dt = (float)((now - s->last_wave_ts_ns) / 1e9); + dt = std::clamp(dt, 0.0f, 0.25f); + } + s->last_wave_ts_ns = now; + + if (s->wave.size() != s->wave_raw.size()) { + s->wave = s->wave_raw; + return; + } + + const float attack_s = std::max(0.0f, s->attack_ms) / 1000.0f; + const float release_s = std::max(0.0f, s->release_ms) / 1000.0f; + + for (size_t i = 0; i < s->wave.size(); ++i) { + const float target = s->wave_raw[i]; + float v = s->wave[i]; + const bool rising = target > v; + const float tau = rising ? attack_s : release_s; + if (tau <= 0.000001f) { + v = target; + } else { + const float a = 1.0f - std::exp(-dt / tau); + v = v + (target - v) * a; + } + if (v < 0.0f) + v = 0.0f; + if (v > 1.0f) + v = 1.0f; + s->wave[i] = v; } } @@ -172,13 +311,11 @@ auto *s = static_cast(param); if (!s || !audio) return; -// Lifetime guard: the callback may fire while the source is being destroyed. if (!s->alive.load(std::memory_order_acquire)) return; s->audio_cb_inflight.fetch_add(1, std::memory_order_acq_rel); -// Re-check after increment in case destroy flipped alive concurrently. if (!s->alive.load(std::memory_order_acquire)) { s->audio_cb_inflight.fetch_sub(1, std::memory_order_acq_rel); return; @@ -306,6 +443,30 @@ static bool on_theme_modified(obs_properties_t *props, obs_property_t *property, return true; } +static bool on_gradient_modified(obs_properties_t *props, obs_property_t *property, obs_data_t *settings) +{ + UNUSED_PARAMETER(property); + UNUSED_PARAMETER(settings); + + const bool use_grad = obs_data_get_bool(settings, SETTING_GRADIENT_ENABLED); + + obs_property_t *p_color = obs_properties_get(props, SETTING_COLOR); + obs_property_t *p_g1 = obs_properties_get(props, SETTING_GRADIENT_COLOR1); + obs_property_t *p_g2 = obs_properties_get(props, SETTING_GRADIENT_COLOR2); + obs_property_t *p_g3 = obs_properties_get(props, SETTING_GRADIENT_COLOR3); + + if (p_color) + obs_property_set_visible(p_color, !use_grad); + if (p_g1) + obs_property_set_visible(p_g1, use_grad); + if (p_g2) + obs_property_set_visible(p_g2, use_grad); + if (p_g3) + obs_property_set_visible(p_g3, use_grad); + + return true; +} + static obs_properties_t *audio_wave_get_properties(void *data) { UNUSED_PARAMETER(data); @@ -321,12 +482,35 @@ static obs_properties_t *audio_wave_get_properties(void *data) obs_properties_add_int(props, SETTING_WIDTH, "Width", 64, 4096, 1); obs_properties_add_int(props, SETTING_HEIGHT, "Height", 32, 2048, 1); - // NEW: global inset, applies to ALL themes via render transform obs_properties_add_float_slider(props, SETTING_INSET, "Inset (relative to canvas)", 0.0, 0.4, 0.01); - obs_properties_add_int_slider(props, SETTING_AMPLITUDE, "Amplitude (%)", 10, 400, 10); - obs_properties_add_int_slider(props, SETTING_CURVE, "Curve Power (%)", 20, 300, 5); - obs_properties_add_int_slider(props, SETTING_FRAME_DENSITY, "Shape Density (%)", 10, 300, 5); + obs_property_t *p_use_grad = obs_properties_add_bool(props, SETTING_GRADIENT_ENABLED, "Use Gradient"); + obs_properties_add_color(props, SETTING_COLOR, "Color"); + obs_properties_add_color(props, SETTING_GRADIENT_COLOR1, "Gradient Color 1"); + obs_properties_add_color(props, SETTING_GRADIENT_COLOR2, "Gradient Color 2"); + obs_properties_add_color(props, SETTING_GRADIENT_COLOR3, "Gradient Color 3"); + { + obs_property_t *pc = obs_properties_get(props, SETTING_COLOR); + obs_property_t *pg1 = obs_properties_get(props, SETTING_GRADIENT_COLOR1); + obs_property_t *pg2 = obs_properties_get(props, SETTING_GRADIENT_COLOR2); + obs_property_t *pg3 = obs_properties_get(props, SETTING_GRADIENT_COLOR3); + if (pc) + obs_property_set_visible(pc, true); + if (pg1) + obs_property_set_visible(pg1, false); + if (pg2) + obs_property_set_visible(pg2, false); + if (pg3) + obs_property_set_visible(pg3, false); + } + if (p_use_grad) + obs_property_set_modified_callback(p_use_grad, on_gradient_modified); + + obs_properties_add_float_slider(props, SETTING_REACT_DB, "React at (dB)", -80.0, -1.0, 1.0); + obs_properties_add_float_slider(props, SETTING_PEAK_DB, "Peak at (dB)", -60.0, 0.0, 1.0); + + obs_properties_add_int_slider(props, SETTING_ATTACK_MS, "Smoothing Attack (ms)", 0, 500, 1); + obs_properties_add_int_slider(props, SETTING_RELEASE_MS, "Smoothing Release (ms)", 0, 1500, 1); obs_property_t *theme_prop = obs_properties_add_list(props, SETTING_THEME, "Theme", OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); @@ -359,12 +543,17 @@ static void audio_wave_get_defaults(obs_data_t *settings) obs_data_set_default_int(settings, SETTING_WIDTH, 800); obs_data_set_default_int(settings, SETTING_HEIGHT, 200); - // NEW: default global inset obs_data_set_default_double(settings, SETTING_INSET, 0.08); - obs_data_set_default_int(settings, SETTING_AMPLITUDE, 200); - obs_data_set_default_int(settings, SETTING_CURVE, 100); - obs_data_set_default_int(settings, SETTING_FRAME_DENSITY, 100); + obs_data_set_default_int(settings, SETTING_COLOR, 0xFFFFFF); + obs_data_set_default_bool(settings, SETTING_GRADIENT_ENABLED, false); + obs_data_set_default_int(settings, SETTING_GRADIENT_COLOR1, 0x00D2FF); + obs_data_set_default_int(settings, SETTING_GRADIENT_COLOR2, 0x9D50BB); + obs_data_set_default_int(settings, SETTING_GRADIENT_COLOR3, 0xFF3CAC); + obs_data_set_default_double(settings, SETTING_REACT_DB, -50.0); + obs_data_set_default_double(settings, SETTING_PEAK_DB, -6.0); + obs_data_set_default_int(settings, SETTING_ATTACK_MS, 35); + obs_data_set_default_int(settings, SETTING_RELEASE_MS, 180); const audio_wave_theme *def = audio_wave_get_default_theme(); if (def) { @@ -388,43 +577,69 @@ static void audio_wave_update(void *data, obs_data_t *settings) s->audio_source_name = obs_data_get_string(settings, SETTING_AUDIO_SOURCE); - s->width = (int)aw_get_int_default(settings, SETTING_WIDTH, 800); - s->height = (int)aw_get_int_default(settings, SETTING_HEIGHT, 400); - - // NEW: global inset (0..0.4) - double inset = aw_get_float_default(settings, SETTING_INSET, 0.08f); - inset = std::clamp(inset, 0.0, 0.4); - s->inset_ratio = (float)inset; - - s->frame_density = (int)aw_get_int_default(settings, SETTING_FRAME_DENSITY, 100); - s->frame_density = std::clamp(s->frame_density, 10, 300); - - int amp_pct = (int)aw_get_int_default(settings, SETTING_AMPLITUDE, 200); - amp_pct = std::clamp(amp_pct, 10, 400); - s->gain = (float)amp_pct / 100.0f; - - int curve_pct = (int)aw_get_int_default(settings, SETTING_CURVE, 100); - curve_pct = std::clamp(curve_pct, 20, 300); - s->curve_power = (float)curve_pct / 100.0f; - - if (s->width < 1) - s->width = 1; - if (s->height < 1) - s->height = 1; + { + std::lock_guard g(s->render_mutex); + + s->width = (int)aw_get_int_default(settings, SETTING_WIDTH, 800); + s->height = (int)aw_get_int_default(settings, SETTING_HEIGHT, 200); + + double inset = aw_get_float_default(settings, SETTING_INSET, 0.08f); + inset = std::clamp(inset, 0.0, 0.4); + s->inset_ratio = (float)inset; + + double react = aw_get_float_default(settings, SETTING_REACT_DB, -50.0f); + double peak = aw_get_float_default(settings, SETTING_PEAK_DB, -6.0f); + react = std::clamp(react, -80.0, -1.0); + peak = std::clamp(peak, -60.0, 0.0); + if (peak <= react) + peak = react + 0.1; + + s->react_db = (float)react; + s->peak_db = (float)peak; + + int attack_ms = aw_get_int_default(settings, SETTING_ATTACK_MS, 35); + int release_ms = aw_get_int_default(settings, SETTING_RELEASE_MS, 180); + attack_ms = std::clamp(attack_ms, 0, 500); + release_ms = std::clamp(release_ms, 0, 1500); + s->attack_ms = (float)attack_ms; + s->release_ms = (float)release_ms; + + if (s->width < 1) + s->width = 1; + if (s->height < 1) + s->height = 1; + + uint32_t color = (uint32_t)aw_get_int_default(settings, SETTING_COLOR, 0xFFFFFF); + if ((color & 0xFFFFFFu) == 0) + color = 0xFFFFFF; + s->color = (color & 0xFFFFFFu); + + const bool use_grad = obs_data_get_bool(settings, SETTING_GRADIENT_ENABLED); + uint32_t g1 = (uint32_t)aw_get_int_default(settings, SETTING_GRADIENT_COLOR1, 0); + uint32_t g2 = (uint32_t)aw_get_int_default(settings, SETTING_GRADIENT_COLOR2, 0); + uint32_t g3 = (uint32_t)aw_get_int_default(settings, SETTING_GRADIENT_COLOR3, 0); + if (use_grad) { + build_gradient_lut(s, g1, g2, g3, true); + } else { + s->gradient_enabled = false; + for (size_t i = 0; i < s->gradient_lut.size(); ++i) + s->gradient_lut[i] = s->color; + } - const char *theme_id = obs_data_get_string(settings, SETTING_THEME); - const audio_wave_theme *new_theme = audio_wave_find_theme(theme_id); + const char *theme_id = obs_data_get_string(settings, SETTING_THEME); + const audio_wave_theme *new_theme = audio_wave_find_theme(theme_id); - if (s->theme && s->theme != new_theme && s->theme->destroy_data) { - s->theme->destroy_data(s); - s->theme_data = nullptr; - } + if (s->theme && s->theme != new_theme && s->theme->destroy_data) { + s->theme->destroy_data(s); + s->theme_data = nullptr; + } - s->theme = new_theme; - s->theme_id = theme_id ? theme_id : ""; + s->theme = new_theme; + s->theme_id = theme_id ? theme_id : ""; - if (s->theme && s->theme->update) { - s->theme->update(s, settings); + if (s->theme && s->theme->update) { + s->theme->update(s, settings); + } } attach_to_audio_source(s); @@ -438,9 +653,13 @@ static void *audio_wave_create(obs_data_t *settings, obs_source_t *source) s->self = source; s->color = 0xFFFFFF; s->mirror = false; + s->gradient_enabled = false; + for (size_t i = 0; i < s->gradient_lut.size(); ++i) + s->gradient_lut[i] = s->color; - // ensure a sane default even before update() s->inset_ratio = 0.08f; + s->react_db = -50.0f; + s->peak_db = -6.0f; audio_wave_update(s, settings); @@ -455,13 +674,10 @@ auto *s = static_cast(data); if (!s) return; -// Stop the audio callback from touching this instance. s->alive.store(false, std::memory_order_release); -// Detach callback first. detach_from_audio_source(s); -// Wait briefly for an in-flight callback to finish (prevents use-after-free). for (int i = 0; i < 2000; ++i) { if (s->audio_cb_inflight.load(std::memory_order_acquire) == 0) break; @@ -469,6 +685,7 @@ for (int i = 0; i < 2000; ++i) { } if (s->theme && s->theme->destroy_data) { + std::lock_guard g(s->render_mutex); s->theme->destroy_data(s); s->theme_data = nullptr; } @@ -524,31 +741,46 @@ static void audio_wave_video_render(void *data, gs_effect_t *effect) UNUSED_PARAMETER(effect); auto *s = static_cast(data); - if (!s || !s->theme || !s->theme->draw) + if (!s) return; - gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); - if (!solid) - return; + std::lock_guard g(s->render_mutex); - gs_eparam_t *color_param = gs_effect_get_param_by_name(solid, "color"); - gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); - if (!tech) + if (!s->theme || !s->theme->draw) return; audio_wave_build_wave(s); + audio_wave_smooth_wave(s); const float w = (float)s->width; const float h = (float)s->height; const float min_dim = std::min(w, h); - // NEW: apply global inset to ALL themes as a render transform const float inset_px = std::max(0.0f, s->inset_ratio) * min_dim; const float inner_w = std::max(1.0f, w - 2.0f * inset_px); const float inner_h = std::max(1.0f, h - 2.0f * inset_px); const float sx = inner_w / w; const float sy = inner_h / h; + if (s->theme->draw_background) { + gs_matrix_push(); + if (inset_px > 0.0f) { + gs_matrix_translate3f(inset_px, inset_px, 0.0f); + gs_matrix_scale3f(sx, sy, 1.0f); + } + s->theme->draw_background(s); + gs_matrix_pop(); + } + + gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); + if (!solid) + return; + + gs_eparam_t *color_param = gs_effect_get_param_by_name(solid, "color"); + gs_technique_t *tech = gs_effect_get_technique(solid, "Solid"); + if (!tech) + return; + size_t passes = gs_technique_begin(tech); for (size_t i = 0; i < passes; ++i) { gs_technique_begin_pass(tech, i); diff --git a/src/audiowave-themes.cpp b/src/audiowave-themes.cpp index 01b6234..70d0d86 100644 --- a/src/audiowave-themes.cpp +++ b/src/audiowave-themes.cpp @@ -5,16 +5,13 @@ #include "theme-hex.hpp" #include "theme-square.hpp" #include "theme-circle.hpp" -#include "theme-abstract.hpp" -#include "theme-doughnut.hpp" -#include "theme-cosmic.hpp" -#include "theme-fluid.hpp" -#include "theme-fluidblob.hpp" -#include "theme-musicmagic.hpp" -#include "theme-magicsquare.hpp" -#include "theme-cartoonframe.hpp" -#include "theme-lightning.hpp" +#include "theme-line.hpp" +#include "theme-star.hpp" +#include "theme-hex.hpp" +#include "theme-square.hpp" +#include "theme-circle.hpp" #include "theme-rounded-bars.hpp" +#include "theme-stacked-columns.hpp" static bool g_themes_registered = false; @@ -30,14 +27,6 @@ void audio_wave_register_builtin_themes() audio_wave_register_hex_theme(); audio_wave_register_square_theme(); audio_wave_register_circle_theme(); - audio_wave_register_abstract_theme(); - audio_wave_register_doughnut_theme(); - audio_wave_register_cosmic_theme(); - audio_wave_register_fluid_theme(); - audio_wave_register_fluidblob_theme(); - audio_wave_register_musicmagic_theme(); - audio_wave_register_magicsquare_theme(); - audio_wave_register_cartoonframe_theme(); - audio_wave_register_lightning_theme(); audio_wave_register_rounded_bars_theme(); + audio_wave_register_stacked_columns_theme(); } diff --git a/src/includes/audio-wave.hpp b/src/includes/audio-wave.hpp index e4445fe..ff08189 100644 --- a/src/includes/audio-wave.hpp +++ b/src/includes/audio-wave.hpp @@ -4,6 +4,9 @@ #include #include +#include +#include +#include #include #include #include @@ -48,32 +51,66 @@ struct audio_wave_source { std::vector samples_left; std::vector samples_right; size_t num_samples = 0; - std::vector wave; // normalized 0..1 amplitudes, built from samples + // Wave data: + // wave_raw: instantaneous (per-frame) normalized 0..1 built from samples + // wave: smoothed values used by themes for rendering + std::vector wave_raw; + std::vector wave; + + // Smoothing (attack/release in milliseconds). Applied to wave_raw -> wave. + float attack_ms = 35.0f; // rising (expand) + float release_ms = 180.0f; // falling (retract) + uint64_t last_wave_ts_ns = 0; // Core visual parameters int width = 800; int height = 200; float inset_ratio = 0.08f; - float gain = 2.0f; // overall amplitude multiplier - float curve_power = 1.0f; // curve shaping power int frame_density = 100; // 10..300 (%), interpreted by themes + // Audio response mapping (dBFS) + float react_db = -50.0f; // where motion starts + float peak_db = -6.0f; // where motion reaches 1.0 + + // Global color / gradient + uint32_t color = 0xFFFFFF; // fallback (0xRRGGBB) + bool gradient_enabled = false; + std::array gradient_lut{}; // precomputed sRGB 0xRRGGBB + + // Theme selection + basic state std::string theme_id; // current theme id std::string theme_style_id; // optional style inside a theme - uint32_t color = 0xFFFFFF; // primary color (fallback) bool mirror = false; // optional horizontal mirroring (if used by theme) // Generic theme palette: colors[0], colors[1], ... std::vector colors; + // Render-state guard (prevents update() vs video_render() races) + std::mutex render_mutex; + // Opaque per-instance data owned by the active theme void *theme_data = nullptr; const audio_wave_theme *theme = nullptr; }; // Small helper: safe color access with fallback + +// Fast gradient lookup (t clamped 0..1). Falls back to s->color if disabled. +inline uint32_t aw_gradient_color_at(const audio_wave_source *s, float t) +{ + if (!s) + return 0xFFFFFF; + if (!s->gradient_enabled) + return s->color; + + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + const int idx = (int)std::lround(t * 255.0f); + return s->gradient_lut[(size_t)std::clamp(idx, 0, 255)]; +} + inline uint32_t audio_wave_get_color(const audio_wave_source *s, size_t index, uint32_t fallback) { if (!s) diff --git a/src/themes/theme-abstract.cpp b/src/themes/theme-abstract.cpp deleted file mode 100644 index 4e8ce48..0000000 --- a/src/themes/theme-abstract.cpp +++ /dev/null @@ -1,398 +0,0 @@ -#include "theme-abstract.hpp" -#include -#include -#include -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_abstract = "abstract"; -static const char *k_theme_name_abstract = "Radial Abstraction"; - -static const char *ABSTRACT_PROP_COLOR_WAVE_A = "abs_color_wave_a"; -static const char *ABSTRACT_PROP_COLOR_WAVE_B = "abs_color_wave_b"; -static const char *ABSTRACT_PROP_COLOR_FILL = "abs_color_fill"; -static const char *ABSTRACT_PROP_COLOR_FIRE = "abs_color_fire"; - -static const char *ABSTRACT_PROP_DB_WAVE_A = "abs_db_wave_a"; -static const char *ABSTRACT_PROP_DB_WAVE_B = "abs_db_wave_b"; -static const char *ABSTRACT_PROP_DB_FIRE = "abs_db_fire"; - -static const char *ABSTRACT_PROP_SEGMENTS = "abs_segments"; -static const char *ABSTRACT_PROP_THICK_A = "abs_thickness_wave_a"; -static const char *ABSTRACT_PROP_THICK_B = "abs_thickness_wave_b"; - -struct abstract_theme_data { - std::vector prev_r1; - std::vector prev_r2; - bool initialized = false; - - float db_wave_a = -10.0f; - float db_wave_b = -20.0f; - float db_fire = -12.0f; - uint32_t segments = 128; - - int thick_a = 2; - int thick_b = 2; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void abstract_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, ABSTRACT_PROP_COLOR_WAVE_A, "Wave A Color"); - obs_properties_add_color(props, ABSTRACT_PROP_COLOR_WAVE_B, "Wave B Color"); - obs_properties_add_color(props, ABSTRACT_PROP_COLOR_FILL, "Fill Color"); - obs_properties_add_color(props, ABSTRACT_PROP_COLOR_FIRE, "Fireworks Color"); - - obs_properties_add_int_slider(props, ABSTRACT_PROP_DB_WAVE_A, "Wave A Target dB", -60, 0, 1); - obs_properties_add_int_slider(props, ABSTRACT_PROP_DB_WAVE_B, "Wave B Target dB", -60, 0, 1); - obs_properties_add_int_slider(props, ABSTRACT_PROP_DB_FIRE, "Fireworks Threshold (dB)", -60, 0, 1); - - obs_properties_add_int_slider(props, ABSTRACT_PROP_SEGMENTS, "Shape Resolution", 32, 512, 8); - obs_properties_add_int_slider(props, ABSTRACT_PROP_THICK_A, "Wave A Thickness", 1, 8, 1); - obs_properties_add_int_slider(props, ABSTRACT_PROP_THICK_B, "Wave B Thickness", 1, 8, 1); -} - -static void abstract_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_wave_a = (uint32_t)aw_get_int_default(settings, ABSTRACT_PROP_COLOR_WAVE_A, 0); - uint32_t col_wave_b = (uint32_t)aw_get_int_default(settings, ABSTRACT_PROP_COLOR_WAVE_B, 0); - uint32_t col_fill = (uint32_t)aw_get_int_default(settings, ABSTRACT_PROP_COLOR_FILL, 0); - uint32_t col_fire = (uint32_t)aw_get_int_default(settings, ABSTRACT_PROP_COLOR_FIRE, 0); - - if (col_wave_a == 0) - col_wave_a = 0xFF00FF; - if (col_wave_b == 0) - col_wave_b = 0x00FFFF; - if (col_fill == 0) - col_fill = 0x220022; - if (col_fire == 0) - col_fire = 0xFFFF00; - - s->color = col_wave_a; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"wave_a", col_wave_a}); - s->colors.push_back(audio_wave_named_color{"wave_b", col_wave_b}); - s->colors.push_back(audio_wave_named_color{"fill", col_fill}); - s->colors.push_back(audio_wave_named_color{"firework", col_fire}); - - int db_a = aw_get_int_default(settings, ABSTRACT_PROP_DB_WAVE_A, -10); - int db_b = aw_get_int_default(settings, ABSTRACT_PROP_DB_WAVE_B, -20); - int db_f = aw_get_int_default(settings, ABSTRACT_PROP_DB_FIRE, -12); - - db_a = std::clamp(db_a, -60, 0); - db_b = std::clamp(db_b, -60, 0); - db_f = std::clamp(db_f, -60, 0); - - int seg = aw_get_int_default(settings, ABSTRACT_PROP_SEGMENTS, 128); - seg = std::clamp(seg, 32, 512); - - int thick_a = aw_get_int_default(settings, ABSTRACT_PROP_THICK_A, 2); - int thick_b = aw_get_int_default(settings, ABSTRACT_PROP_THICK_B, 2); - thick_a = std::clamp(thick_a, 1, 8); - thick_b = std::clamp(thick_b, 1, 8); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new abstract_theme_data{}; - s->theme_data = d; - } - - d->db_wave_a = (float)db_a; - d->db_wave_b = (float)db_b; - d->db_fire = (float)db_f; - d->segments = (uint32_t)seg; - d->thick_a = thick_a; - d->thick_b = thick_b; - - d->initialized = false; -} - -// ───────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────── - -static inline float db_from_amp(float a) -{ - if (a <= 1e-6f) - return -120.0f; - return 20.0f * log10f(a); -} - -static float normalize_db_range(float a, float targetDb, float floorDb) -{ - if (a <= 1e-6f) - return 0.0f; - - float db = db_from_amp(a); - - if (db <= floorDb) - return 0.0f; - - if (targetDb > floorDb) { - float norm = (db - floorDb) / (targetDb - floorDb); - if (norm < 0.0f) - norm = 0.0f; - if (norm > 1.0f) - norm = 1.0f; - return norm; - } - - return 0.0f; -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void abstract_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new abstract_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float Rbase = std::min(w, h) * 0.25f; - const float RextA = std::min(w, h) * 0.20f; - const float RextB = std::min(w, h) * 0.15f; - const float Rfire = std::min(w, h) * 0.30f; - - const uint32_t segments = std::max(d->segments, 32u); - - const uint32_t col_wave_a = audio_wave_get_color(s, 0, s->color); - const uint32_t col_wave_b = audio_wave_get_color(s, 1, col_wave_a); - const uint32_t col_fill = audio_wave_get_color(s, 2, col_wave_b); - const uint32_t col_fire = audio_wave_get_color(s, 3, col_fill); - - std::vector amp(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const size_t idx = (size_t)(u * (float)(frames - 1)); - amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - std::vector amp_smooth(segments); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.20f; - for (uint32_t i = 1; i < segments; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - - float wrap = amp_smooth[segments - 1]; - amp_smooth[0] = 0.5f * (amp_smooth[0] + wrap); - } - - if (d->prev_r1.size() != segments) { - d->prev_r1.assign(segments, Rbase); - d->prev_r2.assign(segments, Rbase * 0.8f); - d->initialized = false; - } - - std::vector r1(segments), r2(segments); - - const float floorDb = -60.0f; - const float alpha_time = 0.30f; - - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - - float nA = normalize_db_range(a, d->db_wave_a, floorDb); - float nB = normalize_db_range(a, d->db_wave_b, floorDb); - - nA = audio_wave_apply_curve(s, nA); - nB = audio_wave_apply_curve(s, nB); - - float r1_target = Rbase + nA * RextA; - float r2_target = Rbase * 0.7f + nB * RextB; - - if (!d->initialized) { - d->prev_r1[i] = r1_target; - d->prev_r2[i] = r2_target; - } - - float r1_s = d->prev_r1[i] + alpha_time * (r1_target - d->prev_r1[i]); - float r2_s = d->prev_r2[i] + alpha_time * (r2_target - d->prev_r2[i]); - - d->prev_r1[i] = r1_s; - d->prev_r2[i] = r2_s; - - r1[i] = r1_s; - r2[i] = r2_s; - } - - d->initialized = true; - - std::vector cos_t(segments), sin_t(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float t = ((float)i / (float)segments) * 2.0f * (float)M_PI; - cos_t[i] = std::cos(t); - sin_t[i] = std::sin(t); - } - - std::vector r_outer(segments); - for (uint32_t i = 0; i < segments; ++i) { - r_outer[i] = std::max(r1[i], r2[i]); - } - - std::vector x_outer(segments), y_outer(segments); - for (uint32_t i = 0; i < segments; ++i) { - x_outer[i] = cx + cos_t[i] * r_outer[i]; - y_outer[i] = cy + sin_t[i] * r_outer[i]; - } - - gs_matrix_push(); - - if (color_param) - audio_wave_set_solid_color(color_param, col_fill); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - uint32_t next = (i + 1) % segments; - - gs_vertex2f(cx, cy); - gs_vertex2f(x_outer[i], y_outer[i]); - gs_vertex2f(x_outer[next], y_outer[next]); - } - gs_render_stop(GS_TRIS); - - if (color_param) - audio_wave_set_solid_color(color_param, col_wave_a); - - int thickA = d->thick_a; - if (thickA < 1) - thickA = 1; - - { - const float half = (float)(thickA - 1) * 0.5f; - - for (int t = 0; t < thickA; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - - float rad = r1[idx] + offset; - if (rad < 0.0f) - rad = 0.0f; - - const float x = cx + cos_t[idx] * rad; - const float y = cy + sin_t[idx] * rad; - - gs_vertex2f(x, y); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_wave_b); - - int thickB = d->thick_b; - if (thickB < 1) - thickB = 1; - - { - const float half = (float)(thickB - 1) * 0.5f; - - for (int t = 0; t < thickB; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - - float rad = r2[idx] + offset; - if (rad < 0.0f) - rad = 0.0f; - - const float x = cx + cos_t[idx] * rad; - const float y = cy + sin_t[idx] * rad; - - gs_vertex2f(x, y); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_fire); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - if (a <= 1e-6f) - continue; - - float db = db_from_amp(a); - if (db < d->db_fire) - continue; - - float over = (db - d->db_fire) / 20.0f; - if (over < 0.0f) - over = 0.0f; - if (over > 1.0f) - over = 1.0f; - - float r_start = r1[i]; - float r_end = r1[i] + over * Rfire; - - const float x_start = cx + cos_t[i] * r_start; - const float y_start = cy + sin_t[i] * r_start; - const float x_end = cx + cos_t[i] * r_end; - const float y_end = cy + sin_t[i] * r_end; - - gs_vertex2f(x_start, y_start); - gs_vertex2f(x_end, y_end); - } - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void abstract_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_abstract_theme = { - k_theme_id_abstract, k_theme_name_abstract, abstract_theme_add_properties, - abstract_theme_update, abstract_theme_draw, abstract_theme_destroy_data, -}; - -void audio_wave_register_abstract_theme() -{ - audio_wave_register_theme(&k_abstract_theme); -} diff --git a/src/themes/theme-abstract.hpp b/src/themes/theme-abstract.hpp deleted file mode 100644 index d7d9ca0..0000000 --- a/src/themes/theme-abstract.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_abstract_theme(); diff --git a/src/themes/theme-cartoonframe.cpp b/src/themes/theme-cartoonframe.cpp deleted file mode 100644 index 8f22e6c..0000000 --- a/src/themes/theme-cartoonframe.cpp +++ /dev/null @@ -1,455 +0,0 @@ -#include "theme-cartoonframe.hpp" - -#include -#include -#include -#include - -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_cartoonframe = "cartoon_frame"; -static const char *k_theme_name_cartoonframe = "Cartoon Camera Frame"; - -static const char *CFR_PROP_COLOR_FRAME = "cfr_color_frame"; -static const char *CFR_PROP_COLOR_SPARK = "cfr_color_spark"; - -static const char *CFR_PROP_FRAME_THICKNESS = "cfr_frame_thickness"; -// REMOVED: static const char *CFR_PROP_FRAME_INSET = "cfr_frame_inset"; -static const char *CFR_PROP_CORNER_LEN = "cfr_corner_length_ratio"; - -static const char *CFR_PROP_SPARK_COUNT = "cfr_spark_count"; -static const char *CFR_PROP_SPARK_LENGTH = "cfr_spark_length"; -static const char *CFR_PROP_SPARK_ENERGY = "cfr_spark_energy"; -static const char *CFR_PROP_SPARK_MIN_LEVEL = "cfr_spark_min_level"; -static const char *CFR_PROP_SPARK_SPEED = "cfr_spark_speed"; - -// ───────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────── - -static float cfr_clamp_float(float v, float lo, float hi) -{ - if (v < lo) - return lo; - if (v > hi) - return hi; - return v; -} - -static float cfr_pseudo_rand01(uint32_t seed) -{ - uint32_t x = seed; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - return (float)(x & 0x00FFFFFFu) / (float)0x01000000u; -} - -struct cfr_spark { - float pos = 0.0f; - float life = 0.0f; - float maxLife = 1.0f; - float speed = 0.0f; -}; - -// Per-source theme data -struct cartoonframe_theme_data { - int frame_thickness = 6; - // REMOVED: float inset_ratio = 0.08f; - float corner_len_ratio = 0.22f; - - uint32_t spark_count = 40; - float spark_length = 50.0f; - float spark_energy = 0.8f; - float spark_min_level = 0.25f; - float spark_speed = 1.0f; - - std::vector sparks; - - float phase = 0.0f; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void cartoonframe_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, CFR_PROP_COLOR_FRAME, "Frame Color"); - obs_properties_add_color(props, CFR_PROP_COLOR_SPARK, "Sparkle Color"); - obs_properties_add_int_slider(props, CFR_PROP_FRAME_THICKNESS, "Frame Thickness", 1, 20, 1); - - // REMOVED: per-theme inset; now global in Audio Wave source settings - // obs_properties_add_float_slider(props, CFR_PROP_FRAME_INSET, "Frame Inset (relative to canvas)", 0.0, 0.4, 0.01); - - obs_properties_add_float_slider(props, CFR_PROP_CORNER_LEN, "Corner Length (fraction of side)", 0.05, 0.5, 0.01); - obs_properties_add_int_slider(props, CFR_PROP_SPARK_COUNT, "Spark Count", 0, 200, 2); - obs_properties_add_int_slider(props, CFR_PROP_SPARK_LENGTH, "Spark Length (px)", 5, 200, 5); - obs_properties_add_float_slider(props, CFR_PROP_SPARK_ENERGY, "Spark Energy Response", 0.0, 2.0, 0.05); - obs_properties_add_float_slider(props, CFR_PROP_SPARK_MIN_LEVEL, "Spark Min Level (0..1)", 0.0, 1.0, 0.05); - obs_properties_add_float_slider(props, CFR_PROP_SPARK_SPEED, "Spark Base Speed", 0.0, 5.0, 0.05); -} - -static void cartoonframe_rebuild_sparks(cartoonframe_theme_data *d) -{ - if (!d) - return; - - d->sparks.clear(); - d->sparks.resize(d->spark_count); - - for (uint32_t i = 0; i < d->spark_count; ++i) { - float r0 = cfr_pseudo_rand01(i * 11u + 3u); - float r1 = cfr_pseudo_rand01(i * 23u + 7u); - float r2 = cfr_pseudo_rand01(i * 41u + 13u); - - cfr_spark s; - s.pos = r0; - s.maxLife = 0.6f + r1 * 1.8f; - s.life = r2 * s.maxLife; - s.speed = 0.5f + r1 * 1.5f; - - d->sparks[i] = s; - } -} - -static void cartoonframe_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_frame = (uint32_t)aw_get_int_default(settings, CFR_PROP_COLOR_FRAME, 0); - uint32_t col_spark = (uint32_t)aw_get_int_default(settings, CFR_PROP_COLOR_SPARK, 0); - - if (col_frame == 0) - col_frame = 0xF2B24B; - if (col_spark == 0) - col_spark = 0xFFFFDD; - - s->color = col_frame; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"frame", col_frame}); - s->colors.push_back(audio_wave_named_color{"sparkles", col_spark}); - - int frame_thickness = (int)aw_get_int_default(settings, CFR_PROP_FRAME_THICKNESS, 6); - frame_thickness = std::clamp(frame_thickness, 1, 20); - - // REMOVED: theme inset read (now global) - // double inset = aw_get_float_default(settings, CFR_PROP_FRAME_INSET, 0.08f); - - double corner_len_ratio = aw_get_float_default(settings, CFR_PROP_CORNER_LEN, 0.22f); - corner_len_ratio = std::clamp(corner_len_ratio, 0.05, 0.5); - - int spark_count = (int)aw_get_int_default(settings, CFR_PROP_SPARK_COUNT, 40); - spark_count = std::clamp(spark_count, 0, 200); - - int spark_len = (int)aw_get_int_default(settings, CFR_PROP_SPARK_LENGTH, 50); - spark_len = std::clamp(spark_len, 5, 200); - - double spark_energy = aw_get_float_default(settings, CFR_PROP_SPARK_ENERGY, 0.8f); - spark_energy = std::clamp(spark_energy, 0.0, 2.0); - - double spark_min_level = aw_get_float_default(settings, CFR_PROP_SPARK_MIN_LEVEL, 0.25f); - spark_min_level = std::clamp(spark_min_level, 0.0, 1.0); - - double spark_speed = aw_get_float_default(settings, CFR_PROP_SPARK_SPEED, 1.0f); - spark_speed = std::clamp(spark_speed, 0.0, 5.0); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new cartoonframe_theme_data{}; - s->theme_data = d; - } - - d->frame_thickness = frame_thickness; - // REMOVED: d->inset_ratio = (float)inset; - d->corner_len_ratio = (float)corner_len_ratio; - - d->spark_count = (uint32_t)spark_count; - d->spark_length = (float)spark_len; - d->spark_energy = (float)spark_energy; - d->spark_min_level = (float)spark_min_level; - d->spark_speed = (float)spark_speed; - - if (d->sparks.size() != d->spark_count) { - cartoonframe_rebuild_sparks(d); - } - - if (s->frame_density < 60) - s->frame_density = 60; -} - -static void cfr_rect_perimeter_point_and_normal(float t, float hx, float hy, float &x, float &y, float &nx, float &ny) -{ - float edge = t * 4.0f; - int side = (int)std::floor(edge); - float u = edge - (float)side; - - switch (side) { - case 0: - default: - x = -hx + 2.0f * hx * u; - y = -hy; - nx = 0.0f; - ny = -1.0f; - break; - case 1: - x = hx; - y = -hy + 2.0f * hy * u; - nx = 1.0f; - ny = 0.0f; - break; - case 2: - x = hx - 2.0f * hx * u; - y = hy; - nx = 0.0f; - ny = 1.0f; - break; - case 3: - x = -hx; - y = hy - 2.0f * hy * u; - nx = -1.0f; - ny = 0.0f; - break; - } -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void cartoonframe_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - if (s->wave.empty()) - audio_wave_build_wave(s); - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new cartoonframe_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - // REMOVED: theme-local inset margin; global inset is applied in audio-wave.cpp render transform - const float hx = std::max(0.0f, w * 0.5f); - const float hy = std::max(0.0f, h * 0.5f); - - const float sideX = 2.0f * hx; - const float sideY = 2.0f * hy; - - const float cornerLenX = sideX * d->corner_len_ratio; - const float cornerLenY = sideY * d->corner_len_ratio; - - const uint32_t col_frame = audio_wave_get_color(s, 0, s->color); - const uint32_t col_spark = audio_wave_get_color(s, 1, 0xFFFFFF); - - float max_a = 0.0f; - for (size_t i = 0; i < frames; ++i) { - float v = s->wave[i]; - if (v > max_a) - max_a = v; - } - max_a = cfr_clamp_float(max_a, 0.0f, 1.0f); - float v_global = audio_wave_apply_curve(s, max_a); - - int base_thick = d->frame_thickness; - int extra_thick = (int)std::round(v_global * 4.0f); - int thick = std::clamp(base_thick + extra_thick, 1, 30); - - gs_matrix_push(); - - if (color_param) - audio_wave_set_solid_color(color_param, col_frame); - - for (int t = 0; t < thick; ++t) { - float offset = (float)t - (float)(thick - 1) * 0.5f; - - gs_render_start(true); - - { - float x0 = cx - hx + offset; - float y0 = cy - hy + offset; - float x1 = x0 + cornerLenX; - float y1 = y0 + cornerLenY; - - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y0); - - gs_vertex2f(x0, y0); - gs_vertex2f(x0, y1); - } - - { - float x0 = cx + hx + offset; - float y0 = cy - hy + offset; - float x1 = x0 - cornerLenX; - float y1 = y0 + cornerLenY; - - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y0); - - gs_vertex2f(x0, y0); - gs_vertex2f(x0, y1); - } - - { - float x0 = cx + hx + offset; - float y0 = cy + hy + offset; - float x1 = x0 - cornerLenX; - float y1 = y0 - cornerLenY; - - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y0); - - gs_vertex2f(x0, y0); - gs_vertex2f(x0, y1); - } - - { - float x0 = cx - hx + offset; - float y0 = cy + hy + offset; - float x1 = x0 + cornerLenX; - float y1 = y0 - cornerLenY; - - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y0); - - gs_vertex2f(x0, y0); - gs_vertex2f(x0, y1); - } - - gs_render_stop(GS_LINES); - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_spark); - - if (d->sparks.size() != d->spark_count) - cartoonframe_rebuild_sparks(d); - - const float dt = 0.03f; - const float base_len = d->spark_length; - - gs_render_start(true); - - for (uint32_t i = 0; i < d->sparks.size(); ++i) { - auto &sp = d->sparks[i]; - - float pos_norm = sp.pos; - pos_norm -= std::floor(pos_norm); - if (pos_norm < 0.0f) - pos_norm += 1.0f; - - size_t idx = (size_t)(pos_norm * (float)(frames - 1)); - if (idx >= frames) - idx = frames - 1; - - float a_local = s->wave[idx]; - a_local = cfr_clamp_float(a_local, 0.0f, 1.0f); - float v_local = audio_wave_apply_curve(s, a_local); - - bool active = (v_local >= d->spark_min_level); - if (!active) { - sp.life += dt * 0.6f; - if (sp.life > sp.maxLife) { - uint32_t seed = i * 97u + 17u; - float r0 = cfr_pseudo_rand01(seed); - float r1 = cfr_pseudo_rand01(seed * 3u + 11u); - float r2 = cfr_pseudo_rand01(seed * 5u + 23u); - - sp.pos = r0; - sp.maxLife = 0.6f + r1 * 1.8f; - sp.life = 0.0f; - sp.speed = 0.5f + r2 * 1.5f; - } - - float speed_scale_idle = 0.3f; - sp.pos += (d->spark_speed + sp.speed * speed_scale_idle) * dt; - sp.pos -= std::floor(sp.pos); - continue; - } - - sp.life += dt * (0.4f + v_local * 2.0f); - if (sp.life > sp.maxLife) { - uint32_t seed = i * 101u + 31u; - float r0 = cfr_pseudo_rand01(seed); - float r1 = cfr_pseudo_rand01(seed * 7u + 19u); - float r2 = cfr_pseudo_rand01(seed * 11u + 23u); - - sp.pos = r0; - sp.maxLife = 0.6f + r1 * 2.0f; - sp.life = 0.0f; - sp.speed = 0.5f + r2 * 1.6f; - } - - float speed_scale = 0.2f + v_local * d->spark_energy; - sp.pos += (d->spark_speed + sp.speed * speed_scale) * dt; - sp.pos -= std::floor(sp.pos); - - float phase = cfr_clamp_float(sp.life / sp.maxLife, 0.0f, 1.0f); - float life_intensity = (phase < 0.5f) ? (phase * 2.0f) : ((1.0f - phase) * 2.0f); - life_intensity = cfr_clamp_float(life_intensity, 0.0f, 1.0f); - - float intensity = life_intensity * (0.3f + 0.7f * v_local); - intensity = cfr_clamp_float(intensity, 0.0f, 1.0f); - - float len = base_len * (0.3f + 0.7f * intensity); - - float px, py, nx, ny; - cfr_rect_perimeter_point_and_normal(sp.pos, hx, hy, px, py, nx, ny); - - float sx = cx + px + nx * (float)thick * 0.5f; - float sy = cy + py + ny * (float)thick * 0.5f; - - float ex = sx + nx * len; - float ey = sy + ny * len; - - gs_vertex2f(sx, sy); - gs_vertex2f(ex, ey); - } - - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void cartoonframe_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_cartoonframe_theme = {k_theme_id_cartoonframe, - k_theme_name_cartoonframe, - cartoonframe_theme_add_properties, - cartoonframe_theme_update, - cartoonframe_theme_draw, - cartoonframe_theme_destroy_data, - nullptr}; - -void audio_wave_register_cartoonframe_theme() -{ - audio_wave_register_theme(&k_cartoonframe_theme); -} diff --git a/src/themes/theme-cartoonframe.hpp b/src/themes/theme-cartoonframe.hpp deleted file mode 100644 index 0870830..0000000 --- a/src/themes/theme-cartoonframe.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_cartoonframe_theme(); diff --git a/src/themes/theme-circle.cpp b/src/themes/theme-circle.cpp index 13041d0..5ea2513 100644 --- a/src/themes/theme-circle.cpp +++ b/src/themes/theme-circle.cpp @@ -11,8 +11,20 @@ static const char *k_theme_id_circle = "circle"; static const char *k_theme_name_circle = "Circle"; static const char *CIRCLE_PROP_STYLE = "circle_style"; -static const char *CIRCLE_PROP_COLOR = "circle_color"; static const char *CIRCLE_PROP_MIRROR = "circle_mirror"; +static const char *P_DENSITY = "shape_density"; + +static bool circle_style_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) +{ + const char *style_id = obs_data_get_string(settings, CIRCLE_PROP_STYLE); + const bool is_rays = (style_id && strcmp(style_id, "rays") == 0); + + obs_property_t *mirror = obs_properties_get(props, CIRCLE_PROP_MIRROR); + if (mirror) + obs_property_set_visible(mirror, is_rays); + + return true; +} static void circle_theme_add_properties(obs_properties_t *props) { @@ -21,9 +33,11 @@ static void circle_theme_add_properties(obs_properties_t *props) obs_property_list_add_string(style, "Orbit", "orbit"); obs_property_list_add_string(style, "Rays", "rays"); + obs_property_set_modified_callback(style, circle_style_modified); - obs_properties_add_color(props, CIRCLE_PROP_COLOR, "Color"); - obs_properties_add_bool(props, CIRCLE_PROP_MIRROR, "Double-sided rays"); + obs_property_t *mirror = obs_properties_add_bool(props, CIRCLE_PROP_MIRROR, "Double-sided rays"); + obs_property_set_visible(mirror, false); + obs_properties_add_int_slider(props, P_DENSITY, "Shape Density (%)", 10, 300, 5); } static void circle_theme_update(audio_wave_source *s, obs_data_t *settings) @@ -37,19 +51,11 @@ static void circle_theme_update(audio_wave_source *s, obs_data_t *settings) s->theme_style_id = style_id; - uint32_t color = (uint32_t)aw_get_int_default(settings, CIRCLE_PROP_COLOR, 0); - if (color == 0) - color = 0x00FFFF; - - s->color = color; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"circle", color}); - - if (s->frame_density < 40) - s->frame_density = 40; + int density = aw_get_int_default(settings, P_DENSITY, 120); + density = std::clamp(density, 10, 300); + s->frame_density = density; - s->mirror = obs_data_get_bool(settings, CIRCLE_PROP_MIRROR); + s->mirror = (strcmp(style_id, "rays") == 0) ? obs_data_get_bool(settings, CIRCLE_PROP_MIRROR) : false; } // ───────────────────────────────────────────── @@ -100,29 +106,46 @@ static void draw_circle_orbit(audio_wave_source *s, gs_eparam_t *color_param) return amp_smooth[i]; }; - const uint32_t circ_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, circ_color); - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - - const float u = (float)idx / (float)segments; + std::vector px(segments), py(segments); + for (uint32_t i = 0; i < segments; ++i) { + const float u = (float)i / (float)segments; const float a = u * 2.0f * (float)M_PI; - - const float v_raw = get_amp(idx); + const float v_raw = get_amp(i); const float v = audio_wave_apply_curve(s, v_raw); const float L = v * L_max; - const float R = R_base + L; + px[i] = cx + std::cos(a) * R; + py[i] = cy + std::sin(a) * R; + } - const float x = cx + std::cos(a) * R; - const float y = cy + std::sin(a) * R; - - gs_vertex2f(x, y); + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const uint32_t j = (i + 1) % segments; + gs_vertex2f(px[i], py[i]); + gs_vertex2f(px[j], py[j]); + } + gs_render_stop(GS_LINES); + } + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const uint32_t j = (i + 1) % segments; + gs_vertex2f(px[i], py[i]); + gs_vertex2f(px[j], py[j]); + } + gs_render_stop(GS_LINES); } - gs_render_stop(GS_LINESTRIP); } static void draw_circle_rays(audio_wave_source *s, gs_eparam_t *color_param) @@ -154,39 +177,74 @@ static void draw_circle_rays(audio_wave_source *s, gs_eparam_t *color_param) const size_t idx = (size_t)(u * (float)(frames - 1)); amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; } + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const float u = (float)i / (float)segments; + const float a = u * 2.0f * (float)M_PI; + + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float L = v * L_max; + + const float nx = std::cos(a); + const float ny = std::sin(a); + + const float x1 = cx + nx * R_base; + const float y1 = cy + ny * R_base; + const float x2 = x1 + nx * L; + const float y2 = y1 + ny * L; + + gs_vertex2f(x1, y1); + gs_vertex2f(x2, y2); + + if (s->mirror) { + gs_vertex2f(x1, y1); + gs_vertex2f(x1 - nx * L, y1 - ny * L); + } + } + gs_render_stop(GS_LINES); + } + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const float u = (float)i / (float)segments; + const float a = u * 2.0f * (float)M_PI; - const uint32_t circ_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, circ_color); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const float a = u * 2.0f * (float)M_PI; - - const float v_raw = amp[i]; - const float v = audio_wave_apply_curve(s, v_raw); - const float L = v * L_max; - - const float nx = std::cos(a); - const float ny = std::sin(a); + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float L = v * L_max; - const float x1 = cx + nx * R_base; - const float y1 = cy + ny * R_base; - const float x2 = x1 + nx * L; - const float y2 = y1 + ny * L; + const float nx = std::cos(a); + const float ny = std::sin(a); - gs_vertex2f(x1, y1); - gs_vertex2f(x2, y2); + const float x1 = cx + nx * R_base; + const float y1 = cy + ny * R_base; + const float x2 = x1 + nx * L; + const float y2 = y1 + ny * L; - if (s->mirror) { - const float x3 = x1 - nx * L; - const float y3 = y1 - ny * L; gs_vertex2f(x1, y1); - gs_vertex2f(x3, y3); + gs_vertex2f(x2, y2); + + if (s->mirror) { + const float x3 = x1 - nx * L; + const float y3 = y1 - ny * L; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } } + gs_render_stop(GS_LINES); } - gs_render_stop(GS_LINES); } static void circle_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) diff --git a/src/themes/theme-cosmic.cpp b/src/themes/theme-cosmic.cpp deleted file mode 100644 index 038bff3..0000000 --- a/src/themes/theme-cosmic.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include "theme-cosmic.hpp" -#include -#include -#include -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_cosmic = "cosmic"; -static const char *k_theme_name_cosmic = "Cosmic Galaxy"; - -static const char *COSMIC_PROP_COLOR_CORE = "cos_color_core"; -static const char *COSMIC_PROP_COLOR_SPIRAL = "cos_color_spiral"; -static const char *COSMIC_PROP_COLOR_HALO = "cos_color_halo"; -static const char *COSMIC_PROP_COLOR_STAR = "cos_color_star"; - -static const char *COSMIC_PROP_SEGMENTS = "cos_segments"; -static const char *COSMIC_PROP_ARM_COUNT = "cos_arm_count"; -static const char *COSMIC_PROP_ARM_STRENGTH = "cos_arm_strength"; -static const char *COSMIC_PROP_HALO_WIDTH = "cos_halo_width"; -static const char *COSMIC_PROP_THICK_SPIRAL = "cos_thickness_spiral"; -static const char *COSMIC_PROP_STAR_THRESH = "cos_star_threshold"; -static const char *COSMIC_PROP_STAR_LENGTH = "cos_star_length"; -static const char *COSMIC_PROP_ROT_SPEED = "cos_rotation_speed"; - -struct cosmic_theme_data { - std::vector prev_r; - bool initialized = false; - - uint32_t segments = 160; - int arm_count = 3; - float arm_strength = 0.4f; - float halo_width = 60.0f; - int thick_spiral = 3; - float star_threshold = 0.3f; - float star_length = 60.0f; - float rot_speed = 0.5f; - float phase = 0.0f; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void cosmic_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, COSMIC_PROP_COLOR_CORE, "Core Color"); - obs_properties_add_color(props, COSMIC_PROP_COLOR_SPIRAL, "Spiral Color"); - obs_properties_add_color(props, COSMIC_PROP_COLOR_HALO, "Halo Color"); - obs_properties_add_color(props, COSMIC_PROP_COLOR_STAR, "Stars Color"); - obs_properties_add_int_slider(props, COSMIC_PROP_SEGMENTS, "Shape Resolution", 48, 512, 8); - obs_properties_add_int_slider(props, COSMIC_PROP_ARM_COUNT, "Spiral Arms", 1, 6, 1); - obs_properties_add_float_slider(props, COSMIC_PROP_ARM_STRENGTH, "Arm Strength", 0.0, 1.0, 0.05); - obs_properties_add_int_slider(props, COSMIC_PROP_HALO_WIDTH, "Halo Width (px)", 10, 300, 5); - obs_properties_add_int_slider(props, COSMIC_PROP_THICK_SPIRAL, "Spiral Thickness", 1, 8, 1); - obs_properties_add_float_slider(props, COSMIC_PROP_STAR_THRESH, "Stars Threshold (0..1)", 0.0, 1.0, 0.01); - obs_properties_add_int_slider(props, COSMIC_PROP_STAR_LENGTH, "Stars Length (px)", 5, 200, 5); - obs_properties_add_float_slider(props, COSMIC_PROP_ROT_SPEED, "Rotation Speed", 0.0, 5.0, 0.1); -} - -static void cosmic_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_core = (uint32_t)aw_get_int_default(settings, COSMIC_PROP_COLOR_CORE, 0); - uint32_t col_spiral = (uint32_t)aw_get_int_default(settings, COSMIC_PROP_COLOR_SPIRAL, 0); - uint32_t col_halo = (uint32_t)aw_get_int_default(settings, COSMIC_PROP_COLOR_HALO, 0); - uint32_t col_star = (uint32_t)aw_get_int_default(settings, COSMIC_PROP_COLOR_STAR, 0); - - if (col_core == 0) - col_core = 0xFFFFFF; - if (col_spiral == 0) - col_spiral = 0x66CCFF; - if (col_halo == 0) - col_halo = 0x101037; - if (col_star == 0) - col_star = 0xFFFF99; - - s->color = col_spiral; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"core", col_core}); - s->colors.push_back(audio_wave_named_color{"spiral", col_spiral}); - s->colors.push_back(audio_wave_named_color{"halo", col_halo}); - s->colors.push_back(audio_wave_named_color{"stars", col_star}); - - int seg = aw_get_int_default(settings, COSMIC_PROP_SEGMENTS, 160); - seg = std::clamp(seg, 48, 512); - - int arm_cnt = aw_get_int_default(settings, COSMIC_PROP_ARM_COUNT, 3); - arm_cnt = std::clamp(arm_cnt, 1, 6); - - double arm_str = aw_get_float_default(settings, COSMIC_PROP_ARM_STRENGTH, 0.4f); - arm_str = std::clamp(arm_str, 0.0, 1.0); - - int halo_w = aw_get_int_default(settings, COSMIC_PROP_HALO_WIDTH, 60); - halo_w = std::clamp(halo_w, 10, 300); - - int thick = aw_get_int_default(settings, COSMIC_PROP_THICK_SPIRAL, 3); - thick = std::clamp(thick, 1, 8); - - double thr = aw_get_float_default(settings, COSMIC_PROP_STAR_THRESH, 0.3f); - thr = std::clamp(thr, 0.0, 1.0); - - int star_len = aw_get_int_default(settings, COSMIC_PROP_STAR_LENGTH, 60); - star_len = std::clamp(star_len, 5, 200); - - double rot = aw_get_float_default(settings, COSMIC_PROP_ROT_SPEED, 0.5f); - rot = std::clamp(rot, 0.0, 5.0); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new cosmic_theme_data{}; - s->theme_data = d; - } - - d->segments = (uint32_t)seg; - d->arm_count = arm_cnt; - d->arm_strength = (float)arm_str; - d->halo_width = (float)halo_w; - d->thick_spiral = thick; - d->star_threshold = (float)thr; - d->star_length = (float)star_len; - d->rot_speed = (float)rot; - - d->initialized = false; -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void cosmic_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new cosmic_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float base_radius = std::min(w, h) * 0.25f; - const float audio_radius = std::min(w, h) * 0.20f; - const uint32_t segments = std::max(d->segments, 48u); - - const uint32_t col_core = audio_wave_get_color(s, 0, s->color); - const uint32_t col_spiral = audio_wave_get_color(s, 1, col_core); - const uint32_t col_halo = audio_wave_get_color(s, 2, col_spiral); - const uint32_t col_star = audio_wave_get_color(s, 3, col_halo); - - std::vector amp(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const size_t idx = (size_t)(u * (float)(frames - 1)); - amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - std::vector amp_smooth(segments); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.20f; - for (uint32_t i = 1; i < segments; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - float wrap = amp_smooth[segments - 1]; - amp_smooth[0] = 0.5f * (amp_smooth[0] + wrap); - } - - if (d->prev_r.size() != segments) { - d->prev_r.assign(segments, base_radius); - d->initialized = false; - } - - std::vector radius(segments); - const float alpha_time = 0.30f; - - d->phase += d->rot_speed * (float)M_PI / 180.0f; - if (d->phase > 2.0f * (float)M_PI) - d->phase -= 2.0f * (float)M_PI; - - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - if (a < 0.0f) - a = 0.0f; - if (a > 1.0f) - a = 1.0f; - - float v = audio_wave_apply_curve(s, a); - - const float angle = ((float)i / (float)segments) * 2.0f * (float)M_PI; - const float arm_mod = std::sin(d->arm_count * angle + d->phase); - - float mod = 1.0f + d->arm_strength * arm_mod; - if (mod < 0.2f) - mod = 0.2f; - - float r_target = base_radius + v * audio_radius * mod; - - if (!d->initialized) - d->prev_r[i] = r_target; - - float r_s = d->prev_r[i] + alpha_time * (r_target - d->prev_r[i]); - d->prev_r[i] = r_s; - radius[i] = r_s; - } - - d->initialized = true; - - std::vector cos_t(segments), sin_t(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float t = ((float)i / (float)segments) * 2.0f * (float)M_PI; - cos_t[i] = std::cos(t); - sin_t[i] = std::sin(t); - } - - std::vector halo_r(segments); - for (uint32_t i = 0; i < segments; ++i) { - halo_r[i] = radius[i] + d->halo_width; - } - - std::vector x_sp(segments), y_sp(segments); - std::vector x_halo(segments), y_halo(segments); - - for (uint32_t i = 0; i < segments; ++i) { - x_sp[i] = cx + cos_t[i] * radius[i]; - y_sp[i] = cy + sin_t[i] * radius[i]; - x_halo[i] = cx + cos_t[i] * halo_r[i]; - y_halo[i] = cy + sin_t[i] * halo_r[i]; - } - - gs_matrix_push(); - - if (color_param) - audio_wave_set_solid_color(color_param, col_halo); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - uint32_t next = (i + 1) % segments; - - gs_vertex2f(x_sp[i], y_sp[i]); - gs_vertex2f(x_halo[i], y_halo[i]); - gs_vertex2f(x_halo[next], y_halo[next]); - - gs_vertex2f(x_sp[i], y_sp[i]); - gs_vertex2f(x_halo[next], y_halo[next]); - gs_vertex2f(x_sp[next], y_sp[next]); - } - gs_render_stop(GS_TRIS); - - if (color_param) - audio_wave_set_solid_color(color_param, col_core); - - const int core_segments = 32; - const float core_radius = base_radius * 0.4f; - - gs_render_start(true); - for (int i = 0; i < core_segments; ++i) { - const float a0 = ((float)i / (float)core_segments) * 2.0f * (float)M_PI; - const float a1 = ((float)(i + 1) / (float)core_segments) * 2.0f * (float)M_PI; - - const float x0 = cx + std::cos(a0) * core_radius; - const float y0 = cy + std::sin(a0) * core_radius; - const float x1 = cx + std::cos(a1) * core_radius; - const float y1 = cy + std::sin(a1) * core_radius; - - gs_vertex2f(cx, cy); - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y1); - } - gs_render_stop(GS_TRIS); - - if (color_param) - audio_wave_set_solid_color(color_param, col_spiral); - - int thick = d->thick_spiral; - if (thick < 1) - thick = 1; - - { - const float half = (float)(thick - 1) * 0.5f; - for (int t = 0; t < thick; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - - float rad = radius[idx] + offset; - if (rad < 0.0f) - rad = 0.0f; - - const float x = cx + cos_t[idx] * rad; - const float y = cy + sin_t[idx] * rad; - - gs_vertex2f(x, y); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_star); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - if (a < 0.0f) - a = 0.0f; - if (a > 1.0f) - a = 1.0f; - - float v = audio_wave_apply_curve(s, a); - if (v < d->star_threshold) - continue; - - float extra = (v - d->star_threshold) / std::max(0.001f, 1.0f - d->star_threshold); - if (extra < 0.0f) - extra = 0.0f; - if (extra > 1.0f) - extra = 1.0f; - - float length = d->star_length * extra; - - const float r_start = halo_r[i] + 2.0f; - const float r_end = r_start + length; - - const float x_start = cx + cos_t[i] * r_start; - const float y_start = cy + sin_t[i] * r_start; - const float x_end = cx + cos_t[i] * r_end; - const float y_end = cy + sin_t[i] * r_end; - - gs_vertex2f(x_start, y_start); - gs_vertex2f(x_end, y_end); - } - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void cosmic_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_cosmic_theme = { - k_theme_id_cosmic, k_theme_name_cosmic, cosmic_theme_add_properties, - cosmic_theme_update, cosmic_theme_draw, cosmic_theme_destroy_data, -}; - -void audio_wave_register_cosmic_theme() -{ - audio_wave_register_theme(&k_cosmic_theme); -} diff --git a/src/themes/theme-cosmic.hpp b/src/themes/theme-cosmic.hpp deleted file mode 100644 index 1c0123c..0000000 --- a/src/themes/theme-cosmic.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_cosmic_theme(); diff --git a/src/themes/theme-doughnut.cpp b/src/themes/theme-doughnut.cpp deleted file mode 100644 index 6434c58..0000000 --- a/src/themes/theme-doughnut.cpp +++ /dev/null @@ -1,362 +0,0 @@ -#include "theme-doughnut.hpp" -#include -#include -#include -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_doughnut = "doughnut"; -static const char *k_theme_name_doughnut = "Doughnut"; - -static const char *DOUGHNUT_PROP_COLOR_OUTER = "doughnut_color_outer"; -static const char *DOUGHNUT_PROP_COLOR_INNER = "doughnut_color_inner"; -static const char *DOUGHNUT_PROP_COLOR_FILL = "doughnut_color_fill"; -static const char *DOUGHNUT_PROP_COLOR_DOTS = "doughnut_color_dots"; - -static const char *DOUGHNUT_PROP_SEGMENTS = "doughnut_segments"; -static const char *DOUGHNUT_PROP_BAND_WIDTH = "doughnut_band_width"; -static const char *DOUGHNUT_PROP_THICK_OUTER = "doughnut_thickness_outer"; -static const char *DOUGHNUT_PROP_THICK_INNER = "doughnut_thickness_inner"; -static const char *DOUGHNUT_PROP_DOT_THRESHOLD = "doughnut_dot_threshold"; -static const char *DOUGHNUT_PROP_DOT_LENGTH = "doughnut_dot_length"; - -struct doughnut_theme_data { - std::vector prev_r; - bool initialized = false; - - uint32_t segments = 128; - float band_width = 40.0f; - int thick_outer = 3; - int thick_inner = 2; - float dot_threshold = 0.25f; - float dot_length = 40.0f; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void doughnut_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, DOUGHNUT_PROP_COLOR_OUTER, "Outer Ring Color"); - obs_properties_add_color(props, DOUGHNUT_PROP_COLOR_INNER, "Inner Ring Color"); - obs_properties_add_color(props, DOUGHNUT_PROP_COLOR_FILL, "Band Fill Color"); - obs_properties_add_color(props, DOUGHNUT_PROP_COLOR_DOTS, "Orbit Dots Color"); - obs_properties_add_int_slider(props, DOUGHNUT_PROP_SEGMENTS, "Shape Resolution", 32, 512, 8); - obs_properties_add_int_slider(props, DOUGHNUT_PROP_BAND_WIDTH, "Band Width (px)", 10, 300, 5); - obs_properties_add_int_slider(props, DOUGHNUT_PROP_THICK_OUTER, "Outer Ring Thickness", 1, 8, 1); - obs_properties_add_int_slider(props, DOUGHNUT_PROP_THICK_INNER, "Inner Ring Thickness", 1, 8, 1); - obs_properties_add_float_slider(props, DOUGHNUT_PROP_DOT_THRESHOLD, "Dot Threshold (0..1)", 0.0, 1.0, 0.01); - obs_properties_add_int_slider(props, DOUGHNUT_PROP_DOT_LENGTH, "Dot Length (px)", 5, 200, 5); -} - -static void doughnut_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_outer = (uint32_t)aw_get_int_default(settings, DOUGHNUT_PROP_COLOR_OUTER, 0); - uint32_t col_inner = (uint32_t)aw_get_int_default(settings, DOUGHNUT_PROP_COLOR_INNER, 0); - uint32_t col_fill = (uint32_t)aw_get_int_default(settings, DOUGHNUT_PROP_COLOR_FILL, 0); - uint32_t col_dots = (uint32_t)aw_get_int_default(settings, DOUGHNUT_PROP_COLOR_DOTS, 0); - - if (col_outer == 0) - col_outer = 0xFF6600; - if (col_inner == 0) - col_inner = 0x00FFAA; - if (col_fill == 0) - col_fill = 0x101020; - if (col_dots == 0) - col_dots = 0xFFFFFF; - - s->color = col_outer; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"outer", col_outer}); - s->colors.push_back(audio_wave_named_color{"inner", col_inner}); - s->colors.push_back(audio_wave_named_color{"fill", col_fill}); - s->colors.push_back(audio_wave_named_color{"dots", col_dots}); - - int seg = aw_get_int_default(settings, DOUGHNUT_PROP_SEGMENTS, 128); - seg = std::clamp(seg, 32, 512); - - int band_w = aw_get_int_default(settings, DOUGHNUT_PROP_BAND_WIDTH, 40); - band_w = std::clamp(band_w, 10, 300); - - int t_outer = aw_get_int_default(settings, DOUGHNUT_PROP_THICK_OUTER, 3); - int t_inner = aw_get_int_default(settings, DOUGHNUT_PROP_THICK_INNER, 2); - t_outer = std::clamp(t_outer, 1, 8); - t_inner = std::clamp(t_inner, 1, 8); - - double thr = aw_get_float_default(settings, DOUGHNUT_PROP_DOT_THRESHOLD, 0.25f); - thr = std::clamp(thr, 0.0, 1.0); - - int dot_len = aw_get_int_default(settings, DOUGHNUT_PROP_DOT_LENGTH, 40); - dot_len = std::clamp(dot_len, 5, 200); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new doughnut_theme_data{}; - s->theme_data = d; - } - - d->segments = (uint32_t)seg; - d->band_width = (float)band_w; - d->thick_outer = t_outer; - d->thick_inner = t_inner; - d->dot_threshold = (float)thr; - d->dot_length = (float)dot_len; - - d->initialized = false; -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void doughnut_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new doughnut_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float Rbase = std::min(w, h) * 0.30f; - const float Rext = std::min(w, h) * 0.25f; - - const uint32_t segments = std::max(d->segments, 32u); - - const uint32_t col_outer = audio_wave_get_color(s, 0, s->color); - const uint32_t col_inner = audio_wave_get_color(s, 1, col_outer); - const uint32_t col_fill = audio_wave_get_color(s, 2, col_inner); - const uint32_t col_dots = audio_wave_get_color(s, 3, col_fill); - - std::vector amp(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const size_t idx = (size_t)(u * (float)(frames - 1)); - amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - std::vector amp_smooth(segments); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.20f; - for (uint32_t i = 1; i < segments; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - - float wrap = amp_smooth[segments - 1]; - amp_smooth[0] = 0.5f * (amp_smooth[0] + wrap); - } - - if (d->prev_r.size() != segments) { - d->prev_r.assign(segments, Rbase); - d->initialized = false; - } - - std::vector r_center(segments); - const float alpha_time = 0.30f; - - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - if (a < 0.0f) - a = 0.0f; - if (a > 1.0f) - a = 1.0f; - - a = audio_wave_apply_curve(s, a); - - float r_target = Rbase + a * Rext; - - if (!d->initialized) - d->prev_r[i] = r_target; - - float r_s = d->prev_r[i] + alpha_time * (r_target - d->prev_r[i]); - d->prev_r[i] = r_s; - r_center[i] = r_s; - } - - d->initialized = true; - - std::vector cos_t(segments), sin_t(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float t = ((float)i / (float)segments) * 2.0f * (float)M_PI; - cos_t[i] = std::cos(t); - sin_t[i] = std::sin(t); - } - - const float half_band = d->band_width * 0.5f; - - std::vector r_inner(segments), r_outer(segments); - for (uint32_t i = 0; i < segments; ++i) { - float ri = r_center[i] - half_band; - float ro = r_center[i] + half_band; - if (ri < 0.0f) - ri = 0.0f; - r_inner[i] = ri; - r_outer[i] = ro; - } - - std::vector x_in(segments), y_in(segments); - std::vector x_out(segments), y_out(segments); - for (uint32_t i = 0; i < segments; ++i) { - x_in[i] = cx + cos_t[i] * r_inner[i]; - y_in[i] = cy + sin_t[i] * r_inner[i]; - x_out[i] = cx + cos_t[i] * r_outer[i]; - y_out[i] = cy + sin_t[i] * r_outer[i]; - } - - gs_matrix_push(); - - if (color_param) - audio_wave_set_solid_color(color_param, col_fill); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - uint32_t next = (i + 1) % segments; - - gs_vertex2f(x_in[i], y_in[i]); - gs_vertex2f(x_out[i], y_out[i]); - gs_vertex2f(x_out[next], y_out[next]); - - gs_vertex2f(x_in[i], y_in[i]); - gs_vertex2f(x_out[next], y_out[next]); - gs_vertex2f(x_in[next], y_in[next]); - } - gs_render_stop(GS_TRIS); - - if (color_param) - audio_wave_set_solid_color(color_param, col_outer); - - int thickO = d->thick_outer; - if (thickO < 1) - thickO = 1; - - { - const float half = (float)(thickO - 1) * 0.5f; - for (int t = 0; t < thickO; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - float rad = r_outer[idx] + offset; - if (rad < 0.0f) - rad = 0.0f; - - const float x = cx + cos_t[idx] * rad; - const float y = cy + sin_t[idx] * rad; - - gs_vertex2f(x, y); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_inner); - - int thickI = d->thick_inner; - if (thickI < 1) - thickI = 1; - - { - const float half = (float)(thickI - 1) * 0.5f; - for (int t = 0; t < thickI; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - float rad = r_inner[idx] + offset; - if (rad < 0.0f) - rad = 0.0f; - - const float x = cx + cos_t[idx] * rad; - const float y = cy + sin_t[idx] * rad; - - gs_vertex2f(x, y); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_dots); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - if (a < 0.0f) - a = 0.0f; - if (a > 1.0f) - a = 1.0f; - - float v = audio_wave_apply_curve(s, a); - if (v < d->dot_threshold) - continue; - - float extra = (v - d->dot_threshold) / std::max(0.001f, 1.0f - d->dot_threshold); - if (extra < 0.0f) - extra = 0.0f; - if (extra > 1.0f) - extra = 1.0f; - - float len = d->dot_length * extra; - - const float r_start = r_outer[i] + 2.0f; - const float r_end = r_start + len; - - const float x_start = cx + cos_t[i] * r_start; - const float y_start = cy + sin_t[i] * r_start; - const float x_end = cx + cos_t[i] * r_end; - const float y_end = cy + sin_t[i] * r_end; - - gs_vertex2f(x_start, y_start); - gs_vertex2f(x_end, y_end); - } - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void doughnut_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_doughnut_theme = { - k_theme_id_doughnut, k_theme_name_doughnut, doughnut_theme_add_properties, - doughnut_theme_update, doughnut_theme_draw, doughnut_theme_destroy_data, -}; - -void audio_wave_register_doughnut_theme() -{ - audio_wave_register_theme(&k_doughnut_theme); -} diff --git a/src/themes/theme-doughnut.hpp b/src/themes/theme-doughnut.hpp deleted file mode 100644 index e14ddff..0000000 --- a/src/themes/theme-doughnut.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_doughnut_theme(); diff --git a/src/themes/theme-fluid.cpp b/src/themes/theme-fluid.cpp deleted file mode 100644 index 0858508..0000000 --- a/src/themes/theme-fluid.cpp +++ /dev/null @@ -1,300 +0,0 @@ -#include "theme-fluid.hpp" -#include -#include -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_fluid = "fluid"; -static const char *k_theme_name_fluid = "Fluid Wave"; - -static const char *FLUID_PROP_COLOR_TOP = "fluid_color_top"; -static const char *FLUID_PROP_COLOR_BOTTOM = "fluid_color_bottom"; -static const char *FLUID_PROP_COLOR_FILL = "fluid_color_fill"; -static const char *FLUID_PROP_COLOR_DROP = "fluid_color_drop"; - -static const char *FLUID_PROP_BAND_HEIGHT = "fluid_band_height"; -static const char *FLUID_PROP_VISCOSITY = "fluid_viscosity"; -static const char *FLUID_PROP_DROP_THRESH = "fluid_drop_threshold"; -static const char *FLUID_PROP_DROP_LENGTH = "fluid_drop_length"; - -struct fluid_theme_data { - std::vector prev_top; - std::vector prev_bottom; - bool initialized = false; - - float band_height = 80.0f; - float viscosity = 0.7f; - float drop_threshold = 0.4f; - float drop_length = 80.0f; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void fluid_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, FLUID_PROP_COLOR_TOP, "Top Outline Color"); - obs_properties_add_color(props, FLUID_PROP_COLOR_BOTTOM, "Bottom Outline Color"); - obs_properties_add_color(props, FLUID_PROP_COLOR_FILL, "Fill Color"); - obs_properties_add_color(props, FLUID_PROP_COLOR_DROP, "Drip Color"); - obs_properties_add_int_slider(props, FLUID_PROP_BAND_HEIGHT, "Band Height (px)", 20, 400, 5); - obs_properties_add_float_slider(props, FLUID_PROP_VISCOSITY, "Viscosity (Smoothness)", 0.0, 1.0, 0.05); - obs_properties_add_float_slider(props, FLUID_PROP_DROP_THRESH, "Drop Threshold (0..1)", 0.0, 1.0, 0.01); - obs_properties_add_int_slider(props, FLUID_PROP_DROP_LENGTH, "Drop Length (px)", 5, 300, 5); -} - -static void fluid_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_top = (uint32_t)aw_get_int_default(settings, FLUID_PROP_COLOR_TOP, 0); - uint32_t col_bottom = (uint32_t)aw_get_int_default(settings, FLUID_PROP_COLOR_BOTTOM, 0); - uint32_t col_fill = (uint32_t)aw_get_int_default(settings, FLUID_PROP_COLOR_FILL, 0); - uint32_t col_drop = (uint32_t)aw_get_int_default(settings, FLUID_PROP_COLOR_DROP, 0); - - if (col_top == 0) - col_top = 0x00FFFF; - if (col_bottom == 0) - col_bottom = 0xFF00FF; - if (col_fill == 0) - col_fill = 0x101020; - if (col_drop == 0) - col_drop = 0xFFFF66; - - s->color = col_top; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"top", col_top}); - s->colors.push_back(audio_wave_named_color{"bottom", col_bottom}); - s->colors.push_back(audio_wave_named_color{"fill", col_fill}); - s->colors.push_back(audio_wave_named_color{"drop", col_drop}); - - int band_h = aw_get_int_default(settings, FLUID_PROP_BAND_HEIGHT, 80); - band_h = std::clamp(band_h, 20, 400); - - double visc = aw_get_float_default(settings, FLUID_PROP_VISCOSITY, 0.7f); - if (visc < 0.0) - visc = 0.0; - if (visc > 1.0) - visc = 1.0; - - double thr = aw_get_float_default(settings, FLUID_PROP_DROP_THRESH, 0.4f); - if (thr < 0.0) - thr = 0.0; - if (thr > 1.0) - thr = 1.0; - - int drop_len = aw_get_int_default(settings, FLUID_PROP_DROP_LENGTH, 80); - drop_len = std::clamp(drop_len, 5, 300); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new fluid_theme_data{}; - s->theme_data = d; - } - - d->band_height = (float)band_h; - d->viscosity = (float)visc; - d->drop_threshold = (float)thr; - d->drop_length = (float)drop_len; - - d->initialized = false; - - if (s->frame_density < 80) - s->frame_density = 80; -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void fluid_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new fluid_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float mid_y = h * 0.5f; - const uint32_t wx = (uint32_t)std::max(1.0f, w); - - const uint32_t col_top = audio_wave_get_color(s, 0, s->color); - const uint32_t col_bottom = audio_wave_get_color(s, 1, col_top); - const uint32_t col_fill = audio_wave_get_color(s, 2, col_bottom); - const uint32_t col_drop = audio_wave_get_color(s, 3, col_fill); - - std::vector amp(wx); - for (uint32_t x = 0; x < wx; ++x) { - const size_t idx = (size_t)((double)x * (double)(frames - 1) / std::max(1.0, (double)(wx - 1))); - amp[x] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - std::vector amp_smooth(wx); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.25f; - for (uint32_t i = 1; i < wx; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - } - - if (d->prev_top.size() != wx) { - d->prev_top.assign(wx, mid_y); - d->prev_bottom.assign(wx, mid_y); - d->initialized = false; - } - - std::vector top_y(wx), bottom_y(wx); - - const float base_alpha = 0.05f; - const float extra_alpha = 0.35f; - const float alpha_time = base_alpha + extra_alpha * (1.0f - d->viscosity); - const float half_band = d->band_height * 0.5f; - - for (uint32_t x = 0; x < wx; ++x) { - float a = amp_smooth[x]; - if (a < 0.0f) - a = 0.0f; - if (a > 1.0f) - a = 1.0f; - - float v = audio_wave_apply_curve(s, a); - - float offset = v * half_band; - - float top_target = mid_y - offset; - float bottom_target = mid_y + offset; - - if (!d->initialized) { - d->prev_top[x] = top_target; - d->prev_bottom[x] = bottom_target; - } - - float ty = d->prev_top[x] + alpha_time * (top_target - d->prev_top[x]); - float by = d->prev_bottom[x] + alpha_time * (bottom_target - d->prev_bottom[x]); - - d->prev_top[x] = ty; - d->prev_bottom[x] = by; - - top_y[x] = ty; - bottom_y[x] = by; - } - - d->initialized = true; - - gs_matrix_push(); - - if (color_param) - audio_wave_set_solid_color(color_param, col_fill); - - gs_render_start(true); - for (uint32_t x = 0; x + 1 < wx; ++x) { - const float x0 = (float)x; - const float x1 = (float)(x + 1); - - const float t0 = top_y[x]; - const float t1 = top_y[x + 1]; - const float b0 = bottom_y[x]; - const float b1 = bottom_y[x + 1]; - - gs_vertex2f(x0, b0); - gs_vertex2f(x0, t0); - gs_vertex2f(x1, b1); - - gs_vertex2f(x1, b1); - gs_vertex2f(x0, t0); - gs_vertex2f(x1, t1); - } - gs_render_stop(GS_TRIS); - - if (color_param) - audio_wave_set_solid_color(color_param, col_top); - - gs_render_start(true); - for (uint32_t x = 0; x < wx; ++x) { - gs_vertex2f((float)x, top_y[x]); - } - gs_render_stop(GS_LINESTRIP); - - if (color_param) - audio_wave_set_solid_color(color_param, col_bottom); - - gs_render_start(true); - for (uint32_t x = 0; x < wx; ++x) { - gs_vertex2f((float)x, bottom_y[x]); - } - gs_render_stop(GS_LINESTRIP); - - if (color_param) - audio_wave_set_solid_color(color_param, col_drop); - - gs_render_start(true); - for (uint32_t x = 0; x < wx; x += 3) { - float a = amp_smooth[x]; - if (a < 0.0f) - a = 0.0f; - if (a > 1.0f) - a = 1.0f; - - float v = audio_wave_apply_curve(s, a); - if (v < d->drop_threshold) - continue; - - float extra = (v - d->drop_threshold) / std::max(0.001f, 1.0f - d->drop_threshold); - if (extra < 0.0f) - extra = 0.0f; - if (extra > 1.0f) - extra = 1.0f; - - float len = d->drop_length * extra; - - const float x0 = (float)x; - const float y0 = bottom_y[x]; - const float y1 = std::min(h, y0 + len); - - gs_vertex2f(x0, y0); - gs_vertex2f(x0, y1); - } - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void fluid_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_fluid_theme = { - k_theme_id_fluid, k_theme_name_fluid, fluid_theme_add_properties, - fluid_theme_update, fluid_theme_draw, fluid_theme_destroy_data, -}; - -void audio_wave_register_fluid_theme() -{ - audio_wave_register_theme(&k_fluid_theme); -} diff --git a/src/themes/theme-fluid.hpp b/src/themes/theme-fluid.hpp deleted file mode 100644 index 0b62bd7..0000000 --- a/src/themes/theme-fluid.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_fluid_theme(); diff --git a/src/themes/theme-fluidblob.cpp b/src/themes/theme-fluidblob.cpp deleted file mode 100644 index 7679d97..0000000 --- a/src/themes/theme-fluidblob.cpp +++ /dev/null @@ -1,351 +0,0 @@ -#include "theme-fluidblob.hpp" -#include "audio-wave.hpp" -#include "audiowave-themes.hpp" - -#include -#include -#include -#include - -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_fluidblob = "fluidblob"; -static const char *k_theme_name_fluidblob = "Fluid Abstract"; - -static const char *FB_PROP_COLOR_OUTLINE = "fb_color_outline"; -static const char *FB_PROP_COLOR_FILL = "fb_color_fill"; -static const char *FB_PROP_COLOR_SPARK = "fb_color_spark"; -static const char *FB_PROP_SEGMENTS = "fb_segments"; -static const char *FB_PROP_VISCOSITY = "fb_viscosity"; -static const char *FB_PROP_NOISE = "fb_noise_amount"; -static const char *FB_PROP_THICK_OUTLINE = "fb_thickness_outline"; -static const char *FB_PROP_ROT_SPEED = "fb_rotation_speed"; -static const char *FB_PROP_SPARK_THRESH = "fb_spark_threshold"; -static const char *FB_PROP_SPARK_LENGTH = "fb_spark_length"; -static const char *FB_PROP_FILL_TRANSP = "fb_fill_transparent"; - -struct fluidblob_theme_data { - std::vector prev_r; - bool initialized = false; - - uint32_t segments = 160; - float viscosity = 0.6f; - float noise_amount = 0.4f; - int outline_thick = 3; - float rot_speed = 0.6f; - float phase = 0.0f; - - float spark_threshold = 0.35f; - float spark_length = 60.0f; - - bool fill_transparent = false; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void fluidblob_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, FB_PROP_COLOR_OUTLINE, "Outline Color"); - obs_properties_add_color(props, FB_PROP_COLOR_FILL, "Fill Color"); - obs_properties_add_color(props, FB_PROP_COLOR_SPARK, "Spark Color"); - obs_properties_add_int_slider(props, FB_PROP_SEGMENTS, "Shape Resolution", 32, 512, 8); - obs_properties_add_float_slider(props, FB_PROP_VISCOSITY, "Viscosity (Smoothness)", 0.0, 1.0, 0.05); - obs_properties_add_float_slider(props, FB_PROP_NOISE, "Organic Wobble Amount", 0.0, 1.0, 0.05); - obs_properties_add_int_slider(props, FB_PROP_THICK_OUTLINE, "Outline Thickness", 1, 8, 1); - obs_properties_add_float_slider(props, FB_PROP_ROT_SPEED, "Rotation Speed", 0.0, 5.0, 0.1); - obs_properties_add_float_slider(props, FB_PROP_SPARK_THRESH, "Spark Threshold (0..1)", 0.0, 1.0, 0.01); - obs_properties_add_int_slider(props, FB_PROP_SPARK_LENGTH, "Spark Length (px)", 5, 200, 5); - obs_properties_add_bool(props, FB_PROP_FILL_TRANSP, "Transparent Fill (outline only)"); -} - -static void fluidblob_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_outline = (uint32_t)aw_get_int_default(settings, FB_PROP_COLOR_OUTLINE, 0); - uint32_t col_fill = (uint32_t)aw_get_int_default(settings, FB_PROP_COLOR_FILL, 0); - uint32_t col_spark = (uint32_t)aw_get_int_default(settings, FB_PROP_COLOR_SPARK, 0); - - if (col_outline == 0) - col_outline = 0x00FFFF; - if (col_fill == 0) - col_fill = 0x101020; - if (col_spark == 0) - col_spark = 0xFFFFAA; - - s->color = col_outline; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"outline", col_outline}); - s->colors.push_back(audio_wave_named_color{"fill", col_fill}); - s->colors.push_back(audio_wave_named_color{"spark", col_spark}); - - int seg = aw_get_int_default(settings, FB_PROP_SEGMENTS, 160); - seg = std::clamp(seg, 32, 512); - - double visc = aw_get_float_default(settings, FB_PROP_VISCOSITY, 0.6f); - visc = std::clamp(visc, 0.0, 1.0); - - double noise = aw_get_float_default(settings, FB_PROP_NOISE, 0.4f); - noise = std::clamp(noise, 0.0, 1.0); - - int thick = aw_get_int_default(settings, FB_PROP_THICK_OUTLINE, 3); - thick = std::clamp(thick, 1, 8); - - double rot = aw_get_float_default(settings, FB_PROP_ROT_SPEED, 0.6f); - rot = std::clamp(rot, 0.0, 5.0); - - double thr = aw_get_float_default(settings, FB_PROP_SPARK_THRESH, 0.35f); - thr = std::clamp(thr, 0.0, 1.0); - - int spark_len = aw_get_int_default(settings, FB_PROP_SPARK_LENGTH, 60); - spark_len = std::clamp(spark_len, 5, 200); - - bool fill_transparent = obs_data_get_bool(settings, FB_PROP_FILL_TRANSP); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new fluidblob_theme_data{}; - s->theme_data = d; - } - - d->segments = (uint32_t)seg; - d->viscosity = (float)visc; - d->noise_amount = (float)noise; - d->outline_thick = thick; - d->rot_speed = (float)rot; - d->spark_threshold = (float)thr; - d->spark_length = (float)spark_len; - d->fill_transparent = fill_transparent; - d->initialized = false; - - if (s->frame_density < 80) - s->frame_density = 80; -} - -// ───────────────────────────────────────────── -// Drawing (blob geometry, outline, sparks) -// ───────────────────────────────────────────── - -static void fluidblob_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - if (s->wave.empty()) - audio_wave_build_wave(s); - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new fluidblob_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float base_radius = std::min(w, h) * 0.28f; - const float audio_radius = std::min(w, h) * 0.25f; - const float noise_radius = std::min(w, h) * 0.15f; - - const uint32_t segments = std::max(d->segments, 32u); - - const uint32_t col_outline = audio_wave_get_color(s, 0, s->color); - const uint32_t col_fill = audio_wave_get_color(s, 1, col_outline); - const uint32_t col_spark = audio_wave_get_color(s, 2, col_fill); - - std::vector amp(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const size_t idx = (size_t)(u * (float)(frames - 1)); - amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - std::vector amp_smooth(segments); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.25f; - for (uint32_t i = 1; i < segments; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - float wrap = amp_smooth[segments - 1]; - amp_smooth[0] = 0.5f * (amp_smooth[0] + wrap); - } - - if (d->prev_r.size() != segments) { - d->prev_r.assign(segments, base_radius); - d->initialized = false; - } - - std::vector radius(segments); - - const float base_alpha = 0.06f; - const float extra_alpha = 0.34f; - const float alpha_time = base_alpha + extra_alpha * (1.0f - d->viscosity); - - d->phase += d->rot_speed * (float)M_PI / 180.0f; - if (d->phase > 2.0f * (float)M_PI) - d->phase -= 2.0f * (float)M_PI; - - const int noise_harmonics = 2 + (int)std::round(d->noise_amount * 3.0f); - - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - a = std::clamp(a, 0.0f, 1.0f); - - float v = audio_wave_apply_curve(s, a); - - float r_target = base_radius + v * audio_radius; - - const float angle = ((float)i / (float)segments) * 2.0f * (float)M_PI; - float wobble = 0.0f; - const float n_amp = d->noise_amount; - - for (int h2 = 1; h2 <= noise_harmonics; ++h2) { - float contrib = std::sin((float)h2 * angle + d->phase + (float)h2 * 0.7f); - wobble += contrib * (1.0f / (float)h2); - } - wobble /= (float)noise_harmonics; - - r_target += wobble * n_amp * noise_radius; - - if (!d->initialized) - d->prev_r[i] = r_target; - - float r_s = d->prev_r[i] + alpha_time * (r_target - d->prev_r[i]); - d->prev_r[i] = r_s; - radius[i] = r_s; - } - - d->initialized = true; - - std::vector cos_t(segments), sin_t(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float t = ((float)i / (float)segments) * 2.0f * (float)M_PI; - cos_t[i] = std::cos(t); - sin_t[i] = std::sin(t); - } - - std::vector x(segments), y(segments); - for (uint32_t i = 0; i < segments; ++i) { - x[i] = cx + cos_t[i] * radius[i]; - y[i] = cy + sin_t[i] * radius[i]; - } - - gs_matrix_push(); - - if (!d->fill_transparent) { - if (color_param) - audio_wave_set_solid_color(color_param, col_fill); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - uint32_t next = (i + 1) % segments; - - gs_vertex2f(cx, cy); - gs_vertex2f(x[i], y[i]); - gs_vertex2f(x[next], y[next]); - } - gs_render_stop(GS_TRIS); - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_outline); - - int thick = d->outline_thick; - if (thick < 1) - thick = 1; - - { - const float half = (float)(thick - 1) * 0.5f; - for (int t = 0; t < thick; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - float r = radius[idx] + offset; - if (r < 0.0f) - r = 0.0f; - - const float px = cx + cos_t[idx] * r; - const float py = cy + sin_t[idx] * r; - - gs_vertex2f(px, py); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_spark); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; i += 2) { - float a = amp_smooth[i]; - a = std::clamp(a, 0.0f, 1.0f); - - float v = audio_wave_apply_curve(s, a); - if (v < d->spark_threshold) - continue; - - float extra = (v - d->spark_threshold) / std::max(0.001f, 1.0f - d->spark_threshold); - extra = std::clamp(extra, 0.0f, 1.0f); - - float len = d->spark_length * extra; - - const float r_start = radius[i] + 2.0f; - const float r_end = r_start + len; - - const float xs = cx + cos_t[i] * r_start; - const float ys = cy + sin_t[i] * r_start; - const float xe = cx + cos_t[i] * r_end; - const float ye = cy + sin_t[i] * r_end; - - gs_vertex2f(xs, ys); - gs_vertex2f(xe, ye); - } - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void fluidblob_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_fluidblob_theme = {k_theme_id_fluidblob, - k_theme_name_fluidblob, - fluidblob_theme_add_properties, - fluidblob_theme_update, - fluidblob_theme_draw, - fluidblob_theme_destroy_data, - nullptr}; - -void audio_wave_register_fluidblob_theme() -{ - audio_wave_register_theme(&k_fluidblob_theme); -} diff --git a/src/themes/theme-fluidblob.hpp b/src/themes/theme-fluidblob.hpp deleted file mode 100644 index 066e44b..0000000 --- a/src/themes/theme-fluidblob.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_fluidblob_theme(); diff --git a/src/themes/theme-hex.cpp b/src/themes/theme-hex.cpp index db5eaaf..8459a51 100644 --- a/src/themes/theme-hex.cpp +++ b/src/themes/theme-hex.cpp @@ -10,8 +10,20 @@ static const char *k_theme_id_hex = "hexagon"; static const char *k_theme_name_hex = "Hexagon"; static const char *HEX_PROP_STYLE = "hex_style"; -static const char *HEX_PROP_COLOR = "hex_color"; static const char *HEX_PROP_MIRROR = "hex_mirror"; +static const char *P_DENSITY = "shape_density"; + +static bool hex_style_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) +{ + const char *style_id = obs_data_get_string(settings, HEX_PROP_STYLE); + const bool is_rays = (style_id && strcmp(style_id, "rays") == 0); + + obs_property_t *mirror = obs_properties_get(props, HEX_PROP_MIRROR); + if (mirror) + obs_property_set_visible(mirror, is_rays); + + return true; +} static void hex_theme_add_properties(obs_properties_t *props) { @@ -20,9 +32,11 @@ static void hex_theme_add_properties(obs_properties_t *props) obs_property_list_add_string(style, "Orbit", "orbit"); obs_property_list_add_string(style, "Rays", "rays"); + obs_property_set_modified_callback(style, hex_style_modified); - obs_properties_add_color(props, HEX_PROP_COLOR, "Color"); - obs_properties_add_bool(props, HEX_PROP_MIRROR, "Double-sided rays"); + obs_property_t *mirror = obs_properties_add_bool(props, HEX_PROP_MIRROR, "Double-sided rays"); + obs_property_set_visible(mirror, false); + obs_properties_add_int_slider(props, P_DENSITY, "Shape Density (%)", 10, 300, 5); } static void hex_theme_update(audio_wave_source *s, obs_data_t *settings) @@ -36,19 +50,11 @@ static void hex_theme_update(audio_wave_source *s, obs_data_t *settings) s->theme_style_id = style_id; - uint32_t color = (uint32_t)aw_get_int_default(settings, HEX_PROP_COLOR, 0); - if (color == 0) - color = 0x00FFCC; - - s->color = color; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"hex", color}); + int density = aw_get_int_default(settings, P_DENSITY, 120); + density = std::clamp(density, 10, 300); + s->frame_density = density; - if (s->frame_density < 50) - s->frame_density = 50; - - s->mirror = obs_data_get_bool(settings, HEX_PROP_MIRROR); + s->mirror = (strcmp(style_id, "rays") == 0) ? obs_data_get_bool(settings, HEX_PROP_MIRROR) : false; } // ───────────────────────────────────────────── @@ -181,22 +187,15 @@ static void draw_hex_orbit(audio_wave_source *s, gs_eparam_t *color_param) return amp_smooth[i]; }; - const uint32_t hex_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, hex_color); - - gs_render_start(true); + std::vector px(segments), py(segments); + const float cx = (float)s->width * 0.5f; + const float cy = (float)s->height * 0.5f; for (uint32_t i = 0; i < segments; ++i) { const float v_raw = get_amp(i); const float v = audio_wave_apply_curve(s, v_raw); const float len = v * max_len; - - const float cx = (float)s->width * 0.5f; - const float cy = (float)s->height * 0.5f; - float x = base_x[i]; float y = base_y[i]; - float dx = x - cx; float dy = y - cy; float l = std::sqrt(dx * dx + dy * dy); @@ -207,13 +206,39 @@ static void draw_hex_orbit(audio_wave_source *s, gs_eparam_t *color_param) dx = 0.0f; dy = -1.0f; } + px[i] = x + dx * len; + py[i] = y + dy * len; + } - const float x2 = x + dx * len; - const float y2 = y + dy * len; - - gs_vertex2f(x2, y2); + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const uint32_t j = (i + 1) % segments; + gs_vertex2f(px[i], py[i]); + gs_vertex2f(px[j], py[j]); + } + gs_render_stop(GS_LINES); + } + } else { + const uint32_t hex_color = audio_wave_get_color(s, 0, s->color); + if (color_param) + audio_wave_set_solid_color(color_param, hex_color); + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const uint32_t j = (i + 1) % segments; + gs_vertex2f(px[i], py[i]); + gs_vertex2f(px[j], py[j]); + } + gs_render_stop(GS_LINES); } - gs_render_stop(GS_LINESTRIP); } static void draw_hex_rays(audio_wave_source *s, gs_eparam_t *color_param) @@ -255,32 +280,66 @@ static void draw_hex_rays(audio_wave_source *s, gs_eparam_t *color_param) amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; } - const uint32_t hex_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, hex_color); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - const float v_raw = amp[i]; - const float v = audio_wave_apply_curve(s, v_raw); - const float len = v * max_len; + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + + const float x1 = base_x[i]; + const float y1 = base_y[i]; + const float x2 = x1 + nx[i] * len; + const float y2 = y1 + ny[i] * len; + + gs_vertex2f(x1, y1); + gs_vertex2f(x2, y2); + + if (s->mirror) { + const float x3 = x1 - nx[i] * len; + const float y3 = y1 - ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } + } + gs_render_stop(GS_LINES); + } + } else { + const uint32_t hex_color = audio_wave_get_color(s, 0, s->color); + if (color_param) + audio_wave_set_solid_color(color_param, hex_color); - const float x1 = base_x[i]; - const float y1 = base_y[i]; - const float x2 = x1 + nx[i] * len; - const float y2 = y1 + ny[i] * len; + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; - gs_vertex2f(x1, y1); - gs_vertex2f(x2, y2); + const float x1 = base_x[i]; + const float y1 = base_y[i]; + const float x2 = x1 + nx[i] * len; + const float y2 = y1 + ny[i] * len; - if (s->mirror) { - const float x3 = x1 - nx[i] * len; - const float y3 = y1 - ny[i] * len; gs_vertex2f(x1, y1); - gs_vertex2f(x3, y3); + gs_vertex2f(x2, y2); + + if (s->mirror) { + const float x3 = x1 - nx[i] * len; + const float y3 = y1 - ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } } + gs_render_stop(GS_LINES); } - gs_render_stop(GS_LINES); } static void hex_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) diff --git a/src/themes/theme-lightning.cpp b/src/themes/theme-lightning.cpp deleted file mode 100644 index ad2fc45..0000000 --- a/src/themes/theme-lightning.cpp +++ /dev/null @@ -1,362 +0,0 @@ -#include "theme-lightning.hpp" - -#include -#include -#include -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_lightning = "lightning"; -static const char *k_theme_name_lightning = "Storm Lightning"; - -// ───────────────────────────────────────────── -// Property keys -// ───────────────────────────────────────────── - -static const char *LIGHT_PROP_COLOR_CORE = "lt_color_core"; -static const char *LIGHT_PROP_COLOR_GLOW = "lt_color_glow"; - -static const char *LIGHT_PROP_DB_THRESHOLD = "lt_db_threshold"; -static const char *LIGHT_PROP_DB_FULLSCALE = "lt_db_fullscale"; - -static const char *LIGHT_PROP_BOLT_COUNT = "lt_bolt_count"; -static const char *LIGHT_PROP_JAGGED = "lt_jagged"; -static const char *LIGHT_PROP_THICK_CORE = "lt_thick_core"; -static const char *LIGHT_PROP_THICK_GLOW = "lt_thick_glow"; - -// ───────────────────────────────────────────── -// Theme data -// ───────────────────────────────────────────── - -struct lightning_theme_data { - std::vector prev_length; - bool initialized = false; - - float db_threshold = -24.0f; - float db_fullscale = -6.0f; - - uint32_t bolts = 64; - int jagged = 8; - int thick_core = 1; - int thick_glow = 3; -}; - -// ───────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────── - -static inline float db_from_amp(float a) -{ - if (a <= 1e-6f) - return -120.0f; - return 20.0f * log10f(a); -} - -static inline float clamp01(float v) -{ - if (v < 0.0f) - return 0.0f; - if (v > 1.0f) - return 1.0f; - return v; -} - -// simple deterministic hash -> [0,1) -static inline float hash11(float x) -{ - float s = sinf(x * 12.9898f) * 43758.5453f; - return s - floorf(s); -} - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void lightning_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, LIGHT_PROP_COLOR_CORE, "Core Lightning Color"); - obs_properties_add_color(props, LIGHT_PROP_COLOR_GLOW, "Glow Color"); - - obs_properties_add_int_slider(props, LIGHT_PROP_DB_THRESHOLD, "Strike Threshold (dB)", -60, 0, 1); - obs_properties_add_int_slider(props, LIGHT_PROP_DB_FULLSCALE, "Full Intensity dB", -60, 0, 1); - - obs_properties_add_int_slider(props, LIGHT_PROP_BOLT_COUNT, "Lightning Rays", 8, 256, 1); - obs_properties_add_int_slider(props, LIGHT_PROP_JAGGED, "Jaggedness", 3, 32, 1); - - obs_properties_add_int_slider(props, LIGHT_PROP_THICK_CORE, "Core Thickness", 1, 8, 1); - obs_properties_add_int_slider(props, LIGHT_PROP_THICK_GLOW, "Glow Thickness", 1, 8, 1); -} - -static void lightning_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - // keep style id predictable - s->theme_style_id = "default"; - - uint32_t col_core = (uint32_t)aw_get_int_default(settings, LIGHT_PROP_COLOR_CORE, 0); - uint32_t col_glow = (uint32_t)aw_get_int_default(settings, LIGHT_PROP_COLOR_GLOW, 0); - - // fallbacks - if (col_core == 0) - col_core = 0xFFFFFF; // bright white core - if (col_glow == 0) - col_glow = 0x33CCFF; // cyan/blue glow - - s->color = col_core; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"core", col_core}); - s->colors.push_back(audio_wave_named_color{"glow", col_glow}); - - int db_th = aw_get_int_default(settings, LIGHT_PROP_DB_THRESHOLD, -24); - int db_fs = aw_get_int_default(settings, LIGHT_PROP_DB_FULLSCALE, -6); - - db_th = std::clamp(db_th, -60, 0); - db_fs = std::clamp(db_fs, -60, 0); - - // ensure fullscale is above threshold, otherwise the mapping breaks - if (db_fs <= db_th) - db_fs = db_th + 1; - - int bolt_count = aw_get_int_default(settings, LIGHT_PROP_BOLT_COUNT, 64); - int jagged = aw_get_int_default(settings, LIGHT_PROP_JAGGED, 8); - int thick_core = aw_get_int_default(settings, LIGHT_PROP_THICK_CORE, 1); - int thick_glow = aw_get_int_default(settings, LIGHT_PROP_THICK_GLOW, 3); - - bolt_count = std::clamp(bolt_count, 8, 256); - jagged = std::clamp(jagged, 3, 32); - thick_core = std::clamp(thick_core, 1, 8); - thick_glow = std::clamp(thick_glow, 1, 8); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new lightning_theme_data{}; - s->theme_data = d; - } - - d->db_threshold = (float)db_th; - d->db_fullscale = (float)db_fs; - d->bolts = (uint32_t)bolt_count; - d->jagged = jagged; - d->thick_core = thick_core; - d->thick_glow = thick_glow; - - d->initialized = false; -} - -// ───────────────────────────────────────────── -// Drawing (NO BACKGROUND) -// ───────────────────────────────────────────── - -static void lightning_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new lightning_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float maxR = std::min(w, h) * 0.5f * 0.95f; // fade out near edges - - const uint32_t bolts = std::max(d->bolts, 8u); - - const uint32_t col_core = audio_wave_get_color(s, 0, s->color); - const uint32_t col_glow = audio_wave_get_color(s, 1, col_core); - - // ensure buffers - if (d->prev_length.size() != bolts) { - d->prev_length.assign(bolts, 0.0f); - d->initialized = false; - } - - // ── Build per-bolt amplitude → radial length - const float alpha_time = 0.25f; // smoothing - for (uint32_t i = 0; i < bolts; ++i) { - const float u = (float)i / (float)(bolts - 1); - const size_t idx = (size_t)(u * (float)(frames - 1)); - - float a = (idx < frames) ? s->wave[idx] : 0.0f; - if (a < 1e-6f) - a = 0.0f; - - float db = db_from_amp(a); - - float length_target = 0.0f; - - // Only create a strike above threshold - if (db > d->db_threshold) { - float t = (db - d->db_threshold) / (d->db_fullscale - d->db_threshold + 1e-3f); - t = clamp01(t); - length_target = t * maxR; - } - - if (!d->initialized) - d->prev_length[i] = length_target; - - float L = d->prev_length[i] + alpha_time * (length_target - d->prev_length[i]); - - if (L < 0.0f) - L = 0.0f; - - d->prev_length[i] = L; - } - - d->initialized = true; - - gs_matrix_push(); - - // Precompute some constants - const float twoPi = 2.0f * (float)M_PI; - const int steps_base = std::max(d->jagged, 3); - - // Max angular offset (radians) for jaggedness - const float maxAngleOffsetBase = 0.35f; - - // ── Glow pass: thicker, softer - if (color_param) - audio_wave_set_solid_color(color_param, col_glow); - - int thickGlow = d->thick_glow; - if (thickGlow < 1) - thickGlow = 1; - - { - const float half = (float)(thickGlow - 1) * 0.5f; - - for (uint32_t i = 0; i < bolts; ++i) { - float L = d->prev_length[i]; - if (L <= 1.0f) - continue; - - const float baseAngle = ((float)i / (float)bolts) * twoPi; - const int steps = steps_base; - const float stepR = L / (float)steps; - - for (int t = 0; t < thickGlow; ++t) { - const float radialOffset = (float)t - half; - - gs_render_start(true); - - for (int j = 0; j <= steps; ++j) { - float rr = stepR * (float)j + radialOffset; - if (rr < 0.0f) - rr = 0.0f; - - // fade jaggedness toward center & edge - float v = (float)j / (float)steps; - float fadeJagged = (1.0f - fabsf(v - 0.7f)); - fadeJagged = clamp01(fadeJagged); - - float n = hash11((float)i * 13.37f + (float)j * 7.91f); - float angleOffset = (n - 0.5f) * maxAngleOffsetBase * fadeJagged; - - float ang = baseAngle + angleOffset; - - const float x = cx + cosf(ang) * rr; - const float y = cy + sinf(ang) * rr; - - gs_vertex2f(x, y); - } - - gs_render_stop(GS_LINESTRIP); - } - } - } - - // ── Core pass: thinner, sharper line sitting on top - if (color_param) - audio_wave_set_solid_color(color_param, col_core); - - int thickCore = d->thick_core; - if (thickCore < 1) - thickCore = 1; - - { - const float half = (float)(thickCore - 1) * 0.5f; - - for (uint32_t i = 0; i < bolts; ++i) { - float L = d->prev_length[i]; - if (L <= 1.0f) - continue; - - const float baseAngle = ((float)i / (float)bolts) * twoPi; - const int steps = steps_base; - const float stepR = L / (float)steps; - - for (int t = 0; t < thickCore; ++t) { - const float radialOffset = (float)t - half; - - gs_render_start(true); - - for (int j = 0; j <= steps; ++j) { - float rr = stepR * (float)j + radialOffset; - if (rr < 0.0f) - rr = 0.0f; - - float v = (float)j / (float)steps; - float fadeJagged = (1.0f - fabsf(v - 0.7f)); - fadeJagged = clamp01(fadeJagged); - - float n = hash11((float)i * 31.17f + (float)j * 19.31f); - float angleOffset = (n - 0.5f) * maxAngleOffsetBase * fadeJagged; - - float ang = baseAngle + angleOffset; - - const float x = cx + cosf(ang) * rr; - const float y = cy + sinf(ang) * rr; - - gs_vertex2f(x, y); - } - - gs_render_stop(GS_LINESTRIP); - } - } - } - - gs_matrix_pop(); -} - -// ───────────────────────────────────────────── -// Destroy -// ───────────────────────────────────────────── - -static void lightning_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -// ───────────────────────────────────────────── -// Registration -// ───────────────────────────────────────────── - -static const audio_wave_theme k_lightning_theme = { - k_theme_id_lightning, k_theme_name_lightning, lightning_theme_add_properties, - lightning_theme_update, lightning_theme_draw, lightning_theme_destroy_data, -}; - -void audio_wave_register_lightning_theme() -{ - audio_wave_register_theme(&k_lightning_theme); -} diff --git a/src/themes/theme-lightning.hpp b/src/themes/theme-lightning.hpp deleted file mode 100644 index 9941e6e..0000000 --- a/src/themes/theme-lightning.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_lightning_theme(); diff --git a/src/themes/theme-line.cpp b/src/themes/theme-line.cpp index 8b087e7..e4b00ca 100644 --- a/src/themes/theme-line.cpp +++ b/src/themes/theme-line.cpp @@ -11,8 +11,6 @@ static const char *k_theme_id_line = "line"; static const char *k_theme_name_line = "Line"; static const char *LINE_PROP_STYLE = "line_style"; -static const char *LINE_PROP_COLOR = "line_color"; -static const char *LINE_PROP_FILL_COLOR = "line_fill_color"; static const char *LINE_PROP_MIRROR = "line_mirror"; static const char *LINE_PROP_CURVE_COUNT = "line_curve_count"; static const char *LINE_PROP_OUTLINE_THICK = "line_outline_thickness"; @@ -33,13 +31,8 @@ static bool line_style_modified(obs_properties_t *props, obs_property_t *propert const char *style_id = obs_data_get_string(settings, LINE_PROP_STYLE); const bool is_filled = (style_id && std::strcmp(style_id, "filled") == 0); - - obs_property_t *fill_color = obs_properties_get(props, LINE_PROP_FILL_COLOR); obs_property_t *curve_cnt = obs_properties_get(props, LINE_PROP_CURVE_COUNT); obs_property_t *outline_th = obs_properties_get(props, LINE_PROP_OUTLINE_THICK); - - if (fill_color) - obs_property_set_visible(fill_color, is_filled); if (curve_cnt) obs_property_set_visible(curve_cnt, is_filled); if (outline_th) @@ -59,10 +52,6 @@ static void line_theme_add_properties(obs_properties_t *props) obs_property_set_modified_callback(style, line_style_modified); - obs_property_t *outline_color = obs_properties_add_color(props, LINE_PROP_COLOR, "Outline Color"); - - obs_property_t *fill_color = obs_properties_add_color(props, LINE_PROP_FILL_COLOR, "Fill Color"); - obs_property_t *mirror = obs_properties_add_bool(props, LINE_PROP_MIRROR, "Mirror vertically"); obs_property_t *curve_cnt = @@ -70,12 +59,9 @@ static void line_theme_add_properties(obs_properties_t *props) obs_property_t *outline_th = obs_properties_add_int_slider(props, LINE_PROP_OUTLINE_THICK, "Outline Thickness", 1, 8, 1); - - obs_property_set_visible(fill_color, false); obs_property_set_visible(curve_cnt, false); obs_property_set_visible(outline_th, false); - UNUSED_PARAMETER(outline_color); UNUSED_PARAMETER(mirror); } @@ -90,20 +76,6 @@ static void line_theme_update(audio_wave_source *s, obs_data_t *settings) s->theme_style_id = style_id; - uint32_t outline = (uint32_t)aw_get_int_default(settings, LINE_PROP_COLOR, 0); - if (outline == 0) - outline = 0xFF0000; - - uint32_t fill = (uint32_t)aw_get_int_default(settings, LINE_PROP_FILL_COLOR, 0); - if (fill == 0) - fill = 0x001A4F; - - s->color = outline; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"outline", outline}); - s->colors.push_back(audio_wave_named_color{"fill", fill}); - s->mirror = obs_data_get_bool(settings, LINE_PROP_MIRROR); int curve_count = aw_get_int_default(settings, LINE_PROP_CURVE_COUNT, 3); @@ -182,10 +154,29 @@ static void draw_line_linear(audio_wave_source *s, gs_eparam_t *color_param, boo return smooth ? amp_smooth[x] : amp[x]; }; - if (color_param) + if (color_param && !s->gradient_enabled) audio_wave_set_solid_color(color_param, s->color); - gs_render_start(true); + if (s->gradient_enabled && color_param) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t x0 = (uint32_t)((uint64_t)b * width_u / bins); + const uint32_t x1 = (uint32_t)((uint64_t)(b + 1) * width_u / bins); + if (x1 <= x0) + continue; + const float tcol = (width_u <= 1) ? 0.0f : ((float)x0 / (float)(width_u - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t x = x0; x < x1; ++x) { + const float v_raw = get_amp(x); + const float v = audio_wave_apply_curve(s, v_raw); + const float y = mid_y - v * (mid_y - top_margin); + gs_vertex2f((float)x, y); + } + gs_render_stop(GS_LINESTRIP); + } + } else { + gs_render_start(true); for (uint32_t x = 0; x < width_u; ++x) { const float v_raw = get_amp(x); const float v = audio_wave_apply_curve(s, v_raw); @@ -193,19 +184,41 @@ static void draw_line_linear(audio_wave_source *s, gs_eparam_t *color_param, boo gs_vertex2f((float)x, y); } gs_render_stop(GS_LINESTRIP); + } if (!s->mirror) return; - gs_render_start(true); - for (uint32_t x = 0; x < width_u; ++x) { - const float v_raw = get_amp(x); - const float v = audio_wave_apply_curve(s, v_raw); - const float y = mid_y - v * (mid_y - top_margin); - const float y_m = mid_y + (mid_y - y); - gs_vertex2f((float)x, y_m); + if (s->gradient_enabled && color_param) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t x0 = (uint32_t)((uint64_t)b * width_u / bins); + const uint32_t x1 = (uint32_t)((uint64_t)(b + 1) * width_u / bins); + if (x1 <= x0) + continue; + const float tcol = (width_u <= 1) ? 0.0f : ((float)x0 / (float)(width_u - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t x = x0; x < x1; ++x) { + const float v_raw = get_amp(x); + const float v = audio_wave_apply_curve(s, v_raw); + const float y = mid_y - v * (mid_y - top_margin); + const float y_m = mid_y + (mid_y - y); + gs_vertex2f((float)x, y_m); + } + gs_render_stop(GS_LINESTRIP); + } + } else { + gs_render_start(true); + for (uint32_t x = 0; x < width_u; ++x) { + const float v_raw = get_amp(x); + const float v = audio_wave_apply_curve(s, v_raw); + const float y = mid_y - v * (mid_y - top_margin); + const float y_m = mid_y + (mid_y - y); + gs_vertex2f((float)x, y_m); + } + gs_render_stop(GS_LINESTRIP); } - gs_render_stop(GS_LINESTRIP); } static void draw_line_bars(audio_wave_source *s, gs_eparam_t *color_param) @@ -228,13 +241,33 @@ static void draw_line_bars(audio_wave_source *s, gs_eparam_t *color_param) amp[x] = (idx < frames) ? s->wave[idx] : 0.0f; } - if (color_param) + const uint32_t step = 3; + + if (!s->gradient_enabled && color_param) audio_wave_set_solid_color(color_param, s->color); - const uint32_t step = 3; + int cur_bin = -1; + const int bins = 64; + cur_bin = 0; + + if (s->gradient_enabled && color_param) { + // start with first bin color + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, 0.0f)); + cur_bin = 0; + } gs_render_start(true); for (uint32_t x = 0; x < width_u; x += step) { + if (s->gradient_enabled && color_param) { + const float tcol = (width_u <= 1) ? 0.0f : ((float)x / (float)(width_u - 1)); + const int b = std::clamp((int)std::floor(tcol * (float)(bins - 1) + 0.5f), 0, bins - 1); + if (b != cur_bin) { + gs_render_stop(GS_LINES); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, (float)b / (float)(bins - 1))); + gs_render_start(true); + cur_bin = b; + } + } const float v_raw = amp[x]; const float v = audio_wave_apply_curve(s, v_raw); const float y = mid_y - v * (mid_y - 4.0f); @@ -273,9 +306,6 @@ static void draw_line_filled(audio_wave_source *s, gs_eparam_t *color_param) s->theme_data = d; } - const uint32_t outline_color = audio_wave_get_color(s, 0, s->color); - const uint32_t fill_color = audio_wave_get_color(s, 1, outline_color); - std::vector amp(width_u); for (uint32_t x = 0; x < width_u; ++x) { const size_t idx = sample_index_for_x(d, x, width_u, frames); @@ -320,42 +350,86 @@ static void draw_line_filled(audio_wave_source *s, gs_eparam_t *color_param) d->initialized = true; - if (color_param) - audio_wave_set_solid_color(color_param, fill_color); + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t x0 = (uint32_t)((uint64_t)b * (width_u - 1) / bins); + const uint32_t x1 = (uint32_t)((uint64_t)(b + 1) * (width_u - 1) / bins); + if (x1 <= x0) + continue; - gs_render_start(true); - for (uint32_t x = 0; x + 1 < width_u; ++x) { - const float y1 = ys[x]; - const float y2 = ys[x + 1]; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + + gs_render_start(true); + for (uint32_t x = x0; x < x1; ++x) { + const float y1 = ys[x]; + const float y2 = ys[x + 1]; - gs_vertex2f((float)x, baseline); - gs_vertex2f((float)x, y1); - gs_vertex2f((float)(x + 1), baseline); + gs_vertex2f((float)x, baseline); + gs_vertex2f((float)x, y1); + gs_vertex2f((float)(x + 1), baseline); - gs_vertex2f((float)(x + 1), baseline); - gs_vertex2f((float)x, y1); - gs_vertex2f((float)(x + 1), y2); + gs_vertex2f((float)(x + 1), baseline); + gs_vertex2f((float)x, y1); + gs_vertex2f((float)(x + 1), y2); - if (s->mirror) { - float y1m = 2.0f * mid_y - y1; - float y2m = 2.0f * mid_y - y2; + if (s->mirror) { + float y1m = 2.0f * mid_y - y1; + float y2m = 2.0f * mid_y - y2; + + y1m = std::clamp(y1m, 0.0f, h); + y2m = std::clamp(y2m, 0.0f, h); + + gs_vertex2f((float)x, 0.0f); + gs_vertex2f((float)x, y1m); + gs_vertex2f((float)(x + 1), 0.0f); - y1m = std::clamp(y1m, 0.0f, h); - y2m = std::clamp(y2m, 0.0f, h); + gs_vertex2f((float)(x + 1), 0.0f); + gs_vertex2f((float)x, y1m); + gs_vertex2f((float)(x + 1), y2m); + } + } + gs_render_stop(GS_TRIS); + } + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); + + gs_render_start(true); + for (uint32_t x = 0; x + 1 < width_u; ++x) { + const float y1 = ys[x]; + const float y2 = ys[x + 1]; + + gs_vertex2f((float)x, baseline); + gs_vertex2f((float)x, y1); + gs_vertex2f((float)(x + 1), baseline); - gs_vertex2f((float)x, 0.0f); - gs_vertex2f((float)x, y1m); - gs_vertex2f((float)(x + 1), 0.0f); + gs_vertex2f((float)(x + 1), baseline); + gs_vertex2f((float)x, y1); + gs_vertex2f((float)(x + 1), y2); - gs_vertex2f((float)(x + 1), 0.0f); - gs_vertex2f((float)x, y1m); - gs_vertex2f((float)(x + 1), y2m); + if (s->mirror) { + float y1m = 2.0f * mid_y - y1; + float y2m = 2.0f * mid_y - y2; + + y1m = std::clamp(y1m, 0.0f, h); + y2m = std::clamp(y2m, 0.0f, h); + + gs_vertex2f((float)x, 0.0f); + gs_vertex2f((float)x, y1m); + gs_vertex2f((float)(x + 1), 0.0f); + + gs_vertex2f((float)(x + 1), 0.0f); + gs_vertex2f((float)x, y1m); + gs_vertex2f((float)(x + 1), y2m); + } } + gs_render_stop(GS_TRIS); } - gs_render_stop(GS_TRIS); - if (color_param) - audio_wave_set_solid_color(color_param, outline_color); + if (color_param && !s->gradient_enabled) + audio_wave_set_solid_color(color_param, s->color); int thick = d->outline_thickness; if (thick < 1) @@ -400,8 +474,12 @@ static void line_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) gs_matrix_push(); if (frames < 2) { - if (color_param) - audio_wave_set_solid_color(color_param, s->color); + if (color_param) { + if (s->gradient_enabled) + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, 0.5f)); + else + audio_wave_set_solid_color(color_param, s->color); + } gs_render_start(true); for (uint32_t x = 0; x < (uint32_t)w; ++x) diff --git a/src/themes/theme-magicsquare.cpp b/src/themes/theme-magicsquare.cpp deleted file mode 100644 index d7ee81b..0000000 --- a/src/themes/theme-magicsquare.cpp +++ /dev/null @@ -1,553 +0,0 @@ -#include "theme-magicsquare.hpp" - -#include -#include -#include -#include - -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_magicsquare = "magic_square"; -static const char *k_theme_name_magicsquare = "Magic Square Sparkles"; - -static const char *MSQ_PROP_COLOR_CORE = "msq_color_core"; -static const char *MSQ_PROP_COLOR_RING = "msq_color_ring"; -static const char *MSQ_PROP_COLOR_SPARK = "msq_color_spark"; - -static const char *MSQ_PROP_SEGMENTS = "msq_segments"; -static const char *MSQ_PROP_VISCOSITY = "msq_viscosity"; -static const char *MSQ_PROP_NOISE = "msq_noise"; -static const char *MSQ_PROP_RING_THICKNESS = "msq_ring_thickness"; -static const char *MSQ_PROP_ROT_SPEED = "msq_rotation_speed"; - -static const char *MSQ_PROP_CORE_SIZE = "msq_core_size"; -static const char *MSQ_PROP_RING_SIZE = "msq_ring_size"; - -static const char *MSQ_PROP_SPARK_COUNT = "msq_spark_count"; -static const char *MSQ_PROP_SPARK_LENGTH = "msq_spark_length"; -static const char *MSQ_PROP_SPARK_ORBIT = "msq_spark_orbit"; -static const char *MSQ_PROP_SPARK_ENERGY = "msq_spark_energy_response"; -static const char *MSQ_PROP_SPARK_MIN_LEVEL = "msq_spark_min_level"; - -// ───────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────── - -static float msq_clamp_float(float v, float lo, float hi) -{ - if (v < lo) - return lo; - if (v > hi) - return hi; - return v; -} - -static float msq_pseudo_rand01(uint32_t seed) -{ - uint32_t x = seed; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - return (float)(x & 0x00FFFFFFu) / (float)0x01000000u; -} - -struct msq_spark { - float pos = 0.0f; - float offset = 0.0f; - float life = 0.0f; - float maxLife = 1.0f; - float speed = 0.0f; -}; - -struct magicsquare_theme_data { - std::vector prev_offset; - bool initialized = false; - - uint32_t segments = 120; - float viscosity = 0.65f; - float noise_amount = 0.3f; - int ring_thickness = 4; - float rot_speed = 0.5f; - float phase = 0.0f; - - float core_size = 1.0f; - float ring_size = 1.0f; - - uint32_t spark_count = 40; - float spark_length = 60.0f; - float spark_orbit_mult = 1.20f; - float spark_energy_resp = 0.6f; - float spark_min_level = 0.25f; - - std::vector sparks; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void magicsquare_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, MSQ_PROP_COLOR_CORE, "Core Glow Color"); - obs_properties_add_color(props, MSQ_PROP_COLOR_RING, "Ring Color"); - obs_properties_add_color(props, MSQ_PROP_COLOR_SPARK, "Sparkle Color"); - - obs_properties_add_int_slider(props, MSQ_PROP_SEGMENTS, "Shape Resolution", 32, 512, 8); - obs_properties_add_float_slider(props, MSQ_PROP_VISCOSITY, "Viscosity (Smoothness)", 0.0, 1.0, 0.05); - obs_properties_add_float_slider(props, MSQ_PROP_NOISE, "Organic Wobble Amount", 0.0, 1.0, 0.05); - obs_properties_add_int_slider(props, MSQ_PROP_RING_THICKNESS, "Ring Thickness", 0, 10, 1); - obs_properties_add_float_slider(props, MSQ_PROP_ROT_SPEED, "Rotation Speed", 0.0, 5.0, 0.1); - - obs_properties_add_float_slider(props, MSQ_PROP_CORE_SIZE, "Core Size", 0.0, 2.0, 0.05); - obs_properties_add_float_slider(props, MSQ_PROP_RING_SIZE, "Ring Size", 0.0, 2.0, 0.05); - - obs_properties_add_int_slider(props, MSQ_PROP_SPARK_COUNT, "Spark Count", 0, 200, 2); - obs_properties_add_int_slider(props, MSQ_PROP_SPARK_LENGTH, "Spark Length (px)", 5, 200, 5); - obs_properties_add_float_slider(props, MSQ_PROP_SPARK_ORBIT, "Spark Orbit Radius", 0.8, 2.0, 0.05); - obs_properties_add_float_slider(props, MSQ_PROP_SPARK_ENERGY, "Spark Energy Response", 0.0, 1.5, 0.05); - obs_properties_add_float_slider(props, MSQ_PROP_SPARK_MIN_LEVEL, "Spark Min Level (0..1)", 0.0, 1.0, 0.05); -} - -static void magicsquare_rebuild_sparks(magicsquare_theme_data *d) -{ - if (!d) - return; - - d->sparks.clear(); - d->sparks.resize(d->spark_count); - - for (uint32_t i = 0; i < d->spark_count; ++i) { - float r0 = msq_pseudo_rand01(i * 11u + 3u); - float r1 = msq_pseudo_rand01(i * 23u + 7u); - float r2 = msq_pseudo_rand01(i * 41u + 13u); - float r3 = msq_pseudo_rand01(i * 59u + 17u); - - msq_spark s; - s.pos = r0; - s.offset = 0.3f + r1 * 0.7f; - s.maxLife = 0.7f + r2 * 1.8f; - s.life = r3 * s.maxLife; - s.speed = 0.2f + r1 * 1.2f; - - d->sparks[i] = s; - } -} - -static void magicsquare_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_core = (uint32_t)aw_get_int_default(settings, MSQ_PROP_COLOR_CORE, 0); - uint32_t col_ring = (uint32_t)aw_get_int_default(settings, MSQ_PROP_COLOR_RING, 0); - uint32_t col_spark = (uint32_t)aw_get_int_default(settings, MSQ_PROP_COLOR_SPARK, 0); - - if (col_core == 0) - col_core = 0xFF99DD; - if (col_ring == 0) - col_ring = 0x66CCFF; - if (col_spark == 0) - col_spark = 0xFFFFCC; - - s->color = col_ring; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"core", col_core}); - s->colors.push_back(audio_wave_named_color{"ring", col_ring}); - s->colors.push_back(audio_wave_named_color{"sparkles", col_spark}); - - int seg = aw_get_int_default(settings, MSQ_PROP_SEGMENTS, 120); - seg = std::clamp(seg, 32, 512); - - double visc = aw_get_float_default(settings, MSQ_PROP_VISCOSITY, 0.65f); - visc = std::clamp(visc, 0.0, 1.0); - - double noise = aw_get_float_default(settings, MSQ_PROP_NOISE, 0.3f); - noise = std::clamp(noise, 0.0, 1.0); - - int ring_thick = aw_get_int_default(settings, MSQ_PROP_RING_THICKNESS, 4); - ring_thick = std::clamp(ring_thick, 0, 10); - - double rot = aw_get_float_default(settings, MSQ_PROP_ROT_SPEED, 0.5f); - rot = std::clamp(rot, 0.0, 5.0); - - double core_size = aw_get_float_default(settings, MSQ_PROP_CORE_SIZE, 1.0f); - core_size = std::clamp(core_size, 0.0, 2.0); - - double ring_size = aw_get_float_default(settings, MSQ_PROP_RING_SIZE, 1.0f); - ring_size = std::clamp(ring_size, 0.0, 2.0); - - int spark_count = aw_get_int_default(settings, MSQ_PROP_SPARK_COUNT, 40); - spark_count = std::clamp(spark_count, 0, 200); - - int spark_len = aw_get_int_default(settings, MSQ_PROP_SPARK_LENGTH, 60); - spark_len = std::clamp(spark_len, 5, 200); - - double spark_orbit = aw_get_float_default(settings, MSQ_PROP_SPARK_ORBIT, 1.20f); - spark_orbit = std::clamp(spark_orbit, 0.8, 2.0); - - double spark_energy = aw_get_float_default(settings, MSQ_PROP_SPARK_ENERGY, 0.6f); - spark_energy = std::clamp(spark_energy, 0.0, 1.5); - - double spark_min_lvl = aw_get_float_default(settings, MSQ_PROP_SPARK_MIN_LEVEL, 0.25f); - spark_min_lvl = std::clamp(spark_min_lvl, 0.0, 1.0); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new magicsquare_theme_data{}; - s->theme_data = d; - } - - d->segments = (uint32_t)seg; - d->viscosity = (float)visc; - d->noise_amount = (float)noise; - d->ring_thickness = ring_thick; - d->rot_speed = (float)rot; - d->core_size = (float)core_size; - d->ring_size = (float)ring_size; - d->spark_count = (uint32_t)spark_count; - d->spark_length = (float)spark_len; - d->spark_orbit_mult = (float)spark_orbit; - d->spark_energy_resp = (float)spark_energy; - d->spark_min_level = (float)spark_min_lvl; - d->initialized = false; - - if (d->sparks.size() != d->spark_count) { - magicsquare_rebuild_sparks(d); - } - - if (s->frame_density < 80) - s->frame_density = 80; -} - -static void msq_square_param(float t, float half, float &x, float &y) -{ - float edge = t * 4.0f; - int side = (int)std::floor(edge); - float u = edge - (float)side; - - switch (side) { - case 0: - default: - x = -half + 2.0f * half * u; - y = -half; - break; - case 1: - x = half; - y = -half + 2.0f * half * u; - break; - case 2: - x = half - 2.0f * half * u; - y = half; - break; - case 3: - x = -half; - y = half - 2.0f * half * u; - break; - } -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void magicsquare_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - if (s->wave.empty()) - audio_wave_build_wave(s); - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new magicsquare_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float min_dim = std::min(w, h); - const float half_base = min_dim * 0.32f; - const float core_half = half_base * 0.6f * d->core_size; - - const float audio_extent = min_dim * 0.20f; - const float noise_extent = min_dim * 0.10f; - - const float orbit_base = half_base * d->spark_orbit_mult; - - const uint32_t segments = std::max(d->segments, 32u); - - const uint32_t col_core = audio_wave_get_color(s, 0, 0xFF99DD); - const uint32_t col_ring = audio_wave_get_color(s, 1, s->color); - const uint32_t col_spark = audio_wave_get_color(s, 2, 0xFFFFFF); - - std::vector amp(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const size_t idx = (size_t)(u * (float)(frames - 1)); - amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - float max_a = 0.0f; - for (float v : amp) - if (v > max_a) - max_a = v; - max_a = msq_clamp_float(max_a, 0.0f, 1.0f); - - std::vector amp_smooth(segments); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.25f; - for (uint32_t i = 1; i < segments; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - float wrap = amp_smooth[segments - 1]; - amp_smooth[0] = 0.5f * (amp_smooth[0] + wrap); - } - - if (d->prev_offset.size() != segments) { - d->prev_offset.assign(segments, 0.0f); - d->initialized = false; - } - std::vector offset(segments); - - const float base_alpha = 0.05f; - const float extra_alpha = 0.35f; - const float alpha_time = base_alpha + extra_alpha * (1.0f - d->viscosity); - - d->phase += d->rot_speed * (float)M_PI / 180.0f; - if (d->phase > 2.0f * (float)M_PI) - d->phase -= 2.0f * (float)M_PI; - - const int noise_harmonics = 2 + (int)std::round(d->noise_amount * 3.0f); - - std::vector base_x(segments), base_y(segments); - std::vector norm_x(segments), norm_y(segments); - - for (uint32_t i = 0; i < segments; ++i) { - const float t = (float)i / (float)segments; - float bx, by; - msq_square_param(t, half_base, bx, by); - base_x[i] = bx; - base_y[i] = by; - - float len = std::sqrt(bx * bx + by * by); - if (len <= 1e-6f) { - norm_x[i] = 0.0f; - norm_y[i] = -1.0f; - } else { - norm_x[i] = bx / len; - norm_y[i] = by / len; - } - } - - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - a = msq_clamp_float(a, 0.0f, 1.0f); - - float v = audio_wave_apply_curve(s, a); - - float target = v * audio_extent; - - const float angle = d->phase + (float)i * 0.15f; - float wobble = 0.0f; - for (int h2 = 1; h2 <= noise_harmonics; ++h2) { - float contrib = std::sin(angle * (float)h2 + (float)h2 * 0.7f); - wobble += contrib * (1.0f / (float)h2); - } - wobble /= (float)noise_harmonics; - target += wobble * d->noise_amount * noise_extent; - - target *= d->ring_size; - - if (!d->initialized) - d->prev_offset[i] = target; - - float off_s = d->prev_offset[i] + alpha_time * (target - d->prev_offset[i]); - d->prev_offset[i] = off_s; - offset[i] = off_s; - } - - d->initialized = true; - - std::vector pos_x(segments), pos_y(segments); - for (uint32_t i = 0; i < segments; ++i) { - pos_x[i] = base_x[i] + norm_x[i] * offset[i]; - pos_y[i] = base_y[i] + norm_y[i] * offset[i]; - } - - gs_matrix_push(); - - if (d->core_size > 0.0f && core_half > 0.0f) { - if (color_param) - audio_wave_set_solid_color(color_param, col_core); - - gs_render_start(true); - - float x0 = cx - core_half; - float y0 = cy - core_half; - float x1 = cx + core_half; - float y1 = cy + core_half; - - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y0); - gs_vertex2f(x1, y1); - - gs_vertex2f(x0, y0); - gs_vertex2f(x1, y1); - gs_vertex2f(x0, y1); - - gs_render_stop(GS_TRIS); - } - - if (d->ring_size > 0.0f && d->ring_thickness > 0) { - if (color_param) - audio_wave_set_solid_color(color_param, col_ring); - - int thickness = d->ring_thickness; - const float half = (float)(thickness - 1) * 0.5f; - - for (int t = 0; t < thickness; ++t) { - const float offset_line = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - - float px = pos_x[idx] + norm_x[idx] * offset_line; - float py = pos_y[idx] + norm_y[idx] * offset_line; - - gs_vertex2f(cx + px, cy + py); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_spark); - - if (d->sparks.size() != d->spark_count) - magicsquare_rebuild_sparks(d); - - const float energy_dt = 0.02f + max_a * 0.08f * d->spark_energy_resp; - const float base_len = d->spark_length; - - gs_render_start(true); - - for (uint32_t i = 0; i < d->sparks.size(); ++i) { - auto &sp = d->sparks[i]; - - float pos_norm = sp.pos; - pos_norm -= std::floor(pos_norm); - if (pos_norm < 0.0f) - pos_norm += 1.0f; - - uint32_t idx = (uint32_t)(pos_norm * (float)segments); - if (idx >= segments) - idx = segments - 1; - - float a_local = amp_smooth[idx]; - a_local = msq_clamp_float(a_local, 0.0f, 1.0f); - float v_local = audio_wave_apply_curve(s, a_local); - - bool active = (v_local >= d->spark_min_level); - - sp.life += energy_dt * (0.3f + v_local * 1.7f); - if (sp.life > sp.maxLife) { - uint32_t seed = i * 97u + 17u; - float r0 = msq_pseudo_rand01(seed); - float r1 = msq_pseudo_rand01(seed * 3u + 11u); - float r2 = msq_pseudo_rand01(seed * 5u + 23u); - - sp.pos = r0; - sp.offset = 0.3f + r1 * 0.7f; - sp.maxLife = 0.6f + r2 * 2.0f; - sp.life = 0.0f; - sp.speed = 0.3f + r1 * 1.5f; - } - - const float speed_scale = (0.05f + v_local * d->spark_energy_resp); - sp.pos += sp.speed * energy_dt * speed_scale; - sp.pos -= std::floor(sp.pos); - - if (!active) - continue; - - float phase = msq_clamp_float(sp.life / sp.maxLife, 0.0f, 1.0f); - float life_intensity = (phase < 0.5f) ? (phase * 2.0f) : ((1.0f - phase) * 2.0f); - life_intensity = msq_clamp_float(life_intensity, 0.0f, 1.0f); - - float intensity = life_intensity * (0.3f + 0.7f * v_local); - intensity = msq_clamp_float(intensity, 0.0f, 1.0f); - - float len = base_len * (0.3f + 0.7f * intensity); - - uint32_t idx2 = (uint32_t)(sp.pos * (float)segments); - if (idx2 >= segments) - idx2 = segments - 1; - - float px_rel = pos_x[idx2]; - float py_rel = pos_y[idx2]; - - float base_dist = std::sqrt(px_rel * px_rel + py_rel * py_rel); - if (base_dist < 1e-4f) - base_dist = orbit_base; - - float orbit_dist = base_dist + sp.offset * orbit_base * 0.4f; - - float nx = px_rel / base_dist; - float ny = py_rel / base_dist; - - float sx = cx + nx * orbit_dist; - float sy = cy + ny * orbit_dist; - - float ex = sx + nx * len; - float ey = sy + ny * len; - - gs_vertex2f(sx, sy); - gs_vertex2f(ex, ey); - } - - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void magicsquare_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_magicsquare_theme = {k_theme_id_magicsquare, - k_theme_name_magicsquare, - magicsquare_theme_add_properties, - magicsquare_theme_update, - magicsquare_theme_draw, - magicsquare_theme_destroy_data, - nullptr}; - -void audio_wave_register_magicsquare_theme() -{ - audio_wave_register_theme(&k_magicsquare_theme); -} diff --git a/src/themes/theme-magicsquare.hpp b/src/themes/theme-magicsquare.hpp deleted file mode 100644 index 6767e0c..0000000 --- a/src/themes/theme-magicsquare.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_magicsquare_theme(); diff --git a/src/themes/theme-musicmagic.cpp b/src/themes/theme-musicmagic.cpp deleted file mode 100644 index 453942b..0000000 --- a/src/themes/theme-musicmagic.cpp +++ /dev/null @@ -1,500 +0,0 @@ -#include "theme-musicmagic.hpp" - -#include -#include -#include -#include - -#include - -#ifndef UNUSED_PARAMETER -#define UNUSED_PARAMETER(x) (void)(x) -#endif - -static const char *k_theme_id_musicmagic = "music_magic"; -static const char *k_theme_name_musicmagic = "Music Magic Sparkles"; - -static const char *MM_PROP_COLOR_CORE = "mm_color_core"; -static const char *MM_PROP_COLOR_RING = "mm_color_ring"; -static const char *MM_PROP_COLOR_SPARK = "mm_color_spark"; - -static const char *MM_PROP_SEGMENTS = "mm_segments"; -static const char *MM_PROP_VISCOSITY = "mm_viscosity"; -static const char *MM_PROP_NOISE = "mm_noise"; -static const char *MM_PROP_RING_THICKNESS = "mm_ring_thickness"; -static const char *MM_PROP_ROT_SPEED = "mm_rotation_speed"; - -static const char *MM_PROP_CORE_SIZE = "mm_core_size"; -static const char *MM_PROP_RING_SIZE = "mm_ring_size"; - -static const char *MM_PROP_SPARK_COUNT = "mm_spark_count"; -static const char *MM_PROP_SPARK_LENGTH = "mm_spark_length"; -static const char *MM_PROP_SPARK_ORBIT = "mm_spark_orbit"; -static const char *MM_PROP_SPARK_ENERGY = "mm_spark_energy_response"; -static const char *MM_PROP_SPARK_MIN_LEVEL = "mm_spark_min_level"; - -// ───────────────────────────────────────────── -// Helpers -// ───────────────────────────────────────────── - -static float mm_clamp_float(float v, float lo, float hi) -{ - if (v < lo) - return lo; - if (v > hi) - return hi; - return v; -} - -static float mm_pseudo_rand01(uint32_t seed) -{ - uint32_t x = seed; - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - return (float)(x & 0x00FFFFFFu) / (float)0x01000000u; -} - -struct musicmagic_spark { - float angle = 0.0f; - float radiusOff = 0.0f; - float life = 0.0f; - float maxLife = 1.0f; - float speed = 0.0f; -}; - -struct musicmagic_theme_data { - std::vector prev_r; - bool initialized = false; - - uint32_t segments = 120; - float viscosity = 0.65f; - float noise_amount = 0.3f; - int ring_thickness = 4; - float rot_speed = 0.5f; - float phase = 0.0f; - - float core_size = 1.0f; - float ring_size = 1.0f; - - uint32_t spark_count = 40; - float spark_length = 60.0f; - float spark_orbit_mult = 1.20f; - float spark_energy_resp = 0.6f; - float spark_min_level = 0.25f; - - std::vector sparks; -}; - -// ───────────────────────────────────────────── -// Properties -// ───────────────────────────────────────────── - -static void musicmagic_theme_add_properties(obs_properties_t *props) -{ - obs_properties_add_color(props, MM_PROP_COLOR_CORE, "Core Glow Color"); - obs_properties_add_color(props, MM_PROP_COLOR_RING, "Ring Color"); - obs_properties_add_color(props, MM_PROP_COLOR_SPARK, "Sparkle Color"); - obs_properties_add_int_slider(props, MM_PROP_SEGMENTS, "Shape Resolution", 32, 512, 8); - obs_properties_add_float_slider(props, MM_PROP_VISCOSITY, "Viscosity (Smoothness)", 0.0, 1.0, 0.05); - obs_properties_add_float_slider(props, MM_PROP_NOISE, "Organic Wobble Amount", 0.0, 1.0, 0.05); - obs_properties_add_int_slider(props, MM_PROP_RING_THICKNESS, "Ring Thickness", 0, 10, 1); - obs_properties_add_float_slider(props, MM_PROP_ROT_SPEED, "Rotation Speed", 0.0, 5.0, 0.1); - obs_properties_add_float_slider(props, MM_PROP_CORE_SIZE, "Core Size", 0.0, 2.0, 0.05); - obs_properties_add_float_slider(props, MM_PROP_RING_SIZE, "Ring Size", 0.0, 2.0, 0.05); - obs_properties_add_int_slider(props, MM_PROP_SPARK_COUNT, "Spark Count", 0, 200, 2); - obs_properties_add_int_slider(props, MM_PROP_SPARK_LENGTH, "Spark Length (px)", 5, 200, 5); - obs_properties_add_float_slider(props, MM_PROP_SPARK_ORBIT, "Spark Orbit Radius", 0.8, 2.0, 0.05); - obs_properties_add_float_slider(props, MM_PROP_SPARK_ENERGY, "Spark Energy Response", 0.0, 1.5, 0.05); - obs_properties_add_float_slider(props, MM_PROP_SPARK_MIN_LEVEL, "Spark Min Level (0..1)", 0.0, 1.0, 0.05); -} - -static void musicmagic_theme_rebuild_sparks(musicmagic_theme_data *d) -{ - if (!d) - return; - - d->sparks.clear(); - d->sparks.resize(d->spark_count); - - for (uint32_t i = 0; i < d->spark_count; ++i) { - float r0 = mm_pseudo_rand01(i * 11u + 3u); - float r1 = mm_pseudo_rand01(i * 23u + 7u); - float r2 = mm_pseudo_rand01(i * 41u + 13u); - float r3 = mm_pseudo_rand01(i * 59u + 17u); - - musicmagic_spark s; - s.angle = r0 * 2.0f * (float)M_PI; - s.radiusOff = (r1 * 0.35f + 0.05f); - s.maxLife = 0.7f + r2 * 1.8f; - s.life = r3 * s.maxLife; - s.speed = (0.2f + r1 * 1.2f); - - d->sparks[i] = s; - } -} - -static void musicmagic_theme_update(audio_wave_source *s, obs_data_t *settings) -{ - if (!s || !settings) - return; - - s->theme_style_id = "default"; - - uint32_t col_core = (uint32_t)aw_get_int_default(settings, MM_PROP_COLOR_CORE, 0); - uint32_t col_ring = (uint32_t)aw_get_int_default(settings, MM_PROP_COLOR_RING, 0); - uint32_t col_spark = (uint32_t)aw_get_int_default(settings, MM_PROP_COLOR_SPARK, 0); - - if (col_core == 0) - col_core = 0xFF66CC; - if (col_ring == 0) - col_ring = 0x7F7FFF; - if (col_spark == 0) - col_spark = 0xFFFFCC; - - s->color = col_ring; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"core", col_core}); - s->colors.push_back(audio_wave_named_color{"ring", col_ring}); - s->colors.push_back(audio_wave_named_color{"sparkles", col_spark}); - - int seg = aw_get_int_default(settings, MM_PROP_SEGMENTS, 120); - seg = std::clamp(seg, 32, 512); - - double visc = aw_get_float_default(settings, MM_PROP_VISCOSITY, 0.65f); - visc = std::clamp(visc, 0.0, 1.0); - - double noise = aw_get_float_default(settings, MM_PROP_NOISE, 0.3f); - noise = std::clamp(noise, 0.0, 1.0); - - int ring_thick = aw_get_int_default(settings, MM_PROP_RING_THICKNESS, 4); - ring_thick = std::clamp(ring_thick, 0, 10); - - double rot = aw_get_float_default(settings, MM_PROP_ROT_SPEED, 0.5f); - rot = std::clamp(rot, 0.0, 5.0); - - double core_size = aw_get_float_default(settings, MM_PROP_CORE_SIZE, 1.0f); - core_size = std::clamp(core_size, 0.0, 2.0); - - double ring_size = aw_get_float_default(settings, MM_PROP_RING_SIZE, 1.0f); - ring_size = std::clamp(ring_size, 0.0, 2.0); - - int spark_count = aw_get_int_default(settings, MM_PROP_SPARK_COUNT, 40); - spark_count = std::clamp(spark_count, 0, 200); - - int spark_len = aw_get_int_default(settings, MM_PROP_SPARK_LENGTH, 60); - spark_len = std::clamp(spark_len, 5, 200); - - double spark_orbit = aw_get_float_default(settings, MM_PROP_SPARK_ORBIT, 1.20f); - spark_orbit = std::clamp(spark_orbit, 0.8, 2.0); - - double spark_energy = aw_get_float_default(settings, MM_PROP_SPARK_ENERGY, 0.6f); - spark_energy = std::clamp(spark_energy, 0.0, 1.5); - - double spark_min_level = aw_get_float_default(settings, MM_PROP_SPARK_MIN_LEVEL, 0.25f); - spark_min_level = std::clamp(spark_min_level, 0.0, 1.0); - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new musicmagic_theme_data{}; - s->theme_data = d; - } - - d->segments = (uint32_t)seg; - d->viscosity = (float)visc; - d->noise_amount = (float)noise; - d->ring_thickness = ring_thick; - d->rot_speed = (float)rot; - d->core_size = (float)core_size; - d->ring_size = (float)ring_size; - d->spark_count = (uint32_t)spark_count; - d->spark_length = (float)spark_len; - d->spark_orbit_mult = (float)spark_orbit; - d->spark_energy_resp = (float)spark_energy; - d->spark_min_level = (float)spark_min_level; - d->initialized = false; - - if (d->sparks.size() != d->spark_count) { - musicmagic_theme_rebuild_sparks(d); - } - - if (s->frame_density < 80) - s->frame_density = 80; -} - -// ───────────────────────────────────────────── -// Drawing -// ───────────────────────────────────────────── - -static void musicmagic_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) -{ - if (!s || s->width <= 0 || s->height <= 0) - return; - - if (s->wave.empty()) - audio_wave_build_wave(s); - - const size_t frames = s->wave.size(); - if (frames < 2) - return; - - auto *d = static_cast(s->theme_data); - if (!d) { - d = new musicmagic_theme_data{}; - s->theme_data = d; - } - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - const float min_dim = std::min(w, h); - const float base_radius0 = min_dim * 0.20f; - const float audio_radius0 = min_dim * 0.22f; - const float noise_radius0 = min_dim * 0.12f; - - const float base_radius = base_radius0 * d->ring_size; - const float audio_radius = audio_radius0 * d->ring_size; - const float noise_radius = noise_radius0 * d->ring_size; - - const float core_radius = base_radius0 * 0.45f * d->core_size; - - const float orbit_base = base_radius0 * d->spark_orbit_mult; - - const uint32_t segments = std::max(d->segments, 32u); - - const uint32_t col_core = audio_wave_get_color(s, 0, 0xFF66CC); - const uint32_t col_ring = audio_wave_get_color(s, 1, s->color); - const uint32_t col_spark = audio_wave_get_color(s, 2, 0xFFFFFF); - - std::vector amp(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float u = (float)i / (float)segments; - const size_t idx = (size_t)(u * (float)(frames - 1)); - amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; - } - - float max_a = 0.0f; - for (float v : amp) - if (v > max_a) - max_a = v; - max_a = mm_clamp_float(max_a, 0.0f, 1.0f); - - std::vector amp_smooth(segments); - if (!amp.empty()) { - float prev = amp[0]; - amp_smooth[0] = prev; - const float alpha_space = 0.25f; - for (uint32_t i = 1; i < segments; ++i) { - prev = prev + alpha_space * (amp[i] - prev); - amp_smooth[i] = prev; - } - float wrap = amp_smooth[segments - 1]; - amp_smooth[0] = 0.5f * (amp_smooth[0] + wrap); - } - - if (d->prev_r.size() != segments) { - d->prev_r.assign(segments, base_radius); - d->initialized = false; - } - std::vector radius(segments); - - const float base_alpha = 0.05f; - const float extra_alpha = 0.35f; - const float alpha_time = base_alpha + extra_alpha * (1.0f - d->viscosity); - - d->phase += d->rot_speed * (float)M_PI / 180.0f; - if (d->phase > 2.0f * (float)M_PI) - d->phase -= 2.0f * (float)M_PI; - - const int noise_harmonics = 2 + (int)std::round(d->noise_amount * 3.0f); - - for (uint32_t i = 0; i < segments; ++i) { - float a = amp_smooth[i]; - a = mm_clamp_float(a, 0.0f, 1.0f); - - float v = audio_wave_apply_curve(s, a); - - float r_target = base_radius + v * audio_radius; - - const float angle = ((float)i / (float)segments) * 2.0f * (float)M_PI; - float wobble = 0.0f; - const float n_amp = d->noise_amount; - - for (int h2 = 1; h2 <= noise_harmonics; ++h2) { - float contrib = std::sin((float)h2 * angle + d->phase + (float)h2 * 0.7f); - wobble += contrib * (1.0f / (float)h2); - } - wobble /= (float)noise_harmonics; - - r_target += wobble * n_amp * noise_radius; - - if (!d->initialized) - d->prev_r[i] = r_target; - - float r_s = d->prev_r[i] + alpha_time * (r_target - d->prev_r[i]); - d->prev_r[i] = r_s; - radius[i] = r_s; - } - - d->initialized = true; - - std::vector cos_t(segments), sin_t(segments); - for (uint32_t i = 0; i < segments; ++i) { - const float t = ((float)i / (float)segments) * 2.0f * (float)M_PI; - cos_t[i] = std::cos(t); - sin_t[i] = std::sin(t); - } - - std::vector x(segments), y(segments); - for (uint32_t i = 0; i < segments; ++i) { - x[i] = cx + cos_t[i] * radius[i]; - y[i] = cy + sin_t[i] * radius[i]; - } - - gs_matrix_push(); - - if (d->core_size > 0.0f && core_radius > 0.0f) { - if (color_param) - audio_wave_set_solid_color(color_param, col_core); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - uint32_t next = (i + 1) % segments; - - float rx0 = cx + cos_t[i] * core_radius; - float ry0 = cy + sin_t[i] * core_radius; - float rx1 = cx + cos_t[next] * core_radius; - float ry1 = cy + sin_t[next] * core_radius; - - gs_vertex2f(cx, cy); - gs_vertex2f(rx0, ry0); - gs_vertex2f(rx1, ry1); - } - gs_render_stop(GS_TRIS); - } - - if (d->ring_size > 0.0f && d->ring_thickness > 0 && base_radius > 0.0f) { - if (color_param) - audio_wave_set_solid_color(color_param, col_ring); - - int thickness = d->ring_thickness; - const float half = (float)(thickness - 1) * 0.5f; - - for (int t = 0; t < thickness; ++t) { - const float offset = (float)t - half; - - gs_render_start(true); - for (uint32_t i = 0; i <= segments; ++i) { - uint32_t idx = (i == segments) ? 0 : i; - - float r = radius[idx] + offset; - if (r < 0.0f) - r = 0.0f; - - float px = cx + cos_t[idx] * r; - float py = cy + sin_t[idx] * r; - - gs_vertex2f(px, py); - } - gs_render_stop(GS_LINESTRIP); - } - } - - if (color_param) - audio_wave_set_solid_color(color_param, col_spark); - - const float energy_dt = 0.02f + max_a * 0.08f * d->spark_energy_resp; - const float base_len = d->spark_length; - - if (d->sparks.size() != d->spark_count) - musicmagic_theme_rebuild_sparks(d); - - gs_render_start(true); - for (uint32_t i = 0; i < d->sparks.size(); ++i) { - auto &sp = d->sparks[i]; - - float norm_angle = sp.angle / (2.0f * (float)M_PI); - norm_angle -= std::floor(norm_angle); - uint32_t idx = (uint32_t)(norm_angle * (float)segments); - if (idx >= segments) - idx = segments - 1; - - float a_local = amp_smooth[idx]; - a_local = mm_clamp_float(a_local, 0.0f, 1.0f); - float v_local = audio_wave_apply_curve(s, a_local); - - bool active = (v_local >= d->spark_min_level); - - sp.life += energy_dt * (0.3f + v_local * 1.7f); - if (sp.life > sp.maxLife) { - uint32_t seed = i * 97u + 17u; - float r0 = mm_pseudo_rand01(seed); - float r1 = mm_pseudo_rand01(seed * 3u + 11u); - float r2 = mm_pseudo_rand01(seed * 5u + 23u); - - sp.angle = r0 * 2.0f * (float)M_PI; - sp.radiusOff = 0.4f + r1 * 0.5f; - sp.maxLife = 0.6f + r2 * 2.0f; - sp.life = 0.0f; - sp.speed = 0.3f + r1 * 1.5f; - } - - const float speed_scale = (0.05f + v_local * d->spark_energy_resp); - sp.angle += sp.speed * energy_dt * speed_scale; - - if (!active) - continue; - - float phase = (sp.life / sp.maxLife); - phase = mm_clamp_float(phase, 0.0f, 1.0f); - float life_intensity = (phase < 0.5f) ? (phase * 2.0f) : ((1.0f - phase) * 2.0f); - life_intensity = mm_clamp_float(life_intensity, 0.0f, 1.0f); - - float intensity = life_intensity * (0.3f + 0.7f * v_local); - intensity = mm_clamp_float(intensity, 0.0f, 1.0f); - - float len = base_len * (0.3f + 0.7f * intensity); - float r_orbit = orbit_base * (1.0f + 0.4f * sp.radiusOff); - - float cs = std::cos(sp.angle); - float sn = std::sin(sp.angle); - - float sx = cx + cs * r_orbit; - float sy = cy + sn * r_orbit; - - float ex = sx + cs * len; - float ey = sy + sn * len; - - gs_vertex2f(sx, sy); - gs_vertex2f(ex, ey); - } - gs_render_stop(GS_LINES); - - gs_matrix_pop(); -} - -static void musicmagic_theme_destroy_data(audio_wave_source *s) -{ - if (!s || !s->theme_data) - return; - - auto *d = static_cast(s->theme_data); - delete d; - s->theme_data = nullptr; -} - -static const audio_wave_theme k_musicmagic_theme = {k_theme_id_musicmagic, - k_theme_name_musicmagic, - musicmagic_theme_add_properties, - musicmagic_theme_update, - musicmagic_theme_draw, - musicmagic_theme_destroy_data, - nullptr}; - -void audio_wave_register_musicmagic_theme() -{ - audio_wave_register_theme(&k_musicmagic_theme); -} diff --git a/src/themes/theme-musicmagic.hpp b/src/themes/theme-musicmagic.hpp deleted file mode 100644 index a6463fc..0000000 --- a/src/themes/theme-musicmagic.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include "audio-wave.hpp" - -void audio_wave_register_musicmagic_theme(); diff --git a/src/themes/theme-rounded-bars.cpp b/src/themes/theme-rounded-bars.cpp index 7a260af..5d72e11 100644 --- a/src/themes/theme-rounded-bars.cpp +++ b/src/themes/theme-rounded-bars.cpp @@ -17,14 +17,9 @@ static const char *k_theme_name_rounded_bars = "Rounded Wobble Bars"; // Property keys // ───────────────────────────────────────────── -static const char *RB_PROP_COLOR_BAR = "rb_color_bar"; - -static const char *RB_PROP_DB_FLOOR = "rb_db_floor"; -static const char *RB_PROP_DB_TARGET = "rb_db_target"; -static const char *RB_PROP_DB_REACT = "rb_db_react"; // from which dB to start reacting static const char *RB_PROP_BAR_COUNT = "rb_bar_count"; static const char *RB_PROP_WOBBLE_INT = "rb_wobble_intensity"; -static const char *RB_PROP_MIRROR_VERT = "rb_mirror_vertical"; // true vertical mirroring +static const char *RB_PROP_MIRROR_VERT = "rb_mirror_vertical"; // ───────────────────────────────────────────── // Helpers @@ -72,21 +67,17 @@ static void rb_draw_rounded_bar(float cx, float y_bottom, float height, float ba const float y_rect_top = y_top_center; const float y_rect_bottom = y_bottom_center; - // Rectangle body if (y_rect_bottom > y_rect_top) { gs_render_start(true); - // Tri 1 gs_vertex2f(left, y_rect_bottom); gs_vertex2f(right, y_rect_bottom); gs_vertex2f(right, y_rect_top); - // Tri 2 gs_vertex2f(left, y_rect_bottom); gs_vertex2f(right, y_rect_top); gs_vertex2f(left, y_rect_top); gs_render_stop(GS_TRIS); } - // Top cap (semicircle) { const int segments = std::max(capSegments, 4); const float step = (float)M_PI / (float)segments; @@ -112,7 +103,6 @@ static void rb_draw_rounded_bar(float cx, float y_bottom, float height, float ba gs_render_stop(GS_TRIS); } - // Bottom cap (semicircle) { const int segments = std::max(capSegments, 4); const float step = (float)M_PI / (float)segments; @@ -148,21 +138,20 @@ static void rb_draw_rounded_bar_half_up(float cx, float centerY, float halfHeigh return; const float radius = barWidth * 0.5f; - const float minHeight = radius; // at least enough for a cap + const float minHeight = radius; if (halfHeight < minHeight) halfHeight = minHeight; - const float y_bottom = centerY; // flat edge at center + const float y_bottom = centerY; const float y_top = centerY - halfHeight; const float left = cx - radius; const float right = cx + radius; - const float y_cap_center = y_top + radius; // cap center + const float y_cap_center = y_top + radius; const float y_rect_top = y_cap_center; const float y_rect_bottom = y_bottom; - // Rectangle body (from cap center down to center line) if (y_rect_bottom > y_rect_top) { gs_render_start(true); gs_vertex2f(left, y_rect_bottom); @@ -175,7 +164,6 @@ static void rb_draw_rounded_bar_half_up(float cx, float centerY, float halfHeigh gs_render_stop(GS_TRIS); } - // Top cap (rounded) { const int segments = std::max(capSegments, 4); const float step = (float)M_PI / (float)segments; @@ -215,7 +203,7 @@ static void rb_draw_rounded_bar_half_down(float cx, float centerY, float halfHei if (halfHeight < minHeight) halfHeight = minHeight; - const float y_top = centerY; // flat edge at center + const float y_top = centerY; const float y_bottom = centerY + halfHeight; const float left = cx - radius; @@ -225,7 +213,6 @@ static void rb_draw_rounded_bar_half_down(float cx, float centerY, float halfHei const float y_rect_top = y_top; const float y_rect_bottom = y_cap_center; - // Rectangle body (from center line down to cap center) if (y_rect_bottom > y_rect_top) { gs_render_start(true); gs_vertex2f(left, y_rect_bottom); @@ -238,7 +225,6 @@ static void rb_draw_rounded_bar_half_down(float cx, float centerY, float halfHei gs_render_stop(GS_TRIS); } - // Bottom cap (rounded) { const int segments = std::max(capSegments, 4); const float step = (float)M_PI / (float)segments; @@ -270,17 +256,12 @@ static void rb_draw_rounded_bar_half_down(float cx, float centerY, float halfHei // ───────────────────────────────────────────── struct rounded_bars_theme_data { - std::vector value; // current displayed extra half-height - std::vector velocity; // spring velocity per bar + std::vector value; + std::vector velocity; bool initialized = false; - float db_floor = -50.0f; - float db_target = -10.0f; - float db_react = -40.0f; // from which dB to start reacting - uint32_t bars = 32; - // mapped from wobble intensity slider float wobble_stiffness = 0.20f; float wobble_damping = 0.80f; @@ -293,12 +274,6 @@ struct rounded_bars_theme_data { static void rounded_bars_theme_add_properties(obs_properties_t *props) { - obs_properties_add_color(props, RB_PROP_COLOR_BAR, "Bar Color"); - - obs_properties_add_int_slider(props, RB_PROP_DB_FLOOR, "Floor dB (silence)", -60, 0, 1); - obs_properties_add_int_slider(props, RB_PROP_DB_REACT, "React from (dB)", -60, 0, 1); - obs_properties_add_int_slider(props, RB_PROP_DB_TARGET, "Full Extra Height dB", -60, 0, 1); - obs_properties_add_int_slider(props, RB_PROP_BAR_COUNT, "Bars", 8, 128, 1); obs_properties_add_int_slider(props, RB_PROP_WOBBLE_INT, "Wobble Intensity", 0, 100, 1); @@ -313,55 +288,23 @@ static void rounded_bars_theme_update(audio_wave_source *s, obs_data_t *settings s->theme_style_id = "default"; - uint32_t col_bar = (uint32_t)aw_get_int_default(settings, RB_PROP_COLOR_BAR, 0); - if (col_bar == 0) - col_bar = 0x00FFCC; // default teal - - s->color = col_bar; - - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"bar", col_bar}); - - int db_floor = aw_get_int_default(settings, RB_PROP_DB_FLOOR, -50); - int db_target = aw_get_int_default(settings, RB_PROP_DB_TARGET, -10); - int db_react = aw_get_int_default(settings, RB_PROP_DB_REACT, -40); - - db_floor = std::clamp(db_floor, -60, 0); - db_target = std::clamp(db_target, -60, 0); - db_react = std::clamp(db_react, -60, 0); - - // ensure sensible ordering: floor <= react < target - if (db_react < db_floor) - db_react = db_floor; - if (db_target <= db_react) - db_target = db_react + 1; - - int bars = aw_get_int_default(settings, RB_PROP_BAR_COUNT, 32); - bars = std::clamp(bars, 8, 128); - - int wobble_int = aw_get_int_default(settings, RB_PROP_WOBBLE_INT, 60); - wobble_int = std::clamp(wobble_int, 0, 100); - - bool mirror_vertical = obs_data_get_bool(settings, RB_PROP_MIRROR_VERT); - - // map wobble_int -> spring params - // more intensity -> stronger spring & less damping (more wobble) - float stiffness = 0.10f + (float)wobble_int * 0.004f; // ~0.10 .. 0.50 - float damping = 0.95f - (float)wobble_int * 0.004f; // ~0.95 .. 0.55 - auto *d = static_cast(s->theme_data); if (!d) { d = new rounded_bars_theme_data{}; s->theme_data = d; } - d->db_floor = (float)db_floor; - d->db_target = (float)db_target; - d->db_react = (float)db_react; - d->bars = (uint32_t)bars; - d->wobble_stiffness = stiffness; - d->wobble_damping = damping; - d->mirror_vertical = mirror_vertical; + const int bars_i = (int)obs_data_get_int(settings, RB_PROP_BAR_COUNT); + const int wobble_i = (int)obs_data_get_int(settings, RB_PROP_WOBBLE_INT); + const bool mirror_v = obs_data_get_bool(settings, RB_PROP_MIRROR_VERT); + + const int bars_clamped = std::max(8, std::min(128, bars_i)); + d->bars = (uint32_t)bars_clamped; + d->mirror_vertical = mirror_v; + + const float t = std::clamp((float)wobble_i / 100.0f, 0.0f, 1.0f); + d->wobble_stiffness = 0.35f + (0.08f - 0.35f) * t; + d->wobble_damping = 0.55f + (0.92f - 0.55f) * t; d->initialized = false; } @@ -390,19 +333,15 @@ static void rounded_bars_theme_draw(audio_wave_source *s, gs_eparam_t *color_par const uint32_t bars = std::max(d->bars, 8u); - const uint32_t col_bar = audio_wave_get_color(s, 0, s->color); - - // ensure buffers - if (d->value.size() != bars) { + if (d->value.size() != bars) { d->value.assign(bars, 0.0f); d->velocity.assign(bars, 0.0f); d->initialized = false; } - // layout: centered horizontally const float marginX = w * 0.05f; const float usableW = w - marginX * 2.0f; - const float gapRatio = 0.20f; // 20% of slot width as gap + const float gapRatio = 0.20f; const float slotW = usableW / (float)bars; float barWidth = slotW * (1.0f - gapRatio); @@ -413,18 +352,14 @@ static void rounded_bars_theme_draw(audio_wave_source *s, gs_eparam_t *color_par const float totalW = (float)bars * (barWidth + gap) - gap; const float startX = (w - totalW) * 0.5f; - // vertically centered system const float centerY = h * 0.5f; - const float maxHalfHeight = h * 0.4f; // max extension from center per side - - // base half-height (always visible) - const float baseFraction = 0.20f; // 20% of maxHalfHeight as base + const float maxHalfHeight = h * 0.4f; + const float baseFraction = 0.20f; const float baseHalfHeight = maxHalfHeight * baseFraction; const float maxExtraHalf = maxHalfHeight - baseHalfHeight; gs_matrix_push(); - // Per-bar target extra half-height from audio std::vector targetExtraHalf(bars); for (uint32_t i = 0; i < bars; ++i) { @@ -436,25 +371,23 @@ static void rounded_bars_theme_draw(audio_wave_source *s, gs_eparam_t *color_par a = 0.0f; float db = rb_db_from_amp(a); - float extraNorm = 0.0f; + const float react_db = s->react_db; + const float peak_db = s->peak_db; - // Only start reacting above db_react - if (db > d->db_react) { - float t = (db - d->db_react) / (d->db_target - d->db_react + 1e-3f); + if (db > react_db) { + float t = (db - react_db) / (peak_db - react_db + 1e-3f); t = rb_clamp01(t); extraNorm = t; } - // Optional global curve (same as other themes) extraNorm = audio_wave_apply_curve(s, extraNorm); targetExtraHalf[i] = extraNorm * maxExtraHalf; } - // Spring / wobble update on extra half-height for (uint32_t i = 0; i < bars; ++i) { - float value = d->value[i]; // extra half-height + float value = d->value[i]; float vel = d->velocity[i]; float target = targetExtraHalf[i]; @@ -467,7 +400,6 @@ static void rounded_bars_theme_draw(audio_wave_source *s, gs_eparam_t *color_par vel = vel * d->wobble_damping + acc; value += vel; - // keep in range if (value < 0.0f) value = 0.0f; if (value > maxExtraHalf) @@ -479,29 +411,25 @@ static void rounded_bars_theme_draw(audio_wave_source *s, gs_eparam_t *color_par d->initialized = true; - // Draw bars (base + extra), centered, optional true vertical mirror - if (color_param) - audio_wave_set_solid_color(color_param, col_bar); - const int capSegments = 12; for (uint32_t i = 0; i < bars; ++i) { + if (color_param) { + const float tcol = (bars <= 1) ? 0.0f : ((float)i / (float)(bars - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + } const float centerX = startX + (float)i * (barWidth + gap) + barWidth * 0.5f; const float extraHalf = d->value[i]; - - // base + extra for each side from center const float halfHeightSide = baseHalfHeight + extraHalf; if (halfHeightSide <= 0.5f) continue; if (!d->mirror_vertical) { - // Non-mirrored: single bar extending UP from center with both ends rounded const float upperBottomY = centerY; const float fullHeight = halfHeightSide; rb_draw_rounded_bar(centerX, upperBottomY, fullHeight, barWidth, capSegments); } else { - // Mirrored: upper half and lower half, both sharing a flat edge at center rb_draw_rounded_bar_half_up(centerX, centerY, halfHeightSide, barWidth, capSegments); rb_draw_rounded_bar_half_down(centerX, centerY, halfHeightSide, barWidth, capSegments); } diff --git a/src/themes/theme-square.cpp b/src/themes/theme-square.cpp index 65e63d5..c686004 100644 --- a/src/themes/theme-square.cpp +++ b/src/themes/theme-square.cpp @@ -10,8 +10,20 @@ static const char *k_theme_id_square = "square"; static const char *k_theme_name_square = "Square"; static const char *SQUARE_PROP_STYLE = "square_style"; -static const char *SQUARE_PROP_COLOR = "square_color"; static const char *SQUARE_PROP_MIRROR = "square_mirror"; +static const char *P_DENSITY = "shape_density"; + +static bool square_style_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) +{ + const char *style_id = obs_data_get_string(settings, SQUARE_PROP_STYLE); + const bool is_rays = (style_id && strcmp(style_id, "rays") == 0); + + obs_property_t *mirror = obs_properties_get(props, SQUARE_PROP_MIRROR); + if (mirror) + obs_property_set_visible(mirror, is_rays); + + return true; +} static void square_theme_add_properties(obs_properties_t *props) { @@ -20,9 +32,12 @@ static void square_theme_add_properties(obs_properties_t *props) obs_property_list_add_string(style, "Orbit", "orbit"); obs_property_list_add_string(style, "Rays", "rays"); + obs_property_set_modified_callback(style, square_style_modified); + + obs_property_t *mirror = obs_properties_add_bool(props, SQUARE_PROP_MIRROR, "Double-sided rays"); + obs_property_set_visible(mirror, false); + obs_properties_add_int_slider(props, P_DENSITY, "Shape Density (%)", 10, 300, 5); - obs_properties_add_color(props, SQUARE_PROP_COLOR, "Color"); - obs_properties_add_bool(props, SQUARE_PROP_MIRROR, "Double-sided rays"); } static void square_theme_update(audio_wave_source *s, obs_data_t *settings) @@ -36,19 +51,12 @@ static void square_theme_update(audio_wave_source *s, obs_data_t *settings) s->theme_style_id = style_id; - uint32_t color = (uint32_t)aw_get_int_default(settings, SQUARE_PROP_COLOR, 0); - if (color == 0) - color = 0x00FFCC; - - s->color = color; + int density = aw_get_int_default(settings, P_DENSITY, 120); + density = std::clamp(density, 10, 300); + s->frame_density = density; - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"square", color}); - if (s->frame_density < 40) - s->frame_density = 40; - - s->mirror = obs_data_get_bool(settings, SQUARE_PROP_MIRROR); + s->mirror = (strcmp(style_id, "rays") == 0) ? obs_data_get_bool(settings, SQUARE_PROP_MIRROR) : false; } // ───────────────────────────────────────────── @@ -181,42 +189,82 @@ static void draw_square_orbit(audio_wave_source *s, gs_eparam_t *color_param) auto get_amp = [&](uint32_t i) -> float { return amp_smooth[i]; }; - - const uint32_t sq_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, sq_color); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - const float v_raw = get_amp(i); - const float v = audio_wave_apply_curve(s, v_raw); - const float len = v * max_len; - - const float w = (float)s->width; - const float h = (float)s->height; - const float cx = w * 0.5f; - const float cy = h * 0.5f; - - float x = base_x[i]; - float y = base_y[i]; - - float dx = x - cx; - float dy = y - cy; - float l = std::sqrt(dx * dx + dy * dy); - if (l > 1e-4f) { - dx /= l; - dy /= l; - } else { - dx = 0.0f; - dy = -1.0f; + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const float v_raw = get_amp(i); + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + + const float w = (float)s->width; + const float h = (float)s->height; + const float cx = w * 0.5f; + const float cy = h * 0.5f; + + float x = base_x[i]; + float y = base_y[i]; + + float dx = x - cx; + float dy = y - cy; + float l = std::sqrt(dx * dx + dy * dy); + if (l > 1e-4f) { + dx /= l; + dy /= l; + } else { + dx = 0.0f; + dy = -1.0f; + } + + const float x2 = x + dx * len; + const float y2 = y + dy * len; + + gs_vertex2f(x2, y2); + } + gs_render_stop(GS_LINESTRIP); } - - const float x2 = x + dx * len; - const float y2 = y + dy * len; - - gs_vertex2f(x2, y2); + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const float v_raw = get_amp(i); + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + + const float w = (float)s->width; + const float h = (float)s->height; + const float cx = w * 0.5f; + const float cy = h * 0.5f; + + float x = base_x[i]; + float y = base_y[i]; + + float dx = x - cx; + float dy = y - cy; + float l = std::sqrt(dx * dx + dy * dy); + if (l > 1e-4f) { + dx /= l; + dy /= l; + } else { + dx = 0.0f; + dy = -1.0f; + } + + const float x2 = x + dx * len; + const float y2 = y + dy * len; + + gs_vertex2f(x2, y2); + } + gs_render_stop(GS_LINESTRIP); } - gs_render_stop(GS_LINESTRIP); } static void draw_square_rays(audio_wave_source *s, gs_eparam_t *color_param) @@ -257,33 +305,58 @@ static void draw_square_rays(audio_wave_source *s, gs_eparam_t *color_param) const size_t idx = (size_t)(u * (float)(frames - 1)); amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; } - - const uint32_t sq_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, sq_color); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - const float v_raw = amp[i]; - const float v = audio_wave_apply_curve(s, v_raw); - const float len = v * max_len; - - const float x1 = base_x[i]; - const float y1 = base_y[i]; - const float x2 = x1 + nx[i] * len; - const float y2 = y1 + ny[i] * len; - - gs_vertex2f(x1, y1); - gs_vertex2f(x2, y2); - - if (s->mirror) { - const float x3 = x1 - nx[i] * len; - const float y3 = y1 - ny[i] * len; + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + const float x1 = base_x[i]; + const float y1 = base_y[i]; + const float x2 = x1 + nx[i] * len; + const float y2 = y1 + ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x2, y2); + if (s->mirror) { + const float x3 = x1 - nx[i] * len; + const float y3 = y1 - ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } + } + gs_render_stop(GS_LINES); + } + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + const float x1 = base_x[i]; + const float y1 = base_y[i]; + const float x2 = x1 + nx[i] * len; + const float y2 = y1 + ny[i] * len; gs_vertex2f(x1, y1); - gs_vertex2f(x3, y3); + gs_vertex2f(x2, y2); + if (s->mirror) { + const float x3 = x1 - nx[i] * len; + const float y3 = y1 - ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } } + gs_render_stop(GS_LINES); } - gs_render_stop(GS_LINES); } static void square_theme_draw(audio_wave_source *s, gs_eparam_t *color_param) diff --git a/src/themes/theme-stacked-columns.cpp b/src/themes/theme-stacked-columns.cpp new file mode 100644 index 0000000..d1d06c6 --- /dev/null +++ b/src/themes/theme-stacked-columns.cpp @@ -0,0 +1,227 @@ +#include "theme-stacked-columns.hpp" +#include +#include +#include +#include + +#ifndef UNUSED_PARAMETER +#define UNUSED_PARAMETER(x) (void)(x) +#endif + +static const char *k_theme_id = "stacked_columns"; +static const char *k_theme_name = "Stacked Columns"; + +static const char *P_STYLE = "sc_style"; +static const char *P_MIRROR = "sc_mirror"; +static const char *P_DOUBLE = "sc_double_side"; +static const char *P_COLUMNS = "sc_columns"; +static const char *P_STACKS = "sc_stacks"; +static const char *P_GAP = "sc_gap_ratio"; + +struct stacked_columns_data { + int columns = 64; + int stacks = 18; + float gap_ratio = 0.18f; // fraction of block height + bool double_side = true; + bool mirror = false; +}; + +static void add_props(obs_properties_t *props) +{ + obs_property_t *style = + obs_properties_add_list(props, P_STYLE, "Style", OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + obs_property_list_add_string(style, "Blocks", "blocks"); + obs_property_list_add_string(style, "Compact Blocks", "compact"); + + obs_properties_add_bool(props, P_DOUBLE, "Double-Sided (centered)"); + obs_properties_add_bool(props, P_MIRROR, "Mirror horizontally"); + + obs_properties_add_int_slider(props, P_COLUMNS, "Columns", 8, 256, 1); + obs_properties_add_int_slider(props, P_STACKS, "Stacks per Column", 4, 48, 1); + obs_properties_add_float_slider(props, P_GAP, "Gap", 0.0, 0.45, 0.01); +} + +static void update(audio_wave_source *s, obs_data_t *settings) +{ + if (!s || !settings) + return; + + const char *style_id = obs_data_get_string(settings, P_STYLE); + if (!style_id || !*style_id) + style_id = "blocks"; + s->theme_style_id = style_id; + + auto *d = static_cast(s->theme_data); + if (!d) { + d = new stacked_columns_data{}; + s->theme_data = d; + } + + d->double_side = obs_data_get_bool(settings, P_DOUBLE); + d->mirror = obs_data_get_bool(settings, P_MIRROR); + + int cols = aw_get_int_default(settings, P_COLUMNS, 64); + cols = std::clamp(cols, 8, 256); + d->columns = cols; + + int stacks = aw_get_int_default(settings, P_STACKS, 18); + stacks = std::clamp(stacks, 4, 48); + d->stacks = stacks; + + float gap = aw_get_float_default(settings, P_GAP, 0.18f); + gap = std::clamp(gap, 0.0f, 0.45f); + d->gap_ratio = gap; +} + +static void destroy(audio_wave_source *s) +{ + if (!s) + return; + auto *d = static_cast(s->theme_data); + delete d; + s->theme_data = nullptr; +} + +static inline float sc_db_from_amp(float a) +{ + if (a <= 1e-6f) + return -120.0f; + return 20.0f * log10f(a); +} + +static inline float sample_wave(const audio_wave_source *s, float t) +{ + if (!s || s->wave.empty()) + return 0.0f; + const size_t n = s->wave.size(); + const float pos = t * (float)(n - 1); + const size_t i0 = (size_t)std::clamp((int)std::floor(pos), 0, (int)n - 1); + const size_t i1 = std::min(i0 + 1, n - 1); + const float a = s->wave[i0]; + const float b = s->wave[i1]; + const float f = pos - (float)i0; + return a + (b - a) * f; +} + +static void draw(audio_wave_source *s, gs_eparam_t *color_param) +{ + if (!s || !color_param) + return; + + auto *d = static_cast(s->theme_data); + if (!d) + return; + + const float w = (float)s->width; + const float h = (float)s->height; + + const int cols = std::max(8, d->columns); + const int stacks = std::max(4, d->stacks); + + const float col_w = w / (float)cols; + const float block_w = col_w * 0.72f; + const float x_pad = (col_w - block_w) * 0.5f; + + const float half_h = h * 0.5f; + const float usable_h = d->double_side ? half_h : h; + const float block_h = usable_h / (float)stacks; + const float gap = std::clamp(d->gap_ratio, 0.0f, 0.45f) * block_h; + const float bh = std::max(1.0f, block_h - gap); + + // Color batching: quantize gradient into 64 bins to reduce effect param churn. + const int bins = 64; + std::vector> bin_rects((size_t)bins); // x,y,w,h packed + + auto bin_for_t = [&](float t) -> int { + int b = (int)std::floor(t * (float)(bins - 1) + 0.5f); + return std::clamp(b, 0, bins - 1); + }; + + for (int c = 0; c < cols; ++c) { + const float t = (cols <= 1) ? 0.0f : ((float)c / (float)(cols - 1)); + float amp = sample_wave(s, d->mirror ? (1.0f - t) : t); + amp = std::clamp(amp, 0.0f, 1.0f); + + // Convert amplitude to dBFS and map to [0..1] using global React/Peak dB + const float db = sc_db_from_amp(amp); + float norm = 0.0f; + if (db > s->react_db) { + norm = (db - s->react_db) / (s->peak_db - s->react_db + 1e-3f); + norm = std::clamp(norm, 0.0f, 1.0f); + } + norm = audio_wave_apply_curve(s, norm); + + // First row always visible + int on = 1 + (int)std::lround(norm * (float)(stacks - 1)); + on = std::clamp(on, 1, stacks); + + const float x0 = (float)c * col_w + x_pad; + const float x1 = x0 + block_w; + + const int b = bin_for_t(t); + auto &rects = bin_rects[(size_t)b]; + + if (d->double_side) { + // build up from center line to top and bottom + for (int k = 0; k < on; ++k) { + const float y_top0 = half_h - (float)(k + 1) * block_h + gap * 0.5f; + const float y_top1 = y_top0 + bh; + rects.insert(rects.end(), {x0, y_top0, x1, y_top1}); + + const float y_bot0 = half_h + (float)k * block_h + gap * 0.5f; + const float y_bot1 = y_bot0 + bh; + rects.insert(rects.end(), {x0, y_bot0, x1, y_bot1}); + } + } else { + // build up from bottom + for (int k = 0; k < on; ++k) { + const float y0 = h - (float)(k + 1) * block_h + gap * 0.5f; + const float y1 = y0 + bh; + rects.insert(rects.end(), {x0, y0, x1, y1}); + } + } + } + + // Emit rectangles per bin with one color each. + for (int b = 0; b < bins; ++b) { + auto &rects = bin_rects[(size_t)b]; + if (rects.empty()) + continue; + + const float t = (float)b / (float)(bins - 1); + const uint32_t col = aw_gradient_color_at(s, t); + audio_wave_set_solid_color(color_param, col); + + gs_render_start(true); + for (size_t i = 0; i + 3 < rects.size(); i += 4) { + const float x0 = rects[i + 0]; + const float y0 = rects[i + 1]; + const float x1 = rects[i + 2]; + const float y1 = rects[i + 3]; + + // two triangles + gs_vertex2f(x0, y0); + gs_vertex2f(x1, y0); + gs_vertex2f(x0, y1); + + gs_vertex2f(x1, y0); + gs_vertex2f(x1, y1); + gs_vertex2f(x0, y1); + } + gs_render_stop(GS_TRIS); + } +} + +void audio_wave_register_stacked_columns_theme() +{ + static audio_wave_theme theme = {}; + theme.id = k_theme_id; + theme.display_name = k_theme_name; + theme.add_properties = add_props; + theme.update = update; + theme.draw = draw; + theme.destroy_data = destroy; + theme.draw_background = nullptr; + + audio_wave_register_theme(&theme); +} diff --git a/src/themes/theme-stacked-columns.hpp b/src/themes/theme-stacked-columns.hpp new file mode 100644 index 0000000..dda0c12 --- /dev/null +++ b/src/themes/theme-stacked-columns.hpp @@ -0,0 +1,4 @@ +#pragma once +#include "audio-wave.hpp" + +void audio_wave_register_stacked_columns_theme(); diff --git a/src/themes/theme-star.cpp b/src/themes/theme-star.cpp index 566910f..d536470 100644 --- a/src/themes/theme-star.cpp +++ b/src/themes/theme-star.cpp @@ -6,8 +6,20 @@ static const char *k_theme_id_star = "star"; static const char *k_theme_name_star = "Star"; static const char *STAR_PROP_STYLE = "star_style"; -static const char *STAR_PROP_COLOR = "star_color"; static const char *STAR_PROP_MIRROR = "star_mirror"; +static const char *P_DENSITY = "shape_density"; + +static bool star_style_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) +{ + const char *style_id = obs_data_get_string(settings, STAR_PROP_STYLE); + const bool is_rays = (style_id && strcmp(style_id, "rays") == 0); + + obs_property_t *mirror = obs_properties_get(props, STAR_PROP_MIRROR); + if (mirror) + obs_property_set_visible(mirror, is_rays); + + return true; +} static void star_theme_add_properties(obs_properties_t *props) { @@ -16,9 +28,12 @@ static void star_theme_add_properties(obs_properties_t *props) obs_property_list_add_string(style, "Linear Orbit", "linear"); obs_property_list_add_string(style, "Rays", "rays"); + obs_property_set_modified_callback(style, star_style_modified); + + obs_property_t *mirror = obs_properties_add_bool(props, STAR_PROP_MIRROR, "Double-sided rays"); + obs_property_set_visible(mirror, false); + obs_properties_add_int_slider(props, P_DENSITY, "Shape Density (%)", 10, 300, 5); - obs_properties_add_color(props, STAR_PROP_COLOR, "Color"); - obs_properties_add_bool(props, STAR_PROP_MIRROR, "Double-sided rays"); } static void star_theme_update(audio_wave_source *s, obs_data_t *settings) @@ -32,19 +47,12 @@ static void star_theme_update(audio_wave_source *s, obs_data_t *settings) s->theme_style_id = style_id; - uint32_t color = (uint32_t)aw_get_int_default(settings, STAR_PROP_COLOR, 0); - if (color == 0) - color = 0xFFFFFF; + int density = aw_get_int_default(settings, P_DENSITY, 140); + density = std::clamp(density, 10, 300); + s->frame_density = density; - s->color = color; - s->colors.clear(); - s->colors.push_back(audio_wave_named_color{"star", color}); - - if (s->frame_density < 80) - s->frame_density = 80; - - s->mirror = obs_data_get_bool(settings, STAR_PROP_MIRROR); + s->mirror = (strcmp(style_id, "rays") == 0) ? obs_data_get_bool(settings, STAR_PROP_MIRROR) : false; } // ───────────────────────────────────────────── @@ -177,37 +185,72 @@ static void draw_star_linear(audio_wave_source *s, gs_eparam_t *color_param) auto get_amp = [&](uint32_t i) -> float { return amp_smooth[i]; }; - - const uint32_t star_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, star_color); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - const float v_raw = get_amp(i); - const float v = audio_wave_apply_curve(s, v_raw); - const float len = v * max_len; - const float x = base_x[i]; - const float y = base_y[i]; - const float cx = (float)s->width * 0.5f; - const float cy = (float)s->height * 0.5f; - float dx = x - cx; - float dy = y - cy; - const float l = std::sqrt(dx * dx + dy * dy); - if (l > 1e-4f) { - dx /= l; - dy /= l; - } else { - dx = 0.0f; - dy = -1.0f; + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const float v_raw = get_amp(i); + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + const float x = base_x[i]; + const float y = base_y[i]; + const float cx = (float)s->width * 0.5f; + const float cy = (float)s->height * 0.5f; + float dx = x - cx; + float dy = y - cy; + const float l = std::sqrt(dx * dx + dy * dy); + if (l > 1e-4f) { + dx /= l; + dy /= l; + } else { + dx = 0.0f; + dy = -1.0f; + } + + const float x2 = x + dx * len; + const float y2 = y + dy * len; + + gs_vertex2f(x2, y2); + } + gs_render_stop(GS_LINESTRIP); } - - const float x2 = x + dx * len; - const float y2 = y + dy * len; - - gs_vertex2f(x2, y2); + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const float v_raw = get_amp(i); + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + const float x = base_x[i]; + const float y = base_y[i]; + const float cx = (float)s->width * 0.5f; + const float cy = (float)s->height * 0.5f; + float dx = x - cx; + float dy = y - cy; + const float l = std::sqrt(dx * dx + dy * dy); + if (l > 1e-4f) { + dx /= l; + dy /= l; + } else { + dx = 0.0f; + dy = -1.0f; + } + + const float x2 = x + dx * len; + const float y2 = y + dy * len; + + gs_vertex2f(x2, y2); + } + gs_render_stop(GS_LINESTRIP); } - gs_render_stop(GS_LINESTRIP); } static void draw_star_rays(audio_wave_source *s, gs_eparam_t *color_param) @@ -248,33 +291,67 @@ static void draw_star_rays(audio_wave_source *s, gs_eparam_t *color_param) const size_t idx = (size_t)(u * (float)(frames - 1)); amp[i] = (idx < frames) ? s->wave[idx] : 0.0f; } + if (color_param && s->gradient_enabled) { + const uint32_t bins = 64; + for (uint32_t b = 0; b < bins; ++b) { + const uint32_t i0 = (uint32_t)((uint64_t)b * segments / bins); + const uint32_t i1 = (uint32_t)((uint64_t)(b + 1) * segments / bins); + if (i1 <= i0) + continue; + + const float tcol = (bins <= 1) ? 0.0f : ((float)b / (float)(bins - 1)); + audio_wave_set_solid_color(color_param, aw_gradient_color_at(s, tcol)); + + gs_render_start(true); + for (uint32_t i = i0; i < i1; ++i) { + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; + + const float x1 = base_x[i]; + const float y1 = base_y[i]; + const float x2 = x1 + nx[i] * len; + const float y2 = y1 + ny[i] * len; + + gs_vertex2f(x1, y1); + gs_vertex2f(x2, y2); + + if (s->mirror) { + const float x3 = x1 - nx[i] * len; + const float y3 = y1 - ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } + } + gs_render_stop(GS_LINES); + } + } else { + if (color_param) + audio_wave_set_solid_color(color_param, s->color); - const uint32_t star_color = audio_wave_get_color(s, 0, s->color); - if (color_param) - audio_wave_set_solid_color(color_param, star_color); - - gs_render_start(true); - for (uint32_t i = 0; i < segments; ++i) { - const float v_raw = amp[i]; - const float v = audio_wave_apply_curve(s, v_raw); - const float len = v * max_len; - - const float x1 = base_x[i]; - const float y1 = base_y[i]; - const float x2 = x1 + nx[i] * len; - const float y2 = y1 + ny[i] * len; + gs_render_start(true); + for (uint32_t i = 0; i < segments; ++i) { + const float v_raw = amp[i]; + const float v = audio_wave_apply_curve(s, v_raw); + const float len = v * max_len; - gs_vertex2f(x1, y1); - gs_vertex2f(x2, y2); + const float x1 = base_x[i]; + const float y1 = base_y[i]; + const float x2 = x1 + nx[i] * len; + const float y2 = y1 + ny[i] * len; - if (s->mirror) { - const float x3 = x1 - nx[i] * len; - const float y3 = y1 - ny[i] * len; gs_vertex2f(x1, y1); - gs_vertex2f(x3, y3); + gs_vertex2f(x2, y2); + + if (s->mirror) { + const float x3 = x1 - nx[i] * len; + const float y3 = y1 - ny[i] * len; + gs_vertex2f(x1, y1); + gs_vertex2f(x3, y3); + } } + gs_render_stop(GS_LINES); } - gs_render_stop(GS_LINES); } static void star_theme_draw(audio_wave_source *s, gs_eparam_t *color_param)