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.

258 Commits

Author SHA1 Message Date
Brianna 22978a0635 Undo feature 2017-09-02 09:49:35 -04:00
tassaron 8411857030 ctrl-c ends commandline mode properly 2017-08-28 18:54:54 -04:00
tassaron ad6dd9f532 undoable Life component grid actions 2017-08-27 19:59:51 -04:00
tassaron 4a310ffb28 file logging can be turned completely off
and various changes to log levels and messages everywhere
2017-08-27 12:10:21 -04:00
tassaron e8a7b18293 disallow suspiciously enormous floats
this stops a bad project file from crashing my computer...
2017-08-27 09:53:18 -04:00
tassaron 85d3b779d0 fixed too-large Color sizes, fixed a redoing bug, rm pointless things
and now Ctrl+Alt+Shift+A gives a bunch of debug info
2017-08-26 21:23:44 -04:00
tassaron 62e2ef18a3 potential dataDir paths in comments for future reference 2017-08-21 07:06:12 -04:00
tassaron 9d9c4076ac added undo button to GUI
with icons that theoretically should look ok cross-platform
2017-08-20 22:04:57 -04:00
tassaron 6bf8a553d6 don't merge undos when setting text with a button
plus changes to life.py for pep8 compliance
2017-08-20 18:36:43 -04:00
tassaron be9eb9077b relative widgets scale properly when undoing at different resolutions 2017-08-20 17:47:00 -04:00
tassaron d4b63e4d46 remove % from log calls 2017-08-19 20:45:44 -04:00
tassaron c07f2426ce fixed issues with undoing relative widgets 2017-08-19 18:32:12 -04:00
tassaron 87e762a8aa undoable preset open, rename, and delete' 2017-08-17 20:12:46 -04:00
tassaron 43ea3bfd73 component updateWrapper and more obvious method names 2017-08-17 15:12:22 -04:00
tassaron c06ca5cdcb undoable add-comp & clear-preset actions 2017-08-17 10:42:15 -04:00
tassaron f66ec40ba6 undoable component movement 2017-08-16 22:17:12 -04:00
tassaron ddb04f3a2f undo merge IDs given per attr instead of widget type 2017-08-16 21:02:53 -04:00
tassaron f65ced2853 merge consecutive actions on the same widget type 2017-08-16 20:44:37 -04:00
tassaron a1d7cbb984 undoable edits for normal component settings; TODO: merge small edits 2017-08-15 22:20:25 -04:00
tassaron 733c005eea undoable removeComponent action 2017-08-14 18:41:45 -04:00
tassaron a327bec4e4 organizing GUImode-specific code 2017-08-14 16:39:53 -04:00
tassaron 39d6a4e5af rm test.wav 2017-08-14 14:30:57 -04:00
tassaron ea1a422cc5 better aevalsrc inputs for spectrum previews 2017-08-14 14:28:30 -04:00
tassaron bed07479f1 faster Spectrum preview & custom VERBOSE loglvl 2017-08-14 10:10:32 -04:00
tassaron 9c8792df9b made an authors file 2017-08-13 15:54:36 -04:00
tassaron a233d36ce2 graceful renderNode crash, code clean-up 2017-08-13 15:43:42 -04:00
tassaron 3f2834529f fix 2017-08-12 23:03:56 -04:00
tassaron d6b6083f80 rv pointless optimization & remove circular imports (again...)
the last commit does showcase an enlightening bug however
2017-08-12 22:51:46 -04:00
tassaron 282f1c4b12 move previewWindow class into new file
and cache frequently-created blank frames
2017-08-12 09:44:11 -04:00
tassaron 64da6f14ce why did I use a dict here? 2017-08-11 19:03:10 -04:00
tassaron c3f128806b Life comp shift buttons and Show Grid option 2017-08-10 21:57:06 -04:00
tassaron bdb006f25d fixed relative image scale bug & Life preset bug
dicts must be alphabetized in AV files
2017-08-10 17:27:59 -04:00
Brianna c5a0c9b364 Merge pull request #55 from djfun/logging
using the builtin logging module
2017-08-10 16:16:07 -04:00
Brianna 2603c63925 Conway's Game of Life component
cellular automata
2017-08-10 16:09:02 -04:00
tassaron 1c4afc96d6 using the builtin logging module 2017-08-10 16:04:41 -04:00
tassaron 8baa24e878 added connected path shape to Life 2017-08-10 09:12:48 -04:00
tassaron 9732f3bdeb rm garbage file 2017-08-10 00:48:07 -04:00
tassaron cacab464c7 more shapes and custom image option for Life 2017-08-10 00:46:31 -04:00
Brianna 8da72ab3cb Merge pull request #53 from djfun/more-text-options
More text component options
2017-08-09 16:49:41 -04:00
tassaron 8b253717f7 Conway's Game of Life component 2017-08-09 16:46:59 -04:00
tassaron 3ed84e1c3e fixed incorrect outline for small-caps 2017-08-08 20:53:51 -04:00
tassaron 4d0daa4336 relative dropshadow 2017-08-08 06:03:14 -04:00
tassaron 354637d34c relative stroke px size & no Qt pen on stroke 2017-08-08 05:57:19 -04:00
tassaron 060a7dc2d2 dropshadow option for Text component 2017-08-07 21:03:01 -04:00
tassaron 998f741495 added stroke and font style options to Text component 2017-08-06 21:52:44 -04:00
Brianna 20905230fe new Waveform & Spectrum component + relative coords for everything
NOTE: projects and presets saved from old versions will not load correctly anymore
2017-08-03 21:16:51 -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
51 changed files with 12281 additions and 1411 deletions

21
.gitignore vendored
View File

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

11
AUTHORS Normal file
View File

@ -0,0 +1,11 @@
Original version created by Martin Kaistra <djfun>
Version 2 created by tassaron and DH4
Contributors:
* Martin Kaistra <djfun>
* Brianna Rainey <tassaron>
* DH4
Pull Requests By:
* HunterwolfAT
* rikai

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.0rc5'
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'
],
}
)

36
src/__init__.py Normal file
View File

@ -0,0 +1,36 @@
import sys
import os
import logging
class Logger(logging.getLoggerClass()):
'''
Custom Logger class to handle custom VERBOSE log level.
Levels used in this program are as follows:
VERBOSE Annoyingly frequent debug messages (e.g, in loops)
DEBUG Ordinary debug information
INFO Expected events that are expensive or irreversible
WARNING A non-fatal error or suspicious behaviour
ERROR Any error that would interrupt the user
CRITICAL Things that really shouldn't happen at all
'''
def __init__(self, name, level=logging.NOTSET):
super().__init__(name, level)
logging.addLevelName(5, "VERBOSE")
def verbose(self, msg, *args, **kwargs):
if self.isEnabledFor(5):
self._log(5, msg, args, **kwargs)
logging.setLoggerClass(Logger)
logging.VERBOSE = 5
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()

190
src/command.py Normal file
View File

@ -0,0 +1,190 @@
'''
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
import signal
from core import Core
class Command(QtCore.QObject):
createVideo = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
self.core = Core()
Core.mode = 'commandline'
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)
# ctrl-c stops the export thread
signal.signal(signal.SIGINT, self.stopVideo)
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()
def stopVideo(self, *args):
self.worker.error = True
self.worker.cancelExport()
self.worker.cancel()
@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

924
src/component.py Normal file
View File

@ -0,0 +1,924 @@
'''
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
import logging
from copy import copy
from toolkit.frame import BlankFrame
from toolkit import (
getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
)
log = logging.getLogger('AVP.ComponentHandler')
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:
log.verbose(
'### %s #%s renders a preview frame ###',
self.__class__.name, str(self.compPos),
)
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='):
_, preset = arg.split('=', 1)
path = os.path.join(self.core.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 loadPresetWrapper(func):
'''Wraps loadPreset to handle the self.openingPreset boolean'''
class openingPreset:
def __init__(self, comp):
self.comp = comp
def __enter__(self):
self.comp.openingPreset = True
def __exit__(self, *args):
self.comp.openingPreset = False
def presetWrapper(self, *args):
with openingPreset(self):
try:
return func(self, *args)
except Exception:
try:
raise ComponentError(self, 'preset loader')
except ComponentError:
return
return presetWrapper
def updateWrapper(func):
'''
Calls _preUpdate before every subclass update().
Afterwards, for non-user updates, calls _autoUpdate().
For undoable updates triggered by the user, calls _userUpdate()
'''
class wrap:
def __init__(self, comp, auto):
self.comp = comp
self.auto = auto
def __enter__(self):
self.comp._preUpdate()
def __exit__(self, *args):
if self.auto or self.comp.openingPreset \
or not hasattr(self.comp.parent, 'undoStack'):
log.verbose('Automatic update')
self.comp._autoUpdate()
else:
log.verbose('User update')
self.comp._userUpdate()
def updateWrapper(self, **kwargs):
auto = kwargs['auto'] if 'auto' in kwargs else False
with wrap(self, auto):
try:
return func(self)
except Exception:
try:
raise ComponentError(self, 'update method')
except ComponentError:
return
return updateWrapper
def widgetWrapper(func):
'''Connects all widgets to update method after the subclass's method'''
class wrap:
def __init__(self, comp):
self.comp = comp
def __enter__(self):
pass
def __exit__(self, *args):
for widgetList in self.comp._allWidgets.values():
for widget in widgetList:
log.verbose('Connecting %s', str(
widget.__class__.__name__))
connectWidget(widget, self.comp.update)
def widgetWrapper(self, *args, **kwargs):
auto = kwargs['auto'] if 'auto' in kwargs else False
with wrap(self):
try:
return func(self, *args, **kwargs)
except Exception:
try:
raise ComponentError(self, 'widget creation')
except ComponentError:
return
return widgetWrapper
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]
decorate = (
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
'loadPreset', 'command',
'update', 'widget',
)
# Auto-decorate methods
for key in decorate:
if key not in attrs:
continue
if key in ('names'):
attrs[key] = classmethod(attrs[key])
elif key in ('audio'):
attrs[key] = property(attrs[key])
elif key == 'command':
attrs[key] = cls.commandWrapper(attrs[key])
elif key == 'previewRender':
attrs[key] = cls.renderWrapper(attrs[key])
elif key == 'preFrameRender':
attrs[key] = cls.initializationWrapper(attrs[key])
elif key == 'properties':
attrs[key] = cls.propertiesWrapper(attrs[key])
elif key == 'error':
attrs[key] = cls.errorWrapper(attrs[key])
elif key == 'loadPreset':
attrs[key] = cls.loadPresetWrapper(attrs[key])
elif key == 'update':
attrs[key] = cls.updateWrapper(attrs[key])
elif key == 'widget' and parents[0] != QtCore.QObject:
attrs[key] = cls.widgetWrapper(attrs[key])
# Turn version string into a number
try:
if 'version' not in attrs:
log.error(
'No version attribute in %s. Defaulting to 1',
attrs['name'])
attrs['version'] = 1
else:
attrs['version'] = int(attrs['version'].split('.')[0])
except ValueError:
log.critical(
'%s component has an invalid version string:\n%s',
attrs['name'], str(attrs['version'])
)
except KeyError:
log.critical('%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
# STATUS VARIABLES
self.currentPreset = None
self._allWidgets = {}
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 = {}
# LOCKING VARIABLES
self.openingPreset = False
self.mergeUndo = True
self._lockedProperties = None
self._lockedError = None
self._lockedSize = None
# If set to a dict, values are used as basis to update relative widgets
self.oldAttrs = None
# Stop lengthy processes in response to this variable
self.canceled = False
def __str__(self):
return self.__class__.name
def __repr__(self):
import pprint
try:
preset = self.savePreset()
except Exception as e:
preset = '%s occurred while saving preset' % str(e)
return (
'Component(module %s, pos %s) (%s)\n'
'Name: %s v%s\nPreset: %s' % (
self.moduleIndex, self.compPos,
object.__repr__(self),
self.__class__.name, str(self.__class__.version),
pprint.pformat(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) and initialize
'''
self.parent = parent
self.settings = parent.settings
log.verbose(
'Creating UI for %s #%s\'s widget',
self.__class__.name, self.compPos
)
self.page = self.loadUi(self.__class__.ui)
# Find all normal widgets which will be connected after subclass method
self._allWidgets = {
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
}
self._allWidgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
def update(self):
'''
Starting point for a component update. A subclass should override
this method, and the base class will then magically insert a call
to either _autoUpdate() or _userUpdate() at the end.
'''
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]
try:
val = presetDict[key]
except KeyError as e:
log.info(
'%s missing value %s. Outdated preset?',
self.currentPreset, str(e)
)
val = getattr(self, 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 _preUpdate(self):
'''Happens before subclass update()'''
for attr in self._relativeWidgets:
self.updateRelativeWidget(attr)
def _userUpdate(self):
'''Happens after subclass update() for an undoable update by user.'''
oldWidgetVals = {
attr: copy(getattr(self, attr))
for attr in self._trackedWidgets
}
newWidgetVals = {
attr: getWidgetValue(widget)
if attr not in self._colorWidgets else rgbFromString(widget.text())
for attr, widget in self._trackedWidgets.items()
}
modifiedWidgets = {
attr: val
for attr, val in newWidgetVals.items()
if val != oldWidgetVals[attr]
}
if modifiedWidgets:
action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets)
self.parent.undoStack.push(action)
def _autoUpdate(self):
'''Happens after subclass update() for an internal component update.'''
newWidgetVals = {
attr: getWidgetValue(widget)
for attr, widget in self._trackedWidgets.items()
}
self.setAttrs(newWidgetVals)
self._sendUpdateSignal()
def setAttrs(self, attrDict):
'''
Sets attrs (linked to trackedWidgets) in this component to
the values in the attrDict. Mutates certain widget values if needed
'''
for attr, val in attrDict.items():
if attr in self._colorWidgets:
# Color Widgets must have a tuple & have a button to update
if type(val) is tuple:
rgbTuple = val
else:
rgbTuple = rgbFromString(val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
self._colorWidgets[attr].setStyleSheet(btnStyle)
setattr(self, attr, rgbTuple)
else:
# Normal tracked widget
setattr(self, attr, val)
log.verbose('Setting %s self.%s to %s' % (
self.__class__.name, attr, val))
def setWidgetValues(self, attrDict):
'''
Sets widgets defined by keys in trackedWidgets in this preset to
the values in the attrDict.
'''
affectedWidgets = [
self._trackedWidgets[attr] for attr in attrDict
]
with blockSignals(affectedWidgets):
for attr, val in attrDict.items():
widget = self._trackedWidgets[attr]
if attr in self._colorWidgets:
val = '%s,%s,%s' % val
setWidgetValue(widget, val)
def _sendUpdateSignal(self):
if not self.core.openingProject:
self.parent.drawPreview()
saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
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
'colorWidgets': identify attr as RGB tuple & update button CSS
'relativeWidgets': change value proportionally to resolution
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, '_{}'.format(kwarg), kwargs[kwarg])
else:
raise ComponentError(
self, 'Nonsensical keywords to trackWidgets.')
except ComponentError:
continue
if kwarg == 'colorWidgets':
def makeColorFunc(attr):
def pickColor_():
self.mergeUndo = False
self.pickColor(
self._trackedWidgets[attr],
self._colorWidgets[attr]
)
self.mergeUndo = True
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)
setattr(
self, attr, getWidgetValue(self._trackedWidgets[attr])
)
self._preUpdate()
self._autoUpdate()
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):
hasVerticalWords = (
lambda attr:
'height' in attr.lower() or
'ypos' in attr.lower() or
attr == 'y'
)
if 'axis' not in kwargs:
axis = self.width
if hasVerticalWords(attr):
axis = self.height
kwargs['axis'] = axis
if 'axis' in kwargs and type(kwargs['axis']) is tuple:
axis = kwargs['axis'][0]
if hasVerticalWords(attr):
axis = kwargs['axis'][1]
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]
if val > 50.0:
log.warning(
'%s #%s attempted to set %s to dangerously high number %s',
self.__class__.name, self.compPos, attr, val
)
val = 50.0
result = math.ceil(kwargs['axis'] * val)
log.verbose(
'Converting %s: f%s to px%s using axis %s',
attr, val, result, kwargs['axis']
)
return result
@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)
with blockSignals(self._trackedWidgets[attr]):
self._trackedWidgets[attr].setValue(pixelVal)
self.update(auto=True)
def getOldAttr(self, attr):
'''
Returns previous state of this attr. Used to determine whether
a relative widget must be updated. Required because undoing/redoing
can make determining the 'previous' value tricky.
'''
if self.oldAttrs is not None:
return self.oldAttrs[attr]
else:
try:
return getattr(self, attr)
except AttributeError:
log.error('Using visible values instead of oldAttrs')
return self._trackedWidgets[attr].value()
def updateRelativeWidget(self, attr):
'''Called by _preUpdate() for each relativeWidget before each update'''
oldUserValue = self.getOldAttr(attr)
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
log.debug(
'Updating %s #%s\'s relative widget: %s',
self.__class__.name, self.compPos, attr)
with blockSignals(self._trackedWidgets[attr]):
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
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.'
log.error("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)
class ComponentUpdate(QtWidgets.QUndoCommand):
'''Command object for making a component action undoable'''
def __init__(self, parent, oldWidgetVals, modifiedVals):
super().__init__(
'change %s component #%s' % (
parent.name, parent.compPos
)
)
self.undone = False
self.res = (int(parent.width), int(parent.height))
self.parent = parent
self.oldWidgetVals = {
attr: copy(val)
if attr not in self.parent._relativeWidgets
else self.parent.floatValForAttr(attr, val, axis=self.res)
for attr, val in oldWidgetVals.items()
if attr in modifiedVals
}
self.modifiedVals = {
attr: val
if attr not in self.parent._relativeWidgets
else self.parent.floatValForAttr(attr, val, axis=self.res)
for attr, val in modifiedVals.items()
}
# Because relative widgets change themselves every update based on
# their previous value, we must store ALL their values in case of undo
self.relativeWidgetValsAfterUndo = {
attr: copy(getattr(self.parent, attr))
for attr in self.parent._relativeWidgets
}
# Determine if this update is mergeable
self.id_ = -1
if len(self.modifiedVals) == 1 and self.parent.mergeUndo:
attr, val = self.modifiedVals.popitem()
self.id_ = sum([ord(letter) for letter in attr[-14:]])
self.modifiedVals[attr] = val
else:
log.warning(
'%s component settings changed at once. (%s)',
len(self.modifiedVals), repr(self.modifiedVals)
)
def id(self):
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
return self.id_
def mergeWith(self, other):
self.modifiedVals.update(other.modifiedVals)
return True
def setWidgetValues(self, attrDict):
'''
Mask the component's usual method to handle our
relative widgets in case the resolution has changed.
'''
newAttrDict = {
attr: val if attr not in self.parent._relativeWidgets
else self.parent.pixelValForAttr(attr, val)
for attr, val in attrDict.items()
}
self.parent.setWidgetValues(newAttrDict)
def redo(self):
if self.undone:
log.info('Redoing component update')
self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
self.setWidgetValues(self.modifiedVals)
self.parent.update(auto=True)
self.parent.oldAttrs = None
if not self.undone:
self.relativeWidgetValsAfterRedo = {
attr: copy(getattr(self.parent, attr))
for attr in self.parent._relativeWidgets
}
self.parent._sendUpdateSignal()
def undo(self):
log.info('Undoing component update')
self.undone = True
self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
self.setWidgetValues(self.oldWidgetVals)
self.parent.update(auto=True)
self.parent.oldAttrs = None

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,119 @@
<?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="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>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<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>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<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>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<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_4">
<item>
<spacer name="horizontalSpacer_4">
<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>

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

@ -0,0 +1,159 @@
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)
# 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)
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 = FloodFrame(self.sizeWidth, self.sizeHeight, (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)

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

@ -0,0 +1,666 @@
<?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="text">
<string>0,0,0</string>
</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="text">
<string>133,133,133</string>
</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>19200</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>10800</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>

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

@ -0,0 +1,126 @@
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.1'
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,
'stretchScale': self.page.spinBox_scale_stretch,
'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):
scale = self.scale if not self.stretched else self.stretchScale
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 scale != 100:
newHeight = int((image.height / 100) * scale)
newWidth = int((image.width / 100) * 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.mergeUndo = False
self.page.lineEdit_image.setText(filename)
self.mergeUndo = True
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')
def savePreset(self):
# Maintain the illusion that the scale spinbox is one widget
scaleBox = self.page.spinBox_scale
stretchScaleBox = self.page.spinBox_scale_stretch
if self.page.checkBox_stretch.isChecked():
scaleBox.setValue(stretchScaleBox.value())
else:
stretchScaleBox.setValue(scaleBox.value())
return super().savePreset()
def update(self):
# Maintain the illusion that the scale spinbox is one widget
scaleBox = self.page.spinBox_scale
stretchScaleBox = self.page.spinBox_scale_stretch
if self.page.checkBox_stretch.isChecked():
scaleBox.setVisible(False)
stretchScaleBox.setVisible(True)
else:
scaleBox.setVisible(True)
stretchScaleBox.setVisible(False)

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

@ -0,0 +1,388 @@
<?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>
<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>
<item>
<widget class="QSpinBox" name="spinBox_scale_stretch">
<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>
</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>

475
src/components/life.py Normal file
View File

@ -0,0 +1,475 @@
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import QUndoCommand
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
import os
import math
from component import Component
from toolkit.frame import BlankFrame, scale
class Component(Component):
name = 'Conway\'s Game of Life'
version = '1.0.0'
def widget(self, *args):
super().widget(*args)
self.scale = 32
self.updateGridSize()
self.startingGrid = set()
self.page.pushButton_pickImage.clicked.connect(self.pickImage)
self.trackWidgets({
'tickRate': self.page.spinBox_tickRate,
'scale': self.page.spinBox_scale,
'color': self.page.lineEdit_color,
'shapeType': self.page.comboBox_shapeType,
'shadow': self.page.checkBox_shadow,
'customImg': self.page.checkBox_customImg,
'showGrid': self.page.checkBox_showGrid,
'image': self.page.lineEdit_image,
}, colorWidgets={
'color': self.page.pushButton_color,
})
self.shiftButtons = (
self.page.toolButton_up,
self.page.toolButton_down,
self.page.toolButton_left,
self.page.toolButton_right,
)
def shiftFunc(i):
def shift():
self.shiftGrid(i)
return shift
shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))]
for i, widget in enumerate(self.shiftButtons):
widget.clicked.connect(shiftFuncs[i])
self.page.spinBox_scale.setValue(self.scale)
self.page.spinBox_scale.valueChanged.connect(self.updateGridSize)
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.mergeUndo = False
self.page.lineEdit_image.setText(filename)
self.mergeUndo = True
def shiftGrid(self, d):
action = ShiftGrid(self, d)
self.parent.undoStack.push(action)
def update(self):
self.updateGridSize()
if self.page.checkBox_customImg.isChecked():
self.page.label_color.setVisible(False)
self.page.lineEdit_color.setVisible(False)
self.page.pushButton_color.setVisible(False)
self.page.label_shape.setVisible(False)
self.page.comboBox_shapeType.setVisible(False)
self.page.label_image.setVisible(True)
self.page.lineEdit_image.setVisible(True)
self.page.pushButton_pickImage.setVisible(True)
else:
self.page.label_color.setVisible(True)
self.page.lineEdit_color.setVisible(True)
self.page.pushButton_color.setVisible(True)
self.page.label_shape.setVisible(True)
self.page.comboBox_shapeType.setVisible(True)
self.page.label_image.setVisible(False)
self.page.lineEdit_image.setVisible(False)
self.page.pushButton_pickImage.setVisible(False)
enabled = (len(self.startingGrid) > 0)
for widget in self.shiftButtons:
widget.setEnabled(enabled)
def previewClickEvent(self, pos, size, button):
pos = (
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
)
action = ClickGrid(self, pos, button)
self.parent.undoStack.push(action)
def updateGridSize(self):
w, h = self.core.resolutions[-1].split('x')
self.gridWidth = int(int(w) / self.scale)
self.gridHeight = int(int(h) / self.scale)
self.pxWidth = math.ceil(self.width / self.gridWidth)
self.pxHeight = math.ceil(self.height / self.gridHeight)
def previewRender(self):
return self.drawGrid(self.startingGrid)
def preFrameRender(self, *args, **kwargs):
super().preFrameRender(*args, **kwargs)
self.progressBarSetText.emit("Computing evolution...")
self.tickGrids = {0: self.startingGrid}
tick = 0
for frameNo in range(
self.tickRate, self.audioArrayLen, self.sampleSize
):
if self.parent.canceled:
break
if frameNo % self.tickRate == 0:
tick += 1
self.tickGrids[tick] = self.gridForTick(tick)
# update progress bar
progress = int(100*(frameNo/self.audioArrayLen))
if progress >= 100:
progress = 100
pStr = "Computing evolution: "+str(progress)+'%'
self.progressBarSetText.emit(pStr)
self.progressBarUpdate.emit(int(progress))
def properties(self):
if self.customImg and (
not self.image or not os.path.exists(self.image)
):
return ['error']
return []
def error(self):
return "No image selected to represent life."
def frameRender(self, frameNo):
tick = math.floor(frameNo / self.tickRate)
grid = self.tickGrids[tick]
return self.drawGrid(grid)
def drawGrid(self, grid):
frame = BlankFrame(self.width, self.height)
def drawCustomImg():
try:
img = Image.open(self.image)
except Exception:
return
img = img.resize((self.pxWidth, self.pxHeight), Image.ANTIALIAS)
frame.paste(img, box=(drawPtX, drawPtY))
def drawShape():
drawer = ImageDraw.Draw(frame)
rect = (
(drawPtX, drawPtY),
(drawPtX + self.pxWidth, drawPtY + self.pxHeight)
)
shape = self.page.comboBox_shapeType.currentText().lower()
# Rectangle
if shape == 'rectangle':
drawer.rectangle(rect, fill=self.color)
# Elliptical
elif shape == 'elliptical':
drawer.ellipse(rect, fill=self.color)
tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int)
smallerShape = (
(drawPtX + tenthX + int(tenthX / 4),
drawPtY + tenthY + int(tenthY / 2)),
(drawPtX + self.pxWidth - tenthX - int(tenthX / 4),
drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)))
)
outlineShape = (
(drawPtX + int(tenthX / 4),
drawPtY + int(tenthY / 2)),
(drawPtX + self.pxWidth - int(tenthX / 4),
drawPtY + self.pxHeight - int(tenthY / 2))
)
# Circle
if shape == 'circle':
drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
# Lilypad
elif shape == 'lilypad':
drawer.pieslice(smallerShape, 290, 250, fill=self.color)
# Pac-Man
elif shape == 'pac-man':
drawer.pieslice(outlineShape, 35, 320, fill=self.color)
hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
# Path
if shape == 'path':
drawer.ellipse(rect, fill=self.color)
rects = {
direction: False
for direction in (
'up', 'down', 'left', 'right',
)
}
for cell in self.nearbyCoords(x, y):
if cell not in grid:
continue
if cell[0] == x:
if cell[1] < y:
rects['up'] = True
if cell[1] > y:
rects['down'] = True
if cell[1] == y:
if cell[0] < x:
rects['left'] = True
if cell[0] > x:
rects['right'] = True
for direction, rect in rects.items():
if rect:
if direction == 'up':
sect = (
(drawPtX, drawPtY),
(drawPtX + self.pxWidth, drawPtY + hY)
)
elif direction == 'down':
sect = (
(drawPtX, drawPtY + hY),
(drawPtX + self.pxWidth,
drawPtY + self.pxHeight)
)
elif direction == 'left':
sect = (
(drawPtX, drawPtY),
(drawPtX + hX,
drawPtY + self.pxHeight)
)
elif direction == 'right':
sect = (
(drawPtX + hX, drawPtY),
(drawPtX + self.pxWidth,
drawPtY + self.pxHeight)
)
drawer.rectangle(sect, fill=self.color)
# Duck
elif shape == 'duck':
duckHead = (
(drawPtX + qX, drawPtY + qY),
(drawPtX + int(qX * 3), drawPtY + int(tY * 2))
)
duckBeak = (
(drawPtX + hX, drawPtY + qY),
(drawPtX + self.pxWidth + qX,
drawPtY + int(qY * 3))
)
duckWing = (
(drawPtX, drawPtY + hY),
rect[1]
)
duckBody = (
(drawPtX + int(qX / 4), drawPtY + int(qY * 3)),
(drawPtX + int(tX * 2), drawPtY + self.pxHeight)
)
drawer.ellipse(duckBody, fill=self.color)
drawer.ellipse(duckHead, fill=self.color)
drawer.pieslice(duckWing, 130, 200, fill=self.color)
drawer.pieslice(duckBeak, 145, 200, fill=self.color)
# Peace
elif shape == 'peace':
line = ((
drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
(drawPtX + hX + int(tenthX / 2),
drawPtY + self.pxHeight - int(tenthY / 2))
)
drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
drawer.rectangle(line, fill=self.color)
def slantLine(difference):
return (
(drawPtX + difference),
(drawPtY + self.pxHeight - qY)
),
(
(drawPtX + hX),
(drawPtY + hY)
)
drawer.line(
slantLine(qX),
fill=self.color,
width=tenthX
)
drawer.line(
slantLine(self.pxWidth - qX),
fill=self.color,
width=tenthX
)
for x, y in grid:
drawPtX = x * self.pxWidth
if drawPtX > self.width:
continue
drawPtY = y * self.pxHeight
if drawPtY > self.height:
continue
if self.customImg:
drawCustomImg()
else:
drawShape()
if self.shadow:
shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
shadImg = shadImg.filter(ImageFilter.GaussianBlur(5.00))
shadImg = ImageChops.offset(shadImg, -2, 2)
shadImg.paste(frame, box=(0, 0), mask=frame)
frame = shadImg
if self.showGrid:
drawer = ImageDraw.Draw(frame)
w, h = scale(0.05, self.width, self.height, int)
for x in range(self.pxWidth, self.width, self.pxWidth):
drawer.rectangle(
((x, 0),
(x + w, self.height)),
fill=self.color,
)
for y in range(self.pxHeight, self.height, self.pxHeight):
drawer.rectangle(
((0, y),
(self.width, y + h)),
fill=self.color,
)
return frame
def gridForTick(self, tick):
'''Given a tick number over 0, returns a new grid set of tuples'''
lastGrid = self.tickGrids[tick - 1]
def neighbours(x, y):
return {
cell for cell in self.nearbyCoords(x, y)
if cell in lastGrid
}
newGrid = set()
for x, y in lastGrid:
surrounding = len(neighbours(x, y))
if surrounding == 2 or surrounding == 3:
newGrid.add((x, y))
potentialNewCells = {
coordTup for origin in lastGrid
for coordTup in list(self.nearbyCoords(*origin))
}
for x, y in potentialNewCells:
if (x, y) in newGrid:
continue
surrounding = len(neighbours(x, y))
if surrounding == 3:
newGrid.add((x, y))
return newGrid
def savePreset(self):
pr = super().savePreset()
pr['GRID'] = sorted(self.startingGrid)
return pr
def loadPreset(self, pr, *args):
self.startingGrid = set(pr['GRID'])
if self.startingGrid:
for widget in self.shiftButtons:
widget.setEnabled(True)
super().loadPreset(pr, *args)
def nearbyCoords(self, x, y):
yield x + 1, y + 1
yield x + 1, y - 1
yield x - 1, y + 1
yield x - 1, y - 1
yield x, y + 1
yield x, y - 1
yield x + 1, y
yield x - 1, y
class ClickGrid(QUndoCommand):
def __init__(self, comp, pos, id_):
super().__init__(
"click %s component #%s" % (comp.name, comp.compPos))
self.comp = comp
self.pos = [pos]
self.id_ = id_
def id(self):
return self.id_
def mergeWith(self, other):
self.pos.extend(other.pos)
return True
def add(self):
for pos in self.pos[:]:
self.comp.startingGrid.add(pos)
self.comp.update(auto=True)
def remove(self):
for pos in self.pos[:]:
self.comp.startingGrid.discard(pos)
self.comp.update(auto=True)
def redo(self):
if self.id_ == 1: # Left-click
self.add()
elif self.id_ == 2: # Right-click
self.remove()
def undo(self):
if self.id_ == 1: # Left-click
self.remove()
elif self.id_ == 2: # Right-click
self.add()
class ShiftGrid(QUndoCommand):
def __init__(self, comp, direction):
super().__init__(
"change %s component #%s" % (comp.name, comp.compPos))
self.comp = comp
self.direction = direction
self.distance = 1
def id(self):
return self.direction
def mergeWith(self, other):
self.distance += other.distance
return True
def newGrid(self, Xchange, Ychange):
return {
(x + Xchange, y + Ychange)
for x, y in self.comp.startingGrid
}
def redo(self):
if self.direction == 0:
newGrid = self.newGrid(0, -self.distance)
elif self.direction == 1:
newGrid = self.newGrid(0, self.distance)
elif self.direction == 2:
newGrid = self.newGrid(-self.distance, 0)
elif self.direction == 3:
newGrid = self.newGrid(self.distance, 0)
self.comp.startingGrid = newGrid
self.comp._sendUpdateSignal()
def undo(self):
if self.direction == 0:
newGrid = self.newGrid(0, self.distance)
elif self.direction == 1:
newGrid = self.newGrid(0, -self.distance)
elif self.direction == 2:
newGrid = self.newGrid(self.distance, 0)
elif self.direction == 3:
newGrid = self.newGrid(-self.distance, 0)
self.comp.startingGrid = newGrid
self.comp._sendUpdateSignal()

405
src/components/life.ui Normal file
View File

@ -0,0 +1,405 @@
<?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="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Simulation Speed</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_tickRate">
<property name="suffix">
<string> frames per tick</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="value">
<number>5</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>
<item>
<widget class="QLineEdit" name="lineEdit_color">
<property name="maximumSize">
<size>
<width>0</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>0,0,0</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Grid Scale</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_scale">
<property name="minimum">
<number>22</number>
</property>
<property name="maximum">
<number>128</number>
</property>
<property name="value">
<number>32</number>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_customImg">
<property name="text">
<string>Custom Image</string>
</property>
</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_8">
<item>
<widget class="QLabel" name="label_image">
<property name="text">
<string>Image</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_image"/>
</item>
<item>
<widget class="QPushButton" name="pushButton_pickImage">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" 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>...</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_color">
<property name="text">
<string>Color</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_color_3">
<property name="maximumSize">
<size>
<width>0</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>0,0,0</string>
</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_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_shape">
<property name="text">
<string>Shape</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_shapeType">
<item>
<property name="text">
<string>Path</string>
</property>
</item>
<item>
<property name="text">
<string>Rectangle</string>
</property>
</item>
<item>
<property name="text">
<string>Elliptical</string>
</property>
</item>
<item>
<property name="text">
<string>Circle</string>
</property>
</item>
<item>
<property name="text">
<string>Lilypad</string>
</property>
</item>
<item>
<property name="text">
<string>Pac-Man</string>
</property>
</item>
<item>
<property name="text">
<string>Duck</string>
</property>
</item>
<item>
<property name="text">
<string>Peace</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<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="QCheckBox" name="checkBox_shadow">
<property name="text">
<string>Shadow</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_showGrid">
<property name="text">
<string>Show Grid</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>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QToolButton" name="toolButton_up">
<property name="text">
<string>Up</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_down">
<property name="text">
<string>Down</string>
</property>
<property name="arrowType">
<enum>Qt::DownArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_left">
<property name="text">
<string>Left</string>
</property>
<property name="arrowType">
<enum>Qt::LeftArrow</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_right">
<property name="text">
<string>Right</string>
</property>
<property name="arrowType">
<enum>Qt::RightArrow</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_9">
<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>
</item>
<item>
<widget class="QTextBrowser" name="textBrowser">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Click the preview window to place a cell. Right-click to remove.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with less than 2 neighbours will die from underpopulation&lt;/p&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with more than 3 neighbours will die from overpopulation.&lt;/p&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- An empty space surrounded by 3 live cells will cause reproduction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="tabStopWidth">
<number>80</number>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
<property name="openLinks">
<bool>false</bool>
</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>586</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>

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

@ -0,0 +1,73 @@
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.mergeUndo = False
self.page.lineEdit_sound.setText(filename)
self.mergeUndo = True
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>

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

@ -0,0 +1,316 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
import time
import logging
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit import checkOutput, connectWidget
from toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
log = logging.getLogger('AVP.Components.Spectrum')
class Component(Component):
name = 'Spectrum'
version = '1.0.1'
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
self.previewSize = (214, 120)
self.previewPipe = None
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):
filterType = self.page.comboBox_filterType.currentIndex()
self.page.stackedWidget.setCurrentIndex(filterType)
if filterType == 3:
self.page.spinBox_hue.setEnabled(False)
else:
self.page.spinBox_hue.setEnabled(True)
if filterType == 2 or filterType == 4:
self.page.checkBox_mono.setEnabled(False)
else:
self.page.checkBox_mono.setEnabled(True)
def previewRender(self):
changedSize = self.updateChunksize()
if not changedSize \
and not self.changedOptions \
and self.previewFrame is not None:
log.debug(
'Spectrum #%s is reusing old preview frame' % self.compPos)
return self.previewFrame
frame = self.getPreviewFrame()
self.changedOptions = False
if not frame:
log.warning(
'Spectrum #%s failed to create a preview frame' % self.compPos)
self.previewFrame = None
return BlankFrame(self.width, self.height)
else:
self.previewFrame = frame
return frame
def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs)
if self.previewPipe is not None:
self.previewPipe.wait()
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',
self.core.junkStream
if genericPreview else inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
command.extend([
'-an',
'-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
'-codec:v', 'rawvideo', '-',
'-frames:v', '1',
])
if self.core.logEnabled:
logFilename = os.path.join(
self.core.logDir, 'preview_%s.log' % str(self.compPos))
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
with open(logFilename, 'w') as logf:
logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
self.previewPipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
else:
self.previewPipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = self.previewPipe.stdout.read(self.chunkSize)
closePipe(self.previewPipe)
frame = self.finalizeFrame(byteFrame)
return frame
def makeFfmpegFilter(self, preview=False, startPt=0):
if preview:
w, h = self.previewSize
else:
w, h = (self.width, self.height)
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' % (
w, h,
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"),
w, h,
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' % (
w, h,
'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"),
w, h,
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"),
w, h,
)
)
if self.filterType < 2:
exampleSnd = exampleSound('freq')
elif self.filterType == 2 or self.filterType == 4:
exampleSnd = exampleSound('stereo')
elif self.filterType == 3:
exampleSnd = exampleSound('white')
return [
'-filter_complex',
'%s%s%s%s [v1]; '
'[v1] %s%s%s%s%s [v]' % (
exampleSnd if preview and genericPreview else '[0:a] ',
'compand=gain=4,' if self.compress else '',
'aformat=channel_layouts=mono,'
if self.mono and self.filterType not in (2, 4) else '',
filter_,
'hflip, ' if self.mirror else '',
'trim=start=%s:end=%s, ' % (
"{0:.3f}".format(startPt + 12),
"{0:.3f}".format(startPt + 12.5)
) if preview else '',
'scale=%sx%s' % scale(
self.scale, self.width, self.height, str),
', hue=h=%s:s=10' % str(self.hue)
if self.hue > 0 and self.filterType != 3 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
frame = BlankFrame(self.width, self.height)
frame.paste(image, box=(self.x, self.y))
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>

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

@ -0,0 +1,203 @@
from PIL import ImageEnhance, ImageFilter, ImageChops
from PyQt5.QtGui import QColor, QFont
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import logging
from component import Component
from toolkit.frame import FramePainter, PaintColor
log = logging.getLogger('AVP.Components.Text')
class Component(Component):
name = 'Title Text'
version = '1.0.1'
def widget(self, *args):
super().widget(*args)
self.title = 'Text'
self.alignment = 1
self.titleFont = QFont()
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.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,
'fontStyle': self.page.comboBox_fontStyle,
'stroke': self.page.spinBox_stroke,
'strokeColor': self.page.lineEdit_strokeColor,
'shadow': self.page.checkBox_shadow,
'shadX': self.page.spinBox_shadX,
'shadY': self.page.spinBox_shadY,
'shadBlur': self.page.spinBox_shadBlur,
}, colorWidgets={
'textColor': self.page.pushButton_textColor,
'strokeColor': self.page.pushButton_strokeColor,
}, relativeWidgets=[
'xPosition', 'yPosition', 'fontSize',
'stroke', 'shadX', 'shadY', 'shadBlur'
])
self.centerXY()
def update(self):
self.titleFont = self.page.fontComboBox_titleFont.currentFont()
if self.page.checkBox_shadow.isChecked():
self.page.label_shadX.setHidden(False)
self.page.spinBox_shadX.setHidden(False)
self.page.spinBox_shadY.setHidden(False)
self.page.label_shadBlur.setHidden(False)
self.page.spinBox_shadBlur.setHidden(False)
else:
self.page.label_shadX.setHidden(True)
self.page.spinBox_shadX.setHidden(True)
self.page.spinBox_shadY.setHidden(True)
self.page.label_shadBlur.setHidden(True)
self.page.spinBox_shadBlur.setHidden(True)
def centerXY(self):
self.setRelativeWidget('xPosition', 0.5)
self.setRelativeWidget('yPosition', 0.521)
def getXY(self):
'''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont)
x = self.pixelValForAttr('xPosition')
if self.alignment == 1: # Middle
offset = int(fm.width(self.title)/2)
x -= offset
if self.alignment == 2: # Right
offset = fm.width(self.title)
x -= 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):
font = self.titleFont
font.setPixelSize(self.fontSize)
font.setStyle(QFont.StyleNormal)
font.setWeight(QFont.Normal)
font.setCapitalization(QFont.MixedCase)
if self.fontStyle == 1:
font.setWeight(QFont.DemiBold)
if self.fontStyle == 2:
font.setWeight(QFont.Bold)
elif self.fontStyle == 3:
font.setStyle(QFont.StyleItalic)
elif self.fontStyle == 4:
font.setWeight(QFont.Bold)
font.setStyle(QFont.StyleItalic)
elif self.fontStyle == 5:
font.setStyle(QFont.StyleOblique)
elif self.fontStyle == 6:
font.setCapitalization(QFont.SmallCaps)
image = FramePainter(width, height)
x, y = self.getXY()
log.debug('Text position translates to %s, %s', x, y)
if self.stroke > 0:
outliner = QtGui.QPainterPathStroker()
outliner.setWidth(self.stroke)
path = QtGui.QPainterPath()
if self.fontStyle == 6:
# PathStroker ignores smallcaps so we need this weird hack
path.addText(x, y, font, self.title[0])
fm = QtGui.QFontMetrics(font)
newX = x + fm.width(self.title[0])
strokeFont = self.page.fontComboBox_titleFont.currentFont()
strokeFont.setCapitalization(QFont.SmallCaps)
strokeFont.setPixelSize(int((self.fontSize / 7) * 5))
strokeFont.setLetterSpacing(QFont.PercentageSpacing, 139)
path.addText(newX, y, strokeFont, self.title[1:])
else:
path.addText(x, y, font, self.title)
path = outliner.createStroke(path)
image.setPen(QtCore.Qt.NoPen)
image.setBrush(PaintColor(*self.strokeColor))
image.drawPath(path)
image.setFont(font)
image.setPen(self.textColor)
image.drawText(x, y, self.title)
# turn QImage into Pillow frame
frame = image.finalize()
if self.shadow:
shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
shadImg = shadImg.filter(ImageFilter.GaussianBlur(self.shadBlur))
shadImg = ImageChops.offset(shadImg, self.shadX, self.shadY)
shadImg.paste(frame, box=(0, 0), mask=frame)
frame = shadImg
return frame
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)

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

@ -0,0 +1,671 @@
<?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="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<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="Expanding" 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>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" 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="Preferred" 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_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">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
</widget>
</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="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Center Text</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="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</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">
<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="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>50</width>
<height>16777215</height>
</size>
</property>
<property name="maximum">
<number>999999999</number>
</property>
</widget>
</item>
</layout>
</item>
<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="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Text Color</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" 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="MaximumSize" stdset="0">
<size>
<width>32</width>
<height>32</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="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string/>
</property>
<property name="prefix">
<string/>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>500</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_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Font Style</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_fontStyle">
<item>
<property name="text">
<string>Normal</string>
</property>
</item>
<item>
<property name="text">
<string>Semi-Bold</string>
</property>
</item>
<item>
<property name="text">
<string>Bold</string>
</property>
</item>
<item>
<property name="text">
<string>Italic</string>
</property>
</item>
<item>
<property name="text">
<string>Bold Italic</string>
</property>
</item>
<item>
<property name="text">
<string>Faux Italic</string>
</property>
</item>
<item>
<property name="text">
<string>Small Caps</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QLineEdit" name="lineEdit_textColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>0</width>
<height>16777215</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>255,255,255</string>
</property>
</widget>
</item>
<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>Stroke</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_stroke">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string>px</string>
</property>
</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>Stroke Color</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_strokeColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>0</width>
<height>16777215</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>0,0,0</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_strokeColor">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" 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="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_2">
<item>
<widget class="QCheckBox" name="checkBox_shadow">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Shadow</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_shadX">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Shadow Offset</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_shadX">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>-4</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_shadY">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>-1000</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>8</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_shadBlur">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Shadow Blur</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="spinBox_shadBlur">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>5.000000000000000</double>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<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>
</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>

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

@ -0,0 +1,226 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
import os
import math
import subprocess
import logging
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo
from toolkit import checkOutput
log = logging.getLogger('AVP.Components.Video')
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)
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.mergeUndo = False
self.page.lineEdit_video.setText(filename)
self.mergeUndo = True
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',
])
if self.core.logEnabled:
logFilename = os.path.join(
self.core.logDir, 'preview_%s.log' % str(self.compPos))
log.debug('Creating ffmpeg process (log at %s)' % logFilename)
with open(logFilename, 'w') as logf:
logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
else:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
frame = self.finalizeFrame(byteFrame)
return frame
def makeFfmpegFilter(self):
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>

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

@ -0,0 +1,215 @@
from PIL import Image
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtGui import QColor
import os
import math
import subprocess
import logging
from component import Component
from toolkit.frame import BlankFrame, scale
from toolkit import checkOutput
from toolkit.ffmpeg import (
openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound
)
log = logging.getLogger('AVP.Components.Waveform')
class Component(Component):
name = 'Waveform'
version = '1.0.0'
def widget(self, *args):
super().widget(*args)
self._image = BlankFrame(self.width, self.height)
self.page.lineEdit_color.setText('255,255,255')
if hasattr(self.parent, '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',
self.core.junkStream
if genericPreview else inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',
]
command.extend(self.makeFfmpegFilter(preview=True, startPt=startPt))
command.extend([
'-an',
'-s:v', '%sx%s' % scale(self.scale, self.width, self.height, str),
'-codec:v', 'rawvideo', '-',
'-frames:v', '1',
])
if self.core.logEnabled:
logFilename = os.path.join(
self.core.logDir, 'preview_%s.log' % str(self.compPos))
log.debug('Creating ffmpeg log at %s', logFilename)
with open(logFilename, 'w') as logf:
logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
else:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, bufsize=10**8
)
byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe)
frame = self.finalizeFrame(byteFrame)
return frame
def makeFfmpegFilter(self, preview=False, startPt=0):
w, h = scale(self.scale, self.width, self.height, str)
if self.amplitude == 0:
amplitude = 'lin'
elif self.amplitude == 1:
amplitude = 'log'
elif self.amplitude == 2:
amplitude = 'sqrt'
elif self.amplitude == 3:
amplitude = 'cbrt'
hexcolor = QColor(*self.color).name()
opacity = "{0:.1f}".format(self.opacity / 100)
genericPreview = self.settings.value("pref_genericPreview")
if self.mode < 3:
filter_ = (
'showwaves='
'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'
)
)
baselineHeight = int(self.height * (4 / 1080))
return [
'-filter_complex',
'%s%s%s'
'%s%s%s [v1]; '
'[v1] scale=%s:%s%s [v]' % (
exampleSound('wave', extra='')
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=%s:color=%s@%s' % (
baselineHeight, 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>

606
src/core.py Normal file
View File

@ -0,0 +1,606 @@
'''
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 logging
import toolkit
log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.WARNING
FILE_LOGLVL = None
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 __repr__(self):
return "\n=~=~=~=\n".join(
[repr(comp) for comp in self.selectedComponents]
)
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
log.debug('Importing component modules')
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, component, loader):
'''
Creates a new component using these args:
(compPos, component obj or moduleIndex, MWindow/Command/Core obj)
'''
if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return -1
if type(component) is int:
# create component using module index in self.modules
moduleIndex = int(component)
log.debug(
'Creating new component from module #%s', str(moduleIndex))
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
)
component.widget(loader)
else:
moduleIndex = -1
log.debug(
'Inserting previously-created %s component', component.name)
component._error.connect(
loader.videoThreadError
)
self.selectedComponents.insert(
compPos,
component
)
if hasattr(loader, 'insertComponent'):
loader.insertComponent(compPos)
self.componentListChanged()
self.updateComponent(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):
log.debug(
'Auto-updating %s #%s',
self.selectedComponents[i], str(i))
self.selectedComponents[i].update(auto=True)
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
comp = self.selectedComponents[compIndex]
comp.loadPreset(
saveValueStore,
presetName
)
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 getPresetDir(self, comp):
'''Get the preset subdir for a particular version of a component'''
return os.path.join(Core.presetDir, comp.name, str(comp.version))
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)
with toolkit.blockSignals(widget):
toolkit.setWidgetValue(widget, value)
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 == -1:
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:
log.warning('%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
log.warning('Project 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)
'''
log.debug('Parsing av file: %s', filepath)
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'''
log.info('Creating %s', filepath)
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:
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'''
import video_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
)
# Windows: C:/Users/<USER>/AppData/Local/audio-visualizer
# macOS: ~/Library/Preferences/audio-visualizer
# Linux: ~/.config/audio-visualizer
with open(os.path.join(wd, 'encoder-options.json')) as json_file:
encoderOptions = json.load(json_file)
settings = {
'canceled': False,
'FFMPEG_BIN': findFfmpeg(),
'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'),
'junkStream': os.path.join(wd, 'gui', 'background.png'),
'encoderOptions': encoderOptions,
'resolutions': [
'1920x1080',
'1280x720',
'854x480',
],
'logDir': os.path.join(dataDir, 'log'),
'logEnabled': 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()
if not os.path.exists(cls.dataDir):
os.makedirs(cls.dataDir)
for neededDirectory in (
cls.presetDir, cls.logDir, cls.settings.value("projectDir")):
if not os.path.exists(neededDirectory):
os.mkdir(neededDirectory)
cls.makeLogger()
@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,
"pref_undoLimit": 10,
}
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)
try:
val = int(val)
except ValueError:
if val == 'true':
val = True
elif val == 'false':
val = False
cls.settings.setValue(key, val)
@staticmethod
def makeLogger():
# send critical log messages to stdout
logStream = logging.StreamHandler()
logStream.setLevel(STDOUT_LOGLVL)
streamFormatter = logging.Formatter(
'<%(name)s> %(levelname)s: %(message)s'
)
logStream.setFormatter(streamFormatter)
log = logging.getLogger('AVP')
log.addHandler(logStream)
if FILE_LOGLVL is not None:
# write log files as well!
Core.logEnabled = True
logFilename = os.path.join(Core.logDir, 'avp_debug.log')
libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
# delete old logs
for log_ in (logFilename, libLogFilename):
if os.path.exists(log_):
os.remove(log_)
logFile = logging.FileHandler(logFilename)
logFile.setLevel(FILE_LOGLVL)
libLogFile = logging.FileHandler(libLogFilename)
libLogFile.setLevel(FILE_LOGLVL)
fileFormatter = logging.Formatter(
'[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
'%(message)s'
)
logFile.setFormatter(fileFormatter)
libLogFile.setFormatter(fileFormatter)
libLog = logging.getLogger()
log.addHandler(logFile)
libLog.addHandler(libLogFile)
# lowest level must be explicitly set on the root Logger
libLog.setLevel(0)
# 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"]
}
}

0
src/gui/__init__.py Normal file
View File

191
src/gui/actions.py Normal file
View File

@ -0,0 +1,191 @@
'''
QCommand classes for every undoable user action performed in the MainWindow
'''
from PyQt5.QtWidgets import QUndoCommand
import os
from copy import copy
from core import Core
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# COMPONENT ACTIONS
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
class AddComponent(QUndoCommand):
def __init__(self, parent, compI, moduleI):
super().__init__(
"create new %s component" %
parent.core.modules[moduleI].Component.name
)
self.parent = parent
self.moduleI = moduleI
self.compI = compI
self.comp = None
def redo(self):
if self.comp is None:
self.parent.core.insertComponent(
self.compI, self.moduleI, self.parent)
else:
# inserting previously-created component
self.parent.core.insertComponent(
self.compI, self.comp, self.parent)
def undo(self):
self.comp = self.parent.core.selectedComponents[self.compI]
self.parent._removeComponent(self.compI)
class RemoveComponent(QUndoCommand):
def __init__(self, parent, selectedRows):
super().__init__('remove component')
self.parent = parent
componentList = self.parent.window.listWidget_componentList
self.selectedRows = [
componentList.row(selected) for selected in selectedRows
]
self.components = [
parent.core.selectedComponents[i] for i in self.selectedRows
]
def redo(self):
self.parent._removeComponent(self.selectedRows[0])
def undo(self):
componentList = self.parent.window.listWidget_componentList
for index, comp in zip(self.selectedRows, self.components):
self.parent.core.insertComponent(
index, comp, self.parent
)
self.parent.drawPreview()
class MoveComponent(QUndoCommand):
def __init__(self, parent, row, newRow, tag):
super().__init__("move component %s" % tag)
self.parent = parent
self.row = row
self.newRow = newRow
self.id_ = ord(tag[0])
def id(self):
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
return self.id_
def mergeWith(self, other):
self.newRow = other.newRow
return True
def do(self, rowa, rowb):
componentList = self.parent.window.listWidget_componentList
page = self.parent.pages.pop(rowa)
self.parent.pages.insert(rowb, page)
item = componentList.takeItem(rowa)
componentList.insertItem(rowb, item)
stackedWidget = self.parent.window.stackedWidget
widget = stackedWidget.removeWidget(page)
stackedWidget.insertWidget(rowb, page)
componentList.setCurrentRow(rowb)
stackedWidget.setCurrentIndex(rowb)
self.parent.core.moveComponent(rowa, rowb)
self.parent.drawPreview(True)
def redo(self):
self.do(self.row, self.newRow)
def undo(self):
self.do(self.newRow, self.row)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# PRESET ACTIONS
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
class ClearPreset(QUndoCommand):
def __init__(self, parent, compI):
super().__init__("clear preset")
self.parent = parent
self.compI = compI
self.component = self.parent.core.selectedComponents[compI]
self.store = self.component.savePreset()
self.store['preset'] = self.component.currentPreset
def redo(self):
self.parent.core.clearPreset(self.compI)
self.parent.updateComponentTitle(self.compI, False)
def undo(self):
self.parent.core.selectedComponents[self.compI].loadPreset(self.store)
self.parent.updateComponentTitle(self.compI, self.store)
class OpenPreset(QUndoCommand):
def __init__(self, parent, presetName, compI):
super().__init__("open %s preset" % presetName)
self.parent = parent
self.presetName = presetName
self.compI = compI
comp = self.parent.core.selectedComponents[compI]
self.store = comp.savePreset()
self.store['preset'] = copy(comp.currentPreset)
def redo(self):
self.parent._openPreset(self.presetName, self.compI)
def undo(self):
self.parent.core.selectedComponents[self.compI].loadPreset(
self.store)
self.parent.parent.updateComponentTitle(self.compI, self.store)
class RenamePreset(QUndoCommand):
def __init__(self, parent, path, oldName, newName):
super().__init__('rename preset')
self.parent = parent
self.path = path
self.oldName = oldName
self.newName = newName
def redo(self):
self.parent.renamePreset(self.path, self.oldName, self.newName)
def undo(self):
self.parent.renamePreset(self.path, self.newName, self.oldName)
class DeletePreset(QUndoCommand):
def __init__(self, parent, compName, vers, presetFile):
self.parent = parent
self.preset = (compName, vers, presetFile)
self.path = os.path.join(
Core.presetDir, compName, str(vers), presetFile
)
self.store = self.parent.core.getPreset(self.path)
self.presetName = self.store['preset']
super().__init__('delete %s preset (%s)' % (self.presetName, compName))
self.loadedPresets = [
i for i, comp in enumerate(self.parent.core.selectedComponents)
if self.presetName == str(comp.currentPreset)
]
def redo(self):
os.remove(self.path)
for i in self.loadedPresets:
self.parent.core.clearPreset(i)
self.parent.parent.updateComponentTitle(i, False)
self.parent.findPresets()
self.parent.drawPresetList()
def undo(self):
self.parent.createNewPreset(*self.preset, self.store)
selectedComponents = self.parent.core.selectedComponents
for i in self.loadedPresets:
selectedComponents[i].currentPreset = self.presetName
self.parent.parent.updateComponentTitle(i)
self.parent.findPresets()
self.parent.drawPresetList()

BIN
src/gui/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

1037
src/gui/mainwindow.py Normal file

File diff suppressed because it is too large Load Diff

835
src/gui/mainwindow.ui Normal file
View File

@ -0,0 +1,835 @@
<?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>
<widget class="QPushButton" name="pushButton_undo">
<property name="text">
<string>Undo</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>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>

361
src/gui/presetmanager.py Normal file
View File

@ -0,0 +1,361 @@
'''
Preset manager object handles all interactions with presets, including
the context menu accessed from MainWindow.
'''
from PyQt5 import QtCore, QtWidgets
import string
import os
import logging
from toolkit import badName
from core import Core
from gui.actions import *
log = logging.getLogger('AVP.Gui.PresetManager')
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()
action = ClearPreset(self.parent, compI)
self.parent.undoStack.push(action)
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
index = compPos if compPos is not None else componentList.currentRow()
if index == -1:
return
action = OpenPreset(self, presetName, index)
self.parent.undoStack.push(action)
def _openPreset(self, presetName, index):
selectedComponents = self.core.selectedComponents
componentName = selectedComponents[index].name.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)
def deletePreset(self, comp, vers, name):
action = DeletePreset(self, comp, vers, name)
self.parent.undoStack.push(action)
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):
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)
if self.presetExists(newPath):
return
action = RenamePreset(self, path, oldName, newName)
self.parent.undoStack.push(action)
break
def renamePreset(self, path, oldName, newName):
oldPath = os.path.join(path, oldName)
newPath = os.path.join(path, newName)
if os.path.exists(newPath):
os.remove(newPath)
os.rename(oldPath, newPath)
self.findPresets()
self.drawPresetList()
path = os.path.dirname(newPath)
for i, comp in enumerate(self.core.selectedComponents):
if self.core.getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
self.parent.drawPreview()
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)

150
src/gui/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>

90
src/gui/preview_thread.py Normal file
View File

@ -0,0 +1,90 @@
'''
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
import logging
from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject
log = logging.getLogger("AVP.Gui.PreviewThread")
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):
try:
nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2:
try:
self.queue.get(block=False)
except Empty:
continue
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
if self.background.width != width \
or self.background.height != height:
self.background = Checkerboard(width, height)
frame = self.background.copy()
log.info('Creating new preview frame')
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
)
log.critical(errMsg)
self.error.emit(errMsg)
break
except RuntimeError as e:
log.error(str(e))
else:
self.frame = ImageQt(frame)
self.imageCreated.emit(QtGui.QImage(self.frame))
except Empty:
True

61
src/gui/preview_win.py Normal file
View File

@ -0,0 +1,61 @@
from PyQt5 import QtCore, QtGui, QtWidgets
import logging
log = logging.getLogger('AVP.Gui.PreviewWindow')
class PreviewWindow(QtWidgets.QLabel):
'''
Paints the preview QLabel in MainWindow 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)
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):
self.pixmap = QtGui.QPixmap(img)
self.repaint()
def mousePressEvent(self, event):
if self.parent.encoding:
return
i = self.parent.window.listWidget_componentList.currentRow()
if i >= 0:
component = self.parent.core.selectedComponents[i]
if not hasattr(component, 'previewClickEvent'):
return
pos = (event.x(), event.y())
size = (self.width(), self.height())
butt = event.button()
log.info('Click event for #%s: %s button %s' % (
i, pos, butt))
component.previewClickEvent(
pos, size, butt
)
@QtCore.pyqtSlot(str)
def threadError(self, msg):
self.parent.showMessage(
msg=msg,
icon='Critical',
parent=self
)
log.info('%', repr(self.parent))

60
src/main.py Normal file
View File

@ -0,0 +1,60 @@
from PyQt5 import uic, QtWidgets
import sys
import os
import logging
from __init__ import wd
log = logging.getLogger('AVP.Main')
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()
log.debug("Finished creating command object")
elif mode == 'GUI':
from gui.mainwindow import MainWindow
window = uic.loadUi(os.path.join(wd, "gui", "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)
log.debug("Finished creating main window")
window.raise_()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

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

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

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

@ -0,0 +1,193 @@
'''
Common functions
'''
from PyQt5 import QtWidgets
import string
import os
import sys
import subprocess
import logging
from copy import copy
from collections import OrderedDict
log = logging.getLogger('AVP.Toolkit.Common')
class blockSignals:
'''
Context manager to temporarily block list of QtWidgets from updating,
and guarantee restoring the previous state afterwards.
'''
def __init__(self, widgets):
if type(widgets) is dict:
self.widgets = concatDictVals(widgets)
else:
self.widgets = (
widgets if hasattr(widgets, '__iter__')
else [widgets]
)
def __enter__(self):
log.verbose(
'Blocking signals for %s',
", ".join([
str(w.__class__.__name__) for w in self.widgets
])
)
self.oldStates = [w.signalsBlocked() for w in self.widgets]
for w in self.widgets:
w.blockSignals(True)
def __exit__(self, *args):
log.verbose(
'Resetting blockSignals to %s', str(bool(sum(self.oldStates))))
for w, state in zip(self.widgets, self.oldStates):
w.blockSignals(state)
def concatDictVals(d):
'''Concatenates all values in given dict into one list.'''
key, value = d.popitem()
d[key] = value
final = copy(value)
if type(final) is not list:
final = [final]
final.extend([val for val in d.values()])
else:
value.extend([item for val in d.values() for item in val])
return final
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:
log.warning('Failed to connect %s ', str(widget.__class__.__name__))
return False
return True
def setWidgetValue(widget, val):
'''Generic setValue method for use with any typical QtWidget'''
log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val))
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:
log.warning('Failed to set %s ', str(widget.__class__.__name__))
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()

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

@ -0,0 +1,481 @@
'''
Tools for using ffmpeg
'''
import numpy
import sys
import os
import subprocess
import threading
import signal
from queue import PriorityQueue
import logging
import core
from toolkit.common import checkOutput, pipeWrapper
log = logging.getLogger('AVP.Toolkit.Ffmpeg')
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):
from component import ComponentError
if core.Core.logEnabled:
logFilename = os.path.join(
core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
)
log.debug('Creating ffmpeg process (log at %s)', logFilename)
with open(logFilename, 'w') as logf:
logf.write(" ".join(self.command) + '\n\n')
with open(logFilename, 'a') as logf:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=logf, bufsize=10**8
)
else:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, 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, FileNotFoundError):
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:
log.error('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:
log.error('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(
style='white', extra='apulsator=offset_l=0.35:offset_r=0.67'):
'''Help generate an example sound for use in creating a preview'''
if style == 'white':
src = '-2+random(0)'
elif style == 'freq':
src = 'sin(1000*t*PI*t)'
elif style == 'wave':
src = 'sin(random(0)*2*PI*t)*tan(random(0)*2*PI*t)'
elif style == 'stereo':
src = '0.1*sin(2*PI*(360-2.5/2)*t) : 0.1*sin(2*PI*(360+2.5/2)*t)'
return "aevalsrc='%s', %s%s" % (src, extra, ', ' if extra else '')

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

@ -0,0 +1,103 @@
'''
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 logging
import core
log = logging.getLogger('AVP.Toolkit.Frame')
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, penStyle):
if type(penStyle) is tuple:
super().setPen(PaintColor(*penStyle))
else:
super().setPen(penStyle)
def finalize(self):
log.verbose("Finalizing FramePainter")
imBytes = self.image.bits().asstring(self.image.byteCount())
frame = Image.frombytes(
'RGBA', (self.image.width(), self.image.height()), imBytes
)
self.end()
return frame
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.
'''
log.debug('Creating new %s*%s checkerboard' % (width, height))
image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open(
os.path.join(core.Core.wd, 'gui', "background.png")),
(0, 0)
)
image = image.resize((width, height))
return image

396
src/video_thread.py Normal file
View File

@ -0,0 +1,396 @@
'''
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
import logging
from component import ComponentError
from toolkit.frame import Checkerboard
from toolkit.ffmpeg import (
openPipe, readAudioFile,
getAudioDuration, createFfmpegCommand
)
log = logging.getLogger("AVP.VideoThread")
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.hertz = 44100
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
'''
def err():
self.closePipe()
self.cancelExport()
self.error = True
msg = 'A render node failed critically.'
log.critical(msg)
comp._error.emit(msg, str(e))
while not self.stopped:
audioI = self.compositeQueue.get()
bgI = int(audioI / self.sampleSize)
frame = None
for layerNo, comp in enumerate(reversed((self.components))):
try:
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)
)
except Exception as e:
err()
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
'''
log.debug('Dispatching Frames for Compositing...')
for audioI in range(0, self.audioArrayLen, 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
self.audioArrayLen = len(self.completeAudioArray)
else:
duration = getAudioDuration(self.inputFile)
self.completeAudioArray = []
self.audioArrayLen = int(
((duration * self.hertz) +
self.hertz) - self.sampleSize)
self.progressBarUpdate.emit(0)
self.progressBarSetText.emit("Starting components...")
canceledByComponent = False
initText = ", ".join([
"%s) %s" % (num, str(component))
for num, component in enumerate(reversed(self.components))
])
print('Loaded Components:', initText)
log.info('Calling preFrameRender for %s', initText)
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
try:
comp.preFrameRender(
audioFile=self.inputFile,
completeAudioArray=self.completeAudioArray,
audioArrayLen=self.audioArrayLen,
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]
)
)
log.error(errMsg)
comp._error.emit(errMsg, compError[1])
break
if 'static' in compProps:
self.staticComponents[compNo] = \
comp.frameRender(0).copy()
if self.canceled:
if canceledByComponent:
log.error(
'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
)
cmd = " ".join(ffmpegCommand)
print('###### FFMPEG COMMAND ######\n%s' % cmd)
print('############################')
log.info('Opening pipe to ffmpeg')
log.info(cmd)
try:
self.out_pipe = openPipe(
ffmpegCommand,
stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
except sp.CalledProcessError:
log.critical('Ffmpeg pipe couldn\'t be created!')
raise
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# 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, self.audioArrayLen, 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 / self.audioArrayLen) * 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')
self.closePipe()
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 closePipe(self):
try:
self.out_pipe.stdin.close()
except BrokenPipeError:
log.error('Broken pipe to ffmpeg!')
if self.out_pipe.stderr is not None:
log.error(self.out_pipe.stderr.read())
self.out_pipe.stderr.close()
self.error = True
self.out_pipe.wait()
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()