12

I am attempting to create a program in python that plays a particular harpsichord note when a certain key is pressed. I want it to remain responsive so you can continue to play more notes (kind of like a normal electric piano.) However, because the wav files that the notes are stored in are about 7-10 seconds long I am experiencing some issues. I can press at least 10 keys per second. So, over the duration of one note I could have around 100 different wav files playing at once. I tried to use winsound, but it was unable to play multiple wav files at once. I then moved on to PyAudio and it works kind of. The only way that I found to accomplish what I wanted was this:

from msvcrt import getch
import pyaudio
import wave
import multiprocessing as mp

#This function is just code for playing a sound in PyAudio
def playNote(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()


if __name__ == "__main__":

    while True:
        #If the 'a' key is pressed: start a new process that calls playNote
        #and pass in the file name for a note. 
        if ord(getch()) == 97: #a

            mp.Process(target=playNote, args=("F:\Project Harpsichord\The wavs\A1.wav",)).start()

        #If the 's' key is pressed: start a new process that calls playNote
        #and pass in the file name for another note. 
        if ord(getch()) == 115: #s

            mp.Process(target=playNote, args=("F:\Project Harpsichord\The wavs\A0.wav",)).start()

Basically whenever I want to play a new wav, I have to start a new process that runs the code in the playNote function. As I already stated I can potentially have up to 100 of these playing at once. Suffice it to say, one hundred copies of the python interpreter all running at once almost crashed my computer. I also tried a similar approach with multi-threading, but had the same problems.

This post shows a way to mix multiple wav files together so they can be played at the same time, but since my program will not necessarily be starting the sounds at the same time I am unsure if this will work. I need an efficient way to play multiple notes at the same time. Whether this comes in the form of another library, or even a different language I really don't care.

Community
  • 1
  • 1
  • 1
    Have you tried using Threading instead of multiprocessing? – disflux Dec 30 '15 at 20:18
  • to play several (upto 8) sounds concurrently, you could use [`pygame.mixer.Sound()`](http://stackoverflow.com/q/6004887/4279). `pygame` also provides a portable way to get keyboard input. – jfs Dec 31 '15 at 01:11
  • @disflux Thanks for your response. Yes I did attempt a similar approach with the Threading module, but had similar lag problems. – Joshua Jurgensmeier Dec 31 '15 at 23:35
  • @J.F.Sebastian Thanks for your response. I have considered looking into using PyGame, but haven't tried it yet. I will definitely check it out. Would you recommend that I run multiple `pygame.mixer.Sound()` functions on separate threads to achieve the number that I want? Eight isn't quite enough (; – Joshua Jurgensmeier Dec 31 '15 at 23:38
  • Admittedly, I don't know a lot about PyAudio, but have you tried/considered keeping a stream open and having some sort of real-time feed filling the stream? Each time a note was triggered you could update a shared array/object between the thread which was updating the stream and the thread which was receiving key events. The thread updating the stream would mix (as in the linked post) each of the 'chunks' together and write that to the stream object. This is bordering on a real-time audio application, and I don't know if that's where you're headed with this project though. – KFox Jan 02 '16 at 04:46
  • @KFox I have considered that exact thing. However, I am unsure how I would implement it. If pygame doesn't solve my problems, I guess I will just think harder about this. Thanks for the reply. – Joshua Jurgensmeier Jan 03 '16 at 19:29
  • My first thought was similar the post you linked. When you start a new sound, create a variable with the numpy array of the remaning waveform of the sound and add the new waveform to it, and immediately start playing the new sound. Hopefully the processing time is fast enough that it plays immediately. You can use scipy.io.wavfile to parse audio to an array. I could probably be convinced to code this up for you, but I need to go now :). Let me know if you are still struggling with this on Monday. – Roman Jan 08 '16 at 09:50
  • @Roman Thanks for your reply. I haven't had a chance to work on this since your comment. I will probably be working on it over the week though. If you want to write the code and post it I would appreciate it, but I should be able to come up with it. – Joshua Jurgensmeier Jan 12 '16 at 23:31
  • @Roman I tried mixing the sounds with numpy arrays as outlined in the post I linked. It did not work well. Just averaging them together produced a very static filled sound. Additionally, it is throwing an exception at the end of the sound file. Any ideas? – Joshua Jurgensmeier Jan 16 '16 at 04:07

2 Answers2

12

I checked out pygame like J.F Sebastian suggested. It ended up being exactly what I needed. I used pygame.mixer.Sound() in conjunction with pygame.mixer.set_num_channels(). Here's what I came up with.

import pygame as pg
import time

pg.mixer.init()
pg.init()

a1Note = pg.mixer.Sound("F:\Project Harpsichord\The wavs\A1.wav")
a2Note = pg.mixer.Sound("F:\Project Harpsichord\The wavs\A0.wav")

pg.mixer.set_num_channels(50)

for i in range(25):
    a1Note.play()
    time.sleep(0.3)
    a2Note.play()
    time.sleep(0.3)
2

This doesn't really solve your problem, but it's too long for the comments, and it may be useful. I gave it a bash, got defeated on a few fronts - giving up and going for pizza. Audio is really not my thing, but it was quite a lot of fun playing around with it.

Give Pydub a look. I've Played around with a couple of methods, but haven't had any satisfactory success. This answer here explains quite a few things regarding adding two signals together nicely. I assume that the static you have is because of clipping.

Sorry that I didn't deliver, but I may as well post all the things I've created in case you or someone else wants to grab something from it:

#using python 2.7
#example animal sounds from http://www.wavsource.com/animals/animals.htm
    #note that those sounds have lots of different sampling rates and encoding types.  Causes problems.
#required installs:
    #numpy
    #scipy
    #matplotlib
    #pyaudio        -sudo apt-get install python-pyaudio
    #pydub:         -pip install pydub


def example():
    "example sounds and random inputs"
    sExampleSoundsDir = "/home/roman/All/Code/sound_files"
    sExampleFile1 = 'bird.wav'
    sExampleFile2 = 'frog.wav'
    oJ = Jurgenmeister(sExampleSoundsDir)

    #load audio into numpy array
    dSound1 = oJ.audio2array(sExampleFile1)
    dSound2 = oJ.audio2array(sExampleFile2)

    #Simply adding the arrays is noisy...
    dResSound1 = oJ.resample(dSound1)
    dResSound2 = oJ.resample(dSound2)
    dJoined = oJ.add_sounds(dResSound1, dResSound2)

    #pydub method
    oJ.overlay_sounds(sExampleFile1, sExampleFile2)

    #listen to the audio - mixed success with these sounds.
    oJ.play_array(dSound1)
    oJ.play_array(dSound2)
    oJ.play_array(dResSound1)
    oJ.play_array(dResSound2)
    oJ.play_array(dJoined)

    #see what the waveform looks like
    oJ.plot_audio(dJoined)




class Jurgenmeister:
    """
    Methods to play as many sounds on command as necessary
    Named in honour of op, and its as good a name as I can come up with myself.
    """

    def __init__(self, sSoundsDir):
        import os
        import random
        lAllSounds = os.listdir(sSoundsDir)
        self.sSoundsDir = sSoundsDir
        self.lAllSounds = lAllSounds
        self.sRandSoundName = lAllSounds[random.randint(0, len(lAllSounds)-1)]



    def play_wave(self, sFileName):
        """PyAudio play a wave file."""

        import pyaudio
        import wave
        iChunk = 1024
        sDir = "{}/{}".format(self.sSoundsDir, sFileName)
        oWave = wave.open(sDir, 'rb')
        oPyaudio = pyaudio.PyAudio()

        oStream = oPyaudio.open(
            format = oPyaudio.get_format_from_width(oWave.getsampwidth()),
            channels = oWave.getnchannels(),
            rate = oWave.getframerate(),
            output = True
        )

        sData = oWave.readframes(iChunk)
        while sData != '':
            oStream.write(sData)
            sData = oWave.readframes(iChunk)

        oStream.stop_stream()
        oStream.close()
        oPyaudio.terminate()



    def audio2array(self, sFileName):
        """
        Returns monotone data for a wav audio file in form:  
            iSampleRate, aNumpySignalArray, aNumpyTimeArray

            Should perhaps do this with scipy again, but I threw that code away because I wanted 
            to try the pyaudio package because of its streaming functions.  They defeated me.
        """
        import wave
        import numpy as np

        sDir = "{}/{}".format(self.sSoundsDir, sFileName)
        oWave = wave.open(sDir,"rb")
        tParams = oWave.getparams()
        iSampleRate = tParams[2]   #frames per second
        iLen = tParams[3]  # number of frames

        #depending on the type of encoding of the file.  Usually 16
        try:
            sSound = oWave.readframes(iLen)
            oWave.close()

            aSound = np.fromstring(sSound, np.int16)
        except ValueError:
            raise ValueError("""wave package seems to want all wav incodings to be in int16, else it throws a mysterious error.
                Short way around it:  find audio encoded in the right format.  Or use scipy.io.wavfile.
                """)

        aTime = np.array( [float(i)/iSampleRate for i in range(len(aSound))] )

        dRet = {
            'iSampleRate': iSampleRate, 
            'aTime': aTime, 
            'aSound': aSound,
            'tParams': tParams
        }

        return dRet



    def resample(self, dSound, iResampleRate=11025):
            """resample audio arrays
            common audio sample rates are 44100, 22050, 11025, 8000

            #creates very noisy results sometimes.
            """
            from scipy import interpolate
            import numpy as np
            aSound = np.array(dSound['aSound'])

            iOldRate = dSound['iSampleRate']
            iOldLen = len(aSound)
            rPeriod = float(iOldLen)/iOldRate
            iNewLen = int(rPeriod*iResampleRate)

            aTime = np.arange(0, rPeriod, 1.0/iOldRate)
            aTime = aTime[0:iOldLen]
            oInterp = interpolate.interp1d(aTime, aSound)

            aResTime = np.arange(0, aTime[-1], 1.0/iResampleRate)
            aTime = aTime[0:iNewLen]

            aResSound = oInterp(aResTime)
            aResSound = np.array(aResSound, np.int16)

            tParams = list(x for x in dSound['tParams'])
            tParams[2] = iResampleRate
            tParams[3] = iNewLen
            tParams = tuple(tParams)

            dResSound = {
                'iSampleRate': iResampleRate, 
                'aTime': aResTime, 
                'aSound': aResSound,
                'tParams': tParams
            }

            return dResSound



    def add_sounds(self, dSound1, dSound2):
        """join two sounds together and return new array
        This method creates a lot of clipping.  Not sure how to get around that.
        """
        if dSound1['iSampleRate'] != dSound2['iSampleRate']:
            raise ValueError('sample rates must be the same.  Please resample first.')

        import numpy as np

        aSound1 = dSound1['aSound']
        aSound2 = dSound2['aSound']

        if len(aSound1) < len(aSound2):
            aRet = aSound2.copy()
            aRet[:len(aSound1)] += aSound1
            aTime = dSound2['aTime']
            tParams = dSound2['tParams']
        else:
            aRet = aSound1.copy()
            aRet[:len(aSound2)] += aSound2
            aTime = dSound1['aTime']
            tParams = dSound1['tParams']


        aRet = np.array(aRet, np.int16)

        dRet = {
            'iSampleRate': dSound1['iSampleRate'], 
            'aTime': aTime,
            'aSound': aRet,
            'tParams': tParams
        }

        return dRet



    def overlay_sounds(self, sFileName1, sFileName2):
        "I think this method warrants a bit more exploration
        Also very noisy."
        from pydub import AudioSegment

        sDir1 = "{}/{}".format(self.sSoundsDir, sFileName1)
        sDir2 = "{}/{}".format(self.sSoundsDir, sFileName2)

        sound1 = AudioSegment.from_wav(sDir1)
        sound2 = AudioSegment.from_wav(sDir2)

        # mix sound2 with sound1, starting at 0ms into sound1)
        output = sound1.overlay(sound2, position=0)

        # save the result
        sDir = "{}/{}".format(self.sSoundsDir, 'OUTPUT.wav')
        output.export(sDir, format="wav")



    def array2audio(self, dSound, sDir=None):
        """
        writes an .wav audio file to disk from an array
        """
        import struct
        import wave
        if sDir ==  None:
            sDir = "{}/{}".format(self.sSoundsDir, 'OUTPUT.wav')

        aSound = dSound['aSound']
        tParams = dSound['tParams']
        sSound = struct.pack('h'*len(aSound), *aSound)

        oWave = wave.open(sDir,"wb")
        oWave.setparams(tParams)
        oWave.writeframes(sSound)
        oWave.close()



    def play_array(self, dSound):
        """Tried to use use pyaudio to play array by just streaming it.  It didn't behave, and I moved on.
        I'm just not getting the pyaudio stream to play without weird distortion 
        when not loading from file.  Perhaps you have more luck.
        """
        self.array2audio(dSound)
        self.play_wave('OUTPUT.wav')



    def plot_audio(self, dSound):
        "just plots the audio array.  Nice to see plots when things are going wrong."
        import matplotlib.pyplot as plt
        plt.plot(dSound['aTime'], dSound['aSound'])
        plt.show()




if __name__ == "__main__":
    example()

I also get this error when I use wave. It still works, so I just ignore it. Problem seems to be widespread. Error lines:

ALSA lib pcm_dsnoop.c:618:(snd_pcm_dsnoop_open) unable to open slave
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
bt_audio_service_open: connect() failed: Connection refused (111)
bt_audio_service_open: connect() failed: Connection refused (111)
bt_audio_service_open: connect() failed: Connection refused (111)
bt_audio_service_open: connect() failed: Connection refused (111)
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started

Good luck!

Community
  • 1
  • 1
Roman
  • 8,826
  • 10
  • 63
  • 103