bastibe / PySoundCard

PySoundCard is an audio library based on PortAudio, CFFI and NumPy
BSD 3-Clause "New" or "Revised" License
87 stars 9 forks source link

Issues with short block sizes? #61

Open MrMho opened 8 years ago

MrMho commented 8 years ago

Although I use a recent computer (Windows 10, Core i5-6500, 8 GB RAM) as well as a good audio interface (Edirol UA-101), I am encountering buffer underflows when using block lengths < 50 ms. I would expect my setup to process much shorter block lengths without any problems.

Example: The following code gives me lots of "Underflow!" messages, whereas a blocksize of 50 ms works fine.

from pysoundcard import Stream, continue_flag
import time

fs = 44100
blocksize = int(0.04 * fs)

def callback(in_data, out_data, time_info, status):
    if status != 0:
        print("Underflow!")
    return continue_flag

oStream = Stream(samplerate=fs, blocksize=blocksize, callback=callback)
oStream.start()
time.sleep(3)
oStream.stop()

Do you have any idea why that is?

Thanks in advance!

bastibe commented 8 years ago

Which Device-API are you using? You should be using WASAPI (or ASIO, but that is more effort). Also, the callback API might be somewhat slower than direct reading/writing.

I can typically use block lengths as short as 4 samples with little trouble.

MrMho commented 8 years ago

I have been using MME. Now I am trying to switch to WASAPI. However, even though the devices I want to specify only differ from the old ones in terms of the used API, Python is raising an error: "RuntimeError: 0.0000: Invalid number of channels"

By the way, am I right in assuming that I have to specify the API by specifying the device ID? Is this the correct way to do it in the first place?

bastibe commented 8 years ago

Yes, you are correct in your assumption. This is an unfortunate implementation detail of Portaudio.

Sometimes, audio devices register in different Windows systems with different channel configurations. Are you using the same number of channels as the device specifies?

MrMho commented 8 years ago

Yes. This is what the device_info function returns:

In [3]: device_info(14)
Out[3]: 
{'default_high_input_latency': 0.01,
 'default_high_output_latency': 0.0,
 'default_low_input_latency': 0.003,
 'default_low_output_latency': 0.0,
 'default_samplerate': 192000.0,
 'hostapi': 1,
 'max_input_channels': 2,
 'max_output_channels': 0,
 'name': u'1-2 (UA-101)'}

# hostapi=1 corresponds to WASAPI

When I am trying to create a stream object like this, it raises the error:

oStream = Stream(samplerate=fs,
                 blocksize=blocksize,
                 callback=callback,
                 device=14,
                 channels=(2,0))
bastibe commented 8 years ago

Can you try opening an InputStream instead? Maybe that would work.

MrMho commented 8 years ago

Using an InputStream does not work either, it returns an error saying "RuntimeError: 0.0000: Invalid device"

I'm still using the same device as before.

bastibe commented 8 years ago

These are portaudio errors. I don't know what the problem is. At this point, pysoundcard is only passing these values to portaudio.

MrMho commented 8 years ago

Ok, it turned out that the sample rate I used for the InputStream did not match the sample rate in the Windows device manager. I fixed that and now it seems to work fine. However, I noticed that even at ridiculously short block lengths the status variable remains zero, which is why I cannot detect buffer underflows.

Example:

from pysoundcard import InputStream, continue_flag
import time

fs = 44100
blocksize = 1

def callback(in_data, time_info, status):    
    time.sleep(0.01) # some processing
    if status != 0:
        print("Underflow!")   
    return continue_flag

oStream = InputStream(samplerate=fs,
                 blocksize=blocksize,
                 callback=callback,
                 device=37,
                 channels=2)

oStream.start()
time.sleep(3)
oStream.stop()
bastibe commented 8 years ago

It might be that an InputStream is never actually underflowing, since portaudio is running in its own thread, which might not block for callbacks. Did you check whether your callback is actually receiving incomplete data? Portaudio might be handing you longer blocks if your callbacks take too long.

MrMho commented 8 years ago

I checked that by writing the incoming data into numpy arrays. Portaudio does not change the block size. Instead, it appears to me that some of the incoming blocks are skipped. It would be nice to have some kind of indicator when that happens. But then again, the improvement in performance due to the change of the API is huge, so I think pysoundcard is now fast enough for my needs. Thanks for your help!

bastibe commented 8 years ago

Sadly, this is just another internal quirk of portaudio, and there is nothing we can do about it on the pysoundcard side. There is a chance that this is a bug in portaudio, though. You could try reporting it on the portaudio mailing list, and maybe someone over there can help you.