Search code examples
pythonaudiopython-multithreadingpyaudio

PyAudio play continuous stream in a thread and let change the frequency


I don't have the experience with threading at all.

All I want to do is to play a sound and be able to change the tone (frequency) in the meantime, using GUI.

This code plays a continuous stream without any peaks or distortions:


class Stream:
    def __init__(self, sample_rate):
        self.p = pyaudio.PyAudio()
        self.sample_rate = sample_rate

        # for paFloat32 sample values must be in range [-1.0, 1.0]
        self.stream = self.p.open(format=pyaudio.paFloat32,
                                  channels=1,
                                  rate=sample_rate,
                                  output=True)
        self.samples = 0.

    def create_sine_tone(self, frequency, duration):
        # generate samples, note conversion to float32 array
        self.samples = (np.sin(2 * np.pi * np.arange(self.sample_rate * duration) * frequency
                               / self.sample_rate)).astype(np.float32)

    def play_sine_tone(self, volume=1.):
        """
        :param frequency:
        :param duration:
        :param volume:
        :param sample_rate:
        :return:
        """

        # play. May repeat with different volume values (if done interactively)
        while 1:
            self.stream.write(volume * self.samples)

    def terminate(self):
        self.p.terminate()

    def finish(self):
        self.stream.stop_stream()
        self.stream.close()

This code creates GUI. Inleft_click and right_click the create_sine_tone() creates a new frequency wave. However, as I understand, it modifies the memory that is used by threading in play_sine_tone and the program crashes.


def main():
    window = Tk()
    window.title("Piano reference")
    window.geometry('350x200')

    s = Stream(44100)

    lbl = Label(window, text="A4")
    lbl.grid(column=2, row=1)

    def left_click(frequency):
        s.create_sine_tone(frequency, 1.)
        t = threading.Thread(target=s.play_sine_tone, args=(1,))
        t.start()
        lbl.configure(text=frequency)

    def right_click(frequency):
        s.create_sine_tone(frequency, 1.)
        t = threading.Thread(target=s.play_sine_tone, args=(1,))
        t.start()
        lbl.configure(text=frequency)

    btn1 = Button(window, text="<<", command=lambda: left_click(100))
    btn2 = Button(window, text=">>", command=lambda: right_click(200))

    btn1.grid(column=0, row=0)
    btn2.grid(column=1, row=0)

    window.mainloop()

How can I modify the wave so the program won't crash? Maybe I could close the thread before changing the frequency?


Solution

  • If all you are trying to do is play different tones that can be controlled using GUI, you may not need threads.

    PySimpleGUI provides an super easy to use GUI builder based on Tkinter (and other tools). Best of all it provides actions based on event that are driven by GUI components.

    On the other hand use of pydub gives us easy way to create different tones and play them. pydub _play_with_simpleaudio method allows us to play tones using simpleAudio in a non-blocking way.

    GUI Controls:

    • '>>' chooses next frequency in multiples of 200 Hz.

    • '<<' chooses previous frequency in multiples of 100 Hz.

    • 'X' to exit gui.

    The only issue I observed was slight clicking sounds on frequency shift.That may need further work.

    Following working code is based on above packages.

    import PySimpleGUI as sg      
    from pydub.generators import Sine
    from pydub import AudioSegment
    from pydub.playback import _play_with_simpleaudio
    import time
    
    sr = 44100  # sample rate
    bd = 16     # bit depth
    l  = 10000.0     # duration in millisec
    
    sg.ChangeLookAndFeel('BluePurple')
    silent = AudioSegment.silent(duration=10000)
    FREQ = 200
    
    def get_sine(freq):
      #create sine wave of given freq
      sine_wave = Sine(freq, sample_rate=sr, bit_depth=bd)
    
      #Convert waveform to audio_segment for playback and export
      sine_segment = sine_wave.to_audio_segment(duration=l)
    
      return sine_segment
    
    # Very basic window.  Return values as a list      
    layout = [
                  [sg.Button('<<'), sg.Button('>>')],
                  [sg.Text('Processing Freq [Hz]:'), sg.Text(size=(15,1), justification='center', key='-OUTPUT-')]
              ]
    
    window = sg.Window('Piano reference', layout)
    
    count = 0
    play_obj = _play_with_simpleaudio(silent)
    
    while 100 <= FREQ <= 20000 :  # Event Loop
        count += 1
        event, values = window.Read()
    
        if event in  (None, 'Exit'):
            break
        if event == '<<':
          if not FREQ < 100:
            FREQ -= 100
            window['-OUTPUT-'].update(FREQ)
    
        if event == '>>':
          if not FREQ > 20000:
            FREQ += 200
            window['-OUTPUT-'].update(FREQ)
    
        print(event, FREQ)
    
        sound = get_sine(FREQ)
    
        try:
          play_obj.stop()
          time.sleep(0.1)
          sound = sound.fade_in(100).fade_out(100)
          play_obj = _play_with_simpleaudio(sound)
          time.sleep(0.1)
        except KeyboardInterrupt:
          play_obj.stop_all()
    
    
    window.close()
    

    Result:

    $ python3 pygui3.py 
    Playing >> 400 Hz
    Playing >> 600 Hz
    Playing >> 800 Hz
    Playing << 700 Hz
    Playing << 600 Hz
    

    GUI:

    enter image description here