Compare commits

...
This repository has been archived on 2020-08-22. You can view files and clone it, but cannot push or open issues or pull requests.

213 Commits

Author SHA1 Message Date
tassaron d78d263996 try to fix flickering live preview during export
by temporarily asking Qt not to clear the widget's paint region
2017-08-06 10:10:04 -04:00
tassaron d04ddba484 image scale needs to be relative 2017-08-03 20:50:22 -04:00
tassaron 98a47a21d9 save presets as floats so project resolution is not relevant
unfortunately this breaks old projects and presets
2017-08-03 20:43:23 -04:00
tassaron ae8a547b77 max spinbox vals scale relatively & less errors when spamming res change
w/h attrs are locked during render so preview thread always get correctly-sized frame
2017-08-03 18:08:49 -04:00
tassaron 219e846984 relativeWidgets might as well be a list 2017-08-03 12:16:57 -04:00
tassaron 6611492b30 relative gradients & last good frame used for preview errors 2017-08-03 00:44:46 -04:00
tassaron 62431a3cfe fontsize is also relative 2017-08-01 22:07:49 -04:00
tassaron 8812c37213 width/height fields should be relative too 2017-08-01 22:04:51 -04:00
tassaron 5784cdbcf8 x/y pixel values update to match output resolution 2017-08-01 21:57:36 -04:00
tassaron 3c1b52205f component class now tracks colorwidgets
so adding new color-selection widgets is now simple
2017-08-01 17:57:39 -04:00
tassaron a472246dab crop aphasermeter so it scales to look bigger 2017-07-30 21:59:42 -04:00
tassaron 65420ce285 more options for the Spectrum component 2017-07-30 21:29:06 -04:00
tassaron b6b45d1270 added Spectrum component with many options
tweaked Waveform, added some ffmpeg logging, made generic widget functions
2017-07-30 13:04:02 -04:00
tassaron db1ea1fc4e generic preview sound for waveform component
with secret preference to use the audio file again
2017-07-29 23:45:37 -04:00
tassaron 1297af61c9 waveform component is working, preview is glitchy 2017-07-29 20:27:46 -04:00
tassaron c1457b6dad starting work on Waveform component
split Video class out of Video component for reuse in Waveform
2017-07-29 13:08:28 -04:00
tassaron 6f8f178778 move code into a new function for future expansion 2017-07-28 22:15:25 -04:00
Brianna ae2af28808 Merge pull request #49 from djfun/toolkit
Code reorganization, more readable component code, better error messages
2017-07-27 22:47:40 -04:00
tassaron 6ecb6df236 some minor bugfixes 2017-07-27 22:15:41 -04:00
tassaron 6fc0398602 quit if project doesn't exist when exporting from commandline 2017-07-27 18:43:02 -04:00
tassaron de1324a6a7 fixed video component eating stdout
+ made height/width into properties to simplify render methods
2017-07-27 17:49:08 -04:00
tassaron 4329b0e947 don't]] always trigger error() 2017-07-25 22:14:34 -04:00
tassaron 03a36d4297 removed pyc 2017-07-25 22:04:50 -04:00
tassaron 15d70474d4 error can be locked within properties()
and simplified the componenterrors again
2017-07-25 22:02:47 -04:00
tassaron 661526b073 repeated errors don't cause repeated windows 2017-07-25 17:44:59 -04:00
tassaron d25dee6afc preset manager uses mainwindow row for every button
and minor changes to componenterrors
2017-07-24 21:22:04 -04:00
DH4 c799305eff Merge pull request #48 from rikai/patch-1
Fixes opening behind other windows on OS X
2017-07-24 04:47:35 -05:00
rikai c517140a51 Fixes opening behind other windows on OS X
Just a quick fix, this will raise the window to the front after it's created.
2017-07-23 20:14:10 -07:00
tassaron d92fc6373f ComponentError exception wraps previewRender
probably where errors are likeliest to be found
2017-07-23 22:55:41 -04:00
tassaron d38109453c better component error messages
fatal errors cancel the export instead of crashing
2017-07-23 17:14:21 -04:00
tassaron bf0890e7c8 components auto-connect & track widgets, less autosave spam
importing toolkit from live interpreter now works
2017-07-23 01:53:54 -04:00
tassaron 450b944b87 add component in context menu, del/ins hotkeys
+ preset manager uses mainwindow component list
2017-07-20 22:37:15 -04:00
tassaron f454814867 ffmpeg functions moved to toolkit, component format simplified
component methods are auto-decorated & settings are now class variables
2017-07-20 20:31:38 -04:00
tassaron b1713d38fa combined toolkit.py & frame.py into toolkit package 2017-07-17 22:07:33 -04:00
Brianna 4becfe3b51 Merge pull request #46 from djfun/extra-sound-options
Extra sound options
2017-07-16 23:16:35 -04:00
tassaron aa464632c6 new hotkey to preview the ffmpeg command 2017-07-16 23:13:00 -04:00
tassaron ec0abd1902 apply complex filters to audio streams from components
tons of sound options could be given now, + installation using setup.py
2017-07-16 14:06:11 -04:00
Brianna f420ad69f5 Merge audio-mixing branch
some basic audio features + more user feedback + merging static components
2017-07-15 19:40:08 -04:00
tassaron 17c8a6703a trying to make setup.py work 2017-07-15 18:59:22 -04:00
tassaron bcb8f27c2e use -t on inputs so ffmpeg knows when to stop filters
+ better feedback in cmd mode
2017-07-15 13:13:53 -04:00
tassaron 62ab09e3f3 Video comp verifies audio streams, videoThread moved into Core
off-by-1 bug fixed in exporting, & use fewer threads for fewer CPUs
2017-07-15 01:00:03 -04:00
tassaron cbbb787615 components automatically drawPreview & save currentPreset
this makes a Component easier to program. also more comments
2017-07-13 21:59:23 -04:00
tassaron d7b678f66d staticComponents list is reversed now 2017-07-13 19:31:00 -04:00
tassaron 06c27a48bc more error messages for blank components 2017-07-13 17:03:25 -04:00
tassaron b7931572a7 added option to include audio from Video components 2017-07-13 14:46:22 -04:00
tassaron 8811b699a9 merge consecutive static components 2017-07-13 00:05:11 -04:00
tassaron 2e37dafd70 fixed various bugs 2017-07-11 06:06:22 -04:00
tassaron 4c3920e630 separated creation of ffmpeg command
for future use to sllow editing the command before starting the export
2017-07-09 21:27:29 -04:00
tassaron f6fbc8d242 a basic Sound component for mixing sounds
to be greatly expanded...
2017-07-09 14:31:19 -04:00
tassaron 94d4acc1f4 more comments + warnings for outdated dependencies 2017-07-09 01:10:06 -04:00
tassaron f027fd4353 more thorough installation directions 2017-07-06 19:52:46 -04:00
tassaron 9986b1c829 image options to mirror & saturate colours
and some friendly docstrings
2017-07-06 12:40:03 -04:00
tassaron 3de45b3629 added basic rotate option to images 2017-07-05 23:04:09 -04:00
tassaron 3f7ead0d1f fixed syntax error
again...
2017-07-05 06:45:30 -04:00
tassaron 52f0f96f16 use decorator for readability 2017-07-05 06:38:36 -04:00
tassaron ad4dc052d8 made code work 2017-07-04 22:18:57 -04:00
tassaron 134779f6e6 updated Windows installation instructions 2017-07-04 20:55:51 -04:00
tassaron 63daa31382 removed pyc 2017-07-04 20:01:14 -04:00
tassaron ba0409829d moved functions into toolkit, fixed CMD appearing on Windows 2017-07-04 19:52:52 -04:00
tassaron 3a6d7ae421 frame-drawing tools for components to share 2017-07-02 21:38:19 -04:00
tassaron 0da275bf1b renamed component base 2017-07-02 20:46:48 -04:00
tassaron 38557f29f9 rm unneeded imports, work on freezing 2017-07-02 14:19:15 -04:00
DH4 0a9002b42c Fixed Text Colors & update button color on manual value change. 2017-07-01 00:01:29 -05:00
tassaron a95ecd7e42 added visualizer options + invalid presets get ignored 2017-06-26 19:07:49 -04:00
tassaron 0c394d77e3 an extra progress bar label for Mac
progressBar text is not visible in native Mac style
2017-06-25 23:05:44 -04:00
tassaron 7b6ef6349b component list not visually disabled
this looks better on Mac
2017-06-25 20:43:06 -04:00
tassaron 4eb2bc9a41 preset delete, rename, save updates both windows 2017-06-25 19:38:58 -04:00
Brianna 9fe370d472 Update readme to match newgui 2017-06-25 18:28:40 -04:00
tassaron 252639e9a2 renamed Original Audio Visualization to Classic Visualizer 2017-06-25 18:12:16 -04:00
tassaron f284acbf19 whitelist is more sensible here than blacklist 2017-06-25 16:13:54 -04:00
Brianna f86c33d0e5 Merge pull request #36 from djfun/newgui-project-settings
Project files save more settings
2017-06-25 15:53:49 -04:00
tassaron 2c82a65d1b needs more tuples 2017-06-25 15:50:31 -04:00
tassaron fc2951379c newlines make project file easier to read 2017-06-25 15:34:33 -04:00
tassaron 6a1a5cd6eb --export commandline option
overrides -i and -o to use saved fields from a project file
2017-06-25 15:31:42 -04:00
tassaron 675a06dd4c project files save settings & out/in fields 2017-06-25 14:27:56 -04:00
Brianna 55423ca4aa Merge pull request #35 from djfun/newgui-bugfixes
Newgui bugfixes
2017-06-25 10:40:48 -04:00
tassaron a2838a0c38 disable some hotkeys while encoding, more friendly error messages 2017-06-25 10:36:32 -04:00
tassaron 45b55d8e2f fixed lack of asterisks after openProject, added asterisk to window title 2017-06-24 23:40:32 -04:00
tassaron e32ba958cb fixing bugs 2017-06-24 23:12:41 -04:00
Brianna 1bb67d1513 Merge pull request #34 from djfun/feature-newgui-qt5
Update to Qt5
2017-06-24 20:02:14 -04:00
tassaron 4d955c5a06 merged with feature-newgui 2017-06-24 19:54:09 -04:00
DH4 83d55593d0 Fixed QtWidgets not imported on some components. 2017-06-23 23:39:22 -05:00
tassaron 680214f518 qt5 fixes
also pep8 compliance
2017-06-23 23:00:24 -04:00
DH4 e92e9d79f9 QT5 Conversion + Directory Structure 2017-06-23 17:38:05 -05:00
tassaron 68ac0cf755 pep8 cleanup 2017-06-23 17:14:39 -04:00
DH4 f3da72ea54 Merge branch 'feature-newgui' of github.com:djfun/audio-visualizer-python into feature-newgui 2017-06-23 10:46:54 -05:00
DH4 84ceff7f54 Fixed Ctrl+End Hotkey 2017-06-23 10:46:32 -05:00
tassaron f8628333af Fixed crash after creating a new project on commandline 2017-06-23 07:42:46 -04:00
tassaron 407ba57e4e fixed crash after creating a project on commandline
because it didn't check for errors while saving
2017-06-23 07:41:11 -04:00
Brianna 5607bff61f Merge pull request #32 from djfun/newgui-commandline
making commandline features from master work with the new component system
2017-06-23 07:18:46 -04:00
Brianna 7d4fb78438 Merge branch 'feature-newgui' into newgui-commandline 2017-06-23 07:12:40 -04:00
DH4 8c9914850e cx_freeze Path Updates 2017-06-23 02:39:56 -05:00
DH4 60d62599f7 Implement Hotkeys 2017-06-23 01:20:59 -05:00
tassaron 3c903794e3 more commandline component options
commandline options that existed before the redesign are now back
2017-06-22 22:23:04 -04:00
tassaron 49cda1bf3a can send multiple arguments to a component 2017-06-22 20:31:04 -04:00
tassaron b21a953dda bugfixes 2017-06-22 19:59:31 -04:00
tassaron 5c74d496a9 preset-loading and basic args from commandline
also made some docstrings more informative
2017-06-22 18:40:34 -04:00
tassaron 82011de966 able to create components from commandline
TODO: make components respond to argument
2017-06-18 21:49:00 -04:00
tassaron 044fddfa9c basic commandline functionality using 3 args
needs more args so components can be modified without gui
2017-06-18 14:46:08 -04:00
tassaron 2c63b05376 exports to ~ if no dir given, fix infinite loop when cancelling 2017-06-17 19:08:18 -04:00
tassaron ffc5966042 ask to save changes before changing current project
also limited total # of components to 50
2017-06-17 11:15:24 -04:00
tassaron aa9926590b spread option for gradients 2017-06-16 20:43:40 -04:00
tassaron 4de39ebe07 color component size, position, and gradient options 2017-06-16 20:01:27 -04:00
tassaron 4b1058781d added width and height to color.ui 2017-06-16 00:01:34 -04:00
Brianna fc7ee6d8e5 Redesigned preset UI + video & image component scaling/positioning
Added preset manager
2017-06-15 23:21:34 -04:00
tassaron ee8031925f drag events for component list now working! 2017-06-15 23:13:36 -04:00
tassaron c05efc73ee various bugfixes, blankFrame method for components
don't crash from broken project files or nonexistent videopaths, and shareable common paths in core.py
2017-06-15 22:15:03 -04:00
tassaron 8603fa12e3 video scaling, position and distortion 2017-06-15 15:09:45 -04:00
tassaron cb639e5c7c clear preset button, disable New Project during export
enable preset manager during export, and clear deleted presets from project files when opened
2017-06-15 11:36:26 -04:00
tassaron 8846af57ba image component stretch/scale/x/y options 2017-06-14 19:37:47 -04:00
tassaron 807e37bddd no keyerror when opening new preset 2017-06-14 17:36:46 -04:00
tassaron 2ad14b7d6c asterisk next to modified preset is more accurate
hopefully
2017-06-13 22:47:18 -04:00
tassaron 307d499f9a adding an asterisk to modified, unsaved presets
flags for unsaved changes saved in project files
2017-06-12 22:34:37 -04:00
tassaron dbbefbf70e split up openProject code for use in importPreset 2017-06-11 23:29:13 -04:00
tassaron 28f07272cc using tab in component list updates widget
and more understandable function names for writing/reading presets
2017-06-11 17:03:40 -04:00
tassaron be5d47f863 can't right-click empty space + color eyedropper 2017-06-11 12:52:29 -04:00
tassaron 59c2c090ab made basic export function, moved more code into core 2017-06-10 14:52:01 -04:00
tassaron b048312882 close button works, dialogs properly parented
hint text wording changed by IamDH4's suggestion
2017-06-10 12:10:05 -04:00
tassaron d3f979ef24 start connecting import/export buttons 2017-06-08 22:56:33 -04:00
tassaron c51d86dd74 preset searchbar works, ui experimentally changed
closebutton where I keep expecting it to be
2017-06-08 22:31:02 -04:00
tassaron 4fc73f1e09 rename and delete buttons in preset manager 2017-06-08 20:32:25 -04:00
tassaron bb1e54b31e saved preset titles, code clean-ups
componentList drag'n'drop disabled for now; will work on it in another branch
2017-06-08 16:50:48 -04:00
tassaron 6079c4fd24 drag'n'drop componentList, move component code to core.py
FIXME: finish implementing drag'n'drop, Down button
2017-06-08 09:56:57 -04:00
tassaron 292d21c203 added submenu for opening presets, moved code 2017-06-07 23:22:55 -04:00
tassaron 6093e701e1 laying some foundations for new preset implementation 2017-06-07 20:30:37 -04:00
tassaron 6f2f02b709 newProject method & various small fixes 2017-06-07 17:09:28 -04:00
DH4 acf2290025 Created projects and presets button. FIXME: Hookup New Project menu item. Hookup preset manager. 2017-06-07 14:32:05 -05:00
DH4 02795503d0 Added Bitrate Selection 2017-06-07 12:33:22 -05:00
DH4 fed9481d9a Merge branch 'feature-newgui' of github.com:djfun/audio-visualizer-python into feature-newgui 2017-06-07 12:00:35 -05:00
DH4 e6beca94a3 Added Encoder Settings, FIXME: Add bitrate options. 2017-06-07 11:59:59 -05:00
tassaron c946133da9 changed video init to use keywords 2017-06-06 20:50:53 -04:00
DH4 231af74ea2 Code cleanup 2017-06-06 10:14:39 -05:00
DH4 0948afd6e8 Use bytes instead of encoding a PNG in text module. 2017-06-06 08:53:27 -05:00
tassaron 6c78c96d80 fixed empty preview frame bug 2017-06-06 08:55:22 -04:00
DH4 6a1deb9b78 Add checkerboard for transpart frames. 2017-06-06 04:04:42 -05:00
DH4 0d1e7459e1 Revert default window size. 2017-06-06 02:08:49 -05:00
DH4 4920fcc034 Merge branch 'component-backgrounds' of github.com:IamDH4/audio-visualizer-python into feature-newgui 2017-06-06 02:07:13 -05:00
DH4 7946e98f22 When out of frames, send last frame to buffer. Added ability to loop video. 2017-06-06 01:57:48 -05:00
tassaron 47509ae2b1 added framebuffer to keep frames in order
NOT WORKING: end of video detection
2017-06-06 01:40:26 -04:00
DH4 be18deece5 Performance Tuning. FIXME: Video component frames are rendered out of order. Video component creates a severe performance bottleneck. 2017-06-05 04:54:58 -05:00
DH4 0a8a2fdf71 Font Offset 2017-06-05 02:38:10 -05:00
tassaron e58a1d0b2d frames are taken straight from the in_pipe 2017-06-04 22:57:19 -04:00
tassaron 277e86f279 not dumping frames anymore, but not working yet either
will finish later
2017-06-04 20:27:43 -04:00
tassaron 39e66ffa2d video component almost working, rm hardcoded backgrounds 2017-06-04 13:00:36 -04:00
tassaron 443c65455a half-finished video component
will finish tomorrow
2017-06-04 00:19:10 -04:00
tassaron cfb8e17b63 basic image component 2017-06-03 22:58:40 -04:00
tassaron 5480b20d40 Merge branch 'feature-newgui' of https://github.com/IamDH4/audio-visualizer-python into component-backgrounds 2017-06-03 21:01:18 -04:00
DH4 5b78a26d80 Add component inserts on top. 2017-06-03 19:54:53 -05:00
tassaron 825b7af6e3 start of background replacement components 2017-06-03 20:39:32 -04:00
DH4 e6d119769f Render order reversed to match component list. 2017-06-03 19:29:25 -05:00
DH4 a3557cbc4f UI Updates, encode lockout, added encoder-options.json. FIXME: Add encoder options to the UI. 2017-06-03 19:10:27 -05:00
DH4 cf197904b8 Add component changed to menu. 2017-06-03 16:46:52 -05:00
Brianna 2d515540aa autosave to help restore unsaved projects in case of a crash 2017-06-03 15:41:57 -04:00
DH4 aae04194a0 Better text defaults 2017-06-03 14:38:21 -05:00
tassaron 2cbae481c5 autosave to help restore unsaved projects in case of a crash 2017-06-03 15:24:52 -04:00
DH4 d417833193 Layout Fixes 2017-06-03 14:16:25 -05:00
DH4 c008571ae2 Changed UI default to Input Settings 2017-06-03 13:35:13 -05:00
tassaron f0ab2f53d6 section structure in avp files 2017-06-03 11:08:52 -04:00
tassaron 0bef283f8d saved project dirs 2017-06-03 10:01:47 -04:00
tassaron fccdee45b2 absolute path to main ui, bg video fixed 2017-06-03 08:46:18 -04:00
DH4 4b56660177 Changed encoding update to signal/slot. 2017-06-03 00:07:30 -05:00
DH4 e33caa9179 Threading changes. 2017-06-02 08:14:04 -05:00
DH4 53598f7a85 Progressbar enhancement. 2017-06-02 03:30:51 -05:00
DH4 73a0492585 Cancel button stops pre-processing too. 2017-06-02 00:30:44 -05:00
DH4 6bf36d0324 Added ability to cancel export. 2017-06-01 23:24:13 -05:00
DH4 7d8e9ab3b1 Added aspect ratio scaling to preview area. 2017-06-01 22:46:45 -05:00
Brianna 30f2ea12df projects can be saved and loaded 2017-06-01 22:17:12 -04:00
tassaron 2768084b30 resolution comboBox gets updated 2017-06-01 20:31:15 -04:00
tassaron d31add0d95 tidying up 2017-06-01 20:21:26 -04:00
tassaron 610db20606 settings.ini now saved/loaded with projects 2017-06-01 19:54:50 -04:00
tassaron 00f5c88584 components can be saved and loaded as projects 2017-06-01 18:47:47 -04:00
tassaron 907ba33e93 a handy showMessage() method
and starting on the project buttons
2017-06-01 17:34:04 -04:00
tassaron 1a24922fee restrict presets to boring characters 2017-06-01 16:16:42 -04:00
tassaron f55d7d1206 saveable titleFont, xPosition glitches fixed 2017-06-01 13:17:36 -04:00
Brianna 11e5ec0439 Merge pull request #6 from IamDH4/feature-rendering-engine
a rendering engine that uses more threads
2017-06-01 12:07:20 -04:00
DH4 fcbe211bf1 Performance boost for static backgrounds. moved drawBars() inside class. 2017-06-01 11:01:51 -05:00
DH4 43073cbd42 Fixed spectrum rendering. Fixed multiple static renders. 2017-06-01 09:52:40 -05:00
DH4 6620f48bfd Merge pull request #5 from IamDH4/tassaron-render-test
minor fixes, comments
2017-06-01 08:11:53 -05:00
tassaron 2afdba04fd minor fixes, comments 2017-06-01 09:05:20 -04:00
tassaron 0b210fb4f0 fix syntax error 2017-05-31 18:00:10 -04:00
Brianna a1ae1dfde9 basic preset functionality 2017-05-31 16:22:58 -04:00
DH4 1eeb763dc3 Fixed frame loop bug. 2017-05-31 04:01:18 -05:00
DH4 c21d6f5ea7 New rendering engine partially implemented. Also added a live preview during rendering. FIXME: spectrum is out of sync / rendering too quickly. 2017-05-31 02:15:09 -05:00
tassaron 9be8f742c6 get confirmation when overwriting presets 2017-05-30 22:34:25 -04:00
tassaron 5295a6d9ae presets are working
except for font because it can't be represented as a string
2017-05-30 22:05:56 -04:00
tassaron ca7e8bdb0d the most simple way of saving dictionaries 2017-05-30 19:31:10 -04:00
Brianna 7240f25deb added static components which don't get called each frame when rendering 2017-05-29 20:47:51 -04:00
Brianna 369ac2a855 Merge branch 'feature-newgui' into feature-newgui-lesstextrenders 2017-05-29 20:47:07 -04:00
tassaron d1852619df Merge branch 'feature-newgui' of https://www.github.com/IamDH4/audio-visualizer-python into feature-newgui 2017-05-29 20:40:59 -04:00
tassaron 8dd7b7d59a added component base class 2017-05-29 20:39:11 -04:00
tassaron 37fd68fd2b remove test video 2017-05-29 17:40:40 -04:00
tassaron 75f1e8af76 title text does not need to generate a new image each frame 2017-05-29 17:38:28 -04:00
Brianna 025bc2c2e6 Merge pull request #2 from IamDH4/feature-newgui-presets
basic start to implementing presets
2017-05-28 23:00:48 -04:00
tassaron db7acbf3ea save empty presets, comboBox populates with preset names 2017-05-28 22:58:13 -04:00
tassaron c0920da4ff savePreset creates a file 2017-05-28 21:24:51 -04:00
tassaron ce414ff960 turned openPreset button into comboBox to fit a new design 2017-05-28 19:50:29 -04:00
tassaron 39944a56a8 create data directory structure 2017-05-28 19:08:50 -04:00
DH4 6433f6d580 Merge pull request #1 from IamDH4/feature-newgui-bugfix-listorder
Fixed component list not affecting render order.  FIXME Reverse the r…
2017-05-28 15:51:20 -05:00
DH4 b2e3716a29 Fixed component list not affecting render order. FIXME Reverse the render order 2017-05-28 15:46:59 -05:00
tassaron d9641d8db3 slight fixes to component UIs 2017-05-28 16:30:18 -04:00
tassaron fa89cd38f2 slight fixes to component UIs
and adding a component changes the stackedWidget
2017-05-28 16:26:06 -04:00
DH4 719e9a4ddf Implemented change list order 2017-05-28 15:05:08 -05:00
DH4 e3079f7a67 Fixed Stack & list sync bug. 2017-05-28 14:19:06 -05:00
tassaron 5101b439df fixed travelling text bug 2017-05-28 14:49:35 -04:00
tassaron 5ed79ff5c6 rm old ui file 2017-05-28 14:20:36 -04:00
tassaron e0eed5bff4 title text is now a component
plus numerous bugs removed and added
2017-05-28 14:19:28 -04:00
DH4 d9a5f2dd34 Fixed Resolution Change in preview. Removed debugging print statements. 2017-05-28 07:36:34 -05:00
DH4 75c1c65c9d Integration with tassaron2 modular design. True Alpha Rendering added, several bug fixes. 2017-05-28 06:34:34 -05:00
DH4 fe13268a84 Created a new UI, several new features to be implemented. FIXME: Resolution change requires an application restart. 2017-05-27 14:32:08 -05:00
DH4 86c6ac8762 Removed .vscode directory and updated .gitignore 2017-05-27 05:40:22 -05:00
DH4 1a8acdbed0 Fixed Scaling Bugs 2017-05-27 04:49:26 -05:00
DH4 f2329e9366 Added automatic scaling of Image and bars. Set title x/y position, and font size based on scale. 2017-05-27 03:06:17 -05:00
DH4 eaee0ab233 Removed hardcoded parameters. Defaults loaded at runtime. 2017-05-26 23:06:47 -05:00
44 changed files with 9892 additions and 1411 deletions

16
.gitignore vendored
View File

@ -1,3 +1,15 @@
__pycache__
settings.ini
build/*
*.py[cod]
build/*
dist/*
env/*
.vscode/*
*.mkv
*.mp4
*.zip
*.tar
*.tar.*
*.exe
ffmpeg
*.bak
*~

View File

@ -1,38 +1,45 @@
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.
You can also give it a background image and set a title text.
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.
I have tested the program on Linux (Ubuntu 14.10) and Windows (Windows 7), it should also work on Mac OS X. 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
------------
You need Python 3, PyQt4, PIL (or Pillow), numpy and the program ffmpeg, which is used to read the audio and render the video.
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. For help installing Pillow-SIMD, see the [Pillow installation guide](http://pillow.readthedocs.io/en/3.1.x/installation.html).
Installation
------------
### Manual installation on Ubuntu
* Get all the python stuff: `sudo apt-get install python3 python3-pyqt4 python3-pil python3-numpy`
* Get ffmpeg/avconv:
You can either use `avconv` from the standard repositories (package `libav-tools`) or get `ffmpeg` from the [website](http://ffmpeg.org/) or from a PPA (e.g. [https://launchpad.net/~jon-severinsson/+archive/ubuntu/ffmpeg](https://launchpad.net/~jon-severinsson/+archive/ubuntu/ffmpeg). The program does automatically detect if you don't have the ffmpeg binary and tries to use avconv instead.
### Manual installation on Ubuntu 16.04
* Install pip: `sudo apt-get install python3-pip`
* 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
* Download and install Python 3.4 from [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/)
* Download and install PyQt4 for Python 3.4 and Qt4 from [http://www.riverbankcomputing.co.uk/software/pyqt/download](http://www.riverbankcomputing.co.uk/software/pyqt/download)
* Download and install numpy from [http://www.scipy.org/scipylib/download.html](http://www.scipy.org/scipylib/download.html). There is an installer available, make sure to get the one for Python 3.4
* Download and install Pillow from [https://pypi.python.org/pypi/Pillow/3.3.0](https://pypi.python.org/pypi/Pillow/3.3.0)
* **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.
* Download and install Python 3.6 from [https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/)
* Add Python to your system PATH (it will ask during the installation process).
* Brave treacherous valley of getting prerequisites to [compile Pillow on Windows](https://www.pypkg.com/pypi/pillow-simd/f/winbuild/README.md). This is necessary because binary builds for Pillow-SIMD are not available.
* **Alternative:** install Pillow instead of Pillow-SIMD, for which binaries *are* available. However this will result in much slower video export times.
* Open command prompt and run: `pip install pyqt5 numpy pillow-simd`
* Download and install ffmpeg from [https://www.ffmpeg.org/download.html](https://www.ffmpeg.org/download.html). You can use the static builds.
* Add ffmpeg to your system PATH environment variable.
* Add ffmpeg to your system PATH, too. [How to edit the PATH on Windows.](https://www.java.com/en/download/help/path.xml)
Download audio-visualizer-python from this repository and run it from the command line with `C:\Python34\python.exe main.py`.
Download audio-visualizer-python from this repository and run it from the command line with `python main.py`.
### Manual installation on macOS
### Manual installation on macOS **[Outdated]**
* Install [Homebrew](http://brew.sh/)
* Use the following commands to install the needed dependencies:

210
core.py
View File

@ -1,210 +0,0 @@
import sys, io, os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtGui import QPainter, QColor
from os.path import expanduser
import subprocess as sp
import numpy
from PIL import Image, ImageDraw, ImageFont
from PIL.ImageQt import ImageQt
import tempfile
from shutil import rmtree
import atexit
class Core():
def __init__(self):
self.lastBackgroundImage = ""
self._image = None
self.FFMPEG_BIN = self.findFfmpeg()
self.tempDir = None
atexit.register(self.deleteTempDir)
def findFfmpeg(self):
if sys.platform == "win32":
return "ffmpeg.exe"
else:
try:
with open(os.devnull, "w") as f:
sp.check_call(['ffmpeg', '-version'], stdout=f, stderr=f)
return "ffmpeg"
except:
return "avconv"
def parseBaseImage(self, backgroundImage, preview=False):
''' determines if the base image is a single frame or list of frames '''
if backgroundImage == "":
return []
else:
_, bgExt = os.path.splitext(backgroundImage)
if not bgExt == '.mp4':
return [backgroundImage]
else:
return self.getVideoFrames(backgroundImage, preview)
def drawBaseImage(self, backgroundFile, titleText, titleFont, fontSize, alignment,\
xOffset, yOffset, textColor, visColor):
if backgroundFile == '':
im = Image.new("RGB", (1280, 720), "black")
else:
im = Image.open(backgroundFile)
if self._image == None or not self.lastBackgroundImage == backgroundFile:
self.lastBackgroundImage = backgroundFile
# resize if necessary
if not im.size == (1280, 720):
im = im.resize((1280, 720), Image.ANTIALIAS)
self._image = ImageQt(im)
self._image1 = QtGui.QImage(self._image)
painter = QPainter(self._image1)
font = titleFont
font.setPixelSize(fontSize)
painter.setFont(font)
painter.setPen(QColor(*textColor))
yPosition = yOffset
fm = QtGui.QFontMetrics(font)
if alignment == 0: #Left
xPosition = xOffset
if alignment == 1: #Middle
xPosition = xOffset - fm.width(titleText)/2
if alignment == 2: #Right
xPosition = xOffset - fm.width(titleText)
painter.drawText(xPosition, yPosition, titleText)
painter.end()
buffer = QtCore.QBuffer()
buffer.open(QtCore.QIODevice.ReadWrite)
self._image1.save(buffer, "PNG")
strio = io.BytesIO()
strio.write(buffer.data())
buffer.close()
strio.seek(0)
return Image.open(strio)
def drawBars(self, spectrum, image, color):
imTop = Image.new("RGBA", (1280, 360))
draw = ImageDraw.Draw(imTop)
r, g, b = color
color2 = (r, g, b, 50)
for j in range(0, 63):
draw.rectangle((10 + j * 20, 325, 10 + j * 20 + 20, 325 - spectrum[j * 4] * 1 - 10), fill=color2)
draw.rectangle((15 + j * 20, 320, 15 + j * 20 + 10, 320 - spectrum[j * 4] * 1), fill=color)
imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
im = Image.new("RGB", (1280, 720), "black")
im.paste(image, (0, 0))
im.paste(imTop, (0, 0), mask=imTop)
im.paste(imBottom, (0, 360), mask=imBottom)
return im
def readAudioFile(self, filename):
command = [ self.FFMPEG_BIN,
'-i', filename,
'-f', 's16le',
'-acodec', 'pcm_s16le',
'-ar', '44100', # ouput will have 44100 Hz
'-ac', '1', # mono (set to '2' for stereo)
'-']
in_pipe = sp.Popen(command, stdout=sp.PIPE, stderr=sp.DEVNULL, bufsize=10**8)
completeAudioArray = numpy.empty(0, dtype="int16")
while True:
# read 2 seconds of audio
raw_audio = in_pipe.stdout.read(88200*4)
if len(raw_audio) == 0:
break
audio_array = numpy.fromstring(raw_audio, dtype="int16")
completeAudioArray = numpy.append(completeAudioArray, audio_array)
# print(audio_array)
in_pipe.kill()
in_pipe.wait()
# add 0s the end
completeAudioArrayCopy = numpy.zeros(len(completeAudioArray) + 44100, dtype="int16")
completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
completeAudioArray = completeAudioArrayCopy
return completeAudioArray
def transformData(self, i, completeAudioArray, sampleSize, smoothConstantDown, smoothConstantUp, lastSpectrum):
if len(completeAudioArray) < (i + sampleSize):
sampleSize = len(completeAudioArray) - i
window = numpy.hanning(sampleSize)
data = completeAudioArray[i:i+sampleSize][::1] * window
paddedSampleSize = 2048
paddedData = numpy.pad(data, (0, paddedSampleSize - sampleSize), 'constant')
spectrum = numpy.fft.fft(paddedData)
sample_rate = 44100
frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
# filter the noise away
# y[y<80] = 0
y = 20 * numpy.log10(y)
y[numpy.isinf(y)] = 0
if lastSpectrum is not None:
lastSpectrum[y < lastSpectrum] = y[y < lastSpectrum] * smoothConstantDown + lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
lastSpectrum[y >= lastSpectrum] = y[y >= lastSpectrum] * smoothConstantUp + lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
else:
lastSpectrum = y
x = frequencies[0:int(paddedSampleSize/2) - 1]
return lastSpectrum
def deleteTempDir(self):
if self.tempDir and os.path.exists(self.tempDir):
rmtree(self.tempDir)
def getVideoFrames(self, videoPath, firstOnly=False):
self.tempDir = os.path.join(tempfile.gettempdir(), 'audio-visualizer-python-data')
# recreate the temporary directory so it is empty
self.deleteTempDir()
os.mkdir(self.tempDir)
if firstOnly:
filename = 'preview%s.jpg' % os.path.basename(videoPath).split('.', 1)[0]
options = '-ss 10 -vframes 1'
else:
filename = '$frame%05d.jpg'
options = ''
sp.call( \
'%s -i "%s" -y %s "%s"' % ( \
self.FFMPEG_BIN,
videoPath,
options,
os.path.join(self.tempDir, filename)
),
shell=True
)
return sorted([os.path.join(self.tempDir, f) for f in os.listdir(self.tempDir)])
@staticmethod
def RGBFromString(string):
''' turns an RGB string like "255, 255, 255" into a tuple '''
try:
tup = tuple([int(i) for i in string.split(',')])
if len(tup) != 3:
raise ValueError
for i in tup:
if i > 255 or i < 0:
raise ValueError
return tup
except:
return (255, 255, 255)

59
freeze.py Normal file
View File

@ -0,0 +1,59 @@
from cx_Freeze import setup, Executable
import sys
import os
from setup import __version__
deps = [os.path.join('src', p) for p in os.listdir('src') if p]
deps.append('ffmpeg.exe' if sys.platform == 'win32' else 'ffmpeg')
buildOptions = dict(
excludes=[
"apport",
"apt",
"curses",
"distutils",
"email",
"html",
"http",
"xmlrpc",
"nose",
'tkinter',
],
includes=[
"encodings",
"json",
"filecmp",
"numpy.core._methods",
"numpy.lib.format",
"PyQt5.QtCore",
"PyQt5.QtGui",
"PyQt5.QtWidgets",
"PyQt5.uic",
"PIL.Image",
"PIL.ImageQt",
"PIL.ImageDraw",
"PIL.ImageEnhance",
],
include_files=deps,
)
base = 'Win32GUI' if sys.platform == 'win32' else None
executables = [
Executable(
'src/main.py',
base=base,
targetName='audio-visualizer-python'
),
]
setup(
name='audio-visualizer-python',
version=__version__,
description='GUI tool to render visualization videos of audio files',
options=dict(build_exe=buildOptions),
executables=executables
)

341
main.py
View File

@ -1,341 +0,0 @@
import sys, io, os
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtGui import QPainter, QColor, QFont
from os.path import expanduser
import subprocess as sp
import numpy
from PIL import Image, ImageDraw, ImageFont
from PIL.ImageQt import ImageQt
import atexit
from queue import Queue
from PyQt4.QtCore import QSettings
import signal
import preview_thread, core, video_thread
class Command(QtCore.QObject):
videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str)
def __init__(self):
QtCore.QObject.__init__(self)
import argparse
self.parser = argparse.ArgumentParser(description='Create a visualization for an audio file')
self.parser.add_argument('-i', '--input', dest='input', help='input audio file', required=True)
self.parser.add_argument('-o', '--output', dest='output', help='output video file', required=True)
self.parser.add_argument('-b', '--background', dest='bgimage', help='background image file', required=True)
self.parser.add_argument('-t', '--text', dest='text', help='title text', required=True)
self.parser.add_argument('-f', '--font', dest='font', help='title font', required=False)
self.parser.add_argument('-s', '--fontsize', dest='fontsize', help='title font size', required=False)
self.parser.add_argument('-c', '--textcolor', dest='textcolor', help='title text color in r,g,b format', required=False)
self.parser.add_argument('-C', '--viscolor', dest='viscolor', help='visualization color in r,g,b format', required=False)
self.parser.add_argument('-x', '--xposition', dest='xposition', help='x position', required=False)
self.parser.add_argument('-y', '--yposition', dest='yposition', help='y position', required=False)
self.parser.add_argument('-a', '--alignment', dest='alignment', help='title alignment', required=False, type=int, choices=[0, 1, 2])
self.args = self.parser.parse_args()
self.settings = QSettings('settings.ini', QSettings.IniFormat)
# load colours as tuples from comma-separated strings
self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255'))
self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255'))
if self.args.textcolor:
self.textColor = core.Core.RGBFromString(self.args.textcolor)
if self.args.viscolor:
self.visColor = core.Core.RGBFromString(self.args.viscolor)
# font settings
if self.args.font:
self.font = QFont(self.args.font)
else:
self.font = QFont(self.settings.value("titleFont", QFont()))
if self.args.fontsize:
self.fontsize = int(self.args.fontsize)
else:
self.fontsize = int(self.settings.value("fontSize", 35))
if self.args.alignment:
self.alignment = int(self.args.alignment)
else:
self.alignment = int(self.settings.value("alignment", 0))
if self.args.xposition:
self.textX = int(self.args.xposition)
else:
self.textX = int(self.settings.value("xPosition", 70))
if self.args.yposition:
self.textY = int(self.args.yposition)
else:
self.textY = int(self.settings.value("yPosition", 375))
ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~"))
self.videoThread = QtCore.QThread(self)
self.videoWorker = video_thread.Worker(self)
self.videoWorker.moveToThread(self.videoThread)
self.videoWorker.videoCreated.connect(self.videoCreated)
self.videoThread.start()
self.videoTask.emit(self.args.bgimage,
self.args.text,
self.font,
self.fontsize,
self.alignment,
self.textX,
self.textY,
self.textColor,
self.visColor,
self.args.input,
self.args.output)
def videoCreated(self):
self.videoThread.quit()
self.videoThread.wait()
self.cleanUp()
def cleanUp(self):
self.settings.setValue("titleFont", self.font.toString())
self.settings.setValue("alignment", str(self.alignment))
self.settings.setValue("fontSize", str(self.fontsize))
self.settings.setValue("xPosition", str(self.textX))
self.settings.setValue("yPosition", str(self.textY))
self.settings.setValue("visColor", '%s,%s,%s' % self.visColor)
self.settings.setValue("textColor", '%s,%s,%s' % self.textColor)
sys.exit(0)
class Main(QtCore.QObject):
newTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple)
processTask = QtCore.pyqtSignal()
videoTask = QtCore.pyqtSignal(str, str, QFont, int, int, int, int, tuple, tuple, str, str)
def __init__(self, window):
QtCore.QObject.__init__(self)
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window
self.core = core.Core()
self.settings = QSettings('settings.ini', QSettings.IniFormat)
# load colors as tuples from a comma-separated string
self.textColor = core.Core.RGBFromString(self.settings.value("textColor", '255, 255, 255'))
self.visColor = core.Core.RGBFromString(self.settings.value("visColor", '255, 255, 255'))
self.previewQueue = Queue()
self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start()
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.processTask.emit)
self.timer.start(500)
window.pushButton_selectInput.clicked.connect(self.openInputFileDialog)
window.pushButton_selectOutput.clicked.connect(self.openOutputFileDialog)
window.pushButton_createVideo.clicked.connect(self.createAudioVisualisation)
window.pushButton_selectBackground.clicked.connect(self.openBackgroundFileDialog)
window.progressBar_create.setValue(0)
window.setWindowTitle("Audio Visualizer")
window.pushButton_selectInput.setText("Select Input Music File")
window.pushButton_selectOutput.setText("Select Output Video File")
window.pushButton_selectBackground.setText("Select Background Image")
window.label_font.setText("Title Font")
window.label_alignment.setText("Title Options")
window.label_colorOptions.setText("Colors")
window.label_fontsize.setText("Fontsize")
window.label_title.setText("Title Text")
window.label_textColor.setText("Text:")
window.label_visColor.setText("Visualizer:")
window.pushButton_createVideo.setText("Create Video")
window.groupBox_create.setTitle("Create")
window.groupBox_settings.setTitle("Settings")
window.groupBox_preview.setTitle("Preview")
window.alignmentComboBox.addItem("Left")
window.alignmentComboBox.addItem("Middle")
window.alignmentComboBox.addItem("Right")
window.fontsizeSpinBox.setValue(35)
window.textXSpinBox.setValue(70)
window.textYSpinBox.setValue(375)
window.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
window.lineEdit_visColor.setText('%s,%s,%s' % self.visColor)
window.pushButton_textColor.clicked.connect(lambda: self.pickColor('text'))
window.pushButton_visColor.clicked.connect(lambda: self.pickColor('vis'))
btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.textColor).name()
window.pushButton_textColor.setStyleSheet(btnStyle)
btnStyle = "QPushButton { background-color : %s; outline: none; }" % QColor(*self.visColor).name()
window.pushButton_visColor.setStyleSheet(btnStyle)
titleFont = self.settings.value("titleFont")
if not titleFont == None:
window.fontComboBox.setCurrentFont(QFont(titleFont))
alignment = self.settings.value("alignment")
if not alignment == None:
window.alignmentComboBox.setCurrentIndex(int(alignment))
fontSize = self.settings.value("fontSize")
if not fontSize == None:
window.fontsizeSpinBox.setValue(int(fontSize))
xPosition = self.settings.value("xPosition")
if not xPosition == None:
window.textXSpinBox.setValue(int(xPosition))
yPosition = self.settings.value("yPosition")
if not yPosition == None:
window.textYSpinBox.setValue(int(yPosition))
window.fontComboBox.currentFontChanged.connect(self.drawPreview)
window.lineEdit_title.textChanged.connect(self.drawPreview)
window.alignmentComboBox.currentIndexChanged.connect(self.drawPreview)
window.textXSpinBox.valueChanged.connect(self.drawPreview)
window.textYSpinBox.valueChanged.connect(self.drawPreview)
window.fontsizeSpinBox.valueChanged.connect(self.drawPreview)
window.lineEdit_textColor.textChanged.connect(self.drawPreview)
window.lineEdit_visColor.textChanged.connect(self.drawPreview)
self.drawPreview()
window.show()
def cleanUp(self):
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
self.settings.setValue("titleFont", self.window.fontComboBox.currentFont().toString())
self.settings.setValue("alignment", str(self.window.alignmentComboBox.currentIndex()))
self.settings.setValue("fontSize", str(self.window.fontsizeSpinBox.value()))
self.settings.setValue("xPosition", str(self.window.textXSpinBox.value()))
self.settings.setValue("yPosition", str(self.window.textYSpinBox.value()))
self.settings.setValue("visColor", self.window.lineEdit_visColor.text())
self.settings.setValue("textColor", self.window.lineEdit_textColor.text())
def openInputFileDialog(self):
inputDir = self.settings.value("inputDir", expanduser("~"))
fileName = QtGui.QFileDialog.getOpenFileName(self.window,
"Open Music File", inputDir, "Music Files (*.mp3 *.wav *.ogg *.flac)");
if not fileName == "":
self.settings.setValue("inputDir", os.path.dirname(fileName))
self.window.label_input.setText(fileName)
def openOutputFileDialog(self):
outputDir = self.settings.value("outputDir", expanduser("~"))
fileName = QtGui.QFileDialog.getSaveFileName(self.window,
"Set Output Video File", outputDir, "Video Files (*.mkv)");
if not fileName == "":
self.settings.setValue("outputDir", os.path.dirname(fileName))
self.window.label_output.setText(fileName)
def openBackgroundFileDialog(self):
backgroundDir = self.settings.value("backgroundDir", expanduser("~"))
fileName = QtGui.QFileDialog.getOpenFileName(self.window,
"Open Background Image", backgroundDir, "Image Files (*.jpg *.png);; Video Files (*.mp4)");
if not fileName == "":
self.settings.setValue("backgroundDir", os.path.dirname(fileName))
self.window.label_background.setText(fileName)
self.drawPreview()
def createAudioVisualisation(self):
ffmpeg_cmd = self.settings.value("ffmpeg_cmd", expanduser("~"))
self.videoThread = QtCore.QThread(self)
self.videoWorker = video_thread.Worker(self)
self.videoWorker.moveToThread(self.videoThread)
self.videoWorker.videoCreated.connect(self.videoCreated)
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
self.videoWorker.progressBarSetText.connect(self.progressBarSetText)
self.videoThread.start()
self.videoTask.emit(self.window.label_background.text(),
self.window.lineEdit_title.text(),
self.window.fontComboBox.currentFont(),
self.window.fontsizeSpinBox.value(),
self.window.alignmentComboBox.currentIndex(),
self.window.textXSpinBox.value(),
self.window.textYSpinBox.value(),
core.Core.RGBFromString(self.window.lineEdit_textColor.text()),
core.Core.RGBFromString(self.window.lineEdit_visColor.text()),
self.window.label_input.text(),
self.window.label_output.text())
def progressBarUpdated(self, value):
self.window.progressBar_create.setValue(value)
def progressBarSetText(self, value):
self.window.progressBar_create.setFormat(value)
def videoCreated(self):
self.videoThread.quit()
self.videoThread.wait()
def drawPreview(self):
self.newTask.emit(self.window.label_background.text(),
self.window.lineEdit_title.text(),
self.window.fontComboBox.currentFont(),
self.window.fontsizeSpinBox.value(),
self.window.alignmentComboBox.currentIndex(),
self.window.textXSpinBox.value(),
self.window.textYSpinBox.value(),
core.Core.RGBFromString(self.window.lineEdit_textColor.text()),
core.Core.RGBFromString(self.window.lineEdit_visColor.text()))
# self.processTask.emit()
def showPreviewImage(self, image):
self._scaledPreviewImage = image
self._previewPixmap = QtGui.QPixmap.fromImage(self._scaledPreviewImage)
self.window.label_preview.setPixmap(self._previewPixmap)
def pickColor(self, colorTarget):
color = QtGui.QColorDialog.getColor()
if color.isValid():
RGBstring = '%s,%s,%s' % (str(color.red()), str(color.green()), str(color.blue()))
btnStyle = "QPushButton { background-color : %s; outline: none; }" % color.name()
if colorTarget == 'text':
self.window.lineEdit_textColor.setText(RGBstring)
window.pushButton_textColor.setStyleSheet(btnStyle)
elif colorTarget == 'vis':
self.window.lineEdit_visColor.setText(RGBstring)
window.pushButton_visColor.setStyleSheet(btnStyle)
if len(sys.argv) > 1:
# command line mode
app = QtGui.QApplication(sys.argv, False)
command = Command()
signal.signal(signal.SIGINT, command.cleanUp)
sys.exit(app.exec_())
else:
# gui mode
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = uic.loadUi("main.ui")
# window.adjustSize()
desc = QtGui.QDesktopWidget()
dpi = desc.physicalDpiX()
topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
window.resize(window.width() * (dpi / 96), window.height() * (dpi / 96))
window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
main = Main(window)
signal.signal(signal.SIGINT, main.cleanUp)
atexit.register(main.cleanUp)
sys.exit(app.exec_())

602
main.ui
View File

@ -1,602 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>635</width>
<height>600</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>635</width>
<height>600</height>
</size>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_settings">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>GroupBox</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButton_selectInput">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_input">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="pushButton_selectOutput">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_output">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="pushButton_selectBackground">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_background">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_font">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QFontComboBox" name="fontComboBox"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_alignment">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QComboBox" name="alignmentComboBox"/>
</item>
<item>
<widget class="QLabel" name="label_fontsize">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="fontsizeSpinBox">
<property name="maximum">
<number>999</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_textX">
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>X</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="textXSpinBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>-99999</number>
</property>
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_textY">
<property name="text">
<string>Y</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="textYSpinBox">
<property name="minimum">
<number>-99999</number>
</property>
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_colorOptions">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_textColor">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_textColor">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_textColor"/>
</item>
<item>
<widget class="QLabel" name="label_visColor">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_visColor">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_visColor"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_title">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_title"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_preview">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>220</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>220</height>
</size>
</property>
<property name="title">
<string>GroupBox</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_preview">
<property name="minimumSize">
<size>
<width>320</width>
<height>180</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>320</width>
<height>180</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_create">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>GroupBox</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QProgressBar" name="progressBar_create">
<property name="value">
<number>24</number>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="textVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_createVideo">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,79 +0,0 @@
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import pyqtSignal, pyqtSlot
from PIL import Image, ImageDraw, ImageFont
from PIL.ImageQt import ImageQt
import core
import time
from queue import Queue, Empty
import numpy
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(['QImage'])
def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self)
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
self.core = core.Core()
self.queue = queue
@pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple)
def createPreviewImage(self, backgroundImage, titleText, titleFont, fontSize,\
alignment, xOffset, yOffset, textColor, visColor):
# print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
dic = {
"backgroundImage": backgroundImage,
"titleText": titleText,
"titleFont": titleFont,
"fontSize": fontSize,
"alignment": alignment,
"xoffset": xOffset,
"yoffset": yOffset,
"textColor" : textColor,
"visColor" : visColor
}
self.queue.put(dic)
@pyqtSlot()
def process(self):
try:
nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2:
try:
self.queue.get(block=False)
except Empty:
continue
bgImage = self.core.parseBaseImage(\
nextPreviewInformation["backgroundImage"],
preview=True
)
if bgImage == []:
bgImage = ''
else:
bgImage = bgImage[0]
im = self.core.drawBaseImage(
bgImage,
nextPreviewInformation["titleText"],
nextPreviewInformation["titleFont"],
nextPreviewInformation["fontSize"],
nextPreviewInformation["alignment"],
nextPreviewInformation["xoffset"],
nextPreviewInformation["yoffset"],
nextPreviewInformation["textColor"],
nextPreviewInformation["visColor"])
spectrum = numpy.fromfunction(lambda x: 0.008*(x-128)**2, (255,), dtype="int16")
im = self.core.drawBars(spectrum, im, nextPreviewInformation["visColor"])
self._image = ImageQt(im)
self._previewImage = QtGui.QImage(self._image)
self._scaledPreviewImage = self._previewImage.scaled(320, 180, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation)
self.imageCreated.emit(self._scaledPreviewImage)
except Empty:
True

View File

@ -1,30 +1,53 @@
from cx_Freeze import setup, Executable
from setuptools import setup
import os
# Dependencies are automatically detected, but it might need
# fine tuning.
buildOptions = dict(packages = [], excludes = [
"apport",
"apt",
"ctypes",
"curses",
"distutils",
"email",
"html",
"http",
"json",
"xmlrpc",
"nose"
], include_files = ["main.ui"])
import sys
base = 'Win32GUI' if sys.platform=='win32' else None
__version__ = '2.0.0.rc3'
executables = [
Executable('main.py', base=base, targetName = 'audio-visualizer-python')
]
setup(name='audio-visualizer-python',
version = '1.0',
description = 'a little GUI tool to render visualization videos of audio files',
options = dict(build_exe = buildOptions),
executables = executables)
def package_files(directory):
paths = []
for (path, directories, filenames) in os.walk(directory):
for filename in filenames:
paths.append(os.path.join('..', path, filename))
return paths
setup(
name='audio_visualizer_python',
version=__version__,
url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui',
license='MIT',
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 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.toolkit',
'avpython.components'
],
package_dir={'avpython': 'src'},
package_data={
'avpython': package_files('src'),
},
install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'],
entry_points={
'gui_scripts': [
'avp = avpython.main:main'
],
}
)

13
src/__init__.py Normal file
View File

@ -0,0 +1,13 @@
import sys
import os
if getattr(sys, 'frozen', False):
# frozen
wd = os.path.dirname(sys.executable)
else:
# unfrozen
wd = os.path.dirname(os.path.realpath(__file__))
# make relative imports work when using /src as a package
sys.path.insert(0, wd)

5
src/__main__.py Normal file
View File

@ -0,0 +1,5 @@
# Allows for launching with python3 -m avpython
from avpython.main import main
main()

BIN
src/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

180
src/command.py Normal file
View File

@ -0,0 +1,180 @@
'''
When using commandline mode, this module's object handles interpreting
the arguments and giving them to Core, which tracks the main program state.
Then it immediately exports a video.
'''
from PyQt5 import QtCore
import argparse
import os
import sys
import time
from core import Core
class Command(QtCore.QObject):
createVideo = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
self.core = Core()
self.dataDir = self.core.dataDir
self.canceled = False
self.parser = argparse.ArgumentParser(
description='Create a visualization for an audio file',
epilog='EXAMPLE COMMAND: main.py myvideotemplate.avp '
'-i ~/Music/song.mp3 -o ~/video.mp4 '
'-c 0 image path=~/Pictures/thisWeeksPicture.jpg '
'-c 1 video "preset=My Logo" -c 2 vis layout=classic'
)
self.parser.add_argument(
'-i', '--input', metavar='SOUND',
help='input audio file'
)
self.parser.add_argument(
'-o', '--output', metavar='OUTPUT',
help='output video file'
)
self.parser.add_argument(
'-e', '--export', action='store_true',
help='use input and output files from project file'
)
# optional arguments
self.parser.add_argument(
'projpath', metavar='path-to-project',
help='open a project file (.avp)', nargs='?')
self.parser.add_argument(
'-c', '--comp', metavar=('LAYER', 'ARG'),
help='first arg must be component NAME to insert at LAYER.'
'"help" for information about possible args for a component.',
nargs='*', action='append')
self.args = self.parser.parse_args()
self.settings = Core.settings
if self.args.projpath:
projPath = self.args.projpath
if not os.path.dirname(projPath):
projPath = os.path.join(
self.settings.value("projectDir"),
projPath
)
if not projPath.endswith('.avp'):
projPath += '.avp'
success = self.core.openProject(self, projPath)
if not success:
quit(1)
self.core.selectedComponents = list(
reversed(self.core.selectedComponents))
self.core.componentListChanged()
if self.args.comp:
for comp in self.args.comp:
pos = comp[0]
name = comp[1]
args = comp[2:]
try:
pos = int(pos)
except ValueError:
print(pos, 'is not a layer number.')
quit(1)
realName = self.parseCompName(name)
if not realName:
print(name, 'is not a valid component name.')
quit(1)
modI = self.core.moduleIndexFor(realName)
i = self.core.insertComponent(pos, modI, self)
for arg in args:
self.core.selectedComponents[i].command(arg)
if self.args.export and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
if 'outputFile' in key:
output = value
if not os.path.dirname(value):
output = os.path.join(
os.path.expanduser('~'),
output
)
if 'audioFile' in key:
input = value
self.createAudioVisualisation(input, output)
elif self.args.input and self.args.output:
self.createAudioVisualisation(self.args.input, self.args.output)
elif 'help' not in sys.argv:
self.parser.print_help()
quit(1)
def createAudioVisualisation(self, input, output):
self.core.selectedComponents = list(
reversed(self.core.selectedComponents))
self.core.componentListChanged()
self.worker = self.core.newVideoWorker(
self, input, output
)
self.worker.videoCreated.connect(self.videoCreated)
self.lastProgressUpdate = time.time()
self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if 'Export ' in value:
# Don't duplicate completion/failure messages
return
if not value.startswith('Exporting') \
and time.time() - self.lastProgressUpdate >= 0.05:
# Show most messages very often
print(value)
elif time.time() - self.lastProgressUpdate >= 2.0:
# Give user time to read ffmpeg's output during the export
print('##### %s' % value)
else:
return
self.lastProgressUpdate = time.time()
@QtCore.pyqtSlot()
def videoCreated(self):
quit(0)
def showMessage(self, **kwargs):
print(kwargs['msg'])
if 'detail' in kwargs:
print(kwargs['detail'])
@QtCore.pyqtSlot(str, str)
def videoThreadError(self, msg, detail):
print(msg)
print(detail)
quit(1)
def drawPreview(self, *args):
pass
def parseCompName(self, name):
'''Deduces a proper component name out of a commandline arg'''
if name.title() in self.core.compNames:
return name.title()
for compName in self.core.compNames:
if name.capitalize() in compName:
return compName
compFileNames = [
os.path.splitext(
os.path.basename(mod.__file__)
)[0]
for mod in self.core.modules
]
for i, compFileName in enumerate(compFileNames):
if name.lower() in compFileName:
return self.core.compNames[i]
return
return None

618
src/component.py Normal file
View File

@ -0,0 +1,618 @@
'''
Base classes for components to import. Read comments for some documentation
on making a valid component.
'''
from PyQt5 import uic, QtCore, QtWidgets
from PyQt5.QtGui import QColor
import os
import sys
import math
import time
from toolkit.frame import BlankFrame
from toolkit import (
getWidgetValue, setWidgetValue, connectWidget, rgbFromString
)
class ComponentMetaclass(type(QtCore.QObject)):
'''
Checks the validity of each Component class and mutates some attrs.
E.g., takes only major version from version string & decorates methods
'''
def initializationWrapper(func):
def initializationWrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except Exception:
try:
raise ComponentError(self, 'initialization process')
except ComponentError:
return
return initializationWrapper
def renderWrapper(func):
def renderWrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except Exception as e:
try:
if e.__class__.__name__.startswith('Component'):
raise
else:
raise ComponentError(self, 'renderer')
except ComponentError:
return BlankFrame()
return renderWrapper
def commandWrapper(func):
'''Intercepts the command() method to check for global args'''
def commandWrapper(self, arg):
if arg.startswith('preset='):
from presetmanager import getPresetDir
_, preset = arg.split('=', 1)
path = os.path.join(getPresetDir(self), preset)
if not os.path.exists(path):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
else:
print('Opening "%s" preset on layer %s' % (
preset, self.compPos)
)
self.core.openPreset(path, self.compPos, preset)
# Don't call the component's command() method
return
else:
return func(self, arg)
return commandWrapper
def propertiesWrapper(func):
'''Intercepts the usual properties if the properties are locked.'''
def propertiesWrapper(self):
if self._lockedProperties is not None:
return self._lockedProperties
else:
try:
return func(self)
except Exception:
try:
raise ComponentError(self, 'properties')
except ComponentError:
return []
return propertiesWrapper
def errorWrapper(func):
'''Intercepts the usual error message if it is locked.'''
def errorWrapper(self):
if self._lockedError is not None:
return self._lockedError
else:
return func(self)
return errorWrapper
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
# Use module name as ui filename by default
attrs['ui'] = '%s.ui' % os.path.splitext(
attrs['__module__'].split('.')[-1]
)[0]
# if parents[0] == QtCore.QObject: else:
decorate = (
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
'frameRender', 'command',
)
# Auto-decorate methods
for key in decorate:
if key not in attrs:
continue
if key in ('names'):
attrs[key] = classmethod(attrs[key])
if key in ('audio'):
attrs[key] = property(attrs[key])
if key == 'command':
attrs[key] = cls.commandWrapper(attrs[key])
if key in ('previewRender', 'frameRender'):
attrs[key] = cls.renderWrapper(attrs[key])
if key == 'preFrameRender':
attrs[key] = cls.initializationWrapper(attrs[key])
if key == 'properties':
attrs[key] = cls.propertiesWrapper(attrs[key])
if key == 'error':
attrs[key] = cls.errorWrapper(attrs[key])
# Turn version string into a number
try:
if 'version' not in attrs:
print(
'No version attribute in %s. Defaulting to 1' %
attrs['name'])
attrs['version'] = 1
else:
attrs['version'] = int(attrs['version'].split('.')[0])
except ValueError:
print('%s component has an invalid version string:\n%s' % (
attrs['name'], str(attrs['version'])))
except KeyError:
print('%s component has no version string.' % attrs['name'])
else:
return super().__new__(cls, name, parents, attrs)
quit(1)
class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
The base class for components to inherit.
'''
name = 'Component'
# ui = 'name_Of_Non_Default_Ui_File'
version = '1.0.0'
# The major version (before the first dot) is used to determine
# preset compatibility; the rest is ignored so it can be non-numeric.
modified = QtCore.pyqtSignal(int, dict)
_error = QtCore.pyqtSignal(str, str)
def __init__(self, moduleIndex, compPos, core):
super().__init__()
self.moduleIndex = moduleIndex
self.compPos = compPos
self.core = core
self.currentPreset = None
self._trackedWidgets = {}
self._presetNames = {}
self._commandArgs = {}
self._colorWidgets = {}
self._colorFuncs = {}
self._relativeWidgets = {}
# pixel values stored as floats
self._relativeValues = {}
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
self._relativeMaximums = {}
self._lockedProperties = None
self._lockedError = None
self._lockedSize = None
# Stop lengthy processes in response to this variable
self.canceled = False
def __str__(self):
return self.__class__.name
def __repr__(self):
try:
preset = self.savePreset()
except Exception as e:
preset = '%s occured while saving preset' % str(e)
return '%s\n%s\n%s' % (
self.__class__.name, str(self.__class__.version), preset
)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Render Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def previewRender(self):
image = BlankFrame(self.width, self.height)
return image
def preFrameRender(self, **kwargs):
'''
Must call super() when subclassing
Triggered only before a video is exported (video_thread.py)
self.audioFile = filepath to the main input audio file
self.completeAudioArray = a list of audio samples
self.sampleSize = number of audio samples per video frame
self.progressBarUpdate = signal to set progress bar number
self.progressBarSetText = signal to set progress bar text
Use the latter two signals to update the MainWindow if needed
for a long initialization procedure (i.e., for a visualizer)
'''
for key, value in kwargs.items():
setattr(self, key, value)
def frameRender(self, frameNo):
audioArrayIndex = frameNo * self.sampleSize
image = BlankFrame(self.width, self.height)
return image
def postFrameRender(self):
pass
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Properties
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def properties(self):
'''
Return a list of properties to signify if your component is
non-animated ('static'), returns sound ('audio'), or has
encountered an error in configuration ('error').
'''
return []
def error(self):
'''
Return a string containing an error message, or None for a default.
Or tuple of two strings for a message with details.
Alternatively use lockError(msgString) within properties()
to skip this method entirely.
'''
return
def audio(self):
'''
Return audio to mix into master as a tuple with two elements:
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 filters/options
to apply to the input stream. See the filter docs for ideas:
https://ffmpeg.org/ffmpeg-filters.html
'''
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Idle Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def widget(self, parent):
'''
Call super().widget(*args) to create the component widget
which also auto-connects any common widgets (e.g., checkBoxes)
to self.update(). Then in a subclass connect special actions
(e.g., pushButtons to select a file/colour) and initialize
'''
self.parent = parent
self.settings = parent.settings
self.page = self.loadUi(self.__class__.ui)
# Connect widget signals
widgets = {
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
}
widgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
for widgetList in widgets.values():
for widget in widgetList:
connectWidget(widget, self.update)
def update(self):
'''
Reads all tracked widget values into instance attributes
and tells the MainWindow that the component was modified.
Call super() at the END if you need to subclass this.
'''
for attr, widget in self._trackedWidgets.items():
if attr in self._colorWidgets:
# Color Widgets: text stored as tuple & update the button color
rgbTuple = rgbFromString(widget.text())
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
self._colorWidgets[attr].setStyleSheet(btnStyle)
setattr(self, attr, rgbTuple)
elif attr in self._relativeWidgets:
# Relative widgets: number scales to fit export resolution
self.updateRelativeWidget(attr)
setattr(self, attr, self._trackedWidgets[attr].value())
else:
# Normal tracked widget
setattr(self, attr, getWidgetValue(widget))
if not self.core.openingProject:
self.parent.drawPreview()
saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
def loadPreset(self, presetDict, presetName=None):
'''
Subclasses should take (presetDict, *args) as args.
Must use super().loadPreset(presetDict, *args) first,
then update self.page widgets using the preset dict.
'''
self.currentPreset = presetName \
if presetName is not None else presetDict['preset']
for attr, widget in self._trackedWidgets.items():
key = attr if attr not in self._presetNames \
else self._presetNames[attr]
val = presetDict[key]
if attr in self._colorWidgets:
widget.setText('%s,%s,%s' % val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*val).name()
)
self._colorWidgets[attr].setStyleSheet(btnStyle)
elif attr in self._relativeWidgets:
self._relativeValues[attr] = val
pixelVal = self.pixelValForAttr(attr, val)
setWidgetValue(widget, pixelVal)
else:
setWidgetValue(widget, val)
def savePreset(self):
saveValueStore = {}
for attr, widget in self._trackedWidgets.items():
presetAttrName = (
attr if attr not in self._presetNames
else self._presetNames[attr]
)
if attr in self._relativeWidgets:
try:
val = self._relativeValues[attr]
except AttributeError:
val = self.floatValForAttr(attr)
else:
val = getattr(self, attr)
saveValueStore[presetAttrName] = val
return saveValueStore
def commandHelp(self):
'''Help text as string for this component's commandline arguments'''
def command(self, arg=''):
'''
Configure a component using an arg from the commandline. This is
never called if global args like 'preset=' are found in the arg.
So simply check for any non-global args in your component and
call super().command() at the end to get a Help message.
'''
print(
self.__class__.name, 'Usage:\n'
'Open a preset for this component:\n'
' "preset=Preset Name"'
)
self.commandHelp()
quit(0)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def trackWidgets(self, trackDict, **kwargs):
'''
Name widgets to track in update(), savePreset(), loadPreset(), and
command(). Requires a dict of attr names as keys, widgets as values
Optional args:
'presetNames': preset variable names to replace attr names
'commandArgs': arg keywords that differ from attr names
NOTE: Any kwarg key set to None will selectively disable tracking.
'''
self._trackedWidgets = trackDict
for kwarg in kwargs:
try:
if kwarg in (
'presetNames',
'commandArgs',
'colorWidgets',
'relativeWidgets',
):
setattr(self, '_%s' % kwarg, kwargs[kwarg])
else:
raise ComponentError(
self, 'Nonsensical keywords to trackWidgets.')
except ComponentError:
continue
if kwarg == 'colorWidgets':
def makeColorFunc(attr):
def pickColor_():
self.pickColor(
self._trackedWidgets[attr],
self._colorWidgets[attr]
)
return pickColor_
self._colorFuncs = {
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
}
for attr, func in self._colorFuncs.items():
self._colorWidgets[attr].clicked.connect(func)
self._colorWidgets[attr].setStyleSheet(
"QPushButton {"
"background-color : #FFFFFF; outline: none; }"
)
if kwarg == 'relativeWidgets':
# store maximum values of spinBoxes to be scaled appropriately
for attr in kwargs[kwarg]:
self._relativeMaximums[attr] = \
self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr)
def pickColor(self, textWidget, button):
'''Use color picker to get color input from the user.'''
dialog = QtWidgets.QColorDialog()
dialog.setOption(QtWidgets.QColorDialog.ShowAlphaChannel, True)
color = dialog.getColor()
if color.isValid():
RGBstring = '%s,%s,%s' % (
str(color.red()), str(color.green()), str(color.blue()))
btnStyle = "QPushButton{background-color: %s; outline: none;}" \
% color.name()
textWidget.setText(RGBstring)
button.setStyleSheet(btnStyle)
def lockProperties(self, propList):
self._lockedProperties = propList
def lockError(self, msg):
self._lockedError = msg
def lockSize(self, w, h):
self._lockedSize = (w, h)
def unlockProperties(self):
self._lockedProperties = None
def unlockError(self):
self._lockedError = None
def unlockSize(self):
self._lockedSize = None
def loadUi(self, filename):
'''Load a Qt Designer ui file to use for this component's widget'''
return uic.loadUi(os.path.join(self.core.componentsPath, filename))
@property
def width(self):
if self._lockedSize is None:
return int(self.settings.value('outputWidth'))
else:
return self._lockedSize[0]
@property
def height(self):
if self._lockedSize is None:
return int(self.settings.value('outputHeight'))
else:
return self._lockedSize[1]
def cancel(self):
'''Stop any lengthy process in response to this variable.'''
self.canceled = True
def reset(self):
self.canceled = False
self.unlockProperties()
self.unlockError()
def relativeWidgetAxis(func):
def relativeWidgetAxis(self, attr, *args, **kwargs):
if 'axis' not in kwargs:
axis = self.width
if 'height' in attr.lower() \
or 'ypos' in attr.lower() or attr == 'y':
axis = self.height
kwargs['axis'] = axis
return func(self, attr, *args, **kwargs)
return relativeWidgetAxis
@relativeWidgetAxis
def pixelValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._relativeValues[attr]
return math.ceil(kwargs['axis'] * val)
@relativeWidgetAxis
def floatValForAttr(self, attr, val=None, **kwargs):
if val is None:
val = self._trackedWidgets[attr].value()
return val / kwargs['axis']
def setRelativeWidget(self, attr, floatVal):
'''Set a relative widget using a float'''
pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal)
def updateRelativeWidget(self, attr):
try:
oldUserValue = getattr(self, attr)
except AttributeError:
oldUserValue = self._trackedWidgets[attr].value()
newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue)
if attr in self._relativeValues:
oldRelativeVal = self._relativeValues[attr]
if oldUserValue == newUserValue \
and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
self._trackedWidgets[attr].blockSignals(True)
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
self._trackedWidgets[attr].blockSignals(False)
if attr not in self._relativeValues \
or oldUserValue != newUserValue:
self._relativeValues[attr] = newRelativeVal
def updateRelativeWidgetMaximum(self, attr):
maxRes = int(self.core.resolutions[0].split('x')[0])
newMaximumValue = self.width * (
self._relativeMaximums[attr] /
maxRes
)
self._trackedWidgets[attr].setMaximum(int(newMaximumValue))
class ComponentError(RuntimeError):
'''Gives the MainWindow a traceback to display, and cancels the export.'''
prevErrors = []
lastTime = time.time()
def __init__(self, caller, name, msg=None):
if msg is None and sys.exc_info()[0] is not None:
msg = str(sys.exc_info()[1])
else:
msg = 'Unknown error.'
print("##### ComponentError by %s's %s: %s" % (
caller.name, name, msg))
# Don't create multiple windows for quickly repeated messages
if len(ComponentError.prevErrors) > 1:
ComponentError.prevErrors.pop()
ComponentError.prevErrors.insert(0, name)
curTime = time.time()
if name in ComponentError.prevErrors[1:] \
and curTime - ComponentError.lastTime < 1.0:
return
ComponentError.lastTime = time.time()
from toolkit import formatTraceback
if sys.exc_info()[0] is not None:
string = (
"%s component (#%s): %s encountered %s %s: %s" % (
caller.__class__.name,
str(caller.compPos),
name,
'an' if any([
sys.exc_info()[0].__name__.startswith(vowel)
for vowel in ('A', 'I', 'U', 'O', 'E')
]) else 'a',
sys.exc_info()[0].__name__,
str(sys.exc_info()[1])
)
)
detail = formatTraceback(sys.exc_info()[2])
else:
string = name
detail = "Attributes:\n%s" % (
"\n".join(
[m for m in dir(caller) if not m.startswith('_')]
)
)
super().__init__(string)
caller.lockError(string)
caller._error.emit(string, detail)

View File

@ -0,0 +1 @@

164
src/components/color.py Normal file
View File

@ -0,0 +1,164 @@
from PIL import Image, ImageDraw
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtGui import QColor
from PIL.ImageQt import ImageQt
import os
from component import Component
from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor
class Component(Component):
name = 'Color'
version = '1.0.0'
def widget(self, *args):
self.x = 0
self.y = 0
super().widget(*args)
self.page.lineEdit_color1.setText('0,0,0')
self.page.lineEdit_color2.setText('133,133,133')
# disable color #2 until non-default 'fill' option gets changed
self.page.lineEdit_color2.setDisabled(True)
self.page.pushButton_color2.setDisabled(True)
self.page.spinBox_width.setValue(
int(self.settings.value("outputWidth")))
self.page.spinBox_height.setValue(
int(self.settings.value("outputHeight")))
self.fillLabels = [
'Solid',
'Linear Gradient',
'Radial Gradient',
]
for label in self.fillLabels:
self.page.comboBox_fill.addItem(label)
self.page.comboBox_fill.setCurrentIndex(0)
self.trackWidgets({
'x': self.page.spinBox_x,
'y': self.page.spinBox_y,
'sizeWidth': self.page.spinBox_width,
'sizeHeight': self.page.spinBox_height,
'trans': self.page.checkBox_trans,
'spread': self.page.comboBox_spread,
'stretch': self.page.checkBox_stretch,
'RG_start': self.page.spinBox_radialGradient_start,
'LG_start': self.page.spinBox_linearGradient_start,
'RG_end': self.page.spinBox_radialGradient_end,
'LG_end': self.page.spinBox_linearGradient_end,
'RG_centre': self.page.spinBox_radialGradient_spread,
'fillType': self.page.comboBox_fill,
'color1': self.page.lineEdit_color1,
'color2': self.page.lineEdit_color2,
}, presetNames={
'sizeWidth': 'width',
'sizeHeight': 'height',
}, colorWidgets={
'color1': self.page.pushButton_color1,
'color2': self.page.pushButton_color2,
}, relativeWidgets=[
'x', 'y',
'sizeWidth', 'sizeHeight',
'LG_start', 'LG_end',
'RG_start', 'RG_end', 'RG_centre',
])
def update(self):
fillType = self.page.comboBox_fill.currentIndex()
if fillType == 0:
self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False)
self.page.checkBox_trans.setEnabled(False)
self.page.checkBox_stretch.setEnabled(False)
self.page.comboBox_spread.setEnabled(False)
else:
self.page.lineEdit_color2.setEnabled(True)
self.page.pushButton_color2.setEnabled(True)
self.page.checkBox_trans.setEnabled(True)
self.page.checkBox_stretch.setEnabled(True)
self.page.comboBox_spread.setEnabled(True)
if self.page.checkBox_trans.isChecked():
self.page.lineEdit_color2.setEnabled(False)
self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(fillType)
super().update()
def previewRender(self):
return self.drawFrame(self.width, self.height)
def properties(self):
return ['static']
def frameRender(self, frameNo):
return self.drawFrame(self.width, self.height)
def drawFrame(self, width, height):
r, g, b = self.color1
shapeSize = (self.sizeWidth, self.sizeHeight)
# in default state, skip all this logic and return a plain fill
if self.fillType == 0 and shapeSize == (width, height) \
and self.x == 0 and self.y == 0:
return FloodFrame(width, height, (r, g, b, 255))
# Return a solid image at x, y
if self.fillType == 0:
frame = BlankFrame(width, height)
image = Image.new("RGBA", shapeSize, (r, g, b, 255))
frame.paste(image, box=(self.x, self.y))
return frame
# Now fills that require using Qt...
elif self.fillType > 0:
image = FramePainter(width, height)
if self.stretch:
w = width
h = height
else:
w = self.sizeWidth
h = self.sizeWidth
if self.fillType == 1: # Linear Gradient
brush = QtGui.QLinearGradient(
self.LG_start,
self.LG_start,
self.LG_end+width/3,
self.LG_end)
elif self.fillType == 2: # Radial Gradient
brush = QtGui.QRadialGradient(
self.RG_start,
self.RG_end,
w, h,
self.RG_centre)
brush.setSpread(self.spread)
brush.setColorAt(0.0, PaintColor(*self.color1))
if self.trans:
brush.setColorAt(1.0, PaintColor(0, 0, 0, 0))
elif self.fillType == 1 and self.stretch:
brush.setColorAt(0.2, PaintColor(*self.color2))
else:
brush.setColorAt(1.0, PaintColor(*self.color2))
image.setBrush(brush)
image.drawRect(
self.x, self.y,
self.sizeWidth, self.sizeHeight
)
return image.finalize()
def commandHelp(self):
print('Specify a color:\n color=255,255,255')
def command(self, arg):
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'color':
self.page.lineEdit_color1.setText(arg)
return
super().command(arg)

660
src/components/color.ui Normal file
View File

@ -0,0 +1,660 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<height>197</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Color #1</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_color1">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_color1">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
<property name="maxLength">
<number>12</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_textColor_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Color #2</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_color2">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_color2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
<property name="maxLength">
<number>12</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_xTitleAlign_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Width</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_width">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>999999999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Height</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_height">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="maximum">
<number>999999999</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_textLayout">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Fill </string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_fill">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>-1</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContentsOnFirstShow</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_trans">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Transparent</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_stretch">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Stretch</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_spread">
<item>
<property name="text">
<string>Pad</string>
</property>
</item>
<item>
<property name="text">
<string>Reflect</string>
</property>
</item>
<item>
<property name="text">
<string>Repeat</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QStackedWidget" name="fillWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="currentIndex">
<number>2</number>
</property>
<widget class="QWidget" name="blank"/>
<widget class="QWidget" name="linearGradient">
<widget class="QWidget" name="horizontalLayoutWidget">
<property name="geometry">
<rect>
<x>-1</x>
<y>0</y>
<width>561</width>
<height>31</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_xTitleAlign_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_linearGradient_start">
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>End</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_linearGradient_end">
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<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>
</widget>
</widget>
<widget class="QWidget" name="radialGradient">
<widget class="QWidget" name="horizontalLayoutWidget_3">
<property name="geometry">
<rect>
<x>-1</x>
<y>-1</y>
<width>561</width>
<height>31</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_xTitleAlign_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Start</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_radialGradient_start">
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>End</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_radialGradient_end">
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Centre</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_radialGradient_spread">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>3</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<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>
</widget>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

102
src/components/image.py Normal file
View File

@ -0,0 +1,102 @@
from PIL import Image, ImageDraw, ImageEnhance
from PyQt5 import QtGui, QtCore, QtWidgets
import os
from component import Component
from toolkit.frame import BlankFrame
class Component(Component):
name = 'Image'
version = '1.0.0'
def widget(self, *args):
super().widget(*args)
self.page.pushButton_image.clicked.connect(self.pickImage)
self.trackWidgets({
'imagePath': self.page.lineEdit_image,
'scale': self.page.spinBox_scale,
'rotate': self.page.spinBox_rotate,
'color': self.page.spinBox_color,
'xPosition': self.page.spinBox_x,
'yPosition': self.page.spinBox_y,
'stretched': self.page.checkBox_stretch,
'mirror': self.page.checkBox_mirror,
}, presetNames={
'imagePath': 'image',
'xPosition': 'x',
'yPosition': 'y',
}, relativeWidgets=[
'xPosition', 'yPosition', 'scale'
])
def previewRender(self):
return self.drawFrame(self.width, self.height)
def properties(self):
props = ['static']
if not os.path.exists(self.imagePath):
props.append('error')
return props
def error(self):
if not self.imagePath:
return "There is no image selected."
if not os.path.exists(self.imagePath):
return "The image selected does not exist!"
def frameRender(self, frameNo):
return self.drawFrame(self.width, self.height)
def drawFrame(self, width, height):
frame = BlankFrame(width, height)
if self.imagePath and os.path.exists(self.imagePath):
image = Image.open(self.imagePath)
# Modify image's appearance
if self.color != 100:
image = ImageEnhance.Color(image).enhance(
float(self.color / 100)
)
if self.mirror:
image = image.transpose(Image.FLIP_LEFT_RIGHT)
if self.stretched and image.size != (width, height):
image = image.resize((width, height), Image.ANTIALIAS)
if self.scale != 100:
newHeight = int((image.height / 100) * self.scale)
newWidth = int((image.width / 100) * self.scale)
image = image.resize((newWidth, newHeight), Image.ANTIALIAS)
# Paste image at correct position
frame.paste(image, box=(self.xPosition, self.yPosition))
if self.rotate != 0:
frame = frame.rotate(self.rotate)
return frame
def pickImage(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Image", imgDir,
"Image Files (%s)" % " ".join(self.core.imageFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
self.update()
def command(self, arg):
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
try:
Image.open(arg)
self.page.lineEdit_image.setText(arg)
self.page.checkBox_stretch.setChecked(True)
return
except OSError as e:
print("Not a supported image format")
quit(1)
super().command(arg)
def commandHelp(self):
print('Load an image:\n path=/filepath/to/image.png')

372
src/components/image.ui Normal file
View File

@ -0,0 +1,372 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<height>197</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Image</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_image">
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_image">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QCheckBox" name="checkBox_stretch">
<property name="text">
<string>Stretch</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_10">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="checkBox_mirror">
<property name="text">
<string>Mirror</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Rotate</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_rotate">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="suffix">
<string notr="true">°</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>359</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Scale</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<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>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<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="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Color</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_color">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

203
src/components/original.py Normal file
View File

@ -0,0 +1,203 @@
import numpy
from PIL import Image, ImageDraw
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtGui import QColor
import os
import time
from copy import copy
from component import Component
from toolkit.frame import BlankFrame
class Component(Component):
name = 'Classic Visualizer'
version = '1.0.0'
def names(*args):
return ['Original Audio Visualization']
def properties(self):
return ['pcm']
def widget(self, *args):
self.scale = 20
self.y = 0
super().widget(*args)
self.page.comboBox_visLayout.addItem("Classic")
self.page.comboBox_visLayout.addItem("Split")
self.page.comboBox_visLayout.addItem("Bottom")
self.page.comboBox_visLayout.addItem("Top")
self.page.comboBox_visLayout.setCurrentIndex(0)
self.page.lineEdit_visColor.setText('255,255,255')
self.trackWidgets({
'visColor': self.page.lineEdit_visColor,
'layout': self.page.comboBox_visLayout,
'scale': self.page.spinBox_scale,
'y': self.page.spinBox_y,
}, colorWidgets={
'visColor': self.page.pushButton_visColor,
}, relativeWidgets=[
'y',
])
def previewRender(self):
spectrum = numpy.fromfunction(
lambda x: float(self.scale)/2500*(x-128)**2, (255,), dtype="int16")
return self.drawBars(
self.width, self.height, spectrum, self.visColor, self.layout
)
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
self.smoothConstantDown = 0.08
self.smoothConstantUp = 0.8
self.lastSpectrum = None
self.spectrumArray = {}
for i in range(0, len(self.completeAudioArray), self.sampleSize):
if self.canceled:
break
self.lastSpectrum = self.transformData(
i, self.completeAudioArray, self.sampleSize,
self.smoothConstantDown, self.smoothConstantUp,
self.lastSpectrum)
self.spectrumArray[i] = copy(self.lastSpectrum)
progress = int(100*(i/len(self.completeAudioArray)))
if progress >= 100:
progress = 100
pStr = "Analyzing audio: "+str(progress)+'%'
self.progressBarSetText.emit(pStr)
self.progressBarUpdate.emit(int(progress))
def frameRender(self, frameNo):
arrayNo = frameNo * self.sampleSize
return self.drawBars(
self.width, self.height,
self.spectrumArray[arrayNo],
self.visColor, self.layout)
def transformData(
self, i, completeAudioArray, sampleSize,
smoothConstantDown, smoothConstantUp, lastSpectrum):
if len(completeAudioArray) < (i + sampleSize):
sampleSize = len(completeAudioArray) - i
window = numpy.hanning(sampleSize)
data = completeAudioArray[i:i+sampleSize][::1] * window
paddedSampleSize = 2048
paddedData = numpy.pad(
data, (0, paddedSampleSize - sampleSize), 'constant')
spectrum = numpy.fft.fft(paddedData)
sample_rate = 44100
frequencies = numpy.fft.fftfreq(len(spectrum), 1./sample_rate)
y = abs(spectrum[0:int(paddedSampleSize/2) - 1])
# filter the noise away
# y[y<80] = 0
y = self.scale * numpy.log10(y)
y[numpy.isinf(y)] = 0
if lastSpectrum is not None:
lastSpectrum[y < lastSpectrum] = \
y[y < lastSpectrum] * smoothConstantDown + \
lastSpectrum[y < lastSpectrum] * (1 - smoothConstantDown)
lastSpectrum[y >= lastSpectrum] = \
y[y >= lastSpectrum] * smoothConstantUp + \
lastSpectrum[y >= lastSpectrum] * (1 - smoothConstantUp)
else:
lastSpectrum = y
x = frequencies[0:int(paddedSampleSize/2) - 1]
return lastSpectrum
def drawBars(self, width, height, spectrum, color, layout):
vH = height-height/8
bF = width / 64
bH = bF / 2
bQ = bF / 4
imTop = BlankFrame(width, height)
draw = ImageDraw.Draw(imTop)
r, g, b = color
color2 = (r, g, b, 125)
bP = height / 1200
for j in range(0, 63):
draw.rectangle((
bH + j * bF, vH+bQ, bH + j * bF + bF, vH + bQ -
spectrum[j * 4] * bP - bH), fill=color2)
draw.rectangle((
bH + bQ + j * bF, vH, bH + bQ + j * bF + bH, vH -
spectrum[j * 4] * bP), fill=color)
imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM)
im = BlankFrame(width, height)
if layout == 0: # Classic
y = self.y - int(height/100*43)
im.paste(imTop, (0, y), mask=imTop)
y = self.y + int(height/100*43)
im.paste(imBottom, (0, y), mask=imBottom)
if layout == 1: # Split
y = self.y + int(height/100*10)
im.paste(imTop, (0, y), mask=imTop)
y = self.y - int(height/100*10)
im.paste(imBottom, (0, y), mask=imBottom)
if layout == 2: # Bottom
y = self.y + int(height/100*10)
im.paste(imTop, (0, y), mask=imTop)
if layout == 3: # Top
y = self.y - int(height/100*10)
im.paste(imBottom, (0, y), mask=imBottom)
return im
def command(self, arg):
if '=' in arg:
key, arg = arg.split('=', 1)
try:
if key == 'color':
self.page.lineEdit_visColor.setText(arg)
return
elif key == 'layout':
if arg == 'classic':
self.page.comboBox_visLayout.setCurrentIndex(0)
elif arg == 'split':
self.page.comboBox_visLayout.setCurrentIndex(1)
elif arg == 'bottom':
self.page.comboBox_visLayout.setCurrentIndex(2)
elif arg == 'top':
self.page.comboBox_visLayout.setCurrentIndex(3)
return
elif key == 'scale':
arg = int(arg)
self.page.spinBox_scale.setValue(arg)
return
elif key == 'y':
arg = int(arg)
self.page.spinBox_y.setValue(arg)
return
except ValueError:
print('You must enter a number.')
quit(1)
super().command(arg)
def commandHelp(self):
print('Give a layout name:\n layout=[classic/split/bottom/top]')
print('Specify a color:\n color=255,255,255')
print('Visualizer scale (20 is default):\n scale=number')
print('Y position:\n y=number')

193
src/components/original.ui Normal file
View File

@ -0,0 +1,193 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>633</width>
<height>178</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>180</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_visLayout">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Layout</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_visLayout"/>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Scale</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="value">
<number>20</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_visColor">
<property name="text">
<string>Color</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_visColor">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_visColor"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::UpDownArrows</enum>
</property>
<property name="minimum">
<number>-5000</number>
</property>
<property name="maximum">
<number>5000</number>
</property>
<property name="singleStep">
<number>10</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

72
src/components/sound.py Normal file
View File

@ -0,0 +1,72 @@
from PyQt5 import QtGui, QtCore, QtWidgets
import os
from component import Component
from toolkit.frame import BlankFrame
class Component(Component):
name = 'Sound'
version = '1.0.0'
def widget(self, *args):
super().widget(*args)
self.page.pushButton_sound.clicked.connect(self.pickSound)
self.trackWidgets({
'sound': self.page.lineEdit_sound,
'chorus': self.page.checkBox_chorus,
'delay': self.page.spinBox_delay,
'volume': self.page.spinBox_volume,
}, commandArgs={
'sound': None,
})
def properties(self):
props = ['static', 'audio']
if not os.path.exists(self.sound):
props.append('error')
return props
def error(self):
if not self.sound:
return "No audio file selected."
if not os.path.exists(self.sound):
return "The audio file selected no longer exists!"
def audio(self):
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("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Sound", sndDir,
"Audio Files (%s)" % " ".join(self.core.audioFormats))
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename)
self.update()
def commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg')
def command(self, arg):
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path':
if '*%s' % os.path.splitext(arg)[1] \
not in self.core.audioFormats:
print("Not a supported audio format")
quit(1)
self.page.lineEdit_sound.setText(arg)
return
super().command(arg)

172
src/components/sound.ui Normal file
View File

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<height>197</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Audio File</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_sound">
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_sound">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<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>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</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>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

284
src/components/spectrum.py Normal file
View File

@ -0,0 +1,284 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
import time
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit import checkOutput, connectWidget
from toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
class Component(Component):
name = 'Spectrum'
version = '1.0.0'
def widget(self, *args):
self.previewFrame = None
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
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,
'mode': self.page.comboBox_mode,
'amplitude': self.page.comboBox_amplitude0,
'amplitude1': self.page.comboBox_amplitude1,
'amplitude2': self.page.comboBox_amplitude2,
'display': self.page.comboBox_display,
'zoom': self.page.spinBox_zoom,
'tc': self.page.spinBox_tc,
'x': self.page.spinBox_x,
'y': self.page.spinBox_y,
'mirror': self.page.checkBox_mirror,
'draw': self.page.checkBox_draw,
'scale': self.page.spinBox_scale,
'color': self.page.comboBox_color,
'compress': self.page.checkBox_compress,
'mono': self.page.checkBox_mono,
'hue': self.page.spinBox_hue,
}, relativeWidgets=[
'x', 'y',
])
for widget in self._trackedWidgets.values():
connectWidget(widget, lambda: self.changed())
def changed(self):
self.changedOptions = True
def update(self):
self.page.stackedWidget.setCurrentIndex(
self.page.comboBox_filterType.currentIndex())
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)
color = self.page.comboBox_color.currentText().lower()
genericPreview = self.settings.value("pref_genericPreview")
if self.filterType == 0: # Spectrum
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'
filter_ = (
'showspectrum=s=%sx%s:slide=scroll:win_func=%s:'
'color=%s:scale=%s,'
'colorkey=color=black:similarity=0.1:blend=0.5' % (
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
self.page.comboBox_window.currentText(),
color, amplitude,
)
)
elif self.filterType == 1: # Histogram
if self.amplitude1 == 0:
amplitude = 'log'
elif self.amplitude1 == 1:
amplitude = 'lin'
if self.display == 0:
display = 'log'
elif self.display == 1:
display = 'sqrt'
elif self.display == 2:
display = 'cbrt'
elif self.display == 3:
display = 'lin'
elif self.display == 4:
display = 'rlog'
filter_ = (
'ahistogram=r=%s:s=%sx%s:dmode=separate:ascale=%s:scale=%s' % (
self.settings.value("outputFrameRate"),
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
amplitude, display
)
)
elif self.filterType == 2: # Vector Scope
if self.amplitude2 == 0:
amplitude = 'log'
elif self.amplitude2 == 1:
amplitude = 'sqrt'
elif self.amplitude2 == 2:
amplitude = 'cbrt'
elif self.amplitude2 == 3:
amplitude = 'lin'
m = self.page.comboBox_mode.currentText()
filter_ = (
'avectorscope=s=%sx%s:draw=%s:m=%s:scale=%s:zoom=%s' % (
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
'line'if self.draw else 'dot',
m, amplitude, str(self.zoom),
)
)
elif self.filterType == 3: # Musical Scale
filter_ = (
'showcqt=r=%s:s=%sx%s:count=30:text=0:tc=%s,'
'colorkey=color=black:similarity=0.1:blend=0.5 ' % (
self.settings.value("outputFrameRate"),
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
str(self.tc),
)
)
elif self.filterType == 4: # Phase
filter_ = (
'aphasemeter=r=%s:s=%sx%s:video=1 [atrash][vtmp1]; '
'[atrash] anullsink; '
'[vtmp1] colorkey=color=black:similarity=0.1:blend=0.5, '
'crop=in_w/8:in_h:(in_w/8)*7:0 '% (
self.settings.value("outputFrameRate"),
self.settings.value("outputWidth"),
self.settings.value("outputHeight"),
)
)
return [
'-filter_complex',
'%s%s%s%s [v1]; '
'[v1] %sscale=%s:%s%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,
', hue=h=%s:s=10' % str(self.hue) if self.hue > 0 else '',
', trim=start=%s:end=%s' % (
"{0:.3f}".format(startPt + 12),
"{0:.3f}".format(startPt + 12.5)
) if preview else '',
', convolution=-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 '
'-1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2'
if self.filterType == 3 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):
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

946
src/components/spectrum.ui Normal file
View File

@ -0,0 +1,946 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<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>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Type</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_filterType">
<item>
<property name="text">
<string>Spectrum</string>
</property>
</item>
<item>
<property name="text">
<string>Histogram</string>
</property>
</item>
<item>
<property name="text">
<string>Vector Scope</string>
</property>
</item>
<item>
<property name="text">
<string>Musical Scale</string>
</property>
</item>
<item>
<property name="text">
<string>Phase</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</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_11">
<property name="text">
<string>Hue</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_hue">
<property name="suffix">
<string>° </string>
</property>
<property name="maximum">
<number>359</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Scale</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<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>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>66</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Window</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_window">
<item>
<property name="text">
<string>hann</string>
</property>
</item>
<item>
<property name="text">
<string>gauss</string>
</property>
</item>
<item>
<property name="text">
<string>tukey</string>
</property>
</item>
<item>
<property name="text">
<string>dolph</string>
</property>
</item>
<item>
<property name="text">
<string>cauchy</string>
</property>
</item>
<item>
<property name="text">
<string>parzen</string>
</property>
</item>
<item>
<property name="text">
<string>poisson</string>
</property>
</item>
<item>
<property name="text">
<string>rect</string>
</property>
</item>
<item>
<property name="text">
<string>bartlett</string>
</property>
</item>
<item>
<property name="text">
<string>hanning</string>
</property>
</item>
<item>
<property name="text">
<string>hamming</string>
</property>
</item>
<item>
<property name="text">
<string>blackman</string>
</property>
</item>
<item>
<property name="text">
<string>welch</string>
</property>
</item>
<item>
<property name="text">
<string>flattop</string>
</property>
</item>
<item>
<property name="text">
<string>bharris</string>
</property>
</item>
<item>
<property name="text">
<string>bnuttall</string>
</property>
</item>
<item>
<property name="text">
<string>lanczos</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Amplitude</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude0">
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
<item>
<property name="text">
<string>4thrt</string>
</property>
</item>
<item>
<property name="text">
<string>5thrt</string>
</property>
</item>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Color </string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_color">
<item>
<property name="text">
<string>Channel</string>
</property>
</item>
<item>
<property name="text">
<string>Intensity</string>
</property>
</item>
<item>
<property name="text">
<string>Rainbow</string>
</property>
</item>
<item>
<property name="text">
<string>Moreland</string>
</property>
</item>
<item>
<property name="text">
<string>Nebulae</string>
</property>
</item>
<item>
<property name="text">
<string>Fire</string>
</property>
</item>
<item>
<property name="text">
<string>Fiery</string>
</property>
</item>
<item>
<property name="text">
<string>Fruit</string>
</property>
</item>
<item>
<property name="text">
<string>Cool</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_2">
<widget class="QWidget" name="verticalLayoutWidget_2">
<property name="geometry">
<rect>
<x>-1</x>
<y>-1</y>
<width>561</width>
<height>31</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Display Scale</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_display">
<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>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Reverse Log</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Amplitude</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude1">
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_3">
<widget class="QWidget" name="verticalLayoutWidget_3">
<property name="geometry">
<rect>
<x>-1</x>
<y>-1</y>
<width>585</width>
<height>64</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>lissajous</string>
</property>
</item>
<item>
<property name="text">
<string>lissajous_xy</string>
</property>
</item>
<item>
<property name="text">
<string>polar</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Amplitude</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_amplitude2">
<item>
<property name="text">
<string>Linear</string>
</property>
</item>
<item>
<property name="text">
<string>Square root</string>
</property>
</item>
<item>
<property name="text">
<string>Cubic root</string>
</property>
</item>
<item>
<property name="text">
<string>Logarithmic</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<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>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_8">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Zoom</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_zoom">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_draw">
<property name="text">
<string>Line</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<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>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_4">
<widget class="QWidget" name="verticalLayoutWidget_4">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>31</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_10">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Timeclamp</string>
</property>
<property name="margin">
<number>4</number>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="spinBox_tc">
<property name="suffix">
<string>s</string>
</property>
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>0.002000000000000</double>
</property>
<property name="maximum">
<double>1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>0.017000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<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>
</layout>
</widget>
</widget>
<widget class="QWidget" name="page_5">
<widget class="QWidget" name="verticalLayoutWidget_5">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>551</width>
<height>31</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11"/>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

137
src/components/text.py Normal file
View File

@ -0,0 +1,137 @@
from PIL import Image, ImageDraw
from PyQt5.QtGui import QColor, QFont
from PyQt5 import QtGui, QtCore, QtWidgets
import os
from component import Component
from toolkit.frame import FramePainter
class Component(Component):
name = 'Title Text'
version = '1.0.1'
def __init__(self, *args):
super().__init__(*args)
self.titleFont = QFont()
def widget(self, *args):
super().widget(*args)
self.textColor = (255, 255, 255)
self.title = 'Text'
self.alignment = 1
self.fontSize = self.height / 13.5
self.page.comboBox_textAlign.addItem("Left")
self.page.comboBox_textAlign.addItem("Middle")
self.page.comboBox_textAlign.addItem("Right")
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
self.page.spinBox_fontSize.setValue(int(self.fontSize))
self.page.lineEdit_title.setText(self.title)
self.page.pushButton_center.clicked.connect(self.centerXY)
self.page.fontComboBox_titleFont.currentFontChanged.connect(
self.update
)
self.trackWidgets({
'textColor': self.page.lineEdit_textColor,
'title': self.page.lineEdit_title,
'alignment': self.page.comboBox_textAlign,
'fontSize': self.page.spinBox_fontSize,
'xPosition': self.page.spinBox_xTextAlign,
'yPosition': self.page.spinBox_yTextAlign,
}, colorWidgets={
'textColor': self.page.pushButton_textColor,
}, relativeWidgets=[
'xPosition', 'yPosition', 'fontSize',
])
self.centerXY()
def update(self):
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
super().update()
def centerXY(self):
self.setRelativeWidget('xPosition', 0.5)
self.setRelativeWidget('yPosition', 0.5)
def getXY(self):
'''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont)
if self.alignment == 0: # Left
x = int(self.xPosition)
if self.alignment == 1: # Middle
offset = int(fm.width(self.title)/2)
x = self.xPosition - offset
if self.alignment == 2: # Right
offset = fm.width(self.title)
x = self.xPosition - offset
return x, self.yPosition
def loadPreset(self, pr, *args):
super().loadPreset(pr, *args)
font = QFont()
font.fromString(pr['titleFont'])
self.page.fontComboBox_titleFont.setCurrentFont(font)
def savePreset(self):
saveValueStore = super().savePreset()
saveValueStore['titleFont'] = self.titleFont.toString()
return saveValueStore
def previewRender(self):
return self.addText(self.width, self.height)
def properties(self):
props = ['static']
if not self.title:
props.append('error')
return props
def error(self):
return "No text provided."
def frameRender(self, frameNo):
return self.addText(self.width, self.height)
def addText(self, width, height):
image = FramePainter(width, height)
self.titleFont.setPixelSize(self.fontSize)
image.setFont(self.titleFont)
image.setPen(self.textColor)
x, y = self.getXY()
image.drawText(x, y, self.title)
return image.finalize()
def commandHelp(self):
print('Enter a string to use as centred white text:')
print(' "title=User Error"')
print('Specify a text color:\n color=255,255,255')
print('Set custom x, y position:\n x=500 y=500')
def command(self, arg):
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'color':
self.page.lineEdit_textColor.setText(arg)
return
elif key == 'size':
self.page.spinBox_fontSize.setValue(int(arg))
return
elif key == 'x':
self.page.spinBox_xTextAlign.setValue(int(arg))
return
elif key == 'y':
self.page.spinBox_yTextAlign.setValue(int(arg))
return
elif key == 'title':
self.page.lineEdit_title.setText(arg)
return
super().command(arg)

343
src/components/text.ui Normal file
View File

@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<height>197</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_title">
<property name="text">
<string>Title</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_title">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Testing New GUI</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Font</string>
</property>
</widget>
</item>
<item>
<widget class="QFontComboBox" name="fontComboBox_titleFont">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_fontSize">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Font Size</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_fontSize">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>500</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLabel" name="label_textColor">
<property name="text">
<string>Text Color</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_textColor"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_textColor">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<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>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_textLayout">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Text Layout</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_textAlign"/>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_center">
<property name="text">
<string>Center</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_xTextAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>999999999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_yTextAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="maximum">
<number>999999999</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

208
src/components/video.py Normal file
View File

@ -0,0 +1,208 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
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):
name = 'Video'
version = '1.0.0'
def widget(self, *args):
self.videoPath = ''
self.badAudio = False
self.x = 0
self.y = 0
self.loopVideo = False
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.page.pushButton_video.clicked.connect(self.pickVideo)
self.trackWidgets({
'videoPath': self.page.lineEdit_video,
'loopVideo': self.page.checkBox_loop,
'useAudio': self.page.checkBox_useAudio,
'distort': self.page.checkBox_distort,
'scale': self.page.spinBox_scale,
'volume': self.page.spinBox_volume,
'xPosition': self.page.spinBox_x,
'yPosition': self.page.spinBox_y,
}, presetNames={
'videoPath': 'video',
'loopVideo': 'loop',
'xPosition': 'x',
'yPosition': 'y',
}, relativeWidgets=[
'xPosition', 'yPosition',
])
def update(self):
if self.page.checkBox_useAudio.isChecked():
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):
self.updateChunksize()
frame = self.getPreviewFrame(self.width, self.height)
if not frame:
return BlankFrame(self.width, self.height)
else:
return frame
def properties(self):
props = []
if hasattr(self.parent, 'window'):
outputFile = self.parent.window.lineEdit_outputFile.text()
else:
outputFile = str(self.parent.args.output)
if not self.videoPath:
self.lockError("There is no video selected.")
elif not os.path.exists(self.videoPath):
self.lockError("The video selected does not exist!")
elif os.path.realpath(self.videoPath) == os.path.realpath(outputFile):
self.lockError("Input and output paths match.")
if self.useAudio:
props.append('audio')
if not testAudioStream(self.videoPath) \
and self.error() is None:
self.lockError(
"Could not identify an audio stream in this video.")
return props
def audio(self):
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)
self.updateChunksize()
self.video = FfmpegVideo(
inputPath=self.videoPath, filter_=self.makeFfmpegFilter(),
width=self.width, height=self.height, chunkSize=self.chunkSize,
frameRate=int(self.settings.value("outputFrameRate")),
parent=self.parent, loopVideo=self.loopVideo,
component=self
) if os.path.exists(self.videoPath) else None
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 pickVideo(self):
imgDir = self.settings.value("componentDir", os.path.expanduser("~"))
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.page, "Choose Video",
imgDir, "Video Files (%s)" % " ".join(self.core.videoFormats)
)
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_video.setText(filename)
self.update()
def getPreviewFrame(self, width, height):
if not self.videoPath or not os.path.exists(self.videoPath):
return
command = [
self.core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-i', self.videoPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
command.extend(self.makeFfmpegFilter())
command.extend([
'-codec:v', 'rawvideo', '-',
'-ss', '90',
'-frames:v', '1',
])
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):
return [
'-filter_complex',
'[0:v] scale=%s:%s' % scale(
self.scale, self.width, self.height, str),
]
def updateChunksize(self):
if self.scale != 100 and not self.distort:
width, height = scale(self.scale, self.width, self.height, int)
else:
width, height = self.width, self.height
self.chunkSize = 4 * width * height
def command(self, arg):
if '=' in arg:
key, arg = arg.split('=', 1)
if key == 'path' and os.path.exists(arg):
if '*%s' % os.path.splitext(arg)[1] in self.core.videoFormats:
self.page.lineEdit_video.setText(arg)
self.page.spinBox_scale.setValue(100)
self.page.checkBox_loop.setChecked(True)
return
else:
print("Not a supported video format")
quit(1)
elif arg == 'audio':
if not self.page.lineEdit_video.text():
print("'audio' option must follow a video selection")
quit(1)
self.page.checkBox_useAudio.setChecked(True)
return
super().command(arg)
def commandHelp(self):
print('Load a video:\n path=/filepath/to/video.mp4')
print('Using audio:\n path=/filepath/to/video.mp4 audio')
def finalizeFrame(self, imageData):
try:
if self.distort:
image = Image.frombytes(
'RGBA',
(self.width, self.height),
imageData)
else:
image = Image.frombytes(
'RGBA',
scale(self.scale, self.width, self.height, int),
imageData)
self._image = image
except ValueError:
# use last good frame
image = self._image
if self.scale != 100 \
or self.xPosition != 0 or self.yPosition != 0:
frame = BlankFrame(self.width, self.height)
frame.paste(image, box=(self.xPosition, self.yPosition))
else:
frame = image
return frame

328
src/components/video.ui Normal file
View File

@ -0,0 +1,328 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<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>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Video</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_video">
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_video">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>1</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
<property name="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QCheckBox" name="checkBox_loop">
<property name="text">
<string>Loop</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<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="QCheckBox" name="checkBox_distort">
<property name="text">
<string>Distort by scale</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Scale</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<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>
</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>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

194
src/components/waveform.py Normal file
View File

@ -0,0 +1,194 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtGui import QColor
import os
import math
import subprocess
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit import checkOutput
from toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
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, 'window'):
self.parent.window.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.window.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', 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',
])
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=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'
'%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 + 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

383
src/components/waveform.ui Normal file
View File

@ -0,0 +1,383 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>586</width>
<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>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>31</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_mode">
<item>
<property name="text">
<string>Cline</string>
</property>
</item>
<item>
<property name="text">
<string>Line</string>
</property>
</item>
<item>
<property name="text">
<string>Point</string>
</property>
</item>
<item>
<property name="text">
<string>Frequency Bar</string>
</property>
</item>
<item>
<property name="text">
<string>Frequency Line</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_xTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>X</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_x">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_yTitleAlign">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Y</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_y">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-10000</number>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Color</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_color">
<property name="inputMethodHints">
<set>Qt::ImhNone</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_color">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="default">
<bool>false</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<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_4">
<property name="text">
<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>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Scale</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<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>
</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">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

527
src/core.py Normal file
View File

@ -0,0 +1,527 @@
'''
Home to the Core class which tracks program state. Used by GUI & commandline
to create a list of components and create a video thread to export.
'''
from PyQt5 import QtCore, QtGui, uic
import sys
import os
import json
from importlib import import_module
import toolkit
import video_thread
class Core:
'''
MainWindow and Command module both use an instance of this class
to store the core program state. This object tracks the components,
talks to the components, handles opening/creating project files
and presets, and creates the video thread to export.
This class also stores constants as class variables.
'''
def __init__(self):
self.importComponents()
self.selectedComponents = []
self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
def importComponents(self):
def findComponents():
for f in os.listdir(Core.componentsPath):
name, ext = os.path.splitext(f)
if name.startswith("__"):
continue
elif ext == '.py':
yield name
self.modules = [
import_module('components.%s' % name)
for name in findComponents()
]
# store canonical module names and indexes
self.moduleIndexes = [i for i in range(len(self.modules))]
self.compNames = [mod.Component.name for mod in self.modules]
# alphabetize modules by Component name
sortedModules = sorted(zip(self.compNames, self.modules))
self.compNames = [y[0] for y in sortedModules]
self.modules = [y[1] for y in sortedModules]
# store alternative names for modules
self.altCompNames = []
for i, mod in enumerate(self.modules):
if hasattr(mod.Component, 'names'):
for name in mod.Component.names():
self.altCompNames.append((name, i))
def componentListChanged(self):
for i, component in enumerate(self.selectedComponents):
component.compPos = i
def insertComponent(self, compPos, moduleIndex, loader):
'''
Creates a new component using these args:
(compPos, moduleIndex in self.modules, MWindow/Command/Core obj)
'''
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return None
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
)
self.selectedComponents.insert(
compPos,
component
)
self.componentListChanged()
self.selectedComponents[compPos]._error.connect(
loader.videoThreadError
)
# init component's widget for loading/saving presets
self.selectedComponents[compPos].widget(loader)
self.updateComponent(compPos)
if hasattr(loader, 'insertComponent'):
loader.insertComponent(compPos)
return compPos
def moveComponent(self, startI, endI):
comp = self.selectedComponents.pop(startI)
self.selectedComponents.insert(endI, comp)
self.componentListChanged()
return endI
def removeComponent(self, i):
self.selectedComponents.pop(i)
self.componentListChanged()
def clearComponents(self):
self.selectedComponents = list()
self.componentListChanged()
def updateComponent(self, i):
# print('updating %s' % self.selectedComponents[i])
self.selectedComponents[i].update()
def moduleIndexFor(self, compName):
try:
index = self.compNames.index(compName)
return self.moduleIndexes[index]
except ValueError:
for altName, modI in self.altCompNames:
if altName == compName:
return self.moduleIndexes[modI]
def clearPreset(self, compIndex):
self.selectedComponents[compIndex].currentPreset = None
def openPreset(self, filepath, compIndex, presetName):
'''Applies a preset to a specific component'''
saveValueStore = self.getPreset(filepath)
if not saveValueStore:
return False
try:
self.selectedComponents[compIndex].loadPreset(
saveValueStore,
presetName
)
except KeyError as e:
print('preset missing value: %s' % e)
self.savedPresets[presetName] = dict(saveValueStore)
return True
def getPreset(self, filepath):
'''Returns the preset dict stored at this filepath'''
if not os.path.exists(filepath):
return False
with open(filepath, 'r') as f:
for line in f:
saveValueStore = toolkit.presetFromString(line.strip())
break
return saveValueStore
def openProject(self, loader, filepath):
''' loader is the object calling this method which must have
its own showMessage(**kwargs) method for displaying errors.
'''
if not os.path.exists(filepath):
loader.showMessage(msg='Project file not found.')
return
errcode, data = self.parseAvFile(filepath)
if errcode == 0:
self.openingProject = True
try:
if hasattr(loader, 'window'):
for widget, value in data['WindowFields']:
widget = eval('loader.window.%s' % widget)
widget.blockSignals(True)
toolkit.setWidgetValue(widget, value)
widget.blockSignals(False)
for key, value in data['Settings']:
Core.settings.setValue(key, value)
for tup in data['Components']:
name, vers, preset = tup
clearThis = False
modified = False
# add loaded named presets to savedPresets dict
if 'preset' in preset and preset['preset'] is not None:
nam = preset['preset']
filepath2 = os.path.join(
Core.presetDir, name, str(vers), nam)
origSaveValueStore = self.getPreset(filepath2)
if origSaveValueStore:
self.savedPresets[nam] = dict(origSaveValueStore)
modified = not origSaveValueStore == preset
else:
# saved preset was renamed or deleted
clearThis = True
# create the actual component object & get its index
i = self.insertComponent(
-1,
self.moduleIndexFor(name),
loader
)
if i is None:
loader.showMessage(msg="Too many components!")
break
try:
if 'preset' in preset and preset['preset'] is not None:
self.selectedComponents[i].loadPreset(
preset
)
else:
self.selectedComponents[i].loadPreset(
preset,
preset['preset']
)
except KeyError as e:
print('%s missing value: %s' % (
self.selectedComponents[i], e)
)
if clearThis:
self.clearPreset(i)
if hasattr(loader, 'updateComponentTitle'):
loader.updateComponentTitle(i, modified)
self.openingProject = False
return True
except Exception:
errcode = 1
data = sys.exc_info()
if errcode == 1:
typ, value, tb = data
if typ.__name__ == 'KeyError':
# probably just an old version, still loadable
print('file missing value: %s' % value)
return
if hasattr(loader, 'createNewProject'):
loader.createNewProject(prompt=False)
msg = '%s: %s\n\n' % (typ.__name__, value)
msg += toolkit.formatTraceback(tb)
loader.showMessage(
msg="Project file '%s' is corrupted." % filepath,
showCancel=False,
icon='Warning',
detail=msg)
self.openingProject = False
return False
def parseAvFile(self, filepath):
'''
Parses an avp (project) or avl (preset package) file.
Returns dictionary with section names as the keys, each one
contains a list of tuples: (compName, version, compPresetDict)
'''
validSections = (
'Components',
'Settings',
'WindowFields'
)
data = {sect: [] for sect in validSections}
try:
with open(filepath, 'r') as f:
def parseLine(line):
'''Decides if a file line is a section header'''
line = line.strip()
newSection = ''
if line.startswith('[') and line.endswith(']') \
and line[1:-1] in validSections:
newSection = line[1:-1]
return line, newSection
section = ''
i = 0
for line in f:
line, newSection = parseLine(line)
if newSection:
section = str(newSection)
continue
if line and section == 'Components':
if i == 0:
lastCompName = str(line)
i += 1
elif i == 1:
lastCompVers = str(line)
i += 1
elif i == 2:
lastCompPreset = toolkit.presetFromString(line)
data[section].append((
lastCompName,
lastCompVers,
lastCompPreset
))
i = 0
elif line and section:
key, value = line.split('=', 1)
data[section].append((key, value.strip()))
return 0, data
except Exception:
return 1, sys.exc_info()
def importPreset(self, filepath):
errcode, data = self.parseAvFile(filepath)
returnList = []
if errcode == 0:
name, vers, preset = data['Components'][0]
presetName = preset['preset'] \
if preset['preset'] else os.path.basename(filepath)[:-4]
newPath = os.path.join(
Core.presetDir,
name,
vers,
presetName
)
if os.path.exists(newPath):
return False, newPath
preset['preset'] = presetName
self.createPresetFile(
name, vers, presetName, preset
)
return True, presetName
elif errcode == 1:
# TODO: an error message
return False, ''
def exportPreset(self, exportPath, compName, vers, origName):
internalPath = os.path.join(
Core.presetDir, compName, str(vers), origName
)
if not os.path.exists(internalPath):
return
if os.path.exists(exportPath):
os.remove(exportPath)
with open(internalPath, 'r') as f:
internalData = [line for line in f]
try:
saveValueStore = toolkit.presetFromString(internalData[0].strip())
self.createPresetFile(
compName, vers,
origName, saveValueStore,
exportPath
)
return True
except Exception:
return False
def createPresetFile(
self, compName, vers, presetName, saveValueStore, filepath=''):
'''Create a preset file (.avl) at filepath using args.
Or if filepath is empty, create an internal preset using args'''
if not filepath:
dirname = os.path.join(Core.presetDir, compName, str(vers))
if not os.path.exists(dirname):
os.makedirs(dirname)
filepath = os.path.join(dirname, presetName)
internal = True
else:
if not filepath.endswith('.avl'):
filepath += '.avl'
internal = False
with open(filepath, 'w') as f:
if not internal:
f.write('[Components]\n')
f.write('%s\n' % compName)
f.write('%s\n' % str(vers))
f.write(toolkit.presetToString(saveValueStore))
def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
settingsKeys = [
'componentDir',
'inputDir',
'outputDir',
'presetDir',
'projectDir',
]
try:
if not filepath.endswith(".avp"):
filepath += '.avp'
if os.path.exists(filepath):
os.remove(filepath)
with open(filepath, 'w') as f:
print('creating %s' % filepath)
f.write('[Components]\n')
for comp in self.selectedComponents:
saveValueStore = comp.savePreset()
saveValueStore['preset'] = comp.currentPreset
f.write('%s\n' % str(comp))
f.write('%s\n' % str(comp.version))
f.write('%s\n' % toolkit.presetToString(saveValueStore))
f.write('\n[Settings]\n')
for key in Core.settings.allKeys():
if key in settingsKeys:
f.write('%s=%s\n' % (key, Core.settings.value(key)))
if window:
f.write('\n[WindowFields]\n')
f.write(
'lineEdit_audioFile=%s\n'
'lineEdit_outputFile=%s\n' % (
window.lineEdit_audioFile.text(),
window.lineEdit_outputFile.text()
)
)
return True
except Exception:
return False
def newVideoWorker(self, loader, audioFile, outputPath):
'''loader is MainWindow or Command object which must own the thread'''
self.videoThread = QtCore.QThread(loader)
videoWorker = video_thread.Worker(
loader, audioFile, outputPath, self.selectedComponents
)
videoWorker.moveToThread(self.videoThread)
videoWorker.videoCreated.connect(self.videoCreated)
self.videoThread.start()
return videoWorker
def videoCreated(self):
self.videoThread.quit()
self.videoThread.wait()
def cancel(self):
Core.canceled = True
def reset(self):
Core.canceled = False
@classmethod
def storeSettings(cls):
'''Store settings/paths to directories as class variables'''
from __init__ import wd
from toolkit.ffmpeg import findFfmpeg
cls.wd = wd
dataDir = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.AppConfigLocation
)
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
encoderOptions = json.load(json_file)
settings = {
'dataDir': dataDir,
'settings': QtCore.QSettings(
os.path.join(dataDir, 'settings.ini'),
QtCore.QSettings.IniFormat),
'presetDir': os.path.join(dataDir, 'presets'),
'componentsPath': os.path.join(wd, 'components'),
'encoderOptions': encoderOptions,
'resolutions': [
'1920x1080',
'1280x720',
'854x480',
],
'FFMPEG_BIN': findFfmpeg(),
'windowHasFocus': False,
'canceled': False,
}
settings['videoFormats'] = toolkit.appendUppercase([
'*.mp4',
'*.mov',
'*.mkv',
'*.avi',
'*.webm',
'*.flv',
])
settings['audioFormats'] = toolkit.appendUppercase([
'*.mp3',
'*.wav',
'*.ogg',
'*.fla',
'*.flac',
'*.aac',
])
settings['imageFormats'] = toolkit.appendUppercase([
'*.png',
'*.jpg',
'*.tif',
'*.tiff',
'*.gif',
'*.bmp',
'*.ico',
'*.xbm',
'*.xpm',
])
# Register all settings as class variables
for classvar, val in settings.items():
setattr(cls, classvar, val)
cls.loadDefaultSettings()
@classmethod
def loadDefaultSettings(cls):
cls.defaultSettings = {
"outputWidth": 1280,
"outputHeight": 720,
"outputFrameRate": 30,
"outputAudioCodec": "AAC",
"outputAudioBitrate": "192",
"outputVideoCodec": "H264",
"outputVideoBitrate": "2500",
"outputVideoFormat": "yuv420p",
"outputPreset": "medium",
"outputFormat": "mp4",
"outputContainer": "MP4",
"projectDir": os.path.join(cls.dataDir, 'projects'),
"pref_insertCompAtTop": True,
"pref_genericPreview": True,
}
for parm, value in cls.defaultSettings.items():
if cls.settings.value(parm) is None:
cls.settings.setValue(parm, value)
# Allow manual editing of prefs. (Surprisingly necessary as Qt seems to
# store True as 'true' but interprets a manually-added 'true' as str.)
for key in cls.settings.allKeys():
if not key.startswith('pref_'):
continue
val = cls.settings.value(key)
if val in ('true', 'false'):
cls.settings.setValue(key, True if val == 'true' else False)
# always store settings in class variables even if a Core object is not created
Core.storeSettings()

130
src/encoder-options.json Normal file
View File

@ -0,0 +1,130 @@
{
"containers":[
{
"name": "MP4",
"container": "mp4",
"default-vcodec": "H264",
"default-acodec": "AAC",
"video-codecs": [
"H264",
"H264 (nvenc)",
"MPEG4"
],
"audio-codecs": [
"AAC",
"AC3",
"MP3"
]
},
{
"name": "MOV",
"container": "mov",
"default-vcodec": "H264",
"default-acodec": "AAC",
"video-codecs": [
"H264",
"H264 (nvenc)",
"MPEG4",
"XVID"
],
"audio-codecs": [
"AAC",
"AC3",
"MP3",
"PCM s16 LE"
]
},
{
"name": "MKV",
"container": "matroska",
"default-vcodec": "H264",
"default-acodec": "AAC",
"video-codecs": [
"H264",
"H264 (nvenc)",
"MPEG4",
"MPEG2",
"DV",
"WMV"
],
"audio-codecs": [
"AAC",
"AC3",
"MP3",
"PCM s16 LE",
"WMA"
]
},
{
"name": "AVI",
"container": "avi",
"default-vcodec": "H264",
"default-acodec": "AAC",
"video-codecs": [
"H264",
"H264 (nvenc)",
"MPEG4",
"MPEG2",
"DV",
"WMV"
],
"audio-codecs": [
"AAC",
"AC3",
"MP3",
"PCM s16 LE",
"WMA"
]
},
{
"name": "WEBM",
"container": "webm",
"default-vcodec": "VP9",
"default-acodec": "Vorbis",
"video-codecs": [
"VP9",
"VP8"
],
"audio-codecs": [
"Vorbis"
]
},
{
"name": "FLV",
"container": "flv",
"default-vcodec": "FLV",
"default-acodec": "Vorbis",
"video-codecs": [
"Sorenson (flv)",
"H264",
"H264 (nvenc)",
"MPEG4"
],
"audio-codecs": [
"MP3",
"PCM s16 LE",
"Vorbis"
]
}
],
"video-codecs":{
"H264": ["libx264"],
"H264 (nvenc)": ["h264_nvenc", "nvenc_h264"],
"MPEG4": ["mpeg4"],
"VP9": ["libvpx-vp9"],
"VP8": ["libvpx"],
"XVID": ["libxvid"],
"Sorenson (flv)": ["flv"],
"MPEG2": ["mp2video"],
"DV": ["dvvideo"],
"WMV": ["wmv2"]
},
"audio-codecs": {
"AAC": ["libfdk_aac", "aac"],
"AC3": ["ac3"],
"MP3": ["libmp3lame"],
"PCM s16 LE": ["pcm_s16le"],
"WMA": ["wmav2"],
"Vorbis": ["libvorbis"]
}
}

59
src/main.py Normal file
View File

@ -0,0 +1,59 @@
from PyQt5 import uic, QtWidgets
import sys
import os
from __init__ import wd
def main():
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("audio-visualizer")
# Determine mode
mode = 'GUI'
if len(sys.argv) > 2:
mode = 'commandline'
elif len(sys.argv) == 2:
if sys.argv[1].startswith('-'):
mode = 'commandline'
else:
# opening a project file with gui
proj = sys.argv[1]
else:
# normal gui launch
proj = None
# Launch program
if mode == 'commandline':
from command import Command
main = Command()
elif mode == 'GUI':
from mainwindow import MainWindow
import atexit
import signal
window = uic.loadUi(os.path.join(wd, "mainwindow.ui"))
# window.adjustSize()
desc = QtWidgets.QDesktopWidget()
dpi = desc.physicalDpiX()
topMargin = 0 if (dpi == 96) else int(10 * (dpi / 96))
window.resize(
window.width() *
(dpi / 96), window.height() *
(dpi / 96)
)
# window.verticalLayout_2.setContentsMargins(0, topMargin, 0, 0)
main = MainWindow(window, proj)
window.raise_()
signal.signal(signal.SIGINT, main.cleanUp)
atexit.register(main.cleanUp)
sys.exit(app.exec_())
if __name__ == "__main__":
main()

975
src/mainwindow.py Normal file
View File

@ -0,0 +1,975 @@
'''
When using GUI mode, this module's object (the main window) takes
user input to construct a program state (stored in the Core object).
This shows a preview of the video being created and allows for saving
projects and exporting the video at a later time.
'''
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtWidgets import QMenu, QShortcut
from PIL import Image
from queue import Queue
import sys
import os
import signal
import filecmp
import time
from core import Core
import preview_thread
from presetmanager import PresetManager
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
class PreviewWindow(QtWidgets.QLabel):
'''
Paints the preview QLabel and maintains the aspect ratio when the
window is resized.
'''
def __init__(self, parent, img):
super(PreviewWindow, self).__init__()
self.parent = parent
self.setFrameStyle(QtWidgets.QFrame.StyledPanel)
self.pixmap = QtGui.QPixmap(img)
self.isFlickering = 0
def paintEvent(self, event):
size = self.size()
painter = QtGui.QPainter(self)
point = QtCore.QPoint(0, 0)
scaledPix = self.pixmap.scaled(
size,
QtCore.Qt.KeepAspectRatio,
transformMode=QtCore.Qt.SmoothTransformation)
# start painting the label from left upper corner
point.setX((size.width() - scaledPix.width())/2)
point.setY((size.height() - scaledPix.height())/2)
painter.drawPixmap(point, scaledPix)
def changePixmap(self, img):
if self.parent.encoding:
# track time to determine if the preview is flickering due to lag
bTime = time.time()
elif self.isFlickering > 0:
# no longer exporting video so reset the paint area to normal
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, False)
self.isFlickering = 0
self.pixmap = QtGui.QPixmap(img)
self.repaint()
if self.parent.encoding and time.time() - bTime > 1.1:
self.isFlickering += 1
if self.isFlickering == 8:
# if export is lagging, temporarily disable clearing the paint area
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True)
@QtCore.pyqtSlot(str)
def threadError(self, msg):
self.parent.showMessage(
msg=msg,
icon='Critical',
parent=self
)
class MainWindow(QtWidgets.QMainWindow):
'''
The MainWindow wraps many Core methods in order to update the GUI
accordingly. E.g., instead of self.core.openProject(), it will use
self.openProject() and update the window titlebar within the wrapper.
MainWindow manages the autosave feature, although Core has the
primary functions for opening and creating project files.
'''
createVideo = QtCore.pyqtSignal()
newTask = QtCore.pyqtSignal(list) # for the preview window
processTask = QtCore.pyqtSignal()
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
# print('main thread id: {}'.format(QtCore.QThread.currentThreadId()))
self.window = window
self.core = Core()
# widgets of component settings
self.pages = []
self.lastAutosave = time.time()
# list of previous five autosave times, used to reduce update spam
self.autosaveTimes = []
self.autosaveCooldown = 0.2
self.encoding = False
# Create data directory, load/create settings
self.dataDir = Core.dataDir
self.presetDir = Core.presetDir
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'presetmanager.ui')), self)
if not os.path.exists(self.dataDir):
os.makedirs(self.dataDir)
for neededDirectory in (
self.presetDir, self.settings.value("projectDir")):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
# Create the preview window and its thread, queues, and timers
self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
self.previewQueue = Queue()
self.previewThread = QtCore.QThread(self)
self.previewWorker = preview_thread.Worker(self, self.previewQueue)
self.previewWorker.error.connect(self.previewWindow.threadError)
self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start()
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.processTask.emit)
self.timer.start(500)
# Begin decorating the window and connecting events
self.window.installEventFilter(self)
componentList = self.window.listWidget_componentList
if sys.platform == 'darwin':
window.progressBar_createVideo.setTextVisible(False)
else:
window.progressLabel.setHidden(True)
window.toolButton_selectAudioFile.clicked.connect(
self.openInputFileDialog)
window.toolButton_selectOutputFile.clicked.connect(
self.openOutputFileDialog)
def changedField():
self.autosave()
self.updateWindowTitle()
window.lineEdit_audioFile.textChanged.connect(changedField)
window.lineEdit_outputFile.textChanged.connect(changedField)
window.progressBar_createVideo.setValue(0)
window.pushButton_createVideo.clicked.connect(
self.createAudioVisualisation)
window.pushButton_Cancel.clicked.connect(self.stopVideo)
for i, container in enumerate(Core.encoderOptions['containers']):
window.comboBox_videoContainer.addItem(container['name'])
if container['name'] == self.settings.value('outputContainer'):
selectedContainer = i
window.comboBox_videoContainer.setCurrentIndex(selectedContainer)
window.comboBox_videoContainer.currentIndexChanged.connect(
self.updateCodecs
)
self.updateCodecs()
for i in range(window.comboBox_videoCodec.count()):
codec = window.comboBox_videoCodec.itemText(i)
if codec == self.settings.value('outputVideoCodec'):
window.comboBox_videoCodec.setCurrentIndex(i)
for i in range(window.comboBox_audioCodec.count()):
codec = window.comboBox_audioCodec.itemText(i)
if codec == self.settings.value('outputAudioCodec'):
window.comboBox_audioCodec.setCurrentIndex(i)
window.comboBox_videoCodec.currentIndexChanged.connect(
self.updateCodecSettings
)
window.comboBox_audioCodec.currentIndexChanged.connect(
self.updateCodecSettings
)
vBitrate = int(self.settings.value('outputVideoBitrate'))
aBitrate = int(self.settings.value('outputAudioBitrate'))
window.spinBox_vBitrate.setValue(vBitrate)
window.spinBox_aBitrate.setValue(aBitrate)
window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings)
window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings)
# Make component buttons
self.compMenu = QMenu()
for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.name)
action.triggered.connect(
lambda _, item=i: self.core.insertComponent(0, item, self)
)
self.window.pushButton_addComponent.setMenu(self.compMenu)
componentList.dropEvent = self.dragComponent
componentList.itemSelectionChanged.connect(
self.changeComponentWidget
)
componentList.itemSelectionChanged.connect(
self.presetManager.clearPresetListSelection
)
self.window.pushButton_removeComponent.clicked.connect(
lambda: self.removeComponent()
)
componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
componentList.customContextMenuRequested.connect(
self.componentContextMenu
)
currentRes = str(self.settings.value('outputWidth'))+'x' + \
str(self.settings.value('outputHeight'))
for i, res in enumerate(Core.resolutions):
window.comboBox_resolution.addItem(res)
if res == currentRes:
currentRes = i
window.comboBox_resolution.setCurrentIndex(currentRes)
window.comboBox_resolution.currentIndexChanged.connect(
self.updateResolution
)
self.window.pushButton_listMoveUp.clicked.connect(
lambda: self.moveComponent(-1)
)
self.window.pushButton_listMoveDown.clicked.connect(
lambda: self.moveComponent(1)
)
# Configure the Projects Menu
self.projectMenu = QMenu()
self.window.menuButton_newProject = self.projectMenu.addAction(
"New Project"
)
self.window.menuButton_newProject.triggered.connect(
lambda: self.createNewProject()
)
self.window.menuButton_openProject = self.projectMenu.addAction(
"Open Project"
)
self.window.menuButton_openProject.triggered.connect(
lambda: self.openOpenProjectDialog()
)
action = self.projectMenu.addAction("Save Project")
action.triggered.connect(self.saveCurrentProject)
action = self.projectMenu.addAction("Save Project As")
action.triggered.connect(self.openSaveProjectDialog)
self.window.pushButton_projects.setMenu(self.projectMenu)
# Configure the Presets Button
self.window.pushButton_presets.clicked.connect(
self.openPresetManager
)
self.updateWindowTitle()
window.show()
if project and project != self.autosavePath:
if not project.endswith('.avp'):
project += '.avp'
# open a project from the commandline
if not os.path.dirname(project):
project = os.path.join(
self.settings.value("projectDir"), project
)
self.currentProject = project
self.settings.setValue("currentProject", project)
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
else:
# open the last currentProject from settings
self.currentProject = self.settings.value("currentProject")
# delete autosave if it's identical to this project
if self.autosaveExists(identical=True):
os.remove(self.autosavePath)
if self.currentProject and os.path.exists(self.autosavePath):
ch = self.showMessage(
msg="Restore unsaved changes in project '%s'?"
% os.path.basename(self.currentProject)[:-4],
showCancel=True)
if ch:
self.saveProjectChanges()
else:
os.remove(self.autosavePath)
self.openProject(self.currentProject, prompt=False)
self.drawPreview(True)
# verify Pillow version
if not self.settings.value("pilMsgShown") \
and 'post' not in Image.PILLOW_VERSION:
self.showMessage(
msg="You are using the standard version of the "
"Python imaging library (Pillow %s). Upgrade "
"to the Pillow-SIMD fork to enable hardware accelerations "
"and export videos faster." % Image.PILLOW_VERSION
)
self.settings.setValue("pilMsgShown", True)
# verify Ffmpeg version
if not self.settings.value("ffmpegMsgShown"):
try:
with open(os.devnull, "w") as f:
ffmpegVers = checkOutput(
['ffmpeg', '-version'], stderr=f
)
goodVersion = str(ffmpegVers).split()[2].startswith('3')
except Exception:
goodVersion = False
else:
goodVersion = True
if not goodVersion:
self.showMessage(
msg="You're using an old version of Ffmpeg. "
"Some features may not work as expected."
)
self.settings.setValue("ffmpegMsgShown", True)
# Hotkeys for projects
QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject)
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
# Hotkeys for component list
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
QtWidgets.QShortcut(
inskey, self.window,
activated=lambda: self.window.pushButton_addComponent.click()
)
for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete):
QtWidgets.QShortcut(
delkey, self.window.listWidget_componentList,
self.removeComponent
)
QtWidgets.QShortcut(
"Ctrl+Space", self.window,
activated=lambda: self.window.listWidget_componentList.setFocus()
)
QtWidgets.QShortcut(
"Ctrl+Shift+S", self.window,
self.presetManager.openSavePresetDialog
)
QtWidgets.QShortcut(
"Ctrl+Shift+C", self.window, self.presetManager.clearPreset
)
QtWidgets.QShortcut(
"Ctrl+Up", self.window.listWidget_componentList,
activated=lambda: self.moveComponent(-1)
)
QtWidgets.QShortcut(
"Ctrl+Down", self.window.listWidget_componentList,
activated=lambda: self.moveComponent(1)
)
QtWidgets.QShortcut(
"Ctrl+Home", self.window.listWidget_componentList,
activated=lambda: self.moveComponent('top')
)
QtWidgets.QShortcut(
"Ctrl+End", self.window.listWidget_componentList,
activated=lambda: self.moveComponent('bottom')
)
# Debug Hotkeys
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
)
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
)
@QtCore.pyqtSlot()
def cleanUp(self, *args):
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
@disableWhenOpeningProject
def updateWindowTitle(self):
appName = 'Audio Visualizer'
try:
if self.currentProject:
appName += ' - %s' % \
os.path.splitext(
os.path.basename(self.currentProject))[0]
if self.autosaveExists(identical=False):
appName += '*'
except AttributeError:
pass
self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict)
def updateComponentTitle(self, pos, presetStore=False):
if type(presetStore) == dict:
name = presetStore['preset']
if name is None or name not in self.core.savedPresets:
modified = False
else:
modified = (presetStore != self.core.savedPresets[name])
else:
modified = bool(presetStore)
if pos < 0:
pos = len(self.core.selectedComponents)-1
title = str(self.core.selectedComponents[pos])
if self.core.selectedComponents[pos].currentPreset:
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
if modified:
title += '*'
self.window.listWidget_componentList.item(pos).setText(title)
def updateCodecs(self):
containerWidget = self.window.comboBox_videoContainer
vCodecWidget = self.window.comboBox_videoCodec
aCodecWidget = self.window.comboBox_audioCodec
index = containerWidget.currentIndex()
name = containerWidget.itemText(index)
self.settings.setValue('outputContainer', name)
vCodecWidget.clear()
aCodecWidget.clear()
for container in Core.encoderOptions['containers']:
if container['name'] == name:
for vCodec in container['video-codecs']:
vCodecWidget.addItem(vCodec)
for aCodec in container['audio-codecs']:
aCodecWidget.addItem(aCodec)
def updateCodecSettings(self):
'''Updates settings.ini to match encoder option widgets'''
vCodecWidget = self.window.comboBox_videoCodec
vBitrateWidget = self.window.spinBox_vBitrate
aBitrateWidget = self.window.spinBox_aBitrate
aCodecWidget = self.window.comboBox_audioCodec
currentVideoCodec = vCodecWidget.currentIndex()
currentVideoCodec = vCodecWidget.itemText(currentVideoCodec)
currentVideoBitrate = vBitrateWidget.value()
currentAudioCodec = aCodecWidget.currentIndex()
currentAudioCodec = aCodecWidget.itemText(currentAudioCodec)
currentAudioBitrate = aBitrateWidget.value()
self.settings.setValue('outputVideoCodec', currentVideoCodec)
self.settings.setValue('outputAudioCodec', currentAudioCodec)
self.settings.setValue('outputVideoBitrate', currentVideoBitrate)
self.settings.setValue('outputAudioBitrate', currentAudioBitrate)
@disableWhenOpeningProject
def autosave(self, force=False):
if not self.currentProject:
if os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
elif force or time.time() - self.lastAutosave >= self.autosaveCooldown:
self.core.createProjectFile(self.autosavePath, self.window)
self.lastAutosave = time.time()
if len(self.autosaveTimes) >= 5:
# Do some math to reduce autosave spam. This gives a smooth
# curve up to 5 seconds cooldown and maintains that for 30 secs
# if a component is continuously updated
timeDiff = self.lastAutosave - self.autosaveTimes.pop()
if not force and timeDiff >= 1.0 \
and timeDiff <= 10.0:
if self.autosaveCooldown / 4.0 < 0.5:
self.autosaveCooldown += 1.0
self.autosaveCooldown = (
5.0 * (self.autosaveCooldown / 5.0)
) + (self.autosaveCooldown / 5.0) * 2
elif force or timeDiff >= self.autosaveCooldown * 5:
self.autosaveCooldown = 0.2
self.autosaveTimes.insert(0, self.lastAutosave)
def autosaveExists(self, identical=True):
'''Determines if creating the autosave should be blocked.'''
try:
if self.currentProject and os.path.exists(self.autosavePath) \
and filecmp.cmp(
self.autosavePath, self.currentProject) == identical:
return True
except FileNotFoundError:
print('project file couldn\'t be located:', self.currentProject)
return identical
return False
def saveProjectChanges(self):
'''Overwrites project file with autosave file'''
try:
os.remove(self.currentProject)
os.rename(self.autosavePath, self.currentProject)
return True
except (FileNotFoundError, IsADirectoryError) as e:
self.showMessage(
msg='Project file couldn\'t be saved.',
detail=str(e))
return False
def openInputFileDialog(self):
inputDir = self.settings.value("inputDir", os.path.expanduser("~"))
fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Audio File",
inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats))
if fileName:
self.settings.setValue("inputDir", os.path.dirname(fileName))
self.window.lineEdit_audioFile.setText(fileName)
def openOutputFileDialog(self):
outputDir = self.settings.value("outputDir", os.path.expanduser("~"))
fileName, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Set Output Video File",
outputDir,
"Video Files (%s);; All Files (*)" % " ".join(
Core.videoFormats))
if fileName:
self.settings.setValue("outputDir", os.path.dirname(fileName))
self.window.lineEdit_outputFile.setText(fileName)
def stopVideo(self):
print('stop')
self.videoWorker.cancel()
self.canceled = True
def createAudioVisualisation(self):
# create output video if mandatory settings are filled in
audioFile = self.window.lineEdit_audioFile.text()
outputPath = self.window.lineEdit_outputFile.text()
if audioFile and outputPath and self.core.selectedComponents:
if not os.path.dirname(outputPath):
outputPath = os.path.join(
os.path.expanduser("~"), outputPath)
if outputPath and os.path.isdir(outputPath):
self.showMessage(
msg='Chosen filename matches a directory, which '
'cannot be overwritten. Please choose a different '
'filename or move the directory.',
icon='Warning',
)
return
else:
if not audioFile or not outputPath:
self.showMessage(
msg="You must select an audio file and output filename."
)
elif not self.core.selectedComponents:
self.showMessage(
msg="Not enough components."
)
return
self.canceled = False
self.progressBarUpdated(-1)
self.videoWorker = self.core.newVideoWorker(
self, audioFile, outputPath
)
self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated)
self.videoWorker.progressBarSetText.connect(
self.progressBarSetText)
self.videoWorker.imageCreated.connect(self.showPreviewImage)
self.videoWorker.encoding.connect(self.changeEncodingStatus)
self.createVideo.emit()
@QtCore.pyqtSlot(str, str)
def videoThreadError(self, msg, detail):
try:
self.stopVideo()
except AttributeError as e:
if 'videoWorker' not in str(e):
raise
self.showMessage(
msg=msg,
detail=detail,
icon='Critical',
)
def changeEncodingStatus(self, status):
self.encoding = status
if status:
self.window.pushButton_createVideo.setEnabled(False)
self.window.pushButton_Cancel.setEnabled(True)
self.window.comboBox_resolution.setEnabled(False)
self.window.stackedWidget.setEnabled(False)
self.window.tab_encoderSettings.setEnabled(False)
self.window.label_audioFile.setEnabled(False)
self.window.toolButton_selectAudioFile.setEnabled(False)
self.window.label_outputFile.setEnabled(False)
self.window.toolButton_selectOutputFile.setEnabled(False)
self.window.lineEdit_audioFile.setEnabled(False)
self.window.lineEdit_outputFile.setEnabled(False)
self.window.pushButton_addComponent.setEnabled(False)
self.window.pushButton_removeComponent.setEnabled(False)
self.window.pushButton_listMoveDown.setEnabled(False)
self.window.pushButton_listMoveUp.setEnabled(False)
self.window.menuButton_newProject.setEnabled(False)
self.window.menuButton_openProject.setEnabled(False)
if sys.platform == 'darwin':
self.window.progressLabel.setHidden(False)
else:
self.window.listWidget_componentList.setEnabled(False)
else:
self.window.pushButton_createVideo.setEnabled(True)
self.window.pushButton_Cancel.setEnabled(False)
self.window.comboBox_resolution.setEnabled(True)
self.window.stackedWidget.setEnabled(True)
self.window.tab_encoderSettings.setEnabled(True)
self.window.label_audioFile.setEnabled(True)
self.window.toolButton_selectAudioFile.setEnabled(True)
self.window.lineEdit_audioFile.setEnabled(True)
self.window.label_outputFile.setEnabled(True)
self.window.toolButton_selectOutputFile.setEnabled(True)
self.window.lineEdit_outputFile.setEnabled(True)
self.window.pushButton_addComponent.setEnabled(True)
self.window.pushButton_removeComponent.setEnabled(True)
self.window.pushButton_listMoveDown.setEnabled(True)
self.window.pushButton_listMoveUp.setEnabled(True)
self.window.menuButton_newProject.setEnabled(True)
self.window.menuButton_openProject.setEnabled(True)
self.window.listWidget_componentList.setEnabled(True)
self.window.progressLabel.setHidden(True)
self.drawPreview(True)
@QtCore.pyqtSlot(int)
def progressBarUpdated(self, value):
self.window.progressBar_createVideo.setValue(value)
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if sys.platform == 'darwin':
self.window.progressLabel.setText(value)
else:
self.window.progressBar_createVideo.setFormat(value)
def updateResolution(self):
resIndex = int(self.window.comboBox_resolution.currentIndex())
res = Core.resolutions[resIndex].split('x')
changed = res[0] != self.settings.value("outputWidth")
self.settings.setValue('outputWidth', res[0])
self.settings.setValue('outputHeight', res[1])
if changed:
for i in range(len(self.core.selectedComponents)):
self.core.updateComponent(i)
def drawPreview(self, force=False, **kwargs):
'''Use autosave keyword arg to force saving or not saving if needed'''
self.newTask.emit(self.core.selectedComponents)
# self.processTask.emit()
if force or 'autosave' in kwargs:
if force or kwargs['autosave']:
self.autosave(True)
else:
self.autosave()
self.updateWindowTitle()
@QtCore.pyqtSlot(QtGui.QImage)
def showPreviewImage(self, image):
self.previewWindow.changePixmap(image)
def showFfmpegCommand(self):
from textwrap import wrap
from toolkit.ffmpeg import createFfmpegCommand
command = createFfmpegCommand(
self.window.lineEdit_audioFile.text(),
self.window.lineEdit_outputFile.text(),
self.core.selectedComponents
)
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
componentList.insertItem(
index,
self.core.selectedComponents[index].name)
componentList.setCurrentRow(index)
# connect to signal that adds an asterisk when modified
self.core.selectedComponents[index].modified.connect(
self.updateComponentTitle)
self.pages.insert(index, self.core.selectedComponents[index].page)
stackedWidget.insertWidget(index, self.pages[index])
stackedWidget.setCurrentIndex(index)
return index
def removeComponent(self):
componentList = self.window.listWidget_componentList
for selected in componentList.selectedItems():
index = componentList.row(selected)
self.window.stackedWidget.removeWidget(self.pages[index])
componentList.takeItem(index)
self.core.removeComponent(index)
self.pages.pop(index)
self.changeComponentWidget()
self.drawPreview()
@disableWhenEncoding
def moveComponent(self, change):
'''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList
if change == 'top':
change = -componentList.currentRow()
elif change == 'bottom':
change = len(componentList)-componentList.currentRow()-1
stackedWidget = self.window.stackedWidget
row = componentList.currentRow()
newRow = row + change
if newRow > -1 and newRow < componentList.count():
self.core.moveComponent(row, newRow)
# update widgets
page = self.pages.pop(row)
self.pages.insert(newRow, page)
item = componentList.takeItem(row)
newItem = componentList.insertItem(newRow, item)
widget = stackedWidget.removeWidget(page)
stackedWidget.insertWidget(newRow, page)
componentList.setCurrentRow(newRow)
stackedWidget.setCurrentIndex(newRow)
self.drawPreview(True)
def getComponentListMousePos(self, position):
'''
Given a QPos, returns the component index under the mouse cursor
or -1 if no component is there.
'''
componentList = self.window.listWidget_componentList
modelIndexes = [
componentList.model().index(i)
for i in range(componentList.count())
]
rects = [
componentList.visualRect(modelIndex)
for modelIndex in modelIndexes
]
mousePos = [rect.contains(position) for rect in rects]
if not any(mousePos):
# Not clicking a component
mousePos = -1
else:
mousePos = mousePos.index(True)
return mousePos
@disableWhenEncoding
def dragComponent(self, event):
'''Used as Qt drop event for the component listwidget'''
componentList = self.window.listWidget_componentList
mousePos = self.getComponentListMousePos(event.pos())
if mousePos > -1:
change = (componentList.currentRow() - mousePos) * -1
else:
change = (componentList.count() - componentList.currentRow() - 1)
self.moveComponent(change)
def changeComponentWidget(self):
selected = self.window.listWidget_componentList.selectedItems()
if selected:
index = self.window.listWidget_componentList.row(selected[0])
self.window.stackedWidget.setCurrentIndex(index)
def openPresetManager(self):
'''Preset manager for importing, exporting, renaming, deleting'''
self.presetManager.show()
def clear(self):
'''Get a blank slate'''
self.core.clearComponents()
self.window.listWidget_componentList.clear()
for widget in self.pages:
self.window.stackedWidget.removeWidget(widget)
self.pages = []
for field in (
self.window.lineEdit_audioFile,
self.window.lineEdit_outputFile
):
field.blockSignals(True)
field.setText('')
field.blockSignals(False)
self.progressBarUpdated(0)
self.progressBarSetText('')
@disableWhenEncoding
def createNewProject(self, prompt=True):
if prompt:
self.openSaveChangesDialog('starting a new project')
self.clear()
self.currentProject = None
self.settings.setValue("currentProject", None)
self.drawPreview(True)
def saveCurrentProject(self):
if self.currentProject:
self.core.createProjectFile(self.currentProject, self.window)
try:
os.remove(self.autosavePath)
except FileNotFoundError:
pass
self.updateWindowTitle()
else:
self.openSaveProjectDialog()
def openSaveChangesDialog(self, phrase):
success = True
if self.autosaveExists(identical=False):
ch = self.showMessage(
msg="You have unsaved changes in project '%s'. "
"Save before %s?" % (
os.path.basename(self.currentProject)[:-4],
phrase
),
showCancel=True)
if ch:
success = self.saveProjectChanges()
if success and os.path.exists(self.autosavePath):
os.remove(self.autosavePath)
def openSaveProjectDialog(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Create Project File",
self.settings.value("projectDir"),
"Project Files (*.avp)")
if not filename:
return
if not filename.endswith(".avp"):
filename += '.avp'
self.settings.setValue("projectDir", os.path.dirname(filename))
self.settings.setValue("currentProject", filename)
self.currentProject = filename
self.core.createProjectFile(filename, self.window)
self.updateWindowTitle()
@disableWhenEncoding
def openOpenProjectDialog(self):
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Open Project File",
self.settings.value("projectDir"),
"Project Files (*.avp)")
self.openProject(filename)
def openProject(self, filepath, prompt=True):
if not filepath or not os.path.exists(filepath) \
or not filepath.endswith('.avp'):
return
self.clear()
# ask to save any changes that are about to get deleted
if prompt:
self.openSaveChangesDialog('opening another project')
self.currentProject = filepath
self.settings.setValue("currentProject", filepath)
self.settings.setValue("projectDir", os.path.dirname(filepath))
# actually load the project using core method
self.core.openProject(self, filepath)
self.drawPreview(autosave=False)
self.updateWindowTitle()
def showMessage(self, **kwargs):
parent = kwargs['parent'] if 'parent' in kwargs else self.window
msg = QtWidgets.QMessageBox(parent)
msg.setModal(True)
msg.setText(kwargs['msg'])
msg.setIcon(
eval('QtWidgets.QMessageBox.%s' % kwargs['icon'])
if 'icon' in kwargs else QtWidgets.QMessageBox.Information
)
msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None)
if 'showCancel'in kwargs and kwargs['showCancel']:
msg.setStandardButtons(
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
else:
msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
ch = msg.exec_()
if ch == 1024:
return True
return False
@disableWhenEncoding
def componentContextMenu(self, QPos):
'''Appears when right-clicking the component list'''
componentList = self.window.listWidget_componentList
self.menu = QMenu()
parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0))
index = self.getComponentListMousePos(QPos)
if index > -1:
# Show preset menu if clicking a component
self.presetManager.findPresets()
menuItem = self.menu.addAction("Save Preset")
menuItem.triggered.connect(
self.presetManager.openSavePresetDialog
)
# submenu for opening presets
try:
presets = self.presetManager.presets[
str(self.core.selectedComponents[index])
]
self.presetSubmenu = QMenu("Open Preset")
self.menu.addMenu(self.presetSubmenu)
for version, presetName in presets:
menuItem = self.presetSubmenu.addAction(presetName)
menuItem.triggered.connect(
lambda _, presetName=presetName:
self.presetManager.openPreset(presetName)
)
except KeyError:
pass
if self.core.selectedComponents[index].currentPreset:
menuItem = self.menu.addAction("Clear Preset")
menuItem.triggered.connect(
self.presetManager.clearPreset
)
self.menu.addSeparator()
# "Add Component" submenu
self.submenu = QMenu("Add")
self.menu.addMenu(self.submenu)
insertCompAtTop = self.settings.value("pref_insertCompAtTop")
for i, comp in enumerate(self.core.modules):
menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect(
lambda _, item=i: self.core.insertComponent(
0 if insertCompAtTop else index, item, self
)
)
self.menu.move(parentPosition + QPos)
self.menu.show()
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate \
or event.type() == QtCore.QEvent.FocusIn:
Core.windowHasFocus = True
elif event.type() == QtCore.QEvent.WindowDeactivate \
or event.type() == QtCore.QEvent.FocusOut:
Core.windowHasFocus = False
return False

828
src/mainwindow.ui Normal file
View File

@ -0,0 +1,828 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1008</width>
<height>575</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>0</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>360</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_previewWrapper">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="topMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_previewSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>420</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_16">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>140</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_projects">
<property name="text">
<string>Projects</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_presets">
<property name="text">
<string>Presets</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QListWidget" name="listWidget_componentList">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
<widget class="QPushButton" name="pushButton_addComponent">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_removeComponent">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_listMoveUp">
<property name="text">
<string>Up</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_listMoveDown">
<property name="text">
<string>Down</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="leftMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>500</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>180</height>
</size>
</property>
<property name="tabPosition">
<enum>QTabWidget::North</enum>
</property>
<property name="tabShape">
<enum>QTabWidget::Rounded</enum>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_exportVideo">
<attribute name="title">
<string>Export Video</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_10">
<property name="margin">
<number>10</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_audioFile">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>80</width>
<height>16777215</height>
</size>
</property>
<property name="baseSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Audio File</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_audioFile">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>28</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_selectAudioFile">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>28</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_outputFile">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Output File</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_outputFile">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>28</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_selectOutputFile">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>28</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QProgressBar" name="progressBar_createVideo">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_createVideo">
<property name="text">
<string>Create Video</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_Cancel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="progressLabel">
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="indent">
<number>-1</number>
</property>
</widget>
</item>
</layout>
<zorder></zorder>
<zorder></zorder>
<zorder>progressLabel</zorder>
</widget>
<widget class="QWidget" name="tab_encoderSettings">
<attribute name="title">
<string>Encoder Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<property name="margin">
<number>10</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<widget class="QLabel" name="label_videoFormat">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Container</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_videoContainer">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_videoPreset">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Resolution</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_resolution">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QLabel" name="label_videoCodec">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Video Codec</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_videoCodec">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_resolution">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Video Bitrate (Kbps)</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_vBitrate">
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<widget class="QLabel" name="label_audioCodec">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Audio Codec</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_audioCodec">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>5</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_audioBitrate">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Audio Bitrate (Kbps)</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_aBitrate">
<property name="maximum">
<number>9999</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>500</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>180</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>180</height>
</size>
</property>
<property name="currentIndex">
<number>-1</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

358
src/presetmanager.py Normal file
View File

@ -0,0 +1,358 @@
'''
Preset manager object handles all interactions with presets, including
the context menu accessed from MainWindow.
'''
from PyQt5 import QtCore, QtWidgets
import string
import os
from toolkit import badName
from core import Core
class PresetManager(QtWidgets.QDialog):
def __init__(self, window, parent):
super().__init__(parent.window)
self.parent = parent
self.core = parent.core
self.settings = parent.settings
self.presetDir = parent.presetDir
if not self.settings.value('presetDir'):
self.settings.setValue(
"presetDir",
os.path.join(parent.dataDir, 'projects'))
self.findPresets()
# window
self.lastFilter = '*'
self.presetRows = [] # list of (comp, vers, name) tuples
self.window = window
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# connect button signals
self.window.pushButton_delete.clicked.connect(
self.openDeletePresetDialog
)
self.window.pushButton_rename.clicked.connect(
self.openRenamePresetDialog
)
self.window.pushButton_import.clicked.connect(
self.openImportDialog
)
self.window.pushButton_export.clicked.connect(
self.openExportDialog
)
self.window.pushButton_close.clicked.connect(
self.window.close
)
# create filter box and preset list
self.drawFilterList()
self.window.comboBox_filter.currentIndexChanged.connect(
lambda: self.drawPresetList(
self.window.comboBox_filter.currentText(),
self.window.lineEdit_search.text()
)
)
# make auto-completion for search bar
self.autocomplete = QtCore.QStringListModel()
completer = QtWidgets.QCompleter()
completer.setModel(self.autocomplete)
self.window.lineEdit_search.setCompleter(completer)
self.window.lineEdit_search.textChanged.connect(
lambda: self.drawPresetList(
self.window.comboBox_filter.currentText(),
self.window.lineEdit_search.text()
)
)
self.drawPresetList('*')
def show(self):
'''Open a new preset manager window from the mainwindow'''
self.findPresets()
self.drawFilterList()
self.drawPresetList('*')
self.window.show()
def findPresets(self):
parseList = []
for dirpath, dirnames, filenames in os.walk(self.presetDir):
# anything without a subdirectory must be a preset folder
if dirnames:
continue
for preset in filenames:
compName = os.path.basename(os.path.dirname(dirpath))
if compName not in self.core.compNames:
continue
compVers = os.path.basename(dirpath)
try:
parseList.append((compName, int(compVers), preset))
except ValueError:
continue
self.presets = {
compName: [
(vers, preset)
for name, vers, preset in parseList
if name == compName
]
for compName, _, __ in parseList
}
def drawPresetList(self, compFilter=None, presetFilter=''):
self.window.listWidget_presets.clear()
if compFilter:
self.lastFilter = str(compFilter)
else:
compFilter = str(self.lastFilter)
self.presetRows = []
presetNames = []
for component, presets in self.presets.items():
if compFilter != '*' and component != compFilter:
continue
for vers, preset in presets:
if not presetFilter or presetFilter in preset:
self.window.listWidget_presets.addItem(
'%s: %s' % (component, preset)
)
self.presetRows.append((component, vers, preset))
if preset not in presetNames:
presetNames.append(preset)
self.autocomplete.setStringList(presetNames)
def drawFilterList(self):
self.window.comboBox_filter.clear()
self.window.comboBox_filter.addItem('*')
for component in self.presets:
self.window.comboBox_filter.addItem(component)
def clearPreset(self, compI=None):
'''Functions on mainwindow level from the context menu'''
compI = self.parent.window.listWidget_componentList.currentRow()
self.core.clearPreset(compI)
self.parent.updateComponentTitle(compI, False)
def openSavePresetDialog(self):
'''Functions on mainwindow level from the context menu'''
window = self.parent.window
selectedComponents = self.core.selectedComponents
componentList = self.parent.window.listWidget_componentList
if componentList.currentRow() == -1:
return
while True:
index = componentList.currentRow()
currentPreset = selectedComponents[index].currentPreset
newName, OK = QtWidgets.QInputDialog.getText(
self.parent.window,
'Audio Visualizer',
'New Preset Name:',
QtWidgets.QLineEdit.Normal,
currentPreset
)
if OK:
if badName(newName):
self.warnMessage(self.parent.window)
continue
if newName:
if index != -1:
selectedComponents[index].currentPreset = newName
saveValueStore = \
selectedComponents[index].savePreset()
saveValueStore['preset'] = newName
componentName = str(selectedComponents[index]).strip()
vers = selectedComponents[index].version
self.createNewPreset(
componentName, vers, newName,
saveValueStore, window=self.parent.window)
self.findPresets()
self.drawPresetList()
self.openPreset(newName, index)
break
def createNewPreset(
self, compName, vers, filename, saveValueStore, **kwargs):
path = os.path.join(self.presetDir, compName, str(vers), filename)
if self.presetExists(path, **kwargs):
return
self.core.createPresetFile(compName, vers, filename, saveValueStore)
def presetExists(self, path, **kwargs):
if os.path.exists(path):
window = self.window \
if 'window' not in kwargs else kwargs['window']
ch = self.parent.showMessage(
msg="%s already exists! Overwrite it?" %
os.path.basename(path),
showCancel=True,
icon='Warning',
parent=window)
if not ch:
# user clicked cancel
return True
return False
def openPreset(self, presetName, compPos=None):
componentList = self.parent.window.listWidget_componentList
selectedComponents = self.core.selectedComponents
index = compPos if compPos is not None else componentList.currentRow()
if index == -1:
return
componentName = str(selectedComponents[index]).strip()
version = selectedComponents[index].version
dirname = os.path.join(self.presetDir, componentName, str(version))
filepath = os.path.join(dirname, presetName)
self.core.openPreset(filepath, index, presetName)
self.parent.updateComponentTitle(index)
self.parent.drawPreview()
def openDeletePresetDialog(self):
row = self.getPresetRow()
if row == -1:
return
comp, vers, name = self.presetRows[row]
ch = self.parent.showMessage(
msg='Really delete %s?' % name,
showCancel=True,
icon='Warning',
parent=self.window
)
if not ch:
return
self.deletePreset(comp, vers, name)
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
if comp.currentPreset == name:
self.clearPreset(i)
def deletePreset(self, comp, vers, name):
filepath = os.path.join(self.presetDir, comp, str(vers), name)
os.remove(filepath)
def warnMessage(self, window=None):
self.parent.showMessage(
msg='Preset names must contain only letters, '
'numbers, and spaces.',
parent=window if window else self.window)
def getPresetRow(self):
row = self.window.listWidget_presets.currentRow()
if row > -1:
return row
# check if component selected in MainWindow has preset loaded
componentList = self.parent.window.listWidget_componentList
compIndex = componentList.currentRow()
if compIndex == -1:
return compIndex
preset = self.core.selectedComponents[compIndex].currentPreset
if preset is None:
return -1
else:
rowTuple = (
self.core.selectedComponents[compIndex].name,
self.core.selectedComponents[compIndex].version,
preset
)
for i, tup in enumerate(self.presetRows):
if rowTuple == tup:
index = i
break
else:
return -1
return index
def openRenamePresetDialog(self):
# TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets
index = self.getPresetRow()
if index == -1:
return
while True:
newName, OK = QtWidgets.QInputDialog.getText(
self.window,
'Preset Manager',
'Rename Preset:',
QtWidgets.QLineEdit.Normal,
self.presetRows[index][2]
)
if OK:
if badName(newName):
self.warnMessage()
continue
if newName:
comp, vers, oldName = self.presetRows[index]
path = os.path.join(
self.presetDir, comp, str(vers))
newPath = os.path.join(path, newName)
oldPath = os.path.join(path, oldName)
if self.presetExists(newPath):
return
if os.path.exists(newPath):
os.remove(newPath)
os.rename(oldPath, newPath)
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
if getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
self.parent.drawPreview()
break
def openImportDialog(self):
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Import Preset File",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
if filename:
# get installed path & ask user to overwrite if needed
path = ''
while True:
if path:
if self.presetExists(path):
break
else:
if os.path.exists(path):
os.remove(path)
success, path = self.core.importPreset(filename)
if success:
break
self.findPresets()
self.drawPresetList()
self.settings.setValue("presetDir", os.path.dirname(filename))
def openExportDialog(self):
index = self.getPresetRow()
if index == -1:
return
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self.window, "Export Preset",
self.settings.value("presetDir"),
"Preset Files (*.avl)")
if filename:
comp, vers, name = self.presetRows[index]
if not self.core.exportPreset(filename, comp, vers, name):
self.parent.showMessage(
msg='Couldn\'t export %s.' % filename,
parent=self.window
)
self.settings.setValue("presetDir", os.path.dirname(filename))
def clearPresetListSelection(self):
self.window.listWidget_presets.setCurrentRow(-1)
def getPresetDir(comp):
'''Get the preset subdir for a particular version of a component'''
return os.path.join(Core.presetDir, str(comp), str(comp.version))

150
src/presetmanager.ui Normal file
View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>presetmanager</class>
<widget class="QWidget" name="presetmanager">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>497</width>
<height>377</height>
</rect>
</property>
<property name="windowTitle">
<string>Preset Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="lineEdit_search">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Filter by name</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_filter">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="listWidget_presets">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QPushButton" name="pushButton_import">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_export">
<property name="text">
<string>Export</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<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="QPushButton" name="pushButton_rename">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Rename</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_delete">
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item alignment="Qt::AlignRight">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt; font-style:italic;&quot;&gt;Right-click components in the main window to create presets&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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="QPushButton" name="pushButton_close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

85
src/preview_thread.py Normal file
View File

@ -0,0 +1,85 @@
'''
Thread that runs to create QImages for MainWindow's preview label.
Processes a queue of component lists.
'''
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
from PIL.ImageQt import ImageQt
from queue import Queue, Empty
import os
from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(QtGui.QImage)
error = pyqtSignal(str)
def __init__(self, parent=None, queue=None):
QtCore.QObject.__init__(self)
parent.newTask.connect(self.createPreviewImage)
parent.processTask.connect(self.process)
self.parent = parent
self.core = parent.core
self.settings = parent.settings
self.queue = queue
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
self.background = Checkerboard(width, height)
@disableWhenOpeningProject
@pyqtSlot(list)
def createPreviewImage(self, components):
dic = {
"components": components,
}
self.queue.put(dic)
@pyqtSlot()
def process(self):
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
try:
nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2:
try:
self.queue.get(block=False)
except Empty:
continue
if self.background.width != width \
or self.background.height != height:
self.background = Checkerboard(width, height)
frame = self.background.copy()
components = nextPreviewInformation["components"]
for component in reversed(components):
try:
component.lockSize(width, height)
newFrame = component.previewRender()
component.unlockSize()
frame = Image.alpha_composite(
frame, newFrame
)
except ValueError as e:
errMsg = "Bad frame returned by %s's preview renderer. " \
"%s. New frame size was %s*%s; should be %s*%s." % (
str(component), str(e).capitalize(),
newFrame.width, newFrame.height,
width, height
)
self.error.emit(errMsg)
break
except RuntimeError as e:
print(e)
else:
self.frame = ImageQt(frame)
self.imageCreated.emit(QtGui.QImage(self.frame))
except Empty:
True

1
src/toolkit/__init__.py Normal file
View File

@ -0,0 +1 @@
from toolkit.common import *

139
src/toolkit/common.py Normal file
View File

@ -0,0 +1,139 @@
'''
Common functions
'''
from PyQt5 import QtWidgets
import string
import os
import sys
import subprocess
from collections import OrderedDict
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
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):
'''Returns string repr of a preset'''
return repr(alphabetizeDict(dictionary))
def presetFromString(string):
'''Turns a string repr of OrderedDict into a regular dict'''
return dict(eval(string))
def appendUppercase(lst):
for form, i in zip(lst, range(len(lst))):
lst.append(form.upper())
return lst
def pipeWrapper(func):
'''A decorator to insert proper kwargs into Popen objects.'''
def pipeWrapper(commandList, **kwargs):
if sys.platform == 'win32':
# Stop CMD window from appearing on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
kwargs['startupinfo'] = startupinfo
if 'bufsize' not in kwargs:
kwargs['bufsize'] = 10**8
if 'stdin' not in kwargs:
kwargs['stdin'] = subprocess.DEVNULL
return func(commandList, **kwargs)
return pipeWrapper
@pipeWrapper
def checkOutput(commandList, **kwargs):
return subprocess.check_output(commandList, **kwargs)
def disableWhenEncoding(func):
def decorator(self, *args, **kwargs):
if self.encoding:
return
else:
return func(self, *args, **kwargs)
return decorator
def disableWhenOpeningProject(func):
def decorator(self, *args, **kwargs):
if self.core.openingProject:
return
else:
return func(self, *args, **kwargs)
return decorator
def rgbFromString(string):
'''Turns an RGB string like "255, 255, 255" into a tuple'''
try:
tup = tuple([int(i) for i in string.split(',')])
if len(tup) != 3:
raise ValueError
for i in tup:
if i > 255 or i < 0:
raise ValueError
return tup
except:
return (255, 255, 255)
def formatTraceback(tb=None):
import traceback
if tb is None:
import sys
tb = sys.exc_info()[2]
return 'Traceback:\n%s' % "\n".join(traceback.format_tb(tb))
def connectWidget(widget, func):
if type(widget) == QtWidgets.QLineEdit:
widget.textChanged.connect(func)
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
widget.valueChanged.connect(func)
elif type(widget) == QtWidgets.QCheckBox:
widget.stateChanged.connect(func)
elif type(widget) == QtWidgets.QComboBox:
widget.currentIndexChanged.connect(func)
else:
return False
return True
def setWidgetValue(widget, val):
'''Generic setValue method for use with any typical QtWidget'''
if type(widget) == QtWidgets.QLineEdit:
widget.setText(val)
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
widget.setValue(val)
elif type(widget) == QtWidgets.QCheckBox:
widget.setChecked(val)
elif type(widget) == QtWidgets.QComboBox:
widget.setCurrentIndex(val)
else:
return False
return True
def getWidgetValue(widget):
if type(widget) == QtWidgets.QLineEdit:
return widget.text()
elif type(widget) == QtWidgets.QSpinBox \
or type(widget) == QtWidgets.QDoubleSpinBox:
return widget.value()
elif type(widget) == QtWidgets.QCheckBox:
return widget.isChecked()
elif type(widget) == QtWidgets.QComboBox:
return widget.currentIndex()

459
src/toolkit/ffmpeg.py Normal file
View File

@ -0,0 +1,459 @@
'''
Tools for using ffmpeg
'''
import numpy
import sys
import os
import subprocess
import threading
import signal
from queue import PriorityQueue
import core
from toolkit.common import checkOutput, pipeWrapper
from component import ComponentError
class FfmpegVideo:
'''Opens a pipe to ffmpeg and stores a buffer of raw video frames.'''
# error from the thread used to fill the buffer
threadError = None
def __init__(self, **kwargs):
mandatoryArgs = [
'inputPath',
'filter_',
'width',
'height',
'frameRate', # frames per second
'chunkSize', # number of bytes in one frame
'parent', # mainwindow object
'component', # component object
]
for arg in mandatoryArgs:
setattr(self, arg, kwargs[arg])
self.frameNo = -1
self.currentFrame = 'None'
self.map_ = None
if 'loopVideo' in kwargs and kwargs['loopVideo']:
self.loopValue = '-1'
else:
self.loopValue = '0'
if 'filter_' in kwargs:
if kwargs['filter_'][0] != '-filter_complex':
kwargs['filter_'].insert(0, '-filter_complex')
else:
kwargs['filter_'] = None
self.command = [
core.Core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-r', str(self.frameRate),
'-stream_loop', self.loopValue,
'-i', self.inputPath,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
if type(kwargs['filter_']) is list:
self.command.extend(
kwargs['filter_']
)
self.command.extend([
'-codec:v', 'rawvideo', '-',
])
self.frameBuffer = PriorityQueue()
self.frameBuffer.maxsize = self.frameRate
self.finishedFrames = {}
self.thread = threading.Thread(
target=self.fillBuffer,
name='FFmpeg Frame-Fetcher'
)
self.thread.daemon = True
self.thread.start()
def frame(self, num):
while True:
if num in self.finishedFrames:
image = self.finishedFrames.pop(num)
return image
i, image = self.frameBuffer.get()
self.finishedFrames[i] = image
self.frameBuffer.task_done()
def fillBuffer(self):
logFilename = os.path.join(
core.Core.dataDir, 'extra_%s.log' % str(self.component.compPos))
with open(logFilename, 'w') as log:
log.write(" ".join(self.command) + '\n\n')
with open(logFilename, 'a') as log:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=log, bufsize=10**8
)
while True:
if self.parent.canceled:
break
self.frameNo += 1
# If we run out of frames, use the last good frame and loop.
try:
if len(self.currentFrame) == 0:
self.frameBuffer.put((self.frameNo-1, self.lastFrame))
continue
except AttributeError:
FfmpegVideo.threadError = ComponentError(
self.component, 'video',
"Video seemed playable but wasn't."
)
break
try:
self.currentFrame = self.pipe.stdout.read(self.chunkSize)
except ValueError:
FfmpegVideo.threadError = ComponentError(
self.component, 'video')
if len(self.currentFrame) != 0:
self.frameBuffer.put((self.frameNo, self.currentFrame))
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
if sys.platform == "win32":
return os.path.join(core.Core.wd, 'ffmpeg.exe')
else:
return os.path.join(core.Core.wd, 'ffmpeg')
else:
if sys.platform == "win32":
return "ffmpeg"
else:
try:
with open(os.devnull, "w") as f:
checkOutput(
['ffmpeg', '-version'], stderr=f
)
return "ffmpeg"
except subprocess.CalledProcessError:
return "avconv"
def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
'''
Constructs the major ffmpeg command used to export the video
'''
if duration == -1:
duration = getAudioDuration(inputFile)
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
duration = "{0:.3f}".format(duration + 0.1) # used by input sources
Core = core.Core
# Test if user has libfdk_aac
encoders = checkOutput(
"%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True
)
encoders = encoders.decode("utf-8")
acodec = Core.settings.value('outputAudioCodec')
options = Core.encoderOptions
containerName = Core.settings.value('outputContainer')
vcodec = Core.settings.value('outputVideoCodec')
vbitrate = str(Core.settings.value('outputVideoBitrate'))+'k'
acodec = Core.settings.value('outputAudioCodec')
abitrate = str(Core.settings.value('outputAudioBitrate'))+'k'
for cont in options['containers']:
if cont['name'] == containerName:
container = cont['container']
break
vencoders = options['video-codecs'][vcodec]
aencoders = options['audio-codecs'][acodec]
for encoder in vencoders:
if encoder in encoders:
vencoder = encoder
break
for encoder in aencoders:
if encoder in encoders:
aencoder = encoder
break
ffmpegCommand = [
Core.FFMPEG_BIN,
'-thread_queue_size', '512',
'-y', # overwrite the output file if it already exists.
# INPUT VIDEO
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-s', '%sx%s' % (
Core.settings.value('outputWidth'),
Core.settings.value('outputHeight'),
),
'-pix_fmt', 'rgba',
'-r', Core.settings.value('outputFrameRate'),
'-t', duration,
'-i', '-', # the video input comes from a pipe
'-an', # the video input has no sound
# INPUT SOUND
'-t', duration,
'-i', inputFile
]
extraAudio = [
comp.audio for comp in components
if 'audio' in comp.properties()
]
segment = createAudioFilterCommand(extraAudio, safeDuration)
ffmpegCommand.extend(segment)
if segment:
# Only map audio from the filters, and video from the pipe
ffmpegCommand.extend([
'-map', '0:v',
'-map', '[a]',
])
ffmpegCommand.extend([
# OUTPUT
'-vcodec', vencoder,
'-acodec', aencoder,
'-b:v', vbitrate,
'-b:a', abitrate,
'-pix_fmt', Core.settings.value('outputVideoFormat'),
'-preset', Core.settings.value('outputPreset'),
'-f', container
])
if acodec == 'aac':
ffmpegCommand.append('-strict')
ffmpegCommand.append('-2')
ffmpegCommand.append(outputFile)
return ffmpegCommand
def createAudioFilterCommand(extraAudio, duration):
'''Add extra inputs and any needed filters to the main ffmpeg command.'''
# NOTE: Global filters are currently hard-coded here for debugging use
globalFilters = 0 # increase to add global filters
if not extraAudio and not globalFilters:
return []
ffmpegCommand = []
# Add -i options for extra input files
extraFilters = {}
for streamNo, params in enumerate(reversed(extraAudio)):
extraInputFile, params = params
ffmpegCommand.extend([
'-t', duration,
# Tell ffmpeg about shorter clips (seemingly not needed)
# streamDuration = getAudioDuration(extraInputFile)
# if streamDuration and streamDuration > float(safeDuration)
# else "{0:.3f}".format(streamDuration),
'-i', extraInputFile
])
# 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])
)
)
# Join all the filters together and combine into 1 stream
extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \
if tmpInputs else ''
ffmpegCommand.extend([
'-filter_complex',
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)
),
])
return ffmpegCommand
def testAudioStream(filename):
'''Test if an audio stream definitely exists'''
audioTestCommand = [
core.Core.FFMPEG_BIN,
'-i', filename,
'-vn', '-f', 'null', '-'
]
try:
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
return False
else:
return True
def getAudioDuration(filename):
'''Try to get duration of audio file as float, or False if not possible'''
command = [core.Core.FFMPEG_BIN, '-i', filename]
try:
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as ex:
fileInfo = ex.output
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]
d = d.split(' ')[3]
d = d.split(':')
duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
break
else:
# String not found in output
return False
return duration
def readAudioFile(filename, videoWorker):
'''
Creates the completeAudioArray given to components
and used to draw the classic visualizer.
'''
duration = getAudioDuration(filename)
if not duration:
print('Audio file doesn\'t exist or unreadable.')
return
command = [
core.Core.FFMPEG_BIN,
'-i', filename,
'-f', 's16le',
'-acodec', 'pcm_s16le',
'-ar', '44100', # ouput will have 44100 Hz
'-ac', '1', # mono (set to '2' for stereo)
'-']
in_pipe = openPipe(
command,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
)
completeAudioArray = numpy.empty(0, dtype="int16")
progress = 0
lastPercent = None
while True:
if core.Core.canceled:
return
# read 2 seconds of audio
progress += 4
raw_audio = in_pipe.stdout.read(88200*4)
if len(raw_audio) == 0:
break
audio_array = numpy.fromstring(raw_audio, dtype="int16")
completeAudioArray = numpy.append(completeAudioArray, audio_array)
percent = int(100*(progress/duration))
if percent >= 100:
percent = 100
if lastPercent != percent:
string = 'Loading audio file: '+str(percent)+'%'
videoWorker.progressBarSetText.emit(string)
videoWorker.progressBarUpdate.emit(percent)
lastPercent = percent
in_pipe.kill()
in_pipe.wait()
# add 0s the end
completeAudioArrayCopy = numpy.zeros(
len(completeAudioArray) + 44100, dtype="int16")
completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
completeAudioArray = completeAudioArrayCopy
return (completeAudioArray, duration)
def exampleSound():
return (
'aevalsrc=tan(random(1)*PI*t)*sin(random(0)*2*PI*t),'
'apulsator=offset_l=0.5:offset_r=0.5,'
)

94
src/toolkit/frame.py Normal file
View File

@ -0,0 +1,94 @@
'''
Common tools for drawing compatible frames in a Component's frameRender()
'''
from PyQt5 import QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
import sys
import os
import math
import core
class FramePainter(QtGui.QPainter):
'''
A QPainter for a blank frame, which can be converted into a
Pillow image with finalize()
'''
def __init__(self, width, height):
image = BlankFrame(width, height)
self.image = QtGui.QImage(ImageQt(image))
super().__init__(self.image)
def setPen(self, RgbTuple):
super().setPen(PaintColor(*RgbTuple))
def finalize(self):
self.end()
imBytes = self.image.bits().asstring(self.image.byteCount())
return Image.frombytes(
'RGBA', (self.image.width(), self.image.height()), imBytes
)
class PaintColor(QtGui.QColor):
'''Reverse the painter colour if the hardware stores RGB values backward'''
def __init__(self, r, g, b, a=255):
if sys.byteorder == 'big':
super().__init__(r, g, b, a)
else:
super().__init__(b, g, r, a)
def scale(scalePercent, width, height, returntype=None):
width = (float(width) / 100.0) * float(scalePercent)
height = (float(height) / 100.0) * float(scalePercent)
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):
if len(args) < 2:
newArgs = list(args)
if len(args) == 0 or len(args) == 1:
height = int(core.Core.settings.value("outputHeight"))
newArgs.append(height)
if len(args) == 0:
width = int(core.Core.settings.value("outputWidth"))
newArgs.insert(0, width)
args = tuple(newArgs)
return framefunc(*args)
return decorator
def FloodFrame(width, height, RgbaTuple):
return Image.new("RGBA", (width, height), RgbaTuple)
@defaultSize
def BlankFrame(width, height):
'''The base frame used by each component to start drawing.'''
return FloodFrame(width, height, (0, 0, 0, 0))
@defaultSize
def Checkerboard(width, height):
'''
A checkerboard to represent transparency to the user.
TODO: Would be cool to generate this image with numpy instead.
'''
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
os.path.join(core.Core.wd, "background.png")),
(0, 0)
)
image = image.resize((width, height))
return image

362
src/video_thread.py Normal file
View File

@ -0,0 +1,362 @@
'''
Thread created to export a video. It has a slot to begin export using
an input file, output path, and component list. During export multiple
threads are created to render the video as quickly as possible. Signals
are emitted to update MainWindow's progress bar, detail text, and preview.
Export can be cancelled with cancel()
'''
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PIL import Image
from PIL.ImageQt import ImageQt
import numpy
import subprocess as sp
import sys
import os
from queue import Queue, PriorityQueue
from threading import Thread, Event
import time
import signal
from component import ComponentError
from toolkit.frame import Checkerboard
from toolkit.ffmpeg import (
openPipe, readAudioFile,
getAudioDuration, createFfmpegCommand
)
class Worker(QtCore.QObject):
imageCreated = pyqtSignal(['QImage'])
videoCreated = pyqtSignal()
progressBarUpdate = pyqtSignal(int)
progressBarSetText = pyqtSignal(str)
encoding = pyqtSignal(bool)
def __init__(self, parent, inputFile, outputFile, components):
QtCore.QObject.__init__(self)
self.core = parent.core
self.settings = parent.settings
self.modules = parent.core.modules
parent.createVideo.connect(self.createVideo)
self.parent = parent
self.components = components
self.outputFile = outputFile
self.inputFile = inputFile
self.sampleSize = 1470 # 44100 / 30 = 1470
self.canceled = False
self.error = False
self.stopped = False
def renderNode(self):
'''
Grabs audio data indices at frames to export, from compositeQueue.
Sends it to the components' frameRender methods in layer order
to create subframes & composite them into the final frame.
The resulting frames are collected in the renderQueue
'''
while not self.stopped:
audioI = self.compositeQueue.get()
bgI = int(audioI / self.sampleSize)
frame = None
for layerNo, comp in enumerate(reversed((self.components))):
if layerNo in self.staticComponents:
if self.staticComponents[layerNo] is None:
# this layer was merged into a following layer
continue
# static component
if frame is None: # bottom-most layer
frame = self.staticComponents[layerNo]
else:
frame = Image.alpha_composite(
frame, self.staticComponents[layerNo]
)
else:
# animated component
if frame is None: # bottom-most layer
frame = comp.frameRender(bgI)
else:
frame = Image.alpha_composite(
frame, comp.frameRender(bgI)
)
self.renderQueue.put([audioI, frame])
self.compositeQueue.task_done()
def renderDispatch(self):
'''
Places audio data indices in the compositeQueue, to be used
by a renderNode later. All indices are multiples of self.sampleSize
sampleSize * frameNo = audioI, AKA audio data starting at frameNo
'''
print('Dispatching Frames for Compositing...')
for audioI in range(0, len(self.completeAudioArray), self.sampleSize):
self.compositeQueue.put(audioI)
def previewDispatch(self):
'''
Grabs frames from the previewQueue, adds them to the checkerboard
and emits a final QImage to the MainWindow for the live preview
'''
background = Checkerboard(self.width, self.height)
while not self.stopped:
audioI, frame = self.previewQueue.get()
if time.time() - self.lastPreview >= 0.06 or audioI == 0:
image = Image.alpha_composite(background.copy(), frame)
self.imageCreated.emit(QtGui.QImage(ImageQt(image)))
self.lastPreview = time.time()
self.previewQueue.task_done()
@pyqtSlot()
def createVideo(self):
numpy.seterr(divide='ignore')
self.encoding.emit(True)
self.extraAudio = []
self.width = int(self.settings.value('outputWidth'))
self.height = int(self.settings.value('outputHeight'))
self.compositeQueue = Queue()
self.compositeQueue.maxsize = 20
self.renderQueue = PriorityQueue()
self.renderQueue.maxsize = 20
self.previewQueue = PriorityQueue()
self.reset()
progressBarValue = 0
self.progressBarUpdate.emit(progressBarValue)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# READ AUDIO, INITIALIZE COMPONENTS, OPEN A PIPE TO FFMPEG
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
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...")
canceledByComponent = False
print('Loaded Components:', ", ".join([
"%s) %s" % (num, str(component))
for num, component in enumerate(reversed(self.components))
]))
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
try:
comp.preFrameRender(
audioFile=self.inputFile,
completeAudioArray=self.completeAudioArray,
sampleSize=self.sampleSize,
progressBarUpdate=self.progressBarUpdate,
progressBarSetText=self.progressBarSetText
)
except ComponentError:
pass
compProps = comp.properties()
if 'error' in compProps or comp._lockedError is not None:
self.cancel()
self.canceled = True
canceledByComponent = True
compError = comp.error() \
if type(comp.error()) is tuple else (comp.error(), '')
errMsg = (
"Component #%s (%s) encountered an error!" % (
str(compNo), comp.name
)
if comp.error() is None else
'Export cancelled by component #%s (%s): %s' % (
str(compNo),
comp.name,
compError[0]
)
)
comp._error.emit(errMsg, compError[1])
break
if 'static' in compProps:
self.staticComponents[compNo] = \
comp.frameRender(0).copy()
if self.canceled:
if canceledByComponent:
print('Export cancelled by component #%s (%s): %s' % (
compNo,
comp.name,
'No message.' if comp.error() is None else (
comp.error() if type(comp.error()) is str
else comp.error()[0])
)
)
self.cancelExport()
return
# Merge consecutive static component frames together
for compNo in range(len(self.components)):
if compNo not in self.staticComponents \
or compNo + 1 not in self.staticComponents:
continue
self.staticComponents[compNo + 1] = Image.alpha_composite(
self.staticComponents.pop(compNo),
self.staticComponents[compNo + 1]
)
self.staticComponents[compNo] = None
ffmpegCommand = createFfmpegCommand(
self.inputFile, self.outputFile, self.components, duration
)
print('###### FFMPEG COMMAND ######\n%s' % " ".join(ffmpegCommand))
print('############################')
self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# START CREATING THE VIDEO
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Make 2 or 3 renderNodes in new threads to create the frames
self.renderThreads = []
try:
numCpus = len(os.sched_getaffinity(0))
except Exception:
numCpus = os.cpu_count()
for i in range(2 if numCpus <= 2 else 3):
self.renderThreads.append(
Thread(target=self.renderNode, name="Render Thread"))
self.renderThreads[i].daemon = True
self.renderThreads[i].start()
self.dispatchThread = Thread(
target=self.renderDispatch, name="Render Dispatch Thread")
self.dispatchThread.daemon = True
self.dispatchThread.start()
self.lastPreview = 0.0
self.previewDispatch = Thread(
target=self.previewDispatch, name="Render Dispatch Thread"
)
self.previewDispatch.daemon = True
self.previewDispatch.start()
# Begin piping into ffmpeg!
frameBuffer = {}
progressBarValue = 0
self.progressBarUpdate.emit(progressBarValue)
self.progressBarSetText.emit("Exporting video...")
if not self.canceled:
for audioI in range(
0, len(self.completeAudioArray), self.sampleSize):
while True:
if audioI in frameBuffer or self.canceled:
# if frame's in buffer, pipe it to ffmpeg
break
# else fetch the next frame & add to the buffer
audioI_, frame = self.renderQueue.get()
frameBuffer[audioI_] = frame
self.renderQueue.task_done()
if self.canceled:
break
try:
self.out_pipe.stdin.write(frameBuffer[audioI].tobytes())
self.previewQueue.put([audioI, frameBuffer.pop(audioI)])
except Exception:
break
# increase progress bar value
completion = (audioI / len(self.completeAudioArray)) * 100
if progressBarValue + 1 <= completion:
progressBarValue = numpy.floor(completion)
self.progressBarUpdate.emit(progressBarValue)
self.progressBarSetText.emit(
"Exporting video: %s%%" % str(int(progressBarValue))
)
numpy.seterr(all='print')
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()
self.error = True
self.out_pipe.wait()
for comp in reversed(self.components):
comp.postFrameRender()
if self.canceled:
print("Export Canceled")
try:
os.remove(self.outputFile)
except Exception:
pass
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Canceled')
else:
if self.error:
print("Export Failed")
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Failed')
else:
print("Export Complete")
self.progressBarUpdate.emit(100)
self.progressBarSetText.emit('Export Complete')
self.error = False
self.canceled = False
self.stopped = True
self.encoding.emit(False)
self.videoCreated.emit()
def cancelExport(self):
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit('Export Canceled')
self.encoding.emit(False)
self.videoCreated.emit()
def updateProgress(self, pStr, pVal):
self.progressBarValue.emit(pVal)
self.progressBarSetText.emit(pStr)
def cancel(self):
self.canceled = True
self.stopped = True
self.core.cancel()
for comp in self.components:
comp.cancel()
try:
self.out_pipe.send_signal(signal.SIGINT)
except Exception:
pass
def reset(self):
self.core.reset()
self.canceled = False
for comp in self.components:
comp.reset()

View File

@ -1,133 +0,0 @@
from PyQt4 import QtCore, QtGui, uic
from PyQt4.QtCore import pyqtSignal, pyqtSlot
from PIL import Image, ImageDraw, ImageFont
from PIL.ImageQt import ImageQt
import core
import numpy
import subprocess as sp
import sys
class Worker(QtCore.QObject):
videoCreated = pyqtSignal()
progressBarUpdate = pyqtSignal(int)
progressBarSetText = pyqtSignal(str)
def __init__(self, parent=None):
QtCore.QObject.__init__(self)
parent.videoTask.connect(self.createVideo)
self.core = core.Core()
@pyqtSlot(str, str, QtGui.QFont, int, int, int, int, tuple, tuple, str, str)
def createVideo(self, backgroundImage, titleText, titleFont, fontSize, alignment,\
xOffset, yOffset, textColor, visColor, inputFile, outputFile):
# print('worker thread id: {}'.format(QtCore.QThread.currentThreadId()))
def getBackgroundAtIndex(i):
return self.core.drawBaseImage(
backgroundFrames[i],
titleText,
titleFont,
fontSize,
alignment,
xOffset,
yOffset,
textColor,
visColor)
progressBarValue = 0
self.progressBarUpdate.emit(progressBarValue)
self.progressBarSetText.emit('Loading background image…')
backgroundFrames = self.core.parseBaseImage(backgroundImage)
if len(backgroundFrames) < 2:
# the base image is not a video so we can draw it now
imBackground = getBackgroundAtIndex(0)
else:
# base images will be drawn while drawing the audio bars
imBackground = None
self.progressBarSetText.emit('Loading audio file…')
completeAudioArray = self.core.readAudioFile(inputFile)
# test if user has libfdk_aac
encoders = sp.check_output(self.core.FFMPEG_BIN + " -encoders -hide_banner", shell=True)
if b'libfdk_aac' in encoders:
acodec = 'libfdk_aac'
else:
acodec = 'aac'
ffmpegCommand = [ self.core.FFMPEG_BIN,
'-y', # (optional) means overwrite the output file if it already exists.
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-s', '1280x720', # size of one frame
'-pix_fmt', 'rgb24',
'-r', '30', # frames per second
'-i', '-', # The input comes from a pipe
'-an',
'-i', inputFile,
'-acodec', acodec, # output audio codec
'-b:a', "192k",
'-vcodec', "libx264",
'-pix_fmt', "yuv420p",
'-preset', "medium",
'-f', "mp4"]
if acodec == 'aac':
ffmpegCommand.append('-strict')
ffmpegCommand.append('-2')
ffmpegCommand.append(outputFile)
out_pipe = sp.Popen(ffmpegCommand,
stdin=sp.PIPE,stdout=sys.stdout, stderr=sys.stdout)
smoothConstantDown = 0.08
smoothConstantUp = 0.8
lastSpectrum = None
sampleSize = 1470
numpy.seterr(divide='ignore')
bgI = 0
for i in range(0, len(completeAudioArray), sampleSize):
# create video for output
lastSpectrum = self.core.transformData(
i,
completeAudioArray,
sampleSize,
smoothConstantDown,
smoothConstantUp,
lastSpectrum)
if imBackground != None:
im = self.core.drawBars(lastSpectrum, imBackground, visColor)
else:
im = self.core.drawBars(lastSpectrum, getBackgroundAtIndex(bgI), visColor)
if bgI < len(backgroundFrames)-1:
bgI += 1
# write to out_pipe
try:
out_pipe.stdin.write(im.tobytes())
finally:
True
# increase progress bar value
if progressBarValue + 1 <= (i / len(completeAudioArray)) * 100:
progressBarValue = numpy.floor((i / len(completeAudioArray)) * 100)
self.progressBarUpdate.emit(progressBarValue)
self.progressBarSetText.emit('%s%%' % str(int(progressBarValue)))
numpy.seterr(all='print')
out_pipe.stdin.close()
if out_pipe.stderr is not None:
print(out_pipe.stderr.read())
out_pipe.stderr.close()
# out_pipe.terminate() # don't terminate ffmpeg too early
out_pipe.wait()
print("Video file created")
self.core.deleteTempDir()
self.progressBarUpdate.emit(100)
self.progressBarSetText.emit('100%')
self.videoCreated.emit()