bastibe/PySoundCard

Don't broadcast mono signals to self.output_channels()

Closed this issue · 5 comments

If a mono signal is given to write(), it's broadcast to self.output_channels().

This only makes sense if self.output_channels() == 2, and even there it's not always The Right Thing. There should be some kind of option to disable broadcasting even for 2 channels.

Also, if it's done in write(), it should also be done in the callback API for symmetry (with the same option to disable it).

I stumbled over this because on Linux/ALSA there is often a device called "default" (which happens to be the default PortAudio device) which has a ridiculously high number of channels (128 in my case), which get mixed together by some ALSA plugin.

When I tried to play back a several-minute-long mono signal, it filled my memory with 128 copies of it.
An error message would have been better in my case.

See my question on the PortAudio mailing list: http://music.columbia.edu/pipermail/portaudio/2014-February/015820.html

See also #6 and #8 for similar issues.

I don't even believe the broadcasting feature belongs in such a low-level interface.
I propose to create a higher-level convenience interface which can play/record NumPy arrays with a single function call.
In such a convenience interface it would make sense to create stereo streams for mono signals and cast them to both channels (but it should still be disableable).

The PortAudio people agreed that it would make sense for 2 channels: http://music.columbia.edu/pipermail/portaudio/2014-February/015831.html

Just a hypothetical example:

import pysoundcard as pa
sig = np.array(...)
pa.init(fs=44100, ...)
pa.play(sig, force_stereo=False)

I was actually just working on that code.

As soon as I push this change, 1-dim arrays will copy that one channel to all channels, while 2-dim matrices will fill the remaining channels with silence. I think this is a good compromise.

I don't think it should be done in the callback though. The callback is specifically for situations where low latency and full timing control is required and I want to keep processing to a mimimum there.

However, it will still fill 128 channels, which is a ridiculous amount of data. Frankly, I don't think PySoundCard should cater for those kinds of setups. You can reduce the amount of channels that are used by portaudio though, by manually reducing the number of channels in the device dict before instanciating the Stream.

I pushed that update right now. It is available as release 0.5.0 (820a916)

Thanks for the changes.
I don't think that it's a good compromise, however.
I think channels should never be automatically filled, except from mono to stereo (and only if the stream itself has only 2 channels and there should be a way to disable it).
Everything else should raise an error.

Actually I personally don't care so much about this, but it was my first experience with PySoundCard when I tried the blocking example according to the README file (using a rather long mono WAV file) which happend to fill my RAM to the brim and started swapping and I could hardly stop it.

I just want to spare others this experience.

As for the 128 channels, this is an oddity of ALSA, but I wouldn't generally rule out such an amount of channels. I regularly work with 64-channel MADI interfaces and colleagues of mine used three of them to drive a 192-channel loudspeaker system.
I would really like to be able to play test tones and make measurements on such a system using PySoundCard.

Finally, regarding the callback: I agree that it's a good idea to put as little Python code into there as possible.
Probably the same argument should be used on the blocking interface.
I will also suggest an additional high-level-interface here, similar to what I suggested for PySoundFile. I hope there will be continuing discussion ...
Depending on the outcome of that it will hopefully be clearer if channels should be duplicated or not.
Until then, I'd suggest to keep this issue open.

I hear what you are saying, and I can see that this is a problem. (Incidentally, this is why I didn't close the issue.)

However, the problem is that portaudio does not afford any way to write to one specific channel without providing data for all the other channels. Thus, we can't make that memory problem go away in PySoundCard.

What you can do, is either limit the number of channels in the device dict, or limit the frame size of your calls to write(). Both will drastically reduce the amount of memory needed.

Here is a proposition: We could introduce a keyword argument for the Stream constructor that sets the desired number of channels in the input and output device. Also, we could introduce a keyword argument to write(), that chops up the data in smaller chunks before actually writing, and only broadcasting one chunk at a time. Would those be acceptable solutions?

Lastly, I recently had a discussion with a fellow audio engineer, who found out that apparently, using the callback had significantly lower latency than using read/write (all other parameters being equal). This is very interesting, and we should probably take that into account for designing the interfaces, i.e. slim down the callback some more and not care about performance in read/write so much. This would provide the callback as a low-level interface and read/write as a high-level interface.

I too am very interested in continuing this discussion. Many interesting thoughts have been brought up in it, which will no doubt lead to a better library.

On 27.02.2014, at 19:52, Matthias Geier notifications@github.com wrote:

Thanks for the changes.
I don't think that it's a good compromise, however.
I think channels should never be automatically filled, except from mono to stereo (and only if the stream itself has only 2 channels and there should be a way to disable it).
Everything else should raise an error.

Actually I personally don't care so much about this, but it was my first experience with PySoundCard when I tried the blocking example according to the README file (using a rather long mono WAV file) which happend to fill my RAM to the brim and started swapping and I could hardly stop it.

I just want to spare others this experience.

As for the 128 channels, this is an oddity of ALSA, but I wouldn't generally rule out such an amount of channels. I regularly work with 64-channel MADI interfaces and colleagues of mine used three of them to drive a 192-channel loudspeaker system.
I would really like to be able to play test tones and make measurements on such a system using PySoundCard.

Finally, regarding the callback: I agree that it's a good idea to put as little Python code into there as possible.
Probably the same argument should be used on the blocking interface.
I will also suggest an additional high-level-interface here, similar to what I suggested for PySoundFile. I hope there will be continuing discussion ...
Depending on the outcome of that it will hopefully be clearer if channels should be duplicated or not.
Until then, I'd suggest to keep this issue open.


Reply to this email directly or view it on GitHub.

The additional arguments to the Stream constructor seem like a good idea to me. I will do some experimenting and then I can probably make some concrete suggestions for adding to/changing the Stream API.

In the meantime, I created a separate issue for talking about a high-level interface (which I mentioned in the original issue description): #19