aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBrianna2017-07-16 23:16:35 -0400
committerGitHub2017-07-16 23:16:35 -0400
commit4becfe3b51ae058dc8600abeac3d1d6b5a1eab76 (patch)
treed0acb70a5fc839a1810a962f2ff9b4a9a931025a /src
parentf420ad69f5f6f3c830fea890a760ce0ce61ba571 (diff)
parentaa464632c64725201dc7584ebf6bf2c3d06b47b6 (diff)
Merge pull request #46 from djfun/extra-sound-options
Extra sound options
Diffstat (limited to 'src')
-rw-r--r--src/__init__.py0
-rw-r--r--src/__main__.py3
-rw-r--r--src/component.py5
-rw-r--r--src/components/sound.py23
-rw-r--r--src/components/sound.ui50
-rw-r--r--src/components/video.py20
-rw-r--r--src/components/video.ui75
-rw-r--r--src/core.py107
-rw-r--r--src/main.py29
-rw-r--r--src/mainwindow.py19
-rw-r--r--src/presetmanager.py1
-rw-r--r--src/preview_thread.py1
-rw-r--r--src/toolkit.py11
-rw-r--r--src/video_thread.py1
14 files changed, 294 insertions, 51 deletions
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/__init__.py
diff --git a/src/__main__.py b/src/__main__.py
new file mode 100644
index 0000000..a68739e
--- /dev/null
+++ b/src/__main__.py
@@ -0,0 +1,3 @@
+from avpython.main import main
+
+main() \ No newline at end of file
diff --git a/src/component.py b/src/component.py
index 2b297d1..adb170e 100644
--- a/src/component.py
+++ b/src/component.py
@@ -178,8 +178,9 @@ class Component(QtCore.QObject):
The first element can be:
- A string (path to audio file),
- Or an object that returns audio data through a pipe
- The second element must be a dictionary of ffmpeg parameters
- to apply to the input stream.
+ The second element must be a dictionary of ffmpeg filters/options
+ to apply to the input stream. See the filter docs for ideas:
+ https://ffmpeg.org/ffmpeg-filters.html
\'''
@classmethod
diff --git a/src/components/sound.py b/src/components/sound.py
index 4a5714b..bd7d002 100644
--- a/src/components/sound.py
+++ b/src/components/sound.py
@@ -17,12 +17,18 @@ class Component(Component):
page.lineEdit_sound.textChanged.connect(self.update)
page.pushButton_sound.clicked.connect(self.pickSound)
+ page.checkBox_chorus.stateChanged.connect(self.update)
+ page.spinBox_delay.valueChanged.connect(self.update)
+ page.spinBox_volume.valueChanged.connect(self.update)
self.page = page
return page
def update(self):
self.sound = self.page.lineEdit_sound.text()
+ self.delay = self.page.spinBox_delay.value()
+ self.volume = self.page.spinBox_volume.value()
+ self.chorus = self.page.checkBox_chorus.isChecked()
super().update()
def previewRender(self, previewWorker):
@@ -46,7 +52,16 @@ class Component(Component):
return "The audio file selected no longer exists!"
def audio(self):
- return (self.sound, {})
+ params = {}
+ if self.delay != 0.0:
+ params['adelay'] = '=%s' % str(int(self.delay * 1000.00))
+ if self.chorus:
+ params['chorus'] = \
+ '=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3'
+ if self.volume != 1.0:
+ params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
+
+ return (self.sound, params)
def pickSound(self):
sndDir = self.settings.value("componentDir", os.path.expanduser("~"))
@@ -66,10 +81,16 @@ class Component(Component):
def loadPreset(self, pr, presetName=None):
super().loadPreset(pr, presetName)
self.page.lineEdit_sound.setText(pr['sound'])
+ self.page.checkBox_chorus.setChecked(pr['chorus'])
+ self.page.spinBox_delay.setValue(pr['delay'])
+ self.page.spinBox_volume.setValue(pr['volume'])
def savePreset(self):
return {
'sound': self.sound,
+ 'chorus': self.chorus,
+ 'delay': self.delay,
+ 'volume': self.volume,
}
def commandHelp(self):
diff --git a/src/components/sound.ui b/src/components/sound.ui
index 5fc00c1..4c11332 100644
--- a/src/components/sound.ui
+++ b/src/components/sound.ui
@@ -88,6 +88,29 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Volume</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_volume">
+ <property name="suffix">
+ <string>x</string>
+ </property>
+ <property name="maximum">
+ <double>10.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.100000000000000</double>
+ </property>
+ <property name="value">
+ <double>1.000000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -100,6 +123,33 @@
</property>
</spacer>
</item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Delay</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_delay">
+ <property name="suffix">
+ <string>s</string>
+ </property>
+ <property name="maximum">
+ <double>9999999.990000000223517</double>
+ </property>
+ <property name="singleStep">
+ <double>0.500000000000000</double>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="checkBox_chorus">
+ <property name="text">
+ <string>Chorus</string>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
<item>
diff --git a/src/components/video.py b/src/components/video.py
index 0b93293..9e3db30 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -45,7 +45,7 @@ class Video:
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter:v', 'scale=%s:%s' % scale(
+ '-filter_complex', '[0:v] scale=%s:%s' % scale(
self.scale, self.width, self.height, str),
'-vcodec', 'rawvideo', '-',
]
@@ -127,6 +127,7 @@ class Component(Component):
page.checkBox_distort.stateChanged.connect(self.update)
page.checkBox_useAudio.stateChanged.connect(self.update)
page.spinBox_scale.valueChanged.connect(self.update)
+ page.spinBox_volume.valueChanged.connect(self.update)
page.spinBox_x.valueChanged.connect(self.update)
page.spinBox_y.valueChanged.connect(self.update)
@@ -139,9 +140,17 @@ class Component(Component):
self.useAudio = self.page.checkBox_useAudio.isChecked()
self.distort = self.page.checkBox_distort.isChecked()
self.scale = self.page.spinBox_scale.value()
+ self.volume = self.page.spinBox_volume.value()
self.xPosition = self.page.spinBox_x.value()
self.yPosition = self.page.spinBox_y.value()
+ if self.useAudio:
+ self.page.label_volume.setEnabled(True)
+ self.page.spinBox_volume.setEnabled(True)
+ else:
+ self.page.label_volume.setEnabled(False)
+ self.page.spinBox_volume.setEnabled(False)
+
super().update()
def previewRender(self, previewWorker):
@@ -193,7 +202,10 @@ class Component(Component):
self.badAudio = False
def audio(self):
- return (self.videoPath, {'map': '-v'})
+ params = {}
+ if self.volume != 1.0:
+ params['volume'] = '=%s:replaygain_noclip=0' % str(self.volume)
+ return (self.videoPath, params)
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
@@ -222,6 +234,7 @@ class Component(Component):
self.page.checkBox_useAudio.setChecked(pr['useAudio'])
self.page.checkBox_distort.setChecked(pr['distort'])
self.page.spinBox_scale.setValue(pr['scale'])
+ self.page.spinBox_volume.setValue(pr['volume'])
self.page.spinBox_x.setValue(pr['x'])
self.page.spinBox_y.setValue(pr['y'])
@@ -233,6 +246,7 @@ class Component(Component):
'useAudio': self.useAudio,
'distort': self.distort,
'scale': self.scale,
+ 'volume': self.volume,
'x': self.xPosition,
'y': self.yPosition,
}
@@ -258,7 +272,7 @@ class Component(Component):
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
- '-filter:v', 'scale=%s:%s' % scale(
+ '-filter_complex', '[0:v] scale=%s:%s' % scale(
self.scale, width, height, str),
'-vcodec', 'rawvideo', '-',
'-ss', '90',
diff --git a/src/components/video.ui b/src/components/video.ui
index 97b7d6f..08d15d3 100644
--- a/src/components/video.ui
+++ b/src/components/video.ui
@@ -10,6 +10,18 @@
<height>197</height>
</rect>
</property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>197</height>
+ </size>
+ </property>
<property name="windowTitle">
<string>Form</string>
</property>
@@ -190,13 +202,6 @@
</widget>
</item>
<item>
- <widget class="QCheckBox" name="checkBox_useAudio">
- <property name="text">
- <string>Use Audio</string>
- </property>
- </widget>
- </item>
- <item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -248,6 +253,62 @@
</layout>
</item>
<item>
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <item>
+ <widget class="QCheckBox" name="checkBox_useAudio">
+ <property name="text">
+ <string>Use Audio</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_volume">
+ <property name="text">
+ <string>Volume</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDoubleSpinBox" name="spinBox_volume">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="suffix">
+ <string>x</string>
+ </property>
+ <property name="minimum">
+ <double>0.000000000000000</double>
+ </property>
+ <property name="maximum">
+ <double>10.000000000000000</double>
+ </property>
+ <property name="singleStep">
+ <double>0.100000000000000</double>
+ </property>
+ <property name="value">
+ <double>1.000000000000000</double>
+ </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>
+ </layout>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
diff --git a/src/core.py b/src/core.py
index 4c12209..a0a028b 100644
--- a/src/core.py
+++ b/src/core.py
@@ -468,7 +468,8 @@ class Core:
'''
Constructs the major ffmpeg command used to export the video
'''
- duration = str(duration)
+ safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
+ duration = "{0:.3f}".format(duration + 0.1) # used by input sources
# Test if user has libfdk_aac
encoders = toolkit.checkOutput(
@@ -526,35 +527,103 @@ class Core:
'-i', inputFile
]
+ # Add extra audio inputs and any needed avfilters
+ # NOTE: Global filters are currently hard-coded here for debugging use
+ globalFilters = 0 # increase to add global filters
extraAudio = [
comp.audio() for comp in self.selectedComponents
if 'audio' in comp.properties()
]
- if extraAudio:
- unwantedVideoStreams = []
- for streamNo, params in enumerate(extraAudio):
+ if extraAudio or globalFilters > 0:
+ # Add -i options for extra input files
+ extraFilters = {}
+ for streamNo, params in enumerate(reversed(extraAudio)):
extraInputFile, params = params
ffmpegCommand.extend([
- '-t', duration,
+ '-t', safeDuration,
+ # Tell ffmpeg about shorter clips (seemingly not needed)
+ # streamDuration = self.getAudioDuration(extraInputFile)
+ # if streamDuration > float(safeDuration)
+ # else "{0:.3f}".format(streamDuration),
'-i', extraInputFile
])
- if 'map' in params and params['map'] == '-v':
- # a video stream to remove
- unwantedVideoStreams.append(streamNo + 1)
+ # Construct dataset of extra filters we'll need to add later
+ for ffmpegFilter in params:
+ if streamNo + 2 not in extraFilters:
+ extraFilters[streamNo + 2] = []
+ extraFilters[streamNo + 2].append((
+ ffmpegFilter, params[ffmpegFilter]
+ ))
+
+ # Start creating avfilters! Popen-style, so don't use semicolons;
+ extraFilterCommand = []
+
+ if globalFilters <= 0:
+ # Dictionary of last-used tmp labels for a given stream number
+ tmpInputs = {streamNo: -1 for streamNo in extraFilters}
+ else:
+ # Insert blank entries for global filters into extraFilters
+ # so the per-stream filters know what input to source later
+ for streamNo in range(len(extraAudio), 0, -1):
+ if streamNo + 1 not in extraFilters:
+ extraFilters[streamNo + 1] = []
+ # Also filter the primary audio track
+ extraFilters[1] = []
+ tmpInputs = {
+ streamNo: globalFilters - 1
+ for streamNo in extraFilters
+ }
+
+ # Add the global filters!
+ # NOTE: list length must = globalFilters, currently hardcoded
+ if tmpInputs:
+ extraFilterCommand.extend([
+ '[%s:a] ashowinfo [%stmp0]' % (
+ str(streamNo),
+ str(streamNo)
+ )
+ for streamNo in tmpInputs
+ ])
+
+ # Now add the per-stream filters!
+ for streamNo, paramList in extraFilters.items():
+ for param in paramList:
+ source = '[%s:a]' % str(streamNo) \
+ if tmpInputs[streamNo] == -1 else \
+ '[%stmp%s]' % (
+ str(streamNo), str(tmpInputs[streamNo])
+ )
+ tmpInputs[streamNo] = tmpInputs[streamNo] + 1
+ extraFilterCommand.append(
+ '%s %s%s [%stmp%s]' % (
+ source, param[0], param[1], str(streamNo),
+ str(tmpInputs[streamNo])
+ )
+ )
- if unwantedVideoStreams:
- ffmpegCommand.extend(['-map', '0'])
- for streamNo in unwantedVideoStreams:
- ffmpegCommand.extend([
- '-map', '-%s:v' % str(streamNo)
- ])
+ # Join all the filters together and combine into 1 stream
+ extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \
+ if tmpInputs else ''
ffmpegCommand.extend([
'-filter_complex',
- 'amix=inputs=%s:duration=first:dropout_transition=3' % str(
- len(extraAudio) + 1
+ extraFilterCommand +
+ '%s amix=inputs=%s:duration=first [a]'
+ % (
+ "".join([
+ '[%stmp%s]' % (str(i), tmpInputs[i])
+ if i in extraFilters else '[%s:a]' % str(i)
+ for i in range(1, len(extraAudio) + 2)
+ ]),
+ str(len(extraAudio) + 1)
),
])
+ # Only map audio from the filters, and video from the pipe
+ ffmpegCommand.extend([
+ '-map', '0:v',
+ '-map', '[a]',
+ ])
+
ffmpegCommand.extend([
# OUTPUT
'-vcodec', vencoder,
@@ -573,7 +642,7 @@ class Core:
ffmpegCommand.append(outputFile)
return ffmpegCommand
- def readAudioFile(self, filename, parent):
+ def getAudioDuration(self, filename):
command = [self.FFMPEG_BIN, '-i', filename]
try:
@@ -588,6 +657,10 @@ class Core:
d = d.split(' ')[3]
d = d.split(':')
duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
+ return duration
+
+ def readAudioFile(self, filename, parent):
+ duration = self.getAudioDuration(filename)
command = [
self.FFMPEG_BIN,
diff --git a/src/main.py b/src/main.py
index 2216d2a..6a9a25e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -2,12 +2,18 @@ from PyQt5 import uic, QtWidgets
import sys
import os
-import core
-import preview_thread
-import video_thread
+def main():
+ if getattr(sys, 'frozen', False):
+ # frozen
+ wd = os.path.dirname(sys.executable)
+ else:
+ # unfrozen
+ wd = os.path.dirname(os.path.realpath(__file__))
+
+ # make local imports work everywhere
+ sys.path.insert(0, wd)
-if __name__ == "__main__":
mode = 'GUI'
if len(sys.argv) > 2:
mode = 'commandline'
@@ -28,22 +34,15 @@ if __name__ == "__main__":
# app.setOrganizationName("audio-visualizer")
if mode == 'commandline':
- from command import *
+ from command import Command
main = Command()
elif mode == 'GUI':
- from mainwindow import *
+ from mainwindow import MainWindow
import atexit
import signal
- if getattr(sys, 'frozen', False):
- # frozen
- wd = os.path.dirname(sys.executable)
- else:
- # unfrozen
- wd = os.path.dirname(os.path.realpath(__file__))
-
window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
# window.adjustSize()
desc = QtWidgets.QDesktopWidget()
@@ -64,3 +63,7 @@ if __name__ == "__main__":
# applicable to both modes
sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/mainwindow.py b/src/mainwindow.py
index 76ed179..ca8e697 100644
--- a/src/mainwindow.py
+++ b/src/mainwindow.py
@@ -305,7 +305,12 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
- QtWidgets.QShortcut("Ctrl+Alt+Shift+R", self.window, self.drawPreview)
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+R", self.window, self.drawPreview
+ )
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
+ )
QtWidgets.QShortcut(
"Ctrl+T", self.window,
@@ -580,6 +585,18 @@ class MainWindow(QtWidgets.QMainWindow):
def showPreviewImage(self, image):
self.previewWindow.changePixmap(image)
+ def showFfmpegCommand(self):
+ from textwrap import wrap
+ command = self.core.createFfmpegCommand(
+ self.window.lineEdit_audioFile.text(),
+ self.window.lineEdit_outputFile.text(),
+ self.core.getAudioDuration(self.window.lineEdit_audioFile.text())
+ )
+ lines = wrap(" ".join(command), 49)
+ self.showMessage(
+ msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
+ )
+
def insertComponent(self, index):
componentList = self.window.listWidget_componentList
stackedWidget = self.window.stackedWidget
diff --git a/src/presetmanager.py b/src/presetmanager.py
index 0028203..6e003a1 100644
--- a/src/presetmanager.py
+++ b/src/presetmanager.py
@@ -6,7 +6,6 @@ from PyQt5 import QtCore, QtWidgets
import string
import os
-import core
import toolkit
diff --git a/src/preview_thread.py b/src/preview_thread.py
index 4ffb7f6..6c33aff 100644
--- a/src/preview_thread.py
+++ b/src/preview_thread.py
@@ -6,7 +6,6 @@ from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
from PIL.ImageQt import ImageQt
-import core
from queue import Queue, Empty
import os
diff --git a/src/toolkit.py b/src/toolkit.py
index 589d8e6..5493f37 100644
--- a/src/toolkit.py
+++ b/src/toolkit.py
@@ -13,11 +13,14 @@ def badName(name):
return any([letter in string.punctuation for letter in name])
+def alphabetizeDict(dictionary):
+ '''Alphabetizes a dict into OrderedDict '''
+ return OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
+
+
def presetToString(dictionary):
- '''Alphabetizes a dict into OrderedDict & returns string repr'''
- return repr(
- OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))
- )
+ '''Returns string repr of a preset'''
+ return repr(alphabetizeDict(dictionary))
def presetFromString(string):
diff --git a/src/video_thread.py b/src/video_thread.py
index 674765a..60db99f 100644
--- a/src/video_thread.py
+++ b/src/video_thread.py
@@ -18,7 +18,6 @@ from threading import Thread, Event
import time
import signal
-import core
from toolkit import openPipe
from frame import Checkerboard