Why does the volume of a PyAudio stream sometimes 'half', and other times, produce unwanted white noise when 'converted bytes' are divided by 2?

Viewed 150

I am 'new' to using the PyAudio module in Python 3.7, and have already done a few 'neat' things with the PyAudio interface. I have figured out how to generate and play some 'custom pitches' bunched together in sequences where the data first gets converted to a -32768 to +32768 range (using int(n).to_bytes() and then n = data.from_bytes()to convert back and forth from bytes to integer, alter the values, and then convert back to bytes again for the stream).

While my values are integers, I can divide by 2 to 'halve' my volume for the 'custom pitches', but, when I divide by 2 if I make 'n' (the integer variable used for the integer values) equal to the converted data of the 'sounds\hello.wav' file, it doesn't 'halve' the volume, but it creates unwanted white noise instead. If I don’t divide by 2, my 'sounds\hello.wav' file plays fine.

Where my comments are in 'all caps', that is where the 'problem' is. The 'capitalized comment section' shows four different 'options' that can be used for the value of 'n', before n gets converted back to bytes, and gets written to the stream. Three of those four 'options' work, but I have been trying to figure out why 'the fourth option' is giving me a 'problem'. The 'four options' that 'reproduces the problem' is why my code generates two warnings, and is not a 'programmatical problem'. What I am working on, might one day help create a whole new technology for sound and music. Here is my code...

import math
import time
import wave

import pyaudio

pitches = 0
position = []
start = time.time()
started = True
oldTime = 0
delta = 0
run_time = 0
val = []
lastVal = []
lastVal2 = []
count = 0

def get_pitches():
    global val
    global run_time
    global lastVal
    global lastVal2
    global position
    global pitches

    n = 0
    val = []
    pitches = 0

    # Store the offset and the increment (through time) into result.
    run_time += delta

    # PITCHES GO HERE.
    n += add_pitch_with_time_stamp(offset = 0.0, increment = 0.0, volume = 0.5, pitch_stamp=[0.01, 0.015, 0.02, 0.015], time_stamp=[.5, .5, .5, .5], transition_time_stamp = [10, 10, 10, 10], voice = "sounds\\ah.wav")
    n += add_pitch_with_time_stamp(offset = 0.0, increment = 0.0, volume = 0.5, pitch_stamp=[0.01, 0.02, 0.03, 0.02], time_stamp=[1, 1, 1, 1], transition_time_stamp = [10, 10, 10, 10], voice = "sounds\\ah.wav")
    #n += add_pitch(offset = .01, increment = .1, volume = 1)
    #n += add_pitch(offset = 0.015, increment = -.001, volume = 1)
    #n += add_pitch(offset = 0.04, increment = 0, volume = 1)

    # Average out the pitches before returning n.
    if pitches != 0:
        n /= pitches

    return n

def add_pitch(offset, increment, volume):
    global pitches
    global delta
    global run_time
    global val
    global lastVal
    global lastVal2
    global position

    # Match the size of arrays for positions and last recorded values.
    if pitches >= len(position):
        position.append(0)
    if pitches >= len(lastVal):
        lastVal.append(0)
    if pitches >= len(lastVal2):
        lastVal2.append(0)

    # Get the calculated pitch for the wave.
    pitch = ((run_time - start) * increment) + offset

    # If the pitch is out of range set the result to 0.
    if 0.3 > pitch >= 0:
        if pitches < len(lastVal):
            lastVal2[pitches] = lastVal[pitches]

        val.append((1 + math.sin(((position[len(val) - 1]) * pitch) * math.pi * 2) * 0.5 * volume) - 0.5)

        if pitches < len(lastVal):
            lastVal[pitches] = val[len(val) - 1]
            result = ((val[len(val) - 1] * 0x7f) + 0x80)
        else:
            result = 0

        # Increase pitches per function call to determine the average value for n.
        pitches += 1
    else:
        result = 0

    return result

def add_pitch_with_time_stamp(offset, increment, volume, pitch_stamp, time_stamp=None, transition_time_stamp=None, voice=None):
    global pitches
    global delta
    global run_time
    global val
    global lastVal
    global lastVal2
    global position

    # Match size for time stamp.
    for i in range(0, len(time_stamp)):
        if (i + 1) > len(time_stamp):
            time_stamp.append(1)

    # Match size for transition time stamp.
    for i in range(0, len(pitch_stamp)):
        if (i + 1) > len(transition_time_stamp):
            transition_time_stamp.append(1)

    # Get a total time modulation from the time stamp.
    time_modulation = 0
    for i in range(0, len(time_stamp)):
        time_modulation += time_stamp[i]

    # Get the time index.
    time_flow = (time.time() - start) % time_modulation

    # Store the time transitions into f.
    f = get_transition_value(pitch_stamp, transition_time_stamp, time_stamp, time_flow)

    # Match the size of arrays for positions and last recorded values.
    if pitches >= len(position):
        position.append(0)
    if pitches >= len(lastVal):
        lastVal.append(0)
    if pitches >= len(lastVal2):
        lastVal2.append(0)

    # Get the calculated pitch for the wave.
    pitch = (((run_time - start) * increment) + (f + offset))

    # If the pitch is out of range set the result to 0.
    if 0.3 > pitch >= 0:
        if pitches < len(lastVal):
            lastVal2[pitches] = lastVal[pitches]

        #print (data2)

        if voice is None:
            val.append((1 + math.sin(((position[len(val) - 1]) * pitch) * math.pi * 2) * 0.5 * volume) - 0.5)
        else:
            val.append((1 + math.sin(((position[len(val) - 1]) * pitch) * math.pi * 2) * 0.5 * volume) - 0.5)

        if pitches < len(lastVal):
            lastVal[pitches] = val[len(val) - 1]
            result = ((val[len(val) - 1] * 0x7f) + 0x80)
        else:
            result = 0

        # Increase pitches per function call to determine the average value for n.
        pitches += 1
    else:
        result = 0

    return result

def get_transition_value(value_list, transition_list, t_stamp, t_flow):
    t_total = 0
    t_position = t_flow
    t_index = 0
    for i in range(0, len(t_stamp)):
        t_total += t_stamp[i]
        if t_flow >= t_total:
            t_position -= t_stamp[i]
            t_index = i + 1

    #t_process is the fraction of time between each transition.
    t_process = t_position / t_stamp[t_index]

    # Get the current value from the time stamp.
    v_floor = value_list[t_index % len(value_list)]

    # Get the next value from the time stamp.
    v_ceil = value_list[(t_index + 1) % len(value_list)]

    # Determine the 'power' between each transition
    transform_power = transition_list[int(t_flow) % len(value_list)]

    return transition(v_floor, v_ceil, math.pow(t_process % 1, transform_power))

def transition (down, up, mid):
    # Another function for finding in between values.
    return (down * (1 - mid)) + (up * mid)

def get_delta_time():
    # Store the delta time into a delta variable.
    global delta
    global oldTime
    delta = time.time()-oldTime
    oldTime = time.time()

def do_pitches():
    global pitches
    global position
    global started
    global lastVal
    global lastVal2
    global count
    global delta

    # Create an interface to PortAudio
    p = pyaudio.PyAudio()

    wf = wave.open("sounds\\hello.wav", 'rb')

    # Open a .Stream object to write the WAV file to
    # 'output = True' indicates that the sound will be played rather than recorded
    stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), # 8bit
                    channels=wf.getnchannels(),
                    rate=wf.getframerate(),
                    output=True)

    try:
        while True:
            # Make a variable called 'n', and set it to 'silent' (0).
            pitches = 0

            # Store pitches in n.
            n: int = 0
            if started:
                position = []
                lastVal = []
                lastVal2 = []
                #n = get_pitches()
                for i in range(0, pitches):
                    position.append(0)
                    lastVal.append(0)
                    lastVal2.append(0)
                started = False

            # Read the voice data 1 frame at a time.
            data2 = wf.readframes(1)

            # Convert the data from byte format, into an integer value ranging from -32768 to 32768.
            v = int.from_bytes(data2, 'big')

            # NOTE:
            #
            # If I use this line only, without dividing the value of n by 2,
            # it works fine.
            #
            # This line takes the values of all the pitches (averaged) placed
            # in the get_pitches() function before later converting 'n' to a
            # byte value (called 'data') and then writing 'data' to the stream.
            #
            n = (transition(n, get_pitches(), 1))

            # NOTE:
            #
            # If I use this line only, without dividing the value of n by 2,
            # it works fine.
            #
            # This line will play a .wav file called 'sound\hello.wav' before
            # later converting n to a byte value (called data) and writing
            # data to the stream.
            #
            n = v

            # NOTE:
            #
            # If I use this line only, dividing the value of n by 2, it
            # works fine.
            #
            # This line takes the values of all the pitches (averaged) in
            # the get_pitches() function ... and will 'halve' the volume
            # (as it is supposed to do since i 'half-ed' the value).
            #
            # The value of n later gets converted to a byte value (called
            # 'data') and gets written to the stream.
            #
            n = (transition(n, get_pitches(), 1)) / 2

            # NOTE:
            #
            # ***problem***: if I use this line only, and dividing the value
            # of n by 2, this produces unwanted white noise instead of
            # 'halving' the volume, even though i divided the value
            # of n by 2.
            #
            # This line should play a .wav file called 'sound\hello.wav'
            # before later converting n to a byte value (called data)
            # and writing data to the stream.
            #
            n = int(v / 2)

            # Convert the value of 'n' into bytes.
            data = int(n).to_bytes(2, 'big')

            #wf.setpos(int((time.time() - start) % wf.getsampwidth()))
            #print (int(time.time() % wf.getsampwidth()))

            # Writing data to stream makes the sound.
            stream.write(data)

            # Write voice to voice stream.
            #stream2.write(data2)

            # Increment position so that the 'n' result (from getPitches)
            # produces a sine-wave.
            for i in range (0, len(position)):
                position[i] += 1

                # Limit each position to 1000 chunks to prevent popping.
                if count % 1000 == 0:
                    position[i] = 0

            get_delta_time()
            count += 1
    except KeyboardInterrupt:
        pass

    # In the case the while loop breaks.
    stream.close()
    p.terminate()

do_pitches()

1 Answers

I found the problem... I took a .wav file called 'top.wav', which had 1 second of the maximum possible positive value that a wav can contain, and another .wav file called 'bottom.wav', which had 1 second of the maximum negative possible value (to see what values those 2 .wav files generate, so I could fully understand how the 'byte system' works).

Instead of converting the byte value of 'data' into an integer (using int.from_bytes()), I discovered a function called struct.unpack(), that takes the byte data, and converts it the RIGHT way, into a tuple with the value (<integer data value>, 0)

I got the real value from using...

decoded[0]

when using the code...

...
fmt = "<" + "h"
if data != b'' and data != b'\x00\x00\x00\x00':
    decoded = struct.unpack(fmt, data)
if data == b'\x00\x00\x00\x00':
    decoded = (0, )
...

Then, I noticed that the value are 'scrambled' to where anything that 'ranges' from 0 to 128, needs to be converted to 128 - (value - 1), and anything that ranges from 129 to 256 needs to be converted to (256 - (value - 128)) - 1... so I had to write a function called 'invert values'...

...
#This function makes values 0 to 128, 128 to 0 and values 129 to 256, 256 to 129.
def invert_values(n):
    if n < 128:
        n = 128 - (n - 1)
    if 128 <= n < 256:
        n = (256 - (n - 128)) - 1
    return n
...

do my arithmetic afterwards,

use...

n = invert_values(n)

before converting n back to a byte value, and my wavs play fine. When I divide by 2, my volume 'halves'.

Related