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

cannot control volume dynamically #72

Closed rekinyz closed 4 years ago

rekinyz commented 4 years ago

there isn't a way to control the volume, in case someone want to implement voice dodge or somthing

bastibe commented 4 years ago

No, the sound card volume can not be adjusted. This is as it should be. Single apps usually do not control sound card volume, but instead have a little volume control that changes the volume of their own audio.

In SoundCard, you can accomplish this simply by multiplying your audio data with a gain factor.

SoundCard does not control your system's sound card. It merely plays and records on it.

dexterdy commented 11 months ago

@bastibe sorry for reviving this old issue, but I can't figure out how to implement this without stutter. I read the sound data using soundfile library, then I feed the data into the play stream. To manipulate volume on the fly, I need to multiply the data with a gain value, but this just causes the stream to stutter.

bastibe commented 11 months ago

Stuttering is usually a sign that you're taking too long to calculate, probably because you're doing things in an inefficient way, or using too short block sizes. Or maybe you mean something else by "stuttering". It's hard to tell from your description.

dexterdy commented 11 months ago

yes, I thought that was the case. I just don't know how to make go faster. I have the stream function in a separate thread and do the multiplication in the main thread when you change the volume. This is what my stream function looks like:


    def _setup_stream(self):
        global exitProgram
        with self.speakers.player(self.samplerate, self.channels, 2000) as player:
            while True:
                if exitProgram.is_set() or self.close:
                    break
                if self.playing:
                    data = self.playData[self.position : self.position + 2000]
                    self.position += 2000
                    if data.size == 0:
                        if self.endedCallback is not None:
                            self.endedCallback()
                        self.position = 0
                        self.playing = False
                        continue
                    player.play(data)
Chum4k3r commented 11 months ago

You should do the multiplication inside the audio thread, directly to the data array:

data = self.currentLevel * self.playData[self.position : self.position + 2000]
dexterdy commented 11 months ago

I tried with a block size of 20000 and while that sounds a lot better, I can still hear clicks on a regular interval when the play function returns and I have to get new data, but that gives me an idea. Perhaps if I prepare the data to be played in another, second thread, I can get smooth playback 🤔

dexterdy commented 11 months ago

You should do the multiplication inside the audio thread, directly to the data array:

data = self.currentLevel * self.playData[self.position : self.position + 2000]

wouldn't that just make it take even more compute time?

Chum4k3r commented 11 months ago

I cannot say upfront because I dont know how you are doing the multiplication on the main thread.

But this is the "standard" way of applying filters to an audio while playing

dexterdy commented 11 months ago

ah well, I just multiply the entire array and save to a variable, which the audio thread when it needs new audio data. I did it on the audio thread before, to just the slice, but since I got the stuttering, I thought it would be better to do on the main thread, so the audio thread wouldn't need to do any calculations

bastibe commented 11 months ago

This can be one of several issues:

Perhaps the main thread is consuming too much CPU time, and doesn't leave enough headroom for the audio thread. Python can only run one thread at any one time, so threading only helps with latency, not throughput.

In this particular case, perhaps the multiplication takes too long when it is applied, thus stalling the audio thread during the multiplication, which causes the stutter.

Or perhaps the "stutter" is not actually an audio problem at all, but the effect of changing the volume too quickly. Any large change in volume will sound like a click if applied instantaneously. You'd need to ramp up the volume over a short time to make it sound smooth. (This is more easily done in the audio thread with a little state machine)

Useful block sizes are usually in the 1000-20000 range, i.e. higher than the default hardware block rate, but low enough to react somewhat quickly to UI events.

dexterdy commented 11 months ago

Hm, then I suspect it is the first issue, the main tread taking too much cpu time. The volume is applied only once to the entire dataset, so that can't be the issue. I wrote the application to support many audio streams at once. Perhaps that's where the issue lies?

bastibe commented 11 months ago

Hard to tell from the outside. Heavily multithreaded code is not a good fit for Python, and real-time does not wait for stragglers.