bastibe / SoundCard

A Pure-Python Real-Time Audio Library
https://soundcard.readthedocs.io
BSD 3-Clause "New" or "Revised" License
680 stars 69 forks source link

Running soundcard in a multiprocess on ubuntu #96

Open Bob-Thomas opened 4 years ago

Bob-Thomas commented 4 years ago

So right now I am testing my system to be compatible with linux as well. Unfortunately this caused some new problems of which I was able to deduct it to issues with soundcard (pulse) and multiprocessing.

It seems when you run SC inside a multiprocessing process it generates this error

Assertion 'o' failed at pulse/operation.c:67, function pa_operation_unref(). Aborting.

I think this happens because it reimports the module when using a multi process causing it to perhaps lock on a shared resource?

The way I tested this was by recreating the scenario in a simple program.

If you run this it will record from all available mics but fail when trying to do this in a process.

To make it work inside you process you need to move the soundcard import inside the process function and remove the normal process() call

from multiprocessing import Process
import time

import soundcard as sc
def process():
    for mic in sc.all_microphones(include_loopback=True):
        time.sleep(0.2)
        with mic.recorder(samplerate=16000, channels=1) as m:
            for i in range(0, 2):
                m.record(numframes=640)
            print("recorded " + mic.name) 

print("Running normally")
process()

print("Running inside a process")
proc = Process(target=process, daemon=True)
proc.start()
proc.join()
bastibe commented 4 years ago

Is this using the forked processes? Perhaps the fork shares some kind of resource that should not be shared. Does it work if you import soundcard inside the process?

Chum4k3r commented 4 years ago

Hello, sirs

First of all, thank you, @bastibe for such a great pypackage.

I'm currently migrating some old portaudio code to soundcard and faced exactly this same issue on multiprocessing.

What made it work to me is running the playback/recording in a separate threading.Thread and any calculations about the recorded or played data in a parallel process.

Although I now Threads are not even close to a good choice in audio, it's working glitch free.

EDIT: Importing soundcard inside the parallel process gave me an error as well.

I'm currently on Linux Mint 19.3

Bob-Thomas commented 4 years ago

Glad I'm not alone in this.

Unfortunately threads are not possible in my current system since it still seems to get blocked and miss audio data.

Importing the soundcard in your parrelel process only works if you import it nowhere else.

bastibe commented 4 years ago

Thank you for your further investigations.

You could try setting a unique program name in each process, using the soundcard._pulse.name property. Perhaps pulse is confused by too many processes sharing the same name?

Or perhaps pulse has a limit on how many processes can access a sound card at the same time?

It might actually help to ask for this on the pulseaudio mailing list. A lot of the error cases of pulseaudio are terribly under-documented. I have to be honest, however, and confess that I don't have the time to do the analysis here. I'll try to assist you as much as I can, but I can't do it myself. I would be incredibly grateful for a pull request!

Chum4k3r commented 4 years ago

I've tried setting different names for different processes but it didn't worked as well.

Just for the record, the following code give me a different pulse assertion error:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# scwire.py

import multiprocessing as mp
import soundcard as sc  # (1)

class Wire:
    def __init__(self, samplerate=48000, blocksize=240, channels={'in': [0, 1], 'out': [0, 1]}):
        self.samplerate = samplerate
        self.blocksize = blocksize
        self.channels = channels
        self.running = mp.Event()
        self.mic = sc.default_microphone()  # (2)
        self.spk = sc.default_speaker()  # (3)
        return

    def start(self):
        with self.mic.recorder(self.samplerate, self.channels['in'], self.blocksize) as r:
            with self.spk.player(self.samplerate, self.channels['out'], self.blocksize) as p:
                self.running.set()
                while self.running.is_set():
                    p.play(r.record(self.blocksize//2))
                r.flush()
        return

    def stop(self):
        self.running.clear()
        return

def list_devices():
    import soundcard as sc
    spks = dict([(spk.name, spk.id) for spk in sc.all_speakers()])
    mics = dict([(mic.name, mic.id) for mic
            in sc.all_microphones(include_loopback=True)])
    return spks, mics

if __name__ == '__main__':
    import sys

    # list_devices()  # (4)

    wire = Wire()
    proc = mp.Process(target=wire.start)
    proc.start()

    while True:
        key = input()
        if key in ['stop', 'exit', 'q']:
            wire.stop()
            break

    sys.exit(proc.join())

By running python scwire.py the output is: Assertion 's' failed at pulse/stream.c:1399, function pa_stream_connect_playback(). Aborting.

But if the lines commented with (1), (2), and (3) are moved inside the start method, the wire completely works. I'll call it "fix".

So, the error mentioned by @Bob-Thomas about Assertion 'o' failed... only happens if soundcard is imported on both parent and child processes, like the case if the line (4) is uncommented after the "fix" is made.

I know it don't get us closer to solving this issue, but shows another pulseaudio behaviour.

bastibe commented 4 years ago

Thank you, @Chum4k3r!

It seems that pulseaudio has some strange multiprocessing behavior, with state being shared and conflicting between parent and child processes. This makes some amount of sense.

I wish I could find some documentation on the matter.

pablodz commented 3 years ago

Same issue here trying to use multiprocessing here. However, importing soundcard inside the process works as @Bob-Thomas mentioned. I think we can handle as an Exception on python-side. Tested on Debian 10 64 bits

from multiprocessing import Process, Queue,current_process

def record_only_process():

    import soundcard as sc

    process_name = current_process().name
    start_time = time.time()

    x=0
    numframes = int(44100*record_seconds)

    default_mic = sc.default_microphone()
    recorder = default_mic.recorder(44100, channels=1, blocksize=256)

    with recorder as r:
        while True:
            data = r.record(numframes)  # record each _x seconds_
            # HERE ADD data to a Queue
            print('{} RECORD: {}'.format(process_name, str(process_queue)))

if __name__ == '__main__':

    p1 = Process(target=record_only_thread, name='[P1]')
    p1.start()
    p1.join()
bastibe commented 3 years ago

By running python scwire.py the output is: Assertion 's' failed at pulse/stream.c:1399, function pa_stream_connect_playback(). Aborting.

Have you tried running the latest git version instead of the somewhat outdated PyPi release?