Transforming Pyplot x axis - python

I am trying to plot an audio sample's amplitude across the time domain. I've used scipy.io.wavfile to read the audio sample and determine sampling rate:
# read .wav file
data = read("/Users/user/Desktop/voice.wav")
# determine sample rate of .wav file
# print(data[0]) # 48000 samples per second
# 48000 (samples per second) * 4 (4 second sample) = 192000 samples overall
# store the data read from .wav file
audio = data[1]
# plot the data
plt.plot(audio[0 : 192000]) # see code above for how this value was determined
This creates a plot displaying amplitude on y axis and sample number on the x axis. How can I transform the x axis to instead show seconds?
I tried using plt.xticks but I don't think this is the correct use case based upon the error I received:
# label title, axis, show the plot
seconds = range(0,4)
plt.xticks(range(0,192000), seconds)
plt.ylabel("Amplitude")
plt.xlabel("Time")
plt.show()
ValueError: The number of FixedLocator locations (192000), usually from a call to set_ticks, does not match the number of ticklabels (4).

You need to pass a t vector to the plotting command, a vector that you can generate on the fly using Numpy, so that after the command execution it is garbage collected (sooner or later, that is)
from numpy import linspace
plt.plot(linspace(0, 4, 192000, endpoint=False), audio[0 : 192000])

Related

Time series dBFS plot output modification - current output plot not as expected (matplotlib)

I'm trying to plot the Amplitude (dBFS) vs. Time (s) plot of an audio (.wav) file using matplotlib. I managed to do that with the following code:
def convert_to_decibel(sample):
ref = 32768 # Using a signed 16-bit PCM format wav file. So, 2^16 is the max. value.
if sample!=0:
return 20 * np.log10(abs(sample) / ref)
else:
return 20 * np.log10(0.000001)
from scipy.io.wavfile import read as readWav
from scipy.fftpack import fft
import matplotlib.pyplot as gplot1
import matplotlib.pyplot as gplot2
import numpy as np
import struct
import gc
wavfile1 = '/home/user01/audio/speech.wav'
wavsamplerate1, wavdata1 = readWav(wavfile1)
wavdlen1 = wavdata1.size
wavdtype1 = wavdata1.dtype
gplot1.rcParams['figure.figsize'] = [15, 5]
pltaxis1 = gplot1.gca()
gplot1.axhline(y=0, c="black")
gplot1.xticks(np.arange(0, 10, 0.5))
gplot1.yticks(np.arange(-200, 200, 5))
gplot1.grid(linestyle = '--')
wavdata3 = np.array([convert_to_decibel(i) for i in wavdata1], dtype=np.int16)
yvals3 = wavdata3
t3 = wavdata3.size / wavsamplerate1
xvals3 = np.linspace(0, t3, wavdata3.size)
pltaxis1.set_xlim([0, t3 + 2])
pltaxis1.set_title('Amplitude (dBFS) vs Time(s)')
pltaxis1.plot(xvals3, yvals3, '-')
which gives the following output:
I had also plotted the Power Spectral Density (PSD, in dBm) using the code below:
from scipy.signal import welch as psd # Computes PSD using Welch's method.
fpsd, wPSD = psd(wavdata1, wavsamplerate1, nperseg=1024)
gplot2.rcParams['figure.figsize'] = [15, 5]
pltpsdm = gplot2.gca()
gplot2.axhline(y=0, c="black")
pltpsdm.plot(fpsd, 20*np.log10(wPSD))
gplot2.xticks(np.arange(0, 4000, 400))
gplot2.yticks(np.arange(-150, 160, 10))
pltpsdm.set_xlim([0, 4000])
pltpsdm.set_ylim([-150, 150])
gplot2.grid(linestyle = '--')
which gives the output as:
The second output above, using the Welch's method plots a more presentable output. The dBFS plot though informative is not very presentable IMO. Is this because of:
the difference in the domains (time in case of 1st output vs frequency in the 2nd output)?
the way plot function is implemented in pyplot?
Also, is there a way I can plot my dBFS output as a peak-to-peak style of plot just like in my PSD (dBm) plot rather than a dense stem plot?
Would be much helpful and would appreciate any pointers, answers or suggestions from experts here as I'm just a beginner with matplotlib and plots in python in general.
TLNR
This has nothing to do with pyplot.
The frequency domain is different from the time domain, but that's not why you didn't get what you want.
The calculation of dbFS in your code is wrong.
You should frame your data, calculate RMSs or peaks in every frame, and then convert that value to dbFS instead of applying this transformation to every sample point.
When we talk about the amplitude, we are talking about a periodic signal. And when we read in a series of data from a sound file, we read in a series of sample points of a signal(may be or be not periodic). The value of every sample point represents a, say, voltage value, or sound pressure value sampled at a specific time.
We assume that, within a very short time interval, maybe 10ms for example, the signal is stationary. Every such interval is called a frame.
Some specific function is applied to each frame usually, to reduce the sudden change at the edge of this frame, and these functions are called window functions. If you did nothing to every frame, you added rectangle windows to them.
An example: when the sampling frequency of your sound is 44100Hz, in a 10ms-long frame, there are 44100*0.01=441 sample points. That's what the nperseg argument means in your psd function but it has nothing to do with dbFS.
Given the knowledge above, now we can talk about the amplitude.
There are two methods a get the value of amplitude in every frame:
The most straightforward one is to get the maximum(peak) values in every frame.
Another one is to calculate the RMS(Root Mean Sqaure) of every frame.
After that, the peak values or RMS values can be converted to dbFS values.
Let's start coding:
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import wavfile
# Determine full scall(maximum possible amplitude) by bit depth
bit_depth = 16
full_scale = 2 ** bit_depth
# dbFS function
to_dbFS = lambda x: 20 * np.log10(x / full_scale)
# Read in the wave file
fname = "01.wav"
fs,data = wavfile.read(fname)
# Determine frame length(number of sample points in a frame) and total frame numbers by window length(how long is a frame in seconds)
window_length = 0.01
signal_length = data.shape[0]
frame_length = int(window_length * fs)
nframes = signal_length // frame_length
# Get frames by broadcast. No overlaps are used.
idx = frame_length * np.arange(nframes)[:,None] + np.arange(frame_length)
frames = data[idx].astype("int64") # Convert to in 64 to avoid integer overflow
# Get RMS and peaks
rms = ((frames**2).sum(axis=1)/frame_length)**.5
peaks = np.abs(frames).max(axis=1)
# Convert them to dbfs
dbfs_rms = to_dbFS(rms)
dbfs_peak = to_dbFS(peaks)
# Let's start to plot
# Get time arrays of every sample point and ever frame
frame_time = np.arange(nframes) * window_length
data_time = np.linspace(0,signal_length/fs,signal_length)
# Plot
f,ax = plt.subplots()
ax.plot(data_time,data,color="k",alpha=.3)
# Plot the dbfs values on a twin x Axes since the y limits are not comparable between data values and dbfs
tax = ax.twinx()
tax.plot(frame_time,dbfs_rms,label="RMS")
tax.plot(frame_time,dbfs_peak,label="Peak")
tax.legend()
f.tight_layout()
# Save serval details
f.savefig("whole.png",dpi=300)
ax.set_xlim(1,2)
f.savefig("1-2sec.png",dpi=300)
ax.set_xlim(1.295,1.325)
f.savefig("1.2-1.3sec.png",dpi=300)
The whole time span looks like(the unit of the right axis is dbFS):
And the voiced part looks like:
You can see that the dbFS values become greater while the amplitudes become greater at the vowel start point:

NP.FFT on python list

Could you please advise me on the following:
I gather data from an Arduino ADC and store the data in a list on a Raspberry Pi 4 with Python 3.
The list is called 'dataList' and contains 1024 10 bits samples. This all works fine: I can reproduce the sampled signal on the Raspberry.
I would like to use the power spectrum of the acquired signal using numpy FFT.
I tried the following:
[see below]
This should illustrate what I'm trying to do; however this produces incoherent output. The sampled signal has a frequency of about 300 Hz. I would be very grateful for any hints in the right direction!
def show_FFT(window):
fft = np.fft.fft (dataList, 1024, -1, None)
for X_value in range (0,512, 1):
Y_value = fft ([X_value]
gfxdraw.pixel (window, X_value, int(abs(Y_value), black)
As you mentioned in your question, you have a data set whith X starting from 0 to... but for numpy.fft.fft you must keep in mind that it is a discrete Fourier transform (DFT) which caculate the fft of equaly spaced samples and i must mntion that it must be a symetric range of dataset from -x to x. You can simply try it with a gausian finction and change the parameters as you wish and see what are the results...
Since you didn''t give any data set here , I would refer you to a generl case with below code:
import numpy as np
from scipy import interpolate
import matplotlib.pyplot as plt
# create data from dataframes
x = np.random.rand(50) #unequaly spaced measurment
x.sort()
y = np.exp(-x*x) #measured signal
based on the answer here you can resample your data into equaly spaced points by:
f = interpolate.interp1d(x, y)
num = 500
xx = np.linspace(x[0], x[-1], num)
yy = f(xx)
plt.close('all')
plt.plot(x,y,'bo')
plt.plot(xx,yy, 'g.-')
plt.show()
enter image description here
then you can make your x data symetric very simply by :
x=xx
y=yy
xsample = x-((x.max()-x.min())/2)
xsample=xsample-(xsample.max()+xsample.min())/2
x=xsample
thne if you try fft you will get the corect results as:
ysample =yy
ysample_fft = np.fft.fftshift(np.abs(np.fft.fft(ysample/ysample.max()))) /
np.sqrt(len(ysample))
plt.plot(xsample,ysample_fft/ysample_fft.max(),'b--')
plt.show()
enter image description here

Fidelity of sound created from frequency domain data

I know it is possible to create .wav file from frequency domain data (magnitude + phase) but I would like to know how close would that be to the real(orginal) sound ? Does it depend on the frequency step for example (or something else).
Second question:
I need to write a code that takes a frequency domain data (magnitude + phase) to build a wav file.
In order to do so, I started by the following code which creates a fake signal --> fft (at this point I have the kind of input(mag + phase) that I would expect for my target code). But it doesn't seem top work fine, could you please help
import numpy as np
from scipy import pi
import matplotlib.pyplot as plt
#%matplotlib inline
from scipy.fftpack import fft
min=0
max=400
def calculateFFT (timeStep,micDataX,micDataY):
n=micDataX.size
FFT=np.fft.fft(micDataY)
fft_amlitude=2*abs(FFT)/n
fft_phase=np.angle(FFT)
fft_freq= np.fft.fftfreq(n, d=timeStep) #not used created manually (7 lines) check pi_fFreqDomainCreateConstantBW it is kept here to compare sizes
upper_bound=int((n)/2)
return fft_freq[1:upper_bound],fft_amlitude[1:upper_bound],fft_phase[1:upper_bound]
def calculateI_FFT (n,amplitude_spect,phase_spect):
data=list()
for mag,phase in zip(amplitude_spect,phase_spect):
data.append((mag*n/2)*(np.cos(phase)+1j* np.sin(phase)))
full_data=list(data)
i_data=np.fft.irfft(data)
return i_data
#sampling rate and time vector
start_time=0 #sec
end_time= 2
sampling_rate=1000 #Hz
N=(end_time-start_time)*sampling_rate
#Freq domain peaks
peak1_hz=60 # freq of peak
peak1_mag= 25
peak2_hz=270 # freq of peak
peak2_mag= 2
#Vibration data generation
time =np.linspace(start_time,end_time,N)
vib_data=peak1_mag*np.sin(2*pi*peak1_hz*time)+peak2_mag*np.sin(2*pi*peak2_hz*time)
#Data plotting
plt.plot(time[min:max],vib_data[min:max])
# fft
time_step=1/sampling_rate
fft_freq,fft_data,fft_phase=calculateFFT(time_step,time,vib_data)
#ifft
i_data=calculateI_FFT(N,fft_data,fft_phase)
#plotting
plt.plot(time[min:max],i_data[min:max])
plt.xlabel("Time (s)")
plt.ylabel("Vibration (g)")
plt.title("Time domain")
plt.show()
The output signal screenshot is attached (blue for original signal Orange for the reconstructed one)
enter image description here
Thank you!

How to decrease the scale of a matplotlib spectrogram in python3

I am analyzing the spectrogram's of .wav files. But after getting the code to finally work, I've run into a small issue. After saving the spectrograms of 700+ .wav files I realize that they all essentially look the same!!! This is not because they are the same audio file, but because I don't know how to change the scale of the plot to be smaller(so I can make out the differences).
I've already tried to fix this issue by looking at this StackOverflow post
Changing plot scale by a factor in matplotlib
I'll show the graph of two different .wav files below
This is .wav #1
This is .wav #2
Believe it or not, these are two different .wav files, but they look super similar. And a computer especially won't be able to pick up the differences in these two .wav files if the scale is this broad.
My code is below
def individualWavToSpectrogram(myAudio, fileNameToSaveTo):
print(myAudio)
#Read file and get sampling freq [ usually 44100 Hz ] and sound object
samplingFreq, mySound = wavfile.read(myAudio)
#Check if wave file is 16bit or 32 bit. 24bit is not supported
mySoundDataType = mySound.dtype
#We can convert our sound array to floating point values ranging from -1 to 1 as follows
mySound = mySound / (2.**15)
#Check sample points and sound channel for duel channel(5060, 2) or (5060, ) for mono channel
mySoundShape = mySound.shape
samplePoints = float(mySound.shape[0])
#Get duration of sound file
signalDuration = mySound.shape[0] / samplingFreq
#If two channels, then select only one channel
#mySoundOneChannel = mySound[:,0]
#if one channel then index like a 1d array, if 2 channel index into 2 dimensional array
if len(mySound.shape) > 1:
mySoundOneChannel = mySound[:,0]
else:
mySoundOneChannel = mySound
#Plotting the tone
# We can represent sound by plotting the pressure values against time axis.
#Create an array of sample point in one dimension
timeArray = numpy.arange(0, samplePoints, 1)
#
timeArray = timeArray / samplingFreq
#Scale to milliSeconds
timeArray = timeArray * 1000
plt.rcParams['agg.path.chunksize'] = 100000
#Plot the tone
plt.plot(timeArray, mySoundOneChannel, color='Black')
#plt.xlabel('Time (ms)')
#plt.ylabel('Amplitude')
print("trying to save")
plt.savefig('/Users/BillyBobJoe/Desktop/' + fileNameToSaveTo + '.jpg')
print("saved")
#plt.show()
#plt.close()
How can I modify this code to increase the sensitivity of the graphing so that the differences between two .wav files is made more distinct?
Thanks!
[UPDATE]
I have tried using
plt.xlim((0, 16000))
But this just adds whitespace to the right of the graph
like
I need a way to change the scale of each unit. so that the graph is filled out when I change the x axis from 0 - 16000
If the question is: how to limit the scale on the xaxis, say to between 0 and 1000, you can do as follows:
plt.xlim((0, 1000))

Compute time Series from PSD python

I have a signal spectrum PSD that looks like :
The frequency range of the PSD is np.linspace(0,2,500). I want to convert this spectrum into a time series of 600s . The code is shown below:
def spectrumToSeries(timeSeries,frequency,psdLoad):
'''
Function that gicen a PSD converts into a time series
'''
#
#Obtian interval frequency
df=frequency[2]-frequency[1]
#Obtian the spectrum amplitudes
amplitude=np.sqrt(2*np.array(psdLoad)*df)
#Pre allocation of matrices
epsilon=np.zeros((len(amplitude)))
randomSeries=np.zeros((len(amplitude)))
#Create time series from spectrum
#Generate random phases between [-2pi,2pi]
epsilon=-np.pi + 2*np.pi*np.random.randn(1,len(amplitude))
#Inverse Fourier
randomSeries=len(timeSeries)*np.real(np.fft.ifft(amplitude*np.exp(epsilon*1j*2*np.pi))));
return randomSeries
However my end result looks like:
timeSeries = spectrumToSeries(thrustBladed,param.frequency,analyticalThrustPSD[iwind])
The x axis is refering the number of points of the time series. However, the time series should be of 600s. Any help? Thanks
The result of your function "spectrumToSeries" is the same length as the array you give in the np.fft.ifft. Because the ifft function returns an array of the same length as the input.
So, because your initial psdLoad array has 500 elements, the "amplitude" array is 500 elements long too, and so as the randomSeries one, which is your function's result.
I don't really get the different inputs of your function. What is the first argument called timeSeries ? Is it an empty matrix of 600 elements awaiting for the result of the function ?
I am trying to compute time series from PSD myself so I'd love to see your function give a good result !
I think that if you want your time series to be 600 elements, you need to have a "frequency" and a "psdLoad" array of 600 elements. So what I am trying to do with my set of data is to fit my psdLoad with a function (psdLoad = f (frequency)). Then I can set the size of my arrays to the length of the timeseries I want at the end, and compute the ifft...
My own data is a record at 1Hz, over a day, so arrays of 86400 elements. I have to apply a filter to it, using a method with PSD. So I compute my PSD, which length is 129 elements, and once I have filtered it I want to end up with my filtered time series.
here is my code :
######################################################################"
## Computation of spectrum values : PSD & frequency ##
######################################################################"
psd_ampl0, freq = mlab.psd(Up13_7april, NFFT=256, Fs=1, detrend=mlab.detrend_linear, window=mlab.window_hanning, noverlap=0.5, sides='onesided')
################################################"
## Computation of the time series from the PSD ##
################################################"
def PSDToSeries(lenTimeSeries,freq,psdLoad):
'''
Function that gicen a PSD converts into a time series
'''
#
#Obtian interval frequency
df=freq[2]-freq[1]
print('df = ', df)
#Obtian the spectrum amplitudes
amplitude=(2*psdLoad*df)**0.5
#Pre allocation of matrices
epsilon=np.zeros((len(amplitude)))
randomSeries=np.zeros((len(amplitude)))
#Create time series from spectrum
#Generate random phases between [-2pi,2pi]
epsilon=-np.pi + 2*np.pi*np.random.randn(1,len(amplitude))
#Inverse Fourier
randomSeries=lenTimeSeries*np.real(np.fft.ifft(amplitude*np.exp(epsilon*1j*2*np.pi)));
return randomSeries
#-------------------------------------------------------------------------
#########################################################"
## Fitting a function on the PSD to add it more points ##
#########################################################"
#def fitting_function(freq,a,b,c,d,e,f):
#return a*(freq**5)+b*(freq**4)+c*(freq**3)+d*(freq**2)+e*freq+f
def fitting_function(freq,a,b,c):
return a*np.exp(freq*b)
# Look for the best fitting parameters of the choosen fitting function #
param_opt, pcov = optim.curve_fit(fitting_function,freq[1:],psd_ampl0[1:])
print('The best fitting parameters are : ',param_opt)
# Definition of the new PSD and frequency arrays extended to 86400 elements #
freq_extend = np.linspace(min(freq),max(freq), 86400)
psd_extend = fitting_function(freq_extend,param_opt[0], param_opt[1], param_opt[2])
#print(psd_allonge)
ts_length = Up13_7april.shape[0] #Length of the timeSeries I want to compute
print('ts_length = ', ts_length)
tsFromPSD = PSDToSeries(ts_length, freq_allonge, psd_allonge)
print('shape tsFromPSD : ', tsFromPSD.shape)
##################"
## Plot section ##
##################"
plt.figure(1)
plt.plot(freq[1:] ,psd_ampl0[1:],marker=',', ls='-',color='SkyBlue', label='original PSD')
plt.plot(freq_allonge, psd_allonge, marker=',', ls='-',color='DarkGreen', label='PSD rallonge')
plt.xlabel('Frequency [Hz]')
plt.ylabel('PSD of raw velocity module [(m/s)²/Hz]')
plt.grid(True)
plt.legend()
plt.figure(2)
plt.plot_date(time7april,Up13_7april, xdate=True, ydate=False, marker=',', ls='-', c='Grey', label='Original Signal')
plt.plot_date(time7april, tsFromPSD[0],xdate=True, ydate=False, marker=',', ls='-', label='After inverse PSD')
plt.suptitle('Original and Corrected time series for the 7th of April')
plt.grid(True)
plt.legend()
plt.show()
The array Up13_7april, is my initial time series, in this code I am just trying to compute the PSD and then come back to a time serie to compare the original signal and the final one. Here is the result :
[Sorry can't post any picture because I'm new to stackoverflow]
So my process is to find a function that fits the PSD. I use the Python scipy function called "optimize.curve_fit". It just gives you the best parameters to fit your data with a function that you provide.
Once I have my parameters, I create new PSD and frequency arrays, of 86400 elements. And finally I use your "PSDToSeries" function to compute the timeSeries.
I'm quite happy with the result... I think I just need to find a better fit of my PSD :
[Sorry can't post any picture because I'm new to stackoverflow]
Any idea ?

Categories