23 #include <AppConfig.h>
24 #include <juce_audio_basics/juce_audio_basics.h>
29 constexpr
double PI = 3.14159265358979323846;
31 inline float clampf(
float v,
float lo,
float hi) {
32 return v < lo ? lo : (v > hi ? hi : v);
35 inline int clampi(
int v,
int lo,
int hi) {
36 return v < lo ? lo : (v > hi ? hi : v);
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;
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);
51 float hz_to_normalized_frequency(
float hz) {
52 if (hz >= 0.0f && hz <= 1.0f)
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);
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)
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);
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))
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);
86 float lp_hi = 0.0f, lp_lo = 0.0f;
89 for (
int s = 0; s < samples; ++s) {
91 for (
int c = 0; c < channels; ++c)
95 lp_hi += alpha_hi * (x - lp_hi);
96 lp_lo += alpha_lo * (x - lp_lo);
97 const float filtered = lp_hi - lp_lo;
99 const float abs_v = std::fabs(filtered);
100 sum_sq += abs_v * abs_v;
101 peak = std::max(peak, abs_v);
104 const float rms = std::sqrt((
float)(sum_sq / samples));
106 const float combined = std::max(rms * 3.6f, peak * 1.15f);
107 return clampf(combined * gain, 0.0f, 1.0f);
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),
124 init_effect_details();
134 void BeatSync::init_effect_details()
139 info.
description =
"Generates an audio-reactive color flash layer, synchronized to the beat.";
144 std::shared_ptr<openshot::Frame>
BeatSync::GetFrame(std::shared_ptr<openshot::Frame> frame, int64_t frame_number)
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()) {
159 if (last_frame_ >= 0 && std::abs(frame_number - last_frame_) >= 2)
161 last_frame_ = frame_number;
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,
168 const float raw_energy = band_energy(frame, low_hz, high_hz, gain_v);
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;
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));
181 if (raw_energy > envelope_)
182 envelope_ = atk_coef * envelope_ + (1.0f - atk_coef) * raw_energy;
184 envelope_ = dec_coef * envelope_ + (1.0f - dec_coef) * raw_energy;
188 float energy = envelope_;
192 energy = clampf((energy - thr) / std::max(0.001f, 1.0f - thr), 0.0f, 1.0f);
195 energy = 1.0f - energy;
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);
207 frame->AddImage(out);
235 }
catch (
const std::exception& e) {
236 throw InvalidJSON(
"JSON is invalid (missing keys or invalid data types)");
251 if (!root[
"invert"].isNull())
invert = root[
"invert"].asBool();
278 root[
"invert"] =
add_property_json(
"Invert",
invert ? 1.0 : 0.0,
"int",
"", NULL, 0, 1,
false, requested_frame);
284 root[
"response_curve"][
"channel"] =
"all";
287 return root.toStyledString();