Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR: jack.Ringbuffer example #113

Open
lysdexic-audio opened this issue Dec 29, 2021 · 6 comments
Open

FR: jack.Ringbuffer example #113

lysdexic-audio opened this issue Dec 29, 2021 · 6 comments

Comments

@lysdexic-audio
Copy link

lysdexic-audio commented Dec 29, 2021

Hi there,

Cheers for a great, easy to use library :) I think it would be nice to show an example using jack.Ringbuffer() since it would be helpful for some folks to see the intended implementation.

Seems there was some discussion of this here: #59 but never materialised into an example for this lib.

I often see Queue being used with jackclient-python and never the ringbuffer.

Any ideas why this feature isn't really used?

edit: I see/get #18 - but for folks who don't care too much about the occasional dropout (many use cases where dropouts are fine) be nice to see how to keep everything in JACK without going to extra dependencies :)

@lysdexic-audio
Copy link
Author

Something like this?

class AudioBuffer(object):

    def __init__(self, blocksize):
        self.ringbuffer = jack.RingBuffer(blocksize*2)
        self.blocksize = blocksize

    def insert(self, data):
        if self.ringbuffer.write_space < (len(data)):
            # no space in the ringbuffer, discard it
            return
        self.ringbuffer.write(data)

    def __iter__(self):
        while True:
            while self.ringbuffer.read_space < self.blocksize:
                time.sleep(0.05) #wait a little bit to not use all the cpu
            data = self.ringbuffer.read(self.blocksize)
            yield data


clientname = 'jackclient'

client = jack.Client(clientname)
blocksize = client.blocksize
samplerate = client.samplerate

if client.status.server_started:
    print('JACK server started')
if client.status.name_not_unique:
    print('unique name {0!r} assigned'.format(client.name))

event = threading.Event()
audio_buffer = AudioBuffer(blocksize)

@client.set_process_callback
def process(frames):
    assert frames == client.blocksize
    for i in client.inports:
        data = i.get_buffer()[:]
        audio_buffer.insert(data)

@client.set_shutdown_callback
def shutdown(status, reason):
    print('JACK shutdown!')
    print('status:', status)
    print('reason:', reason)
    event.set()

capture = []
# create two port pairs
for number in 1, 2:
    client.inports.register('input_{0}'.format(number))
    capture.append(client.get_port_by_name(f'mycaptureport{number}'))

with client:
    for src, dest in zip(capture, client.inports):
        client.connect(src, dest)

    print('Press Ctrl+C to stop')

    try:
        for data in audio_buffer:
            vals = np.frombuffer(data, dtype=numpy.float32)
            print(vals)
        event.wait()
    except KeyboardInterrupt:
        print("Exiting..")
        pass

@mgeier
Copy link
Member

mgeier commented Dec 29, 2021

Thanks for bringing this up!

I think an example might be nice, but I consider the jack.RingBuffer quite an advanced feature, so we probably shouldn't encourage everyone to use it by default.

I often see Queue being used with jackclient-python and never the ringbuffer.

TBH, I don't see a lot of code, so I don't actually know if and how this module is used.

If you have links to public projects using it, please share them here!

Any ideas why this feature isn't really used?

I don't know, but maybe that's a good thing?
I would probably not generally recommend using it.

It is quite error-prone and I think there aren't many cases where it has a clear advantage over alternatives.

I think queue.Queue is much easier to use correctly.

for folks who don't care too much about the occasional dropout (many use cases where dropouts are fine) ...

If you don't care about an occasional dropout, you probably don't need the JACK ring buffer in the first place.

The whole reason to use it would be to avoid locking, exactly to avoid dropouts.
However, if you use Python in the process callback, the GIL (global interpreter lock) will be locked anyway, so you cannot avoid locking in this case.

I think the only situation where the JACK ring buffer is really useful is when the process callback is implemented in a compiled language (like e.g. C), but this is not yet supported (see also #59 (comment)).
I've created the https://github.com/spatialaudio/python-rtmixer project to experiment with such a callback function, and a similar thing could probably done with this jack module.

... be nice to see how to keep everything in JACK without going to extra dependencies :)

Sure, but queue.Queue is in the standard library, so there is no extra dependency?

@lysdexic-audio
Copy link
Author

lysdexic-audio commented Dec 29, 2021

Hey! Thanks for the quick reply

If you don't care about an occasional dropout, you probably don't need the JACK ring buffer in the first place.

ah interesting - well, I guess i've been having success using collections.deque instead of Queue also based on some stuff here. edit: reading a little further, seems this isn't such a good idea maybe..

I've created the https://github.com/spatialaudio/python-rtmixer project to experiment with such a callback function, and a similar thing could probably done with this jack module.

yeah I was looking at this! and this is what I meant when I was talking about extra dependencies. RTMixer looks fantastic - but in my case, I'm actually using other JACK functions like the transport controls and the CPU monitoring, which jackclient-python is perfect for. It would be wonderful to have it all under one roof. Dropouts are kind of okay to deal with for most of the time - but for Audio/Visual sync it can be a little jarring, it would be so nice to eliminate it

It seems the CFFI is at work within this lib - so could this be implemented here too?

https://github.com/spatialaudio/python-pa-ringbuffer

@mgeier
Copy link
Member

mgeier commented Jan 1, 2022

well, I guess i've been having success using collections.deque instead of Queue also based on some stuff here. edit: reading a little further, seems this isn't such a good idea maybe..

Yeah, as the SO answers say, queue.Queue has been made for inter-thread communication, collections.deque hasn't.
If it works for you, it works accidentally. You shouldn't use it in this case.

I've created the https://github.com/spatialaudio/python-rtmixer project to experiment with such a callback function, and a similar thing could probably done with this jack module.

yeah I was looking at this! and this is what I meant when I was talking about extra dependencies. RTMixer looks fantastic - but in my case, I'm actually using other JACK functions like the transport controls and the CPU monitoring, which jackclient-python is perfect for. It would be wonderful to have it all under one roof.

It seems the CFFI is at work within this lib - so could this be implemented here too?

https://github.com/spatialaudio/python-pa-ringbuffer

The PortAudio ring buffer is not necessary here, it does about the same thing as the JACK ring buffer.

You can already use jack.RingBuffer in Python code (as you have shown with your example code), but using a process callback function implemented in C is currently not supported.
I guess it could be implemented in a similar way to how I've implemented it in the sounddevice module: https://github.com/spatialaudio/python-sounddevice/blob/a56cdb96c9c8e3d23b877bbcc7d26bd0cda231e0/sounddevice.py#L884-L885.

@lysdexic-audio
Copy link
Author

On my Raspberry Pi4, using the Queue method brings my CPU usage up to 60-70%

I wonder, would C based process callback function optimise this also?

@mgeier
Copy link
Member

mgeier commented Jan 9, 2022

I don't think the choice of queue should have a significant influence on the average CPU usage.

It's more about the worst-case behavior.
If the process callback is implemented in Python, both taking the GIL and the (more or less random) invocation of the garbage collector might cause large pauses. If those pauses are too long, they will lead to xruns.

If you implement the process callback in C (or some other compiled language like C++ or Rust, and probably it also works with Cython + nogil?), you can avoid both problems.

Using Python + Queue will introduce additional locking, which might or might not make the situation worse.

If you want to reduce the average CPU usage, you can try to optimize your Python code, maybe using some NumPy functions?
If that's not enough, you can of course again consider using a compiled language, which might be more efficient (or not, because NumPy can already be very fast).

Either way, you should try to do some profiling to find out where the most CPU load is generated, this can often be counter-intuitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants