2
\$\begingroup\$

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()
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

A lot of things here are already good, like:

  • fs, duration and t being members on SoundStimuli
  • The choice of which things to initialize in the first __init__ (mostly)

Things that could be improved:

  • SoundStimuli is arguably SoundStimulus
  • self.signal should not be an optional member variable; the base class should be made effectively abstract and the signal should be returned from a getter or property whose base implementation raises NotImplementedError.
  • In play, the absence of a signal should be fatal, so you should naively access self.signal, and let any exceptions fall through to the caller
  • After p.open, wrap the rest in a try, and put the close() in a finally
  • convert2float is not a great idea - it replaces a member with a different version of the same member. That means that before this is called, the instance is by definition in an invalid state, which should be avoided.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.