DavidS1992
DavidS1992

Reputation: 915

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?

Upvotes: 2

Views: 2873

Answers (2)

Anil_M
Anil_M

Reputation: 11453

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

Upvotes: 1

DavidS1992
DavidS1992

Reputation: 915

I can't find the answer. What I know:

  1. There are "clicks" in the looping sound. One has to generate the full period of the signal
  2. You can use the stream with callback so the threading is moved under the hood of PyAudio. Using it I am able to run the program in the accepted way
  3. I had some ideas with creating two threading objects that will be the fields of Stream class. Then they can use 2 different streams and threads to start and stop. I wasn't able to make it work though - I think the threads are not waiting for the finish()

Upvotes: 0

Related Questions