My goal is to use Python to play sounds with the following requirements in a computer game context.
Take some input WAV file and randomly Vary the pitch to +/- 50% of original. Changing the sample rate seems to be an easy way to do this with PyDub.
Play the sound.
Be able to call this function rapidly so that long and short duration sounds overlap in actual playback.
I have spent over 24 work-hours searching for a way to meet all these requirements. I have done this before in Visual Basic and I was surprised at how difficult it is in Python.
Here is what I know so far:
PyGame.Mixer can play overlapping sounds concurrently, but it must play them all at the same sample rate. There doesn't appear to be a way to vary pitch.
PyDub can vary pitch by changing samplerate, but it can't play overlapping sounds with its basic playback. And, I have to write the output sound to file then immediately load it back, which feels wasteful.
WinSound can play PyDub's varying-samplerate sounds, but not with concurrent playback, not even with threading.
Playsound package does not work with python 3.6.
PyAudio can play PyDub's varying-samplerate sounds with concurrent playback if I use Threading, however, any more than a couple times and it causes horrible memory problems that quickly make Python crash.
My question: How can I achieve my 3 goals above without causing problems?
Here is the best result that I have so far (this is the PyAudio version which causes a crash if tested more than once or twice):
from pydub import AudioSegment
from random import random, seed
from time import sleep
import os
import threading
import pyaudio
import wave
def PlayAsyncWithRandPitch(WavPath):
MyBaseFilename = os.path.basename(WavPath)
sound = AudioSegment.from_file(WavPath, format="wav")
seed()
octaves = ((random()-0.50))
print("random octave factor for this sound is: "+str(octaves))
print("current sound frame rate:"+str(sound.frame_rate))
new_sample_rate = int(sound.frame_rate * (2.0 ** octaves))
print("new sound frame rate:"+str(new_sample_rate))
newpitchsound = sound._spawn(sound.raw_data, overrides={'frame_rate': new_sample_rate})
MyTotalNewPath = os.getcwd()+"\\Soundfiles\\Temp\\Mod_"+MyBaseFilename
newpitchsound.export(MyTotalNewPath, format="wav")
SoundThread = threading.Thread(target=PAPlay, args=(MyTotalNewPath,))
SoundThread.start()
#=======================================================================================
#This function is just code for playing a sound in PyAudio
def PAPlay(filename):
CHUNK = 1024
wf = wave.open(filename, 'rb')
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(CHUNK)
while data != '':
stream.write(data)
data = wf.readframes(CHUNK)
stream.stop_stream()
stream.close()
p.terminate()
return
if __name__ == "__main__":
#Example sounds to test if more than one can play at once
PlayAsyncWithRandPitch(os.getcwd()+'\\Soundfiles\\RifleMiss.WAV')
sleep(0.2)
PlayAsyncWithRandPitch(os.getcwd()+'\\Soundfiles\\splash.wav')
sleep(0.2)
PlayAsyncWithRandPitch(os.getcwd()+'\\Soundfiles\\sparkhit1.WAV')
sleep(5.0)
Thank you in advance for your kind help!