From 231af74ea2b247bd73fcdfc44657b7fea2ab1620 Mon Sep 17 00:00:00 2001 From: DH4 Date: Tue, 6 Jun 2017 10:14:39 -0500 Subject: [PATCH] Code cleanup --- command.py | 122 ++++++++ components/__base__.py | 44 +-- components/color.py | 55 ++-- components/image.py | 24 +- components/original.py | 78 +++-- components/text.py | 23 +- components/video.py | 74 +++-- core.py | 173 ++++++----- main.py | 691 +++-------------------------------------- mainwindow.py | 586 ++++++++++++++++++++++++++++++++++ preview_thread.py | 85 +++-- video_thread.py | 68 ++-- 12 files changed, 1107 insertions(+), 916 deletions(-) create mode 100644 command.py create mode 100644 mainwindow.py diff --git a/command.py b/command.py new file mode 100644 index 0000000..a610d8c --- /dev/null +++ b/command.py @@ -0,0 +1,122 @@ +# FIXME: commandline functionality broken until we decide how to implement it +''' +class Command(QtCore.QObject): + + videoTask = QtCore.pyqtSignal(str, str, str, list) + + def __init__(self): + QtCore.QObject.__init__(self) + self.modules = [] + self.selectedComponents = [] + + import argparse + self.parser = argparse.ArgumentParser( + description='Create a visualization for an audio file') + self.parser.add_argument( + '-i', '--input', dest='input', help='input audio file', required=True) + self.parser.add_argument( + '-o', '--output', dest='output', + help='output video file', required=True) + self.parser.add_argument( + '-b', '--background', dest='bgimage', + help='background image file', required=True) + self.parser.add_argument( + '-t', '--text', dest='text', help='title text', required=True) + self.parser.add_argument( + '-f', '--font', dest='font', help='title font', required=False) + self.parser.add_argument( + '-s', '--fontsize', dest='fontsize', + help='title font size', required=False) + self.parser.add_argument( + '-c', '--textcolor', dest='textcolor', + help='title text color in r,g,b format', required=False) + self.parser.add_argument( + '-C', '--viscolor', dest='viscolor', + help='visualization color in r,g,b format', required=False) + self.parser.add_argument( + '-x', '--xposition', dest='xposition', + help='x position', required=False) + self.parser.add_argument( + '-y', '--yposition', dest='yposition', + help='y position', required=False) + self.parser.add_argument( + '-a', '--alignment', dest='alignment', + help='title alignment', required=False, + type=int, choices=[0, 1, 2]) + self.args = self.parser.parse_args() + + self.settings = QSettings('settings.ini', QSettings.IniFormat) + LoadDefaultSettings(self) + + # load colours as tuples from comma-separated strings + self.textColor = core.Core.RGBFromString( + self.settings.value("textColor", '255, 255, 255')) + self.visColor = core.Core.RGBFromString( + self.settings.value("visColor", '255, 255, 255')) + if self.args.textcolor: + self.textColor = core.Core.RGBFromString(self.args.textcolor) + if self.args.viscolor: + self.visColor = core.Core.RGBFromString(self.args.viscolor) + + # font settings + if self.args.font: + self.font = QFont(self.args.font) + else: + self.font = QFont(self.settings.value("titleFont", QFont())) + + if self.args.fontsize: + self.fontsize = int(self.args.fontsize) + else: + self.fontsize = int(self.settings.value("fontSize", 35)) + if self.args.alignment: + self.alignment = int(self.args.alignment) + else: + self.alignment = int(self.settings.value("alignment", 0)) + + if self.args.xposition: + self.textX = int(self.args.xposition) + else: + self.textX = int(self.settings.value("xPosition", 70)) + + if self.args.yposition: + self.textY = int(self.args.yposition) + else: + self.textY = int(self.settings.value("yPosition", 375)) + + ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) + + self.videoThread = QtCore.QThread(self) + self.videoWorker = video_thread.Worker(self) + + self.videoWorker.moveToThread(self.videoThread) + self.videoWorker.videoCreated.connect(self.videoCreated) + + self.videoThread.start() + self.videoTask.emit(self.args.bgimage, + self.args.text, + self.font, + self.fontsize, + self.alignment, + self.textX, + self.textY, + self.textColor, + self.visColor, + self.args.input, + self.args.output, + self.selectedComponents) + + def videoCreated(self): + self.videoThread.quit() + self.videoThread.wait() + self.cleanUp() + + def cleanUp(self): + self.settings.setValue("titleFont", self.font.toString()) + self.settings.setValue("alignment", str(self.alignment)) + self.settings.setValue("fontSize", str(self.fontsize)) + self.settings.setValue("xPosition", str(self.textX)) + self.settings.setValue("yPosition", str(self.textY)) + self.settings.setValue("visColor", '%s,%s,%s' % self.visColor) + self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) + sys.exit(0) +''' diff --git a/components/__base__.py b/components/__base__.py index f564aad..94ac6f2 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -1,5 +1,6 @@ from PyQt4 import QtGui + class Component: def __str__(self): return self.__doc__ @@ -14,7 +15,7 @@ class Component: def reset(self): self.canceled = False - + def preFrameRender(self, **kwargs): for var, value in kwargs.items(): exec('self.%s = value' % var) @@ -22,32 +23,35 @@ class Component: def pickColor(self): color = QtGui.QColorDialog.getColor() if color.isValid(): - RGBstring = '%s,%s,%s' % (str(color.red()), str(color.green()), str(color.blue())) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % color.name() + RGBstring = '%s,%s,%s' % ( + str(color.red()), str(color.green()), str(color.blue())) + btnStyle = "QPushButton{background-color: %s; outline: none;}" \ + % color.name() return RGBstring, btnStyle else: return None, None def RGBFromString(self, string): - ''' turns an RGB string like "255, 255, 255" into a tuple ''' - try: - tup = tuple([int(i) for i in string.split(',')]) - if len(tup) != 3: - raise ValueError - for i in tup: - if i > 255 or i < 0: - raise ValueError - return tup - except: - return (255, 255, 255) + ''' turns an RGB string like "255, 255, 255" into a tuple ''' + try: + tup = tuple([int(i) for i in string.split(',')]) + if len(tup) != 3: + raise ValueError + for i in tup: + if i > 255 or i < 0: + raise ValueError + return tup + except: + return (255, 255, 255) ''' ### Reference methods for creating a new component ### (Inherit from this class and define these) - + def widget(self, parent): self.parent = parent - page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'example.ui')) + page = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'example.ui')) # connect widgets signals self.page = page return page @@ -55,13 +59,13 @@ class Component: def update(self): # read widget values self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) image = Image.new("RGBA", (width, height), (0,0,0,0)) return image - + def frameRender(self, moduleNo, frameNo): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) @@ -70,10 +74,10 @@ class Component: def loadPreset(self, presetDict): # update widgets using a preset dict - + def savePreset(self): return {} - + def cancel(self): self.canceled = True diff --git a/components/color.py b/components/color.py index c2a49e2..b050fbd 100644 --- a/components/color.py +++ b/components/color.py @@ -4,31 +4,39 @@ from PyQt4.QtGui import QColor import os from . import __base__ + class Component(__base__.Component): '''Color''' def widget(self, parent): self.parent = parent - page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'color.ui')) - - self.color1 = (0,0,0) - self.color2 = (133,133,133) + page = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'color.ui')) + + self.color1 = (0, 0, 0) + self.color2 = (133, 133, 133) self.x = 0 self.y = 0 - + page.lineEdit_color1.setText('%s,%s,%s' % self.color1) page.lineEdit_color2.setText('%s,%s,%s' % self.color2) - page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.color1).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.color1).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.color2).name() + page.pushButton_color1.setStyleSheet(btnStyle) - page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.color2).name() page.pushButton_color2.setStyleSheet(btnStyle) + page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) + page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) + # disable color #2 until non-default 'fill' option gets changed page.lineEdit_color2.setDisabled(True) page.pushButton_color2.setDisabled(True) page.spinBox_x.setValue(self.x) page.spinBox_x.setValue(self.y) - + page.lineEdit_color1.textChanged.connect(self.update) page.lineEdit_color2.textChanged.connect(self.update) page.spinBox_x.valueChanged.connect(self.update) @@ -42,39 +50,44 @@ class Component(__base__.Component): self.x = self.page.spinBox_x.value() self.y = self.page.spinBox_y.value() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - + def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) return ['static'] - + def frameRender(self, moduleNo, arrayNo, frameNo): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - + def drawFrame(self, width, height): - r,g,b = self.color1 + r, g, b = self.color1 return Image.new("RGBA", (width, height), (r, g, b, 255)) def loadPreset(self, pr): self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['color1']).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color1']).name() + + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['color2']).name() + self.page.pushButton_color1.setStyleSheet(btnStyle) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['color2']).name() self.page.pushButton_color2.setStyleSheet(btnStyle) - + def savePreset(self): return { - 'color1' : self.color1, - 'color2' : self.color2, + 'color1': self.color1, + 'color2': self.color2, } - + def pickColor(self, num): RGBstring, btnStyle = super().pickColor() if not RGBstring: diff --git a/components/image.py b/components/image.py index ffbb117..f9a92ca 100644 --- a/components/image.py +++ b/components/image.py @@ -3,26 +3,28 @@ from PyQt4 import uic, QtGui, QtCore import os from . import __base__ + class Component(__base__.Component): '''Image''' def widget(self, parent): self.parent = parent self.settings = parent.settings - page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'image.ui')) + page = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'image.ui')) self.imagePath = '' self.x = 0 self.y = 0 - + page.lineEdit_image.textChanged.connect(self.update) page.pushButton_image.clicked.connect(self.pickImage) - + self.page = page return page def update(self): self.imagePath = self.page.lineEdit_image.text() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) @@ -36,9 +38,9 @@ class Component(__base__.Component): width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) return self.drawFrame(width, height) - + def drawFrame(self, width, height): - frame = Image.new("RGBA", (width, height), (0,0,0,0)) + frame = Image.new("RGBA", (width, height), (0, 0, 0, 0)) if self.imagePath and os.path.exists(self.imagePath): image = Image.open(self.imagePath) if image.size != (width, height): @@ -48,16 +50,16 @@ class Component(__base__.Component): def loadPreset(self, pr): self.page.lineEdit_image.setText(pr['image']) - + def savePreset(self): return { - 'image' : self.imagePath, + 'image': self.imagePath, } - + def pickImage(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) - filename = QtGui.QFileDialog.getOpenFileName(self.page, - "Choose Image", imgDir, "Image Files (*.jpg *.png)") + filename = QtGui.QFileDialog.getOpenFileName( + self.page, "Choose Image", imgDir, "Image Files (*.jpg *.png)") if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) diff --git a/components/original.py b/components/original.py index b0a7579..4d0e83b 100644 --- a/components/original.py +++ b/components/original.py @@ -2,7 +2,8 @@ import numpy from PIL import Image, ImageDraw from PyQt4 import uic, QtGui from PyQt4.QtGui import QColor -import os, random +import os +import random from . import __base__ import time from copy import copy @@ -12,24 +13,25 @@ class Component(__base__.Component): '''Original Audio Visualization''' def widget(self, parent): self.parent = parent - self.visColor = (255,255,255) + self.visColor = (255, 255, 255) - page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'original.ui')) + page = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'original.ui')) page.comboBox_visLayout.addItem("Classic") page.comboBox_visLayout.addItem("Split") page.comboBox_visLayout.addItem("Bottom") - #visLayoutValue = int(self.settings.value('visLayout')) page.comboBox_visLayout.setCurrentIndex(0) page.comboBox_visLayout.currentIndexChanged.connect(self.update) page.lineEdit_visColor.setText('%s,%s,%s' % self.visColor) page.pushButton_visColor.clicked.connect(lambda: self.pickColor()) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.visColor).name() + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*self.visColor).name() page.pushButton_visColor.setStyleSheet(btnStyle) page.lineEdit_visColor.textChanged.connect(self.update) self.page = page self.canceled = False return page - + def update(self): self.layout = self.page.comboBox_visLayout.currentIndex() self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text()) @@ -37,21 +39,25 @@ class Component(__base__.Component): def loadPreset(self, pr): self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor']) - btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*pr['visColor']).name() + btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + % QColor(*pr['visColor']).name() self.page.pushButton_visColor.setStyleSheet(btnStyle) self.page.comboBox_visLayout.setCurrentIndex(pr['layout']) - + def savePreset(self): - return { 'layout' : self.layout, - 'visColor' : self.visColor, - } + return { + 'layout': self.layout, + 'visColor': self.visColor, + } def previewRender(self, previewWorker): - spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16") + spectrum = numpy.fromfunction( + lambda x: 0.008*(x-128)**2, (255,), dtype="int16") width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) - return self.drawBars(width, height, spectrum, self.visColor, self.layout) - + return self.drawBars( + width, height, spectrum, self.visColor, self.layout) + def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) self.smoothConstantDown = 0.08 @@ -64,19 +70,24 @@ class Component(__base__.Component): for i in range(0, len(self.completeAudioArray), self.sampleSize): if self.canceled: break - self.lastSpectrum = self.transformData(i, self.completeAudioArray, self.sampleSize, - self.smoothConstantDown, self.smoothConstantUp, self.lastSpectrum) + self.lastSpectrum = self.transformData( + i, self.completeAudioArray, self.sampleSize, + self.smoothConstantDown, self.smoothConstantUp, + self.lastSpectrum) self.spectrumArray[i] = copy(self.lastSpectrum) progress = int(100*(i/len(self.completeAudioArray))) if progress >= 100: progress = 100 - pStr = "Analyzing audio: "+ str(progress) +'%' + pStr = "Analyzing audio: "+str(progress)+'%' self.progressBarSetText.emit(pStr) self.progressBarUpdate.emit(int(progress)) - + def frameRender(self, moduleNo, arrayNo, frameNo): - return self.drawBars(self.width, self.height, self.spectrumArray[arrayNo], self.visColor, self.layout) + return self.drawBars( + self.width, self.height, + self.spectrumArray[arrayNo], + self.visColor, self.layout) def pickColor(self): RGBstring, btnStyle = super().pickColor() @@ -85,14 +96,17 @@ class Component(__base__.Component): self.page.lineEdit_visColor.setText(RGBstring) self.page.pushButton_visColor.setStyleSheet(btnStyle) - def transformData(self, i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum): + def transformData( + self, i, completeAudioArray, sampleSize, + smoothConstantDown, smoothConstantUp, lastSpectrum): if len(completeAudioArray) < (i + sampleSize): sampleSize = len(completeAudioArray) - i window = numpy.hanning(sampleSize) data = completeAudioArray[i:i+sampleSize][::1] * window paddedSampleSize = 2048 - paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), 'constant') + paddedData = numpy.pad( + data, (0, paddedSampleSize - sampleSize), 'constant') spectrum = numpy.fft.fft(paddedData) sample_rate = 44100 frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate) @@ -106,8 +120,13 @@ class Component(__base__.Component): y[numpy.isinf(y)] = 0 if lastSpectrum is not None: - lastSpectrum[y < lastSpectrum] = y[y < lastSpectrum] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown) - lastSpectrum[y >= lastSpectrum] = y[y >= lastSpectrum] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp) + lastSpectrum[y < lastSpectrum] = \ + y[y < lastSpectrum] * smoothConstantDown + \ + lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown) + + lastSpectrum[y >= lastSpectrum] = \ + y[y >= lastSpectrum] * smoothConstantUp + \ + lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp) else: lastSpectrum = y @@ -120,7 +139,7 @@ class Component(__base__.Component): bF = width / 64 bH = bF / 2 bQ = bF / 4 - imTop = Image.new("RGBA", (width, height),(0,0,0,0)) + imTop = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(imTop) r, g, b = color color2 = (r, g, b, 125) @@ -128,12 +147,17 @@ class Component(__base__.Component): bP = height / 1200 for j in range(0, 63): - draw.rectangle((bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ - spectrum[j * 4] * bP - bH), fill=color2) - draw.rectangle((bH + bQ + j * bF, vH , bH + bQ + j * bF + bH, vH - spectrum[j * 4] * bP), fill=color) + draw.rectangle(( + bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ - + spectrum[j * 4] * bP - bH), fill=color2) + + draw.rectangle(( + bH + bQ + j * bF, vH, bH + bQ + j * bF + bH, vH - + spectrum[j * 4] * bP), fill=color) imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM) - im = Image.new("RGBA", (width, height),(0,0,0,0)) + im = Image.new("RGBA", (width, height), (0, 0, 0, 0)) if layout == 0: y = 0 - int(height/100*43) diff --git a/components/text.py b/components/text.py index 56a9502..6cdc0dd 100644 --- a/components/text.py +++ b/components/text.py @@ -25,9 +25,7 @@ class Component(__base__.Component): self.yPosition = height / 2 * 1.036 page = uic.loadUi(os.path.join( - os.path.dirname(os.path.realpath(__file__)), - 'text.ui' - )) + os.path.dirname(os.path.realpath(__file__)), 'text.ui')) page.comboBox_textAlign.addItem("Left") page.comboBox_textAlign.addItem("Middle") page.comboBox_textAlign.addItem("Right") @@ -61,7 +59,8 @@ class Component(__base__.Component): self.fontSize = self.page.spinBox_fontSize.value() self.xPosition = self.page.spinBox_xTextAlign.value() self.yPosition = self.page.spinBox_yTextAlign.value() - self.textColor = self.RGBFromString(self.page.lineEdit_textColor.text()) + self.textColor = self.RGBFromString( + self.page.lineEdit_textColor.text()) self.parent.drawPreview() def getXY(self): @@ -95,14 +94,14 @@ class Component(__base__.Component): def savePreset(self): return { - 'title': self.title, - 'titleFont': self.titleFont.toString(), - 'alignment': self.alignment, - 'fontSize': self.fontSize, - 'xPosition': self.xPosition, - 'yPosition': self.yPosition, - 'textColor': self.textColor - } + 'title': self.title, + 'titleFont': self.titleFont.toString(), + 'alignment': self.alignment, + 'fontSize': self.fontSize, + 'xPosition': self.xPosition, + 'yPosition': self.yPosition, + 'textColor': self.textColor + } def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) diff --git a/components/video.py b/components/video.py index de91264..67a96dd 100644 --- a/components/video.py +++ b/components/video.py @@ -1,12 +1,18 @@ from PIL import Image, ImageDraw from PyQt4 import uic, QtGui, QtCore -import os, subprocess, threading +import os +import subprocess +import threading from queue import PriorityQueue from . import __base__ + class Video: '''Video Component Frame-Fetcher''' - def __init__(self, ffmpeg, videoPath, width, height, frameRate, chunkSize, parent, loopVideo): + def __init__( + self, ffmpeg, videoPath, width, height, + frameRate, chunkSize, parent, loopVideo): + self.parent = parent self.chunkSize = chunkSize self.size = (width, height) @@ -27,15 +33,18 @@ class Video: '-filter:v', 'scale='+str(width)+':'+str(height), '-vcodec', 'rawvideo', '-', ] - + self.frameBuffer = PriorityQueue() self.frameBuffer.maxsize = int(frameRate) self.finishedFrames = {} - - self.thread = threading.Thread(target=self.fillBuffer, name=self.__doc__) + + self.thread = threading.Thread( + target=self.fillBuffer, + name=self.__doc__ + ) self.thread.daemon = True self.thread.start() - + def frame(self, num): while True: if num in self.finishedFrames: @@ -44,9 +53,12 @@ class Video: i, image = self.frameBuffer.get() self.finishedFrames[i] = image self.frameBuffer.task_done() - - def fillBuffer(self): - self.pipe = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + + def fillBuffer(self): + self.pipe = subprocess.Popen( + self.command, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, bufsize=10**8 + ) while True: if self.parent.canceled: break @@ -58,26 +70,29 @@ class Video: continue self.currentFrame = self.pipe.stdout.read(self.chunkSize) - #print('creating frame #%s' % str(self.frameNo)) if len(self.currentFrame) != 0: self.frameBuffer.put((self.frameNo, self.currentFrame)) self.lastFrame = self.currentFrame + class Component(__base__.Component): '''Video''' def widget(self, parent): self.parent = parent self.settings = parent.settings - page = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'video.ui')) + page = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'video.ui' + )) self.videoPath = '' self.x = 0 self.y = 0 self.loopVideo = False - + page.lineEdit_video.textChanged.connect(self.update) page.pushButton_video.clicked.connect(self.pickVideo) page.checkBox_loop.stateChanged.connect(self.update) - + self.page = page return page @@ -85,46 +100,50 @@ class Component(__base__.Component): self.videoPath = self.page.lineEdit_video.text() self.loopVideo = self.page.checkBox_loop.isChecked() self.parent.drawPreview() - + def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) self.chunkSize = 4*width*height frame = self.getPreviewFrame(width, height) if not frame: - return Image.new("RGBA", (width, height),(0,0,0,0)) + return Image.new("RGBA", (width, height), (0, 0, 0, 0)) else: return frame - + def preFrameRender(self, **kwargs): super().preFrameRender(**kwargs) width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) self.chunkSize = 4*width*height - self.video = Video(self.parent.core.FFMPEG_BIN, self.videoPath, + self.video = Video( + self.parent.core.FFMPEG_BIN, self.videoPath, width, height, self.settings.value("outputFrameRate"), - self.chunkSize, self.parent, self.loopVideo) - + self.chunkSize, self.parent, self.loopVideo + ) + def frameRender(self, moduleNo, arrayNo, frameNo): return self.video.frame(frameNo) def loadPreset(self, pr): self.page.lineEdit_video.setText(pr['video']) - + def savePreset(self): return { - 'video' : self.videoPath, + 'video': self.videoPath, } - + def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) - filename = QtGui.QFileDialog.getOpenFileName(self.page, - "Choose Video", imgDir, "Video Files (*.mp4 *.mov)") + filename = QtGui.QFileDialog.getOpenFileName( + self.page, "Choose Video", + imgDir, "Video Files (*.mp4 *.mov)" + ) if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) self.update() - + def getPreviewFrame(self, width, height): if not self.videoPath or not os.path.exists(self.videoPath): return @@ -139,7 +158,10 @@ class Component(__base__.Component): '-ss', '90', '-vframes', '1', ] - pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8) + pipe = subprocess.Popen( + command, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, bufsize=10**8 + ) byteFrame = pipe.stdout.read(self.chunkSize) image = Image.frombytes('RGBA', (width, height), byteFrame) pipe.stdout.close() diff --git a/core.py b/core.py index 99403f1..8ea884b 100644 --- a/core.py +++ b/core.py @@ -1,4 +1,6 @@ -import sys, io, os +import sys +import io +import os from PyQt4 import QtCore, QtGui, uic from os.path import expanduser import subprocess as sp @@ -10,103 +12,106 @@ import atexit import time from collections import OrderedDict + class Core(): - def __init__(self): - self.FFMPEG_BIN = self.findFfmpeg() - self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data') - if not os.path.exists(self.tempDir): - os.makedirs(self.tempDir) - atexit.register(self.deleteTempDir) + def __init__(self): + self.FFMPEG_BIN = self.findFfmpeg() + self.tempDir = os.path.join( + tempfile.gettempdir(), 'audio-visualizer-python-data') + if not os.path.exists(self.tempDir): + os.makedirs(self.tempDir) + atexit.register(self.deleteTempDir) - def findFfmpeg(self): - if sys.platform == "win32": - return "ffmpeg.exe" - else: - try: - with open(os.devnull, "w") as f: - sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f) - return "ffmpeg" - except: - return "avconv" + def findFfmpeg(self): + if sys.platform == "win32": + return "ffmpeg.exe" + else: + try: + with open(os.devnull, "w") as f: + sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f) + return "ffmpeg" + except: + return "avconv" - def readAudioFile(self, filename, parent): - command = [ self.FFMPEG_BIN, - '-i', filename] + def readAudioFile(self, filename, parent): + command = [self.FFMPEG_BIN, '-i', filename] - try: - fileInfo = sp.check_output(command, stderr=sp.STDOUT, shell=False) - except sp.CalledProcessError as ex: - fileInfo = ex.output - pass + try: + fileInfo = sp.check_output(command, stderr=sp.STDOUT, shell=False) + except sp.CalledProcessError as ex: + fileInfo = ex.output + pass - info = fileInfo.decode("utf-8").split('\n') - for line in info: - if 'Duration' in line: - d = line.split(',')[0] - d = d.split(' ')[3] - d = d.split(':') - duration = float(d[0])*3600 + float(d[1])*60 + float(d[2]) + info = fileInfo.decode("utf-8").split('\n') + for line in info: + if 'Duration' in line: + d = line.split(',')[0] + d = d.split(' ')[3] + d = d.split(':') + duration = float(d[0])*3600 + float(d[1])*60 + float(d[2]) - command = [ self.FFMPEG_BIN, - '-i', filename, - '-f', 's16le', - '-acodec', 'pcm_s16le', - '-ar', '44100', # ouput will have 44100 Hz - '-ac', '1', # mono (set to '2' for stereo) - '-'] - in_pipe = sp.Popen(command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8) - - completeAudioArray = numpy.empty(0, dtype="int16") + command = [ + self.FFMPEG_BIN, + '-i', filename, + '-f', 's16le', + '-acodec', 'pcm_s16le', + '-ar', '44100', # ouput will have 44100 Hz + '-ac', '1', # mono (set to '2' for stereo) + '-'] + in_pipe = sp.Popen( + command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8) - progress = 0 - lastPercent = None - while True: - if self.canceled: - break - # read 2 seconds of audio - progress = progress + 4 - raw_audio = in_pipe.stdout.read(88200*4) - if len(raw_audio) == 0: - break - audio_array = numpy.fromstring(raw_audio, dtype="int16") - completeAudioArray = numpy.append(completeAudioArray, audio_array) + completeAudioArray = numpy.empty(0, dtype="int16") - percent = int(100*(progress/duration)) - if percent >= 100: - percent = 100 + progress = 0 + lastPercent = None + while True: + if self.canceled: + break + # read 2 seconds of audio + progress = progress + 4 + raw_audio = in_pipe.stdout.read(88200*4) + if len(raw_audio) == 0: + break + audio_array = numpy.fromstring(raw_audio, dtype="int16") + completeAudioArray = numpy.append(completeAudioArray, audio_array) - if lastPercent != percent: - string = 'Loading audio file: '+str(percent)+'%' - parent.progressBarSetText.emit(string) - parent.progressBarUpdate.emit(percent) + percent = int(100*(progress/duration)) + if percent >= 100: + percent = 100 - lastPercent = percent - + if lastPercent != percent: + string = 'Loading audio file: '+str(percent)+'%' + parent.progressBarSetText.emit(string) + parent.progressBarUpdate.emit(percent) - in_pipe.kill() - in_pipe.wait() + lastPercent = percent - # add 0s the end - completeAudioArrayCopy = numpy.zeros(len(completeAudioArray) + 44100, dtype="int16") - completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray - completeAudioArray = completeAudioArrayCopy + in_pipe.kill() + in_pipe.wait() - return completeAudioArray + # add 0s the end + completeAudioArrayCopy = numpy.zeros( + len(completeAudioArray) + 44100, dtype="int16") + completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray + completeAudioArray = completeAudioArrayCopy - def deleteTempDir(self): - try: - rmtree(self.tempDir) - except FileNotFoundError: - pass + return completeAudioArray - def cancel(self): - self.canceled = True + def deleteTempDir(self): + try: + rmtree(self.tempDir) + except FileNotFoundError: + pass - def reset(self): - self.canceled = False - - @staticmethod - def stringOrderedDict(dictionary): - sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) - return repr(sorted_) + def cancel(self): + self.canceled = True + + def reset(self): + self.canceled = False + + @staticmethod + def stringOrderedDict(dictionary): + sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) + return repr(sorted_) diff --git a/main.py b/main.py index c75a7f7..78c1d9b 100644 --- a/main.py +++ b/main.py @@ -1,668 +1,67 @@ -import sys, io, os, shutil, atexit, string, signal, filecmp, time -from os.path import expanduser -from queue import Queue from importlib import import_module from collections import OrderedDict -from PyQt4 import QtCore, QtGui, uic -from PyQt4.QtCore import QSettings, QModelIndex, Qt -from PyQt4.QtGui import QDesktopServices, QMenu +from PyQt4 import QtGui, uic +from PyQt4.QtCore import Qt +import sys +import io +import os +import atexit +import signal -import preview_thread, core, video_thread +import core +import preview_thread +import video_thread +from mainwindow import * -# FIXME: commandline functionality broken until we decide how to implement it -''' -class Command(QtCore.QObject): - - videoTask = QtCore.pyqtSignal(str, str, str, list) - - def __init__(self): - QtCore.QObject.__init__(self) - self.modules = [] - self.selectedComponents = [] - - import argparse - self.parser = argparse.ArgumentParser(description='Create a visualization for an audio file') - self.parser.add_argument('-i', '--input', dest='input', help='input audio file', required=True) - self.parser.add_argument('-o', '--output', dest='output', help='output video file', required=True) - self.parser.add_argument('-b', '--background', dest='bgimage', help='background image file', required=True) - self.parser.add_argument('-t', '--text', dest='text', help='title text', required=True) - self.parser.add_argument('-f', '--font', dest='font', help='title font', required=False) - self.parser.add_argument('-s', '--fontsize', dest='fontsize', help='title font size', required=False) - self.parser.add_argument('-c', '--textcolor', dest='textcolor', help='title text color in r,g,b format', required=False) - self.parser.add_argument('-C', '--viscolor', dest='viscolor', help='visualization color in r,g,b format', required=False) - self.parser.add_argument('-x', '--xposition', dest='xposition', help='x position', required=False) - self.parser.add_argument('-y', '--yposition', dest='yposition', help='y position', required=False) - self.parser.add_argument('-a', '--alignment', dest='alignment', help='title alignment', required=False, type=int, choices=[0, 1, 2]) - self.args = self.parser.parse_args() - - self.settings = QSettings('settings.ini', QSettings.IniFormat) - LoadDefaultSettings(self) - - # load colours as tuples from comma-separated strings - self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255')) - self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255')) - if self.args.textcolor: - self.textColor = core.Core.RGBFromString(self.args.textcolor) - if self.args.viscolor: - self.visColor = core.Core.RGBFromString(self.args.viscolor) - - # font settings - if self.args.font: - self.font = QFont(self.args.font) - else: - self.font = QFont(self.settings.value("titleFont", QFont())) - - if self.args.fontsize: - self.fontsize = int(self.args.fontsize) - else: - self.fontsize = int(self.settings.value("fontSize", 35)) - if self.args.alignment: - self.alignment = int(self.args.alignment) - else: - self.alignment = int(self.settings.value("alignment", 0)) - - if self.args.xposition: - self.textX = int(self.args.xposition) - else: - self.textX = int(self.settings.value("xPosition", 70)) - - if self.args.yposition: - self.textY = int(self.args.yposition) - else: - self.textY = int(self.settings.value("yPosition", 375)) - - ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - - self.videoThread = QtCore.QThread(self) - self.videoWorker = video_thread.Worker(self) - - self.videoWorker.moveToThread(self.videoThread) - self.videoWorker.videoCreated.connect(self.videoCreated) - - self.videoThread.start() - self.videoTask.emit(self.args.bgimage, - self.args.text, - self.font, - self.fontsize, - self.alignment, - self.textX, - self.textY, - self.textColor, - self.visColor, - self.args.input, - self.args.output, - self.selectedComponents) - - def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() - self.cleanUp() - - def cleanUp(self): - self.settings.setValue("titleFont", self.font.toString()) - self.settings.setValue("alignment", str(self.alignment)) - self.settings.setValue("fontSize", str(self.fontsize)) - self.settings.setValue("xPosition", str(self.textX)) - self.settings.setValue("yPosition", str(self.textY)) - self.settings.setValue("visColor", '%s,%s,%s' % self.visColor) - self.settings.setValue("textColor", '%s,%s,%s' % self.textColor) - sys.exit(0) -''' - -class PreviewWindow(QtGui.QLabel): - def __init__(self, parent, img): - super(PreviewWindow, self).__init__() - self.parent = parent - self.setFrameStyle(QtGui.QFrame.StyledPanel) - self.pixmap = QtGui.QPixmap(img) - - def paintEvent(self, event): - size = self.size() - painter = QtGui.QPainter(self) - point = QtCore.QPoint(0,0) - scaledPix = self.pixmap.scaled(size, Qt.KeepAspectRatio, transformMode = Qt.SmoothTransformation) - # start painting the label from left upper corner - point.setX((size.width() - scaledPix.width())/2) - point.setY((size.height() - scaledPix.height())/2) - #print point.x(), ' ', point.y() - painter.drawPixmap(point, scaledPix) - - def changePixmap(self, img): - self.pixmap = QtGui.QPixmap(img) - self.repaint() - -class Main(QtCore.QObject): - - newTask = QtCore.pyqtSignal(list) - processTask = QtCore.pyqtSignal() - videoTask = QtCore.pyqtSignal(str, str, list) - - def __init__(self, window): - QtCore.QObject.__init__(self) - - # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) - self.window = window - self.core = core.Core() - self.pages = [] - self.selectedComponents = [] - self.lastAutosave = time.time() - - # create data directory, load/create settings - self.dataDir = QDesktopServices.storageLocation(QDesktopServices.DataLocation) - self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') - self.presetDir = os.path.join(self.dataDir, 'presets') - self.settings = QSettings(os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) - LoadDefaultSettings(self) - if not os.path.exists(self.dataDir): - os.makedirs(self.dataDir) - for neededDirectory in (self.presetDir, self.settings.value("projectDir")): - if not os.path.exists(neededDirectory): - os.mkdir(neededDirectory) - - # - self.previewQueue = Queue() - self.previewThread = QtCore.QThread(self) - self.previewWorker = preview_thread.Worker(self, self.previewQueue) - self.previewWorker.moveToThread(self.previewThread) - self.previewWorker.imageCreated.connect(self.showPreviewImage) - self.previewThread.start() - - self.timer = QtCore.QTimer(self) - self.timer.timeout.connect(self.processTask.emit) - self.timer.start(500) - - # begin decorating the window and connecting events - window.toolButton_selectAudioFile.clicked.connect(self.openInputFileDialog) - window.toolButton_selectOutputFile.clicked.connect(self.openOutputFileDialog) - window.progressBar_createVideo.setValue(0) - window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation) - window.pushButton_Cancel.clicked.connect(self.stopVideo) - window.setWindowTitle("Audio Visualizer") - - self.previewWindow = PreviewWindow(self, os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.png")) - window.verticalLayout_previewWrapper.addWidget(self.previewWindow) - - self.modules = self.findComponents() - self.compMenu = QMenu() - for i, comp in enumerate(self.modules): - action = self.compMenu.addAction(comp.Component.__doc__) - action.triggered[()].connect( lambda item=i: self.insertComponent(item)) - - self.window.pushButton_addComponent.setMenu(self.compMenu) - window.listWidget_componentList.clicked.connect(lambda _: self.changeComponentWidget()) - - self.window.pushButton_removeComponent.clicked.connect(lambda _: self.removeComponent()) - - currentRes = str(self.settings.value('outputWidth'))+'x'+str(self.settings.value('outputHeight')) - for i, res in enumerate(self.resolutions): - window.comboBox_resolution.addItem(res) - if res == currentRes: - currentRes = i - window.comboBox_resolution.setCurrentIndex(currentRes) - window.comboBox_resolution.currentIndexChanged.connect(self.updateResolution) - - self.window.pushButton_listMoveUp.clicked.connect(self.moveComponentUp) - self.window.pushButton_listMoveDown.clicked.connect(self.moveComponentDown) - - self.window.pushButton_savePreset.clicked.connect(self.openSavePresetDialog) - self.window.comboBox_openPreset.currentIndexChanged.connect(self.openPreset) - self.window.pushButton_saveAs.clicked.connect(self.openSaveProjectDialog) - self.window.pushButton_saveProject.clicked.connect(self.saveCurrentProject) - self.window.pushButton_openProject.clicked.connect(self.openOpenProjectDialog) - - # show the window and load current project - window.show() - self.currentProject = self.settings.value("currentProject") - if self.currentProject and os.path.exists(self.autosavePath) \ - and filecmp.cmp(self.autosavePath, self.currentProject): - # delete autosave if it's identical to the project - os.remove(self.autosavePath) - - if self.currentProject and os.path.exists(self.autosavePath): - ch = self.showMessage("Restore unsaved changes in project '%s'?" % os.path.basename(self.currentProject)[:-4], True) - if ch: - os.remove(self.currentProject) - os.rename(self.autosavePath, self.currentProject) - else: - os.remove(self.autosavePath) - - self.openProject(self.currentProject) - self.drawPreview() - - def cleanUp(self): - self.timer.stop() - self.previewThread.quit() - self.previewThread.wait() - self.autosave() - - def autosave(self): - if time.time() - self.lastAutosave >= 1.0: - if os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - self.createProjectFile(self.autosavePath) - self.lastAutosave = time.time() - - def openInputFileDialog(self): - inputDir = self.settings.value("inputDir", expanduser("~")) - - fileName = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)"); - - if not fileName == "": - self.settings.setValue("inputDir", os.path.dirname(fileName)) - self.window.lineEdit_audioFile.setText(fileName) - - def openOutputFileDialog(self): - outputDir = self.settings.value("outputDir", expanduser("~")) - - fileName = QtGui.QFileDialog.getSaveFileName(self.window, - "Set Output Video File", outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)"); - - if not fileName == "": - self.settings.setValue("outputDir", os.path.dirname(fileName)) - self.window.lineEdit_outputFile.setText(fileName) - - def stopVideo(self): - print('stop') - self.videoWorker.cancel() - self.canceled = True - - def createAudioVisualisation(self): - # create output video if mandatory settings are filled in - if self.window.lineEdit_audioFile.text() and self.window.lineEdit_outputFile.text(): - self.canceled = False - self.progressBarUpdated(-1) - ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) - self.videoThread = QtCore.QThread(self) - self.videoWorker = video_thread.Worker(self) - self.videoWorker.moveToThread(self.videoThread) - self.videoWorker.videoCreated.connect(self.videoCreated) - self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) - self.videoWorker.progressBarSetText.connect(self.progressBarSetText) - self.videoWorker.imageCreated.connect(self.showPreviewImage) - self.videoWorker.encoding.connect(self.changeEncodingStatus) - self.videoThread.start() - self.videoTask.emit(self.window.lineEdit_audioFile.text(), - self.window.lineEdit_outputFile.text(), - self.selectedComponents) - else: - self.showMessage("You must select an audio file and output filename.") - - def progressBarUpdated(self, value): - self.window.progressBar_createVideo.setValue(value) - - def changeEncodingStatus(self, status): - if status: - self.window.pushButton_createVideo.setEnabled(False) - self.window.pushButton_Cancel.setEnabled(True) - self.window.comboBox_resolution.setEnabled(False) - self.window.stackedWidget.setEnabled(False) - self.window.tab_encoderSettings.setEnabled(False) - self.window.label_audioFile.setEnabled(False) - self.window.toolButton_selectAudioFile.setEnabled(False) - self.window.label_outputFile.setEnabled(False) - self.window.toolButton_selectOutputFile.setEnabled(False) - self.window.lineEdit_audioFile.setEnabled(False) - self.window.lineEdit_outputFile.setEnabled(False) - self.window.pushButton_addComponent.setEnabled(False) - self.window.pushButton_removeComponent.setEnabled(False) - self.window.pushButton_listMoveDown.setEnabled(False) - self.window.pushButton_listMoveUp.setEnabled(False) - self.window.comboBox_openPreset.setEnabled(False) - self.window.pushButton_removePreset.setEnabled(False) - self.window.pushButton_savePreset.setEnabled(False) - self.window.pushButton_openProject.setEnabled(False) - self.window.listWidget_componentList.setEnabled(False) - else: - self.window.pushButton_createVideo.setEnabled(True) - self.window.pushButton_Cancel.setEnabled(False) - self.window.comboBox_resolution.setEnabled(True) - self.window.stackedWidget.setEnabled(True) - self.window.tab_encoderSettings.setEnabled(True) - self.window.label_audioFile.setEnabled(True) - self.window.toolButton_selectAudioFile.setEnabled(True) - self.window.lineEdit_audioFile.setEnabled(True) - self.window.label_outputFile.setEnabled(True) - self.window.toolButton_selectOutputFile.setEnabled(True) - self.window.lineEdit_outputFile.setEnabled(True) - self.window.pushButton_addComponent.setEnabled(True) - self.window.pushButton_removeComponent.setEnabled(True) - self.window.pushButton_listMoveDown.setEnabled(True) - self.window.pushButton_listMoveUp.setEnabled(True) - self.window.comboBox_openPreset.setEnabled(True) - self.window.pushButton_removePreset.setEnabled(True) - self.window.pushButton_savePreset.setEnabled(True) - self.window.pushButton_openProject.setEnabled(True) - self.window.listWidget_componentList.setEnabled(True) - - def progressBarSetText(self, value): - self.window.progressBar_createVideo.setFormat(value) - - def videoCreated(self): - self.videoThread.quit() - self.videoThread.wait() - - def updateResolution(self): - resIndex = int(window.comboBox_resolution.currentIndex()) - res = self.resolutions[resIndex].split('x') - self.settings.setValue('outputWidth',res[0]) - self.settings.setValue('outputHeight',res[1]) - self.drawPreview() - - def drawPreview(self): - self.newTask.emit(self.selectedComponents) - # self.processTask.emit() - self.autosave() - - def showPreviewImage(self, image): - self.previewWindow.changePixmap(image) - - def findComponents(self): - def findComponents(): - srcPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'components') - if os.path.exists(srcPath): - for f in sorted(os.listdir(srcPath)): - name, ext = os.path.splitext(f) - if name.startswith("__"): - continue - elif ext == '.py': - yield name - return [import_module('components.%s' % name) for name in findComponents()] - - def addComponent(self, moduleIndex): - index = len(self.pages) - self.selectedComponents.append(self.modules[moduleIndex].Component()) - self.window.listWidget_componentList.addItem(self.selectedComponents[-1].__doc__) - self.pages.append(self.selectedComponents[-1].widget(self)) - self.window.listWidget_componentList.setCurrentRow(index) - self.window.stackedWidget.addWidget(self.pages[-1]) - self.window.stackedWidget.setCurrentIndex(index) - self.selectedComponents[-1].update() - self.updateOpenPresetComboBox(self.selectedComponents[-1]) - - def insertComponent(self, moduleIndex): - self.selectedComponents.insert(0, self.modules[moduleIndex].Component()) - self.window.listWidget_componentList.insertItem(0, self.selectedComponents[0].__doc__) - self.pages.insert(0, self.selectedComponents[0].widget(self)) - self.window.listWidget_componentList.setCurrentRow(0) - self.window.stackedWidget.insertWidget(0, self.pages[0]) - self.window.stackedWidget.setCurrentIndex(0) - self.selectedComponents[0].update() - self.updateOpenPresetComboBox(self.selectedComponents[0]) - - def removeComponent(self): - for selected in self.window.listWidget_componentList.selectedItems(): - index = self.window.listWidget_componentList.row(selected) - self.window.stackedWidget.removeWidget(self.pages[index]) - self.window.listWidget_componentList.takeItem(index) - self.selectedComponents.pop(index) - self.pages.pop(index) - self.changeComponentWidget() - self.drawPreview() - - def changeComponentWidget(self): - selected = self.window.listWidget_componentList.selectedItems() - if selected: - index = self.window.listWidget_componentList.row(selected[0]) - self.window.stackedWidget.setCurrentIndex(index) - self.updateOpenPresetComboBox(self.selectedComponents[index]) - - def moveComponentUp(self): - row = self.window.listWidget_componentList.currentRow() - if row > 0: - module = self.selectedComponents[row] - self.selectedComponents.pop(row) - self.selectedComponents.insert(row - 1,module) - page = self.pages[row] - self.pages.pop(row) - self.pages.insert(row - 1, page) - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row - 1, item) - widget = self.window.stackedWidget.removeWidget(page) - self.window.stackedWidget.insertWidget(row - 1, page) - self.window.listWidget_componentList.setCurrentRow(row - 1) - self.window.stackedWidget.setCurrentIndex(row -1) - self.drawPreview() - - def moveComponentDown(self): - row = self.window.listWidget_componentList.currentRow() - if row != -1 and row < len(self.pages)+1: - module = self.selectedComponents[row] - self.selectedComponents.pop(row) - self.selectedComponents.insert(row + 1,module) - page = self.pages[row] - self.pages.pop(row) - self.pages.insert(row + 1, page) - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row + 1, item) - widget = self.window.stackedWidget.removeWidget(page) - self.window.stackedWidget.insertWidget(row + 1, page) - self.window.listWidget_componentList.setCurrentRow(row + 1) - self.window.stackedWidget.setCurrentIndex(row + 1) - self.drawPreview() - - def updateOpenPresetComboBox(self, component): - self.window.comboBox_openPreset.clear() - self.window.comboBox_openPreset.addItem("Component Presets") - destination = os.path.join(self.presetDir, - str(component).strip(), str(component.version())) - if not os.path.exists(destination): - os.makedirs(destination) - for f in os.listdir(destination): - self.window.comboBox_openPreset.addItem(f) - - def openSavePresetDialog(self): - if self.window.listWidget_componentList.currentRow() == -1: - return - while True: - newName, OK = QtGui.QInputDialog.getText(QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') - badName = False - for letter in newName: - if letter in string.punctuation: - badName = True - if badName: - # some filesystems don't like bizarre characters - self.showMessage("Preset names must contain only letters, numbers, and spaces.") - continue - if OK and newName: - index = self.window.listWidget_componentList.currentRow() - if index != -1: - saveValueStore = self.selectedComponents[index].savePreset() - componentName = str(self.selectedComponents[index]).strip() - vers = self.selectedComponents[index].version() - self.createPresetFile(componentName, vers, saveValueStore, newName) - break - - def createPresetFile(self, componentName, version, saveValueStore, filename): - dirname = os.path.join(self.presetDir, componentName, str(version)) - if not os.path.exists(dirname): - os.makedirs(dirname) - filepath = os.path.join(dirname, filename) - if os.path.exists(filepath): - ch = self.showMessage("%s already exists! Overwrite it?" % filename, True, QtGui.QMessageBox.Warning) - if not ch: - return - # remove old copies of the preset - for i in range(0, self.window.comboBox_openPreset.count()): - if self.window.comboBox_openPreset.itemText(i) == filename: - self.window.comboBox_openPreset.removeItem(i) - with open(filepath, 'w') as f: - f.write(core.Core.stringOrderedDict(saveValueStore)) - self.window.comboBox_openPreset.addItem(filename) - self.window.comboBox_openPreset.setCurrentIndex(self.window.comboBox_openPreset.count()-1) - - def openPreset(self): - if self.window.comboBox_openPreset.currentIndex() < 1: - return - index = self.window.listWidget_componentList.currentRow() - if index == -1: - return - filename = self.window.comboBox_openPreset.itemText(self.window.comboBox_openPreset.currentIndex()) - componentName = str(self.selectedComponents[index]).strip() - version = self.selectedComponents[index].version() - dirname = os.path.join(self.presetDir, componentName, str(version)) - filepath = os.path.join(dirname, filename) - if not os.path.exists(filepath): - self.window.comboBox_openPreset.removeItem(self.window.comboBox_openPreset.currentIndex()) - return - with open(filepath, 'r') as f: - for line in f: - saveValueStore = dict(eval(line.strip())) - break - self.selectedComponents[index].loadPreset(saveValueStore) - self.drawPreview() - - def saveCurrentProject(self): - if self.currentProject: - self.createProjectFile(self.currentProject) - else: - self.openSaveProjectDialog() - - def openSaveProjectDialog(self): - filename = QtGui.QFileDialog.getSaveFileName(self.window, - "Create Project File", self.settings.value("projectDir"), - "Project Files (*.avp)") - if not filename: - return - self.createProjectFile(filename) - - def createProjectFile(self, filepath): - if not filepath.endswith(".avp"): - filepath += '.avp' - with open(filepath, 'w') as f: - print('creating %s' % filepath) - f.write('[Components]\n') - for comp in self.selectedComponents: - saveValueStore = comp.savePreset() - f.write('%s\n' % str(comp)) - f.write('%s\n' % str(comp.version())) - f.write('%s\n' % core.Core.stringOrderedDict(saveValueStore)) - if filepath != self.autosavePath: - self.settings.setValue("projectDir", os.path.dirname(filepath)) - self.settings.setValue("currentProject", filepath) - self.currentProject = filepath - - def openOpenProjectDialog(self): - filename = QtGui.QFileDialog.getOpenFileName(self.window, - "Open Project File", self.settings.value("projectDir"), - "Project Files (*.avp)") - self.openProject(filename) - - def openProject(self, filepath): - if not filepath or not os.path.exists(filepath) or not filepath.endswith('.avp'): - return - self.clear() - self.currentProject = filepath - self.settings.setValue("currentProject", filepath) - self.settings.setValue("projectDir", os.path.dirname(filepath)) - compNames = [mod.Component.__doc__ for mod in self.modules] - try: - with open(filepath, 'r') as f: - validSections = ('Components') - section = '' - def parseLine(line): - line = line.strip() - newSection = '' - if line.startswith('[') and line.endswith(']') and line[1:-1] in validSections: - newSection = line[1:-1] - return line, newSection - - i = 0 - for line in f: - line, newSection = parseLine(line) - if newSection: - section = str(newSection) - continue - if line and section == 'Components': - if i == 0: - compIndex = compNames.index(line) - self.addComponent(compIndex) - i += 1 - elif i == 1: - # version, not used yet - i += 1 - elif i == 2: - saveValueStore = dict(eval(line)) - self.selectedComponents[-1].loadPreset(saveValueStore) - i = 0 - except (IndexError, ValueError, KeyError, NameError, SyntaxError, AttributeError, TypeError) as e: - self.clear() - typ, value, _ = sys.exc_info() - msg = '%s: %s' % (typ.__name__, value) - self.showMessage("Project file '%s' is corrupted." % filepath, False, - QtGui.QMessageBox.Warning, msg) - - def showMessage(self, string, showCancel=False, icon=QtGui.QMessageBox.Information, detail=None): - msg = QtGui.QMessageBox() - msg.setIcon(icon) - msg.setText(string) - msg.setDetailedText(detail) - if showCancel: - msg.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) - else: - msg.setStandardButtons(QtGui.QMessageBox.Ok) - ch = msg.exec_() - if ch == 1024: - return True - return False - - def clear(self): - ''' empty out all components and fields, get a blank slate ''' - self.selectedComponents = [] - self.window.listWidget_componentList.clear() - for widget in self.pages: - self.window.stackedWidget.removeWidget(widget) - self.pages = [] def LoadDefaultSettings(self): - self.resolutions = [ - '1920x1080', - '1280x720', - '854x480' + self.resolutions = [ + '1920x1080', + '1280x720', + '854x480' ] - default = { - "outputWidth": 1280, - "outputHeight": 720, - "outputFrameRate": 30, - "outputAudioCodec": "aac", - "outputAudioBitrate": "192k", - "outputVideoCodec": "libx264", - "outputVideoFormat": "yuv420p", - "outputPreset": "medium", - "outputFormat": "mp4", - "projectDir" : os.path.join(self.dataDir, 'projects'), - } - - for parm, value in default.items(): - if self.settings.value(parm) == None: - self.settings.setValue(parm,value) + default = { + "outputWidth": 1280, + "outputHeight": 720, + "outputFrameRate": 30, + "outputAudioCodec": "aac", + "outputAudioBitrate": "192k", + "outputVideoCodec": "libx264", + "outputVideoFormat": "yuv420p", + "outputPreset": "medium", + "outputFormat": "mp4", + "projectDir": os.path.join(self.dataDir, 'projects'), + } + for parm, value in default.items(): + if self.settings.value(parm) is None: + self.settings.setValue(parm, value) -''' ####### commandline functionality broken until we decide how to implement it -if len(sys.argv) > 1: - # command line mode - app = QtGui.QApplication(sys.argv, False) - command = Command() - signal.signal(signal.SIGINT, command.cleanUp) - sys.exit(app.exec_()) -else: -''' -# gui mode if __name__ == "__main__": + ''' FIXME commandline functionality broken until we decide how to implement + if len(sys.argv) > 1: + # command line mode + app = QtGui.QApplication(sys.argv, False) + command = Command() + signal.signal(signal.SIGINT, command.cleanUp) + sys.exit(app.exec_()) + else: + ''' app = QtGui.QApplication(sys.argv) app.setApplicationName("audio-visualizer") app.setOrganizationName("audio-visualizer") - window = uic.loadUi(os.path.join(os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) + window = uic.loadUi(os.path.join( + os.path.dirname(os.path.realpath(__file__)), "mainwindow.ui")) # window.adjustSize() desc = QtGui.QDesktopWidget() dpi = desc.physicalDpiX() - + topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96)) window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96)) - #window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) - - main = Main(window) + # window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0) + + main = MainWindow(window) signal.signal(signal.SIGINT, main.cleanUp) atexit.register(main.cleanUp) diff --git a/mainwindow.py b/mainwindow.py new file mode 100644 index 0000000..b779298 --- /dev/null +++ b/mainwindow.py @@ -0,0 +1,586 @@ +from os.path import expanduser +from queue import Queue +from importlib import import_module +from collections import OrderedDict +from PyQt4 import QtCore, QtGui +from PyQt4.QtCore import QSettings, Qt +from PyQt4.QtGui import QDesktopServices, QMenu +import sys +import io +import os +import string +import signal +import filecmp +import time + +import core +import preview_thread +import video_thread +from main import LoadDefaultSettings + + +class PreviewWindow(QtGui.QLabel): + def __init__(self, parent, img): + super(PreviewWindow, self).__init__() + self.parent = parent + self.setFrameStyle(QtGui.QFrame.StyledPanel) + self.pixmap = QtGui.QPixmap(img) + + def paintEvent(self, event): + size = self.size() + painter = QtGui.QPainter(self) + point = QtCore.QPoint(0, 0) + scaledPix = self.pixmap.scaled( + size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation) + + # start painting the label from left upper corner + point.setX((size.width() - scaledPix.width())/2) + point.setY((size.height() - scaledPix.height())/2) + painter.drawPixmap(point, scaledPix) + + def changePixmap(self, img): + self.pixmap = QtGui.QPixmap(img) + self.repaint() + + +class MainWindow(QtCore.QObject): + + newTask = QtCore.pyqtSignal(list) + processTask = QtCore.pyqtSignal() + videoTask = QtCore.pyqtSignal(str, str, list) + + def __init__(self, window): + QtCore.QObject.__init__(self) + + # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) + self.window = window + self.core = core.Core() + self.pages = [] + self.selectedComponents = [] + self.lastAutosave = time.time() + + # create data directory, load/create settings + self.dataDir = QDesktopServices.storageLocation( + QDesktopServices.DataLocation) + self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') + self.presetDir = os.path.join(self.dataDir, 'presets') + self.settings = QSettings( + os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) + LoadDefaultSettings(self) + if not os.path.exists(self.dataDir): + os.makedirs(self.dataDir) + for neededDirectory in ( + self.presetDir, self.settings.value("projectDir")): + if not os.path.exists(neededDirectory): + os.mkdir(neededDirectory) + + # + self.previewQueue = Queue() + self.previewThread = QtCore.QThread(self) + self.previewWorker = preview_thread.Worker(self, self.previewQueue) + self.previewWorker.moveToThread(self.previewThread) + self.previewWorker.imageCreated.connect(self.showPreviewImage) + self.previewThread.start() + + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(self.processTask.emit) + self.timer.start(500) + + # begin decorating the window and connecting events + window.toolButton_selectAudioFile.clicked.connect( + self.openInputFileDialog) + + window.toolButton_selectOutputFile.clicked.connect( + self.openOutputFileDialog) + + window.progressBar_createVideo.setValue(0) + + window.pushButton_createVideo.clicked.connect( + self.createAudioVisualisation) + + window.pushButton_Cancel.clicked.connect(self.stopVideo) + window.setWindowTitle("Audio Visualizer") + + self.previewWindow = PreviewWindow(self, os.path.join( + os.path.dirname(os.path.realpath(__file__)), "background.png")) + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + + self.modules = self.findComponents() + self.compMenu = QMenu() + for i, comp in enumerate(self.modules): + action = self.compMenu.addAction(comp.Component.__doc__) + action.triggered[()].connect( + lambda item=i: self.insertComponent(item)) + + self.window.pushButton_addComponent.setMenu(self.compMenu) + window.listWidget_componentList.clicked.connect( + lambda _: self.changeComponentWidget()) + + self.window.pushButton_removeComponent.clicked.connect( + lambda _: self.removeComponent()) + + currentRes = str(self.settings.value('outputWidth'))+'x' + \ + str(self.settings.value('outputHeight')) + for i, res in enumerate(self.resolutions): + window.comboBox_resolution.addItem(res) + if res == currentRes: + currentRes = i + window.comboBox_resolution.setCurrentIndex(currentRes) + window.comboBox_resolution.currentIndexChanged.connect( + self.updateResolution) + + self.window.pushButton_listMoveUp.clicked.connect( + self.moveComponentUp) + self.window.pushButton_listMoveDown.clicked.connect( + self.moveComponentDown) + self.window.pushButton_savePreset.clicked.connect( + self.openSavePresetDialog) + self.window.comboBox_openPreset.currentIndexChanged.connect( + self.openPreset) + self.window.pushButton_saveAs.clicked.connect( + self.openSaveProjectDialog) + self.window.pushButton_saveProject.clicked.connect( + self.saveCurrentProject) + self.window.pushButton_openProject.clicked.connect( + self.openOpenProjectDialog) + + # show the window and load current project + window.show() + self.currentProject = self.settings.value("currentProject") + if self.currentProject and os.path.exists(self.autosavePath) \ + and filecmp.cmp(self.autosavePath, self.currentProject): + # delete autosave if it's identical to the project + os.remove(self.autosavePath) + + if self.currentProject and os.path.exists(self.autosavePath): + ch = self.showMessage( + "Restore unsaved changes in project '%s'?" + % os.path.basename(self.currentProject)[:-4], True) + if ch: + os.remove(self.currentProject) + os.rename(self.autosavePath, self.currentProject) + else: + os.remove(self.autosavePath) + + self.openProject(self.currentProject) + self.drawPreview() + + def cleanUp(self): + self.timer.stop() + self.previewThread.quit() + self.previewThread.wait() + self.autosave() + + def autosave(self): + if time.time() - self.lastAutosave >= 1.0: + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + self.createProjectFile(self.autosavePath) + self.lastAutosave = time.time() + + def openInputFileDialog(self): + inputDir = self.settings.value("inputDir", expanduser("~")) + + fileName = QtGui.QFileDialog.getOpenFileName( + self.window, "Open Music File", + inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)") + + if not fileName == "": + self.settings.setValue("inputDir", os.path.dirname(fileName)) + self.window.lineEdit_audioFile.setText(fileName) + + def openOutputFileDialog(self): + outputDir = self.settings.value("outputDir", expanduser("~")) + + fileName = QtGui.QFileDialog.getSaveFileName( + self.window, "Set Output Video File", + outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)") + + if not fileName == "": + self.settings.setValue("outputDir", os.path.dirname(fileName)) + self.window.lineEdit_outputFile.setText(fileName) + + def stopVideo(self): + print('stop') + self.videoWorker.cancel() + self.canceled = True + + def createAudioVisualisation(self): + # create output video if mandatory settings are filled in + if self.window.lineEdit_audioFile.text() and \ + self.window.lineEdit_outputFile.text(): + self.canceled = False + self.progressBarUpdated(-1) + ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~")) + self.videoThread = QtCore.QThread(self) + self.videoWorker = video_thread.Worker(self) + self.videoWorker.moveToThread(self.videoThread) + self.videoWorker.videoCreated.connect(self.videoCreated) + self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) + self.videoWorker.progressBarSetText.connect( + self.progressBarSetText) + self.videoWorker.imageCreated.connect(self.showPreviewImage) + self.videoWorker.encoding.connect(self.changeEncodingStatus) + self.videoThread.start() + self.videoTask.emit( + self.window.lineEdit_audioFile.text(), + self.window.lineEdit_outputFile.text(), + self.selectedComponents) + else: + self.showMessage( + "You must select an audio file and output filename.") + + def progressBarUpdated(self, value): + self.window.progressBar_createVideo.setValue(value) + + def changeEncodingStatus(self, status): + if status: + self.window.pushButton_createVideo.setEnabled(False) + self.window.pushButton_Cancel.setEnabled(True) + self.window.comboBox_resolution.setEnabled(False) + self.window.stackedWidget.setEnabled(False) + self.window.tab_encoderSettings.setEnabled(False) + self.window.label_audioFile.setEnabled(False) + self.window.toolButton_selectAudioFile.setEnabled(False) + self.window.label_outputFile.setEnabled(False) + self.window.toolButton_selectOutputFile.setEnabled(False) + self.window.lineEdit_audioFile.setEnabled(False) + self.window.lineEdit_outputFile.setEnabled(False) + self.window.pushButton_addComponent.setEnabled(False) + self.window.pushButton_removeComponent.setEnabled(False) + self.window.pushButton_listMoveDown.setEnabled(False) + self.window.pushButton_listMoveUp.setEnabled(False) + self.window.comboBox_openPreset.setEnabled(False) + self.window.pushButton_removePreset.setEnabled(False) + self.window.pushButton_savePreset.setEnabled(False) + self.window.pushButton_openProject.setEnabled(False) + self.window.listWidget_componentList.setEnabled(False) + else: + self.window.pushButton_createVideo.setEnabled(True) + self.window.pushButton_Cancel.setEnabled(False) + self.window.comboBox_resolution.setEnabled(True) + self.window.stackedWidget.setEnabled(True) + self.window.tab_encoderSettings.setEnabled(True) + self.window.label_audioFile.setEnabled(True) + self.window.toolButton_selectAudioFile.setEnabled(True) + self.window.lineEdit_audioFile.setEnabled(True) + self.window.label_outputFile.setEnabled(True) + self.window.toolButton_selectOutputFile.setEnabled(True) + self.window.lineEdit_outputFile.setEnabled(True) + self.window.pushButton_addComponent.setEnabled(True) + self.window.pushButton_removeComponent.setEnabled(True) + self.window.pushButton_listMoveDown.setEnabled(True) + self.window.pushButton_listMoveUp.setEnabled(True) + self.window.comboBox_openPreset.setEnabled(True) + self.window.pushButton_removePreset.setEnabled(True) + self.window.pushButton_savePreset.setEnabled(True) + self.window.pushButton_openProject.setEnabled(True) + self.window.listWidget_componentList.setEnabled(True) + + def progressBarSetText(self, value): + self.window.progressBar_createVideo.setFormat(value) + + def videoCreated(self): + self.videoThread.quit() + self.videoThread.wait() + + def updateResolution(self): + resIndex = int(window.comboBox_resolution.currentIndex()) + res = self.resolutions[resIndex].split('x') + self.settings.setValue('outputWidth', res[0]) + self.settings.setValue('outputHeight', res[1]) + self.drawPreview() + + def drawPreview(self): + self.newTask.emit(self.selectedComponents) + # self.processTask.emit() + self.autosave() + + def showPreviewImage(self, image): + self.previewWindow.changePixmap(image) + + def findComponents(self): + def findComponents(): + srcPath = os.path.join( + os.path.dirname(os.path.realpath(__file__)), 'components') + if os.path.exists(srcPath): + for f in sorted(os.listdir(srcPath)): + name, ext = os.path.splitext(f) + if name.startswith("__"): + continue + elif ext == '.py': + yield name + return [ + import_module('components.%s' % name) + for name in findComponents()] + + def addComponent(self, moduleIndex): + index = len(self.pages) + self.selectedComponents.append(self.modules[moduleIndex].Component()) + self.window.listWidget_componentList.addItem( + self.selectedComponents[-1].__doc__) + self.pages.append(self.selectedComponents[-1].widget(self)) + self.window.listWidget_componentList.setCurrentRow(index) + self.window.stackedWidget.addWidget(self.pages[-1]) + self.window.stackedWidget.setCurrentIndex(index) + self.selectedComponents[-1].update() + self.updateOpenPresetComboBox(self.selectedComponents[-1]) + + def insertComponent(self, moduleIndex): + self.selectedComponents.insert( + 0, self.modules[moduleIndex].Component()) + self.window.listWidget_componentList.insertItem( + 0, self.selectedComponents[0].__doc__) + self.pages.insert(0, self.selectedComponents[0].widget(self)) + self.window.listWidget_componentList.setCurrentRow(0) + self.window.stackedWidget.insertWidget(0, self.pages[0]) + self.window.stackedWidget.setCurrentIndex(0) + self.selectedComponents[0].update() + self.updateOpenPresetComboBox(self.selectedComponents[0]) + + def removeComponent(self): + for selected in self.window.listWidget_componentList.selectedItems(): + index = self.window.listWidget_componentList.row(selected) + self.window.stackedWidget.removeWidget(self.pages[index]) + self.window.listWidget_componentList.takeItem(index) + self.selectedComponents.pop(index) + self.pages.pop(index) + self.changeComponentWidget() + self.drawPreview() + + def changeComponentWidget(self): + selected = self.window.listWidget_componentList.selectedItems() + if selected: + index = self.window.listWidget_componentList.row(selected[0]) + self.window.stackedWidget.setCurrentIndex(index) + self.updateOpenPresetComboBox(self.selectedComponents[index]) + + def moveComponentUp(self): + row = self.window.listWidget_componentList.currentRow() + if row > 0: + module = self.selectedComponents[row] + self.selectedComponents.pop(row) + self.selectedComponents.insert(row - 1, module) + page = self.pages[row] + self.pages.pop(row) + self.pages.insert(row - 1, page) + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row - 1, item) + widget = self.window.stackedWidget.removeWidget(page) + self.window.stackedWidget.insertWidget(row - 1, page) + self.window.listWidget_componentList.setCurrentRow(row - 1) + self.window.stackedWidget.setCurrentIndex(row - 1) + self.drawPreview() + + def moveComponentDown(self): + row = self.window.listWidget_componentList.currentRow() + if row != -1 and row < len(self.pages)+1: + module = self.selectedComponents[row] + self.selectedComponents.pop(row) + self.selectedComponents.insert(row + 1, module) + page = self.pages[row] + self.pages.pop(row) + self.pages.insert(row + 1, page) + item = self.window.listWidget_componentList.takeItem(row) + self.window.listWidget_componentList.insertItem(row + 1, item) + widget = self.window.stackedWidget.removeWidget(page) + self.window.stackedWidget.insertWidget(row + 1, page) + self.window.listWidget_componentList.setCurrentRow(row + 1) + self.window.stackedWidget.setCurrentIndex(row + 1) + self.drawPreview() + + def updateOpenPresetComboBox(self, component): + self.window.comboBox_openPreset.clear() + self.window.comboBox_openPreset.addItem("Component Presets") + destination = os.path.join( + self.presetDir, str(component).strip(), str(component.version())) + if not os.path.exists(destination): + os.makedirs(destination) + for f in os.listdir(destination): + self.window.comboBox_openPreset.addItem(f) + + def openSavePresetDialog(self): + if self.window.listWidget_componentList.currentRow() == -1: + return + while True: + newName, OK = QtGui.QInputDialog.getText( + QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') + badName = False + for letter in newName: + if letter in string.punctuation: + badName = True + if badName: + # some filesystems don't like bizarre characters + self.showMessage("Preset names must contain only letters, \ + numbers, and spaces.") + continue + if OK and newName: + index = self.window.listWidget_componentList.currentRow() + if index != -1: + saveValueStore = \ + self.selectedComponents[index].savePreset() + componentName = str(self.selectedComponents[index]).strip() + vers = self.selectedComponents[index].version() + self.createPresetFile( + componentName, vers, saveValueStore, newName) + break + + def createPresetFile( + self, componentName, version, saveValueStore, filename): + dirname = os.path.join(self.presetDir, componentName, str(version)) + if not os.path.exists(dirname): + os.makedirs(dirname) + filepath = os.path.join(dirname, filename) + if os.path.exists(filepath): + ch = self.showMessage( + "%s already exists! Overwrite it?" % filename, + True, QtGui.QMessageBox.Warning) + if not ch: + return + # remove old copies of the preset + for i in range(0, self.window.comboBox_openPreset.count()): + if self.window.comboBox_openPreset.itemText(i) == filename: + self.window.comboBox_openPreset.removeItem(i) + with open(filepath, 'w') as f: + f.write(core.Core.stringOrderedDict(saveValueStore)) + self.window.comboBox_openPreset.addItem(filename) + self.window.comboBox_openPreset.setCurrentIndex( + self.window.comboBox_openPreset.count()-1) + + def openPreset(self): + if self.window.comboBox_openPreset.currentIndex() < 1: + return + index = self.window.listWidget_componentList.currentRow() + if index == -1: + return + filename = self.window.comboBox_openPreset.itemText( + self.window.comboBox_openPreset.currentIndex()) + componentName = str(self.selectedComponents[index]).strip() + version = self.selectedComponents[index].version() + dirname = os.path.join(self.presetDir, componentName, str(version)) + filepath = os.path.join(dirname, filename) + if not os.path.exists(filepath): + self.window.comboBox_openPreset.removeItem( + self.window.comboBox_openPreset.currentIndex()) + return + with open(filepath, 'r') as f: + for line in f: + saveValueStore = dict(eval(line.strip())) + break + self.selectedComponents[index].loadPreset(saveValueStore) + self.drawPreview() + + def saveCurrentProject(self): + if self.currentProject: + self.createProjectFile(self.currentProject) + else: + self.openSaveProjectDialog() + + def openSaveProjectDialog(self): + filename = QtGui.QFileDialog.getSaveFileName( + self.window, "Create Project File", + self.settings.value("projectDir"), + "Project Files (*.avp)") + if not filename: + return + self.createProjectFile(filename) + + def createProjectFile(self, filepath): + if not filepath.endswith(".avp"): + filepath += '.avp' + with open(filepath, 'w') as f: + print('creating %s' % filepath) + f.write('[Components]\n') + for comp in self.selectedComponents: + saveValueStore = comp.savePreset() + f.write('%s\n' % str(comp)) + f.write('%s\n' % str(comp.version())) + f.write('%s\n' % core.Core.stringOrderedDict(saveValueStore)) + if filepath != self.autosavePath: + self.settings.setValue("projectDir", os.path.dirname(filepath)) + self.settings.setValue("currentProject", filepath) + self.currentProject = filepath + + def openOpenProjectDialog(self): + filename = QtGui.QFileDialog.getOpenFileName( + self.window, "Open Project File", + self.settings.value("projectDir"), + "Project Files (*.avp)") + self.openProject(filename) + + def openProject(self, filepath): + if not filepath or not os.path.exists(filepath) \ + or not filepath.endswith('.avp'): + return + self.clear() + self.currentProject = filepath + self.settings.setValue("currentProject", filepath) + self.settings.setValue("projectDir", os.path.dirname(filepath)) + compNames = [mod.Component.__doc__ for mod in self.modules] + try: + with open(filepath, 'r') as f: + validSections = ('Components') + section = '' + + def parseLine(line): + line = line.strip() + newSection = '' + + if line.startswith('[') and line.endswith(']') \ + and line[1:-1] in validSections: + newSection = line[1:-1] + + return line, newSection + + i = 0 + for line in f: + line, newSection = parseLine(line) + if newSection: + section = str(newSection) + continue + if line and section == 'Components': + if i == 0: + compIndex = compNames.index(line) + self.addComponent(compIndex) + i += 1 + elif i == 1: + # version, not used yet + i += 1 + elif i == 2: + saveValueStore = dict(eval(line)) + self.selectedComponents[-1].loadPreset( + saveValueStore) + i = 0 + except (IndexError, ValueError, KeyError, NameError, + SyntaxError, AttributeError, TypeError) as e: + self.clear() + typ, value, _ = sys.exc_info() + msg = '%s: %s' % (typ.__name__, value) + self.showMessage( + "Project file '%s' is corrupted." % filepath, False, + QtGui.QMessageBox.Warning, msg) + + def showMessage( + self, string, showCancel=False, + icon=QtGui.QMessageBox.Information, detail=None): + msg = QtGui.QMessageBox() + msg.setIcon(icon) + msg.setText(string) + msg.setDetailedText(detail) + if showCancel: + msg.setStandardButtons( + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) + else: + msg.setStandardButtons(QtGui.QMessageBox.Ok) + ch = msg.exec_() + if ch == 1024: + return True + return False + + def clear(self): + ''' empty out all components and fields, get a blank slate ''' + self.selectedComponents = [] + self.window.listWidget_componentList.clear() + for widget in self.pages: + self.window.stackedWidget.removeWidget(widget) + self.pages = [] diff --git a/preview_thread.py b/preview_thread.py index 04683ae..d54dba5 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -9,53 +9,52 @@ import numpy import os from copy import copy + class Worker(QtCore.QObject): - imageCreated = pyqtSignal(['QImage']) + imageCreated = pyqtSignal(['QImage']) - def __init__(self, parent=None, queue=None): - QtCore.QObject.__init__(self) - parent.newTask.connect(self.createPreviewImage) - parent.processTask.connect(self.process) - self.core = core.Core() - self.queue = queue - self.core.settings = parent.settings - self.stackedWidget = parent.window.stackedWidget - self.background = Image.new("RGBA", (1920, 1080),(0,0,0,0)) - self.background.paste(Image.open(os.path.join(os.path.dirname(os.path.realpath(__file__)),"background.png"))) - + def __init__(self, parent=None, queue=None): + QtCore.QObject.__init__(self) + parent.newTask.connect(self.createPreviewImage) + parent.processTask.connect(self.process) + self.core = core.Core() + self.queue = queue + self.core.settings = parent.settings + self.stackedWidget = parent.window.stackedWidget + self.background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0)) + self.background.paste(Image.open(os.path.join( + os.path.dirname(os.path.realpath(__file__)), "background.png"))) + @pyqtSlot(str, list) + def createPreviewImage(self, components): + dic = { + "components": components, + } + self.queue.put(dic) - @pyqtSlot(str, list) - def createPreviewImage(self, components): - # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) - dic = { - "components": components, - } - self.queue.put(dic) - - @pyqtSlot() - def process(self): - try: - nextPreviewInformation = self.queue.get(block=False) - while self.queue.qsize() >= 2: + @pyqtSlot() + def process(self): try: - self.queue.get(block=False) + nextPreviewInformation = self.queue.get(block=False) + while self.queue.qsize() >= 2: + try: + self.queue.get(block=False) + except Empty: + continue + + width = int(self.core.settings.value('outputWidth')) + height = int(self.core.settings.value('outputHeight')) + frame = copy(self.background) + frame = frame.resize((width, height)) + + components = nextPreviewInformation["components"] + for component in reversed(components): + frame = Image.alpha_composite( + frame, component.previewRender(self)) + + self._image = ImageQt(frame) + self.imageCreated.emit(QtGui.QImage(self._image)) + except Empty: - continue - - width = int(self.core.settings.value('outputWidth')) - height = int(self.core.settings.value('outputHeight')) - frame = copy(self.background) - frame = frame.resize((width,height)) - - components = nextPreviewInformation["components"] - for component in reversed(components): - #newFrame = Image.alpha_composite(frame,) - frame = Image.alpha_composite(frame,component.previewRender(self)) - - self._image = ImageQt(frame) - self.imageCreated.emit(QtGui.QImage(self._image)) - - except Empty: - True + True diff --git a/video_thread.py b/video_thread.py index e880263..5897ff0 100644 --- a/video_thread.py +++ b/video_thread.py @@ -13,6 +13,7 @@ import time from copy import copy import signal + class Worker(QtCore.QObject): imageCreated = pyqtSignal(['QImage']) @@ -40,16 +41,19 @@ class Worker(QtCore.QObject): frame = None for compNo, comp in reversed(list(enumerate(self.components))): - if compNo in self.staticComponents and self.staticComponents[compNo] != None: + if compNo in self.staticComponents and \ + self.staticComponents[compNo] is not None: if frame is None: frame = self.staticComponents[compNo] else: - frame = Image.alpha_composite(frame, self.staticComponents[compNo]) + frame = Image.alpha_composite( + frame, self.staticComponents[compNo]) else: if frame is None: frame = comp.frameRender(compNo, i[0], i[1]) else: - frame = Image.alpha_composite(frame, comp.frameRender(compNo, i[0], i[1])) + frame = Image.alpha_composite( + frame, comp.frameRender(compNo, i[0], i[1])) self.renderQueue.put([i[0], frame]) self.compositeQueue.task_done() @@ -63,8 +67,9 @@ class Worker(QtCore.QObject): self.bgI += 1 def previewDispatch(self): - background = Image.new("RGBA", (1920, 1080),(0,0,0,0)) - background.paste(Image.open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "background.png"))) + background = Image.new("RGBA", (1920, 1080), (0, 0, 0, 0)) + background.paste(Image.open(os.path.join( + os.path.dirname(os.path.realpath(__file__)), "background.png"))) background = background.resize((self.width, self.height)) while not self.stopped: @@ -83,11 +88,10 @@ class Worker(QtCore.QObject): self.encoding.emit(True) self.components = components self.outputFile = outputFile - self.bgI = 0 # tracked video frame + self.bgI = 0 # tracked video frame self.reset() self.width = int(self.core.settings.value('outputWidth')) self.height = int(self.core.settings.value('outputHeight')) - # print('worker thread id: {}'.format(QtCore.QThread.currentThreadId())) progressBarValue = 0 self.progressBarUpdate.emit(progressBarValue) @@ -95,21 +99,24 @@ class Worker(QtCore.QObject): self.completeAudioArray = self.core.readAudioFile(inputFile, self) # test if user has libfdk_aac - encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) + encoders = sp.check_output( + self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True) acodec = self.core.settings.value('outputAudioCodec') - + if b'libfdk_aac' in encoders and acodec == 'aac': acodec = 'libfdk_aac' ffmpegCommand = [ self.core.FFMPEG_BIN, '-thread_queue_size', '512', - '-y', # (optional) means overwrite the output file if it already exists. + '-y', # overwrite the output file if it already exists. '-f', 'rawvideo', '-vcodec', 'rawvideo', '-s', str(self.width)+'x'+str(self.height), # size of one frame '-pix_fmt', 'rgba', - '-r', self.core.settings.value('outputFrameRate'), # frames per second + + # frames per second + '-r', self.core.settings.value('outputFrameRate'), '-i', '-', # The input comes from a pipe '-an', '-i', inputFile, @@ -126,14 +133,16 @@ class Worker(QtCore.QObject): ffmpegCommand.append('-2') ffmpegCommand.append(outputFile) - self.out_pipe = sp.Popen(ffmpegCommand, stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout) + self.out_pipe = sp.Popen( + ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout) # create video for output numpy.seterr(divide='ignore') # initialize components print('loaded components:', - ["%s%s" % (num, str(component)) for num, component in enumerate(self.components)]) + ["%s%s" % (num, str(component)) for num, + component in enumerate(self.components)]) self.staticComponents = {} numComps = len(self.components) for compNo, comp in enumerate(self.components): @@ -149,7 +158,8 @@ class Worker(QtCore.QObject): ) if properties and 'static' in properties: - self.staticComponents[compNo] = copy(comp.frameRender(compNo, 0, 0)) + self.staticComponents[compNo] = copy( + comp.frameRender(compNo, 0, 0)) self.progressBarUpdate.emit(100) self.compositeQueue = Queue() @@ -159,17 +169,20 @@ class Worker(QtCore.QObject): self.previewQueue = PriorityQueue() self.renderThreads = [] - # create threads to render frames and send them back here for piping out + # Threads to render frames and send them back here for piping out for i in range(3): - self.renderThreads.append(Thread(target=self.renderNode, name="Render Thread")) + self.renderThreads.append( + Thread(target=self.renderNode, name="Render Thread")) self.renderThreads[i].daemon = True self.renderThreads[i].start() - self.dispatchThread = Thread(target=self.renderDispatch, name="Render Dispatch Thread") + self.dispatchThread = Thread( + target=self.renderDispatch, name="Render Dispatch Thread") self.dispatchThread.daemon = True self.dispatchThread.start() - self.previewDispatch = Thread(target=self.previewDispatch, name="Render Dispatch Thread") + self.previewDispatch = Thread( + target=self.previewDispatch, name="Render Dispatch Thread") self.previewDispatch.daemon = True self.previewDispatch.start() @@ -197,10 +210,13 @@ class Worker(QtCore.QObject): break # increase progress bar value - if progressBarValue + 1 <= (i / len(self.completeAudioArray)) * 100: - progressBarValue = numpy.floor((i / len(self.completeAudioArray)) * 100) + if progressBarValue + 1 <= (i / len(self.completeAudioArray)) \ + * 100: + progressBarValue = numpy.floor( + (i / len(self.completeAudioArray)) * 100) self.progressBarUpdate.emit(progressBarValue) - pStr = "Exporting video: " + str(int(progressBarValue)) + "%" + pStr = "Exporting video: " + str(int(progressBarValue)) \ + + "%" self.progressBarSetText.emit(pStr) numpy.seterr(all='print') @@ -220,7 +236,7 @@ class Worker(QtCore.QObject): pass self.progressBarUpdate.emit(0) self.progressBarSetText.emit('Export Canceled') - + else: if self.error: print("Export Failed") @@ -230,14 +246,14 @@ class Worker(QtCore.QObject): print("Export Complete") self.progressBarUpdate.emit(100) self.progressBarSetText.emit('Export Complete') - + self.error = False self.canceled = False self.parent.drawPreview() self.stopped = True self.encoding.emit(False) self.videoCreated.emit() - + def updateProgress(self, pStr, pVal): self.progressBarValue.emit(pVal) self.progressBarSetText.emit(pStr) @@ -245,10 +261,10 @@ class Worker(QtCore.QObject): def cancel(self): self.canceled = True self.core.cancel() - + for comp in self.components: comp.cancel() - + try: self.out_pipe.send_signal(signal.SIGINT) except: