diff options
-rw-r--r-- | README.md | 41 | ||||
-rw-r--r-- | assets/in.mp4 | bin | 0 -> 263267 bytes | |||
-rw-r--r-- | assets/out.mp4 | bin | 0 -> 1776458 bytes | |||
-rw-r--r-- | main.py | 16 | ||||
-rw-r--r-- | requirements.txt | 7 | ||||
-rw-r--r-- | src/VCR_OSD_MONO_1.001.ttf | bin | 0 -> 75864 bytes | |||
-rw-r--r-- | src/VHSImage.py | 184 | ||||
-rw-r--r-- | src/VHSVideo.py | 87 | ||||
-rw-r--r-- | src/VaporSong.py | 182 | ||||
-rw-r--r-- | src/__init__.py | 0 |
10 files changed, 503 insertions, 14 deletions
@@ -1,20 +1,21 @@ # vaporwave generator 旺育栄 -A vaporwave music (+art, +video soon, I promise) generator bodged together using code from various sources. Runs on Python3 +A vaporwave music + image + video (+art soon, I promise) generator bodged together using code from various sources. Runs on Python 3. VHSVideo option is really really slow (Seconds per frame is 7.) ``` -usage: main.py [-h] [-M] [-P] [-V] [-i INPUT] +usage: main.py [-h] [-M] [-P] [-V] [-v] [-i INPUT] [-o OUTPUT] -This program takes YouTube URL or title of a song and converts it into -vaporwave +| V A P O R W A V E || G E N E R A T O R | optional arguments: -h, --help show this help message and exit -M, --music generate v a p o r w a v e music -P, --picture generate VHS Style image - -V, --version show program version + -V, --video VHS Style Video + -v, --version show program version -i INPUT, --input INPUT - + -o OUTPUT, --output OUTPUT + Output for specifying output video ``` If the program gives an error for sox, try running `ulimit -n 999'` @@ -27,9 +28,17 @@ Linking to Bandcamp soon ### V H S I M A G E -![]("assets/in-vhs.jpg") +#### Input + +![](assets/in-vhs.jpg?raw=true "Input VHS") + +#### Output + +![](assets/out-vhs.jpg?raw=true "Output VHS") + +### V H S V I D E O -![]("assets/out-vhs.jpg") +See `in.mp4` and `out.mp4` in the `assets` folder ## Installation @@ -41,7 +50,7 @@ Windows is unsupported at this time ( I need to find a way to use aubio's python #### Linux ``` -sudo apt install ffmpeg libavl1 sox +sudo apt install ffmpeg ffprobe libavl1 sox pip install -r requirements.txt ``` @@ -72,9 +81,19 @@ python3 main.py -M -i Song Title `python3 main.py -P -i "image.jpg"` +### V H S V I D E O + +`python3 main.py -V -i "video.mp4" -o "output.mp4"` + ## Bugs -This project is a result of bodging and therefore has tons of bugs which need to be ironed out +This project is a result of bodging and therefore has tons of bugs which need to be ironed out. I need to swat some bugs in the VHSVideo file. + +There might be a problem with the generated video not having audio, for that run the following + +`ffmpeg -i video.mp4 -vn -acodec copy output-audio.aac` +`ffmpeg -i output.mp4 -i output-audio.aac -c copy output-with-audio.mp4` + ## To-Do @@ -82,7 +101,7 @@ This project is a result of bodging and therefore has tons of bugs which need to [] Clean the Code [] Add Artwork Generator [x] VHS Picture Styler ( Added in v1.5 ) -[] Add Video Generator +[x] Add Video Generator [] Add Custom Date to VHS Styler ## Credits diff --git a/assets/in.mp4 b/assets/in.mp4 Binary files differnew file mode 100644 index 0000000..9ffe713 --- /dev/null +++ b/assets/in.mp4 diff --git a/assets/out.mp4 b/assets/out.mp4 Binary files differnew file mode 100644 index 0000000..fc0616b --- /dev/null +++ b/assets/out.mp4 @@ -1,5 +1,6 @@ from src.VaporSong import VaporSong from src.VHSImage import generateVHSStyle +from src.VHSVideo import VHS_Vid import os import sys import youtube_dl @@ -12,22 +13,25 @@ import urllib.parse import argparse import time -version = 1.5 +version = 2.0 style = False -text = 'This program takes YouTube URL or title of a song and converts it into vaporwave' +text = '| V A P O R W A V E || G E N E R A T O R |' parser = argparse.ArgumentParser(description = text) parser.add_argument("-M", "--music", help="generate v a p o r w a v e music", action="store_true") parser.add_argument("-P", "--picture", help="generate VHS Style image", action="store_true") -parser.add_argument("-V", "--version", help="show program version", action="store_true") +parser.add_argument("-V","--video", help="VHS Style Video", action="store_true") +parser.add_argument("-v", "--version", help="show program version", action="store_true") parser.add_argument("-i", "--input") +parser.add_argument("-o","--output", help="Output for specifying output video") args = parser.parse_args() music = False picture = False +video = False if args.version: print("vaporwave generator 旺育栄", version) @@ -36,8 +40,12 @@ if args.music: music = True elif args.picture: picture = True +elif args.video: + video = True if args.input: query = args.input +if args.output: + outfile = args.output else: parser.print_help() exit @@ -160,4 +168,6 @@ if music: gen_vapor(name, title) elif picture: generateVHSStyle(query,"out.jpg") +elif video: + VHS_Vid(query, outfile)
\ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bd32e17 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +youtube_dl +logzero +urllib +re +PIL +imagio +numpy
\ No newline at end of file diff --git a/src/VCR_OSD_MONO_1.001.ttf b/src/VCR_OSD_MONO_1.001.ttf Binary files differnew file mode 100644 index 0000000..dcca687 --- /dev/null +++ b/src/VCR_OSD_MONO_1.001.ttf diff --git a/src/VHSImage.py b/src/VHSImage.py new file mode 100644 index 0000000..81f446d --- /dev/null +++ b/src/VHSImage.py @@ -0,0 +1,184 @@ +import requests +import datetime +from PIL import Image +from PIL import ImageFont +from PIL import ImageDraw +import colorsys +import json +import numpy as np +import imageio +import random +from math import floor +import os +import logzero +from logzero import logger +from logzero import setup_logger +import random + + +def generate_offsets(array_size, max_offset): + periodicity = random.randint(1, 10) + periodicity = random.random() * periodicity + offsets = [] + for i in range(array_size): + # print(floor(max_offset*np.sin(periodicity*(i*np.pi/180)))) + offsets.append(floor(max_offset*np.sin(periodicity*(i*np.pi/180)))) + return offsets + + +def hueChange(img, offset): + # https://stackoverflow.com/questions/27041559/rgb-to-hsv-python-change-hue-continuously/27042106 + # It's better to raise an exception than silently return None if img is not + # an Image. + img.load() + r, g, b = img.split() + r_data = [] + g_data = [] + b_data = [] + + offset = offset/100. + for rd, gr, bl in zip(r.getdata(), g.getdata(), b.getdata()): + h, s, v = colorsys.rgb_to_hsv(rd/255.0, bl/255.0, gr/255.0) + # print(h, s, v) + # print(rd, gr, bl) + rgb = colorsys.hsv_to_rgb(h, s+offset, v) + rd, bl, gr = [int(x*255.) for x in rgb] + # print(rd, gr, bl) + r_data.append(rd) + g_data.append(gr) + b_data.append(bl) + + r.putdata(r_data) + g.putdata(g_data) + b.putdata(b_data) + return Image.merge('RGB',(r,g,b)) + +def decision(probability): + return random.random() < probability + +def getImage(out_name="image.jpg"): + now = datetime.datetime.now() + dt_str = now.strftime("%Y-%m-%dT%H:%M:%Sz") + newurl = url.format(WIDTH=3840, HEIGHT=2160, DATE=dt_str) + r = requests.get(newurl) + img_data = r.json()["batchrsp"]["items"][0] + # ["ad"]["image_fullscreen_001_landscape"]["u"] + img_url = json.loads(img_data["item"])["ad"]["image_fullscreen_001_landscape"]["u"] + r = requests.get(img_url) + with open(out_name, "wb") as f: + f.write(r.content) + +def mod_image_repeat_rows(imgname, chance_of_row_repeat=0, max_row_repeats=0, min_row_repeats=0, save=True, out_name="image.jpg"): + img = Image.open(imgname) + pixels = img.load() + width, height = img.size + + repeat = False + num_repeats = 0 + times_to_repeat = 0 + row_to_repeat = [] + offsets = [] + for y in range(height): + if not repeat and decision(chance_of_row_repeat): + repeat = True + times_to_repeat = random.randint(min_row_repeats, max_row_repeats) + offsets = generate_offsets(times_to_repeat, random.randint(10, 50)) + for x in range(width): + r, g, b = img.getpixel((x, y)) + + if repeat and len(row_to_repeat) != width: + pixels[x, y] = (r, g, b) + row_to_repeat.append((r, g, b)) + elif repeat: + try: + pixels[x, y] = row_to_repeat[x + offsets[num_repeats]] + except Exception as e: + pixels[x, y] = row_to_repeat[x - offsets[num_repeats]] + else: + pixels[x, y] = (r, g, b) + + if repeat: + num_repeats += 1 + if num_repeats >= times_to_repeat: + repeat = False + times_to_repeat = 0 + num_repeats = 0 + row_to_repeat = [] + offsets = [] + if save: + img.save(out_name) + + +def add_date(img_path, out_name="image.jpg", bottom_offset=0): + date_obj = datetime.datetime.now() + date_str_1 = date_obj.strftime("%p %H:%M") + date_str_2 = date_obj.strftime("%b. %d %Y") + corner_offset = 50 + img = Image.open(img_path) + width, height = img.size + draw = ImageDraw.Draw(img) + font = ImageFont.truetype("src/VCR_OSD_MONO_1.001.ttf", 64) + draw.text((corner_offset, (height-150-bottom_offset)), date_str_1, (255, 255, 255), font=font) + draw.text((corner_offset, (height-75)-bottom_offset), date_str_2, (255, 255, 255), font=font) + draw.text((corner_offset, 25), "|| PAUSE", (255, 255, 255), font=font) + img.save(out_name) + +def add_img_noise(imgpath, intensity=1, out_name="image.jpg"): + img = imageio.imread(imgpath, pilmode='RGB') + noise1 = img + intensity * img.std() * np.random.random(img.shape) + imageio.imwrite(out_name, noise1) + +def offset_hue(image, out_name="image.jpg"): + if isinstance(image, str): + image = Image.open(image) + image = hueChange(image, 25) + image.save(out_name) + +def build_background(out_name, taskbar_offset): + #getImage(out_name="start.jpg") + offset_hue("start.jpg", out_name="saturated.jpg") + mod_image_repeat_rows("saturated.jpg", 0.012, 50, 10, out_name="shifted.jpg") + add_img_noise("shifted.jpg", out_name="noisy.jpg") + add_date("noisy.jpg", out_name=out_name, bottom_offset=taskbar_offset) + +""" +if __name__ == "__main__": + build_background("bkg.jpg", 25) +""" + +def generateVHSStyle(infile, outfile, silence=False): + if silence: + cut_rows = bool(random.getrandbits(1)) + offset = random.choice([0,5,10,15,20,25]) + offset_hue(infile,"saturated.jpg") + if cut_rows: + mod_image_repeat_rows("saturated.jpg", 0.012, 50, 10, True, "shifted.jpg") + else: + mod_image_repeat_rows("saturated.jpg", 0, 0, 0, True, "shifted.jpg") + add_date("shifted.jpg","noisy.jpg") + add_date("noisy.jpg",outfile, bottom_offset=offset) + os.remove("shifted.jpg") + os.remove("saturated.jpg") + os.remove("noisy.jpg") + else: + cut_rows = bool(random.getrandbits(1)) + offset = random.choice([0,5,10,15,20,25]) + logger.info("Saturating the image") + offset_hue(infile,"saturated.jpg") + if cut_rows: + logger.info("Shifting the image") + mod_image_repeat_rows("saturated.jpg", 0.012, 50, 10, True, "shifted.jpg") + else: + logger.info("Not applying lines effect") + mod_image_repeat_rows("saturated.jpg", 0, 0, 0, True, "shifted.jpg") + logger.info("Adding noise") + add_date("shifted.jpg","noisy.jpg") + logger.info("Adding text") + add_date("noisy.jpg",outfile, bottom_offset=offset) + logger.info("Generated Image: out.jpg") + logger.info("Removing residual files") + os.remove("shifted.jpg") + os.remove("saturated.jpg") + os.remove("noisy.jpg") + +#generateVHSStyle("s.jpg","o.jpg")
\ No newline at end of file diff --git a/src/VHSVideo.py b/src/VHSVideo.py new file mode 100644 index 0000000..e644074 --- /dev/null +++ b/src/VHSVideo.py @@ -0,0 +1,87 @@ +import os +import cv2 +from src.VHSImage import generateVHSStyle +from os.path import isfile, join +import numpy as np +import subprocess +import logzero +from logzero import logger +from logzero import setup_logger + +def SaveVid(path): + vidObj = cv2.VideoCapture(path) + count = 0 + success = 1 + while success: + success, image = vidObj.read() + cv2.imwrite("frames/"+str(count)+".jpg", image) + #os.rename("frames/"+str(count)+".jpg", os.path.splitext("frames/"+str(count)+".jpg")[0]) + count += 1 + +def Style(pathToFrames): + files = [f for f in os.listdir(pathToFrames) if isfile(join(pathToFrames, f))] + count = 0 + for i in files: + count += 1 + f = str(i) + fi = pathToFrames + f + out = fi + ".jpg" + + generateVHSStyle(fi, out, silence=True) + os.rename(out, fi) + print("--------") + print("On Frame: ") + print(count) + print("Out of") + print(len(files)) + print("--------") + cwd = os.getcwd() + os.chdir(pathToFrames) + c = "find ./ -name \"*.jpg\" -exec sh -c 'mv $0 `basename \"$0\" .jpg`' '{}' \; ;" + os.system(c) + os.chdir(cwd) + +def generateVideo(outfile, path, infile): + frame_array = [] + files = [int(f) for f in os.listdir(path) if isfile(join(path, f))] + files.sort() + + duration = subprocess.check_output(['ffprobe', '-i', infile, '-show_entries', 'format=duration', '-v', 'quiet', '-of', 'csv=%s' % ("p=0")]) + fps = len(files)/float(duration) + print("FPS", fps) + + for i in range(len(files)): + filename=path + str(files[i]) + img = cv2.imread(filename) + height, width, layers = img.shape + size = (width,height) + frame_array.append(img) + out = cv2.VideoWriter(outfile,cv2.VideoWriter_fourcc(*'MP4V'), fps, size) + for i in range(len(frame_array)): + out.write(frame_array[i]) + out.release() + + +def VHS_Vid(infile, outfile): + path = './frames/' + os.system("rm frames/*") + os.system("mkdir frames") + logger.info("Exctracting Frames") + try: + SaveVid(infile) + except: + logger.debug("Extracted Frames") + logger.info("Applying A E S T H E T I C S") + Style(path) + logger.info("Generating Vidio") + generateVideo("temp.mp4", path, infile) + logger.info("Extracting audio of original video") + os.system("ffmpeg -i %(infile)s -vn -acodec copy output-audio.aac") + logger.info("Merging audio") + os.system("ffmpeg -i temp.mp4 -i output-audio.aac -c copy %(outfile)s") + logger.info("Removing residual files") + os.remove("temp.mp4") + os.remove("output-audio.aac") + + +#VHS_Vid("video.mp4","video2.mp4")
\ No newline at end of file diff --git a/src/VaporSong.py b/src/VaporSong.py new file mode 100644 index 0000000..bb75646 --- /dev/null +++ b/src/VaporSong.py @@ -0,0 +1,182 @@ +import os +import subprocess +import re +from random import randint + +import logzero +from logzero import logger +from logzero import setup_logger + +CONFIDENCE_THRESH = 0.02 + +class VaporSong: + + # Slows down Track + + def slow_down(src, rate, dest): + cmd = "sox -G -D " + src + " " + dest + " speed " + str(rate) + os.system(cmd) + return dest + + # Adds Reverb + + def reverbize(src, dest): + cmd = "sox -G -D " + src + " " + dest + " reverb 100 fade 5 -0 7" # idk what this does tbh, https://stackoverflow.com/a/57767238/8386344 + os.system(cmd) + return dest + + + # Crops "src" from "start" plus "start + dur" and return it in "dest" + def crop(src,dest,start,dur): + cmd = "sox " + src + " " + dest + " trim " + " " + str(start) + " " + str(dur) + os.system(cmd) + + + # Randomly crops a part of the song of at most max_sec_len. + def random_crop(src, max_sec_len, dest): + out = subprocess.check_output(["soxi","-D",src]).rstrip() + f_len = int(float(out)) + if (f_len <= max_sec_len): + os.system("cp " + src + " " + dest) + return + else: + start_region = f_len - max_sec_len + start = randint(0,start_region) + VaporSong.crop(src,dest,start,max_sec_len) + + + # Given a file, returns a list of [beats, confidence], executable based on audibo's test-beattracking.c + # TODO: Move away from executable and use aubio's Python module + def fetchbeats(src): + beat_matrix = [] + if os.name == 'posix': + beats = subprocess.check_output(["noah", "get-beats",src]).rstrip() + else: + beats = subprocess.check_output(["get-beats",src]).rstrip() + beats_ary = beats.splitlines() + for i in beats_ary: + record = i.split() + record[0] = float(record[0])/1000.0 + record[1] = float(record[1]) + beat_matrix.append(record) + return beat_matrix + + # Splits an audio file into beats according to beat_matrix list + + def split_beat(src,beat_matrix): + split_files = [] + for i in range(0,len(beat_matrix)-1): + + if(beat_matrix[i][1] > CONFIDENCE_THRESH): + dur = (beat_matrix[i+1][0] - beat_matrix[i][0]) + out = src.split(".")[0]+str(i)+".wav" + VaporSong.crop(src,out,beat_matrix[i][0],dur) + split_files.append(out) + return split_files + + # Combines a list of sections + + def combine(sections,dest): + tocomb = [] + tocomb.append("sox") + tocomb.append("-G") + for section in sections: + for sample in section: + tocomb.append(sample) + tocomb.append(dest) + tmpFileLimit = len(tocomb) + 256 # in case the program messes up, it does not actually frick up your system + n = str(tmpFileLimit) + #logger.info("Setting file limit to ", n) + os.system("ulimit -n " + n) + subprocess.check_output(tocomb) + return dest + + # Arbitrarily groups beats into lists of 4, 6, 8, or 9, perfect for looping. + + def generate_sections(ary): + sections = [] + beats = [4,6,8,9] + index = 0 + while(index != len(ary)): + current_beat = beats[randint(0,len(beats)-1)] + new_section = [] + while((current_beat != 0) and (index != len(ary))): + new_section.append(ary[index]) + current_beat -= 1 + index += 1 + sections.append(new_section) + return sections + + + # given a list of sections, selects some of them and duplicates them, perfect for that vaporwave looping effect + def dup_sections(sections): + new_section = [] + for section in sections: + new_section.append(section) + if(randint(0,1) == 0): + new_section.append(section) + return new_section + + # a passage is a list of sections. This takes some sections and groups them into passages. + + def make_passages(sections): + passages = [] + index = 0 + while(index != len(sections)): + passage_len = randint(1,4) + passage = [] + while(index != len(sections) and passage_len > 0): + passage.append(sections[index]) + index += 1 + passage_len -= 1 + passages.append(passage) + return passages + + # Given all of our passages, picks some of them and inserts them into a list some number of times. + + def reorder_passages(passages): + new_passages = [] + passage_count = randint(5,12) + while(passage_count != 0): + passage = passages[randint(0,len(passages)-1)] + passage_count -= 1 + dup = randint(1,4) + while(dup != 0): + dup -= 1 + new_passages.append(passage) + return new_passages + + # converts a list of passages to a list of sections. + + def flatten(passages): + sections = [] + for passage in passages: + for section in passage: + sections.append(section) + return sections + + # It's all coming together + + def vaporize_song(fname, title): + logger.info("Slowing down the music") + VaporSong.slow_down(fname, 0.7, "beats/out.wav") + #logger.info("Cropping") + #VaporSong.random_crop("beats/out.wav",150,"beats/outcrop.wav") + logger.info("Doing Beat Analysis") + bm = VaporSong.fetchbeats("beats/out.wav") + logger.info("Split into beats") + splitd = VaporSong.split_beat("beats/out.wav",bm) + #group beats to sections + logger.info("Divide into sections") + sections = VaporSong.generate_sections(splitd) + logger.info("Duping Sections") + sdup = VaporSong.dup_sections(sections) + # group sections into passages + paslist = VaporSong.make_passages(sdup) + # reorder packages + pasloop = VaporSong.reorder_passages(paslist) + sectionflat = VaporSong.flatten(pasloop) + logger.info("Mastering & Reverbing") + VaporSong.combine(sectionflat,"beats/out_norev.wav") + VaporSong.reverbize("beats/out_norev.wav","./" + (re.sub(r"\W+|_", " ", title)).replace(" ","_") + ".wav") + logger.info("Generated V A P O R W A V E") diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/__init__.py |