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

Simple audio record example #80

Open karangejo opened 4 years ago

karangejo commented 4 years ago

It would be great if someone could add an example of recording audio to the examples in the documentation. If I figure it out I will try to add it. Thanks for the great library!

HaHeho commented 4 years ago

The most basic approach would be to gather the incoming blocks into an internal large numpy array. From there you could save this data in whatever format you want. This simple approach is probably not a good idea for recordings of really meaningful lengths. You'd have to find a solution to continuously write data on disk during run-time. The soundfile package used in the play_file.py example can probably do that for you.

Otherwise, on OSX (and probably Linux) I can really recommend ecasound to record and playback an arbitrary number of channels from any JACK client. I believe you'd have to compile it yourself to get JACK support, although that was very painless if I remember correctly.

mgeier commented 4 years ago

If I figure it out I will try to add it.

Yes please, that would be great!

As @HaHeho mentioned, the play_file.py example could be a good starting point.

You can also have a look at this example using the sounddevice module: https://github.com/spatialaudio/python-sounddevice/blob/master/examples/rec_unlimited.py

vyhyb commented 3 years ago

Hi, don't you have any further clues? I am trying to record about 2 seconds of audio, so I could probably use the "direct" method @HaHeho mentioned, but how exactly could I collect the incoming data? Sorry for this question, but I am a newbie in this area.

mgeier commented 3 years ago

@vyhyb If you know exactly how long the recording is supposed to be, and if the size of the recording easily fits into the available RAM, you can create a NumPy array of the appropriate size and shape (e.g. with np.zeros() or np.empty()) before activating your JACK client.

Then (after activating the client), in the process callback you can fill this array block by block. You just have to keep track of the place in the array where the next block of data is supposed to be written to (e.g. with a global variable).

Also, you have to be careful at the end of the recording, because the last block of data to be written might be smaller than the block size of the data provided to the process callback.

Sorry for this question, but I am a newbie in this area.

This is no problem at all!

But there are very many ways to do this kind of thing, the best way depends on your exact requirements.

The more exactly you can specify your requirements, the more and better targeted help we can provide (hopefully!).

vyhyb commented 3 years ago

Thanks for your answer! Now I'll try my best and may return with some more specific questions in the near future.

vyhyb commented 3 years ago

So I have an example using soundfile. It worked first few times, but now it throws an XRun and nothing is recorded at all. I don't know what the XRun exactly means, but The JACK Server says:

Mon Mar 15 17:22:42 2021: New client 'ImpedanceTubeTest2' with PID 18661
Mon Mar 15 17:22:42 2021: Connecting 'system:capture_1' to 'ImpedanceTubeTest2:in_1'
Mon Mar 15 17:22:42 2021: ERROR: JackEngine::XRun: client = ImpedanceTubeTest2 was not finished, state = Running
Mon Mar 15 17:22:42 2021: ERROR: JackAudioDriver::ProcessGraphAsyncMaster: Process error
Mon Mar 15 17:22:42 2021: Disconnecting 'system:capture_1' from 'ImpedanceTubeTest2:in_1'
17:22:42.138 Prohlédnutí činné zapojovací desky JACK...
17:22:42.141 Spojení JACK změněno.
17:22:42.344 Prohlédnutí činné zapojovací desky JACK...
17:22:51.103 Nákres spojení JACK změněn.
Mon Mar 15 17:22:51 2021: ERROR: Failed to find port 'ImpedanceTubeTest2:in_1' to destroy
17:22:51.194 Prohlédnutí činné zapojovací desky JACK...
17:22:51.197 Spojení JACK změněno.
17:22:51.400 Prohlédnutí činné zapojovací desky JACK...
Mon Mar 15 17:22:51 2021: Client 'ImpedanceTubeTest2' with PID 18661 is out

The Period size is already quite big so I think the problem is probably somewhere else.. Any suggestions?

The original script:

import numpy as np
import queue
import sys
import threading

buffersize = 2
clientname = "ImpedanceTubeTest2"

q = queue.Queue(maxsize=buffersize)
global blocks
blocks = 0
global rec_blocks
event = threading.Event()

def print_error(*args):
    print(*args, file=sys.stderr)

def xrun(delay):
    print_error("An xrun occured, increase JACK's period size?")

def shutdown(status, reason):
    print_error('JACK shutdown!')
    print_error('status:', status)
    print_error('reason:', reason)
    event.set()

def stop_callback(msg=''):
    if msg:
        print_error(msg)
    event.set()
    raise jack.CallbackExit

def process(frames):
    if frames != blocksize:
        stop_callback('blocksize must not be changed, I quit!')
    try:
        q.put_nowait(client.inports[0].get_array()[:])
    except (queue.Full):
        print("Full Queue")
        stop_callback()
    if blocks >= rec_blocks:
        stop_callback()  # Recording is finished

try:
    import jack
    import soundfile as sf

    client = jack.Client(clientname, no_start_server=True)
    blocksize = client.blocksize
    samplerate = client.samplerate
    rec_blocks = 1000
    client.set_xrun_callback(xrun)
    client.set_shutdown_callback(shutdown)
    client.set_process_callback(process)
    sf.SoundFile('pok_rec.wav', mode='w', samplerate=samplerate, channels=1)
    with sf.SoundFile('pok_rec.wav', mode = 'r+') as f:
        client.inports.register('in_1')
        with client:
            target_ports = client.get_ports(
                is_physical=True, is_output=True, is_audio=True)

            client.inports[0].connect(target_ports[0])

            timeout = blocksize * buffersize / samplerate
            f.write(q.get(timeout=timeout))
            for i in range(rec_blocks):
                f.seek(0,sf.SEEK_END)
                f.write(q.get(timeout=timeout))
                blocks += 1
            event.wait()  # # # Wait until recording is finished
except (queue.Empty):
    # A timeout occured, i.e. there was an error in the callback
    print("Empty Queue")

Update: Now it works almost every time with the code above, but sometimes there is still the problem with

Tue Mar 16 09:52:18 2021: ERROR: JackEngine::XRun: client = ImpedanceTubeTest2 was not finished, state = Triggered
Tue Mar 16 09:52:18 2021: ERROR: JackAudioDriver::ProcessGraphAsyncMaster: Process error

Is it caused by hardware or is it just a Python thing? I am using a regular Kubuntu 20.10 on laptop and Steinberg UR22 sound card.

mgeier commented 3 years ago

Sorry, I don't know ...

Does the error occur with other examples as well?

Did you try to set the --nperiods setting of jackd to 3 (instead of the default 2)?

The code looks fine except for two little things which are most likely unrelated:

vyhyb commented 3 years ago

Thank you for your response. The error seems to be related only to my code or maybe to the debug mode of VSCode, it is probably not related to the Python module. Now it works again just fine.

Did you try to set the --nperiods setting of jackd to 3 (instead of the default 2)?

I did and it seems to be more stable.

  • Is there a reason why you call the SoundFile constructor twice? Why do you need to open the file for reading? And even if you need that, does 'w+' or 'x+' not work?

I did not know, how to make every time a new file and then fill it with data. The 'w+' works great. Also the second advise was really useful, thank you ones more! Next step is to make it play and record at the same time, I hope there will be no problem :).

This is the final code:

import numpy as np
import queue
import sys
import threading

buffersize = 3
clientname = "RecTest"
rec_time = 10
q = queue.Queue(maxsize=buffersize)
global rec_end
rec_end = False
event = threading.Event()

def print_error(*args):
    print(*args, file=sys.stderr)

def xrun(delay):
    print_error("An xrun occured, increase JACK's period size?")

def shutdown(status, reason):
    print_error('JACK shutdown!')
    print_error('status:', status)
    print_error('reason:', reason)
    event.set()

def stop_callback(msg=''):
    if msg:
        print_error(msg)
    event.set()
    raise jack.CallbackExit

def process(frames):
    if frames != blocksize:
        stop_callback('blocksize must not be changed, I quit!')
    if rec_end:
        stop_callback()  # Recording is finished
    try:
        q.put_nowait(client.inports[0].get_array()[:])
    except (queue.Full):
        print("Full Queue")
        stop_callback()

try:
    import jack
    import soundfile as sf

    client = jack.Client(clientname, no_start_server=True)
    blocksize = client.blocksize
    samplerate = client.samplerate
    rec_blocks = int(rec_time * samplerate / blocksize)
    client.set_xrun_callback(xrun)
    client.set_shutdown_callback(shutdown)
    client.set_process_callback(process)
    with sf.SoundFile('pok_rec.wav', mode = 'w+', samplerate=samplerate, channels=1) as f:
        client.inports.register('in_1')
        with client:
            target_ports = client.get_ports(
                is_physical=True, is_output=True, is_audio=True)

            client.inports[0].connect(target_ports[0])

            timeout = blocksize * buffersize / samplerate
            f.write(q.get(timeout=timeout))
            for i in range(rec_blocks):
                f.write(q.get(timeout=timeout))
            rec_end = True
            event.wait()  # # # Wait until recording is finished
except (queue.Empty):
    # A timeout occured, i.e. there was an error in the callback
    print("Empty Queue")
mgeier commented 3 years ago

Are there still any open questions in this issue?

Any further comments?

If not, I think we can close this.

vyhyb commented 3 years ago

All questions resolved, thank you :).