From ec0abd190273b7b636c7085d7caed8220ab09172 Mon Sep 17 00:00:00 2001
From: tassaron
Date: Sun, 16 Jul 2017 14:06:11 -0400
Subject: apply complex filters to audio streams from components
tons of sound options could be given now, + installation using setup.py
---
README.md | 21 +++++-----
setup.py | 24 ++++++++---
src/component.py | 5 ++-
src/components/sound.py | 23 ++++++++++-
src/components/sound.ui | 50 +++++++++++++++++++++++
src/components/video.py | 16 +++++++-
src/components/video.ui | 75 +++++++++++++++++++++++++++++++----
src/core.py | 103 ++++++++++++++++++++++++++++++++++++++++--------
src/main.py | 2 +-
src/toolkit.py | 11 ++++--
10 files changed, 283 insertions(+), 47 deletions(-)
diff --git a/README.md b/README.md
index 658a22d..9149b4f 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,31 @@
audio-visualizer-python
=======================
+**We need a good name that is not as generic as "audio-visualizer-python"!**
-This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. The component setup can be saved as a Project and exporting can be automated using commandline options.
+This is a little GUI tool which creates an audio visualization video from an input audio file. Different components can be added and layered to change the resulting video and add images, videos, gradients, text, etc. Encoding options can be changed with a variety of different output containers.
-The program works on Linux, macOS, and Windows. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and send me a pull request and/or file an issue on this project.
+Projects can be created from the GUI and used in commandline mode for easy automation of video production. Create a template project named `template` with your typical visualizers and watermarks, and add text to the top layer from commandline:
+`avp template -c 99 text "title=Episode 371" -i /this/weeks/audio.ogg -o out`
-I also need a good name that is not as generic as "audio-visualizer-python"!
+For more information use `avp --help` or for help with a particular component use `avp -c 0 componentName help`.
+
+The program works on Linux, macOS, and Windows. If you encounter problems running it or have other bug reports or features that you wish to see implemented, please fork the project and submit a pull request and/or file an issue on this project.
Dependencies
------------
-Python 3, PyQt5, pillow-simd, numpy, and ffmpeg 3.3
+Python 3.4, FFmpeg 3.3, PyQt5, Pillow-SIMD, NumPy
-**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times.
+**Note:** Pillow may be used as a drop-in replacement for Pillow-SIMD if problems are encountered installing. However this will result in much slower video export times. For help troubleshooting installation problems, the * For any problems with installing Pillow-SIMD, see the [Pillow installation guide](http://pillow.readthedocs.io/en/3.1.x/installation.html).
Installation
------------
### Manual installation on Ubuntu 16.04
* Install pip: `sudo apt-get install python3-pip`
-* Install [prerequisites to compile Pillow](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-linux):`sudo apt-get install python3-dev python3-setuptools libtiff5-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk`
-* Prerequisites on **Fedora**:`sudo dnf install python3-devel redhat-rpm-config libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel`
-* Install dependencies from PyPI: `sudo pip3 install pyqt5 numpy pillow-simd`
+* If Pillow is installed, it must be removed. Nothing should break because Pillow-SIMD is simply a drop-in replacement with better performance.
+* Download audio-visualizer-python from this repository and run `sudo pip3 install .` in this directory
* Install `ffmpeg` from the [website](http://ffmpeg.org/) or from a PPA (e.g. [https://launchpad.net/~jonathonf/+archive/ubuntu/ffmpeg-3](https://launchpad.net/~jonathonf/+archive/ubuntu/ffmpeg-3)). NOTE: `ffmpeg` in the standard repos is too old (v2.8). Old versions and `avconv` may be used but full functionality is only guaranteed with `ffmpeg` 3.3 or higher.
-Download audio-visualizer-python from this repository and run it with `python3 main.py`.
+Run the program with `avp` or `python3 -m avpython`
### Manual installation on Windows
* **Warning:** [Compiling Pillow is difficult on Windows](http://pillow.readthedocs.io/en/3.1.x/installation.html#building-on-windows) and required for the best experience.
diff --git a/setup.py b/setup.py
index 4ef6077..71dc51f 100644
--- a/setup.py
+++ b/setup.py
@@ -12,11 +12,25 @@ def package_files(directory):
setup(
name='audio_visualizer_python',
- version='2.0.0',
- description='A little GUI tool to create audio visualization " \
- "videos out of audio files',
+ version='2.0.0rc1',
+ url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui',
license='MIT',
- url='https://github.com/djfun/audio-visualizer-python',
+ description='Create audio visualization videos from a GUI or commandline',
+ long_description="Create customized audio visualization videos and save "
+ "them as Projects to continue editing later. Different components can "
+ "be added and layered to add visualizers, images, videos, gradients, "
+ "text, etc. Use Projects created in the GUI with commandline mode to "
+ "automate your video production workflow without learning any complex "
+ "syntax.",
+ classifiers=[
+ 'Development Status :: 4 - Beta',
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Intended Audience :: End Users/Desktop',
+ 'Topic :: Multimedia :: Video :: Non-Linear Editor',
+ ],
+ keywords=['visualizer', 'visualization', 'commandline video',
+ 'video editor', 'ffmpeg', 'podcast']
packages=[
'avpython',
'avpython.components'
@@ -25,7 +39,7 @@ setup(
package_data={
'avpython': package_files('src'),
},
- install_requires=['olefile', 'Pillow-SIMD', 'PyQt5', 'numpy'],
+ install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'],
entry_points={
'gui_scripts': [
'avp = avpython.main:main'
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
@@ -87,6 +87,29 @@
-
+
-
+
+
+ Volume
+
+
+
+ -
+
+
+ x
+
+
+ 10.000000000000000
+
+
+ 0.100000000000000
+
+
+ 1.000000000000000
+
+
+
-
@@ -100,6 +123,33 @@
+ -
+
+
+ Delay
+
+
+
+ -
+
+
+ s
+
+
+ 9999999.990000000223517
+
+
+ 0.500000000000000
+
+
+
+ -
+
+
+ Chorus
+
+
+
-
diff --git a/src/components/video.py b/src/components/video.py
index 0b93293..e1f182c 100644
--- a/src/components/video.py
+++ b/src/components/video.py
@@ -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,
}
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 @@
197
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 197
+
+
Form
@@ -189,13 +201,6 @@
- -
-
-
- Use Audio
-
-
-
-
@@ -247,6 +252,62 @@
+ -
+
+
-
+
+
+ Use Audio
+
+
+
+ -
+
+
+ Volume
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ x
+
+
+ 0.000000000000000
+
+
+ 10.000000000000000
+
+
+ 0.100000000000000
+
+
+ 1.000000000000000
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
-
diff --git a/src/core.py b/src/core.py
index 4c12209..324b04f 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,99 @@ 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,
'-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!
+ 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 +638,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 +653,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 317237c..6a9a25e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -12,7 +12,7 @@ def main():
wd = os.path.dirname(os.path.realpath(__file__))
# make local imports work everywhere
- sys.path.append(wd)
+ sys.path.insert(0, wd)
mode = 'GUI'
if len(sys.argv) > 2:
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):
--
cgit v1.2.3