spatialaudio / jackclient-python

🂻 JACK Audio Connection Kit (JACK) Client for Python :snake:
https://jackclient-python.readthedocs.io/
MIT License
131 stars 26 forks source link

plot get_array() data in real-time #85

Closed oskude closed 4 years ago

oskude commented 4 years ago

sorry if this is out of scope :smirk:, but i'm a python/audio-science noob and am just so happy how easy we can create a jack client with this library, so blamethank you! :sweat_smile:

so for funsies, i wanted to "plot" the data coming from get_array(), in real-time... my first omg-it-does-something experiment:

screenshot

import jack
import numpy
import threading
import pygame

client  = jack.Client("foovju")
event   = threading.Event()
input_1 = client.inports.register("input_1")

black   = 0, 0, 0
white   = 255, 255, 255
size    = width, height = client.blocksize, int(client.blocksize / 2)
screen  = pygame.display.set_mode(size)

pygame.display.init()

@client.set_process_callback
def process(frames):
    assert frames == client.blocksize
    data = input_1.get_array()
    screen.fill(black)
    x = 0; h = height / 2
    for d in data:
        y = int(h * (d + 1))
        pygame.draw.rect(screen, white, [x, y, 1, 1])
        x += 1
    pygame.display.flip()

with client:
    print('Press Ctrl+C to stop')
    try:
        event.wait()
    except KeyboardInterrupt:
        print('\nInterrupted by user')

it uses ~50% of one ~3GHz cpu, and its "tanking" jack badly...

any tips or tricks on making this use less cpu? (or affect jack less)

pssst, wanna see some cool waves? try this as audio source: https://ultimae.bandcamp.com/track/security or for mind-blow, try: https://www.youtube.com/user/jerobeamfenderson1 ... dammit, now i wanna know how that oscilloscope works! :yet_another_rabbit_hole:

oskude commented 4 years ago

thanks to https://stackoverflow.com/questions/25904537/how-do-i-send-data-to-a-running-python-thread i got it to not tank jack at all (well, no xruns and only ~1% jack-usage) :heart_eyes:

import jack
import numpy
import threading
import pygame
from queue import Queue

class Plotter (threading.Thread):
    def __init__ (self, queue, size, args=(), kwargs=None):
        threading.Thread.__init__(self, args=(), kwargs=None)
        self.daemon = True
        self.queue  = queue
        self.black  = 0, 0, 0
        self.white  = 255, 255, 255
        self.halfh  = int(size[1] / 2)
        self.screen = pygame.display.set_mode(size)
        pygame.display.init()

    def run (self):
        while True:
            data = self.queue.get()
            if data is None:
                return
            self.paint(data)

    def paint (self, data):
        self.screen.fill(self.black)
        x = 0
        for d in data:
            y = int(self.halfh * (d + 1))
            pygame.draw.rect(self.screen, self.white, [x, y, 1, 1])
            x += 1
        pygame.display.flip()

client  = jack.Client("foovju")
event   = threading.Event()
input_1 = client.inports.register("input_1")

que     = Queue()
size    = client.blocksize, int(client.blocksize / 2)
plotter = Plotter(que, size)
plotter.start()

@client.set_process_callback
def process(frames):
    assert frames == client.blocksize
    data = input_1.get_array()
    plotter.queue.put(data)

with client:
    print('Press Ctrl+C to stop')
    try:
        event.wait()
    except KeyboardInterrupt:
        print('\nInterrupted by user')

i would still like to know any tips or tricks to make this use less cpu.

but please feel free to close this ticket (and maybe add this (or similar) to examples). now if you excuse me, i got some waves to ride :joy:

oskude commented 4 years ago

just for the records, thanks to https://stackoverflow.com/questions/6339057/draw-a-transparent-rectangle-in-pygame we get a fade out effect by filling the screen with slightly transparent black insted. (i guess cpu usage is higher thou).

diff --git a/main.py b/main.py
index b9456de..c411e9a 100644
--- a/main.py
+++ b/main.py
@@ -9,10 +9,11 @@ class Plotter (threading.Thread):
        threading.Thread.__init__(self, args=(), kwargs=None)
        self.daemon = True
        self.queue  = queue
-       self.black  = 0, 0, 0
        self.white  = 255, 255, 255
        self.halfh  = int(size[1] / 2)
        self.screen = pygame.display.set_mode(size)
+       self.blend  = pygame.Surface(size, pygame.SRCALPHA|pygame.HWSURFACE)
+       self.blend.fill([0, 0, 0, 64]) # TODO: make alpha user controllable. with midi fader!
        pygame.display.init()

    def run (self):
@@ -23,7 +24,7 @@ class Plotter (threading.Thread):
            self.paint(data)

    def paint (self, data):
-       self.screen.fill(self.black)
+       self.screen.blit(self.blend, [0, 0])
        x = 0
        for d in data:
            y = int(self.halfh * (d + 1))

ok, i stop spamming now :innocent:

oskude commented 4 years ago

holy transistors batman, did you know about gluOrtho2D()?

AFAIK the data from get_array() is an array of floats, ranging from -1.0 to 1.0 and lets say length is 1024, then we could plot our data AS IS thanks to gluOrtho2D(0, 1024, -1.0, 1.0) A.W.E.S.O.M.E:

import random
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *

screen = pygame.display.set_mode([1024, 512], pygame.DOUBLEBUF|pygame.OPENGL)
pygame.display.init()

#glPointSize(2.0) # TODO: use if screen width not same as data points!!!
glColor3f(1.0, 1.0, 1.0)
glClearColor(0.0, 0.0, 0.0, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
gluOrtho2D(0, 1024, -1.0, 1.0) # 1024 would be our number of data points
glFlush()

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            quit()

    glClear(GL_COLOR_BUFFER_BIT)
    glBegin(GL_POINTS)
    for x in range(0, 1024):
        y = random.uniform(-1.0, 1.0) # this would be our vanilla data point
        glVertex2f(x,y)
    glEnd()
    glFlush()

    pygame.display.flip()
    pygame.time.wait(25)

yeah, sorry, thats just a proof-of-concept on the drawing side, cause i was not able to draw (or use any gl* commands) from any jack process function, it just segfaults :scream: :sob: :zzz:

oskude commented 4 years ago

moving on, thanks for all the fishsnakes and sorry for the noise :heart:

mgeier commented 4 years ago

I typically suggest to use a queue, but you have already done that in your second comment.

It's normally a good idea to separate the audio callback from the drawing function. Depending on your JACK block size, the audio callback might run more often than the drawing function (or not).

Then, if you want to show a signal on the screen, you normally don't need the full sampling rate. Normally you can heavily downsample the signal. This way, the drawing function has to handle much less data. That's what I've done in this example: https://github.com/spatialaudio/python-sounddevice/blob/master/examples/plot_input.py

You seem to have moved on to C in the meantime, but my comments may be helpful there as well.

If you are using C, you can even use a better (because lock-free) queue: the JACK ringbuffer https://jackaudio.org/api/ringbuffer_8h.html

And you should not write to a global variable in your process callback! Just use the ringbuffer to shove audio data around ...