python – Style and code optimization to generate smple sinusoidal sounds and plot signal/fft

I made a small python script to create an audio sound used as stimuli. It’s one of my first uses of super(), and I do not know if I did it right. I’m looking for any improvement in both optimization and style you could suggest.

import pyaudio
import numpy as np

class SoundStimuli:
    def __init__(self, fs=44100, duration=1.0):
        self.fs = fs
        self.duration = duration
        self.t = np.linspace(0, duration, int(duration*fs), endpoint=False)
        self.signal = None
    
    def play(self):
        if self.signal is None:
            print ('WARNING: No audio signal to play.')
            return
        
        p = pyaudio.PyAudio()

        # for paFloat32 sample values must be in range (-1.0, 1.0)
        stream = p.open(format=pyaudio.paFloat32,
                        channels=1,
                        rate=self.fs,
                        output=True)
        
        # play. May repeat with different volume values (if done interactively)
        stream.write(self.signal)
        
        stream.stop_stream()
        stream.close()
        p.terminate()
        
    def convert2float32(self):
        self.signal = self.signal.astype(np.float32)
        
    def plot_signal(self, ax, **plt_kwargs):
        if self.signal is None:
            print ('WARNING: No audio signal to plot.')
            return
        
        ax.plot(self.t, self.signal, **plt_kwargs)
        
    def plot_signal_fft(self, ax, **plt_kwargs):
        fft_freq = np.fft.rfftfreq(self.t.shape(0), 1.0/self.fs)
        fft = np.abs(np.fft.rfft(self.signal))
        ax.plot(fft_freq, fft, **plt_kwargs)
    
class ASSR(SoundStimuli):
    def __init__(self, fc, fm, fs=44100, duration=1.0):
        super().__init__(fs, duration)
        self.fc = fc
        self.fm = fm
    
    def classical_AM(self):
        self.assr_amplitude = (1-np.sin(2*np.pi*self.fm*self.t))
        self.signal = self.assr_amplitude * np.sin(2*np.pi*self.fc*self.t).astype(np.float32)
    
    def DSBSC_AM(self):
        self.assr_amplitude = np.sin(2*np.pi*self.fm*self.t)
        self.signal = self.assr_amplitude * np.sin(2*np.pi*self.fc*self.t).astype(np.float32)
        
    def plot_signal(self, ax, amplitude_linestyle='--', amplitude_color='crimson', **plt_kwargs):
        super().plot_signal(ax, **plt_kwargs)
        ax.plot(self.t, self.assr_amplitude, linestyle=amplitude_linestyle, color=amplitude_color)

The class ASSR is imported in a second script where the stimuli is created:

sound = ASSR(fs=44100, duration=1.0, fc=1000, fm=40)
sound.classical_AM()

However, I do not play it using the .play() method because it looks like pyaudio doesn’t accept the PyAudio and stream to be open/close outside the scope of .write(). And since I do not want to open a new stream every time I play a sound, I ended up using .play() to test the sound; and then create in the main program where ASSR is imported a new audio stream with different .write(sound.signal) calls.

# Initialize audio stream
p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paFloat32,
                channels=1,
                rate=sound.fs,
                output=True)

# Do stuff
stream.write(sound.signal)
# Do stuff
stream.write(sound.signal)

stream.stop_stream()
stream.close()
p.terminate()