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