OpenShot Library | libopenshot 1.1.1
Loading...
Searching...
No Matches
BeatSync.cpp
Go to the documentation of this file.
1
9// Copyright (c) 2008-2026 OpenShot Studios, LLC
10//
11// SPDX-License-Identifier: LGPL-3.0-or-later
12
13#include "BeatSync.h"
14#include "Exceptions.h"
15#include "Timeline.h"
16
17#include <algorithm>
18#include <cmath>
19#include <vector>
20
21#include <QImage>
22
23#include <AppConfig.h>
24#include <juce_audio_basics/juce_audio_basics.h>
25
26using namespace openshot;
27
28namespace {
29 constexpr double PI = 3.14159265358979323846;
30
31 inline float clampf(float v, float lo, float hi) {
32 return v < lo ? lo : (v > hi ? hi : v);
33 }
34
35 inline int clampi(int v, int lo, int hi) {
36 return v < lo ? lo : (v > hi ? hi : v);
37 }
38
39 inline int blend_channel(int low, int high, int inv, int blend) {
40 return (clampi(low, 0, 255) * inv + clampi(high, 0, 255) * blend) >> 8;
41 }
42
43 // Logarithmic normalized [0,1] → Hz mapping (20–20000 Hz), matching AudioVisualization
44 float normalized_frequency_to_hz(float value) {
45 const float min_hz = 20.0f;
46 const float max_hz = 20000.0f;
47 const float n = clampf(value, 0.0f, 1.0f);
48 return min_hz * std::pow(max_hz / min_hz, n);
49 }
50
51 float hz_to_normalized_frequency(float hz) {
52 if (hz >= 0.0f && hz <= 1.0f)
53 return hz;
54 const float min_hz = 20.0f;
55 const float max_hz = 20000.0f;
56 hz = clampf(hz, min_hz, max_hz);
57 return clampf(std::log(hz / min_hz) / std::log(max_hz / min_hz), 0.0f, 1.0f);
58 }
59
60 // Time-domain bandpass energy: difference of two first-order IIR low-pass filters.
61 // Returns linear 0-1 based on RMS + peak of the filtered signal, scaled by gain.
62 // This mirrors the reactive_level() approach in AudioVisualization but adds band filtering.
63 float band_energy(const std::shared_ptr<Frame>& frame, float low_hz, float high_hz, float gain) {
64 const int samples = frame->GetAudioSamplesCount();
65 const int channels = frame->GetAudioChannelsCount();
66 const int sample_rate = std::max(1, frame->SampleRate());
67 if (samples <= 0 || channels <= 0)
68 return 0.0f;
69
70 const float nyquist = sample_rate * 0.5f;
71 low_hz = clampf(low_hz, 0.0f, nyquist - 1.0f);
72 high_hz = clampf(high_hz, low_hz + 1.0f, nyquist);
73
74 // First-order IIR coefficients: alpha = dt / (tau + dt), tau = 1/(2*pi*fc)
75 const float dt = 1.0f / sample_rate;
76 const float alpha_hi = dt / (dt + 1.0f / (2.0f * (float)PI * high_hz));
77 const float alpha_lo = low_hz > 1.0f
78 ? dt / (dt + 1.0f / (2.0f * (float)PI * low_hz))
79 : 0.0f; // no high-pass when low_hz is at/near DC
80
81 auto* buffer = frame->GetAudioSampleBuffer();
82 std::vector<const float*> ch(channels);
83 for (int c = 0; c < channels; ++c)
84 ch[c] = buffer->getReadPointer(c);
85
86 float lp_hi = 0.0f, lp_lo = 0.0f;
87 double sum_sq = 0.0;
88 float peak = 0.0f;
89 for (int s = 0; s < samples; ++s) {
90 float x = 0.0f;
91 for (int c = 0; c < channels; ++c)
92 x += ch[c][s];
93 x /= channels;
94
95 lp_hi += alpha_hi * (x - lp_hi); // low-pass at high_hz
96 lp_lo += alpha_lo * (x - lp_lo); // low-pass at low_hz
97 const float filtered = lp_hi - lp_lo; // bandpass = difference
98
99 const float abs_v = std::fabs(filtered);
100 sum_sq += abs_v * abs_v;
101 peak = std::max(peak, abs_v);
102 }
103
104 const float rms = std::sqrt((float)(sum_sq / samples));
105 // Combine RMS and peak with the same weighting reactive_level() uses
106 const float combined = std::max(rms * 3.6f, peak * 1.15f);
107 return clampf(combined * gain, 0.0f, 1.0f);
108 }
109}
110
112 low_color((unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)255),
113 high_color((unsigned char)255, (unsigned char)255, (unsigned char)255, (unsigned char)255),
114 intensity(2.0),
115 threshold(0.1),
116 attack_ms(10.0),
117 decay_ms(200.0),
118 frequency_low(0.0),
119 frequency_high(1.0),
120 invert(false),
121 envelope_(0.0f),
122 last_frame_(-1)
123{
124 init_effect_details();
125}
126
128 BeatSync()
129{
130 this->high_color = color;
131 this->intensity = intensity;
132}
133
134void BeatSync::init_effect_details()
135{
137 info.class_name = "BeatSync";
138 info.name = "Beat Sync";
139 info.description = "Generates an audio-reactive color flash layer, synchronized to the beat.";
140 info.has_audio = false;
141 info.has_video = true;
142}
143
144std::shared_ptr<openshot::Frame> BeatSync::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
145{
146 const std::shared_ptr<QImage> frame_image = frame->GetImage();
147 int width = frame_image ? std::max(1, frame_image->width()) : 1;
148 int height = frame_image ? std::max(1, frame_image->height()) : 1;
149 if ((width <= 1 || height <= 1) && ParentTimeline()) {
150 if (Timeline* timeline = dynamic_cast<Timeline*>(ParentTimeline())) {
151 if (timeline->info.width > 1 && timeline->info.height > 1) {
152 width = timeline->info.width;
153 height = timeline->info.height;
154 }
155 }
156 }
157
158 // Reset envelope on seek (discontinuous frame access)
159 if (last_frame_ >= 0 && std::abs(frame_number - last_frame_) >= 2)
160 envelope_ = 0.0f;
161 last_frame_ = frame_number;
162
163 // --- Audio energy for the configured frequency band ---
164 const float low_hz = normalized_frequency_to_hz(clampf(frequency_low.GetValue(frame_number), 0.0f, 1.0f));
165 const float high_hz = std::max(low_hz + 1.0f,
166 normalized_frequency_to_hz(clampf(frequency_high.GetValue(frame_number), 0.0f, 1.0f)));
167 const float gain_v = std::max(0.01f, (float)intensity.GetValue(frame_number));
168 const float raw_energy = band_energy(frame, low_hz, high_hz, gain_v);
169
170 // --- IIR envelope follower ---
171 // Derive per-frame time constant from audio context
172 const int samples = frame->GetAudioSamplesCount();
173 const int sample_rate = std::max(1, frame->SampleRate());
174 const float frame_sec = (samples > 0) ? (float)samples / sample_rate : 1.0f / 24.0f;
175
176 const float atk = clampf((float)attack_ms.GetValue(frame_number), 1.0f, 5000.0f);
177 const float dec = clampf((float)decay_ms.GetValue(frame_number), 1.0f, 5000.0f);
178 const float atk_coef = std::exp(-frame_sec / (atk * 0.001f));
179 const float dec_coef = std::exp(-frame_sec / (dec * 0.001f));
180
181 if (raw_energy > envelope_)
182 envelope_ = atk_coef * envelope_ + (1.0f - atk_coef) * raw_energy;
183 else
184 envelope_ = dec_coef * envelope_ + (1.0f - dec_coef) * raw_energy;
185
186 // --- Apply threshold and response curve ---
187 const float thr = clampf((float)threshold.GetValue(frame_number), 0.0f, 1.0f);
188 float energy = envelope_;
189 if (energy <= thr) {
190 energy = 0.0f;
191 } else {
192 energy = clampf((energy - thr) / std::max(0.001f, 1.0f - thr), 0.0f, 1.0f);
193 }
194 if (invert)
195 energy = 1.0f - energy;
196
197 const float response = clampf(response_curve.Sample(energy, frame_number), 0.0f, 1.0f);
198 const int blend = clampi(static_cast<int>(response * 256.0f + 0.5f), 0, 256);
199 const int inv = 256 - blend;
200 auto out = std::make_shared<QImage>(width, height, QImage::Format_RGBA8888_Premultiplied);
201 out->fill(QColor(
202 blend_channel(low_color.red.GetInt(frame_number), high_color.red.GetInt(frame_number), inv, blend),
203 blend_channel(low_color.green.GetInt(frame_number), high_color.green.GetInt(frame_number), inv, blend),
204 blend_channel(low_color.blue.GetInt(frame_number), high_color.blue.GetInt(frame_number), inv, blend),
205 blend_channel(low_color.alpha.GetInt(frame_number), high_color.alpha.GetInt(frame_number), inv, blend)));
206
207 frame->AddImage(out);
208 return frame;
209}
210
211std::string BeatSync::Json() const {
212 return JsonValue().toStyledString();
213}
214
215Json::Value BeatSync::JsonValue() const {
216 Json::Value root = EffectBase::JsonValue();
217 root["type"] = info.class_name;
218 root["low_color"] = low_color.JsonValue();
219 root["high_color"] = high_color.JsonValue();
220 root["intensity"] = intensity.JsonValue();
221 root["threshold"] = threshold.JsonValue();
222 root["attack_ms"] = attack_ms.JsonValue();
223 root["decay_ms"] = decay_ms.JsonValue();
224 root["frequency_low"] = frequency_low.JsonValue();
225 root["frequency_high"] = frequency_high.JsonValue();
226 root["invert"] = invert;
227 root["response_curve"] = response_curve.JsonValue();
228 return root;
229}
230
231void BeatSync::SetJson(const std::string value) {
232 try {
233 const Json::Value root = openshot::stringToJson(value);
234 SetJsonValue(root);
235 } catch (const std::exception& e) {
236 throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
237 }
238}
239
240void BeatSync::SetJsonValue(const Json::Value root) {
242 if (!root["low_color"].isNull()) low_color.SetJsonValue(root["low_color"]);
243 if (!root["high_color"].isNull()) high_color.SetJsonValue(root["high_color"]);
244 if (!root["color"].isNull()) high_color.SetJsonValue(root["color"]);
245 if (!root["intensity"].isNull()) intensity.SetJsonValue(root["intensity"]);
246 if (!root["threshold"].isNull()) threshold.SetJsonValue(root["threshold"]);
247 if (!root["attack_ms"].isNull()) attack_ms.SetJsonValue(root["attack_ms"]);
248 if (!root["decay_ms"].isNull()) decay_ms.SetJsonValue(root["decay_ms"]);
249 if (!root["frequency_low"].isNull()) frequency_low.SetJsonValue(root["frequency_low"]);
250 if (!root["frequency_high"].isNull()) frequency_high.SetJsonValue(root["frequency_high"]);
251 if (!root["invert"].isNull()) invert = root["invert"].asBool();
252 if (!root["response_curve"].isNull()) response_curve.SetJsonValue(root["response_curve"]);
253}
254
255std::string BeatSync::PropertiesJSON(int64_t requested_frame) const {
256 Json::Value root = BasePropertiesJSON(requested_frame);
257
258 root["low_color"] = add_property_json("Low Color", 0.0, "color", "", &low_color.red, 0, 255, false, requested_frame);
259 root["low_color"]["red"] = add_property_json("Red", low_color.red.GetValue(requested_frame), "float", "", &low_color.red, 0, 255, false, requested_frame);
260 root["low_color"]["green"] = add_property_json("Green", low_color.green.GetValue(requested_frame), "float", "", &low_color.green, 0, 255, false, requested_frame);
261 root["low_color"]["blue"] = add_property_json("Blue", low_color.blue.GetValue(requested_frame), "float", "", &low_color.blue, 0, 255, false, requested_frame);
262 root["low_color"]["alpha"] = add_property_json("Alpha", low_color.alpha.GetValue(requested_frame), "float", "", &low_color.alpha, 0, 255, false, requested_frame);
263
264 root["high_color"] = add_property_json("High Color", 0.0, "color", "", &high_color.red, 0, 255, false, requested_frame);
265 root["high_color"]["red"] = add_property_json("Red", high_color.red.GetValue(requested_frame), "float", "", &high_color.red, 0, 255, false, requested_frame);
266 root["high_color"]["green"] = add_property_json("Green", high_color.green.GetValue(requested_frame), "float", "", &high_color.green, 0, 255, false, requested_frame);
267 root["high_color"]["blue"] = add_property_json("Blue", high_color.blue.GetValue(requested_frame), "float", "", &high_color.blue, 0, 255, false, requested_frame);
268 root["high_color"]["alpha"] = add_property_json("Alpha", high_color.alpha.GetValue(requested_frame), "float", "", &high_color.alpha, 0, 255, false, requested_frame);
269
270 root["intensity"] = add_property_json("Intensity", intensity.GetValue(requested_frame), "float", "", &intensity, 0.0, 10.0, false, requested_frame);
271 root["threshold"] = add_property_json("Threshold", threshold.GetValue(requested_frame), "float", "", &threshold, 0.0, 1.0, false, requested_frame);
272 root["attack_ms"] = add_property_json("Attack (ms)", attack_ms.GetValue(requested_frame), "float", "", &attack_ms, 1.0, 500.0, false, requested_frame);
273 root["decay_ms"] = add_property_json("Decay (ms)", decay_ms.GetValue(requested_frame), "float", "", &decay_ms, 1.0, 2000.0, false, requested_frame);
274
275 root["frequency_low"] = add_property_json("Low Frequency", hz_to_normalized_frequency(frequency_low.GetValue(requested_frame)), "float", "Normalized frequency floor: 0 = 20 Hz, 1 = 20 kHz", &frequency_low, 0.0, 1.0, false, requested_frame);
276 root["frequency_high"] = add_property_json("High Frequency", hz_to_normalized_frequency(frequency_high.GetValue(requested_frame)), "float", "Normalized frequency ceiling: 0 = 20 Hz, 1 = 20 kHz", &frequency_high, 0.0, 1.0, false, requested_frame);
277
278 root["invert"] = add_property_json("Invert", invert ? 1.0 : 0.0, "int", "", NULL, 0, 1, false, requested_frame);
279 root["invert"]["choices"].append(add_property_choice_json("No", 0, invert ? 1 : 0));
280 root["invert"]["choices"].append(add_property_choice_json("Yes", 1, invert ? 1 : 0));
281
282 root["response_curve"] = add_property_json("Response Curve", 0.0, "colorgrade_curve", response_curve.Summary(requested_frame), NULL, 0.0, 1.0, false, requested_frame);
283 root["response_curve"]["curve"] = response_curve.JsonValue();
284 root["response_curve"]["channel"] = "all";
285 root["response_curve"]["summary"] = response_curve.Summary(requested_frame);
286
287 return root.toStyledString();
288}
Header file for BeatSync effect class.
Header file for all Exception classes.
Header file for Timeline class.
void SetJsonValue(const Json::Value &root)
float Sample(float input, int64_t frame_number) const
std::string Summary(int64_t frame_number) const
Json::Value JsonValue() const
std::string PropertiesJSON(int64_t requested_frame) const override
Definition BeatSync.cpp:255
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition BeatSync.cpp:240
Keyframe frequency_low
Definition BeatSync.h:42
Keyframe frequency_high
Definition BeatSync.h:43
Keyframe intensity
Definition BeatSync.h:38
void SetJson(const std::string value) override
Load JSON string into this object.
Definition BeatSync.cpp:231
std::shared_ptr< openshot::Frame > GetFrame(int64_t frame_number) override
This method is required for all derived classes of ClipBase, and returns a new openshot::Frame object...
Definition BeatSync.h:50
Keyframe attack_ms
Definition BeatSync.h:40
Keyframe threshold
Definition BeatSync.h:39
AnimatedCurve response_curve
Definition BeatSync.h:45
std::string Json() const override
Generate JSON string of this object.
Definition BeatSync.cpp:211
Keyframe decay_ms
Definition BeatSync.h:41
Json::Value JsonValue() const override
Generate Json::Value for this object.
Definition BeatSync.cpp:215
Json::Value add_property_choice_json(std::string name, int value, int selected_value) const
Generate JSON choice for a property (dropdown properties)
Definition ClipBase.cpp:136
openshot::TimelineBase * timeline
Pointer to the parent timeline instance (if any)
Definition ClipBase.h:40
virtual openshot::TimelineBase * ParentTimeline()
Get the associated Timeline pointer (if any)
Definition ClipBase.h:94
Json::Value add_property_json(std::string name, float value, std::string type, std::string memo, const Keyframe *keyframe, float min_value, float max_value, bool readonly, int64_t requested_frame) const
Generate JSON for a property.
Definition ClipBase.cpp:100
This class represents a color (used on the timeline and clips)
Definition Color.h:27
openshot::Keyframe blue
Curve representing the red value (0 - 255)
Definition Color.h:32
openshot::Keyframe red
Curve representing the red value (0 - 255)
Definition Color.h:30
openshot::Keyframe green
Curve representing the green value (0 - 255)
Definition Color.h:31
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition Color.cpp:118
openshot::Keyframe alpha
Curve representing the alpha value (0 - 255)
Definition Color.h:33
Json::Value JsonValue() const
Generate Json::Value for this object.
Definition Color.cpp:87
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
Json::Value BasePropertiesJSON(int64_t requested_frame) const
Generate JSON object of base properties (recommended to be used by all effects)
virtual void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
EffectInfoStruct info
Information about the current effect.
Definition EffectBase.h:114
Exception for invalid JSON.
Definition Exceptions.h:224
A Keyframe is a collection of Point instances, which is used to vary a number or property over time.
Definition KeyFrame.h:53
int GetInt(int64_t index) const
Get the rounded INT value at a specific index.
Definition KeyFrame.cpp:282
void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
Definition KeyFrame.cpp:372
double GetValue(int64_t index) const
Get the value at a specific index.
Definition KeyFrame.cpp:258
Json::Value JsonValue() const
Generate Json::Value for this object.
Definition KeyFrame.cpp:339
This class represents a timeline.
Definition Timeline.h:154
This namespace is the default namespace for all code in the openshot library.
const Json::Value stringToJson(const std::string value)
Definition Json.cpp:16
bool has_video
Determines if this effect manipulates the image of a frame.
Definition EffectBase.h:43
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition EffectBase.h:44
std::string class_name
The class name of the effect.
Definition EffectBase.h:39
std::string name
The name of the effect.
Definition EffectBase.h:40
std::string description
The description of this effect and what it does.
Definition EffectBase.h:41