diff --git a/src/components/original.py b/src/components/original.py index 3d1a574..621af6f 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -18,6 +18,9 @@ class Component(Component): def names(*args): return ['Original Audio Visualization'] + def properties(self): + return ['pcm'] + def widget(self, *args): self.visColor = (255, 255, 255) self.scale = 20 diff --git a/src/components/video.py b/src/components/video.py index d3460ff..6cd16e5 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -4,10 +4,10 @@ import os import math import subprocess -from component import Component, ComponentError -from toolkit.frame import BlankFrame -from toolkit.ffmpeg import testAudioStream, FfmpegVideo -from toolkit import openPipe, closePipe, checkOutput, scale +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo +from toolkit import checkOutput class Component(Component): @@ -132,7 +132,7 @@ class Component(Component): ] command.extend(self.makeFfmpegFilter()) command.extend([ - '-vcodec', 'rawvideo', '-', + '-codec:v', 'rawvideo', '-', '-ss', '90', '-frames:v', '1', ]) diff --git a/src/components/waveform.py b/src/components/waveform.py index 487a3bb..375b3fc 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -5,10 +5,10 @@ import os import math import subprocess -from component import Component, ComponentError -from toolkit.frame import BlankFrame -from toolkit import openPipe, checkOutput, rgbFromString -from toolkit.ffmpeg import FfmpegVideo +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit import checkOutput, rgbFromString, pickColor +from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo class Component(Component): @@ -21,17 +21,27 @@ class Component(Component): self.page.lineEdit_color.setText('%s,%s,%s' % self.color) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ - % QColor(*self.color1).name() - self.page.lineEdit_color.setStylesheet(btnStyle) + % QColor(*self.color).name() + self.page.pushButton_color.setStyleSheet(btnStyle) self.page.pushButton_color.clicked.connect(lambda: self.pickColor()) + self.page.spinBox_scale.valueChanged.connect(self.updateChunksize) + + if hasattr(self.parent, 'window'): + self.parent.window.lineEdit_audioFile.textChanged.connect( + self.update + ) self.trackWidgets( { 'mode': self.page.comboBox_mode, + 'amplitude': self.page.comboBox_amplitude, 'x': self.page.spinBox_x, 'y': self.page.spinBox_y, 'mirror': self.page.checkBox_mirror, 'scale': self.page.spinBox_scale, + 'opacity': self.page.spinBox_opacity, + 'compress': self.page.checkBox_compress, + 'mono': self.page.checkBox_mono, } ) @@ -42,6 +52,26 @@ class Component(Component): self.page.pushButton_color.setStyleSheet(btnStyle) super().update() + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) + + self.page.lineEdit_color.setText('%s,%s,%s' % pr['color']) + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color']).name() + self.page.pushButton_color.setStyleSheet(btnStyle) + + def savePreset(self): + saveValueStore = super().savePreset() + saveValueStore['color'] = self.color + return saveValueStore + + def pickColor(self): + RGBstring, btnStyle = pickColor() + if not RGBstring: + return + self.page.lineEdit_color.setText(RGBstring) + self.page.pushButton_color.setStyleSheet(btnStyle) + def previewRender(self): self.updateChunksize() frame = self.getPreviewFrame(self.width, self.height) @@ -53,10 +83,11 @@ class Component(Component): def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) self.updateChunksize() + w, h = scale(self.scale, self.width, self.height, str) self.video = FfmpegVideo( inputPath=self.audioFile, - filter_=makeFfmpegFilter(), - width=self.width, height=self.height, + filter_=self.makeFfmpegFilter(), + width=w, height=h, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), parent=self.parent, component=self, @@ -65,7 +96,7 @@ class Component(Component): def frameRender(self, frameNo): if FfmpegVideo.threadError is not None: raise FfmpegVideo.threadError - return finalizeFrame(self.video.frame(frameNo)) + return self.finalizeFrame(self.video.frame(frameNo)) def postFrameRender(self): closePipe(self.video.pipe) @@ -74,18 +105,25 @@ class Component(Component): inputFile = self.parent.window.lineEdit_audioFile.text() if not inputFile or not os.path.exists(inputFile): return + duration = getAudioDuration(inputFile) + if not duration: + return + startPt = duration / 3 command = [ self.core.FFMPEG_BIN, '-thread_queue_size', '512', + '-r', self.settings.value("outputFrameRate"), + '-ss', "{0:.3f}".format(startPt), '-i', inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', ] - command.extend(self.makeFfmpegFilter()) + command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) command.extend([ - '-vcodec', 'rawvideo', '-', - '-ss', '90', + '-an', + '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str), + '-codec:v', 'rawvideo', '-', '-frames:v', '1', ]) pipe = openPipe( @@ -95,45 +133,57 @@ class Component(Component): byteFrame = pipe.stdout.read(self.chunkSize) closePipe(pipe) - frame = finalizeFrame(self, byteFrame, width, height) + frame = self.finalizeFrame(byteFrame) return frame - def makeFfmpegFilter(self): + def makeFfmpegFilter(self, preview=False, startPt=0): w, h = scale(self.scale, self.width, self.height, str) + if self.amplitude == 0: + amplitude = 'lin' + elif self.amplitude == 1: + amplitude = 'log' + elif self.amplitude == 2: + amplitude = 'sqrt' + elif self.amplitude == 3: + amplitude = 'cbrt' + hexcolor = QColor(*self.color).name() + opacity = "{0:.1f}".format(self.opacity / 100) + return [ '-filter_complex', - '[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % ( - w, h, self.mode, + '[0:a] %s%s' + 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; ' + '[v1] scale=%s:%s%s [v]' % ( + 'compand=gain=2,' if self.compress else '', + 'aformat=channel_layouts=mono,' if self.mono else '', + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + str(self.page.comboBox_mode.currentText()).lower(), + hexcolor, opacity, amplitude, + ', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=4:color=%s@%s' % ( + hexcolor, opacity + ) if self.mode < 2 else '', + ', hflip' if self.mirror else'', + w, h, + ', trim=duration=%s' % "{0:.3f}".format(startPt + 1) if preview else '', ), '-map', '[v]', - '-map', '0:a', ] def updateChunksize(self): - if self.scale != 100: - width, height = scale(self.scale, self.width, self.height, int) - else: - width, height = self.width, self.height + width, height = scale(self.scale, self.width, self.height, int) self.chunkSize = 4 * width * height - -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) - if returntype == str: - return (str(math.ceil(width)), str(math.ceil(height))) - elif returntype == int: - return (math.ceil(width), math.ceil(height)) - else: - return (width, height) - - -def finalizeFrame(self, imageData, width, height): - # frombytes goes here - if self.scale != 100 \ - or self.x != 0 or self.y != 0: - frame = BlankFrame(width, height) - frame.paste(image, box=(self.x, self.y)) - else: - frame = image - return frame + def finalizeFrame(self, imageData): + image = Image.frombytes( + 'RGBA', + scale(self.scale, self.width, self.height, int), + imageData + ) + if self.scale != 100 \ + or self.x != 0 or self.y != 0: + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.x, self.y)) + else: + frame = image + return frame diff --git a/src/components/waveform.ui b/src/components/waveform.ui index 5d62150..0e40380 100644 --- a/src/components/waveform.ui +++ b/src/components/waveform.ui @@ -226,9 +226,31 @@ - + - Mirror + Opacity + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 @@ -263,6 +285,75 @@ + + + + + + Compress + + + + + + + Mono + + + + + + + Mirror + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Amplitude + + + + + + + + Linear + + + + + Logarithmic + + + + + Square root + + + + + Cubic root + + + + + + diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 128ed08..5d424e0 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,22 +6,9 @@ import string import os import sys import subprocess -import signal -import math from collections import OrderedDict -def scale(scale, width, height, returntype=None): - width = (float(width) / 100.0) * float(scale) - height = (float(height) / 100.0) * float(scale) - if returntype == str: - return (str(math.ceil(width)), str(math.ceil(height))) - elif returntype == int: - return (math.ceil(width), math.ceil(height)) - else: - return (width, height) - - def badName(name): '''Returns whether a name contains non-alphanumeric chars''' return any([letter in string.punctuation for letter in name]) @@ -69,14 +56,6 @@ def checkOutput(commandList, **kwargs): return subprocess.check_output(commandList, **kwargs) -@pipeWrapper -def openPipe(commandList, **kwargs): - return subprocess.Popen(commandList, **kwargs) - -def closePipe(pipe): - pipe.stdout.close() - pipe.send_signal(signal.SIGINT) - def disableWhenEncoding(func): def decorator(self, *args, **kwargs): if self.encoding: diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index fea9d4e..e37282f 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -6,10 +6,12 @@ import sys import os import subprocess import threading +import signal from queue import PriorityQueue import core -from toolkit.common import checkOutput, openPipe +from toolkit.common import checkOutput, pipeWrapper +from component import ComponentError class FfmpegVideo: @@ -60,7 +62,8 @@ class FfmpegVideo: kwargs['filter_'] ) self.command.extend([ - '-vcodec', 'rawvideo', '-', + '-s:v', '%sx%s' % (self.width, self.height), + '-codec:v', 'rawvideo', '-', ]) self.frameBuffer = PriorityQueue() @@ -85,9 +88,11 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): + import sys + print(self.command) self.pipe = openPipe( self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, bufsize=10**8 + stderr=sys.__stdout__, bufsize=10**8 ) while True: if self.parent.canceled: @@ -100,7 +105,7 @@ class FfmpegVideo: self.frameBuffer.put((self.frameNo-1, self.lastFrame)) continue except AttributeError: - Video.threadError = ComponentError(self.component, 'video') + FfmpegVideo.threadError = ComponentError(self.component, 'video') break self.currentFrame = self.pipe.stdout.read(self.chunkSize) @@ -109,6 +114,16 @@ class FfmpegVideo: self.lastFrame = self.currentFrame +@pipeWrapper +def openPipe(commandList, **kwargs): + return subprocess.Popen(commandList, **kwargs) + + +def closePipe(pipe): + pipe.stdout.close() + pipe.send_signal(signal.SIGINT) + + def findFfmpeg(): if getattr(sys, 'frozen', False): # The application is frozen @@ -347,7 +362,12 @@ def getAudioDuration(filename): except subprocess.CalledProcessError as ex: fileInfo = ex.output - info = fileInfo.decode("utf-8").split('\n') + try: + info = fileInfo.decode("utf-8").split('\n') + except UnicodeDecodeError as e: + print('Unicode error:', str(e)) + return False + for line in info: if 'Duration' in line: d = line.split(',')[0] diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index b66e037..f42d4c9 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -6,6 +6,7 @@ from PIL import Image from PIL.ImageQt import ImageQt import sys import os +import math import core @@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor): super().__init__(b, g, r, a) +def scale(scale, width, height, returntype=None): + width = (float(width) / 100.0) * float(scale) + height = (float(height) / 100.0) * float(scale) + if returntype == str: + return (str(math.ceil(width)), str(math.ceil(height))) + elif returntype == int: + return (math.ceil(width), math.ceil(height)) + else: + return (width, height) + + def defaultSize(framefunc): '''Makes width/height arguments optional''' def decorator(*args): diff --git a/src/video_thread.py b/src/video_thread.py index f27ec21..5963def 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -19,9 +19,11 @@ import time import signal from component import ComponentError -from toolkit import openPipe -from toolkit.ffmpeg import readAudioFile, createFfmpegCommand from toolkit.frame import Checkerboard +from toolkit.ffmpeg import ( + openPipe, readAudioFile, + getAudioDuration, createFfmpegCommand +) class Worker(QtCore.QObject): @@ -132,15 +134,24 @@ class Worker(QtCore.QObject): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ - - self.progressBarSetText.emit("Loading audio file...") - audioFileTraits = readAudioFile( - self.inputFile, self - ) - if audioFileTraits is None: - self.cancelExport() - return - self.completeAudioArray, duration = audioFileTraits + if any([ + True if 'pcm' in comp.properties() else False + for comp in self.components + ]): + self.progressBarSetText.emit("Loading audio file...") + audioFileTraits = readAudioFile( + self.inputFile, self + ) + if audioFileTraits is None: + self.cancelExport() + return + self.completeAudioArray, duration = audioFileTraits + else: + duration = getAudioDuration(self.inputFile) + class FakeList: + def __len__(self): + return int((duration * 44100) + 44100) - 1470 + self.completeAudioArray = FakeList() self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") @@ -284,7 +295,10 @@ class Worker(QtCore.QObject): numpy.seterr(all='print') - self.out_pipe.stdin.close() + try: + self.out_pipe.stdin.close() + except BrokenPipeError: + print('Broken pipe to ffmpeg!') if self.out_pipe.stderr is not None: print(self.out_pipe.stderr.read()) self.out_pipe.stderr.close()