From b6b45d12702f18f041acf65b0d5e34714835ecb4 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 30 Jul 2017 13:04:02 -0400 Subject: added Spectrum component with many options tweaked Waveform, added some ffmpeg logging, made generic widget functions --- src/components/spectrum.py | 239 +++++++++++++++++++ src/components/spectrum.ui | 582 +++++++++++++++++++++++++++++++++++++++++++++ src/components/waveform.py | 48 ++-- src/components/waveform.ui | 21 +- 4 files changed, 869 insertions(+), 21 deletions(-) create mode 100644 src/components/spectrum.py create mode 100644 src/components/spectrum.ui (limited to 'src/components') diff --git a/src/components/spectrum.py b/src/components/spectrum.py new file mode 100644 index 0000000..261d9cc --- /dev/null +++ b/src/components/spectrum.py @@ -0,0 +1,239 @@ +from PIL import Image +from PyQt5 import QtGui, QtCore, QtWidgets +from PyQt5.QtGui import QColor +import os +import math +import subprocess +import time + +from component import Component +from toolkit.frame import BlankFrame, scale +from toolkit import checkOutput, rgbFromString, pickColor, connectWidget +from toolkit.ffmpeg import ( + openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound +) + + +class Component(Component): + name = 'Spectrum' + version = '1.0.0' + + def widget(self, *args): + self.color = (255, 255, 255) + self.previewFrame = None + super().widget(*args) + self.chunkSize = 4 * self.width * self.height + self.changedOptions = True + + if hasattr(self.parent, 'window'): + # update preview when audio file changes (if genericPreview is off) + self.parent.window.lineEdit_audioFile.textChanged.connect( + self.update + ) + + self.trackWidgets( + { + 'filterType': self.page.comboBox_filterType, + 'window': self.page.comboBox_window, + '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, + 'color': self.page.comboBox_color, + 'compress': self.page.checkBox_compress, + 'mono': self.page.checkBox_mono, + } + ) + for widget in self._trackedWidgets.values(): + connectWidget(widget, lambda: self.changed()) + + def changed(self): + self.changedOptions = True + + def update(self): + count = self.page.stackedWidget.count() + i = self.page.comboBox_filterType.currentIndex() + self.page.stackedWidget.setCurrentIndex(i if i < count else count - 1) + super().update() + + def previewRender(self): + changedSize = self.updateChunksize() + if not changedSize \ + and not self.changedOptions \ + and self.previewFrame is not None: + return self.previewFrame + + frame = self.getPreviewFrame() + self.changedOptions = False + if not frame: + self.previewFrame = None + return BlankFrame(self.width, self.height) + else: + self.previewFrame = frame + return frame + + 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_=self.makeFfmpegFilter(), + width=w, height=h, + chunkSize=self.chunkSize, + frameRate=int(self.settings.value("outputFrameRate")), + parent=self.parent, component=self, + ) + + def frameRender(self, frameNo): + if FfmpegVideo.threadError is not None: + raise FfmpegVideo.threadError + return self.finalizeFrame(self.video.frame(frameNo)) + + def postFrameRender(self): + closePipe(self.video.pipe) + + def getPreviewFrame(self): + genericPreview = self.settings.value("pref_genericPreview") + startPt = 0 + if not genericPreview: + 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', + os.path.join(self.core.wd, 'background.png') + if genericPreview else inputFile, + '-f', 'image2pipe', + '-pix_fmt', 'rgba', + ] + command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt)) + command.extend([ + '-an', + '-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str), + '-codec:v', 'rawvideo', '-', + '-frames:v', '1', + ]) + logFilename = os.path.join( + self.core.dataDir, 'preview_%s.log' % str(self.compPos)) + with open(logFilename, 'w') as log: + log.write(" ".join(command) + '\n\n') + with open(logFilename, 'a') as log: + pipe = openPipe( + command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=log, bufsize=10**8 + ) + byteFrame = pipe.stdout.read(self.chunkSize) + closePipe(pipe) + + frame = self.finalizeFrame(byteFrame) + return frame + + def makeFfmpegFilter(self, preview=False, startPt=0): + w, h = scale(self.scale, self.width, self.height, str) + if self.amplitude == 0: + amplitude = 'sqrt' + elif self.amplitude == 1: + amplitude = 'cbrt' + elif self.amplitude == 2: + amplitude = '4thrt' + elif self.amplitude == 3: + amplitude = '5thrt' + elif self.amplitude == 4: + amplitude = 'lin' + elif self.amplitude == 5: + amplitude = 'log' + color = self.page.comboBox_color.currentText().lower() + genericPreview = self.settings.value("pref_genericPreview") + + if self.filterType == 0: # Spectrum + filter_ = ( + 'showspectrum=s=%sx%s:slide=scroll:win_func=%s:' + 'color=%s:scale=%s' % ( + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + self.page.comboBox_window.currentText(), + color, amplitude, + ) + ) + elif self.filterType == 1: # Histogram + filter_ = ( + 'ahistogram=r=%s:s=%sx%s:dmode=separate' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + elif self.filterType == 2: # Vector Scope + filter_ = ( + 'avectorscope=s=%sx%s:draw=line:m=polar:scale=log' % ( + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + elif self.filterType == 3: # Musical Scale + filter_ = ( + 'showcqt=r=%s:s=%sx%s:count=30:text=0' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + elif self.filterType == 4: # Phase + filter_ = ( + 'aphasemeter=r=%s:s=%sx%s:mpc=white:video=1[atrash][vtmp]; ' + '[atrash] anullsink; [vtmp] null' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + ) + ) + + return [ + '-filter_complex', + '%s%s%s%s%s [v1]; ' + '[v1] scale=%s:%s%s [v]' % ( + exampleSound() if preview and genericPreview else '[0:a] ', + 'compand=gain=4,' if self.compress else '', + 'aformat=channel_layouts=mono,' if self.mono else '', + filter_, + ', hflip' if self.mirror else'', + w, h, + ', trim=start=%s:end=%s' % ( + "{0:.3f}".format(startPt + 15), + "{0:.3f}".format(startPt + 15.5) + ) if preview else '', + ), + '-map', '[v]', + ] + + def updateChunksize(self): + width, height = scale(self.scale, self.width, self.height, int) + oldChunkSize = int(self.chunkSize) + self.chunkSize = 4 * width * height + changed = self.chunkSize != oldChunkSize + return changed + + def finalizeFrame(self, imageData): + image = Image.frombytes( + 'RGBA', + scale(self.scale, self.width, self.height, int), + imageData + ) + if self.scale != 100 \ + or self.x != 0 or self.y != 0: + frame = BlankFrame(self.width, self.height) + frame.paste(image, box=(self.x, self.y)) + else: + frame = image + return frame diff --git a/src/components/spectrum.ui b/src/components/spectrum.ui new file mode 100644 index 0000000..59ca0b8 --- /dev/null +++ b/src/components/spectrum.ui @@ -0,0 +1,582 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + + 0 + 0 + + + + + 0 + 197 + + + + Form + + + + + + 4 + + + + + + + + 0 + 0 + + + + Type + + + + + + + + Spectrum + + + + + Histogram + + + + + Vector Scope + + + + + Musical Scale + + + + + Phase + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + + 0 + 0 + + + + X + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + -10000 + + + 10000 + + + + + + + + 0 + 0 + + + + Y + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + 0 + 0 + + + + -10000 + + + 10000 + + + 0 + + + + + + + + + + + Compress + + + + + + + Mono + + + + + + + Mirror + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Scale + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 + + + + + + + + + + 0 + 0 + + + + false + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + 0 + 0 + 561 + 72 + + + + + QLayout::SetMaximumSize + + + 0 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + 31 + 0 + + + + Window + + + 4 + + + + + + + + hann + + + + + gauss + + + + + tukey + + + + + dolph + + + + + cauchy + + + + + parzen + + + + + poisson + + + + + rect + + + + + bartlett + + + + + hanning + + + + + hamming + + + + + blackman + + + + + welch + + + + + flattop + + + + + bharris + + + + + bnuttall + + + + + lanczos + + + + + + + + + 0 + 0 + + + + Amplitude + + + 4 + + + + + + + + Square root + + + + + Cubic root + + + + + 4thrt + + + + + 5thrt + + + + + Linear + + + + + Logarithmic + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 10 + 20 + + + + + + + + + + + + + 0 + 0 + + + + Color + + + 4 + + + + + + + + Channel + + + + + Intensity + + + + + Rainbow + + + + + Moreland + + + + + Nebulae + + + + + Fire + + + + + Fiery + + + + + Fruit + + + + + Cool + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 10 + 20 + + + + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + diff --git a/src/components/waveform.py b/src/components/waveform.py index b4b19e9..6c5133d 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -8,7 +8,9 @@ import subprocess from component import Component from toolkit.frame import BlankFrame, scale from toolkit import checkOutput, rgbFromString, pickColor -from toolkit.ffmpeg import openPipe, closePipe, getAudioDuration, FfmpegVideo +from toolkit.ffmpeg import ( + openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound +) class Component(Component): @@ -112,6 +114,8 @@ class Component(Component): if not duration: return startPt = duration / 3 + if startPt + 3 > duration: + startPt += startPt - 3 command = [ self.core.FFMPEG_BIN, @@ -154,29 +158,43 @@ class Component(Component): hexcolor = QColor(*self.color).name() opacity = "{0:.1f}".format(self.opacity / 100) genericPreview = self.settings.value("pref_genericPreview") + if self.mode < 3: + filter_ = 'showwaves=r=%s:s=%sx%s:mode=%s:colors=%s@%s:scale=%s' % ( + self.settings.value("outputFrameRate"), + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + self.page.comboBox_mode.currentText().lower() + if self.mode != 3 else 'p2p', + hexcolor, opacity, amplitude, + ) + elif self.mode > 2: + filter_ = ( + 'showfreqs=s=%sx%s:mode=%s:colors=%s@%s' + ':ascale=%s:fscale=%s' % ( + self.settings.value("outputWidth"), + self.settings.value("outputHeight"), + 'line' if self.mode == 4 else 'bar', + hexcolor, opacity, amplitude, + 'log' if self.mono else 'lin' + ) + ) return [ '-filter_complex', '%s%s%s' - 'showwaves=r=30:s=%sx%s:mode=%s:colors=%s@%s:scale=%s%s%s [v1]; ' - '[v1] scale=%s:%s%s,setpts=2.0*PTS [v]' % ( - 'aevalsrc=sin(1*2*PI*t)*sin(880*2*PI*t),' - if preview and genericPreview else '[0:a] ', - 'compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2' - ',' if self.compress and not preview else ( - 'compand=gain=5,' 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, + '%s%s%s [v1]; ' + '[v1] scale=%s:%s%s [v]' % ( + exampleSound() if preview and genericPreview else '[0:a] ', + 'compand=gain=4,' if self.compress else '', + 'aformat=channel_layouts=mono,' + if self.mono and self.mode < 3 else '', + filter_, ', 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) + ', trim=duration=%s' % "{0:.3f}".format(startPt + 3) if preview else '', ), '-map', '[v]', diff --git a/src/components/waveform.ui b/src/components/waveform.ui index 0e40380..5473f33 100644 --- a/src/components/waveform.ui +++ b/src/components/waveform.ui @@ -66,12 +66,17 @@ - P2p + Point - Point + Frequency Bar + + + + + Frequency Line @@ -180,12 +185,16 @@ - Wave Color + Color - + + + Qt::ImhNone + + @@ -244,10 +253,10 @@ % - 10 + 0 - 400 + 100 100 -- cgit v1.2.3