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()
1 Answer 1
A lot of things here are already good, like:
fs
,duration
andt
being members onSoundStimuli
- The choice of which things to initialize in the first
__init__
(mostly)
Things that could be improved:
SoundStimuli
is arguablySoundStimulus
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 raisesNotImplementedError
.- In
play
, the absence of a signal should be fatal, so you should naively accessself.signal
, and let any exceptions fall through to the caller - After
p.open
, wrap the rest in atry
, and put theclose()
in afinally
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.