aboutsummaryrefslogtreecommitdiff
path: root/src/avp/components/waveform.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/avp/components/waveform.py')
-rw-r--r--src/avp/components/waveform.py230
1 files changed, 230 insertions, 0 deletions
diff --git a/src/avp/components/waveform.py b/src/avp/components/waveform.py
new file mode 100644
index 0000000..7dc0b99
--- /dev/null
+++ b/src/avp/components/waveform.py
@@ -0,0 +1,230 @@
+from PIL import Image
+from PyQt6 import QtGui, QtCore, QtWidgets
+from PyQt6.QtGui import QColor
+import os
+import math
+import subprocess
+import logging
+
+from ..component import Component
+from ..toolkit.frame import BlankFrame, scale
+from ..toolkit import checkOutput
+from ..toolkit.ffmpeg import (
+ openPipe,
+ closePipe,
+ getAudioDuration,
+ FfmpegVideo,
+ exampleSound,
+)
+
+
+log = logging.getLogger("AVP.Components.Waveform")
+
+
+class Component(Component):
+ name = "Waveform"
+ version = "1.0.0"
+
+ def widget(self, *args):
+ super().widget(*args)
+ self._image = BlankFrame(self.width, self.height)
+
+ self.page.lineEdit_color.setText("255,255,255")
+
+ if hasattr(self.parent, "lineEdit_audioFile"):
+ self.parent.lineEdit_audioFile.textChanged.connect(self.update)
+
+ self.trackWidgets(
+ {
+ "color": self.page.lineEdit_color,
+ "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,
+ },
+ colorWidgets={
+ "color": self.page.pushButton_color,
+ },
+ relativeWidgets=[
+ "x",
+ "y",
+ ],
+ )
+
+ def previewRender(self):
+ self.updateChunksize()
+ frame = self.getPreviewFrame(self.width, self.height)
+ if not frame:
+ return BlankFrame(self.width, self.height)
+ else:
+ 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,
+ debug=True,
+ )
+
+ 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, width, height):
+ genericPreview = self.settings.value("pref_genericPreview")
+ startPt = 0
+ if not genericPreview:
+ inputFile = self.parent.lineEdit_audioFile.text()
+ if not inputFile or not os.path.exists(inputFile):
+ return
+ duration = getAudioDuration(inputFile)
+ if not duration:
+ return
+ startPt = duration / 3
+ if startPt + 3 > duration:
+ startPt += startPt - 3
+
+ command = [
+ self.core.FFMPEG_BIN,
+ "-thread_queue_size",
+ "512",
+ "-r",
+ str(self.settings.value("outputFrameRate")),
+ "-ss",
+ "{0:.3f}".format(startPt),
+ "-i",
+ self.core.junkStream 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",
+ ]
+ )
+ if self.core.logEnabled:
+ logFilename = os.path.join(
+ self.core.logDir, "preview_%s.log" % str(self.compPos)
+ )
+ log.debug("Creating ffmpeg log at %s", logFilename)
+ with open(logFilename, "w") as logf:
+ logf.write(" ".join(command) + "\n\n")
+ with open(logFilename, "a") as logf:
+ pipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=logf,
+ bufsize=10**8,
+ )
+ else:
+ pipe = openPipe(
+ command,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ 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 = "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)
+ genericPreview = self.settings.value("pref_genericPreview")
+ if self.mode < 3:
+ filter_ = (
+ "showwaves="
+ f'r={str(self.settings.value("outputFrameRate"))}:'
+ f's={self.settings.value("outputWidth")}x{self.settings.value("outputHeight")}:'
+ f'mode={self.page.comboBox_mode.currentText().lower() if self.mode != 3 else "p2p"}:'
+ f"colors={hexcolor}@{opacity}:scale={amplitude}"
+ )
+ elif self.mode > 2:
+ filter_ = (
+ f'showfreqs=s={str(self.settings.value("outputWidth"))}x{str(self.settings.value("outputHeight"))}:'
+ f'mode={"line" if self.mode == 4 else "bar"}:'
+ f"colors={hexcolor}@{opacity}"
+ f":ascale={amplitude}:fscale={'log' if self.mono else 'lin'}"
+ )
+
+ baselineHeight = int(self.height * (4 / 1080))
+ return [
+ "-filter_complex",
+ f"{exampleSound('wave', extra='') if preview and genericPreview else '[0:a] '}"
+ f"{'compand=gain=4,' if self.compress else ''}"
+ f"{'aformat=channel_layouts=mono,' if self.mono and self.mode < 3 else ''}"
+ f"{filter_}"
+ f"{', drawbox=x=(iw-w)/2:y=(ih-h)/2:w=iw:h=%s:color=%s@%s' % (baselineHeight, hexcolor, opacity) if self.mode < 2 else ''}"
+ f"{', hflip' if self.mirror else''}"
+ " [v1]; "
+ "[v1] scale=%s:%s%s [v]"
+ % (
+ w,
+ h,
+ ", trim=duration=%s" % "{0:.3f}".format(startPt + 3) if preview else "",
+ ),
+ "-map",
+ "[v]",
+ ]
+
+ def updateChunksize(self):
+ width, height = scale(self.scale, self.width, self.height, int)
+ self.chunkSize = 4 * width * height
+
+ def finalizeFrame(self, imageData):
+ try:
+ image = Image.frombytes(
+ "RGBA",
+ scale(self.scale, self.width, self.height, int),
+ imageData,
+ )
+ self._image = image
+ except ValueError:
+ image = self._image
+ 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