Search code examples
pythonnumpyaudiopyaudio

Sound played with pyaudio seems correct but too short


I am making a small program to deliver a simple amplitude modulated sinusoidal sound. The sound is called ASSR which stands for Auditory Steady-State Response which is a type of reproductible brain activity response. I chose to use pyaudio, but it is very poorly documented. If you know any other libraries to play a numpy array/sound, I'm listening ;)

The sound delivered is a 1000 Hz carrier sinusoidal sound with the amplitude modulated at 40 Hz. My problem is as follows: when I play the sound with pyaudio; it doesn't last 1 second. It last about 500 ms only. Yet, the array .signal is 44100 elements long and the sampling frequency is 44100 Hz. Moreover, if I save the array to a .wav file with scipy, I do get a 1s recording which sound exactly the same as the 500 ms of sound delivered by pyaudio.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import pyaudio
import numpy as np
from scipy.io.wavfile import write

class Sound:
    def __init__(self, fs=44100, duration=1.0):
        self.fs = int(fs)
        self.duration = duration
        self.t = np.linspace(0, duration, int(duration*fs), endpoint=False)
        self.signal = None
    
    def play(self):
        p = pyaudio.PyAudio()
        stream = p.open(format=pyaudio.paFloat32,
                        channels=1,
                        rate=self.fs,
                        output=True)
        try:
            stream.write(self.signal)
        except:
            raise
        finally:
            stream.stop_stream()
            stream.close()
        p.terminate()
        
    def write(self, fname):
        data = np.int16(self.signal/np.max(np.abs(self.signal)) * 32767) # Scale
        write(fname, self.fs, data)
    
class ASSR(Sound):
    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)
        self.signal = self.signal / np.max(self.signal) # Nomalized in [-1, 1]
        self.signal = self.signal.astype(np.float32)
        
if __name__ == '__main__':
    sound = ASSR(fc=1000, fm=40)
    sound.classical_AM()
    sound.play()

You can find above a minimalist reproductible example. My code is a bit longer as I implemented some error checking and some alternative equations for the sound.


Solution

  • I believe the solution is to transform the signal array into bytes.

    stream.write(signal.tobytes())
    

    If someone has the documentation/explanation behind this to build more confidence in this fix, please add it.