Juce Python Bindings

Anyone interested in juce bindings with minimal effort ?

from juce_bindings import juce, START_JUCE_APPLICATION


class MainComponent(juce.Component):
    def paint(self, g):
        g.fillAll(juce.Colours.black)

        random = juce.Random.getSystemRandom()
        rect = juce.Rectangle[int](0, 0, 20, 20)
 
        for _ in range(100):
            g.setColour(juce.Colour(
                random.nextInt(256),
                random.nextInt(256),
                random.nextInt(256)))
 
            rect.setCentre(random.nextInt(self.getWidth()), random.nextInt(self.getHeight()))
            g.drawRect(rect)


class MainWindow(juce.DocumentWindow):
    def __init__(self):
        super().__init__(
            juce.JUCEApplication.getInstance().getApplicationName(),
            juce.Colours.red, 
            juce.DocumentWindow.allButtons,
            True)

        self.component = MainComponent()

        self.setResizable(True, True)
        self.setContentNonOwned(self.component, True)
        self.centreWithSize(400, 300)
        self.setVisible(True)

    def __del__(self):
        del self.component

    def closeButtonPressed(self):
        juce.JUCEApplication.getInstance().systemRequestedQuit()


class Application(juce.JUCEApplication):
    def getApplicationName(self):
        return "Super JUCE-o-matic"

    def getApplicationVersion(self):
        return "1.0"

    def initialise(self, commandLine):
        self.window = MainWindow()

    def shutdown(self):
        if hasattr(self, "window") and self.window:
            self.window.setVisible(False)
            del self.window


if __name__ == "__main__":
    START_JUCE_APPLICATION(Application)

6 Likes

This looks interesting, please reveal more!

i will for sure, once i have everything in place. the actual bindings code is just 20 lines of code, using the current juce headers. i will look into creating actual non interactive bindings that can be deployed as wheels.

1 Like

I’ve opened up the repo for the juce python bindings. It’s in early alpha stage but it’s possible already do a lot of cool things. I will improve the packaging for all the supported platforms and publish the wheels as soon as i can. For now, you will need to build it yourself, so you can enjoy it here:

In the near future, it will be as easy as doing:

pip install popsicle

cheers !

9 Likes

Very interesting!

Also I edited the post title, hope that’s OK.

Initial support for osx wheels is up in pypi, you can install it easily with pip now (linux is coming soon).

Check out the examples to see what is possible… How similar to C++ is the flexgrid example is stunning popsicle/layout_flexgrid.py at master · kunitoki/popsicle · GitHub !

1 Like

This is awesome! I was thinking about trying something similar using SWIG recently. I hadn’t heard of cppyy, it looks really nice.

It looks like pypi has disabled or limited some of their API functionality, so I wasn’t able to search for or install the package using pip. I ended up building from source, and noticed a couple of minor things while following the README:

  • pip3 install cppyy>=1.9.1 should be quoted like pip3 install "cppyy>=1.9.1" to avoid being misinterpreted by some shells (I think these must be double quotes to work on Windows).
  • cmake -G "Ninja Multi-Config" should be cmake -G "Ninja Multi-Config" .. since we previously pushd'd into the build directory.

It might also be helpful to mention that JUCE is a submodule that needs to be cloned with git clone --recurse-submodules or git submodule update --init.

Ultimately it was pretty quick and easy to get things up and running, great work!

Which api should be limited? I have no issue installing cppyy via pip.

Thanks for the feedback, will apply those changes asap.

More pythonizations might be welcome, especially when juce take ownership of a raw pointer, as it will be double deleted both by juce and python (there is a workaround for that called python_owns in cppyy that needs to be applied). In case you get crashes on exit, this is likely to be the culprit.

Also i’m working on the threading part, to allow seamless usage with juce thread and friends.

Apparently the pypi search API is currently disabled. I ran into the error described in this issue when searching for popsicle.

Supposedly, pip install is unaffected by this, and I was able to install cppyy successfully via pip. However, when I tried to install popsicle, it failed with the error message: “No matching distribution found for popsicle”.

I don’t know enough about pip to know if the two issues are related – there are a few people in the GitHub issues who report pip install not working for them either, but it’s possible there’s something else going on that I’m not aware of!

AFAICT there’s no workaround for the disabled search API at the moment, I just wanted to mention it here in case anyone else runs into the same issues. Building and installing the wheel locally worked perfectly.

Ah that is related to the fact that the wheel is a binary wheel not compatible with your osx, i used github actions with macosx-latest to build the wheels (i think ABI is macosx-10.15-x86_64), so i might need to downgrade the platform when building them.

Which python version do you have? Be sure you use at least 3.6

I was testing with Python 3.8.2 on MacOS 11.1 (Intel).

Yeah not sure a binary wheel made on 10 can be used on 11. I noticed github actions have big sur in preview, i will try making the wheels there

It seems you are hitting Accept macosx_10_9 platform for Big Sur · Issue #9138 · pypa/pip · GitHub can you try again bumping your pip version ?

1 Like

That did the trick! After upgrading to pip version 20.3.3, pip install popsicle works like a charm.

This is awesome!
Does pospicle also work on rpi4?

Not sure, never tried. If cppyy runs there it’s definately possible

Some sneek peek of the next version of popsicle. The project has been going through a massive overhaul and the module release is progressing !

The popsicle provided juce_python module can be used standalone (import it from python scripts, shipping as a cross platform wheel with no dependencies on external packages) or embedded in current juce apps to allow extending them with python scripts.

More informations to come.

13 Likes

This is ridiculous. Good work!

1 Like

Some other examples of the possibilities that could be reached by giving JUCE classes access to the python ecosystem: an integration of numpy, matplotlib and multiprocessing with JUCE Component, DrawableImage, Timer and ComponentAnimator.

import io
import multiprocessing
import queue

import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg

from juce_init import START_JUCE_COMPONENT
import popsicle as juce


def make_plot(fig):
	x = np.linspace(0, 10, 11)
	y = [3.9, 4.4, 10.8, 10.3, 11.2, 13.1, 14.1,  9.9, 13.9, 15.1, 12.5]

	a, b = np.polyfit(x, y, deg=1)
	y_est = a * x + b
	y_err = x.std() * np.sqrt(1/len(x) + (x - x.mean())**2 / np.sum((x - x.mean())**2))

	ax = fig.add_subplot(111)
	ax.plot(x, y_est, '-')
	ax.fill_between(x, y_est - y_err, y_est + y_err, alpha=0.2)
	ax.plot(x, y, 'o', color='tab:brown')


def generate_plot_png(q, width, height):
	fig = Figure(figsize=(width / 100, height / 100), dpi=100)
	canvas = FigureCanvasAgg(fig)

	make_plot(fig)
	canvas.draw()

	with io.BytesIO() as img_buf:
		canvas.print_png(img_buf)
		q.put(img_buf.getbuffer().tobytes())


class MainContentComponent(juce.Component, juce.Timer):
	img = None
	q = multiprocessing.Queue()
	process = None
	width = 600
	height = 400
	time = 0.0

	def __init__(self):
		juce.Component.__init__(self)
		juce.Timer.__init__(self)

		self.drawableImage = juce.DrawableImage()
		self.drawableImage.setOpaque(True)
		self.addChildComponent(self.drawableImage)

		self.setSize(int(self.width), int(self.height))
		self.setOpaque(True)

		self.process = multiprocessing.Process(target=generate_plot_png, args=[self.q, self.width, self.height])
		self.process.start()

		self.startTimerHz(24)

	def timerCallback(self):
		if not self.drawableImage.getImage().isValid():
			try:
				image_data = self.q.get_nowait()

				self.drawableImage.setImage(self.createImageFromBuffer(image_data))

				juce.Desktop.getInstance().getAnimator().fadeIn(self.drawableImage, 1000)

			except queue.Empty:
				pass

		else:
			if not juce.Desktop.getInstance().getAnimator().isAnimating(self.drawableImage):
				self.stopTimer()

		self.time += juce.degreesToRadians(6.0)
		self.repaint()

	def createImageFromBuffer(self, image_data) -> juce.Image:
		return juce.ImageCache.getFromMemory(image_data)

	def paint(self, g: juce.Graphics):
		g.fillAll(juce.Colours.white)

		if not self.drawableImage.isVisible() or juce.Desktop.getInstance().getAnimator().isAnimating(self.drawableImage):
			b = self.getLocalBounds()
			center = juce.Point[float](b.getCentreX(), b.getCentreY())

			p = juce.Path()
			p.addStar(center, 5, 25, 60, self.time)

			g.setColour(juce.Colours.blueviolet)
			g.fillPath(p)

	def resized(self):
		self.drawableImage.setBounds(self.getLocalBounds())
		self.drawableImage.setBoundingBox(self.getLocalBounds().toFloat())


if __name__ == "__main__":
	START_JUCE_COMPONENT(MainContentComponent, name="Matplotlib Example")

Untitled

4 Likes

Synchronous image processing using OpenCV into a juce::Image, slider changes are retriggering the image manipulation and the resulting image is converted into juce Image pixels and repainted:

opencv_integration

It’s running snappier and smoother than what i expected (the gif compression doesn’t enough credit to it).