├── .gitattributes ├── Images ├── Preview.gif ├── logo.png ├── preview1.png ├── preview2.png └── preview_GUI.png ├── README.md └── SourceCode ├── FanAudioVisualizer.py ├── FanBlender.py ├── FanBlender_Example.py ├── FanBlender_GUI.py ├── FanWheels_PIL.py ├── FanWheels_ffmpeg.py ├── LICENSE.txt ├── LanguagePack.py ├── Old_Version_Source_Code_Before_V110.zip ├── QtImages.py ├── QtStyle.py ├── QtViewer.py ├── QtWheels.py ├── QtWindows.py ├── ReadMe.txt ├── Source.zip ├── _CheckEnvironment.py ├── _Installer.py └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Images/Preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/Images/Preview.gif -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/Images/logo.png -------------------------------------------------------------------------------- /Images/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/Images/preview1.png -------------------------------------------------------------------------------- /Images/preview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/Images/preview2.png -------------------------------------------------------------------------------- /Images/preview_GUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/Images/preview_GUI.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fanseline Visualizer - 帆室邻音频可视化视频制作工具 2 | 3 | Convert audio files to visualized video 4 | 将音频文件转化为可视化视频 5 | 6 | Ver.1.1.6 发行版 Release for Windows, macOS, Ubuntu X64: 7 | https://github.com/FerryYoungFan/FanselineVisualizer/releases 8 | 9 | See How to use: 10 | 查看如何使用: 11 | https://www.youtube.com/watch?v=ziSsiIvTB_o 12 | https://www.bilibili.com/video/BV1AD4y1D7fd 13 |
14 | ![Image](https://github.com/FerryYoungFan/FanselineVisualizer/blob/master/Images/Preview.gif) 15 | ![Image](https://github.com/FerryYoungFan/FanselineVisualizer/blob/master/Images/preview1.png) 16 | ![Image](https://github.com/FerryYoungFan/FanselineVisualizer/blob/master/Images/preview2.png) 17 | ![Image](https://github.com/FerryYoungFan/FanselineVisualizer/blob/master/Images/preview_GUI.png) 18 | 19 | --- 20 | ## Support this Project - 支持此项目 21 | 22 | If you like this software, please star this repository on GitHub. 23 | ... or you can directly support me with money: 24 | https://afdian.net/@Fanseline 25 | 26 | 如果您喜欢本软件,请在GitHub给此项目打星。 27 | ……或者您也可以直接给咱打钱: 28 | https://afdian.net/@Fanseline 29 | -------------------------------------------------------------------------------- /SourceCode/FanAudioVisualizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from FanWheels_PIL import * 5 | import numpy as np 6 | from pydub import AudioSegment 7 | 8 | 9 | class AudioAnalyzer: 10 | def __init__(self, file_path, ffmpeg_path, fps=30, fq_low=20, fq_up=6000, bins=80, smooth=0, beat_detect=60, 11 | low_range=10): 12 | AudioSegment.ffmpeg = ffmpeg_path 13 | sound = AudioSegment.from_file(file_path) 14 | self.samples = np.asarray(sound.get_array_of_samples(), dtype=np.float) 15 | self.fq_low = fq_low 16 | self.fq_up = fq_up 17 | self.bins = bins 18 | self.low_range = np.clip(low_range / 100, 0.0, 1.0) 19 | if smooth: 20 | self.smooth = smooth 21 | else: 22 | self.smooth = 0 23 | if np.max(self.samples) != 0: 24 | self.samples = self.samples / np.max(self.samples) 25 | self.sample_rate = sound.frame_rate 26 | self.T = 1.0 / self.sample_rate 27 | 28 | self.fps = fps 29 | self.totalFrames = self.getTotalFrames() 30 | self.hist_stack = [None] * self.totalFrames 31 | 32 | self.beat_stack = np.ones(self.totalFrames) 33 | self.beat_calc_stack = [None] * self.totalFrames 34 | self.beat_detect = beat_detect 35 | self.beat_thres = (100 - beat_detect) / 100 36 | 37 | def fftAnalyzer(self, start_p, stop_p, fq_low=20, fq_up=6000, bins=80): 38 | freq_array = np.zeros(bins) 39 | if stop_p <= 0: 40 | return freq_array 41 | if start_p < 0: 42 | start_p = 0 43 | if start_p >= self.samples.shape[0] - self.sample_rate / fq_low: 44 | return freq_array 45 | if stop_p >= self.samples.shape[0]: 46 | stop_p = self.samples.shape[0] - 1 47 | y = self.samples[start_p:stop_p] 48 | N = y.shape[0] 49 | yf = np.fft.fft(y) 50 | yf_fq = 2.0 / N * np.abs(yf[:N // 2]) 51 | xf = np.linspace(0.0, 1.0 / (2.0 * self.T), N // 2) # Frequency domain: 0 to 1/(2T) 52 | yf_cut = yf_fq[np.where(np.logical_and(xf >= fq_low, xf <= fq_up))] 53 | xf_cut = xf[np.where(np.logical_and(xf >= fq_low, xf <= fq_up))] 54 | xf_log = squareSpace(1, len(yf_cut) + 1, bins + 1) 55 | for i in range(bins): 56 | win_low, win_up = int(xf_log[i]), int(xf_log[i + 1]) 57 | if win_low < 0: 58 | win_low = 0 59 | if win_up > len(yf_cut) - 1: 60 | win_up = len(yf_cut) - 1 61 | if win_up - win_low > 0: 62 | freq_array[i] = np.sum(yf_cut[win_low:win_up]) * psyModel((xf_cut[win_low] + xf_cut[win_low]) / 2) 63 | else: 64 | freq_array[i] = 0 65 | return freq_array 66 | 67 | def getSampleRate(self): 68 | return self.sample_rate 69 | 70 | def getLength(self): 71 | return self.samples.shape[0] 72 | 73 | def getTotalFrames(self): 74 | return int(self.fps * self.getLength() / self.getSampleRate()) + 1 75 | 76 | def getHistAtFrame(self, index): 77 | smooth = int(round(self.smooth * self.fps / 30)) 78 | if smooth >= 1 + 4: 79 | fcount = 0 80 | freq_acc = np.zeros(self.bins) 81 | for i in range(smooth - 4 + 1): 82 | fcount = fcount + 2 83 | if index - i < 0: 84 | pass 85 | else: 86 | if self.hist_stack[index - i] is None: 87 | left, right = self.getRange(index - i, smooth) 88 | self.hist_stack[index - i] = self.fftAnalyzer(left, right, self.fq_low, self.fq_up, self.bins) 89 | freq_acc += self.hist_stack[index - i] 90 | if index + i > len(self.hist_stack) - 1: 91 | pass 92 | else: 93 | if self.hist_stack[index + i] is None: 94 | left, right = self.getRange(index + i, smooth) 95 | self.hist_stack[index + i] = self.fftAnalyzer(left, right, self.fq_low, self.fq_up, self.bins) 96 | freq_acc += self.hist_stack[index + i] 97 | return freq_acc / fcount 98 | 99 | else: 100 | if self.hist_stack[index] is None: 101 | left, right = self.getRange(index, smooth) 102 | self.hist_stack[index] = self.fftAnalyzer(left, right, self.fq_low, self.fq_up, self.bins) 103 | return self.hist_stack[index] 104 | 105 | def getBeatAtFrame(self, index): 106 | if self.beat_detect > 0: 107 | index = self.clipRange(index) 108 | left = self.clipRange(index - self.fps / 6) 109 | right = self.clipRange(index + self.fps / 6) 110 | for i in range(left, right + 1): 111 | if self.hist_stack[i] is None: 112 | self.getHistAtFrame(i) 113 | if self.beat_calc_stack[i] is None: 114 | calc_nums = self.bins * self.low_range # Frequency range for the bass 115 | maxv = np.max(self.hist_stack[i][0:int(np.ceil(calc_nums))]) ** 2 116 | avgv = np.average(self.hist_stack[i][0:int(np.ceil(calc_nums))]) 117 | self.beat_calc_stack[i] = np.sqrt(max(maxv, avgv)) 118 | 119 | slice_stack = self.beat_calc_stack[left:right + 1] 120 | current_max = np.max(slice_stack) 121 | index_max = np.where(slice_stack == current_max)[0][0] 122 | standby = np.sum(self.beat_stack[index:right + 1] == 1.0) == len(self.beat_stack[index:right + 1]) 123 | 124 | if self.beat_calc_stack[index] >= self.beat_thres and index - left == index_max and standby: 125 | self.beat_stack[index:right + 1] = list(1 + 0.05 * (np.linspace(1, 0, right + 1 - index) ** 2)) 126 | return self.beat_stack[index] 127 | 128 | def clipRange(self, index): 129 | if index >= self.totalFrames: 130 | return self.totalFrames - 1 131 | if index < 0: 132 | return 0 133 | return int(round(index)) 134 | 135 | def getRange(self, idx, smooth=0): # Get FFT range 136 | if idx < 0: 137 | idx = -5 138 | if idx > self.totalFrames: 139 | idx = -5 140 | middle = idx * self.getSampleRate() / self.fps 141 | offset = self.sample_rate / 20 142 | if smooth == 1: 143 | lt = int(round(middle - 1 * offset)) 144 | rt = int(round(middle + 2 * offset)) 145 | elif smooth == 2: 146 | lt = int(round(middle - 2 * offset)) 147 | rt = int(round(middle + 2 * offset)) 148 | elif 2 < smooth <= 5: 149 | lt = int(round(middle - 2 * offset)) 150 | rt = int(round(middle + 4 * offset)) 151 | elif smooth > 5: 152 | lt = int(round(middle - 3 * offset)) 153 | rt = int(round(middle + 6 * offset)) 154 | else: 155 | lt = int(round(middle - 1 * offset)) 156 | rt = int(round(middle + 1 * offset)) 157 | return lt, rt 158 | 159 | 160 | def circle(draw, center, radius, fill): 161 | draw.ellipse((center[0] - radius + 1, center[1] - radius + 1, center[0] + radius - 1, center[1] + radius - 1), 162 | fill=fill, outline=None) 163 | 164 | 165 | def rectangle(draw, center, radius, fill): 166 | draw.rectangle((center[0] - radius + 1, center[1] - radius + 1, center[0] + radius - 1, center[1] + radius - 1), 167 | fill=fill, outline=None) 168 | 169 | 170 | def getCycleHue(start, end, bins, index, cycle=1): 171 | div = end - start 172 | fac = index / bins * cycle 173 | ratio = abs(round(fac) - fac) * 2 174 | return (div * ratio + start) / 360 175 | 176 | 177 | def getColor(bins, index, color_mode="color4x", bright=1.0, sat=0.8): 178 | brt = 0.4 + bright * 0.6 179 | if color_mode == "color4x": 180 | return hsv_to_rgb(4 * index / bins, sat, brt) + (255,) 181 | if color_mode == "color2x": 182 | return hsv_to_rgb(2 * index / bins, sat, brt) + (255,) 183 | if color_mode == "color1x": 184 | return hsv_to_rgb(1 * index / bins, sat, brt) + (255,) 185 | if color_mode == "white": 186 | return hsv_to_rgb(0, 0, 1.0) + (255,) 187 | if color_mode == "black": 188 | return hsv_to_rgb(0, 0, 0) + (255,) 189 | if color_mode == "gray": 190 | return hsv_to_rgb(0, 0, brt) + (255,) 191 | if color_mode == "red": 192 | return hsv_to_rgb(0, sat, brt) + (255,) 193 | if color_mode == "green": 194 | return hsv_to_rgb(120 / 360, sat, brt) + (255,) 195 | if color_mode == "blue": 196 | return hsv_to_rgb(211 / 360, sat, brt) + (255,) 197 | if color_mode == "yellow": 198 | return hsv_to_rgb(49 / 360, sat, brt) + (255,) 199 | if color_mode == "magenta": 200 | return hsv_to_rgb(328 / 360, sat, brt) + (255,) 201 | if color_mode == "purple": 202 | return hsv_to_rgb(274 / 360, sat, brt) + (255,) 203 | if color_mode == "cyan": 204 | return hsv_to_rgb(184 / 360, sat, brt) + (255,) 205 | if color_mode == "lightgreen": 206 | return hsv_to_rgb(135 / 360, sat, brt) + (255,) 207 | if color_mode == "green-blue": 208 | return hsv_to_rgb(getCycleHue(122, 220, bins, index, 4), sat, brt) + (255,) 209 | if color_mode == "magenta-purple": 210 | return hsv_to_rgb(getCycleHue(300, 370, bins, index, 4), sat, brt) + (255,) 211 | if color_mode == "red-yellow": 212 | return hsv_to_rgb(getCycleHue(-5, 40, bins, index, 4), sat, brt) + (255,) 213 | if color_mode == "yellow-green": 214 | return hsv_to_rgb(getCycleHue(42, 147, bins, index, 4), sat, brt) + (255,) 215 | if color_mode == "blue-purple": 216 | return hsv_to_rgb(getCycleHue(208, 313, bins, index, 4), sat, brt) + (255,) 217 | 218 | try: 219 | clist = tuple(color_mode) 220 | if len(clist) == 3: 221 | return clist + (255,) 222 | else: 223 | return clist 224 | except: 225 | return hsv_to_rgb(0, 0, brt) + (255,) 226 | 227 | 228 | class AudioVisualizer: 229 | def __init__(self, img, rad_min, rad_max, line_thick=1.0, blur=5, style=0): 230 | self.background = img.copy() 231 | self.width, self.height = self.background.size 232 | self.render_size = min(self.width, self.height) 233 | self.mdpx, self.mdpy = self.render_size / 2, self.render_size / 2 234 | self.line_thick = line_thick 235 | if style in [1, 2, 4, 6, 7, 11, 12, 15, 16, 21, 22]: 236 | self.rad_min = rad_min + line_thick * 1.5 237 | self.rad_max = rad_max - line_thick * 1.5 238 | elif style in [3, 5]: 239 | self.rad_min = rad_min + line_thick / 2 240 | self.rad_max = rad_max - line_thick * 1.5 241 | elif style in [8]: 242 | self.rad_min = rad_min + line_thick * 1.5 243 | self.rad_max = rad_max 244 | elif style in [18]: 245 | self.rad_min = rad_min 246 | self.rad_max = rad_max 247 | else: 248 | self.rad_min = rad_min + line_thick / 2 249 | self.rad_max = rad_max - line_thick / 2 250 | self.rad_div = self.rad_max - self.rad_min 251 | self.blur = blur 252 | self.style = style 253 | 254 | def getFrame(self, hist, amplify=5, color_mode="color4x", bright=1.0, saturation=1.0, use_glow=True, rotate=0.0, 255 | fps=30.0, frame_pt=0, bg_mode=0, fg_img=None, fg_resize=1.0, quality=3, preview=False): 256 | bins = hist.shape[0] 257 | 258 | quality_list = [1, 1, 2, 2, 4, 8] 259 | ratio = quality_list[quality] # Antialiasing ratio 260 | 261 | line_thick = int(round(self.line_thick * ratio)) 262 | line_thick_bold = int(round(self.line_thick * ratio * 1.5)) 263 | line_thick_slim = int(round(self.line_thick * ratio / 2)) 264 | 265 | canvas = Image.new('RGBA', (self.render_size * ratio, self.render_size * ratio), (255, 255, 255, 0)) 266 | draw = ImageDraw.Draw(canvas) 267 | 268 | line_graph_prev = None 269 | 270 | if self.style in [0, 1, 2, 3, 4, 5, 6, 7, 18, 19, 20, 21, 22]: 271 | hist = np.power(hist * 1.5, 1.5) 272 | hist = np.clip(hist * amplify, 0, 1) 273 | 274 | for i in range(bins): # Draw Spectrum 275 | color = getColor(bins, i, color_mode, bright, saturation) 276 | if self.style == 0: # Solid Line 277 | line_points = [self.getAxis(bins, i, self.rad_min, ratio), 278 | self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio)] 279 | draw.line(line_points, width=line_thick, fill=color) 280 | circle(draw, line_points[0], line_thick_slim, color) 281 | circle(draw, line_points[1], line_thick_slim, color) 282 | 283 | elif self.style == 1: # Dot Line 284 | p_gap = line_thick_bold 285 | p_size = line_thick_bold 286 | if p_gap + p_size > 0: 287 | p_n = int(((hist[i] * self.rad_div) + p_gap) / (p_gap + p_size)) 288 | circle(draw, self.getAxis(bins, i, self.rad_min, ratio), line_thick_bold, color) 289 | for ip in range(p_n): 290 | p_rad = (p_gap + p_size) * ip 291 | circle(draw, self.getAxis(bins, i, self.rad_min + p_rad, ratio), line_thick_bold, color) 292 | 293 | elif self.style == 2: # Single Dot 294 | circle(draw, self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio), line_thick_bold, 295 | color) 296 | 297 | elif self.style == 3: # Stem Plot: Solid Single 298 | line_points = [self.getAxis(bins, i, self.rad_min, ratio), 299 | self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio)] 300 | draw.line(line_points, width=line_thick, fill=color) 301 | circle(draw, line_points[0], line_thick_slim, color) 302 | circle(draw, line_points[1], line_thick_bold, color) 303 | 304 | elif self.style == 4: # Stem Plot: Solid Double 305 | line_points = [self.getAxis(bins, i, self.rad_min, ratio), 306 | self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio)] 307 | draw.line(line_points, width=line_thick, fill=color) 308 | circle(draw, line_points[0], line_thick_bold, color) 309 | circle(draw, line_points[1], line_thick_bold, color) 310 | 311 | elif self.style == 5: # Stem Plot: Dashed Single 312 | p_gap = line_thick_slim 313 | p_size = line_thick_slim 314 | if p_gap + p_size > 0: 315 | p_n = int(((hist[i] * self.rad_div) + p_size) / (p_gap + p_size)) 316 | for ip in range(p_n): 317 | p_rad = (p_gap + p_size) * ip 318 | circle(draw, self.getAxis(bins, i, self.rad_min + p_rad, ratio), line_thick_slim, color) 319 | circle(draw, self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio), line_thick_bold, 320 | color) 321 | 322 | elif self.style == 6: # Stem Plot: Dashed Double 323 | p_gap = line_thick_slim 324 | p_size = line_thick_slim 325 | if p_gap + p_size > 0: 326 | p_n = int(((hist[i] * self.rad_div) + p_size) / (p_gap + p_size)) 327 | for ip in range(p_n): 328 | p_rad = (p_gap + p_size) * ip 329 | circle(draw, self.getAxis(bins, i, self.rad_min + p_rad, ratio), line_thick_slim, color) 330 | circle(draw, self.getAxis(bins, i, self.rad_min, ratio), line_thick_bold, color) 331 | circle(draw, self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio), line_thick_bold, 332 | color) 333 | 334 | elif self.style == 7: # Double Dot 335 | circle(draw, self.getAxis(bins, i, self.rad_min, ratio), line_thick_bold, color) 336 | circle(draw, self.getAxis(bins, i, self.rad_min + hist[i] * self.rad_div, ratio), line_thick_bold, 337 | color) 338 | 339 | elif self.style == 8: # Concentric 340 | if i % 12 == 0: 341 | lower = i 342 | upper = i + 11 343 | if upper >= len(hist): 344 | upper = len(hist) - 1 345 | local_mean = np.mean(hist[-upper - 1:-lower - 1]) * 2 346 | if local_mean > 1: 347 | local_mean = 1 348 | radius = self.rad_min + local_mean * self.rad_div 349 | left = (self.mdpx - radius) * ratio 350 | right = (self.mdpx + radius) * ratio 351 | up = (self.mdpy - radius) * ratio 352 | down = (self.mdpy + radius) * ratio 353 | draw.ellipse((left, up, right, down), fill=None, outline=color, width=line_thick_bold) 354 | 355 | elif self.style == 9: # Classic Line: Center 356 | mid_y = self.mdpy * ratio 357 | y_scale = 0.85 358 | low = mid_y + self.rad_max * ratio * hist[i] * y_scale 359 | up = mid_y - self.rad_max * ratio * hist[i] * y_scale 360 | gap = self.rad_max * ratio * 2 / (bins - 1) 361 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 362 | line_points = [(x_offset, low), (x_offset, up)] 363 | draw.line(line_points, width=line_thick, fill=color) 364 | circle(draw, line_points[0], line_thick_slim, color) 365 | circle(draw, line_points[1], line_thick_slim, color) 366 | 367 | elif self.style == 10: # Classic Line: Bottom 368 | mid_y = self.mdpy * ratio 369 | y_scale = 0.85 370 | low = mid_y + self.rad_max * ratio * y_scale 371 | up = low - self.rad_max * ratio * hist[i] * y_scale * 2 372 | gap = self.rad_max * ratio * 2 / (bins - 1) 373 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 374 | line_points = [(x_offset, low), (x_offset, up)] 375 | draw.line(line_points, width=line_thick, fill=color) 376 | circle(draw, line_points[0], line_thick_slim, color) 377 | circle(draw, line_points[1], line_thick_slim, color) 378 | 379 | elif self.style == 11: # Classic Round Dot: Center 380 | mid_y = self.mdpy * ratio 381 | y_scale = 0.85 382 | low = mid_y + self.rad_max * ratio * hist[i] * y_scale 383 | gap = self.rad_max * ratio * 2 / (bins - 1) 384 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 385 | p_gap = line_thick_bold * 2 386 | p_size = line_thick_bold 387 | if p_gap + p_size > 0: 388 | p_n = int((low - mid_y + p_gap) / (p_gap + p_size)) 389 | if p_n < 1: 390 | p_n = 1 391 | for ip in range(p_n): 392 | d_y = ip * (p_gap + p_size) 393 | circle(draw, (x_offset, mid_y + d_y), line_thick_bold, color) 394 | circle(draw, (x_offset, mid_y - d_y), line_thick_bold, color) 395 | 396 | elif self.style == 12: # Classic Round Dot: Bottom 397 | mid_y = self.mdpy * ratio 398 | y_scale = 0.85 399 | low = mid_y + self.rad_max * ratio * y_scale 400 | up = low - self.rad_max * ratio * hist[i] * y_scale * 2 401 | gap = self.rad_max * ratio * 2 / (bins - 1) 402 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 403 | p_gap = line_thick_bold * 2 404 | p_size = line_thick_bold 405 | if p_gap + p_size > 0: 406 | p_n = int((low - up + p_gap) / (p_gap + p_size)) 407 | if p_n < 1: 408 | p_n = 1 409 | for ip in range(p_n): 410 | p_y = low - ip * (p_gap + p_size) 411 | circle(draw, (x_offset, p_y), line_thick_bold, color) 412 | 413 | elif self.style == 13: # Classic Square Dot: Center 414 | mid_y = self.mdpy * ratio 415 | y_scale = 0.85 416 | low = mid_y + self.rad_max * ratio * hist[i] * y_scale 417 | gap = self.rad_max * ratio * 2 / (bins - 1) 418 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 419 | p_gap = line_thick_bold * 2 420 | p_size = line_thick_bold 421 | if p_gap + p_size > 0: 422 | p_n = int((low - mid_y + p_gap) / (p_gap + p_size)) 423 | if p_n < 1: 424 | p_n = 1 425 | for ip in range(p_n): 426 | d_y = ip * (p_gap + p_size) 427 | rectangle(draw, (x_offset, mid_y + d_y), line_thick_bold, color) 428 | rectangle(draw, (x_offset, mid_y - d_y), line_thick_bold, color) 429 | 430 | elif self.style == 14: # Classic Square Dot: Bottom 431 | mid_y = self.mdpy * ratio 432 | y_scale = 0.85 433 | low = mid_y + self.rad_max * ratio * y_scale 434 | up = low - self.rad_max * ratio * hist[i] * y_scale * 2 435 | gap = self.rad_max * ratio * 2 / (bins - 1) 436 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 437 | p_gap = line_thick_bold * 2 438 | p_size = line_thick_bold 439 | if p_gap + p_size > 0: 440 | p_n = int((low - up + p_gap) / (p_gap + p_size)) 441 | if p_n < 1: 442 | p_n = 1 443 | for ip in range(p_n): 444 | p_y = low - ip * (p_gap + p_size) 445 | rectangle(draw, (x_offset, p_y), line_thick_bold, color) 446 | 447 | elif self.style == 15: # Classic Rectangle: Center 448 | mid_y = self.mdpy * ratio 449 | y_scale = 0.85 450 | low = mid_y + self.rad_max * ratio * hist[i] * y_scale 451 | up = mid_y - self.rad_max * ratio * hist[i] * y_scale 452 | gap = self.rad_max * ratio * 2 / (bins - 1) 453 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 454 | draw.rectangle((x_offset - line_thick_bold, low + line_thick_bold, x_offset + line_thick_bold, 455 | up - line_thick_bold), fill=color) 456 | 457 | elif self.style == 16: # Classic Rectangle: Bottom 458 | mid_y = self.mdpy * ratio 459 | y_scale = 0.85 460 | low = mid_y + self.rad_max * ratio * y_scale 461 | up = low - self.rad_max * ratio * hist[i] * y_scale * 2 462 | gap = self.rad_max * ratio * 2 / (bins - 1) 463 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 464 | draw.rectangle((x_offset - line_thick_bold, low + line_thick_bold, x_offset + line_thick_bold, 465 | up - line_thick_bold), fill=color) 466 | 467 | elif self.style == 17: # Line Graph 468 | mid_y = self.mdpy * ratio 469 | y_scale = 0.85 470 | low = mid_y + self.rad_max * ratio * y_scale 471 | up = low - self.rad_max * ratio * hist[i] * y_scale * 2 472 | gap = self.rad_max * ratio * 2 / (bins - 1) 473 | x_offset = gap * i + self.mdpx * ratio - self.rad_max * ratio 474 | if line_graph_prev is None: 475 | line_graph_prev = [(x_offset, low), (x_offset, up)] 476 | draw.line(((x_offset, low), (x_offset, up)), width=line_thick, fill=color) 477 | circle(draw, (x_offset, low), line_thick_slim, color) 478 | circle(draw, (x_offset, up), line_thick_slim, color) 479 | 480 | draw.line((line_graph_prev[1], (x_offset, up)), width=line_thick, fill=color) 481 | circle(draw, line_graph_prev[1], line_thick_slim, color) 482 | circle(draw, (x_offset, up), line_thick_slim, color) 483 | 484 | if i >= bins - 1: 485 | draw.line(((x_offset, low), (x_offset, up)), width=line_thick, fill=color) 486 | circle(draw, (x_offset, low), line_thick_slim, color) 487 | circle(draw, (x_offset, up), line_thick_slim, color) 488 | line_graph_prev = [(x_offset, low), (x_offset, up)] 489 | 490 | elif self.style == 18: # Zooming Circles 491 | center_rad = (self.rad_max - self.rad_min) / 2 + self.rad_min 492 | center = self.getAxis(bins, i, center_rad, ratio) 493 | center_next = self.getAxis(bins, i + 1, center_rad, ratio) 494 | center_gap = np.sqrt((center_next[0] - center[0]) ** 2 + (center_next[1] - center[1]) ** 2) / 2 * ratio 495 | max_gap = min(self.rad_div * ratio, center_gap) 496 | factor = np.power(np.clip(self.line_thick * 20 / min(self.width, self.height), 0.0, 1.0), 0.3) 497 | rad_draw = int(round(hist[i] * factor * max_gap / 2)) 498 | circle(draw, center, rad_draw, color) 499 | 500 | elif self.style == 19: # Solid Line: Center 501 | line_points = [ 502 | self.getAxis(bins, i, self.rad_min + self.rad_div / 2 - hist[i] * self.rad_div / 2, ratio), 503 | self.getAxis(bins, i, self.rad_min + self.rad_div / 2 + hist[i] * self.rad_div / 2, ratio)] 504 | draw.line(line_points, width=line_thick, fill=color) 505 | circle(draw, line_points[0], line_thick_slim, color) 506 | circle(draw, line_points[1], line_thick_slim, color) 507 | 508 | elif self.style == 20: # Solid Line: Reverse 509 | line_points = [ 510 | self.getAxis(bins, i, self.rad_min + self.rad_div - hist[i] * self.rad_div, ratio), 511 | self.getAxis(bins, i, self.rad_min + self.rad_div, ratio)] 512 | draw.line(line_points, width=line_thick, fill=color) 513 | circle(draw, line_points[0], line_thick_slim, color) 514 | circle(draw, line_points[1], line_thick_slim, color) 515 | 516 | elif self.style == 21: # Double Dot: Center 517 | circle(draw, self.getAxis(bins, i, self.rad_min + self.rad_div / 2 - hist[i] * self.rad_div / 2, ratio), 518 | line_thick_bold, color) 519 | circle(draw, self.getAxis(bins, i, self.rad_min + self.rad_div / 2 + hist[i] * self.rad_div / 2, ratio), 520 | line_thick_bold, 521 | color) 522 | 523 | elif self.style == 22: # Double Dot: Reverse 524 | circle(draw, 525 | self.getAxis(bins, i, self.rad_min + self.rad_div - hist[i] * self.rad_div, ratio), 526 | line_thick_bold, color) 527 | circle(draw, self.getAxis(bins, i, self.rad_min + self.rad_div, ratio), line_thick_bold, 528 | color) 529 | 530 | else: # Othewise (-1): No Spectrum 531 | pass 532 | 533 | if use_glow: 534 | canvas = glowFx(canvas, self.blur * ratio, 1.5) 535 | 536 | canvas = canvas.resize((self.render_size, self.render_size), Image.ANTIALIAS) 537 | 538 | if fg_img is not None and bg_mode > -2 and (not bg_mode == 2): 539 | if rotate != 0: 540 | angle = -(rotate * frame_pt / fps / 60) * 360 541 | fg_img = fg_img.rotate(angle, resample=Image.BICUBIC) 542 | if fg_resize != 1.0: 543 | fg_img = resizeRatio(fg_img, fg_resize) 544 | canvas = pasteMiddle(fg_img, canvas) 545 | 546 | return pasteMiddle(canvas, self.background.copy()) 547 | 548 | def getAxis(self, bins, index, radius, ratio): 549 | div = 2 * np.pi / bins 550 | angle = div * index - np.pi / 2 - np.pi * 5 / 6 551 | ox = (self.mdpx + radius * np.cos(angle)) * ratio 552 | oy = (self.mdpy + radius * np.sin(angle)) * ratio 553 | return ox, oy 554 | 555 | 556 | def squareSpace(start, end, num): 557 | sqrt_start = np.sqrt(start) 558 | sqrt_end = np.sqrt(end) 559 | sqrt_space = np.linspace(sqrt_start, sqrt_end, num=num) 560 | return np.round(np.power(sqrt_space, 2)).astype(int) 561 | 562 | 563 | def linearRange(startx, endx, starty, endy, x): 564 | return (endy - starty) / (endx - startx) * (x - startx) + starty 565 | 566 | 567 | psy_map = [[10, 32, 3.0, 2.5], 568 | [32, 48, 2.5, 1.0], 569 | [48, 64, 1.0, 1.0], 570 | [64, 150, 1.0, 1.3], 571 | [150, 2000, 1.3, 1.0], 572 | [2000, 8000, 1.0, 1.2], 573 | [8000, 24000, 1.2, 4.0]] 574 | 575 | 576 | def psyModel(freq): # Get psychoacoustical model 577 | for item in psy_map: 578 | if item[0] <= freq < item[1]: 579 | return linearRange(item[0], item[1], item[2], item[3], freq) 580 | return 0 581 | 582 | 583 | if __name__ == '__main__': 584 | #print(linearRange(8000, 20000, 1, 0, 10000)) 585 | pass 586 | -------------------------------------------------------------------------------- /SourceCode/FanBlender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Fanseline Visualizer 6 | 帆室邻音频可视化视频制作工具 7 | https://github.com/FerryYoungFan/FanselineVisualizer 8 | 9 | By Twitter @FanKetchup 10 | https://twitter.com/FanKetchup 11 | """ 12 | 13 | __version__ = "1.1.6" # Work with PYQT5 14 | 15 | from FanWheels_PIL import * 16 | from FanWheels_ffmpeg import * 17 | from FanAudioVisualizer import AudioVisualizer, AudioAnalyzer 18 | 19 | import imageio 20 | import imageio_ffmpeg 21 | import numpy as np 22 | 23 | import threading, os, sys 24 | from PyQt5 import QtCore # Notice: You can use time.sleep() if you are not using PyQt5 25 | 26 | 27 | class blendingThread(threading.Thread): 28 | def __init__(self, threadID, name, counter, parent, total_thread, thread_num): 29 | threading.Thread.__init__(self) 30 | self.threadID = threadID 31 | self.name = name 32 | self.counter = counter 33 | self.parent = parent 34 | self.total_thread = total_thread 35 | self.thread_num = thread_num 36 | self.frame_pt = thread_num 37 | 38 | def run(self): 39 | while self.frame_pt < self.parent.total_frames and self.parent.isRunning: 40 | self.parent.frame_lock[self.frame_pt] = self.thread_num + 1 41 | self.parent.frame_buffer[self.frame_pt] = self.parent.visualizer.getFrame( 42 | hist=self.parent.analyzer.getHistAtFrame(self.frame_pt), 43 | amplify=self.parent._amplify, 44 | color_mode=self.parent.spectrum_color, 45 | bright=self.parent._bright, 46 | saturation=self.parent._saturation, 47 | use_glow=self.parent.use_glow, 48 | rotate=self.parent.rotate, 49 | fps=self.parent.fps, 50 | frame_pt=self.frame_pt, 51 | bg_mode=self.parent.bg_mode, 52 | fg_img=self.parent.fg_img, 53 | fg_resize=self.parent.analyzer.getBeatAtFrame(self.frame_pt), 54 | quality=self.parent.quality) 55 | self.frame_pt = self.frame_pt + self.total_thread 56 | print("Thread {0} -end".format(self.thread_num)) 57 | 58 | 59 | class encodingThread(threading.Thread): 60 | def __init__(self, threadID, name, counter, parent): 61 | threading.Thread.__init__(self) 62 | self.threadID = threadID 63 | self.name = name 64 | self.counter = counter 65 | self.parent = parent 66 | 67 | def run(self): 68 | realTimePrev = None 69 | while self.parent.encoder_pt < self.parent.total_frames and self.parent.isRunning: 70 | if self.parent.frame_buffer[self.parent.encoder_pt] is not None: 71 | self.parent.writer.append_data(np.asarray(self.parent.frame_buffer[self.parent.encoder_pt])) 72 | realTimePrev = self.parent.frame_buffer[self.parent.encoder_pt] 73 | self.parent.frame_buffer[self.parent.encoder_pt] = None 74 | self.parent.encoder_pt = self.parent.encoder_pt + 1 75 | else: 76 | if realTimePrev: 77 | self.parent.previewRealTime(realTimePrev) 78 | realTimePrev = None 79 | self.parent.log("Processing:{0}/{1}".format(self.parent.encoder_pt, self.parent.total_frames)) 80 | self.parent.progress(self.parent.encoder_pt, self.parent.total_frames) 81 | 82 | # The following 3 lines can be replaced by time.sleep(0.5) if you are not using PyQt5 83 | loop = QtCore.QEventLoop() 84 | QtCore.QTimer.singleShot(500, loop.quit) 85 | loop.exec_() 86 | 87 | self.parent.previewRealTime(realTimePrev) 88 | realTimePrev = None 89 | self.parent.log("Processing:{0}/{1}".format(self.parent.encoder_pt, self.parent.total_frames)) 90 | self.parent.progress(self.parent.encoder_pt, self.parent.total_frames) 91 | if self.parent.encoder_pt >= self.parent.total_frames: 92 | self.parent.log("Rendering Done!") 93 | 94 | 95 | class FanBlender: 96 | def __init__(self): 97 | self.color_dic = { 98 | "Rainbow 4x": "color4x", 99 | "Rainbow 2x": "color2x", 100 | "Rainbow 1x": "color1x", 101 | "White": "white", 102 | "Black": "black", 103 | "Gray": "gray", 104 | "Red": "red", 105 | "Green": "green", 106 | "Blue": "blue", 107 | "Yellow": "yellow", 108 | "Magenta": "magenta", 109 | "Purple": "purple", 110 | "Cyan": "cyan", 111 | "Light Green": "lightgreen", 112 | "Gradient: Green - Blue": "green-blue", 113 | "Gradient: Magenta - Purple": "magenta-purple", 114 | "Gradient: Red - Yellow": "red-yellow", 115 | "Gradient: Yellow - Green": "yellow-green", 116 | "Gradient: Blue - Purple": "blue-purple", 117 | } 118 | self.image_path = None 119 | self.bg_path = None 120 | self.sound_path = None 121 | self.logo_path = None 122 | self.output_path = None 123 | self.bg_img = None 124 | self.logofile = None 125 | 126 | self.text_bottom = "" 127 | self.font = getPath("Source/font.otf") 128 | 129 | self.frame_width = 540 130 | self.frame_height = 540 131 | self.fps = 30 132 | self.bit_rate = 0.6 # in Mb/s 133 | self.audio_bit_rate = 320 # in kb/s 134 | self.audio_normal = False 135 | 136 | self.spectrum_color = "color4x" 137 | self._bright = 1.0 138 | self._saturation = 1.0 139 | self.bins = 80 140 | self.smooth = 0 141 | self.fq_low = 20 142 | self.fq_up = 1500 143 | self.scalar = 1.0 144 | 145 | self._debug_bg = False 146 | self._temp_audio_path = getPath("Temp/temp.wav") 147 | self._temp_video_path = getPath("Temp/temp.mp4") 148 | 149 | try: 150 | self._ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe() 151 | except: 152 | self._ffmpeg_path = None 153 | 154 | self._frame_size = 0 155 | self._relsize = 1.0 156 | self._font_size = 0 157 | self._text_color = (0, 0, 0, 0) 158 | self._text_glow = False 159 | self._yoffset = 0 160 | # A good ratio for text and frame size 161 | 162 | self._blur = 0 163 | self._blur_bg = 0 164 | self.blur_bg = True 165 | self.use_glow = False 166 | self.style = 0 167 | self.linewidth = 1.0 168 | self.rotate = 0 169 | self.beat_detect = 0 170 | self.low_range = 10 171 | self._line_thick = 0 172 | 173 | self._amplify = self.setAmplify() 174 | 175 | self.visualizer = None 176 | self.analyzer = None 177 | self.fg_img = None 178 | 179 | self.writer = None 180 | self.total_frames = None 181 | self.frame_buffer = [] 182 | self.frame_pt = 0 183 | self.encoder_pt = 0 184 | 185 | self.isRunning = False 186 | self._console = None 187 | 188 | self.frame_lock = None 189 | 190 | self.bg_mode = 0 191 | self.quality = 3 192 | self.bg_blended = False 193 | self.ffmpegCheck() 194 | 195 | def calcRel(self): 196 | self._frame_size = min(self.frame_width, self.frame_height) 197 | self._font_size = int(round(30 / 1080 * self._frame_size * self._relsize)) 198 | self._blur = int(round(2 / 1080 * self._frame_size)) 199 | self._blur_bg = int(round(41 / 1080 * self._frame_size)) 200 | self._line_thick = self.linewidth * 4 / 1080 * self._frame_size 201 | self._yoffset = clip(self.frame_height - self._font_size * 2.1, self.frame_height * 0.95, 202 | self.frame_height * 0.95 - self._font_size) 203 | 204 | def ffmpegCheck(self): 205 | if self._ffmpeg_path is None: 206 | self.log("Error: FFMPEG not found!") 207 | if self._console: 208 | try: 209 | self._console.ffmpegWarning() 210 | except: 211 | pass 212 | return False 213 | else: 214 | return True 215 | 216 | def setAmplify(self): 217 | return self.scalar * 5 * np.sqrt(self.bins / 80) * np.power(1500 / (self.fq_up - self.fq_low), 0.5) 218 | 219 | def ensure_dir(self, file_path): 220 | directory = os.path.dirname(file_path) 221 | if not os.path.exists(directory): 222 | os.makedirs(directory) 223 | 224 | def fileError(self, filepath): 225 | if filepath is not None: 226 | self.log("Error: File {0} does not exist.".format(filepath)) 227 | 228 | def setConsole(self, console=None): 229 | if console: 230 | self._console = console 231 | 232 | def audioError(self): 233 | if self._console: 234 | try: 235 | self._console.audioWarning() 236 | except: 237 | pass 238 | 239 | def log(self, content=""): 240 | print(content) 241 | if self._console: 242 | try: 243 | self._console.log(content) 244 | except: 245 | pass 246 | 247 | def progress(self, value, total): 248 | if self._console: 249 | try: 250 | self._console.progressbar(value, total) 251 | except: 252 | return 253 | 254 | def freezeConsole(self, flag=True): 255 | if self._console: 256 | try: 257 | self._console.freeze(flag) 258 | except: 259 | return 260 | 261 | def setFilePath(self, image_path=None, bg_path=None, sound_path=None, logo_path=None): 262 | self.image_path = self.imgFileCheck(image_path) 263 | self.bg_path = self.imgFileCheck(bg_path) 264 | self.logo_path = self.imgFileCheck(logo_path) 265 | 266 | if sound_path is not None: 267 | if os.path.isfile(sound_path): 268 | self.sound_path = sound_path 269 | else: 270 | self.fileError(sound_path) 271 | 272 | def imgFileCheck(self, file_path): 273 | if file_path is None: 274 | return None 275 | if isinstance(file_path, tuple) and (len(file_path) in [3, 4]): 276 | self.visualizer = None 277 | self.bg_blended = False 278 | if len(file_path) == 3: 279 | return file_path + (255,) 280 | return file_path # RGBA Color Tuple 281 | if os.path.isfile(file_path): 282 | self.visualizer = None 283 | self.bg_blended = False 284 | return file_path 285 | return None 286 | 287 | def setOutputPath(self, output_path="", filename=""): 288 | if not filename: 289 | filename = "Visualize.mp4" 290 | if output_path: 291 | self.ensure_dir(os.path.join(output_path, filename)) 292 | self.output_path = cvtFileName(os.path.join(output_path, filename), "mp4") 293 | 294 | def setText(self, text="", font="", relsize=None, text_color=(255, 255, 255, 255), text_glow=None): 295 | self.text_bottom = text 296 | if not font: 297 | if os.path.exists(getPath("Source/font.otf")): 298 | font = getPath("Source/font.otf") 299 | elif os.path.exists(getPath("Source/font.ttf")): 300 | font = getPath("Source/font.ttf") 301 | else: 302 | font = "Arial.ttf" 303 | if relsize is not None: 304 | self._relsize = clip(relsize, 0.1, 5) 305 | if text_color is not None: 306 | self._text_color = text_color 307 | if text_glow is not None: 308 | self._text_glow = text_glow 309 | self.font = font 310 | 311 | def setSpec(self, bins=None, lower=None, upper=None, color=None, bright=None, saturation=None, scalar=None, 312 | smooth=None, style=None, linewidth=None, rotate=None, beat_detect=None, low_range=None): 313 | if bins is not None: 314 | self.bins = int(clip(bins, 2, 250)) 315 | 316 | if lower is not None: 317 | self.fq_low = int(clip(lower, 16, 22000)) 318 | 319 | if upper is not None: 320 | upper = int(clip(upper, 16, 22000)) 321 | if upper <= self.fq_low: 322 | upper = self.fq_low + 1 323 | self.fq_up = int(upper) 324 | 325 | if color is not None: 326 | self.spectrum_color = color 327 | 328 | if bright is not None: 329 | self._bright = clip(bright, 0, 1) 330 | 331 | if saturation is not None: 332 | self._saturation = clip(saturation, 0, 1) 333 | 334 | if scalar is not None: 335 | self.scalar = clip(scalar, 0.1, 10) 336 | 337 | if smooth is not None: 338 | self.smooth = int(round(clip(smooth, 0, 15))) 339 | 340 | if style is not None: 341 | self.style = style 342 | 343 | if linewidth is not None: 344 | self.linewidth = clip(linewidth, 0.01, 50) 345 | 346 | if rotate is not None: 347 | self.rotate = float(rotate) 348 | 349 | if beat_detect is not None: 350 | self.beat_detect = clip(beat_detect, 0, 100) 351 | 352 | if low_range is not None: 353 | self.low_range = clip(low_range, 0, 100) 354 | 355 | self._amplify = self.setAmplify() 356 | self.visualizer = None 357 | 358 | def setVideoInfo(self, width=None, height=None, fps=None, br_Mbps=None, blur_bg=None, 359 | use_glow=None, bg_mode=None, quality=None): 360 | if width is not None: 361 | self.frame_width = int(clip(width, 16, 4096)) 362 | if height is not None: 363 | self.frame_height = int(clip(height, 16, 4096)) 364 | if fps is not None: 365 | self.fps = clip(fps, 1, 120) 366 | if br_Mbps is None: 367 | br_Mbps = 15 * (self.frame_width * self.frame_height * self.fps) / (1920 * 1080 * 30) 368 | self.bit_rate = br_Mbps 369 | else: 370 | self.bit_rate = clip(br_Mbps, 0.01, 200) 371 | 372 | if blur_bg is not None: 373 | self.blur_bg = blur_bg 374 | 375 | if use_glow is not None: 376 | self.use_glow = use_glow 377 | 378 | if bg_mode is not None: 379 | self.bg_mode = bg_mode 380 | 381 | if quality is not None: 382 | self.quality = int(clip(quality, 1, 5)) 383 | 384 | self.visualizer = None 385 | self.bg_blended = False 386 | 387 | def setAudioInfo(self, normal=None, br_kbps=None): 388 | if normal is not None: 389 | self.audio_normal = normal 390 | 391 | if br_kbps is not None: 392 | self.audio_bit_rate = int(round(clip(br_kbps, 5, 10000))) 393 | 394 | def genBackground(self, forceRefresh=False, preview=False): 395 | self.calcRel() 396 | if not self.bg_blended or forceRefresh: 397 | self.log("Rendering Background...") 398 | 399 | image = openImage(self.image_path, "RGBA", 400 | ["Source/fallback.png", "Source/fallback.jpg", (127, 127, 127, 127)]) 401 | bg = openImage(self.bg_path, "RGB", [None]) 402 | self.logofile = openImage(self.logo_path, "RGBA", [None]) 403 | 404 | if bg is None: 405 | bg = image.copy() 406 | 407 | if self.bg_mode < 0: 408 | self.bg_img = Image.new("RGBA", (self.frame_width, self.frame_height), (0, 0, 0, 0)) 409 | else: 410 | if self.blur_bg: 411 | self.bg_img = genBG(bg, size=(self.frame_width, self.frame_height), blur=self._blur_bg, bright=0.3) 412 | else: 413 | self.bg_img = genBG(bg, size=(self.frame_width, self.frame_height), blur=0, bright=1.0) 414 | self.log("Rendering Background... Done!") 415 | 416 | if (self.bg_mode >= -1) and (not self.bg_mode == 2): 417 | self.fg_img = cropCircle(image, size=self._frame_size // 2, quality=self.quality) 418 | base_fg = self.fg_img.split()[-1] 419 | base_w = base_fg.size[0] + int(round(self._blur * 4)) 420 | base_h = base_fg.size[1] + int(round(self._blur * 4)) 421 | base_fg = pasteMiddle(base_fg, Image.new("L", (base_w, base_h), 0)) 422 | base_fg = base_fg.filter(ImageFilter.GaussianBlur(radius=self._blur * 2)) 423 | white_fg = Image.new("L", (base_w, base_h), 0) 424 | self.fg_img = pasteMiddle(self.fg_img, Image.merge('RGBA', (white_fg, white_fg, white_fg, base_fg))) 425 | 426 | self.bg_img = glowText(self.bg_img, self.text_bottom, self._font_size, self.font, color=self._text_color, 427 | blur=self._blur, logo=self.logofile, use_glow=self._text_glow, yoffset=self._yoffset) 428 | self.bg_blended = True 429 | 430 | rad_max = (self._yoffset - self.frame_height / 2) * 0.97 431 | if self.text_bottom is None or self.text_bottom == "": 432 | if self.logo_path is None or not os.path.exists(self.logo_path): 433 | rad_max = self.frame_height / 2 434 | self.visualizer = AudioVisualizer(img=self.bg_img, 435 | rad_min=self._frame_size / 4 * 1.1, 436 | rad_max=min(rad_max, self._frame_size / 2.1), 437 | line_thick=self._line_thick, 438 | blur=self._blur, style=self.style) 439 | 440 | def previewBackground(self, localViewer=False, forceRefresh=False): 441 | self.genBackground(forceRefresh) 442 | xs = np.linspace(0, 10 * np.pi, self.bins) 443 | ys = (0.5 + 0.5 * np.cos(xs)) * self.scalar 444 | frame_sample = self.visualizer.getFrame(hist=ys, amplify=1, color_mode=self.spectrum_color, bright=self._bright, 445 | saturation=self._saturation, use_glow=self.use_glow, rotate=self.rotate, 446 | fps=30, frame_pt=90, bg_mode=self.bg_mode, fg_img=self.fg_img, 447 | fg_resize=(self.beat_detect / 100) * 0.05 + 1, quality=self.quality) 448 | if localViewer: 449 | frame_sample.show() 450 | return frame_sample 451 | 452 | def previewRealTime(self, img): 453 | if self._console: 454 | try: 455 | self._console.realTime(img) 456 | except: 457 | return 458 | 459 | def genAnalyzer(self): 460 | if not self.ffmpegCheck(): 461 | return 462 | if self.sound_path is None or not os.path.exists(str(self.sound_path)): 463 | self.log("Error: Audio file not found!") 464 | return 465 | try: 466 | self.ensure_dir(self._temp_audio_path) 467 | toTempWaveFile(self.sound_path, self._temp_audio_path, self._console) 468 | except: 469 | self.fileError(self.sound_path) 470 | self.analyzer = None 471 | return 472 | try: 473 | self.analyzer = AudioAnalyzer(file_path=self._temp_audio_path, 474 | ffmpeg_path=self._ffmpeg_path, 475 | fps=self.fps, 476 | fq_low=self.fq_low, 477 | fq_up=self.fq_up, 478 | bins=self.bins, 479 | smooth=self.smooth, 480 | beat_detect=self.beat_detect, 481 | low_range=self.low_range) 482 | except: 483 | self.fileError(self._temp_audio_path) 484 | self.analyzer = None 485 | return 486 | 487 | def runBlending(self): 488 | if self.isRunning: 489 | return 490 | self.removeTemp() 491 | self.freezeConsole(True) 492 | self.genBackground(forceRefresh=True) 493 | if not self.ffmpegCheck(): 494 | self.freezeConsole(False) 495 | return 496 | self.isRunning = True 497 | self.genAnalyzer() 498 | 499 | if not self.isRunning: 500 | self.removeTemp() 501 | self.isRunning = False 502 | self.freezeConsole(False) 503 | return 504 | 505 | if self._temp_audio_path is None or not os.path.exists(str(self._temp_audio_path)): 506 | self.freezeConsole(False) 507 | self.audioError() 508 | self.isRunning = False 509 | return 510 | 511 | if self.analyzer is None: 512 | self.log("Error: Analyzer not found") 513 | self.isRunning = False 514 | self.freezeConsole(False) 515 | return 516 | if self.visualizer is None: 517 | self.log("Error: Visualizer not found") 518 | self.isRunning = False 519 | self.freezeConsole(False) 520 | return 521 | 522 | if self.bg_mode < 0: 523 | self.writer = imageio.get_writer(cvtFileName(self._temp_video_path, "mov"), 524 | fps=self.fps, 525 | macro_block_size=None, 526 | format='FFMPEG', codec="png", pixelformat="rgba") 527 | else: 528 | self.writer = imageio.get_writer(self._temp_video_path, fps=self.fps, macro_block_size=None, 529 | bitrate=int(self.bit_rate * 1000000)) 530 | self.total_frames = self.analyzer.getTotalFrames() 531 | self.frame_buffer = [None] * self.total_frames 532 | self.encoder_pt = 0 533 | 534 | thread_encode = encodingThread(1, "Thread-1", 1, self) 535 | thread_stack = [] 536 | thread_num = 4 537 | cpu_count = os.cpu_count() 538 | if cpu_count is not None: 539 | thread_num = cpu_count // 2 540 | if thread_num < 1: 541 | thread_num = 1 542 | print("CPU Thread for Rendering: " + str(thread_num)) 543 | 544 | self.frame_lock = np.zeros(self.total_frames, dtype=np.uint8) 545 | for ith in range(thread_num): 546 | thread_stack.append(blendingThread(ith + 2, "Thread-" + str(ith + 2), ith + 2, self, thread_num, ith)) 547 | thread_encode.start() 548 | for ith in range(thread_num): 549 | thread_stack[ith].start() 550 | thread_encode.join() 551 | for ith in range(thread_num): 552 | thread_stack[ith].join() 553 | self.writer.close() 554 | if self.isRunning: 555 | self.log("Output path: ") 556 | self.log(str(self.output_path)) 557 | self.log("Combining Videos...") 558 | audio_br = str(self.audio_bit_rate) + "k" 559 | if self.bg_mode < 0: 560 | combineVideo(cvtFileName(self._temp_video_path, "mov"), self.sound_path, 561 | cvtFileName(self.output_path, "mov"), audio_br, self.audio_normal, console=self._console) 562 | else: 563 | combineVideo(self._temp_video_path, self.sound_path, self.output_path, audio_br, self.audio_normal, 564 | console=self._console) 565 | self.log("Combining Videos... Done!") 566 | else: 567 | self.log("Rendering Aborted!") 568 | 569 | self.analyzer = None 570 | self.removeTemp() 571 | self.isRunning = False 572 | self.freezeConsole(False) 573 | 574 | def getOutputPath(self): 575 | if self.bg_mode < 0: 576 | return cvtFileName(self.output_path, "mov") 577 | else: 578 | return cvtFileName(self.output_path, "mp4") 579 | 580 | def removeTemp(self): 581 | def removeFile(file_path): 582 | try: 583 | os.remove(file_path) 584 | except: 585 | pass 586 | 587 | removeFile(self._temp_audio_path) 588 | removeFile(self._temp_video_path) 589 | removeFile(cvtFileName(self._temp_video_path, "mov")) 590 | 591 | 592 | def clip(value, low_in=0.0, up_in=0.0): 593 | if value is None: 594 | return 0 595 | if low_in > up_in: 596 | low, up = up_in, low_in 597 | else: 598 | low, up = low_in, up_in 599 | if value < low: 600 | return low 601 | if value > up: 602 | return up 603 | return value 604 | 605 | 606 | def getPath(fileName): # for different operating systems 607 | path = os.path.join(os.path.dirname(sys.argv[0]), fileName) 608 | path = path.replace("\\", "/") 609 | return path 610 | 611 | 612 | if __name__ == '__main__': 613 | # Example of Using FanBlender 614 | 615 | fb = FanBlender() # Initialize Blender (Render) 616 | 617 | fb.setFilePath(image_path="Source/fallback.png", 618 | bg_path="Source/background.jpg", 619 | sound_path="Source/test.mp3", 620 | logo_path="Source/logo.png") 621 | """ 622 | Set file path. 623 | You can also use RGBA color as path to generate a monocolor image: e.g. image_path=(255,0,0,255) 624 | """ 625 | 626 | fb.setOutputPath(output_path="./Output", 627 | filename="test.mp4") # Set Output Path 628 | 629 | fb.setText(text="Your Text Here", font="Source/font.otf", 630 | relsize=1.0, text_color=(255, 255, 255, 255), text_glow=True) 631 | """ 632 | Set text at the Bottom (Relative Font Size: 0.3 - 5.0) 633 | Text color format: RGBA 634 | """ 635 | 636 | fb.setSpec(bins=60, lower=20, upper=1500, 637 | color=fb.color_dic["Gradient: Green - Blue"], bright=0.6, saturation=0.8, 638 | scalar=1.0, smooth=2, 639 | style=1, linewidth=1.0, 640 | rotate=1.5, beat_detect=50, low_range=10) 641 | """ 642 | Set Spectrum: 643 | bins: Number of spectrums 644 | lower: Lower Frequency (Analyzer's Frequency) (in Hz) 645 | upper: Upper Frequency (Analyzer's Frequency) (in Hz) 646 | color: Color of Spectrum 647 | bright: Brightness of Spectrum 648 | saturation: Color Saturation of Spectrum 649 | scalar: Sensitivity (Scalar) of Analyzer (Default:1.0) 650 | smooth: Stabilize Spectrum (Range: 0 - 15) 651 | style: 0-22 for Different Spectrum Styles (-1 for None) 652 | linewidth: Relative Width of Spectrum Line (0.5-20) 653 | rotate: Rotate Foreground (r/min, Positive for Clockwise) 654 | beat_detect: Sensitivity of Beat Detector (in %) 655 | low_range: Low Frequency Range for Beat Detector (relative analyzer's frequency range) (in %) 656 | """ 657 | fb.setVideoInfo(width=480, height=480, fps=30.0, br_Mbps=1.0, 658 | blur_bg=True, use_glow=True, bg_mode=0, quality=3) 659 | """ 660 | Video info 661 | br_Mbps: Bit Rate of Video (Mbps) 662 | blur_bg: Blur the background 663 | use_glow: Add Glow Effect to Spectrum and Text 664 | bg_mode: 0: Normal Background, 2: Background Only, -1: Transparent Background, -2: Spectrum Only 665 | quality: Antialiasing Quality (1-5, default 3) 666 | """ 667 | fb.setAudioInfo(normal=False, br_kbps=192) # Audio info 668 | 669 | fb.previewBackground(localViewer=True) # Preview before rendering 670 | 671 | fb.runBlending() # Render the video 672 | -------------------------------------------------------------------------------- /SourceCode/FanBlender_Example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from FanBlender import FanBlender 5 | 6 | """ 7 | Audio Visualizer - Example 8 | By Twitter @FanKetchup 9 | https://github.com/FerryYoungFan/FanselineVisualizer 10 | """ 11 | 12 | if __name__ == '__main__': 13 | # Example of Using FanBlender 14 | 15 | fb = FanBlender() # Initialize Blender (Render) 16 | 17 | fb.setFilePath(image_path="Source/fallback.png", 18 | bg_path="Source/background.jpg", 19 | sound_path="Source/test.mp3", 20 | logo_path="Source/logo.png") 21 | """ 22 | Set file path. 23 | You can also use RGBA color as path to generate a monocolor image: e.g. image_path=(255,0,0,255) 24 | """ 25 | 26 | fb.setOutputPath(output_path="./Output", 27 | filename="test.mp4") # Set Output Path 28 | 29 | fb.setText(text="Your Text Here", font="Source/font.otf", 30 | relsize=1.0, text_color=(255, 255, 255, 255), text_glow=True) 31 | """ 32 | Set text at the Bottom (Relative Font Size: 0.3 - 5.0) 33 | Text color format: RGBA 34 | """ 35 | 36 | fb.setSpec(bins=60, lower=20, upper=1500, 37 | color=fb.color_dic["Gradient: Green - Blue"], bright=0.6, saturation=0.8, 38 | scalar=1.0, smooth=2, 39 | style=1, linewidth=1.0, 40 | rotate=1.5, beat_detect=50, low_range=10) 41 | """ 42 | Set Spectrum: 43 | bins: Number of spectrums 44 | lower: Lower Frequency (Analyzer's Frequency) (in Hz) 45 | upper: Upper Frequency (Analyzer's Frequency) (in Hz) 46 | color: Color of Spectrum 47 | bright: Brightness of Spectrum 48 | saturation: Color Saturation of Spectrum 49 | scalar: Sensitivity (Scalar) of Analyzer (Default:1.0) 50 | smooth: Stabilize Spectrum (Range: 0 - 15) 51 | style: 0-22 for Different Spectrum Styles (-1 for None) 52 | linewidth: Relative Width of Spectrum Line (0.5-20) 53 | rotate: Rotate Foreground (r/min, Positive for Clockwise) 54 | beat_detect: Sensitivity of Beat Detector (in %) 55 | low_range: Low Frequency Range for Beat Detector (relative analyzer's frequency range) (in %) 56 | """ 57 | fb.setVideoInfo(width=480, height=480, fps=30.0, br_Mbps=1.0, 58 | blur_bg=True, use_glow=True, bg_mode=0, quality=3) 59 | """ 60 | Video info 61 | br_Mbps: Bit Rate of Video (Mbps) 62 | blur_bg: Blur the background 63 | use_glow: Add Glow Effect to Spectrum and Text 64 | bg_mode: 0: Normal Background, 2: Background Only, -1: Transparent Background, -2: Spectrum Only 65 | quality: Antialiasing Quality (1-5, default 3) 66 | """ 67 | fb.setAudioInfo(normal=False, br_kbps=192) # Audio info 68 | 69 | fb.previewBackground(localViewer=True) # Preview before rendering 70 | 71 | fb.runBlending() # Render the video 72 | -------------------------------------------------------------------------------- /SourceCode/FanBlender_GUI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from QtViewer import PhotoViewer, ImageSelectWindow 5 | from QtWindows import * 6 | from LanguagePack import * 7 | from FanBlender import FanBlender, getPath, __version__ 8 | import threading, time, pickle 9 | 10 | 11 | class InfoBridge: 12 | def __init__(self, parent): 13 | self.parent = parent 14 | self.value = 0 15 | self.total = 100 16 | self.prepare = 0 # Prepare for audio pre-process 17 | self.combine = 0 # Final combination 18 | self.img_cache = None 19 | 20 | def log(self, content=""): 21 | pass 22 | 23 | def progressbar(self, value, total): 24 | self.value = value 25 | self.total = total 26 | 27 | def freeze(self, flag=True): 28 | if flag: 29 | self.parent.setWindowTitle(self.parent.windowName + " " + self.parent.lang["(Rendering...)"]) 30 | self.parent.blendWindow.freezeWindow(True) 31 | else: 32 | self.parent.isRunning = False 33 | 34 | def realTime(self, img): 35 | self.img_cache = img 36 | 37 | def audioWarning(self): 38 | self.parent.error_log = 1 39 | 40 | def ffmpegWarning(self): 41 | self.parent.error_log = 2 42 | 43 | 44 | class MainWindow(QtWidgets.QWidget): 45 | def __init__(self, lang_in, vdic_in, lang_code_in, first_run_in): 46 | super(MainWindow, self).__init__() 47 | self.lang = lang_in 48 | self.lang_code = lang_code_in 49 | self.first_run = first_run_in 50 | self.windowName = self.lang["Fanseline Visualizer"] + " - v" + __version__ 51 | self.setWindowTitle(self.windowName) 52 | setWindowIcons(self) 53 | 54 | self.fb = FanBlender() 55 | self.infoBridge = InfoBridge(self) 56 | self.fb.setConsole(self.infoBridge) 57 | self.vdic = vdic_in 58 | self.vdic_stack = [] 59 | 60 | self.audio_formats = " (*.mp3;*.wav;*.ogg;*.aac;*.flac;*.ape;*.m4a;*.m4r;*.wma;*.mp2;*.mmf);;" 61 | self.audio_formats_arr = getFormats(self.audio_formats) 62 | self.video_formats = " (*.mp4;*.wmv;*.avi;*.flv;*.mov;*.mkv;*.rm;*.rmvb);;" 63 | self.video_formats_arr = getFormats(self.video_formats) 64 | self.image_formats = " (*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.ico;*.dib;*.webp;*.tiff;*.tga;*.icns);;" 65 | self.image_formats_arr = getFormats(self.image_formats) 66 | 67 | left = QtWidgets.QFrame(self) 68 | left.setStyleSheet(viewerStyle) 69 | VBlayout_l = QtWidgets.QVBoxLayout(left) 70 | self.viewer = PhotoViewer(self) 71 | VBlayout_l.addWidget(self.viewer) 72 | VBlayout_l.setSpacing(0) 73 | VBlayout_l.setContentsMargins(0, 0, 0, 0) 74 | 75 | self.mainMenu = MainMenu(self) 76 | self.imageSelector = ImageSelectWindow(self) 77 | self.audioSetting = AudioSettingWindow(self) 78 | self.videoSetting = VideoSettingWindow(self) 79 | self.textWindow = TextWindow(self) 80 | self.imageSetting = ImageSettingWindow(self) 81 | self.spectrumColor = SpectrumColorWindow(self) 82 | self.spectrumStyle = SpectrumStyleWindow(self) 83 | self.blendWindow = BlendWindow(self) 84 | self.aboutWindow = AboutWindow(self) 85 | 86 | right = QtWidgets.QFrame(self) 87 | VBlayout_r = QtWidgets.QVBoxLayout(right) 88 | VBlayout_r.setAlignment(QtCore.Qt.AlignTop) 89 | VBlayout_r.addWidget(self.mainMenu) 90 | VBlayout_r.addWidget(self.imageSelector) 91 | VBlayout_r.addWidget(self.audioSetting) 92 | VBlayout_r.addWidget(self.videoSetting) 93 | VBlayout_r.addWidget(self.textWindow) 94 | VBlayout_r.addWidget(self.imageSetting) 95 | VBlayout_r.addWidget(self.spectrumColor) 96 | VBlayout_r.addWidget(self.spectrumStyle) 97 | VBlayout_r.addWidget(self.blendWindow) 98 | VBlayout_r.addWidget(self.aboutWindow) 99 | 100 | mainBox = QtWidgets.QHBoxLayout(self) 101 | mainBox.setSpacing(0) 102 | mainBox.setContentsMargins(5, 5, 0, 5) 103 | 104 | splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) 105 | QtWidgets.QApplication.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks')) 106 | splitter.addWidget(left) 107 | splitter.addWidget(right) 108 | splitter.setStretchFactor(0, 1) 109 | splitter.setStretchFactor(1, -1) 110 | splitter.setSizes([1, 330]) 111 | mainBox.addWidget(splitter) 112 | 113 | self.setStyleSheet(stylepack) 114 | 115 | self.setAcceptDrops(True) 116 | self.canDrop = True 117 | 118 | self.resetLang = False 119 | global first_run, reset_lang 120 | if first_run or reset_lang: 121 | self.aboutWindow.show() 122 | first_run = False 123 | else: 124 | # self.aboutWindow.show() 125 | # self.blendWindow.show() 126 | self.mainMenu.show() 127 | 128 | self.timer = QtCore.QTimer(self) 129 | self.timer_ffmpeg = QtCore.QTimer(self) 130 | self.timer_ffmpeg.timeout.connect(self.ffmpegCheck) 131 | self.timer_ffmpeg.start(1000) 132 | self.error_log = 0 133 | self.isRunning = False 134 | self.stopWatch = time.time() 135 | self.time_cache = "" 136 | 137 | def ffmpegCheck(self): 138 | if not self.fb.ffmpegCheck(): 139 | showInfo(self, self.lang["Notice"], self.lang["FFMPEG not found, please install FFMPEG!"]) 140 | self.timer_ffmpeg.disconnect() 141 | self.timer_ffmpeg.stop() 142 | 143 | def dragEnterEvent(self, event): 144 | if event.mimeData().hasUrls: 145 | event.accept() 146 | else: 147 | event.ignore() 148 | 149 | def dropEvent(self, event): 150 | for url in event.mimeData().urls(): 151 | self.fileEvent(url.toLocalFile()) 152 | break 153 | 154 | def fileEvent(self, path): 155 | if self.canDrop: 156 | suffix = getFileSuffix(path)[1:].lower() 157 | if suffix == "fvsav": 158 | self.loadProject(path) 159 | elif suffix in self.audio_formats_arr or suffix in self.video_formats_arr: 160 | self.mainMenu.le_audio_path.setText(path) 161 | self.mainMenu.checkFilePath() 162 | elif suffix in self.image_formats_arr: 163 | self.imageSelector.selector1.fileEvent(path) 164 | else: 165 | showInfo(self, self.lang["Notice"], self.lang["Sorry, this file is not supported!"]) 166 | 167 | def hideAllMenu(self): 168 | self.mainMenu.hide() 169 | self.imageSelector.hide() 170 | self.audioSetting.hide() 171 | self.videoSetting.hide() 172 | self.textWindow.hide() 173 | self.imageSetting.hide() 174 | self.spectrumColor.hide() 175 | self.spectrumStyle.hide() 176 | self.blendWindow.hide() 177 | self.aboutWindow.hide() 178 | 179 | def setAll(self): 180 | self.fb.setFilePath(image_path=self.vdic["image_path"], 181 | bg_path=self.vdic["bg_path"], 182 | sound_path=self.vdic["sound_path"], 183 | logo_path=self.vdic["logo_path"]) 184 | self.fb.setOutputPath(output_path=self.vdic["output_path"], 185 | filename=self.vdic["filename"]) 186 | self.fb.setText(text=self.vdic["text"], font=self.vdic["font"], relsize=self.vdic["relsize"], 187 | text_color=self.vdic["text_color"], text_glow=self.vdic["text_glow"]) 188 | self.fb.setSpec(bins=self.vdic["bins"], lower=self.vdic["lower"], upper=self.vdic["upper"], 189 | color=self.vdic["color"], bright=self.vdic["bright"], saturation=self.vdic["saturation"], 190 | scalar=self.vdic["scalar"], smooth=self.vdic["smooth"], 191 | style=self.vdic["style"], linewidth=self.vdic["linewidth"], 192 | rotate=self.vdic["rotate"], 193 | beat_detect=self.vdic["beat_detect"], low_range=self.vdic["low_range"]) 194 | self.fb.setVideoInfo(width=self.vdic["width"], height=self.vdic["height"], 195 | fps=self.vdic["fps"], br_Mbps=self.vdic["br_Mbps"], 196 | blur_bg=self.vdic["blur_bg"], use_glow=self.vdic["use_glow"], 197 | bg_mode=self.vdic["bg_mode"], quality=self.vdic["quality"]) 198 | self.fb.setAudioInfo(normal=self.vdic["normal"], br_kbps=self.vdic["br_kbps"]) 199 | 200 | def refreshAll(self): 201 | self.setBusy(True) 202 | self.setAll() 203 | self.viewer.imshow(self.fb.previewBackground(localViewer=False, forceRefresh=True)) 204 | self.setBusy(False) 205 | 206 | def refreshLocal(self): 207 | self.setBusy(True) 208 | self.fb.setText(text=self.vdic["text"], font=self.vdic["font"], relsize=self.vdic["relsize"], 209 | text_color=self.vdic["text_color"], text_glow=self.vdic["text_glow"]) 210 | self.fb.setSpec(bins=self.vdic["bins"], lower=self.vdic["lower"], upper=self.vdic["upper"], 211 | color=self.vdic["color"], bright=self.vdic["bright"], saturation=self.vdic["saturation"], 212 | scalar=self.vdic["scalar"], smooth=self.vdic["smooth"], 213 | style=self.vdic["style"], linewidth=self.vdic["linewidth"], 214 | rotate=self.vdic["rotate"], 215 | beat_detect=self.vdic["beat_detect"], low_range=self.vdic["low_range"]) 216 | self.viewer.imshow(self.fb.previewBackground(localViewer=False)) 217 | self.setBusy(False) 218 | 219 | def vdicBackup(self): 220 | self.vdic_stack.append(self.vdic) 221 | 222 | def setBusy(self, busyFlag): 223 | if busyFlag: 224 | self.setWindowTitle(" ".join([self.windowName, self.lang["(Computing...)"]])) 225 | QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) 226 | else: 227 | QApplication.restoreOverrideCursor() 228 | self.setWindowTitle(self.windowName) 229 | 230 | def getBrief(self): 231 | brief = "

" 232 | brief += "" + self.lang["Output Path:"] + "
" 233 | if self.vdic["bg_mode"] >= 0: 234 | output = self.vdic["output_path"] + convertFileFormat(self.vdic["filename"], "mp4") 235 | else: 236 | output = self.vdic["output_path"] + convertFileFormat(self.vdic["filename"], "mov") 237 | brief += "" + output + "

" 238 | brief += "" + self.lang["Audio Path:"] + "
" 239 | brief += "" + self.vdic["sound_path"] + "

" 240 | brief += "

" 241 | brief += "" + self.lang["Video Settings:"] + "
" 242 | brief += self.lang["Video Size:"] + " " + str(self.vdic["width"]) + "x" + str(self.vdic["height"]) + "
" 243 | brief += self.lang["FPS:"] + " " + str(self.vdic["fps"]) + "
" 244 | brief += self.lang["Video BR:"] + " " + str(self.vdic["br_Mbps"]) + " (Mbps)
" 245 | for key, item in self.videoSetting.items_quality.items(): 246 | if self.vdic["quality"] == item: 247 | brief += self.lang["Render Quality:"] + " " + str(key) + "
" 248 | break 249 | brief += self.lang["Audio BR:"] + " " + str(self.vdic["br_kbps"]) + " (kbps)
" 250 | brief += self.lang["Volume Normalize:"] + " " 251 | if self.vdic["normal"]: 252 | brief += "" + self.lang["ON"] + "" 253 | else: 254 | brief += self.lang["OFF"] 255 | brief += "
" 256 | brief += self.lang["Analyzer Range:"] + " " + str(self.vdic["lower"]) + " ~ " + str( 257 | self.vdic["upper"]) + " Hz
" 258 | brief += self.lang["Spectrum Stabilize:"] + " " + str(self.vdic["smooth"]) + "
" 259 | for key, item in self.imageSetting.items_bg_mode.items(): 260 | if [self.vdic["blur_bg"], self.vdic["bg_mode"]] == item: 261 | brief += self.lang["BG Mode:"] + " " + key 262 | break 263 | 264 | brief += "

" 265 | return brief 266 | 267 | def getIntro(self): 268 | intro = "

" + self.lang["Version: "] + "" + __version__ + "

" 269 | intro += "{0}
".format(self.lang["Project Website: "]) 270 | intro += """ 271 | 272 | https://github.com/FerryYoungFan/FanselineVisualizer 273 |

274 | """ 275 | intro += "{0}
".format(self.lang["Support me if you like this application:"]) 276 | intro += """ 277 | 278 | https://afdian.net/@Fanseline 279 |

280 | """ 281 | intro += "{0}
".format(self.lang["About me:"]) 282 | intro += """ 283 | 284 | GitHub      285 | 286 | Twitter      287 | 288 | Pixiv 289 |
290 | """ 291 | intro += "{0}
".format(self.lang["Special thanks to:"]) 292 | intro += """ 293 | 294 | 小岛美奈子      295 | 296 | Dougie Doggies
297 | 298 | 神楽坂雅詩      299 | 300 | L_liu      Tony      301 |
302 | """ 303 | intro += "{0}
".format(self.lang["... and all people who support me!"]) 304 | return intro 305 | 306 | def startBlending(self): 307 | self.setAll() 308 | saveConfig() 309 | self.infoBridge = InfoBridge(self) 310 | self.fb.setConsole(self.infoBridge) 311 | self.error_log = 0 312 | self.isRunning = True 313 | self.timer.timeout.connect(self.realTimePreview) 314 | self.timer.start(200) 315 | self.stopWatch = time.time() 316 | self.time_cache = "" 317 | 318 | th_blend = threading.Thread(target=self.fb.runBlending) 319 | th_blend.setDaemon(True) 320 | th_blend.start() 321 | 322 | def stopBlending(self): 323 | self.fb.isRunning = False 324 | self.error_log = -1 325 | 326 | def realTimePreview(self): 327 | info = self.getBrief() 328 | if self.infoBridge.total != 0 and self.infoBridge.value == 0: 329 | self.blendWindow.prgbar.setValue(int(self.infoBridge.prepare * 1000)) 330 | self.blendWindow.prgbar.setStyleSheet(progressbarStyle2) 331 | elif self.infoBridge.total != 0 and 0 < self.infoBridge.value < self.infoBridge.total: 332 | self.blendWindow.prgbar.setValue(int(self.infoBridge.value / self.infoBridge.total * 1000)) 333 | self.blendWindow.prgbar.setStyleSheet(progressbarStyle1) 334 | elif self.infoBridge.total != 0 and self.infoBridge.value >= self.infoBridge.total: 335 | self.blendWindow.prgbar.setValue(int(self.infoBridge.combine * 1000)) 336 | self.blendWindow.prgbar.setStyleSheet(progressbarStyle2) 337 | else: 338 | self.blendWindow.prgbar.setValue(0) 339 | if self.fb.isRunning or self.isRunning: 340 | if self.infoBridge.img_cache is not None: 341 | self.viewer.imshow(self.infoBridge.img_cache) 342 | self.infoBridge.img_cache = None 343 | if self.infoBridge.value > 0: 344 | elapsed = time.time() - self.stopWatch 345 | blended = self.infoBridge.value 346 | togo = self.infoBridge.total - self.infoBridge.value 347 | time_remain = secondToTime(elapsed / blended * togo) 348 | self.time_cache = self.lang["Remaining Time:"] + " " + time_remain + "
" 349 | 350 | if self.time_cache != "": 351 | info += "" + self.lang["Rendering:"] + " " + str( 352 | self.infoBridge.value) + " / " + str(self.infoBridge.total) + "
" 353 | info += self.time_cache 354 | else: 355 | info += "" + self.lang["Analyzing Audio..."] + " " \ 356 | + str(round(self.infoBridge.prepare * 100)) + "%
" 357 | 358 | info += self.lang["Elapsed Time:"] + " " + secondToTime(time.time() - self.stopWatch) + "
" 359 | 360 | if self.infoBridge.value >= self.infoBridge.total: 361 | info += self.lang["Compositing Audio..."] + " " \ 362 | + str(round(self.infoBridge.combine * 100)) + "%" 363 | 364 | self.blendWindow.textview.setHtml(info) 365 | self.blendWindow.textview.moveCursor(QtGui.QTextCursor.End) 366 | else: 367 | print("error log:" + str(self.error_log)) 368 | if self.error_log == -1: 369 | info += "

" + self.lang["Rendering Aborted!"] + "

" 370 | elif self.error_log == 1: 371 | showInfo(self, self.lang["Notice"], self.lang["Sorry, this audio file is not supported!"]) 372 | info += "

" + self.lang["Rendering Aborted!"] + "

" 373 | elif self.error_log == 2: 374 | showInfo(self, self.lang["Notice"], self.lang["FFMPEG not found, please install FFMPEG!"]) 375 | info += "

" + self.lang["Rendering Aborted!"] + "

" 376 | else: 377 | info += "

" + self.lang["Mission Complete!"] + "

" 378 | 379 | self.error_log = 0 380 | info += self.lang["Elapsed Time:"] + " " + secondToTime(time.time() - self.stopWatch) + "
" 381 | self.blendWindow.textview.setHtml(info) 382 | self.blendWindow.textview.moveCursor(QtGui.QTextCursor.End) 383 | self.timer.stop() 384 | self.timer.disconnect() 385 | self.setWindowTitle(self.windowName) 386 | self.blendWindow.freezeWindow(False) 387 | 388 | def saveProject(self): 389 | if self.vdic["output_path"]: 390 | if self.vdic["filename"]: 391 | fpath = self.vdic["output_path"] + getFileName(self.vdic["sound_path"], False) 392 | else: 393 | fpath = self.vdic["output_path"] 394 | else: 395 | fpath = "" 396 | file_, filetype = QtWidgets.QFileDialog.getSaveFileName(self, 397 | self.lang["Save Project as..."], 398 | fpath + ".fvsav", 399 | self.lang["FV Project Files"] + " (*.fvsav)") 400 | if file_: 401 | saveConfig(file_) 402 | 403 | def loadProject(self, drag_path=None): 404 | if drag_path is None: 405 | selector = self.lang["FV Project Files"] + " (*.fvsav);;" 406 | selector = selector + self.lang["All Files"] + " (*.*)" 407 | if self.vdic["output_path"]: 408 | fpath = self.vdic["output_path"] 409 | else: 410 | fpath = "" 411 | file_, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.lang["Open Project File..."], fpath, 412 | selector) 413 | if not file_: 414 | print("No File!") 415 | else: 416 | vdic_open = loadConfig(file_) 417 | if vdic_open: 418 | self.vdic = vdic_open 419 | self.mainMenu.show() 420 | self.refreshAll() 421 | else: 422 | showInfo(self, self.lang["File Error"], self.lang["Cannot Read this Project!"]) 423 | else: 424 | vdic_open = loadConfig(drag_path) 425 | if vdic_open: 426 | self.vdic = vdic_open 427 | self.mainMenu.show() 428 | self.refreshAll() 429 | else: 430 | showInfo(self, self.lang["File Error"], self.lang["Cannot Read this Project!"]) 431 | 432 | def closeEvent(self, event): 433 | global close_app 434 | if self.isRunning: 435 | showInfo(self, self.lang["Notice"], self.lang["Please stop rendering before quit!"]) 436 | event.ignore() 437 | elif self.resetLang: 438 | global reset_lang 439 | reset_lang = True 440 | saveConfig() 441 | close_app = False 442 | event.accept() 443 | else: 444 | saveConfig() 445 | close_app = True 446 | self.fb.removeTemp() 447 | event.accept() 448 | 449 | 450 | def secondToTime(time_sec): 451 | hour = int(time_sec // 3600) 452 | minute = int((time_sec - hour * 3600) // 60) 453 | second = int((time_sec - hour * 3600 - minute * 60) // 1) 454 | if hour < 10: 455 | hour = "0" + str(hour) 456 | if minute < 10: 457 | minute = "0" + str(minute) 458 | if second < 10: 459 | second = "0" + str(second) 460 | return str(hour) + ":" + str(minute) + ":" + str(second) 461 | 462 | 463 | config_path = getPath("FVConfig.fconfig") 464 | config = None 465 | lang_code = "en" 466 | lang = lang_en 467 | vdic_pre = { 468 | "image_path": getPath("Source/fallback.png"), 469 | "bg_path": (200, 200, 200, 200), 470 | "sound_path": None, 471 | "logo_path": getPath("Source/logo.png"), 472 | "output_path": None, 473 | "filename": None, 474 | "text": "Fanseline Visualizer", 475 | "font": getPath("Source/font.otf"), 476 | "relsize": 1.0, 477 | "text_color": (255, 255, 255, 255), 478 | "text_glow": True, 479 | "bins": 48, 480 | "lower": 55, 481 | "upper": 6000, 482 | "color": "gray", 483 | "bright": 0.6, 484 | "saturation": 0.5, 485 | "scalar": 1.0, 486 | "smooth": 4, 487 | "style": 0, 488 | "linewidth": 1.0, 489 | "width": 480, 490 | "height": 480, 491 | "fps": 30.0, 492 | "br_Mbps": 1.0, 493 | "blur_bg": True, 494 | "use_glow": False, 495 | "bg_mode": 0, 496 | "rotate": 0, 497 | "normal": False, 498 | "br_kbps": 320, 499 | "beat_detect": 0, 500 | "low_range": 12, 501 | "quality": 3, 502 | } 503 | vdic = vdic_pre 504 | close_app = False 505 | first_run = True 506 | reset_lang = False 507 | 508 | 509 | def loadConfig(user_path=None): 510 | global config, lang, lang_code, vdic, first_run 511 | if user_path is None: 512 | f_path = config_path 513 | else: 514 | f_path = user_path 515 | if user_path is None: 516 | try: 517 | with open(f_path, "rb") as handle: 518 | config = pickle.load(handle) 519 | if config["__version__"] != __version__: 520 | raise Exception("VersionError") 521 | lang_code = config["lang_code"] 522 | print(config) 523 | first_run = False 524 | except: 525 | print("no config") 526 | return False 527 | else: 528 | try: 529 | with open(f_path, "rb") as handle: 530 | config = pickle.load(handle) 531 | print("Loaded Project Version: " + config["__version__"]) 532 | print(config) 533 | first_run = False 534 | except: 535 | return None 536 | 537 | vdic_read = config["vdic"] 538 | 539 | for key, value in vdic_read.items(): 540 | try: 541 | vdic[key] = vdic_read[key] 542 | except: 543 | pass 544 | 545 | if user_path is None: 546 | if lang_code == "cn_s": 547 | lang = lang_cn_s 548 | else: 549 | lang = lang_en 550 | else: 551 | return vdic.copy() 552 | 553 | 554 | def saveConfig(user_path=None): 555 | global config, lang, lang_code, vdic, vdic_pre, appMainWindow 556 | if user_path is None: 557 | f_path = config_path 558 | else: 559 | f_path = user_path 560 | 561 | try: 562 | lang_code = appMainWindow.lang_code 563 | vdic = appMainWindow.vdic 564 | except: 565 | vdic = vdic_pre 566 | lang_code = "en" 567 | 568 | config = { 569 | "__version__": __version__, 570 | "lang_code": lang_code, 571 | "vdic": vdic, 572 | } 573 | try: 574 | with open(f_path, 'wb') as handle: 575 | pickle.dump(config, handle, protocol=pickle.HIGHEST_PROTOCOL) 576 | with open(f_path, "rb") as handle: 577 | config_test = pickle.load(handle) 578 | if config_test["__version__"] != __version__: 579 | raise Exception("AuthorityError") 580 | except: 581 | showInfo(appMainWindow, appMainWindow.lang["Notice"], appMainWindow.lang["Error! Cannot save config!"]) 582 | 583 | 584 | if __name__ == '__main__': 585 | appMainWindow = None 586 | app = QtWidgets.QApplication(sys.argv) 587 | _font = None 588 | font_sets = ["Source/font.otf", "Source/font.ttf"] 589 | for font_path in font_sets: 590 | if os.path.exists(getPath(font_path)): 591 | try: 592 | fontid = QtGui.QFontDatabase.addApplicationFont(font_path) 593 | _fontstr = QtGui.QFontDatabase.applicationFontFamilies(fontid)[0] 594 | stylepack = stylepack.replace("Arial", _fontstr) 595 | break 596 | except: 597 | pass 598 | 599 | 600 | def startMain(): 601 | global appMainWindow, lang, vdic, first_run, _font 602 | loadConfig() 603 | appMainWindow = MainWindow(lang, vdic, lang_code, first_run) 604 | appMainWindow.resize(1050, 700) 605 | appMainWindow.show() 606 | appMainWindow.refreshAll() 607 | 608 | 609 | while not close_app: 610 | startMain() 611 | app.exec_() 612 | 613 | sys.exit() 614 | -------------------------------------------------------------------------------- /SourceCode/FanWheels_PIL.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from PIL import Image, ImageFilter, ImageDraw, ImageEnhance, ImageChops, ImageFont 5 | import os, sys 6 | 7 | 8 | def getPath(fileName): # for different operating systems 9 | path = os.path.join(os.path.dirname(sys.argv[0]), fileName) 10 | return path 11 | 12 | 13 | def imageOrColor(path, mode): 14 | if isinstance(path, str): 15 | try: 16 | img = Image.open(getPath(path)).convert(mode) 17 | if img is not None: 18 | return img 19 | else: 20 | raise Exception("Can not open image") 21 | except: 22 | return None 23 | if isinstance(path, tuple): 24 | try: 25 | if mode == "RGB": 26 | img = Image.new(mode, (512, 512), path[:3]) 27 | else: 28 | img = Image.new(mode, (512, 512), path[:4]) 29 | if img is not None: 30 | return img 31 | else: 32 | raise Exception("Can not generate image") 33 | except: 34 | return None 35 | return None 36 | 37 | 38 | def openImage(path, mode="RGBA", fallbacks=None): 39 | img = imageOrColor(path, mode) 40 | if img is None and isinstance(fallbacks, list): 41 | for fallback in fallbacks: 42 | if fallback is None: 43 | return None 44 | img = imageOrColor(fallback, mode) 45 | if img is not None: 46 | return img 47 | else: 48 | return img 49 | if mode == "RGBA": 50 | img = Image.new('RGB', (512, 512), (0, 0, 0)) 51 | else: 52 | img = Image.new('RGBA', (512, 512), (0, 0, 0, 255)) 53 | return img 54 | 55 | 56 | def cropToCenter(img): 57 | width, height = img.size 58 | square = min(width, height) 59 | left = (width - square) / 2 60 | top = (height - square) / 2 61 | right = (width + square) / 2 62 | bottom = (height + square) / 2 63 | im = img.crop((left, top, right, bottom)) 64 | return im 65 | 66 | 67 | def cropCircle(img, size=None, quality=3): 68 | img = cropToCenter(img) 69 | if size is not None: 70 | img = img.resize((size, size), Image.ANTIALIAS) 71 | # Antialiasing Drawing 72 | width, height = img.size 73 | old_mask = img.split()[-1] 74 | quality_list = [1, 1, 2, 4, 8, 8] 75 | scale = quality_list[quality] 76 | size_anti = width * scale, height * scale 77 | mask = Image.new('L', size_anti, 255) 78 | draw = ImageDraw.Draw(mask) 79 | draw.ellipse((0, 0) + size_anti, fill=0) 80 | mask = mask.resize((size, size), Image.ANTIALIAS) 81 | mask = ImageChops.subtract(old_mask, mask) 82 | img.putalpha(mask) 83 | return img 84 | 85 | 86 | def resizeRatio(img, ratio): 87 | width, height = img.size 88 | width = int(round(width * ratio)) 89 | height = int(round(height * ratio)) 90 | return img.resize((width, height), Image.ANTIALIAS) 91 | 92 | 93 | def cropBG(img, size): 94 | width, height = img.size 95 | if (width < size[0] and height < size[1]) or (width > size[0] and height > size[1]): 96 | img = resizeRatio(img, size[0] / width) 97 | width, height = img.size 98 | if height < size[1]: 99 | img = resizeRatio(img, size[1] / height) 100 | width, height = img.size 101 | elif width < size[0] and height >= size[1]: 102 | img = resizeRatio(img, size[0] / width) 103 | width, height = img.size 104 | elif width >= size[0] and height < size[1]: 105 | img = resizeRatio(img, size[1] / height) 106 | width, height = img.size 107 | 108 | left = (width - size[0]) / 2 109 | top = (height - size[1]) / 2 110 | right = (width + size[0]) / 2 111 | bottom = (height + size[1]) / 2 112 | 113 | img = img.crop((left, top, right, bottom)) 114 | return img 115 | 116 | 117 | def genBG(img, size, blur=41, bright=0.35): 118 | img = cropBG(img, size) 119 | img_blur = img.filter(ImageFilter.GaussianBlur(radius=blur)) 120 | enhancer = ImageEnhance.Brightness(img_blur) 121 | output = enhancer.enhance(bright) 122 | return output 123 | 124 | 125 | def pasteMiddle(fg, bg, glow=False, blur=2, bright=1): 126 | fg_w, fg_h = fg.size 127 | bg_w, bg_h = bg.size 128 | offset = ((bg_w - fg_w) // 2, (bg_h - fg_h) // 2) 129 | 130 | if glow: 131 | brt = int(round(bright * 255)) 132 | if brt > 255: 133 | brt = 255 134 | elif brt < 0: 135 | brt = 0 136 | canvas = Image.new('RGBA', (bg_w, bg_h), (brt, brt, brt, 0)) 137 | canvas.paste(fg, offset, fg) 138 | mask = canvas.split()[-1] 139 | mask = mask.point(lambda i: i * bright) 140 | mask = mask.filter(ImageFilter.GaussianBlur(radius=blur)) 141 | mask = mask.resize((bg_w, bg_h)) 142 | if bg.mode == "L": 143 | canvas = mask 144 | elif bg.mode == "RGB": 145 | canvas = Image.merge('RGB', (mask, mask, mask)) 146 | elif bg.mode == "RGBA": 147 | canvas = Image.merge('RGBA', (mask, mask, mask, mask)) 148 | bg = ImageChops.add(bg, canvas) 149 | 150 | bg.paste(fg, offset, fg) 151 | return bg 152 | 153 | 154 | def glowText(img, text=None, font_size=35, font_set=None, color=(255, 255, 255, 255), blur=2, logo=None, use_glow=True, 155 | yoffset=0): 156 | width, height = img.size 157 | width = width 158 | height = height 159 | font_size = font_size 160 | blur = blur 161 | 162 | if font_set is None: 163 | _font = ImageFont.truetype("arial.ttf", font_size) 164 | else: 165 | _font = "arial.ttf" 166 | for font_i in [font_set, getPath("Source/font.ttf"), getPath("Source/font.otf"), "arial.ttf"]: 167 | try: 168 | _font = ImageFont.truetype(font_i, font_size) 169 | break 170 | except: 171 | print("Cannot Use Font: {0}".format(font_i)) 172 | 173 | canvas = Image.new('RGBA', (width, height), color[:-1] + (0,)) 174 | draw = ImageDraw.Draw(canvas) 175 | if text: 176 | w, h = draw.textsize(text, font=_font) 177 | else: 178 | w, h = 0, 0 179 | xoffset = 0 180 | if logo is not None: 181 | lg_w, lg_h = logo.size 182 | hoffset = 1.1 183 | lg_nh = round(font_size * hoffset) 184 | lg_nw = round(lg_w * lg_nh / lg_h) 185 | logo = logo.resize((lg_nw, lg_nh), Image.ANTIALIAS) 186 | if text: 187 | xoffset = lg_nw + font_size / 4 188 | else: 189 | xoffset = lg_nw 190 | w = w + xoffset 191 | _x_logo = int(round((width - w) / 2)) 192 | _y_logo = int(round(yoffset - font_size * (hoffset - 1) / 2)) 193 | try: 194 | canvas.paste(logo, (_x_logo, _y_logo), logo) 195 | except: 196 | canvas.paste(logo, (_x_logo, _y_logo)) 197 | if text: 198 | draw.text(((width - w) / 2 + xoffset, yoffset), text, fill=color, font=_font) 199 | if use_glow: 200 | mask_blur = canvas.split()[-1] 201 | mask_blur = mask_blur.filter(ImageFilter.GaussianBlur(radius=blur * 2)) 202 | fg_blur = canvas.split()[0] 203 | fg_blur = fg_blur.filter(ImageFilter.GaussianBlur(radius=blur * 2)) 204 | glow_text = Image.merge("RGBA", (fg_blur, fg_blur, fg_blur, mask_blur)) 205 | glow_text = glow_text.resize(img.size, Image.ANTIALIAS) 206 | canvas = canvas.resize(img.size, Image.ANTIALIAS) 207 | img.paste(glow_text, (0, 0), mask=glow_text) 208 | img.paste(canvas, (0, 0), mask=canvas) 209 | else: 210 | canvas = canvas.resize(img.size, Image.ANTIALIAS) 211 | img.paste(canvas, (0, 0), mask=canvas) 212 | return img 213 | 214 | 215 | def hsv_to_rgb(h, s, v): 216 | if s == 0.0: 217 | v = int(v * 255) 218 | return v, v, v 219 | i = int(h * 6.) 220 | f = (h * 6.) - i 221 | p, q, t = int(255 * (v * (1. - s))), int(255 * (v * (1. - s * f))), int(255 * (v * (1. - s * (1. - f)))) 222 | v = int(v * 255) 223 | i %= 6 224 | if i == 0: return v, t, p 225 | if i == 1: return q, v, p 226 | if i == 2: return p, v, t 227 | if i == 3: return p, q, v 228 | if i == 4: return t, p, v 229 | if i == 5: return v, p, q 230 | 231 | 232 | def rgb_to_hsv(r, g, b): 233 | r, g, b = r / 255.0, g / 255.0, b / 255.0 234 | mx = max(r, g, b) 235 | mn = min(r, g, b) 236 | df = mx - mn 237 | if mx == mn: 238 | h = 0 239 | elif mx == r: 240 | h = (60 * ((g - b) / df) + 360) % 360 241 | elif mx == g: 242 | h = (60 * ((b - r) / df) + 120) % 360 243 | elif mx == b: 244 | h = (60 * ((r - g) / df) + 240) % 360 245 | if mx == 0: 246 | s = 0 247 | else: 248 | s = df / mx 249 | v = mx 250 | return h / 360, s, v 251 | 252 | 253 | def hex_to_rgb(value): 254 | value = value.lstrip('#') 255 | lv = len(value) 256 | return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3)) 257 | 258 | 259 | def rgb_to_hex(rgb): 260 | return '#%02x%02x%02x' % rgb 261 | 262 | 263 | def glowFx(image, radius=0, brt=1.5): 264 | if radius > 0: 265 | base = image.copy() 266 | image = image.filter(ImageFilter.GaussianBlur(radius=radius)) 267 | base = ImageChops.add(image, base) 268 | return base 269 | else: 270 | return image 271 | 272 | # def glowFx(image, radius=0, brt=1.5): 273 | # if radius > 0: 274 | # base = image.copy() 275 | # image = image.filter(ImageFilter.BoxBlur(radius=radius)) 276 | # enhancer = ImageEnhance.Brightness(image) 277 | # image = enhancer.enhance(brt) 278 | # base.paste(image, (0, 0), image) 279 | # return base 280 | # else: 281 | # return image 282 | -------------------------------------------------------------------------------- /SourceCode/FanWheels_ffmpeg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import imageio_ffmpeg 5 | import subprocess 6 | 7 | 8 | def time2second(time_str): 9 | hour = int(time_str[:-9]) 10 | minute = int(time_str[-8:-6]) 11 | second = int(time_str[-5:-3]) 12 | msecond = int(time_str[-2:]) 13 | return (hour * 3600) + (minute * 60) + second + (msecond / 1000) 14 | 15 | 16 | def ffcmd(args="", console=None, task=0): 17 | ffpath = imageio_ffmpeg.get_ffmpeg_exe() 18 | cmd = ffpath + " " + args 19 | print("ffmpeg:", cmd) 20 | 21 | duration = None 22 | progress = 0 23 | process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", shell=True, 24 | universal_newlines=True) 25 | for line in process.stdout: 26 | input_index = line.find("Input") # "Duration" in file name 27 | if duration is None: 28 | duration_index = line.find("Duration:") 29 | if input_index == -1 and duration_index != -1: 30 | duration_time = line.split(": ")[1].split(",")[0] 31 | duration = time2second(duration_time) 32 | else: 33 | time_index = line.find("time=") 34 | if input_index == -1 and time_index != -1: 35 | now_time = line.split("time=")[1].split(" ")[0] 36 | if duration > 0: 37 | progress = time2second(now_time) / duration 38 | print("Progress: " + str(round(progress * 100)) + "%") 39 | if console is not None: 40 | try: 41 | if task == 1: 42 | console.prepare = progress 43 | elif task == 2: 44 | console.combine = progress 45 | if not console.parent.fb.isRunning: 46 | print("terminate!!!") 47 | process.kill() 48 | break 49 | except: 50 | pass 51 | 52 | print("ffmpeg: Done!") 53 | 54 | 55 | def toTempWaveFile(file_in, file_out, console=None): 56 | cmd = '-i \"{0}\" -ar 44100 -ac 1 -filter:a loudnorm -y \"{1}\"'.format(file_in, file_out) 57 | ffcmd(cmd, console, task=1) 58 | 59 | 60 | def combineVideo(video, audio, file_out, audio_quality="320k", normal=False, console=None): 61 | cmd = '-i \"{0}\" -itsoffset 0.0 -i \"{1}\" '.format(video, audio) 62 | cmd += '-map 0:v:0 -c:v copy -map 1:a:0 -b:a {0} -c:a aac '.format(audio_quality) 63 | if normal: 64 | cmd += '-filter:a loudnorm ' 65 | cmd += '-metadata description=\"Rendered by Fanseline Visualizer\" ' 66 | cmd += '-y \"{0}\"'.format(file_out) 67 | ffcmd(cmd, console, task=2) 68 | 69 | 70 | def cvtFileName(path, new_format=None): 71 | if new_format is None: 72 | return path 73 | last_dot = 0 74 | for i in range(len(path)): 75 | if path[i] == ".": 76 | last_dot = i 77 | ftype = path[last_dot + 1:] 78 | if new_format[0] == ".": 79 | pass 80 | else: 81 | new_format = "." + new_format 82 | if "/" in ftype or "\\" in ftype: 83 | outpath = path + new_format 84 | else: 85 | outpath = path[:last_dot] + new_format 86 | return outpath 87 | 88 | 89 | if __name__ == '__main__': 90 | ffcmd("-version") 91 | -------------------------------------------------------------------------------- /SourceCode/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FerryYoungFan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNEC -------------------------------------------------------------------------------- /SourceCode/LanguagePack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | lang_cn_s = { 5 | "Fanseline Visualizer": "Fanseline Visualizer", 6 | "Stop Rendering": "取消渲染", 7 | "(Rendering...)": "(渲染中……)", 8 | "(Computing...)": "(计算中……)", 9 | "ON": "开", 10 | "OFF": "关", 11 | "Render & Export": "渲染输出", 12 | "Open": "打开", 13 | "Use Color": "使用纯色", 14 | "No Image File": "没有图片文件", 15 | "Frequency Analyzer Preset": "频率分析预设", 16 | "Audio": "音频", 17 | "Video": "视频", 18 | "Spectrum": "频谱", 19 | "Render": "渲染", 20 | "": "FFT低频", 21 | "": "FFT高频", 22 | "Analyzer Frequency Range (Hz)": "频率分析器范围(赫兹)", 23 | "Analyzer Range:": "分析器范围:", 24 | 25 | "": "<请打开一个音频文件>", 26 | "": "<请打开一个字体文件>", 27 | "Select Font": "选择字体", 28 | "Open Audio File": "打开音频文件", 29 | "Audio Settings": "音频设置", 30 | "Video Settings": "视频设置", 31 | "Video Preset": "视频预设", 32 | "Auto Video Bit Rate": "自动视频码率", 33 | "Frame Rate (FPS)": "帧率(FPS)", 34 | "": "<帧率>", 35 | "Frame Size": "帧大小", 36 | "": "<宽>", 37 | "": "<高>", 38 | "Video Bit Rate (Mbps)": "视频码率 (Mbps)", 39 | "": "<码率>", 40 | "Audio Bit Rate": "音频码率", 41 | "Volume Normalize": "音量标准化", 42 | "Volume Normalize:": "音量标准化:", 43 | "Remove": "移除", 44 | "Foreground": "主图片", 45 | "Background": "背景图片", 46 | "Logo": "小图标", 47 | "Select Audio": "选择音频", 48 | "Select Images": "选择图片", 49 | "Back to Main Menu": "返回主菜单", 50 | "Select an Image": "选择一张图片", 51 | "Select Audio File": "选择音频文件", 52 | "Select Color": "选择颜色", 53 | "Customize Color": "自定义颜色", 54 | "Foreground Selected: ": "已选择主图片: ", 55 | "Background Selected: ": "已选择背景图片: ", 56 | "Logo Selected: ": "已选择图标: ", 57 | "Audio Selected: ": "已选择音频: ", 58 | "Image Files": "图片文件", 59 | "All Files": "全部文件", 60 | "Audio Files": "音频文件", 61 | "Video Files": "视频文件", 62 | "Font Files": "字体文件", 63 | "Font Size": "文字大小", 64 | "Text Settings": "文字设置", 65 | "Edit Text": "编辑文字", 66 | "Clear Text": "清空文本", 67 | "Refresh Preview": "刷新预览", 68 | "": "<在此输入文字>", 69 | "Use File Name": "使用文件名", 70 | "Select Font File": "选择字体文件", 71 | "Image Settings": "图片设置", 72 | "Background Mode": "背景模式", 73 | "Spin Foreground": "旋转主图", 74 | "No Spin": "不旋转", 75 | "Clockwise": "顺时针旋转", 76 | "Counterclockwise": "逆时针旋转", 77 | "Spin Speed (rpm)": "旋转速度 (转/分钟)", 78 | "": "<旋转速度>", 79 | "Customize": "自定义", 80 | "Spectrum Color": "频谱颜色", 81 | "Spectrum Saturation": "频谱饱和度", 82 | "Spectrum Brightness": "频谱亮度", 83 | "Glow Effect (Slow)": "发光效果 (缓慢)", 84 | 85 | "Spectrum Style": "频谱样式", 86 | "Spectrum Number": "频谱数量", 87 | "Spectrum Thickness": "频谱线宽", 88 | "Spectrum Scalar": "频谱条灵敏倍率", 89 | "Spectrum Stabilize": "频谱防抖", 90 | 91 | "Save Project": "保存项目", 92 | "Load Project": "读取项目", 93 | "Save Project as...": "项目保存为……", 94 | "FV Project Files": "FV项目文件", 95 | "Open Project File...": "打开项目文件……", 96 | "File Error": "文件错误", 97 | "Cannot Read this Project!": "无法读取该项目!", 98 | 99 | "Save as PNG": "保存为PNG", 100 | "Save as JPG": "保存为JPG", 101 | "Save as BMP": "保存为BMP", 102 | "PNG Files": "PNG文件", 103 | "JPG Files": "JPG文件", 104 | "BMP Files": "BMP文件", 105 | "Files": "文件", 106 | "Output Config": "输出配置", 107 | "Rendering:": "渲染中:", 108 | "Compositing Audio...": "音频合成中……", 109 | "Analyzing Audio...": "分析音频中……", 110 | "Mission Complete!": "任务完成!", 111 | "Elapsed Time:": "已用时:", 112 | "Remaining Time:": "剩余时间:", 113 | 114 | "Output Path:": "输出路径:", 115 | "Audio Path:": "音频路径:", 116 | "Audio Settings:": "音频设置:", 117 | "Video Settings:": "视频设置:", 118 | 119 | "Start Rendering": "开始渲染", 120 | "Ready to Render": "准备渲染", 121 | "About FVisualizer": "关于本软件", 122 | "About": "关于", 123 | "Version: ": "版本:", 124 | "Support me if you like this application:": "如果你喜欢这个应用,欢迎来赞助我:", 125 | "About me:": "关于我:", 126 | "Special thanks to:": "特别感谢:", 127 | "... and all people who support me!": "……以及所有支持我的人!", 128 | 129 | "Output Path": "输出路径", 130 | "Select Path": "选择路径", 131 | "": "<输出路径>", 132 | "Untitled_Video": "未命名视频", 133 | 134 | "This folder is not exist!": "该文件夹不存在!", 135 | "Output folder is not exist!": "输出文件夹不存在!", 136 | "Sorry, this file is not supported!": "抱歉,该文件不受支持!", 137 | "Sorry, this audio file is not supported!": "抱歉,该音频文件不受支持!", 138 | 139 | "Yes": "是", 140 | "No": "否", 141 | "OK": "确定", 142 | 143 | "Rendering Aborted!": "渲染中断!", 144 | 145 | "_Visualize": "_音频可视化", 146 | "Output Path Selected: ": "已选择输出路径: ", 147 | "Font Selected: ": "已选择字体: ", 148 | "Cannot Render": "无法开始渲染", 149 | "Save Image as...": "图片另存为……", 150 | "Snap": "快照", 151 | "Cannot Save Image!": "无法储存图片!", 152 | 153 | "Please select the correct audio file!": "请先正确选择音频文件!", 154 | "Please select the correct output path!": "请正确选择输出路径!", 155 | "Please input the corrent file name!": "请正确输入输出文件名!", 156 | "Cannot open this image file!": "无法打开该图片!", 157 | "Stop Rendering...": "正在取消渲染……", 158 | 159 | "Audio (REQUIRED)": "声音文件【必选】", 160 | "Output (REQUIRED)": "输出路径【必选】", 161 | "Foreground Image": "选择主图片", 162 | "Background Image": "选择背景图片", 163 | "Logo File": "选择小图标", 164 | "Your Text:": "请输入文字:", 165 | "Font Size:": "字体大小:", 166 | "Text Brt.:": "文字亮度:", 167 | "Font File": "选择字体文件", 168 | "Video Size:": "视频尺寸:", 169 | "FPS:": "视频帧率:", 170 | "Render Quality:": "渲染质量:", 171 | "Video BR:": "视频码率:", 172 | "Auto Bit Rate": "自动视频码率", 173 | "Analyze Freq:": "频率分析范围:", 174 | "to": "至", 175 | "Hz": "赫兹", 176 | "Spectrum Num:": "频谱条的数量:", 177 | "Spectrum Scalar:": "频谱条灵敏倍率:", 178 | "Spec. Brt.:": "频谱亮度:", 179 | "Spec. Sat.:": "频谱饱和度:", 180 | "Spectrum Stabilize:": "频谱条防抖:", 181 | "Spec. Hue:": "频谱色调:", 182 | 183 | "Rainbow 4x": "彩虹 4x", 184 | "Rainbow 2x": "彩虹 2x", 185 | "Rainbow 1x": "彩虹 1x", 186 | "White": "白色", 187 | "Black": "黑色", 188 | "Gray": "灰色", 189 | "Red": "红色", 190 | "Green": "绿色", 191 | "Blue": "蓝色", 192 | "Yellow": "黄色", 193 | "Magenta": "品红", 194 | "Purple": "紫色", 195 | "Cyan": "青色", 196 | "Light Green": "浅绿", 197 | "Green - Blue": "渐变:蓝-绿", 198 | "Magenta - Purple": "渐变:红-紫", 199 | "Red - Yellow": "渐变:红-黄", 200 | "Yellow - Green": "渐变:黄-绿", 201 | "Blue - Purple": "渐变:蓝-紫", 202 | 203 | "Audio BR:": "音频码率:", 204 | "Normalize Volume": "标准化音量", 205 | "Frequency Analyzer Preset:": "频率分析预设:", 206 | "Pop Music": "流行音乐", 207 | "Classical Music": "古典音乐", 208 | "Calm Music": "舒缓音乐", 209 | "Orchestral Music": "管弦乐", 210 | "Piano: Narrow Band": "钢琴:窄频", 211 | "Piano: Wide Band": "钢琴:宽频", 212 | "Violin": "小提琴", 213 | "Guitar": "吉他", 214 | "Natural Sound": "自然音", 215 | "Voice": "语音", 216 | 217 | "Video Preset:": "视频预设:", 218 | "Square": "正方形", 219 | "Landscape": "横屏", 220 | "Portrait": "竖屏", 221 | "Very Low": "非常低", 222 | "Low": "低", 223 | "Medium - Default": "中等 - 默认", 224 | "High": "高", 225 | "Very High": "非常高", 226 | "Render Quality": "渲染品质", 227 | 228 | "(Slow)": "(缓慢)", 229 | "Preview": "预览", 230 | "Welcome to use": "欢迎使用", 231 | "-Please Select-": "-请选择-", 232 | "Notice": "通知", 233 | "Please stop rendering before quit!": "请在退出前取消渲染!", 234 | "Are you sure to overwrite this file?": "确定覆盖文件吗?", 235 | "Are you sure to stop rendering?": "确定取消渲染吗?", 236 | 237 | "Spectrum Style:": "频谱风格:", 238 | "Solid Line": "实线频谱", 239 | "Solid Line: Center": "实线频谱:居中", 240 | "Solid Line: Reverse": "实线频谱:反向", 241 | "Dot Line": "点线频谱", 242 | "Single Dot": "单点频谱", 243 | "Double Dot": "双点频谱", 244 | "Double Dot: Center": "双点频谱:居中", 245 | "Double Dot: Reverse": "双点频谱:反向", 246 | "Concentric": "同心圆", 247 | "Line Graph": "折线图", 248 | "Zooming Circles": "缩放圆形", 249 | "Classic Line: Center": "经典实线:中心", 250 | "Classic Line: Bottom": "经典实线:底部", 251 | "Classic Rectangle: Center": "经典长方形:中心", 252 | "Classic Rectangle: Bottom": "经典长方形:底部", 253 | "Classic Round Dot: Center": "经典圆点:中心", 254 | "Classic Round Dot: Bottom": "经典圆点:底部", 255 | "Classic Square Dot: Center": "经典方点:中心", 256 | "Classic Square Dot: Bottom": "经典方点:底部", 257 | "Stem Plot: Solid Single": "Stem图:单点实线", 258 | "Stem Plot: Solid Double": "Stem图:双点实线", 259 | "Stem Plot: Dashed Single": "Stem图:单点虚线", 260 | "Stem Plot: Dashed Double": "Stem图:双点虚线", 261 | "No Spectrum": "无频谱", 262 | 263 | "Line Width:": "频谱线宽:", 264 | "BG Mode:": "背景模式:", 265 | "Blurred BG Image": "模糊背景", 266 | "Normal BG Image": "正常背景", 267 | "Blurred BG Only": "仅模糊背景", 268 | "Normal BG Only": "仅正常背景", 269 | "Transparent": "透明背景", 270 | "Spectrum Only": "仅频谱", 271 | "Glow Text": "发光文字", 272 | "Glow Spec. (SLOW)": "发光频谱【缓慢】", 273 | "Project Website: ": "项目网站:", 274 | "Spin FG(rpm):": "旋转主图(转/分):", 275 | "Oscillate Foreground": "振动主图", 276 | "Oscillate FG to Bass (%)": "随低音振动主图(%)", 277 | "Bass Frequency Range (%)": "低音频率范围(%)", 278 | 279 | "Error! Cannot save config!": "错误!不能保存配置!", 280 | "FFMPEG not found, please install FFMPEG!": "FFMPEG未安装,请安装FFMPEG!", 281 | 282 | "Adjust audio volume according to EBU R128 loudness standard.": "根据 EBU R128 标准调整音频的音量。", 283 | "Visualized spectrum will be generated according to this frequency range.": "软件将根据此频率范围绘制可视化频谱。", 284 | "Oscillate FG image accroding to the bass.": "根据低音振动主图", 285 | "Sensitivity of the bass (beat) detector.": "重低音(节拍)检测器的灵敏度。", 286 | "Relative frequency of bass to analyzer frequency range.": "重低音频率占分析器总频率的相对比例。", 287 | } 288 | 289 | lang_keys = list(lang_cn_s.keys()) 290 | lang_en = {} 291 | for lang_key in lang_keys: 292 | lang_en[lang_key] = lang_key 293 | -------------------------------------------------------------------------------- /SourceCode/Old_Version_Source_Code_Before_V110.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/SourceCode/Old_Version_Source_Code_Before_V110.zip -------------------------------------------------------------------------------- /SourceCode/QtStyle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # font-family: "Microsoft Yahei",Helvetica,Arial,"Hiragino Sans GB","Heiti SC","WenQuanYi Micro Hei",sans-serif; 5 | 6 | stylepack = """ 7 | QWidget { 8 | background-color: #202020; 9 | font-family: "Arial" 10 | } 11 | 12 | QToolTip { 13 | background-color: #898B8D; 14 | color: #222222; 15 | border:None; 16 | } 17 | QLabel[fontset="0"]{ 18 | font-size:15px; 19 | color:#666666; 20 | } 21 | QLabel[fontset="1"]{ 22 | font-size:15px; 23 | color:#999999; 24 | } 25 | QLabel[fontset="2"]{ 26 | font-size:15px; 27 | color:#4A88C7; 28 | font-weight: 600; 29 | margin:10px 5px 0px; 30 | } 31 | QLabel[fontset="3"]{ 32 | font-size:15px; 33 | color:#5A98D7; 34 | } 35 | QLabel[fontset="4"]{ 36 | font-size:15px; 37 | color:#999999; 38 | font-weight: 600; 39 | margin:10px 5px 0px; 40 | } 41 | QComboBox { 42 | border: 1px solid gray; 43 | border-radius: 3px; 44 | padding: 5px; 45 | font-size:15px; 46 | font-weight:600; 47 | color:#222222; 48 | background:#898B8D; 49 | margin:3px 3px 3px; 50 | } 51 | QComboBox::drop-down { 52 | subcontrol-origin: padding; 53 | subcontrol-position: top right; 54 | width: 20px; 55 | border-left-width: 1px; 56 | border-left-color: darkgray; 57 | border-left-style: solid; 58 | border-top-right-radius: 3px; 59 | border-bottom-right-radius: 3px; 60 | } 61 | QComboBox::down-arrow { 62 | image: url(./GUI/Image/down_arrow.png); 63 | } 64 | QComboBox QAbstractItemView{ 65 | border: 0px; 66 | margin: 0px; 67 | color:#222222; 68 | font-weight:600; 69 | background:#898B8D; 70 | selection-background-color:#437BB5; 71 | outline: none; 72 | } 73 | QListView{ 74 | font-size:15px; 75 | font-weight:600; 76 | } 77 | QListView::item { 78 | height: 40px; 79 | } 80 | QColorDialog QWidget{ 81 | background-color: #898B8D; 82 | } 83 | QColorDialog { 84 | background-color: #898B8D; 85 | } 86 | """ 87 | 88 | sliderStyleDefault = """ 89 | QSlider{ 90 | margin: 0px 10px 10px; 91 | } 92 | QSlider::groove:horizontal { 93 | height: 20px; 94 | border-radius: 10px; 95 | margin: -5px 0; 96 | } 97 | 98 | QSlider::handle:horizontal { 99 | background: #898B8D; 100 | width: 30px; 101 | height: 30px; 102 | border-radius: 5; 103 | } 104 | 105 | QSlider::handle:hover { 106 | background: #5E6264; 107 | } 108 | 109 | QSlider::handle:pressed { 110 | background: #3D3F40; 111 | } 112 | 113 | QSlider::add-page:horizontal { 114 | background: #313335; 115 | } 116 | 117 | QSlider::sub-page:horizontal { 118 | background: #4A88C7; 119 | } 120 | 121 | 122 | """ 123 | sliderStyle1 = """ 124 | QSlider{ 125 | margin: 0px 10px 10px; 126 | } 127 | QSlider::groove:horizontal { 128 | height: 20px; 129 | border-radius: 10px; 130 | margin: -5px 0; 131 | } 132 | 133 | QSlider::handle:horizontal { 134 | background: #898B8D; 135 | width: 30px; 136 | height: 20px; 137 | border-radius: 5; 138 | } 139 | 140 | QSlider::handle:hover { 141 | background: #5E6264; 142 | } 143 | 144 | QSlider::handle:pressed { 145 | background: #3D3F40; 146 | } 147 | QSlider::add-page:horizontal { 148 | background: #313335; 149 | } 150 | 151 | QSlider::sub-page:horizontal { 152 | background: #405368; 153 | } 154 | """ 155 | sliderStyle2 = """ 156 | QSlider{ 157 | margin: 0px 10px 10px; 158 | } 159 | QSlider::groove:horizontal { 160 | height: 20px; 161 | border-radius: 10px; 162 | margin:-5px 0; 163 | } 164 | 165 | QSlider::handle:horizontal { 166 | background: #898B8D; 167 | width: 30px; 168 | height: 20px; 169 | border-radius: 5; 170 | } 171 | 172 | QSlider::handle:hover { 173 | background: #5E6264; 174 | } 175 | 176 | QSlider::handle:pressed { 177 | background: #3D3F40; 178 | } 179 | QSlider::add-page:horizontal { 180 | background: #405368; 181 | } 182 | 183 | QSlider::sub-page:horizontal { 184 | background: #313335; 185 | } 186 | """ 187 | sliderStyle3 = """ 188 | QSlider{ 189 | margin: 0px 10px 10px; 190 | } 191 | QSlider::groove:horizontal { 192 | height: 20px; 193 | border-radius: 10px; 194 | margin: -5px 0; 195 | } 196 | 197 | QSlider::handle:horizontal { 198 | background: #898B8D; 199 | width: 30px; 200 | height: 20px; 201 | border-radius: 5; 202 | } 203 | 204 | QSlider::handle:hover { 205 | background: #5E6264; 206 | } 207 | 208 | QSlider::handle:pressed { 209 | background: #3D3F40; 210 | } 211 | QSlider::add-page:horizontal { 212 | background: #9C9642; 213 | } 214 | QSlider::sub-page:horizontal { 215 | background: #437BB5; 216 | } 217 | """ 218 | sliderStyle4 = """ 219 | QSlider{ 220 | margin: 0px 10px 10px; 221 | } 222 | QSlider::groove:horizontal { 223 | height: 20px; 224 | border-radius: 10px; 225 | margin: -5px 0; 226 | } 227 | 228 | QSlider::handle:horizontal { 229 | background: #898B8D; 230 | width: 30px; 231 | height: 20px; 232 | border-radius: 5; 233 | } 234 | 235 | QSlider::handle:hover { 236 | background: #5E6264; 237 | } 238 | 239 | QSlider::handle:pressed { 240 | background: #3D3F40; 241 | } 242 | QSlider::add-page:horizontal { 243 | background: #A343B5; 244 | } 245 | QSlider::sub-page:horizontal { 246 | background: #40874A; 247 | } 248 | """ 249 | sliderStyle5 = """ 250 | QSlider{ 251 | margin: 0px 10px 10px; 252 | } 253 | QSlider::groove:horizontal { 254 | height: 20px; 255 | border-radius: 10px; 256 | margin: -5px 0; 257 | } 258 | 259 | QSlider::handle:horizontal { 260 | background: #898B8D; 261 | width: 30px; 262 | height: 20px; 263 | border-radius: 5; 264 | } 265 | 266 | QSlider::handle:hover { 267 | background: #5E6264; 268 | } 269 | 270 | QSlider::handle:pressed { 271 | background: #3D3F40; 272 | } 273 | QSlider::add-page:horizontal { 274 | background: #4A88C7; 275 | } 276 | QSlider::sub-page:horizontal { 277 | background: #405368; 278 | } 279 | """ 280 | pushButtonStyle1 = """ 281 | QPushButton { 282 | background: #898B8D; 283 | padding:8px 3px 5px; 284 | margin:3px 3px 3px; 285 | border-radius: 3; 286 | font-size:15px; 287 | font-weight: 600; 288 | color:#222222; 289 | } 290 | QPushButton:hover { 291 | background: #5E6264; 292 | } 293 | QPushButton:pressed { 294 | background: #3D3F40; 295 | } 296 | QPushButton:disabled{ 297 | background: #111111; 298 | color:#444444; 299 | } 300 | """ 301 | 302 | pushButtonStyle2 = """ 303 | QPushButton { 304 | background: #437BB5; 305 | padding:8px 3px 5px; 306 | margin:3px 3px 3px; 307 | border-radius: 3; 308 | font-size:15px; 309 | font-weight: 600; 310 | color:#CCCCCC; 311 | } 312 | QPushButton:hover { 313 | background: #3C6691; 314 | } 315 | QPushButton:pressed { 316 | background: #2E363F; 317 | color:#666666; 318 | } 319 | QPushButton:disabled{ 320 | background: #111111; 321 | color:#444444; 322 | } 323 | """ 324 | 325 | pushButtonStyle3 = """ 326 | QPushButton { 327 | background: #B54643; 328 | padding:8px 3px 5px; 329 | margin:3px 3px 3px; 330 | border-radius: 3; 331 | font-size:15px; 332 | font-weight: 600; 333 | color:#CCCCCC; 334 | } 335 | QPushButton:hover { 336 | background: #913A37; 337 | } 338 | QPushButton:pressed { 339 | background: #3F2E2E; 340 | color:#666666; 341 | } 342 | QPushButton:disabled{ 343 | background: #111111; 344 | color:#444444; 345 | } 346 | """ 347 | 348 | pushButtonStyle4 = """ 349 | QPushButton { 350 | background: #40874A; 351 | padding:8px 3px 5px; 352 | margin:3px 3px 3px; 353 | border-radius: 3; 354 | font-size:15px; 355 | font-weight: 600; 356 | color:#CCCCCC; 357 | } 358 | QPushButton:hover { 359 | background: #3E7746; 360 | } 361 | QPushButton:pressed { 362 | background: #354F39; 363 | color:#666666; 364 | } 365 | QPushButton:disabled{ 366 | background: #111111; 367 | color:#444444; 368 | } 369 | """ 370 | 371 | pushButtonStyle5 = """ 372 | QPushButton { 373 | background: #9C9642; 374 | padding:8px 3px 5px; 375 | margin:3px 3px 3px; 376 | border-radius: 3; 377 | font-size:15px; 378 | font-weight: 600; 379 | color:#2C2C2C; 380 | } 381 | QPushButton:hover { 382 | background: #7F7B43; 383 | } 384 | QPushButton:pressed { 385 | background: #545030; 386 | } 387 | QPushButton:disabled{ 388 | background: #111111; 389 | color:#444444; 390 | } 391 | """ 392 | 393 | pushButtonStyle6 = """ 394 | QPushButton { 395 | background: #898B8D; 396 | padding:8px 3px 5px; 397 | margin:3px 3px 3px; 398 | border-radius: 3; 399 | font-size:15px; 400 | font-weight: 600; 401 | color:#B52222; 402 | } 403 | QPushButton:hover { 404 | background: #5E6264; 405 | } 406 | QPushButton:pressed { 407 | background: #3D3F40; 408 | } 409 | QPushButton:disabled{ 410 | background: #111111; 411 | color:#444444; 412 | } 413 | """ 414 | 415 | viewerStyle = """ 416 | QFrame{ 417 | border: 1px solid #437BB5; 418 | border-top:0px; 419 | border-bottom:0px; 420 | border-left:0px; 421 | } 422 | """ 423 | 424 | lineEditStyle = """ 425 | QLineEdit{ 426 | outline: none; 427 | border:2px solid #437BB5; 428 | background: #B9BBBD; 429 | padding:8px 3px 5px; 430 | margin:3px 3px 3px; 431 | border-radius: 3; 432 | font-size:15px; 433 | font-weight: 600; 434 | color:#222222; 435 | } 436 | QLineEdit:disabled{ 437 | border:2px solid #111111; 438 | background: #111111; 439 | color:#444444; 440 | } 441 | QMenu { 442 | background: #898B8D; 443 | padding:8px 3px 5px; 444 | font-size:15px; 445 | font-weight: 600; 446 | color:#222222; 447 | } 448 | QMenu::item{ 449 | margin:2px; 450 | padding:5px; 451 | } 452 | QMenu::item::selected { 453 | background-color: #5E6264; 454 | } 455 | QMenu::item::disabled { 456 | color:#4E5254 457 | } 458 | """ 459 | 460 | scrollBarStyle = """ 461 | QScrollBar:horizontal { 462 | border: none; 463 | background: none; 464 | height: 20px; 465 | margin: 0px 20px 0 20px; 466 | } 467 | 468 | QScrollBar::handle:horizontal { 469 | background: #898B8D; 470 | min-width: 20px; 471 | } 472 | 473 | QScrollBar::add-line:horizontal { 474 | background: none; 475 | width: 20px; 476 | subcontrol-position: right; 477 | subcontrol-origin: margin; 478 | 479 | } 480 | 481 | QScrollBar::sub-line:horizontal { 482 | background: none; 483 | width: 20px; 484 | subcontrol-position: top left; 485 | subcontrol-origin: margin; 486 | position: absolute; 487 | } 488 | 489 | QScrollBar:left-arrow:horizontal, QScrollBar::right-arrow:horizontal { 490 | width: 20px; 491 | height: 20px; 492 | background: #898B8D; 493 | } 494 | 495 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { 496 | background: #202020; 497 | } 498 | 499 | /* VERTICAL */ 500 | QScrollBar:vertical { 501 | border: none; 502 | background: none; 503 | width: 20px; 504 | margin: 20px 0 20px 0; 505 | } 506 | 507 | QScrollBar::handle:vertical { 508 | background: #898B8D; 509 | min-height: 20px; 510 | } 511 | 512 | QScrollBar::add-line:vertical { 513 | background: none; 514 | height: 20px; 515 | subcontrol-position: bottom; 516 | subcontrol-origin: margin; 517 | } 518 | 519 | QScrollBar::sub-line:vertical { 520 | background: none; 521 | height: 20px; 522 | subcontrol-position: top left; 523 | subcontrol-origin: margin; 524 | position: absolute; 525 | } 526 | 527 | QScrollBar:up-arrow:vertical, QScrollBar::down-arrow:vertical { 528 | width: 20px; 529 | height: 20px; 530 | background: #898B8D; 531 | } 532 | 533 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 534 | background: #202020; 535 | } 536 | """ 537 | 538 | scrollBarStyle2 = """ 539 | /* VERTICAL */ 540 | QScrollBar:vertical { 541 | border: none; 542 | background: none; 543 | width: 20px; 544 | margin: 0 0 0 0; 545 | } 546 | 547 | QScrollBar::handle:vertical { 548 | background: #437BB5; 549 | min-height: 20px; 550 | } 551 | 552 | QScrollBar::add-line:vertical { 553 | background: none; 554 | height: 0px; 555 | subcontrol-position: bottom; 556 | subcontrol-origin: margin; 557 | } 558 | 559 | QScrollBar::sub-line:vertical { 560 | background: none; 561 | height: 0px; 562 | subcontrol-position: top left; 563 | subcontrol-origin: margin; 564 | position: absolute; 565 | } 566 | 567 | QScrollBar:up-arrow:vertical, QScrollBar::down-arrow:vertical { 568 | width: 20px; 569 | height: 0px; 570 | background: #898B8D; 571 | } 572 | 573 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 574 | background: #202020; 575 | } 576 | """ 577 | 578 | menuStyle = """ 579 | QMenu { 580 | background: #898B8D; 581 | padding:8px 3px 5px; 582 | font-size:15px; 583 | font-weight: 600; 584 | color:#222222; 585 | } 586 | QMenu::item{ 587 | margin:2px; 588 | padding:5px; 589 | } 590 | QMenu::item::selected { 591 | background-color: #5E6264; 592 | } 593 | QMenu::item::disabled { 594 | color:#4E5254 595 | } 596 | """ 597 | 598 | progressbarStyle1 = """ 599 | QProgressBar{ 600 | border: 2px solid #898B8D; 601 | border-radius: 3px; 602 | background-color: #222222; 603 | margin:3px 3px 3px; 604 | text-align: center; 605 | color:#CCCCCC; 606 | } 607 | QProgressBar::chunk { 608 | background-color: #40874A; 609 | width: 1px; 610 | } 611 | """ 612 | 613 | progressbarStyle2 = """ 614 | QProgressBar{ 615 | border: 2px solid #898B8D; 616 | border-radius: 3px; 617 | background-color: #222222; 618 | margin:3px 3px 3px; 619 | text-align: center; 620 | color:#CCCCCC; 621 | } 622 | QProgressBar::chunk { 623 | background-color: #3C6691; 624 | width: 1px; 625 | } 626 | """ 627 | 628 | textBrowserStyle = """ 629 | 630 | QTextBrowser{ 631 | background: #202020; 632 | color: #898B8D; 633 | font-weight:600; 634 | font-size:20em; 635 | } 636 | 637 | QTextBrowser hr{ 638 | color:red; 639 | background:red; 640 | } 641 | 642 | QScrollBar:horizontal { 643 | border: none; 644 | background: none; 645 | height: 12px; 646 | margin: 0; 647 | } 648 | 649 | QScrollBar::handle:horizontal { 650 | background: #437BB5; 651 | min-width: 12px; 652 | } 653 | 654 | QScrollBar::add-line:horizontal { 655 | background: none; 656 | width: 12px; 657 | subcontrol-position: right; 658 | subcontrol-origin: margin; 659 | 660 | } 661 | 662 | QScrollBar::sub-line:horizontal { 663 | background: none; 664 | width: 12px; 665 | subcontrol-position: top left; 666 | subcontrol-origin: margin; 667 | position: absolute; 668 | } 669 | 670 | QScrollBar:left-arrow:horizontal, QScrollBar::right-arrow:horizontal { 671 | width: 0px; 672 | height: 12px; 673 | background: #898B8D; 674 | } 675 | 676 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { 677 | background: #202020; 678 | } 679 | 680 | 681 | QScrollBar:vertical { 682 | border: none; 683 | background: none; 684 | width: 12px; 685 | margin: 0 0 0 0; 686 | } 687 | 688 | QScrollBar::handle:vertical { 689 | background: #437BB5; 690 | min-height: 12px; 691 | } 692 | 693 | QScrollBar::add-line:vertical { 694 | background: none; 695 | height: 0px; 696 | subcontrol-position: bottom; 697 | subcontrol-origin: margin; 698 | } 699 | 700 | QScrollBar::sub-line:vertical { 701 | background: none; 702 | height: 0px; 703 | subcontrol-position: top left; 704 | subcontrol-origin: margin; 705 | position: absolute; 706 | } 707 | 708 | QScrollBar:up-arrow:vertical, QScrollBar::down-arrow:vertical { 709 | width: 12px; 710 | height: 0px; 711 | background: #898B8D; 712 | } 713 | 714 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 715 | background: #202020; 716 | } 717 | 718 | """ 719 | -------------------------------------------------------------------------------- /SourceCode/QtViewer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from QtWheels import * 5 | from QtImages import img_pack, pil2qt 6 | from PIL import Image 7 | 8 | 9 | class PhotoViewer(QtWidgets.QGraphicsView): 10 | photoClicked = QtCore.pyqtSignal(QtCore.QPoint) 11 | 12 | def __init__(self, parent): 13 | super(PhotoViewer, self).__init__(parent) 14 | self.parent = parent 15 | self.lang = parent.lang 16 | self._zoom = 0 17 | self._empty = True 18 | self._scene = QtWidgets.QGraphicsScene(self) 19 | self._photo = PixmapWithDrop(self, self.parent.fileEvent) 20 | self._photo.setTransformationMode(1) 21 | 22 | self.lang = parent.lang 23 | 24 | self._scene.addItem(self._photo) 25 | self.parent = parent 26 | self.setScene(self._scene) 27 | self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 28 | self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 29 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 30 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 31 | self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(10, 10, 10))) 32 | self.setFrameShape(QtWidgets.QFrame.NoFrame) 33 | self.img_cache = None 34 | 35 | self.oldSize = None 36 | 37 | def hasPhoto(self): 38 | return not self._empty 39 | 40 | def fitInView(self, scale=True): 41 | rect = QtCore.QRectF(self._photo.pixmap().rect()) 42 | if not rect.isNull(): 43 | self.setSceneRect(rect) 44 | if self.hasPhoto(): 45 | unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) 46 | self.scale(1 / unity.width(), 1 / unity.height()) 47 | viewrect = self.viewport().rect() 48 | scenerect = self.transform().mapRect(rect) 49 | factor = min(viewrect.width() / scenerect.width(), 50 | viewrect.height() / scenerect.height()) 51 | self.scale(factor, factor) 52 | self._zoom = 0 53 | 54 | def imshow(self, image=None): 55 | if image is None: 56 | image = img_pack["nofile"] 57 | self._photo.setPixmap(pil2qt(image)) 58 | self.setDragMode(QtWidgets.QGraphicsView.NoDrag) 59 | else: 60 | try: 61 | self._photo.setPixmap(pil2qt(image)) 62 | self.img_cache = image.copy() 63 | self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag) 64 | except: 65 | image = img_pack["nofile"] 66 | self._photo.setPixmap(pil2qt(image)) 67 | self.setDragMode(QtWidgets.QGraphicsView.NoDrag) 68 | self._empty = False 69 | if not self.oldSize == self.img_cache.size: 70 | self.fitInView() 71 | self.oldSize = self.img_cache.size 72 | 73 | def wheelEvent(self, event): 74 | if self.hasPhoto(): 75 | if event.angleDelta().y() > 0: 76 | factor = 1.25 77 | self._zoom += 1 78 | else: 79 | factor = 0.8 80 | self._zoom -= 1 81 | if self._zoom > 0: 82 | self.scale(factor, factor) 83 | elif self._zoom <= 0: 84 | self.fitInView() 85 | 86 | def mousePressEvent(self, event): 87 | super(PhotoViewer, self).mousePressEvent(event) 88 | point = self.mapToScene(event.pos()).toPoint() 89 | 90 | def mouseMoveEvent(self, event): 91 | super(PhotoViewer, self).mouseMoveEvent(event) 92 | 93 | def mouseReleaseEvent(self, event): 94 | super(PhotoViewer, self).mouseReleaseEvent(event) 95 | 96 | def contextMenuEvent(self, event): 97 | menu = QtWidgets.QMenu(self) 98 | menu.setStyleSheet(menuStyle) 99 | 100 | if not self.parent.isRunning: 101 | refresh = menu.addAction(self.lang["Refresh Preview"]) 102 | menu.addSeparator() 103 | save_prj = menu.addAction(self.lang["Save Project"]) 104 | load_prj = menu.addAction(self.lang["Load Project"]) 105 | menu.addSeparator() 106 | else: 107 | refresh = None 108 | 109 | savepng = menu.addAction(self.lang["Save as PNG"]) 110 | savejpg = menu.addAction(self.lang["Save as JPG"]) 111 | savebmp = menu.addAction(self.lang["Save as BMP"]) 112 | 113 | action = menu.exec_(self.mapToGlobal(event.pos())) 114 | 115 | if self.dragMode() == QtWidgets.QGraphicsView.ScrollHandDrag and self.img_cache is not None: 116 | if not self.parent.isRunning: 117 | if action == refresh: 118 | self.parent.refreshAll() 119 | elif action == save_prj: 120 | self.parent.saveProject() 121 | elif action == load_prj: 122 | self.parent.loadProject() 123 | 124 | elif action == savepng: 125 | file_, filetype = QtWidgets.QFileDialog.getSaveFileName(self, 126 | self.lang["Save as PNG"], 127 | self.lang["Snap"] + ".png", 128 | self.lang["PNG Files"] + " (*.png)") 129 | if file_: 130 | self.img_cache.save(file_, quality=100) 131 | elif action == savejpg: 132 | file_, filetype = QtWidgets.QFileDialog.getSaveFileName(self, 133 | self.lang["Save as JPG"], 134 | self.lang["Snap"] + ".jpg", 135 | self.lang["JPG Files"] + " (*.jpg)") 136 | if file_: 137 | self.img_cache.save(file_, quality=100) 138 | elif action == savebmp: 139 | file_, filetype = QtWidgets.QFileDialog.getSaveFileName(self, 140 | self.lang["Save as BMP"], 141 | self.lang["Snap"] + ".bmp", 142 | self.lang["BMP Files"] + " (*.bmp)") 143 | if file_: 144 | self.img_cache.save(file_, quality=100) 145 | 146 | def getPixmap(self): 147 | return self._photo.pixmap() 148 | 149 | def resizeEvent(self, event): 150 | if self.width() > 0: 151 | self.fitInView() 152 | super(PhotoViewer, self).resizeEvent(event) 153 | 154 | 155 | class ThumbViewer(QtWidgets.QGraphicsView): 156 | photoClicked = QtCore.pyqtSignal(QtCore.QPoint) 157 | 158 | def __init__(self, parent): 159 | super(ThumbViewer, self).__init__(parent) 160 | self.parent = parent 161 | self.lang = parent.lang 162 | self._zoom = 0 163 | self._empty = True 164 | self._scene = QtWidgets.QGraphicsScene(self) 165 | self._photo = PixmapWithDrop(self, self.parent.fileEvent) 166 | self._photo.setTransformationMode(1) 167 | 168 | self._scene.addItem(self._photo) 169 | self.parent = parent 170 | self.setScene(self._scene) 171 | self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 172 | self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 173 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 174 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 175 | self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(32, 32, 32))) 176 | self.setFrameShape(QtWidgets.QFrame.NoFrame) 177 | self.imshow(None) 178 | 179 | def hasPhoto(self): 180 | return not self._empty 181 | 182 | def fitInView(self, scale=True): 183 | rect = QtCore.QRectF(self._photo.pixmap().rect()) 184 | if not rect.isNull(): 185 | self.setSceneRect(rect) 186 | if self.hasPhoto(): 187 | unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) 188 | self.scale(1 / unity.width(), 1 / unity.height()) 189 | viewrect = self.viewport().rect() 190 | scenerect = self.transform().mapRect(rect) 191 | factor = min(viewrect.width() / scenerect.width(), 192 | viewrect.height() / scenerect.height()) 193 | self.scale(factor, factor) 194 | self._zoom = 0 195 | 196 | def imshow(self, image=None): 197 | if image is None: 198 | image = img_pack["nofile"] 199 | self._photo.setPixmap(pil2qt(image)) 200 | else: 201 | try: 202 | self._photo.setPixmap(pil2qt(image)) 203 | except: 204 | image = img_pack["nofile"] 205 | self._photo.setPixmap(pil2qt(image)) 206 | self._empty = False 207 | self.fitInView() 208 | 209 | def getPixmap(self): 210 | return self._photo.pixmap() 211 | 212 | def resizeEvent(self, event): 213 | if self.width() > 0: 214 | self.fitInView() 215 | super(ThumbViewer, self).resizeEvent(event) 216 | 217 | 218 | class ImageSelector(QtWidgets.QWidget): 219 | def __init__(self, parent, title, image_path=None): 220 | super(ImageSelector, self).__init__() 221 | self.lang = parent.lang 222 | self.parent = parent 223 | self.image_formats = parent.image_formats 224 | self.image_formats_arr = getFormats(self.image_formats) 225 | 226 | left = QtWidgets.QFrame(self) 227 | VBlayout_l = QtWidgets.QVBoxLayout(left) 228 | 229 | self.viewer = ThumbViewer(self) 230 | VBlayout_l.addWidget(self.viewer) 231 | VBlayout_l.setSpacing(0) 232 | VBlayout_l.setContentsMargins(0, 0, 0, 0) 233 | 234 | right = QtWidgets.QFrame(self) 235 | VBlayout_r = QtWidgets.QVBoxLayout(right) 236 | self.btn_open = genButton(self, self.lang["Open"], None, self.btn_open_release, style=2) 237 | self.btn_remove = genButton(self, self.lang["Remove"], None, self.btn_remove_release, style=5) 238 | self.btn_color = genButton(self, self.lang["Use Color"], None, self.btn_color_release) 239 | self.btn_color.setIcon(pil2icon(img_pack["color"])) 240 | tlabel = genLabel(self, title, 1) 241 | tlabel.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter) 242 | VBlayout_r.addWidget(tlabel) 243 | HBlayout_small = QtWidgets.QHBoxLayout() 244 | HBlayout_small.addWidget(self.btn_open) 245 | HBlayout_small.addWidget(self.btn_remove) 246 | VBlayout_r.addLayout(HBlayout_small) 247 | VBlayout_r.addWidget(self.btn_color) 248 | 249 | right.setMinimumWidth(150) 250 | 251 | mainlayout = QtWidgets.QVBoxLayout(self) 252 | 253 | HBlayout = QtWidgets.QHBoxLayout() 254 | HBlayout.setSpacing(0) 255 | HBlayout.setContentsMargins(0, 0, 0, 0) 256 | HBlayout.addWidget(left) 257 | HBlayout.addWidget(right) 258 | mainlayout.addLayout(HBlayout) 259 | 260 | self.viewer.fitInView() 261 | if image_path is not None: 262 | self.image_vidc = image_path 263 | self.image_path = self.parent.parent.vdic[self.image_vidc] 264 | self.image = None 265 | 266 | def fileEvent(self, path): 267 | if getFileSuffix(path)[1:].lower() in self.image_formats_arr: 268 | self.parent.parent.vdic[self.image_vidc] = path 269 | self.image_path = path 270 | self.imshow() 271 | self.parent.parent.refreshAll() 272 | else: 273 | showInfo(self, self.lang["Notice"], self.lang["Sorry, this file is not supported!"]) 274 | 275 | def btn_open_release(self): 276 | selector = self.lang["Image Files"] + self.image_formats 277 | selector = selector + self.lang["All Files"] + " (*.*)" 278 | if self.image_path is not None and isinstance(self.image_path,str) and os.path.exists(self.image_path): 279 | folder = self.image_path 280 | else: 281 | folder = "" 282 | file_, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.lang["Select an Image"], folder, selector) 283 | if not file_: 284 | print("No File!") 285 | else: 286 | self.parent.parent.vdic[self.image_vidc] = file_ 287 | self.image_path = file_ 288 | self.imshow() 289 | self.parent.parent.refreshAll() 290 | 291 | def btn_color_release(self): 292 | if isinstance(self.parent.parent.vdic[self.image_vidc], tuple): 293 | oldColor = self.parent.parent.vdic[self.image_vidc] 294 | else: 295 | oldColor = (255, 255, 255, 255) 296 | colorDialog = QtWidgets.QColorDialog() 297 | color = colorDialog.getColor(QtGui.QColor(*oldColor), self, self.lang["Select Color"]) 298 | if color.isValid(): 299 | self.parent.parent.vdic[self.image_vidc] = color.getRgb() 300 | self.imshow() 301 | self.parent.parent.refreshAll() 302 | 303 | def imshow(self): 304 | self.image_path = self.parent.parent.vdic[self.image_vidc] 305 | if isinstance(self.image_path, str): 306 | try: 307 | self.image = Image.open(self.image_path).convert('RGBA') 308 | self.btn_remove.setEnabled(True) 309 | self.viewer.setToolTip(self.image_path.replace("\\", "/")) 310 | except: 311 | self.image_path = None 312 | self.parent.parent.vdicBackup() 313 | self.parent.parent.vdic[self.image_vidc] = None 314 | self.image = None 315 | self.btn_remove.setEnabled(False) 316 | self.viewer.setToolTip(self.lang["No Image File"]) 317 | if self.isVisible(): 318 | showInfo(self, self.lang["Notice"], self.lang["Cannot open this image file!"], False) 319 | elif isinstance(self.image_path, tuple): 320 | self.image = Image.new("RGB", (512, 512), self.image_path[:3]) 321 | self.btn_remove.setEnabled(True) 322 | self.viewer.setToolTip(("#%02x%02x%02x" % self.image_path[:3]).upper()) 323 | else: 324 | self.image_path = None 325 | self.parent.parent.vdicBackup() 326 | self.parent.parent.vdic[self.image_vidc] = None 327 | self.image = None 328 | self.btn_remove.setEnabled(False) 329 | self.viewer.setToolTip(self.lang["No Image File"]) 330 | self.viewer.imshow(self.image) 331 | 332 | def btn_remove_release(self): 333 | self.parent.parent.vdic[self.image_vidc] = None 334 | self.image_path = None 335 | self.imshow() 336 | self.parent.parent.refreshAll() 337 | 338 | 339 | class ImageSelectWindow(QtWidgets.QWidget): 340 | def __init__(self, parent): 341 | super(ImageSelectWindow, self).__init__() 342 | self.parent = parent 343 | self.lang = parent.lang 344 | self.image_formats = parent.image_formats 345 | self.selector1 = ImageSelector(self, self.lang["Foreground"], "image_path") 346 | self.selector2 = ImageSelector(self, self.lang["Background"], "bg_path") 347 | self.selector3 = ImageSelector(self, self.lang["Logo"], "logo_path") 348 | VBlayout = QtWidgets.QVBoxLayout(self) 349 | VBlayout.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignCenter) 350 | VBlayout.addWidget(genLabel(self, self.lang["Select Images"], 2)) 351 | VBlayout.addWidget(self.selector1) 352 | VBlayout.addWidget(self.selector2) 353 | VBlayout.addWidget(self.selector3) 354 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 355 | btn_back.setIcon(pil2icon(img_pack["back"])) 356 | VBlayout.addWidget(btn_back) 357 | VBlayout.setSpacing(15) 358 | VBlayout.setContentsMargins(0, 0, 0, 0) 359 | 360 | def btn_back_release(self): 361 | self.parent.mainMenu.show() 362 | 363 | def show(self): 364 | self.parent.hideAllMenu() 365 | self.selector1.imshow() 366 | self.selector2.imshow() 367 | self.selector3.imshow() 368 | super().show() 369 | -------------------------------------------------------------------------------- /SourceCode/QtWheels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys, os 5 | from PyQt5 import QtGui, QtCore, QtWidgets 6 | import numpy as np 7 | from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication 8 | from QtImages import * 9 | from QtStyle import * 10 | 11 | 12 | class FSlider(QtWidgets.QWidget): 13 | def __init__(self, parent, slName="", minv=0.0, maxv=100.0, stepv=1.0, ivalue=0.0, releaseEvent=None, 14 | changeEvent=None, sl_type=0): 15 | super(FSlider, self).__init__(parent) 16 | self.slider = QtWidgets.QSlider(self) 17 | self.slider.setOrientation(QtCore.Qt.Horizontal) 18 | 19 | if releaseEvent: 20 | self.slider.sliderReleased.connect(releaseEvent) 21 | 22 | self.slider.valueChanged.connect(self.do_changeEvent) 23 | 24 | self.do_releaseEvent = releaseEvent 25 | self.sl_changeEvent = changeEvent 26 | 27 | label_minimum = QtWidgets.QLabel(alignment=QtCore.Qt.AlignLeft) 28 | label_maximum = QtWidgets.QLabel(alignment=QtCore.Qt.AlignRight) 29 | self.label_name = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter) 30 | self.label_name.setText(slName) 31 | self.slName = slName 32 | label_minimum.setNum(minv) 33 | label_maximum.setNum(maxv) 34 | label_minimum.setProperty("fontset", 0) 35 | label_maximum.setProperty("fontset", 0) 36 | self.label_name.setProperty("fontset", 1) 37 | 38 | VBlayout = QtWidgets.QVBoxLayout(self) 39 | HBlayout = QtWidgets.QHBoxLayout() 40 | HBlayout.addWidget(label_minimum) 41 | HBlayout.addWidget(self.label_name) 42 | HBlayout.addWidget(label_maximum) 43 | VBlayout.addLayout(HBlayout) 44 | VBlayout.addWidget(self.slider) 45 | 46 | self.minv = minv 47 | self.maxv = maxv 48 | self.stepv = stepv 49 | self.divs = 50 50 | 51 | self.intFlag = False 52 | 53 | self.setStyle(sl_type) 54 | 55 | if isinstance(minv, int) and isinstance(maxv, int) and isinstance(stepv, int): 56 | self.intFlag = True 57 | self.slider.setMinimum(minv) 58 | self.slider.setMaximum(maxv) 59 | self.slider.setSingleStep(stepv) 60 | else: 61 | self.intFlag = False 62 | self.slider.setMinimum(0) 63 | self.divs = int((self.maxv - self.minv) / self.stepv) 64 | self.slider.setMaximum(self.divs) 65 | self.slider.setSingleStep(1) 66 | 67 | self.setValue(ivalue) 68 | self.slider.keyPressEvent = self.nullEvent 69 | self.slider.dragMoveEvent = self.nullEvent 70 | self.slider.setFocusPolicy(QtCore.Qt.NoFocus) 71 | self.slider.wheelEvent = self.wheelEvent 72 | 73 | def setStyle(self, sl_type): 74 | if sl_type == 1: 75 | self.slider.setStyleSheet(sliderStyle1) 76 | elif sl_type == 2: 77 | self.slider.setStyleSheet(sliderStyle2) 78 | elif sl_type == 3: 79 | self.slider.setStyleSheet(sliderStyle3) 80 | elif sl_type == 4: 81 | self.slider.setStyleSheet(sliderStyle4) 82 | elif sl_type == 5: 83 | self.slider.setStyleSheet(sliderStyle5) 84 | else: 85 | self.slider.setStyleSheet(sliderStyleDefault) 86 | 87 | def wheelEvent(self, event): 88 | super().wheelEvent(event) 89 | if event.angleDelta().y() > 0: 90 | newv = self.getValue() + self.stepv 91 | if newv > self.maxv: 92 | self.setValue(self.maxv) 93 | else: 94 | self.setValue(self.getValue() + self.stepv) 95 | else: 96 | newv = self.getValue() - self.stepv 97 | if newv < self.minv: 98 | self.setValue(self.minv) 99 | else: 100 | self.setValue(self.getValue() - self.stepv) 101 | self.do_releaseEvent() 102 | 103 | def nullEvent(self, *args): 104 | pass 105 | 106 | def getValue(self): 107 | if self.intFlag: 108 | return self.slider.value() 109 | else: 110 | return round(((self.slider.value() / self.divs) * (self.maxv - self.minv) + self.minv) * 100) / 100 111 | 112 | def setValue(self, ivalue): 113 | if self.intFlag: 114 | self.slider.setValue(ivalue) 115 | else: 116 | self.slider.setValue(int(round((ivalue - self.minv) / (self.maxv - self.minv) * self.divs))) 117 | 118 | def do_changeEvent(self): 119 | self.slider.setToolTip(str(self.slName) + ": " + str(self.getValue())) 120 | self.label_name.setText(str(self.slName) + ": " + str(self.getValue())) 121 | if self.sl_changeEvent is not None: 122 | self.sl_changeEvent() 123 | 124 | 125 | def genLabel(window, content="No Content", fonttype=2): 126 | label = QtWidgets.QLabel(content, window) 127 | label.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom) 128 | label.setProperty("fontset", fonttype) 129 | return label 130 | 131 | 132 | class HintLabel(QtWidgets.QWidget): 133 | def __init__(self, parent, content="No Content", fonttype=2, pixmap=None, hint=None): 134 | super(HintLabel, self).__init__(parent) 135 | self.label = QtWidgets.QLabel(content, parent) 136 | self.label.setProperty("fontset", fonttype) 137 | self.layout = QtWidgets.QHBoxLayout(self) 138 | self.layout.setAlignment(QtCore.Qt.AlignCenter | QtCore.Qt.AlignBottom) 139 | if pixmap: 140 | self.pixmap = QtWidgets.QLabel("", parent) 141 | self.pixmap.setPixmap(pil2qt(pixmap)) 142 | self.pixmap.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) 143 | self.label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight) 144 | self.layout.addWidget(self.label) 145 | self.layout.addWidget(self.pixmap) 146 | if hint: 147 | self.setToolTip(hint) 148 | else: 149 | self.layout.addWidget(self.label) 150 | 151 | 152 | def genButton(window, text="", press_event=None, release_event=None, shortcut=None, style=1, tooltip=None): 153 | btn = QtWidgets.QPushButton(window) 154 | btn.setText(text) 155 | btn.setFocusPolicy(QtCore.Qt.NoFocus) 156 | if press_event is not None: 157 | btn.pressed.connect(press_event) 158 | if release_event is not None: 159 | btn.released.connect(release_event) 160 | if shortcut is not None: 161 | btn.setShortcut(QtGui.QKeySequence(shortcut)) 162 | 163 | if style == 2: 164 | btn.setStyleSheet(pushButtonStyle2) 165 | elif style == 3: 166 | btn.setStyleSheet(pushButtonStyle3) 167 | elif style == 4: 168 | btn.setStyleSheet(pushButtonStyle4) 169 | elif style == 5: 170 | btn.setStyleSheet(pushButtonStyle5) 171 | elif style == 6: 172 | btn.setStyleSheet(pushButtonStyle6) 173 | else: 174 | btn.setStyleSheet(pushButtonStyle1) 175 | 176 | btn.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 177 | if tooltip is None and shortcut is not None: 178 | if shortcut == "Return": 179 | shortcut = "Enter" 180 | elif shortcut == "Escape": 181 | shortcut = "Esc" 182 | btn.setToolTip(text + ": (" + shortcut + ")") 183 | return btn 184 | 185 | 186 | class _ComboBox(QtWidgets.QComboBox): 187 | def __init__(self, parent=None): 188 | super(_ComboBox, self).__init__(parent) 189 | self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 190 | 191 | def paintEvent(self, event): 192 | super().paintEvent(event) 193 | painter = QtGui.QPainter(self) 194 | painter.drawPixmap(QtCore.QPoint(self.width() - 26, 7), pil2qt(img_pack["down_arrow"])) 195 | 196 | 197 | class ComboBox(QtWidgets.QWidget): 198 | def __init__(self, parent, items, combo_event=None, title=""): 199 | super(ComboBox, self).__init__() 200 | self.parent = parent 201 | self.items = items 202 | self.combo_event = combo_event 203 | VBlayout = QtWidgets.QVBoxLayout(self) 204 | self.combo = _ComboBox(self) 205 | 206 | if title: 207 | VBlayout.addWidget(genLabel(self, title)) 208 | VBlayout.addWidget(self.combo) 209 | VBlayout.setSpacing(0) 210 | VBlayout.setContentsMargins(0, 0, 0, 0) 211 | self.setItems(self.items) 212 | self.combo.currentIndexChanged.connect(self.combo_select) 213 | self.combo.setView(QtWidgets.QListView()) 214 | self.setStyleSheet(scrollBarStyle2) 215 | 216 | def combo_select(self): 217 | if self.combo_event is not None: 218 | self.combo_event() 219 | 220 | def setValue(self, value_in): 221 | index = 0 222 | for name, value in self.items: 223 | if str(value_in) == str(name) or str(value_in) == str(value): 224 | self.combo.setCurrentIndex(index) 225 | break 226 | index = index + 1 227 | 228 | def getValue(self): 229 | return self.items[self.combo.currentIndex()][1] 230 | 231 | def getText(self): 232 | return self.items[self.combo.currentIndex()][0] 233 | 234 | def setItems(self, items): 235 | cvt_items = [] 236 | if isinstance(items, dict): 237 | for kname, value in items.items(): 238 | cvt_items.append((kname, value)) 239 | self.combo.addItem(kname) 240 | self.items = cvt_items 241 | return 242 | for item in items: 243 | if not isinstance(item, list) and not isinstance(item, tuple): 244 | cvt_items.append([item, item]) 245 | self.combo.addItem(str(item)) 246 | else: 247 | self.combo.addItem(item[0]) 248 | cvt_items.append(item) 249 | self.items = cvt_items 250 | 251 | 252 | class QSwitch(QtWidgets.QPushButton): 253 | def __init__(self, parent=None): 254 | super().__init__(parent) 255 | self.lang = parent.lang 256 | self.setCheckable(True) 257 | self.setMinimumWidth(66) 258 | self.setMinimumHeight(40) 259 | self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 260 | 261 | def paintEvent(self, event): 262 | label = self.lang["ON"] if self.isChecked() else self.lang["OFF"] 263 | bg_color = QtGui.QColor(67, 123, 181) if self.isChecked() else QtGui.QColor(137, 139, 141) 264 | 265 | radius = 15 266 | width = 40 267 | pen_width = 2 268 | center = self.rect().center() 269 | 270 | painter = QtGui.QPainter(self) 271 | painter.setRenderHint(QtGui.QPainter.Antialiasing) 272 | painter.translate(center) 273 | painter.setBrush(QtGui.QColor(20, 20, 20)) 274 | if not self.isChecked(): 275 | pen = QtGui.QPen(QtGui.QColor(137, 139, 141)) 276 | else: 277 | pen = QtGui.QPen(QtGui.QColor(67, 123, 181)) 278 | pen.setWidth(pen_width) 279 | painter.setPen(pen) 280 | painter.drawRoundedRect(QtCore.QRect(-width, -radius, 2 * width, 2 * radius), radius, radius) 281 | painter.setBrush(QtGui.QBrush(bg_color)) 282 | sw_rect = QtCore.QRect(-radius, -radius, width + radius, 2 * radius) 283 | 284 | if not self.isChecked(): 285 | sw_rect.moveLeft(-width) 286 | painter.drawRoundedRect(sw_rect, radius, radius) 287 | 288 | if not self.isChecked(): 289 | pen = QtGui.QPen(QtGui.QColor(20, 20, 20)) 290 | else: 291 | pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) 292 | pen.setWidth(pen_width) 293 | painter.setPen(pen) 294 | 295 | painter.drawText(sw_rect, QtCore.Qt.AlignCenter, label) 296 | 297 | 298 | class Spacing(QtWidgets.QWidget): 299 | def __init__(self, parent, size): 300 | super(Spacing, self).__init__() 301 | self.parent = parent 302 | space = QtWidgets.QWidget() 303 | space.setFixedHeight(size) 304 | VBlayout = QtWidgets.QVBoxLayout(self) 305 | VBlayout.addWidget(space) 306 | 307 | 308 | class LineEdit(QtWidgets.QLineEdit): 309 | def __init__(self, parent, place_holder=None, fo_event=None, num_range=None, isInt=False, scroll=None): 310 | super(LineEdit, self).__init__() 311 | self.parent = parent 312 | self.setStyleSheet(lineEditStyle) 313 | self.isInt = isInt 314 | self.fo_event = fo_event 315 | self.scroll_delta = scroll 316 | if place_holder is not None: 317 | self.setPlaceholderText(place_holder) 318 | 319 | self.num_range = num_range 320 | if self.num_range is not None: 321 | if self.isInt: 322 | validator = QtGui.QIntValidator() 323 | else: 324 | validator = QtGui.QDoubleValidator() 325 | self.setValidator(validator) 326 | 327 | self.returnPressed.connect(self.enterPressEvent) 328 | 329 | def focusOutEvent(self, QFocusEvent=None): 330 | if QFocusEvent: 331 | super().focusOutEvent(QFocusEvent) 332 | self.setText(str(self.numCheck())) 333 | if self.fo_event: 334 | self.fo_event() 335 | 336 | def enterPressEvent(self, *args): 337 | self.clearFocus() 338 | 339 | def numCheck(self): 340 | if self.num_range is not None: 341 | txt = self.text() 342 | if not txt: 343 | txt = 0 344 | else: 345 | try: 346 | txt = float(txt) 347 | except: 348 | txt = 0 349 | if txt < min(self.num_range): 350 | txt = min(self.num_range) 351 | elif txt > max(self.num_range): 352 | txt = max(self.num_range) 353 | if self.isInt: 354 | return int(round(txt)) 355 | else: 356 | return round(txt * 100) / 100 357 | else: 358 | return self.text() 359 | 360 | def getText(self): 361 | return super().text() 362 | 363 | def wheelEvent(self, event): 364 | if self.scroll_delta is not None: 365 | delta = self.scroll_delta if event.angleDelta().y() > 0 else -self.scroll_delta 366 | val = self.numCheck() + delta 367 | self.setText(str(val)) 368 | self.focusOutEvent() 369 | event.accept() 370 | 371 | 372 | class PixmapWithDrop(QtWidgets.QGraphicsPixmapItem): 373 | def __init__(self, parent, fileEvent=None): 374 | super(PixmapWithDrop, self).__init__() 375 | self.parent = parent 376 | self.lang = parent.lang 377 | self.setAcceptDrops(True) 378 | self.fileEvent = fileEvent 379 | self.setShapeMode(1) # For Drop Event 380 | 381 | def dragEnterEvent(self, event): 382 | if event.mimeData().hasUrls: 383 | event.accept() 384 | else: 385 | event.ignore() 386 | 387 | def dropEvent(self, event): 388 | for url in event.mimeData().urls(): 389 | if self.fileEvent is not None: 390 | self.fileEvent(url.toLocalFile()) 391 | break 392 | 393 | 394 | def showInfo(parent, title="", msg="", question=False): 395 | if question: 396 | box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Question, title, msg) 397 | qyes = box.addButton(parent.lang["Yes"], QtWidgets.QMessageBox.YesRole) 398 | qno = box.addButton(parent.lang["No"], QtWidgets.QMessageBox.NoRole) 399 | setWindowIcons(box) 400 | box.exec_() 401 | if box.clickedButton() == qyes: 402 | return True 403 | else: 404 | return False 405 | else: 406 | box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Information, title, msg) 407 | qyes = box.addButton(parent.lang["OK"], QtWidgets.QMessageBox.YesRole) 408 | setWindowIcons(box) 409 | box.exec_() 410 | if box.clickedButton() == qyes: 411 | return True 412 | else: 413 | return False 414 | 415 | 416 | def getFormats(strv): 417 | arrv = strv.replace(" ", "").replace(";", "").replace("(", "").replace(")", "").replace("*", "") 418 | arrv = arrv.split(".")[1:] 419 | return arrv 420 | 421 | 422 | def isValidFormat(path, format_arr): 423 | if not os.path.exists(path): 424 | return False 425 | elif path[-1] in ["/", "\\"] or not "." in path.replace("\\", "/").split("/")[-1]: 426 | return False 427 | elif not path.replace("\\", "/").split("/")[-1].split(".")[-1] in format_arr: 428 | return False 429 | return True 430 | 431 | 432 | def ensureDir(file_path): 433 | directory = os.path.dirname(file_path) 434 | if not os.path.exists(directory): 435 | os.makedirs(directory) 436 | 437 | 438 | def convertFileFormat(path, new_format=None): 439 | if not path: 440 | return "" 441 | if new_format is None: 442 | return path 443 | if not "." in path: 444 | if new_format[0] == ".": 445 | return path + new_format 446 | else: 447 | return path + "." + new_format 448 | 449 | last_dot = 0 450 | for i in range(len(path)): 451 | if path[i] == ".": 452 | last_dot = i 453 | ftype = path[last_dot + 1:] 454 | if new_format[0] == ".": 455 | pass 456 | else: 457 | new_format = "." + new_format 458 | if "/" in ftype or "\\" in ftype: 459 | outpath = path + new_format 460 | else: 461 | outpath = path[:last_dot] + new_format 462 | return outpath 463 | 464 | 465 | def getFileName(path="", suffix=True): 466 | if not path: 467 | return "" 468 | fname = path.replace("\\", "/").split("/")[-1] 469 | if not suffix: 470 | fname = ".".join(fname.split(".")[:-1]) 471 | return fname 472 | 473 | 474 | def getFileSuffix(fname=""): 475 | if not fname: 476 | return "" 477 | if not "." in fname: 478 | return "" 479 | return "." + fname.split(".")[-1] 480 | 481 | 482 | def getFilePath(path=""): 483 | if not path: 484 | return "" 485 | fname = path.replace("\\", "/").split("/")[-1] 486 | if "." in fname: 487 | path = "/".join(path.replace("\\", "/").split("/")[:-1]) 488 | else: 489 | path = "/".join(path.replace("\\", "/").split("/")) 490 | 491 | if path[-1] != "/": 492 | path = path + "/" 493 | return path 494 | 495 | 496 | def makeLegalFileName(fname=""): 497 | if not fname: 498 | return "untitled" 499 | illegal_dict = ["/", "\\", ":", "*", "?", "\"", "<", ">", "|"] 500 | for char in illegal_dict: 501 | fname.replace(char, "_") 502 | return fname 503 | 504 | 505 | def protectPath(path=""): 506 | if not os.path.exists(path): 507 | return path 508 | else: 509 | fpath = getFilePath(path) 510 | fname = getFileName(path, False) 511 | suffix = getFileSuffix(getFileName(path, True)) 512 | name_counter = 1 513 | new_path = fpath + fname + "(" + str(name_counter) + ")" + suffix 514 | while os.path.exists(new_path): 515 | name_counter = name_counter + 1 516 | new_path = fpath + fname + "(" + str(name_counter) + ")" + suffix 517 | return new_path 518 | 519 | 520 | def joinPath(path="", filename=""): 521 | path = path.replace("\\", "/") 522 | if path[-1] != "/": 523 | path = path + "/" 524 | return str(path) + str(filename) 525 | 526 | 527 | def testFunc(*args): 528 | print("test!!!") 529 | 530 | 531 | if __name__ == '__main__': 532 | print(getFormats(" (*.mp3;*.wav;*.ogg;*.aac;*.flac;*.ape;*.m4a;*.m4r;*.wma;*.mp2;*.mmf);;")) 533 | -------------------------------------------------------------------------------- /SourceCode/QtWindows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from QtWheels import * 5 | import os 6 | from FanWheels_PIL import hsv_to_rgb, rgb_to_hsv 7 | 8 | 9 | class MainMenu(QtWidgets.QWidget): 10 | def __init__(self, parent): 11 | super(MainMenu, self).__init__() 12 | self.parent = parent 13 | self.lang = parent.lang 14 | VBlayout = QtWidgets.QVBoxLayout(self) 15 | 16 | VBlayout.addWidget(genLabel(self, self.lang["Audio"])) 17 | self.le_audio_path = LineEdit(self, self.lang[""], self.checkFilePath) 18 | VBlayout.addWidget(self.le_audio_path) 19 | 20 | self.btn_audio = genButton(self, self.lang["Select Audio"], None, self.btn_audio_release, style=2) 21 | self.btn_audio.setIcon(pil2icon(img_pack["select_audio"])) 22 | VBlayout.addWidget(self.btn_audio) 23 | 24 | self.btn_audio_set = genButton(self, self.lang["Audio Settings"], None, self.btn_audio_set_release) 25 | self.btn_audio_set.setIcon(pil2icon(img_pack["audio_settings"])) 26 | VBlayout.addWidget(self.btn_audio_set) 27 | 28 | VBlayout.addWidget(genLabel(self, self.lang["Video"])) 29 | 30 | self.btn_images = genButton(self, self.lang["Select Images"], None, self.btn_images_release, style=2) 31 | self.btn_images.setIcon(pil2icon(img_pack["select_image"])) 32 | VBlayout.addWidget(self.btn_images) 33 | 34 | self.btn_image_set = genButton(self, self.lang["Image Settings"], None, self.btn_image_set_release) 35 | self.btn_image_set.setIcon(pil2icon(img_pack["image_settings"])) 36 | VBlayout.addWidget(self.btn_image_set) 37 | 38 | self.btn_text_set = genButton(self, self.lang["Text Settings"], None, self.btn_text_set_release) 39 | self.btn_text_set.setIcon(pil2icon(img_pack["text"])) 40 | VBlayout.addWidget(self.btn_text_set) 41 | 42 | self.btn_video_set = genButton(self, self.lang["Video Settings"], None, self.btn_video_set_release) 43 | self.btn_video_set.setIcon(pil2icon(img_pack["video_settings"])) 44 | VBlayout.addWidget(self.btn_video_set) 45 | 46 | VBlayout.addWidget(genLabel(self, self.lang["Spectrum"])) 47 | 48 | self.btn_spec_style = genButton(self, self.lang["Spectrum Style"], None, self.btn_spec_style_release) 49 | self.btn_spec_style.setIcon(pil2icon(img_pack["spectrum"])) 50 | VBlayout.addWidget(self.btn_spec_style) 51 | 52 | self.btn_spec_color = genButton(self, self.lang["Spectrum Color"], None, self.btn_spec_color_release) 53 | self.btn_spec_color.setIcon(pil2icon(img_pack["color"])) 54 | VBlayout.addWidget(self.btn_spec_color) 55 | 56 | VBlayout.addWidget(genLabel(self, self.lang["Render"])) 57 | 58 | self.btn_blend = genButton(self, self.lang["Ready to Render"], None, self.btn_blend_release, "F5", style=4) 59 | self.btn_blend.setIcon(pil2icon(img_pack["run"])) 60 | VBlayout.addWidget(self.btn_blend) 61 | 62 | self.btn_about = genButton(self, self.lang["About FVisualizer"], None, self.btn_about_release) 63 | self.btn_about.setIcon(pil2icon(img_pack["info"])) 64 | VBlayout.addWidget(self.btn_about) 65 | 66 | VBlayout.setSpacing(4) 67 | VBlayout.setContentsMargins(0, 0, 0, 0) 68 | 69 | self.audio_formats = self.parent.audio_formats 70 | self.audio_formats_arr = self.parent.audio_formats_arr 71 | self.video_formats = self.parent.video_formats 72 | self.video_formats_arr = self.parent.video_formats_arr 73 | 74 | def show(self): 75 | self.parent.hideAllMenu() 76 | self.showAudioFilePath() 77 | super().show() 78 | 79 | def checkFilePath(self): 80 | if self.le_audio_path.text() == "": 81 | self.parent.vdic["sound_path"] = None 82 | return 83 | isAudio = isValidFormat(self.le_audio_path.text(), self.audio_formats_arr) 84 | isVideo = isValidFormat(self.le_audio_path.text(), self.video_formats_arr) 85 | if (not (isAudio or isVideo)) or (not os.path.isfile(self.le_audio_path.text())): 86 | showInfo(self, self.lang["Cannot Render"], self.lang["Please select the correct audio file!"], False) 87 | self.le_audio_path.setText(self.parent.vdic["sound_path"]) 88 | else: 89 | self.parent.vdic["sound_path"] = self.le_audio_path.text() 90 | self.parent.vdic["filename"] = getFileName(self.le_audio_path.text(), False) + self.lang[ 91 | "_Visualize"] + ".mp4" 92 | if not self.parent.vdic["output_path"]: 93 | self.parent.vdic["output_path"] = getFilePath(self.le_audio_path.text()) 94 | 95 | def btn_images_release(self): 96 | self.parent.imageSelector.show() 97 | 98 | def btn_image_set_release(self): 99 | self.parent.imageSetting.show() 100 | 101 | def btn_audio_release(self): 102 | selector = self.lang["Audio Files"] + self.audio_formats 103 | selector = selector + self.lang["Video Files"] + self.video_formats 104 | selector = selector + self.lang["All Files"] + " (*.*)" 105 | if self.parent.vdic["sound_path"]: 106 | dir_in = getFilePath(self.parent.vdic["sound_path"]) 107 | else: 108 | dir_in = "" 109 | 110 | file_, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.lang["Select Audio File"], dir_in, selector) 111 | if not file_: 112 | print("No File!") 113 | else: 114 | self.parent.vdic["sound_path"] = file_ 115 | self.parent.vdic["filename"] = getFileName(file_, False) + self.lang["_Visualize"] + ".mp4" 116 | self.showAudioFilePath() 117 | 118 | def btn_audio_set_release(self): 119 | self.parent.audioSetting.show() 120 | 121 | def btn_text_set_release(self): 122 | self.parent.textWindow.show() 123 | 124 | def btn_spec_color_release(self): 125 | self.parent.spectrumColor.show() 126 | 127 | def btn_spec_style_release(self): 128 | self.parent.spectrumStyle.show() 129 | 130 | def btn_video_set_release(self): 131 | self.parent.videoSetting.show() 132 | 133 | def btn_blend_release(self): 134 | self.checkFilePath() 135 | if self.parent.vdic["sound_path"] is not None: 136 | if os.path.isfile(self.parent.vdic["sound_path"]): 137 | self.parent.blendWindow.show() 138 | return 139 | else: 140 | showInfo(self, self.lang["Cannot Render"], self.lang["Please select the correct audio file!"]) 141 | 142 | def btn_about_release(self): 143 | self.parent.aboutWindow.show() 144 | 145 | def showAudioFilePath(self): 146 | if self.parent.vdic["sound_path"] is not None: 147 | self.le_audio_path.setText(self.parent.vdic["sound_path"].replace("\\", "/")) 148 | else: 149 | self.le_audio_path.setText("") 150 | 151 | 152 | class AudioSettingWindow(QtWidgets.QWidget): 153 | def __init__(self, parent): 154 | super(AudioSettingWindow, self).__init__() 155 | self.parent = parent 156 | self.lang = parent.lang 157 | VBlayout = QtWidgets.QVBoxLayout(self) 158 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 159 | 160 | items_bra = [("320 kbps", 320), ("256 kbps", 256), ("192 kbps", 192), 161 | ("128 kbps", 128), ("96 kbps", 96), ("64 kbps", 64), ("48 kbps", 48)] 162 | self.combo_bra = ComboBox(self, items_bra, self.combo_bra_select, self.lang["Audio Bit Rate"]) 163 | VBlayout.addWidget(self.combo_bra) 164 | 165 | VBlayout.addWidget(HintLabel(self, self.lang["Volume Normalize"], 2, img_pack["what"], 166 | self.lang["Adjust audio volume according to EBU R128 loudness standard."])) 167 | 168 | self.ck_normal = QSwitch(self) 169 | self.ck_normal.released.connect(self.ck_normal_release) 170 | VBlayout.addWidget(self.ck_normal) 171 | 172 | items_prea = { 173 | self.lang["-Please Select-"]: -1, 174 | self.lang["Pop Music"]: [32, 12000, False], 175 | self.lang["Classical Music"]: [40, 7200, False], 176 | self.lang["Calm Music"]: [56, 2400, False], 177 | self.lang["Orchestral Music"]: [42, 12000, False], 178 | self.lang["Piano: Narrow Band"]: [55, 2100, False], 179 | self.lang["Piano: Wide Band"]: [42, 4400, False], 180 | self.lang["Violin"]: [150, 4000, False], 181 | self.lang["Guitar"]: [80, 2200, False], 182 | self.lang["Natural Sound"]: [32, 15000, False], 183 | self.lang["Voice"]: [100, 2200, True], 184 | } 185 | self.combo_prea = ComboBox(self, items_prea, self.combo_prea_select, self.lang["Frequency Analyzer Preset"]) 186 | VBlayout.addWidget(self.combo_prea) 187 | 188 | VBlayout.addWidget(HintLabel(self, self.lang["Analyzer Frequency Range (Hz)"], 2, img_pack["what"], 189 | self.lang[ 190 | "Visualized spectrum will be generated according to this frequency range."])) 191 | 192 | self.le_lower = LineEdit(self, self.lang[""], self.crossCheckLower, [16, 22050], True, scroll=50) 193 | self.le_upper = LineEdit(self, self.lang[""], self.crossCheckUpper, [16, 22050], True, scroll=50) 194 | 195 | HBlayout = QtWidgets.QHBoxLayout() 196 | 197 | HBlayout.addWidget(self.le_lower) 198 | label_to = genLabel(self, self.lang["to"]) 199 | label_to.setAlignment(QtCore.Qt.AlignTop) 200 | HBlayout.addWidget(label_to) 201 | HBlayout.addWidget(self.le_upper) 202 | 203 | VBlayout.addLayout(HBlayout) 204 | 205 | VBlayout.setAlignment(QtCore.Qt.AlignBottom) 206 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 207 | btn_back.setIcon(pil2icon(img_pack["back"])) 208 | VBlayout.addWidget(btn_back) 209 | 210 | VBlayout.setSpacing(15) 211 | VBlayout.setContentsMargins(0, 0, 0, 0) 212 | 213 | def combo_prea_select(self): 214 | preseta = self.combo_prea.getValue() 215 | if preseta != -1: 216 | self.le_lower.setText(str(preseta[0])) 217 | self.le_upper.setText(str(preseta[1])) 218 | self.ck_normal.setChecked(preseta[2]) 219 | self.parent.vdic["br_kbps"] = self.combo_bra.getValue() 220 | self.parent.vdic["lower"] = self.le_lower.numCheck() 221 | self.parent.vdic["upper"] = self.le_upper.numCheck() 222 | self.parent.vdic["normal"] = self.ck_normal.isChecked() 223 | 224 | def combo_bra_select(self): 225 | self.parent.vdic["br_kbps"] = self.combo_bra.getValue() 226 | 227 | def show(self): 228 | self.parent.hideAllMenu() 229 | self.combo_bra.setValue(self.parent.vdic["br_kbps"]) 230 | self.combo_prea.setValue(-1) 231 | self.combo_prea.setValue([self.parent.vdic["lower"], self.parent.vdic["upper"], self.parent.vdic["normal"]]) 232 | self.ck_normal.setChecked(self.parent.vdic["normal"]) 233 | self.le_lower.setText(str(self.parent.vdic["lower"])) 234 | self.le_upper.setText(str(self.parent.vdic["upper"])) 235 | super().show() 236 | 237 | def crossCheckLower(self): 238 | if self.le_lower.numCheck() >= self.le_upper.numCheck(): 239 | self.le_upper.setText(str(self.le_lower.numCheck() + 1)) 240 | self.parent.vdic["lower"] = self.le_lower.numCheck() 241 | self.parent.vdic["upper"] = self.le_upper.numCheck() 242 | 243 | def crossCheckUpper(self): 244 | if self.le_lower.numCheck() >= self.le_upper.numCheck(): 245 | self.le_lower.setText(str(self.le_upper.numCheck() - 1)) 246 | self.parent.vdic["lower"] = self.le_lower.numCheck() 247 | self.parent.vdic["upper"] = self.le_upper.numCheck() 248 | 249 | def ck_normal_release(self): 250 | self.parent.vdic["normal"] = self.ck_normal.isChecked() 251 | 252 | def btn_back_release(self): 253 | self.parent.mainMenu.show() 254 | 255 | 256 | class VideoSettingWindow(QtWidgets.QWidget): 257 | def __init__(self, parent): 258 | super(VideoSettingWindow, self).__init__() 259 | self.parent = parent 260 | self.lang = parent.lang 261 | VBlayout = QtWidgets.QVBoxLayout(self) 262 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 263 | 264 | items_prev = { 265 | self.lang["-Please Select-"]: -1, 266 | self.lang["Square"] + " (1080x1080)": [1080, 1080, 30, self.getDefaultBR(1080, 1080, 30, 5)], 267 | self.lang["Square"] + " (1024x1024)": [1024, 1024, 30, self.getDefaultBR(1024, 1024, 30, 5)], 268 | self.lang["Square"] + " (720x720)": [720, 720, 30, self.getDefaultBR(720, 720, 30, 4)], 269 | self.lang["Square"] + " (512x512)": [512, 512, 30, self.getDefaultBR(512, 512, 30, 4)], 270 | self.lang["Square"] + " (480x480)": [480, 480, 30, self.getDefaultBR(480, 480, 30, 4)], 271 | self.lang["Landscape"] + " (1920x1080)": [1920, 1080, 30, self.getDefaultBR(1920, 1080, 30, 5)], 272 | self.lang["Landscape"] + " (1280x720)": [1280, 720, 30, self.getDefaultBR(1280, 720, 30, 4)], 273 | self.lang["Landscape"] + " (854x480)": [854, 480, 30, self.getDefaultBR(854, 480, 30, 4)], 274 | self.lang["Portrait"] + " (1080x1920)": [1080, 1920, 30, self.getDefaultBR(1920, 1080, 30, 5)], 275 | self.lang["Portrait"] + " (720x1280)": [720, 1280, 30, self.getDefaultBR(1280, 720, 30, 4)], 276 | self.lang["Portrait"] + " (480x854)": [480, 854, 30, self.getDefaultBR(854, 480, 30, 4)], 277 | "2k (2560x1440)": [2560, 1440, 30, self.getDefaultBR(2560, 1440, 30, 5)], 278 | } 279 | self.combo_prev = ComboBox(self, items_prev, self.combo_prev_select, self.lang["Video Preset"]) 280 | VBlayout.addWidget(self.combo_prev) 281 | 282 | self.items_quality = { 283 | self.lang["Very Low"] + " (1x, 1x)": 1, 284 | self.lang["Low"] + " (2x, 2x)": 2, 285 | self.lang["Medium - Default"] + " (2x, 4x)": 3, 286 | self.lang["High"] + " (4x, 8x)": 4, 287 | self.lang["Very High"] + " (8x, 8x)": 5, 288 | } 289 | self.combo_quality = ComboBox(self, self.items_quality, self.combo_quality_select, self.lang["Render Quality"]) 290 | VBlayout.addWidget(self.combo_quality) 291 | 292 | VBlayout.addWidget(genLabel(self, self.lang["Frame Size"])) 293 | self.le_width = LineEdit(self, self.lang[""], self.setVideoSize, [16, 4096], True, scroll=100) 294 | self.le_height = LineEdit(self, self.lang[""], self.setVideoSize, [16, 4096], True, scroll=100) 295 | 296 | HBlayout = QtWidgets.QHBoxLayout() 297 | HBlayout.addWidget(self.le_width) 298 | label_to = genLabel(self, "x") 299 | label_to.setAlignment(QtCore.Qt.AlignTop) 300 | HBlayout.addWidget(label_to) 301 | HBlayout.addWidget(self.le_height) 302 | VBlayout.addLayout(HBlayout) 303 | 304 | VBlayout.addWidget(genLabel(self, self.lang["Frame Rate (FPS)"])) 305 | self.le_fps = LineEdit(self, self.lang[""], self.setFps, [1.0, 120], False, scroll=1) 306 | VBlayout.addWidget(self.le_fps) 307 | 308 | VBlayout.addWidget(genLabel(self, self.lang["Video Bit Rate (Mbps)"])) 309 | self.le_brv = LineEdit(self, self.lang[""], self.setBRV, [0.01, 200], False, scroll=0.1) 310 | VBlayout.addWidget(self.le_brv) 311 | 312 | VBlayout.addWidget(genLabel(self, self.lang["Auto Video Bit Rate"])) 313 | self.ck_auto = QSwitch(self) 314 | self.ck_auto.released.connect(self.autoBitRate) 315 | VBlayout.addWidget(self.ck_auto) 316 | self.ck_auto.setChecked(True) 317 | 318 | VBlayout.setAlignment(QtCore.Qt.AlignBottom) 319 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 320 | btn_back.setIcon(pil2icon(img_pack["back"])) 321 | VBlayout.addWidget(btn_back) 322 | 323 | VBlayout.setContentsMargins(0, 0, 0, 0) 324 | 325 | def combo_quality_select(self): 326 | quality = self.combo_quality.getValue() 327 | self.parent.vdic["quality"] = quality 328 | if self.isVisible(): 329 | self.parent.refreshAll() 330 | 331 | def combo_prev_select(self): 332 | presetv = self.combo_prev.getValue() 333 | if presetv != -1: 334 | self.le_width.setText(str(presetv[0])) 335 | self.le_height.setText(str(presetv[1])) 336 | self.le_fps.setText(str(presetv[2])) 337 | self.le_brv.setText(str(presetv[3])) 338 | 339 | self.parent.vdic["width"] = self.le_width.numCheck() 340 | self.parent.vdic["height"] = self.le_height.numCheck() 341 | self.parent.vdic["fps"] = self.le_fps.numCheck() 342 | self.parent.vdic["br_Mbps"] = self.le_brv.numCheck() 343 | 344 | self.le_brv.focusOutEvent() 345 | if self.isVisible(): 346 | self.parent.refreshAll() 347 | 348 | def setVideoSize(self): 349 | self.parent.vdic["width"] = self.le_width.numCheck() 350 | self.parent.vdic["height"] = self.le_height.numCheck() 351 | if self.isVisible(): 352 | self.parent.refreshAll() 353 | self.autoBitRate() 354 | 355 | def setFps(self): 356 | self.parent.vdic["fps"] = self.le_fps.numCheck() 357 | self.autoBitRate() 358 | 359 | def setBRV(self): 360 | self.parent.vdic["br_Mbps"] = self.le_brv.numCheck() 361 | 362 | def autoBitRate(self): 363 | if self.ck_auto.isChecked(): 364 | brv = self.getDefaultBR(self.le_width.numCheck(), self.le_height.numCheck(), self.le_fps.numCheck(), 4) 365 | self.le_brv.setText(str(brv)) 366 | self.le_brv.setText(str(self.le_brv.numCheck())) 367 | self.le_brv.clearFocus() 368 | 369 | def getDefaultBR(self, width, height, fps, quality=3): 370 | if quality == 5: 371 | return 20 * (width * height * fps) / (1920 * 1080 * 30) 372 | elif quality == 4: 373 | return 12 * (width * height * fps) / (1920 * 1080 * 30) 374 | elif quality == 3: 375 | return 7 * (width * height * fps) / (1920 * 1080 * 30) 376 | elif quality == 2: 377 | return 2 * (width * height * fps) / (1920 * 1080 * 30) 378 | elif quality == 1: 379 | return (width * height * fps) / (1920 * 1080 * 30) 380 | elif quality == 0: 381 | return 0.5 * (width * height * fps) / (1920 * 1080 * 30) 382 | else: 383 | return 12 * (width * height * fps) / (1920 * 1080 * 30) 384 | 385 | def show(self): 386 | self.parent.hideAllMenu() 387 | self.combo_prev.setValue(-1) 388 | self.combo_quality.setValue(self.parent.vdic["quality"]) 389 | self.le_width.setText(str(self.parent.vdic["width"])) 390 | self.le_height.setText(str(self.parent.vdic["height"])) 391 | self.le_brv.setText(str(self.parent.vdic["br_Mbps"])) 392 | self.le_fps.setText(str(self.parent.vdic["fps"])) 393 | super().show() 394 | 395 | def btn_back_release(self): 396 | self.parent.mainMenu.show() 397 | 398 | 399 | class TextWindow(QtWidgets.QWidget): 400 | def __init__(self, parent): 401 | super(TextWindow, self).__init__() 402 | self.parent = parent 403 | self.lang = parent.lang 404 | self.oldColor = self.parent.vdic["text_color"] 405 | VBlayout = QtWidgets.QVBoxLayout(self) 406 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 407 | 408 | VBlayout.addWidget(genLabel(self, self.lang["Edit Text"])) 409 | 410 | self.le_text = LineEdit(self, self.lang[""], self.setMainText) 411 | VBlayout.addWidget(self.le_text) 412 | 413 | self.btn_filename = genButton(self, self.lang["Use File Name"], None, self.btn_filename_release) 414 | self.btn_filename.setIcon(pil2icon(img_pack["file"])) 415 | VBlayout.addWidget(self.btn_filename) 416 | 417 | self.btn_clear = genButton(self, self.lang["Clear Text"], None, self.btn_clear_release, style=5) 418 | self.btn_clear.setIcon(pil2icon(img_pack["delete"])) 419 | VBlayout.addWidget(self.btn_clear) 420 | 421 | VBlayout.addWidget(genLabel(self, self.lang["Text Settings"])) 422 | 423 | self.sl_font = FSlider(self, self.lang["Font Size"], 0.3, 5.0, 0.1, 1.0, self.sl_font_release, None) 424 | VBlayout.addWidget(self.sl_font) 425 | 426 | self.btn_color = genButton(self, self.lang["Select Color"], None, self.btn_color_release) 427 | self.btn_color.setIcon(pil2icon(img_pack["color"])) 428 | VBlayout.addWidget(self.btn_color) 429 | 430 | self.le_font_path = LineEdit(self, self.lang[""], self.checkFont) 431 | VBlayout.addWidget(self.le_font_path) 432 | 433 | self.btn_font = genButton(self, self.lang["Select Font"], None, self.btn_font_release, style=2) 434 | self.btn_font.setIcon(pil2icon(img_pack["font"])) 435 | VBlayout.addWidget(self.btn_font) 436 | 437 | VBlayout.addWidget(genLabel(self, self.lang["Glow Text"])) 438 | self.ck_glow_text = QSwitch(self) 439 | self.ck_glow_text.released.connect(self.ck_glow_text_release) 440 | VBlayout.addWidget(self.ck_glow_text) 441 | 442 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 443 | btn_back.setIcon(pil2icon(img_pack["back"])) 444 | VBlayout.addWidget(btn_back) 445 | 446 | VBlayout.setContentsMargins(0, 0, 0, 0) 447 | 448 | def setMainText(self): 449 | self.parent.vdic["text"] = self.le_text.text() 450 | self.parent.refreshLocal() 451 | 452 | def btn_filename_release(self): 453 | if self.parent.vdic["filename"]: 454 | self.le_text.setText(getFileName(self.parent.vdic["sound_path"], False)) 455 | self.setMainText() 456 | 457 | def btn_clear_release(self): 458 | self.le_text.setText("") 459 | self.setMainText() 460 | 461 | def sl_font_release(self): 462 | self.parent.vdic["relsize"] = self.sl_font.getValue() 463 | self.parent.refreshLocal() 464 | 465 | def checkFont(self): 466 | if self.le_font_path.text() is not None: 467 | self.parent.vdic["font"] = self.le_font_path.text() 468 | self.parent.refreshLocal() 469 | 470 | def btn_font_release(self): 471 | selector = self.lang["Font Files"] + " (*.ttf;*.otf);;" 472 | selector = selector + self.lang["All Files"] + " (*.*)" 473 | 474 | font_dir = "" 475 | if os.path.exists(self.parent.vdic["font"]): 476 | font_dir = self.parent.vdic["font"] 477 | 478 | file_, filetype = QtWidgets.QFileDialog.getOpenFileName(self, self.lang["Select Font File"], font_dir, selector) 479 | if not file_: 480 | print("No File!") 481 | else: 482 | self.parent.vdic["font"] = file_ 483 | self.le_font_path.setText(file_) 484 | self.parent.refreshLocal() 485 | 486 | def btn_color_release(self): 487 | self.oldColor = self.parent.vdic["text_color"] 488 | color = QtWidgets.QColorDialog.getColor(QtGui.QColor(*self.oldColor), self, self.lang["Select Color"], 489 | options=QtWidgets.QColorDialog.ShowAlphaChannel) 490 | if color.isValid(): 491 | self.parent.vdic["text_color"] = color.getRgb() 492 | self.parent.refreshLocal() 493 | 494 | def ck_glow_text_release(self): 495 | self.parent.vdic["text_glow"] = self.ck_glow_text.isChecked() 496 | self.parent.refreshLocal() 497 | 498 | def show(self): 499 | self.parent.hideAllMenu() 500 | self.le_text.setText(self.parent.vdic["text"]) 501 | self.sl_font.setValue(self.parent.vdic["relsize"]) 502 | self.oldColor = self.parent.vdic["text_color"] 503 | self.le_font_path.setText(self.parent.vdic["font"]) 504 | self.ck_glow_text.setChecked(self.parent.vdic["text_glow"]) 505 | super().show() 506 | 507 | def btn_back_release(self): 508 | self.parent.mainMenu.show() 509 | 510 | 511 | class ImageSettingWindow(QtWidgets.QWidget): 512 | def __init__(self, parent): 513 | super(ImageSettingWindow, self).__init__() 514 | self.parent = parent 515 | self.lang = parent.lang 516 | self.oldColor = self.parent.vdic["text_color"] 517 | VBlayout = QtWidgets.QVBoxLayout(self) 518 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 519 | 520 | self.items_bg_mode = { 521 | self.lang["Blurred BG Image"] + " (MP4, H264)": [True, 0], 522 | self.lang["Normal BG Image"] + " (MP4, H264)": [False, 0], 523 | self.lang["Blurred BG Only"] + " (MP4, H264)": [True, 2], 524 | self.lang["Normal BG Only"] + " (MP4, H264)": [False, 2], 525 | self.lang["Transparent"] + " (MOV, PNG)": [False, -1], 526 | self.lang["Spectrum Only"] + " (MOV, PNG)": [False, -2], 527 | } 528 | 529 | self.combo_bg_mode = ComboBox(self, self.items_bg_mode, self.combo_bg_mode_select, self.lang["Background Mode"]) 530 | VBlayout.addWidget(self.combo_bg_mode) 531 | 532 | items_spin = { 533 | self.lang["No Spin"]: 0, 534 | self.lang["Clockwise"]: 1, 535 | self.lang["Counterclockwise"]: -1, 536 | } 537 | self.combo_spin = ComboBox(self, items_spin, self.combo_spin_select, self.lang["Spin Foreground"]) 538 | 539 | VBlayout.addWidget(self.combo_spin) 540 | self.lb_speed = genLabel(self, self.lang["Spin Speed (rpm)"]) 541 | VBlayout.addWidget(self.lb_speed) 542 | 543 | self.le_speed = LineEdit(self, self.lang[""], self.le_speed_check, [-200, 200], False, 1.0) 544 | VBlayout.addWidget(self.le_speed) 545 | 546 | self.lb_beat = HintLabel(self, self.lang["Oscillate Foreground"], 2, img_pack["what"], 547 | self.lang["Oscillate FG image accroding to the bass."] + "\n\n" + \ 548 | self.lang["Oscillate FG to Bass (%)"] + ": " + \ 549 | self.lang["Sensitivity of the bass (beat) detector."] + "\n" + \ 550 | self.lang["Bass Frequency Range (%)"] + ": " + \ 551 | self.lang["Relative frequency of bass to analyzer frequency range."]) 552 | VBlayout.addWidget(self.lb_beat) 553 | 554 | self.sl_beat_detect = FSlider(self, self.lang["Oscillate FG to Bass (%)"], 0, 100, 1, 1, 555 | self.sl_beat_detect_release, None) 556 | VBlayout.addWidget(self.sl_beat_detect) 557 | 558 | self.sl_low_range = FSlider(self, self.lang["Bass Frequency Range (%)"], 0, 100, 1, 1, 559 | self.sl_low_range_release, None) 560 | VBlayout.addWidget(self.sl_low_range) 561 | 562 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 563 | btn_back.setIcon(pil2icon(img_pack["back"])) 564 | VBlayout.addWidget(btn_back) 565 | VBlayout.setContentsMargins(0, 0, 0, 0) 566 | 567 | def combo_bg_mode_select(self): 568 | bg_setv = self.combo_bg_mode.getValue() 569 | self.parent.vdic["blur_bg"] = bg_setv[0] 570 | self.parent.vdic["bg_mode"] = bg_setv[1] 571 | self.visible_check() 572 | if self.isVisible(): 573 | self.parent.refreshAll() 574 | 575 | def combo_spin_select(self): 576 | if self.isVisible(): 577 | direction = self.combo_spin.getValue() 578 | speed = self.le_speed.numCheck() 579 | if direction == 0 and speed != 0: 580 | self.le_speed.setText(str(0)) 581 | speed = 0 582 | elif direction != 0 and speed == 0: 583 | self.le_speed.setText(str(1.0)) 584 | speed = 1.0 585 | self.parent.vdic["rotate"] = direction * speed 586 | if self.isVisible(): 587 | self.parent.refreshLocal() 588 | 589 | def le_speed_check(self): 590 | direction = self.combo_spin.getValue() 591 | speed = self.le_speed.numCheck() 592 | if speed < 0: 593 | speed = speed * -1 594 | self.le_speed.setText(str(speed)) 595 | self.combo_spin.setValue(-direction) 596 | elif speed > 0 and direction == 0: 597 | self.combo_spin.setValue(1) 598 | elif speed == 0 and direction != 0: 599 | self.combo_spin.setValue(0) 600 | self.combo_spin_select() 601 | 602 | def sl_low_range_release(self): 603 | self.parent.vdic["low_range"] = self.sl_low_range.getValue() 604 | 605 | def sl_beat_detect_release(self): 606 | self.parent.vdic["beat_detect"] = self.sl_beat_detect.getValue() 607 | if self.sl_beat_detect.getValue() > 0: 608 | self.sl_low_range.setStyle(0) 609 | else: 610 | self.sl_low_range.setStyle(1) 611 | self.parent.refreshLocal() 612 | 613 | def visible_check(self): 614 | bg_mode = self.parent.vdic["bg_mode"] 615 | elems = [self.combo_spin, self.lb_speed, self.le_speed, self.lb_beat, self.sl_beat_detect, self.sl_low_range] 616 | if bg_mode >= -1 and not bg_mode == 2: 617 | for elem in elems: 618 | elem.show() 619 | else: 620 | for elem in elems: 621 | elem.hide() 622 | 623 | def show(self): 624 | self.parent.hideAllMenu() 625 | bg_modev = [self.parent.vdic["blur_bg"], self.parent.vdic["bg_mode"]] 626 | self.combo_bg_mode.setValue(bg_modev) 627 | if self.parent.vdic["rotate"] == 0: 628 | direction = 0 629 | speed = 0 630 | else: 631 | direction = int(abs(self.parent.vdic["rotate"]) / self.parent.vdic["rotate"]) 632 | speed = abs(self.parent.vdic["rotate"]) 633 | self.combo_spin.setValue(direction) 634 | self.le_speed.setText(str(speed)) 635 | self.sl_low_range.setValue(self.parent.vdic["low_range"]) 636 | self.sl_beat_detect.setValue(self.parent.vdic["beat_detect"]) 637 | if self.sl_beat_detect.getValue() > 0: 638 | self.sl_low_range.setStyle(0) 639 | else: 640 | self.sl_low_range.setStyle(1) 641 | self.visible_check() 642 | super().show() 643 | 644 | def btn_back_release(self): 645 | self.parent.mainMenu.show() 646 | 647 | 648 | class SpectrumColorWindow(QtWidgets.QWidget): 649 | def __init__(self, parent): 650 | super(SpectrumColorWindow, self).__init__() 651 | self.parent = parent 652 | self.lang = parent.lang 653 | self.oldColor = None 654 | VBlayout = QtWidgets.QVBoxLayout(self) 655 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 656 | 657 | color_dic = { 658 | self.lang["Customize"]: None, 659 | self.lang["Gray"]: "gray", 660 | self.lang["White"]: "white", 661 | self.lang["Black"]: "black", 662 | self.lang["Red"]: "red", 663 | self.lang["Green"]: "green", 664 | self.lang["Blue"]: "blue", 665 | self.lang["Yellow"]: "yellow", 666 | self.lang["Magenta"]: "magenta", 667 | self.lang["Purple"]: "purple", 668 | self.lang["Cyan"]: "cyan", 669 | self.lang["Light Green"]: "lightgreen", 670 | self.lang["Green - Blue"]: "green-blue", 671 | self.lang["Magenta - Purple"]: "magenta-purple", 672 | self.lang["Red - Yellow"]: "red-yellow", 673 | self.lang["Yellow - Green"]: "yellow-green", 674 | self.lang["Blue - Purple"]: "blue-purple", 675 | self.lang["Rainbow 1x"]: "color1x", 676 | self.lang["Rainbow 2x"]: "color2x", 677 | self.lang["Rainbow 4x"]: "color4x", 678 | 679 | } 680 | self.combo_color = ComboBox(self, color_dic, self.combo_color_select, self.lang["Spectrum Color"]) 681 | VBlayout.addWidget(self.combo_color) 682 | 683 | self.btn_color = genButton(self, self.lang["Customize Color"], None, self.btn_color_release) 684 | self.btn_color.setIcon(pil2icon(img_pack["color"])) 685 | VBlayout.addWidget(self.btn_color) 686 | 687 | self.sl_sat = FSlider(self, self.lang["Spectrum Saturation"], 0.0, 1.0, 0.01, 0.8, self.sl_sat_release, None) 688 | VBlayout.addWidget(self.sl_sat) 689 | 690 | self.sl_brt = FSlider(self, self.lang["Spectrum Brightness"], 0.0, 1.0, 0.01, 0.8, self.sl_brt_release, None) 691 | VBlayout.addWidget(self.sl_brt) 692 | 693 | VBlayout.addWidget(genLabel(self, self.lang["Glow Effect (Slow)"])) 694 | self.ck_glow = QSwitch(self) 695 | self.ck_glow.released.connect(self.ck_glow_release) 696 | VBlayout.addWidget(self.ck_glow) 697 | 698 | btn_goto = genButton(self, self.lang["Spectrum Style"], None, self.btn_goto_release) 699 | btn_goto.setIcon(pil2icon(img_pack["spectrum"])) 700 | VBlayout.addWidget(btn_goto) 701 | 702 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 703 | btn_back.setIcon(pil2icon(img_pack["back"])) 704 | VBlayout.addWidget(btn_back) 705 | VBlayout.setContentsMargins(0, 0, 0, 0) 706 | 707 | def combo_color_select(self, color_by_slider=False): 708 | vcolor = self.combo_color.getValue() 709 | if vcolor is not None: 710 | self.parent.vdic["color"] = vcolor 711 | if self.isVisible(): 712 | self.parent.refreshLocal() 713 | self.sl_sat.setStyle(0) 714 | self.sl_brt.setStyle(0) 715 | self.btn_color.hide() 716 | else: 717 | self.btn_color.show() 718 | if not isinstance(self.parent.vdic["color"], tuple): 719 | self.parent.vdic["color"] = (0, 255, 255, 255) 720 | if color_by_slider: 721 | old_color = self.parent.vdic["color"] 722 | old_color_hsv = rgb_to_hsv(*old_color[:3]) 723 | new_color_hsv = old_color_hsv[0], self.parent.vdic["saturation"], self.parent.vdic["bright"] 724 | new_color = hsv_to_rgb(*new_color_hsv) 725 | self.parent.vdic["color"] = new_color + (old_color[3],) 726 | else: 727 | self.sl_sat.setStyle(1) 728 | self.sl_brt.setStyle(1) 729 | color_hsv = rgb_to_hsv(*(self.parent.vdic["color"][:3])) 730 | self.sl_sat.setValue(color_hsv[1]) 731 | self.sl_brt.setValue(color_hsv[2]) 732 | if self.isVisible(): 733 | self.parent.refreshLocal() 734 | 735 | def sl_sat_release(self): 736 | self.parent.vdic["saturation"] = self.sl_sat.getValue() 737 | if self.combo_color.getValue() is None: 738 | self.combo_color_select(True) 739 | else: 740 | self.combo_color_select() 741 | 742 | def sl_brt_release(self): 743 | self.parent.vdic["bright"] = self.sl_brt.getValue() 744 | if self.combo_color.getValue() is None: 745 | self.combo_color_select(True) 746 | else: 747 | self.combo_color_select() 748 | 749 | def btn_color_release(self): 750 | self.combo_color.setValue(None) 751 | if isinstance(self.parent.vdic["color"], tuple): 752 | self.oldColor = self.parent.vdic["color"] 753 | else: 754 | self.oldColor = (255, 255, 255, 255) 755 | color = QtWidgets.QColorDialog.getColor(QtGui.QColor(*self.oldColor), self, self.lang["Select Color"], 756 | options=QtWidgets.QColorDialog.ShowAlphaChannel) 757 | if color.isValid(): 758 | self.parent.vdic["color"] = color.getRgb() 759 | color_hsv = rgb_to_hsv(*(self.parent.vdic["color"][:3])) 760 | self.sl_sat.setValue(color_hsv[1]) 761 | self.sl_brt.setValue(color_hsv[2]) 762 | self.sl_sat.setStyle(1) 763 | self.sl_brt.setStyle(1) 764 | if self.isVisible(): 765 | self.parent.refreshLocal() 766 | 767 | def ck_glow_release(self): 768 | self.parent.vdic["use_glow"] = self.ck_glow.isChecked() 769 | if self.isVisible(): 770 | self.parent.refreshAll() 771 | 772 | def show(self): 773 | self.parent.hideAllMenu() 774 | if isinstance(self.parent.vdic["color"], tuple): 775 | self.combo_color.setValue(None) 776 | self.sl_sat.setStyle(1) 777 | self.sl_brt.setStyle(1) 778 | color_hsv = rgb_to_hsv(*(self.parent.vdic["color"][:3])) 779 | self.sl_sat.setValue(color_hsv[1]) 780 | self.sl_brt.setValue(color_hsv[2]) 781 | else: 782 | self.combo_color.setValue(self.parent.vdic["color"]) 783 | self.sl_sat.setStyle(0) 784 | self.sl_brt.setStyle(0) 785 | self.sl_sat.setValue(self.parent.vdic["saturation"]) 786 | self.sl_brt.setValue(self.parent.vdic["bright"]) 787 | self.ck_glow.setChecked(self.parent.vdic["use_glow"]) 788 | super().show() 789 | 790 | def btn_goto_release(self): 791 | self.parent.spectrumStyle.show() 792 | 793 | def btn_back_release(self): 794 | self.parent.mainMenu.show() 795 | 796 | 797 | class SpectrumStyleWindow(QtWidgets.QWidget): 798 | def __init__(self, parent): 799 | super(SpectrumStyleWindow, self).__init__() 800 | self.parent = parent 801 | self.lang = parent.lang 802 | self.oldColor = None 803 | VBlayout = QtWidgets.QVBoxLayout(self) 804 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 805 | 806 | style_dic = { 807 | self.lang["Solid Line"]: 0, 808 | self.lang["Solid Line: Center"]: 19, 809 | self.lang["Solid Line: Reverse"]: 20, 810 | self.lang["Dot Line"]: 1, 811 | self.lang["Single Dot"]: 2, 812 | self.lang["Double Dot"]: 7, 813 | self.lang["Double Dot: Center"]: 21, 814 | self.lang["Double Dot: Reverse"]: 22, 815 | self.lang["Concentric"]: 8, 816 | self.lang["Line Graph"]: 17, 817 | self.lang["Zooming Circles"]: 18, 818 | self.lang["Classic Line: Center"]: 9, 819 | self.lang["Classic Line: Bottom"]: 10, 820 | self.lang["Classic Rectangle: Center"]: 15, 821 | self.lang["Classic Rectangle: Bottom"]: 16, 822 | self.lang["Classic Round Dot: Center"]: 11, 823 | self.lang["Classic Round Dot: Bottom"]: 12, 824 | self.lang["Classic Square Dot: Center"]: 13, 825 | self.lang["Classic Square Dot: Bottom"]: 14, 826 | self.lang["Stem Plot: Solid Single"]: 3, 827 | self.lang["Stem Plot: Solid Double"]: 4, 828 | self.lang["Stem Plot: Dashed Single"]: 5, 829 | self.lang["Stem Plot: Dashed Double"]: 6, 830 | self.lang["No Spectrum"]: -1, 831 | } 832 | 833 | self.combo_style = ComboBox(self, style_dic, self.combo_style_select, self.lang["Spectrum Style"]) 834 | VBlayout.addWidget(self.combo_style) 835 | 836 | self.sl_bins = FSlider(self, self.lang["Spectrum Number"], 4, 200, 3, 48, self.sl_bins_release, None) 837 | VBlayout.addWidget(self.sl_bins) 838 | 839 | self.sl_linewidth = FSlider(self, self.lang["Spectrum Thickness"], 0.1, 15, 0.05, 1.0, 840 | self.sl_linewidth_release, 841 | None) 842 | VBlayout.addWidget(self.sl_linewidth) 843 | 844 | self.sl_scalar = FSlider(self, self.lang["Spectrum Scalar"], 0.02, 5.0, 0.02, 1.0, self.sl_scalar_release, None) 845 | VBlayout.addWidget(self.sl_scalar) 846 | 847 | self.sl_smooth = FSlider(self, self.lang["Spectrum Stabilize"], 0, 15, 1, 0, self.sl_smooth_release, None) 848 | VBlayout.addWidget(self.sl_smooth) 849 | 850 | btn_goto = genButton(self, self.lang["Spectrum Color"], None, self.btn_goto_release) 851 | btn_goto.setIcon(pil2icon(img_pack["color"])) 852 | VBlayout.addWidget(btn_goto) 853 | 854 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 855 | btn_back.setIcon(pil2icon(img_pack["back"])) 856 | VBlayout.addWidget(btn_back) 857 | VBlayout.setContentsMargins(0, 0, 0, 0) 858 | 859 | def combo_style_select(self): 860 | self.parent.vdic["style"] = self.combo_style.getValue() 861 | self.parent.refreshLocal() 862 | 863 | def sl_bins_release(self): 864 | self.parent.vdic["bins"] = self.sl_bins.getValue() 865 | self.parent.refreshLocal() 866 | 867 | def sl_linewidth_release(self): 868 | self.parent.vdic["linewidth"] = self.sl_linewidth.getValue() 869 | self.parent.refreshLocal() 870 | 871 | def sl_scalar_release(self): 872 | self.parent.vdic["scalar"] = self.sl_scalar.getValue() 873 | self.parent.refreshLocal() 874 | 875 | def sl_smooth_release(self): 876 | self.parent.vdic["smooth"] = self.sl_smooth.getValue() 877 | 878 | def show(self): 879 | self.parent.hideAllMenu() 880 | 881 | self.combo_style.setValue(self.parent.vdic["style"]) 882 | self.sl_bins.setValue(self.parent.vdic["bins"]) 883 | self.sl_linewidth.setValue(self.parent.vdic["linewidth"]) 884 | self.sl_scalar.setValue(self.parent.vdic["scalar"]) 885 | self.sl_smooth.setValue(self.parent.vdic["smooth"]) 886 | 887 | super().show() 888 | 889 | def btn_goto_release(self): 890 | self.parent.spectrumColor.show() 891 | 892 | def btn_back_release(self): 893 | self.parent.mainMenu.show() 894 | 895 | 896 | class BlendWindow(QtWidgets.QWidget): 897 | def __init__(self, parent): 898 | super(BlendWindow, self).__init__() 899 | self.parent = parent 900 | self.lang = parent.lang 901 | VBlayout = QtWidgets.QVBoxLayout(self) 902 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 903 | 904 | VBlayout.addWidget(genLabel(self, self.lang["Output Path"])) 905 | 906 | self.le_path = LineEdit(self, self.lang[""], self.enterPathCheck) 907 | VBlayout.addWidget(self.le_path) 908 | 909 | self.btn_output_path = genButton(self, self.lang["Select Path"], None, self.btn_output_path_release, style=2) 910 | self.btn_output_path.setIcon(pil2icon(img_pack["folder"])) 911 | VBlayout.addWidget(self.btn_output_path) 912 | 913 | VBlayout.addWidget(genLabel(self, self.lang["Output Config"])) 914 | 915 | self.textview = QtWidgets.QTextBrowser(self) 916 | VBlayout.addWidget(self.textview) 917 | self.textview.setStyleSheet(textBrowserStyle) 918 | self.textview.zoomIn(1) 919 | 920 | self.prgbar = QtWidgets.QProgressBar(self) 921 | self.prgbar.setMaximum(1000) 922 | self.prgbar.setMinimum(0) 923 | self.prgbar.setValue(0) 924 | self.prgbar.setStyleSheet(progressbarStyle1) 925 | VBlayout.addWidget(self.prgbar) 926 | self.prgbar.hide() 927 | 928 | self.btn_stop = genButton(self, self.lang["Stop Rendering"], None, self.btn_stop_release, style=3) 929 | self.btn_stop.setIcon(pil2icon(img_pack["stop"])) 930 | VBlayout.addWidget(self.btn_stop) 931 | self.btn_stop.hide() 932 | 933 | self.btn_blend = genButton(self, self.lang["Start Rendering"], None, self.btn_blend_release, "Ctrl+F5", style=4) 934 | self.btn_blend.setIcon(pil2icon(img_pack["run"])) 935 | VBlayout.addWidget(self.btn_blend) 936 | 937 | self.btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 938 | self.btn_back.setIcon(pil2icon(img_pack["back"])) 939 | 940 | VBlayout.addWidget(self.btn_back) 941 | VBlayout.setContentsMargins(0, 0, 0, 0) 942 | 943 | def btn_output_path_release(self): 944 | if self.parent.vdic["bg_mode"] >= 0: 945 | suffix = ".mp4" 946 | else: 947 | suffix = ".mov" 948 | if self.parent.vdic["filename"] is not None: 949 | fname = self.parent.vdic["filename"] 950 | else: 951 | fname = self.lang["Untitled_Video"] 952 | fname = convertFileFormat(fname, suffix) 953 | selector = suffix[1:].upper() + " " + self.lang["Files"] + " (*" + suffix + ")" 954 | if self.parent.vdic["output_path"]: 955 | dir_out = joinPath(self.parent.vdic["output_path"], fname) 956 | else: 957 | dir_out = fname 958 | file_, filetype = QtWidgets.QFileDialog.getSaveFileName(self, 959 | self.lang["Select Path"], 960 | dir_out, 961 | selector) 962 | if file_: 963 | self.parent.vdic["output_path"] = getFilePath(file_) 964 | self.parent.vdic["filename"] = getFileName(file_, True) 965 | self.showFilePath() 966 | 967 | def showFilePath(self): 968 | if (self.parent.vdic["output_path"] is not None) and (self.parent.vdic["filename"] is not None): 969 | fullpath = joinPath(self.parent.vdic["output_path"], self.parent.vdic["filename"]) 970 | if self.parent.vdic["bg_mode"] >= 0: 971 | fullpath = convertFileFormat(fullpath, "mp4") 972 | else: 973 | fullpath = convertFileFormat(fullpath, "mov") 974 | self.le_path.setText(fullpath) 975 | self.textview.setHtml(self.parent.getBrief()) 976 | self.textview.moveCursor(QtGui.QTextCursor.End) 977 | 978 | def enterPathCheck(self): 979 | path = self.le_path.text() 980 | if path == "": 981 | self.parent.vdic["output_path"] = None 982 | self.parent.vdic["filename"] = None 983 | elif not os.path.exists(getFilePath(path)): 984 | showInfo(self, self.lang["Notice"], self.lang["This folder is not exist!"], False) 985 | self.showFilePath() 986 | else: 987 | self.parent.vdic["output_path"] = getFilePath(path) 988 | if self.parent.vdic["bg_mode"] >= 0: 989 | self.parent.vdic["filename"] = convertFileFormat(getFileName(path, True), "mp4") 990 | else: 991 | self.parent.vdic["filename"] = convertFileFormat(getFileName(path, True), "mov") 992 | self.showFilePath() 993 | self.le_path.clearFocus() 994 | 995 | def btn_blend_release(self): 996 | self.le_path.clearFocus() 997 | path = self.le_path.text() 998 | if not os.path.exists(getFilePath(path)): 999 | showInfo(self, self.lang["Notice"], self.lang["Output folder is not exist!"], False) 1000 | self.showFilePath() 1001 | return 1002 | path = self.le_path.getText() 1003 | if os.path.exists(path): 1004 | reply = showInfo(self, self.lang["Notice"], self.lang["Are you sure to overwrite this file?"], True) 1005 | if not reply: 1006 | return 1007 | self.parent.startBlending() 1008 | self.prgbar.setValue(0) 1009 | 1010 | def btn_stop_release(self): 1011 | reply = showInfo(self, self.lang["Notice"], self.lang["Are you sure to stop rendering?"], True) 1012 | if reply: 1013 | self.parent.stopBlending() 1014 | self.btn_stop.setEnabled(False) 1015 | 1016 | def freezeWindow(self, flag=True): 1017 | if flag: 1018 | self.btn_back.hide() 1019 | self.btn_blend.hide() 1020 | self.le_path.setEnabled(False) 1021 | self.btn_output_path.setEnabled(False) 1022 | self.btn_output_path.setIcon(pil2icon(img_pack["folder_black"])) 1023 | loop = QtCore.QEventLoop() 1024 | QtCore.QTimer.singleShot(100, loop.quit) 1025 | loop.exec_() 1026 | self.prgbar.show() 1027 | self.btn_stop.show() 1028 | self.btn_stop.setEnabled(True) 1029 | 1030 | else: 1031 | self.btn_stop.hide() 1032 | self.prgbar.hide() 1033 | loop = QtCore.QEventLoop() 1034 | QtCore.QTimer.singleShot(100, loop.quit) 1035 | loop.exec_() 1036 | self.btn_back.show() 1037 | self.btn_blend.show() 1038 | self.le_path.setEnabled(True) 1039 | self.btn_output_path.setEnabled(True) 1040 | self.btn_output_path.setIcon(pil2icon(img_pack["folder"])) 1041 | self.textview.moveCursor(QtGui.QTextCursor.End) 1042 | 1043 | def show(self): 1044 | self.parent.hideAllMenu() 1045 | self.parent.canDrop = False 1046 | self.showFilePath() 1047 | 1048 | super().show() 1049 | 1050 | def btn_back_release(self): 1051 | self.parent.mainMenu.show() 1052 | self.parent.canDrop = True 1053 | 1054 | 1055 | class AboutWindow(QtWidgets.QWidget): 1056 | def __init__(self, parent): 1057 | super(AboutWindow, self).__init__() 1058 | self.parent = parent 1059 | self.lang = parent.lang 1060 | VBlayout = QtWidgets.QVBoxLayout(self) 1061 | VBlayout.setAlignment(QtCore.Qt.AlignTop) 1062 | 1063 | items_lang = { 1064 | "English": "en", 1065 | "简体中文": "cn_s" 1066 | } 1067 | self.combo_lang = ComboBox(self, items_lang, self.combo_lang_select, "Language / 语言") 1068 | VBlayout.addWidget(self.combo_lang) 1069 | 1070 | self.logo = QtWidgets.QLabel(self) 1071 | self.logo.setPixmap(pil2qt(img_pack["about_logo"])) 1072 | self.logo.setAlignment(QtCore.Qt.AlignCenter) 1073 | VBlayout.addWidget(self.logo) 1074 | 1075 | self.textview = QtWidgets.QTextBrowser(self) 1076 | VBlayout.addWidget(self.textview) 1077 | self.textview.setHtml(self.parent.getIntro()) 1078 | self.textview.setOpenExternalLinks(True) 1079 | self.textview.setStyleSheet(textBrowserStyle) 1080 | self.textview.zoomIn(1) 1081 | 1082 | btn_back = genButton(self, self.lang["Back to Main Menu"], None, self.btn_back_release, "Escape") 1083 | btn_back.setIcon(pil2icon(img_pack["back"])) 1084 | VBlayout.addWidget(btn_back) 1085 | 1086 | VBlayout.setSpacing(15) 1087 | VBlayout.setContentsMargins(0, 0, 0, 0) 1088 | 1089 | def combo_lang_select(self): 1090 | if self.isVisible(): 1091 | lang_code = self.combo_lang.getValue() 1092 | self.parent.lang_code = lang_code 1093 | self.parent.resetLang = True 1094 | self.parent.close() 1095 | 1096 | def show(self): 1097 | self.parent.hideAllMenu() 1098 | self.combo_lang.setValue(self.parent.lang_code) 1099 | super().show() 1100 | 1101 | def btn_back_release(self): 1102 | self.parent.mainMenu.show() 1103 | -------------------------------------------------------------------------------- /SourceCode/ReadMe.txt: -------------------------------------------------------------------------------- 1 | Welcome to Fanseline Visualizer! 2 | 欢迎使用“帆室邻音频可视化制作工具”! 3 | 4 | By Twitter @FanKetchup 5 | https://github.com/FerryYoungFan/FanselineVisualizer 6 | 7 | 8 | **************************************************************************************** 9 | **************************************************************************************** 10 | Notice: 11 | Recommend Python3 version: 3.7.4 12 | 13 | Since v1.0.6: Run "_CheckEnvironment.py" with python3 willcheck and install the 14 | required environment automatically. (Network conenction required) 15 | 16 | Example: 17 | python3 FanBlender_GUI.py 18 | 19 | However, you can still install the environment manually 20 | (see followings if you want to do so). 21 | 22 | ---------------------------------------------------------------------------------------- 23 | 24 | 注意: 25 | 推荐使用的 Python3 版本: 3.7.4 26 | 27 | 在v1.0.6之后:python3 运行 "_CheckEnvironment.py" 会自动检查和安装所需环境。(需要联网) 28 | 29 | 运行例子: 30 | python3 FanBlender_GUI.py 31 | 32 | 但您仍然可以手动安装环境(如果您想这么做请继续往下看) 33 | **************************************************************************************** 34 | **************************************************************************************** 35 | 36 | 37 | 38 | ---------------------------------------------------------------------------------------- 39 | ---------------------------------------------------------------------------------------- 40 | Require / 需求: 41 | 42 | Python 3.7 V 3.7.4 43 | 44 | numpy V 1.19.0 45 | Pillow V 7.2.0 46 | imageio V 2.9.0 47 | imageio-ffmpeg V 0.4.2 48 | PyQt5 V 5.15.0 49 | pydub V 0.24.1* 50 | (* No need to install ffmpeg for pydub, since it shares ffmpeg with imageio-ffmpeg.) 51 | (* 无需为pydub专门安装ffmpeg,因为其与imageio-ffmpeg共享ffmpeg) 52 | ---------------------------------------------------------------------------------------- 53 | ---------------------------------------------------------------------------------------- 54 | 55 | 56 | 57 | 58 | ---------------------------------------------------------------------------------------- 59 | ---------------------------------------------------------------------------------------- 60 | Please run these in Terminal: 61 | 请在终端运行如下内容: 62 | 63 | pip3 install numpy 64 | pip3 install Pillow 65 | pip3 install imageio 66 | pip3 install imageio-ffmpeg 67 | pip3 install PyQt5 68 | pip3 install pydub 69 | 70 | If ffmpeg is not installed: 71 | 如果 ffmpeg 没有安装: 72 | (Example for Ubuntu) 73 | sudo apt update 74 | sudo apt-get install -y ffmpeg 75 | 76 | Finally run: 77 | 最后运行: 78 | python3 FanBlender_GUI.py 79 | 80 | ---------------------------------------------------------------------------------------- 81 | ---------------------------------------------------------------------------------------- 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /SourceCode/Source.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FerryYoungFan/FanselineVisualizer/451288d62e09db6c2f06bfc14d02c62983722071/SourceCode/Source.zip -------------------------------------------------------------------------------- /SourceCode/_CheckEnvironment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | """ 7 | Python 3.7 V 3.7.4 8 | 9 | numpy V 1.19.0 10 | Pillow V 7.2.0 11 | imageio V 2.9.0 12 | imageio-ffmpeg V 0.4.2 13 | PyQt5 V 5.15.0 14 | pydub V 0.24.1* 15 | 16 | (* No need to install ffmpeg for pydub, since it shares ffmpeg with imageio-ffmpeg.) 17 | """ 18 | 19 | 20 | def checkEnvironment(showInfo=True): 21 | if showInfo: 22 | print("Checking Python Environment...") 23 | try: 24 | import numpy 25 | except: 26 | if showInfo: 27 | print("Numpy not found, try to install:") 28 | os.system("pip3 install numpy==1.19.0") 29 | 30 | try: 31 | import PIL 32 | except: 33 | if showInfo: 34 | print("Pillow not found, try to install:") 35 | os.system("pip3 install Pillow==7.2.0") 36 | 37 | try: 38 | import imageio 39 | except: 40 | if showInfo: 41 | print("imageio not found, try to install:") 42 | os.system("pip3 install imageio==2.9.0") 43 | 44 | try: 45 | import imageio_ffmpeg 46 | except: 47 | if showInfo: 48 | print("imageio-ffmpeg not found, try to install:") 49 | os.system("pip3 install imageio-ffmpeg==0.4.2") 50 | 51 | try: 52 | import PyQt5 53 | except: 54 | if showInfo: 55 | print("PyQt5 not found, try to install:") 56 | os.system("pip3 install PyQt5==5.15.0") 57 | 58 | try: 59 | import pydub 60 | print("↑↑↑ Please ignore this pydub runtime warning ↑↑↑") 61 | except: 62 | if showInfo: 63 | print("pydub not found, try to install:") 64 | os.system("pip3 install pydub==0.24.1") 65 | 66 | not_install = None 67 | try: 68 | not_install = "Numpy" 69 | import numpy 70 | not_install = "Pillow" 71 | import PIL 72 | not_install = "iamgeio" 73 | import imageio 74 | not_install = "imageio-ffmpeg" 75 | import imageio_ffmpeg 76 | not_install = "pydub" 77 | import pydub 78 | if showInfo: 79 | print("Check Environment - Done!") 80 | except: 81 | print("Error: Environment Error") 82 | print("{0} not installed.".format(not_install)) 83 | 84 | 85 | if __name__ == '__main__': 86 | checkEnvironment(True) 87 | -------------------------------------------------------------------------------- /SourceCode/_Installer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | If you want to use pyinstaller for this project, 6 | simply run: 7 | " 8 | python3 _Installer.py 9 | " 10 | in terminal 11 | """ 12 | 13 | import os, shutil 14 | 15 | try: 16 | from _CheckEnvironment import checkEnvironment 17 | 18 | checkEnvironment(True) 19 | except: 20 | pass 21 | 22 | try: 23 | import PyInstaller.__main__ 24 | except: 25 | print("check pyinstaller: not installed. Now install pyinstaller:") 26 | os.system('pip3 install pyinstaller') 27 | import PyInstaller.__main__ 28 | 29 | from sys import platform 30 | 31 | app_name = "FanselineVisualizer" 32 | main_file = "FanBlender_GUI.py" 33 | 34 | if platform == "darwin": 35 | icon_path = "./Source_/icon-mac.icns" 36 | output_list = ["--name=%s" % app_name, "--windowed"] 37 | if os.path.exists("./Source"): 38 | output_list.append("--add-data=%s" % "./Source:Source/") 39 | if os.path.exists("./Source_/MacOS_Support"): 40 | pass 41 | if os.path.exists(icon_path): 42 | output_list.append("--icon=%s" % icon_path) 43 | output_list.append(main_file) 44 | else: 45 | icon_path = "./Source/icon-large_Desktop_256x256.ico" 46 | output_list = ["--noconfirm", "--name=%s" % app_name] 47 | if os.path.exists("./Source"): 48 | if os.name == "nt": 49 | output_list.append("--add-data=%s" % "./Source;Source/") 50 | else: 51 | output_list.append("--add-data=%s" % "./Source:Source/") 52 | output_list.append("--onedir") 53 | output_list.append("--windowed") 54 | if os.path.exists(icon_path): 55 | output_list.append("--icon=%s" % icon_path) 56 | output_list.append(main_file) 57 | 58 | excludes = ['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter'] 59 | for ex in excludes: 60 | output_list.append("--exclude-module=" + ex) 61 | 62 | PyInstaller.__main__.run(output_list) 63 | 64 | print("Removing Unused files...") 65 | 66 | if platform == "darwin": 67 | shutil.rmtree("./dist/"+app_name+".app/Contents/Resources/imageio",True) 68 | else: 69 | shutil.rmtree("./dist/" + app_name + "/imageio", True) 70 | if os.name == "nt": 71 | os.remove("./dist/" + app_name + "/opengl32sw.dll") 72 | os.remove("./dist/" + app_name + "/d3dcompiler_47.dll") 73 | 74 | -------------------------------------------------------------------------------- /SourceCode/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.19.0 2 | Pillow==7.2.0 3 | imageio==2.9.0 4 | imageio-ffmpeg==0.4.2 5 | PyQt5==5.15.0 6 | pydub==0.24.1 --------------------------------------------------------------------------------