waveform component is working, preview is glitchy

This commit is contained in:
tassaron 2017-07-29 20:27:46 -04:00
parent c1457b6dad
commit 1297af61c9
8 changed files with 256 additions and 87 deletions

View File

@ -18,6 +18,9 @@ class Component(Component):
def names(*args): def names(*args):
return ['Original Audio Visualization'] return ['Original Audio Visualization']
def properties(self):
return ['pcm']
def widget(self, *args): def widget(self, *args):
self.visColor = (255, 255, 255) self.visColor = (255, 255, 255)
self.scale = 20 self.scale = 20

View File

@ -4,10 +4,10 @@ import os
import math import math
import subprocess import subprocess
from component import Component, ComponentError from component import Component
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame, scale
from toolkit.ffmpeg import testAudioStream, FfmpegVideo from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
from toolkit import openPipe, closePipe, checkOutput, scale from toolkit import checkOutput
class Component(Component): class Component(Component):
@ -132,7 +132,7 @@ class Component(Component):
] ]
command.extend(self.makeFfmpegFilter()) command.extend(self.makeFfmpegFilter())
command.extend([ command.extend([
'-vcodec', 'rawvideo', '-', '-codec:v', 'rawvideo', '-',
'-ss', '90', '-ss', '90',
'-frames:v', '1', '-frames:v', '1',
]) ])

View File

@ -5,10 +5,10 @@ import os
import math import math
import subprocess import subprocess
from component import Component, ComponentError from component import Component
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame, scale
from toolkit import openPipe, checkOutput, rgbFromString from toolkit import checkOutput, rgbFromString, pickColor
from toolkit.ffmpeg import FfmpegVideo from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo
class Component(Component): class Component(Component):
@ -21,17 +21,27 @@ class Component(Component):
self.page.lineEdit_color.setText('%s,%s,%s' % self.color) self.page.lineEdit_color.setText('%s,%s,%s' % self.color)
btnStyle = "QPushButton { background-color : %s; outline: none; }" \ btnStyle = "QPushButton { background-color : %s; outline: none; }" \
% QColor(*self.color1).name() % QColor(*self.color).name()
self.page.lineEdit_color.setStylesheet(btnStyle) self.page.pushButton_color.setStyleSheet(btnStyle)
self.page.pushButton_color.clicked.connect(lambda: self.pickColor()) 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( self.trackWidgets(
{ {
'mode': self.page.comboBox_mode, 'mode': self.page.comboBox_mode,
'amplitude': self.page.comboBox_amplitude,
'x': self.page.spinBox_x, 'x': self.page.spinBox_x,
'y': self.page.spinBox_y, 'y': self.page.spinBox_y,
'mirror': self.page.checkBox_mirror, 'mirror': self.page.checkBox_mirror,
'scale': self.page.spinBox_scale, '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) self.page.pushButton_color.setStyleSheet(btnStyle)
super().update() 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): def previewRender(self):
self.updateChunksize() self.updateChunksize()
frame = self.getPreviewFrame(self.width, self.height) frame = self.getPreviewFrame(self.width, self.height)
@ -53,10 +83,11 @@ class Component(Component):
def preFrameRender(self, **kwargs): def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs) super().preFrameRender(**kwargs)
self.updateChunksize() self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo( self.video = FfmpegVideo(
inputPath=self.audioFile, inputPath=self.audioFile,
filter_=makeFfmpegFilter(), filter_=self.makeFfmpegFilter(),
width=self.width, height=self.height, width=w, height=h,
chunkSize=self.chunkSize, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")), frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, component=self, parent=self.parent, component=self,
@ -65,7 +96,7 @@ class Component(Component):
def frameRender(self, frameNo): def frameRender(self, frameNo):
if FfmpegVideo.threadError is not None: if FfmpegVideo.threadError is not None:
raise FfmpegVideo.threadError raise FfmpegVideo.threadError
return finalizeFrame(self.video.frame(frameNo)) return self.finalizeFrame(self.video.frame(frameNo))
def postFrameRender(self): def postFrameRender(self):
closePipe(self.video.pipe) closePipe(self.video.pipe)
@ -74,18 +105,25 @@ class Component(Component):
inputFile = self.parent.window.lineEdit_audioFile.text() inputFile = self.parent.window.lineEdit_audioFile.text()
if not inputFile or not os.path.exists(inputFile): if not inputFile or not os.path.exists(inputFile):
return return
duration = getAudioDuration(inputFile)
if not duration:
return
startPt = duration / 3
command = [ command = [
self.core.FFMPEG_BIN, self.core.FFMPEG_BIN,
'-thread_queue_size', '512', '-thread_queue_size', '512',
'-r', self.settings.value("outputFrameRate"),
'-ss', "{0:.3f}".format(startPt),
'-i', inputFile, '-i', inputFile,
'-f', 'image2pipe', '-f', 'image2pipe',
'-pix_fmt', 'rgba', '-pix_fmt', 'rgba',
] ]
command.extend(self.makeFfmpegFilter()) command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
command.extend([ command.extend([
'-vcodec', 'rawvideo', '-', '-an',
'-ss', '90', '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
'-codec:v', 'rawvideo', '-',
'-frames:v', '1', '-frames:v', '1',
]) ])
pipe = openPipe( pipe = openPipe(
@ -95,45 +133,57 @@ class Component(Component):
byteFrame = pipe.stdout.read(self.chunkSize) byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe) closePipe(pipe)
frame = finalizeFrame(self, byteFrame, width, height) frame = self.finalizeFrame(byteFrame)
return frame return frame
def makeFfmpegFilter(self): def makeFfmpegFilter(self, preview=False, startPt=0):
w, h = scale(self.scale, self.width, self.height, str) 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 [ return [
'-filter_complex', '-filter_complex',
'[0:a] showwaves=s=%sx%s:mode=%s,format=rgba [v]' % ( '[0:a] %s%s'
w, h, self.mode, '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', '[v]',
'-map', '0:a',
] ]
def updateChunksize(self): def updateChunksize(self):
if self.scale != 100: width, height = scale(self.scale, self.width, self.height, int)
width, height = scale(self.scale, self.width, self.height, int)
else:
width, height = self.width, self.height
self.chunkSize = 4 * width * height self.chunkSize = 4 * width * height
def finalizeFrame(self, imageData):
def scale(scale, width, height, returntype=None): image = Image.frombytes(
width = (float(width) / 100.0) * float(scale) 'RGBA',
height = (float(height) / 100.0) * float(scale) scale(self.scale, self.width, self.height, int),
if returntype == str: imageData
return (str(math.ceil(width)), str(math.ceil(height))) )
elif returntype == int: if self.scale != 100 \
return (math.ceil(width), math.ceil(height)) or self.x != 0 or self.y != 0:
else: frame = BlankFrame(self.width, self.height)
return (width, height) frame.paste(image, box=(self.x, self.y))
else:
frame = image
def finalizeFrame(self, imageData, width, height): return frame
# 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

View File

@ -226,9 +226,31 @@
</spacer> </spacer>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkBox_mirror"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>Mirror</string> <string>Opacity</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_opacity">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>10</number>
</property>
<property name="maximum">
<number>400</number>
</property>
<property name="value">
<number>100</number>
</property> </property>
</widget> </widget>
</item> </item>
@ -263,6 +285,75 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QCheckBox" name="checkBox_compress">
<property name="text">
<string>Compress</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mono">
<property name="text">
<string>Mono</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mirror">
<property name="text">
<string>Mirror</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Amplitude</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude">
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">

View File

@ -6,22 +6,9 @@ import string
import os import os
import sys import sys
import subprocess import subprocess
import signal
import math
from collections import OrderedDict 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): def badName(name):
'''Returns whether a name contains non-alphanumeric chars''' '''Returns whether a name contains non-alphanumeric chars'''
return any([letter in string.punctuation for letter in name]) return any([letter in string.punctuation for letter in name])
@ -69,14 +56,6 @@ def checkOutput(commandList, **kwargs):
return subprocess.check_output(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 disableWhenEncoding(func):
def decorator(self, *args, **kwargs): def decorator(self, *args, **kwargs):
if self.encoding: if self.encoding:

View File

@ -6,10 +6,12 @@ import sys
import os import os
import subprocess import subprocess
import threading import threading
import signal
from queue import PriorityQueue from queue import PriorityQueue
import core import core
from toolkit.common import checkOutput, openPipe from toolkit.common import checkOutput, pipeWrapper
from component import ComponentError
class FfmpegVideo: class FfmpegVideo:
@ -60,7 +62,8 @@ class FfmpegVideo:
kwargs['filter_'] kwargs['filter_']
) )
self.command.extend([ self.command.extend([
'-vcodec', 'rawvideo', '-', '-s:v', '%sx%s' % (self.width, self.height),
'-codec:v', 'rawvideo', '-',
]) ])
self.frameBuffer = PriorityQueue() self.frameBuffer = PriorityQueue()
@ -85,9 +88,11 @@ class FfmpegVideo:
self.frameBuffer.task_done() self.frameBuffer.task_done()
def fillBuffer(self): def fillBuffer(self):
import sys
print(self.command)
self.pipe = openPipe( self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8 stderr=sys.__stdout__, bufsize=10**8
) )
while True: while True:
if self.parent.canceled: if self.parent.canceled:
@ -100,7 +105,7 @@ class FfmpegVideo:
self.frameBuffer.put((self.frameNo-1, self.lastFrame)) self.frameBuffer.put((self.frameNo-1, self.lastFrame))
continue continue
except AttributeError: except AttributeError:
Video.threadError = ComponentError(self.component, 'video') FfmpegVideo.threadError = ComponentError(self.component, 'video')
break break
self.currentFrame = self.pipe.stdout.read(self.chunkSize) self.currentFrame = self.pipe.stdout.read(self.chunkSize)
@ -109,6 +114,16 @@ class FfmpegVideo:
self.lastFrame = self.currentFrame 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(): def findFfmpeg():
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
# The application is frozen # The application is frozen
@ -347,7 +362,12 @@ def getAudioDuration(filename):
except subprocess.CalledProcessError as ex: except subprocess.CalledProcessError as ex:
fileInfo = ex.output 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: for line in info:
if 'Duration' in line: if 'Duration' in line:
d = line.split(',')[0] d = line.split(',')[0]

View File

@ -6,6 +6,7 @@ from PIL import Image
from PIL.ImageQt import ImageQt from PIL.ImageQt import ImageQt
import sys import sys
import os import os
import math
import core import core
@ -41,6 +42,17 @@ class PaintColor(QtGui.QColor):
super().__init__(b, g, r, a) 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): def defaultSize(framefunc):
'''Makes width/height arguments optional''' '''Makes width/height arguments optional'''
def decorator(*args): def decorator(*args):

View File

@ -19,9 +19,11 @@ import time
import signal import signal
from component import ComponentError from component import ComponentError
from toolkit import openPipe
from toolkit.ffmpeg import readAudioFile, createFfmpegCommand
from toolkit.frame import Checkerboard from toolkit.frame import Checkerboard
from toolkit.ffmpeg import (
openPipe, readAudioFile,
getAudioDuration, createFfmpegCommand
)
class Worker(QtCore.QObject): class Worker(QtCore.QObject):
@ -132,15 +134,24 @@ class Worker(QtCore.QObject):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG # READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
if any([
self.progressBarSetText.emit("Loading audio file...") True if 'pcm' in comp.properties() else False
audioFileTraits = readAudioFile( for comp in self.components
self.inputFile, self ]):
) self.progressBarSetText.emit("Loading audio file...")
if audioFileTraits is None: audioFileTraits = readAudioFile(
self.cancelExport() self.inputFile, self
return )
self.completeAudioArray, duration = audioFileTraits 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.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...") self.progressBarSetText.emit("Starting components...")
@ -284,7 +295,10 @@ class Worker(QtCore.QObject):
numpy.seterr(all='print') 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: if self.out_pipe.stderr is not None:
print(self.out_pipe.stderr.read()) print(self.out_pipe.stderr.read())
self.out_pipe.stderr.close() self.out_pipe.stderr.close()