Definately, and you can even reach great performance as well by using tricks (like integrating with Cython or nimpy), check this out:
nim_audio_example.py
import nimporter
import nim_audio # Here we import the compiled nim
import numpy as np
from juce_init import START_JUCE_COMPONENT
import popsicle as juce
class AudioCallback(juce.AudioIODeviceCallback):
gain = 1.0
time = 0.0
device = None
def audioDeviceAboutToStart(self, device: juce.AudioIODevice):
print("starting", device, "at", device.getCurrentSampleRate())
self.device = device
def audioDeviceIOCallbackWithContext(self, inputs, numInputChannels, outputs, numOutputChannels, numSamples, context):
time = self.time
for output in outputs:
nout = np.array(output, copy=False)
time = nim_audio.process_output(nout.data, numSamples, self.gain, self.time)
self.time = time
def audioDeviceError(self, errorMessage: str):
print("error", errorMessage)
def audioDeviceStopped(self):
print("stopping")
class MainContentComponent(juce.Component):
manager = juce.AudioDeviceManager()
audio_callback = AudioCallback()
def __init__(self):
juce.Component.__init__(self)
self.manager.addAudioCallback(self.audio_callback)
result = self.manager.initialiseWithDefaultDevices(0, 2)
if result:
print(result)
self.button = juce.TextButton("Silence!")
self.addAndMakeVisible(self.button)
self.button.onStateChange = lambda: self.onButtonStateChange()
self.setSize(600, 400)
self.setOpaque(True)
def visibilityChanged(self):
if not self.isVisible() and self.manager:
self.manager.removeAudioCallback(self.audio_callback)
self.manager.closeAudioDevice()
def onButtonStateChange(self):
if self.button.getState() == juce.Button.ButtonState.buttonDown:
self.audio_callback.gain = 0.25
else:
self.audio_callback.gain = 1.0
def paint(self, g: juce.Graphics):
g.fillAll(juce.Colours.black)
def resized(self):
bounds = self.getLocalBounds()
self.button.setBounds(bounds.reduced(100))
if __name__ == "__main__":
START_JUCE_COMPONENT(MainContentComponent, name="Audio Device Example")
And this is the DSP part, which is written in Nim and compiled to a binary python dependency on import from python side, allowing to reach great performance (and the ability to be hot swapped):
nim_audio.nim
import nimpy
import nimpy/raw_buffers
import std/[math, random]
proc `+`[T](a: ptr T, b: int): ptr T =
cast[ptr T](cast[uint](a) + cast[uint](b * a[].sizeof))
proc process_output(a: PyObject, numSamples: int, gain: float, t: float): float {.exportpy.} =
var buffer: RawPyBuffer
a.getBuffer(buffer, PyBUF_WRITABLE or PyBUF_ND)
var p = cast[ptr float32](buffer.buf)
var time = t
for i in 0 ..< numSamples:
p[] = (time.degToRad().sin() + (rand(2.0) - 1.0) * 0.125) * 0.5 * gain
p = p + 1
time += 2.0
buffer.release()
return time
In this example i went further and allowed the inner loops to be compiled as well (in nim for this example, mainly because of the great nimpy (GitHub - yglukhov/nimpy: Nim - Python bridge) and nimporter (GitHub - Pebaz/nimporter: Compile Nim Extensions for Python On Import!) facilities but pretty much can be any other compiled language with python integration).
As you can see i made JUCE input output channel buffers (the ones feeding the audioDeviceIOCallbackWithContext) compatible with the buffer protocol of numpy so they can be fed into no-copy numpy arrays and manipulated efficiently (nout = np.array(output, copy=False)). Iβve done the same for the juce::AudioBuffer<> class, so interoperability is awesome.
As an example of how do number crunching and audio gen efficiently in numpy:
def audioDeviceIOCallbackWithContext(self, inputs, numInputChannels, outputs, numOutputChannels, numSamples, context):
start = self.time
end = start + 2.0 * numSamples
self.buffer[:] = (
((np.random.random(numSamples) * 2.0 - 1.0) * 0.025) + np.sin(np.deg2rad(np.linspace(start, end, numSamples)))
) * self.gain
self.time = end % 360.0
for output in outputs:
nout = np.array(output, copy=False)
nout[:] = self.buffer
This is fast, not as fast as what you can reach with C++ only, but usable definately and with uncomparable flexibility. And could be even optimised more (like compiling the inner loops as i shown before).
I have several other plans for better coexistence and performance.
This is just the beginning.