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

>2 channel(s) on multi-channel input devices? #45

Closed valkyriesavage closed 9 years ago

valkyriesavage commented 9 years ago

Is it possible to select channel(s) on multi-channel audio devices when creating a stream? In particular, I have a 16 channel USB soundcard from which I need to stream each channel separately. I don't see options in Stream that support selecting which channel(s) it reads from.

(or even to select 3+ channels; it would be ok if I just had to use the first n channels)

bastibe commented 9 years ago

This is a limitation of the underlying library we are using: You can select a number of channels to read, but it will always select channels 1...N; You can't select an arbitrary selection of channels such as 2, 5, and 7. Also note that on some platforms, selecting a non-power-of-two number of channels may fail due to a bug.

By default, all channels of the given sound device are selected. You can get a list of all devices using devices(), and pass any of these to the Stream constructor as input_device or output_device. Each device is a dict that has a field for input_channels and output_channels. If you lower these values before passing the dict to the Stream constructor, your Stream will have fewer channels.

Some sound drivers choose to expose their channels in weird ways on some platforms. I have seen sound drivers that create sound devices in pairs of two channels, or provide different combinations in different sound devices. Maybe your sound card provides both a stereo device and a all-channels device.

valkyriesavage commented 9 years ago

Thanks!

mgeier commented 9 years ago

Are you still interested in using only a subset of channels?

In case you are:

It is indeed not possible to make a channel selection (other than the first N channels) using the generic PortAudio API. However, PortAudio provides a few HostAPI-specific function where this can be done for certain HostAPIs, see http://portaudio.com/docs/v19-doxydocs/group__public__header.html. PySoundCard doesn't (yet?) support any of those, but if you want us to implement certain things, please comment on issue #16.

The platform-independent solution would be to activate a whole range of channels, and fill the ones that are not needed with zeros. This is a certain amount of useless work, but in most applications this will not be a problem.

I'm actually currently working on adding this feature to PySoundCard; if you are interested, you can check out my "playrec" branch: https://github.com/bastibe/PySoundCard/tree/playrec. This provides a play() function which accepts a mapping argument. In this argument, you can specify a list of channels where the channels of the given array are played back. There are also the functions rec() and playrec(), which take similar arguments. If you have any suggestions for improvement, feel free to comment on issue #19 or contribute to the Wiki page: https://github.com/bastibe/PySoundCard/wiki/High-Level-API.

valkyriesavage commented 9 years ago

Thank you! I will definitely take a look at the wiki; I didn't expect to be doing so much audio processing, but here I am.

I actually have a related question: setting the input_channels variable doesn't seem to do "the right thing".

image

I tried that way, where I only get 2 channels from the source set to 18 channels (its default). I have also tried with 4 channels:

image

Where I also seem to get only 2 channels out. Is there something else I need to do other than what I've done there? If it helps, when I query the stream with s.input_channels, it does return 2 instead of what I set it to...

valkyriesavage commented 9 years ago

Oh, somehow it doesn't work if I feed it in for initialization, but if I initialize with that dictionary and then manually set s.input_channels = 4 it works alright...?

image

valkyriesavage commented 9 years ago

Hrm, actually...

image

image

image

All four of the channels I'm getting, in spite of the fact that I'm using 4 mics, are identical. And they have a strange quality: about half the time I get real data, and half the time I get 0s.

bastibe commented 9 years ago

It should be input_device, not inputDevice.

mgeier commented 9 years ago

Currently, the Stream constructor takes arbitrary keyword arguments without checking them, this is why inputDevice is accepted and ignored without raising an error. This should be fixed once #43 is merged.

I discourage repeated calls to Stream.read(), because there is no way to guarantee that (or check if) audio data is read without gaps in between. It's much more reliable to use the callback API for recordings that are longer than a few blocks. Of course the callback API is more complicated to use, that's why I wrote the convenience functions play(), rec() and playrec(), which are currently in the above-mentioned "playrec" branch: https://github.com/bastibe/PySoundCard/tree/playrec. I hope this will be available soon in an official release.

You should check this out; then your example will reduce to (and it will hopefully work!):

mydata = pysoundcard.rec(100 * CHUNK, samplerate=RATE, channels=CHANNELS)

The first argument is the number of samples you want to record.

BTW: you shouldn't specify a chunk size unless you have a reason to; if you don't specify a chunk size, the library uses appropriate chunking on its own.

valkyriesavage commented 9 years ago

Aha, most excellent. I had been using callbacks before, but was trying to get a MWE up with 4 channels to see how it worked. I've been specifying the block length so that I can do the analysis components of my project more easily (easier to compare things when I know all my FFT bins are the same).

Thanks so much, both.