Kohane
Kohane

Reputation: 11

How to change sound volume in real time

I need to change sound volume in real time with Python 3.6, using PyBinSim and Anaconda, but not mandatory (it can be just Python 3.6 and any other library good for this). The situation is the following: With a laptop and a microphone we're recording sounds and playing them back immediately, but we need to change the sound volume between recording and playback. I tried some code samples but I can't make it work without errors. Any ideas or suggestions? Thanks in advance!

Code examples

    import sounddevice as sd 
import time
import numpy as np 
from scipy import signal

duration = 10 #seconds

def callback(indata, outdata, frames, time):
        outdata[:] = indata
def print_sound(indata, outdata, frames, time, status):
    volum_norm = np.linalg.norm(indata) * 10
    print("|") * int(volum_norm)

with sd.Stream(callback=print_sound):
    sd.sleep(duration * 1000)

    import sounddevice as sd 
import time
import numpy as np 
from scipy import signal
from ctypes import cast, POINTER
from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume

duration = 1000 #seconds 

def callback(indata, outdata, frames, time, status):
    if status:
        print(status)
        outdata[:]=indata

devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(
   IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))

# Control volume
#volume.SetMasterVolumeLevel(-0.0, None) #max
#volume.SetMasterVolumeLevel(-5.0, None) #72%
volume.SetMasterVolumeLevel(-10.0, None) #72%
volume.SetMasterVolumeLevel(-10.0, None) #51%

with sd.Stream(channels=2, callback=callback):
    sd.sleep(duration*1000)

The second example returns no errors but when tested on a Win10 laptop there is no sound to be heard either.

Upvotes: 1

Views: 4960

Answers (1)

Anil_M
Anil_M

Reputation: 11443

pyaudio has a method for blocking callback. Which means that you can record from microphone in chunks and then play them back. This method is suitable for manipulation of audio chunks to one's needs.

However, instead of converting chunks to numpy arrays , manipulating and converting back to chunks, which can be pretty messy, audioop provides simpler methods to get power of each chunk and then increase/decrease per one's needs.

Below code is a modification of pyaudio callback code.

As per audioop documentation (below) we can use audioop.rms to get RMS value of fragment(in our case CHUNK). While this is not needed for changing audio volume, it can be used as debug to see if indeed changes are actually taking place on given audio chunk.

audioop.rms(fragment, width):   
    ''' Return the root-mean-square of the fragment, i.e. sqrt(sum(S_i^2)/n).
    This is a measure of the power in an audio signal.'''

Next, we multiply all values within a chunk by some factor.
e.g. multiplying by a factor of (2) will roughly double the RMS value. and hence increase volume level accordingly.

audioop.mul(fragment, width, factor):  
        '''Return a fragment that has all samples in the original fragment  
           multiplied by the floating-point value factor.   
          Samples are truncated in case of overflow.'''

Armed with these two method, we simply use code from pyaudio site and modify volume of each chunk as per multiplication factor.

I've also added code to collect old and new RMS values in a dict of list rmsdict for each chunk. Simply printing them on screen was causing clicking sound due to break in processing.

The audio from microphone was recorded for (3) seconds. You can try different lengths.

Working Code

import pyaudio
import audioop
from collections import OrderedDict


class DefaultListOrderedDict(OrderedDict):
    '''
    Ordered Dict of list to hold old and new RMS values
    '''
    def __missing__(self,k):
        self[k] = []
        return self[k]

FORMAT = pyaudio.paInt16
CHUNK = 2048
WIDTH = 2
CHANNELS = 1
RATE = 44100
RECORD_SECONDS = 3
FACTOR = 2    

rmsdict = DefaultListOrderedDict()

p = pyaudio.PyAudio()

stream = p.open(format=FORMAT,
                channels=CHANNELS,
                rate=RATE,
                input=True,
                output=True,
                frames_per_buffer=CHUNK)

print("* recording from microphone")

for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
    data = stream.read(CHUNK)
    rmsdict["chunk{}".format(i)].append(audioop.rms(data, 2))
    new_data = audioop.mul(data, WIDTH, FACTOR)
    rmsdict["chunk{}".format(i)].append(audioop.rms(new_data, 2))
    stream.write(new_data, CHUNK)
print("* done recording")

stream.stop_stream()
stream.close()

p.terminate()

Below is data from rmsdict. I've truncated it for visibility.

Python 2.7.9 (default, Dec 10 2014, 12:24:55) [MSC v.1500 32 bit (Intel)] on win32 Type "copyright", "credits" or "license()" for more information.

================================ RESTART ================================

  • recording from microphone
  • done recording

rmsdict DefaultListOrderedDict([('chunk0', [71, 143]), ('chunk1', [77, 155]), ('chunk2', [45, 91]), ('chunk3', [29, 59]), ('chunk4', [33, 66]), ('chunk5', [35, 71]), ('chunk6', [27, 54]), ('chunk7', [17, 34]), ('chunk8', [18, 36]), ('chunk9', [142, 285]), ('chunk10', [1628, 3257]), ('chunk11', [2666, 5333]), ('chunk12', [2174, 4348]),
..
..
('chunk51', [1723, 3446]), ('chunk52', [1524, 3049]), ('chunk53', [1329, 2659]), ('chunk54', [1166, 2333]), ('chunk55', [573, 1147]), ('chunk56', [1777, 3555]), ('chunk57', [1588, 3176]), ('chunk58', [698, 1397]), ('chunk59', [383, 767]), ('chunk60', [152, 305]), ('chunk61', [799, 1599]), ('chunk62', [2158, 4316])])

Upvotes: 2

Related Questions