25 #include <QConicalGradient>
26 #include <QLinearGradient>
29 #include <QPainterPath>
32 #include <QRadialGradient>
34 #include <AppConfig.h>
35 #include <juce_audio_basics/juce_audio_basics.h>
40 constexpr
double PI = 3.14159265358979323846;
42 float clampf(
float value,
float min_value,
float max_value) {
43 return std::max(min_value, std::min(max_value, value));
46 int clampi(
int value,
int min_value,
int max_value) {
47 return std::max(min_value, std::min(max_value, value));
50 QColor color_at(
const Color& color, int64_t frame_number) {
52 clampi(color.
red.
GetInt(frame_number), 0, 255),
54 clampi(color.
blue.
GetInt(frame_number), 0, 255),
58 QColor mix_color(
const QColor& a,
const QColor& b,
float amount) {
59 amount = clampf(amount, 0.0f, 1.0f);
60 const float inv = 1.0f - amount;
62 clampi(std::lround(a.red() * inv + b.red() * amount), 0, 255),
63 clampi(std::lround(a.green() * inv + b.green() * amount), 0, 255),
64 clampi(std::lround(a.blue() * inv + b.blue() * amount), 0, 255),
65 clampi(std::lround(a.alpha() * inv + b.alpha() * amount), 0, 255));
68 QColor alpha_color(QColor color,
float alpha_scale) {
69 color.setAlpha(clampi(std::lround(color.alpha() * alpha_scale), 0, 255));
73 QColor hue_shift(QColor color,
int degrees) {
77 int a = color.alpha();
78 color.getHsv(&h, &s, &v, &a);
81 return QColor::fromHsv((h + degrees + 360) % 360, s, v, a);
84 QColor rainbow_color(
const QColor& seed,
float position,
float spread) {
89 seed.getHsv(&h, &s, &v, &a);
92 const int hue = (h +
static_cast<int>(std::lround(clampf(position, 0.0f, 1.0f) * 360.0f))) % 360;
93 const int saturation = clampi(std::lround(s * (0.65f + clampf(spread, 0.0f, 1.0f) * 0.35f)), 0, 255);
94 const int value = clampi(std::lround(v * (0.88f + clampf(spread, 0.0f, 1.0f) * 0.12f)), 0, 255);
95 return QColor::fromHsv(hue, saturation, value, a);
98 template<
typename Gradient>
99 void set_rainbow_stops(Gradient& gradient,
const QColor& seed,
float spread,
float alpha_scale = 1.0f) {
100 const float stops[] = {0.0f, 0.16f, 0.33f, 0.50f, 0.66f, 0.83f, 1.0f};
101 for (
float stop : stops)
102 gradient.setColorAt(stop, alpha_color(rainbow_color(seed, stop, spread), alpha_scale));
113 Palette make_palette(
const QColor& base,
float glow_amount,
int style) {
116 palette.dark = mix_color(base, QColor(0, 0, 0, base.alpha()), 0.62f);
117 palette.light = mix_color(base, QColor(255, 255, 255, base.alpha()), 0.28f);
118 palette.accent = mix_color(base, palette.light, 0.35f);
120 palette.light = base;
121 palette.accent = base;
123 palette.light = mix_color(base, QColor(255, 255, 255, base.alpha()), 0.42f);
124 palette.accent = palette.light;
126 palette.light = mix_color(base, QColor(255, 255, 255, base.alpha()), 0.18f);
127 palette.accent = palette.light;
129 palette.glow = alpha_color(palette.light, 0.18f + glow_amount * 0.65f);
133 Palette make_palette(
const QColor& base,
float glow_amount,
int style,
float color_spread) {
134 Palette palette = make_palette(base, glow_amount, style);
135 color_spread = clampf(color_spread, 0.0f, 1.0f);
136 palette.dark = mix_color(base, palette.dark, color_spread);
137 palette.light = mix_color(base, palette.light, color_spread);
138 palette.accent = mix_color(base, palette.accent, color_spread);
139 palette.glow = alpha_color(mix_color(base, palette.glow, color_spread), 0.18f + glow_amount * 0.65f);
143 float style_glow_amount(
int style,
float glow) {
145 return clampf(std::max(glow, 0.45f), 0.0f, 1.0f);
147 return clampf(std::max(glow, 0.22f), 0.0f, 1.0f);
153 float style_stroke_width(
int style,
float glow) {
157 return 2.4f + glow * 1.4f;
159 return 1.8f + glow * 1.2f;
160 return 1.5f + glow * 1.6f;
163 float style_fill_alpha(
int style) {
173 QBrush vertical_style_fill(
float x0,
float y0,
float x1,
float y1,
const Palette& palette,
int style,
float alpha_scale = 1.0f) {
175 return QBrush(alpha_color(palette.base, style_fill_alpha(style) * alpha_scale));
177 QLinearGradient grad(x0, y0, x1, y1);
179 grad.setColorAt(0.0, alpha_color(palette.light, 0.92f * alpha_scale));
180 grad.setColorAt(0.5, alpha_color(palette.base, 0.70f * alpha_scale));
181 grad.setColorAt(1.0, alpha_color(palette.base, 0.28f * alpha_scale));
183 grad.setColorAt(0.0, alpha_color(palette.light, 0.48f * alpha_scale));
184 grad.setColorAt(0.5, alpha_color(palette.base, 0.78f * alpha_scale));
185 grad.setColorAt(1.0, alpha_color(palette.base, 0.48f * alpha_scale));
187 grad.setColorAt(0.0, alpha_color(palette.light, 0.72f * alpha_scale));
188 grad.setColorAt(0.62, alpha_color(palette.base, 0.72f * alpha_scale));
189 grad.setColorAt(1.0, alpha_color(palette.base, 0.38f * alpha_scale));
194 float normalized_frequency_to_hz(
float value,
bool high_frequency) {
198 const float min_hz = 20.0f;
199 const float max_hz = 20000.0f;
200 const float normalized = clampf(value, 0.0f, 1.0f);
201 if (high_frequency && normalized <= 0.0f)
202 return min_hz + 1.0f;
203 return min_hz * std::pow(max_hz / min_hz, normalized);
206 float hz_to_normalized_frequency(
float value) {
207 if (value >= 0.0f && value <= 1.0f)
210 const float min_hz = 20.0f;
211 const float max_hz = 20000.0f;
212 const float hz = clampf(value, min_hz, max_hz);
213 return clampf(std::log(hz / min_hz) / std::log(max_hz / min_hz), 0.0f, 1.0f);
216 std::vector<float> channel_samples(
const std::shared_ptr<Frame>& frame,
int channel,
int wanted_points,
float gain,
float smooth) {
217 std::vector<float> values(std::max(2, wanted_points), 0.0f);
218 const int samples = frame->GetAudioSamplesCount();
219 const int channels = frame->GetAudioChannelsCount();
220 if (samples <= 0 || channels <= 0)
223 const int safe_channel = clampi(channel, 0, channels - 1);
224 const float *audio = frame->GetAudioSampleBuffer()->getReadPointer(safe_channel);
225 float previous = 0.0f;
226 for (
int i = 0; i < static_cast<int>(values.size()); ++i) {
227 const int start = (i * samples) / values.size();
228 const int end = std::max(start + 1, ((i + 1) * samples) /
static_cast<int>(values.size()));
230 float peak_magnitude = 0.0f;
231 for (
int sample = start; sample < end && sample < samples; ++sample) {
232 const float value = audio[sample];
233 const float magnitude = std::fabs(value);
234 if (magnitude > peak_magnitude) {
236 peak_magnitude = magnitude;
239 const float scaled = clampf(peak * gain, -1.0f, 1.0f);
240 values[i] = previous * smooth + scaled * (1.0f - smooth);
241 previous = values[i];
246 std::vector<float> combined_samples(
const std::shared_ptr<Frame>& frame,
int wanted_points,
float gain,
float smooth) {
247 std::vector<float> values(std::max(2, wanted_points), 0.0f);
248 const int samples = frame->GetAudioSamplesCount();
249 const int channels = frame->GetAudioChannelsCount();
250 if (samples <= 0 || channels <= 0)
253 std::vector<const float*> channel_data;
254 channel_data.reserve(channels);
255 for (
int channel = 0; channel < channels; ++channel)
256 channel_data.push_back(frame->GetAudioSampleBuffer()->getReadPointer(channel));
258 float previous = 0.0f;
259 for (
int i = 0; i < static_cast<int>(values.size()); ++i) {
260 const int start = (i * samples) / values.size();
261 const int end = std::max(start + 1, ((i + 1) * samples) /
static_cast<int>(values.size()));
263 float peak_magnitude = 0.0f;
264 for (
int sample = start; sample < end && sample < samples; ++sample) {
265 float mixed = std::accumulate(channel_data.begin(), channel_data.end(), 0.0f,
266 [sample](
float total,
const float* channel) {
267 return total + channel[sample];
270 const float magnitude = std::fabs(mixed);
271 if (magnitude > peak_magnitude) {
273 peak_magnitude = magnitude;
276 const float scaled = clampf(peak * gain, -1.0f, 1.0f);
277 values[i] = previous * smooth + scaled * (1.0f - smooth);
278 previous = values[i];
283 float reactive_level(
const std::shared_ptr<Frame>& frame,
int channel,
float gain) {
284 const int samples = frame->GetAudioSamplesCount();
285 const int channels = frame->GetAudioChannelsCount();
286 if (samples <= 0 || channels <= 0)
291 const int first_channel = channel >= 0 ? clampi(channel, 0, channels - 1) : 0;
292 const int last_channel = channel >= 0 ? first_channel + 1 : channels;
293 for (
int c = first_channel; c < last_channel; ++c) {
294 const float *audio = frame->GetAudioSampleBuffer()->getReadPointer(c);
295 for (
int i = 0; i < samples; ++i) {
296 const float magnitude = std::fabs(audio[i]);
297 total += magnitude * magnitude;
298 peak = std::max(peak, magnitude);
302 const float rms = std::sqrt(total / (samples * (last_channel - first_channel)));
303 const float mixed = std::max(rms * 3.6f, peak * 1.15f);
304 return std::pow(clampf(mixed * gain, 0.0f, 1.0f), 0.55f);
307 float band_level(
const std::vector<float>& bins,
float start,
float end) {
311 const int first = clampi(std::lround(start * bins.size()), 0,
static_cast<int>(bins.size()) - 1);
312 const int last = clampi(std::lround(end * bins.size()), first + 1,
static_cast<int>(bins.size()));
313 const float total = std::accumulate(bins.begin() + first, bins.begin() + last, 0.0f);
314 return clampf(total / (last - first), 0.0f, 1.0f);
317 std::vector<float> spectrum_bins(
const std::shared_ptr<Frame>& frame,
int bins,
float gain,
float smooth,
float low_hz,
float high_hz) {
318 std::vector<float> result(std::max(2, bins), 0.0f);
319 const int samples = frame->GetAudioSamplesCount();
320 const int channels = frame->GetAudioChannelsCount();
321 const int sample_rate = std::max(1, frame->SampleRate());
322 if (samples <= 4 || channels <= 0)
325 const int max_source_samples = std::min(samples, 2048);
326 const float nyquist = sample_rate * 0.5f;
327 low_hz = clampf(low_hz, 1.0f, nyquist);
328 high_hz = clampf(high_hz, low_hz + 1.0f, nyquist);
330 std::vector<const float*> channel_data;
331 channel_data.reserve(channels);
332 for (
int channel = 0; channel < channels; ++channel)
333 channel_data.push_back(frame->GetAudioSampleBuffer()->getReadPointer(channel));
335 std::vector<double> mixed_windowed(max_source_samples, 0.0);
336 for (
int sample = 0; sample < max_source_samples; ++sample) {
338 for (
int channel = 0; channel < channels; ++channel)
339 mixed += channel_data[channel][sample];
341 const double window = 0.5 - 0.5 * std::cos((2.0 * PI * sample) / (max_source_samples - 1));
342 mixed_windowed[sample] = mixed * window;
345 std::vector<float> magnitudes(result.size(), 0.0f);
346 const int result_size =
static_cast<int>(result.size());
347 #pragma omp parallel for if(result_size * max_source_samples >= 32768) schedule(static)
348 for (
int bin = 0; bin < result_size; ++bin) {
349 const float t = (bin + 0.5f) / result_size;
350 const float hz = low_hz * std::pow(high_hz / low_hz, t);
351 const double phase_delta = 2.0 * PI * hz / sample_rate;
352 const double step_real = std::cos(phase_delta);
353 const double step_imag = std::sin(phase_delta);
354 double phase_real = 1.0;
355 double phase_imag = 0.0;
359 for (
int sample = 0; sample < max_source_samples; ++sample) {
360 const double value = mixed_windowed[sample];
361 real += value * phase_real;
362 imag -= value * phase_imag;
364 const double next_real = phase_real * step_real - phase_imag * step_imag;
365 phase_imag = phase_real * step_imag + phase_imag * step_real;
366 phase_real = next_real;
369 magnitudes[bin] = clampf(std::sqrt(real * real + imag * imag) / max_source_samples * gain * 8.0f, 0.0f, 1.0f);
372 float previous = 0.0f;
373 for (
int bin = 0; bin < result_size; ++bin) {
374 result[bin] = previous * smooth + magnitudes[bin] * (1.0f - smooth);
375 previous = result[bin];
380 void draw_glow_path(QPainter& painter,
const QPainterPath&
path,
const Palette& palette,
float width,
float glow) {
382 QPen glow_pen(palette.glow, width + glow * 18.0f, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
383 painter.setPen(glow_pen);
384 painter.drawPath(
path);
386 QPen pen(palette.base, width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
388 painter.drawPath(
path);
391 void draw_glow_polyline(QPainter& painter,
const QPolygonF& points,
const Palette& palette,
float width,
float glow) {
392 if (points.size() < 2)
395 QPen glow_pen(palette.glow, width + glow * 14.0f, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
396 painter.setPen(glow_pen);
397 painter.drawPolyline(points);
399 QPen pen(palette.base, width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
401 painter.drawPolyline(points);
408 color((unsigned char)68, (unsigned char)170, (unsigned char)255, (unsigned char)255),
420 init_effect_details();
430 void AudioVisualization::init_effect_details()
435 info.
description =
"Render waveform, spectrum, and other transparent audio visualizations.";
442 const std::shared_ptr<QImage> frame_image = frame->GetImage();
443 int width = frame_image ? std::max(1, frame_image->width()) : 1;
444 int height = frame_image ? std::max(1, frame_image->height()) : 1;
445 if ((width <= 1 || height <= 1) && ParentTimeline()) {
453 const float intensity_value = clampf(
intensity.
GetValue(frame_number), 0.0f, 10.0f);
454 const float smoothing_value = clampf(
smoothing.
GetValue(frame_number), 0.0f, 1.0f);
455 const float detail_value = clampf(
detail.
GetValue(frame_number), 0.0f, 1.0f);
456 const float glow_value = clampf(
glow.
GetValue(frame_number), 0.0f, 1.0f);
464 const float low_hz = uses_frequency ? normalized_frequency_to_hz(
frequency_low.
GetValue(frame_number),
false) : 20.0f;
465 const float high_hz = uses_frequency
468 const int channels = std::max(1, frame->GetAudioChannelsCount());
475 auto visual = std::make_shared<QImage>(width, height,
476 use_filled_waveform_argb ? QImage::Format_ARGB32_Premultiplied : QImage::Format_RGBA8888_Premultiplied);
478 if (frame_image->width() == width && frame_image->height() == height)
479 *visual = frame_image->copy();
481 *visual = frame_image->scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
483 visual->fill(Qt::transparent);
486 QPainter painter(visual.get());
488 (frame_image->width() != width || frame_image->height() != height)) {
489 painter.setRenderHint(QPainter::SmoothPixmapTransform,
true);
492 const QColor base = color_at(
color, frame_number);
493 const float styled_glow = style_glow_amount(
style, glow_value);
494 const Palette palette = make_palette(base, styled_glow,
style, color_spread_value);
497 painter.fillRect(visual->rect(), palette.dark);
499 QLinearGradient grad(0, 0, 0, height);
500 grad.setColorAt(0.0, alpha_color(palette.dark, 0.0f));
501 grad.setColorAt(1.0, alpha_color(palette.dark, 0.55f));
502 painter.fillRect(visual->rect(), grad);
504 QLinearGradient grad(0, 0, width, height);
505 grad.setColorAt(0.0, alpha_color(palette.dark, 0.55f));
506 grad.setColorAt(1.0, alpha_color(palette.accent, 0.35f));
507 painter.fillRect(visual->rect(), grad);
510 if (frame->GetAudioSamplesCount() <= 0) {
512 frame->AddImage(visual);
516 const float gain = std::max(0.01f, intensity_value);
517 const float stroke = style_stroke_width(
style, styled_glow);
519 painter.setRenderHint(QPainter::Antialiasing, !is_waveform_mode || styled_glow > 0.01f || stroke > 1.05f);
521 if (is_waveform_mode) {
522 painter.setRenderHint(QPainter::Antialiasing,
false);
523 const int lanes = split ? std::min(channels, 8) : (overlay ? std::min(channels, 8) : 1);
524 const int columns = clampi(std::lround(width * (0.75f + detail_value * 0.25f)), 16, std::max(16, width));
525 for (
int lane = 0; lane < lanes; ++lane) {
526 const std::vector<float> values = (split || overlay) ? channel_samples(frame, lane, columns, gain, smoothing_value)
527 : combined_samples(frame, columns, gain, smoothing_value);
528 const float lane_top = split ? height * lane /
static_cast<float>(lanes) : 0.0f;
529 const float lane_height = split ? height /
static_cast<float>(lanes) :
static_cast<float>(height);
530 const float center_y = lane_top + lane_height * 0.5f;
531 const float amplitude = lane_height * 0.44f;
533 ? rainbow_color(base, lane /
static_cast<float>(std::max(1, lanes)), color_spread_value)
534 : (overlay ? mix_color(base, hue_shift(base, lane * 18), color_spread_value * 0.18f) : (lane % 2 ? palette.accent : base));
535 const Palette lane_palette = make_palette(lane_base, styled_glow,
style, color_spread_value);
538 top_edge.reserve(
static_cast<int>(values.size()));
539 for (
int i = 0; i < static_cast<int>(values.size()); ++i) {
540 const float x = (values.size() <= 1) ? 0.0f : (width - 1) * i /
static_cast<float>(values.size() - 1);
541 top_edge.append(QPointF(x, center_y - values[i] * amplitude));
544 const float alpha_scale = overlay ? 0.48f : 1.0f;
547 painter.setPen(Qt::NoPen);
548 if (styled_glow > 0.01f) {
550 QLinearGradient glow_grad(0, 0, width, 0);
551 set_rainbow_stops(glow_grad, base, color_spread_value, 0.78f * alpha_scale);
552 painter.setBrush(QBrush(glow_grad));
554 painter.setBrush(alpha_color(lane_palette.glow, 0.78f * alpha_scale));
556 const int glow_step = 4;
557 const int glow_pad = std::max(1,
static_cast<int>(std::ceil(styled_glow * 5.0f)));
558 for (
int i = 0; i < static_cast<int>(values.size()); i += glow_step) {
559 const int x0 = (i * width) /
static_cast<int>(values.size());
560 const int x1 = ((std::min<int>(i + glow_step, values.size())) * width) /
static_cast<int>(values.size());
561 float envelope = 0.0f;
562 for (
int j = i; j < std::min<int>(i + glow_step, values.size()); ++j) {
563 const float magnitude = std::fabs(values[j]) * amplitude;
564 envelope = std::max(envelope, values[j] == 0.0f ? 0.0f : std::max(1.0f, magnitude));
566 const int y0 = clampi(
static_cast<int>(std::floor(center_y - envelope)) - glow_pad, 0, height);
567 const int y1 = clampi(
static_cast<int>(std::ceil(center_y + envelope)) + glow_pad, y0 + 1, height);
568 painter.drawRect(QRect(x0, y0, std::max(1, x1 - x0), y1 - y0));
573 QLinearGradient fill_grad(0, 0, width, 0);
574 set_rainbow_stops(fill_grad, base, color_spread_value, style_fill_alpha(
style) * alpha_scale);
575 painter.setBrush(QBrush(fill_grad));
577 painter.setBrush(vertical_style_fill(0, lane_top, 0, lane_top + lane_height, lane_palette,
style, alpha_scale));
579 for (
int i = 0; i < static_cast<int>(values.size()); ++i) {
580 const int x0 = (i * width) /
static_cast<int>(values.size());
581 const int x1 = ((i + 1) * width) /
static_cast<int>(values.size());
582 const float magnitude = std::fabs(values[i]) * amplitude;
583 const float envelope = values[i] == 0.0f ? 0.0f : std::max(1.0f, magnitude);
584 const int y0 = clampi(
static_cast<int>(std::floor(center_y - envelope)), 0, height);
585 const int y1 = clampi(
static_cast<int>(std::ceil(center_y + envelope)), y0 + 1, height);
586 painter.drawRect(QRect(x0, y0, std::max(1, x1 - x0), y1 - y0));
589 const float line_width = std::max(1.0f, stroke);
591 if (styled_glow > 0.01f) {
592 QLinearGradient glow_grad(0, 0, width, 0);
593 set_rainbow_stops(glow_grad, base, color_spread_value, 0.88f * alpha_scale);
594 painter.setPen(QPen(QBrush(glow_grad), line_width + styled_glow * 16.0f, Qt::SolidLine, Qt::FlatCap));
595 painter.drawPolyline(top_edge);
597 QLinearGradient line_grad(0, 0, width, 0);
598 set_rainbow_stops(line_grad, base, color_spread_value, alpha_scale);
599 painter.setPen(QPen(QBrush(line_grad), line_width, Qt::SolidLine, Qt::FlatCap));
600 painter.drawPolyline(top_edge);
602 if (styled_glow > 0.01f) {
603 painter.setPen(QPen(alpha_color(lane_palette.glow, 0.88f * alpha_scale),
604 line_width + styled_glow * 16.0f, Qt::SolidLine, Qt::FlatCap));
605 painter.drawPolyline(top_edge);
607 painter.setPen(QPen(alpha_color(lane_palette.base, alpha_scale),
608 line_width, Qt::SolidLine, Qt::FlatCap));
609 painter.drawPolyline(top_edge);
614 const int bars = clampi(std::lround(16 + detail_value * 112), 8, std::max(8, width / 3));
615 const std::vector<float> bins = spectrum_bins(frame, bars, gain, smoothing_value, low_hz, high_hz);
616 const float gap = std::max(1.0f, width /
static_cast<float>(bars) * 0.18f);
617 const float bar_width = std::max(1.0f, width /
static_cast<float>(bars) - gap);
618 for (
int i = 0; i < bars; ++i) {
619 const float x = i * (bar_width + gap);
620 const float h = std::max(1.0f, bins[i] * height * 0.88f);
621 QRectF rect(x, height - h, bar_width, h);
623 ? make_palette(rainbow_color(base, i /
static_cast<float>(std::max(1, bars - 1)), color_spread_value), styled_glow,
style, color_spread_value)
625 if (styled_glow > 0.01f)
626 painter.fillRect(rect.adjusted(-styled_glow * 2.0f, -styled_glow * 5.0f, styled_glow * 2.0f, 0), alpha_color(bar_palette.glow, 0.55f));
627 painter.fillRect(rect, vertical_style_fill(0, rect.top(), 0, height, bar_palette,
style));
630 const int bins_count = clampi(std::lround(56 + detail_value * 220), 40, std::max(40, width / 2));
631 const std::vector<float> bins = spectrum_bins(frame, bins_count, gain, smoothing_value, low_hz, high_hz);
632 std::vector<QPointF> points;
633 points.reserve(bins_count);
634 for (
int i = 0; i < bins_count; ++i) {
635 const float x = i * width /
static_cast<float>(std::max(1, bins_count - 1));
636 const float h = std::max(1.0f, bins[i] * height * 0.84f);
637 points.emplace_back(x, height - h);
641 if (!points.empty()) {
642 ridge.moveTo(points.front());
643 for (
size_t i = 0; i + 1 < points.size(); ++i) {
644 const QPointF& p0 = points[i == 0 ? i : i - 1];
645 const QPointF& p1 = points[i];
646 const QPointF& p2 = points[i + 1];
647 const QPointF& p3 = points[std::min(i + 2, points.size() - 1)];
648 const QPointF c1 = p1 + (p2 - p0) / 6.0;
649 const QPointF c2 = p2 - (p3 - p1) / 6.0;
650 ridge.cubicTo(c1, c2, p2);
654 QBrush terrain_brush;
656 QLinearGradient rainbow_fill(0, 0, width, 0);
657 set_rainbow_stops(rainbow_fill, base, color_spread_value, style_fill_alpha(
style));
658 terrain_brush = QBrush(rainbow_fill);
660 terrain_brush = vertical_style_fill(0, 0, 0, height, palette,
style);
662 if (points.size() > 1) {
663 QImage fill_layer(width, height, QImage::Format_RGBA8888_Premultiplied);
664 fill_layer.fill(Qt::transparent);
665 QPainter fill_painter(&fill_layer);
666 fill_painter.fillRect(fill_layer.rect(), terrain_brush);
669 QImage mask(width, height, QImage::Format_RGBA8888_Premultiplied);
670 mask.fill(Qt::transparent);
671 QPainter mask_painter(&mask);
672 mask_painter.setRenderHint(QPainter::Antialiasing,
false);
673 mask_painter.setPen(QPen(Qt::white, 1.0f));
674 for (
int x = 0; x < width; ++x) {
675 const float position = x * (points.size() - 1) /
static_cast<float>(std::max(1, width - 1));
676 const int index = clampi(
static_cast<int>(std::floor(
position)), 0,
static_cast<int>(points.size()) - 2);
678 const float y = points[index].y() * (1.0f - mix) + points[index + 1].y() * mix;
679 mask_painter.drawLine(QPointF(x + 0.5f, y), QPointF(x + 0.5f, height));
683 fill_painter.begin(&fill_layer);
684 fill_painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
685 fill_painter.drawImage(0, 0, mask);
687 painter.drawImage(0, 0, fill_layer);
689 const float ridge_width = std::max(1.0f, stroke * 0.75f);
691 QLinearGradient glow_grad(0, 0, width, 0);
692 set_rainbow_stops(glow_grad, base, color_spread_value, 0.45f);
693 if (styled_glow > 0.01f) {
694 painter.setPen(QPen(QBrush(glow_grad), ridge_width + styled_glow * 10.0f, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
695 painter.drawPath(ridge);
697 QLinearGradient ridge_grad(0, 0, width, 0);
698 set_rainbow_stops(ridge_grad, base, color_spread_value);
699 painter.setPen(QPen(QBrush(ridge_grad), ridge_width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
700 painter.drawPath(ridge);
702 draw_glow_path(painter, ridge, palette, ridge_width, styled_glow * 0.55f);
705 const int segments = clampi(std::lround(48 + detail_value * 192), 24, 256);
706 const std::vector<float> bins = spectrum_bins(frame, segments, gain, smoothing_value, low_hz, high_hz);
707 const QPointF center(width * 0.5, height * 0.5);
708 const float radius = std::min(width, height) * 0.24f;
709 const float spike = std::min(width, height) * 0.24f;
711 for (
int i = 0; i <= segments; ++i) {
712 const int idx = i % segments;
713 const double angle = -PI * 0.5 + (2.0 * PI * i / segments);
714 const float r = radius + bins[idx] * spike;
715 const QPointF p(center.x() + std::cos(angle) * r, center.y() + std::sin(angle) * r);
721 QConicalGradient grad(center, -90);
723 set_rainbow_stops(grad, base, color_spread_value);
725 grad.setColorAt(0.0, palette.base);
726 grad.setColorAt(0.45, palette.light);
727 grad.setColorAt(1.0, palette.accent);
729 if (styled_glow > 0.01f) {
730 QBrush glow_brush(palette.glow);
732 QConicalGradient glow_grad(center, -90);
733 set_rainbow_stops(glow_grad, base, color_spread_value, 0.48f);
734 glow_brush = QBrush(glow_grad);
736 QPen glow_pen(glow_brush, stroke + styled_glow * 16.0f, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
737 painter.setPen(glow_pen);
738 painter.drawPath(ring);
740 painter.setPen(QPen(QBrush(grad), stroke + 1.0f, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
741 painter.drawPath(ring);
743 const int bars = clampi(std::lround(36 + detail_value * 156), 24, 220);
744 const std::vector<float> bins = spectrum_bins(frame, bars, gain, smoothing_value, low_hz, high_hz);
745 const QPointF center(width * 0.5, height * 0.5);
746 const float min_dimension = std::min(width, height);
747 const float inner_radius = min_dimension * 0.22f;
748 const float max_length = min_dimension * 0.28f;
749 const float bar_width = std::max(1.0f,
static_cast<float>((2.0 * PI * inner_radius / bars) * 0.55));
750 for (
int i = 0; i < bars; ++i) {
751 const double angle = -PI * 0.5 + (2.0 * PI * i / bars);
752 const float length = std::max(min_dimension * 0.012f, bins[i] * max_length);
753 const QPointF
start(center.x() + std::cos(angle) * inner_radius, center.y() + std::sin(angle) * inner_radius);
754 const QPointF
end(center.x() + std::cos(angle) * (inner_radius + length), center.y() + std::sin(angle) * (inner_radius + length));
756 ? rainbow_color(base, i /
static_cast<float>(std::max(1, bars - 1)), color_spread_value)
757 : mix_color(palette.base, palette.light, bins[i] * 0.35f);
758 if (styled_glow > 0.01f) {
760 painter.setPen(QPen(glow_color, bar_width + styled_glow * 8.0f, Qt::SolidLine, Qt::RoundCap));
763 painter.setPen(QPen(c, bar_width, Qt::SolidLine, Qt::RoundCap));
769 auto result =
GetFrame(frame, frame_number);
773 painter.setRenderHint(QPainter::Antialiasing,
false);
774 const int count = clampi(std::lround(96 + detail_value * 224), 64, 320);
775 const int samples = frame->GetAudioSamplesCount();
776 const float *left = frame->GetAudioSampleBuffer()->getReadPointer(0);
777 const float *right = frame->GetAudioSampleBuffer()->getReadPointer(1);
779 trace.reserve(count);
780 float previous_x = width * 0.5f;
781 float previous_y = height * 0.5f;
782 for (
int i = 0; i < count; ++i) {
783 const int sample = clampi((i * samples) / count, 0, samples - 1);
784 const float raw_x = width * 0.5f + clampf((left[sample] - right[sample]) * gain * 0.75f, -1.0f, 1.0f) * width * 0.38f;
785 const float raw_y = height * 0.5f - clampf((left[sample] + right[sample]) * gain * 0.38f, -1.0f, 1.0f) * height * 0.42f;
786 const float x = previous_x * 0.72f + raw_x * 0.28f;
787 const float y = previous_y * 0.72f + raw_y * 0.28f;
788 trace.append(QPointF(x, y));
792 painter.setPen(QPen(alpha_color(palette.dark, 0.35f), 1.0f));
793 painter.drawLine(QPointF(width * 0.5f, height * 0.12f), QPointF(width * 0.5f, height * 0.88f));
794 painter.drawLine(QPointF(width * 0.12f, height * 0.5f), QPointF(width * 0.88f, height * 0.5f));
795 const float trace_width = std::max(1.0f, stroke);
797 QRectF rainbow_bounds = trace.boundingRect();
798 const qreal min_span = std::min(width, height) * 0.22;
799 if (rainbow_bounds.width() < min_span)
800 rainbow_bounds.adjust((rainbow_bounds.width() - min_span) * 0.5, 0.0, (min_span - rainbow_bounds.width()) * 0.5, 0.0);
801 if (rainbow_bounds.height() < min_span)
802 rainbow_bounds.adjust(0.0, (rainbow_bounds.height() - min_span) * 0.5, 0.0, (min_span - rainbow_bounds.height()) * 0.5);
803 rainbow_bounds = rainbow_bounds.intersected(QRectF(0, 0, width, height));
804 if (styled_glow > 0.01f) {
805 QLinearGradient glow_grad(rainbow_bounds.left(), rainbow_bounds.center().y(), rainbow_bounds.right(), rainbow_bounds.center().y());
806 set_rainbow_stops(glow_grad, base, color_spread_value, 0.42f);
807 painter.setPen(QPen(QBrush(glow_grad), trace_width + styled_glow * 10.0f, Qt::SolidLine, Qt::FlatCap, Qt::BevelJoin));
808 painter.drawPolyline(trace);
810 QLinearGradient trace_grad(rainbow_bounds.left(), rainbow_bounds.center().y(), rainbow_bounds.right(), rainbow_bounds.center().y());
811 set_rainbow_stops(trace_grad, base, color_spread_value);
812 painter.setPen(QPen(QBrush(trace_grad), trace_width, Qt::SolidLine, Qt::FlatCap, Qt::BevelJoin));
813 painter.drawPolyline(trace);
815 if (styled_glow > 0.01f) {
816 QPen glow_pen(palette.glow, trace_width + styled_glow * 10.0f, Qt::SolidLine, Qt::FlatCap, Qt::BevelJoin);
817 painter.setPen(glow_pen);
818 painter.drawPolyline(trace);
820 QPen pen(palette.base, trace_width, Qt::SolidLine, Qt::FlatCap, Qt::BevelJoin);
822 painter.drawPolyline(trace);
825 const int count = clampi(std::lround(220 + detail_value * 920), 160, 1280);
826 const float level = reactive_level(frame, -1, gain);
827 const std::vector<float> bands = spectrum_bins(frame, 36, gain, smoothing_value, low_hz, high_hz);
828 const float low = band_level(bands, 0.0f, 0.18f);
829 const float mid = band_level(bands, 0.18f, 0.56f);
830 const float high = band_level(bands, 0.56f, 1.0f);
831 const float time = frame_number * (0.028f + level * 0.075f);
832 const QPointF center(width * 0.5f, height * (0.55f - level * 0.08f));
833 const float max_radius = std::sqrt(width * width + height * height) * 0.42f;
834 const float swirl = 0.65f + low * 2.4f;
835 const float twist = 0.45f + mid * 2.1f;
836 const float sparkle = 0.2f + high * 1.8f;
837 std::uniform_real_distribution<float> dist(0.0f, 1.0f);
838 for (
int i = 0; i < count; ++i) {
839 std::mt19937 rng(
static_cast<uint32_t
>((i + 1) * 747796405U));
840 const float seed = dist(rng);
841 const float arm = std::floor(dist(rng) * 5.0f);
842 const float age = std::fmod(seed + time * (0.18f + dist(rng) * 0.62f + level * 0.55f), 1.0f);
843 const float eased_age = std::pow(age, 0.58f);
844 const float base_angle = (arm / 5.0f) * 2.0f * PI + dist(rng) * 0.42f;
845 const float orbit = base_angle + eased_age * swirl * PI + std::sin(time + seed * 12.0f) * twist * 0.22f;
846 const float wave = std::sin(eased_age * PI * (2.0f + sparkle) + seed * 10.0f);
847 const float radius = (0.06f + eased_age * (0.88f + low * 0.35f)) * max_radius * (0.62f + level * 0.74f);
848 const float ribbon = wave * (height * 0.018f + mid * height * 0.055f);
849 const QPointF direction(std::cos(orbit), std::sin(orbit));
850 const QPointF normal(-direction.y(), direction.x());
852 center.x() + direction.x() * radius + normal.x() * ribbon,
853 center.y() + direction.y() * radius + normal.y() * ribbon);
854 const float trail_length = 16.0f + low * 58.0f + level * 64.0f;
856 p.x() - direction.x() * trail_length - normal.x() * ribbon * 0.35f,
857 p.y() - direction.y() * trail_length - normal.y() * ribbon * 0.35f);
858 const float fade = std::sin(age * PI);
859 const float size = 0.42f + fade * (0.95f + high * 2.25f + level * 2.0f);
861 ? rainbow_color(base, std::fmod(age + arm / 5.0f, 1.0f), color_spread_value)
862 : mix_color(palette.base, palette.light, fade * (0.25f + color_spread_value * 0.35f));
863 c = alpha_color(c, (0.12f + level * 0.52f + high * 0.22f) * fade);
864 QPen trail_pen(alpha_color(c, 0.26f + mid * 0.16f), std::max(0.45f, size * (0.28f + low * 0.12f)), Qt::SolidLine, Qt::RoundCap);
865 painter.setPen(trail_pen);
866 painter.drawLine(tail, p);
868 painter.setBrush(alpha_color(c, 1.0f));
870 QRadialGradient grad(p, size * (2.2f + styled_glow * 4.0f));
871 grad.setColorAt(0.0, c);
872 grad.setColorAt(1.0, alpha_color(c, 0.0f));
873 painter.setBrush(grad);
875 painter.setPen(Qt::NoPen);
876 painter.drawEllipse(p, size * 2.0f, size * 2.0f);
879 const int meters = split ? std::min(channels, 8) : 1;
880 const int segments = clampi(std::lround(8 + detail_value * 32), 6, 48);
881 for (
int meter = 0; meter < meters; ++meter) {
882 const float level = reactive_level(frame, split ? meter : -1, gain);
883 const float lane_width = width /
static_cast<float>(meters);
884 const float x0 = meter * lane_width + lane_width * 0.08f;
885 const float seg_gap = std::max(1.0f, height * 0.01f);
886 const float seg_h = (height * 0.88f - seg_gap * (segments - 1)) / segments;
887 for (
int segment = 0; segment < segments; ++segment) {
888 const float t = (segment + 1) /
static_cast<float>(segments);
889 const bool on = t <= level;
894 c = t > 0.82f ? QColor(255, 86, 68, base.alpha()) : (t > 0.55f ? mix_color(palette.accent, QColor(255, 214, 90, base.alpha()), (t - 0.55f) / 0.27f) : mix_color(palette.base, palette.light, t * 1.5f));
897 c = alpha_color(palette.dark, 0.25f);
898 const float y = height * 0.94f - (segment + 1) * seg_h - segment * seg_gap;
899 painter.fillRect(QRectF(x0, y, lane_width * 0.84f, seg_h), c);
905 frame->AddImage(visual);
917 root[
"style"] =
style;
938 catch (
const std::exception& e)
940 throw InvalidJSON(
"JSON is invalid (missing keys or invalid data types)");
946 if (!root[
"visualization_type"].isNull())
948 if (!root[
"style"].isNull())
949 style = root[
"style"].asInt();
950 if (!root[
"color"].isNull())
952 if (!root[
"intensity"].isNull())
954 if (!root[
"smoothing"].isNull())
956 if (!root[
"detail"].isNull())
958 if (!root[
"glow"].isNull())
960 if (!root[
"color_spread"].isNull())
962 if (!root[
"color_mode"].isNull())
964 if (!root[
"channel_layout"].isNull())
966 if (!root[
"frequency_low"].isNull())
968 if (!root[
"frequency_high"].isNull())
970 if (!root[
"background"].isNull())
1026 return root.toStyledString();