OpenShot Library | libopenshot 0.5.0
Loading...
Searching...
No Matches
AnalogTape.cpp
Go to the documentation of this file.
1
8
9// Copyright (c) 2008-2025 OpenShot Studios, LLC
10//
11// SPDX-License-Identifier: LGPL-3.0-or-later
12
13#include "AnalogTape.h"
14#include "Clip.h"
15#include "Exceptions.h"
16#include "ReaderBase.h"
17#include "Timeline.h"
18
19#include <algorithm>
20#include <cmath>
21
22using namespace openshot;
23
25 : tracking(0.55), bleed(0.65), softness(0.40), noise(0.50), stripe(0.25f),
26 staticBands(0.20f), seed_offset(0) {
27 init_effect_details();
28}
29
31 Keyframe st, Keyframe sb, int seed)
32 : tracking(t), bleed(b), softness(s), noise(n), stripe(st),
33 staticBands(sb), seed_offset(seed) {
34 init_effect_details();
35}
36
37void AnalogTape::init_effect_details() {
39 info.class_name = "AnalogTape";
40 info.name = "Analog Tape";
41 info.description = "Vintage home video wobble, bleed, and grain.";
42 info.has_video = true;
43 info.has_audio = false;
44}
45
46static inline float lerp(float a, float b, float t) { return a + (b - a) * t; }
47
48std::shared_ptr<Frame> AnalogTape::GetFrame(std::shared_ptr<Frame> frame,
49 int64_t frame_number) {
50 std::shared_ptr<QImage> img = frame->GetImage();
51 int w = img->width();
52 int h = img->height();
53 int Uw = (w + 1) / 2;
54 int stride = img->bytesPerLine() / 4;
55 uint32_t *base = reinterpret_cast<uint32_t *>(img->bits());
56
57 if (w != last_w || h != last_h) {
58 last_w = w;
59 last_h = h;
60 Y.resize(w * h);
61 U.resize(Uw * h);
62 V.resize(Uw * h);
63 tmpY.resize(w * h);
64 tmpU.resize(Uw * h);
65 tmpV.resize(Uw * h);
66 dx.resize(h);
67 }
68
69
70#ifdef _OPENMP
71#pragma omp parallel for
72#endif
73 for (int y = 0; y < h; ++y) {
74 uint32_t *row = base + y * stride;
75 float *yrow = &Y[y * w];
76 float *urow = &U[y * Uw];
77 float *vrow = &V[y * Uw];
78 for (int x2 = 0; x2 < Uw; ++x2) {
79 int x0 = x2 * 2;
80 uint32_t p0 = row[x0];
81 float r0 = ((p0 >> 16) & 0xFF) / 255.0f;
82 float g0 = ((p0 >> 8) & 0xFF) / 255.0f;
83 float b0 = (p0 & 0xFF) / 255.0f;
84 float y0 = 0.299f * r0 + 0.587f * g0 + 0.114f * b0;
85 float u0 = -0.14713f * r0 - 0.28886f * g0 + 0.436f * b0;
86 float v0 = 0.615f * r0 - 0.51499f * g0 - 0.10001f * b0;
87 yrow[x0] = y0;
88
89 float u, v;
90 if (x0 + 1 < w) {
91 uint32_t p1 = row[x0 + 1];
92 float r1 = ((p1 >> 16) & 0xFF) / 255.0f;
93 float g1 = ((p1 >> 8) & 0xFF) / 255.0f;
94 float b1 = (p1 & 0xFF) / 255.0f;
95 float y1 = 0.299f * r1 + 0.587f * g1 + 0.114f * b1;
96 float u1 = -0.14713f * r1 - 0.28886f * g1 + 0.436f * b1;
97 float v1 = 0.615f * r1 - 0.51499f * g1 - 0.10001f * b1;
98 yrow[x0 + 1] = y1;
99 u = (u0 + u1) * 0.5f;
100 v = (v0 + v1) * 0.5f;
101 } else {
102 u = u0;
103 v = v0;
104 }
105 urow[x2] = u;
106 vrow[x2] = v;
107 }
108 }
109
110 Fraction fps(1, 1);
111 Clip *clip = (Clip *)ParentClip();
112 Timeline *timeline = nullptr;
113 if (clip && clip->ParentTimeline())
114 timeline = (Timeline *)clip->ParentTimeline();
115 else if (ParentTimeline())
116 timeline = (Timeline *)ParentTimeline();
117 if (timeline)
118 fps = timeline->info.fps;
119 else if (clip && clip->Reader())
120 fps = clip->Reader()->info.fps;
121 double fps_d = fps.ToDouble();
122 double t = fps_d > 0 ? frame_number / fps_d : frame_number;
123
124 const float k_track = tracking.GetValue(frame_number);
125 const float k_bleed = bleed.GetValue(frame_number);
126 const float k_soft = softness.GetValue(frame_number);
127 const float k_noise = noise.GetValue(frame_number);
128 const float k_stripe = stripe.GetValue(frame_number);
129 const float k_bands = staticBands.GetValue(frame_number);
130
131 int r_y = std::round(lerp(0.0f, 2.0f, k_soft));
132 if (k_noise > 0.6f)
133 r_y = std::min(r_y, 1);
134 if (r_y > 0) {
135#ifdef _OPENMP
136#pragma omp parallel for
137#endif
138 for (int y = 0; y < h; ++y)
139 box_blur_row(&Y[y * w], &tmpY[y * w], w, r_y);
140 Y.swap(tmpY);
141 }
142
143 float shift = lerp(0.0f, 2.5f, k_bleed);
144 int r_c = std::round(lerp(0.0f, 3.0f, k_bleed));
145 float sat = 1.0f - 0.30f * k_bleed;
146 float shift_h = shift * 0.5f;
147#ifdef _OPENMP
148#pragma omp parallel for
149#endif
150 for (int y = 0; y < h; ++y) {
151 const float *srcU = &U[y * Uw];
152 const float *srcV = &V[y * Uw];
153 float *dstU = &tmpU[y * Uw];
154 float *dstV = &tmpV[y * Uw];
155 for (int x = 0; x < Uw; ++x) {
156 float xs = std::clamp(x - shift_h, 0.0f, float(Uw - 1));
157 int x0 = int(xs);
158 int x1 = std::min(x0 + 1, Uw - 1);
159 float t = xs - x0;
160 dstU[x] = srcU[x0] * (1 - t) + srcU[x1] * t;
161 dstV[x] = srcV[x0] * (1 - t) + srcV[x1] * t;
162 }
163 }
164 U.swap(tmpU);
165 V.swap(tmpV);
166
167 if (r_c > 0) {
168#ifdef _OPENMP
169#pragma omp parallel for
170#endif
171 for (int y = 0; y < h; ++y)
172 box_blur_row(&U[y * Uw], &tmpU[y * Uw], Uw, r_c);
173 U.swap(tmpU);
174#ifdef _OPENMP
175#pragma omp parallel for
176#endif
177 for (int y = 0; y < h; ++y)
178 box_blur_row(&V[y * Uw], &tmpV[y * Uw], Uw, r_c);
179 V.swap(tmpV);
180 }
181
182 uint32_t SEED = fnv1a_32(Id()) ^ (uint32_t)seed_offset;
183 uint32_t schedSalt = (uint32_t)(k_bands * 64.0f) ^
184 ((uint32_t)(k_stripe * 64.0f) << 8) ^
185 ((uint32_t)(k_noise * 64.0f) << 16);
186 uint32_t SCHED_SEED = SEED ^ fnv1a_32(schedSalt, 0x9e3779b9u);
187 const float PI = 3.14159265358979323846f;
188
189 float sigmaY = lerp(0.0f, 0.08f, k_noise);
190 const float decay = 0.88f + 0.08f * k_noise;
191 const float amp = 0.18f * k_noise;
192 const float baseP = 0.0025f + 0.02f * k_noise;
193
194 float Hfixed = lerp(0.0f, 0.12f * h, k_stripe);
195 float Gfixed = 0.10f * k_stripe;
196 float Nfixed = 1.0f + 1.5f * k_stripe;
197
198 float rate = 0.4f * k_bands;
199 int dur_frames = std::round(lerp(1.0f, 6.0f, k_bands));
200 float Hburst = lerp(0.06f * h, 0.25f * h, k_bands);
201 float Gburst = lerp(0.10f, 0.25f, k_bands);
202 float sat_band = lerp(0.8f, 0.5f, k_bands);
203 float Nburst = 1.0f + 2.0f * k_bands;
204
205 struct Band { float center; double t0; };
206 std::vector<Band> bands;
207 if (k_bands > 0.0f && rate > 0.0f) {
208 const double win_len = 0.25;
209 int win_idx = int(t / win_len);
210 double lambda = rate * win_len *
211 (0.25 + 1.5f * row_density(SCHED_SEED, frame_number, 0));
212 double prob_ge1 = 1.0 - std::exp(-lambda);
213 double prob_ge2 = 1.0 - std::exp(-lambda) - lambda * std::exp(-lambda);
214
215 auto spawn_band = [&](int kseed) {
216 float r1 = hash01(SCHED_SEED, uint32_t(win_idx), 11 + kseed, 0);
217 float start = r1 * win_len;
218 float center =
219 hash01(SCHED_SEED, uint32_t(win_idx), 12 + kseed, 0) * (h - Hburst) +
220 0.5f * Hburst;
221 double t0 = win_idx * win_len + start;
222 double t1 = t0 + dur_frames / (fps_d > 0 ? fps_d : 1.0);
223 if (t >= t0 && t < t1)
224 bands.push_back({center, t0});
225 };
226
227 float r = hash01(SCHED_SEED, uint32_t(win_idx), 9, 0);
228 if (r < prob_ge1)
229 spawn_band(0);
230 if (r < prob_ge2)
231 spawn_band(1);
232 }
233
234 float ft = 2.0f;
235 int kf = int(std::floor(t * ft));
236 float a = float(t * ft - kf);
237
238#ifdef _OPENMP
239#pragma omp parallel for
240#endif
241 for (int y = 0; y < h; ++y) {
242 float bandF = 0.0f;
243 if (Hfixed > 0.0f && y >= h - Hfixed)
244 bandF = (y - (h - Hfixed)) / std::max(1.0f, Hfixed);
245 float burstF = 0.0f;
246 for (const auto &b : bands) {
247 float halfH = Hburst * 0.5f;
248 float dist = std::abs(y - b.center);
249 float profile = std::max(0.0f, 1.0f - dist / halfH);
250 float life = float((t - b.t0) * fps_d);
251 float env = (life < 1.0f)
252 ? life
253 : (life < dur_frames - 1 ? 1.0f
254 : std::max(0.0f, dur_frames - life));
255 burstF = std::max(burstF, profile * env);
256 }
257
258 float sat_row = 1.0f - (1.0f - sat_band) * burstF;
259 if (burstF > 0.0f && sat_row != 1.0f) {
260 float *urow = &U[y * Uw];
261 float *vrow = &V[y * Uw];
262 for (int xh = 0; xh < Uw; ++xh) {
263 urow[xh] *= sat_row;
264 vrow[xh] *= sat_row;
265 }
266 }
267
268 float rowBias = row_density(SEED, frame_number, y);
269 float p = baseP * (0.25f + 1.5f * rowBias);
270 p *= (1.0f + 1.5f * bandF + 2.0f * burstF);
271
272 float hum = 0.008f * k_noise *
273 std::sin(2 * PI * (y * (6.0f / h) + 0.08f * t));
274 uint32_t s0 = SEED ^ 0x9e37u * kf ^ 0x85ebu * y;
275 uint32_t s1 = SEED ^ 0x9e37u * (kf + 1) ^ 0x85ebu * y ^ 0x1234567u;
276 auto step = [](uint32_t &s) {
277 s ^= s << 13;
278 s ^= s >> 17;
279 s ^= s << 5;
280 return s;
281 };
282 float lift = Gfixed * bandF + Gburst * burstF;
283 float rowSigma = sigmaY * (1 + (Nfixed - 1) * bandF +
284 (Nburst - 1) * burstF);
285 float k = 0.15f + 0.35f * hash01(SEED, uint32_t(frame_number), y, 777);
286 float sL = 0.0f, sR = 0.0f;
287 for (int x = 0; x < w; ++x) {
288 if (hash01(SEED, uint32_t(frame_number), y, x) < p)
289 sL = 1.0f;
290 if (hash01(SEED, uint32_t(frame_number), y, w - 1 - x) < p * 0.7f)
291 sR = 1.0f;
292 float n = ((step(s0) & 0xFFFFFF) / 16777215.0f) * (1 - a) +
293 ((step(s1) & 0xFFFFFF) / 16777215.0f) * a;
294 int idx = y * w + x;
295 float mt = std::clamp((Y[idx] - 0.2f) / (0.8f - 0.2f), 0.0f, 1.0f);
296 float val = Y[idx] + lift + rowSigma * (2 * n - 1) *
297 (0.6f + 0.4f * mt) + hum;
298 float streak = amp * (sL + sR);
299 float newY = val + streak * (k + (1.0f - val));
300 Y[idx] = std::clamp(newY, 0.0f, 1.0f);
301 sL *= decay;
302 sR *= decay;
303 }
304 }
305
306 float A = lerp(0.0f, 3.0f, k_track); // pixels
307 float f = lerp(0.25f, 1.2f, k_track); // Hz
308 float Hsk = lerp(0.0f, 0.10f * h, k_track); // pixels
309 float S = lerp(0.0f, 5.0f, k_track); // pixels
310 float phase = 2 * PI * (f * t) + 0.7f * (SEED * 0.001f);
311 for (int y = 0; y < h; ++y) {
312 float base = A * std::sin(2 * PI * 0.0035f * y + phase);
313 float skew = (y >= h - Hsk)
314 ? S * ((y - (h - Hsk)) / std::max(1.0f, Hsk))
315 : 0.0f;
316 dx[y] = base + skew;
317 }
318
319 auto remap_line = [&](const float *src, float *dst, int width, float scale) {
320#ifdef _OPENMP
321#pragma omp parallel for
322#endif
323 for (int y = 0; y < h; ++y) {
324 float off = dx[y] * scale;
325 const float *s = src + y * width;
326 float *d = dst + y * width;
327 int start = std::max(0, int(std::ceil(-off)));
328 int end = std::min(width, int(std::floor(width - off)));
329 float xs = start + off;
330 int x0 = int(xs);
331 float t = xs - x0;
332 for (int x = start; x < end; ++x) {
333 int x1 = x0 + 1;
334 d[x] = s[x0] * (1 - t) + s[x1] * t;
335 xs += 1.0f;
336 x0 = int(xs);
337 t = xs - x0;
338 }
339 for (int x = 0; x < start; ++x)
340 d[x] = s[0];
341 for (int x = end; x < width; ++x)
342 d[x] = s[width - 1];
343 }
344 };
345
346 remap_line(Y.data(), tmpY.data(), w, 1.0f);
347 Y.swap(tmpY);
348 remap_line(U.data(), tmpU.data(), Uw, 0.5f);
349 U.swap(tmpU);
350 remap_line(V.data(), tmpV.data(), Uw, 0.5f);
351 V.swap(tmpV);
352
353#ifdef _OPENMP
354#pragma omp parallel for
355#endif
356 for (int y = 0; y < h; ++y) {
357 float *yrow = &Y[y * w];
358 float *urow = &U[y * Uw];
359 float *vrow = &V[y * Uw];
360 uint32_t *row = base + y * stride;
361 for (int x = 0; x < w; ++x) {
362 float xs = x * 0.5f;
363 int x0 = int(xs);
364 int x1 = std::min(x0 + 1, Uw - 1);
365 float t = xs - x0;
366 float u = (urow[x0] * (1 - t) + urow[x1] * t) * sat;
367 float v = (vrow[x0] * (1 - t) + vrow[x1] * t) * sat;
368 float yv = yrow[x];
369 float r = yv + 1.13983f * v;
370 float g = yv - 0.39465f * u - 0.58060f * v;
371 float b = yv + 2.03211f * u;
372 int R = int(std::clamp(r, 0.0f, 1.0f) * 255.0f);
373 int G = int(std::clamp(g, 0.0f, 1.0f) * 255.0f);
374 int B = int(std::clamp(b, 0.0f, 1.0f) * 255.0f);
375 uint32_t A = row[x] & 0xFF000000u;
376 row[x] = A | (R << 16) | (G << 8) | B;
377 }
378 }
379
380 return frame;
381}
382
383// JSON
384std::string AnalogTape::Json() const { return JsonValue().toStyledString(); }
385
386Json::Value AnalogTape::JsonValue() const {
387 Json::Value root = EffectBase::JsonValue();
388 root["type"] = info.class_name;
389 root["tracking"] = tracking.JsonValue();
390 root["bleed"] = bleed.JsonValue();
391 root["softness"] = softness.JsonValue();
392 root["noise"] = noise.JsonValue();
393 root["stripe"] = stripe.JsonValue();
394 root["static_bands"] = staticBands.JsonValue();
395 root["seed_offset"] = seed_offset;
396 return root;
397}
398
399void AnalogTape::SetJson(const std::string value) {
400 try {
401 Json::Value root = openshot::stringToJson(value);
402 SetJsonValue(root);
403 } catch (const std::exception &) {
404 throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
405 }
406}
407
408void AnalogTape::SetJsonValue(const Json::Value root) {
410 if (!root["tracking"].isNull())
411 tracking.SetJsonValue(root["tracking"]);
412 if (!root["bleed"].isNull())
413 bleed.SetJsonValue(root["bleed"]);
414 if (!root["softness"].isNull())
415 softness.SetJsonValue(root["softness"]);
416 if (!root["noise"].isNull())
417 noise.SetJsonValue(root["noise"]);
418 if (!root["stripe"].isNull())
419 stripe.SetJsonValue(root["stripe"]);
420 if (!root["static_bands"].isNull())
421 staticBands.SetJsonValue(root["static_bands"]);
422 if (!root["seed_offset"].isNull())
423 seed_offset = root["seed_offset"].asInt();
424}
425
426std::string AnalogTape::PropertiesJSON(int64_t requested_frame) const {
427 Json::Value root = BasePropertiesJSON(requested_frame);
428 root["tracking"] =
429 add_property_json("Tracking", tracking.GetValue(requested_frame), "float",
430 "", &tracking, 0, 1, false, requested_frame);
431 root["bleed"] =
432 add_property_json("Bleed", bleed.GetValue(requested_frame), "float", "",
433 &bleed, 0, 1, false, requested_frame);
434 root["softness"] =
435 add_property_json("Softness", softness.GetValue(requested_frame), "float",
436 "", &softness, 0, 1, false, requested_frame);
437 root["noise"] =
438 add_property_json("Noise", noise.GetValue(requested_frame), "float", "",
439 &noise, 0, 1, false, requested_frame);
440 root["stripe"] =
441 add_property_json("Stripe", stripe.GetValue(requested_frame), "float",
442 "Bottom tracking stripe brightness and noise.",
443 &stripe, 0, 1, false, requested_frame);
444 root["static_bands"] =
445 add_property_json("Static Bands", staticBands.GetValue(requested_frame),
446 "float",
447 "Short bright static bands and extra dropouts.",
448 &staticBands, 0, 1, false, requested_frame);
449 root["seed_offset"] =
450 add_property_json("Seed Offset", seed_offset, "int", "", NULL, 0, 1000,
451 false, requested_frame);
452 return root.toStyledString();
453}
Header file for AnalogTape effect class.
Header file for Clip class.
Header file for all Exception classes.
Header file for ReaderBase class.
Header file for Timeline class.
int seed_offset
seed offset for deterministic randomness
Definition AnalogTape.h:108
Keyframe stripe
bottom tracking stripe strength
Definition AnalogTape.h:106
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Keyframe tracking
tracking wobble amount
Definition AnalogTape.h:102
void SetJson(const std::string value) override
Load JSON string into this object.
Json::Value JsonValue() const override
Generate Json::Value for this object.
Keyframe noise
grain/dropouts amount
Definition AnalogTape.h:105
Keyframe softness
luma blur radius
Definition AnalogTape.h:104
std::string PropertiesJSON(int64_t requested_frame) const override
std::shared_ptr< openshot::Frame > GetFrame(std::shared_ptr< openshot::Frame > frame, int64_t frame_number) override
This method is required for all derived classes of ClipBase, and returns a modified openshot::Frame o...
Keyframe bleed
color bleed amount
Definition AnalogTape.h:103
Keyframe staticBands
burst static band strength
Definition AnalogTape.h:107
std::string Json() const override
Generate JSON string of this object.
float start
The position in seconds to start playing (used to trim the beginning of a clip).
Definition ClipBase.h:37
std::string Id() const
Get the Id of this clip object.
Definition ClipBase.h:85
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:91
float end
The position in seconds to end playing (used to trim the ending of a clip).
Definition ClipBase.h:38
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:96
virtual Json::Value JsonValue() const
Generate Json::Value for this object.
openshot::ClipBase * ParentClip()
Parent clip object of this effect (which can be unparented and NULL).
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.
openshot::ClipBase * clip
Pointer to the parent clip instance (if any).
Definition EffectBase.h:59
EffectInfoStruct info
Information about the current effect.
Definition EffectBase.h:69
Exception for invalid JSON.
Definition Exceptions.h:218
A Keyframe is a collection of Point instances, which is used to vary a number or property over time.
Definition KeyFrame.h:53
This namespace is the default namespace for all code in the openshot library.
Definition Compressor.h:29
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:40
bool has_audio
Determines if this effect manipulates the audio of a frame.
Definition EffectBase.h:41
std::string class_name
The class name of the effect.
Definition EffectBase.h:36
std::string name
The name of the effect.
Definition EffectBase.h:37
std::string description
The description of this effect and what it does.
Definition EffectBase.h:38