PySimpleGUI / PySimpleGUI-Widgets

Rainmeter-like Widgets for your desktop using the easy to use PySimpleGUI package

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rainmeter-like "Shape" drawing function?

Kodikuu opened this issue · comments

I took a good look searching through for "Graph", "Vector", and "Shape", I'm looking for something that would let me build something like my Rainmeter visualiser;
image

I've long been looking for a properly useable Python plugin for Rainmeter. This looks like something I could use the other way around (almost a Rainmeter implementation in Python). Realistically, the only thing i'm missing here is Shapes.

There's a draw lines and polygon methods that may get you what you close.

The Graph Element is what you need to look at in the call reference.

Looks like draw_lines (DrawLines) didn't make it into the call reference. I'll add it. Here's the docstring.

def DrawLines(self, points, color='black', width=1):
    """
    Draw a series of lines given list of points

    :param points: list of points that define the polygon
    :type points: List[Union[Tuple[int, int], Tuple[float, float]]]
    :param color: Color of the line
    :type color: (str)
    :param width: width of line in pixels
    :type width: (int)
    :return: id returned from tktiner or None if user closed the window. id is used when you
    :rtype: Union[int, None]
    """

And you can always use matplotlib drawing. Perhaps strip it of the axis, etc, so that it doesn't look like a matplotlib drawing.

Is there functionality to "fill" the resultant polygon?

Nevermind, found it. Made a search for Polygon. Thank you.

Be sure and look at the call reference when looking for object, functions, member functions, etc. It's easily searchable with Control+F as it sounds like maybe you found.

This tab in the main docs:
http://Calls.PySimpleGUI.org

has all of the available calls and a description of each parameter for the calls.

Definitely interested in what you end up making!

It is super easy to make a basic "desk widget" with PySimpleGUI as it only requires a couple of parameters to turn off the titlebar and to turn on grab_anywhere.

Come back and post a screenshot or link!

Will do. I'm mainly just planning on using this like Rainmeter once I've gotten to grips with it. The restrictions I've had to work around when interfacing with Python in the past have been frustrating and inefficient.

I run each of my "widgets" as individual programs, although with a newly released multi-window interface, it's possible to have a single program that will control all of the widgets at one time.

I recommend running each widget as a separate Python program. This will enable Windows/Linux to utilize all of your processor's cores and the overall overhead per program is small given today's massive computers. The ones I'm currently running take about 20 MB per program. While "wasteful" to have a 20 line program have a 20 MB overhead, when there are many Gigabytes of free RAM to use it's not a problem.

These are the ones I keep running all the time and they don't put much stress on my system. I have no idea how it would compare to Rainmeter as I've not tried to do a side by side compare of CPU / RAM use for equivalent widgets.

image

Indeed, that was what I was planning on. Not a direct Rainmeter implementation so much as a replacement.
I have a 3950X, so I certainly have cores to spare.

Only made a little text widget so far to test, i'm enjoying the font sizing being accurate. Rainmeter's font sizing isn't, and it's such a pain

image

gif
Time-based animation at 165Hz via draw_polygon, this is exactly what i'm looking for

Wooowwwwwwwww

Can you post your test code?

Absolutely. Here's unrestrained rendering, and more points, with an FPS output;
text_widget.zip

EDIT: The only thing I don't like is the lack of anti-aliasing

WOW that's smooth!

I expected lots of artifacts but it's not that bad. Really fancy interface you whipped up. I'm genuinely impressed.

I don't see why there'd be artefacts. This is pretty basic really anyhow. Next step for me, if i'm going with my visualiser first, is to build or find something to run an FFT over the system audio. I can scale the y-coord of each polygon point by each FFT bin and... voila, visualiser

If you add a right click menu, then you can easily exit:

window = sg.Window('This is not Rainmeter', layout, no_titlebar=True, grab_anywhere=True, margins=(0, 0), element_padding=(0, 0), alpha_channel=ALPHA, finalize=True, right_click_menu=[[''], 'Exit'])

Nice, thanks!

Here's what i'm aiming for, for reference, the rainmeter vis I built a while back; https://www.youtube.com/watch?v=rt2iIW41Rag

That should be easy to achieve in terms of the window itself. Set a transparent background color and then you can make the window as large as you want. It's how I was able to make these balls bounce all over the screen. The window is the size of the entire screen with the "transparent color" set to the theme's background color.

Desktop Bouncing Balls

Well, I've run through a few iterations now;
V1 Last night; https://youtu.be/96Nl7eKnTyo
V2 was a write-off
V3 this morning; https://youtu.be/CIppGgpjcaY
V4 this afternoon; https://youtu.be/2Aj_mds0Xkg

Wow that was QUICK!

V4 looks like you're getting super close to what you showed earlier.

Seeing how you managed all this in about 24 hours that's super-impressive! Very nice. Looking forward to seeing the code someday.

Now that I basically have it down, it's time to separate the audio stream from the render loop so that neither holds up the other. Once i've done that, which will basically be V5, i'll drop a zip here. Feel free to do whatever with it

There's a new multi-threading function that could help decouple things. I dunno.

The call is write_event_value and it allows you to stuff your own events into an event loop. The idea is that a thread can do stuff and when it has something that's ready to output to the window, you call write_event_value and it'll show up in your calls to window.read()

Depending on how your write your thread and event loop you may be able to make it really efficient by running your event loop with no timeout values set. If the loop runs fast enough to keep up with the data, then you won't need to do any polling for data by using a timeout. You can do a simple window.read() and have it pend waiting for data.

V2 already worked with a callback function on the stream reader, I scrapped it because I did the FFT differently and the output was garbage. Basically just rewriting V4 in V2's layout

And there we go, the best i'll get. There's some floating point arithmetic differences between Python and the C++ library Rainmeter uses that I can't overcome, so it'll always be slightly different.

Grey is V5 w/ render and processing separated, white is V4 which is fully synchronous, bottom is Vector as usual; https://youtu.be/8-Am8jnSVaA

PyVis.zip
Some of the values here are specific to my system and capabilities, but here you go

That was QUICK of you! I'll give it a try. It didn't work the first time but as you said, I'll need to do some tuning.

image
Remove, add devname="Name of your RECORDING device here"

If you want to visualise output, you will need something like vb-cable or some other form of loopback device so that you can capture from it

Visualiser.zip
Been busy with work, but here's a rebuilt version using SoundCard instead of PyAudio.
It will fetch your default output device in loopback mode by default, no need to muck about with that bit.
Width and height are defined at the top of main.pyw

Does this require 3.8?

I installed the soundcard package and then ran into trouble with the asyncio.run statement.

Should only require 3.7. Throw me the error if need be. Though i'm on 3.8

I'm installing soundcard for 3.8. I run 3.6 normally.

That's odd you're on 3.8 and it's working. When I installed soundcard it pulled in numpy, etc. But I get this error now. Wonder if I need to restart my system or something following the install.

X:\Python\python3.8>python "E:\DownloadsE\Visualiser\main.pyw"
Traceback (most recent call last):
File "X:\Python\python3.8\lib\site-packages\numpy\core_init_.py", line 22, in
from . import multiarray
File "X:\Python\python3.8\lib\site-packages\numpy\core\multiarray.py", line 12, in
from . import overrides
File "X:\Python\python3.8\lib\site-packages\numpy\core\overrides.py", line 7, in
from numpy.core._multiarray_umath import (
ImportError: DLL load failed while importing _multiarray_umath: The specified module could not be found.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "E:\DownloadsE\Visualiser\main.pyw", line 2, in
import audio_analyser
File "E:\DownloadsE\Visualiser\audio_analyser.py", line 1, in
from audio_device import AudioDevice
File "E:\DownloadsE\Visualiser\audio_device.py", line 1, in
import soundcard as sc
File "X:\Python\python3.8\lib\site-packages\soundcard_init_.py", line 13, in
from soundcard.mediafoundation import *
File "X:\Python\python3.8\lib\site-packages\soundcard\mediafoundation.py", line 5, in
import numpy
File "X:\Python\python3.8\lib\site-packages\numpy_init_.py", line 140, in
from . import core
File "X:\Python\python3.8\lib\site-packages\numpy\core_init_.py", line 48, in
raise ImportError(msg)
ImportError:

IMPORTANT: PLEASE READ THIS FOR ADVICE ON HOW TO SOLVE THIS ISSUE!

Importing the numpy C-extensions failed. This error can happen for
many reasons, often due to issues with your setup or how NumPy was
installed.

We have compiled some common reasons and troubleshooting tips at:

https://numpy.org/devdocs/user/troubleshooting-importerror.html

Please note and check the following:

  • The Python version is: Python3.8 from "X:\Python\python3.8\python.exe"
  • The NumPy version is: "1.19.1"

and make sure that they are the versions you expect.
Please carefully study the documentation linked above for further help.

Original error was: DLL load failed while importing _multiarray_umath: The specified module could not be found.

You made sure to install/update numpy for 3.8?

I tested 3.6 on my Linux laptop, yeah, doesn't work on 3.6. asyncio.run was added in 3.7

X:\Python\python3.8>python -m pip install numpy
Requirement already satisfied: numpy in x:\python\python3.8\lib\site-packages (1.19.1)

Yea, it installed numpy when I installed soundcard.

I'm setup for 3.6 by default so not sure if maybe the Pythonpath has it confused. I'm able to run other 3.8 stuff though.

Not sure what to suggest. It's clearly numpy that's failing though

It may well be your path/pythonpath causing problems. Try using a venv

Yea, I'll give it a go later with a virtual setup.

I created a virtual env in PyCharm for 3.8 and upgraded to 3.8.5. I still get the same numpy problem.

I'll try downgrading the numpy release and see if that helps. Very weird that you're able to run 3.8.

exit - downgrading numpy to 1.18.5 didn't do anything to help the problem


Please note and check the following:

  * The Python version is: Python3.8 from "C:\Users\mike\venv\Virtual3.8\Scripts\python.exe"
  * The NumPy version is: "1.18.5"

and make sure that they are the versions you expect.
Please carefully study the documentation linked above for further help.

Original error was: DLL load failed while importing _multiarray_umath: The specified module could not be found.

I gave this to a friend and they were able to run it without issue.

A little googling suggest that you need to add 3.8's library path to your PATH

Can you send me that link?

I've tried adding both the lib, and libs of 3.8 to my path and I continue to get the same error.

Also, why would I need to modify that path if I'm running venv within pycharm?

I'm able to run PySimpleGUI using 3.8 with no problems, both from a dos window and the new venv I made in PyCharm. It just seems like numpy is struggling. It's clearly finding the correct numpy or else it wouldn't crash the way it is.

I can't risk completely wiping my system and reworking all of my installs. It would be great to get this working of course, but it's just such a struggle that shouldn't be.

Progress....

I blew away my 3.7 install that was having trouble with tkinter anyway and rebuilt it.

I am able to run your demo now!

But, the location is off. I noticed that the window position is not set to (0,0) like I would expect it to be. Otherwise the window will attempt to be centered when created. When I did this it put the graph at the top instead of the bottom so I'll have to dig in a little more.

I used a similar technique of taking the entire screen in a bouncing balls screensaver demo and set the window to (0,0) for that demo. I haven't looked at your logic yet, but at least it's running an making the graph!

Thanks for all the help.

Here's my modified version.

I set the size of the graph to be the same as the screen size and moved the window to (0,0). This essentially makes your entire window one huge graph with 0,0 in the bottom left corner. It's odd you were able to get it to work without setting any position of the window because the window position is computed based on the size of the screen and the size of the window. Windows, by default, are centered on the screen (dead center).

import PySimpleGUI as sg
import audio_analyser
import asyncio

# Prep GUI
SCREENSIZE = sg.Window.get_screen_size()
GRID_SIZE = SCREENSIZE  # I'm the size of the visualiser
# GRID_SIZE = (sg.Window.get_screen_size()[0], 150)
print(GRID_SIZE)
UPDATE_FREQUENCY_MILLISECONDS = 0
THEME = 'Dark purple 6 '
ALPHA = 1
sg.theme(THEME)
graph = sg.Graph(GRID_SIZE,  (0, 0), GRID_SIZE,key='-GRAPH-', enable_events=True, background_color="#FFFFFF")
layout = [[graph]]
window = sg.Window('This is not Rainmeter', layout, location=(0,0), no_titlebar=True, grab_anywhere=True, margins=(0, 0), element_padding=(0, 0), alpha_channel=ALPHA, finalize=True, right_click_menu=[[''], 'Exit'])
window.set_transparent_color("#FFFFFF")


async def run():
    loop = asyncio.get_running_loop()
    asyncio.set_event_loop(loop)
    await asyncio.gather(a.device.frame_loop(), a.work_loop(), work(), loop=loop)


async def work():
    while True:
        await a.ready.wait()
        bins = a.bins
        a.ready.clear()
        # GUI step
        step = GRID_SIZE[0]/(len(bins)-1)
        wavepoints = [(0, 0)] + [(step*val, 150*bins[val]) for val in range(len(bins))] + [(GRID_SIZE[0], 0)]

        wave = graph.draw_polygon(wavepoints, fill_color="#312F2F")

        event, values = window.read(timeout=UPDATE_FREQUENCY_MILLISECONDS)
        if event == sg.WIN_CLOSED or event == 'Exit':
            exit()
        if event == '-GRAPH-':  # exit if clicked in the bottom left 20 x 20 pixel area
            if values['-GRAPH-'][0] < 20 and values['-GRAPH-'][1] < 20:
                break
        # erase figures so they can be redrawn
        graph.delete_figure(wave)

if __name__ == "__main__":
    a = audio_analyser.AudioAnalyser()
    asyncio.run(run())

Yeah, it's centered for me too. Getting the output right was my focus more than the positioning

Thanks for the tweaked version!

Next up, when I get around to it, will be making this easier to configure

I recommend setting your refresh to something like 10 ms. 0 is usually a bad idea as it can consume the core entirely and starve other processes as a result. It ran smoothly for me at 10. I've not tried other values.

That'd be true if there wasn't a wait in there. The render loop waits on the processing loop, which waits on the capture loop. The capture loop will block until it has new samples. It should all only iterate as quickly as it fetches new samples. By default, that'd be roughly every 10ms or so as it is, making the extra 10ms wait unnecessary

Each block of samples on a 44100Hz output device will be 441 samples by default, 10ms

Been a little while, busy with other things. I have just one last little thing that's bugging me now;
The lack of anti-aliasing.

I know this is a tricky/impossible thing with tk, so I was wondering what the chances of getting graph draw functions implemented on QT are, along with making a transparent window on QT? (And maybe also making use of the alpha channel?)

Didn't want you to think I'm ignoring you.... I posted over the in the main PySimpleGUI repository "Announcements" issue that I'm taking a break for the Issues for a week. I'll be back on them next week.

No problem there, I'm in no rush.

I rebuilt my visualiser entirely, adopting a functional approach over object-oriented. It's far easier to modify now.

I'm butting heads with tkinter now, it alone is taking ~9ms of my 10ms update budget. I built a JIT'd version that uses numba.njit() to speed up the audio processing to alleviate it somewhat, but it's not enough.

The result of that is most visible when playing a pure tone; The entire visualiser periodically "flashes", the whole thing moving up instead of just a spike in one section.

Sometimes though, it doesn't take so long, and the whole thing plays smoothly.

PyVector.zip

Oh wow.... That's a rare thing to hear coming from someone these days.... functional approach being easier than OO.

I'm sorry that I'm so far behind in everything right now. You're not forgotten!

It's easier in that I can make a two-line change and pipe the output of any step to the rendering step, which lets me literally see what's going on. Since I know what each step should look like, I can use that to quickly debug issues.

This approach has also let me decouple the capture+FFT from the filtering and binning, meaning I can now render any number of differently-configured visualisers off of a single capture+FFT loop.

Don't worry about not answering my stuff, it's really not a problem at all. If you keep developing as you are, I'll eventually have most of what I want anyway.

Wow @Kodikuu ! Thank you so very much! Really meant a lot getting your support. Really awesome of you.

'Tis the least I could do. Keep up the good work!

Alas, I've broadened my skillset and skipped town to PySide2, which has the anti-aliasing and GPU acceleration I was after, as well as more flexibility with the fill

image

Nice. Should be a pretty easy thing for you. I appreciate the help you've provided. You helped push stuff forward. I'm not answering GitHub issues for the past month as I'm focusing 100% on making a new course, but wasn't going to not reply here :-)

Post a link to your repo. There's something in there I could probably learn from.

I don't tend to work with github at all, unless it's for work, but i'll re-build it in a repo piece by piece later when I have the time.

A nice bonus of PySide2 (or PySide6, which turned out to be a drop-in replacement) is that as it has a functional alpha channel, there's no colour key needed for transparency, and it almost works fine on Linux without modification. The only issue being the responsiveness of the audio capture needing some work.

This is hideously inefficient, because I decided to re-do it with QThreads, but it works; https://github.com/Kodikuu/PyVector-Pyside6